Compare commits
3 commits
02ccc88d24
...
4f59f3bc14
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f59f3bc14 | ||
|
|
2ff7d1759e | ||
|
|
c46729f365 |
27 changed files with 1071 additions and 3398 deletions
50
README.md
50
README.md
|
|
@ -10,21 +10,21 @@ Live at [lilastudy.com](https://lilastudy.com).
|
||||||
|
|
||||||
## Stack
|
## Stack
|
||||||
|
|
||||||
| Layer | Technology |
|
| Layer | Technology |
|
||||||
|---|---|
|
| ------------ | ---------------------------------- |
|
||||||
| Monorepo | pnpm workspaces |
|
| Monorepo | pnpm workspaces |
|
||||||
| Frontend | React 18, Vite, TypeScript |
|
| Frontend | React 18, Vite, TypeScript |
|
||||||
| Routing | TanStack Router |
|
| Routing | TanStack Router |
|
||||||
| Server state | TanStack Query |
|
| Server state | TanStack Query |
|
||||||
| Styling | Tailwind CSS |
|
| Styling | Tailwind CSS |
|
||||||
| Backend | Node.js, Express, TypeScript |
|
| Backend | Node.js, Express, TypeScript |
|
||||||
| Database | PostgreSQL + Drizzle ORM |
|
| Database | PostgreSQL + Drizzle ORM |
|
||||||
| Validation | Zod (shared schemas) |
|
| Validation | Zod (shared schemas) |
|
||||||
| Auth | Better Auth (Google + GitHub) |
|
| Auth | Better Auth (Google + GitHub) |
|
||||||
| Realtime | WebSockets (`ws` library) |
|
| Realtime | WebSockets (`ws` library) |
|
||||||
| Testing | Vitest, supertest |
|
| Testing | Vitest, supertest |
|
||||||
| Deployment | Docker Compose, Caddy, Hetzner VPS |
|
| Deployment | Docker Compose, Caddy, Hetzner VPS |
|
||||||
| CI/CD | Forgejo Actions |
|
| CI/CD | Forgejo Actions |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -156,15 +156,15 @@ pnpm --filter web test
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
| Phase | Description | Status |
|
| Phase | Description | Status |
|
||||||
|---|---|---|
|
| ----- | ---------------------------------------------------------------------- | ------ |
|
||||||
| 0 | Foundation — monorepo, tooling, dev environment | ✅ |
|
| 0 | Foundation — monorepo, tooling, dev environment | ✅ |
|
||||||
| 1 | Vocabulary data pipeline + REST API | ✅ |
|
| 1 | Vocabulary data pipeline + REST API | ✅ |
|
||||||
| 2 | Singleplayer quiz UI | ✅ |
|
| 2 | Singleplayer quiz UI | ✅ |
|
||||||
| 3 | Auth (Google + GitHub) | ✅ |
|
| 3 | Auth (Google + GitHub) | ✅ |
|
||||||
| 4 | Multiplayer lobby (WebSockets) | ✅ |
|
| 4 | Multiplayer lobby (WebSockets) | ✅ |
|
||||||
| 5 | Multiplayer game (real-time, server timer) | ✅ |
|
| 5 | Multiplayer game (real-time, server timer) | ✅ |
|
||||||
| 6 | Production deployment + CI/CD | ✅ |
|
| 6 | Production deployment + CI/CD | ✅ |
|
||||||
| 7 | Hardening (rate limiting, error boundaries, monitoring, accessibility) | 🔄 |
|
| 7 | Hardening (rate limiting, error boundaries, monitoring, accessibility) | 🔄 |
|
||||||
|
|
||||||
See `documentation/roadmap.md` for task-level detail.
|
See `documentation/roadmap.md` for task-level detail.
|
||||||
|
|
|
||||||
10
apps/api/src/lib/utils.ts
Normal file
10
apps/api/src/lib/utils.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
export const shuffleArray = <T>(array: T[]): T[] => {
|
||||||
|
const result = [...array];
|
||||||
|
for (let i = result.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
const temp = result[i]!;
|
||||||
|
result[i] = result[j]!;
|
||||||
|
result[j] = temp;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
@ -10,13 +10,14 @@ import type {
|
||||||
} from "@lila/shared";
|
} from "@lila/shared";
|
||||||
import { InMemoryGameSessionStore } from "../gameSessionStore/index.js";
|
import { InMemoryGameSessionStore } from "../gameSessionStore/index.js";
|
||||||
import { NotFoundError } from "../errors/AppError.js";
|
import { NotFoundError } from "../errors/AppError.js";
|
||||||
|
import { shuffleArray } from "../lib/utils.js";
|
||||||
|
|
||||||
const gameSessionStore = new InMemoryGameSessionStore();
|
const gameSessionStore = new InMemoryGameSessionStore();
|
||||||
|
|
||||||
export const createGameSession = async (
|
export const createGameSession = async (
|
||||||
request: GameRequest,
|
request: GameRequest,
|
||||||
): Promise<GameSession> => {
|
): Promise<GameSession> => {
|
||||||
const correctAnswers = await getGameTerms(
|
const terms = await getGameTerms(
|
||||||
request.source_language,
|
request.source_language,
|
||||||
request.target_language,
|
request.target_language,
|
||||||
request.pos,
|
request.pos,
|
||||||
|
|
@ -27,19 +28,19 @@ export const createGameSession = async (
|
||||||
const answerKey = new Map<string, number>();
|
const answerKey = new Map<string, number>();
|
||||||
|
|
||||||
const questions: GameQuestion[] = await Promise.all(
|
const questions: GameQuestion[] = await Promise.all(
|
||||||
correctAnswers.map(async (correctAnswer) => {
|
terms.map(async (term) => {
|
||||||
const distractorTexts = await getDistractors(
|
const distractorTexts = await getDistractors(
|
||||||
correctAnswer.termId,
|
term.termId,
|
||||||
correctAnswer.targetText,
|
term.targetText,
|
||||||
request.target_language,
|
request.target_language,
|
||||||
request.pos,
|
request.pos,
|
||||||
request.difficulty,
|
request.difficulty,
|
||||||
3,
|
3,
|
||||||
);
|
);
|
||||||
|
|
||||||
const optionTexts = [correctAnswer.targetText, ...distractorTexts];
|
const optionTexts = [term.targetText, ...distractorTexts];
|
||||||
const shuffledTexts = shuffle(optionTexts);
|
const shuffledTexts = shuffleArray(optionTexts);
|
||||||
const correctOptionId = shuffledTexts.indexOf(correctAnswer.targetText);
|
const correctOptionId = shuffledTexts.indexOf(term.targetText);
|
||||||
|
|
||||||
const options: AnswerOption[] = shuffledTexts.map((text, index) => ({
|
const options: AnswerOption[] = shuffledTexts.map((text, index) => ({
|
||||||
optionId: index,
|
optionId: index,
|
||||||
|
|
@ -51,8 +52,8 @@ export const createGameSession = async (
|
||||||
|
|
||||||
return {
|
return {
|
||||||
questionId,
|
questionId,
|
||||||
prompt: correctAnswer.sourceText,
|
prompt: term.sourceText,
|
||||||
gloss: correctAnswer.sourceGloss,
|
gloss: term.sourceGloss,
|
||||||
options,
|
options,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
@ -64,17 +65,6 @@ export const createGameSession = async (
|
||||||
return { sessionId, questions };
|
return { sessionId, questions };
|
||||||
};
|
};
|
||||||
|
|
||||||
const shuffle = <T>(array: T[]): T[] => {
|
|
||||||
const result = [...array];
|
|
||||||
for (let i = result.length - 1; i > 0; i--) {
|
|
||||||
const j = Math.floor(Math.random() * (i + 1));
|
|
||||||
const temp = result[i]!;
|
|
||||||
result[i] = result[j]!;
|
|
||||||
result[j] = temp;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const evaluateAnswer = async (
|
export const evaluateAnswer = async (
|
||||||
submission: AnswerSubmission,
|
submission: AnswerSubmission,
|
||||||
): Promise<AnswerResult> => {
|
): Promise<AnswerResult> => {
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,11 @@ export const QuestionCard = ({
|
||||||
Round {questionNumber}/{totalQuestions}
|
Round {questionNumber}/{totalQuestions}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs font-semibold text-(--color-text-muted)">
|
<div className="text-xs font-semibold text-(--color-text-muted)">
|
||||||
{currentResult ? "Checked" : selectedOptionId !== null ? "Ready" : "Pick one"}
|
{currentResult
|
||||||
|
? "Checked"
|
||||||
|
: selectedOptionId !== null
|
||||||
|
? "Ready"
|
||||||
|
: "Pick one"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -73,14 +77,14 @@ export const QuestionCard = ({
|
||||||
|
|
||||||
<div className="w-full rounded-3xl border border-(--color-primary-light) bg-white/55 dark:bg-black/10 backdrop-blur shadow-sm p-4">
|
<div className="w-full rounded-3xl border border-(--color-primary-light) bg-white/55 dark:bg-black/10 backdrop-blur shadow-sm p-4">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
{question.options.map((option) => (
|
{question.options.map((option) => (
|
||||||
<OptionButton
|
<OptionButton
|
||||||
key={option.optionId}
|
key={option.optionId}
|
||||||
text={option.text}
|
text={option.text}
|
||||||
state={getOptionState(option.optionId)}
|
state={getOptionState(option.optionId)}
|
||||||
onSelect={() => handleSelect(option.optionId)}
|
onSelect={() => handleSelect(option.optionId)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,9 @@ export const MultiplayerScoreScreen = ({
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className={`text-sm font-semibold ${
|
className={`text-sm font-semibold ${
|
||||||
isCurrentUser ? "text-(--color-text)" : "text-(--color-text)"
|
isCurrentUser
|
||||||
|
? "text-(--color-text)"
|
||||||
|
: "text-(--color-text)"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{player.user.name}
|
{player.user.name}
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,7 @@ type ConfettiBurstProps = {
|
||||||
count?: number;
|
count?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Piece = {
|
type Piece = { id: number; style: React.CSSProperties & ConfettiVars };
|
||||||
id: number;
|
|
||||||
style: React.CSSProperties & ConfettiVars;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ConfettiVars = {
|
type ConfettiVars = {
|
||||||
["--x0"]: string;
|
["--x0"]: string;
|
||||||
|
|
@ -56,7 +53,9 @@ export const ConfettiBurst = ({
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const pieces = useMemo<Piece[]>(() => {
|
const pieces = useMemo<Piece[]>(() => {
|
||||||
const seed = hashStringToUint32(`${instanceId}:${count}:${colors.join(",")}`);
|
const seed = hashStringToUint32(
|
||||||
|
`${instanceId}:${count}:${colors.join(",")}`,
|
||||||
|
);
|
||||||
const rand = mulberry32(seed);
|
const rand = mulberry32(seed);
|
||||||
const rnd = (min: number, max: number) => min + rand() * (max - min);
|
const rnd = (min: number, max: number) => min + rand() * (max - min);
|
||||||
|
|
||||||
|
|
@ -100,4 +99,3 @@ export const ConfettiBurst = ({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,9 @@ function MultiplayerPage() {
|
||||||
|
|
||||||
{/* Join lobby */}
|
{/* Join lobby */}
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<h2 className="text-lg font-bold text-(--color-text)">Join a lobby</h2>
|
<h2 className="text-lg font-bold text-(--color-text)">
|
||||||
|
Join a lobby
|
||||||
|
</h2>
|
||||||
<p className="text-sm text-(--color-text-muted)">
|
<p className="text-sm text-(--color-text-muted)">
|
||||||
Enter the code shared by your host.
|
Enter the code shared by your host.
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -5,8 +5,8 @@
|
||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "NodeNext",
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"rootDir": ".",
|
"rootDir": ".",
|
||||||
"types": ["node"],
|
"types": ["node"]
|
||||||
},
|
},
|
||||||
"references": [{ "path": "../packages/shared" }],
|
"references": [{ "path": "../packages/shared" }],
|
||||||
"include": ["./**/*"],
|
"include": ["./**/*"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -103,10 +103,10 @@ Directionally right, timing is unclear. Revisit when the next/now work is done.
|
||||||
Shipped milestones, newest first.
|
Shipped milestones, newest first.
|
||||||
|
|
||||||
- **04 - 2026 - t00001 - Docker credential helper**
|
- **04 - 2026 - t00001 - Docker credential helper**
|
||||||
- **04 - 2026 - Pin dependencies in package.json** - Unpinned deps in a CI/CD pipeline are a real risk.
|
- **04 - 2026 - Pin dependencies in package.json** - Unpinned deps in a CI/CD pipeline are a real risk.
|
||||||
- **04 - 2026 - React error boundaries** - Catch and display runtime errors gracefully instead of crashing the entire app.
|
- **04 - 2026 - React error boundaries** - Catch and display runtime errors gracefully instead of crashing the entire app.
|
||||||
- **04 - 2026 - 404 and redirect handling** - Unknown routes return raw errors. Add a catch-all route on the frontend for client-side 404s.
|
- **04 - 2026 - 404 and redirect handling** - Unknown routes return raw errors. Add a catch-all route on the frontend for client-side 404s.
|
||||||
- **04 - 2026 - Multiplayer GameService unit tests** - round evaluation, scoring, tie-breaking, timeout handling
|
- **04 - 2026 - Multiplayer GameService unit tests** - round evaluation, scoring, tie-breaking, timeout handling
|
||||||
- **04 - 2026 - Security headers with helmet** - Add helmet middleware to set secure HTTP response headers.
|
- **04 - 2026 - Security headers with helmet** - Add helmet middleware to set secure HTTP response headers.
|
||||||
- **04 - 2026 - Rate limiting on API endpoints** - At minimum: auth endpoints (brute force prevention) and game endpoints (spam prevention)
|
- **04 - 2026 - Rate limiting on API endpoints** - At minimum: auth endpoints (brute force prevention) and game endpoints (spam prevention)
|
||||||
- **04 - 2026 — Migrations in deploy pipeline** — Drizzle migrate runs as a CI/CD step before the API container restarts
|
- **04 - 2026 — Migrations in deploy pipeline** — Drizzle migrate runs as a CI/CD step before the API container restarts
|
||||||
|
|
|
||||||
|
|
@ -55,13 +55,13 @@ See **Setup** for download instructions.
|
||||||
|
|
||||||
Per-language JSON files in `sources/cefr/` provide the initial CEFR level annotations. These files do not cover the full vocabulary extracted from OMW — coverage varies by language. Gaps and disagreements are handled by the enrich stage.
|
Per-language JSON files in `sources/cefr/` provide the initial CEFR level annotations. These files do not cover the full vocabulary extracted from OMW — coverage varies by language. Gaps and disagreements are handled by the enrich stage.
|
||||||
|
|
||||||
| Language | File |
|
| Language | File |
|
||||||
|---|---|
|
| -------- | ---------------------- |
|
||||||
| English | `sources/cefr/en.json` |
|
| English | `sources/cefr/en.json` |
|
||||||
| Italian | `sources/cefr/it.json` |
|
| Italian | `sources/cefr/it.json` |
|
||||||
| Spanish | `sources/cefr/es.json` |
|
| Spanish | `sources/cefr/es.json` |
|
||||||
| German | `sources/cefr/de.json` |
|
| German | `sources/cefr/de.json` |
|
||||||
| French | `sources/cefr/fr.json` |
|
| French | `sources/cefr/fr.json` |
|
||||||
|
|
||||||
These files are committed to git. For per-language coverage detail see `COVERAGE.md`.
|
These files are committed to git. For per-language coverage detail see `COVERAGE.md`.
|
||||||
|
|
||||||
|
|
@ -102,13 +102,13 @@ See `LLM-SETUP.md`.
|
||||||
|
|
||||||
The pipeline runs in five stages. Each stage is independent and can be re-run without affecting the others.
|
The pipeline runs in five stages. Each stage is independent and can be re-run without affecting the others.
|
||||||
|
|
||||||
| Stage | What it does |
|
| Stage | What it does |
|
||||||
|---|---|
|
| ----------- | -------------------------------------------------------------------- |
|
||||||
| 1. Extract | Reads OMW SQLite database, outputs normalized JSON per language |
|
| 1. Extract | Reads OMW SQLite database, outputs normalized JSON per language |
|
||||||
| 2. Annotate | Merges CEFR source files into extracted data, adds source file votes |
|
| 2. Annotate | Merges CEFR source files into extracted data, adds source file votes |
|
||||||
| 3. Enrich | Runs local LLMs in two rounds — generation then voting |
|
| 3. Enrich | Runs local LLMs in two rounds — generation then voting |
|
||||||
| 4. Merge | Resolves votes, derives difficulty, splits into final and flagged |
|
| 4. Merge | Resolves votes, derives difficulty, splits into final and flagged |
|
||||||
| 5. Compare | Generates COVERAGE.md with detailed quality report |
|
| 5. Compare | Generates COVERAGE.md with detailed quality report |
|
||||||
|
|
||||||
### 1. Extract
|
### 1. Extract
|
||||||
|
|
||||||
|
|
@ -137,11 +137,11 @@ Each record in the output looks like this:
|
||||||
"fr": ["comptable"]
|
"fr": ["comptable"]
|
||||||
},
|
},
|
||||||
"glosses": {
|
"glosses": {
|
||||||
"en": ["(usually followed by 'to') having the necessary means or skill or know-how or authority to do something"]
|
"en": [
|
||||||
|
"(usually followed by 'to') having the necessary means or skill or know-how or authority to do something"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"examples": {
|
"examples": { "en": ["able to swim", "she was able to program her computer"] }
|
||||||
"en": ["able to swim", "she was able to program her computer"]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -158,6 +158,7 @@ Words appearing in the CEFR source file multiple times with different CEFR level
|
||||||
|
|
||||||
**Input:** `stage-1-extract/output/omw.json` + `stage-2-annotate/sources/cefr/{lang}.json`
|
**Input:** `stage-1-extract/output/omw.json` + `stage-2-annotate/sources/cefr/{lang}.json`
|
||||||
**Output:**
|
**Output:**
|
||||||
|
|
||||||
- `stage-2-annotate/output/{lang}.json` — one per language
|
- `stage-2-annotate/output/{lang}.json` — one per language
|
||||||
- `stage-2-annotate/output/conflicts.json` — cross-language conflicts for review
|
- `stage-2-annotate/output/conflicts.json` — cross-language conflicts for review
|
||||||
|
|
||||||
|
|
@ -177,20 +178,14 @@ Each record in the output extends the OMW record with a `votes` field and any ad
|
||||||
"es": ["capaz"],
|
"es": ["capaz"],
|
||||||
"fr": ["comptable"]
|
"fr": ["comptable"]
|
||||||
},
|
},
|
||||||
"glosses": {
|
"glosses": { "en": ["having the necessary means or skill to do something"] },
|
||||||
"en": ["having the necessary means or skill to do something"]
|
|
||||||
},
|
|
||||||
"examples": {
|
"examples": {
|
||||||
"en": [
|
"en": [
|
||||||
{ "text": "able to swim", "source": "omw" },
|
{ "text": "able to swim", "source": "omw" },
|
||||||
{ "text": "She was able to finish the task.", "source": "cefr" }
|
{ "text": "She was able to finish the task.", "source": "cefr" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"votes": {
|
"votes": { "en": { "able": { "cefr_source": "B1" } } }
|
||||||
"en": {
|
|
||||||
"able": { "cefr_source": "B1" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -297,9 +292,7 @@ Each record in the votes file looks like this:
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"examples": {
|
"examples": {
|
||||||
"en": [
|
"en": [{ "text": "the dog barked at the stranger", "source": "omw" }],
|
||||||
{ "text": "the dog barked at the stranger", "source": "omw" }
|
|
||||||
],
|
|
||||||
"fr": {
|
"fr": {
|
||||||
"candidates": [
|
"candidates": [
|
||||||
{ "text": "le chien a aboyé", "source": "model_1" },
|
{ "text": "le chien a aboyé", "source": "model_1" },
|
||||||
|
|
@ -311,8 +304,14 @@ Each record in the votes file looks like this:
|
||||||
"descriptions": {
|
"descriptions": {
|
||||||
"en": {
|
"en": {
|
||||||
"candidates": [
|
"candidates": [
|
||||||
{ "text": "a common household pet known for loyalty", "source": "model_1" },
|
{
|
||||||
{ "text": "a domesticated animal and loyal companion", "source": "model_2" }
|
"text": "a common household pet known for loyalty",
|
||||||
|
"source": "model_1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "a domesticated animal and loyal companion",
|
||||||
|
"source": "model_2"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"votes": { "model_1": 2, "model_2": 1 }
|
"votes": { "model_1": 2, "model_2": 1 }
|
||||||
}
|
}
|
||||||
|
|
@ -334,14 +333,15 @@ Reads the votes file per language and resolves the final value for every field.
|
||||||
|
|
||||||
**Difficulty mapping:**
|
**Difficulty mapping:**
|
||||||
|
|
||||||
| CEFR | Difficulty |
|
| CEFR | Difficulty |
|
||||||
|---|---|
|
| ------ | ------------ |
|
||||||
| A1, A2 | easy |
|
| A1, A2 | easy |
|
||||||
| B1, B2 | intermediate |
|
| B1, B2 | intermediate |
|
||||||
| C1, C2 | hard |
|
| C1, C2 | hard |
|
||||||
|
|
||||||
**Input:** `stage-3-enrich/output/votes/{lang}_votes.json`
|
**Input:** `stage-3-enrich/output/votes/{lang}_votes.json`
|
||||||
**Output:**
|
**Output:**
|
||||||
|
|
||||||
- `stage-4-merge/output/final/{lang}.json` — fully resolved, ready for seeding
|
- `stage-4-merge/output/final/{lang}.json` — fully resolved, ready for seeding
|
||||||
- `stage-4-merge/output/flagged/{lang}.json` — CEFR majority not reached, needs manual review before seeding
|
- `stage-4-merge/output/flagged/{lang}.json` — CEFR majority not reached, needs manual review before seeding
|
||||||
|
|
||||||
|
|
@ -360,21 +360,15 @@ Each record in `final/{lang}.json` looks like this:
|
||||||
{ "text": "dog", "cefr_level": "A1", "difficulty": "easy" },
|
{ "text": "dog", "cefr_level": "A1", "difficulty": "easy" },
|
||||||
{ "text": "canine", "cefr_level": "B2", "difficulty": "intermediate" }
|
{ "text": "canine", "cefr_level": "B2", "difficulty": "intermediate" }
|
||||||
],
|
],
|
||||||
"it": [
|
"it": [{ "text": "cane", "cefr_level": "A1", "difficulty": "easy" }]
|
||||||
{ "text": "cane", "cefr_level": "A1", "difficulty": "easy" }
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"glosses": {
|
"glosses": {
|
||||||
"en": { "text": "a domesticated carnivorous mammal", "source": "omw" },
|
"en": { "text": "a domesticated carnivorous mammal", "source": "omw" },
|
||||||
"fr": { "text": "un mammifère carnivore domestiqué", "source": "model_1" }
|
"fr": { "text": "un mammifère carnivore domestiqué", "source": "model_1" }
|
||||||
},
|
},
|
||||||
"examples": {
|
"examples": {
|
||||||
"en": [
|
"en": [{ "text": "the dog barked at the stranger", "source": "omw" }],
|
||||||
{ "text": "the dog barked at the stranger", "source": "omw" }
|
"fr": [{ "text": "le chien a aboyé", "source": "model_1" }]
|
||||||
],
|
|
||||||
"fr": [
|
|
||||||
{ "text": "le chien a aboyé", "source": "model_1" }
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"descriptions": {
|
"descriptions": {
|
||||||
"en": {
|
"en": {
|
||||||
|
|
@ -400,6 +394,7 @@ output quality per language. Run this after merge to verify output before
|
||||||
seeding the database.
|
seeding the database.
|
||||||
|
|
||||||
**Input:**
|
**Input:**
|
||||||
|
|
||||||
- `stage-4-merge/output/final/{lang}.json`
|
- `stage-4-merge/output/final/{lang}.json`
|
||||||
- `stage-4-merge/output/flagged/{lang}.json`
|
- `stage-4-merge/output/flagged/{lang}.json`
|
||||||
|
|
||||||
|
|
@ -436,12 +431,12 @@ pnpm --filter @lila/pipeline compare
|
||||||
|
|
||||||
These values are defined in `packages/shared/src/constants.ts` and enforced by database check constraints. The pipeline filters out any entries that violate them.
|
These values are defined in `packages/shared/src/constants.ts` and enforced by database check constraints. The pipeline filters out any entries that violate them.
|
||||||
|
|
||||||
| Constant | Values |
|
| Constant | Values |
|
||||||
|---|---|
|
| --------------- | ------------------------------------- |
|
||||||
| Languages | `en`, `it`, `de`, `es`, `fr` |
|
| Languages | `en`, `it`, `de`, `es`, `fr` |
|
||||||
| Parts of speech | `noun`, `verb`, `adjective`, `adverb` |
|
| Parts of speech | `noun`, `verb`, `adjective`, `adverb` |
|
||||||
| CEFR levels | `A1`, `A2`, `B1`, `B2`, `C1`, `C2` |
|
| CEFR levels | `A1`, `A2`, `B1`, `B2`, `C1`, `C2` |
|
||||||
| Difficulty | `easy`, `intermediate`, `hard` |
|
| Difficulty | `easy`, `intermediate`, `hard` |
|
||||||
|
|
||||||
Adding a new value to any of these requires a constants update and a database migration before re-running the pipeline. See **Adding a new language** for the full steps — the same process applies for new parts of speech.
|
Adding a new value to any of these requires a constants update and a database migration before re-running the pipeline. See **Adding a new language** for the full steps — the same process applies for new parts of speech.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -243,13 +243,13 @@ Automated build and deploy via Forgejo Actions. On every push to `main`, the pip
|
||||||
|
|
||||||
### Secrets (stored in Forgejo repo settings → Actions → Secrets)
|
### Secrets (stored in Forgejo repo settings → Actions → Secrets)
|
||||||
|
|
||||||
| Secret | Value |
|
| Secret | Value |
|
||||||
|---|---|
|
| ----------------- | ----------------------------------------- |
|
||||||
| REGISTRY_USER | Forgejo username |
|
| REGISTRY_USER | Forgejo username |
|
||||||
| REGISTRY_PASSWORD | Forgejo password |
|
| REGISTRY_PASSWORD | Forgejo password |
|
||||||
| SSH_PRIVATE_KEY | Contents of `~/.ssh/ci-runner` on the VPS |
|
| SSH_PRIVATE_KEY | Contents of `~/.ssh/ci-runner` on the VPS |
|
||||||
| SSH_HOST | VPS IP address |
|
| SSH_HOST | VPS IP address |
|
||||||
| SSH_USER | `lila` |
|
| SSH_USER | `lila` |
|
||||||
|
|
||||||
### Runner Configuration
|
### Runner Configuration
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,12 @@ and production scripts.
|
||||||
|
|
||||||
## Hardware (dev machine)
|
## Hardware (dev machine)
|
||||||
|
|
||||||
| Component | Spec |
|
| Component | Spec |
|
||||||
|---|---|
|
| --------- | --------------------------------------------------------------- |
|
||||||
| CPU | Intel Core i7-6500U (2 cores / 4 threads @ 3.10 GHz) |
|
| CPU | Intel Core i7-6500U (2 cores / 4 threads @ 3.10 GHz) |
|
||||||
| RAM | 8 GB |
|
| RAM | 8 GB |
|
||||||
| GPU | NVIDIA GeForce GTX 950M — 4 GB VRAM (Maxwell, CUDA compute 5.0) |
|
| GPU | NVIDIA GeForce GTX 950M — 4 GB VRAM (Maxwell, CUDA compute 5.0) |
|
||||||
| OS | Debian GNU/Linux 13 (trixie) x86_64 |
|
| OS | Debian GNU/Linux 13 (trixie) x86_64 |
|
||||||
|
|
||||||
**Local inference verdict:** viable for small/quantized models, not for
|
**Local inference verdict:** viable for small/quantized models, not for
|
||||||
production runs. See the [Local inference](#local-inference-llamacpp) section
|
production runs. See the [Local inference](#local-inference-llamacpp) section
|
||||||
|
|
@ -28,12 +28,12 @@ The enrich script uses a single, swappable provider config. All providers
|
||||||
except Anthropic expose an OpenAI-compatible API, so the same client code
|
except Anthropic expose an OpenAI-compatible API, so the same client code
|
||||||
works across all of them — only `baseURL`, `apiKey`, and `model` change.
|
works across all of them — only `baseURL`, `apiKey`, and `model` change.
|
||||||
|
|
||||||
| Provider | Use case | Cost | Rate limits |
|
| Provider | Use case | Cost | Rate limits |
|
||||||
|---|---|---|---|
|
| ---------------------- | --------------------------------------------- | ------------------ | ---------------------- |
|
||||||
| llama.cpp (local) | Quality testing, overnight dev runs | Free (electricity) | None |
|
| llama.cpp (local) | Quality testing, overnight dev runs | Free (electricity) | None |
|
||||||
| OpenRouter (free tier) | Quality comparison, multi-model evaluation | Free | 50 req/day, 20 req/min |
|
| OpenRouter (free tier) | Quality comparison, multi-model evaluation | Free | 50 req/day, 20 req/min |
|
||||||
| OpenRouter (paid) | Production runs if local quality insufficient | Pay-per-token | None |
|
| OpenRouter (paid) | Production runs if local quality insufficient | Pay-per-token | None |
|
||||||
| Anthropic API | Quality baseline / reference | Pay-per-token | Standard |
|
| Anthropic API | Quality baseline / reference | Pay-per-token | Standard |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -58,12 +58,12 @@ in hybrid mode, slower than full-GPU but much faster than pure CPU.
|
||||||
|
|
||||||
Practical estimates for this hardware (~3.5 GB VRAM usable after drivers):
|
Practical estimates for this hardware (~3.5 GB VRAM usable after drivers):
|
||||||
|
|
||||||
| Model size | Q4 VRAM | Mode | Est. speed |
|
| Model size | Q4 VRAM | Mode | Est. speed |
|
||||||
|---|---|---|---|
|
| ---------- | ------- | ----------------------------- | ------------ |
|
||||||
| 3B | ~2.0 GB | Full GPU | ~15–20 tok/s |
|
| 3B | ~2.0 GB | Full GPU | ~15–20 tok/s |
|
||||||
| 4B | ~2.5 GB | Full GPU | ~12–18 tok/s |
|
| 4B | ~2.5 GB | Full GPU | ~12–18 tok/s |
|
||||||
| 7B | ~4.5 GB | Hybrid (~26/32 layers on GPU) | ~8–12 tok/s |
|
| 7B | ~4.5 GB | Hybrid (~26/32 layers on GPU) | ~8–12 tok/s |
|
||||||
| 13B+ | ~8 GB+ | CPU-heavy hybrid | too slow |
|
| 13B+ | ~8 GB+ | CPU-heavy hybrid | too slow |
|
||||||
|
|
||||||
### Recommended local models
|
### Recommended local models
|
||||||
|
|
||||||
|
|
@ -71,6 +71,7 @@ Two candidates worth testing, covering different points on the size/quality
|
||||||
tradeoff:
|
tradeoff:
|
||||||
|
|
||||||
**Gemma 4 E4B Instruct (Q4 / UD-Q4_K_XL)**
|
**Gemma 4 E4B Instruct (Q4 / UD-Q4_K_XL)**
|
||||||
|
|
||||||
- GGUF file: `gemma-4-E4B-it-UD-Q4_K_XL.gguf` (~2.5 GB)
|
- GGUF file: `gemma-4-E4B-it-UD-Q4_K_XL.gguf` (~2.5 GB)
|
||||||
- Source: https://huggingface.co/unsloth/gemma-4-E4B-it-GGUF
|
- Source: https://huggingface.co/unsloth/gemma-4-E4B-it-GGUF
|
||||||
- Runs fully on GPU. Brand new (April 2025), built for edge hardware, 140+
|
- Runs fully on GPU. Brand new (April 2025), built for edge hardware, 140+
|
||||||
|
|
@ -78,6 +79,7 @@ tradeoff:
|
||||||
to test.
|
to test.
|
||||||
|
|
||||||
**Qwen2.5 7B Instruct (Q4_K_M)**
|
**Qwen2.5 7B Instruct (Q4_K_M)**
|
||||||
|
|
||||||
- GGUF file: `Qwen2.5-7B-Instruct-Q4_K_M.gguf` (~4.5 GB)
|
- GGUF file: `Qwen2.5-7B-Instruct-Q4_K_M.gguf` (~4.5 GB)
|
||||||
- Source: https://huggingface.co/Qwen/Qwen2.5-7B-Instruct-GGUF
|
- Source: https://huggingface.co/Qwen/Qwen2.5-7B-Instruct-GGUF
|
||||||
- Runs in hybrid mode (~26 of 32 layers on GPU, rest on CPU), ~8–12 tok/s.
|
- Runs in hybrid mode (~26 of 32 layers on GPU, rest on CPU), ~8–12 tok/s.
|
||||||
|
|
@ -107,6 +109,7 @@ wget -O models/qwen2.5-3b-instruct-q4_k_m.gguf \
|
||||||
### Starting the server
|
### Starting the server
|
||||||
|
|
||||||
**Gemma 4 E4B** (full GPU):
|
**Gemma 4 E4B** (full GPU):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./build/bin/llama-server \
|
./build/bin/llama-server \
|
||||||
--model models/gemma-4-e4b-it-ud-q4_k_xl.gguf \
|
--model models/gemma-4-e4b-it-ud-q4_k_xl.gguf \
|
||||||
|
|
@ -117,6 +120,7 @@ wget -O models/qwen2.5-3b-instruct-q4_k_m.gguf \
|
||||||
```
|
```
|
||||||
|
|
||||||
**Qwen2.5 7B** (hybrid — tune `--n-gpu-layers` to fit your VRAM):
|
**Qwen2.5 7B** (hybrid — tune `--n-gpu-layers` to fit your VRAM):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./build/bin/llama-server \
|
./build/bin/llama-server \
|
||||||
--model models/qwen2.5-7b-instruct-q4_k_m.gguf \
|
--model models/qwen2.5-7b-instruct-q4_k_m.gguf \
|
||||||
|
|
@ -163,15 +167,16 @@ object changes.
|
||||||
|
|
||||||
Ranked by expected multilingual generation quality for en/it/de/fr/es:
|
Ranked by expected multilingual generation quality for en/it/de/fr/es:
|
||||||
|
|
||||||
| Model ID | Params | Notes |
|
| Model ID | Params | Notes |
|
||||||
|---|---|---|
|
| ---------------------------------------- | --------------------- | ------------------------------------------------------------------------------------ |
|
||||||
| `qwen/qwen3-coder:free` | 480B MoE (35B active) | Best free option. Strong multilingual despite "coder" label. Use as quality ceiling. |
|
| `qwen/qwen3-coder:free` | 480B MoE (35B active) | Best free option. Strong multilingual despite "coder" label. Use as quality ceiling. |
|
||||||
| `qwen/qwen3-next-80b-a3b-instruct:free` | 80B MoE (3B active) | Smaller Qwen, useful comparison point. |
|
| `qwen/qwen3-next-80b-a3b-instruct:free` | 80B MoE (3B active) | Smaller Qwen, useful comparison point. |
|
||||||
| `nvidia/nemotron-3-super-120b-a12b:free` | 120B MoE (12B active) | 262K context, supports structured output. |
|
| `nvidia/nemotron-3-super-120b-a12b:free` | 120B MoE (12B active) | 262K context, supports structured output. |
|
||||||
| `google/gemma-4-31b-it:free` | 31B | 140+ language support, good European language coverage. |
|
| `google/gemma-4-31b-it:free` | 31B | 140+ language support, good European language coverage. |
|
||||||
| `zhipuai/glm-4.5-air:free` | MoE | Multilingual-focused. |
|
| `zhipuai/glm-4.5-air:free` | MoE | Multilingual-focused. |
|
||||||
|
|
||||||
**Skip for this pipeline:**
|
**Skip for this pipeline:**
|
||||||
|
|
||||||
- Llama models — weaker European language generation than Qwen/Gemma
|
- Llama models — weaker European language generation than Qwen/Gemma
|
||||||
- Mistral free tier — requests may be used for model training
|
- Mistral free tier — requests may be used for model training
|
||||||
|
|
||||||
|
|
@ -194,7 +199,7 @@ change this object and re-run.
|
||||||
// config.ts
|
// config.ts
|
||||||
|
|
||||||
export type ProviderConfig = {
|
export type ProviderConfig = {
|
||||||
name: string; // used for output folder naming
|
name: string; // used for output folder naming
|
||||||
baseURL: string;
|
baseURL: string;
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
model: string;
|
model: string;
|
||||||
|
|
@ -205,8 +210,8 @@ export type ProviderConfig = {
|
||||||
export const LOCAL_QWEN3B: ProviderConfig = {
|
export const LOCAL_QWEN3B: ProviderConfig = {
|
||||||
name: "local-qwen2.5-3b",
|
name: "local-qwen2.5-3b",
|
||||||
baseURL: "http://127.0.0.1:8080/v1",
|
baseURL: "http://127.0.0.1:8080/v1",
|
||||||
apiKey: "none", // llama.cpp ignores this
|
apiKey: "none", // llama.cpp ignores this
|
||||||
model: "qwen2.5-3b", // llama.cpp ignores model name, uses loaded model
|
model: "qwen2.5-3b", // llama.cpp ignores model name, uses loaded model
|
||||||
maxTokens: 512,
|
maxTokens: 512,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -231,7 +236,7 @@ export const OR_GEMMA4_31B: ProviderConfig = {
|
||||||
// Anthropic (reference baseline — different adapter required)
|
// Anthropic (reference baseline — different adapter required)
|
||||||
export const ANTHROPIC_SONNET: ProviderConfig = {
|
export const ANTHROPIC_SONNET: ProviderConfig = {
|
||||||
name: "anthropic-sonnet",
|
name: "anthropic-sonnet",
|
||||||
baseURL: "https://api.anthropic.com/v1", // adapter handles format difference
|
baseURL: "https://api.anthropic.com/v1", // adapter handles format difference
|
||||||
apiKey: process.env.ANTHROPIC_API_KEY!,
|
apiKey: process.env.ANTHROPIC_API_KEY!,
|
||||||
model: "claude-sonnet-4-6",
|
model: "claude-sonnet-4-6",
|
||||||
maxTokens: 512,
|
maxTokens: 512,
|
||||||
|
|
@ -239,6 +244,7 @@ export const ANTHROPIC_SONNET: ProviderConfig = {
|
||||||
```
|
```
|
||||||
|
|
||||||
Output from each run lands in:
|
Output from each run lands in:
|
||||||
|
|
||||||
```
|
```
|
||||||
stage-3-enrich/test/output/{provider.name}/results.json
|
stage-3-enrich/test/output/{provider.name}/results.json
|
||||||
stage-3-enrich/test/output/{provider.name}/metrics.json
|
stage-3-enrich/test/output/{provider.name}/metrics.json
|
||||||
|
|
@ -252,21 +258,21 @@ The evaluate script compares all `metrics.json` files side by side.
|
||||||
|
|
||||||
The test script measures the following per provider run:
|
The test script measures the following per provider run:
|
||||||
|
|
||||||
| Metric | What it measures |
|
| Metric | What it measures |
|
||||||
|---|---|
|
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| **JSON parse rate** | % of responses that are valid, schema-compliant JSON. Critical — a failed parse is a wasted call. Target: >97% |
|
| **JSON parse rate** | % of responses that are valid, schema-compliant JSON. Critical — a failed parse is a wasted call. Target: >97% |
|
||||||
| **Field coverage** | % of records where all required fields are present (cefr votes for all translations, descriptions for all languages, glosses/examples for fr/es) |
|
| **Field coverage** | % of records where all required fields are present (cefr votes for all translations, descriptions for all languages, glosses/examples for fr/es) |
|
||||||
| **CEFR agreement** | For records that have a `cefr_source` vote, % where the model agrees. Measures calibration. |
|
| **CEFR agreement** | For records that have a `cefr_source` vote, % where the model agrees. Measures calibration. |
|
||||||
| **Language correctness** | Manual spot-check only — automated detection not reliable enough |
|
| **Language correctness** | Manual spot-check only — automated detection not reliable enough |
|
||||||
| **Tokens/second** | Local only. Indicates overnight run feasibility |
|
| **Tokens/second** | Local only. Indicates overnight run feasibility |
|
||||||
|
|
||||||
### Decision thresholds
|
### Decision thresholds
|
||||||
|
|
||||||
| Metric | Threshold | Action if below |
|
| Metric | Threshold | Action if below |
|
||||||
|---|---|---|
|
| --------------- | --------- | ---------------------------------------------- |
|
||||||
| JSON parse rate | < 97% | Do not use this model for production |
|
| JSON parse rate | < 97% | Do not use this model for production |
|
||||||
| Field coverage | < 95% | Prompt needs revision before production |
|
| Field coverage | < 95% | Prompt needs revision before production |
|
||||||
| CEFR agreement | < 70% | Model lacks vocabulary knowledge for this task |
|
| CEFR agreement | < 70% | Model lacks vocabulary knowledge for this task |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
# notes
|
# notes
|
||||||
|
|
||||||
|
|
||||||
## prompt
|
## prompt
|
||||||
|
|
||||||
ive attached the readme of my project. this is my current task:
|
ive attached the readme of my project. this is my current task:
|
||||||
|
|
@ -46,7 +45,7 @@ laptop: verify if docker containers run on startup (they shouldnt)
|
||||||
### vps setup
|
### vps setup
|
||||||
|
|
||||||
- monitoring and logging (eg via chrootkit or rkhunter, logwatch/monit => mails daily with summary)
|
- monitoring and logging (eg via chrootkit or rkhunter, logwatch/monit => mails daily with summary)
|
||||||
<<<<<<< HEAD
|
<<<<<<< HEAD
|
||||||
- ~~keep the vps clean (e.g. old docker images/containers)~~ ✅ CI/CD pipeline runs `docker image prune -f` after deploy
|
- ~~keep the vps clean (e.g. old docker images/containers)~~ ✅ CI/CD pipeline runs `docker image prune -f` after deploy
|
||||||
|
|
||||||
### ~~cd/ci pipeline~~ ✅ RESOLVED
|
### ~~cd/ci pipeline~~ ✅ RESOLVED
|
||||||
|
|
@ -55,9 +54,9 @@ Forgejo Actions with runner on VPS, Forgejo built-in container registry. See `de
|
||||||
|
|
||||||
### ~~postgres backups~~ ✅ RESOLVED
|
### ~~postgres backups~~ ✅ RESOLVED
|
||||||
|
|
||||||
Daily pg_dump cron job, 7-day retention, dev laptop auto-sync via rsync. See `deployment.md`.
|
# Daily pg_dump cron job, 7-day retention, dev laptop auto-sync via rsync. See `deployment.md`.
|
||||||
=======
|
|
||||||
>>>>>>> dev
|
> > > > > > > dev
|
||||||
|
|
||||||
### try now option
|
### try now option
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,10 +61,12 @@ export const evaluateAnswer = async (
|
||||||
store: GameSessionStore,
|
store: GameSessionStore,
|
||||||
): Promise<AnswerResult> => {
|
): Promise<AnswerResult> => {
|
||||||
const session = await store.get(submission.sessionId);
|
const session = await store.get(submission.sessionId);
|
||||||
if (!session) throw new NotFoundError(`Game session not found: ${submission.sessionId}`);
|
if (!session)
|
||||||
|
throw new NotFoundError(`Game session not found: ${submission.sessionId}`);
|
||||||
|
|
||||||
const correctOptionId = session.answers.get(submission.questionId);
|
const correctOptionId = session.answers.get(submission.questionId);
|
||||||
if (correctOptionId === undefined) throw new NotFoundError(`Question not found: ${submission.questionId}`);
|
if (correctOptionId === undefined)
|
||||||
|
throw new NotFoundError(`Question not found: ${submission.questionId}`);
|
||||||
|
|
||||||
// delete answered question; delete session when all questions are answered
|
// delete answered question; delete session when all questions are answered
|
||||||
session.answers.delete(submission.questionId);
|
session.answers.delete(submission.questionId);
|
||||||
|
|
@ -84,10 +86,14 @@ export const evaluateAnswer = async (
|
||||||
```ts
|
```ts
|
||||||
// ✅ option B — TTL in InMemoryGameSessionStore
|
// ✅ option B — TTL in InMemoryGameSessionStore
|
||||||
export class InMemoryGameSessionStore implements GameSessionStore {
|
export class InMemoryGameSessionStore implements GameSessionStore {
|
||||||
private sessions = new Map<string, { data: GameSessionData; expiresAt: number }>();
|
private sessions = new Map<
|
||||||
|
string,
|
||||||
|
{ data: GameSessionData; expiresAt: number }
|
||||||
|
>();
|
||||||
private readonly ttlMs: number;
|
private readonly ttlMs: number;
|
||||||
|
|
||||||
constructor(ttlMs = 30 * 60 * 1000) { // 30 minutes default
|
constructor(ttlMs = 30 * 60 * 1000) {
|
||||||
|
// 30 minutes default
|
||||||
this.ttlMs = ttlMs;
|
this.ttlMs = ttlMs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -115,51 +121,13 @@ export class InMemoryGameSessionStore implements GameSessionStore {
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. `shuffle` is defined after it's used
|
|
||||||
|
|
||||||
**Problem**
|
|
||||||
|
|
||||||
`shuffle` is called inside `createGameSession` but defined below it. It works at runtime (module evaluation order), but reads as if the file was written top-to-bottom without a plan.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// ❌ shuffle appears after the function that calls it
|
|
||||||
export const createGameSession = async (...) => {
|
|
||||||
const shuffledTexts = shuffle(optionTexts); // used here
|
|
||||||
};
|
|
||||||
|
|
||||||
const shuffle = <T>(array: T[]): T[] => { ... }; // defined down here
|
|
||||||
```
|
|
||||||
|
|
||||||
**Fix — move helpers to the top, exports to the bottom**
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// ✅ utilities first, then exported functions
|
|
||||||
const shuffle = <T>(array: T[]): T[] => {
|
|
||||||
const result = [...array];
|
|
||||||
for (let i = result.length - 1; i > 0; i--) {
|
|
||||||
const j = Math.floor(Math.random() * (i + 1));
|
|
||||||
const temp = result[i]!;
|
|
||||||
result[i] = result[j]!;
|
|
||||||
result[j] = temp;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createGameSession = async (...) => { ... };
|
|
||||||
export const evaluateAnswer = async (...) => { ... };
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
**Problem**
|
**Problem**
|
||||||
|
|
||||||
`GameRequest.rounds` is typed as `string` in `@lila/shared`, forcing the service to cast it every time:
|
`GameRequest.rounds` is typed as `string` in `@lila/shared`, forcing the service to cast it every time:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
// ❌ why is a round count a string?
|
// ❌ why is a round count a string?
|
||||||
Number(request.rounds)
|
Number(request.rounds);
|
||||||
```
|
```
|
||||||
|
|
||||||
**Fix — fix the schema in `@lila/shared`**
|
**Fix — fix the schema in `@lila/shared`**
|
||||||
|
|
@ -181,8 +149,6 @@ The `z.coerce.number()` handles the case where the value arrives as a string fro
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. `correctAnswers` is a misleading variable name
|
|
||||||
|
|
||||||
**Problem**
|
**Problem**
|
||||||
|
|
||||||
The variable holds `terms` — word pairs fetched from the database. Calling them `correctAnswers` jumps ahead semantically; they only become "correct answers" once options are constructed around them.
|
The variable holds `terms` — word pairs fetched from the database. Calling them `correctAnswers` jumps ahead semantically; they only become "correct answers" once options are constructed around them.
|
||||||
|
|
@ -242,7 +208,10 @@ it("correct answer appears exactly once in options even if distractor matches",
|
||||||
// simulate getDistractors returning the correct answer as one of the distractors
|
// simulate getDistractors returning the correct answer as one of the distractors
|
||||||
mockGetDistractors.mockResolvedValueOnce(["cane", "wrong2", "wrong3"]);
|
mockGetDistractors.mockResolvedValueOnce(["cane", "wrong2", "wrong3"]);
|
||||||
|
|
||||||
const session = await createGameSession(validRequest, new InMemoryGameSessionStore());
|
const session = await createGameSession(
|
||||||
|
validRequest,
|
||||||
|
new InMemoryGameSessionStore(),
|
||||||
|
);
|
||||||
const question = session.questions[0]!;
|
const question = session.questions[0]!;
|
||||||
const optionTexts = question.options.map((o) => o.text);
|
const optionTexts = question.options.map((o) => o.text);
|
||||||
|
|
||||||
|
|
@ -323,16 +292,14 @@ his `sessionId`.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
// GameSessionStore.ts
|
// GameSessionStore.ts
|
||||||
export type GameSessionData = {
|
export type GameSessionData = { answers: Map<string, number>; userId: string };
|
||||||
answers: Map<string, number>;
|
|
||||||
userId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
// evaluateAnswer
|
// evaluateAnswer
|
||||||
const session = await store.get(submission.sessionId);
|
const session = await store.get(submission.sessionId);
|
||||||
|
|
||||||
if (!session) throw new NotFoundError(`Game session not found`);
|
if (!session) throw new NotFoundError(`Game session not found`);
|
||||||
if (session.userId !== requestingUserId) throw new NotFoundError(`Game session not found`);
|
if (session.userId !== requestingUserId)
|
||||||
|
throw new NotFoundError(`Game session not found`);
|
||||||
// ^^^ same error — don't confirm the session exists to the wrong user
|
// ^^^ same error — don't confirm the session exists to the wrong user
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -364,8 +331,9 @@ if (terms.length === 0) {
|
||||||
it("throws when getGameTerms returns no terms", async () => {
|
it("throws when getGameTerms returns no terms", async () => {
|
||||||
mockGetGameTerms.mockResolvedValue([]);
|
mockGetGameTerms.mockResolvedValue([]);
|
||||||
|
|
||||||
await expect(createGameSession(validRequest, new InMemoryGameSessionStore()))
|
await expect(
|
||||||
.rejects.toThrow("No terms found");
|
createGameSession(validRequest, new InMemoryGameSessionStore()),
|
||||||
|
).rejects.toThrow("No terms found");
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -387,8 +355,9 @@ it("throws when getGameTerms returns no terms", async () => {
|
||||||
it("propagates getDistractors failure", async () => {
|
it("propagates getDistractors failure", async () => {
|
||||||
mockGetDistractors.mockRejectedValue(new Error("db timeout"));
|
mockGetDistractors.mockRejectedValue(new Error("db timeout"));
|
||||||
|
|
||||||
await expect(createGameSession(validRequest, new InMemoryGameSessionStore()))
|
await expect(
|
||||||
.rejects.toThrow("db timeout");
|
createGameSession(validRequest, new InMemoryGameSessionStore()),
|
||||||
|
).rejects.toThrow("db timeout");
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,9 +51,9 @@ This is the full vision. The current implementation already covers most of it; r
|
||||||
|
|
||||||
### What is CUT from the MVP
|
### What is CUT from the MVP
|
||||||
|
|
||||||
| Feature | Why cut |
|
| Feature | Why cut |
|
||||||
| ------------------------------- | -------------------------------------- |
|
| --------------------- | ---------- |
|
||||||
| User stats / profiles | Needs auth |
|
| User stats / profiles | Needs auth |
|
||||||
|
|
||||||
These are not deleted from the plan — they are deferred. The architecture is already designed to support them. See Section 11 (Post-MVP Ladder).
|
These are not deleted from the plan — they are deferred. The architecture is already designed to support them. See Section 11 (Post-MVP Ladder).
|
||||||
|
|
||||||
|
|
@ -63,22 +63,22 @@ These are not deleted from the plan — they are deferred. The architecture is a
|
||||||
|
|
||||||
The monorepo structure and tooling are already set up. This is the full stack.
|
The monorepo structure and tooling are already set up. This is the full stack.
|
||||||
|
|
||||||
| Layer | Technology | Status |
|
| Layer | Technology | Status |
|
||||||
| ------------ | ------------------------------ | ----------- |
|
| ------------ | ------------------------------ | ------------------------------------------------------ |
|
||||||
| Monorepo | pnpm workspaces | ✅ |
|
| Monorepo | pnpm workspaces | ✅ |
|
||||||
| Frontend | React 18, Vite, TypeScript | ✅ |
|
| Frontend | React 18, Vite, TypeScript | ✅ |
|
||||||
| Routing | TanStack Router | ✅ |
|
| Routing | TanStack Router | ✅ |
|
||||||
| Server state | TanStack Query | ✅ |
|
| Server state | TanStack Query | ✅ |
|
||||||
| Client state | Zustand | ✅ |
|
| Client state | Zustand | ✅ |
|
||||||
| Styling | Tailwind CSS + shadcn/ui | ✅ |
|
| Styling | Tailwind CSS + shadcn/ui | ✅ |
|
||||||
| Backend | Node.js, Express, TypeScript | ✅ |
|
| Backend | Node.js, Express, TypeScript | ✅ |
|
||||||
| Database | PostgreSQL + Drizzle ORM | ✅ |
|
| Database | PostgreSQL + Drizzle ORM | ✅ |
|
||||||
| Validation | Zod (shared schemas) | ✅ |
|
| Validation | Zod (shared schemas) | ✅ |
|
||||||
| Testing | Vitest, supertest | ✅ |
|
| Testing | Vitest, supertest | ✅ |
|
||||||
| Auth | Better Auth (Google + GitHub) | ✅ |
|
| Auth | Better Auth (Google + GitHub) | ✅ |
|
||||||
| Deployment | Docker Compose, Caddy, Hetzner | ✅ |
|
| Deployment | Docker Compose, Caddy, Hetzner | ✅ |
|
||||||
| CI/CD | Forgejo Actions | ✅ |
|
| CI/CD | Forgejo Actions | ✅ |
|
||||||
| Realtime | WebSockets (`ws` library) | ✅ |
|
| Realtime | WebSockets (`ws` library) | ✅ |
|
||||||
| Cache | Valkey | ⚠️ optional (used locally; production/state hardening) |
|
| Cache | Valkey | ⚠️ optional (used locally; production/state hardening) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -288,26 +288,27 @@ After completing a task: share the code, ask what to refactor and why. The LLM s
|
||||||
## 11. Post-MVP Ladder
|
## 11. Post-MVP Ladder
|
||||||
|
|
||||||
<<<<<<< HEAD
|
<<<<<<< HEAD
|
||||||
| Phase | What it adds | Status |
|
| Phase | What it adds | Status |
|
||||||
| ----------------- | ------------------------------------------------------------------------------- | ------ |
|
| ----------------- | ------------------------------------------------------------------------------- | ------ |
|
||||||
| Auth | Better Auth (Google + GitHub), embedded in Express API, user rows in DB | ✅ |
|
| Auth | Better Auth (Google + GitHub), embedded in Express API, user rows in DB | ✅ |
|
||||||
| Deployment | Docker Compose, Caddy, Forgejo, CI/CD, Hetzner VPS | ✅ |
|
| Deployment | Docker Compose, Caddy, Forgejo, CI/CD, Hetzner VPS | ✅ |
|
||||||
| Hardening (partial) | CI/CD pipeline, DB backups | ✅ |
|
| Hardening (partial) | CI/CD pipeline, DB backups | ✅ |
|
||||||
| User Stats | Games played, score history, profile page | ❌ |
|
| User Stats | Games played, score history, profile page | ❌ |
|
||||||
| Multiplayer Lobby | Room creation, join by code, WebSocket connection | ❌ |
|
| Multiplayer Lobby | Room creation, join by code, WebSocket connection | ❌ |
|
||||||
| Multiplayer Game | Simultaneous answers, server timer, live scores, winner screen | ❌ |
|
| Multiplayer Game | Simultaneous answers, server timer, live scores, winner screen | ❌ |
|
||||||
| Hardening (rest) | Rate limiting, error boundaries, monitoring, accessibility | ❌ |
|
| Hardening (rest) | Rate limiting, error boundaries, monitoring, accessibility | ❌ |
|
||||||
=======
|
=======
|
||||||
| Phase | What it adds | Status |
|
| Phase | What it adds | Status |
|
||||||
| ------------------- | ----------------------------------------------------------------------- | ------ |
|
| ------------------- | ----------------------------------------------------------------------- | ------ |
|
||||||
| Auth | Better Auth (Google + GitHub), embedded in Express API, user rows in DB | ✅ |
|
| Auth | Better Auth (Google + GitHub), embedded in Express API, user rows in DB | ✅ |
|
||||||
| Deployment | Docker Compose, Caddy, Forgejo, CI/CD, Hetzner VPS | ✅ |
|
| Deployment | Docker Compose, Caddy, Forgejo, CI/CD, Hetzner VPS | ✅ |
|
||||||
| Hardening (partial) | CI/CD pipeline, DB backups | ✅ |
|
| Hardening (partial) | CI/CD pipeline, DB backups | ✅ |
|
||||||
| User Stats | Games played, score history, profile page | ❌ |
|
| User Stats | Games played, score history, profile page | ❌ |
|
||||||
| Multiplayer Lobby | Room creation, join by code, WebSocket connection | ✅ |
|
| Multiplayer Lobby | Room creation, join by code, WebSocket connection | ✅ |
|
||||||
| Multiplayer Game | Simultaneous answers, server timer, live scores, winner screen | ✅ |
|
| Multiplayer Game | Simultaneous answers, server timer, live scores, winner screen | ✅ |
|
||||||
| Hardening (rest) | Rate limiting, error boundaries, monitoring, accessibility | ❌ |
|
| Hardening (rest) | Rate limiting, error boundaries, monitoring, accessibility | ❌ |
|
||||||
>>>>>>> dev
|
|
||||||
|
> > > > > > > dev
|
||||||
|
|
||||||
### Future Data Model Extensions (deferred, additive)
|
### Future Data Model Extensions (deferred, additive)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
# Ticket Blueprint
|
# Ticket Blueprint
|
||||||
|
|
||||||
Two formats depending on task type. Choose based on whether a meaningful
|
Two formats depending on task type. Choose based on whether a meaningful
|
||||||
decision between options was made.
|
decision between options was made.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Format A — ADR (architectural/infrastructural decisions)
|
## Format A — ADR (architectural/infrastructural decisions)
|
||||||
|
|
||||||
Use when: you chose between options with long-term consequences.
|
Use when: you chose between options with long-term consequences.
|
||||||
Prefix: `adr-`
|
Prefix: `adr-`
|
||||||
|
|
||||||
|
|
@ -14,45 +15,56 @@ Prefix: `adr-`
|
||||||
# ADR: <title>
|
# ADR: <title>
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
Accepted | Superseded by | Deprecated
|
Accepted | Superseded by | Deprecated
|
||||||
|
|
||||||
## Date
|
## Date
|
||||||
|
|
||||||
YYYY-MM-DD
|
YYYY-MM-DD
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
What is the problem? Why does it need to be solved?
|
What is the problem? Why does it need to be solved?
|
||||||
|
|
||||||
## Decision
|
## Decision
|
||||||
|
|
||||||
What was chosen and why in one or two sentences.
|
What was chosen and why in one or two sentences.
|
||||||
|
|
||||||
## Options considered
|
## Options considered
|
||||||
|
|
||||||
### Option A — <name> ✅
|
### Option A — <name> ✅
|
||||||
|
|
||||||
Description. Why it was chosen.
|
Description. Why it was chosen.
|
||||||
|
|
||||||
### Option B — <name>
|
### Option B — <name>
|
||||||
|
|
||||||
Description. Why it was rejected.
|
Description. Why it was rejected.
|
||||||
|
|
||||||
## Consequences
|
## Consequences
|
||||||
|
|
||||||
- What gets better
|
- What gets better
|
||||||
- What gets worse or more complex
|
- What gets worse or more complex
|
||||||
- Operational implications
|
- Operational implications
|
||||||
- What breaks if this needs to be redone
|
- What breaks if this needs to be redone
|
||||||
|
|
||||||
## Affected files / machines
|
## Affected files / machines
|
||||||
|
|
||||||
- List files, servers, or systems touched
|
- List files, servers, or systems touched
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- Links to relevant docs
|
- Links to relevant docs
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Setup guide / implementation notes
|
## Setup guide / implementation notes
|
||||||
|
|
||||||
Step-by-step of what was actually done.
|
Step-by-step of what was actually done.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Format B — Task (features, fixes, chores)
|
## Format B — Task (features, fixes, chores)
|
||||||
|
|
||||||
Use when: routine task with a clear solution.
|
Use when: routine task with a clear solution.
|
||||||
Prefix: `feat-` / `fix-` / `chore-`
|
Prefix: `feat-` / `fix-` / `chore-`
|
||||||
|
|
||||||
|
|
@ -61,17 +73,23 @@ Prefix: `feat-` / `fix-` / `chore-`
|
||||||
# <prefix>: <title>
|
# <prefix>: <title>
|
||||||
|
|
||||||
## Problem
|
## Problem
|
||||||
|
|
||||||
What was wrong or missing?
|
What was wrong or missing?
|
||||||
|
|
||||||
## Options considered
|
## Options considered
|
||||||
|
|
||||||
### Option A — <name> ✅
|
### Option A — <name> ✅
|
||||||
|
|
||||||
### Option B — <name>
|
### Option B — <name>
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
What was done and why.
|
What was done and why.
|
||||||
|
|
||||||
## Files changed
|
## Files changed
|
||||||
|
|
||||||
- `path/to/file.ts`
|
- `path/to/file.ts`
|
||||||
|
|
||||||
## Commit
|
## Commit
|
||||||
|
|
||||||
`<type>: <message>`
|
`<type>: <message>`
|
||||||
|
|
|
||||||
|
|
@ -87,9 +87,7 @@ pass init <your-key-id>
|
||||||
Replace the entire file contents with:
|
Replace the entire file contents with:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{ "credsStore": "pass" }
|
||||||
"credsStore": "pass"
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 6. Re-login to registries
|
### 6. Re-login to registries
|
||||||
|
|
|
||||||
|
|
@ -136,7 +136,7 @@ Rejected because: coercion is for untrusted or uncontrolled inputs (form fields,
|
||||||
|
|
||||||
6. In `apps/web/src/components/game/GameSetup.tsx`:
|
6. In `apps/web/src/components/game/GameSetup.tsx`:
|
||||||
- Update `SettingGroup` props to accept `string | number`:
|
- Update `SettingGroup` props to accept `string | number`:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
type SettingGroupProps = {
|
type SettingGroupProps = {
|
||||||
options: readonly (string | number)[];
|
options: readonly (string | number)[];
|
||||||
|
|
|
||||||
37
documentation/tickets/t00003.md
Normal file
37
documentation/tickets/t00003.md
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
# refactor: extract shuffleArray to lib/utils, rename correctAnswers to terms
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
Two readability issues in `gameService.ts`:
|
||||||
|
|
||||||
|
1. `shuffle` was defined as a private function at the bottom of `gameService.ts`, after the function that calls it. It is a pure generic utility with no dependency on game domain logic, so it had no business living there.
|
||||||
|
|
||||||
|
2. The variable holding terms fetched from the database was named `correctAnswers`. These are word pairs — they only become "correct answers" once options are built around them. The name was premature and misleading.
|
||||||
|
|
||||||
|
## Options considered
|
||||||
|
|
||||||
|
### Option A — Move `shuffle` up in the same file
|
||||||
|
|
||||||
|
Simple, no new files. Fixes the ordering issue but keeps a generic utility buried in domain code.
|
||||||
|
|
||||||
|
### Option B — Extract to `lib/utils.ts` ✅
|
||||||
|
|
||||||
|
Move `shuffle` (renamed `shuffleArray`) to `apps/api/src/lib/utils.ts` and import it. Cleaner separation: domain logic stays in services, generic utilities live in `lib/`.
|
||||||
|
|
||||||
|
Chosen because `lib/` already exists, the function is reusable, and it gives future utilities a home.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
- Created `apps/api/src/lib/utils.ts` with `shuffleArray`
|
||||||
|
- Renamed `shuffle` → `shuffleArray` for clarity at the call site
|
||||||
|
- Removed the inline `shuffle` from `gameService.ts` and imported from `lib/utils.ts`
|
||||||
|
- Renamed `correctAnswers` → `terms` and `correctAnswer` → `term` throughout `gameService.ts`
|
||||||
|
|
||||||
|
## Files changed
|
||||||
|
|
||||||
|
- `apps/api/src/lib/utils.ts` — created
|
||||||
|
- `apps/api/src/services/gameService.ts` — removed `shuffle`, updated import, renamed variables
|
||||||
|
|
||||||
|
## Commit
|
||||||
|
|
||||||
|
`refactor: extract shuffleArray to lib/utils, rename correctAnswers to terms`
|
||||||
|
|
@ -110,12 +110,8 @@
|
||||||
"name": "account_user_id_user_id_fk",
|
"name": "account_user_id_user_id_fk",
|
||||||
"tableFrom": "account",
|
"tableFrom": "account",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["user_id"],
|
||||||
"user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
|
|
@ -149,12 +145,8 @@
|
||||||
"name": "deck_terms_deck_id_decks_id_fk",
|
"name": "deck_terms_deck_id_decks_id_fk",
|
||||||
"tableFrom": "deck_terms",
|
"tableFrom": "deck_terms",
|
||||||
"tableTo": "decks",
|
"tableTo": "decks",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["deck_id"],
|
||||||
"deck_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
},
|
},
|
||||||
|
|
@ -162,12 +154,8 @@
|
||||||
"name": "deck_terms_term_id_terms_id_fk",
|
"name": "deck_terms_term_id_terms_id_fk",
|
||||||
"tableFrom": "deck_terms",
|
"tableFrom": "deck_terms",
|
||||||
"tableTo": "terms",
|
"tableTo": "terms",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["term_id"],
|
||||||
"term_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
|
|
@ -175,10 +163,7 @@
|
||||||
"compositePrimaryKeys": {
|
"compositePrimaryKeys": {
|
||||||
"deck_terms_deck_id_term_id_pk": {
|
"deck_terms_deck_id_term_id_pk": {
|
||||||
"name": "deck_terms_deck_id_term_id_pk",
|
"name": "deck_terms_deck_id_term_id_pk",
|
||||||
"columns": [
|
"columns": ["deck_id", "term_id"]
|
||||||
"deck_id",
|
|
||||||
"term_id"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"uniqueConstraints": {},
|
"uniqueConstraints": {},
|
||||||
|
|
@ -265,10 +250,7 @@
|
||||||
"unique_deck_name": {
|
"unique_deck_name": {
|
||||||
"name": "unique_deck_name",
|
"name": "unique_deck_name",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": [
|
"columns": ["name", "source_language"]
|
||||||
"name",
|
|
||||||
"source_language"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
|
|
@ -336,12 +318,8 @@
|
||||||
"name": "lobbies_host_user_id_user_id_fk",
|
"name": "lobbies_host_user_id_user_id_fk",
|
||||||
"tableFrom": "lobbies",
|
"tableFrom": "lobbies",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["host_user_id"],
|
||||||
"host_user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
|
|
@ -351,9 +329,7 @@
|
||||||
"lobbies_code_unique": {
|
"lobbies_code_unique": {
|
||||||
"name": "lobbies_code_unique",
|
"name": "lobbies_code_unique",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": [
|
"columns": ["code"]
|
||||||
"code"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
|
|
@ -402,12 +378,8 @@
|
||||||
"name": "lobby_players_lobby_id_lobbies_id_fk",
|
"name": "lobby_players_lobby_id_lobbies_id_fk",
|
||||||
"tableFrom": "lobby_players",
|
"tableFrom": "lobby_players",
|
||||||
"tableTo": "lobbies",
|
"tableTo": "lobbies",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["lobby_id"],
|
||||||
"lobby_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
},
|
},
|
||||||
|
|
@ -415,12 +387,8 @@
|
||||||
"name": "lobby_players_user_id_user_id_fk",
|
"name": "lobby_players_user_id_user_id_fk",
|
||||||
"tableFrom": "lobby_players",
|
"tableFrom": "lobby_players",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["user_id"],
|
||||||
"user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
|
|
@ -428,10 +396,7 @@
|
||||||
"compositePrimaryKeys": {
|
"compositePrimaryKeys": {
|
||||||
"lobby_players_lobby_id_user_id_pk": {
|
"lobby_players_lobby_id_user_id_pk": {
|
||||||
"name": "lobby_players_lobby_id_user_id_pk",
|
"name": "lobby_players_lobby_id_user_id_pk",
|
||||||
"columns": [
|
"columns": ["lobby_id", "user_id"]
|
||||||
"lobby_id",
|
|
||||||
"user_id"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"uniqueConstraints": {},
|
"uniqueConstraints": {},
|
||||||
|
|
@ -515,12 +480,8 @@
|
||||||
"name": "session_user_id_user_id_fk",
|
"name": "session_user_id_user_id_fk",
|
||||||
"tableFrom": "session",
|
"tableFrom": "session",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["user_id"],
|
||||||
"user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
|
|
@ -530,9 +491,7 @@
|
||||||
"session_token_unique": {
|
"session_token_unique": {
|
||||||
"name": "session_token_unique",
|
"name": "session_token_unique",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": [
|
"columns": ["token"]
|
||||||
"token"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
|
|
@ -588,12 +547,8 @@
|
||||||
"name": "term_glosses_term_id_terms_id_fk",
|
"name": "term_glosses_term_id_terms_id_fk",
|
||||||
"tableFrom": "term_glosses",
|
"tableFrom": "term_glosses",
|
||||||
"tableTo": "terms",
|
"tableTo": "terms",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["term_id"],
|
||||||
"term_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
|
|
@ -603,10 +558,7 @@
|
||||||
"unique_term_gloss": {
|
"unique_term_gloss": {
|
||||||
"name": "unique_term_gloss",
|
"name": "unique_term_gloss",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": [
|
"columns": ["term_id", "language_code"]
|
||||||
"term_id",
|
|
||||||
"language_code"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
|
|
@ -641,12 +593,8 @@
|
||||||
"name": "term_topics_term_id_terms_id_fk",
|
"name": "term_topics_term_id_terms_id_fk",
|
||||||
"tableFrom": "term_topics",
|
"tableFrom": "term_topics",
|
||||||
"tableTo": "terms",
|
"tableTo": "terms",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["term_id"],
|
||||||
"term_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
},
|
},
|
||||||
|
|
@ -654,12 +602,8 @@
|
||||||
"name": "term_topics_topic_id_topics_id_fk",
|
"name": "term_topics_topic_id_topics_id_fk",
|
||||||
"tableFrom": "term_topics",
|
"tableFrom": "term_topics",
|
||||||
"tableTo": "topics",
|
"tableTo": "topics",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["topic_id"],
|
||||||
"topic_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
|
|
@ -667,10 +611,7 @@
|
||||||
"compositePrimaryKeys": {
|
"compositePrimaryKeys": {
|
||||||
"term_topics_term_id_topic_id_pk": {
|
"term_topics_term_id_topic_id_pk": {
|
||||||
"name": "term_topics_term_id_topic_id_pk",
|
"name": "term_topics_term_id_topic_id_pk",
|
||||||
"columns": [
|
"columns": ["term_id", "topic_id"]
|
||||||
"term_id",
|
|
||||||
"topic_id"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"uniqueConstraints": {},
|
"uniqueConstraints": {},
|
||||||
|
|
@ -744,10 +685,7 @@
|
||||||
"unique_source_id": {
|
"unique_source_id": {
|
||||||
"name": "unique_source_id",
|
"name": "unique_source_id",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": [
|
"columns": ["source", "source_id"]
|
||||||
"source",
|
|
||||||
"source_id"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
|
|
@ -803,9 +741,7 @@
|
||||||
"topics_slug_unique": {
|
"topics_slug_unique": {
|
||||||
"name": "topics_slug_unique",
|
"name": "topics_slug_unique",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": [
|
"columns": ["slug"]
|
||||||
"slug"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
|
|
@ -901,12 +837,8 @@
|
||||||
"name": "translations_term_id_terms_id_fk",
|
"name": "translations_term_id_terms_id_fk",
|
||||||
"tableFrom": "translations",
|
"tableFrom": "translations",
|
||||||
"tableTo": "terms",
|
"tableTo": "terms",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["term_id"],
|
||||||
"term_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
|
|
@ -916,11 +848,7 @@
|
||||||
"unique_translations": {
|
"unique_translations": {
|
||||||
"name": "unique_translations",
|
"name": "unique_translations",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": [
|
"columns": ["term_id", "language_code", "text"]
|
||||||
"term_id",
|
|
||||||
"language_code",
|
|
||||||
"text"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
|
|
@ -997,9 +925,7 @@
|
||||||
"user_email_unique": {
|
"user_email_unique": {
|
||||||
"name": "user_email_unique",
|
"name": "user_email_unique",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": [
|
"columns": ["email"]
|
||||||
"email"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
|
|
@ -1080,9 +1006,5 @@
|
||||||
"roles": {},
|
"roles": {},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
"views": {},
|
"views": {},
|
||||||
"_meta": {
|
"_meta": { "columns": {}, "schemas": {}, "tables": {} }
|
||||||
"columns": {},
|
}
|
||||||
"schemas": {},
|
|
||||||
"tables": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -110,12 +110,8 @@
|
||||||
"name": "account_user_id_user_id_fk",
|
"name": "account_user_id_user_id_fk",
|
||||||
"tableFrom": "account",
|
"tableFrom": "account",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["user_id"],
|
||||||
"user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
|
|
@ -149,12 +145,8 @@
|
||||||
"name": "deck_terms_deck_id_decks_id_fk",
|
"name": "deck_terms_deck_id_decks_id_fk",
|
||||||
"tableFrom": "deck_terms",
|
"tableFrom": "deck_terms",
|
||||||
"tableTo": "decks",
|
"tableTo": "decks",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["deck_id"],
|
||||||
"deck_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
},
|
},
|
||||||
|
|
@ -162,12 +154,8 @@
|
||||||
"name": "deck_terms_term_id_terms_id_fk",
|
"name": "deck_terms_term_id_terms_id_fk",
|
||||||
"tableFrom": "deck_terms",
|
"tableFrom": "deck_terms",
|
||||||
"tableTo": "terms",
|
"tableTo": "terms",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["term_id"],
|
||||||
"term_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
|
|
@ -175,10 +163,7 @@
|
||||||
"compositePrimaryKeys": {
|
"compositePrimaryKeys": {
|
||||||
"deck_terms_deck_id_term_id_pk": {
|
"deck_terms_deck_id_term_id_pk": {
|
||||||
"name": "deck_terms_deck_id_term_id_pk",
|
"name": "deck_terms_deck_id_term_id_pk",
|
||||||
"columns": [
|
"columns": ["deck_id", "term_id"]
|
||||||
"deck_id",
|
|
||||||
"term_id"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"uniqueConstraints": {},
|
"uniqueConstraints": {},
|
||||||
|
|
@ -265,10 +250,7 @@
|
||||||
"unique_deck_name": {
|
"unique_deck_name": {
|
||||||
"name": "unique_deck_name",
|
"name": "unique_deck_name",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": [
|
"columns": ["name", "source_language"]
|
||||||
"name",
|
|
||||||
"source_language"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
|
|
@ -336,12 +318,8 @@
|
||||||
"name": "lobbies_host_user_id_user_id_fk",
|
"name": "lobbies_host_user_id_user_id_fk",
|
||||||
"tableFrom": "lobbies",
|
"tableFrom": "lobbies",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["host_user_id"],
|
||||||
"host_user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
|
|
@ -351,9 +329,7 @@
|
||||||
"lobbies_code_unique": {
|
"lobbies_code_unique": {
|
||||||
"name": "lobbies_code_unique",
|
"name": "lobbies_code_unique",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": [
|
"columns": ["code"]
|
||||||
"code"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
|
|
@ -402,12 +378,8 @@
|
||||||
"name": "lobby_players_lobby_id_lobbies_id_fk",
|
"name": "lobby_players_lobby_id_lobbies_id_fk",
|
||||||
"tableFrom": "lobby_players",
|
"tableFrom": "lobby_players",
|
||||||
"tableTo": "lobbies",
|
"tableTo": "lobbies",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["lobby_id"],
|
||||||
"lobby_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
},
|
},
|
||||||
|
|
@ -415,12 +387,8 @@
|
||||||
"name": "lobby_players_user_id_user_id_fk",
|
"name": "lobby_players_user_id_user_id_fk",
|
||||||
"tableFrom": "lobby_players",
|
"tableFrom": "lobby_players",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["user_id"],
|
||||||
"user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
|
|
@ -428,10 +396,7 @@
|
||||||
"compositePrimaryKeys": {
|
"compositePrimaryKeys": {
|
||||||
"lobby_players_lobby_id_user_id_pk": {
|
"lobby_players_lobby_id_user_id_pk": {
|
||||||
"name": "lobby_players_lobby_id_user_id_pk",
|
"name": "lobby_players_lobby_id_user_id_pk",
|
||||||
"columns": [
|
"columns": ["lobby_id", "user_id"]
|
||||||
"lobby_id",
|
|
||||||
"user_id"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"uniqueConstraints": {},
|
"uniqueConstraints": {},
|
||||||
|
|
@ -515,12 +480,8 @@
|
||||||
"name": "session_user_id_user_id_fk",
|
"name": "session_user_id_user_id_fk",
|
||||||
"tableFrom": "session",
|
"tableFrom": "session",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["user_id"],
|
||||||
"user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
|
|
@ -530,9 +491,7 @@
|
||||||
"session_token_unique": {
|
"session_token_unique": {
|
||||||
"name": "session_token_unique",
|
"name": "session_token_unique",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": [
|
"columns": ["token"]
|
||||||
"token"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
|
|
@ -604,12 +563,8 @@
|
||||||
"name": "term_examples_term_id_terms_id_fk",
|
"name": "term_examples_term_id_terms_id_fk",
|
||||||
"tableFrom": "term_examples",
|
"tableFrom": "term_examples",
|
||||||
"tableTo": "terms",
|
"tableTo": "terms",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["term_id"],
|
||||||
"term_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
|
|
@ -619,11 +574,7 @@
|
||||||
"unique_term_example": {
|
"unique_term_example": {
|
||||||
"name": "unique_term_example",
|
"name": "unique_term_example",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": [
|
"columns": ["term_id", "language_code", "text"]
|
||||||
"term_id",
|
|
||||||
"language_code",
|
|
||||||
"text"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
|
|
@ -684,12 +635,8 @@
|
||||||
"name": "term_glosses_term_id_terms_id_fk",
|
"name": "term_glosses_term_id_terms_id_fk",
|
||||||
"tableFrom": "term_glosses",
|
"tableFrom": "term_glosses",
|
||||||
"tableTo": "terms",
|
"tableTo": "terms",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["term_id"],
|
||||||
"term_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
|
|
@ -699,10 +646,7 @@
|
||||||
"unique_term_gloss": {
|
"unique_term_gloss": {
|
||||||
"name": "unique_term_gloss",
|
"name": "unique_term_gloss",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": [
|
"columns": ["term_id", "language_code"]
|
||||||
"term_id",
|
|
||||||
"language_code"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
|
|
@ -737,12 +681,8 @@
|
||||||
"name": "term_topics_term_id_terms_id_fk",
|
"name": "term_topics_term_id_terms_id_fk",
|
||||||
"tableFrom": "term_topics",
|
"tableFrom": "term_topics",
|
||||||
"tableTo": "terms",
|
"tableTo": "terms",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["term_id"],
|
||||||
"term_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
},
|
},
|
||||||
|
|
@ -750,12 +690,8 @@
|
||||||
"name": "term_topics_topic_id_topics_id_fk",
|
"name": "term_topics_topic_id_topics_id_fk",
|
||||||
"tableFrom": "term_topics",
|
"tableFrom": "term_topics",
|
||||||
"tableTo": "topics",
|
"tableTo": "topics",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["topic_id"],
|
||||||
"topic_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
|
|
@ -763,10 +699,7 @@
|
||||||
"compositePrimaryKeys": {
|
"compositePrimaryKeys": {
|
||||||
"term_topics_term_id_topic_id_pk": {
|
"term_topics_term_id_topic_id_pk": {
|
||||||
"name": "term_topics_term_id_topic_id_pk",
|
"name": "term_topics_term_id_topic_id_pk",
|
||||||
"columns": [
|
"columns": ["term_id", "topic_id"]
|
||||||
"term_id",
|
|
||||||
"topic_id"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"uniqueConstraints": {},
|
"uniqueConstraints": {},
|
||||||
|
|
@ -840,10 +773,7 @@
|
||||||
"unique_source_id": {
|
"unique_source_id": {
|
||||||
"name": "unique_source_id",
|
"name": "unique_source_id",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": [
|
"columns": ["source", "source_id"]
|
||||||
"source",
|
|
||||||
"source_id"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
|
|
@ -899,9 +829,7 @@
|
||||||
"topics_slug_unique": {
|
"topics_slug_unique": {
|
||||||
"name": "topics_slug_unique",
|
"name": "topics_slug_unique",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": [
|
"columns": ["slug"]
|
||||||
"slug"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
|
|
@ -997,12 +925,8 @@
|
||||||
"name": "translations_term_id_terms_id_fk",
|
"name": "translations_term_id_terms_id_fk",
|
||||||
"tableFrom": "translations",
|
"tableFrom": "translations",
|
||||||
"tableTo": "terms",
|
"tableTo": "terms",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["term_id"],
|
||||||
"term_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
|
|
@ -1012,11 +936,7 @@
|
||||||
"unique_translations": {
|
"unique_translations": {
|
||||||
"name": "unique_translations",
|
"name": "unique_translations",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": [
|
"columns": ["term_id", "language_code", "text"]
|
||||||
"term_id",
|
|
||||||
"language_code",
|
|
||||||
"text"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
|
|
@ -1093,9 +1013,7 @@
|
||||||
"user_email_unique": {
|
"user_email_unique": {
|
||||||
"name": "user_email_unique",
|
"name": "user_email_unique",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": [
|
"columns": ["email"]
|
||||||
"email"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
|
|
@ -1176,9 +1094,5 @@
|
||||||
"roles": {},
|
"roles": {},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
"views": {},
|
"views": {},
|
||||||
"_meta": {
|
"_meta": { "columns": {}, "schemas": {}, "tables": {} }
|
||||||
"columns": {},
|
}
|
||||||
"schemas": {},
|
|
||||||
"tables": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -110,12 +110,8 @@
|
||||||
"name": "account_user_id_user_id_fk",
|
"name": "account_user_id_user_id_fk",
|
||||||
"tableFrom": "account",
|
"tableFrom": "account",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["user_id"],
|
||||||
"user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
|
|
@ -149,12 +145,8 @@
|
||||||
"name": "deck_terms_deck_id_decks_id_fk",
|
"name": "deck_terms_deck_id_decks_id_fk",
|
||||||
"tableFrom": "deck_terms",
|
"tableFrom": "deck_terms",
|
||||||
"tableTo": "decks",
|
"tableTo": "decks",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["deck_id"],
|
||||||
"deck_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
},
|
},
|
||||||
|
|
@ -162,12 +154,8 @@
|
||||||
"name": "deck_terms_term_id_terms_id_fk",
|
"name": "deck_terms_term_id_terms_id_fk",
|
||||||
"tableFrom": "deck_terms",
|
"tableFrom": "deck_terms",
|
||||||
"tableTo": "terms",
|
"tableTo": "terms",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["term_id"],
|
||||||
"term_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
|
|
@ -175,10 +163,7 @@
|
||||||
"compositePrimaryKeys": {
|
"compositePrimaryKeys": {
|
||||||
"deck_terms_deck_id_term_id_pk": {
|
"deck_terms_deck_id_term_id_pk": {
|
||||||
"name": "deck_terms_deck_id_term_id_pk",
|
"name": "deck_terms_deck_id_term_id_pk",
|
||||||
"columns": [
|
"columns": ["deck_id", "term_id"]
|
||||||
"deck_id",
|
|
||||||
"term_id"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"uniqueConstraints": {},
|
"uniqueConstraints": {},
|
||||||
|
|
@ -265,10 +250,7 @@
|
||||||
"unique_deck_name": {
|
"unique_deck_name": {
|
||||||
"name": "unique_deck_name",
|
"name": "unique_deck_name",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": [
|
"columns": ["name", "source_language"]
|
||||||
"name",
|
|
||||||
"source_language"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
|
|
@ -355,12 +337,8 @@
|
||||||
"name": "lobbies_host_user_id_user_id_fk",
|
"name": "lobbies_host_user_id_user_id_fk",
|
||||||
"tableFrom": "lobbies",
|
"tableFrom": "lobbies",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["host_user_id"],
|
||||||
"host_user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
|
|
@ -370,9 +348,7 @@
|
||||||
"lobbies_code_unique": {
|
"lobbies_code_unique": {
|
||||||
"name": "lobbies_code_unique",
|
"name": "lobbies_code_unique",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": [
|
"columns": ["code"]
|
||||||
"code"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
|
|
@ -421,12 +397,8 @@
|
||||||
"name": "lobby_players_lobby_id_lobbies_id_fk",
|
"name": "lobby_players_lobby_id_lobbies_id_fk",
|
||||||
"tableFrom": "lobby_players",
|
"tableFrom": "lobby_players",
|
||||||
"tableTo": "lobbies",
|
"tableTo": "lobbies",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["lobby_id"],
|
||||||
"lobby_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
},
|
},
|
||||||
|
|
@ -434,12 +406,8 @@
|
||||||
"name": "lobby_players_user_id_user_id_fk",
|
"name": "lobby_players_user_id_user_id_fk",
|
||||||
"tableFrom": "lobby_players",
|
"tableFrom": "lobby_players",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["user_id"],
|
||||||
"user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
|
|
@ -447,10 +415,7 @@
|
||||||
"compositePrimaryKeys": {
|
"compositePrimaryKeys": {
|
||||||
"lobby_players_lobby_id_user_id_pk": {
|
"lobby_players_lobby_id_user_id_pk": {
|
||||||
"name": "lobby_players_lobby_id_user_id_pk",
|
"name": "lobby_players_lobby_id_user_id_pk",
|
||||||
"columns": [
|
"columns": ["lobby_id", "user_id"]
|
||||||
"lobby_id",
|
|
||||||
"user_id"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"uniqueConstraints": {},
|
"uniqueConstraints": {},
|
||||||
|
|
@ -534,12 +499,8 @@
|
||||||
"name": "session_user_id_user_id_fk",
|
"name": "session_user_id_user_id_fk",
|
||||||
"tableFrom": "session",
|
"tableFrom": "session",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["user_id"],
|
||||||
"user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
|
|
@ -549,9 +510,7 @@
|
||||||
"session_token_unique": {
|
"session_token_unique": {
|
||||||
"name": "session_token_unique",
|
"name": "session_token_unique",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": [
|
"columns": ["token"]
|
||||||
"token"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
|
|
@ -623,12 +582,8 @@
|
||||||
"name": "term_examples_term_id_terms_id_fk",
|
"name": "term_examples_term_id_terms_id_fk",
|
||||||
"tableFrom": "term_examples",
|
"tableFrom": "term_examples",
|
||||||
"tableTo": "terms",
|
"tableTo": "terms",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["term_id"],
|
||||||
"term_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
|
|
@ -638,11 +593,7 @@
|
||||||
"unique_term_example": {
|
"unique_term_example": {
|
||||||
"name": "unique_term_example",
|
"name": "unique_term_example",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": [
|
"columns": ["term_id", "language_code", "text"]
|
||||||
"term_id",
|
|
||||||
"language_code",
|
|
||||||
"text"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
|
|
@ -703,12 +654,8 @@
|
||||||
"name": "term_glosses_term_id_terms_id_fk",
|
"name": "term_glosses_term_id_terms_id_fk",
|
||||||
"tableFrom": "term_glosses",
|
"tableFrom": "term_glosses",
|
||||||
"tableTo": "terms",
|
"tableTo": "terms",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["term_id"],
|
||||||
"term_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
|
|
@ -718,10 +665,7 @@
|
||||||
"unique_term_gloss": {
|
"unique_term_gloss": {
|
||||||
"name": "unique_term_gloss",
|
"name": "unique_term_gloss",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": [
|
"columns": ["term_id", "language_code"]
|
||||||
"term_id",
|
|
||||||
"language_code"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
|
|
@ -756,12 +700,8 @@
|
||||||
"name": "term_topics_term_id_terms_id_fk",
|
"name": "term_topics_term_id_terms_id_fk",
|
||||||
"tableFrom": "term_topics",
|
"tableFrom": "term_topics",
|
||||||
"tableTo": "terms",
|
"tableTo": "terms",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["term_id"],
|
||||||
"term_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
},
|
},
|
||||||
|
|
@ -769,12 +709,8 @@
|
||||||
"name": "term_topics_topic_id_topics_id_fk",
|
"name": "term_topics_topic_id_topics_id_fk",
|
||||||
"tableFrom": "term_topics",
|
"tableFrom": "term_topics",
|
||||||
"tableTo": "topics",
|
"tableTo": "topics",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["topic_id"],
|
||||||
"topic_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
|
|
@ -782,10 +718,7 @@
|
||||||
"compositePrimaryKeys": {
|
"compositePrimaryKeys": {
|
||||||
"term_topics_term_id_topic_id_pk": {
|
"term_topics_term_id_topic_id_pk": {
|
||||||
"name": "term_topics_term_id_topic_id_pk",
|
"name": "term_topics_term_id_topic_id_pk",
|
||||||
"columns": [
|
"columns": ["term_id", "topic_id"]
|
||||||
"term_id",
|
|
||||||
"topic_id"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"uniqueConstraints": {},
|
"uniqueConstraints": {},
|
||||||
|
|
@ -859,10 +792,7 @@
|
||||||
"unique_source_id": {
|
"unique_source_id": {
|
||||||
"name": "unique_source_id",
|
"name": "unique_source_id",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": [
|
"columns": ["source", "source_id"]
|
||||||
"source",
|
|
||||||
"source_id"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
|
|
@ -918,9 +848,7 @@
|
||||||
"topics_slug_unique": {
|
"topics_slug_unique": {
|
||||||
"name": "topics_slug_unique",
|
"name": "topics_slug_unique",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": [
|
"columns": ["slug"]
|
||||||
"slug"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
|
|
@ -1016,12 +944,8 @@
|
||||||
"name": "translations_term_id_terms_id_fk",
|
"name": "translations_term_id_terms_id_fk",
|
||||||
"tableFrom": "translations",
|
"tableFrom": "translations",
|
||||||
"tableTo": "terms",
|
"tableTo": "terms",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["term_id"],
|
||||||
"term_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
|
|
@ -1031,11 +955,7 @@
|
||||||
"unique_translations": {
|
"unique_translations": {
|
||||||
"name": "unique_translations",
|
"name": "unique_translations",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": [
|
"columns": ["term_id", "language_code", "text"]
|
||||||
"term_id",
|
|
||||||
"language_code",
|
|
||||||
"text"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
|
|
@ -1112,9 +1032,7 @@
|
||||||
"user_email_unique": {
|
"user_email_unique": {
|
||||||
"name": "user_email_unique",
|
"name": "user_email_unique",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": [
|
"columns": ["email"]
|
||||||
"email"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
|
|
@ -1195,9 +1113,5 @@
|
||||||
"roles": {},
|
"roles": {},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
"views": {},
|
"views": {},
|
||||||
"_meta": {
|
"_meta": { "columns": {}, "schemas": {}, "tables": {} }
|
||||||
"columns": {},
|
}
|
||||||
"schemas": {},
|
|
||||||
"tables": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -110,12 +110,8 @@
|
||||||
"name": "account_user_id_user_id_fk",
|
"name": "account_user_id_user_id_fk",
|
||||||
"tableFrom": "account",
|
"tableFrom": "account",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["user_id"],
|
||||||
"user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
|
|
@ -149,12 +145,8 @@
|
||||||
"name": "deck_terms_deck_id_decks_id_fk",
|
"name": "deck_terms_deck_id_decks_id_fk",
|
||||||
"tableFrom": "deck_terms",
|
"tableFrom": "deck_terms",
|
||||||
"tableTo": "decks",
|
"tableTo": "decks",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["deck_id"],
|
||||||
"deck_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
},
|
},
|
||||||
|
|
@ -162,12 +154,8 @@
|
||||||
"name": "deck_terms_term_id_terms_id_fk",
|
"name": "deck_terms_term_id_terms_id_fk",
|
||||||
"tableFrom": "deck_terms",
|
"tableFrom": "deck_terms",
|
||||||
"tableTo": "terms",
|
"tableTo": "terms",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["term_id"],
|
||||||
"term_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
|
|
@ -175,10 +163,7 @@
|
||||||
"compositePrimaryKeys": {
|
"compositePrimaryKeys": {
|
||||||
"deck_terms_deck_id_term_id_pk": {
|
"deck_terms_deck_id_term_id_pk": {
|
||||||
"name": "deck_terms_deck_id_term_id_pk",
|
"name": "deck_terms_deck_id_term_id_pk",
|
||||||
"columns": [
|
"columns": ["deck_id", "term_id"]
|
||||||
"deck_id",
|
|
||||||
"term_id"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"uniqueConstraints": {},
|
"uniqueConstraints": {},
|
||||||
|
|
@ -265,10 +250,7 @@
|
||||||
"unique_deck_name": {
|
"unique_deck_name": {
|
||||||
"name": "unique_deck_name",
|
"name": "unique_deck_name",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": [
|
"columns": ["name", "source_language"]
|
||||||
"name",
|
|
||||||
"source_language"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
|
|
@ -336,12 +318,8 @@
|
||||||
"name": "lobbies_host_user_id_user_id_fk",
|
"name": "lobbies_host_user_id_user_id_fk",
|
||||||
"tableFrom": "lobbies",
|
"tableFrom": "lobbies",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["host_user_id"],
|
||||||
"host_user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
|
|
@ -351,9 +329,7 @@
|
||||||
"lobbies_code_unique": {
|
"lobbies_code_unique": {
|
||||||
"name": "lobbies_code_unique",
|
"name": "lobbies_code_unique",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": [
|
"columns": ["code"]
|
||||||
"code"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
|
|
@ -402,12 +378,8 @@
|
||||||
"name": "lobby_players_lobby_id_lobbies_id_fk",
|
"name": "lobby_players_lobby_id_lobbies_id_fk",
|
||||||
"tableFrom": "lobby_players",
|
"tableFrom": "lobby_players",
|
||||||
"tableTo": "lobbies",
|
"tableTo": "lobbies",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["lobby_id"],
|
||||||
"lobby_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
},
|
},
|
||||||
|
|
@ -415,12 +387,8 @@
|
||||||
"name": "lobby_players_user_id_user_id_fk",
|
"name": "lobby_players_user_id_user_id_fk",
|
||||||
"tableFrom": "lobby_players",
|
"tableFrom": "lobby_players",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["user_id"],
|
||||||
"user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
|
|
@ -428,10 +396,7 @@
|
||||||
"compositePrimaryKeys": {
|
"compositePrimaryKeys": {
|
||||||
"lobby_players_lobby_id_user_id_pk": {
|
"lobby_players_lobby_id_user_id_pk": {
|
||||||
"name": "lobby_players_lobby_id_user_id_pk",
|
"name": "lobby_players_lobby_id_user_id_pk",
|
||||||
"columns": [
|
"columns": ["lobby_id", "user_id"]
|
||||||
"lobby_id",
|
|
||||||
"user_id"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"uniqueConstraints": {},
|
"uniqueConstraints": {},
|
||||||
|
|
@ -515,12 +480,8 @@
|
||||||
"name": "session_user_id_user_id_fk",
|
"name": "session_user_id_user_id_fk",
|
||||||
"tableFrom": "session",
|
"tableFrom": "session",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["user_id"],
|
||||||
"user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
|
|
@ -530,9 +491,7 @@
|
||||||
"session_token_unique": {
|
"session_token_unique": {
|
||||||
"name": "session_token_unique",
|
"name": "session_token_unique",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": [
|
"columns": ["token"]
|
||||||
"token"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
|
|
@ -604,12 +563,8 @@
|
||||||
"name": "term_examples_term_id_terms_id_fk",
|
"name": "term_examples_term_id_terms_id_fk",
|
||||||
"tableFrom": "term_examples",
|
"tableFrom": "term_examples",
|
||||||
"tableTo": "terms",
|
"tableTo": "terms",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["term_id"],
|
||||||
"term_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
|
|
@ -619,11 +574,7 @@
|
||||||
"unique_term_example": {
|
"unique_term_example": {
|
||||||
"name": "unique_term_example",
|
"name": "unique_term_example",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": [
|
"columns": ["term_id", "language_code", "text"]
|
||||||
"term_id",
|
|
||||||
"language_code",
|
|
||||||
"text"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
|
|
@ -684,12 +635,8 @@
|
||||||
"name": "term_glosses_term_id_terms_id_fk",
|
"name": "term_glosses_term_id_terms_id_fk",
|
||||||
"tableFrom": "term_glosses",
|
"tableFrom": "term_glosses",
|
||||||
"tableTo": "terms",
|
"tableTo": "terms",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["term_id"],
|
||||||
"term_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
|
|
@ -699,10 +646,7 @@
|
||||||
"unique_term_gloss": {
|
"unique_term_gloss": {
|
||||||
"name": "unique_term_gloss",
|
"name": "unique_term_gloss",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": [
|
"columns": ["term_id", "language_code"]
|
||||||
"term_id",
|
|
||||||
"language_code"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
|
|
@ -737,12 +681,8 @@
|
||||||
"name": "term_topics_term_id_terms_id_fk",
|
"name": "term_topics_term_id_terms_id_fk",
|
||||||
"tableFrom": "term_topics",
|
"tableFrom": "term_topics",
|
||||||
"tableTo": "terms",
|
"tableTo": "terms",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["term_id"],
|
||||||
"term_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
},
|
},
|
||||||
|
|
@ -750,12 +690,8 @@
|
||||||
"name": "term_topics_topic_id_topics_id_fk",
|
"name": "term_topics_topic_id_topics_id_fk",
|
||||||
"tableFrom": "term_topics",
|
"tableFrom": "term_topics",
|
||||||
"tableTo": "topics",
|
"tableTo": "topics",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["topic_id"],
|
||||||
"topic_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
|
|
@ -763,10 +699,7 @@
|
||||||
"compositePrimaryKeys": {
|
"compositePrimaryKeys": {
|
||||||
"term_topics_term_id_topic_id_pk": {
|
"term_topics_term_id_topic_id_pk": {
|
||||||
"name": "term_topics_term_id_topic_id_pk",
|
"name": "term_topics_term_id_topic_id_pk",
|
||||||
"columns": [
|
"columns": ["term_id", "topic_id"]
|
||||||
"term_id",
|
|
||||||
"topic_id"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"uniqueConstraints": {},
|
"uniqueConstraints": {},
|
||||||
|
|
@ -840,10 +773,7 @@
|
||||||
"unique_source_id": {
|
"unique_source_id": {
|
||||||
"name": "unique_source_id",
|
"name": "unique_source_id",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": [
|
"columns": ["source", "source_id"]
|
||||||
"source",
|
|
||||||
"source_id"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
|
|
@ -899,9 +829,7 @@
|
||||||
"topics_slug_unique": {
|
"topics_slug_unique": {
|
||||||
"name": "topics_slug_unique",
|
"name": "topics_slug_unique",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": [
|
"columns": ["slug"]
|
||||||
"slug"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
|
|
@ -997,12 +925,8 @@
|
||||||
"name": "translations_term_id_terms_id_fk",
|
"name": "translations_term_id_terms_id_fk",
|
||||||
"tableFrom": "translations",
|
"tableFrom": "translations",
|
||||||
"tableTo": "terms",
|
"tableTo": "terms",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["term_id"],
|
||||||
"term_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
|
|
@ -1012,11 +936,7 @@
|
||||||
"unique_translations": {
|
"unique_translations": {
|
||||||
"name": "unique_translations",
|
"name": "unique_translations",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": [
|
"columns": ["term_id", "language_code", "text"]
|
||||||
"term_id",
|
|
||||||
"language_code",
|
|
||||||
"text"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
|
|
@ -1093,9 +1013,7 @@
|
||||||
"user_email_unique": {
|
"user_email_unique": {
|
||||||
"name": "user_email_unique",
|
"name": "user_email_unique",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": [
|
"columns": ["email"]
|
||||||
"email"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
|
|
@ -1176,9 +1094,5 @@
|
||||||
"roles": {},
|
"roles": {},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
"views": {},
|
"views": {},
|
||||||
"_meta": {
|
"_meta": { "columns": {}, "schemas": {}, "tables": {} }
|
||||||
"columns": {},
|
}
|
||||||
"schemas": {},
|
|
||||||
"tables": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -80,4 +80,4 @@
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,11 @@
|
||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "NodeNext",
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"types": ["vitest/globals"],
|
"types": ["vitest/globals"]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src",
|
"src",
|
||||||
"vitest.config.ts",
|
"vitest.config.ts",
|
||||||
"../../data-pipeline/archive/packages-db-src-old-seeding-scripts/data",
|
"../../data-pipeline/archive/packages-db-src-old-seeding-scripts/data"
|
||||||
],
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
{ "path": "./packages/db" },
|
{ "path": "./packages/db" },
|
||||||
{ "path": "./apps/web" },
|
{ "path": "./apps/web" },
|
||||||
{ "path": "./apps/api" },
|
{ "path": "./apps/api" },
|
||||||
{ "path": "./data-pipeline" },
|
{ "path": "./data-pipeline" }
|
||||||
],
|
],
|
||||||
"files": [],
|
"files": []
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue