From 9c22dd1605737b096df0b7c7566614203dea96cc Mon Sep 17 00:00:00 2001 From: bh0fer Date: Sat, 12 Sep 2026 12:57:06 +0000 Subject: [PATCH 01/12] setup integration testing ai-assisted setup --- .devcontainer/devcontainer.json | 6 +- .devcontainer/docker-compose.yml | 1 + package.json | 10 +- src/tests/integration/documentRoots.test.ts | 64 +++ src/tests/integration/documents.test.ts | 85 +++ src/tests/integration/globalSetup.ts | 37 ++ src/tests/integration/helpers.ts | 49 ++ src/tests/integration/setup.ts | 37 ++ vitest.config.ts | 23 + yarn.lock | 551 +++++++++++++++++++- 10 files changed, 844 insertions(+), 19 deletions(-) create mode 100644 src/tests/integration/documentRoots.test.ts create mode 100644 src/tests/integration/documents.test.ts create mode 100644 src/tests/integration/globalSetup.ts create mode 100644 src/tests/integration/helpers.ts create mode 100644 src/tests/integration/setup.ts create mode 100644 vitest.config.ts diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index f146668..fa758d1 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -32,7 +32,8 @@ "ms-ossdata.vscode-pgsql", "GitHub.copilot", "GitHub.vscode-pull-request-github", - "Gruntfuggly.todo-tree" + "Gruntfuggly.todo-tree", + "vitest.explorer" ], "settings": { "todo-tree.ripgrep.ripgrep": "/usr/bin/rg", @@ -67,7 +68,8 @@ } }, "remoteEnv": { - "DATABASE_URL": "postgresql://postgres:postgres@db:5432/teaching_api?schema=public" + "DATABASE_URL": "postgresql://postgres:postgres@db:5432/teaching_api?schema=public", + "TEST_DATABASE_URL": "postgresql://postgres:postgres@db:5432/teaching_api_test?schema=public" } // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. // "remoteUser": "root" diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index e167c6e..8fadb2e 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -15,6 +15,7 @@ services: environment: - NODE_ENV=development - DATABASE_URL=postgresql://postgres:postgres@db:5432/teaching_api?schema=public + - TEST_DATABASE_URL=postgresql://postgres:postgres@db:5432/teaching_api_test?schema=public - PORT=3002 # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function. diff --git a/package.json b/package.json index 44421d3..ead72a7 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,8 @@ "db:seed": "yarn prisma db seed", "db:reset": "tsx prisma/reset.ts", "db:recreate": "yarn run db:reset && yarn run db:migrate && yarn run db:seed", + "test": "vitest run", + "test:watch": "vitest", "sentry:sourcemaps": "sentry-cli sourcemaps inject --org $SENTRY_ORG --project $SENTRY_PROJECT ./dist && sentry-cli sourcemaps upload --org $SENTRY_ORG --project $SENTRY_PROJECT ./dist" }, "dependencies": { @@ -46,6 +48,7 @@ "@types/morgan": "^1.9.10", "@types/node": "^25.0.3", "@types/pg": "^8.20.0", + "@types/supertest": "^7.2.1", "@typescript-eslint/eslint-plugin": "^8.63.0", "@typescript-eslint/parser": "^8.63.0", "dotenv-cli": "^11.0.0", @@ -60,12 +63,15 @@ "prisma-dbml-generator": "^0.12.0", "prisma-docs-generator": "^0.8.0", "prisma-erd-generator": "^2.4.3", + "supertest": "^7.2.2", "tsconfig-paths": "^4.2.0", "tsx": "^4.23.0", - "typescript": "^7.0.2" + "typescript": "^7.0.2", + "vite": "^8.3.0", + "vitest": "^5.0.0" }, "engines": { - "node": "24.16.x || 24.18.x" + "node": "24.16.x || 24.18.x || 24.21.x" }, "optionalDependencies": { "puppeteer": "^24.34.0" diff --git a/src/tests/integration/documentRoots.test.ts b/src/tests/integration/documentRoots.test.ts new file mode 100644 index 0000000..3f16f03 --- /dev/null +++ b/src/tests/integration/documentRoots.test.ts @@ -0,0 +1,64 @@ +import { randomUUID } from 'crypto'; +import request from 'supertest'; +import { afterEach, describe, expect, it } from 'vitest'; +import { Access } from '../../../prisma/generated/enums.js'; +import app from '../../app.js'; +import { Role } from '../../models/User.js'; +import { API_URL, agentAs, createTestUser, deleteTestDocumentRoots, deleteTestUsers } from './helpers.js'; + +describe('DocumentRoots (integration)', () => { + const userIds: string[] = []; + const documentRootIds: string[] = []; + + afterEach(async () => { + await deleteTestDocumentRoots(...documentRootIds.splice(0)); + await deleteTestUsers(...userIds.splice(0)); + }); + + it('lets a student create and fetch a document root', async () => { + const user = await createTestUser(Role.STUDENT); + userIds.push(user.id); + const agent = agentAs(user.id); + + const documentRootId = randomUUID(); + documentRootIds.push(documentRootId); + + const createRes = await agent + .post(`${API_URL}/documentRoots/${documentRootId}`) + .send({ access: Access.RW_DocumentRoot }); + + expect(createRes.status).toBe(200); + expect(createRes.body.id).toBe(documentRootId); + expect(createRes.body.access).toBe(Access.RW_DocumentRoot); + + const getRes = await agent.get(`${API_URL}/documentRoots/${documentRootId}`); + expect(getRes.status).toBe(200); + expect(getRes.body.id).toBe(documentRootId); + expect(getRes.body.documents).toEqual([]); + }); + + it('rejects unauthenticated requests', async () => { + const documentRootId = randomUUID(); + const res = await request(app).get(`${API_URL}/documentRoots/${documentRootId}`); + expect(res.status).toBe(401); + }); + + it('only allows an admin to delete a document root', async () => { + const student = await createTestUser(Role.STUDENT); + const admin = await createTestUser(Role.ADMIN); + userIds.push(student.id, admin.id); + + const documentRootId = randomUUID(); + documentRootIds.push(documentRootId); + await agentAs(student.id) + .post(`${API_URL}/documentRoots/${documentRootId}`) + .send({ access: Access.RW_DocumentRoot }); + + const forbidden = await agentAs(student.id).delete(`${API_URL}/documentRoots/${documentRootId}`); + expect(forbidden.status).toBe(403); + + const ok = await agentAs(admin.id).delete(`${API_URL}/documentRoots/${documentRootId}`); + expect(ok.status).toBe(200); + documentRootIds.length = 0; // already deleted + }); +}); diff --git a/src/tests/integration/documents.test.ts b/src/tests/integration/documents.test.ts new file mode 100644 index 0000000..f03cc4a --- /dev/null +++ b/src/tests/integration/documents.test.ts @@ -0,0 +1,85 @@ +import { randomUUID } from 'crypto'; +import { afterEach, describe, expect, it } from 'vitest'; +import { Access } from '../../../prisma/generated/enums.js'; +import { Role } from '../../models/User.js'; +import { API_URL, agentAs, createTestUser, deleteTestDocumentRoots, deleteTestUsers } from './helpers.js'; + +describe('Documents (integration)', () => { + const userIds: string[] = []; + const documentRootIds: string[] = []; + + afterEach(async () => { + // deleting the document root cascades and removes documents created on it + await deleteTestDocumentRoots(...documentRootIds.splice(0)); + await deleteTestUsers(...userIds.splice(0)); + }); + + it('creates, reads, updates and deletes a document', async () => { + const user = await createTestUser(Role.STUDENT); + userIds.push(user.id); + const agent = agentAs(user.id); + + const documentRootId = randomUUID(); + documentRootIds.push(documentRootId); + await agent + .post(`${API_URL}/documentRoots/${documentRootId}`) + .send({ access: Access.RW_DocumentRoot }); + + const createRes = await agent.post(`${API_URL}/documents`).send({ + type: 'test-type', + documentRootId, + data: { foo: 'bar' } + }); + expect(createRes.status).toBe(200); + expect(createRes.body.documentRootId).toBe(documentRootId); + expect(createRes.body.authorId).toBe(user.id); + expect(createRes.body.data).toEqual({ foo: 'bar' }); + const documentId = createRes.body.id as string; + + const getRes = await agent.get(`${API_URL}/documents/${documentId}`); + expect(getRes.status).toBe(200); + // GET /documents/:id returns { document, highestPermission } + expect(getRes.body.document.data).toEqual({ foo: 'bar' }); + + const updateRes = await agent + .put(`${API_URL}/documents/${documentId}`) + .send({ data: { foo: 'baz' } }); + expect(updateRes.status).toBe(204); + + const getAfterUpdate = await agent.get(`${API_URL}/documents/${documentId}`); + expect(getAfterUpdate.body.document.data).toEqual({ foo: 'baz' }); + + const deleteRes = await agent.delete(`${API_URL}/documents/${documentId}`); + expect(deleteRes.status).toBe(204); + + const getAfterDelete = await agent.get(`${API_URL}/documents/${documentId}`); + expect(getAfterDelete.status).toBe(200); + expect(getAfterDelete.body).toBeNull(); + }); + + it('does not allow a user without access to read another users document data', async () => { + const owner = await createTestUser(Role.STUDENT); + const stranger = await createTestUser(Role.STUDENT); + userIds.push(owner.id, stranger.id); + + const documentRootId = randomUUID(); + documentRootIds.push(documentRootId); + await agentAs(owner.id) + .post(`${API_URL}/documentRoots/${documentRootId}`) + .send({ access: Access.RW_DocumentRoot, sharedAccess: Access.None_DocumentRoot }); + + const createRes = await agentAs(owner.id) + .post(`${API_URL}/documents`) + .send({ + type: 'test-type', + documentRootId, + data: { secret: true } + }); + expect(createRes.status).toBe(200); + const documentId = createRes.body.id as string; + + const strangerRes = await agentAs(stranger.id).get(`${API_URL}/documents/${documentId}`); + expect(strangerRes.status).toBe(200); + expect(strangerRes.body).toBeNull(); + }); +}); diff --git a/src/tests/integration/globalSetup.ts b/src/tests/integration/globalSetup.ts new file mode 100644 index 0000000..bb5606b --- /dev/null +++ b/src/tests/integration/globalSetup.ts @@ -0,0 +1,37 @@ +import { execSync } from 'child_process'; +import { Client } from 'pg'; + +/** + * Runs once before the whole test suite: makes sure the dedicated test + * database exists and has all migrations (incl. views) applied. + */ +const TEST_DATABASE_URL = process.env.TEST_DATABASE_URL || process.env.DATABASE_URL; + +const ensureDatabaseExists = async (databaseUrl: string) => { + const url = new URL(databaseUrl); + const dbName = url.pathname.replace(/^\//, ''); + const adminUrl = new URL(databaseUrl); + adminUrl.pathname = '/postgres'; + + const client = new Client({ connectionString: adminUrl.toString() }); + await client.connect(); + try { + const { rowCount } = await client.query('SELECT 1 FROM pg_database WHERE datname = $1', [dbName]); + if (rowCount === 0) { + await client.query(`CREATE DATABASE "${dbName}"`); + } + } finally { + await client.end(); + } +}; + +export default async function globalSetup() { + if (!TEST_DATABASE_URL) { + throw new Error('TEST_DATABASE_URL (or DATABASE_URL) must be set to run the integration tests'); + } + await ensureDatabaseExists(TEST_DATABASE_URL); + execSync('yarn prisma migrate deploy', { + stdio: 'inherit', + env: { ...process.env, DATABASE_URL: TEST_DATABASE_URL } + }); +} diff --git a/src/tests/integration/helpers.ts b/src/tests/integration/helpers.ts new file mode 100644 index 0000000..ccab524 --- /dev/null +++ b/src/tests/integration/helpers.ts @@ -0,0 +1,49 @@ +import request from 'supertest'; +import { randomUUID } from 'crypto'; +import app from '../../app.js'; +import prisma from '../../prisma.js'; +import { Role } from '../../models/User.js'; + +export const API_URL = '/api/v1'; + +/** + * Builds a supertest agent that authenticates as the given user by setting the + * `x-test-user-id` header, which is picked up by the mocked auth session in ./setup.ts. + */ +export const agentAs = (userId: string) => { + const withHeader = (req: request.Test) => req.set('x-test-user-id', userId); + return { + get: (url: string) => withHeader(request(app).get(url)), + post: (url: string) => withHeader(request(app).post(url)), + put: (url: string) => withHeader(request(app).put(url)), + delete: (url: string) => withHeader(request(app).delete(url)) + }; +}; + +export const createTestUser = async (role: Role = Role.STUDENT) => { + const id = randomUUID(); + return prisma.user.create({ + data: { + id, + email: `${id}@test.gbsl.ch`, + firstName: 'Test', + lastName: 'User', + name: 'Test User', + role + } + }); +}; + +export const deleteTestUsers = async (...ids: string[]) => { + if (ids.length === 0) { + return; + } + await prisma.user.deleteMany({ where: { id: { in: ids } } }); +}; + +export const deleteTestDocumentRoots = async (...ids: string[]) => { + if (ids.length === 0) { + return; + } + await prisma.documentRoot.deleteMany({ where: { id: { in: ids } } }); +}; diff --git a/src/tests/integration/setup.ts b/src/tests/integration/setup.ts new file mode 100644 index 0000000..f49eb55 --- /dev/null +++ b/src/tests/integration/setup.ts @@ -0,0 +1,37 @@ +import { afterAll, vi } from 'vitest'; +import prisma from '../../prisma.js'; + +/** + * The real auth flow relies on better-auth cookies/sessions. For integration tests we + * bypass that and resolve the acting user directly from the `x-test-user-id` header, + * see `agentAs` in ./helpers.ts. + */ +vi.mock('../../auth.js', () => ({ + auth: { + api: { + getSession: async ({ headers }: { headers: Headers }) => { + const userId = headers.get('x-test-user-id'); + if (!userId) { + return null; + } + const { default: testPrisma } = await import('../../prisma.js'); + const user = await testPrisma.user.findUnique({ where: { id: userId } }); + if (!user) { + return null; + } + return { user, session: { id: 'test-session', userId: user.id } }; + } + } + } +})); + +// socket.io is not started in the test process, so notifications are no-ops +vi.mock('../../socketIoServer.js', () => ({ + initialize: vi.fn(), + getIo: vi.fn(), + notify: vi.fn() +})); + +afterAll(async () => { + await prisma.$disconnect(); +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..45d12fd --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from 'vitest/config'; + +// integration tests run against a dedicated database (TEST_DATABASE_URL) so the +// development database is never touched by the test suite. +const testDatabaseUrl = process.env.TEST_DATABASE_URL || process.env.DATABASE_URL; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['src/tests/**/*.test.ts'], + globalSetup: ['./src/tests/integration/globalSetup.ts'], + setupFiles: ['./src/tests/integration/setup.ts'], + // the tests share one database, so they must not run in parallel + fileParallelism: false, + testTimeout: 20_000, + hookTimeout: 30_000, + env: { + NODE_ENV: 'test', + DATABASE_URL: testDatabaseUrl + } + } +}); diff --git a/yarn.lock b/yarn.lock index c7f317a..899cfb3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -490,11 +490,29 @@ dependencies: "@swc/helpers" "^0.5.0" +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz#f4c663e862f06dc98ca4d453862c46902789a18d" + integrity sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw== + "@jridgewell/sourcemap-codec@^1.5.5": version "1.5.5" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== +"@jridgewell/trace-mapping@0.3.31": + version "0.3.31" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0" + integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + "@kurkle/color@^0.3.0": version "0.3.4" resolved "https://registry.yarnpkg.com/@kurkle/color/-/color-0.3.4.tgz#4d4ff677e1609214fc71c580125ddddd86abcabf" @@ -623,6 +641,11 @@ resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-2.2.0.tgz#84fb45ac9332925d643b80f89ceb0ea2f21dba95" integrity sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA== +"@noble/hashes@^1.1.5": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.8.0.tgz#cee43d801fcef9644b11b8194857695acd5f815a" + integrity sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A== + "@noble/hashes@^2.0.1": version "2.2.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-2.2.0.tgz#22da1d16a469954fce877055d559900a6c73b63b" @@ -719,6 +742,18 @@ resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.42.0.tgz#38f10edd26f02931bf3b7d664ba59bea6279a384" integrity sha512-icc5xCzndZfhuJMy5oqk5AvloWquR7jtae74qzpkKkhGp8BivK+oCcEXgGnjCdTfp8hA44l+w8gE8yYJbocJJw== +"@oxc-project/types@=0.149.0": + version "0.149.0" + resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.149.0.tgz#328c1d19403980199c869676871db9ed46932ff8" + integrity sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA== + +"@paralleldrive/cuid2@^2.2.2": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz#3d62ea9e7be867d3fa94b9897fab5b0ae187d784" + integrity sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw== + dependencies: + "@noble/hashes" "^1.1.5" + "@pkgr/core@^0.3.6": version "0.3.6" resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.3.6.tgz#3569708bd4be4d8870ba32bf1c456dac81600d97" @@ -1198,6 +1233,86 @@ resolved "https://registry.yarnpkg.com/@react-types/shared/-/shared-3.36.0.tgz#52e713c6bae8e117967bf1d19d89db3a56219037" integrity sha512-DkP/H0C2YjjS7gZWKNqOmU8a16qHPjQNdzMwmTq9SzplM6Iw0kVMTZ0OIoe6FOgGqa+FwMsE2QbPjh/n3g/jXQ== +"@rolldown/binding-android-arm-eabi@1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.8.tgz#55f0a8e97eb87a0873ea1466d2b4af9510739589" + integrity sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw== + +"@rolldown/binding-android-arm64@1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.8.tgz#3b783ee41120b2fefac41bd8e6d711f4ce7f82a7" + integrity sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ== + +"@rolldown/binding-darwin-arm64@1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.8.tgz#56c27646e8faae70aa56065a3b686b1d7de9d8b4" + integrity sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA== + +"@rolldown/binding-darwin-x64@1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.8.tgz#3b9da1032c897ce191cb23601dbf9887ce6895ae" + integrity sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA== + +"@rolldown/binding-freebsd-x64@1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.8.tgz#43d4257704b15204aa42059e36a638b63c0e402d" + integrity sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ== + +"@rolldown/binding-linux-arm-gnueabihf@1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.8.tgz#d95b9fe9f04f2278cb2eef98e2cf20199c3fe5d0" + integrity sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q== + +"@rolldown/binding-linux-arm64-gnu@1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.8.tgz#07f33840b650b6a3f5954ccfc43b13629374182a" + integrity sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew== + +"@rolldown/binding-linux-arm64-musl@1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.8.tgz#357922929ec19c05f6d8eabd0899a053bab1a08b" + integrity sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA== + +"@rolldown/binding-linux-ppc64-gnu@1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.8.tgz#0bc7b29262d3022d8cf57876d70d819387bd3dc9" + integrity sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA== + +"@rolldown/binding-linux-s390x-gnu@1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.8.tgz#3a3d653becc48975025b8922056822662b7d4414" + integrity sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ== + +"@rolldown/binding-linux-x64-gnu@1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.8.tgz#c14ec9af7dbc2b1e459b36e292d57b27565f6737" + integrity sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g== + +"@rolldown/binding-linux-x64-musl@1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.8.tgz#40c2888d1ac87d9e39c6fc6799c060510e7a4d69" + integrity sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA== + +"@rolldown/binding-openharmony-arm64@1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.8.tgz#3e021f4e77283ca111c709d3ed6775419f471056" + integrity sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg== + +"@rolldown/binding-win32-arm64-msvc@1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.8.tgz#32b6c6a335ad14f97025bb2bc90135222405bb76" + integrity sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A== + +"@rolldown/binding-win32-x64-msvc@1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.8.tgz#df4e965896c61411b96566abf345f46141f498c4" + integrity sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng== + +"@rolldown/pluginutils@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz#e3fcee093fbb5ce765e1ad088ff4de2889f6f9be" + integrity sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw== + "@sentry/cli-darwin@3.6.0": version "3.6.0" resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-3.6.0.tgz#8186789f4cd8c14251f290a0ad27221dcccfa4c7" @@ -1364,6 +1479,14 @@ "@types/connect" "*" "@types/node" "*" +"@types/chai@^5.2.2": + version "5.2.3" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-5.2.3.tgz#8e9cd9e1c3581fa6b341a5aed5588eb285be0b4a" + integrity sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA== + dependencies: + "@types/deep-eql" "*" + assertion-error "^2.0.1" + "@types/connect@*": version "3.4.38" resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" @@ -1371,6 +1494,11 @@ dependencies: "@types/node" "*" +"@types/cookiejar@^2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.5.tgz#14a3e83fa641beb169a2dd8422d91c3c345a9a78" + integrity sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q== + "@types/cors@^2.8.12", "@types/cors@^2.8.19": version "2.8.19" resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.19.tgz#d93ea2673fd8c9f697367f5eeefc2bbfa94f0342" @@ -1602,12 +1730,17 @@ dependencies: "@types/ms" "*" +"@types/deep-eql@*": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/deep-eql/-/deep-eql-4.0.2.tgz#334311971d3a07121e7eb91b684a605e7eea9cbd" + integrity sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw== + "@types/esrecurse@^4.3.1": version "4.3.1" resolved "https://registry.yarnpkg.com/@types/esrecurse/-/esrecurse-4.3.1.tgz#6f636af962fbe6191b830bd676ba5986926bccec" integrity sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw== -"@types/estree@^1.0.6", "@types/estree@^1.0.8": +"@types/estree@^1.0.0", "@types/estree@^1.0.6", "@types/estree@^1.0.8": version "1.0.9" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.9.tgz#cf3f0e876d7bee15a93ab925b82bf570a3904a24" integrity sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg== @@ -1651,6 +1784,11 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== +"@types/methods@^1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@types/methods/-/methods-1.1.4.tgz#d3b7ac30ac47c91054ea951ce9eed07b1051e547" + integrity sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ== + "@types/minimist@^1.2.0": version "1.2.5" resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.5.tgz#ec10755e871497bcd83efe927e43ec46e8c0747e" @@ -1726,6 +1864,24 @@ "@types/http-errors" "*" "@types/node" "*" +"@types/superagent@^8.1.0": + version "8.1.11" + resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-8.1.11.tgz#14da75aa2f916dcdd6fb2a90a8fb24a9a1a86d08" + integrity sha512-KA7srSW/HENDtOw9DOqaFLgWuMqN9WgjEw62lh9dpvRaZDkhdOkazASd7X7i2eMUYLHa1U37ZttnePsH5zTDHw== + dependencies: + "@types/cookiejar" "^2.1.5" + "@types/methods" "^1.1.4" + "@types/node" "*" + form-data "^4.0.0" + +"@types/supertest@^7.2.1": + version "7.2.1" + resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-7.2.1.tgz#165e99f10fd652027cf1eaa74b55081b6daa0965" + integrity sha512-4CbBvoYVLHL7+yhbYrZET0vsvuyXTC05aRe7dNQkwMzm56auceoy6Yu3K50uZmwfHna1os3CMSgM/3QVkUtPTw== + dependencies: + "@types/methods" "^1.1.4" + "@types/superagent" "^8.1.0" + "@types/triple-beam@^1.3.2": version "1.3.5" resolved "https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.5.tgz#74fef9ffbaa198eb8b588be029f38b00299caa2c" @@ -1954,6 +2110,21 @@ d3-selection "^3.0.0" d3-transition "^3.0.1" +"@vitest/mocker@5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-5.0.0.tgz#06455717273dc13b92ad4eca64e4e501d76f533d" + integrity sha512-66PGTMIiVJP3t4a5yxU9qPtf7MdTBs8jmToMvy+HVflB3Yy13WJZTtPePdvU+wjRV02SKK5doLbSA6o9pwOmiA== + dependencies: + "@jridgewell/trace-mapping" "0.3.31" + "@vitest/spy" "5.0.0" + estree-walker "^3.0.3" + magic-string "^1.2.3" + +"@vitest/spy@5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-5.0.0.tgz#4ca353a880fcabae3eabf558126ae8f5542ccbfd" + integrity sha512-uy+luWBAPw9XfthoHi5AkfHUnuPYEESjl0p/r+meoBnU8bxg5GDQ3Ey8MjcJ6sqahkL4PFyrvfMJJBw7LbU06g== + "@xmldom/is-dom-node@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@xmldom/is-dom-node/-/is-dom-node-1.0.1.tgz#83b9f3e1260fb008061c6fa787b93a00f9be0629" @@ -2160,6 +2331,11 @@ arrify@^1.0.1: resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" integrity sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA== +asap@^2.0.0: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== + asn1@^0.2.4: version "0.2.6" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" @@ -2167,6 +2343,11 @@ asn1@^0.2.4: dependencies: safer-buffer "~2.1.0" +assertion-error@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7" + integrity sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA== + ast-types@^0.13.4: version "0.13.4" resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.13.4.tgz#ee0d77b343263965ecc3fb62da16e7222b2b6782" @@ -2189,6 +2370,11 @@ async@^3.2.3: resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + aws-ssl-profiles@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz#157dd77e9f19b1d123678e93f120e6f193022641" @@ -2453,6 +2639,11 @@ camelcase@^5.3.1: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== +chai@^6.2.2: + version "6.2.2" + resolved "https://registry.yarnpkg.com/chai/-/chai-6.2.2.tgz#ae41b52c9aca87734505362717f3255facda360e" + integrity sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg== + chalk@^5.0.1: version "5.6.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.6.2.tgz#b1238b6e23ea337af71c7f8a295db5af0c158aea" @@ -2591,6 +2782,13 @@ color@^5.0.2: color-convert "^3.1.3" color-string "^2.1.3" +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + commander@7: version "7.2.0" resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" @@ -2611,6 +2809,11 @@ commondir@^1.0.1: resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== +component-emitter@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.1.tgz#ef1d5796f7d93f135ee6fb684340b26403c97d17" + integrity sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ== + compress-commons@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-4.1.2.tgz#6542e59cb63e1f46a8b21b0e06f9a32e4c8b06df" @@ -2653,7 +2856,7 @@ content-type@^2.0.0: resolved "https://registry.yarnpkg.com/content-type/-/content-type-2.0.0.tgz#2fb3ede69dffa0af78ca7c4ce7589680638b56df" integrity sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ== -cookie-signature@^1.2.1: +cookie-signature@^1.2.1, cookie-signature@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.2.2.tgz#57c7fc3cc293acab9fec54d73e15690ebe4a1793" integrity sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg== @@ -2668,6 +2871,11 @@ cookie@^0.7.1, cookie@~0.7.1, cookie@~0.7.2: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== +cookiejar@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b" + integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw== + core-util-is@~1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" @@ -3064,7 +3272,7 @@ debug@2.6.9: dependencies: ms "2.0.0" -debug@4, debug@^4, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5, debug@^4.4.0, debug@^4.4.1, debug@^4.4.3, debug@~4.4.1: +debug@4, debug@^4, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5, debug@^4.3.7, debug@^4.4.0, debug@^4.4.1, debug@^4.4.3, debug@~4.4.1: version "4.4.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== @@ -3136,6 +3344,11 @@ delaunator@5: dependencies: robust-predicates "^3.0.2" +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + denque@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" @@ -3156,11 +3369,24 @@ destroy@1.2.0, destroy@~1.2.0: resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== +detect-libc@^2.0.3: + version "2.1.2" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad" + integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ== + devtools-protocol@0.0.1608973: version "0.0.1608973" resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1608973.tgz#56e0a2a999b06d416ee928ca06aeba95a5880515" integrity sha512-Tpm17fxYzt+J7VrGdc1k8YdRqS3YV7se/M6KeemEqvUbq/n7At1rWVuXMxQgpWkdwSdIEKYbU//Bve+Shm4YNQ== +dezalgo@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81" + integrity sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig== + dependencies: + asap "^2.0.0" + wrappy "1" + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -3314,6 +3540,11 @@ es-module-lexer@^2.1.0, es-module-lexer@^2.2.0: resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-2.3.0.tgz#fda770234c345064c122eb905e1c4200ffa4ce7e" integrity sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw== +es-module-lexer@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-2.3.2.tgz#311fa4f40168c1975c505477c51b23234d41ad55" + integrity sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw== + es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz#a2d0b373205724dfa525d23b0c3e1b1ca582c99b" @@ -3321,6 +3552,16 @@ es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: dependencies: es-errors "^1.3.0" +es-set-tostringtag@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" + integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== + dependencies: + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + es-toolkit@^1.45.1, es-toolkit@^1.49.0: version "1.49.0" resolved "https://registry.yarnpkg.com/es-toolkit/-/es-toolkit-1.49.0.tgz#93c5b031865792fc03cbf5bd20c132a4f976a52a" @@ -3486,6 +3727,13 @@ estraverse@^5.1.0, estraverse@^5.2.0: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== +estree-walker@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d" + integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g== + dependencies: + "@types/estree" "^1.0.0" + esutils@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" @@ -3518,6 +3766,11 @@ execa@5.1.1: signal-exit "^3.0.3" strip-final-newline "^2.0.0" +expect-type@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-1.4.0.tgz#24edf7f0cc69a44d008567ba4594ab96f3c3a3d6" + integrity sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA== + express@^4.18.2: version "4.22.2" resolved "https://registry.yarnpkg.com/express/-/express-4.22.2.tgz#c17ae0981e5efc24b22272f0e041c4662503b700" @@ -3648,6 +3901,11 @@ fast-levenshtein@^2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== +fast-safe-stringify@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" + integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== + fast-uri@^3.0.1: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.3.tgz#f695a40f006aba505631573a0021ddb21194ad11" @@ -3792,6 +4050,26 @@ foreground-child@3.3.1: cross-spawn "^7.0.6" signal-exit "^4.0.1" +form-data@^4.0.0, form-data@^4.0.5: + version "4.0.6" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.6.tgz#28e864e1b786dbebb68db1f452f9635278665827" + integrity sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + es-set-tostringtag "^2.1.0" + hasown "^2.0.4" + mime-types "^2.1.35" + +formidable@^3.5.4: + version "3.5.4" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-3.5.4.tgz#ac9a593b951e829b3298f21aa9a2243932f32ed9" + integrity sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug== + dependencies: + "@paralleldrive/cuid2" "^2.2.2" + dezalgo "^1.0.4" + once "^1.4.0" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -3860,7 +4138,7 @@ get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.2.5, get-intrinsic@^1.3.0: +get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== @@ -4011,11 +4289,18 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-symbols@^1.1.0: +has-symbols@^1.0.3, has-symbols@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== +has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + hasha@5.2.2: version "5.2.2" resolved "https://registry.yarnpkg.com/hasha/-/hasha-5.2.2.tgz#a48477989b3b327aea3c04f53096d816d97522a1" @@ -4024,7 +4309,7 @@ hasha@5.2.2: is-stream "^2.0.0" type-fest "^0.8.0" -hasown@^2.0.2, hasown@^2.0.3: +hasown@^2.0.2, hasown@^2.0.3, hasown@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.4.tgz#8c62d8cb90beb2aad5d0a5b67581ad9854c3f003" integrity sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A== @@ -4479,6 +4764,80 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +lightningcss-android-arm64@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz#9a6841f88ae50fc83502903892b41af41bc2b907" + integrity sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg== + +lightningcss-darwin-arm64@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz#c0f2c31c0bfd19fa4dd3f18e957a1f1a152097d6" + integrity sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg== + +lightningcss-darwin-x64@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz#cb0705965acb538c6683949ce6925fb3cdf7c361" + integrity sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ== + +lightningcss-freebsd-x64@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz#763538828b26bab2680dadafcc84ee78b0eb502b" + integrity sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg== + +lightningcss-linux-arm-gnueabihf@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz#6862e3176a331aedbdec1ed352b4d7d0dd0784de" + integrity sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ== + +lightningcss-linux-arm64-gnu@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz#c6a3a2ed15141daf6bdc2628930f8e39bdf473aa" + integrity sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg== + +lightningcss-linux-arm64-musl@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz#7fa1334971fc82845f9827df6ef8a0b20914bac6" + integrity sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ== + +lightningcss-linux-x64-gnu@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz#8b927862ea8c2bbc6831a46509244b50d9936e55" + integrity sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg== + +lightningcss-linux-x64-musl@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz#0c525bb077dfd94404c059cfe42dad797e96aeaf" + integrity sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw== + +lightningcss-win32-arm64-msvc@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz#850ee1103dac989cfab50e3ac22d1a69e394e63d" + integrity sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA== + +lightningcss-win32-x64-msvc@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz#e343ae152eed3609dc6e11949d1a3bf39a1c946f" + integrity sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA== + +lightningcss@^1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss/-/lightningcss-1.33.0.tgz#c08867d71a79385c6e190214fd72fef3e5f95f0b" + integrity sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA== + dependencies: + detect-libc "^2.0.3" + optionalDependencies: + lightningcss-android-arm64 "1.33.0" + lightningcss-darwin-arm64 "1.33.0" + lightningcss-darwin-x64 "1.33.0" + lightningcss-freebsd-x64 "1.33.0" + lightningcss-linux-arm-gnueabihf "1.33.0" + lightningcss-linux-arm64-gnu "1.33.0" + lightningcss-linux-arm64-musl "1.33.0" + lightningcss-linux-x64-gnu "1.33.0" + lightningcss-linux-x64-musl "1.33.0" + lightningcss-win32-arm64-msvc "1.33.0" + lightningcss-win32-x64-msvc "1.33.0" + lines-and-columns@^1.1.6: version "1.2.4" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" @@ -4569,6 +4928,13 @@ magic-string@^0.30.21, magic-string@~0.30.0: dependencies: "@jridgewell/sourcemap-codec" "^1.5.5" +magic-string@^1.2.3: + version "1.3.1" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-1.3.1.tgz#47ce661c2d5ce577a64c9bd6204828b435ecc822" + integrity sha512-rm91zr2Ou+XueDTohjQQjdQEcYM6zVi8KVUCG8Ec3vHwUEKrhSdCNyfuIywkA6hcCAteIn0ZOtAHA6eGpiX+Pg== + dependencies: + "@jridgewell/sourcemap-codec" "^1.6.0" + make-dir@3.1.0, make-dir@^3.0.0, make-dir@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" @@ -4681,7 +5047,7 @@ mermaid@^11.14.0: ts-dedent "^2.2.0" uuid "^11.1.0 || ^12 || ^13 || ^14.0.0" -methods@~1.1.2: +methods@^1.1.2, methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== @@ -4704,6 +5070,13 @@ mime-db@^1.54.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5" integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== +mime-types@^2.1.35, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + mime-types@^3.0.0, mime-types@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-3.0.2.tgz#39002d4182575d5af036ffa118100f2524b2e2ab" @@ -4711,18 +5084,16 @@ mime-types@^3.0.0, mime-types@^3.0.2: dependencies: mime-db "^1.54.0" -mime-types@~2.1.24, mime-types@~2.1.34: - version "2.1.35" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" - mime@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== +mime@2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== + mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" @@ -4826,6 +5197,11 @@ named-placeholders@^1.1.3: dependencies: lru.min "^1.1.0" +nanoid@^3.3.18: + version "3.3.19" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.19.tgz#336d4aa4bcd4fb24d2cddede7ffeae40bec03f0a" + integrity sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug== + nanostores@^1.1.1: version "1.4.0" resolved "https://registry.yarnpkg.com/nanostores/-/nanostores-1.4.0.tgz#9acc8a6026533dc0410c37cb3b076db00163d277" @@ -4957,6 +5333,11 @@ object-inspect@^1.13.3, object-inspect@^1.13.4: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== +obug@^2.1.4: + version "2.2.1" + resolved "https://registry.yarnpkg.com/obug/-/obug-2.2.1.tgz#9c453efeeda822050eb9774242e969c784d64d78" + integrity sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q== + ohash@^2.0.11: version "2.0.11" resolved "https://registry.yarnpkg.com/ohash/-/ohash-2.0.11.tgz#60b11e8cff62ca9dee88d13747a5baa145f5900b" @@ -5262,6 +5643,11 @@ picomatch@^4.0.4: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.5.tgz#51ea57a17d86f605f81039595fbc40ed06a55fab" integrity sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A== +picomatch@^4.0.7: + version "4.0.7" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.7.tgz#6313360034ccb36b3dc61ecbdff78121f90fe21f" + integrity sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA== + pkg-dir@^4.1.0: version "4.2.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" @@ -5296,6 +5682,15 @@ points-on-path@^0.2.1: path-data-parser "0.1.0" points-on-curve "0.2.0" +postcss@^8.5.28: + version "8.5.28" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.28.tgz#da4563a99a06e62d6c1cd1acae363224bcaed6e9" + integrity sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A== + dependencies: + nanoid "^3.3.18" + picocolors "^1.1.1" + source-map-js "^1.2.1" + postgres-array@3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-3.0.4.tgz#4efcaf4d2c688d8bcaa8620ed13f35f299f7528c" @@ -5504,6 +5899,14 @@ qs@^6.14.0, qs@^6.15.2, qs@~6.15.1: es-define-property "^1.0.1" side-channel "^1.1.1" +qs@^6.14.1: + version "6.16.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.16.0.tgz#c22c723a28a920f3aacdce8289fabd43eccb79fd" + integrity sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA== + dependencies: + es-define-property "^1.0.1" + side-channel "^1.1.1" + queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" @@ -5738,6 +6141,30 @@ robust-predicates@^3.0.2: resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.3.tgz#1099061b3349e2c5abec6c2ab0acd440d24d4062" integrity sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA== +rolldown@~1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.2.8.tgz#3a18ad3c74809f05abe9130229b2b8b4249e9e21" + integrity sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ== + dependencies: + "@oxc-project/types" "=0.149.0" + "@rolldown/pluginutils" "^1.0.0" + optionalDependencies: + "@rolldown/binding-android-arm-eabi" "1.2.8" + "@rolldown/binding-android-arm64" "1.2.8" + "@rolldown/binding-darwin-arm64" "1.2.8" + "@rolldown/binding-darwin-x64" "1.2.8" + "@rolldown/binding-freebsd-x64" "1.2.8" + "@rolldown/binding-linux-arm-gnueabihf" "1.2.8" + "@rolldown/binding-linux-arm64-gnu" "1.2.8" + "@rolldown/binding-linux-arm64-musl" "1.2.8" + "@rolldown/binding-linux-ppc64-gnu" "1.2.8" + "@rolldown/binding-linux-s390x-gnu" "1.2.8" + "@rolldown/binding-linux-x64-gnu" "1.2.8" + "@rolldown/binding-linux-x64-musl" "1.2.8" + "@rolldown/binding-openharmony-arm64" "1.2.8" + "@rolldown/binding-win32-arm64-msvc" "1.2.8" + "@rolldown/binding-win32-x64-msvc" "1.2.8" + rou3@^0.7.12: version "0.7.12" resolved "https://registry.yarnpkg.com/rou3/-/rou3-0.7.12.tgz#cac17425c04abddba854a42385cabfe0b971a179" @@ -5957,6 +6384,11 @@ side-channel@^1.1.1: side-channel-map "^1.0.1" side-channel-weakmap "^1.0.2" +siginfo@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/siginfo/-/siginfo-2.0.0.tgz#32e76c70b79724e3bb567cb9d543eb858ccfaf30" + integrity sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g== + signal-exit@^3.0.2, signal-exit@^3.0.3: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" @@ -6044,6 +6476,11 @@ socks@^2.8.3: ip-address "^10.1.1" smart-buffer "^4.2.0" +source-map-js@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + source-map@^0.6.0, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" @@ -6090,6 +6527,11 @@ stack-trace@0.0.x: resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" integrity sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg== +stackback@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b" + integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw== + statuses@^2.0.1, statuses@^2.0.2, statuses@~2.0.1, statuses@~2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382" @@ -6100,6 +6542,11 @@ std-env@3.10.0: resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.10.0.tgz#d810b27e3a073047b2b5e40034881f5ea6f9c83b" integrity sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg== +std-env@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/std-env/-/std-env-4.2.0.tgz#8ebe0ec60485668ab47227b312f4254cdf80c9d3" + integrity sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw== + streamx@^2.12.5, streamx@^2.15.0, streamx@^2.25.0: version "2.28.0" resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.28.0.tgz#035ab56057b7ed2211b51d532e6973f0f99fbf11" @@ -6168,6 +6615,30 @@ stylis@^4.3.6: resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.4.0.tgz#c5846c9345f4bfc51bd0cbd7ca35a0744f485a5d" integrity sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA== +superagent@^10.3.0: + version "10.3.0" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-10.3.0.tgz#ff1e39e7976b63f8084291d65f5bfbbbbd156989" + integrity sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ== + dependencies: + component-emitter "^1.3.1" + cookiejar "^2.1.4" + debug "^4.3.7" + fast-safe-stringify "^2.1.1" + form-data "^4.0.5" + formidable "^3.5.4" + methods "^1.1.2" + mime "2.6.0" + qs "^6.14.1" + +supertest@^7.2.2: + version "7.2.2" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-7.2.2.tgz#dac3ee25a2aa59942a7f641e50c838a7c8819204" + integrity sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA== + dependencies: + cookie-signature "^1.2.2" + methods "^1.1.2" + superagent "^10.3.0" + supports-color@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -6303,12 +6774,22 @@ text-hex@1.0.x: resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== +tinybench@6.1.4: + version "6.1.4" + resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-6.1.4.tgz#f855bb3ad1f2fe85cf624490d58bb4f4f5d30da8" + integrity sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ== + +tinyexec@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-1.3.0.tgz#aacc1dbb1d4e93e6ad8dd64944e09f9ad147a474" + integrity sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ== + tinyexec@^1.0.1: version "1.2.4" resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-1.2.4.tgz#ae45bb2edebda94c70f4ea897e0f1243e470db71" integrity sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg== -tinyglobby@^0.2.15: +tinyglobby@^0.2.15, tinyglobby@^0.2.17: version "0.2.17" resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.17.tgz#562a9a6c9eb2b3b123d39719f9af5bb44fcd7631" integrity sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g== @@ -6582,6 +7063,38 @@ vary@^1, vary@^1.1.2, vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== +vite@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/vite/-/vite-8.3.0.tgz#f9565cfd4879d58d28fa64d3c7aff3b372d2ccc8" + integrity sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ== + dependencies: + lightningcss "^1.33.0" + picomatch "^4.0.7" + postcss "^8.5.28" + rolldown "~1.2.6" + tinyglobby "^0.2.17" + optionalDependencies: + fsevents "~2.3.3" + +vitest@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-5.0.0.tgz#a85f075e65bc7c87cce4df2f9114214fbd92f245" + integrity sha512-gpsMNoRhMjMktVxPtstOH4/PJuPyovVaMDr4oDilXaGH1EcqM2OE96SoHT2VIQ6fTGtTjqmHDrEu2X9RQiXf8Q== + dependencies: + "@types/chai" "^5.2.2" + "@vitest/mocker" "5.0.0" + chai "^6.2.2" + es-module-lexer "^2.3.2" + expect-type "^1.4.0" + magic-string "^1.2.3" + obug "^2.1.4" + picomatch "^4.0.7" + std-env "^4.2.0" + tinybench "6.1.4" + tinyexec "1.3.0" + tinyglobby "^0.2.17" + why-is-node-running "^2.3.0" + webdriver-bidi-protocol@0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz#d411e7b8e158408d83bb166b0b4f1054fa3f077e" @@ -6607,6 +7120,14 @@ which@^2.0.1, which@^2.0.2: dependencies: isexe "^2.0.0" +why-is-node-running@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz#a3f69a97107f494b3cdc3bdddd883a7d65cebf04" + integrity sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w== + dependencies: + siginfo "^2.0.0" + stackback "0.0.2" + winston-transport@^4.9.0: version "4.9.0" resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.9.0.tgz#3bba345de10297654ea6f33519424560003b3bf9" From 3ab8192d0674d9d950d54f6d2ca6224f0027c186 Mon Sep 17 00:00:00 2001 From: bh0fer Date: Sat, 12 Sep 2026 13:01:41 +0000 Subject: [PATCH 02/12] refine comment --- src/tests/integration/documentRoots.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/integration/documentRoots.test.ts b/src/tests/integration/documentRoots.test.ts index 3f16f03..407feec 100644 --- a/src/tests/integration/documentRoots.test.ts +++ b/src/tests/integration/documentRoots.test.ts @@ -59,6 +59,6 @@ describe('DocumentRoots (integration)', () => { const ok = await agentAs(admin.id).delete(`${API_URL}/documentRoots/${documentRootId}`); expect(ok.status).toBe(200); - documentRootIds.length = 0; // already deleted + documentRootIds.length = 0; // already deleted - prevent afterEach from trying to delete it again }); }); From cf782d75cd65969faca00a8e40bb4359ce5a75b4 Mon Sep 17 00:00:00 2001 From: bh0fer Date: Sat, 12 Sep 2026 13:03:34 +0000 Subject: [PATCH 03/12] add gh workflow --- .github/workflows/test.yml | 48 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..1b99c5f --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,48 @@ +name: Tests + +on: + pull_request: + push: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:17.2 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: teaching_api + ports: + - 5432:5432 + options: >- + --health-cmd="pg_isready -U postgres -d teaching_api" + --health-interval=10s + --health-timeout=5s + --health-retries=5 + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: '24.18' + cache: 'yarn' + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Generate Prisma client + run: yarn prisma generate + + - name: Run tests + env: + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/teaching_api?schema=public + TEST_DATABASE_URL: postgresql://postgres:postgres@localhost:5432/teaching_api_test?schema=public + run: yarn test From a06c4ba519a9d70d15ed78c4f96ad6df790c3814 Mon Sep 17 00:00:00 2001 From: bh0fer Date: Sat, 12 Sep 2026 13:05:04 +0000 Subject: [PATCH 04/12] use test db only --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1b99c5f..4ee994e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,11 +16,11 @@ jobs: env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres - POSTGRES_DB: teaching_api + POSTGRES_DB: teaching_api_test ports: - 5432:5432 options: >- - --health-cmd="pg_isready -U postgres -d teaching_api" + --health-cmd="pg_isready -U postgres -d teaching_api_test" --health-interval=10s --health-timeout=5s --health-retries=5 @@ -43,6 +43,6 @@ jobs: - name: Run tests env: - DATABASE_URL: postgresql://postgres:postgres@localhost:5432/teaching_api?schema=public + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/teaching_api_test?schema=public TEST_DATABASE_URL: postgresql://postgres:postgres@localhost:5432/teaching_api_test?schema=public run: yarn test From 62fed3a2f556ec5b2a027d15585d3e16eb87eeef Mon Sep 17 00:00:00 2001 From: bh0fer Date: Sat, 12 Sep 2026 13:14:25 +0000 Subject: [PATCH 05/12] fix ci --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4ee994e..925dc86 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,6 +39,8 @@ jobs: run: yarn install --frozen-lockfile - name: Generate Prisma client + env: + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/teaching_api_test?schema=public run: yarn prisma generate - name: Run tests From cf7b5b8ec53dadb43820a33f45d3dba1aef0dcca Mon Sep 17 00:00:00 2001 From: bh0fer Date: Sat, 12 Sep 2026 13:32:27 +0000 Subject: [PATCH 06/12] wipe db after each test --- src/tests/integration/documentRoots.test.ts | 17 ++--------- src/tests/integration/documents.test.ts | 17 ++--------- src/tests/integration/helpers.ts | 31 +++++++++++++-------- src/tests/integration/setup.ts | 7 ++++- 4 files changed, 30 insertions(+), 42 deletions(-) diff --git a/src/tests/integration/documentRoots.test.ts b/src/tests/integration/documentRoots.test.ts index 407feec..480a0e6 100644 --- a/src/tests/integration/documentRoots.test.ts +++ b/src/tests/integration/documentRoots.test.ts @@ -1,27 +1,17 @@ import { randomUUID } from 'crypto'; import request from 'supertest'; -import { afterEach, describe, expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; import { Access } from '../../../prisma/generated/enums.js'; import app from '../../app.js'; import { Role } from '../../models/User.js'; -import { API_URL, agentAs, createTestUser, deleteTestDocumentRoots, deleteTestUsers } from './helpers.js'; +import { API_URL, agentAs, createTestUser } from './helpers.js'; describe('DocumentRoots (integration)', () => { - const userIds: string[] = []; - const documentRootIds: string[] = []; - - afterEach(async () => { - await deleteTestDocumentRoots(...documentRootIds.splice(0)); - await deleteTestUsers(...userIds.splice(0)); - }); - it('lets a student create and fetch a document root', async () => { const user = await createTestUser(Role.STUDENT); - userIds.push(user.id); const agent = agentAs(user.id); const documentRootId = randomUUID(); - documentRootIds.push(documentRootId); const createRes = await agent .post(`${API_URL}/documentRoots/${documentRootId}`) @@ -46,10 +36,8 @@ describe('DocumentRoots (integration)', () => { it('only allows an admin to delete a document root', async () => { const student = await createTestUser(Role.STUDENT); const admin = await createTestUser(Role.ADMIN); - userIds.push(student.id, admin.id); const documentRootId = randomUUID(); - documentRootIds.push(documentRootId); await agentAs(student.id) .post(`${API_URL}/documentRoots/${documentRootId}`) .send({ access: Access.RW_DocumentRoot }); @@ -59,6 +47,5 @@ describe('DocumentRoots (integration)', () => { const ok = await agentAs(admin.id).delete(`${API_URL}/documentRoots/${documentRootId}`); expect(ok.status).toBe(200); - documentRootIds.length = 0; // already deleted - prevent afterEach from trying to delete it again }); }); diff --git a/src/tests/integration/documents.test.ts b/src/tests/integration/documents.test.ts index f03cc4a..cd256a6 100644 --- a/src/tests/integration/documents.test.ts +++ b/src/tests/integration/documents.test.ts @@ -1,26 +1,15 @@ import { randomUUID } from 'crypto'; -import { afterEach, describe, expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; import { Access } from '../../../prisma/generated/enums.js'; import { Role } from '../../models/User.js'; -import { API_URL, agentAs, createTestUser, deleteTestDocumentRoots, deleteTestUsers } from './helpers.js'; +import { API_URL, agentAs, createTestUser } from './helpers.js'; describe('Documents (integration)', () => { - const userIds: string[] = []; - const documentRootIds: string[] = []; - - afterEach(async () => { - // deleting the document root cascades and removes documents created on it - await deleteTestDocumentRoots(...documentRootIds.splice(0)); - await deleteTestUsers(...userIds.splice(0)); - }); - it('creates, reads, updates and deletes a document', async () => { const user = await createTestUser(Role.STUDENT); - userIds.push(user.id); const agent = agentAs(user.id); const documentRootId = randomUUID(); - documentRootIds.push(documentRootId); await agent .post(`${API_URL}/documentRoots/${documentRootId}`) .send({ access: Access.RW_DocumentRoot }); @@ -60,10 +49,8 @@ describe('Documents (integration)', () => { it('does not allow a user without access to read another users document data', async () => { const owner = await createTestUser(Role.STUDENT); const stranger = await createTestUser(Role.STUDENT); - userIds.push(owner.id, stranger.id); const documentRootId = randomUUID(); - documentRootIds.push(documentRootId); await agentAs(owner.id) .post(`${API_URL}/documentRoots/${documentRootId}`) .send({ access: Access.RW_DocumentRoot, sharedAccess: Access.None_DocumentRoot }); diff --git a/src/tests/integration/helpers.ts b/src/tests/integration/helpers.ts index ccab524..eff461d 100644 --- a/src/tests/integration/helpers.ts +++ b/src/tests/integration/helpers.ts @@ -34,16 +34,25 @@ export const createTestUser = async (role: Role = Role.STUDENT) => { }); }; -export const deleteTestUsers = async (...ids: string[]) => { - if (ids.length === 0) { - return; - } - await prisma.user.deleteMany({ where: { id: { in: ids } } }); -}; +export const resetDatabase = async () => { + await prisma.$executeRawUnsafe(` + DO $reset$ + DECLARE + tables text; + BEGIN + SELECT string_agg( + format('TRUNCATE TABLE %I.%I RESTART IDENTITY CASCADE', schemaname, tablename), + '; ' + ) + INTO tables + FROM pg_tables + WHERE schemaname = 'public' + AND tablename <> '_prisma_migrations'; -export const deleteTestDocumentRoots = async (...ids: string[]) => { - if (ids.length === 0) { - return; - } - await prisma.documentRoot.deleteMany({ where: { id: { in: ids } } }); + IF tables IS NOT NULL THEN + EXECUTE tables; + END IF; + END + $reset$; + `); }; diff --git a/src/tests/integration/setup.ts b/src/tests/integration/setup.ts index f49eb55..42ee58f 100644 --- a/src/tests/integration/setup.ts +++ b/src/tests/integration/setup.ts @@ -1,5 +1,6 @@ -import { afterAll, vi } from 'vitest'; +import { afterAll, afterEach, vi } from 'vitest'; import prisma from '../../prisma.js'; +import { resetDatabase } from './helpers.js'; /** * The real auth flow relies on better-auth cookies/sessions. For integration tests we @@ -32,6 +33,10 @@ vi.mock('../../socketIoServer.js', () => ({ notify: vi.fn() })); +afterEach(async () => { + await resetDatabase(); +}); + afterAll(async () => { await prisma.$disconnect(); }); From 4fb8edfe90bca590de0003c4c4aebff2f8c41d11 Mon Sep 17 00:00:00 2001 From: bh0fer Date: Sat, 12 Sep 2026 13:35:38 +0000 Subject: [PATCH 07/12] add users integration test --- src/tests/integration/users.test.ts | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/tests/integration/users.test.ts diff --git a/src/tests/integration/users.test.ts b/src/tests/integration/users.test.ts new file mode 100644 index 0000000..b89f474 --- /dev/null +++ b/src/tests/integration/users.test.ts @@ -0,0 +1,29 @@ +import { randomUUID } from 'crypto'; +import request from 'supertest'; +import { describe, expect, it } from 'vitest'; +import app from '../../app.js'; +import { Role } from '../../models/User.js'; +import { API_URL, agentAs, createTestUser } from './helpers.js'; + +describe('Users (integration)', () => { + it('returns the authenticated user and allows looking it up by id', async () => { + const user = await createTestUser(Role.STUDENT); + const agent = agentAs(user.id); + + const currentUserRes = await agent.get(`${API_URL}/user`); + expect(currentUserRes.status).toBe(200); + expect(currentUserRes.body.id).toBe(user.id); + expect(currentUserRes.body.email).toBe(user.email); + + const findUserRes = await agent.get(`${API_URL}/users/${user.id}`); + expect(findUserRes.status).toBe(200); + expect(findUserRes.body.id).toBe(user.id); + expect(findUserRes.body.email).toBe(user.email); + }); + + it('rejects unauthenticated user requests', async () => { + const res = await request(app).get(`${API_URL}/users/${randomUUID()}`); + + expect(res.status).toBe(401); + }); +}); From 806d5ba3ef9a4763ee06c3950df69ccb094faf65 Mon Sep 17 00:00:00 2001 From: bh0fer Date: Sat, 12 Sep 2026 14:16:47 +0000 Subject: [PATCH 08/12] add users document test --- src/controllers/documentRoots.ts | 24 ------------- src/routes/router.ts | 10 +----- src/tests/integration/users.test.ts | 52 +++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 33 deletions(-) diff --git a/src/controllers/documentRoots.ts b/src/controllers/documentRoots.ts index 0c98f99..0e0b8ad 100644 --- a/src/controllers/documentRoots.ts +++ b/src/controllers/documentRoots.ts @@ -22,30 +22,6 @@ export const findMany: RequestHandler = async res.json(documents); }; -export const findManyFor: RequestHandler< - { id: string /** userId */ }, - any, - any, - { ignoreMissingRoots?: boolean; type?: string; ids: string[] } -> = async (req, res, next) => { - if (!req.params.id) { - throw new HTTP400Error('Missing user id'); - } - const canLoad = (req as any).user!.id === req.params.id || hasElevatedAccess((req as any).user?.role); - if (!canLoad) { - throw new HTTP403Error('Not Authorized'); - } - const ids = Array.isArray(req.query.ids) ? req.query.ids : [req.query.ids]; - if (ids.length === 0 || !req.query.ids) { - return res.json([]); - } - const documents = await DocumentRoot.findManyModels(req.params.id, ids, { - ignoreMissingRoots: !!req.query.ignoreMissingRoots, - documentType: req.query.type && (req.query.type as string | undefined) - }); - res.json(documents); -}; - export const findMultipleFor: RequestHandler< { id: string /** userId */ }, any, diff --git a/src/routes/router.ts b/src/routes/router.ts index 1a7da43..2eb3724 100644 --- a/src/routes/router.ts +++ b/src/routes/router.ts @@ -34,7 +34,6 @@ import { update as updateDocumentRoot, permissions as allPermissions, singlePermissions as allPermissionsFor, - findManyFor as findManyDocumentRootsFor, findMultipleFor as findMultipleDocumentRootsFor, allDocuments, destroy as deleteDocumentRoot, @@ -63,14 +62,7 @@ router.get('/user', user); router.get('/users', allUsers); router.get('/users/:id', findUser); router.put('/users/:id', updateUser); -/** - * TODO: remove once [post] /users/:id/documentRoots is established and clients are updated - * - * @optional ?ignoreMissingRoots: boolean - * @optional ?type: string -> filter included documents by provided type - * @requires ?ids: string[] - */ -router.get('/users/:id/documentRoots', findManyDocumentRootsFor); + /** * a post endpoint to prevent issues with long query strings when requesting * many document roots for a user diff --git a/src/tests/integration/users.test.ts b/src/tests/integration/users.test.ts index b89f474..5a937be 100644 --- a/src/tests/integration/users.test.ts +++ b/src/tests/integration/users.test.ts @@ -1,8 +1,10 @@ import { randomUUID } from 'crypto'; import request from 'supertest'; import { describe, expect, it } from 'vitest'; +import { Access } from '../../../prisma/generated/enums.js'; import app from '../../app.js'; import { Role } from '../../models/User.js'; +import prisma from '../../prisma.js'; import { API_URL, agentAs, createTestUser } from './helpers.js'; describe('Users (integration)', () => { @@ -26,4 +28,54 @@ describe('Users (integration)', () => { expect(res.status).toBe(401); }); + + it('returns document roots for the requested user', async () => { + const user = await createTestUser(Role.STUDENT); + const otherUser = await createTestUser(Role.STUDENT); + const documentRootId = randomUUID(); + const agent = agentAs(user.id); + + const createRootRes = await agent.post(`${API_URL}/documentRoots/${documentRootId}`).send({ + access: Access.RW_DocumentRoot + }); + expect(createRootRes.status).toBe(200); + + const ownDocumentRes = await agent.post(`${API_URL}/documents`).send({ + type: 'document', + documentRootId, + data: { owner: user.id } + }); + expect(ownDocumentRes.status).toBe(200); + + await prisma.document.create({ + data: { + type: 'document', + documentRootId, + authorId: otherUser.id, + data: { owner: otherUser.id } + } + }); + + const res = await agent + .post(`${API_URL}/users/${user.id}/documentRoots`) + .send({ documentRootIds: [documentRootId] }); + + expect(res.status).toBe(200); + expect(res.body).toHaveLength(1); + expect(res.body[0].id).toBe(documentRootId); + expect(res.body[0].documents).toHaveLength(1); + expect(res.body[0].documents[0].authorId).toBe(user.id); + expect(res.body[0].documents[0].data).toEqual({ owner: user.id }); + }); + + it('does not allow a user to request another users document roots', async () => { + const user = await createTestUser(Role.STUDENT); + const otherUser = await createTestUser(Role.STUDENT); + + const res = await agentAs(user.id) + .get(`${API_URL}/users/${otherUser.id}/documentRoots`) + .query({ ids: randomUUID() }); + + expect(res.status).toBe(403); + }); }); From 2f8a7797b78536f957adcfb8d8f408cc36f9de4e Mon Sep 17 00:00:00 2001 From: bh0fer Date: Sat, 12 Sep 2026 15:04:10 +0000 Subject: [PATCH 09/12] fix test --- src/tests/integration/users.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/integration/users.test.ts b/src/tests/integration/users.test.ts index 5a937be..01dffdc 100644 --- a/src/tests/integration/users.test.ts +++ b/src/tests/integration/users.test.ts @@ -73,8 +73,8 @@ describe('Users (integration)', () => { const otherUser = await createTestUser(Role.STUDENT); const res = await agentAs(user.id) - .get(`${API_URL}/users/${otherUser.id}/documentRoots`) - .query({ ids: randomUUID() }); + .post(`${API_URL}/users/${otherUser.id}/documentRoots`) + .send({ documentRootIds: [randomUUID()] }); expect(res.status).toBe(403); }); From 1f528d6f60ea1b05d348f3ebbeea89e8ef427da2 Mon Sep 17 00:00:00 2001 From: bh0fer Date: Sun, 13 Sep 2026 13:46:58 +0000 Subject: [PATCH 10/12] update readme and fix seed --- .example.env | 20 ++- README.legacy.md | 344 +++++++++++++++++++++++++++++++++++++++++++++++ README.md | 315 ++++++++++--------------------------------- prisma/seed.ts | 7 +- 4 files changed, 432 insertions(+), 254 deletions(-) create mode 100644 README.legacy.md diff --git a/.example.env b/.example.env index 4368687..dec7fab 100644 --- a/.example.env +++ b/.example.env @@ -1,11 +1,9 @@ -DATABASE_URL="postgresql://user:pw@localhost:5432/teaching_website" -USER_ID="b6651212-0765-4d1c-ba5c-71632bf53d2a" -USER_EMAIL="Max.Muster@gbsl.ch" -ALLOWED_ORIGINS="http://localhost:3000" -ALLOW_SUBDOMAINS="false" -MSAL_CLIENT_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -MSAL_TENANT_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -ADMIN_USER_GROUP_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -GITHUB_CLIENT_SECRET="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -GITHUB_CLIENT_ID="xxxxxxxxxxxxxxxxxxxx" -GITHUB_REDIRECT_URI="http://localhost:3000/gh-callback" \ No newline at end of file +USER_ID="" +USER_EMAIL="" +APP_NAME="infTeachingApi" +ALLOWED_ORIGINS="gbsl.website" +BETTER_AUTH_SECRET="$(openssl rand -base64 32)" +BETTER_AUTH_URL="http://localhost:3002" +MSAL_CLIENT_ID="" +MSAL_CLIENT_SECRET="" +MSAL_TENANT_ID="" \ No newline at end of file diff --git a/README.legacy.md b/README.legacy.md new file mode 100644 index 0000000..4237389 --- /dev/null +++ b/README.legacy.md @@ -0,0 +1,344 @@ +# Teaching Website Backend + +The backend for our [teaching website](https://github.com/GBSL-Informatik/teaching-dev). + +## Run the Project with VS Code + +The Project is ready to be used with dev containers. You only need a recent version of [Docker](https://www.docker.com/). Then you can reopen the project in a devcontainer (`Ctrl+Shift+P` > `Dev Containers: Reopen in Container`). + +Setup your local env - inside the devcontainer **you should not set** `DATABASE_URL` yourself. + +```bash +USER_ID="" +USER_EMAIL="" +``` + +The `USER_ID` and `USER_EMAIL` are used only for seeding the database and are not strictly needed. +The `USER_ID` is the `ID` attribute from the response of [Graph-Explorer/v1.0/me](https://developer.microsoft.com/en-us/graph/graph-explorer) - you need to log in first... + + +The Project builds and you can run +1. `yarn run db:migrate && yarn run db:seed` (needed only on the initial startup) +2. `yarn run dev` + +And done... + +## Dev Dependencies + +In order to use `.env` files, the [dotenv-cli](https://www.npmjs.com/package/dotenv-cli) must be installed globally: + +```bash +yarn global add dotenv-cli +``` + +## Concepts + +### User Roles + +A user can have one of the following roles: +- `ADMIN`: The user has full access to the system and can manage all resources. This includes + - CRUD\* operations on all resources + - Manage user roles and permissions + - Access to all system settings +- `TEACHER`: The user can manage their own resources and has limited access to other users' resources. This includes + - CRUD\* operations on their own resources + - CRUD\* operations on StudentGroups (can create new groups, can add/remove users to/from groups they have admin access to) + - When a teacher creates a studentGroup and adds students to this group, the students are referenced as **managed** users. + - Can read docuements from managed users. + - Can CRUD user- and group-permissions for managed users and administrated groups. +- `STUDENT`: The user has limited access to the system and can only manage their own resources. + +\* Documents can be updated always only by the user who created them. Except the document has excplicite shared permissions with other users/groups. + +## Code Formatting + +For a consistent code style, the project uses [Prettier](https://prettier.io/). To format the code, run + +```bash +yarn run format +``` + +to format all typescript files. + +## Environment Variables + +| Variable | Description | Example | +|:-----------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------| +| `DATABASE_URL` | The URL to connect to the PostgreSQL database. | `postgresql://{user}:{pw}@localhost:5432/{db_name}` | +| `USER_EMAIL` | The email of the user to be created on seeding. | `reto.holz@gbsl.ch` | +| `USER_ID` | The UUID of the user to be created on seeding. \* | `fc0dfc19-d4a3-4354-afef-b5706046b368` | +| `NO_AUTH` | If set (and not running `production` mode), clients can authenticate as any user by supplying `{'email': 'some@email.ch'}` in the `Auhorization` header, for any user email in the database\*\* | `NO_AUTH=true` | +| `PORT` | (optional) The port the server should listen on. | `3002` (default) | +| `ALLOWED_ORIGINS` | A comma-separated list of origins allowed to access the api. E.g. teaching-dev.gbsl.website | `localhost:3000` | +| `ALLOW_SUBDOMAINS` | Wheter subdomains from `ALLOWED_DOMAINS` should be granted access too. | `false` | +| `SESSION_SECRET` | The secret for the session cookie.\*\*\* | `secret` | +| `MSAL_CLIENT_ID` | The client id for the web api from Azure. | | +| `MSAL_TENANT_ID` | The Tenant ID from your Azure instance | | +| `APP_NAME` | The name of the app. Used for the cookie name prefix `{APP_NAME}ApiKey` | `xyzTeaching`, default: `twa` | +| `NETLIFY_PROJECT_NAME` | When set to the netlify project name (e.g. `teaching-dev`), the app will allow requests from `https://deploy-preview-\d+--teaching-dev.netlify.app` and use `sameSite=none` instead of strict. | | +| `ADMIN_USER_GROUP_ID` | The UUID of the group that should be used as the admin group. For this group a RW-Permission will be always added to a newly created document root when it's access is not RW | default: "" | +| `GITHUB_CLIENT_SECRET` | Used for the CMS to work properly. Register an app under https://github.com/settings/apps. | | +| `GITHUB_CLIENT_ID` | | | +| `GITHUB_REDIRECT_URI` | | | +| `SENTRY_PROJECT` | Error Tracking: Sentry Project Name, e.g. `events-api`. | | +| `SENTRY_ORG` | Error Tracking: Sentry Organisation, e.g. your sentry username. | | +| `SENTRY_DSN` | Error Tracking: Sentry DSN. | | +| `SENTRY_AUTH_TOKEN` | Error Tracking: Auth token for uploading sourcemaps to sentry. Get it by configuring your app with `npx @sentry/wizard@latest -i sourcemaps`. | | +| `SENTRY_TRACES_SAMPLE_RATE` | Sampling rate for Sentry traces. | `0.1` (default) | + +\* When using MSAL Auth, use your `localAccountId` (check your local-storage when signed in, eg. at https://ofi.gbsl.website).
+\*\* To change users, clear LocalStorage to delete the API key created upon first authentication.
+\*\*\* Generate a secret with `openssl rand -base64 32`. + +These variables are stored in a `.env` file in the root directory. Make sure to not check this file into version control. Copy the `.example.env` file and fill in the values as described above. + +```bash +cp .example.env .env +``` + +## Database + +### Database Views + +The access policies and users documents are implemented as database views. To keep track of views and changes, make sure to use `yarn db:migrate-views` when changing views: + +1. Edit or create a new view file in `prisma/view-migrations/views/`. +2. Make sure the dependencies are correct in [migrate.config.yml](prisma/view-migrations/migrate.config.yml). +3. Run `yarn db:migrate-views` to create a new migration for the changed views (this won't run `prisma migrate:dev`, it only creates the migration files). +4. Eventually change the [schema.prisma](prisma/schema.prisma) file to reflect changes in the views (e.g. new fields). +5. Run `yarn run prisma migrate:dev` to create a new migration for the schema changes. + +> [!WARNING] +> Never edit views directly in a prisma migration file (under `prisma/migrations/`), as these files are auto-generated and will be overwritten the next time `yarn db:migrate-views` is run. + + +### Docker Compose + +Run `scripts/purge_dev_services.sh` or the `purge_dev_services` run config to remove all containers **and volumes** associated with the dev services. + +Run +```bash +docker compose --file dev_services.compose.yml up +``` +to start the dev services. + +#### Postgres +`docker compose` rebuilds the container PostgreSQL container on restart. When building the container, all files in `db/docker/sql` are copied to `/docker-entrypoint-initdb.d/`. They are executed by PostgreSQL **only** if **no volume exists** yet. These init files reflect the expected database setup for a production deployment, including a dedicated user for the backend. + +The `db/scripts` directory contains files for purging the DB. Volumes stay intact, which means that the aforementioned init scripts will not be run again. + +The following users are created: +- Admin: `postgres` / `qSpEx2Zz8BS9` +- User for DB `teaching_api`: `teaching_api` / `zW4SMEXLHpXXxxk` + +→ For the teaching-api, the resulting DB URL is `postgresql://teaching_api:zW4SMEXLHpXXxxk@localhost:5432/teaching_api`. + +#### Local Setup + +To set up a local dev database, run + +```bash +psql postgres # sudo -u postgres psql + +postgres=> CREATE ROLE teaching_api WITH LOGIN PASSWORD 'teaching_api'; +postgres=> ALTER ROLE teaching_api CREATEDB; +postgres=> \du +postgres=> \q + +psql -d postgres -h localhost -U teaching_api + +postgres=> CREATE DATABASE teaching_api; +postgres=> CREATE DATABASE teaching_api_test; # for testing +postgres=> \list +postgres=> \c teaching_api +``` + +make sure to set the db-name and the password in the `.env` file: + +```bash +DATABASE_URL="postgresql://teaching_api:teaching_api@localhost:5432/teaching_api" +``` + +#### Create the Database + +Run all prisma migrations: + +```bash +yarn db:migrate +``` + +or when you change the schema during development, run + +```bash +yarn db:migrate:dev +``` + +to be prompted for a version name. + +#### Seed Database + +To seed the database with some basic *users*, *documents*, *groups* and relations between them, run + +```bash +yarn db:seed +``` + +the seed file is located in `prisma/seed.ts`. It will create +- a user for `USER_EMAIL` and `USER_ID` from the .env file (if present) +- a test user `foo@bar.ch` with the uuid `4e90b891-7e31-4a49-9ac7-a71a0ad6863a` +- a group `test_group` with the memebers + +#### Reset Database + +To reset the database, run + +```bash +yarn db:reset +``` + +This will +- drop all tables +- drop all database types + +#### Recreate Database + +when you want to reset, migrate and seed the database, run + +```bash +yarn db:recreate +``` + +### Prisma Studio + +Run Prisma Studio - a simplistic local database viewer - with + +```bash +yarn run prisma studio +``` + +### Generate Database Documentation + +run + +```bash +yarn run prisma generate +``` + +this will generate +- [docs](public/prisma-docs/index.html) with the [prisma-docs-generator](https://github.com/pantharshit00/prisma-docs-generator) +- [schema.dbml](prisma/dbml/schema.dbml) with the [prisma-dbml-generator](https://notiz.dev/blog/prisma-dbml-generator) + +the docs will be publically available under `/prisma/index.html`. + +### Undo last migration (dev mode only!!!!) +### connect to current db +```bash +psql -d postgres -h localhost -U teaching_website -d teaching_website +``` + +### delete last migration +```sql +DELETE FROM _prisma_migrations WHERE started_at = (SELECT MAX(started_at)FROM _prisma_migrations); +``` + +### undo your migration, e.g. drop a view or remove a column +```sql +drop view view_name; -- drop view +ALTER TABLE table_name DROP COLUMN column_name; -- drop column +``` + +### disconnect +\q + +## Deployment +### PostgreSQL +- Set up a database and user according to the scripts in `db/docker/sql`. +- Create the required tables according to `db/scripts/01_create_tables.sql` (probably...). + +## Next steps +- Introduce an ORM and connect to DB. +- Introduce passport.js and set up a first authenticated endpoint (username / password) + + + +## Dokku + +```bash +dokku apps:create dev-teaching-api +dokku domains:add dev-teaching-api domain.tld + +dokku postgres:create dev-teaching-api +dokku postgres:link dev-teaching-api dev-teaching-api + +dokku config:set dev-teaching-api MSAL_CLIENT_ID="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" +dokku config:set dev-teaching-api MSAL_TENANT_ID="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" +dokku config:set --no-restart dev-teaching-api DOKKU_LETSENCRYPT_EMAIL="foo@bar.ch" +dokku config:set dev-teaching-api SESSION_SECRET="$(openssl rand -base64 32)" +dokku config:set dev-teaching-api ALLOWED_ORIGINS="tdev.tld" +dokku config:set dev-teaching-api ALLOW_SUBDOMAINS="false" + +mkdir /home/dokku/dev-teaching-api/nginx.conf.d/ +echo 'client_max_body_size 5m;' > /home/dokku/dev-teaching-api/nginx.conf.d/upload.conf +chown dokku:dokku /home/dokku/dev-teaching-api/nginx.conf.d/upload.conf +service nginx reload + +dokku nginx:set dev-teaching-api x-forwarded-proto-value '$http_x_forwarded_proto' +dokku nginx:set dev-teaching-api x-forwarded-for-value '$http_x_forwarded_for' +dokku nginx:set dev-teaching-api x-forwarded-port-value '$http_x_forwarded_port' + +# backup db +# dokku postgres:backup-auth dev-teaching-api +dokku postgres:backup-auth dev-teaching-api auto s3v4 https://92bdb68939987bdbf6207ccde70891de.eu.r2.cloudflarestorage.com +dokku postgres:backup dev-teaching-api +dokku postgres:backup-set-encryption dev-teaching-api +dokku postgres:backup-schedule dev-teaching-api "0 3 * * *" fs-informatik # daily backup at 3am + + +######### on local machine ######### +# 1. add the remote to your project: +git remote add dokku dokku@:dev-teaching-api +# 2. push the code to the dokku server: +git push dokku +# or if you want to push a branch other than the main: +# git push dokku :main + +################# on the server ################# - firs one who does it... +dokku letsencrypt:enable dev-teaching-api +## when it succeeds, re-enable the cloudflare proxy for domain.tld... +``` + +## Dump from production + +```bash +# inside shell of VSCode DevContainer (with configured dokku git remote) +dokku postgres:export dev-teaching-api > tdev-backup.dump +psql -U postgres -h localhost -c 'drop database if exists teaching_api;' +psql -U postgres -h localhost -c 'create database teaching_api;' +pg_restore -h localhost --verbose --clean --no-owner --no-privileges -U postgres -d teaching_api < tdev-backup.dump +yarn run prisma migrate dev + +# when ai-pr was once merged/deployed to the db, run `delete from _prisma_migrations where migration_name ilike '%_ai_%';` +``` + +### Troubleshooting + +#### Dokku `Unknown buildpack version` + +```bash +docker pull gliderlabs/herokuish:latest +# when this does not help, try additionally: +dokku buildpacks:set-property stack gliderlabs/herokuish:latest +dokku repo:purge-cache +``` + +## Speed Improvements +If the API and the Database are running on the same server, you can improve the speed by disabling the tcp connection for the database. This can be done by setting the `DATABASE_URL` to `postgresql://teaching_website:teaching_website@localhost/teaching_website?sslmode=disable`. + +## CMS + +For the cms to work properly, you need to register a github app under https://github.com/settings/apps. The following settings are required: + +- Callback URL: + - `http://localhost:3000/gh-callback` for local development + - `https://teaching-dev.domain.ch/gh-callback` for the productive environment + - No whitelist-URL's can be added, so you'd need to add for each deploy-preview a separate url... \ No newline at end of file diff --git a/README.md b/README.md index 560b367..0b797c9 100644 --- a/README.md +++ b/README.md @@ -1,99 +1,65 @@ # Teaching Website Backend -The backend for our teaching website. + +[![Prettier Check](https://github.com/GBSL-Informatik/teaching-api/actions/workflows/prettier-check.yml/badge.svg)](https://github.com/GBSL-Informatik/teaching-api/actions/workflows/prettier-check.yml) [![Tests](https://github.com/GBSL-Informatik/teaching-api/actions/workflows/test.yml/badge.svg)](https://github.com/GBSL-Informatik/teaching-api/actions/workflows/test.yml) + +The backend for our [teaching website](https://github.com/GBSL-Informatik/teaching-dev). ## Run the Project with VS Code The Project is ready to be used with dev containers. You only need a recent version of [Docker](https://www.docker.com/). Then you can reopen the project in a devcontainer (`Ctrl+Shift+P` > `Dev Containers: Reopen in Container`). -Setup your local env - inside the devcontainer **you should not set** `DATABASE_URL` yourself. +Setup your local env: ```bash -USER_ID="" -USER_EMAIL="" +cp .example.env .env ``` +and fill in the values for `USER_ID` and `USER_EMAIL` in the `.env` file: + The `USER_ID` and `USER_EMAIL` are used only for seeding the database and are not strictly needed. The `USER_ID` is the `ID` attribute from the response of [Graph-Explorer/v1.0/me](https://developer.microsoft.com/en-us/graph/graph-explorer) - you need to log in first... +(since the `DATABASE_URL` is automatically generated by the devcontainer, **you should not set** `DATABASE_URL` yourself) -The Project builds and you can run -1. `yarn run db:migrate && yarn run db:seed` (needed only on the initial startup) -2. `yarn run dev` - -And done... - -## Dev Dependencies - -In order to use `.env` files, the [dotenv-cli](https://www.npmjs.com/package/dotenv-cli) must be installed globally: - +Before the first startup, setup the local dev db: ```bash -yarn global add dotenv-cli +yarn run db:migrate +yarn run db:seed ``` -## Concepts - -### User Roles - -A user can have one of the following roles: -- `ADMIN`: The user has full access to the system and can manage all resources. This includes - - CRUD\* operations on all resources - - Manage user roles and permissions - - Access to all system settings -- `TEACHER`: The user can manage their own resources and has limited access to other users' resources. This includes - - CRUD\* operations on their own resources - - CRUD\* operations on StudentGroups (can create new groups, can add/remove users to/from groups they have admin access to) - - When a teacher creates a studentGroup and adds students to this group, the students are referenced as **managed** users. - - Can read docuements from managed users. - - Can CRUD user- and group-permissions for managed users and administrated groups. -- `STUDENT`: The user has limited access to the system and can only manage their own resources. - -\* Documents can be updated always only by the user who created them. Except the document has excplicite shared permissions with other users/groups. - -## Code Formatting - -For a consistent code style, the project uses [Prettier](https://prettier.io/). To format the code, run - +and then start the server with ```bash -yarn run format +yarn run dev ``` -to format all typescript files. - ## Environment Variables -| Variable | Description | Example | -|:-----------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------| -| `DATABASE_URL` | The URL to connect to the PostgreSQL database. | `postgresql://{user}:{pw}@localhost:5432/{db_name}` | -| `USER_EMAIL` | The email of the user to be created on seeding. | `reto.holz@gbsl.ch` | -| `USER_ID` | The UUID of the user to be created on seeding. \* | `fc0dfc19-d4a3-4354-afef-b5706046b368` | -| `NO_AUTH` | If set (and not running `production` mode), clients can authenticate as any user by supplying `{'email': 'some@email.ch'}` in the `Auhorization` header, for any user email in the database\*\* | `NO_AUTH=true` | -| `PORT` | (optional) The port the server should listen on. | `3002` (default) | -| `ALLOWED_ORIGINS` | A comma-separated list of origins allowed to access the api. E.g. teaching-dev.gbsl.website | `localhost:3000` | -| `ALLOW_SUBDOMAINS` | Wheter subdomains from `ALLOWED_DOMAINS` should be granted access too. | `false` | -| `SESSION_SECRET` | The secret for the session cookie.\*\*\* | `secret` | -| `MSAL_CLIENT_ID` | The client id for the web api from Azure. | | -| `MSAL_TENANT_ID` | The Tenant ID from your Azure instance | | -| `APP_NAME` | The name of the app. Used for the cookie name prefix `{APP_NAME}ApiKey` | `xyzTeaching`, default: `twa` | -| `NETLIFY_PROJECT_NAME` | When set to the netlify project name (e.g. `teaching-dev`), the app will allow requests from `https://deploy-preview-\d+--teaching-dev.netlify.app` and use `sameSite=none` instead of strict. | | -| `ADMIN_USER_GROUP_ID` | The UUID of the group that should be used as the admin group. For this group a RW-Permission will be always added to a newly created document root when it's access is not RW | default: "" | -| `GITHUB_CLIENT_SECRET` | Used for the CMS to work properly. Register an app under https://github.com/settings/apps. | | -| `GITHUB_CLIENT_ID` | | | -| `GITHUB_REDIRECT_URI` | | | -| `SENTRY_PROJECT` | Error Tracking: Sentry Project Name, e.g. `events-api`. | | -| `SENTRY_ORG` | Error Tracking: Sentry Organisation, e.g. your sentry username. | | -| `SENTRY_DSN` | Error Tracking: Sentry DSN. | | -| `SENTRY_AUTH_TOKEN` | Error Tracking: Auth token for uploading sourcemaps to sentry. Get it by configuring your app with `npx @sentry/wizard@latest -i sourcemaps`. | | -| `SENTRY_TRACES_SAMPLE_RATE` | Sampling rate for Sentry traces. | `0.1` (default) | - -\* When using MSAL Auth, use your `localAccountId` (check your local-storage when signed in, eg. at https://ofi.gbsl.website).
-\*\* To change users, clear LocalStorage to delete the API key created upon first authentication.
-\*\*\* Generate a secret with `openssl rand -base64 32`. - -These variables are stored in a `.env` file in the root directory. Make sure to not check this file into version control. Copy the `.example.env` file and fill in the values as described above. - -```bash -cp .example.env .env -``` +| Variable | Env | Description | Example | +|:----------------------------|:-------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------| +| `USER_EMAIL` | `dev` | The email of the user to be created on seeding. | `reto.holz@gbsl.ch` | +| `USER_ID` | `dev` | The UUID of the user to be created on seeding. \* | `fc0dfc19-d4a3-4354-afef-b5706046b368` | +| `PORT` | | (optional) The port the server should listen on. | `3002` (default) | +| `SESSION_SECRET` | | The secret for the session cookie. Generate with `openssl rand -base64 32` | `secret` | +| `MSAL_CLIENT_ID` | | The client id for the web api from Azure. | | +| `MSAL_TENANT_ID` | | The Tenant ID from your Azure instance | | +| `APP_NAME` | | The name of the app. Used for the cookie name prefix `{APP_NAME}ApiKey` | `xyzTeaching`, default: `twa` | +| `ADMIN_USER_GROUP_ID` | | The UUID of the group that should be used as the admin group. For this group a RW-Permission will be always added to a newly created document root when it's access is not RW | default: "" | +| `GITHUB_CLIENT_SECRET` | | Used for the CMS to work properly. Register an app under https://github.com/settings/apps. | | +| `GITHUB_CLIENT_ID` | | | | +| `GITHUB_REDIRECT_URI` | | | | +| `DATABASE_URL` | `prod` | The URL to connect to the PostgreSQL database. | `postgresql://{user}:{pw}@localhost:5432/{db_name}` | +| `ALLOWED_ORIGINS` | `prod` | A comma-separated list of origins allowed to access the api. E.g. teaching-dev.gbsl.website | `localhost:3000` | +| `ALLOW_SUBDOMAINS` | `prod` | Wheter subdomains from `ALLOWED_DOMAINS` should be granted access too. | `false` | +| `NETLIFY_PROJECT_NAME` | `prod` | When set to the netlify project name (e.g. `teaching-dev`), the app will allow requests from `https://deploy-preview-\d+--teaching-dev.netlify.app` and use `sameSite=none` instead of strict. | | +| `SENTRY_PROJECT` | `prod` | Error Tracking: Sentry Project Name, e.g. `events-api`. | | +| `SENTRY_ORG` | `prod` | Error Tracking: Sentry Organisation, e.g. your sentry username. | | +| `SENTRY_DSN` | `prod` | Error Tracking: Sentry DSN. | | +| `SENTRY_AUTH_TOKEN` | `prod` | Error Tracking: Auth token for uploading sourcemaps to sentry. Get it by configuring your app with `npx @sentry/wizard@latest -i sourcemaps`. | | +| `SENTRY_TRACES_SAMPLE_RATE` | `prod` | Sampling rate for Sentry traces. | `0.1` (default) | + +\* When using MSAL Auth, log in and get your id from https://developer.microsoft.com/en-us/graph/graph-explorer.
+ +These variables are stored in a `.env` file in the root directory. Make sure to not check this file into version control. ## Database @@ -110,76 +76,17 @@ The access policies and users documents are implemented as database views. To ke > [!WARNING] > Never edit views directly in a prisma migration file (under `prisma/migrations/`), as these files are auto-generated and will be overwritten the next time `yarn db:migrate-views` is run. +### DB Scripts -### Docker Compose - -Run `scripts/purge_dev_services.sh` or the `purge_dev_services` run config to remove all containers **and volumes** associated with the dev services. -Run ```bash -docker compose --file dev_services.compose.yml up -``` -to start the dev services. - -#### Postgres -`docker compose` rebuilds the container PostgreSQL container on restart. When building the container, all files in `db/docker/sql` are copied to `/docker-entrypoint-initdb.d/`. They are executed by PostgreSQL **only** if **no volume exists** yet. These init files reflect the expected database setup for a production deployment, including a dedicated user for the backend. - -The `db/scripts` directory contains files for purging the DB. Volumes stay intact, which means that the aforementioned init scripts will not be run again. - -The following users are created: -- Admin: `postgres` / `qSpEx2Zz8BS9` -- User for DB `teaching_api`: `teaching_api` / `zW4SMEXLHpXXxxk` - -→ For the teaching-api, the resulting DB URL is `postgresql://teaching_api:zW4SMEXLHpXXxxk@localhost:5432/teaching_api`. - -#### Local Setup - -To set up a local dev database, run - -```bash -psql postgres # sudo -u postgres psql - -postgres=> CREATE ROLE teaching_api WITH LOGIN PASSWORD 'teaching_api'; -postgres=> ALTER ROLE teaching_api CREATEDB; -postgres=> \du -postgres=> \q - -psql -d postgres -h localhost -U teaching_api - -postgres=> CREATE DATABASE teaching_api; -postgres=> CREATE DATABASE teaching_api_test; # for testing -postgres=> \list -postgres=> \c teaching_api -``` - -make sure to set the db-name and the password in the `.env` file: - -```bash -DATABASE_URL="postgresql://teaching_api:teaching_api@localhost:5432/teaching_api" -``` - -#### Create the Database - -Run all prisma migrations: - -```bash -yarn db:migrate -``` - -or when you change the schema during development, run - -```bash -yarn db:migrate:dev -``` - -to be prompted for a version name. - -#### Seed Database - -To seed the database with some basic *users*, *documents*, *groups* and relations between them, run - -```bash -yarn db:seed +# Run all prisma migrations: +yarn db:migrate # equivalent to yarn run prisma migrate deploy +yarn db:migrate:dev # equivalent to yarn run prisma migrate dev +yarn db:seed # * seeds some basic *users*, *documents* and *groups* +yarn db:reset # resets the database (drops all tables and types) +yarn db:recreate # resets, migrates and seeds the database +yarn run prisma generate # generates the prisma client (sometimes needed after changing the schema) ``` the seed file is located in `prisma/seed.ts`. It will create @@ -187,25 +94,18 @@ the seed file is located in `prisma/seed.ts`. It will create - a test user `foo@bar.ch` with the uuid `4e90b891-7e31-4a49-9ac7-a71a0ad6863a` - a group `test_group` with the memebers -#### Reset Database +### Dump from production -To reset the database, run +When running inside VSCode DevContainer and have configured a git remote called `dokku` pointing to the production server, you can dump the production database and restore it locally with the following commands: ```bash -yarn db:reset +dokku postgres:export dev-teaching-api > tdev-backup.dump +psql -U postgres -h localhost -c 'drop database if exists teaching_api;' +psql -U postgres -h localhost -c 'create database teaching_api;' +pg_restore -h localhost --verbose --clean --no-owner --no-privileges -U postgres -d teaching_api < tdev-backup.dump +yarn run prisma migrate dev ``` -This will -- drop all tables -- drop all database types - -#### Recreate Database - -when you want to reset, migrate and seed the database, run - -```bash -yarn db:recreate -``` ### Prisma Studio @@ -215,113 +115,44 @@ Run Prisma Studio - a simplistic local database viewer - with yarn run prisma studio ``` -### Generate Database Documentation - -run - -```bash -yarn run prisma generate -``` - -this will generate -- [docs](public/prisma-docs/index.html) with the [prisma-docs-generator](https://github.com/pantharshit00/prisma-docs-generator) -- [schema.dbml](prisma/dbml/schema.dbml) with the [prisma-dbml-generator](https://notiz.dev/blog/prisma-dbml-generator) +
+Undo last migration (dev mode only!!!!) -the docs will be publically available under `/prisma/index.html`. +> [!WARNING] +> Only do this in development, otherwise data loss is possible. -### Undo last migration (dev mode only!!!!) -### connect to current db ```bash -psql -d postgres -h localhost -U teaching_website -d teaching_website +psql -d teaching_api -h localhost -U postgres ``` +(Password is set inside [docker-compose.yml](.devcontainer/docker-compose.yml)) -### delete last migration ```sql +-- delete last migration DELETE FROM _prisma_migrations WHERE started_at = (SELECT MAX(started_at)FROM _prisma_migrations); -``` -### undo your migration, e.g. drop a view or remove a column -```sql +-- undo your migration, e.g. drop a view or remove a column drop view view_name; -- drop view ALTER TABLE table_name DROP COLUMN column_name; -- drop column -``` - -### disconnect +-- disconnect \q +``` +
-## Deployment -### PostgreSQL -- Set up a database and user according to the scripts in `db/docker/sql`. -- Create the required tables according to `db/scripts/01_create_tables.sql` (probably...). - -## Next steps -- Introduce an ORM and connect to DB. -- Introduce passport.js and set up a first authenticated endpoint (username / password) - - +## Code Formatting -## Dokku +For a consistent code style, the project uses [Prettier](https://prettier.io/). To format the code, run ```bash -dokku apps:create dev-teaching-api -dokku domains:add dev-teaching-api domain.tld - -dokku postgres:create dev-teaching-api -dokku postgres:link dev-teaching-api dev-teaching-api - -dokku config:set dev-teaching-api MSAL_CLIENT_ID="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" -dokku config:set dev-teaching-api MSAL_TENANT_ID="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" -dokku config:set --no-restart dev-teaching-api DOKKU_LETSENCRYPT_EMAIL="foo@bar.ch" -dokku config:set dev-teaching-api SESSION_SECRET="$(openssl rand -base64 32)" -dokku config:set dev-teaching-api ALLOWED_ORIGINS="tdev.tld" -dokku config:set dev-teaching-api ALLOW_SUBDOMAINS="false" - -mkdir /home/dokku/dev-teaching-api/nginx.conf.d/ -echo 'client_max_body_size 5m;' > /home/dokku/dev-teaching-api/nginx.conf.d/upload.conf -chown dokku:dokku /home/dokku/dev-teaching-api/nginx.conf.d/upload.conf -service nginx reload - -dokku nginx:set dev-teaching-api x-forwarded-proto-value '$http_x_forwarded_proto' -dokku nginx:set dev-teaching-api x-forwarded-for-value '$http_x_forwarded_for' -dokku nginx:set dev-teaching-api x-forwarded-port-value '$http_x_forwarded_port' - -# backup db -# dokku postgres:backup-auth dev-teaching-api -dokku postgres:backup-auth dev-teaching-api auto s3v4 https://92bdb68939987bdbf6207ccde70891de.eu.r2.cloudflarestorage.com -dokku postgres:backup dev-teaching-api -dokku postgres:backup-set-encryption dev-teaching-api -dokku postgres:backup-schedule dev-teaching-api "0 3 * * *" fs-informatik # daily backup at 3am - - -######### on local machine ######### -# 1. add the remote to your project: -git remote add dokku dokku@:dev-teaching-api -# 2. push the code to the dokku server: -git push dokku -# or if you want to push a branch other than the main: -# git push dokku :main - -################# on the server ################# - firs one who does it... -dokku letsencrypt:enable dev-teaching-api -## when it succeeds, re-enable the cloudflare proxy for domain.tld... +yarn run format ``` -## Dump from production - -```bash -# inside shell of VSCode DevContainer (with configured dokku git remote) -dokku postgres:export dev-teaching-api > tdev-backup.dump -psql -U postgres -h localhost -c 'drop database if exists teaching_api;' -psql -U postgres -h localhost -c 'create database teaching_api;' -pg_restore -h localhost --verbose --clean --no-owner --no-privileges -U postgres -d teaching_api < tdev-backup.dump -yarn run prisma migrate dev +to format all typescript files. -# when ai-pr was once merged/deployed to the db, run `delete from _prisma_migrations where migration_name ilike '%_ai_%';` -``` +## Dokku -### Troubleshooting +See [tdev docs](https://teaching-dev.gbsl.website/docs/dokku/tdev-api/api/) -#### Dokku `Unknown buildpack version` +### Troubleshooting `Unknown buildpack version` ```bash docker pull gliderlabs/herokuish:latest diff --git a/prisma/seed.ts b/prisma/seed.ts index 9081545..7b7e94b 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -17,7 +17,12 @@ async function main() { } const users = await prisma.user.createMany({ - data: seedUsers.map((user) => ({ ...user, email: user.email.toLowerCase() })) + data: seedUsers.map((user) => ({ + ...user, + email: user.email.toLowerCase(), + name: `${user.firstName} ${user.lastName}`, + emailVerified: true + })) }); console.log('Created users:\n' + seedUsers.map((u) => `- ${u.email}`).join('\n')); From 46ce15e939447048d5aa2b889481c03595598b3f Mon Sep 17 00:00:00 2001 From: bh0fer Date: Sun, 13 Sep 2026 14:06:35 +0000 Subject: [PATCH 11/12] cleanup repo --- .idea/.gitignore | 8 - .idea/codeStyles/codeStyleConfig.xml | 5 - .idea/dataSources.xml | 17 - .idea/misc.xml | 6 - .idea/modules.xml | 8 - .idea/runConfigurations/dev.xml | 12 - .idea/runConfigurations/dev_services.xml | 13 - .../runConfigurations/purge_dev_services.xml | 17 - .idea/sqldialects.xml | 9 - .idea/teaching-website-backend.iml | 9 - .idea/vcs.xml | 6 - bin/create-dokku.sh | 68 - db/docker/PostgreSQL.dockerfile | 2 - db/docker/sql/00_create_user_and_db.sql | 2 - http_requests/.example.env | 1 - http_requests/basic_checks.http | 21 - http_requests/documents.http | 40 - http_requests/groups.http | 51 - http_requests/users.http | 40 - package.json | 2 +- prisma/erd-2025-12-30.svg | 1 - prisma/erd-2026-09-13.md | 215 +++ prisma/schema.prisma | 2 +- yarn.lock | 1164 +---------------- 24 files changed, 224 insertions(+), 1495 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/codeStyles/codeStyleConfig.xml delete mode 100644 .idea/dataSources.xml delete mode 100644 .idea/misc.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/runConfigurations/dev.xml delete mode 100644 .idea/runConfigurations/dev_services.xml delete mode 100644 .idea/runConfigurations/purge_dev_services.xml delete mode 100644 .idea/sqldialects.xml delete mode 100644 .idea/teaching-website-backend.iml delete mode 100644 .idea/vcs.xml delete mode 100644 bin/create-dokku.sh delete mode 100644 db/docker/PostgreSQL.dockerfile delete mode 100644 db/docker/sql/00_create_user_and_db.sql delete mode 100644 http_requests/.example.env delete mode 100644 http_requests/basic_checks.http delete mode 100644 http_requests/documents.http delete mode 100644 http_requests/groups.http delete mode 100644 http_requests/users.http delete mode 100644 prisma/erd-2025-12-30.svg create mode 100644 prisma/erd-2026-09-13.md diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b8..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml deleted file mode 100644 index a55e7a1..0000000 --- a/.idea/codeStyles/codeStyleConfig.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml deleted file mode 100644 index 90a5fb6..0000000 --- a/.idea/dataSources.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - postgresql - true - org.postgresql.Driver - jdbc:postgresql://localhost:5432/teaching_api - - - - - - $ProjectFileDir$ - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 07115cd..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index b387e8b..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/runConfigurations/dev.xml b/.idea/runConfigurations/dev.xml deleted file mode 100644 index 46d8ec8..0000000 --- a/.idea/runConfigurations/dev.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - -