mirror of
https://github.com/linux-odyssey/linux-odyssey.git
synced 2026-11-06 03:20:13 +08:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae790dc6a2 | ||
|
|
4ed8358738 | ||
|
|
ba5026fa56 | ||
|
|
4cf79a164c | ||
|
|
e449f9987b | ||
|
|
419440a14f | ||
|
|
54225528b3 | ||
|
|
634dc8f987 | ||
|
|
77577d9b8d | ||
|
|
2feedc329c |
No files matched your search
@@ -3,3 +3,4 @@
|
|||||||
**/dist
|
**/dist
|
||||||
**/tsconfig.tsbuildinfo
|
**/tsconfig.tsbuildinfo
|
||||||
data
|
data
|
||||||
|
postgres-data
|
||||||
@@ -3,3 +3,4 @@ vite.config.js
|
|||||||
data
|
data
|
||||||
mongo-init.js
|
mongo-init.js
|
||||||
dist
|
dist
|
||||||
|
postgres-data
|
||||||
+3
-1
@@ -2,7 +2,9 @@ module.exports = {
|
|||||||
root: true,
|
root: true,
|
||||||
env: {
|
env: {
|
||||||
node: true,
|
node: true,
|
||||||
jest: true,
|
},
|
||||||
|
globals: {
|
||||||
|
Bun: 'readonly',
|
||||||
},
|
},
|
||||||
plugins: ['@typescript-eslint', 'cypress', 'vue'],
|
plugins: ['@typescript-eslint', 'cypress', 'vue'],
|
||||||
extends: [
|
extends: [
|
||||||
|
|||||||
@@ -11,18 +11,21 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- uses: actions/setup-node@v3
|
- uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
bun-version: 1.3.14
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn install
|
run: bun install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Type check
|
||||||
|
run: bun run type-check
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: yarn build
|
run: bun run build
|
||||||
|
|
||||||
- name: Lint
|
- name: Lint
|
||||||
run: yarn lint
|
run: bun run lint
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: yarn test
|
run: bun run test
|
||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
run: docker-compose -f docker-compose.testing.yml build base backend app-test cypress
|
run: docker-compose -f docker-compose.testing.yml build base backend app-test cypress
|
||||||
|
|
||||||
- name: Build Test Quests
|
- name: Build Test Quests
|
||||||
run: docker-compose -f docker-compose.testing.yml run --rm backend yarn build:quests
|
run: docker-compose -f docker-compose.testing.yml run --rm backend bun run build:quests
|
||||||
|
|
||||||
- name: Run Cypress Tests
|
- name: Run Cypress Tests
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ pids
|
|||||||
*.seed
|
*.seed
|
||||||
*.pid.lock
|
*.pid.lock
|
||||||
data
|
data
|
||||||
|
postgres-data
|
||||||
|
|
||||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
lib-cov
|
lib-cov
|
||||||
|
|||||||
Vendored
+4
-4
@@ -4,10 +4,10 @@
|
|||||||
{
|
{
|
||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "File Graph Yarn Jest",
|
"name": "File Graph Tests",
|
||||||
"runtimeExecutable": "yarn",
|
"runtimeExecutable": "bun",
|
||||||
"args": ["test", "--runInBand"],
|
"args": ["test", "packages/file-graph"],
|
||||||
"cwd": "${workspaceFolder}/packages/file-graph",
|
"cwd": "${workspaceFolder}",
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
"internalConsoleOptions": "neverOpen"
|
"internalConsoleOptions": "neverOpen"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,16 +5,15 @@
|
|||||||
|
|
||||||
## TLDR
|
## TLDR
|
||||||
|
|
||||||
yarn install
|
bun install
|
||||||
cp .env.sample .env
|
cp .env.sample .env
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
yarn dev
|
bun run dev
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- NodeJS v20+
|
- Bun: v1.3+
|
||||||
- Docker
|
- Docker
|
||||||
- Yarn: v1.22
|
|
||||||
|
|
||||||
## Known Issues
|
## Known Issues
|
||||||
|
|
||||||
@@ -26,7 +25,7 @@ Windows user please refer to [docs/windows-setup.md](docs/windows-setup.md) for
|
|||||||
|
|
||||||
git clone https://github.com/lancatlin/linux-odyssey.git
|
git clone https://github.com/lancatlin/linux-odyssey.git
|
||||||
cd linux-odyssey
|
cd linux-odyssey
|
||||||
yarn install
|
bun install
|
||||||
|
|
||||||
## Project Structures
|
## Project Structures
|
||||||
|
|
||||||
@@ -104,7 +103,7 @@ Customized `entrypoint.sh` for each quest is not yet supported.
|
|||||||
|
|
||||||
To build all quests:
|
To build all quests:
|
||||||
|
|
||||||
yarn build:quests
|
bun run build:quests
|
||||||
|
|
||||||
The base image is built with docker-compose, so you can use `docker compose build base` to rebuild it. Once you rebuild the base image, you have to rebuild all quests.
|
The base image is built with docker-compose, so you can use `docker compose build base` to rebuild it. Once you rebuild the base image, you have to rebuild all quests.
|
||||||
|
|
||||||
@@ -134,11 +133,11 @@ Copy `.env.sample` to `.env` and customize it for your needs.
|
|||||||
|
|
||||||
Run dev server (frontend, backend):
|
Run dev server (frontend, backend):
|
||||||
|
|
||||||
yarn dev
|
bun run dev
|
||||||
|
|
||||||
To develop analytics dashboard:
|
To develop analytics dashboard:
|
||||||
|
|
||||||
yarn analytics
|
bun run analytics
|
||||||
|
|
||||||
The default username and password is `alex` and `Alex1234`.
|
The default username and password is `alex` and `Alex1234`.
|
||||||
|
|
||||||
@@ -146,12 +145,12 @@ The default username and password is `alex` and `Alex1234`.
|
|||||||
|
|
||||||
Unit tests:
|
Unit tests:
|
||||||
|
|
||||||
yarn test:unit
|
bun run test
|
||||||
|
|
||||||
Run Cypress locally, using the current development containers:
|
Run Cypress locally, using the current development containers:
|
||||||
|
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
yarn test:e2e
|
bun run test:e2e
|
||||||
|
|
||||||
Run Cypress in container, create brand-new containers along with it:
|
Run Cypress in container, create brand-new containers along with it:
|
||||||
|
|
||||||
@@ -183,7 +182,7 @@ Start the service
|
|||||||
|
|
||||||
# Server side
|
# Server side
|
||||||
docker compose -f docker-compose.prod.yml pull
|
docker compose -f docker-compose.prod.yml pull
|
||||||
yarn build:quests
|
bun run build:quests
|
||||||
docker compose -f docker-compose.prod.yml up -d
|
docker compose -f docker-compose.prod.yml up -d
|
||||||
|
|
||||||
### Necessary Configuration
|
### Necessary Configuration
|
||||||
@@ -197,4 +196,4 @@ You can copy from `.env.sample`
|
|||||||
|
|
||||||
You can generate a random key by running
|
You can generate a random key by running
|
||||||
|
|
||||||
yarn setup
|
bun run setup
|
||||||
+11
-4
@@ -1,17 +1,24 @@
|
|||||||
# Base stage
|
# Base stage
|
||||||
FROM node:20-alpine AS build
|
FROM oven/bun:1.3.14-alpine AS build
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package.json yarn.lock ./
|
COPY package.json bun.lock ./
|
||||||
COPY app/package.json ./app/package.json
|
COPY app/package.json ./app/package.json
|
||||||
COPY server/package.json ./server/package.json
|
COPY server/package.json ./server/package.json
|
||||||
RUN yarn install --frozen-lockfile
|
COPY services/analytics/package.json ./services/analytics/package.json
|
||||||
|
COPY tests/package.json ./tests/package.json
|
||||||
|
COPY packages/constants/package.json ./packages/constants/package.json
|
||||||
|
COPY packages/file-graph/package.json ./packages/file-graph/package.json
|
||||||
|
COPY packages/game/package.json ./packages/game/package.json
|
||||||
|
COPY packages/models/package.json ./packages/models/package.json
|
||||||
|
COPY packages/utils/package.json ./packages/utils/package.json
|
||||||
|
RUN bun install --frozen-lockfile --filter app
|
||||||
|
|
||||||
COPY tsconfig.json ./tsconfig.json
|
COPY tsconfig.json ./tsconfig.json
|
||||||
COPY server ./server
|
COPY server ./server
|
||||||
COPY packages ./packages
|
COPY packages ./packages
|
||||||
COPY app ./app
|
COPY app ./app
|
||||||
RUN yarn workspace app build
|
RUN bun run --filter app build
|
||||||
|
|
||||||
# Production stage
|
# Production stage
|
||||||
FROM nginx:stable-alpine AS prod
|
FROM nginx:stable-alpine AS prod
|
||||||
|
|||||||
@@ -16,6 +16,10 @@
|
|||||||
"@fortawesome/free-regular-svg-icons": "^6.4.0",
|
"@fortawesome/free-regular-svg-icons": "^6.4.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.4.0",
|
"@fortawesome/free-solid-svg-icons": "^6.4.0",
|
||||||
"@fortawesome/vue-fontawesome": "^3.0.3",
|
"@fortawesome/vue-fontawesome": "^3.0.3",
|
||||||
|
"@odyssey/constants": "workspace:*",
|
||||||
|
"@odyssey/file-graph": "workspace:*",
|
||||||
|
"@odyssey/game": "workspace:*",
|
||||||
|
"@odyssey/utils": "workspace:*",
|
||||||
"@trpc/client": "^11.1.0",
|
"@trpc/client": "^11.1.0",
|
||||||
"@types/dompurify": "^3.0.5",
|
"@types/dompurify": "^3.0.5",
|
||||||
"axios": "^1.20.0",
|
"axios": "^1.20.0",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { ref, onMounted, watch, computed } from 'vue'
|
import { ref, onMounted, watch, computed } from 'vue'
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from 'vue-toastification'
|
||||||
import { DAG } from '../../../packages/utils'
|
import { DAG } from '@odyssey/utils'
|
||||||
import { trpc } from '../utils/trpc'
|
import { trpc } from '../utils/trpc'
|
||||||
import useUserProfile from '../store/userProfile'
|
import useUserProfile from '../store/userProfile'
|
||||||
import QuestIntro from './QuestIntro.vue'
|
import QuestIntro from './QuestIntro.vue'
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from 'vue-toastification'
|
||||||
|
import type { QuestLocale } from '@odyssey/constants'
|
||||||
import { useSession } from '../../store/session'
|
import { useSession } from '../../store/session'
|
||||||
import { openBugReport, openQuestSurvey } from '../../utils/formbricks'
|
import { openBugReport, openQuestSurvey } from '../../utils/formbricks'
|
||||||
import { trpc } from '../../utils/trpc'
|
import { trpc } from '../../utils/trpc'
|
||||||
import type { QuestLocale } from '../../../../packages/constants'
|
|
||||||
|
|
||||||
const { t, locale } = useI18n()
|
const { t, locale } = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import type { QuestLocale } from '@odyssey/constants'
|
||||||
|
import { SUPPORTED_LOCALES, LOCALE_NAME_MAP } from '@odyssey/constants'
|
||||||
import { setLocale } from '../../i18n'
|
import { setLocale } from '../../i18n'
|
||||||
import type { QuestLocale } from '../../../../packages/constants'
|
|
||||||
import {
|
|
||||||
SUPPORTED_LOCALES,
|
|
||||||
LOCALE_NAME_MAP,
|
|
||||||
} from '../../../../packages/constants'
|
|
||||||
|
|
||||||
const { t, locale } = useI18n({ useScope: 'global' })
|
const { t, locale } = useI18n({ useScope: 'global' })
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createI18n } from 'vue-i18n'
|
import { createI18n } from 'vue-i18n'
|
||||||
import type { QuestLocale } from '../../../packages/constants'
|
import type { QuestLocale } from '@odyssey/constants'
|
||||||
import { SUPPORTED_LOCALES } from '../../../packages/constants'
|
import { SUPPORTED_LOCALES } from '@odyssey/constants'
|
||||||
import en from './locales/en.json'
|
import en from './locales/en.json'
|
||||||
import zhTW from './locales/zh-TW.json'
|
import zhTW from './locales/zh-TW.json'
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import type { QuestLocale } from '../../../packages/constants'
|
import type { QuestLocale } from '@odyssey/constants'
|
||||||
import {
|
import { FileGraph, type FileGraphUpdateEvent } from '@odyssey/file-graph'
|
||||||
FileGraph,
|
import type { IResponse, ITask } from '@odyssey/game'
|
||||||
type FileGraphUpdateEvent,
|
|
||||||
} from '../../../packages/file-graph'
|
|
||||||
import type { IResponse, ITask } from '../../../packages/game'
|
|
||||||
import type { SessionDetail } from '../../../server/src/routers/sessionRouter'
|
import type { SessionDetail } from '../../../server/src/routers/sessionRouter'
|
||||||
import Socket from '../utils/socket'
|
import Socket from '../utils/socket'
|
||||||
import SocketTerminal from '../utils/terminal'
|
import SocketTerminal from '../utils/terminal'
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
import type { FileGraph } from '../../packages/file-graph'
|
import type { FileGraph } from '@odyssey/file-graph'
|
||||||
import type { IResponse, ITask } from '../../packages/game'
|
import type { IResponse, ITask } from '@odyssey/game'
|
||||||
|
|
||||||
export interface Task {
|
export interface Task {
|
||||||
id: number
|
id: number
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { isValidUsername } from '../../../packages/utils'
|
import { isValidUsername } from '@odyssey/utils'
|
||||||
import AuthForm from '../components/AuthForm.vue'
|
import AuthForm from '../components/AuthForm.vue'
|
||||||
import DynamicBackground from '../components/DynamicBackground.vue'
|
import DynamicBackground from '../components/DynamicBackground.vue'
|
||||||
import HeaderPart from '../components/header/HeaderPart.vue'
|
import HeaderPart from '../components/header/HeaderPart.vue'
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import validator from 'validator'
|
import validator from 'validator'
|
||||||
import { isValidUsername } from '../../../packages/utils'
|
import { isValidUsername } from '@odyssey/utils'
|
||||||
import { passwordPolicy } from '../../../packages/constants'
|
import { passwordPolicy } from '@odyssey/constants'
|
||||||
import AuthForm from '../components/AuthForm.vue'
|
import AuthForm from '../components/AuthForm.vue'
|
||||||
import DynamicBackground from '../components/DynamicBackground.vue'
|
import DynamicBackground from '../components/DynamicBackground.vue'
|
||||||
import HeaderPart from '../components/header/HeaderPart.vue'
|
import HeaderPart from '../components/header/HeaderPart.vue'
|
||||||
|
|||||||
@@ -1,19 +1,9 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import path from 'path'
|
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [vue()],
|
plugins: [vue()],
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'@game': path.resolve(__dirname, '../packages/game'),
|
|
||||||
'@models': path.resolve(__dirname, '../packages/models'),
|
|
||||||
'@utils': path.resolve(__dirname, '../packages/utils'),
|
|
||||||
'@constants': path.resolve(__dirname, '../packages/constants'),
|
|
||||||
'@file-graph': path.resolve(__dirname, '../packages/file-graph'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
server: {
|
server: {
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
proxy: {
|
proxy: {
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
presets: [
|
|
||||||
['@babel/preset-env', { targets: { node: 'current' } }],
|
|
||||||
'@babel/preset-typescript',
|
|
||||||
],
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[test]
|
||||||
|
coverage = true
|
||||||
|
coverageDir = "coverage"
|
||||||
+21
-1
@@ -22,7 +22,6 @@ services:
|
|||||||
|
|
||||||
db:
|
db:
|
||||||
image: mongo:6
|
image: mongo:6
|
||||||
restart: always
|
|
||||||
networks:
|
networks:
|
||||||
- internal
|
- internal
|
||||||
ports:
|
ports:
|
||||||
@@ -30,6 +29,26 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./data:/data/db
|
- ./data:/data/db
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
image: postgres:18.6
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
volumes:
|
||||||
|
- ./postgres-data:/var/lib/postgresql
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: commander
|
||||||
|
POSTGRES_DB: odyssey-dev
|
||||||
|
POSTGRES_PASSWORD: mysecretpassword
|
||||||
|
|
||||||
|
adminer:
|
||||||
|
image: adminer
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
ports:
|
||||||
|
- 18080:8080
|
||||||
|
|
||||||
base:
|
base:
|
||||||
image: linuxodyssey/quest-base
|
image: linuxodyssey/quest-base
|
||||||
networks:
|
networks:
|
||||||
@@ -37,6 +56,7 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: ./quests/Dockerfile
|
dockerfile: ./quests/Dockerfile
|
||||||
|
command: exit 0
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- 'host.docker.internal:host-gateway'
|
- 'host.docker.internal:host-gateway'
|
||||||
labels:
|
labels:
|
||||||
|
|||||||
+9
-15
@@ -74,29 +74,23 @@ sudo apt update
|
|||||||
sudo apt install -y git curl ca-certificates build-essential
|
sudo apt install -y git curl ca-certificates build-essential
|
||||||
```
|
```
|
||||||
|
|
||||||
### Install Node.js 20
|
### Install Bun
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
curl -fsSL https://bun.sh/install | bash
|
||||||
sudo apt install -y nodejs
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Install Yarn 1.22
|
Restart your terminal (or run `source ~/.bashrc`) so `bun` is on your `PATH`.
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo npm install -g yarn@1.22.22
|
|
||||||
```
|
|
||||||
|
|
||||||
Verify the installations:
|
Verify the installations:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git --version
|
git --version
|
||||||
node --version
|
node --version
|
||||||
npm --version
|
bun --version
|
||||||
yarn --version
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The Node.js version should be `20.x` or later, and the Yarn version should be `1.22.x`.
|
The Node.js version should be `20.x` or later, and the Bun version should be `1.3` or later.
|
||||||
|
|
||||||
## 4. Clone the Project
|
## 4. Clone the Project
|
||||||
|
|
||||||
@@ -130,10 +124,10 @@ code .
|
|||||||
From the project root, run:
|
From the project root, run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn install
|
bun install
|
||||||
```
|
```
|
||||||
|
|
||||||
Warnings during installation may be harmless. The installation succeeded if Yarn finishes with a `Done` message and does not display `error Command failed`.
|
Warnings during installation may be harmless. The installation succeeded if Bun finishes with an `installed` message and does not display an `error:` message.
|
||||||
|
|
||||||
If a `gyp` error appears, confirm that it belongs to an optional dependency before ignoring it.
|
If a `gyp` error appears, confirm that it belongs to an optional dependency before ignoring it.
|
||||||
|
|
||||||
@@ -180,7 +174,7 @@ docker compose down
|
|||||||
Start the frontend and backend development servers:
|
Start the frontend and backend development servers:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn dev
|
bun run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Keep the terminal running and open the following address in your browser:
|
Keep the terminal running and open the following address in your browser:
|
||||||
@@ -194,5 +188,5 @@ To stop the development servers, press `Ctrl+C`.
|
|||||||
You can verify that the project builds successfully by running:
|
You can verify that the project builds successfully by running:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn build
|
bun run build
|
||||||
```
|
```
|
||||||
-201
@@ -1,201 +0,0 @@
|
|||||||
/**
|
|
||||||
* For a detailed explanation regarding each configuration property, visit:
|
|
||||||
* https://jestjs.io/docs/configuration
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { Config } from 'jest'
|
|
||||||
|
|
||||||
const config: Config = {
|
|
||||||
// All imported modules in your tests should be mocked automatically
|
|
||||||
// automock: false,
|
|
||||||
|
|
||||||
// Stop running tests after `n` failures
|
|
||||||
// bail: 0,
|
|
||||||
|
|
||||||
// The directory where Jest should store its cached dependency information
|
|
||||||
// cacheDirectory: "/tmp/jest_rs",
|
|
||||||
|
|
||||||
// Automatically clear mock calls, instances, contexts and results before every test
|
|
||||||
// clearMocks: false,
|
|
||||||
|
|
||||||
// Indicates whether the coverage information should be collected while executing the test
|
|
||||||
collectCoverage: true,
|
|
||||||
|
|
||||||
// An array of glob patterns indicating a set of files for which coverage information should be collected
|
|
||||||
// collectCoverageFrom: undefined,
|
|
||||||
|
|
||||||
// The directory where Jest should output its coverage files
|
|
||||||
coverageDirectory: 'coverage',
|
|
||||||
|
|
||||||
// An array of regexp pattern strings used to skip coverage collection
|
|
||||||
// coveragePathIgnorePatterns: [
|
|
||||||
// "/node_modules/"
|
|
||||||
// ],
|
|
||||||
|
|
||||||
// Indicates which provider should be used to instrument code for coverage
|
|
||||||
coverageProvider: 'v8',
|
|
||||||
|
|
||||||
// A list of reporter names that Jest uses when writing coverage reports
|
|
||||||
// coverageReporters: [
|
|
||||||
// "json",
|
|
||||||
// "text",
|
|
||||||
// "lcov",
|
|
||||||
// "clover"
|
|
||||||
// ],
|
|
||||||
|
|
||||||
// An object that configures minimum threshold enforcement for coverage results
|
|
||||||
// coverageThreshold: undefined,
|
|
||||||
|
|
||||||
// A path to a custom dependency extractor
|
|
||||||
// dependencyExtractor: undefined,
|
|
||||||
|
|
||||||
// Make calling deprecated APIs throw helpful error messages
|
|
||||||
// errorOnDeprecated: false,
|
|
||||||
|
|
||||||
// The default configuration for fake timers
|
|
||||||
// fakeTimers: {
|
|
||||||
// "enableGlobally": false
|
|
||||||
// },
|
|
||||||
|
|
||||||
// Force coverage collection from ignored files using an array of glob patterns
|
|
||||||
// forceCoverageMatch: [],
|
|
||||||
|
|
||||||
// A path to a module which exports an async function that is triggered once before all test suites
|
|
||||||
// globalSetup: undefined,
|
|
||||||
|
|
||||||
// A path to a module which exports an async function that is triggered once after all test suites
|
|
||||||
// globalTeardown: undefined,
|
|
||||||
|
|
||||||
// A set of global variables that need to be available in all test environments
|
|
||||||
// globals: {},
|
|
||||||
|
|
||||||
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
|
|
||||||
// maxWorkers: "50%",
|
|
||||||
|
|
||||||
// An array of directory names to be searched recursively up from the requiring module's location
|
|
||||||
// moduleDirectories: [
|
|
||||||
// "node_modules"
|
|
||||||
// ],
|
|
||||||
|
|
||||||
// An array of file extensions your modules use
|
|
||||||
// moduleFileExtensions: [
|
|
||||||
// "js",
|
|
||||||
// "mjs",
|
|
||||||
// "cjs",
|
|
||||||
// "jsx",
|
|
||||||
// "ts",
|
|
||||||
// "tsx",
|
|
||||||
// "json",
|
|
||||||
// "node"
|
|
||||||
// ],
|
|
||||||
|
|
||||||
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
|
|
||||||
moduleNameMapper: {
|
|
||||||
'^(\\.{1,2}/.*)\\.js$': '$1', // Redirect .js imports to .ts files
|
|
||||||
},
|
|
||||||
|
|
||||||
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
|
||||||
// modulePathIgnorePatterns: [],
|
|
||||||
|
|
||||||
// Activates notifications for test results
|
|
||||||
// notify: false,
|
|
||||||
|
|
||||||
// An enum that specifies notification mode. Requires { notify: true }
|
|
||||||
// notifyMode: "failure-change",
|
|
||||||
|
|
||||||
// A preset that is used as a base for Jest's configuration
|
|
||||||
// preset: undefined,
|
|
||||||
|
|
||||||
// Run tests from one or more projects
|
|
||||||
// projects: undefined,
|
|
||||||
|
|
||||||
// Use this configuration option to add custom reporters to Jest
|
|
||||||
// reporters: undefined,
|
|
||||||
|
|
||||||
// Automatically reset mock state before every test
|
|
||||||
// resetMocks: false,
|
|
||||||
|
|
||||||
// Reset the module registry before running each individual test
|
|
||||||
// resetModules: false,
|
|
||||||
|
|
||||||
// A path to a custom resolver
|
|
||||||
// resolver: undefined,
|
|
||||||
|
|
||||||
// Automatically restore mock state and implementation before every test
|
|
||||||
// restoreMocks: false,
|
|
||||||
|
|
||||||
// The root directory that Jest should scan for tests and modules within
|
|
||||||
// rootDir: undefined,
|
|
||||||
|
|
||||||
// A list of paths to directories that Jest should use to search for files in
|
|
||||||
// roots: [
|
|
||||||
// "<rootDir>"
|
|
||||||
// ],
|
|
||||||
|
|
||||||
// Allows you to use a custom runner instead of Jest's default test runner
|
|
||||||
// runner: "jest-runner",
|
|
||||||
|
|
||||||
// The paths to modules that run some code to configure or set up the testing environment before each test
|
|
||||||
// setupFiles: [],
|
|
||||||
|
|
||||||
// A list of paths to modules that run some code to configure or set up the testing framework before each test
|
|
||||||
// setupFilesAfterEnv: [],
|
|
||||||
|
|
||||||
// The number of seconds after which a test is considered as slow and reported as such in the results.
|
|
||||||
// slowTestThreshold: 5,
|
|
||||||
|
|
||||||
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
|
|
||||||
// snapshotSerializers: [],
|
|
||||||
|
|
||||||
// The test environment that will be used for testing
|
|
||||||
// testEnvironment: "jest-environment-node",
|
|
||||||
|
|
||||||
// Options that will be passed to the testEnvironment
|
|
||||||
// testEnvironmentOptions: {},
|
|
||||||
|
|
||||||
// Adds a location field to test results
|
|
||||||
// testLocationInResults: false,
|
|
||||||
|
|
||||||
// The glob patterns Jest uses to detect test files
|
|
||||||
// testMatch: [
|
|
||||||
// "**/__tests__/**/*.[jt]s?(x)",
|
|
||||||
// "**/?(*.)+(spec|test).[tj]s?(x)"
|
|
||||||
// ],
|
|
||||||
|
|
||||||
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
|
||||||
// testPathIgnorePatterns: [
|
|
||||||
// "/node_modules/"
|
|
||||||
// ],
|
|
||||||
|
|
||||||
// The regexp pattern or array of patterns that Jest uses to detect test files
|
|
||||||
// testRegex: [],
|
|
||||||
|
|
||||||
// This option allows the use of a custom results processor
|
|
||||||
// testResultsProcessor: undefined,
|
|
||||||
|
|
||||||
// This option allows use of a custom test runner
|
|
||||||
// testRunner: "jest-circus/runner",
|
|
||||||
|
|
||||||
// A map from regular expressions to paths to transformers
|
|
||||||
// transform: undefined,
|
|
||||||
|
|
||||||
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
|
||||||
// transformIgnorePatterns: [
|
|
||||||
// "/node_modules/",
|
|
||||||
// "\\.pnp\\.[^\\/]+$"
|
|
||||||
// ],
|
|
||||||
|
|
||||||
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
|
||||||
// unmockedModulePathPatterns: undefined,
|
|
||||||
|
|
||||||
// Indicates whether each individual test should be reported during the run
|
|
||||||
// verbose: undefined,
|
|
||||||
|
|
||||||
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
|
|
||||||
// watchPathIgnorePatterns: [],
|
|
||||||
|
|
||||||
// Whether to use watchman for file crawling
|
|
||||||
// watchman: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default config
|
|
||||||
+18
-25
@@ -13,31 +13,27 @@
|
|||||||
"tests"
|
"tests"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"app": "yarn workspace app dev",
|
"app": "bun run --filter app dev",
|
||||||
"build": "yarn workspace server build && yarn workspace app build && yarn workspace analytics build",
|
"build": "bun run --filter app build",
|
||||||
"dev": "concurrently -n \"App,Server\" -c \"bgBlue.bold,bgGreen.bold\" \"yarn workspace app dev\" \"yarn workspace server dev\"",
|
"dev": "concurrently -n \"App,Server\" -c \"bgBlue.bold,bgGreen.bold\" \"bun run --filter app dev\" \"bun run --filter server dev\"",
|
||||||
"server": "yarn workspace server dev",
|
"server": "bun run --filter server dev",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"format": "prettier --ignore-path .prettierignore --write app server",
|
"format": "prettier --ignore-path .prettierignore --write app server",
|
||||||
"test": "yarn node --experimental-vm-modules --no-warnings $(yarn bin jest)",
|
"test": "bun test",
|
||||||
"test:e2e": "yarn workspace tests test",
|
"test:e2e": "bun run --filter tests test",
|
||||||
"test:open": "yarn workspace tests test:open",
|
"test:open": "bun run --filter tests test:open",
|
||||||
"analytics": "yarn workspace analytics dev",
|
"analytics": "bun run --filter analytics dev",
|
||||||
"setup": "yarn workspace server setup",
|
"setup": "bun run --filter server setup",
|
||||||
"clean": "yarn workspace server clean",
|
"clean": "bun run --filter server clean",
|
||||||
"build:quests": "yarn workspace server build:quests"
|
"build:quests": "bun run --filter server build:quests",
|
||||||
|
"type-check": "bun run --filter server --filter analytics --filter app type-check"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.29.6",
|
"@types/bun": "^1.4.2",
|
||||||
"@babel/preset-env": "^7.24.0",
|
|
||||||
"@babel/preset-typescript": "^7.23.3",
|
|
||||||
"@jest/globals": "^29.7.0",
|
|
||||||
"@types/jest": "^29.5.12",
|
|
||||||
"@types/node": "^20.10.4",
|
"@types/node": "^20.10.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||||
"@typescript-eslint/parser": "^6.21.0",
|
"@typescript-eslint/parser": "^6.21.0",
|
||||||
"@vue/eslint-config-prettier": "^7.1.0",
|
"@vue/eslint-config-prettier": "^7.1.0",
|
||||||
"babel-jest": "^29.7.0",
|
|
||||||
"concurrently": "^8.2.2",
|
"concurrently": "^8.2.2",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-config-airbnb-base": "^15.0.0",
|
"eslint-config-airbnb-base": "^15.0.0",
|
||||||
@@ -48,16 +44,13 @@
|
|||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-vue": "^9.15.1",
|
"eslint-plugin-vue": "^9.15.1",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"jest": "^29.7.0",
|
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.0.0",
|
||||||
"ts-node": "^10.9.2",
|
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"vue-eslint-parser": "^9.4.2",
|
"vue-eslint-parser": "^9.4.2"
|
||||||
"zod-to-json-schema": "^3.24.5"
|
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
|
"packageManager": "bun@1.3.14",
|
||||||
"resolutions": {
|
"overrides": {
|
||||||
"**/@mapbox/node-pre-gyp/tar": "^7.5.19",
|
"@mapbox/node-pre-gyp/tar": "^7.5.19",
|
||||||
"**/@cypress/request/form-data": "^2.5.4"
|
"@cypress/request/form-data": "^2.5.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "@odyssey/constants",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"main": "index.ts",
|
||||||
|
"types": "index.ts",
|
||||||
|
"exports": {
|
||||||
|
".": "./index.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"zod": "^3.24.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Copy this file to `.env` and replace the placeholder with your real connection string.
|
||||||
|
# Connection string for PostgreSQL.
|
||||||
|
# Requires PostgreSQL >= 15.
|
||||||
|
|
||||||
|
DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
src/prisma/contract.json linguist-generated
|
||||||
|
src/prisma/contract.d.ts linguist-generated
|
||||||
|
src/prisma/ops.json linguist-generated
|
||||||
|
src/prisma/migration.json linguist-generated
|
||||||
|
migrations/snapshots/**/contract.json linguist-generated
|
||||||
|
migrations/snapshots/**/contract.d.ts linguist-generated
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# dependencies (bun install)
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# output
|
||||||
|
out
|
||||||
|
dist
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# code coverage
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# logs
|
||||||
|
logs
|
||||||
|
_.log
|
||||||
|
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# caches
|
||||||
|
.eslintcache
|
||||||
|
.cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# IntelliJ based IDEs
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Finder (MacOS) folder config
|
||||||
|
.DS_Store
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# @odyssey/database
|
||||||
|
|
||||||
|
To install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
To run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
This project was created using `bun init` in bun v1.3.14. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"from": null,
|
||||||
|
"to": "6fecb00f7a40489f8c1a9a4dad506cc6d4b95789cbb32519634132f006530c04",
|
||||||
|
"providedInvariants": [],
|
||||||
|
"createdAt": "2026-10-11T08:21:15.726Z",
|
||||||
|
"migrationHash": "99b3a1febb9f739bf258c99ebe1dbfe692b86792e8eb511fc7eef8e985e8e8cd"
|
||||||
|
}
|
||||||
@@ -0,0 +1,268 @@
|
|||||||
|
#!/usr/bin/env -S node
|
||||||
|
import type { Contract as End } from '../../snapshots/6fecb00f7a40489f8c1a9a4dad506cc6d4b95789cbb32519634132f006530c04/contract';
|
||||||
|
import endContract from '../../snapshots/6fecb00f7a40489f8c1a9a4dad506cc6d4b95789cbb32519634132f006530c04/contract.json' with { type: 'json' };
|
||||||
|
import {
|
||||||
|
Migration,
|
||||||
|
MigrationCLI,
|
||||||
|
checkExpression,
|
||||||
|
col,
|
||||||
|
primaryKey,
|
||||||
|
} from '@prisma/orm-postgres/migration';
|
||||||
|
|
||||||
|
export default class M extends Migration<never, End> {
|
||||||
|
override readonly endContractJson = endContract;
|
||||||
|
|
||||||
|
override get operations() {
|
||||||
|
return [
|
||||||
|
this.createSchema({ schema: 'public' }),
|
||||||
|
this.createTable({
|
||||||
|
schema: 'public',
|
||||||
|
table: 'Command',
|
||||||
|
columns: [
|
||||||
|
col('command', 'text', { notNull: true, codecRef: { codecId: 'pg/text@1' } }),
|
||||||
|
col('error', 'text', { codecRef: { codecId: 'pg/text@1' } }),
|
||||||
|
col('id', 'BIGSERIAL', { notNull: true, codecRef: { codecId: 'pg/int8@1' } }),
|
||||||
|
col('output', 'text', { codecRef: { codecId: 'pg/text@1' } }),
|
||||||
|
col('pwd', 'text', { notNull: true, codecRef: { codecId: 'pg/text@1' } }),
|
||||||
|
col('sessionId', 'int8', { notNull: true, codecRef: { codecId: 'pg/int8@1' } }),
|
||||||
|
col('stage', 'text', { codecRef: { codecId: 'pg/text@1' } }),
|
||||||
|
col('timestamp', 'timestamptz', {
|
||||||
|
notNull: true,
|
||||||
|
codecRef: { codecId: 'pg/timestamptz-temporal@1' },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
constraints: [primaryKey(['id'])],
|
||||||
|
}),
|
||||||
|
this.createTable({
|
||||||
|
schema: 'public',
|
||||||
|
table: 'LoginAttempt',
|
||||||
|
columns: [
|
||||||
|
col('id', 'BIGSERIAL', { notNull: true, codecRef: { codecId: 'pg/int8@1' } }),
|
||||||
|
col('ip', 'text', { notNull: true, codecRef: { codecId: 'pg/text@1' } }),
|
||||||
|
col('loginMethod', 'text', { notNull: true, codecRef: { codecId: 'pg/text@1' } }),
|
||||||
|
col('message', 'text', { codecRef: { codecId: 'pg/text@1' } }),
|
||||||
|
col('success', 'bool', { notNull: true, codecRef: { codecId: 'pg/bool@1' } }),
|
||||||
|
col('time', 'timestamptz', {
|
||||||
|
notNull: true,
|
||||||
|
codecRef: { codecId: 'pg/timestamptz-temporal@1' },
|
||||||
|
}),
|
||||||
|
col('userAgent', 'text', { notNull: true, codecRef: { codecId: 'pg/text@1' } }),
|
||||||
|
col('userId', 'int8', { codecRef: { codecId: 'pg/int8@1' } }),
|
||||||
|
col('username', 'text', { notNull: true, codecRef: { codecId: 'pg/text@1' } }),
|
||||||
|
],
|
||||||
|
constraints: [
|
||||||
|
primaryKey(['id']),
|
||||||
|
checkExpression(
|
||||||
|
'LoginAttempt_loginMethod_check_f8485f11',
|
||||||
|
`"loginMethod" IN ('password', 'google', 'github')`,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
this.createTable({
|
||||||
|
schema: 'public',
|
||||||
|
table: 'Session',
|
||||||
|
columns: [
|
||||||
|
col('completedEvents', 'text[]', {
|
||||||
|
notNull: true,
|
||||||
|
codecRef: { codecId: 'pg/text@1', many: true },
|
||||||
|
}),
|
||||||
|
col('containerId', 'text', { codecRef: { codecId: 'pg/text@1' } }),
|
||||||
|
col('containerName', 'text', { codecRef: { codecId: 'pg/text@1' } }),
|
||||||
|
col('createdAt', 'timestamptz', {
|
||||||
|
notNull: true,
|
||||||
|
codecRef: { codecId: 'pg/timestamptz-temporal@1' },
|
||||||
|
}),
|
||||||
|
col('finishedAt', 'timestamptz', { codecRef: { codecId: 'pg/timestamptz-temporal@1' } }),
|
||||||
|
col('graph', 'jsonb', { notNull: true, codecRef: { codecId: 'pg/jsonb@1' } }),
|
||||||
|
col('id', 'BIGSERIAL', { notNull: true, codecRef: { codecId: 'pg/int8@1' } }),
|
||||||
|
col('lastActivityAt', 'timestamptz', {
|
||||||
|
notNull: true,
|
||||||
|
codecRef: { codecId: 'pg/timestamptz-temporal@1' },
|
||||||
|
}),
|
||||||
|
col('locale', 'text', { notNull: true, codecRef: { codecId: 'pg/text@1' } }),
|
||||||
|
col('quest', 'text', { notNull: true, codecRef: { codecId: 'pg/text@1' } }),
|
||||||
|
col('status', 'text', { notNull: true, codecRef: { codecId: 'pg/text@1' } }),
|
||||||
|
col('userId', 'int8', { notNull: true, codecRef: { codecId: 'pg/int8@1' } }),
|
||||||
|
],
|
||||||
|
constraints: [
|
||||||
|
primaryKey(['id']),
|
||||||
|
checkExpression(
|
||||||
|
'Session_completedEvents_elem_not_null_b55d23f4',
|
||||||
|
'array_position("completedEvents", NULL) IS NULL',
|
||||||
|
),
|
||||||
|
checkExpression(
|
||||||
|
'Session_status_check_8fa5bbc6',
|
||||||
|
`"status" IN ('active', 'finished', 'inactive')`,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
this.createTable({
|
||||||
|
schema: 'public',
|
||||||
|
table: 'User',
|
||||||
|
columns: [
|
||||||
|
col('createdAt', 'timestamptz', {
|
||||||
|
notNull: true,
|
||||||
|
codecRef: { codecId: 'pg/timestamptz-temporal@1' },
|
||||||
|
}),
|
||||||
|
col('email', 'text', { notNull: true, codecRef: { codecId: 'pg/text@1' } }),
|
||||||
|
col('id', 'BIGSERIAL', { notNull: true, codecRef: { codecId: 'pg/int8@1' } }),
|
||||||
|
col('isGuest', 'bool', { notNull: true, codecRef: { codecId: 'pg/bool@1' } }),
|
||||||
|
col('name', 'text', { codecRef: { codecId: 'pg/text@1' } }),
|
||||||
|
col('updatedAt', 'timestamptz', {
|
||||||
|
notNull: true,
|
||||||
|
codecRef: { codecId: 'pg/timestamptz-temporal@1' },
|
||||||
|
}),
|
||||||
|
col('username', 'text', { notNull: true, codecRef: { codecId: 'pg/text@1' } }),
|
||||||
|
],
|
||||||
|
constraints: [primaryKey(['id'])],
|
||||||
|
}),
|
||||||
|
this.createTable({
|
||||||
|
schema: 'public',
|
||||||
|
table: 'UserLoginMethod',
|
||||||
|
columns: [
|
||||||
|
col('createdAt', 'timestamptz', {
|
||||||
|
notNull: true,
|
||||||
|
codecRef: { codecId: 'pg/timestamptz-temporal@1' },
|
||||||
|
}),
|
||||||
|
col('displayName', 'text', { codecRef: { codecId: 'pg/text@1' } }),
|
||||||
|
col('hashedPassword', 'text', { codecRef: { codecId: 'pg/text@1' } }),
|
||||||
|
col('oauthId', 'text', { codecRef: { codecId: 'pg/text@1' } }),
|
||||||
|
col('type', 'text', { notNull: true, codecRef: { codecId: 'pg/text@1' } }),
|
||||||
|
col('updatedAt', 'timestamptz', {
|
||||||
|
notNull: true,
|
||||||
|
codecRef: { codecId: 'pg/timestamptz-temporal@1' },
|
||||||
|
}),
|
||||||
|
col('userId', 'int8', { notNull: true, codecRef: { codecId: 'pg/int8@1' } }),
|
||||||
|
],
|
||||||
|
constraints: [
|
||||||
|
primaryKey(['userId', 'type']),
|
||||||
|
checkExpression(
|
||||||
|
'UserLoginMethod_type_check_6814314f',
|
||||||
|
`"type" IN ('password', 'google', 'github')`,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
this.createTable({
|
||||||
|
schema: 'public',
|
||||||
|
table: 'UserProgress',
|
||||||
|
columns: [
|
||||||
|
col('finishedAt', 'timestamptz', { codecRef: { codecId: 'pg/timestamptz-temporal@1' } }),
|
||||||
|
col('quest', 'text', { notNull: true, codecRef: { codecId: 'pg/text@1' } }),
|
||||||
|
col('startedAt', 'timestamptz', {
|
||||||
|
notNull: true,
|
||||||
|
codecRef: { codecId: 'pg/timestamptz-temporal@1' },
|
||||||
|
}),
|
||||||
|
col('userId', 'int8', { notNull: true, codecRef: { codecId: 'pg/int8@1' } }),
|
||||||
|
],
|
||||||
|
constraints: [primaryKey(['userId', 'quest'])],
|
||||||
|
}),
|
||||||
|
this.addUnique({
|
||||||
|
schema: 'public',
|
||||||
|
table: 'User',
|
||||||
|
constraint: 'User_email_key',
|
||||||
|
columns: ['email'],
|
||||||
|
}),
|
||||||
|
this.addUnique({
|
||||||
|
schema: 'public',
|
||||||
|
table: 'User',
|
||||||
|
constraint: 'User_username_key',
|
||||||
|
columns: ['username'],
|
||||||
|
}),
|
||||||
|
this.addUnique({
|
||||||
|
schema: 'public',
|
||||||
|
table: 'UserLoginMethod',
|
||||||
|
constraint: 'UserLoginMethod_type_oauthId_key',
|
||||||
|
columns: ['type', 'oauthId'],
|
||||||
|
}),
|
||||||
|
this.createIndex({
|
||||||
|
schema: 'public',
|
||||||
|
table: 'Command',
|
||||||
|
index: 'Command_sessionId_idx_29f415d4',
|
||||||
|
columns: ['sessionId'],
|
||||||
|
}),
|
||||||
|
this.createIndex({
|
||||||
|
schema: 'public',
|
||||||
|
table: 'LoginAttempt',
|
||||||
|
index: 'LoginAttempt_userId_idx_a489d58a',
|
||||||
|
columns: ['userId'],
|
||||||
|
}),
|
||||||
|
this.createIndex({
|
||||||
|
schema: 'public',
|
||||||
|
table: 'Session',
|
||||||
|
index: 'Session_userId_idx_a489d58a',
|
||||||
|
columns: ['userId'],
|
||||||
|
}),
|
||||||
|
this.createIndex({
|
||||||
|
schema: 'public',
|
||||||
|
table: 'Session',
|
||||||
|
index: 'Session_userId_quest_idx_b13626bf',
|
||||||
|
columns: ['userId', 'quest'],
|
||||||
|
}),
|
||||||
|
this.createIndex({
|
||||||
|
schema: 'public',
|
||||||
|
table: 'Session',
|
||||||
|
index: 'Session_userId_status_idx_e4a128ba',
|
||||||
|
columns: ['userId', 'status'],
|
||||||
|
}),
|
||||||
|
this.createIndex({
|
||||||
|
schema: 'public',
|
||||||
|
table: 'UserLoginMethod',
|
||||||
|
index: 'UserLoginMethod_userId_idx_a489d58a',
|
||||||
|
columns: ['userId'],
|
||||||
|
}),
|
||||||
|
this.createIndex({
|
||||||
|
schema: 'public',
|
||||||
|
table: 'UserProgress',
|
||||||
|
index: 'UserProgress_userId_idx_a489d58a',
|
||||||
|
columns: ['userId'],
|
||||||
|
}),
|
||||||
|
this.addForeignKey({
|
||||||
|
schema: 'public',
|
||||||
|
table: 'Command',
|
||||||
|
foreignKey: {
|
||||||
|
name: 'Command_sessionId_fkey',
|
||||||
|
columns: ['sessionId'],
|
||||||
|
references: { schema: 'public', table: 'Session', columns: ['id'] },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
this.addForeignKey({
|
||||||
|
schema: 'public',
|
||||||
|
table: 'LoginAttempt',
|
||||||
|
foreignKey: {
|
||||||
|
name: 'LoginAttempt_userId_fkey',
|
||||||
|
columns: ['userId'],
|
||||||
|
references: { schema: 'public', table: 'User', columns: ['id'] },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
this.addForeignKey({
|
||||||
|
schema: 'public',
|
||||||
|
table: 'Session',
|
||||||
|
foreignKey: {
|
||||||
|
name: 'Session_userId_fkey',
|
||||||
|
columns: ['userId'],
|
||||||
|
references: { schema: 'public', table: 'User', columns: ['id'] },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
this.addForeignKey({
|
||||||
|
schema: 'public',
|
||||||
|
table: 'UserLoginMethod',
|
||||||
|
foreignKey: {
|
||||||
|
name: 'UserLoginMethod_userId_fkey',
|
||||||
|
columns: ['userId'],
|
||||||
|
references: { schema: 'public', table: 'User', columns: ['id'] },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
this.addForeignKey({
|
||||||
|
schema: 'public',
|
||||||
|
table: 'UserProgress',
|
||||||
|
foreignKey: {
|
||||||
|
name: 'UserProgress_userId_fkey',
|
||||||
|
columns: ['userId'],
|
||||||
|
references: { schema: 'public', table: 'User', columns: ['id'] },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MigrationCLI.run(import.meta.url, M);
|
||||||
@@ -0,0 +1,862 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "schema.public",
|
||||||
|
"label": "Create schema \"public\"",
|
||||||
|
"operationClass": "additive",
|
||||||
|
"target": {
|
||||||
|
"id": "postgres"
|
||||||
|
},
|
||||||
|
"precheck": [],
|
||||||
|
"execute": [
|
||||||
|
{
|
||||||
|
"description": "Create schema \"public\"",
|
||||||
|
"sql": "CREATE SCHEMA IF NOT EXISTS \"public\"",
|
||||||
|
"params": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"postcheck": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "table.Command",
|
||||||
|
"label": "Create table \"Command\"",
|
||||||
|
"summary": "Creates table \"Command\"",
|
||||||
|
"operationClass": "additive",
|
||||||
|
"target": {
|
||||||
|
"id": "postgres",
|
||||||
|
"details": {
|
||||||
|
"schema": "public",
|
||||||
|
"objectType": "table",
|
||||||
|
"name": "Command"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"precheck": [
|
||||||
|
{
|
||||||
|
"description": "ensure table \"Command\" does not exist",
|
||||||
|
"sql": "SELECT (to_regclass($1)) IS NULL AS \"result\"",
|
||||||
|
"params": [
|
||||||
|
"\"public\".\"Command\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"execute": [
|
||||||
|
{
|
||||||
|
"description": "create table \"Command\"",
|
||||||
|
"sql": "CREATE TABLE \"public\".\"Command\" (\n \"command\" text NOT NULL,\n \"error\" text,\n \"id\" BIGSERIAL NOT NULL,\n \"output\" text,\n \"pwd\" text NOT NULL,\n \"sessionId\" int8 NOT NULL,\n \"stage\" text,\n \"timestamp\" timestamptz NOT NULL,\n PRIMARY KEY (\"id\")\n)",
|
||||||
|
"params": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"postcheck": [
|
||||||
|
{
|
||||||
|
"description": "verify table \"Command\" exists",
|
||||||
|
"sql": "SELECT (to_regclass($1)) IS NOT NULL AS \"result\"",
|
||||||
|
"params": [
|
||||||
|
"\"public\".\"Command\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "table.LoginAttempt",
|
||||||
|
"label": "Create table \"LoginAttempt\"",
|
||||||
|
"summary": "Creates table \"LoginAttempt\"",
|
||||||
|
"operationClass": "additive",
|
||||||
|
"target": {
|
||||||
|
"id": "postgres",
|
||||||
|
"details": {
|
||||||
|
"schema": "public",
|
||||||
|
"objectType": "table",
|
||||||
|
"name": "LoginAttempt"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"precheck": [
|
||||||
|
{
|
||||||
|
"description": "ensure table \"LoginAttempt\" does not exist",
|
||||||
|
"sql": "SELECT (to_regclass($1)) IS NULL AS \"result\"",
|
||||||
|
"params": [
|
||||||
|
"\"public\".\"LoginAttempt\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"execute": [
|
||||||
|
{
|
||||||
|
"description": "create table \"LoginAttempt\"",
|
||||||
|
"sql": "CREATE TABLE \"public\".\"LoginAttempt\" (\n \"id\" BIGSERIAL NOT NULL,\n \"ip\" text NOT NULL,\n \"loginMethod\" text NOT NULL,\n \"message\" text,\n \"success\" bool NOT NULL,\n \"time\" timestamptz NOT NULL,\n \"userAgent\" text NOT NULL,\n \"userId\" int8,\n \"username\" text NOT NULL,\n PRIMARY KEY (\"id\"),\n CONSTRAINT \"LoginAttempt_loginMethod_check_f8485f11\" CHECK (\"loginMethod\" IN ('password', 'google', 'github'))\n)",
|
||||||
|
"params": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"postcheck": [
|
||||||
|
{
|
||||||
|
"description": "verify table \"LoginAttempt\" exists",
|
||||||
|
"sql": "SELECT (to_regclass($1)) IS NOT NULL AS \"result\"",
|
||||||
|
"params": [
|
||||||
|
"\"public\".\"LoginAttempt\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "table.Session",
|
||||||
|
"label": "Create table \"Session\"",
|
||||||
|
"summary": "Creates table \"Session\"",
|
||||||
|
"operationClass": "additive",
|
||||||
|
"target": {
|
||||||
|
"id": "postgres",
|
||||||
|
"details": {
|
||||||
|
"schema": "public",
|
||||||
|
"objectType": "table",
|
||||||
|
"name": "Session"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"precheck": [
|
||||||
|
{
|
||||||
|
"description": "ensure table \"Session\" does not exist",
|
||||||
|
"sql": "SELECT (to_regclass($1)) IS NULL AS \"result\"",
|
||||||
|
"params": [
|
||||||
|
"\"public\".\"Session\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"execute": [
|
||||||
|
{
|
||||||
|
"description": "create table \"Session\"",
|
||||||
|
"sql": "CREATE TABLE \"public\".\"Session\" (\n \"completedEvents\" text[] NOT NULL,\n \"containerId\" text,\n \"containerName\" text,\n \"createdAt\" timestamptz NOT NULL,\n \"finishedAt\" timestamptz,\n \"graph\" jsonb NOT NULL,\n \"id\" BIGSERIAL NOT NULL,\n \"lastActivityAt\" timestamptz NOT NULL,\n \"locale\" text NOT NULL,\n \"quest\" text NOT NULL,\n \"status\" text NOT NULL,\n \"userId\" int8 NOT NULL,\n PRIMARY KEY (\"id\"),\n CONSTRAINT \"Session_completedEvents_elem_not_null_b55d23f4\" CHECK (array_position(\"completedEvents\", NULL) IS NULL),\n CONSTRAINT \"Session_status_check_8fa5bbc6\" CHECK (\"status\" IN ('active', 'finished', 'inactive'))\n)",
|
||||||
|
"params": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"postcheck": [
|
||||||
|
{
|
||||||
|
"description": "verify table \"Session\" exists",
|
||||||
|
"sql": "SELECT (to_regclass($1)) IS NOT NULL AS \"result\"",
|
||||||
|
"params": [
|
||||||
|
"\"public\".\"Session\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "table.User",
|
||||||
|
"label": "Create table \"User\"",
|
||||||
|
"summary": "Creates table \"User\"",
|
||||||
|
"operationClass": "additive",
|
||||||
|
"target": {
|
||||||
|
"id": "postgres",
|
||||||
|
"details": {
|
||||||
|
"schema": "public",
|
||||||
|
"objectType": "table",
|
||||||
|
"name": "User"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"precheck": [
|
||||||
|
{
|
||||||
|
"description": "ensure table \"User\" does not exist",
|
||||||
|
"sql": "SELECT (to_regclass($1)) IS NULL AS \"result\"",
|
||||||
|
"params": [
|
||||||
|
"\"public\".\"User\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"execute": [
|
||||||
|
{
|
||||||
|
"description": "create table \"User\"",
|
||||||
|
"sql": "CREATE TABLE \"public\".\"User\" (\n \"createdAt\" timestamptz NOT NULL,\n \"email\" text NOT NULL,\n \"id\" BIGSERIAL NOT NULL,\n \"isGuest\" bool NOT NULL,\n \"name\" text,\n \"updatedAt\" timestamptz NOT NULL,\n \"username\" text NOT NULL,\n PRIMARY KEY (\"id\")\n)",
|
||||||
|
"params": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"postcheck": [
|
||||||
|
{
|
||||||
|
"description": "verify table \"User\" exists",
|
||||||
|
"sql": "SELECT (to_regclass($1)) IS NOT NULL AS \"result\"",
|
||||||
|
"params": [
|
||||||
|
"\"public\".\"User\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "table.UserLoginMethod",
|
||||||
|
"label": "Create table \"UserLoginMethod\"",
|
||||||
|
"summary": "Creates table \"UserLoginMethod\"",
|
||||||
|
"operationClass": "additive",
|
||||||
|
"target": {
|
||||||
|
"id": "postgres",
|
||||||
|
"details": {
|
||||||
|
"schema": "public",
|
||||||
|
"objectType": "table",
|
||||||
|
"name": "UserLoginMethod"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"precheck": [
|
||||||
|
{
|
||||||
|
"description": "ensure table \"UserLoginMethod\" does not exist",
|
||||||
|
"sql": "SELECT (to_regclass($1)) IS NULL AS \"result\"",
|
||||||
|
"params": [
|
||||||
|
"\"public\".\"UserLoginMethod\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"execute": [
|
||||||
|
{
|
||||||
|
"description": "create table \"UserLoginMethod\"",
|
||||||
|
"sql": "CREATE TABLE \"public\".\"UserLoginMethod\" (\n \"createdAt\" timestamptz NOT NULL,\n \"displayName\" text,\n \"hashedPassword\" text,\n \"oauthId\" text,\n \"type\" text NOT NULL,\n \"updatedAt\" timestamptz NOT NULL,\n \"userId\" int8 NOT NULL,\n PRIMARY KEY (\"userId\", \"type\"),\n CONSTRAINT \"UserLoginMethod_type_check_6814314f\" CHECK (\"type\" IN ('password', 'google', 'github'))\n)",
|
||||||
|
"params": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"postcheck": [
|
||||||
|
{
|
||||||
|
"description": "verify table \"UserLoginMethod\" exists",
|
||||||
|
"sql": "SELECT (to_regclass($1)) IS NOT NULL AS \"result\"",
|
||||||
|
"params": [
|
||||||
|
"\"public\".\"UserLoginMethod\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "table.UserProgress",
|
||||||
|
"label": "Create table \"UserProgress\"",
|
||||||
|
"summary": "Creates table \"UserProgress\"",
|
||||||
|
"operationClass": "additive",
|
||||||
|
"target": {
|
||||||
|
"id": "postgres",
|
||||||
|
"details": {
|
||||||
|
"schema": "public",
|
||||||
|
"objectType": "table",
|
||||||
|
"name": "UserProgress"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"precheck": [
|
||||||
|
{
|
||||||
|
"description": "ensure table \"UserProgress\" does not exist",
|
||||||
|
"sql": "SELECT (to_regclass($1)) IS NULL AS \"result\"",
|
||||||
|
"params": [
|
||||||
|
"\"public\".\"UserProgress\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"execute": [
|
||||||
|
{
|
||||||
|
"description": "create table \"UserProgress\"",
|
||||||
|
"sql": "CREATE TABLE \"public\".\"UserProgress\" (\n \"finishedAt\" timestamptz,\n \"quest\" text NOT NULL,\n \"startedAt\" timestamptz NOT NULL,\n \"userId\" int8 NOT NULL,\n PRIMARY KEY (\"userId\", \"quest\")\n)",
|
||||||
|
"params": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"postcheck": [
|
||||||
|
{
|
||||||
|
"description": "verify table \"UserProgress\" exists",
|
||||||
|
"sql": "SELECT (to_regclass($1)) IS NOT NULL AS \"result\"",
|
||||||
|
"params": [
|
||||||
|
"\"public\".\"UserProgress\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "unique.User.User_email_key",
|
||||||
|
"label": "Add unique constraint on \"User\" (email)",
|
||||||
|
"operationClass": "additive",
|
||||||
|
"target": {
|
||||||
|
"id": "postgres",
|
||||||
|
"details": {
|
||||||
|
"schema": "public",
|
||||||
|
"objectType": "unique",
|
||||||
|
"name": "User_email_key",
|
||||||
|
"table": "User"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"precheck": [
|
||||||
|
{
|
||||||
|
"description": "ensure constraint \"User_email_key\" does not exist",
|
||||||
|
"sql": "SELECT NOT EXISTS (SELECT 1 AS \"one\" FROM \"pg_constraint\" AS \"c\" INNER JOIN \"pg_namespace\" AS \"n\" ON \"n\".\"oid\" = \"c\".\"connamespace\" WHERE (\"c\".\"conname\" = $1 AND \"n\".\"nspname\" = $2 AND \"c\".\"conrelid\" = to_regclass($3))) AS \"result\"",
|
||||||
|
"params": [
|
||||||
|
"User_email_key",
|
||||||
|
"public",
|
||||||
|
"\"public\".\"User\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"execute": [
|
||||||
|
{
|
||||||
|
"description": "add unique constraint \"User_email_key\"",
|
||||||
|
"sql": "ALTER TABLE \"public\".\"User\" ADD CONSTRAINT \"User_email_key\" UNIQUE (\"email\")"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"postcheck": [
|
||||||
|
{
|
||||||
|
"description": "verify constraint \"User_email_key\" exists",
|
||||||
|
"sql": "SELECT EXISTS (SELECT 1 AS \"one\" FROM \"pg_constraint\" AS \"c\" INNER JOIN \"pg_namespace\" AS \"n\" ON \"n\".\"oid\" = \"c\".\"connamespace\" WHERE (\"c\".\"conname\" = $1 AND \"n\".\"nspname\" = $2 AND \"c\".\"conrelid\" = to_regclass($3))) AS \"result\"",
|
||||||
|
"params": [
|
||||||
|
"User_email_key",
|
||||||
|
"public",
|
||||||
|
"\"public\".\"User\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "unique.User.User_username_key",
|
||||||
|
"label": "Add unique constraint on \"User\" (username)",
|
||||||
|
"operationClass": "additive",
|
||||||
|
"target": {
|
||||||
|
"id": "postgres",
|
||||||
|
"details": {
|
||||||
|
"schema": "public",
|
||||||
|
"objectType": "unique",
|
||||||
|
"name": "User_username_key",
|
||||||
|
"table": "User"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"precheck": [
|
||||||
|
{
|
||||||
|
"description": "ensure constraint \"User_username_key\" does not exist",
|
||||||
|
"sql": "SELECT NOT EXISTS (SELECT 1 AS \"one\" FROM \"pg_constraint\" AS \"c\" INNER JOIN \"pg_namespace\" AS \"n\" ON \"n\".\"oid\" = \"c\".\"connamespace\" WHERE (\"c\".\"conname\" = $1 AND \"n\".\"nspname\" = $2 AND \"c\".\"conrelid\" = to_regclass($3))) AS \"result\"",
|
||||||
|
"params": [
|
||||||
|
"User_username_key",
|
||||||
|
"public",
|
||||||
|
"\"public\".\"User\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"execute": [
|
||||||
|
{
|
||||||
|
"description": "add unique constraint \"User_username_key\"",
|
||||||
|
"sql": "ALTER TABLE \"public\".\"User\" ADD CONSTRAINT \"User_username_key\" UNIQUE (\"username\")"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"postcheck": [
|
||||||
|
{
|
||||||
|
"description": "verify constraint \"User_username_key\" exists",
|
||||||
|
"sql": "SELECT EXISTS (SELECT 1 AS \"one\" FROM \"pg_constraint\" AS \"c\" INNER JOIN \"pg_namespace\" AS \"n\" ON \"n\".\"oid\" = \"c\".\"connamespace\" WHERE (\"c\".\"conname\" = $1 AND \"n\".\"nspname\" = $2 AND \"c\".\"conrelid\" = to_regclass($3))) AS \"result\"",
|
||||||
|
"params": [
|
||||||
|
"User_username_key",
|
||||||
|
"public",
|
||||||
|
"\"public\".\"User\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "unique.UserLoginMethod.UserLoginMethod_type_oauthId_key",
|
||||||
|
"label": "Add unique constraint on \"UserLoginMethod\" (type, oauthId)",
|
||||||
|
"operationClass": "additive",
|
||||||
|
"target": {
|
||||||
|
"id": "postgres",
|
||||||
|
"details": {
|
||||||
|
"schema": "public",
|
||||||
|
"objectType": "unique",
|
||||||
|
"name": "UserLoginMethod_type_oauthId_key",
|
||||||
|
"table": "UserLoginMethod"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"precheck": [
|
||||||
|
{
|
||||||
|
"description": "ensure constraint \"UserLoginMethod_type_oauthId_key\" does not exist",
|
||||||
|
"sql": "SELECT NOT EXISTS (SELECT 1 AS \"one\" FROM \"pg_constraint\" AS \"c\" INNER JOIN \"pg_namespace\" AS \"n\" ON \"n\".\"oid\" = \"c\".\"connamespace\" WHERE (\"c\".\"conname\" = $1 AND \"n\".\"nspname\" = $2 AND \"c\".\"conrelid\" = to_regclass($3))) AS \"result\"",
|
||||||
|
"params": [
|
||||||
|
"UserLoginMethod_type_oauthId_key",
|
||||||
|
"public",
|
||||||
|
"\"public\".\"UserLoginMethod\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"execute": [
|
||||||
|
{
|
||||||
|
"description": "add unique constraint \"UserLoginMethod_type_oauthId_key\"",
|
||||||
|
"sql": "ALTER TABLE \"public\".\"UserLoginMethod\" ADD CONSTRAINT \"UserLoginMethod_type_oauthId_key\" UNIQUE (\"type\", \"oauthId\")"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"postcheck": [
|
||||||
|
{
|
||||||
|
"description": "verify constraint \"UserLoginMethod_type_oauthId_key\" exists",
|
||||||
|
"sql": "SELECT EXISTS (SELECT 1 AS \"one\" FROM \"pg_constraint\" AS \"c\" INNER JOIN \"pg_namespace\" AS \"n\" ON \"n\".\"oid\" = \"c\".\"connamespace\" WHERE (\"c\".\"conname\" = $1 AND \"n\".\"nspname\" = $2 AND \"c\".\"conrelid\" = to_regclass($3))) AS \"result\"",
|
||||||
|
"params": [
|
||||||
|
"UserLoginMethod_type_oauthId_key",
|
||||||
|
"public",
|
||||||
|
"\"public\".\"UserLoginMethod\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "index.Command.Command_sessionId_idx_29f415d4",
|
||||||
|
"label": "Create index \"Command_sessionId_idx_29f415d4\" on \"Command\"",
|
||||||
|
"operationClass": "additive",
|
||||||
|
"target": {
|
||||||
|
"id": "postgres",
|
||||||
|
"details": {
|
||||||
|
"schema": "public",
|
||||||
|
"objectType": "index",
|
||||||
|
"name": "Command_sessionId_idx_29f415d4",
|
||||||
|
"table": "Command"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"precheck": [
|
||||||
|
{
|
||||||
|
"description": "ensure index \"Command_sessionId_idx_29f415d4\" does not exist",
|
||||||
|
"sql": "SELECT (to_regclass($1)) IS NULL AS \"result\"",
|
||||||
|
"params": [
|
||||||
|
"\"public\".\"Command_sessionId_idx_29f415d4\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"execute": [
|
||||||
|
{
|
||||||
|
"description": "create index \"Command_sessionId_idx_29f415d4\"",
|
||||||
|
"sql": "CREATE INDEX \"Command_sessionId_idx_29f415d4\" ON \"public\".\"Command\" (\"sessionId\")",
|
||||||
|
"params": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"postcheck": [
|
||||||
|
{
|
||||||
|
"description": "verify index \"Command_sessionId_idx_29f415d4\" exists",
|
||||||
|
"sql": "SELECT (to_regclass($1)) IS NOT NULL AS \"result\"",
|
||||||
|
"params": [
|
||||||
|
"\"public\".\"Command_sessionId_idx_29f415d4\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "index.LoginAttempt.LoginAttempt_userId_idx_a489d58a",
|
||||||
|
"label": "Create index \"LoginAttempt_userId_idx_a489d58a\" on \"LoginAttempt\"",
|
||||||
|
"operationClass": "additive",
|
||||||
|
"target": {
|
||||||
|
"id": "postgres",
|
||||||
|
"details": {
|
||||||
|
"schema": "public",
|
||||||
|
"objectType": "index",
|
||||||
|
"name": "LoginAttempt_userId_idx_a489d58a",
|
||||||
|
"table": "LoginAttempt"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"precheck": [
|
||||||
|
{
|
||||||
|
"description": "ensure index \"LoginAttempt_userId_idx_a489d58a\" does not exist",
|
||||||
|
"sql": "SELECT (to_regclass($1)) IS NULL AS \"result\"",
|
||||||
|
"params": [
|
||||||
|
"\"public\".\"LoginAttempt_userId_idx_a489d58a\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"execute": [
|
||||||
|
{
|
||||||
|
"description": "create index \"LoginAttempt_userId_idx_a489d58a\"",
|
||||||
|
"sql": "CREATE INDEX \"LoginAttempt_userId_idx_a489d58a\" ON \"public\".\"LoginAttempt\" (\"userId\")",
|
||||||
|
"params": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"postcheck": [
|
||||||
|
{
|
||||||
|
"description": "verify index \"LoginAttempt_userId_idx_a489d58a\" exists",
|
||||||
|
"sql": "SELECT (to_regclass($1)) IS NOT NULL AS \"result\"",
|
||||||
|
"params": [
|
||||||
|
"\"public\".\"LoginAttempt_userId_idx_a489d58a\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "index.Session.Session_userId_idx_a489d58a",
|
||||||
|
"label": "Create index \"Session_userId_idx_a489d58a\" on \"Session\"",
|
||||||
|
"operationClass": "additive",
|
||||||
|
"target": {
|
||||||
|
"id": "postgres",
|
||||||
|
"details": {
|
||||||
|
"schema": "public",
|
||||||
|
"objectType": "index",
|
||||||
|
"name": "Session_userId_idx_a489d58a",
|
||||||
|
"table": "Session"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"precheck": [
|
||||||
|
{
|
||||||
|
"description": "ensure index \"Session_userId_idx_a489d58a\" does not exist",
|
||||||
|
"sql": "SELECT (to_regclass($1)) IS NULL AS \"result\"",
|
||||||
|
"params": [
|
||||||
|
"\"public\".\"Session_userId_idx_a489d58a\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"execute": [
|
||||||
|
{
|
||||||
|
"description": "create index \"Session_userId_idx_a489d58a\"",
|
||||||
|
"sql": "CREATE INDEX \"Session_userId_idx_a489d58a\" ON \"public\".\"Session\" (\"userId\")",
|
||||||
|
"params": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"postcheck": [
|
||||||
|
{
|
||||||
|
"description": "verify index \"Session_userId_idx_a489d58a\" exists",
|
||||||
|
"sql": "SELECT (to_regclass($1)) IS NOT NULL AS \"result\"",
|
||||||
|
"params": [
|
||||||
|
"\"public\".\"Session_userId_idx_a489d58a\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "index.Session.Session_userId_quest_idx_b13626bf",
|
||||||
|
"label": "Create index \"Session_userId_quest_idx_b13626bf\" on \"Session\"",
|
||||||
|
"operationClass": "additive",
|
||||||
|
"target": {
|
||||||
|
"id": "postgres",
|
||||||
|
"details": {
|
||||||
|
"schema": "public",
|
||||||
|
"objectType": "index",
|
||||||
|
"name": "Session_userId_quest_idx_b13626bf",
|
||||||
|
"table": "Session"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"precheck": [
|
||||||
|
{
|
||||||
|
"description": "ensure index \"Session_userId_quest_idx_b13626bf\" does not exist",
|
||||||
|
"sql": "SELECT (to_regclass($1)) IS NULL AS \"result\"",
|
||||||
|
"params": [
|
||||||
|
"\"public\".\"Session_userId_quest_idx_b13626bf\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"execute": [
|
||||||
|
{
|
||||||
|
"description": "create index \"Session_userId_quest_idx_b13626bf\"",
|
||||||
|
"sql": "CREATE INDEX \"Session_userId_quest_idx_b13626bf\" ON \"public\".\"Session\" (\"userId\", \"quest\")",
|
||||||
|
"params": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"postcheck": [
|
||||||
|
{
|
||||||
|
"description": "verify index \"Session_userId_quest_idx_b13626bf\" exists",
|
||||||
|
"sql": "SELECT (to_regclass($1)) IS NOT NULL AS \"result\"",
|
||||||
|
"params": [
|
||||||
|
"\"public\".\"Session_userId_quest_idx_b13626bf\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "index.Session.Session_userId_status_idx_e4a128ba",
|
||||||
|
"label": "Create index \"Session_userId_status_idx_e4a128ba\" on \"Session\"",
|
||||||
|
"operationClass": "additive",
|
||||||
|
"target": {
|
||||||
|
"id": "postgres",
|
||||||
|
"details": {
|
||||||
|
"schema": "public",
|
||||||
|
"objectType": "index",
|
||||||
|
"name": "Session_userId_status_idx_e4a128ba",
|
||||||
|
"table": "Session"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"precheck": [
|
||||||
|
{
|
||||||
|
"description": "ensure index \"Session_userId_status_idx_e4a128ba\" does not exist",
|
||||||
|
"sql": "SELECT (to_regclass($1)) IS NULL AS \"result\"",
|
||||||
|
"params": [
|
||||||
|
"\"public\".\"Session_userId_status_idx_e4a128ba\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"execute": [
|
||||||
|
{
|
||||||
|
"description": "create index \"Session_userId_status_idx_e4a128ba\"",
|
||||||
|
"sql": "CREATE INDEX \"Session_userId_status_idx_e4a128ba\" ON \"public\".\"Session\" (\"userId\", \"status\")",
|
||||||
|
"params": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"postcheck": [
|
||||||
|
{
|
||||||
|
"description": "verify index \"Session_userId_status_idx_e4a128ba\" exists",
|
||||||
|
"sql": "SELECT (to_regclass($1)) IS NOT NULL AS \"result\"",
|
||||||
|
"params": [
|
||||||
|
"\"public\".\"Session_userId_status_idx_e4a128ba\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "index.UserLoginMethod.UserLoginMethod_userId_idx_a489d58a",
|
||||||
|
"label": "Create index \"UserLoginMethod_userId_idx_a489d58a\" on \"UserLoginMethod\"",
|
||||||
|
"operationClass": "additive",
|
||||||
|
"target": {
|
||||||
|
"id": "postgres",
|
||||||
|
"details": {
|
||||||
|
"schema": "public",
|
||||||
|
"objectType": "index",
|
||||||
|
"name": "UserLoginMethod_userId_idx_a489d58a",
|
||||||
|
"table": "UserLoginMethod"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"precheck": [
|
||||||
|
{
|
||||||
|
"description": "ensure index \"UserLoginMethod_userId_idx_a489d58a\" does not exist",
|
||||||
|
"sql": "SELECT (to_regclass($1)) IS NULL AS \"result\"",
|
||||||
|
"params": [
|
||||||
|
"\"public\".\"UserLoginMethod_userId_idx_a489d58a\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"execute": [
|
||||||
|
{
|
||||||
|
"description": "create index \"UserLoginMethod_userId_idx_a489d58a\"",
|
||||||
|
"sql": "CREATE INDEX \"UserLoginMethod_userId_idx_a489d58a\" ON \"public\".\"UserLoginMethod\" (\"userId\")",
|
||||||
|
"params": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"postcheck": [
|
||||||
|
{
|
||||||
|
"description": "verify index \"UserLoginMethod_userId_idx_a489d58a\" exists",
|
||||||
|
"sql": "SELECT (to_regclass($1)) IS NOT NULL AS \"result\"",
|
||||||
|
"params": [
|
||||||
|
"\"public\".\"UserLoginMethod_userId_idx_a489d58a\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "index.UserProgress.UserProgress_userId_idx_a489d58a",
|
||||||
|
"label": "Create index \"UserProgress_userId_idx_a489d58a\" on \"UserProgress\"",
|
||||||
|
"operationClass": "additive",
|
||||||
|
"target": {
|
||||||
|
"id": "postgres",
|
||||||
|
"details": {
|
||||||
|
"schema": "public",
|
||||||
|
"objectType": "index",
|
||||||
|
"name": "UserProgress_userId_idx_a489d58a",
|
||||||
|
"table": "UserProgress"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"precheck": [
|
||||||
|
{
|
||||||
|
"description": "ensure index \"UserProgress_userId_idx_a489d58a\" does not exist",
|
||||||
|
"sql": "SELECT (to_regclass($1)) IS NULL AS \"result\"",
|
||||||
|
"params": [
|
||||||
|
"\"public\".\"UserProgress_userId_idx_a489d58a\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"execute": [
|
||||||
|
{
|
||||||
|
"description": "create index \"UserProgress_userId_idx_a489d58a\"",
|
||||||
|
"sql": "CREATE INDEX \"UserProgress_userId_idx_a489d58a\" ON \"public\".\"UserProgress\" (\"userId\")",
|
||||||
|
"params": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"postcheck": [
|
||||||
|
{
|
||||||
|
"description": "verify index \"UserProgress_userId_idx_a489d58a\" exists",
|
||||||
|
"sql": "SELECT (to_regclass($1)) IS NOT NULL AS \"result\"",
|
||||||
|
"params": [
|
||||||
|
"\"public\".\"UserProgress_userId_idx_a489d58a\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "foreignKey.Command.Command_sessionId_fkey",
|
||||||
|
"label": "Add foreign key \"Command_sessionId_fkey\" on \"Command\"",
|
||||||
|
"operationClass": "additive",
|
||||||
|
"target": {
|
||||||
|
"id": "postgres",
|
||||||
|
"details": {
|
||||||
|
"schema": "public",
|
||||||
|
"objectType": "foreignKey",
|
||||||
|
"name": "Command_sessionId_fkey",
|
||||||
|
"table": "Command"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"precheck": [
|
||||||
|
{
|
||||||
|
"description": "ensure FK \"Command_sessionId_fkey\" does not exist",
|
||||||
|
"sql": "SELECT NOT EXISTS (SELECT 1 AS \"one\" FROM \"pg_constraint\" AS \"c\" INNER JOIN \"pg_namespace\" AS \"n\" ON \"n\".\"oid\" = \"c\".\"connamespace\" WHERE (\"c\".\"conname\" = $1 AND \"n\".\"nspname\" = $2 AND \"c\".\"conrelid\" = to_regclass($3))) AS \"result\"",
|
||||||
|
"params": [
|
||||||
|
"Command_sessionId_fkey",
|
||||||
|
"public",
|
||||||
|
"\"public\".\"Command\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"execute": [
|
||||||
|
{
|
||||||
|
"description": "add FK \"Command_sessionId_fkey\"",
|
||||||
|
"sql": "ALTER TABLE \"public\".\"Command\"\nADD CONSTRAINT \"Command_sessionId_fkey\"\nFOREIGN KEY (\"sessionId\")\nREFERENCES \"public\".\"Session\" (\"id\")"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"postcheck": [
|
||||||
|
{
|
||||||
|
"description": "verify FK \"Command_sessionId_fkey\" exists",
|
||||||
|
"sql": "SELECT EXISTS (SELECT 1 AS \"one\" FROM \"pg_constraint\" AS \"c\" INNER JOIN \"pg_namespace\" AS \"n\" ON \"n\".\"oid\" = \"c\".\"connamespace\" WHERE (\"c\".\"conname\" = $1 AND \"n\".\"nspname\" = $2 AND \"c\".\"conrelid\" = to_regclass($3))) AS \"result\"",
|
||||||
|
"params": [
|
||||||
|
"Command_sessionId_fkey",
|
||||||
|
"public",
|
||||||
|
"\"public\".\"Command\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "foreignKey.LoginAttempt.LoginAttempt_userId_fkey",
|
||||||
|
"label": "Add foreign key \"LoginAttempt_userId_fkey\" on \"LoginAttempt\"",
|
||||||
|
"operationClass": "additive",
|
||||||
|
"target": {
|
||||||
|
"id": "postgres",
|
||||||
|
"details": {
|
||||||
|
"schema": "public",
|
||||||
|
"objectType": "foreignKey",
|
||||||
|
"name": "LoginAttempt_userId_fkey",
|
||||||
|
"table": "LoginAttempt"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"precheck": [
|
||||||
|
{
|
||||||
|
"description": "ensure FK \"LoginAttempt_userId_fkey\" does not exist",
|
||||||
|
"sql": "SELECT NOT EXISTS (SELECT 1 AS \"one\" FROM \"pg_constraint\" AS \"c\" INNER JOIN \"pg_namespace\" AS \"n\" ON \"n\".\"oid\" = \"c\".\"connamespace\" WHERE (\"c\".\"conname\" = $1 AND \"n\".\"nspname\" = $2 AND \"c\".\"conrelid\" = to_regclass($3))) AS \"result\"",
|
||||||
|
"params": [
|
||||||
|
"LoginAttempt_userId_fkey",
|
||||||
|
"public",
|
||||||
|
"\"public\".\"LoginAttempt\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"execute": [
|
||||||
|
{
|
||||||
|
"description": "add FK \"LoginAttempt_userId_fkey\"",
|
||||||
|
"sql": "ALTER TABLE \"public\".\"LoginAttempt\"\nADD CONSTRAINT \"LoginAttempt_userId_fkey\"\nFOREIGN KEY (\"userId\")\nREFERENCES \"public\".\"User\" (\"id\")"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"postcheck": [
|
||||||
|
{
|
||||||
|
"description": "verify FK \"LoginAttempt_userId_fkey\" exists",
|
||||||
|
"sql": "SELECT EXISTS (SELECT 1 AS \"one\" FROM \"pg_constraint\" AS \"c\" INNER JOIN \"pg_namespace\" AS \"n\" ON \"n\".\"oid\" = \"c\".\"connamespace\" WHERE (\"c\".\"conname\" = $1 AND \"n\".\"nspname\" = $2 AND \"c\".\"conrelid\" = to_regclass($3))) AS \"result\"",
|
||||||
|
"params": [
|
||||||
|
"LoginAttempt_userId_fkey",
|
||||||
|
"public",
|
||||||
|
"\"public\".\"LoginAttempt\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "foreignKey.Session.Session_userId_fkey",
|
||||||
|
"label": "Add foreign key \"Session_userId_fkey\" on \"Session\"",
|
||||||
|
"operationClass": "additive",
|
||||||
|
"target": {
|
||||||
|
"id": "postgres",
|
||||||
|
"details": {
|
||||||
|
"schema": "public",
|
||||||
|
"objectType": "foreignKey",
|
||||||
|
"name": "Session_userId_fkey",
|
||||||
|
"table": "Session"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"precheck": [
|
||||||
|
{
|
||||||
|
"description": "ensure FK \"Session_userId_fkey\" does not exist",
|
||||||
|
"sql": "SELECT NOT EXISTS (SELECT 1 AS \"one\" FROM \"pg_constraint\" AS \"c\" INNER JOIN \"pg_namespace\" AS \"n\" ON \"n\".\"oid\" = \"c\".\"connamespace\" WHERE (\"c\".\"conname\" = $1 AND \"n\".\"nspname\" = $2 AND \"c\".\"conrelid\" = to_regclass($3))) AS \"result\"",
|
||||||
|
"params": [
|
||||||
|
"Session_userId_fkey",
|
||||||
|
"public",
|
||||||
|
"\"public\".\"Session\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"execute": [
|
||||||
|
{
|
||||||
|
"description": "add FK \"Session_userId_fkey\"",
|
||||||
|
"sql": "ALTER TABLE \"public\".\"Session\"\nADD CONSTRAINT \"Session_userId_fkey\"\nFOREIGN KEY (\"userId\")\nREFERENCES \"public\".\"User\" (\"id\")"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"postcheck": [
|
||||||
|
{
|
||||||
|
"description": "verify FK \"Session_userId_fkey\" exists",
|
||||||
|
"sql": "SELECT EXISTS (SELECT 1 AS \"one\" FROM \"pg_constraint\" AS \"c\" INNER JOIN \"pg_namespace\" AS \"n\" ON \"n\".\"oid\" = \"c\".\"connamespace\" WHERE (\"c\".\"conname\" = $1 AND \"n\".\"nspname\" = $2 AND \"c\".\"conrelid\" = to_regclass($3))) AS \"result\"",
|
||||||
|
"params": [
|
||||||
|
"Session_userId_fkey",
|
||||||
|
"public",
|
||||||
|
"\"public\".\"Session\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "foreignKey.UserLoginMethod.UserLoginMethod_userId_fkey",
|
||||||
|
"label": "Add foreign key \"UserLoginMethod_userId_fkey\" on \"UserLoginMethod\"",
|
||||||
|
"operationClass": "additive",
|
||||||
|
"target": {
|
||||||
|
"id": "postgres",
|
||||||
|
"details": {
|
||||||
|
"schema": "public",
|
||||||
|
"objectType": "foreignKey",
|
||||||
|
"name": "UserLoginMethod_userId_fkey",
|
||||||
|
"table": "UserLoginMethod"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"precheck": [
|
||||||
|
{
|
||||||
|
"description": "ensure FK \"UserLoginMethod_userId_fkey\" does not exist",
|
||||||
|
"sql": "SELECT NOT EXISTS (SELECT 1 AS \"one\" FROM \"pg_constraint\" AS \"c\" INNER JOIN \"pg_namespace\" AS \"n\" ON \"n\".\"oid\" = \"c\".\"connamespace\" WHERE (\"c\".\"conname\" = $1 AND \"n\".\"nspname\" = $2 AND \"c\".\"conrelid\" = to_regclass($3))) AS \"result\"",
|
||||||
|
"params": [
|
||||||
|
"UserLoginMethod_userId_fkey",
|
||||||
|
"public",
|
||||||
|
"\"public\".\"UserLoginMethod\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"execute": [
|
||||||
|
{
|
||||||
|
"description": "add FK \"UserLoginMethod_userId_fkey\"",
|
||||||
|
"sql": "ALTER TABLE \"public\".\"UserLoginMethod\"\nADD CONSTRAINT \"UserLoginMethod_userId_fkey\"\nFOREIGN KEY (\"userId\")\nREFERENCES \"public\".\"User\" (\"id\")"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"postcheck": [
|
||||||
|
{
|
||||||
|
"description": "verify FK \"UserLoginMethod_userId_fkey\" exists",
|
||||||
|
"sql": "SELECT EXISTS (SELECT 1 AS \"one\" FROM \"pg_constraint\" AS \"c\" INNER JOIN \"pg_namespace\" AS \"n\" ON \"n\".\"oid\" = \"c\".\"connamespace\" WHERE (\"c\".\"conname\" = $1 AND \"n\".\"nspname\" = $2 AND \"c\".\"conrelid\" = to_regclass($3))) AS \"result\"",
|
||||||
|
"params": [
|
||||||
|
"UserLoginMethod_userId_fkey",
|
||||||
|
"public",
|
||||||
|
"\"public\".\"UserLoginMethod\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "foreignKey.UserProgress.UserProgress_userId_fkey",
|
||||||
|
"label": "Add foreign key \"UserProgress_userId_fkey\" on \"UserProgress\"",
|
||||||
|
"operationClass": "additive",
|
||||||
|
"target": {
|
||||||
|
"id": "postgres",
|
||||||
|
"details": {
|
||||||
|
"schema": "public",
|
||||||
|
"objectType": "foreignKey",
|
||||||
|
"name": "UserProgress_userId_fkey",
|
||||||
|
"table": "UserProgress"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"precheck": [
|
||||||
|
{
|
||||||
|
"description": "ensure FK \"UserProgress_userId_fkey\" does not exist",
|
||||||
|
"sql": "SELECT NOT EXISTS (SELECT 1 AS \"one\" FROM \"pg_constraint\" AS \"c\" INNER JOIN \"pg_namespace\" AS \"n\" ON \"n\".\"oid\" = \"c\".\"connamespace\" WHERE (\"c\".\"conname\" = $1 AND \"n\".\"nspname\" = $2 AND \"c\".\"conrelid\" = to_regclass($3))) AS \"result\"",
|
||||||
|
"params": [
|
||||||
|
"UserProgress_userId_fkey",
|
||||||
|
"public",
|
||||||
|
"\"public\".\"UserProgress\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"execute": [
|
||||||
|
{
|
||||||
|
"description": "add FK \"UserProgress_userId_fkey\"",
|
||||||
|
"sql": "ALTER TABLE \"public\".\"UserProgress\"\nADD CONSTRAINT \"UserProgress_userId_fkey\"\nFOREIGN KEY (\"userId\")\nREFERENCES \"public\".\"User\" (\"id\")"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"postcheck": [
|
||||||
|
{
|
||||||
|
"description": "verify FK \"UserProgress_userId_fkey\" exists",
|
||||||
|
"sql": "SELECT EXISTS (SELECT 1 AS \"one\" FROM \"pg_constraint\" AS \"c\" INNER JOIN \"pg_namespace\" AS \"n\" ON \"n\".\"oid\" = \"c\".\"connamespace\" WHERE (\"c\".\"conname\" = $1 AND \"n\".\"nspname\" = $2 AND \"c\".\"conrelid\" = to_regclass($3))) AS \"result\"",
|
||||||
|
"params": [
|
||||||
|
"UserProgress_userId_fkey",
|
||||||
|
"public",
|
||||||
|
"\"public\".\"UserProgress\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
Generated
Vendored
+1609
File diff suppressed because it is too large.
Load diff
+1
@@ -0,0 +1 @@
|
|||||||
|
{"_generated":{"message":"This file is automatically generated by \"prisma contract emit\".","regenerate":"To regenerate, run: prisma contract emit","warning":"⚠️ GENERATED FILE - DO NOT EDIT"},"capabilities":{"postgres":{"distinctOn":true,"forKeyShare":true,"forNoKeyUpdate":true,"jsonAgg":true,"lateral":true,"limit":true,"orderBy":true,"returning":true},"sql":{"checkConstraint":true,"defaultInInsert":true,"enums":true,"forShare":true,"forUpdate":true,"insertOnConflictSkip":true,"insertOnConflictWithoutTarget":true,"lateral":true,"lockNowait":true,"lockOf":true,"lockSkipLocked":true,"returning":true,"scalarList":true}},"domain":{"namespaces":{"public":{"enum":{"LoginMethod":{"codecId":"pg/text@1","members":[{"name":"Password","value":"password"},{"name":"Google","value":"google"},{"name":"GitHub","value":"github"}]},"SessionStatus":{"codecId":"pg/text@1","members":[{"name":"Active","value":"active"},{"name":"Finished","value":"finished"},{"name":"Inactive","value":"inactive"}]}},"models":{"Command":{"fields":{"command":{"nullable":false,"type":{"codecId":"pg/text@1","kind":"scalar"}},"error":{"nullable":true,"type":{"codecId":"pg/text@1","kind":"scalar"}},"id":{"nullable":false,"type":{"codecId":"pg/int8@1","kind":"scalar"}},"output":{"nullable":true,"type":{"codecId":"pg/text@1","kind":"scalar"}},"pwd":{"nullable":false,"type":{"codecId":"pg/text@1","kind":"scalar"}},"sessionId":{"nullable":false,"type":{"codecId":"pg/int8@1","kind":"scalar"}},"stage":{"nullable":true,"type":{"codecId":"pg/text@1","kind":"scalar"}},"timestamp":{"nullable":false,"type":{"codecId":"pg/timestamptz-temporal@1","kind":"scalar"}}},"relations":{"session":{"cardinality":"N:1","nullable":false,"on":{"localFields":["sessionId"],"targetFields":["id"]},"to":{"model":"Session","namespace":"public"}}},"storage":{"fields":{"command":{"column":"command"},"error":{"column":"error"},"id":{"column":"id"},"output":{"column":"output"},"pwd":{"column":"pwd"},"sessionId":{"column":"sessionId"},"stage":{"column":"stage"},"timestamp":{"column":"timestamp"}},"namespaceId":"public","table":"Command"}},"LoginAttempt":{"fields":{"id":{"nullable":false,"type":{"codecId":"pg/int8@1","kind":"scalar"}},"ip":{"nullable":false,"type":{"codecId":"pg/text@1","kind":"scalar"}},"loginMethod":{"nullable":false,"type":{"codecId":"pg/text@1","kind":"scalar"},"valueSet":{"entityKind":"enum","entityName":"LoginMethod","namespaceId":"public","plane":"domain"}},"message":{"nullable":true,"type":{"codecId":"pg/text@1","kind":"scalar"}},"success":{"nullable":false,"type":{"codecId":"pg/bool@1","kind":"scalar"}},"time":{"nullable":false,"type":{"codecId":"pg/timestamptz-temporal@1","kind":"scalar"}},"userAgent":{"nullable":false,"type":{"codecId":"pg/text@1","kind":"scalar"}},"userId":{"nullable":true,"type":{"codecId":"pg/int8@1","kind":"scalar"}},"username":{"nullable":false,"type":{"codecId":"pg/text@1","kind":"scalar"}}},"relations":{"user":{"cardinality":"N:1","nullable":true,"on":{"localFields":["userId"],"targetFields":["id"]},"to":{"model":"User","namespace":"public"}}},"storage":{"fields":{"id":{"column":"id"},"ip":{"column":"ip"},"loginMethod":{"column":"loginMethod"},"message":{"column":"message"},"success":{"column":"success"},"time":{"column":"time"},"userAgent":{"column":"userAgent"},"userId":{"column":"userId"},"username":{"column":"username"}},"namespaceId":"public","table":"LoginAttempt"}},"Session":{"fields":{"completedEvents":{"many":{"elementNullable":false},"nullable":false,"type":{"codecId":"pg/text@1","kind":"scalar"}},"containerId":{"nullable":true,"type":{"codecId":"pg/text@1","kind":"scalar"}},"containerName":{"nullable":true,"type":{"codecId":"pg/text@1","kind":"scalar"}},"createdAt":{"nullable":false,"type":{"codecId":"pg/timestamptz-temporal@1","kind":"scalar"}},"finishedAt":{"nullable":true,"type":{"codecId":"pg/timestamptz-temporal@1","kind":"scalar"}},"graph":{"nullable":false,"type":{"codecId":"pg/jsonb@1","kind":"scalar"}},"id":{"nullable":false,"type":{"codecId":"pg/int8@1","kind":"scalar"}},"lastActivityAt":{"nullable":false,"type":{"codecId":"pg/timestamptz-temporal@1","kind":"scalar"}},"locale":{"nullable":false,"type":{"codecId":"pg/text@1","kind":"scalar"}},"quest":{"nullable":false,"type":{"codecId":"pg/text@1","kind":"scalar"}},"status":{"nullable":false,"type":{"codecId":"pg/text@1","kind":"scalar"},"valueSet":{"entityKind":"enum","entityName":"SessionStatus","namespaceId":"public","plane":"domain"}},"userId":{"nullable":false,"type":{"codecId":"pg/int8@1","kind":"scalar"}}},"relations":{"commands":{"cardinality":"1:N","on":{"localFields":["id"],"targetFields":["sessionId"]},"to":{"model":"Command","namespace":"public"}},"user":{"cardinality":"N:1","nullable":false,"on":{"localFields":["userId"],"targetFields":["id"]},"to":{"model":"User","namespace":"public"}}},"storage":{"fields":{"completedEvents":{"column":"completedEvents"},"containerId":{"column":"containerId"},"containerName":{"column":"containerName"},"createdAt":{Line truncated
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "@odyssey/database",
|
||||||
|
"module": "src/index.ts",
|
||||||
|
"type": "module",
|
||||||
|
"devDependencies": {
|
||||||
|
"@odyssey/models": "workspace:*",
|
||||||
|
"@prisma/cli-engine": "0.7.0",
|
||||||
|
"@types/bun": "latest",
|
||||||
|
"@types/node": "^26.6.5",
|
||||||
|
"prisma": "^8.0.0-rc.22"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"contract:emit": "prisma contract emit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@prisma/orm-postgres": "^8.0.0-rc.17",
|
||||||
|
"dotenv": "^18.0.7",
|
||||||
|
"temporal-polyfill": "^1.0.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# Welcome to Prisma ORM!
|
||||||
|
|
||||||
|
Prisma ORM lets you query your database in simple, easy-to-read TypeScript. Define what your data looks like, and Prisma ORM gives you a fully typed client — with autocomplete for every table, column, and relation.
|
||||||
|
|
||||||
|
This project is set up for PostgreSQL. Prisma ORM also supports other databases.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- **PostgreSQL 15 or newer.** Older servers are not supported. Run `SELECT version()` against your server to verify.
|
||||||
|
- The CLI never connects to your database without explicit consent. Pass `--probe-db` to `bun prisma orm init` if you want `init` to verify the server version itself.
|
||||||
|
|
||||||
|
## Your data contract
|
||||||
|
|
||||||
|
Your data contract is the heart of your application. It lives at [`src/prisma/contract.prisma`](src/prisma/contract.prisma) and describes your models:
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
model User {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
email String @unique
|
||||||
|
username String?
|
||||||
|
name String?
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Every model you define in your contract can be queried from your app. Your editor will autocomplete the query methods and show you what type each model field is:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { db } from './src/prisma/db';
|
||||||
|
|
||||||
|
const user = await db.orm.public.User
|
||||||
|
.where({ email: 'alice@example.com' })
|
||||||
|
.first();
|
||||||
|
|
||||||
|
// Your editor will show the type of user as
|
||||||
|
// { id: number; email: string; username: string | null; name: string | null; createdAt: Date; posts: Post[] } | null
|
||||||
|
```
|
||||||
|
|
||||||
|
Your contract has two companion files in the same directory:
|
||||||
|
|
||||||
|
- **`contract.json`** — this tells your application what models exist, just like `package-lock.json` tells your package manager what dependencies your project has
|
||||||
|
- **`contract.d.ts`** — this powers autocomplete and type checking in your editor
|
||||||
|
|
||||||
|
Commit both files to git. When you change your contract, run `bun prisma contract emit` to update them.
|
||||||
|
|
||||||
|
If you use a framework like Next.js or Vite, the Prisma ORM plugin will do this for you automatically.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
[`prisma.config.ts`](prisma.config.ts) tells the CLI where your contract lives and how to connect to your database. It loads environment variables from `.env` automatically:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import 'dotenv/config';
|
||||||
|
import { definePrismaConfig } from '@prisma/cli-engine';
|
||||||
|
import { defineConfig as ormConfig } from '@prisma/orm-postgres/config';
|
||||||
|
|
||||||
|
export default definePrismaConfig({
|
||||||
|
orm: ormConfig({
|
||||||
|
contract: './src/prisma/contract.prisma',
|
||||||
|
db: {
|
||||||
|
connection: process.env['DATABASE_URL']!,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Notice the `DATABASE_URL` above? It's defined in your [`.env`](./.env) file:
|
||||||
|
|
||||||
|
```env
|
||||||
|
DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
|
||||||
|
```
|
||||||
|
|
||||||
|
You can customize how your environment variables are loaded by changing or removing the `import 'dotenv/config'` line.
|
||||||
|
|
||||||
|
## Quick reference
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun prisma contract emit # Update contract.json and contract.d.ts
|
||||||
|
bun prisma db init # Create tables in the database
|
||||||
|
bun prisma migration status # Show migration status
|
||||||
|
```
|
||||||
|
|
||||||
|
### Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| [`src/prisma/contract.prisma`](src/prisma/contract.prisma) | Your data contract — define your models here |
|
||||||
|
| [`prisma.config.ts`](prisma.config.ts) | CLI configuration |
|
||||||
|
| [`src/prisma/db.ts`](src/prisma/db.ts) | Database client — `import { db } from './src/prisma/db'` |
|
||||||
|
| `src/prisma/contract.json` | Compiled contract (generated) |
|
||||||
|
| `src/prisma/contract.d.ts` | Contract types (generated) |
|
||||||
|
|
||||||
|
### Workflow
|
||||||
|
|
||||||
|
1. Edit [`src/prisma/contract.prisma`](src/prisma/contract.prisma) to add or change models.
|
||||||
|
2. Run `bun prisma contract emit` to regenerate the contract.
|
||||||
|
3. Query your models — your IDE will autocomplete everything.
|
||||||
|
|
||||||
|
## Monorepo notes (pnpm workspaces)
|
||||||
|
|
||||||
|
If this project lives inside a pnpm workspace, a few things are worth knowing:
|
||||||
|
|
||||||
|
- **Catalogs.** When the workspace's `pnpm-workspace.yaml` defines a `catalogs` entry for `prisma` or `@prisma/orm-postgres`, pnpm uses the catalog version everywhere — `init` does too. If you wanted the published `latest` instead, update or remove the catalog entry, then re-run `pnpm install`.
|
||||||
|
- **`pnpm dlx`.** `pnpm dlx prisma@latest orm init …` works in any directory. Inside a workspace, pnpm still resolves dependencies through the workspace's catalog/overrides rather than the registry; expect the installed Prisma ORM packages to reflect the workspace's catalog rather than `latest`.
|
||||||
|
- **`pnpm` → `npm` fallback.** If `pnpm` ever fails to install Prisma ORM with a `workspace:*` or `catalog:` resolution error (a leak in a published artefact), `init` falls back to `npm install` and surfaces a warning. Once the offending package republishes a clean version you can switch back with `pnpm install`.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import { definePrismaConfig } from '@prisma/cli-engine';
|
||||||
|
import { defineConfig as ormConfig } from '@prisma/orm-postgres/config';
|
||||||
|
|
||||||
|
export default definePrismaConfig({
|
||||||
|
orm: ormConfig({
|
||||||
|
contract: "./src/prisma/contract.prisma",
|
||||||
|
db: {
|
||||||
|
connection: process.env['DATABASE_URL']!,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
@@ -0,0 +1,403 @@
|
|||||||
|
/**
|
||||||
|
* One-shot migration of the legacy Mongo data into Postgres.
|
||||||
|
*
|
||||||
|
* MONGO_URL=... DATABASE_URL=... bun run scripts/migrate-from-mongo.ts [--dry-run] [--force]
|
||||||
|
*
|
||||||
|
* - Stop the server before running; this is a snapshot copy, not a sync.
|
||||||
|
* - Everything runs in ONE Postgres transaction, so a failure leaves Postgres untouched
|
||||||
|
* and the script can simply be re-run.
|
||||||
|
* - Refuses to run if any target table is non-empty, unless --force is given
|
||||||
|
* (--force does not delete anything; it only skips the check).
|
||||||
|
* - New ids are assigned here (sequential), so relations can be rewired from ObjectIds.
|
||||||
|
* The id sequences are advanced afterwards.
|
||||||
|
* - Rows that cannot be migrated (orphans, unknown providers) are written to
|
||||||
|
* migration-skipped.json and counted in the summary.
|
||||||
|
*/
|
||||||
|
import 'dotenv/config'
|
||||||
|
import { writeFileSync } from 'node:fs'
|
||||||
|
import { Temporal } from 'temporal-polyfill'
|
||||||
|
import {
|
||||||
|
connectDB,
|
||||||
|
User,
|
||||||
|
UserProfile,
|
||||||
|
Session,
|
||||||
|
Command,
|
||||||
|
LoginAttempt,
|
||||||
|
} from '@odyssey/models'
|
||||||
|
import { db } from '../src/prisma/db'
|
||||||
|
|
||||||
|
const DRY_RUN = process.argv.includes('--dry-run')
|
||||||
|
const FORCE = process.argv.includes('--force')
|
||||||
|
const BATCH_SIZE = 1000
|
||||||
|
|
||||||
|
const LOGIN_METHODS = ['password', 'google', 'github'] as const
|
||||||
|
type LoginMethod = (typeof LOGIN_METHODS)[number]
|
||||||
|
const isLoginMethod = (v: string): v is LoginMethod =>
|
||||||
|
(LOGIN_METHODS as readonly string[]).includes(v)
|
||||||
|
|
||||||
|
type Tx = Parameters<Parameters<typeof db.transaction>[0]>[0]
|
||||||
|
|
||||||
|
const skipped: {
|
||||||
|
table: string
|
||||||
|
reason: string
|
||||||
|
id?: string
|
||||||
|
detail?: unknown
|
||||||
|
}[] = []
|
||||||
|
const stats: Record<
|
||||||
|
string,
|
||||||
|
{ source: number; inserted: number; skipped: number }
|
||||||
|
> = {}
|
||||||
|
|
||||||
|
function track(table: string) {
|
||||||
|
return (stats[table] ??= { source: 0, inserted: 0, skipped: 0 })
|
||||||
|
}
|
||||||
|
|
||||||
|
function skip(table: string, reason: string, id?: unknown, detail?: unknown) {
|
||||||
|
track(table).skipped++
|
||||||
|
skipped.push({
|
||||||
|
table,
|
||||||
|
reason,
|
||||||
|
id: id === undefined ? undefined : String(id),
|
||||||
|
detail,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const instant = (d: Date) => Temporal.Instant.fromEpochMilliseconds(d.getTime())
|
||||||
|
const instantOrNull = (d?: Date | null) => (d ? instant(d) : null)
|
||||||
|
|
||||||
|
// Postgres text cannot hold NUL bytes, which some captured terminal output contains.
|
||||||
|
let nulStripped = 0
|
||||||
|
function text(v: string): string
|
||||||
|
function text(v: string | null | undefined): string | null
|
||||||
|
function text(v: string | null | undefined) {
|
||||||
|
if (v == null) return null
|
||||||
|
if (!v.includes('\0')) return v
|
||||||
|
nulStripped++
|
||||||
|
return v.replaceAll('\0', '')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mongoose subdocuments carry _id; the Postgres graph column stores the bare tree.
|
||||||
|
interface GraphNode {
|
||||||
|
path: string
|
||||||
|
type: string
|
||||||
|
discovered: boolean
|
||||||
|
children?: GraphNode[]
|
||||||
|
}
|
||||||
|
function cleanGraph(node: any): GraphNode {
|
||||||
|
const out: GraphNode = {
|
||||||
|
path: node?.path ?? '/',
|
||||||
|
// legacy sessions used 'folder' for directories
|
||||||
|
type: node?.type === 'file' ? 'file' : 'directory',
|
||||||
|
discovered: node?.discovered ?? false,
|
||||||
|
}
|
||||||
|
if (node?.children?.length) out.children = node.children.map(cleanGraph)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// A lean Mongoose Map comes back as a plain object, a hydrated one as a Map.
|
||||||
|
const entries = <T>(m: Map<string, T> | Record<string, T> | undefined) =>
|
||||||
|
m instanceof Map ? [...m.entries()] : Object.entries(m ?? {})
|
||||||
|
|
||||||
|
async function insert(tx: Tx, table: string, plan: unknown, count: number) {
|
||||||
|
if (DRY_RUN || count === 0) {
|
||||||
|
track(table).inserted += count
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const { affectedRows } = await tx.execute(plan as never)
|
||||||
|
track(table).inserted += Number(affectedRows)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function* batches<T>(cursor: AsyncIterable<T>, size: number) {
|
||||||
|
let buf: T[] = []
|
||||||
|
for await (const item of cursor) {
|
||||||
|
buf.push(item)
|
||||||
|
if (buf.length >= size) {
|
||||||
|
yield buf
|
||||||
|
buf = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (buf.length) yield buf
|
||||||
|
}
|
||||||
|
|
||||||
|
async function migrate(tx: Tx) {
|
||||||
|
const t = tx.sql.public
|
||||||
|
const userIds = new Map<string, bigint>()
|
||||||
|
const userIdsByUsername = new Map<string, bigint>()
|
||||||
|
const sessionIds = new Map<string, bigint>()
|
||||||
|
const seenEmails = new Set<string>()
|
||||||
|
const seenOAuth = new Set<string>()
|
||||||
|
let nextUserId = 1n
|
||||||
|
let nextSessionId = 1n
|
||||||
|
let nextCommandId = 1n
|
||||||
|
let nextAttemptId = 1n
|
||||||
|
|
||||||
|
// --- User + UserLoginMethod
|
||||||
|
for await (const users of batches(
|
||||||
|
User.find().sort({ _id: 1 }).lean().cursor(),
|
||||||
|
BATCH_SIZE
|
||||||
|
)) {
|
||||||
|
const userRows: any[] = []
|
||||||
|
const methodRows: any[] = []
|
||||||
|
for (const u of users) {
|
||||||
|
track('User').source++
|
||||||
|
const email = u.email.trim().toLowerCase()
|
||||||
|
if (seenEmails.has(email)) {
|
||||||
|
throw new Error(
|
||||||
|
`Duplicate email after lowercasing: ${email} (user ${u._id}). Resolve it in Mongo first.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
seenEmails.add(email)
|
||||||
|
|
||||||
|
const id = nextUserId++
|
||||||
|
userIds.set(String(u._id), id)
|
||||||
|
userIdsByUsername.set(u.username, id)
|
||||||
|
userRows.push({
|
||||||
|
id,
|
||||||
|
email,
|
||||||
|
username: u.username,
|
||||||
|
name: null,
|
||||||
|
createdAt: instant(u.createdAt),
|
||||||
|
updatedAt: instant(u.updatedAt),
|
||||||
|
isGuest: u.isGuest ?? false,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (u.hashedPassword) {
|
||||||
|
methodRows.push({
|
||||||
|
userId: id,
|
||||||
|
type: 'password',
|
||||||
|
hashedPassword: u.hashedPassword,
|
||||||
|
oauthId: null,
|
||||||
|
displayName: null,
|
||||||
|
createdAt: instant(u.createdAt),
|
||||||
|
updatedAt: instant(u.updatedAt),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for (const [key, login] of entries(u.socialLogins)) {
|
||||||
|
const provider = (login.provider ?? key).toLowerCase()
|
||||||
|
if (!isLoginMethod(provider) || provider === 'password') {
|
||||||
|
skip('UserLoginMethod', 'unknown provider', u._id, { provider })
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const dedupe = `${provider}:${login.id}`
|
||||||
|
if (seenOAuth.has(dedupe)) {
|
||||||
|
skip('UserLoginMethod', 'duplicate (type, oauthId)', u._id, {
|
||||||
|
provider,
|
||||||
|
oauthId: login.id,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seenOAuth.add(dedupe)
|
||||||
|
methodRows.push({
|
||||||
|
userId: id,
|
||||||
|
type: provider,
|
||||||
|
hashedPassword: null,
|
||||||
|
oauthId: login.id,
|
||||||
|
displayName: login.displayName ?? null,
|
||||||
|
createdAt: instant(u.createdAt),
|
||||||
|
updatedAt: instant(u.updatedAt),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await insert(tx, 'User', t.User.insert(userRows).build(), userRows.length)
|
||||||
|
track('UserLoginMethod').source += methodRows.length
|
||||||
|
await insert(
|
||||||
|
tx,
|
||||||
|
'UserLoginMethod',
|
||||||
|
t.UserLoginMethod.insert(methodRows).build(),
|
||||||
|
methodRows.length
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- UserProgress (UserProfile.progress map, flattened)
|
||||||
|
for await (const profiles of batches(
|
||||||
|
UserProfile.find().lean().cursor(),
|
||||||
|
BATCH_SIZE
|
||||||
|
)) {
|
||||||
|
const rows: any[] = []
|
||||||
|
for (const p of profiles) {
|
||||||
|
const userId = userIds.get(String(p.user))
|
||||||
|
const items = entries(p.progress)
|
||||||
|
track('UserProgress').source += items.length
|
||||||
|
if (userId === undefined) {
|
||||||
|
for (const [quest] of items)
|
||||||
|
skip('UserProgress', 'user not found', p.user, { quest })
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for (const [key, prog] of items) {
|
||||||
|
rows.push({
|
||||||
|
userId,
|
||||||
|
quest: prog.quest ?? key,
|
||||||
|
startedAt: instant(prog.startedAt),
|
||||||
|
// `completed` is no longer stored: finishedAt IS NOT NULL means completed.
|
||||||
|
finishedAt: prog.completed
|
||||||
|
? instantOrNull(prog.finishedAt ?? (p as any).updatedAt)
|
||||||
|
: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await insert(
|
||||||
|
tx,
|
||||||
|
'UserProgress',
|
||||||
|
t.UserProgress.insert(rows).build(),
|
||||||
|
rows.length
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Session
|
||||||
|
for await (const sessions of batches(
|
||||||
|
Session.find().sort({ _id: 1 }).lean().cursor(),
|
||||||
|
BATCH_SIZE
|
||||||
|
)) {
|
||||||
|
const rows: any[] = []
|
||||||
|
for (const s of sessions) {
|
||||||
|
track('Session').source++
|
||||||
|
const userId = userIds.get(String(s.user))
|
||||||
|
if (userId === undefined) {
|
||||||
|
skip('Session', 'user not found', s._id, { user: String(s.user) })
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const id = nextSessionId++
|
||||||
|
sessionIds.set(String(s._id), id)
|
||||||
|
rows.push({
|
||||||
|
id,
|
||||||
|
userId,
|
||||||
|
quest: s.quest,
|
||||||
|
// sessions created before localization have no locale; they were zh-TW
|
||||||
|
locale: s.locale ?? 'zh-TW',
|
||||||
|
containerId: s.containerId ?? null,
|
||||||
|
containerName: s.containerName ?? null,
|
||||||
|
status: s.status,
|
||||||
|
createdAt: instant(s.createdAt),
|
||||||
|
lastActivityAt: instant(s.lastActivityAt ?? s.updatedAt),
|
||||||
|
finishedAt: instantOrNull(s.finishedAt),
|
||||||
|
completedEvents: s.completedEvents ?? [],
|
||||||
|
graph: cleanGraph(s.graph),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
await insert(tx, 'Session', t.Session.insert(rows).build(), rows.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Command (largest collection)
|
||||||
|
for await (const commands of batches(
|
||||||
|
Command.find().sort({ _id: 1 }).lean().cursor(),
|
||||||
|
BATCH_SIZE
|
||||||
|
)) {
|
||||||
|
const rows: any[] = []
|
||||||
|
for (const c of commands) {
|
||||||
|
track('Command').source++
|
||||||
|
const sessionId = sessionIds.get(String(c.session))
|
||||||
|
if (sessionId === undefined) {
|
||||||
|
skip('Command', 'session not found', c._id, {
|
||||||
|
session: String(c.session),
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rows.push({
|
||||||
|
id: nextCommandId++,
|
||||||
|
sessionId,
|
||||||
|
timestamp: instant(c.createdAt),
|
||||||
|
pwd: text(c.pwd),
|
||||||
|
command: text(c.command),
|
||||||
|
output: text(c.output),
|
||||||
|
error: text(c.error),
|
||||||
|
stage: text(c.stage),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
await insert(tx, 'Command', t.Command.insert(rows).build(), rows.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- LoginAttempt (userId stays null when the username no longer exists)
|
||||||
|
for await (const attempts of batches(
|
||||||
|
LoginAttempt.find().sort({ _id: 1 }).lean().cursor(),
|
||||||
|
BATCH_SIZE
|
||||||
|
)) {
|
||||||
|
const rows = attempts.map((a) => {
|
||||||
|
track('LoginAttempt').source++
|
||||||
|
return {
|
||||||
|
id: nextAttemptId++,
|
||||||
|
username: a.username,
|
||||||
|
userId: userIdsByUsername.get(a.username) ?? null,
|
||||||
|
ip: a.ip,
|
||||||
|
loginMethod: 'password' as const, // legacy attempts were password-only
|
||||||
|
userAgent: a.userAgent,
|
||||||
|
time: instant(a.time ?? (a as any).createdAt),
|
||||||
|
success: a.success,
|
||||||
|
message: a.message ?? null,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await insert(
|
||||||
|
tx,
|
||||||
|
'LoginAttempt',
|
||||||
|
t.LoginAttempt.insert(rows).build(),
|
||||||
|
rows.length
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Advance the autoincrement sequences past the ids we assigned explicitly.
|
||||||
|
// Identifiers can't be bind parameters, so each statement is spelled out.
|
||||||
|
if (!DRY_RUN) {
|
||||||
|
const { raw } = db
|
||||||
|
const plans = [
|
||||||
|
raw.sql`SELECT setval(pg_get_serial_sequence('"User"', 'id'), COALESCE((SELECT MAX(id) FROM "User"), 0) + 1, false)`,
|
||||||
|
raw.sql`SELECT setval(pg_get_serial_sequence('"Session"', 'id'), COALESCE((SELECT MAX(id) FROM "Session"), 0) + 1, false)`,
|
||||||
|
raw.sql`SELECT setval(pg_get_serial_sequence('"Command"', 'id'), COALESCE((SELECT MAX(id) FROM "Command"), 0) + 1, false)`,
|
||||||
|
raw.sql`SELECT setval(pg_get_serial_sequence('"LoginAttempt"', 'id'), COALESCE((SELECT MAX(id) FROM "LoginAttempt"), 0) + 1, false)`,
|
||||||
|
]
|
||||||
|
for (const plan of plans)
|
||||||
|
await tx.execute(plan.affectedCount().build() as never)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assertTargetEmpty() {
|
||||||
|
const { orm } = db
|
||||||
|
const found = await Promise.all([
|
||||||
|
orm.public.User.first(),
|
||||||
|
orm.public.UserLoginMethod.first(),
|
||||||
|
orm.public.UserProgress.first(),
|
||||||
|
orm.public.Session.first(),
|
||||||
|
orm.public.Command.first(),
|
||||||
|
orm.public.LoginAttempt.first(),
|
||||||
|
])
|
||||||
|
if (found.some(Boolean)) {
|
||||||
|
throw new Error(
|
||||||
|
'Target tables are not empty. Truncate them or pass --force.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const mongoUrl = process.env['MONGO_URL']
|
||||||
|
if (!mongoUrl) throw new Error('MONGO_URL is not set')
|
||||||
|
const mongo = await connectDB(mongoUrl)
|
||||||
|
console.log(
|
||||||
|
DRY_RUN ? 'DRY RUN: nothing will be written' : 'Migrating Mongo -> Postgres'
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!FORCE && !DRY_RUN) await assertTargetEmpty()
|
||||||
|
await db.transaction((tx) => migrate(tx as Tx))
|
||||||
|
} finally {
|
||||||
|
await mongo.close()
|
||||||
|
await db.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
console.table(stats)
|
||||||
|
if (nulStripped)
|
||||||
|
console.log(`Stripped NUL bytes from ${nulStripped} command text fields`)
|
||||||
|
writeFileSync('migration-skipped.json', JSON.stringify(skipped, null, 2))
|
||||||
|
console.log(
|
||||||
|
`Skipped rows written to migration-skipped.json (${skipped.length})`
|
||||||
|
)
|
||||||
|
for (const [table, s] of Object.entries(stats)) {
|
||||||
|
if (!DRY_RUN && s.source !== s.inserted + s.skipped) {
|
||||||
|
throw new Error(
|
||||||
|
`Count mismatch for ${table}: ${s.source} != ${s.inserted} + ${s.skipped}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export { db } from './prisma/db'
|
||||||
|
|
||||||
|
console.log('Hello via Bun!')
|
||||||
+1609
File diff suppressed because it is too large.
Load diff
+1412
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,102 @@
|
|||||||
|
// use prisma-8
|
||||||
|
model User {
|
||||||
|
id BigInt @id @default(autoincrement())
|
||||||
|
email String @unique
|
||||||
|
username String @unique
|
||||||
|
name String?
|
||||||
|
createdAt DateTime
|
||||||
|
updatedAt DateTime
|
||||||
|
isGuest Boolean
|
||||||
|
|
||||||
|
loginMethods UserLoginMethod[]
|
||||||
|
progress UserProgress[]
|
||||||
|
sessions Session[]
|
||||||
|
loginAttempts LoginAttempt[]
|
||||||
|
}
|
||||||
|
|
||||||
|
enum LoginMethod {
|
||||||
|
@@type("pg/text@1")
|
||||||
|
Password = "password"
|
||||||
|
Google = "google"
|
||||||
|
GitHub = "github"
|
||||||
|
}
|
||||||
|
|
||||||
|
// User login using one of the login method
|
||||||
|
model UserLoginMethod {
|
||||||
|
type LoginMethod
|
||||||
|
userId BigInt
|
||||||
|
createdAt DateTime
|
||||||
|
updatedAt DateTime
|
||||||
|
hashedPassword String?
|
||||||
|
oauthId String?
|
||||||
|
displayName String?
|
||||||
|
|
||||||
|
@@id([userId, type])
|
||||||
|
@@unique([type, oauthId])
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store what quest a user has completed
|
||||||
|
model UserProgress {
|
||||||
|
userId BigInt
|
||||||
|
quest String
|
||||||
|
startedAt DateTime
|
||||||
|
finishedAt DateTime?
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
|
||||||
|
@@id([userId, quest])
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SessionStatus {
|
||||||
|
@@type("pg/text@1")
|
||||||
|
Active = "active"
|
||||||
|
Finished = "finished"
|
||||||
|
Inactive = "inactive"
|
||||||
|
}
|
||||||
|
|
||||||
|
model Session {
|
||||||
|
id BigInt @id @default(autoincrement())
|
||||||
|
userId BigInt
|
||||||
|
quest String
|
||||||
|
locale String
|
||||||
|
containerId String?
|
||||||
|
containerName String?
|
||||||
|
status SessionStatus
|
||||||
|
createdAt DateTime
|
||||||
|
lastActivityAt DateTime
|
||||||
|
finishedAt DateTime?
|
||||||
|
completedEvents String[]
|
||||||
|
graph Jsonb
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
commands Command[]
|
||||||
|
|
||||||
|
@@index([userId, quest])
|
||||||
|
@@index([userId, status])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Command {
|
||||||
|
id BigInt @id @default(autoincrement())
|
||||||
|
sessionId BigInt
|
||||||
|
timestamp DateTime
|
||||||
|
pwd String
|
||||||
|
command String
|
||||||
|
output String?
|
||||||
|
error String?
|
||||||
|
stage String?
|
||||||
|
session Session @relation(fields: [sessionId], references: [id])
|
||||||
|
|
||||||
|
@@index([sessionId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model LoginAttempt {
|
||||||
|
id BigInt @id @default(autoincrement())
|
||||||
|
username String
|
||||||
|
userId BigInt?
|
||||||
|
ip String
|
||||||
|
loginMethod LoginMethod
|
||||||
|
userAgent String
|
||||||
|
time DateTime
|
||||||
|
success Boolean
|
||||||
|
message String?
|
||||||
|
user User? @relation(fields: [userId], references: [id])
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import 'dotenv/config'
|
||||||
|
import postgres from '@prisma/orm-postgres/runtime'
|
||||||
|
import type { Contract } from './contract.d'
|
||||||
|
import contractJson from './contract.json' with { type: 'json' }
|
||||||
|
|
||||||
|
export const db = postgres<Contract>({
|
||||||
|
contractJson,
|
||||||
|
url: process.env['DATABASE_URL']!,
|
||||||
|
})
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
// Environment setup & latest features
|
||||||
|
"lib": ["ESNext"],
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "preserve",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"allowJs": true,
|
||||||
|
"types": [
|
||||||
|
"bun",
|
||||||
|
"node"
|
||||||
|
],
|
||||||
|
|
||||||
|
// Bundler mode
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
// Best practices
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
|
||||||
|
// Some stricter flags (disabled by default)
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noPropertyAccessFromIndexSignature": false,
|
||||||
|
"resolveJsonModule": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"name": "@odyssey/file-graph",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"main": "index.ts",
|
||||||
|
"types": "index.ts",
|
||||||
|
"exports": {
|
||||||
|
".": "./index.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, test, expect } from '@jest/globals'
|
import { describe, test, expect, beforeEach } from 'bun:test'
|
||||||
import File from '../file.js'
|
import File from '../file.js'
|
||||||
|
|
||||||
describe('File', () => {
|
describe('File', () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, test, expect } from '@jest/globals'
|
import { describe, test, expect, beforeEach } from 'bun:test'
|
||||||
import FileGraph from '../fileGraph.js'
|
import FileGraph from '../fileGraph.js'
|
||||||
import FileNode from '../fileNode.js'
|
import FileNode from '../fileNode.js'
|
||||||
import { FileObject } from '../file.js'
|
import { FileObject } from '../file.js'
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, test, expect } from '@jest/globals'
|
import { describe, test, expect, beforeEach } from 'bun:test'
|
||||||
import { ParentNotExistsError } from '../errors.js'
|
import { ParentNotExistsError } from '../errors.js'
|
||||||
import FileNode, { IFileNode } from '../fileNode.js'
|
import FileNode, { IFileNode } from '../fileNode.js'
|
||||||
import { FileObject } from '../file.js'
|
import { FileObject } from '../file.js'
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FileGraph, IFileNode } from '../file-graph'
|
import { FileGraph, IFileNode } from '@odyssey/file-graph'
|
||||||
import { Quest } from './Quest'
|
import { Quest } from './Quest'
|
||||||
import {
|
import {
|
||||||
IQuest,
|
IQuest,
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "@odyssey/game",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"main": "index.ts",
|
||||||
|
"types": "index.ts",
|
||||||
|
"exports": {
|
||||||
|
".": "./index.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@odyssey/file-graph": "workspace:*",
|
||||||
|
"zod": "^3.24.2",
|
||||||
|
"zod-to-json-schema": "^3.24.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IFileNode } from '../../file-graph'
|
import { IFileNode } from '@odyssey/file-graph'
|
||||||
|
|
||||||
export * from './condition.js'
|
export * from './condition.js'
|
||||||
export * from './stage.js'
|
export * from './stage.js'
|
||||||
|
|||||||
@@ -1,28 +1,7 @@
|
|||||||
import { test, expect } from '@jest/globals'
|
import { test, expect } from 'bun:test'
|
||||||
import { IFileExistenceChecker, IFileInput } from '../schema'
|
|
||||||
import { checkFiles } from '../condition/FileMatcher.js'
|
import { checkFiles } from '../condition/FileMatcher.js'
|
||||||
import { Condition } from '../condition/Condition.js'
|
import { Condition } from '../condition/Condition.js'
|
||||||
|
import { MockFileChecker } from './mockFileChecker'
|
||||||
export class MockFileChecker implements IFileExistenceChecker {
|
|
||||||
private files: IFileInput[] = [
|
|
||||||
{
|
|
||||||
path: '/home/user/hello.txt',
|
|
||||||
type: 'file',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/home/user/world.txt',
|
|
||||||
type: 'file',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/home/user/Downloads',
|
|
||||||
type: 'directory',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
async exists(file: IFileInput): Promise<boolean> {
|
|
||||||
return this.files.some((f) => f.path === file.path && f.type === file.type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test('file matcher', async () => {
|
test('file matcher', async () => {
|
||||||
const checker = new MockFileChecker()
|
const checker = new MockFileChecker()
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { test, expect } from '@jest/globals'
|
import { it, test, expect } from 'bun:test'
|
||||||
import { Condition } from '../condition/Condition'
|
import { Condition } from '../condition/Condition'
|
||||||
import { OutputMatcher } from '../condition/OutputMatcher'
|
import { OutputMatcher } from '../condition/OutputMatcher'
|
||||||
import { PwdMatcher } from '../condition/PwdMatcher'
|
import { PwdMatcher } from '../condition/PwdMatcher'
|
||||||
import { CommandMatcher } from '../condition/CommandMatcher'
|
import { CommandMatcher } from '../condition/CommandMatcher'
|
||||||
import { ErrorMatcher } from '../condition/ErrorMatcher'
|
import { ErrorMatcher } from '../condition/ErrorMatcher'
|
||||||
import { conditionSchema } from '../schema'
|
import { conditionSchema } from '../schema'
|
||||||
import { MockFileChecker } from './asyncCondition.test'
|
import { MockFileChecker } from './mockFileChecker'
|
||||||
|
|
||||||
test('command match', () => {
|
test('command match', () => {
|
||||||
const matcher = new CommandMatcher('^echo start$')
|
const matcher = new CommandMatcher('^echo start$')
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { expect, it, describe } from '@jest/globals'
|
import { describe, it, expect } from 'bun:test'
|
||||||
import { IQuest } from '../schema'
|
import { IQuest } from '../schema'
|
||||||
import { GameSession } from '../Session'
|
import { GameSession } from '../Session'
|
||||||
import { MockFileChecker } from './asyncCondition.test'
|
import { MockFileChecker } from './mockFileChecker'
|
||||||
|
|
||||||
const quest: IQuest = {
|
const quest: IQuest = {
|
||||||
id: 'quest1',
|
id: 'quest1',
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import type { IFileExistenceChecker, IFileInput } from '../schema'
|
||||||
|
|
||||||
|
export class MockFileChecker implements IFileExistenceChecker {
|
||||||
|
private files: IFileInput[] = [
|
||||||
|
{
|
||||||
|
path: '/home/user/hello.txt',
|
||||||
|
type: 'file',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/home/user/world.txt',
|
||||||
|
type: 'file',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/home/user/Downloads',
|
||||||
|
type: 'directory',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
async exists(file: IFileInput): Promise<boolean> {
|
||||||
|
return this.files.some((f) => f.path === file.path && f.type === file.type)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { expect, describe, it } from '@jest/globals'
|
import { describe, it, expect } from 'bun:test'
|
||||||
import { IQuest } from '../schema'
|
import { IQuest } from '../schema'
|
||||||
import { GameSession } from '../Session'
|
import { GameSession } from '../Session'
|
||||||
import { MockFileChecker } from './asyncCondition.test'
|
import { MockFileChecker } from './mockFileChecker'
|
||||||
|
|
||||||
const quest: IQuest = {
|
const quest: IQuest = {
|
||||||
id: 'quest1',
|
id: 'quest1',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { test, expect } from '@jest/globals'
|
import { test, expect } from 'bun:test'
|
||||||
import { Stage } from '../Stage'
|
import { Stage } from '../Stage'
|
||||||
import { MockFileChecker } from './asyncCondition.test'
|
import { MockFileChecker } from './mockFileChecker'
|
||||||
|
|
||||||
const checker = new MockFileChecker()
|
const checker = new MockFileChecker()
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import mongoose from 'mongoose'
|
import mongoose from 'mongoose'
|
||||||
|
|
||||||
export { User, IUser } from './user.js'
|
export { User, type IUser } from './user.js'
|
||||||
export { Session, ISession } from './session.js'
|
export { Session, type ISession } from './session.js'
|
||||||
export { Command, ICommand } from './command.js'
|
export { Command, type ICommand } from './command.js'
|
||||||
export { UserProfile, IUserProfile, IProgress } from './userProfile.js'
|
export {
|
||||||
export { ILoginAttempt, LoginAttempt } from './loginAttempt.js'
|
UserProfile,
|
||||||
|
type IUserProfile,
|
||||||
|
type IProgress,
|
||||||
|
} from './userProfile.js'
|
||||||
|
export { type ILoginAttempt, LoginAttempt } from './loginAttempt.js'
|
||||||
|
|
||||||
export async function connectDB(uri: string) {
|
export async function connectDB(uri: string) {
|
||||||
const db = await mongoose.connect(uri)
|
const db = await mongoose.connect(uri)
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "@odyssey/models",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"main": "index.ts",
|
||||||
|
"types": "index.ts",
|
||||||
|
"exports": {
|
||||||
|
".": "./index.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@odyssey/constants": "workspace:*",
|
||||||
|
"mongoose": "^8.24.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { model, Schema, Types } from 'mongoose'
|
import { model, Schema, Types } from 'mongoose'
|
||||||
import type { QuestLocale } from '../constants'
|
import type { QuestLocale } from '@odyssey/constants'
|
||||||
import { SUPPORTED_LOCALES } from '../constants'
|
import { SUPPORTED_LOCALES } from '@odyssey/constants'
|
||||||
|
|
||||||
export interface INode {
|
export interface INode {
|
||||||
path: string
|
path: string
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"name": "@odyssey/utils",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"main": "index.ts",
|
||||||
|
"types": "index.ts",
|
||||||
|
"exports": {
|
||||||
|
".": "./index.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { test, expect } from '@jest/globals'
|
import { test, expect } from 'bun:test'
|
||||||
import { isValidEmail, isValidUsername } from '../auth'
|
import { isValidEmail, isValidUsername } from '../auth'
|
||||||
|
|
||||||
test('check valid username', () => {
|
test('check valid username', () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, it, expect } from '@jest/globals'
|
import { describe, it, expect } from 'bun:test'
|
||||||
import { buildFileCheckCmd, File } from '../commands'
|
import { buildFileCheckCmd, File } from '../commands'
|
||||||
|
|
||||||
describe('buildFileCheckCmd function', () => {
|
describe('buildFileCheckCmd function', () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, it, expect } from '@jest/globals'
|
import { describe, it, expect } from 'bun:test'
|
||||||
import DAG from '../dag'
|
import DAG from '../dag'
|
||||||
|
|
||||||
describe('dag layers', () => {
|
describe('dag layers', () => {
|
||||||
|
|||||||
+16
-15
@@ -1,27 +1,28 @@
|
|||||||
FROM node:20-alpine AS build
|
FROM oven/bun:1.3.14-alpine AS prod
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY ./package.json yarn.lock ./
|
COPY package.json bun.lock ./
|
||||||
|
COPY app/package.json ./app/package.json
|
||||||
COPY server/package.json ./server/package.json
|
COPY server/package.json ./server/package.json
|
||||||
RUN yarn install
|
COPY services/analytics/package.json ./services/analytics/package.json
|
||||||
|
COPY tests/package.json ./tests/package.json
|
||||||
|
COPY packages/constants/package.json ./packages/constants/package.json
|
||||||
|
COPY packages/file-graph/package.json ./packages/file-graph/package.json
|
||||||
|
COPY packages/game/package.json ./packages/game/package.json
|
||||||
|
COPY packages/models/package.json ./packages/models/package.json
|
||||||
|
COPY packages/utils/package.json ./packages/utils/package.json
|
||||||
|
RUN bun install --frozen-lockfile --production --filter server
|
||||||
|
|
||||||
# Install docker cli
|
# Install docker cli
|
||||||
COPY server ./server
|
|
||||||
COPY packages ./packages
|
|
||||||
COPY tsconfig.json ./tsconfig.json
|
|
||||||
RUN yarn workspace server build
|
|
||||||
|
|
||||||
# Production
|
|
||||||
FROM node:20-alpine AS prod
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
COPY --from=docker:dind /usr/local/bin/docker /usr/local/bin/
|
COPY --from=docker:dind /usr/local/bin/docker /usr/local/bin/
|
||||||
COPY --from=build /app/node_modules ./node_modules
|
COPY tsconfig.json ./tsconfig.json
|
||||||
COPY --from=build /app/server ./server
|
COPY packages ./packages
|
||||||
|
COPY server ./server
|
||||||
COPY ./quests ./quests
|
COPY ./quests ./quests
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
WORKDIR /app/server
|
WORKDIR /app/server
|
||||||
CMD [ "yarn", "start" ]
|
CMD [ "bun", "src/index.ts" ]
|
||||||
|
|
||||||
ENV HOST=0.0.0.0
|
ENV HOST=0.0.0.0
|
||||||
ENV PORT=3000
|
ENV PORT=3000
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { connectDB, User } from '../../packages/models'
|
import { connectDB, User } from '@odyssey/models'
|
||||||
|
|
||||||
async function migrate() {
|
async function migrate() {
|
||||||
await connectDB(
|
await connectDB(
|
||||||
|
|||||||
+11
-16
@@ -3,15 +3,19 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"description": "Linux Odyssey Server",
|
"description": "Linux Odyssey Server",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node dist/server/src/index.js",
|
"start": "bun src/index.ts",
|
||||||
"setup": "tsx src/cli/setup.ts",
|
"setup": "bun src/cli/setup.ts",
|
||||||
"clean": "tsx src/cli/clean.ts",
|
"clean": "bun src/cli/clean.ts",
|
||||||
"build": "tsc",
|
|
||||||
"type-check": "tsc --noEmit",
|
"type-check": "tsc --noEmit",
|
||||||
"dev": "tsx watch --include \"../quests/**/*.yml\" src/index.ts",
|
"dev": "bun scripts/dev.ts",
|
||||||
"build:quests": "tsx src/cli/build.ts"
|
"build:quests": "bun src/cli/build.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@odyssey/constants": "workspace:*",
|
||||||
|
"@odyssey/game": "workspace:*",
|
||||||
|
"@odyssey/models": "workspace:*",
|
||||||
|
"@odyssey/utils": "workspace:*",
|
||||||
|
"@odyssey/database": "workspace:*",
|
||||||
"@trpc/server": "^11.8.0",
|
"@trpc/server": "^11.8.0",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"commander": "^11.0.0",
|
"commander": "^11.0.0",
|
||||||
@@ -36,20 +40,17 @@
|
|||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
"socket.io": "^4.7.1",
|
"socket.io": "^4.7.1",
|
||||||
"ssh2": "^1.16.0",
|
|
||||||
"swagger-ui-express": "^5.0.0",
|
"swagger-ui-express": "^5.0.0",
|
||||||
"tsx": "^4.19.3",
|
|
||||||
"validator": "^13.15.22",
|
"validator": "^13.15.22",
|
||||||
"winston": "^3.11.0",
|
"winston": "^3.11.0",
|
||||||
"yaml": "^2.8.3",
|
"yaml": "^2.8.3",
|
||||||
"zod": "^3.24.2"
|
"zod": "^3.24.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@swc/core": "^1.4.2",
|
|
||||||
"@swc/helpers": "^0.5.6",
|
|
||||||
"@types/bcrypt": "^5.0.2",
|
"@types/bcrypt": "^5.0.2",
|
||||||
"@types/dockerode": "^3.3.23",
|
"@types/dockerode": "^3.3.23",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/express-serve-static-core": "^5.1.3",
|
||||||
"@types/express-session": "^1.17.10",
|
"@types/express-session": "^1.17.10",
|
||||||
"@types/jsonwebtoken": "^9.0.5",
|
"@types/jsonwebtoken": "^9.0.5",
|
||||||
"@types/minimist": "^1.2.5",
|
"@types/minimist": "^1.2.5",
|
||||||
@@ -59,14 +60,8 @@
|
|||||||
"@types/passport-jwt": "^4.0.1",
|
"@types/passport-jwt": "^4.0.1",
|
||||||
"@types/passport-local": "^1.0.38",
|
"@types/passport-local": "^1.0.38",
|
||||||
"@types/passport-oauth2": "^1.4.15",
|
"@types/passport-oauth2": "^1.4.15",
|
||||||
"@types/source-map-support": "^0",
|
|
||||||
"@types/ssh2": "^1.15.1",
|
|
||||||
"@types/swagger-ui-express": "^4.1.6",
|
"@types/swagger-ui-express": "^4.1.6",
|
||||||
"@types/validator": "^13.11.9",
|
"@types/validator": "^13.11.9",
|
||||||
"concurrently": "^8.2.2",
|
|
||||||
"jest": "^29.6.1",
|
|
||||||
"regenerator-runtime": "^0.14.1",
|
|
||||||
"source-map-support": "^0.5.21",
|
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
// Dev server with auto-restart. `bun --watch` only tracks imported modules,
|
||||||
|
// but quests are read from YAML at startup, so rewrite the entry file (same
|
||||||
|
// contents) whenever a quest file changes to trigger a restart. Bun ignores
|
||||||
|
// timestamp-only changes, so `touch` would not work.
|
||||||
|
import { watch, readFileSync, writeFileSync } from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
const entry = path.join(import.meta.dir, '../src/index.ts')
|
||||||
|
const questsDir = path.join(import.meta.dir, '../../quests')
|
||||||
|
|
||||||
|
const server = Bun.spawn(['bun', '--watch', entry], {
|
||||||
|
stdio: ['inherit', 'inherit', 'inherit'],
|
||||||
|
})
|
||||||
|
|
||||||
|
let timer: ReturnType<typeof setTimeout> | undefined
|
||||||
|
watch(questsDir, { recursive: true }, (_event, filename) => {
|
||||||
|
if (!filename?.endsWith('.yml')) return
|
||||||
|
clearTimeout(timer)
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
writeFileSync(entry, readFileSync(entry))
|
||||||
|
}, 100)
|
||||||
|
})
|
||||||
|
|
||||||
|
const stop = () => server.kill()
|
||||||
|
process.on('SIGINT', stop)
|
||||||
|
process.on('SIGTERM', stop)
|
||||||
|
process.exit(await server.exited)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Request, Response } from 'express'
|
import type { Request, Response } from 'express'
|
||||||
import { GameSession, commandSchema } from '../../../../packages/game'
|
import { GameSession, commandSchema } from '@odyssey/game'
|
||||||
import { Command, Session } from '../../../../packages/models'
|
import { Command, Session } from '@odyssey/models'
|
||||||
import { pushToSession } from '../socket'
|
import { pushToSession } from '../socket'
|
||||||
import { finishSession } from '../../models/sessionManager'
|
import { finishSession } from '../../models/sessionManager'
|
||||||
import { asyncHandler } from '../../middleware/error'
|
import { asyncHandler } from '../../middleware/error'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Router, Request, Response, Express } from 'express'
|
import { Router, Request, Response, Express } from 'express'
|
||||||
import passport from 'passport'
|
import passport from 'passport'
|
||||||
import { NextFunction } from 'express-serve-static-core'
|
import { NextFunction } from 'express-serve-static-core'
|
||||||
import { LoginAttempt } from '../../../../packages/models'
|
import { LoginAttempt } from '@odyssey/models'
|
||||||
import enabledMethods from '../../auth/passport.js'
|
import enabledMethods from '../../auth/passport.js'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { Router } from 'express'
|
import { Router } from 'express'
|
||||||
import passport from 'passport'
|
|
||||||
import commands from './commandRouter.js'
|
import commands from './commandRouter.js'
|
||||||
import authRouter from './authRouter.js'
|
import authRouter from './authRouter.js'
|
||||||
import authRequired from '../../middleware/authRequired.js'
|
|
||||||
import config from '../../config.js'
|
import config from '../../config.js'
|
||||||
import leaderboardController from '../controllers/leaderboardController.js'
|
import leaderboardController from '../controllers/leaderboardController.js'
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Server } from 'socket.io'
|
|||||||
import type { Socket } from 'socket.io'
|
import type { Socket } from 'socket.io'
|
||||||
import validator from 'validator'
|
import validator from 'validator'
|
||||||
import type { Server as HttpServer } from 'http'
|
import type { Server as HttpServer } from 'http'
|
||||||
import { Session } from '../../../packages/models'
|
import { Session } from '@odyssey/models'
|
||||||
import SessionMiddleware from '../middleware/session.js'
|
import SessionMiddleware from '../middleware/session.js'
|
||||||
import logger from '../utils/logger.js'
|
import logger from '../utils/logger.js'
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { check } from 'express-validator'
|
import { check } from 'express-validator'
|
||||||
|
|
||||||
import { isValidUsername, isValidEmail } from '../../../../packages/utils'
|
import { isValidUsername, isValidEmail } from '@odyssey/utils'
|
||||||
import { User } from '../../../../packages/models'
|
import { User } from '@odyssey/models'
|
||||||
import { passwordPolicy } from '../../../../packages/constants'
|
import { passwordPolicy } from '@odyssey/constants'
|
||||||
import { noError } from '../../middleware/validator.js'
|
import { noError } from '../../middleware/validator.js'
|
||||||
|
|
||||||
const checkUsername = () =>
|
const checkUsername = () =>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Profile } from 'passport'
|
import type { Profile } from 'passport'
|
||||||
import type { VerifyCallback } from 'passport-oauth2'
|
import type { VerifyCallback } from 'passport-oauth2'
|
||||||
import { User } from '../../../packages/models'
|
import { User } from '@odyssey/models'
|
||||||
import logger from '../utils/logger.js'
|
import logger from '../utils/logger.js'
|
||||||
|
|
||||||
export default async function oauthVerify(
|
export default async function oauthVerify(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import passport from 'passport'
|
import passport from 'passport'
|
||||||
import type { Express } from 'express'
|
import type { Express } from 'express'
|
||||||
import { UserProfile } from '../../../packages/models'
|
import { UserProfile } from '@odyssey/models'
|
||||||
import passwordStrategy from './passwordStrategy.js'
|
import passwordStrategy from './passwordStrategy.js'
|
||||||
import jwtStrategy from './jwtStrategy.js'
|
import jwtStrategy from './jwtStrategy.js'
|
||||||
import googleStrategy from './googleStrategy.js'
|
import googleStrategy from './googleStrategy.js'
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import bcrypt from 'bcrypt'
|
|||||||
import { Strategy as LocalStrategy } from 'passport-local'
|
import { Strategy as LocalStrategy } from 'passport-local'
|
||||||
import type { IVerifyOptions } from 'passport-local'
|
import type { IVerifyOptions } from 'passport-local'
|
||||||
import type { Express } from 'express'
|
import type { Express } from 'express'
|
||||||
import { User } from '../../../packages/models'
|
import { User } from '@odyssey/models'
|
||||||
|
|
||||||
async function verifyPassword(
|
async function verifyPassword(
|
||||||
username: string,
|
username: string,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { connectDB } from '../../../packages/models'
|
import { connectDB } from '@odyssey/models'
|
||||||
import { removeExpired } from '../containers/expiryChecker'
|
import { removeExpired } from '../containers/expiryChecker'
|
||||||
import config from '../config'
|
import config from '../config'
|
||||||
import logger from '../utils/logger'
|
import logger from '../utils/logger'
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ function createConfig() {
|
|||||||
|
|
||||||
function getProjectRoot(): string {
|
function getProjectRoot(): string {
|
||||||
// Start from the current file's directory
|
// Start from the current file's directory
|
||||||
let currentDir = __dirname
|
let currentDir = import.meta.dir
|
||||||
|
|
||||||
// Keep going up until we find the root package.json with workspaces
|
// Keep going up until we find the root package.json with workspaces
|
||||||
while (currentDir !== path.parse(currentDir).root) {
|
while (currentDir !== path.parse(currentDir).root) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { execFile } from 'child_process'
|
import { execFile } from 'child_process'
|
||||||
|
|
||||||
import { buildFileCheckCmd } from '../../../packages/utils'
|
import { buildFileCheckCmd } from '@odyssey/utils'
|
||||||
import type { IFileExistenceChecker, IFileInput } from '../../../packages/game'
|
import type { IFileExistenceChecker, IFileInput } from '@odyssey/game'
|
||||||
|
|
||||||
function sanitizeId(id: string) {
|
function sanitizeId(id: string) {
|
||||||
return id.replace(/[^a-zA-Z0-9]/g, '')
|
return id.replace(/[^a-zA-Z0-9]/g, '')
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import fs from 'fs/promises'
|
import fs from 'fs/promises'
|
||||||
import Docker from 'dockerode'
|
import Docker from 'dockerode'
|
||||||
import type { QuestLocale } from '../../../packages/constants'
|
import type { QuestLocale } from '@odyssey/constants'
|
||||||
import config, { getQuestImage } from '../config.js'
|
import config, { getQuestImage } from '../config.js'
|
||||||
import logger from '../utils/logger.js'
|
import logger from '../utils/logger.js'
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import schedule from 'node-schedule'
|
import schedule from 'node-schedule'
|
||||||
import type { HydratedDocument } from 'mongoose'
|
import type { HydratedDocument } from 'mongoose'
|
||||||
import { Session } from '../../../packages/models'
|
import { Session } from '@odyssey/models'
|
||||||
import type { ISession } from '../../../packages/models'
|
import type { ISession } from '@odyssey/models'
|
||||||
import config from '../config.js'
|
import config from '../config.js'
|
||||||
import { deleteContainer } from './docker.js'
|
import { deleteContainer } from './docker.js'
|
||||||
import logger from '../utils/logger.js'
|
import logger from '../utils/logger.js'
|
||||||
|
|||||||
+4
-2
@@ -5,7 +5,8 @@ import YAML from 'yaml'
|
|||||||
import swaggerUI from 'swagger-ui-express'
|
import swaggerUI from 'swagger-ui-express'
|
||||||
import passport from 'passport'
|
import passport from 'passport'
|
||||||
import * as trpcExpress from '@trpc/server/adapters/express'
|
import * as trpcExpress from '@trpc/server/adapters/express'
|
||||||
import { connectDB } from '../../packages/models'
|
import { connectDB } from '@odyssey/models'
|
||||||
|
import { db } from '@odyssey/database'
|
||||||
|
|
||||||
import './auth/passport.js'
|
import './auth/passport.js'
|
||||||
import socketServer from './api/socket.js'
|
import socketServer from './api/socket.js'
|
||||||
@@ -22,9 +23,10 @@ import { appRouter } from './routers/index.js'
|
|||||||
import { createContext } from './trpc.js'
|
import { createContext } from './trpc.js'
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
|
console.log(db)
|
||||||
if (!config.secret) {
|
if (!config.secret) {
|
||||||
logger.error(
|
logger.error(
|
||||||
'No SECRET_KEY found in .env! To set up a persistent key, please run `yarn setup`:'
|
'No SECRET_KEY found in .env! To set up a persistent key, please run `bun run setup`:'
|
||||||
)
|
)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import rateLimit from 'express-rate-limit'
|
import rateLimit from 'express-rate-limit'
|
||||||
import type { Options } from 'express-rate-limit'
|
import type { Options } from 'express-rate-limit'
|
||||||
import type { Request, Response, NextFunction } from 'express'
|
import type { Request, Response, NextFunction } from 'express'
|
||||||
|
import { LoginAttempt } from '@odyssey/models'
|
||||||
import config from '../config.js'
|
import config from '../config.js'
|
||||||
import { LoginAttempt } from '../../../packages/models'
|
|
||||||
import logger from '../utils/logger.js'
|
import logger from '../utils/logger.js'
|
||||||
|
|
||||||
function createRateLimiter(options: Partial<Options>) {
|
function createRateLimiter(options: Partial<Options>) {
|
||||||
|
|||||||
@@ -3,13 +3,9 @@ import fs from 'fs/promises'
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import yaml from 'yaml'
|
import yaml from 'yaml'
|
||||||
|
|
||||||
import type { QuestLocale } from '../../../packages/constants'
|
import type { QuestLocale } from '@odyssey/constants'
|
||||||
import { SUPPORTED_LOCALES } from '../../../packages/constants'
|
import { SUPPORTED_LOCALES } from '@odyssey/constants'
|
||||||
import {
|
import { questSchema, globalExceptionSchema, IQuest } from '@odyssey/game'
|
||||||
questSchema,
|
|
||||||
globalExceptionSchema,
|
|
||||||
IQuest,
|
|
||||||
} from '../../../packages/game'
|
|
||||||
import logger from '../utils/logger.js'
|
import logger from '../utils/logger.js'
|
||||||
import config from '../config.js'
|
import config from '../config.js'
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { HydratedDocument } from 'mongoose'
|
import { HydratedDocument } from 'mongoose'
|
||||||
import type { QuestLocale } from '../../../packages/constants'
|
import type { QuestLocale } from '@odyssey/constants'
|
||||||
import { LOCALE_NAME_MAP } from '../../../packages/constants'
|
import { LOCALE_NAME_MAP } from '@odyssey/constants'
|
||||||
import { Session, UserProfile } from '../../../packages/models'
|
import { Session, UserProfile } from '@odyssey/models'
|
||||||
import type { ISession, IUser } from '../../../packages/models'
|
import type { ISession, IUser } from '@odyssey/models'
|
||||||
import {
|
import {
|
||||||
createContainer,
|
createContainer,
|
||||||
deleteContainer,
|
deleteContainer,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Express } from 'express'
|
import { Express } from 'express'
|
||||||
import { format } from 'date-fns'
|
import { format } from 'date-fns'
|
||||||
import { User, UserProfile } from '../../../packages/models'
|
import { User, UserProfile } from '@odyssey/models'
|
||||||
import { hashPassword } from '../utils/auth.js'
|
import { hashPassword } from '../utils/auth.js'
|
||||||
|
|
||||||
export async function createUser(
|
export async function createUser(
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { TRPCError } from '@trpc/server'
|
import { TRPCError } from '@trpc/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { SUPPORTED_LOCALES } from '@odyssey/constants'
|
||||||
import { router, publicProcedure } from '../trpc.js'
|
import { router, publicProcedure } from '../trpc.js'
|
||||||
import { questManager } from '../models/quest.js'
|
import { questManager } from '../models/quest.js'
|
||||||
import { SUPPORTED_LOCALES } from '../../../packages/constants'
|
|
||||||
|
|
||||||
export const questRouter = router({
|
export const questRouter = router({
|
||||||
getQuests: publicProcedure
|
getQuests: publicProcedure
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { TRPCError } from '@trpc/server'
|
import { TRPCError } from '@trpc/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { SUPPORTED_LOCALES } from '../../../packages/constants'
|
import { SUPPORTED_LOCALES } from '@odyssey/constants'
|
||||||
import { ISession, Session } from '../../../packages/models'
|
import { ISession, Session } from '@odyssey/models'
|
||||||
import { GameSession, VoidFileExistenceChecker } from '../../../packages/game'
|
import { GameSession, VoidFileExistenceChecker } from '@odyssey/game'
|
||||||
import { createNewSession, isQuestUnlocked } from '../models/sessionManager.js'
|
import { createNewSession, isQuestUnlocked } from '../models/sessionManager.js'
|
||||||
import config from '../config.js'
|
import config from '../config.js'
|
||||||
import { protectedProcedure, router } from '../trpc.js'
|
import { protectedProcedure, router } from '../trpc.js'
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { TRPCError } from '@trpc/server'
|
import { TRPCError } from '@trpc/server'
|
||||||
import { UserProfile } from '../../../packages/models'
|
import { UserProfile } from '@odyssey/models'
|
||||||
import { protectedProcedure, router } from '../trpc.js'
|
import { protectedProcedure, router } from '../trpc.js'
|
||||||
|
|
||||||
export const userRouter = router({
|
export const userRouter = router({
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { User } from '../../../packages/models'
|
import { User } from '@odyssey/models'
|
||||||
import config from '../config.js'
|
import config from '../config.js'
|
||||||
import { createUser } from '../models/userManager.js'
|
import { createUser } from '../models/userManager.js'
|
||||||
import logger from './logger.js'
|
import logger from './logger.js'
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
{
|
{
|
||||||
"extends": "../tsconfig.json",
|
"extends": "../tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "NodeNext",
|
"module": "Preserve",
|
||||||
"moduleResolution": "nodenext",
|
"moduleResolution": "bundler",
|
||||||
"esModuleInterop": true,
|
"target": "ESNext",
|
||||||
"outDir": "./dist"
|
"types": ["bun", "node"],
|
||||||
|
"noEmit": true
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src", "scripts", "migrations"],
|
||||||
"exclude": ["node_modules", "src/*.test.js", "dist"],
|
"exclude": ["node_modules", "src/*.test.js"]
|
||||||
"ts-node": {
|
|
||||||
"swc": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,27 +1,26 @@
|
|||||||
FROM node:20-alpine AS build
|
FROM oven/bun:1.3.14-alpine AS prod
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY ./package.json yarn.lock ./
|
COPY package.json bun.lock ./
|
||||||
|
COPY app/package.json ./app/package.json
|
||||||
|
COPY server/package.json ./server/package.json
|
||||||
COPY services/analytics/package.json ./services/analytics/package.json
|
COPY services/analytics/package.json ./services/analytics/package.json
|
||||||
RUN yarn install --frozen-lockfile
|
COPY tests/package.json ./tests/package.json
|
||||||
|
COPY packages/constants/package.json ./packages/constants/package.json
|
||||||
|
COPY packages/file-graph/package.json ./packages/file-graph/package.json
|
||||||
|
COPY packages/game/package.json ./packages/game/package.json
|
||||||
|
COPY packages/models/package.json ./packages/models/package.json
|
||||||
|
COPY packages/utils/package.json ./packages/utils/package.json
|
||||||
|
RUN bun install --frozen-lockfile --production --filter analytics
|
||||||
|
|
||||||
COPY tsconfig.json ./tsconfig.json
|
COPY tsconfig.json ./tsconfig.json
|
||||||
COPY packages ./packages
|
COPY packages ./packages
|
||||||
COPY services/analytics ./services/analytics
|
COPY services/analytics ./services/analytics
|
||||||
|
|
||||||
RUN yarn workspace analytics build
|
|
||||||
|
|
||||||
# Production stage
|
|
||||||
FROM node:20-alpine AS prod
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
COPY --from=build /app/node_modules ./node_modules
|
|
||||||
COPY --from=build /app/services/analytics ./services/analytics
|
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
ENV HOST=0.0.0.0
|
ENV HOST=0.0.0.0
|
||||||
ENV PORT=3001
|
ENV PORT=3001
|
||||||
EXPOSE 3001
|
EXPOSE 3001
|
||||||
|
|
||||||
WORKDIR /app/services/analytics
|
WORKDIR /app/services/analytics
|
||||||
CMD [ "yarn", "start" ]
|
CMD [ "bun", "src/index.ts" ]
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "analytics",
|
"name": "analytics",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "src/index.js",
|
"main": "src/index.ts",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "GPL",
|
"license": "GPL",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node dist/services/analytics/src/index.js",
|
"start": "bun src/index.ts",
|
||||||
"build": "tsc",
|
|
||||||
"type-check": "tsc --noEmit",
|
"type-check": "tsc --noEmit",
|
||||||
"dev": "tsx watch src/index.ts"
|
"dev": "bun --watch src/index.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@odyssey/models": "workspace:*",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.20.0",
|
"express": "^4.20.0",
|
||||||
"express-basic-auth": "^1.2.1",
|
"express-basic-auth": "^1.2.1",
|
||||||
@@ -19,12 +19,7 @@
|
|||||||
"zod": "^3.24.2"
|
"zod": "^3.24.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@swc/core": "^1.4.2",
|
|
||||||
"@swc/helpers": "^0.5.6",
|
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/node": "^20.11.20",
|
|
||||||
"regenerator-runtime": "^0.14.1",
|
|
||||||
"tsx": "^4.19.3",
|
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { engine } from 'express-handlebars'
|
import { engine } from 'express-handlebars'
|
||||||
import basicAuth from 'express-basic-auth'
|
import basicAuth from 'express-basic-auth'
|
||||||
import { connectDB } from '../../../packages/models'
|
import { connectDB } from '@odyssey/models'
|
||||||
import config from './config'
|
import config from './config'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Command } from '../../../../packages/models'
|
import { Command } from '@odyssey/models'
|
||||||
import Pagination from './pagination'
|
import Pagination from './pagination'
|
||||||
|
|
||||||
// eslint-disable-next-line import/prefer-default-export
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { UserProfile } from '../../../../packages/models'
|
import { UserProfile } from '@odyssey/models'
|
||||||
|
|
||||||
export interface QuestProgress {
|
export interface QuestProgress {
|
||||||
_id: string
|
_id: string
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Command, Session } from '../../../../packages/models'
|
import { Command, Session } from '@odyssey/models'
|
||||||
import Pagination from './pagination'
|
import Pagination from './pagination'
|
||||||
import {
|
import {
|
||||||
CommandObject,
|
CommandObject,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { HydratedDocument, Types } from 'mongoose'
|
import { HydratedDocument, Types } from 'mongoose'
|
||||||
import { Session, User, IUser, ISession } from '../../../../packages/models'
|
import { Session, User, IUser, ISession } from '@odyssey/models'
|
||||||
import Pagination from './pagination'
|
import Pagination from './pagination'
|
||||||
import { UserSessionDetail } from '../interface'
|
import { UserSessionDetail } from '../interface'
|
||||||
|
|
||||||
|
|||||||
Loaded 100 of 106 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user