mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 14:36:31 +00:00
migrations + seeders
This commit is contained in:
@@ -62,7 +62,7 @@ Nest owns everything: every domain is a DI module under `server/src/nest/<domain
|
||||
- **`/mcp` bodies are raw by design** — the parser wrappers exempt `/mcp` so the MCP SDK reads the untouched stream; never `@Body()` a `/mcp` route.
|
||||
- **Auth is default-deny**: global guards run as `APP_GUARD`s; public routes opt out with `@Public()`, and a boot-time ratchet (`validate-route-guards.ts`) refuses any public route not on its allow-list.
|
||||
- **Trip-scoped routes** verify trip access (404) and the permission (403) — `@UseGuards(JwtAuthGuard, TripAccessGuard)` + `@RequirePermission('<action>')` per handler — and forward `X-Socket-Id` to the broadcast so the originating client doesn't echo its own change. **Never gate a multipart upload with a guard** (the client sees ECONNRESET instead of 403 — check in the handler).
|
||||
- **DB** (`server/src/db/`): `better-sqlite3`, WAL, FK on. Migrations are positional and append-only. `NODE_ENV=test` gives each vitest worker an isolated `:memory:` DB.
|
||||
- **DB** (`server/src/db/`): `better-sqlite3`, WAL, FK on. Schema, migrations and seeding are **MikroORM's** (`db/migrations/`, `db/seeders/`), applied at boot from `buildApp()`; the ORM shares the one better-sqlite3 connection rather than opening its own. Queries are still raw SQL through `DatabaseService`. `NODE_ENV=test` gives each vitest worker an isolated `:memory:` DB.
|
||||
- **Realtime**: `/ws` is a Nest gateway in `server/src/nest/realtime/` — inject `RealtimeService`; `server/src/websocket.ts` is a legacy re-export stub.
|
||||
- **MCP**: tools are declared in `<domain>.mcp.ts` files beside each module, on the `server/src/nest-mcp/` decorator layer. A validation or permission change on a REST route must land in the parallel MCP tool in the same change. See `MCP.md`.
|
||||
- **Addons** are admin-toggleable feature modules keyed by `ADDON_IDS` in `server/src/addons.ts`.
|
||||
|
||||
Generated
+170
-2
@@ -4806,6 +4806,137 @@
|
||||
"integrity": "sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@mikro-orm/cli": {
|
||||
"version": "7.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@mikro-orm/cli/-/cli-7.2.1.tgz",
|
||||
"integrity": "sha512-YNt8HKsb90awZqcionp1oZgL2ZLRyGl76x0lYdcfVkYkYY0zrNQs3ZZ76+6AS/FXSX5oEp6q94LCIiYwnr04jA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mikro-orm/core": "7.2.1",
|
||||
"mikro-orm": "7.2.1",
|
||||
"yargs": "17.7.2"
|
||||
},
|
||||
"bin": {
|
||||
"mikro-orm": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 22.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mikro-orm/cli/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@mikro-orm/cli/node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@mikro-orm/cli/node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@mikro-orm/cli/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@mikro-orm/cli/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@mikro-orm/cli/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@mikro-orm/cli/node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@mikro-orm/cli/node_modules/yargs": {
|
||||
"version": "17.7.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^8.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.3",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@mikro-orm/cli/node_modules/yargs-parser": {
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@mikro-orm/core": {
|
||||
"version": "7.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@mikro-orm/core/-/core-7.2.1.tgz",
|
||||
@@ -4842,6 +4973,21 @@
|
||||
"@mikro-orm/core": "7.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@mikro-orm/migrations": {
|
||||
"version": "7.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@mikro-orm/migrations/-/migrations-7.2.1.tgz",
|
||||
"integrity": "sha512-sxGcB7na0AkcREsD5U3xP+Psb/nhQFkx7/eY1Nsqq9MBs67O5RoCL97bn3lmgq6GwKZFbktMuuOGozq42+GaNg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mikro-orm/sql": "7.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 22.17.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@mikro-orm/core": "7.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@mikro-orm/nestjs": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@mikro-orm/nestjs/-/nestjs-7.1.0.tgz",
|
||||
@@ -4878,6 +5024,18 @@
|
||||
"@mikro-orm/core": "7.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@mikro-orm/seeder": {
|
||||
"version": "7.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@mikro-orm/seeder/-/seeder-7.2.1.tgz",
|
||||
"integrity": "sha512-aZtpFXNW8MnP4/e351SMLuVuarHK3g713puMjRcHmLN38j+VLVL/uSoDAk+/KTP2neuNOe+5I8ioC3fTJI/qDQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 22.17.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@mikro-orm/core": "7.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@mikro-orm/sql": {
|
||||
"version": "7.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@mikro-orm/sql/-/sql-7.2.1.tgz",
|
||||
@@ -11465,7 +11623,6 @@
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -16002,6 +16159,15 @@
|
||||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mikro-orm": {
|
||||
"version": "7.2.1",
|
||||
"resolved": "https://registry.npmjs.org/mikro-orm/-/mikro-orm-7.2.1.tgz",
|
||||
"integrity": "sha512-IUUYgO5T+S67sw1Xp5r3vL0JeYnkampUIRJaHG6AzTcjujRbyRUOEc6NVAIsojKdXq9WbWRU0hI6ivUv9qSDLA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 22.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mime": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
|
||||
@@ -22279,7 +22445,6 @@
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@@ -22482,8 +22647,11 @@
|
||||
"dependencies": {
|
||||
"@aws-lite/client": "^0.23.7",
|
||||
"@aws-lite/s3": "^0.2.7",
|
||||
"@mikro-orm/cli": "^7.2.1",
|
||||
"@mikro-orm/migrations": "^7.2.1",
|
||||
"@mikro-orm/nestjs": "^7.1.0",
|
||||
"@mikro-orm/postgresql": "^7.2.1",
|
||||
"@mikro-orm/seeder": "^7.2.1",
|
||||
"@modelcontextprotocol/sdk": "^1.30.0",
|
||||
"@nestjs/common": "^11.2.5",
|
||||
"@nestjs/config": "^4.0.4",
|
||||
|
||||
+2
-2
@@ -32,14 +32,14 @@ Nest owns everything. Every domain is a DI module under `src/nest/<domain>/` (`c
|
||||
- **Scheduling**: `src/nest/scheduling/` (`CronRegistrarService`) is the one way to schedule a cron. Jobs are `*.job.ts` providers in their owning domain. The registrar refuses to schedule under `NODE_ENV=test` (an integration test pins it). **Never use a `@Cron` decorator** — it bypasses that gate.
|
||||
- **Plugins** (`src/nest/plugins/`): the child-process runtime (`supervisor/`, `runtime/plugin-host-entry.ts`, `protocol/envelope.ts`) is **intentionally not Nest** — it is the security boundary. `host/rpc-host.ts` registers an RPC method at spawn only if the plugin holds the unlocking permission (registration = authorization). Install-time gates live in `install/`; the manifest's `trek` version range fails closed, and `TREK_PLUGINS_IGNORE_TREK_RANGE` downgrades that to a warning. The protocol tables in `protocol/envelope.ts` (+ `src/plugin-event-sink.ts`) are **generated** into `plugin-sdk/` and `shared/` — run `gen:plugin-facts` after touching them and commit the output. `runtime/egress-policy.ts` is a hand-kept mirror guarded by a parity test in `plugin-sdk/`; change both sides together.
|
||||
- **Storage**: `src/nest/storage/` is the upload driver abstraction (local / S3 / mirror behind one interface, with an admin surface and a usage job). Domain services never touch the filesystem or the S3 SDK directly.
|
||||
- **DB** (`src/db/`): `better-sqlite3`, WAL, FK on; `database.ts` runs `schema.ts` → `migrations.ts` → `seeds.ts`. `NODE_ENV=test` ⇒ isolated `:memory:` DB per vitest worker; `TREK_DB_FILE` ⇒ explicit file DB (Playwright harness).
|
||||
- **DB** (`src/db/`): `better-sqlite3`, WAL, FK on. `database.ts` only opens the connection now; the schema comes from the **MikroORM** migrations in `db/migrations/` and the seeders in `db/seeders/`, applied by `db/orm.ts` from `buildApp()` before anything reads. `db/orm-driver.ts` hands the ORM that same connection instead of opening a second one — with `:memory:` under test, a second connection is a second, empty database. `schema.ts`/`migrations.ts`/`seeds.ts` still exist for the test suite only, guarded by `tests/unit/db/schema-parity.test.ts`. `NODE_ENV=test` ⇒ isolated `:memory:` DB per vitest worker, opened from a snapshot `tests/global-setup.ts` migrates once per run; `TREK_DB_FILE` ⇒ explicit file DB (Playwright harness).
|
||||
|
||||
## Rules for new code
|
||||
|
||||
- **Thin controller → injectable provider.** Never resurrect a plain function-module service; don't grow god services — split a domain into several modules when it outgrows one class (trips and auth already are).
|
||||
- **Inject, don't reach for module globals.** No new imports of the global `db` proxy from `nest/` layers; no new code depending on the `ws` singleton or event-sink globals. Modules must be importable without side effects.
|
||||
- **Transactions are not optional.** Multi-statement writes go in `db.transaction()`; never hand-roll `BEGIN`/`COMMIT`. Bind values with `?`; identifiers only via a literal allow-list.
|
||||
- **Migrations are append-only.** Identity is positional (array index == `schema_version`) — never reorder, insert mid-list, or delete; no env interpolation in SQL.
|
||||
- **Migrations are append-only.** A migration is identified by its class name in `mikro_orm_migrations`, and files sort by the timestamp in that name — never rename or delete one that has shipped, and never edit its body; add a new one. No env interpolation in SQL. (The old positional `schema_version` array is gone; `db/migrations.ts` is retained for tests and is not appended to.)
|
||||
- **Every write endpoint validates** through the Zod pipe with a schema from `@trek/shared` — no `Record<string, unknown>` + ad-hoc checks. Also verify every referenced id exists and belongs to the same trip.
|
||||
- **Every outbound `fetch`** gets a timeout, a response-size cap and boundary validation — no `as`-casting provider responses, no silent `catch → null`. User-influenced URLs go through the SSRF guard. Never fall back to another user's API key.
|
||||
- **Fail closed.** Unrecognized flag values mean OFF; sandbox/permission opt-outs refuse in production; never pick a less-safe path based on the *absence* of a file. GET handlers must not write.
|
||||
|
||||
@@ -24,11 +24,20 @@
|
||||
"test:e2e": "vitest run tests/e2e",
|
||||
"test:coverage": "vitest run --coverage"
|
||||
},
|
||||
"mikro-orm": {
|
||||
"configPaths": [
|
||||
"./src/mikro-orm.config.ts",
|
||||
"./dist/mikro-orm.config.js"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-lite/client": "^0.23.7",
|
||||
"@aws-lite/s3": "^0.2.7",
|
||||
"@mikro-orm/cli": "^7.2.1",
|
||||
"@mikro-orm/migrations": "^7.2.1",
|
||||
"@mikro-orm/nestjs": "^7.1.0",
|
||||
"@mikro-orm/postgresql": "^7.2.1",
|
||||
"@mikro-orm/seeder": "^7.2.1",
|
||||
"@modelcontextprotocol/sdk": "^1.30.0",
|
||||
"@nestjs/common": "^11.2.5",
|
||||
"@nestjs/config": "^4.0.4",
|
||||
|
||||
@@ -1,8 +1,39 @@
|
||||
import { execSync } from 'node:child_process';
|
||||
import { existsSync, rmSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
// Resolved from this file rather than cwd, so the directory that gets removed is
|
||||
// always this package's own dist no matter where the script is invoked from.
|
||||
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||
const distDir = join(packageRoot, 'dist');
|
||||
|
||||
// tsc only ever adds to dist, so a module deleted or renamed in src leaves its
|
||||
// compiled twin behind. For most code that is merely stale, but the migrator
|
||||
// globs dist/db/migrations at boot: an orphan there is a live migration with no
|
||||
// source, and it runs. Same for dist/db/seeders. Nothing else writes into dist —
|
||||
// the client bundle goes to server/public and the runtime assets the Dockerfile
|
||||
// needs are copied in separately — so it is safe to drop wholesale.
|
||||
rmSync(distDir, { recursive: true, force: true });
|
||||
|
||||
let tscExitedNonZero = false;
|
||||
try {
|
||||
execSync('tsc -p tsconfig.build.json', { stdio: 'inherit' });
|
||||
execSync('tsc -p tsconfig.build.json', { stdio: 'inherit', cwd: packageRoot });
|
||||
} catch {
|
||||
tscExitedNonZero = true;
|
||||
}
|
||||
|
||||
// `noEmitOnError` is off, so tsc exits non-zero on type errors and still writes
|
||||
// dist — that case is deliberate, and gated by `npm run typecheck`. A tsc that
|
||||
// never ran at all (not on PATH, crashed) is indistinguishable from here, and
|
||||
// now that dist is emptied first it would leave nothing behind while still
|
||||
// reporting success. Checking for the entry point tells the two apart.
|
||||
if (!existsSync(join(distDir, 'index.js'))) {
|
||||
console.error('[build] tsc emitted nothing — dist is empty. Run via `npm run build` so tsc is on PATH.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (tscExitedNonZero) {
|
||||
console.warn('[build] tsc reported type errors — emitting anyway (gated by `npm run typecheck`).');
|
||||
}
|
||||
|
||||
|
||||
+26
-20
@@ -1,25 +1,28 @@
|
||||
import nodeHttp from 'node:http';
|
||||
import express from 'express';
|
||||
import type { Request, Response, NextFunction, RequestHandler } from 'express';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { ExpressAdapter } from '@nestjs/platform-express';
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import type { ConfigType } from '@nestjs/config';
|
||||
import { AppModule } from './nest/app.module';
|
||||
import { httpConfig } from './nest/app-config';
|
||||
import { attachOrm, runSchemaBootstrap } from './db/orm';
|
||||
import { applyGlobalMiddleware, routingCspOrigins } from './middleware/globalMiddleware';
|
||||
import { SettingsService } from './nest/settings/settings.service';
|
||||
import { applyPlatformUploads, applyPlatformStatic } from './nest/platform/platform.routes';
|
||||
import { httpConfig } from './nest/app-config';
|
||||
import { AppModule } from './nest/app.module';
|
||||
import { apiDocsEnabled } from './nest/common/api-docs.kill-switch';
|
||||
import { validateBodyContracts } from './nest/common/validate-body-contracts';
|
||||
import { validateManagedRoutes } from './nest/common/validate-managed-routes';
|
||||
import { validateRouteGuards } from './nest/common/validate-route-guards';
|
||||
import { setupApiDocs } from './nest/platform/api-docs';
|
||||
import { MCP_METADATA_MIDDLEWARE } from './nest/platform/mcp-metadata.middleware';
|
||||
import { validateBodyContracts } from './nest/common/validate-body-contracts';
|
||||
import { validateRouteGuards } from './nest/common/validate-route-guards';
|
||||
import { validateManagedRoutes } from './nest/common/validate-managed-routes';
|
||||
import { applyPlatformUploads, applyPlatformStatic } from './nest/platform/platform.routes';
|
||||
import { TrekWsAdapter } from './nest/realtime/trek-ws.adapter';
|
||||
import { SettingsService } from './nest/settings/settings.service';
|
||||
import { StorageService } from './nest/storage/storage.service';
|
||||
import { MikroORM } from '@mikro-orm/core';
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import type { ConfigType } from '@nestjs/config';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { ExpressAdapter } from '@nestjs/platform-express';
|
||||
import { MAX_COLLECTION_FILE_BYTES } from '@trek/shared';
|
||||
|
||||
import express from 'express';
|
||||
import type { Request, Response, NextFunction, RequestHandler } from 'express';
|
||||
import nodeHttp from 'node:http';
|
||||
|
||||
/**
|
||||
* Builds the unified TREK NestJS application that serves the ENTIRE surface — the
|
||||
* former Express app is gone. One builder is shared by the production bootstrap
|
||||
@@ -71,6 +74,13 @@ export async function buildApp(): Promise<INestApplication> {
|
||||
// route can verify a provider's HMAC signature over the exact payload (the
|
||||
// parsed JSON alone can't be re-serialised byte-for-byte).
|
||||
const app = await NestFactory.create(AppModule, new ExpressAdapter(), { rawBody: true });
|
||||
// Schema first, before ANY consumer reads it. `database.ts` only opens the
|
||||
// connection now; migrating and seeding is MikroORM's job and it is async, so
|
||||
// this is the earliest point it can happen. It has to stay above the
|
||||
// SettingsService resolution below, which is the boot's first DB read.
|
||||
const orm = app.get(MikroORM);
|
||||
attachOrm(orm);
|
||||
await runSchemaBootstrap(orm);
|
||||
const instance = app.getHttpAdapter().getInstance();
|
||||
// The http server is created HERE, not by the caller after buildApp returns,
|
||||
// and that ordering is the whole point: Nest binds gateways during app.init(),
|
||||
@@ -95,10 +105,7 @@ export async function buildApp(): Promise<INestApplication> {
|
||||
const asUrl = (value: unknown) => (typeof value === 'string' ? value : null);
|
||||
applyGlobalMiddleware(instance, {
|
||||
http,
|
||||
extraConnectSrc: routingCspOrigins([
|
||||
asUrl(defaults?.routing_base_url),
|
||||
asUrl(defaults?.valhalla_base_url),
|
||||
]),
|
||||
extraConnectSrc: routingCspOrigins([asUrl(defaults?.routing_base_url), asUrl(defaults?.valhalla_base_url)]),
|
||||
});
|
||||
// Same pre-init consumption bridge as httpConfig above: the StorageService
|
||||
// instance is resolvable before init, and the handlers only *register* here —
|
||||
@@ -159,8 +166,7 @@ export async function buildApp(): Promise<INestApplication> {
|
||||
const json = express.json({ limit: '100kb', verify: rawBodyKeeper });
|
||||
const urlencoded = express.urlencoded({ limit: '100kb', extended: true, verify: rawBodyKeeper });
|
||||
const isMcp = (req: Request) => req.path === '/mcp' || req.path === '/mcp/';
|
||||
const isBookWrite = (req: Request) =>
|
||||
req.method === 'PUT' && /^\/api\/journeys\/\d+\/book$/.test(req.path);
|
||||
const isBookWrite = (req: Request) => req.method === 'PUT' && /^\/api\/journeys\/\d+\/book$/.test(req.path);
|
||||
const isListFile = (req: Request) =>
|
||||
req.method === 'POST' && /^\/api\/addons\/collections\/(import|gpx\/read|\d+\/import)$/.test(req.path);
|
||||
|
||||
|
||||
+113
-53
@@ -1,45 +1,41 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { readEnv } from '../app-config';
|
||||
import { applyDurabilityPragmas } from './durability';
|
||||
import { createTables } from './schema';
|
||||
import { runMigrations } from './migrations';
|
||||
import { runSeeds } from './seeds';
|
||||
import { Place, Tag } from '../types';
|
||||
import { resolveDbPath } from './db-path';
|
||||
import { applyDurabilityPragmas } from './durability';
|
||||
import { readSchemaSnapshot } from './schema-snapshot';
|
||||
|
||||
import Database from 'better-sqlite3';
|
||||
|
||||
// In test mode each vitest worker gets an isolated in-memory DB so that
|
||||
// parallel forks can't race on the same file or share migration state.
|
||||
const isTest = readEnv().app.isTest;
|
||||
|
||||
let dbPath: string;
|
||||
if (isTest) {
|
||||
dbPath = ':memory:';
|
||||
} else if (readEnv().db.trekDbFile) {
|
||||
// Explicit DB file (used by the Playwright E2E harness to run against an
|
||||
// isolated, throwaway database instead of the real data/travel.db). Purely
|
||||
// additive — when unset the default path below is used exactly as before.
|
||||
dbPath = readEnv().db.trekDbFile!;
|
||||
const dir = path.dirname(dbPath);
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
} else {
|
||||
const dataDir = path.join(__dirname, '../../data');
|
||||
if (!fs.existsSync(dataDir)) {
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
}
|
||||
dbPath = path.join(dataDir, 'travel.db');
|
||||
}
|
||||
const dbPath = resolveDbPath();
|
||||
|
||||
let _db: Database.Database | null = null;
|
||||
|
||||
/**
|
||||
* Opens the connection and applies the pragmas. It deliberately does NOT build
|
||||
* the schema: migrations and seeding are MikroORM's job now and run from
|
||||
* `db/orm.ts` during `buildApp()`, because the migrator is async and this
|
||||
* function is called at module load.
|
||||
*
|
||||
* The one exception is test mode, where there is no `buildApp()` to hook into
|
||||
* for the handful of suites that use this singleton directly. Those open a copy
|
||||
* of a schema snapshot the vitest global setup migrated once for the whole run.
|
||||
*/
|
||||
function initDb(): void {
|
||||
if (_db) {
|
||||
try { _db.exec('PRAGMA wal_checkpoint(TRUNCATE)'); } catch (e) {}
|
||||
try { _db.close(); } catch (e) {}
|
||||
try {
|
||||
_db.exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
||||
} catch (e) {}
|
||||
try {
|
||||
_db.close();
|
||||
} catch (e) {}
|
||||
_db = null;
|
||||
}
|
||||
|
||||
_db = new Database(dbPath);
|
||||
const snapshot = isTest ? readSchemaSnapshot() : null;
|
||||
_db = snapshot ? new Database(snapshot) : new Database(dbPath);
|
||||
// Ahead of the journal switch now: changing journal_mode needs an exclusive
|
||||
// lock, which a sibling process (reset-admin, the rotation script) may hold.
|
||||
_db.exec('PRAGMA busy_timeout = 5000');
|
||||
@@ -50,15 +46,22 @@ function initDb(): void {
|
||||
if (dbPath !== ':memory:') {
|
||||
console.log(`[DB] journal_mode=${durability.journalMode}, synchronous=${durability.synchronous}`);
|
||||
}
|
||||
|
||||
createTables(_db);
|
||||
runMigrations(_db);
|
||||
|
||||
runSeeds(_db);
|
||||
}
|
||||
|
||||
initDb();
|
||||
|
||||
/**
|
||||
* The live better-sqlite3 handle, for the MikroORM dialect in `orm-driver.ts`.
|
||||
*
|
||||
* Deliberately not the `db` Proxy below: Kysely holds whatever it is given for
|
||||
* the life of its client, and a Proxy would hide the swap a restore performs.
|
||||
* The driver calls this on every connect so it always binds the current handle.
|
||||
*/
|
||||
function getRawConnection(): Database.Database {
|
||||
if (!_db) throw new Error('Database connection is not available (restore in progress?)');
|
||||
return _db;
|
||||
}
|
||||
|
||||
const db = new Proxy({} as Database.Database, {
|
||||
get(_, prop: string | symbol) {
|
||||
if (!_db) throw new Error('Database connection is not available (restore in progress?)');
|
||||
@@ -71,7 +74,16 @@ const db = new Proxy({} as Database.Database, {
|
||||
},
|
||||
});
|
||||
|
||||
if (readEnv().demo.enabled) {
|
||||
/**
|
||||
* Demo seeding, which used to run here at module load.
|
||||
*
|
||||
* It has to follow the schema now: `demo-seed.ts` inserts places against
|
||||
* hard-coded category ids 1-9, and with `foreign_keys = ON` those rows only
|
||||
* resolve once the category seeder has run. Called from the schema bootstrap in
|
||||
* `db/orm.ts`, immediately after the seeders.
|
||||
*/
|
||||
function runDemoSeed(): void {
|
||||
if (!readEnv().demo.enabled) return;
|
||||
try {
|
||||
const { seedDemoData } = require('../demo/demo-seed');
|
||||
seedDemoData(_db);
|
||||
@@ -80,19 +92,40 @@ if (readEnv().demo.enabled) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lets `db/orm.ts` rebuild the ORM's connection when this module swaps handles,
|
||||
* without this module importing the ORM (which imports this module back).
|
||||
*/
|
||||
let onReinitialize: (() => Promise<void>) | null = null;
|
||||
function registerReinitializeHook(hook: () => Promise<void>): void {
|
||||
onReinitialize = hook;
|
||||
}
|
||||
|
||||
function closeDb(): void {
|
||||
if (_db) {
|
||||
try { _db.exec('PRAGMA wal_checkpoint(TRUNCATE)'); } catch (e) {}
|
||||
try { _db.close(); } catch (e) {}
|
||||
try {
|
||||
_db.exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
||||
} catch (e) {}
|
||||
try {
|
||||
_db.close();
|
||||
} catch (e) {}
|
||||
_db = null;
|
||||
console.log('[DB] Database connection closed');
|
||||
}
|
||||
}
|
||||
|
||||
function reinitialize(): void {
|
||||
/**
|
||||
* Reopens the connection after a restore has swapped the file underneath us.
|
||||
*
|
||||
* Async now, because bringing a restored backup up to date means running the
|
||||
* migrations, and the hook also has to rebuild the ORM's Kysely client — it
|
||||
* cached the handle this function is about to replace.
|
||||
*/
|
||||
async function reinitialize(): Promise<void> {
|
||||
console.log('[DB] Reinitializing database connection after restore...');
|
||||
if (_db) closeDb();
|
||||
initDb();
|
||||
if (onReinitialize) await onReinitialize();
|
||||
console.log('[DB] Database reinitialized successfully');
|
||||
}
|
||||
|
||||
@@ -111,37 +144,51 @@ interface PlaceWithTags extends Place {
|
||||
}
|
||||
|
||||
function getPlaceWithTags(placeId: number | string): PlaceWithTags | null {
|
||||
const place = db.prepare(`
|
||||
const place = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT p.*, c.name as category_name, c.color as category_color, c.icon as category_icon
|
||||
FROM places p
|
||||
LEFT JOIN categories c ON p.category_id = c.id
|
||||
WHERE p.id = ?
|
||||
`).get(placeId) as PlaceWithCategory | undefined;
|
||||
`,
|
||||
)
|
||||
.get(placeId) as PlaceWithCategory | undefined;
|
||||
|
||||
if (!place) return null;
|
||||
|
||||
const tags = db.prepare(`
|
||||
const tags = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT t.* FROM tags t
|
||||
JOIN place_tags pt ON t.id = pt.tag_id
|
||||
WHERE pt.place_id = ?
|
||||
`).all(placeId) as Tag[];
|
||||
`,
|
||||
)
|
||||
.all(placeId) as Tag[];
|
||||
|
||||
// Collaborative ratings (#1435): every voter with username/avatar for the
|
||||
// who-voted tooltip; the displayed value is the average.
|
||||
const ratings = db.prepare(`
|
||||
const ratings = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT pr.user_id, u.username, u.avatar, pr.rating FROM place_ratings pr
|
||||
JOIN users u ON pr.user_id = u.id
|
||||
WHERE pr.place_id = ? ORDER BY pr.created_at
|
||||
`).all(placeId) as { user_id: number; username: string; avatar: string | null; rating: number }[];
|
||||
`,
|
||||
)
|
||||
.all(placeId) as { user_id: number; username: string; avatar: string | null; rating: number }[];
|
||||
|
||||
return {
|
||||
...place,
|
||||
category: place.category_id ? {
|
||||
id: place.category_id,
|
||||
name: place.category_name!,
|
||||
color: place.category_color!,
|
||||
icon: place.category_icon!,
|
||||
} : null,
|
||||
category: place.category_id
|
||||
? {
|
||||
id: place.category_id,
|
||||
name: place.category_name!,
|
||||
color: place.category_color!,
|
||||
icon: place.category_icon!,
|
||||
}
|
||||
: null,
|
||||
tags,
|
||||
ratings,
|
||||
rating_avg: ratings.length > 0 ? ratings.reduce((s, r) => s + r.rating, 0) / ratings.length : null,
|
||||
@@ -156,17 +203,30 @@ interface TripAccess {
|
||||
}
|
||||
|
||||
function canAccessTrip(tripId: number | string, userId: number): TripAccess | undefined {
|
||||
return db.prepare(`
|
||||
return db
|
||||
.prepare(
|
||||
`
|
||||
SELECT t.id, t.user_id, t.currency FROM trips t
|
||||
LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ?
|
||||
WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)
|
||||
`).get(userId, tripId, userId) as TripAccess | undefined;
|
||||
`,
|
||||
)
|
||||
.get(userId, tripId, userId) as TripAccess | undefined;
|
||||
}
|
||||
|
||||
function isOwner(tripId: number | string, userId: number): boolean {
|
||||
return !!db.prepare('SELECT id FROM trips WHERE id = ? AND user_id = ?').get(tripId, userId);
|
||||
}
|
||||
|
||||
|
||||
export { db, closeDb, reinitialize, getPlaceWithTags, canAccessTrip, isOwner };
|
||||
export {
|
||||
db,
|
||||
closeDb,
|
||||
reinitialize,
|
||||
getPlaceWithTags,
|
||||
canAccessTrip,
|
||||
isOwner,
|
||||
getRawConnection,
|
||||
registerReinitializeHook,
|
||||
runDemoSeed,
|
||||
};
|
||||
export type { TripAccess, PlaceWithTags };
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { readEnv } from '../app-config';
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* The SQLite file this process talks to.
|
||||
*
|
||||
* Shared by `database.ts` (which opens the connection) and `mikro-orm.config.ts`
|
||||
* (which needs the same target for the CLI and for the Nest-owned ORM). Kept in
|
||||
* one place because a config that names a different file than the connection is
|
||||
* the kind of mistake that only shows up as an empty database.
|
||||
*
|
||||
* Creates the containing directory as a side effect, exactly as the original
|
||||
* inline block in `database.ts` did.
|
||||
*/
|
||||
export function resolveDbPath(): string {
|
||||
// In test mode each vitest worker gets an isolated in-memory DB so that
|
||||
// parallel forks can't race on the same file or share migration state.
|
||||
if (readEnv().app.isTest) return ':memory:';
|
||||
|
||||
const explicit = readEnv().db.trekDbFile;
|
||||
if (explicit) {
|
||||
// Explicit DB file (used by the Playwright E2E harness to run against an
|
||||
// isolated, throwaway database instead of the real data/travel.db).
|
||||
const dir = path.dirname(explicit);
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
return explicit;
|
||||
}
|
||||
|
||||
const dataDir = path.join(__dirname, '../../data');
|
||||
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
|
||||
return path.join(dataDir, 'travel.db');
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import type Database from 'better-sqlite3';
|
||||
* scanning the addon shelf should find the product they actually run.
|
||||
*/
|
||||
|
||||
interface ProviderRow {
|
||||
export interface ProviderRow {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
@@ -26,7 +26,7 @@ interface ProviderRow {
|
||||
sort_order: number;
|
||||
}
|
||||
|
||||
interface ProviderFieldRow {
|
||||
export interface ProviderFieldRow {
|
||||
provider_id: string;
|
||||
field_key: string;
|
||||
label: string;
|
||||
@@ -38,7 +38,7 @@ interface ProviderFieldRow {
|
||||
sort_order: number;
|
||||
}
|
||||
|
||||
const PROVIDERS: ProviderRow[] = [
|
||||
export const DOCUMENT_PROVIDERS: ProviderRow[] = [
|
||||
{
|
||||
id: 'paperless',
|
||||
name: 'Paperless-ngx',
|
||||
@@ -82,56 +82,286 @@ const PROVIDERS: ProviderRow[] = [
|
||||
* instance with a self-signed certificate is common enough to need the switch
|
||||
* and not common enough to justify shipping it on.
|
||||
*/
|
||||
const FIELDS: ProviderFieldRow[] = [
|
||||
export const DOCUMENT_PROVIDER_FIELDS: ProviderFieldRow[] = [
|
||||
// Paperless-ngx: token auth, tag scope.
|
||||
{ provider_id: 'paperless', field_key: 'base_url', label: 'providerUrl', input_type: 'url', placeholder: 'https://paperless.example.com', hint: null, required: 1, secret: 0, sort_order: 0 },
|
||||
{ provider_id: 'paperless', field_key: 'api_token', label: 'providerApiToken', input_type: 'password', placeholder: 'API token', hint: 'hintPaperlessToken', required: 1, secret: 1, sort_order: 1 },
|
||||
{ provider_id: 'paperless', field_key: 'allow_insecure_tls', label: 'allowInsecureTls', input_type: 'checkbox', placeholder: null, hint: null, required: 0, secret: 0, sort_order: 2 },
|
||||
{
|
||||
provider_id: 'paperless',
|
||||
field_key: 'base_url',
|
||||
label: 'providerUrl',
|
||||
input_type: 'url',
|
||||
placeholder: 'https://paperless.example.com',
|
||||
hint: null,
|
||||
required: 1,
|
||||
secret: 0,
|
||||
sort_order: 0,
|
||||
},
|
||||
{
|
||||
provider_id: 'paperless',
|
||||
field_key: 'api_token',
|
||||
label: 'providerApiToken',
|
||||
input_type: 'password',
|
||||
placeholder: 'API token',
|
||||
hint: 'hintPaperlessToken',
|
||||
required: 1,
|
||||
secret: 1,
|
||||
sort_order: 1,
|
||||
},
|
||||
{
|
||||
provider_id: 'paperless',
|
||||
field_key: 'allow_insecure_tls',
|
||||
label: 'allowInsecureTls',
|
||||
input_type: 'checkbox',
|
||||
placeholder: null,
|
||||
hint: null,
|
||||
required: 0,
|
||||
secret: 0,
|
||||
sort_order: 2,
|
||||
},
|
||||
|
||||
// Papra: bearer key, and an organisation the key must already belong to.
|
||||
{ provider_id: 'papra', field_key: 'base_url', label: 'providerUrl', input_type: 'url', placeholder: 'https://papra.example.com', hint: null, required: 1, secret: 0, sort_order: 0 },
|
||||
{ provider_id: 'papra', field_key: 'api_key', label: 'providerApiKey', input_type: 'password', placeholder: 'ppapi_…', hint: 'hintPapraKey', required: 1, secret: 1, sort_order: 1 },
|
||||
{ provider_id: 'papra', field_key: 'organization_id', label: 'providerOrganization', input_type: 'text', placeholder: 'org_…', hint: 'hintPapraOrg', required: 1, secret: 0, sort_order: 2 },
|
||||
{ provider_id: 'papra', field_key: 'allow_insecure_tls', label: 'allowInsecureTls', input_type: 'checkbox', placeholder: null, hint: null, required: 0, secret: 0, sort_order: 3 },
|
||||
{
|
||||
provider_id: 'papra',
|
||||
field_key: 'base_url',
|
||||
label: 'providerUrl',
|
||||
input_type: 'url',
|
||||
placeholder: 'https://papra.example.com',
|
||||
hint: null,
|
||||
required: 1,
|
||||
secret: 0,
|
||||
sort_order: 0,
|
||||
},
|
||||
{
|
||||
provider_id: 'papra',
|
||||
field_key: 'api_key',
|
||||
label: 'providerApiKey',
|
||||
input_type: 'password',
|
||||
placeholder: 'ppapi_…',
|
||||
hint: 'hintPapraKey',
|
||||
required: 1,
|
||||
secret: 1,
|
||||
sort_order: 1,
|
||||
},
|
||||
{
|
||||
provider_id: 'papra',
|
||||
field_key: 'organization_id',
|
||||
label: 'providerOrganization',
|
||||
input_type: 'text',
|
||||
placeholder: 'org_…',
|
||||
hint: 'hintPapraOrg',
|
||||
required: 1,
|
||||
secret: 0,
|
||||
sort_order: 2,
|
||||
},
|
||||
{
|
||||
provider_id: 'papra',
|
||||
field_key: 'allow_insecure_tls',
|
||||
label: 'allowInsecureTls',
|
||||
input_type: 'checkbox',
|
||||
placeholder: null,
|
||||
hint: null,
|
||||
required: 0,
|
||||
secret: 0,
|
||||
sort_order: 3,
|
||||
},
|
||||
|
||||
// Nextcloud: an app password, never the account password. The latter breaks
|
||||
// the moment the account turns on 2FA or logs in through OIDC.
|
||||
{ provider_id: 'nextcloud', field_key: 'base_url', label: 'providerUrl', input_type: 'url', placeholder: 'https://cloud.example.com', hint: null, required: 1, secret: 0, sort_order: 0 },
|
||||
{ provider_id: 'nextcloud', field_key: 'login_name', label: 'providerUsername', input_type: 'text', placeholder: 'username', hint: 'hintNextcloudLogin', required: 1, secret: 0, sort_order: 1 },
|
||||
{ provider_id: 'nextcloud', field_key: 'app_password', label: 'providerAppPassword', input_type: 'password', placeholder: 'app password', hint: 'hintNextcloudAppPassword', required: 1, secret: 1, sort_order: 2 },
|
||||
{ provider_id: 'nextcloud', field_key: 'base_path', label: 'providerBasePath', input_type: 'text', placeholder: '/TREK', hint: 'hintBasePath', required: 0, secret: 0, sort_order: 3 },
|
||||
{ provider_id: 'nextcloud', field_key: 'allow_insecure_tls', label: 'allowInsecureTls', input_type: 'checkbox', placeholder: null, hint: null, required: 0, secret: 0, sort_order: 4 },
|
||||
{
|
||||
provider_id: 'nextcloud',
|
||||
field_key: 'base_url',
|
||||
label: 'providerUrl',
|
||||
input_type: 'url',
|
||||
placeholder: 'https://cloud.example.com',
|
||||
hint: null,
|
||||
required: 1,
|
||||
secret: 0,
|
||||
sort_order: 0,
|
||||
},
|
||||
{
|
||||
provider_id: 'nextcloud',
|
||||
field_key: 'login_name',
|
||||
label: 'providerUsername',
|
||||
input_type: 'text',
|
||||
placeholder: 'username',
|
||||
hint: 'hintNextcloudLogin',
|
||||
required: 1,
|
||||
secret: 0,
|
||||
sort_order: 1,
|
||||
},
|
||||
{
|
||||
provider_id: 'nextcloud',
|
||||
field_key: 'app_password',
|
||||
label: 'providerAppPassword',
|
||||
input_type: 'password',
|
||||
placeholder: 'app password',
|
||||
hint: 'hintNextcloudAppPassword',
|
||||
required: 1,
|
||||
secret: 1,
|
||||
sort_order: 2,
|
||||
},
|
||||
{
|
||||
provider_id: 'nextcloud',
|
||||
field_key: 'base_path',
|
||||
label: 'providerBasePath',
|
||||
input_type: 'text',
|
||||
placeholder: '/TREK',
|
||||
hint: 'hintBasePath',
|
||||
required: 0,
|
||||
secret: 0,
|
||||
sort_order: 3,
|
||||
},
|
||||
{
|
||||
provider_id: 'nextcloud',
|
||||
field_key: 'allow_insecure_tls',
|
||||
label: 'allowInsecureTls',
|
||||
input_type: 'checkbox',
|
||||
placeholder: null,
|
||||
hint: null,
|
||||
required: 0,
|
||||
secret: 0,
|
||||
sort_order: 4,
|
||||
},
|
||||
|
||||
// OpenCloud: auth-app token, spaces instead of folders.
|
||||
{ provider_id: 'opencloud', field_key: 'base_url', label: 'providerUrl', input_type: 'url', placeholder: 'https://opencloud.example.com', hint: null, required: 1, secret: 0, sort_order: 0 },
|
||||
{ provider_id: 'opencloud', field_key: 'username', label: 'providerUsername', input_type: 'text', placeholder: 'username', hint: null, required: 1, secret: 0, sort_order: 1 },
|
||||
{ provider_id: 'opencloud', field_key: 'app_token', label: 'providerAppToken', input_type: 'password', placeholder: 'app token', hint: 'hintOpenCloudToken', required: 1, secret: 1, sort_order: 2 },
|
||||
{ provider_id: 'opencloud', field_key: 'allow_insecure_tls', label: 'allowInsecureTls', input_type: 'checkbox', placeholder: null, hint: null, required: 0, secret: 0, sort_order: 3 },
|
||||
{
|
||||
provider_id: 'opencloud',
|
||||
field_key: 'base_url',
|
||||
label: 'providerUrl',
|
||||
input_type: 'url',
|
||||
placeholder: 'https://opencloud.example.com',
|
||||
hint: null,
|
||||
required: 1,
|
||||
secret: 0,
|
||||
sort_order: 0,
|
||||
},
|
||||
{
|
||||
provider_id: 'opencloud',
|
||||
field_key: 'username',
|
||||
label: 'providerUsername',
|
||||
input_type: 'text',
|
||||
placeholder: 'username',
|
||||
hint: null,
|
||||
required: 1,
|
||||
secret: 0,
|
||||
sort_order: 1,
|
||||
},
|
||||
{
|
||||
provider_id: 'opencloud',
|
||||
field_key: 'app_token',
|
||||
label: 'providerAppToken',
|
||||
input_type: 'password',
|
||||
placeholder: 'app token',
|
||||
hint: 'hintOpenCloudToken',
|
||||
required: 1,
|
||||
secret: 1,
|
||||
sort_order: 2,
|
||||
},
|
||||
{
|
||||
provider_id: 'opencloud',
|
||||
field_key: 'allow_insecure_tls',
|
||||
label: 'allowInsecureTls',
|
||||
input_type: 'checkbox',
|
||||
placeholder: null,
|
||||
hint: null,
|
||||
required: 0,
|
||||
secret: 0,
|
||||
sort_order: 3,
|
||||
},
|
||||
|
||||
// Synology: DSM has no app tokens at all, so this is a real account. The OTP
|
||||
// is used once to obtain a device token, which is what gets stored.
|
||||
{ provider_id: 'synologydrive', field_key: 'base_url', label: 'providerUrl', input_type: 'url', placeholder: 'https://nas.example.com:5001', hint: 'hintSynologyUrl', required: 1, secret: 0, sort_order: 0 },
|
||||
{ provider_id: 'synologydrive', field_key: 'username', label: 'providerUsername', input_type: 'text', placeholder: 'username', hint: 'hintSynologyUser', required: 1, secret: 0, sort_order: 1 },
|
||||
{ provider_id: 'synologydrive', field_key: 'password', label: 'providerPassword', input_type: 'password', placeholder: 'password', hint: null, required: 1, secret: 1, sort_order: 2 },
|
||||
{ provider_id: 'synologydrive', field_key: 'otp_code', label: 'providerOTP', input_type: 'text', placeholder: '123456', hint: 'hintSynologyOtp', required: 0, secret: 1, sort_order: 3 },
|
||||
{ provider_id: 'synologydrive', field_key: 'base_path', label: 'providerBasePath', input_type: 'text', placeholder: '/trek', hint: 'hintBasePath', required: 0, secret: 0, sort_order: 4 },
|
||||
{ provider_id: 'synologydrive', field_key: 'allow_insecure_tls', label: 'allowInsecureTls', input_type: 'checkbox', placeholder: null, hint: null, required: 0, secret: 0, sort_order: 5 },
|
||||
{
|
||||
provider_id: 'synologydrive',
|
||||
field_key: 'base_url',
|
||||
label: 'providerUrl',
|
||||
input_type: 'url',
|
||||
placeholder: 'https://nas.example.com:5001',
|
||||
hint: 'hintSynologyUrl',
|
||||
required: 1,
|
||||
secret: 0,
|
||||
sort_order: 0,
|
||||
},
|
||||
{
|
||||
provider_id: 'synologydrive',
|
||||
field_key: 'username',
|
||||
label: 'providerUsername',
|
||||
input_type: 'text',
|
||||
placeholder: 'username',
|
||||
hint: 'hintSynologyUser',
|
||||
required: 1,
|
||||
secret: 0,
|
||||
sort_order: 1,
|
||||
},
|
||||
{
|
||||
provider_id: 'synologydrive',
|
||||
field_key: 'password',
|
||||
label: 'providerPassword',
|
||||
input_type: 'password',
|
||||
placeholder: 'password',
|
||||
hint: null,
|
||||
required: 1,
|
||||
secret: 1,
|
||||
sort_order: 2,
|
||||
},
|
||||
{
|
||||
provider_id: 'synologydrive',
|
||||
field_key: 'otp_code',
|
||||
label: 'providerOTP',
|
||||
input_type: 'text',
|
||||
placeholder: '123456',
|
||||
hint: 'hintSynologyOtp',
|
||||
required: 0,
|
||||
secret: 1,
|
||||
sort_order: 3,
|
||||
},
|
||||
{
|
||||
provider_id: 'synologydrive',
|
||||
field_key: 'base_path',
|
||||
label: 'providerBasePath',
|
||||
input_type: 'text',
|
||||
placeholder: '/trek',
|
||||
hint: 'hintBasePath',
|
||||
required: 0,
|
||||
secret: 0,
|
||||
sort_order: 4,
|
||||
},
|
||||
{
|
||||
provider_id: 'synologydrive',
|
||||
field_key: 'allow_insecure_tls',
|
||||
label: 'allowInsecureTls',
|
||||
input_type: 'checkbox',
|
||||
placeholder: null,
|
||||
hint: null,
|
||||
required: 0,
|
||||
secret: 0,
|
||||
sort_order: 5,
|
||||
},
|
||||
];
|
||||
|
||||
export function seedDocumentProviders(db: Database.Database): void {
|
||||
const insertProvider = db.prepare(
|
||||
'INSERT OR IGNORE INTO document_providers (id, name, description, icon, enabled, sort_order) VALUES (?, ?, ?, ?, 0, ?)',
|
||||
);
|
||||
for (const p of PROVIDERS) insertProvider.run(p.id, p.name, p.description, p.icon, p.sort_order);
|
||||
for (const p of DOCUMENT_PROVIDERS) insertProvider.run(p.id, p.name, p.description, p.icon, p.sort_order);
|
||||
|
||||
const insertField = db.prepare(
|
||||
`INSERT OR IGNORE INTO document_provider_fields
|
||||
(provider_id, field_key, label, input_type, placeholder, hint, required, secret, sort_order)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
);
|
||||
for (const f of FIELDS) {
|
||||
insertField.run(f.provider_id, f.field_key, f.label, f.input_type, f.placeholder, f.hint, f.required, f.secret, f.sort_order);
|
||||
for (const f of DOCUMENT_PROVIDER_FIELDS) {
|
||||
insertField.run(
|
||||
f.provider_id,
|
||||
f.field_key,
|
||||
f.label,
|
||||
f.input_type,
|
||||
f.placeholder,
|
||||
f.hint,
|
||||
f.required,
|
||||
f.secret,
|
||||
f.sort_order,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const DOCUMENT_PROVIDER_SEED_IDS = PROVIDERS.map((p) => p.id);
|
||||
export const DOCUMENT_PROVIDER_SEED_IDS = DOCUMENT_PROVIDERS.map((p) => p.id);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Collection, type Ref, defineEntity, p, EntityRepository } from '@mikro-orm/core';
|
||||
import { CollabMessageReactions } from './CollabMessageReactions.entity';
|
||||
import { TripFiles } from './TripFiles.entity.entity';
|
||||
import { TripFiles } from './TripFiles.entity';
|
||||
import { Trips } from './Trips.entity';
|
||||
import { Users } from './Users.entity';
|
||||
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
import { PrimaryKeyProp, type Ref, defineEntity, p, EntityRepository } from '@mikro-orm/core';
|
||||
import { DayAssignments } from './DayAssignments.entity';
|
||||
import { Trips } from './Trips.entity';
|
||||
import { PrimaryKeyProp, type Ref, defineEntity, p, EntityRepository } from '@mikro-orm/core';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export class RoadtripDayBoundaries {
|
||||
[PrimaryKeyProp]?: ['trip', 'dayNumber'];
|
||||
@@ -8,7 +24,7 @@ export class RoadtripDayBoundaries {
|
||||
dayNumber!: number;
|
||||
fromAssignment!: Ref<DayAssignments>;
|
||||
toAssignment?: Ref<DayAssignments> | null;
|
||||
fraction!: unknown;
|
||||
fraction!: number; // was `unknown`
|
||||
}
|
||||
|
||||
export class RoadtripDayBoundariesRepository extends EntityRepository<RoadtripDayBoundaries> {}
|
||||
@@ -19,7 +35,7 @@ export const RoadtripDayBoundariesSchema = defineEntity({
|
||||
checks: [
|
||||
{
|
||||
name: 'roadtrip_day_boundaries_fraction_check',
|
||||
expression: 'fraction BETWEEN 0 AND 1), PRIMARY KEY (trip_id, day_number',
|
||||
expression: 'fraction BETWEEN 0 AND 1',
|
||||
},
|
||||
{
|
||||
name: 'roadtrip_day_boundaries_day_number_check',
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import type { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Helpers shared by the migrations that replay the pre-MikroORM history.
|
||||
*
|
||||
* They live outside `db/migrations/` on purpose: the migrator globs that
|
||||
* directory with `!(*.d).{js,ts,cjs}` and would try to load a helper module as
|
||||
* a migration class.
|
||||
*/
|
||||
|
||||
/** A single `pragma_table_info` row — only the column name is ever read. */
|
||||
interface ColumnRow {
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a column only when the table does not already have it.
|
||||
*
|
||||
* The legacy bootstrap ran `schema.ts::createTables()` before the numbered
|
||||
* migrations, and that file was a *moving* snapshot of the schema rather than a
|
||||
* frozen v0 — so a database created late already had columns that early steps
|
||||
* add here. The old runner absorbed the clash by catching "duplicate column
|
||||
* name"; this checks first instead, so a genuine failure still propagates.
|
||||
*
|
||||
* `definition` is the full column spec, e.g. `status TEXT DEFAULT 'pending'`.
|
||||
*/
|
||||
export async function addColumnIfMissing(
|
||||
migration: Migration,
|
||||
table: string,
|
||||
column: string,
|
||||
definition: string,
|
||||
): Promise<void> {
|
||||
const existing = (await migration.execute('select name from pragma_table_info(?) where name = ?', [
|
||||
table,
|
||||
column,
|
||||
])) as ColumnRow[];
|
||||
if (existing.length > 0) return;
|
||||
await migration.execute(`alter table \`${table}\` add column ${definition}`);
|
||||
}
|
||||
|
||||
/** True when `table` exists in the current database. */
|
||||
export async function tableExists(migration: Migration, table: string): Promise<boolean> {
|
||||
const rows = await migration.execute(`select name from sqlite_master where type = 'table' and name = ?`, [table]);
|
||||
return rows.length > 0;
|
||||
}
|
||||
|
||||
/** The `CREATE TABLE` DDL SQLite stored for `table`, or null when it does not exist. */
|
||||
export async function tableSql(migration: Migration, table: string): Promise<string | null> {
|
||||
const rows = (await migration.execute(`select sql from sqlite_master where name = ?`, [table])) as {
|
||||
sql: string | null;
|
||||
}[];
|
||||
return rows[0]?.sql ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a statement whose failure the legacy step logged and swallowed.
|
||||
*
|
||||
* Kept as its own helper so "this step is allowed to fail" stays visible in the
|
||||
* generated migrations instead of hiding behind a bare try/catch.
|
||||
*/
|
||||
export async function execBestEffort(migration: Migration, sql: string, params?: unknown[]): Promise<void> {
|
||||
try {
|
||||
await migration.execute(sql, params);
|
||||
} catch (err: unknown) {
|
||||
console.warn('[migrations] non-fatal step failed:', err instanceof Error ? err.message : err);
|
||||
}
|
||||
}
|
||||
|
||||
/** The column names of `table`, or an empty set when the table does not exist. */
|
||||
export async function columnNames(migration: Migration, table: string): Promise<Set<string>> {
|
||||
const rows = (await migration.execute('select name from pragma_table_info(?)', [table])) as ColumnRow[];
|
||||
return new Set(rows.map((r) => r.name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a statement that the legacy step allowed to fail *only* because the table
|
||||
* it touches may not exist yet on that database. Any other error still throws.
|
||||
*/
|
||||
export async function execUnlessTableMissing(migration: Migration, sql: string, params?: unknown[]): Promise<void> {
|
||||
try {
|
||||
await migration.execute(sql, params);
|
||||
} catch (err: unknown) {
|
||||
if (!(err instanceof Error) || !err.message.includes('no such table')) throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a group of statements the legacy step wrapped in a single swallowing
|
||||
* try/catch. A failure part-way through leaves the earlier statements applied,
|
||||
* which is exactly what the original did.
|
||||
*/
|
||||
export async function bestEffort(run: () => Promise<void>): Promise<void> {
|
||||
try {
|
||||
await run();
|
||||
} catch (err: unknown) {
|
||||
console.warn('[migrations] non-fatal step failed:', err instanceof Error ? err.message : err);
|
||||
}
|
||||
}
|
||||
|
||||
/** A booked stay whose check-in day has no matching stop on the plan. */
|
||||
interface UnplacedStay {
|
||||
id: number;
|
||||
place_id: number;
|
||||
start_day_id: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives every booked stay the day stop it would get today, and returns how many
|
||||
* were placed.
|
||||
*
|
||||
* Road-trip mode builds its drive out of `day_assignments` alone, so a hotel
|
||||
* booked in Days mode was invisible there. Two legacy steps run this same sweep:
|
||||
* once at the release, and once more afterwards for the bookings written in the
|
||||
* seconds while the old container was still answering. The `NOT EXISTS` decides
|
||||
* both times, so repeating it is safe — a stay that already has its stop is
|
||||
* passed over, and a place the traveller planned by hand keeps its row unclaimed.
|
||||
*/
|
||||
export async function attachStayStopsToCheckInDay(migration: Migration): Promise<number> {
|
||||
const stays = (await migration.execute(`
|
||||
SELECT a.id, a.place_id, a.start_day_id
|
||||
FROM day_accommodations a
|
||||
WHERE a.place_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM day_assignments da
|
||||
WHERE da.day_id = a.start_day_id AND da.place_id = a.place_id
|
||||
)
|
||||
ORDER BY a.id
|
||||
`)) as UnplacedStay[];
|
||||
|
||||
for (const stay of stays) {
|
||||
// Arriving somewhere is what the day was for, so the stop goes last — the
|
||||
// same position a stay booked today lands in.
|
||||
await migration.execute(
|
||||
`INSERT INTO day_assignments (day_id, place_id, order_index, accommodation_id)
|
||||
VALUES (?, ?, COALESCE((SELECT MAX(order_index) + 1 FROM day_assignments WHERE day_id = ?), 0), ?)`,
|
||||
[stay.start_day_id, stay.place_id, stay.start_day_id, stay.id],
|
||||
);
|
||||
await migration.execute(
|
||||
`UPDATE places SET stop_type = 'hotel' WHERE id = ? AND (stop_type IS NULL OR stop_type = '')`,
|
||||
[stay.place_id],
|
||||
);
|
||||
}
|
||||
|
||||
return stays.length;
|
||||
}
|
||||
@@ -0,0 +1,774 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Baseline schema — every table `db/schema.ts::createTables()` created on boot,
|
||||
* before the numbered steps in `db/migrations.ts` ran.
|
||||
*
|
||||
* This is not a frozen "v0": `createTables()` was edited as features landed, so it
|
||||
* is a snapshot of the schema as it stood when the ORM migration took place. The
|
||||
* `IF NOT EXISTS` guards are kept verbatim, and the numbered steps that follow
|
||||
* replay on top of it — each one guarded so a column this file already declares
|
||||
* is not added twice.
|
||||
*
|
||||
* The trailing `schema_version` table is the legacy runner's own bookkeeping,
|
||||
* which it created before running step 1. It is left empty here: the migration
|
||||
* state now lives in the migrator's table, and nothing in the app reads this
|
||||
* one. It is kept because step 129 rebuilds it and it is still an entity.
|
||||
*/
|
||||
export class Migration20200101000000_baseline_schema extends Migration {
|
||||
override name = 'Migration20200101000000_baseline_schema';
|
||||
|
||||
override up(): void {
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'user',
|
||||
maps_api_key TEXT,
|
||||
unsplash_api_key TEXT,
|
||||
amap_api_key TEXT,
|
||||
openweather_api_key TEXT,
|
||||
avatar TEXT,
|
||||
oidc_sub TEXT,
|
||||
oidc_issuer TEXT,
|
||||
last_login DATETIME,
|
||||
mfa_enabled INTEGER DEFAULT 0,
|
||||
mfa_secret TEXT,
|
||||
mfa_backup_codes TEXT,
|
||||
immich_url TEXT,
|
||||
immich_access_token TEXT,
|
||||
synology_url TEXT,
|
||||
synology_username TEXT,
|
||||
synology_password TEXT,
|
||||
synology_sid TEXT,
|
||||
must_change_password INTEGER DEFAULT 0,
|
||||
password_version INTEGER NOT NULL DEFAULT 0,
|
||||
feed_token TEXT,
|
||||
is_guest INTEGER NOT NULL DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS password_reset_tokens (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
token_hash TEXT NOT NULL UNIQUE,
|
||||
expires_at DATETIME NOT NULL,
|
||||
consumed_at DATETIME,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
created_ip TEXT
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS idx_prt_user ON password_reset_tokens(user_id)`);
|
||||
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS idx_prt_hash ON password_reset_tokens(token_hash)`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS webauthn_credentials (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
credential_id TEXT NOT NULL UNIQUE,
|
||||
public_key BLOB NOT NULL,
|
||||
counter INTEGER NOT NULL DEFAULT 0,
|
||||
transports TEXT,
|
||||
device_type TEXT,
|
||||
backed_up INTEGER NOT NULL DEFAULT 0,
|
||||
name TEXT,
|
||||
aaguid TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
last_used_at DATETIME
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS idx_webauthn_credentials_user ON webauthn_credentials(user_id)`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS webauthn_challenges (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
challenge TEXT NOT NULL UNIQUE,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||
type TEXT NOT NULL,
|
||||
expires_at INTEGER NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS idx_webauthn_challenges_expires ON webauthn_challenges(expires_at)`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
key TEXT NOT NULL,
|
||||
value TEXT,
|
||||
UNIQUE(user_id, key)
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS trips (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
start_date TEXT,
|
||||
end_date TEXT,
|
||||
currency TEXT DEFAULT 'EUR',
|
||||
cover_image TEXT,
|
||||
is_archived INTEGER DEFAULT 0,
|
||||
reminder_days INTEGER DEFAULT 3,
|
||||
feed_token TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS days (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
trip_id INTEGER NOT NULL REFERENCES trips(id) ON DELETE CASCADE,
|
||||
day_number INTEGER NOT NULL,
|
||||
date TEXT,
|
||||
notes TEXT,
|
||||
title TEXT,
|
||||
UNIQUE(trip_id, day_number)
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS categories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
color TEXT DEFAULT '#6366f1',
|
||||
icon TEXT DEFAULT '📍',
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS tags (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
color TEXT DEFAULT '#10b981',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS places (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
trip_id INTEGER NOT NULL REFERENCES trips(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
lat REAL,
|
||||
lng REAL,
|
||||
address TEXT,
|
||||
category_id INTEGER REFERENCES categories(id) ON DELETE SET NULL,
|
||||
price REAL,
|
||||
currency TEXT,
|
||||
reservation_status TEXT DEFAULT 'none',
|
||||
reservation_notes TEXT,
|
||||
reservation_datetime TEXT,
|
||||
place_time TEXT,
|
||||
end_time TEXT,
|
||||
duration_minutes INTEGER DEFAULT 60,
|
||||
notes TEXT,
|
||||
image_url TEXT,
|
||||
google_place_id TEXT,
|
||||
google_ftid TEXT,
|
||||
website TEXT,
|
||||
phone TEXT,
|
||||
transport_mode TEXT DEFAULT 'walking',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS place_tags (
|
||||
place_id INTEGER NOT NULL REFERENCES places(id) ON DELETE CASCADE,
|
||||
tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (place_id, tag_id)
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS place_ratings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
place_id INTEGER NOT NULL REFERENCES places(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
rating INTEGER NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(place_id, user_id)
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS day_assignments (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
day_id INTEGER NOT NULL REFERENCES days(id) ON DELETE CASCADE,
|
||||
place_id INTEGER NOT NULL REFERENCES places(id) ON DELETE CASCADE,
|
||||
order_index INTEGER DEFAULT 0,
|
||||
notes TEXT,
|
||||
reservation_status TEXT DEFAULT 'none',
|
||||
reservation_notes TEXT,
|
||||
reservation_datetime TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS packing_items (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
trip_id INTEGER NOT NULL REFERENCES trips(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
checked INTEGER DEFAULT 0,
|
||||
category TEXT,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS photos (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
trip_id INTEGER NOT NULL REFERENCES trips(id) ON DELETE CASCADE,
|
||||
day_id INTEGER REFERENCES days(id) ON DELETE SET NULL,
|
||||
place_id INTEGER REFERENCES places(id) ON DELETE SET NULL,
|
||||
filename TEXT NOT NULL,
|
||||
original_name TEXT NOT NULL,
|
||||
file_size INTEGER,
|
||||
mime_type TEXT,
|
||||
caption TEXT,
|
||||
taken_at TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS trip_files (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
trip_id INTEGER NOT NULL REFERENCES trips(id) ON DELETE CASCADE,
|
||||
place_id INTEGER REFERENCES places(id) ON DELETE SET NULL,
|
||||
reservation_id INTEGER REFERENCES reservations(id) ON DELETE SET NULL,
|
||||
filename TEXT NOT NULL,
|
||||
original_name TEXT NOT NULL,
|
||||
file_size INTEGER,
|
||||
mime_type TEXT,
|
||||
description TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS reservations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
trip_id INTEGER NOT NULL REFERENCES trips(id) ON DELETE CASCADE,
|
||||
day_id INTEGER REFERENCES days(id) ON DELETE SET NULL,
|
||||
end_day_id INTEGER REFERENCES days(id) ON DELETE SET NULL,
|
||||
place_id INTEGER REFERENCES places(id) ON DELETE SET NULL,
|
||||
assignment_id INTEGER REFERENCES day_assignments(id) ON DELETE SET NULL,
|
||||
title TEXT NOT NULL,
|
||||
accommodation_id TEXT,
|
||||
reservation_time TEXT,
|
||||
reservation_end_time TEXT,
|
||||
location TEXT,
|
||||
confirmation_number TEXT,
|
||||
notes TEXT,
|
||||
status TEXT DEFAULT 'pending',
|
||||
type TEXT DEFAULT 'other',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS trip_members (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
trip_id INTEGER NOT NULL REFERENCES trips(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
invited_by INTEGER REFERENCES users(id),
|
||||
added_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(trip_id, user_id)
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS day_notes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
day_id INTEGER NOT NULL REFERENCES days(id) ON DELETE CASCADE,
|
||||
trip_id INTEGER NOT NULL REFERENCES trips(id) ON DELETE CASCADE,
|
||||
text TEXT NOT NULL,
|
||||
time TEXT,
|
||||
icon TEXT DEFAULT '📝',
|
||||
sort_order REAL DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS app_settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS budget_items (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
trip_id INTEGER NOT NULL REFERENCES trips(id) ON DELETE CASCADE,
|
||||
category TEXT NOT NULL DEFAULT 'Other',
|
||||
name TEXT NOT NULL,
|
||||
total_price REAL NOT NULL DEFAULT 0,
|
||||
persons INTEGER DEFAULT NULL,
|
||||
days INTEGER DEFAULT NULL,
|
||||
note TEXT,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`
|
||||
-- Addon system
|
||||
CREATE TABLE IF NOT EXISTS addons (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
type TEXT NOT NULL DEFAULT 'global',
|
||||
icon TEXT DEFAULT 'Puzzle',
|
||||
enabled INTEGER DEFAULT 0,
|
||||
config TEXT DEFAULT '{}',
|
||||
sort_order INTEGER DEFAULT 0
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS photo_providers (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
icon TEXT DEFAULT 'Image',
|
||||
enabled INTEGER DEFAULT 0,
|
||||
sort_order INTEGER DEFAULT 0
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS photo_provider_fields (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
provider_id TEXT NOT NULL REFERENCES photo_providers(id) ON DELETE CASCADE,
|
||||
field_key TEXT NOT NULL,
|
||||
label TEXT NOT NULL,
|
||||
input_type TEXT NOT NULL DEFAULT 'text',
|
||||
placeholder TEXT,
|
||||
hint TEXT,
|
||||
required INTEGER DEFAULT 0,
|
||||
secret INTEGER DEFAULT 0,
|
||||
settings_key TEXT,
|
||||
payload_key TEXT,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
UNIQUE(provider_id, field_key)
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`
|
||||
-- Vacay addon tables
|
||||
CREATE TABLE IF NOT EXISTS vacay_plans (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
owner_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
block_weekends INTEGER DEFAULT 1,
|
||||
holidays_enabled INTEGER DEFAULT 0,
|
||||
holidays_region TEXT DEFAULT '',
|
||||
school_holidays_enabled INTEGER DEFAULT 0,
|
||||
company_holidays_enabled INTEGER DEFAULT 1,
|
||||
carry_over_enabled INTEGER DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(owner_id)
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS vacay_plan_members (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
plan_id INTEGER NOT NULL REFERENCES vacay_plans(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
status TEXT DEFAULT 'pending',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(plan_id, user_id)
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS vacay_user_colors (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
plan_id INTEGER NOT NULL REFERENCES vacay_plans(id) ON DELETE CASCADE,
|
||||
color TEXT DEFAULT '#6366f1',
|
||||
UNIQUE(user_id, plan_id)
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS vacay_years (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
plan_id INTEGER NOT NULL REFERENCES vacay_plans(id) ON DELETE CASCADE,
|
||||
year INTEGER NOT NULL,
|
||||
UNIQUE(plan_id, year)
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS vacay_user_years (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
plan_id INTEGER NOT NULL REFERENCES vacay_plans(id) ON DELETE CASCADE,
|
||||
year INTEGER NOT NULL,
|
||||
vacation_days INTEGER DEFAULT 30,
|
||||
carried_over INTEGER DEFAULT 0,
|
||||
UNIQUE(user_id, plan_id, year)
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS vacay_entries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
plan_id INTEGER NOT NULL REFERENCES vacay_plans(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
date TEXT NOT NULL,
|
||||
note TEXT DEFAULT '',
|
||||
UNIQUE(user_id, plan_id, date)
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS vacay_company_holidays (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
plan_id INTEGER NOT NULL REFERENCES vacay_plans(id) ON DELETE CASCADE,
|
||||
date TEXT NOT NULL,
|
||||
note TEXT DEFAULT '',
|
||||
UNIQUE(plan_id, date)
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS vacay_holiday_calendars (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
plan_id INTEGER NOT NULL REFERENCES vacay_plans(id) ON DELETE CASCADE,
|
||||
type TEXT NOT NULL DEFAULT 'public_holiday',
|
||||
region TEXT NOT NULL,
|
||||
label TEXT,
|
||||
color TEXT NOT NULL DEFAULT '#fecaca',
|
||||
sort_order INTEGER NOT NULL DEFAULT 0
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`
|
||||
-- Collections addon tables
|
||||
CREATE TABLE IF NOT EXISTS collections (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
owner_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
color TEXT DEFAULT '#6366f1',
|
||||
icon TEXT DEFAULT 'Bookmark',
|
||||
cover_image TEXT,
|
||||
links TEXT,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS collection_members (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
collection_id INTEGER NOT NULL REFERENCES collections(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
role TEXT NOT NULL DEFAULT 'editor',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(collection_id, user_id)
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS collection_places (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
collection_id INTEGER NOT NULL REFERENCES collections(id) ON DELETE CASCADE,
|
||||
owner_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
saved_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
lat REAL,
|
||||
lng REAL,
|
||||
address TEXT,
|
||||
category_id INTEGER REFERENCES categories(id) ON DELETE SET NULL,
|
||||
price REAL,
|
||||
currency TEXT,
|
||||
notes TEXT,
|
||||
image_url TEXT,
|
||||
google_place_id TEXT,
|
||||
google_ftid TEXT,
|
||||
osm_id TEXT,
|
||||
website TEXT,
|
||||
phone TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'idea',
|
||||
source_trip_id INTEGER,
|
||||
source_place_id INTEGER,
|
||||
links TEXT,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS collection_place_ratings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
collection_place_id INTEGER NOT NULL REFERENCES collection_places(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
rating INTEGER NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(collection_place_id, user_id)
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS collection_place_tags (
|
||||
collection_place_id INTEGER NOT NULL REFERENCES collection_places(id) ON DELETE CASCADE,
|
||||
tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (collection_place_id, tag_id)
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS idx_collection_places_collection ON collection_places(collection_id)`);
|
||||
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS idx_collection_members_user ON collection_members(user_id)`);
|
||||
|
||||
this.addSql(
|
||||
`CREATE INDEX IF NOT EXISTS idx_collection_place_tags_place ON collection_place_tags(collection_place_id)`,
|
||||
);
|
||||
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS idx_collection_place_tags_tag ON collection_place_tags(tag_id)`);
|
||||
|
||||
this.addSql(`
|
||||
-- Per-collection custom labels (distinct from the instance-wide tags table):
|
||||
-- each list defines its own labels and every place can carry several.
|
||||
CREATE TABLE IF NOT EXISTS collection_labels (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
collection_id INTEGER NOT NULL REFERENCES collections(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
color TEXT DEFAULT '#6366f1',
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS collection_place_labels (
|
||||
collection_place_id INTEGER NOT NULL REFERENCES collection_places(id) ON DELETE CASCADE,
|
||||
label_id INTEGER NOT NULL REFERENCES collection_labels(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (collection_place_id, label_id)
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS idx_collection_labels_collection ON collection_labels(collection_id)`);
|
||||
|
||||
this.addSql(
|
||||
`CREATE INDEX IF NOT EXISTS idx_collection_place_labels_place ON collection_place_labels(collection_place_id)`,
|
||||
);
|
||||
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS idx_collection_place_labels_label ON collection_place_labels(label_id)`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS day_accommodations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
trip_id INTEGER NOT NULL REFERENCES trips(id) ON DELETE CASCADE,
|
||||
place_id INTEGER REFERENCES places(id) ON DELETE SET NULL,
|
||||
start_day_id INTEGER NOT NULL REFERENCES days(id) ON DELETE CASCADE,
|
||||
end_day_id INTEGER NOT NULL REFERENCES days(id) ON DELETE CASCADE,
|
||||
check_in TEXT,
|
||||
check_in_end TEXT,
|
||||
check_out TEXT,
|
||||
confirmation TEXT,
|
||||
notes TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`
|
||||
-- Collab addon tables
|
||||
CREATE TABLE IF NOT EXISTS collab_notes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
trip_id INTEGER NOT NULL REFERENCES trips(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
category TEXT DEFAULT 'General',
|
||||
title TEXT NOT NULL,
|
||||
content TEXT,
|
||||
color TEXT DEFAULT '#6366f1',
|
||||
pinned INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS collab_polls (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
trip_id INTEGER NOT NULL REFERENCES trips(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
question TEXT NOT NULL,
|
||||
options TEXT NOT NULL,
|
||||
multiple INTEGER DEFAULT 0,
|
||||
closed INTEGER DEFAULT 0,
|
||||
deadline TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS collab_poll_votes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
poll_id INTEGER NOT NULL REFERENCES collab_polls(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
option_index INTEGER NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(poll_id, user_id, option_index)
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS collab_messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
trip_id INTEGER NOT NULL REFERENCES trips(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
text TEXT NOT NULL,
|
||||
reply_to INTEGER REFERENCES collab_messages(id) ON DELETE SET NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS idx_collab_notes_trip ON collab_notes(trip_id)`);
|
||||
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS idx_collab_polls_trip ON collab_polls(trip_id)`);
|
||||
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS idx_collab_messages_trip ON collab_messages(trip_id)`);
|
||||
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS idx_places_trip_id ON places(trip_id)`);
|
||||
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS idx_places_category_id ON places(category_id)`);
|
||||
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS idx_days_trip_id ON days(trip_id)`);
|
||||
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS idx_day_assignments_day_id ON day_assignments(day_id)`);
|
||||
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS idx_day_assignments_place_id ON day_assignments(place_id)`);
|
||||
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS idx_place_tags_place_id ON place_tags(place_id)`);
|
||||
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS idx_place_tags_tag_id ON place_tags(tag_id)`);
|
||||
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS idx_trip_members_trip_id ON trip_members(trip_id)`);
|
||||
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS idx_trip_members_user_id ON trip_members(user_id)`);
|
||||
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS idx_packing_items_trip_id ON packing_items(trip_id)`);
|
||||
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS idx_budget_items_trip_id ON budget_items(trip_id)`);
|
||||
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS idx_reservations_trip_id ON reservations(trip_id)`);
|
||||
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS idx_trip_files_trip_id ON trip_files(trip_id)`);
|
||||
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS idx_day_notes_day_id ON day_notes(day_id)`);
|
||||
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS idx_photos_trip_id ON photos(trip_id)`);
|
||||
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)`);
|
||||
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS idx_day_accommodations_trip_id ON day_accommodations(trip_id)`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS assignment_participants (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
assignment_id INTEGER NOT NULL REFERENCES day_assignments(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
UNIQUE(assignment_id, user_id)
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(
|
||||
`CREATE INDEX IF NOT EXISTS idx_assignment_participants_assignment ON assignment_participants(assignment_id)`,
|
||||
);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS audit_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
action TEXT NOT NULL,
|
||||
resource TEXT,
|
||||
details TEXT,
|
||||
ip TEXT
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS idx_audit_log_created ON audit_log(created_at DESC)`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS notifications (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
type TEXT NOT NULL CHECK(type IN ('simple', 'boolean', 'navigate')),
|
||||
scope TEXT NOT NULL CHECK(scope IN ('trip', 'user', 'admin')),
|
||||
target INTEGER NOT NULL,
|
||||
sender_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
recipient_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
title_key TEXT NOT NULL,
|
||||
title_params TEXT DEFAULT '{}',
|
||||
text_key TEXT NOT NULL,
|
||||
text_params TEXT DEFAULT '{}',
|
||||
positive_text_key TEXT,
|
||||
negative_text_key TEXT,
|
||||
positive_callback TEXT,
|
||||
negative_callback TEXT,
|
||||
response TEXT CHECK(response IN ('positive', 'negative')),
|
||||
navigate_text_key TEXT,
|
||||
navigate_target TEXT,
|
||||
is_read INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(
|
||||
`CREATE INDEX IF NOT EXISTS idx_notifications_recipient ON notifications(recipient_id, is_read, created_at DESC)`,
|
||||
);
|
||||
|
||||
this.addSql(
|
||||
`CREATE INDEX IF NOT EXISTS idx_notifications_recipient_created ON notifications(recipient_id, created_at DESC)`,
|
||||
);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS notification_channel_preferences (
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
event_type TEXT NOT NULL,
|
||||
channel TEXT NOT NULL,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (user_id, event_type, channel)
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS idx_ncp_user ON notification_channel_preferences(user_id)`);
|
||||
|
||||
this.addSql(
|
||||
`CREATE TABLE IF NOT EXISTS migrations (id integer PRIMARY KEY AUTOINCREMENT NOT NULL, timestamp bigint NOT NULL, name varchar NOT NULL)`,
|
||||
);
|
||||
|
||||
this.addSql(`CREATE TABLE IF NOT EXISTS schema_version (version INTEGER NOT NULL)`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 1 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101000100_users_add_unsplash_api_key extends Migration {
|
||||
override name = 'Migration20200101000100_users_add_unsplash_api_key';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(this, 'users', 'unsplash_api_key', `unsplash_api_key TEXT`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 2 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101000200_users_add_openweather_api_key extends Migration {
|
||||
override name = 'Migration20200101000200_users_add_openweather_api_key';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(this, 'users', 'openweather_api_key', `openweather_api_key TEXT`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 3 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101000300_places_add_duration_minutes extends Migration {
|
||||
override name = 'Migration20200101000300_places_add_duration_minutes';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(this, 'places', 'duration_minutes', `duration_minutes INTEGER DEFAULT 60`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 4 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101000400_places_add_notes extends Migration {
|
||||
override name = 'Migration20200101000400_places_add_notes';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(this, 'places', 'notes', `notes TEXT`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 5 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101000500_places_add_image_url extends Migration {
|
||||
override name = 'Migration20200101000500_places_add_image_url';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(this, 'places', 'image_url', `image_url TEXT`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 6 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101000600_places_add_transport_mode extends Migration {
|
||||
override name = 'Migration20200101000600_places_add_transport_mode';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(this, 'places', 'transport_mode', `transport_mode TEXT DEFAULT 'walking'`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 7 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101000700_days_add_title extends Migration {
|
||||
override name = 'Migration20200101000700_days_add_title';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(this, 'days', 'title', `title TEXT`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 8 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101000800_reservations_add_status extends Migration {
|
||||
override name = 'Migration20200101000800_reservations_add_status';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(this, 'reservations', 'status', `status TEXT DEFAULT 'pending'`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 9 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101000900_trip_files_add_reservation_id extends Migration {
|
||||
override name = 'Migration20200101000900_trip_files_add_reservation_id';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(
|
||||
this,
|
||||
'trip_files',
|
||||
'reservation_id',
|
||||
`reservation_id INTEGER REFERENCES reservations(id) ON DELETE SET NULL`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 10 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101001000_reservations_add_type extends Migration {
|
||||
override name = 'Migration20200101001000_reservations_add_type';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(this, 'reservations', 'type', `type TEXT DEFAULT 'other'`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 11 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101001100_trips_add_cover_image extends Migration {
|
||||
override name = 'Migration20200101001100_trips_add_cover_image';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(this, 'trips', 'cover_image', `cover_image TEXT`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 12 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101001200_day_notes_add_icon extends Migration {
|
||||
override name = 'Migration20200101001200_day_notes_add_icon';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(this, 'day_notes', 'icon', `icon TEXT DEFAULT '📝'`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 13 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101001300_trips_add_is_archived extends Migration {
|
||||
override name = 'Migration20200101001300_trips_add_is_archived';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(this, 'trips', 'is_archived', `is_archived INTEGER DEFAULT 0`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 14 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101001400_categories_add_user_id extends Migration {
|
||||
override name = 'Migration20200101001400_categories_add_user_id';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(this, 'categories', 'user_id', `user_id INTEGER REFERENCES users(id) ON DELETE SET NULL`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 15 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101001500_users_add_avatar extends Migration {
|
||||
override name = 'Migration20200101001500_users_add_avatar';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(this, 'users', 'avatar', `avatar TEXT`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 16 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101001600_users_add_oidc_sub extends Migration {
|
||||
override name = 'Migration20200101001600_users_add_oidc_sub';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(this, 'users', 'oidc_sub', `oidc_sub TEXT`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 17 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101001700_users_add_oidc_issuer extends Migration {
|
||||
override name = 'Migration20200101001700_users_add_oidc_issuer';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(this, 'users', 'oidc_issuer', `oidc_issuer TEXT`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 18 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101001800_users_add_last_login extends Migration {
|
||||
override name = 'Migration20200101001800_users_add_last_login';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(this, 'users', 'last_login', `last_login DATETIME`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { tableSql } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 19 (`db/migrations.ts`).
|
||||
*
|
||||
* Drops the `NOT NULL DEFAULT 1` on `budget_items.persons`/`days` so an expense
|
||||
* can leave them unset. SQLite cannot alter a column default, hence the rebuild;
|
||||
* it only fires on databases whose stored DDL still carries the old constraint.
|
||||
*
|
||||
* The legacy step matched the bare string `NOT NULL DEFAULT 1` anywhere in the
|
||||
* table's DDL. That is narrowed here to the two columns it is actually about:
|
||||
* step 132 later adds `exchange_rate REAL NOT NULL DEFAULT 1`, so the loose test
|
||||
* reports true on a fully-migrated table and the 10-column rebuild below then
|
||||
* fails against its 17 columns. The legacy runner never hit that because
|
||||
* `schema_version` stopped the step re-running; nothing gates it here.
|
||||
*/
|
||||
export class Migration20200101001900_create_budget_items_new extends Migration {
|
||||
override name = 'Migration20200101001900_create_budget_items_new';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
const sql = await tableSql(this, 'budget_items');
|
||||
if (!sql || !/\bpersons\s+INTEGER\s+NOT\s+NULL\s+DEFAULT\s+1/i.test(sql)) return;
|
||||
|
||||
await this.execute(`
|
||||
CREATE TABLE budget_items_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
trip_id INTEGER NOT NULL REFERENCES trips(id) ON DELETE CASCADE,
|
||||
category TEXT NOT NULL DEFAULT 'Other',
|
||||
name TEXT NOT NULL,
|
||||
total_price REAL NOT NULL DEFAULT 0,
|
||||
persons INTEGER DEFAULT NULL,
|
||||
days INTEGER DEFAULT NULL,
|
||||
note TEXT,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
await this.execute(`INSERT INTO budget_items_new SELECT * FROM budget_items`);
|
||||
await this.execute(`DROP TABLE budget_items`);
|
||||
await this.execute(`ALTER TABLE budget_items_new RENAME TO budget_items`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 20 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101002000_day_accommodations_add_check_in extends Migration {
|
||||
override name = 'Migration20200101002000_day_accommodations_add_check_in';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(this, 'day_accommodations', 'check_in', `check_in TEXT`);
|
||||
|
||||
await addColumnIfMissing(this, 'day_accommodations', 'check_out', `check_out TEXT`);
|
||||
|
||||
await addColumnIfMissing(this, 'day_accommodations', 'confirmation', `confirmation TEXT`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 21 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101002100_places_add_end_time extends Migration {
|
||||
override name = 'Migration20200101002100_places_add_end_time';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(this, 'places', 'end_time', `end_time TEXT`);
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import { addColumnIfMissing, execBestEffort } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 22 (`db/migrations.ts`).
|
||||
*
|
||||
* Moves reservation state off the place and onto the day assignment, so the same
|
||||
* place booked on two days keeps two independent bookings. The copy is
|
||||
* best-effort: it reads `places` columns that a later step removes, and the
|
||||
* original step logged and swallowed a failure rather than blocking the boot.
|
||||
*/
|
||||
export class Migration20200101002200_day_assignments_add_reservation_status extends Migration {
|
||||
override name = 'Migration20200101002200_day_assignments_add_reservation_status';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(this, 'day_assignments', 'reservation_status', `reservation_status TEXT DEFAULT 'none'`);
|
||||
await addColumnIfMissing(this, 'day_assignments', 'reservation_notes', `reservation_notes TEXT`);
|
||||
await addColumnIfMissing(this, 'day_assignments', 'reservation_datetime', `reservation_datetime TEXT`);
|
||||
|
||||
await execBestEffort(
|
||||
this,
|
||||
`
|
||||
UPDATE day_assignments SET
|
||||
reservation_status = (SELECT reservation_status FROM places WHERE places.id = day_assignments.place_id),
|
||||
reservation_notes = (SELECT reservation_notes FROM places WHERE places.id = day_assignments.place_id),
|
||||
reservation_datetime = (SELECT reservation_datetime FROM places WHERE places.id = day_assignments.place_id)
|
||||
WHERE place_id IN (SELECT id FROM places WHERE reservation_status IS NOT NULL AND reservation_status != 'none')
|
||||
`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 23 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101002300_reservations_add_assignment_id extends Migration {
|
||||
override name = 'Migration20200101002300_reservations_add_assignment_id';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(
|
||||
this,
|
||||
'reservations',
|
||||
'assignment_id',
|
||||
`assignment_id INTEGER REFERENCES day_assignments(id) ON DELETE SET NULL`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 24 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101002400_create_assignment_participants extends Migration {
|
||||
override name = 'Migration20200101002400_create_assignment_participants';
|
||||
|
||||
override up(): void {
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS assignment_participants (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
assignment_id INTEGER NOT NULL REFERENCES day_assignments(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
UNIQUE(assignment_id, user_id)
|
||||
)
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { execBestEffort } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 25 (`db/migrations.ts`).
|
||||
*
|
||||
* The Collab addon: shared notes, polls and trip chat.
|
||||
*/
|
||||
export class Migration20200101002500_create_collab_notes extends Migration {
|
||||
override name = 'Migration20200101002500_create_collab_notes';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await this.execute(`
|
||||
CREATE TABLE IF NOT EXISTS collab_notes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
trip_id INTEGER NOT NULL REFERENCES trips(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
category TEXT DEFAULT 'General',
|
||||
title TEXT NOT NULL,
|
||||
content TEXT,
|
||||
color TEXT DEFAULT '#6366f1',
|
||||
pinned INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
await this.execute(`
|
||||
CREATE TABLE IF NOT EXISTS collab_polls (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
trip_id INTEGER NOT NULL REFERENCES trips(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
question TEXT NOT NULL,
|
||||
options TEXT NOT NULL,
|
||||
multiple INTEGER DEFAULT 0,
|
||||
closed INTEGER DEFAULT 0,
|
||||
deadline TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
await this.execute(`
|
||||
CREATE TABLE IF NOT EXISTS collab_poll_votes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
poll_id INTEGER NOT NULL REFERENCES collab_polls(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
option_index INTEGER NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(poll_id, user_id, option_index)
|
||||
)
|
||||
`);
|
||||
await this.execute(`
|
||||
CREATE TABLE IF NOT EXISTS collab_messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
trip_id INTEGER NOT NULL REFERENCES trips(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
text TEXT NOT NULL,
|
||||
reply_to INTEGER REFERENCES collab_messages(id) ON DELETE SET NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
await this.execute(`CREATE INDEX IF NOT EXISTS idx_collab_notes_trip ON collab_notes(trip_id)`);
|
||||
await this.execute(`CREATE INDEX IF NOT EXISTS idx_collab_polls_trip ON collab_polls(trip_id)`);
|
||||
await this.execute(`CREATE INDEX IF NOT EXISTS idx_collab_messages_trip ON collab_messages(trip_id)`);
|
||||
|
||||
await execBestEffort(
|
||||
this,
|
||||
`INSERT OR IGNORE INTO addons (id, name, description, type, icon, enabled, sort_order) VALUES ('collab', 'Collab', 'Notes, polls, and live chat for trip collaboration', 'trip', 'Users', 1, 6)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { addColumnIfMissing, execBestEffort } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 26 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101002600_day_assignments_add_assignment_time extends Migration {
|
||||
override name = 'Migration20200101002600_day_assignments_add_assignment_time';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(this, 'day_assignments', 'assignment_time', `assignment_time TEXT`);
|
||||
|
||||
await addColumnIfMissing(this, 'day_assignments', 'assignment_end_time', `assignment_end_time TEXT`);
|
||||
|
||||
await execBestEffort(
|
||||
this,
|
||||
`
|
||||
UPDATE day_assignments SET
|
||||
assignment_time = (SELECT place_time FROM places WHERE places.id = day_assignments.place_id),
|
||||
assignment_end_time = (SELECT end_time FROM places WHERE places.id = day_assignments.place_id)
|
||||
`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 27 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101002700_create_budget_item_members extends Migration {
|
||||
override name = 'Migration20200101002700_create_budget_item_members';
|
||||
|
||||
override up(): void {
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS budget_item_members (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
budget_item_id INTEGER NOT NULL REFERENCES budget_items(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
paid INTEGER NOT NULL DEFAULT 0,
|
||||
UNIQUE(budget_item_id, user_id)
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS idx_budget_item_members_item ON budget_item_members(budget_item_id)`);
|
||||
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS idx_budget_item_members_user ON budget_item_members(user_id)`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 28 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101002800_create_collab_message_reactions extends Migration {
|
||||
override name = 'Migration20200101002800_create_collab_message_reactions';
|
||||
|
||||
override up(): void {
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS collab_message_reactions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
message_id INTEGER NOT NULL REFERENCES collab_messages(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
emoji TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(message_id, user_id, emoji)
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS idx_collab_reactions_msg ON collab_message_reactions(message_id)`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 29 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101002900_collab_messages_add_deleted extends Migration {
|
||||
override name = 'Migration20200101002900_collab_messages_add_deleted';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(this, 'collab_messages', 'deleted', `deleted INTEGER DEFAULT 0`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 30 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101003000_trip_files_add_note_id extends Migration {
|
||||
override name = 'Migration20200101003000_trip_files_add_note_id';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(
|
||||
this,
|
||||
'trip_files',
|
||||
'note_id',
|
||||
`note_id INTEGER REFERENCES collab_notes(id) ON DELETE SET NULL`,
|
||||
);
|
||||
|
||||
await addColumnIfMissing(this, 'collab_notes', 'website', `website TEXT`);
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 31 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101003100_reservations_add_reservation_end_time extends Migration {
|
||||
override name = 'Migration20200101003100_reservations_add_reservation_end_time';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(this, 'reservations', 'reservation_end_time', `reservation_end_time TEXT`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 32 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101003200_places_add_osm_id extends Migration {
|
||||
override name = 'Migration20200101003200_places_add_osm_id';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(this, 'places', 'osm_id', `osm_id TEXT`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 33 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101003300_trip_files_add_uploaded_by extends Migration {
|
||||
override name = 'Migration20200101003300_trip_files_add_uploaded_by';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(
|
||||
this,
|
||||
'trip_files',
|
||||
'uploaded_by',
|
||||
`uploaded_by INTEGER REFERENCES users(id) ON DELETE SET NULL`,
|
||||
);
|
||||
|
||||
await addColumnIfMissing(this, 'trip_files', 'starred', `starred INTEGER DEFAULT 0`);
|
||||
|
||||
await addColumnIfMissing(this, 'trip_files', 'deleted_at', `deleted_at TEXT`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 34 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101003400_reservations_add_accommodation_id extends Migration {
|
||||
override name = 'Migration20200101003400_reservations_add_accommodation_id';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(
|
||||
this,
|
||||
'reservations',
|
||||
'accommodation_id',
|
||||
`accommodation_id INTEGER REFERENCES day_accommodations(id) ON DELETE SET NULL`,
|
||||
);
|
||||
|
||||
await addColumnIfMissing(this, 'reservations', 'metadata', `metadata TEXT`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 35 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101003500_create_invite_tokens extends Migration {
|
||||
override name = 'Migration20200101003500_create_invite_tokens';
|
||||
|
||||
override up(): void {
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS invite_tokens (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
token TEXT UNIQUE NOT NULL,
|
||||
max_uses INTEGER NOT NULL DEFAULT 1,
|
||||
used_count INTEGER NOT NULL DEFAULT 0,
|
||||
expires_at TEXT,
|
||||
created_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 36 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101003600_users_add_mfa_enabled extends Migration {
|
||||
override name = 'Migration20200101003600_users_add_mfa_enabled';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(this, 'users', 'mfa_enabled', `mfa_enabled INTEGER DEFAULT 0`);
|
||||
|
||||
await addColumnIfMissing(this, 'users', 'mfa_secret', `mfa_secret TEXT`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 37 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101003700_create_packing_category_assignees extends Migration {
|
||||
override name = 'Migration20200101003700_create_packing_category_assignees';
|
||||
|
||||
override up(): void {
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS packing_category_assignees (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
trip_id INTEGER NOT NULL REFERENCES trips(id) ON DELETE CASCADE,
|
||||
category_name TEXT NOT NULL,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
UNIQUE(trip_id, category_name, user_id)
|
||||
)
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { execBestEffort } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 38 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101003800_create_packing_templates extends Migration {
|
||||
override name = 'Migration20200101003800_create_packing_templates';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await this.execute(`
|
||||
CREATE TABLE IF NOT EXISTS packing_templates (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
created_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
await this.execute(`
|
||||
CREATE TABLE IF NOT EXISTS packing_template_categories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
template_id INTEGER NOT NULL REFERENCES packing_templates(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0
|
||||
)
|
||||
`);
|
||||
|
||||
await execBestEffort(this, `DROP TABLE IF EXISTS packing_template_items`);
|
||||
|
||||
await this.execute(`
|
||||
CREATE TABLE packing_template_items (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
category_id INTEGER NOT NULL REFERENCES packing_template_categories(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0
|
||||
)
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 39 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101003900_create_packing_bags extends Migration {
|
||||
override name = 'Migration20200101003900_create_packing_bags';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await this.execute(`
|
||||
CREATE TABLE IF NOT EXISTS packing_bags (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
trip_id INTEGER NOT NULL REFERENCES trips(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
color TEXT NOT NULL DEFAULT '#6366f1',
|
||||
weight_limit_grams INTEGER,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
await addColumnIfMissing(this, 'packing_items', 'weight_grams', `weight_grams INTEGER`);
|
||||
|
||||
await addColumnIfMissing(
|
||||
this,
|
||||
'packing_items',
|
||||
'bag_id',
|
||||
`bag_id INTEGER REFERENCES packing_bags(id) ON DELETE SET NULL`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 40 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101004000_create_visited_countries extends Migration {
|
||||
override name = 'Migration20200101004000_create_visited_countries';
|
||||
|
||||
override up(): void {
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS visited_countries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
country_code TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(user_id, country_code)
|
||||
)
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 41 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101004100_create_bucket_list extends Migration {
|
||||
override name = 'Migration20200101004100_create_bucket_list';
|
||||
|
||||
override up(): void {
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS bucket_list (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
lat REAL,
|
||||
lng REAL,
|
||||
country_code TEXT,
|
||||
notes TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 42 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101004200_vacay_plans_add_weekend_days extends Migration {
|
||||
override name = 'Migration20200101004200_vacay_plans_add_weekend_days';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(this, 'vacay_plans', 'weekend_days', `weekend_days TEXT DEFAULT '0,6'`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { addColumnIfMissing, execBestEffort } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 43 (`db/migrations.ts`).
|
||||
*
|
||||
* Immich integration: per-user credentials, the linked-asset table, and the
|
||||
* Photos addon tile (disabled by default).
|
||||
*/
|
||||
export class Migration20200101004300_users_add_immich_url extends Migration {
|
||||
override name = 'Migration20200101004300_users_add_immich_url';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(this, 'users', 'immich_url', `immich_url TEXT`);
|
||||
await addColumnIfMissing(this, 'users', 'immich_api_key', `immich_api_key TEXT`);
|
||||
|
||||
await this.execute(`
|
||||
CREATE TABLE IF NOT EXISTS trip_photos (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
trip_id INTEGER NOT NULL REFERENCES trips(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
immich_asset_id TEXT NOT NULL,
|
||||
shared INTEGER NOT NULL DEFAULT 1,
|
||||
added_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(trip_id, user_id, immich_asset_id)
|
||||
)
|
||||
`);
|
||||
|
||||
await execBestEffort(
|
||||
this,
|
||||
`INSERT INTO addons (id, name, type, icon, enabled, sort_order) VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
['memories', 'Photos', 'trip', 'Image', 0, 7],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 44 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101004400_create_file_links extends Migration {
|
||||
override name = 'Migration20200101004400_create_file_links';
|
||||
|
||||
override up(): void {
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS file_links (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
file_id INTEGER NOT NULL REFERENCES trip_files(id) ON DELETE CASCADE,
|
||||
reservation_id INTEGER REFERENCES reservations(id) ON DELETE CASCADE,
|
||||
assignment_id INTEGER REFERENCES day_assignments(id) ON DELETE CASCADE,
|
||||
place_id INTEGER REFERENCES places(id) ON DELETE CASCADE,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(file_id, reservation_id),
|
||||
UNIQUE(file_id, assignment_id),
|
||||
UNIQUE(file_id, place_id)
|
||||
)
|
||||
`);
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 45 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101004500_reservations_add_day_plan_position extends Migration {
|
||||
override name = 'Migration20200101004500_reservations_add_day_plan_position';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(this, 'reservations', 'day_plan_position', `day_plan_position REAL DEFAULT NULL`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 46 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101004600_budget_items_add_paid_by_user_id extends Migration {
|
||||
override name = 'Migration20200101004600_budget_items_add_paid_by_user_id';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(this, 'budget_items', 'paid_by_user_id', `paid_by_user_id INTEGER REFERENCES users(id)`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 47 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101004700_bucket_list_add_target_date extends Migration {
|
||||
override name = 'Migration20200101004700_bucket_list_add_target_date';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(this, 'bucket_list', 'target_date', `target_date TEXT DEFAULT NULL`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 48 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101004800_create_notification_preferences extends Migration {
|
||||
override name = 'Migration20200101004800_create_notification_preferences';
|
||||
|
||||
override up(): void {
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS notification_preferences (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
notify_trip_invite INTEGER DEFAULT 1,
|
||||
notify_booking_change INTEGER DEFAULT 1,
|
||||
notify_trip_reminder INTEGER DEFAULT 1,
|
||||
notify_vacay_invite INTEGER DEFAULT 1,
|
||||
notify_photos_shared INTEGER DEFAULT 1,
|
||||
notify_collab_message INTEGER DEFAULT 1,
|
||||
notify_packing_tagged INTEGER DEFAULT 1,
|
||||
notify_webhook INTEGER DEFAULT 0,
|
||||
UNIQUE(user_id)
|
||||
)
|
||||
`);
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 49 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101004900_notification_preferences_add_notify_vacay_invite extends Migration {
|
||||
override name = 'Migration20200101004900_notification_preferences_add_notify_vacay_invite';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(
|
||||
this,
|
||||
'notification_preferences',
|
||||
'notify_vacay_invite',
|
||||
`notify_vacay_invite INTEGER DEFAULT 1`,
|
||||
);
|
||||
|
||||
await addColumnIfMissing(
|
||||
this,
|
||||
'notification_preferences',
|
||||
'notify_photos_shared',
|
||||
`notify_photos_shared INTEGER DEFAULT 1`,
|
||||
);
|
||||
|
||||
await addColumnIfMissing(
|
||||
this,
|
||||
'notification_preferences',
|
||||
'notify_collab_message',
|
||||
`notify_collab_message INTEGER DEFAULT 1`,
|
||||
);
|
||||
|
||||
await addColumnIfMissing(
|
||||
this,
|
||||
'notification_preferences',
|
||||
'notify_packing_tagged',
|
||||
`notify_packing_tagged INTEGER DEFAULT 1`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 50 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101005000_create_share_tokens extends Migration {
|
||||
override name = 'Migration20200101005000_create_share_tokens';
|
||||
|
||||
override up(): void {
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS share_tokens (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
trip_id INTEGER NOT NULL REFERENCES trips(id) ON DELETE CASCADE,
|
||||
token TEXT NOT NULL UNIQUE,
|
||||
created_by INTEGER NOT NULL REFERENCES users(id),
|
||||
share_map INTEGER DEFAULT 1,
|
||||
share_bookings INTEGER DEFAULT 1,
|
||||
share_packing INTEGER DEFAULT 0,
|
||||
share_budget INTEGER DEFAULT 0,
|
||||
share_collab INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 51 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101005100_share_tokens_add_share_map extends Migration {
|
||||
override name = 'Migration20200101005100_share_tokens_add_share_map';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(this, 'share_tokens', 'share_map', `share_map INTEGER DEFAULT 1`);
|
||||
|
||||
await addColumnIfMissing(this, 'share_tokens', 'share_bookings', `share_bookings INTEGER DEFAULT 1`);
|
||||
|
||||
await addColumnIfMissing(this, 'share_tokens', 'share_packing', `share_packing INTEGER DEFAULT 0`);
|
||||
|
||||
await addColumnIfMissing(this, 'share_tokens', 'share_budget', `share_budget INTEGER DEFAULT 0`);
|
||||
|
||||
await addColumnIfMissing(this, 'share_tokens', 'share_collab', `share_collab INTEGER DEFAULT 0`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 52 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101005200_create_audit_log extends Migration {
|
||||
override name = 'Migration20200101005200_create_audit_log';
|
||||
|
||||
override up(): void {
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS audit_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
action TEXT NOT NULL,
|
||||
resource TEXT,
|
||||
details TEXT,
|
||||
ip TEXT
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS idx_audit_log_created ON audit_log(created_at DESC)`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 53 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101005300_users_add_mfa_backup_codes extends Migration {
|
||||
override name = 'Migration20200101005300_users_add_mfa_backup_codes';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(this, 'users', 'mfa_backup_codes', `mfa_backup_codes TEXT`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 54 (`db/migrations.ts`).
|
||||
*
|
||||
* MCP long-lived API tokens
|
||||
*/
|
||||
export class Migration20200101005400_mcp_long_lived_api_tokens extends Migration {
|
||||
override name = 'Migration20200101005400_mcp_long_lived_api_tokens';
|
||||
|
||||
override up(): void {
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS mcp_tokens (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
token_hash TEXT NOT NULL,
|
||||
token_prefix TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
last_used_at DATETIME
|
||||
)
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { execBestEffort } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 55 (`db/migrations.ts`).
|
||||
*
|
||||
* MCP addon entry.
|
||||
*/
|
||||
export class Migration20200101005500_mcp_addon_entry extends Migration {
|
||||
override name = 'Migration20200101005500_mcp_addon_entry';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await execBestEffort(
|
||||
this,
|
||||
`INSERT OR IGNORE INTO addons (id, name, description, type, icon, enabled, sort_order) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
['mcp', 'MCP', 'Model Context Protocol for AI assistant integration', 'integration', 'Terminal', 0, 12],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 56 (`db/migrations.ts`).
|
||||
*
|
||||
* Index on mcp_tokens.token_hash
|
||||
*/
|
||||
export class Migration20200101005600_index_on_mcp_tokens extends Migration {
|
||||
override name = 'Migration20200101005600_index_on_mcp_tokens';
|
||||
|
||||
override up(): void {
|
||||
this.addSql(`CREATE UNIQUE INDEX IF NOT EXISTS idx_mcp_tokens_hash ON mcp_tokens(token_hash)`);
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { execBestEffort } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 57 (`db/migrations.ts`).
|
||||
*
|
||||
* Ensure MCP addon type is 'integration'.
|
||||
*/
|
||||
export class Migration20200101005700_ensure_mcp_addon_type_is_integration extends Migration {
|
||||
override name = 'Migration20200101005700_ensure_mcp_addon_type_is_integration';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await execBestEffort(this, `UPDATE addons SET type = 'integration' WHERE id = 'mcp'`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 58 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101005800_places_add_route_geometry extends Migration {
|
||||
override name = 'Migration20200101005800_places_add_route_geometry';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(this, 'places', 'route_geometry', `route_geometry TEXT`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 59 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101005900_users_add_must_change_password extends Migration {
|
||||
override name = 'Migration20200101005900_users_add_must_change_password';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(this, 'users', 'must_change_password', `must_change_password INTEGER DEFAULT 0`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 60 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101010000_trips_add_reminder_days extends Migration {
|
||||
override name = 'Migration20200101010000_trips_add_reminder_days';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(this, 'trips', 'reminder_days', `reminder_days INTEGER DEFAULT 3`);
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { encrypt_api_key } from '../../nest/common/crypto/apiKeyCrypto';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 61 (`db/migrations.ts`).
|
||||
*
|
||||
* Encrypt any plaintext oidc_client_secret left in app_settings.
|
||||
*/
|
||||
export class Migration20200101010100_encrypt_any_plaintext_oidc_client_secret_left extends Migration {
|
||||
override name = 'Migration20200101010100_encrypt_any_plaintext_oidc_client_secret_left';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
const rows = (await this.execute(`SELECT value FROM app_settings WHERE key = 'oidc_client_secret'`)) as {
|
||||
value: string | null;
|
||||
}[];
|
||||
const value = rows[0]?.value;
|
||||
if (!value || value.startsWith('enc:v1:')) return;
|
||||
await this.execute(`UPDATE app_settings SET value = ? WHERE key = 'oidc_client_secret'`, [encrypt_api_key(value)]);
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { encrypt_api_key } from '../../nest/common/crypto/apiKeyCrypto';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 62 (`db/migrations.ts`).
|
||||
*
|
||||
* Encrypt any plaintext smtp_pass left in app_settings.
|
||||
*/
|
||||
export class Migration20200101010200_encrypt_any_plaintext_smtp_pass_left_in extends Migration {
|
||||
override name = 'Migration20200101010200_encrypt_any_plaintext_smtp_pass_left_in';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
const rows = (await this.execute(`SELECT value FROM app_settings WHERE key = 'smtp_pass'`)) as {
|
||||
value: string | null;
|
||||
}[];
|
||||
const value = rows[0]?.value;
|
||||
if (!value || value.startsWith('enc:v1:')) return;
|
||||
await this.execute(`UPDATE app_settings SET value = ? WHERE key = 'smtp_pass'`, [encrypt_api_key(value)]);
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { encrypt_api_key } from '../../nest/common/crypto/apiKeyCrypto';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 63 (`db/migrations.ts`).
|
||||
*
|
||||
* Encrypt any plaintext immich_api_key values in the users table.
|
||||
*/
|
||||
export class Migration20200101010300_encrypt_any_plaintext_immich_api_key_values extends Migration {
|
||||
override name = 'Migration20200101010300_encrypt_any_plaintext_immich_api_key_values';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
const rows = (await this.execute(
|
||||
`SELECT id, immich_api_key FROM users WHERE immich_api_key IS NOT NULL AND immich_api_key != '' AND immich_api_key NOT LIKE 'enc:v1:%'`,
|
||||
)) as { id: number; immich_api_key: string }[];
|
||||
for (const row of rows) {
|
||||
await this.execute(`UPDATE users SET immich_api_key = ? WHERE id = ?`, [
|
||||
encrypt_api_key(row.immich_api_key),
|
||||
row.id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 64 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101010400_budget_items_add_expense_date extends Migration {
|
||||
override name = 'Migration20200101010400_budget_items_add_expense_date';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(this, 'budget_items', 'expense_date', `expense_date TEXT DEFAULT NULL`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 65 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101010500_create_trip_album_links extends Migration {
|
||||
override name = 'Migration20200101010500_create_trip_album_links';
|
||||
|
||||
override up(): void {
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS trip_album_links (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
trip_id INTEGER NOT NULL REFERENCES trips(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
immich_album_id TEXT NOT NULL,
|
||||
album_name TEXT NOT NULL DEFAULT '',
|
||||
sync_enabled INTEGER NOT NULL DEFAULT 1,
|
||||
last_synced_at DATETIME,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(trip_id, user_id, immich_album_id)
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS idx_trip_album_links_trip ON trip_album_links(trip_id)`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 66 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101010600_create_notifications extends Migration {
|
||||
override name = 'Migration20200101010600_create_notifications';
|
||||
|
||||
override up(): void {
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS notifications (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
type TEXT NOT NULL CHECK(type IN ('simple', 'boolean', 'navigate')),
|
||||
scope TEXT NOT NULL CHECK(scope IN ('trip', 'user', 'admin')),
|
||||
target INTEGER NOT NULL,
|
||||
sender_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
recipient_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
title_key TEXT NOT NULL,
|
||||
title_params TEXT DEFAULT '{}',
|
||||
text_key TEXT NOT NULL,
|
||||
text_params TEXT DEFAULT '{}',
|
||||
positive_text_key TEXT,
|
||||
negative_text_key TEXT,
|
||||
positive_callback TEXT,
|
||||
negative_callback TEXT,
|
||||
response TEXT CHECK(response IN ('positive', 'negative')),
|
||||
navigate_text_key TEXT,
|
||||
navigate_target TEXT,
|
||||
is_read INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(
|
||||
`CREATE INDEX IF NOT EXISTS idx_notifications_recipient ON notifications(recipient_id, is_read, created_at DESC)`,
|
||||
);
|
||||
|
||||
this.addSql(
|
||||
`CREATE INDEX IF NOT EXISTS idx_notifications_recipient_created ON notifications(recipient_id, created_at DESC)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { columnNames, tableExists } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
const TRIP_PHOTOS_DDL = `
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
trip_id INTEGER NOT NULL REFERENCES trips(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
asset_id TEXT NOT NULL,
|
||||
provider TEXT NOT NULL DEFAULT 'immich',
|
||||
shared INTEGER NOT NULL DEFAULT 1,
|
||||
added_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(trip_id, user_id, asset_id, provider)
|
||||
`;
|
||||
|
||||
/**
|
||||
* Legacy migration step 67 (`db/migrations.ts`).
|
||||
*
|
||||
* Normalize trip_photos to the provider-based schema used by current routes.
|
||||
* The table arrived in two shapes — Immich-only (`immich_asset_id`) and the
|
||||
* provider-aware one — so the rebuild reads whichever columns are actually
|
||||
* present and fills the rest with the Immich defaults.
|
||||
*/
|
||||
export class Migration20200101010700_create_trip_photos extends Migration {
|
||||
override name = 'Migration20200101010700_create_trip_photos';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
if (!(await tableExists(this, 'trip_photos'))) {
|
||||
await this.execute(`CREATE TABLE trip_photos (${TRIP_PHOTOS_DDL})`);
|
||||
await this.execute(`CREATE INDEX IF NOT EXISTS idx_trip_photos_trip ON trip_photos(trip_id)`);
|
||||
return;
|
||||
}
|
||||
|
||||
const names = await columnNames(this, 'trip_photos');
|
||||
const assetSource = names.has('asset_id') ? 'asset_id' : names.has('immich_asset_id') ? 'immich_asset_id' : null;
|
||||
if (!assetSource) return;
|
||||
|
||||
const providerExpr = names.has('provider')
|
||||
? `CASE WHEN provider IS NULL OR provider = '' THEN 'immich' ELSE provider END`
|
||||
: `'immich'`;
|
||||
const sharedExpr = names.has('shared') ? 'COALESCE(shared, 1)' : '1';
|
||||
const addedAtExpr = names.has('added_at') ? 'COALESCE(added_at, CURRENT_TIMESTAMP)' : 'CURRENT_TIMESTAMP';
|
||||
|
||||
await this.execute(`CREATE TABLE trip_photos_new (${TRIP_PHOTOS_DDL})`);
|
||||
await this.execute(`
|
||||
INSERT OR IGNORE INTO trip_photos_new (trip_id, user_id, asset_id, provider, shared, added_at)
|
||||
SELECT trip_id, user_id, ${assetSource}, ${providerExpr}, ${sharedExpr}, ${addedAtExpr}
|
||||
FROM trip_photos
|
||||
WHERE ${assetSource} IS NOT NULL AND TRIM(${assetSource}) != ''
|
||||
`);
|
||||
await this.execute(`DROP TABLE trip_photos`);
|
||||
await this.execute(`ALTER TABLE trip_photos_new RENAME TO trip_photos`);
|
||||
await this.execute(`CREATE INDEX IF NOT EXISTS idx_trip_photos_trip ON trip_photos(trip_id)`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { columnNames, tableExists } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
const ALBUM_LINKS_DDL = `
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
trip_id INTEGER NOT NULL REFERENCES trips(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
provider TEXT NOT NULL,
|
||||
album_id TEXT NOT NULL,
|
||||
album_name TEXT NOT NULL DEFAULT '',
|
||||
sync_enabled INTEGER NOT NULL DEFAULT 1,
|
||||
last_synced_at DATETIME,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(trip_id, user_id, provider, album_id)
|
||||
`;
|
||||
|
||||
/**
|
||||
* Legacy migration step 68 (`db/migrations.ts`).
|
||||
*
|
||||
* Normalize trip_album_links to the provider + album_id schema used by current
|
||||
* routes, reading whichever of the two historical shapes the database has.
|
||||
*/
|
||||
export class Migration20200101010800_create_trip_album_links extends Migration {
|
||||
override name = 'Migration20200101010800_create_trip_album_links';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
if (!(await tableExists(this, 'trip_album_links'))) {
|
||||
await this.execute(`CREATE TABLE trip_album_links (${ALBUM_LINKS_DDL})`);
|
||||
await this.execute(`CREATE INDEX IF NOT EXISTS idx_trip_album_links_trip ON trip_album_links(trip_id)`);
|
||||
return;
|
||||
}
|
||||
|
||||
const names = await columnNames(this, 'trip_album_links');
|
||||
const albumIdSource = names.has('album_id') ? 'album_id' : names.has('immich_album_id') ? 'immich_album_id' : null;
|
||||
if (!albumIdSource) return;
|
||||
|
||||
const providerExpr = names.has('provider')
|
||||
? `CASE WHEN provider IS NULL OR provider = '' THEN 'immich' ELSE provider END`
|
||||
: `'immich'`;
|
||||
const albumNameExpr = names.has('album_name') ? `COALESCE(album_name, '')` : `''`;
|
||||
const syncEnabledExpr = names.has('sync_enabled') ? 'COALESCE(sync_enabled, 1)' : '1';
|
||||
const lastSyncedExpr = names.has('last_synced_at') ? 'last_synced_at' : 'NULL';
|
||||
const createdAtExpr = names.has('created_at') ? 'COALESCE(created_at, CURRENT_TIMESTAMP)' : 'CURRENT_TIMESTAMP';
|
||||
|
||||
await this.execute(`CREATE TABLE trip_album_links_new (${ALBUM_LINKS_DDL})`);
|
||||
await this.execute(`
|
||||
INSERT OR IGNORE INTO trip_album_links_new (trip_id, user_id, provider, album_id, album_name, sync_enabled, last_synced_at, created_at)
|
||||
SELECT trip_id, user_id, ${providerExpr}, ${albumIdSource}, ${albumNameExpr}, ${syncEnabledExpr}, ${lastSyncedExpr}, ${createdAtExpr}
|
||||
FROM trip_album_links
|
||||
WHERE ${albumIdSource} IS NOT NULL AND TRIM(${albumIdSource}) != ''
|
||||
`);
|
||||
await this.execute(`DROP TABLE trip_album_links`);
|
||||
await this.execute(`ALTER TABLE trip_album_links_new RENAME TO trip_album_links`);
|
||||
await this.execute(`CREATE INDEX IF NOT EXISTS idx_trip_album_links_trip ON trip_album_links(trip_id)`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 69 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101010900_users_add_synology_url extends Migration {
|
||||
override name = 'Migration20200101010900_users_add_synology_url';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(this, 'users', 'synology_url', `synology_url TEXT`);
|
||||
|
||||
await addColumnIfMissing(this, 'users', 'synology_username', `synology_username TEXT`);
|
||||
|
||||
await addColumnIfMissing(this, 'users', 'synology_password', `synology_password TEXT`);
|
||||
|
||||
await addColumnIfMissing(this, 'users', 'synology_sid', `synology_sid TEXT`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { execUnlessTableMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
const PROVIDER_FIELDS: [string, string, string, string, string, number, number, string | null, string, number][] = [
|
||||
[
|
||||
'synologyphotos',
|
||||
'synology_url',
|
||||
'providerUrl',
|
||||
'url',
|
||||
'https://synology.example.com',
|
||||
1,
|
||||
0,
|
||||
'synology_url',
|
||||
'synology_url',
|
||||
0,
|
||||
],
|
||||
[
|
||||
'synologyphotos',
|
||||
'synology_username',
|
||||
'providerUsername',
|
||||
'text',
|
||||
'Username',
|
||||
1,
|
||||
0,
|
||||
'synology_username',
|
||||
'synology_username',
|
||||
1,
|
||||
],
|
||||
[
|
||||
'synologyphotos',
|
||||
'synology_password',
|
||||
'providerPassword',
|
||||
'password',
|
||||
'Password',
|
||||
1,
|
||||
1,
|
||||
null,
|
||||
'synology_password',
|
||||
2,
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Legacy migration step 70 (`db/migrations.ts`).
|
||||
*
|
||||
* Seed the Synology Photos provider and its fields in existing databases. Both
|
||||
* statements are skipped when the provider tables do not exist yet — on those
|
||||
* installs the seeder creates the rows instead.
|
||||
*/
|
||||
export class Migration20200101011000_step_070 extends Migration {
|
||||
override name = 'Migration20200101011000_step_070';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await execUnlessTableMissing(
|
||||
this,
|
||||
`
|
||||
INSERT INTO photo_providers (id, name, description, icon, enabled, sort_order)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
name = excluded.name,
|
||||
description = excluded.description,
|
||||
icon = excluded.icon,
|
||||
enabled = excluded.enabled,
|
||||
sort_order = excluded.sort_order
|
||||
`,
|
||||
[
|
||||
'synologyphotos',
|
||||
'Synology Photos',
|
||||
'Synology Photos integration with separate account settings',
|
||||
'Image',
|
||||
0,
|
||||
1,
|
||||
],
|
||||
);
|
||||
|
||||
for (const field of PROVIDER_FIELDS) {
|
||||
await execUnlessTableMissing(
|
||||
this,
|
||||
`
|
||||
INSERT INTO photo_provider_fields
|
||||
(provider_id, field_key, label, input_type, placeholder, required, secret, settings_key, payload_key, sort_order)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(provider_id, field_key) DO UPDATE SET
|
||||
label = excluded.label,
|
||||
input_type = excluded.input_type,
|
||||
placeholder = excluded.placeholder,
|
||||
required = excluded.required,
|
||||
secret = excluded.secret,
|
||||
settings_key = excluded.settings_key,
|
||||
payload_key = excluded.payload_key,
|
||||
sort_order = excluded.sort_order
|
||||
`,
|
||||
field,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { columnNames } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 71 (`db/migrations.ts`).
|
||||
*
|
||||
* Remove the stored config column from photo_providers now that it is generated
|
||||
* from the provider id.
|
||||
*/
|
||||
export class Migration20200101011100_step_071 extends Migration {
|
||||
override name = 'Migration20200101011100_step_071';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
const names = await columnNames(this, 'photo_providers');
|
||||
if (!names.has('config')) return;
|
||||
await this.execute(`ALTER TABLE photo_providers DROP COLUMN config`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { columnNames } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 72 (`db/migrations.ts`).
|
||||
*
|
||||
* The Immich-only column names become provider-neutral ones. A database that
|
||||
* already went through step 67/68 is left alone.
|
||||
*/
|
||||
export class Migration20200101011200_step_072 extends Migration {
|
||||
override name = 'Migration20200101011200_step_072';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
const names = await columnNames(this, 'trip_photos');
|
||||
if (names.has('asset_id') && !names.has('immich_asset_id')) return;
|
||||
|
||||
await this.execute('ALTER TABLE `trip_photos` RENAME COLUMN immich_asset_id TO asset_id');
|
||||
await this.execute('ALTER TABLE `trip_photos` ADD COLUMN provider TEXT NOT NULL DEFAULT "immich"');
|
||||
await this.execute('ALTER TABLE `trip_album_links` ADD COLUMN provider TEXT NOT NULL DEFAULT "immich"');
|
||||
await this.execute('ALTER TABLE `trip_album_links` RENAME COLUMN immich_album_id TO album_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 73 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101011300_trip_photos_add_album_link_id extends Migration {
|
||||
override name = 'Migration20200101011300_trip_photos_add_album_link_id';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(
|
||||
this,
|
||||
'trip_photos',
|
||||
'album_link_id',
|
||||
`album_link_id INTEGER REFERENCES trip_album_links(id) ON DELETE SET NULL DEFAULT NULL`,
|
||||
);
|
||||
|
||||
await this.execute(`CREATE INDEX IF NOT EXISTS idx_trip_photos_album_link ON trip_photos(album_link_id)`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 74 (`db/migrations.ts`).
|
||||
*
|
||||
* Migration 68: Todo items
|
||||
*/
|
||||
export class Migration20200101011400_todo_items extends Migration {
|
||||
override name = 'Migration20200101011400_todo_items';
|
||||
|
||||
override up(): void {
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS todo_items (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
trip_id INTEGER NOT NULL REFERENCES trips(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
checked INTEGER DEFAULT 0,
|
||||
category TEXT,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
due_date TEXT,
|
||||
description TEXT,
|
||||
assigned_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
priority INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS idx_todo_items_trip_id ON todo_items(trip_id)`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS todo_category_assignees (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
trip_id INTEGER NOT NULL REFERENCES trips(id) ON DELETE CASCADE,
|
||||
category_name TEXT NOT NULL,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
UNIQUE(trip_id, category_name, user_id)
|
||||
)
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { execBestEffort } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 75 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101011500_backfill_addons extends Migration {
|
||||
override name = 'Migration20200101011500_backfill_addons';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await execBestEffort(this, `UPDATE addons SET enabled = 0 WHERE id = 'memories'`);
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 76 (`db/migrations.ts`).
|
||||
*
|
||||
* Migration 69: Place region cache for sub-national Atlas regions
|
||||
*/
|
||||
export class Migration20200101011600_place_region_cache_for_sub_national_atlas extends Migration {
|
||||
override name = 'Migration20200101011600_place_region_cache_for_sub_national_atlas';
|
||||
|
||||
override up(): void {
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS place_regions (
|
||||
place_id INTEGER PRIMARY KEY REFERENCES places(id) ON DELETE CASCADE,
|
||||
country_code TEXT NOT NULL,
|
||||
region_code TEXT NOT NULL,
|
||||
region_name TEXT NOT NULL
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS idx_place_regions_country ON place_regions(country_code)`);
|
||||
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS idx_place_regions_region ON place_regions(region_code)`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 77 (`db/migrations.ts`).
|
||||
*/
|
||||
export class Migration20200101011700_create_visited_regions extends Migration {
|
||||
override name = 'Migration20200101011700_create_visited_regions';
|
||||
|
||||
override up(): void {
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS visited_regions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
region_code TEXT NOT NULL,
|
||||
region_name TEXT NOT NULL,
|
||||
country_code TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(user_id, region_code)
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS idx_visited_regions_country ON visited_regions(country_code)`);
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
import { tableExists } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/** Old single-row-per-user columns → the event_type they become. */
|
||||
const EVENT_COLUMNS: Record<string, string> = {
|
||||
trip_invite: 'notify_trip_invite',
|
||||
booking_change: 'notify_booking_change',
|
||||
trip_reminder: 'notify_trip_reminder',
|
||||
vacay_invite: 'notify_vacay_invite',
|
||||
photos_shared: 'notify_photos_shared',
|
||||
collab_message: 'notify_collab_message',
|
||||
packing_tagged: 'notify_packing_tagged',
|
||||
};
|
||||
|
||||
/**
|
||||
* Legacy migration step 78 (`db/migrations.ts`).
|
||||
*
|
||||
* Migration 71: Normalized per-user per-channel notification preferences.
|
||||
*
|
||||
* Only the *disabled* combinations are written: a missing row means enabled, so
|
||||
* copying the defaults across would bloat the table for no change in behaviour.
|
||||
*/
|
||||
export class Migration20200101011800_normalized_per_user_per_channel_notification_preferences extends Migration {
|
||||
override name = 'Migration20200101011800_normalized_per_user_per_channel_notification_preferences';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await this.execute(`
|
||||
CREATE TABLE IF NOT EXISTS notification_channel_preferences (
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
event_type TEXT NOT NULL,
|
||||
channel TEXT NOT NULL,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (user_id, event_type, channel)
|
||||
)
|
||||
`);
|
||||
await this.execute(`CREATE INDEX IF NOT EXISTS idx_ncp_user ON notification_channel_preferences(user_id)`);
|
||||
|
||||
// The old table does not exist on fresh installs.
|
||||
const oldPrefs = (await tableExists(this, 'notification_preferences'))
|
||||
? ((await this.execute(`SELECT * FROM notification_preferences`)) as Record<string, number>[])
|
||||
: [];
|
||||
|
||||
for (const row of oldPrefs) {
|
||||
const userId = row.user_id;
|
||||
const webhookEnabled = row.notify_webhook ?? 0;
|
||||
for (const [eventType, col] of Object.entries(EVENT_COLUMNS)) {
|
||||
const emailEnabled = row[col] ?? 1;
|
||||
if (!emailEnabled) {
|
||||
await this.execute(
|
||||
`INSERT OR IGNORE INTO notification_channel_preferences (user_id, event_type, channel, enabled) VALUES (?, ?, ?, ?)`,
|
||||
[userId, eventType, 'email', 0],
|
||||
);
|
||||
}
|
||||
if (!webhookEnabled) {
|
||||
await this.execute(
|
||||
`INSERT OR IGNORE INTO notification_channel_preferences (user_id, event_type, channel, enabled) VALUES (?, ?, ?, ?)`,
|
||||
[userId, eventType, 'webhook', 0],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copy the existing single-channel setting to the new plural key.
|
||||
await this.execute(`
|
||||
INSERT OR IGNORE INTO app_settings (key, value)
|
||||
SELECT 'notification_channels', value FROM app_settings WHERE key = 'notification_channel'
|
||||
`);
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 79 (`db/migrations.ts`).
|
||||
*
|
||||
* Migration 72: Drop the old notification_preferences table (data migrated to notification_channel_preferences in migration 71)
|
||||
*/
|
||||
export class Migration20200101011900_drop_the_old_notification_preferences_table extends Migration {
|
||||
override name = 'Migration20200101011900_drop_the_old_notification_preferences_table';
|
||||
|
||||
override up(): void {
|
||||
this.addSql(`DROP TABLE IF EXISTS notification_preferences`);
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 80 (`db/migrations.ts`).
|
||||
*
|
||||
* Migration 73: Add reservation_id to budget_items for linking budget entries to reservations
|
||||
*/
|
||||
export class Migration20200101012000_add_reservation_id_to_budget_items_for extends Migration {
|
||||
override name = 'Migration20200101012000_add_reservation_id_to_budget_items_for';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(
|
||||
this,
|
||||
'budget_items',
|
||||
'reservation_id',
|
||||
`reservation_id INTEGER REFERENCES reservations(id) ON DELETE SET NULL DEFAULT NULL`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 81 (`db/migrations.ts`).
|
||||
*
|
||||
* Migration 74: Add quantity to packing_items + user_id to packing_bags +
|
||||
* bag_members table. The single `user_id` becomes the first row of the new
|
||||
* membership table; the column itself stays for the steps that still read it.
|
||||
*/
|
||||
export class Migration20200101012100_add_quantity_to_packing_items_user_id extends Migration {
|
||||
override name = 'Migration20200101012100_add_quantity_to_packing_items_user_id';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(this, 'packing_items', 'quantity', `quantity INTEGER NOT NULL DEFAULT 1`);
|
||||
await addColumnIfMissing(
|
||||
this,
|
||||
'packing_bags',
|
||||
'user_id',
|
||||
`user_id INTEGER REFERENCES users(id) ON DELETE SET NULL DEFAULT NULL`,
|
||||
);
|
||||
|
||||
await this.execute(`
|
||||
CREATE TABLE IF NOT EXISTS packing_bag_members (
|
||||
bag_id INTEGER NOT NULL REFERENCES packing_bags(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (bag_id, user_id)
|
||||
)
|
||||
`);
|
||||
await this.execute(`CREATE INDEX IF NOT EXISTS idx_packing_bag_members_bag ON packing_bag_members(bag_id)`);
|
||||
|
||||
const bagsWithUser = (await this.execute(`SELECT id, user_id FROM packing_bags WHERE user_id IS NOT NULL`)) as {
|
||||
id: number;
|
||||
user_id: number;
|
||||
}[];
|
||||
for (const bag of bagsWithUser) {
|
||||
await this.execute(`INSERT OR IGNORE INTO packing_bag_members (bag_id, user_id) VALUES (?, ?)`, [
|
||||
bag.id,
|
||||
bag.user_id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/** A reservation that still carries the old single, trip-wide plan position. */
|
||||
interface PositionedReservation {
|
||||
id: number;
|
||||
trip_id: number;
|
||||
reservation_time: string | null;
|
||||
reservation_end_time: string | null;
|
||||
day_plan_position: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy migration step 82 (`db/migrations.ts`).
|
||||
*
|
||||
* Per-day positions for multi-day reservations: one global position becomes one
|
||||
* row per day the reservation spans, so a stay can sit in a different slot on
|
||||
* each of its days.
|
||||
*/
|
||||
export class Migration20200101012200_per_day_positions_for_multi_day_reservations extends Migration {
|
||||
override name = 'Migration20200101012200_per_day_positions_for_multi_day_reservations';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await this.execute(`
|
||||
CREATE TABLE IF NOT EXISTS reservation_day_positions (
|
||||
reservation_id INTEGER NOT NULL REFERENCES reservations(id) ON DELETE CASCADE,
|
||||
day_id INTEGER NOT NULL REFERENCES days(id) ON DELETE CASCADE,
|
||||
position REAL NOT NULL,
|
||||
PRIMARY KEY (reservation_id, day_id)
|
||||
)
|
||||
`);
|
||||
|
||||
const reservations = (await this.execute(
|
||||
`SELECT id, trip_id, reservation_time, reservation_end_time, day_plan_position FROM reservations WHERE day_plan_position IS NOT NULL`,
|
||||
)) as PositionedReservation[];
|
||||
|
||||
for (const r of reservations) {
|
||||
const startDate = r.reservation_time?.split('T')[0];
|
||||
if (!startDate) continue;
|
||||
const endDate = r.reservation_end_time?.split('T')[0] || startDate;
|
||||
|
||||
const matchingDays = (await this.execute(`SELECT id FROM days WHERE trip_id = ? AND date >= ? AND date <= ?`, [
|
||||
r.trip_id,
|
||||
startDate,
|
||||
endDate,
|
||||
])) as { id: number }[];
|
||||
|
||||
for (const day of matchingDays) {
|
||||
await this.execute(
|
||||
`INSERT OR IGNORE INTO reservation_day_positions (reservation_id, day_id, position) VALUES (?, ?, ?)`,
|
||||
[r.id, day.id, r.day_plan_position],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 83 (`db/migrations.ts`).
|
||||
*
|
||||
* Budget category ordering. Existing categories are seeded in alphabetical order
|
||||
* per trip, which is the order they were displayed in before this table existed.
|
||||
*/
|
||||
export class Migration20200101012300_budget_category_ordering extends Migration {
|
||||
override name = 'Migration20200101012300_budget_category_ordering';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await this.execute(`
|
||||
CREATE TABLE IF NOT EXISTS budget_category_order (
|
||||
trip_id INTEGER NOT NULL REFERENCES trips(id) ON DELETE CASCADE,
|
||||
category TEXT NOT NULL,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (trip_id, category)
|
||||
)
|
||||
`);
|
||||
|
||||
const rows = (await this.execute(
|
||||
`SELECT DISTINCT trip_id, category FROM budget_items ORDER BY trip_id, category`,
|
||||
)) as { trip_id: number; category: string }[];
|
||||
|
||||
let lastTripId = -1;
|
||||
let index = 0;
|
||||
for (const row of rows) {
|
||||
if (row.trip_id !== lastTripId) {
|
||||
lastTripId = row.trip_id;
|
||||
index = 0;
|
||||
}
|
||||
await this.execute(
|
||||
`INSERT OR IGNORE INTO budget_category_order (trip_id, category, sort_order) VALUES (?, ?, ?)`,
|
||||
[row.trip_id, row.category, index++],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { execBestEffort } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 84 (`db/migrations.ts`).
|
||||
*
|
||||
* Naver list import addon (default off).
|
||||
*/
|
||||
export class Migration20200101012400_naver_list_import_addon extends Migration {
|
||||
override name = 'Migration20200101012400_naver_list_import_addon';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await execBestEffort(
|
||||
this,
|
||||
`INSERT OR IGNORE INTO addons (id, name, description, type, icon, enabled, sort_order) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
['naver_list_import', 'Naver List Import', 'Import places from shared Naver Maps lists', 'trip', 'Link2', 0, 13],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 85 (`db/migrations.ts`).
|
||||
*
|
||||
* Migration: OAuth 2.1 clients, consents, and tokens for MCP
|
||||
*/
|
||||
export class Migration20200101012500_oauth_2 extends Migration {
|
||||
override name = 'Migration20200101012500_oauth_2';
|
||||
|
||||
override up(): void {
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS oauth_clients (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
client_id TEXT UNIQUE NOT NULL,
|
||||
client_secret_hash TEXT NOT NULL,
|
||||
redirect_uris TEXT NOT NULL DEFAULT '[]',
|
||||
allowed_scopes TEXT NOT NULL DEFAULT '[]',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS idx_oauth_clients_user ON oauth_clients(user_id)`);
|
||||
|
||||
this.addSql(`CREATE UNIQUE INDEX IF NOT EXISTS idx_oauth_clients_client_id ON oauth_clients(client_id)`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS oauth_consents (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
client_id TEXT NOT NULL REFERENCES oauth_clients(client_id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
scopes TEXT NOT NULL DEFAULT '[]',
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(client_id, user_id)
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS oauth_tokens (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
client_id TEXT NOT NULL REFERENCES oauth_clients(client_id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
access_token_hash TEXT UNIQUE NOT NULL,
|
||||
refresh_token_hash TEXT UNIQUE NOT NULL,
|
||||
scopes TEXT NOT NULL DEFAULT '[]',
|
||||
access_token_expires_at DATETIME NOT NULL,
|
||||
refresh_token_expires_at DATETIME NOT NULL,
|
||||
revoked_at DATETIME,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS idx_oauth_tokens_user ON oauth_tokens(user_id)`);
|
||||
|
||||
this.addSql(`CREATE UNIQUE INDEX IF NOT EXISTS idx_oauth_tokens_access ON oauth_tokens(access_token_hash)`);
|
||||
|
||||
this.addSql(`CREATE UNIQUE INDEX IF NOT EXISTS idx_oauth_tokens_refresh ON oauth_tokens(refresh_token_hash)`);
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 86 (`db/migrations.ts`).
|
||||
*
|
||||
* Migration: Refresh-token rotation chain tracking for replay detection
|
||||
*/
|
||||
export class Migration20200101012600_refresh_token_rotation_chain_tracking_for_replay extends Migration {
|
||||
override name = 'Migration20200101012600_refresh_token_rotation_chain_tracking_for_replay';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(
|
||||
this,
|
||||
'oauth_tokens',
|
||||
'parent_token_id',
|
||||
`parent_token_id INTEGER REFERENCES oauth_tokens(id)`,
|
||||
);
|
||||
|
||||
await this.execute(`CREATE INDEX IF NOT EXISTS idx_oauth_tokens_parent ON oauth_tokens(parent_token_id)`);
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { addColumnIfMissing } from '../migration-utils';
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* Legacy migration step 87 (`db/migrations.ts`).
|
||||
*
|
||||
* Migration: Public client support for browser-initiated dynamic registration (DCR)
|
||||
*/
|
||||
export class Migration20200101012700_public_client_support_for_browser_initiated_dynamic extends Migration {
|
||||
override name = 'Migration20200101012700_public_client_support_for_browser_initiated_dynamic';
|
||||
|
||||
override async up(): Promise<void> {
|
||||
await addColumnIfMissing(this, 'oauth_clients', 'is_public', `is_public INTEGER NOT NULL DEFAULT 0`);
|
||||
|
||||
await addColumnIfMissing(this, 'oauth_clients', 'created_via', `created_via TEXT NOT NULL DEFAULT 'settings_ui'`);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user