mirror of
https://github.com/linux-odyssey/linux-odyssey.git
synced 2026-11-06 03:20:13 +08:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae790dc6a2 | ||
|
|
4ed8358738 | ||
|
|
ba5026fa56 | ||
|
|
4cf79a164c |
No files matched your search
@@ -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
|
||||||
|
|||||||
+1
-1
@@ -30,7 +30,7 @@
|
|||||||
"socket.io-client": "^4.7.1",
|
"socket.io-client": "^4.7.1",
|
||||||
"validator": "^13.15.22",
|
"validator": "^13.15.22",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
"vue-i18n": "11.4.14",
|
"vue-i18n": "11.3.0",
|
||||||
"vue-router": "^4.2.4",
|
"vue-router": "^4.2.4",
|
||||||
"vue-toastification": "^2.0.0-rc.5",
|
"vue-toastification": "^2.0.0-rc.5",
|
||||||
"vue3-lottie": "^3.2.0",
|
"vue3-lottie": "^3.2.0",
|
||||||
|
|||||||
@@ -87,16 +87,15 @@ const handleChange = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Guest account currently disabled
|
const registerGuest = async () => {
|
||||||
// const registerGuest = async () => {
|
try {
|
||||||
// try {
|
await api.post('/auth/register-guest')
|
||||||
// await api.post('/auth/register-guest')
|
router.push({ name: 'map' })
|
||||||
// router.push({ name: 'map' })
|
} catch (err) {
|
||||||
// } catch (err) {
|
console.error(err)
|
||||||
// console.error(err)
|
toast.error('Something went wrong.')
|
||||||
// toast.error('Something went wrong.')
|
}
|
||||||
// }
|
}
|
||||||
// }
|
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<!-- login page -->
|
<!-- login page -->
|
||||||
@@ -242,13 +241,13 @@ const handleChange = () => {
|
|||||||
</RouterLink></span
|
</RouterLink></span
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
<!-- <button
|
<button
|
||||||
id="register-guest"
|
id="register-guest"
|
||||||
class="rounded-lg py-2 mt-3 bg-bg-primary text-text-secondary w-full border-text-secondary border-2"
|
class="rounded-lg py-2 mt-3 bg-bg-primary text-text-secondary w-full border-text-secondary border-2"
|
||||||
@click="registerGuest()"
|
@click="registerGuest()"
|
||||||
>
|
>
|
||||||
{{ t('authform.continue_as_guest') }}
|
{{ t('authform.continue_as_guest') }}
|
||||||
</button> -->
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
traefik:
|
traefik:
|
||||||
image: traefik:v3.6.9
|
image: traefik:v3.6.9
|
||||||
restart: always
|
|
||||||
ports:
|
ports:
|
||||||
- 8000:8000
|
- 8000:8000
|
||||||
- 8080:8080
|
- 8080:8080
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
version: '3'
|
||||||
services:
|
services:
|
||||||
traefik:
|
traefik:
|
||||||
image: traefik:v3.6.9
|
image: traefik:v3.6.9
|
||||||
@@ -28,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:
|
||||||
@@ -35,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:
|
||||||
|
|||||||
@@ -8,6 +8,6 @@
|
|||||||
".": "./index.ts"
|
".": "./index.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"zod": "^4.6.7"
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@odyssey/file-graph": "workspace:*",
|
"@odyssey/file-graph": "workspace:*",
|
||||||
"zod": "^4.6.7",
|
"zod": "^3.24.2",
|
||||||
"zod-to-json-schema": "^3.24.5"
|
"zod-to-json-schema": "^3.24.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+2
-2
@@ -15,6 +15,7 @@
|
|||||||
"@odyssey/game": "workspace:*",
|
"@odyssey/game": "workspace:*",
|
||||||
"@odyssey/models": "workspace:*",
|
"@odyssey/models": "workspace:*",
|
||||||
"@odyssey/utils": "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",
|
||||||
@@ -38,13 +39,12 @@
|
|||||||
"passport-google-oidc": "^0.1.0",
|
"passport-google-oidc": "^0.1.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
"rate-limiter-flexible": "^11.2.3",
|
|
||||||
"socket.io": "^4.7.1",
|
"socket.io": "^4.7.1",
|
||||||
"swagger-ui-express": "^5.0.0",
|
"swagger-ui-express": "^5.0.0",
|
||||||
"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": "^4.6.7"
|
"zod": "^3.24.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bcrypt": "^5.0.2",
|
"@types/bcrypt": "^5.0.2",
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ async function recordLogin(req: Request, success: boolean, message: string) {
|
|||||||
|
|
||||||
router.post('/register', authenticateRateLimit, registerValidators, register)
|
router.post('/register', authenticateRateLimit, registerValidators, register)
|
||||||
|
|
||||||
// router.post('/register-guest', authenticateRateLimit, registerGuest)
|
router.post('/register-guest', authenticateRateLimit, registerGuest)
|
||||||
|
|
||||||
router.post('/logout', logout)
|
router.post('/logout', logout)
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ 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 '@odyssey/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,6 +23,7 @@ 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 `bun run setup`:'
|
'No SECRET_KEY found in .env! To set up a persistent key, please run `bun run setup`:'
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
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 { RateLimiterMemory } from 'rate-limiter-flexible'
|
|
||||||
import { TRPCError } from '@trpc/server'
|
|
||||||
import { LoginAttempt } from '@odyssey/models'
|
import { LoginAttempt } from '@odyssey/models'
|
||||||
import config from '../config.js'
|
import config from '../config.js'
|
||||||
import logger from '../utils/logger.js'
|
import logger from '../utils/logger.js'
|
||||||
import { middleware } from '../trpc.js'
|
|
||||||
|
|
||||||
function createRateLimiter(options: Partial<Options>) {
|
function createRateLimiter(options: Partial<Options>) {
|
||||||
if (config.testing.enabled) {
|
if (config.testing.enabled) {
|
||||||
@@ -25,32 +22,13 @@ export const authenticateRateLimit = createRateLimiter({
|
|||||||
max: 5,
|
max: 5,
|
||||||
})
|
})
|
||||||
|
|
||||||
export function trpcRateLimit({
|
export const sessionRateLimit = createRateLimiter({
|
||||||
points,
|
windowMs: 10 * 1000,
|
||||||
duration,
|
max: 1,
|
||||||
}: {
|
keyGenerator: (req: Request) => {
|
||||||
points: number
|
return req.user ? (req.user as any)._id : req.ip || ''
|
||||||
duration: number
|
},
|
||||||
}) {
|
|
||||||
const limiter = new RateLimiterMemory({ points, duration })
|
|
||||||
return middleware(async (opts) => {
|
|
||||||
if (config.testing.enabled) {
|
|
||||||
return opts.next()
|
|
||||||
}
|
|
||||||
const key = opts.ctx.user?.id ?? opts.ctx.ip ?? ''
|
|
||||||
try {
|
|
||||||
await limiter.consume(key)
|
|
||||||
} catch {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: 'TOO_MANY_REQUESTS',
|
|
||||||
message: 'Too many requests. Please try again later.',
|
|
||||||
})
|
})
|
||||||
}
|
|
||||||
return opts.next()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const sessionRateLimit = trpcRateLimit({ points: 3, duration: 60 })
|
|
||||||
|
|
||||||
export async function rateLimitByAccount(
|
export async function rateLimitByAccount(
|
||||||
req: Request,
|
req: Request,
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import config from '../config.js'
|
|||||||
import { protectedProcedure, router } from '../trpc.js'
|
import { protectedProcedure, router } from '../trpc.js'
|
||||||
import { questManager } from '../models/quest.js'
|
import { questManager } from '../models/quest.js'
|
||||||
import { genJWT } from '../utils/auth'
|
import { genJWT } from '../utils/auth'
|
||||||
import { sessionRateLimit } from '../middleware/rateLimiter.js'
|
|
||||||
|
|
||||||
export type SessionDetail = Awaited<ReturnType<typeof sessionDetail>>
|
export type SessionDetail = Awaited<ReturnType<typeof sessionDetail>>
|
||||||
|
|
||||||
@@ -48,7 +47,6 @@ async function sessionDetail(session: ISession) {
|
|||||||
|
|
||||||
export const sessionRouter = router({
|
export const sessionRouter = router({
|
||||||
createSession: protectedProcedure
|
createSession: protectedProcedure
|
||||||
.use(sessionRateLimit)
|
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
questId: z.string(),
|
questId: z.string(),
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,7 @@ const t = initTRPC.context<AppContext>().create()
|
|||||||
* Export reusable router and procedure helpers
|
* Export reusable router and procedure helpers
|
||||||
* that can be used throughout the router
|
* that can be used throughout the router
|
||||||
*/
|
*/
|
||||||
export const { router, procedure: publicProcedure, middleware } = t
|
export const { router, procedure: publicProcedure } = t
|
||||||
|
|
||||||
export const protectedProcedure = publicProcedure.use(async (opts) => {
|
export const protectedProcedure = publicProcedure.use(async (opts) => {
|
||||||
if (opts.ctx.isAuthenticated() && !('newUser' in opts.ctx.user)) {
|
if (opts.ctx.isAuthenticated() && !('newUser' in opts.ctx.user)) {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
"express-basic-auth": "^1.2.1",
|
"express-basic-auth": "^1.2.1",
|
||||||
"express-handlebars": "^7.1.2",
|
"express-handlebars": "^7.1.2",
|
||||||
"mongoose": "^8.24.1",
|
"mongoose": "^8.24.1",
|
||||||
"zod": "^4.6.7"
|
"zod": "^3.24.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
|
|||||||
Reference in new issue
Block a user