diff --git a/AGENTS.md b/AGENTS.md index 69201bf..fe54e1f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -100,5 +100,7 @@ v1 отдаётся с корня, v2 — с префиксом `/v2`. v1 нам - **Тесты не читают `.env`**: `dotenv` в `src/plugins/env.ts` отключён при `NODE_ENV=test`, переменные приходят из `vitest.config.ts`. Иначе прогон зависел бы от локального файла разработчика. -- **База**: in-memory SQLite (`src/plugins/drizzle.ts`), пересоздаётся при каждом - запуске. Для постоянного хранения нужен файл или настоящая СУБД. +- **База**: PGlite (`src/plugins/drizzle.ts`) — postgres в wasm, внутри процесса + и в памяти, пересоздаётся при каждом запуске. Взят ради нативных типов: + `timestamptz` в схеме это время, а не integer с кодеком поверх. Для + постоянного хранения нужен каталог данных или отдельный сервер postgres. diff --git a/Dockerfile b/Dockerfile index e23b6c6..b68c659 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,11 +2,8 @@ # TypeScript сам, снимая типы. Поэтому образ копирует исходники как есть. FROM node:26-slim AS deps -# better-sqlite3 — нативный модуль, и под slim готового бинаря может не быть: -# без тулчейна установка падает на сборке. -RUN apt-get update \ - && apt-get install -y --no-install-recommends python3 make g++ ca-certificates \ - && rm -rf /var/lib/apt/lists/* +# Тулчейна для нативных модулей больше нет: PGlite — wasm, собирать под +# платформу нечего. # corepack из образов node 26 убран, поэтому pnpm ставится явно — версией из # packageManager, чтобы лок-файл читался тем же, что и локально. @@ -34,4 +31,8 @@ COPY --chown=node:node tsp-output ./tsp-output EXPOSE 3000 # Через node_modules напрямую, а не pnpm exec: в рантайме pnpm не нужен. -CMD ["node", "node_modules/fastify-cli/cli.js", "start", "-l", "info", "-a", "0.0.0.0", "src/app.ts"] +# +# --plugin-timeout поднят с дефолтных 10 секунд: drizzle поднимает PGlite и +# прогоняет миграции, и на холодном старте в контейнере с урезанным CPU это +# укладывается не всегда. Без запаса контейнер просто не поднимется. +CMD ["node", "node_modules/fastify-cli/cli.js", "start", "-l", "info", "-a", "0.0.0.0", "--plugin-timeout", "60000", "src/app.ts"] diff --git a/Makefile b/Makefile index 35b80fd..f4f2b0d 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ migration-generate: # check вызывается флагами, а не через конфиг: читая drizzle.config.ts, он # принимает dialect за параметр AWS Data API и падает (drizzle-kit 0.31). migration-check: - pnpm exec drizzle-kit check --dialect sqlite --out ./drizzle + pnpm exec drizzle-kit check --dialect postgresql --out ./drizzle pnpm exec drizzle-kit generate @test -z "$$(git status --porcelain drizzle)" || { \ echo "Схема изменилась без миграции — запустите make migration-generate:"; \ diff --git a/README.md b/README.md index ef40ce3..6a6c575 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,9 @@ REST API на [Fastify](https://fastify.dev/), собранный «по-взр проверка запросов тоже не пишутся руками. `make generate-check` в CI не даёт сгенерированному разойтись со спекой. - **База через [Drizzle](https://orm.drizzle.team/)**: схема в `src/db/schema.ts`, - миграции генерируются по ней. + миграции генерируются по ней. Под ней [PGlite](https://pglite.dev/) — postgres + в wasm, внутри процесса и в памяти: отдельного сервиса не нужно, а типы + нативные, `timestamptz` в схеме это время, а не число с кодеком поверх. - **Валидация входа** отдельным слоем в `src/validators/`, а не внутри обработчика. - **Две версии API из одной спеки.** `@added(Versions.v2)` в TypeSpec — и v2 получает поле, которого нет в v1; каждая версия отдаётся со своим документом diff --git a/compose.yaml b/compose.yaml index c75d9e0..9a66a04 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,5 +1,5 @@ -# Локальный запуск собранного образа. Базы отдельным сервисом нет: она -# in-memory внутри процесса и пересоздаётся при каждом старте. +# Локальный запуск собранного образа. Базы отдельным сервисом нет: PGlite — +# postgres в wasm — живёт в памяти процесса и пересоздаётся при каждом старте. services: api: build: . diff --git a/drizzle.config.ts b/drizzle.config.ts index 6075d70..171bf52 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,7 +1,7 @@ import { defineConfig } from "drizzle-kit"; export default defineConfig({ - dialect: "sqlite", + dialect: "postgresql", schema: "./src/db/schema.ts", // out: "./drizzle", }); diff --git a/drizzle/0000_nasty_marvel_zombies.sql b/drizzle/0000_nasty_marvel_zombies.sql new file mode 100644 index 0000000..aaea2cf --- /dev/null +++ b/drizzle/0000_nasty_marvel_zombies.sql @@ -0,0 +1,31 @@ +CREATE TABLE "course_lessons" ( + "id" integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "course_lessons_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1), + "name" text NOT NULL, + "courseId" integer NOT NULL, + "body" text NOT NULL, + "created_at" timestamp with time zone NOT NULL, + "updated_at" timestamp with time zone NOT NULL +); +--> statement-breakpoint +CREATE TABLE "courses" ( + "id" integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "courses_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1), + "name" text NOT NULL, + "creator_id" integer NOT NULL, + "description" text NOT NULL, + "created_at" timestamp with time zone NOT NULL, + "updated_at" timestamp with time zone NOT NULL +); +--> statement-breakpoint +CREATE TABLE "users" ( + "id" integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "users_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1), + "full_name" text, + "email" text NOT NULL, + "password_digest" text NOT NULL, + "phone" text, + "created_at" timestamp with time zone NOT NULL, + "updated_at" timestamp with time zone NOT NULL, + CONSTRAINT "users_email_unique" UNIQUE("email") +); +--> statement-breakpoint +ALTER TABLE "course_lessons" ADD CONSTRAINT "course_lessons_courseId_courses_id_fk" FOREIGN KEY ("courseId") REFERENCES "public"."courses"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "courses" ADD CONSTRAINT "courses_creator_id_users_id_fk" FOREIGN KEY ("creator_id") REFERENCES "public"."users"("id") ON DELETE restrict ON UPDATE no action; \ No newline at end of file diff --git a/drizzle/0000_omniscient_warbound.sql b/drizzle/0000_omniscient_warbound.sql deleted file mode 100644 index abdc44e..0000000 --- a/drizzle/0000_omniscient_warbound.sql +++ /dev/null @@ -1,27 +0,0 @@ -CREATE TABLE `course_lessons` ( - `id` integer PRIMARY KEY NOT NULL, - `name` text NOT NULL, - `courseId` integer NOT NULL, - `body` text NOT NULL, - `created_at` text DEFAULT (unixepoch()) NOT NULL, - FOREIGN KEY (`courseId`) REFERENCES `courses`(`id`) ON UPDATE no action ON DELETE no action -); ---> statement-breakpoint -CREATE TABLE `courses` ( - `id` integer PRIMARY KEY NOT NULL, - `name` text NOT NULL, - `creator_id` integer NOT NULL, - `description` text NOT NULL, - `created_at` text DEFAULT (unixepoch()) NOT NULL, - FOREIGN KEY (`creator_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action -); ---> statement-breakpoint -CREATE TABLE `users` ( - `id` integer PRIMARY KEY NOT NULL, - `full_name` text, - `email` text NOT NULL, - `updated_at` text, - `created_at` text DEFAULT (unixepoch()) NOT NULL -); ---> statement-breakpoint -CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`); \ No newline at end of file diff --git a/drizzle/0001_friendly_forge.sql b/drizzle/0001_friendly_forge.sql deleted file mode 100644 index 9e4891e..0000000 --- a/drizzle/0001_friendly_forge.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE `users` ADD `password_digest` text NOT NULL; \ No newline at end of file diff --git a/drizzle/0002_handy_paladin.sql b/drizzle/0002_handy_paladin.sql deleted file mode 100644 index ba3ca0f..0000000 --- a/drizzle/0002_handy_paladin.sql +++ /dev/null @@ -1,48 +0,0 @@ --- Пересоздание таблиц под таймстемпы: created_at из text в integer, updated_at --- добавлен и стал NOT NULL. --- --- SELECT'ы поправлены руками. drizzle-kit сгенерировал перенос updated_at из --- courses и course_lessons, где такой колонки никогда не было, — миграция --- падала с «no such column: updated_at». Для них updated_at заполняется из --- created_at, для users — из старого значения, если оно было. -PRAGMA foreign_keys=OFF;--> statement-breakpoint -CREATE TABLE `__new_course_lessons` ( - `id` integer PRIMARY KEY NOT NULL, - `name` text NOT NULL, - `courseId` integer NOT NULL, - `body` text NOT NULL, - `created_at` integer NOT NULL, - `updated_at` integer NOT NULL, - FOREIGN KEY (`courseId`) REFERENCES `courses`(`id`) ON UPDATE no action ON DELETE no action -); ---> statement-breakpoint -INSERT INTO `__new_course_lessons`("id", "name", "courseId", "body", "created_at", "updated_at") SELECT "id", "name", "courseId", "body", CAST("created_at" AS integer), CAST("created_at" AS integer) FROM `course_lessons`;--> statement-breakpoint -DROP TABLE `course_lessons`;--> statement-breakpoint -ALTER TABLE `__new_course_lessons` RENAME TO `course_lessons`;--> statement-breakpoint -CREATE TABLE `__new_courses` ( - `id` integer PRIMARY KEY NOT NULL, - `name` text NOT NULL, - `creator_id` integer NOT NULL, - `description` text NOT NULL, - `created_at` integer NOT NULL, - `updated_at` integer NOT NULL, - FOREIGN KEY (`creator_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action -); ---> statement-breakpoint -INSERT INTO `__new_courses`("id", "name", "creator_id", "description", "created_at", "updated_at") SELECT "id", "name", "creator_id", "description", CAST("created_at" AS integer), CAST("created_at" AS integer) FROM `courses`;--> statement-breakpoint -DROP TABLE `courses`;--> statement-breakpoint -ALTER TABLE `__new_courses` RENAME TO `courses`;--> statement-breakpoint -CREATE TABLE `__new_users` ( - `id` integer PRIMARY KEY NOT NULL, - `full_name` text, - `email` text NOT NULL, - `password_digest` text NOT NULL, - `created_at` integer NOT NULL, - `updated_at` integer NOT NULL -); ---> statement-breakpoint -INSERT INTO `__new_users`("id", "full_name", "email", "password_digest", "created_at", "updated_at") SELECT "id", "full_name", "email", "password_digest", CAST("created_at" AS integer), COALESCE(CAST("updated_at" AS integer), CAST("created_at" AS integer)) FROM `users`;--> statement-breakpoint -DROP TABLE `users`;--> statement-breakpoint -ALTER TABLE `__new_users` RENAME TO `users`;--> statement-breakpoint -CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);--> statement-breakpoint -PRAGMA foreign_keys=ON; diff --git a/drizzle/0003_common_sabra.sql b/drizzle/0003_common_sabra.sql deleted file mode 100644 index 833bafd..0000000 --- a/drizzle/0003_common_sabra.sql +++ /dev/null @@ -1,28 +0,0 @@ -PRAGMA foreign_keys=OFF;--> statement-breakpoint -CREATE TABLE `__new_course_lessons` ( - `id` integer PRIMARY KEY NOT NULL, - `name` text NOT NULL, - `courseId` integer NOT NULL, - `body` text NOT NULL, - `created_at` integer NOT NULL, - `updated_at` integer NOT NULL, - FOREIGN KEY (`courseId`) REFERENCES `courses`(`id`) ON UPDATE no action ON DELETE cascade -); ---> statement-breakpoint -INSERT INTO `__new_course_lessons`("id", "name", "courseId", "body", "created_at", "updated_at") SELECT "id", "name", "courseId", "body", "created_at", "updated_at" FROM `course_lessons`;--> statement-breakpoint -DROP TABLE `course_lessons`;--> statement-breakpoint -ALTER TABLE `__new_course_lessons` RENAME TO `course_lessons`;--> statement-breakpoint -PRAGMA foreign_keys=ON;--> statement-breakpoint -CREATE TABLE `__new_courses` ( - `id` integer PRIMARY KEY NOT NULL, - `name` text NOT NULL, - `creator_id` integer NOT NULL, - `description` text NOT NULL, - `created_at` integer NOT NULL, - `updated_at` integer NOT NULL, - FOREIGN KEY (`creator_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade -); ---> statement-breakpoint -INSERT INTO `__new_courses`("id", "name", "creator_id", "description", "created_at", "updated_at") SELECT "id", "name", "creator_id", "description", "created_at", "updated_at" FROM `courses`;--> statement-breakpoint -DROP TABLE `courses`;--> statement-breakpoint -ALTER TABLE `__new_courses` RENAME TO `courses`; \ No newline at end of file diff --git a/drizzle/0004_giant_hammerhead.sql b/drizzle/0004_giant_hammerhead.sql deleted file mode 100644 index 45a9824..0000000 --- a/drizzle/0004_giant_hammerhead.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE `users` ADD `phone` text; \ No newline at end of file diff --git a/drizzle/0005_curious_joshua_kane.sql b/drizzle/0005_curious_joshua_kane.sql deleted file mode 100644 index f95e434..0000000 --- a/drizzle/0005_curious_joshua_kane.sql +++ /dev/null @@ -1,28 +0,0 @@ -PRAGMA foreign_keys=OFF;--> statement-breakpoint -CREATE TABLE `__new_course_lessons` ( - `id` integer PRIMARY KEY NOT NULL, - `name` text NOT NULL, - `courseId` integer NOT NULL, - `body` text NOT NULL, - `created_at` integer NOT NULL, - `updated_at` integer NOT NULL, - FOREIGN KEY (`courseId`) REFERENCES `courses`(`id`) ON UPDATE no action ON DELETE restrict -); ---> statement-breakpoint -INSERT INTO `__new_course_lessons`("id", "name", "courseId", "body", "created_at", "updated_at") SELECT "id", "name", "courseId", "body", "created_at", "updated_at" FROM `course_lessons`;--> statement-breakpoint -DROP TABLE `course_lessons`;--> statement-breakpoint -ALTER TABLE `__new_course_lessons` RENAME TO `course_lessons`;--> statement-breakpoint -PRAGMA foreign_keys=ON;--> statement-breakpoint -CREATE TABLE `__new_courses` ( - `id` integer PRIMARY KEY NOT NULL, - `name` text NOT NULL, - `creator_id` integer NOT NULL, - `description` text NOT NULL, - `created_at` integer NOT NULL, - `updated_at` integer NOT NULL, - FOREIGN KEY (`creator_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE restrict -); ---> statement-breakpoint -INSERT INTO `__new_courses`("id", "name", "creator_id", "description", "created_at", "updated_at") SELECT "id", "name", "creator_id", "description", "created_at", "updated_at" FROM `courses`;--> statement-breakpoint -DROP TABLE `courses`;--> statement-breakpoint -ALTER TABLE `__new_courses` RENAME TO `courses`; \ No newline at end of file diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json index d0ba3ac..fdb6526 100644 --- a/drizzle/meta/0000_snapshot.json +++ b/drizzle/meta/0000_snapshot.json @@ -1,47 +1,59 @@ { - "version": "6", - "dialect": "sqlite", - "id": "3d6c5f9b-cbab-4fbb-b936-cbe84a1c98f6", + "id": "f0b51729-a97e-4851-a191-5adf4dcd5eb4", "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", "tables": { - "course_lessons": { + "public.course_lessons": { "name": "course_lessons", + "schema": "", "columns": { "id": { "name": "id", "type": "integer", "primaryKey": true, "notNull": true, - "autoincrement": false + "identity": { + "type": "byDefault", + "name": "course_lessons_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } }, "name": { "name": "name", "type": "text", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "courseId": { "name": "courseId", "type": "integer", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "body": { "name": "body", "type": "text", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "created_at": { "name": "created_at", - "type": "text", + "type": "timestamp with time zone", "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())" + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true } }, "indexes": {}, @@ -50,53 +62,72 @@ "name": "course_lessons_courseId_courses_id_fk", "tableFrom": "course_lessons", "tableTo": "courses", - "columnsFrom": ["courseId"], - "columnsTo": ["id"], - "onDelete": "no action", + "columnsFrom": [ + "courseId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", "onUpdate": "no action" } }, "compositePrimaryKeys": {}, - "uniqueConstraints": {} + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false }, - "courses": { + "public.courses": { "name": "courses", + "schema": "", "columns": { "id": { "name": "id", "type": "integer", "primaryKey": true, "notNull": true, - "autoincrement": false + "identity": { + "type": "byDefault", + "name": "courses_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } }, "name": { "name": "name", "type": "text", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "creator_id": { "name": "creator_id", "type": "integer", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "description": { "name": "description", "type": "text", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "created_at": { "name": "created_at", - "type": "text", + "type": "timestamp with time zone", "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())" + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true } }, "indexes": {}, @@ -105,74 +136,106 @@ "name": "courses_creator_id_users_id_fk", "tableFrom": "courses", "tableTo": "users", - "columnsFrom": ["creator_id"], - "columnsTo": ["id"], - "onDelete": "no action", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", "onUpdate": "no action" } }, "compositePrimaryKeys": {}, - "uniqueConstraints": {} + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false }, - "users": { + "public.users": { "name": "users", + "schema": "", "columns": { "id": { "name": "id", "type": "integer", "primaryKey": true, "notNull": true, - "autoincrement": false + "identity": { + "type": "byDefault", + "name": "users_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } }, "full_name": { "name": "full_name", "type": "text", "primaryKey": false, - "notNull": false, - "autoincrement": false + "notNull": false }, "email": { "name": "email", "type": "text", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, - "updated_at": { - "name": "updated_at", + "password_digest": { + "name": "password_digest", "type": "text", "primaryKey": false, - "notNull": false, - "autoincrement": false + "notNull": true + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false }, "created_at": { "name": "created_at", - "type": "text", + "type": "timestamp with time zone", "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())" + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true } }, - "indexes": { + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { "users_email_unique": { "name": "users_email_unique", - "columns": ["email"], - "isUnique": true + "nullsNotDistinct": false, + "columns": [ + "email" + ] } }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false } }, "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, "_meta": { + "columns": {}, "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} + "tables": {} } -} +} \ No newline at end of file diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json deleted file mode 100644 index 2f62a5e..0000000 --- a/drizzle/meta/0001_snapshot.json +++ /dev/null @@ -1,199 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "31bfd2e6-7a14-443a-b57b-47de42564d25", - "prevId": "3d6c5f9b-cbab-4fbb-b936-cbe84a1c98f6", - "tables": { - "course_lessons": { - "name": "course_lessons", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "courseId": { - "name": "courseId", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "body": { - "name": "body", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())" - } - }, - "indexes": {}, - "foreignKeys": { - "course_lessons_courseId_courses_id_fk": { - "name": "course_lessons_courseId_courses_id_fk", - "tableFrom": "course_lessons", - "tableTo": "courses", - "columnsFrom": [ - "courseId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "courses": { - "name": "courses", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "creator_id": { - "name": "creator_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())" - } - }, - "indexes": {}, - "foreignKeys": { - "courses_creator_id_users_id_fk": { - "name": "courses_creator_id_users_id_fk", - "tableFrom": "courses", - "tableTo": "users", - "columnsFrom": [ - "creator_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "users": { - "name": "users", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "full_name": { - "name": "full_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "password_digest": { - "name": "password_digest", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())" - } - }, - "indexes": { - "users_email_unique": { - "name": "users_email_unique", - "columns": [ - "email" - ], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - } - }, - "views": {}, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} \ No newline at end of file diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json deleted file mode 100644 index 7a5fc43..0000000 --- a/drizzle/meta/0002_snapshot.json +++ /dev/null @@ -1,210 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "71e237eb-50bc-4f3c-a0ae-200bd03e64c7", - "prevId": "31bfd2e6-7a14-443a-b57b-47de42564d25", - "tables": { - "course_lessons": { - "name": "course_lessons", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "courseId": { - "name": "courseId", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "body": { - "name": "body", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "course_lessons_courseId_courses_id_fk": { - "name": "course_lessons_courseId_courses_id_fk", - "tableFrom": "course_lessons", - "tableTo": "courses", - "columnsFrom": [ - "courseId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "courses": { - "name": "courses", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "creator_id": { - "name": "creator_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "courses_creator_id_users_id_fk": { - "name": "courses_creator_id_users_id_fk", - "tableFrom": "courses", - "tableTo": "users", - "columnsFrom": [ - "creator_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "users": { - "name": "users", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "full_name": { - "name": "full_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "password_digest": { - "name": "password_digest", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "users_email_unique": { - "name": "users_email_unique", - "columns": [ - "email" - ], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - } - }, - "views": {}, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} \ No newline at end of file diff --git a/drizzle/meta/0003_snapshot.json b/drizzle/meta/0003_snapshot.json deleted file mode 100644 index a6bcc72..0000000 --- a/drizzle/meta/0003_snapshot.json +++ /dev/null @@ -1,210 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "e77b7827-d467-4a50-b814-bb8a0138c27b", - "prevId": "71e237eb-50bc-4f3c-a0ae-200bd03e64c7", - "tables": { - "course_lessons": { - "name": "course_lessons", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "courseId": { - "name": "courseId", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "body": { - "name": "body", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "course_lessons_courseId_courses_id_fk": { - "name": "course_lessons_courseId_courses_id_fk", - "tableFrom": "course_lessons", - "tableTo": "courses", - "columnsFrom": [ - "courseId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "courses": { - "name": "courses", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "creator_id": { - "name": "creator_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "courses_creator_id_users_id_fk": { - "name": "courses_creator_id_users_id_fk", - "tableFrom": "courses", - "tableTo": "users", - "columnsFrom": [ - "creator_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "users": { - "name": "users", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "full_name": { - "name": "full_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "password_digest": { - "name": "password_digest", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "users_email_unique": { - "name": "users_email_unique", - "columns": [ - "email" - ], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - } - }, - "views": {}, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} \ No newline at end of file diff --git a/drizzle/meta/0004_snapshot.json b/drizzle/meta/0004_snapshot.json deleted file mode 100644 index f168dd5..0000000 --- a/drizzle/meta/0004_snapshot.json +++ /dev/null @@ -1,217 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "3e23df0c-00c3-464d-a10c-e0b7b1c05374", - "prevId": "e77b7827-d467-4a50-b814-bb8a0138c27b", - "tables": { - "course_lessons": { - "name": "course_lessons", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "courseId": { - "name": "courseId", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "body": { - "name": "body", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "course_lessons_courseId_courses_id_fk": { - "name": "course_lessons_courseId_courses_id_fk", - "tableFrom": "course_lessons", - "tableTo": "courses", - "columnsFrom": [ - "courseId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "courses": { - "name": "courses", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "creator_id": { - "name": "creator_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "courses_creator_id_users_id_fk": { - "name": "courses_creator_id_users_id_fk", - "tableFrom": "courses", - "tableTo": "users", - "columnsFrom": [ - "creator_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "users": { - "name": "users", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "full_name": { - "name": "full_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "password_digest": { - "name": "password_digest", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "phone": { - "name": "phone", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "users_email_unique": { - "name": "users_email_unique", - "columns": [ - "email" - ], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - } - }, - "views": {}, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} \ No newline at end of file diff --git a/drizzle/meta/0005_snapshot.json b/drizzle/meta/0005_snapshot.json deleted file mode 100644 index 5dd132a..0000000 --- a/drizzle/meta/0005_snapshot.json +++ /dev/null @@ -1,217 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "b62f5816-6b6b-4870-b90f-7b560edea87f", - "prevId": "3e23df0c-00c3-464d-a10c-e0b7b1c05374", - "tables": { - "course_lessons": { - "name": "course_lessons", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "courseId": { - "name": "courseId", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "body": { - "name": "body", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "course_lessons_courseId_courses_id_fk": { - "name": "course_lessons_courseId_courses_id_fk", - "tableFrom": "course_lessons", - "tableTo": "courses", - "columnsFrom": [ - "courseId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "restrict", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "courses": { - "name": "courses", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "creator_id": { - "name": "creator_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "courses_creator_id_users_id_fk": { - "name": "courses_creator_id_users_id_fk", - "tableFrom": "courses", - "tableTo": "users", - "columnsFrom": [ - "creator_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "restrict", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "users": { - "name": "users", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "full_name": { - "name": "full_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "password_digest": { - "name": "password_digest", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "phone": { - "name": "phone", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "users_email_unique": { - "name": "users_email_unique", - "columns": [ - "email" - ], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - } - }, - "views": {}, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index c15db94..9e7a827 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -1,47 +1,12 @@ { "version": "7", - "dialect": "sqlite", + "dialect": "postgresql", "entries": [ { "idx": 0, - "version": "6", - "when": 1724955209819, - "tag": "0000_omniscient_warbound", - "breakpoints": true - }, - { - "idx": 1, - "version": "6", - "when": 1787444426840, - "tag": "0001_friendly_forge", - "breakpoints": true - }, - { - "idx": 2, - "version": "6", - "when": 1787444748820, - "tag": "0002_handy_paladin", - "breakpoints": true - }, - { - "idx": 3, - "version": "6", - "when": 1787446852568, - "tag": "0003_common_sabra", - "breakpoints": true - }, - { - "idx": 4, - "version": "6", - "when": 1787542465771, - "tag": "0004_giant_hammerhead", - "breakpoints": true - }, - { - "idx": 5, - "version": "6", - "when": 1787572540422, - "tag": "0005_curious_joshua_kane", + "version": "7", + "when": 1787575623941, + "tag": "0000_nasty_marvel_zombies", "breakpoints": true } ] diff --git a/package.json b/package.json index 47d7890..599e6d0 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "test": "test" }, "dependencies": { + "@electric-sql/pglite": "^0.5.6", "@fastify/autoload": "^6.5.0", "@fastify/cors": "^11.3.0", "@fastify/env": "^7.0.0", @@ -29,7 +30,6 @@ "@scalar/fastify-api-reference": "^1.66.1", "@typespec/openapi": "^1.15.0", "ajv-formats": "^3.0.1", - "better-sqlite3": "^13.0.3", "drizzle-orm": "^0.45.2", "es-toolkit": "^1.51.0", "fastify": "^5.12.1", @@ -44,7 +44,6 @@ "@hey-api/openapi-ts": "0.0.0-next-20260819153534", "@redocly/cli": "^2.47.0", "@stoplight/prism-cli": "^5.16.0", - "@types/better-sqlite3": "^9.6.0", "@typespec/compiler": "^1.15.0", "@typespec/http": "^1.15.0", "@typespec/json-schema": "^1.15.0", @@ -64,8 +63,8 @@ }, "scripts": { "test": "vitest run", - "start": "NODE_OPTIONS='--import ./src/telemetry.ts' fastify start -l info src/app.ts", - "dev": "FASTIFY_AUTOLOAD_TYPESCRIPT=1 NODE_OPTIONS='--import ./src/telemetry.ts' fastify start -w -l info -P src/app.ts", + "start": "NODE_OPTIONS='--import ./src/telemetry.ts' fastify start -l info --plugin-timeout 60000 src/app.ts", + "dev": "FASTIFY_AUTOLOAD_TYPESCRIPT=1 NODE_OPTIONS='--import ./src/telemetry.ts' fastify start -w -l info -P --plugin-timeout 60000 src/app.ts", "lint": "oxlint --config=.oxlintrc.json .", "lint:fix": "oxfmt --ignore-path=.oxfmtignore . && oxlint --config=.oxlintrc.json --fix .", "format": "oxfmt --ignore-path=.oxfmtignore .", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a61c9ad..b6f5ccc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@electric-sql/pglite': + specifier: ^0.5.6 + version: 0.5.6 '@fastify/autoload': specifier: ^6.5.0 version: 6.5.0 @@ -62,12 +65,9 @@ importers: ajv-formats: specifier: ^3.0.1 version: 3.0.1(ajv@8.20.0) - better-sqlite3: - specifier: ^13.0.3 - version: 13.0.3 drizzle-orm: specifier: ^0.45.2 - version: 0.45.2(@opentelemetry/api@1.9.1)(@types/better-sqlite3@9.6.0)(better-sqlite3@13.0.3) + version: 0.45.2(@electric-sql/pglite@0.5.6)(@opentelemetry/api@1.9.1) es-toolkit: specifier: ^1.51.0 version: 1.51.0 @@ -102,9 +102,6 @@ importers: '@stoplight/prism-cli': specifier: ^5.16.0 version: 5.16.0(supports-color@7.2.0) - '@types/better-sqlite3': - specifier: ^9.6.0 - version: 9.6.0 '@typespec/compiler': specifier: ^1.15.0 version: 1.15.0(@types/node@26.2.0) @@ -171,7 +168,6 @@ packages: '@babel/parser@7.29.8': resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==} engines: {node: '>=6.0.0'} - hasBin: true '@babel/types@7.29.8': resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} @@ -184,6 +180,9 @@ packages: '@drizzle-team/brocli@0.10.2': resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} + '@electric-sql/pglite@0.5.6': + resolution: {integrity: sha512-ipn6AOHouIRwNSVL+0hIRuM3yDwAWgRmvZuj+45sudzKpmdPbG8hQuQDZ4A8l7U8E0l7u6pJ/VtMEW42w+vEKQ==} + '@esbuild-kit/core-utils@3.3.2': resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} deprecated: 'Merged into tsx: https://tsx.hirok.io' @@ -707,7 +706,6 @@ packages: '@grpc/proto-loader@0.8.1': resolution: {integrity: sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==} engines: {node: '>=6'} - hasBin: true '@hey-api/codegen-cli@0.0.0-next-20260819153534': resolution: {integrity: sha512-m3OQdK5mQU60MfenUR4frRV2bWG11yk3MwKaU7i5oNmhAzSyjDGZCWy/VcQ0QaqBDs4zAU1KdQ0/va7BbZQmPg==} @@ -724,7 +722,6 @@ packages: '@hey-api/openapi-ts@0.0.0-next-20260819153534': resolution: {integrity: sha512-QdzJDPN/HlM8rOf2psnvytFVE4IlZmIZ1IXyroupKVVj5dXo/yMJDHCTLsuE3aT6NzllCbuF0E2HagWu6hiOEQ==} engines: {node: '>=22.18.0'} - hasBin: true '@hey-api/shared@0.0.0-next-20260819153534': resolution: {integrity: sha512-t5QbDNrfknI5hfU8ZoR9k0oI/RH4mlD1NcHyOukGo3l0sm+9raR64hXtMd6RA+Oqyrmb6icKaVbKAfvMw3yAoQ==} @@ -1379,7 +1376,6 @@ packages: '@redocly/cli@2.47.0': resolution: {integrity: sha512-4sv638LZxiUSsNGqfWPzA2hrNM4KrNRc9jOaH9igqDJlZimiGCYliVpbdrWvfL/YlaSnIBpJE1O1CveXDYTTWw==} engines: {node: '>=22.12.0 || >=20.19.0 <21.0.0', npm: '>=10'} - hasBin: true '@rolldown/binding-android-arm64@1.2.3': resolution: {integrity: sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==} @@ -1527,7 +1523,6 @@ packages: '@seriousme/openapi-schema-validator@2.9.1': resolution: {integrity: sha512-EgGqVIP8xiKHmNTHWbrxec+RhD/WPUay7D/erEc7vWoZKxTT9f2aCEu1egKVpxcEbT1z0wdAbWFR9o2Is5FJEw==} - hasBin: true '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -1538,7 +1533,6 @@ packages: '@stoplight/json-schema-generator@1.0.2': resolution: {integrity: sha512-FzSLFoIZc6Lmw3oRE7kU6YUrl5gBmUs//rY59jdFipBoSyTPv5NyqeyTg5mvT6rY1F3qTLU3xgzRi/9Pb9eZpA==} - hasBin: true '@stoplight/json-schema-merge-allof@0.7.8': resolution: {integrity: sha512-JTDt6GYpCWQSb7+UW1P91IAp/pcLWis0mmEzWVFcLsrNgtUYK7JLtYYz0ZPSR4QVL0fJ0YQejM+MPq5iNDFO4g==} @@ -1564,7 +1558,6 @@ packages: '@stoplight/prism-cli@5.16.0': resolution: {integrity: sha512-lkkchTfCVwRjo4GC/lDOK4dFZ8Sxggw4O0V5lpiIKPHRpm1MFnLDsjA55PuP5EZHcmV0jpcQn/qVBOoN2YRVtg==} engines: {node: '>=24.18.0'} - hasBin: true '@stoplight/prism-core@5.16.0': resolution: {integrity: sha512-akuIOfe2jvyPbAu9XOIXP+UpiVl+rctU8laJsxvWsFX2X4m8rANTWC2/682/f35MtJv3h5jCYB4tAt8qJWx/OA==} @@ -1597,9 +1590,6 @@ packages: resolution: {integrity: sha512-JZlVFE6/dYpP9tQmV0/ADfn32L9uFarHWxfcRhReKUnljz1ZiUM5zpX+PH8h5CJs6lao3TuFqnPm9IJJCEkE2w==} engines: {node: '>=10.8'} - '@types/better-sqlite3@9.6.0': - resolution: {integrity: sha512-ZEEwBSgMu7GYJOynoagg5X9JbxfL6dTJDsgViJIqh67jV44kyOr9RXfmFjLK5rzC4MWssP06t9hu/JwGDnUbCg==} - '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -1753,7 +1743,6 @@ packages: '@typespec/compiler@1.15.0': resolution: {integrity: sha512-NbCIRAIzyozchlSaGPVuyV43bC+y+OpXYCuT+8M8kt35Ln9zWzLFcIt7irofQ5QUXBZrHsBFpI5fTOFdCGeWCA==} engines: {node: '>=22.0.0'} - hasBin: true '@typespec/http@1.15.0': resolution: {integrity: sha512-LNyzYHyX3C0xyQQ262jhvIaVzTn7DqiDB2gz45w1m6ngJH9ArGU8XjO3o7PaZkl/nuB5MTpImnsXpHIV1rbi4Q==} @@ -1774,7 +1763,6 @@ packages: '@typespec/openapi3@1.15.0': resolution: {integrity: sha512-Gc6OjBiAtPo3ohlkrDxQKt8+xdeR54tC+Cgu3YLlxUxrWHWYOfWQXzp4NX+sraiFjF6kX7t0G1A1kY7rgy73qw==} engines: {node: '>=22.0.0'} - hasBin: true peerDependencies: '@typespec/compiler': ^1.15.0 '@typespec/events': ^0.85.0 @@ -1949,10 +1937,6 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} - better-sqlite3@13.0.3: - resolution: {integrity: sha512-RbOBxmLBG8uvFUc15X9+9SFemKcQ0WBuISBVkpuiaUB2qblC8UWlHEjdWVoZ8AdhSwmoEgsiXKfopX0CQxaACQ==} - engines: {node: '>=22'} - binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -2155,7 +2139,6 @@ packages: drizzle-kit@0.31.10: resolution: {integrity: sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==} - hasBin: true drizzle-orm@0.45.2: resolution: {integrity: sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==} @@ -2296,17 +2279,14 @@ packages: esbuild@0.18.20: resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} engines: {node: '>=12'} - hasBin: true esbuild@0.25.12: resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} - hasBin: true esbuild@0.28.2: resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} engines: {node: '>=18'} - hasBin: true escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} @@ -2319,7 +2299,6 @@ packages: esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} - hasBin: true estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -2377,7 +2356,6 @@ packages: fast-xml-parser@5.11.0: resolution: {integrity: sha512-9IGxMqvqLOnqP+Egi1nqDHKv5k8aZ7r9n558enxcucmyVGEBNPAU+MOg/8jPIS7rO7sSq4gFm1/nHtiaubMruw==} - hasBin: true fastfall@1.5.1: resolution: {integrity: sha512-KH6p+Z8AKPXnmA7+Iz2Lh8ARCMr+8WNPVludm1LGkZoD2MjY6LVnRMtTKhkdzI+jr0RzQWXKzKyBJm1zoHEL4Q==} @@ -2385,7 +2363,6 @@ packages: fastify-cli@8.0.0: resolution: {integrity: sha512-7Jme/6on3e+IbEGr7D9S1QF1KAahNnsTOcS8rcKzwluCtr54+UA8BetdFxRYyN+zO1yNQnM1NlD00xfwkLYLjw==} - hasBin: true fastify-metrics@13.2.1: resolution: {integrity: sha512-kUJOy6pwNgQATSoUQeWesZ8m8YEnc48wE0SFDLqjI01cWClDEWM6BRCMa6P8MtS1080VX/8TY6Z/LAjig1b7SA==} @@ -2400,7 +2377,6 @@ packages: fastify-openapi-glue@4.11.4: resolution: {integrity: sha512-5dRnF19RXgTXH1dcBoR9uf0mFBeP6k73GO4kPMbKzgCuIRjBSJYdolOrWix3w6rSI8SY+wXpMzeE6EqRUf6O2A==} engines: {node: '>=20.0.0'} - hasBin: true fastify-plugin@4.5.1: resolution: {integrity: sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==} @@ -2488,7 +2464,6 @@ packages: generify@4.2.0: resolution: {integrity: sha512-b4cVhbPfbgbCZtK0dcUc1lASitXGEAIqukV5DDAyWm25fomWnV+C+a1yXvqikcRZXHN2j0pSDyj3cTfzq8pC7Q==} - hasBin: true get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} @@ -2514,7 +2489,6 @@ packages: giget@3.3.1: resolution: {integrity: sha512-r+mvuDjrjMpsdw46Kmeydb8bdHm7wOKw8wNBtTndkjbPjgAp5oUJUxRE76wZFknxIPokfWvep2qSXK37aXE6zg==} - hasBin: true github-slugger@2.0.0: resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} @@ -2616,12 +2590,10 @@ packages: is-docker@2.2.1: resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} engines: {node: '>=8'} - hasBin: true is-docker@3.0.0: resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - hasBin: true is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} @@ -2642,7 +2614,6 @@ packages: is-inside-container@1.0.0: resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} engines: {node: '>=14.16'} - hasBin: true is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} @@ -2681,7 +2652,6 @@ packages: jiti@2.7.0: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} - hasBin: true joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} @@ -2695,11 +2665,9 @@ packages: js-yaml@3.15.1: resolution: {integrity: sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==} - hasBin: true js-yaml@5.2.0: resolution: {integrity: sha512-YeLUMlvR4Ou1B119LIaM0r65JvbOBooJDc9yEu0dClb/uSC5P4FrLU8OCCz/HXWvtPoIrR0dRzABTjo1sTN9Bw==} - hasBin: true jsep@1.4.0: resolution: {integrity: sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==} @@ -2719,11 +2687,9 @@ packages: json-schema-faker@0.5.8: resolution: {integrity: sha512-sqzPEbEDlpiH8U1tfmJHScXHy52onvMxITPsHyhe/jhS83g8TX6ruvRqt/ot1bXUPRsh7Ps1sWqJiBxIXmW5Xw==} - hasBin: true json-schema-faker@0.5.9: resolution: {integrity: sha512-fNKLHgDvfGNNTX1zqIjqFMJjCLzJ2kvnJ831x4aqkAoeE4jE2TxvpJdhOnk3JU3s42vFzmXvkpbYzH5H3ncAzg==} - hasBin: true json-schema-ref-parser@6.1.0: resolution: {integrity: sha512-pXe9H1m6IgIpXmE5JSb8epilNTGsmTb2iPohAXpOdhqGFbQjNeHHsZxU+C8w6T81GZxSPFLeUoqDJmzxx5IGuw==} @@ -2741,7 +2707,6 @@ packages: jsonpath-plus@10.4.0: resolution: {integrity: sha512-T92WWatJXmhBbKsgH/0hl+jxjdXrifi5IKeMY02DWggRxX0UElcbVzPlmgLTbvsPeW1PasQ6xE2Q75stkhGbsA==} engines: {node: '>=18.0.0'} - hasBin: true jsonpointer@5.0.1: resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} @@ -2749,7 +2714,6 @@ packages: jsonrepair@3.15.0: resolution: {integrity: sha512-wy8OTjwsJwQRnQJkKnMJJ9vcytRdBPAgIF/Hy6+s1dAj42BHMKiyL8JzEieIl3JY7idt8eyHwBWTO8mh/+mtwA==} - hasBin: true lefthook-darwin-arm64@2.1.10: resolution: {integrity: sha512-nw+X8wRNDoUUV6WSteyKBbcLySq+fsmZt5WV/s50ZJpysmsDKJOUMln6SllNfP+60dzUahAO7REco/2633BsLg==} @@ -2803,7 +2767,6 @@ packages: lefthook@2.1.10: resolution: {integrity: sha512-K7mM4WoqMwqfXYK11EHy+lSH1uW8XHni3Yn/bSqyerPkUPygGdf3xn18JoV5HyA06xuQL3ofGAOjG01QX9oJ4w==} - hasBin: true leven@4.1.0: resolution: {integrity: sha512-KZ9W9nWDT7rF7Dazg8xyLHGLrmpgq2nVNFUckhqdW3szVP6YhCpp/RAnpmVExA9JvrMynjwSLVrEj3AepHR6ew==} @@ -2985,7 +2948,6 @@ packages: mkdirp@0.5.6: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} - hasBin: true mnemonist@0.40.4: resolution: {integrity: sha512-ZAv+KNavneRVzu4tUeOgzkScI3W5BGwZ3rkxIpKtzzVgfTtWQFN1CgX0U72cyvyh3iTuHL3SiSmrQxTlryEIcw==} @@ -2998,7 +2960,6 @@ packages: mustache@4.2.0: resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} - hasBin: true mute-stream@3.0.0: resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} @@ -3012,7 +2973,6 @@ packages: nanoid@5.1.16: resolution: {integrity: sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==} engines: {node: ^18 || >=20} - hasBin: true negotiator@0.6.3: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} @@ -3038,7 +2998,6 @@ packages: npm-check-updates@23.0.2: resolution: {integrity: sha512-t5tv+d4sP+WbSwcDAFBwDxSUJRomc+TJoURPu7q5B/19toUsR/7eshRxBdWdE7iYw80iE4O4D/7OvCvIcaG8ug==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0, npm: '>=10.0.0'} - hasBin: true object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} @@ -3074,7 +3033,6 @@ packages: oxfmt@0.64.0: resolution: {integrity: sha512-XZ4GFBN/PLbXKq+0zrgpQfPKYuJlUuj+nzZJY7UpIbFMNyefNLCdN9EwViycNqnYcv0wrn0jXcQLlqJp8RCKBg==} engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true peerDependencies: svelte: ^5.0.0 vite-plus: '*' @@ -3087,7 +3045,6 @@ packages: oxlint@1.79.0: resolution: {integrity: sha512-hVJ9hq9m2unPS+Of4eJJgCPdIeCC+3DHEUX3tkmrPJr3OK2hz7PhXwgC+ZP71ZcYu8cCDEtQrqLxWNvxBppBVg==} engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true peerDependencies: oxlint-tsgolint: '>=7.0.2001' vite-plus: '*' @@ -3165,7 +3122,6 @@ packages: pino-pretty@13.1.3: resolution: {integrity: sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==} - hasBin: true pino-std-serializers@3.2.0: resolution: {integrity: sha512-EqX4pwDPrt3MuOAAUBMU0Tk5kR/YcCM5fNPEzgCO2zJ5HfX0vbiH9HbJglnyeQsN96Kznae6MWD47pZB5avTrg==} @@ -3175,11 +3131,9 @@ packages: pino@10.3.1: resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} - hasBin: true pino@6.14.0: resolution: {integrity: sha512-iuhEDel3Z3hF9Jfe44DPXR8l07bhjuFY3GMHIXbjnY9XcafbyDDwl2sN2vw2GjMPf5Nkoe+OFao7ffn9SXaKDg==} - hasBin: true pkg-conf@2.1.0: resolution: {integrity: sha512-C+VUP+8jis7EsQZIhDYmS5qlNtjv2yP4SNtjXK9AP1ZcTRlnSfuumaTnRfYZnYgUUYVIKqL0fRvmUGDV2fmp6g==} @@ -3211,7 +3165,6 @@ packages: prettier@3.9.6: resolution: {integrity: sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==} engines: {node: '>=14'} - hasBin: true pretty-data@0.40.0: resolution: {integrity: sha512-YFLnEdDEDnkt/GEhet5CYZHCvALw6+Elyb/tp8kQG03ZSIuzeaDWpZYndCXwgqu4NAjh1PI534dhDS1mHarRnQ==} @@ -3325,7 +3278,6 @@ packages: safe-regex2@5.1.1: resolution: {integrity: sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==} - hasBin: true safe-stable-stringify@1.1.1: resolution: {integrity: sha512-ERq4hUjKDbJfE4+XtZLFPCDi8Vb1JqaxAPTxWFLBx8XcAlf9Bda/ZJdVezs/NAfsMQScyIlUMx+Yeu7P7rx5jw==} @@ -3346,12 +3298,10 @@ packages: semver@7.6.3: resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} engines: {node: '>=10'} - hasBin: true semver@7.8.5: resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} - hasBin: true set-cookie-parser@2.7.2: resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} @@ -3539,7 +3489,6 @@ packages: tsx@4.23.12: resolution: {integrity: sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==} engines: {node: '>=18.0.0'} - hasBin: true type-fest@5.8.0: resolution: {integrity: sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==} @@ -3556,7 +3505,6 @@ packages: typescript@7.0.2: resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} engines: {node: '>=16.20.0'} - hasBin: true undici-types@8.3.0: resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} @@ -3581,7 +3529,6 @@ packages: uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). - hasBin: true validate.io-array@1.0.6: resolution: {integrity: sha512-DeOy7CnPEziggrOO5CZhVKJw6S3Yi7e9e65R1Nl/RTN1vTQKnzjfvks0/8kQ40FP/dsjRAOd4hxmJ7uLa6vxkg==} @@ -3652,7 +3599,6 @@ packages: vitest@4.1.11: resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} - hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 @@ -3705,7 +3651,6 @@ packages: vscode-languageserver@10.1.0: resolution: {integrity: sha512-9gEWpXkYGXoqG7pBnE8O8hx/yP7+Aabn4+peQ3KDicQv6qunHSWyLTud3OF0w4S2+HfDD+5HqYKiXQW9HAU6mA==} - hasBin: true walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} @@ -3723,7 +3668,6 @@ packages: why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} - hasBin: true wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} @@ -3763,7 +3707,6 @@ packages: yaml@2.9.0: resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} engines: {node: '>= 14.6'} - hasBin: true yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} @@ -3817,6 +3760,8 @@ snapshots: '@drizzle-team/brocli@0.10.2': {} + '@electric-sql/pglite@0.5.6': {} + '@esbuild-kit/core-utils@3.3.2': dependencies: esbuild: 0.18.20 @@ -5034,10 +4979,6 @@ snapshots: '@stoplight/yaml-ast-parser': 0.0.50 tslib: 2.8.1 - '@types/better-sqlite3@9.6.0': - dependencies: - '@types/node': 26.2.0 - '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -5318,10 +5259,6 @@ snapshots: balanced-match@4.0.4: {} - better-sqlite3@13.0.3: - dependencies: - node-addon-api: 8.9.2 - binary-extensions@2.3.0: {} bintrees@1.0.2: {} @@ -5519,11 +5456,10 @@ snapshots: esbuild: 0.25.12 tsx: 4.23.12 - drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/better-sqlite3@9.6.0)(better-sqlite3@13.0.3): + drizzle-orm@0.45.2(@electric-sql/pglite@0.5.6)(@opentelemetry/api@1.9.1): optionalDependencies: + '@electric-sql/pglite': 0.5.6 '@opentelemetry/api': 1.9.1 - '@types/better-sqlite3': 9.6.0 - better-sqlite3: 13.0.3 dunder-proto@1.0.1: dependencies: @@ -6290,7 +6226,8 @@ snapshots: negotiator@0.6.3: {} - node-addon-api@8.9.2: {} + node-addon-api@8.9.2: + optional: true node-fetch@2.7.0: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b9c9ef5..b0c903e 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -7,6 +7,5 @@ allowBuilds: '@scarf/scarf': true 'lefthook': true - 'better-sqlite3': true 'esbuild': true 'protobufjs': true diff --git a/scripts/contract-test.sh b/scripts/contract-test.sh index 68c32f0..7b2eb88 100755 --- a/scripts/contract-test.sh +++ b/scripts/contract-test.sh @@ -16,9 +16,12 @@ export JWT_SECRET="${JWT_SECRET:-$(openssl rand -hex 32)}" # подряд и упирается в него, а не в поведение API. export RATE_LIMIT_MAX=1000000 # Шеддинг под нагрузкой мешает так же, как лимитер: schemathesis шлёт сотни -# запросов подряд и упирается в 503, а не в поведение API. -export MAX_EVENT_LOOP_DELAY=600000 -export MAX_EVENT_LOOP_UTILIZATION=1 +# запросов подряд и упирается в 503, а не в поведение API. Ноль, а не заведомо +# большой порог: под нагрузкой гистограмма monitorEventLoopDelay возвращает +# mean = Infinity, и любое конечное значение оказывается меньше. Ноль +# under-pressure понимает как «проверку не делать вовсе». +export MAX_EVENT_LOOP_DELAY=0 +export MAX_EVENT_LOOP_UTILIZATION=0 # Порт проверяется до запуска: если на нём кто-то уже слушает, прогон уходил в # чужой процесс и зеленел, ничего не проверив в текущем коде. Молчаливое ложное @@ -30,7 +33,7 @@ if curl -sf "$BASE/openapi.json" >/dev/null 2>&1; then fi LOG=$(mktemp) -pnpm exec fastify start -l error -p "$PORT" src/app.ts >"$LOG" 2>&1 & +pnpm exec fastify start -l error -p "$PORT" --plugin-timeout 60000 src/app.ts >"$LOG" 2>&1 & APP_PID=$! trap 'kill "$APP_PID" 2>/dev/null || true; rm -f "$LOG"' EXIT diff --git a/src/app.ts b/src/app.ts index 19c40a5..d6988ea 100644 --- a/src/app.ts +++ b/src/app.ts @@ -13,6 +13,15 @@ import serviceHandlersV2 from "./routes/v2/index.ts"; export interface AppOptions extends FastifyServerOptions, Partial {} +// Ключи не проверяются: в колонку уезжают значения, а поля, которых нет в +// схеме, сериализатор и валидаторы всё равно отбрасывают. +function containsNul(value: unknown): boolean { + if (typeof value === "string") return value.includes("\u0000"); + if (Array.isArray(value)) return value.some(containsNul); + if (value !== null && typeof value === "object") return Object.values(value).some(containsNul); + return false; +} + // Pass --options via CLI arguments in command to enable these options. const options: AppOptions = {}; @@ -51,6 +60,20 @@ const app: FastifyPluginAsync = async (fastify, opts): Promise }); }); + // Postgres не хранит NUL (U+0000) в text-колонках: строка с ним не + // сохраняется в принципе. Без этой проверки такой ввод доезжал до insert и + // уходил наружу как 500 — нашёл контрактный прогон, генерируя строки со + // спецсимволами. Sqlite такое принимал, поэтому раньше проверки не было. + // + // Хук общий, а не правило в валидаторах: ограничение не бизнес-правило + // конкретной модели, а свойство хранилища, и касается каждого текстового + // поля во всех операциях. + fastify.addHook("preValidation", async (request) => { + if (containsNul(request.body)) { + throw httpErrors.badRequest("Text fields must not contain the NUL character (U+0000)"); + } + }); + fastify.addContentTypeParser( "application/problem+json", { parseAs: "string" }, diff --git a/src/db/schema.ts b/src/db/schema.ts index 4efe206..ae40e40 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -1,24 +1,28 @@ -import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; +import { integer, pgTable, text, timestamp } from "drizzle-orm/pg-core"; -// Точность миллисекундная: из updatedAt строится ETag, а при секундной две -// правки в пределах одной секунды дают одинаковый валидатор. +// Нативный timestamptz, а не число: тип колонки теперь говорит, что в ней +// лежит время. Раньше это был integer с unix-миллисекундами, и смысл колонки +// держался на кодеке drizzle да на комментарии рядом. // // Таймстемпы ведёт drizzle, а не SQL-дефолты: updatedAt иначе не обновляется // никогда — обработчики его не писали, а DEFAULT срабатывает только на INSERT. -// Тип integer, а не text: раньше default (unixepoch()) клал число в текстовую -// колонку, и наружу уезжала строка вида "1787349516". const timestamps = { - createdAt: integer("created_at", { mode: "timestamp_ms" }) + createdAt: timestamp("created_at", { withTimezone: true }) .notNull() .$defaultFn(() => new Date()), - updatedAt: integer("updated_at", { mode: "timestamp_ms" }) + updatedAt: timestamp("updated_at", { withTimezone: true }) .notNull() .$defaultFn(() => new Date()) .$onUpdate(() => new Date()), }; -export const users = sqliteTable("users", { - id: integer("id").primaryKey(), +// byDefault, а не always: identity always отвергает вставку с явным id, и +// сиды с тестами, которые заводят строку целиком, упёрлись бы в него. В +// sqlite id был алиасом rowid и подставлялся сам — byDefault повторяет это. +const id = integer("id").primaryKey().generatedByDefaultAsIdentity(); + +export const users = pgTable("users", { + id, fullName: text("full_name"), email: text("email").notNull().unique(), passwordDigest: text("password_digest").notNull(), @@ -29,8 +33,8 @@ export const users = sqliteTable("users", { ...timestamps, }); -export const courses = sqliteTable("courses", { - id: integer("id").primaryKey(), +export const courses = pgTable("courses", { + id, name: text("name").notNull(), // Запрет, а не каскад: курс не перестаёт существовать от того, что автор // ушёл. Что делать с осиротевшими курсами — решение приложения, и оно @@ -42,8 +46,8 @@ export const courses = sqliteTable("courses", { ...timestamps, }); -export const courseLessons = sqliteTable("course_lessons", { - id: integer("id").primaryKey(), +export const courseLessons = pgTable("course_lessons", { + id, name: text("name").notNull(), // Тоже запрет, хотя урок вне курса не существует: удаление уроков делает // обработчик в транзакции. Поведение видно в коде, а не только в миграции. diff --git a/src/plugins/drizzle.ts b/src/plugins/drizzle.ts index fd79bd7..f6645a1 100644 --- a/src/plugins/drizzle.ts +++ b/src/plugins/drizzle.ts @@ -1,15 +1,23 @@ -import Database from "better-sqlite3"; +import { PGlite } from "@electric-sql/pglite"; -import { drizzle } from "drizzle-orm/better-sqlite3"; -import { migrate } from "drizzle-orm/better-sqlite3/migrator"; +import { drizzle } from "drizzle-orm/pglite"; +import { migrate } from "drizzle-orm/pglite/migrator"; import fp from "fastify-plugin"; import * as schemas from "../db/schema.ts"; export default fp( async (fastify) => { - const sqlite = new Database(":memory:"); - const db = drizzle(sqlite, { schema: schemas }); - migrate(db, { migrationsFolder: "drizzle" }); + // PGlite без аргументов — postgres в памяти процесса, отдельного сервиса + // по-прежнему нет. Взят ради нативных типов: timestamptz в схеме это + // время, а не integer, которому смысл придаёт кодек drizzle. + const client = new PGlite(); + const db = drizzle(client, { schema: schemas }); + await migrate(db, { migrationsFolder: "drizzle" }); + + // Инстанс закрывается вместе с приложением: в отличие от sqlite :memory:, + // за каждым PGlite стоит wasm-куча в десятки мегабайт, а тесты поднимают + // приложение десятки раз в одном процессе. + fastify.addHook("onClose", () => client.close()); // Сиды — инструмент разработки, и импорт у них динамический не для красоты: // db/seeds.ts тянет @faker-js/faker из devDependencies, поэтому со diff --git a/src/plugins/observability.ts b/src/plugins/observability.ts index 806a59a..b9e25ff 100644 --- a/src/plugins/observability.ts +++ b/src/plugins/observability.ts @@ -18,7 +18,7 @@ export default fp( routeOpts: { logLevel: "warn" }, }, healthCheck: async () => { - await fastify.db.get(sql`select 1`); + await fastify.db.execute(sql`select 1`); return true; }, healthCheckInterval: 5_000, diff --git a/src/routes/api/courses.ts b/src/routes/api/courses.ts index 9ee30af..1d9b10e 100644 --- a/src/routes/api/courses.ts +++ b/src/routes/api/courses.ts @@ -82,11 +82,11 @@ const handlers = defineHandlers({ // Уроки удаляются явно и в одной транзакции с курсом, а не каскадом из // миграции: урок вне курса не существует, но поведение должно быть видно в // коде и покрыто тестом, а не спрятано в DDL. - request.db.transaction((tx) => { - tx.delete(schemas.courseLessons) - .where(eq(schemas.courseLessons.courseId, request.params.id)) - .run(); - tx.delete(schemas.courses).where(eq(schemas.courses.id, request.params.id)).run(); + await request.db.transaction(async (tx) => { + await tx + .delete(schemas.courseLessons) + .where(eq(schemas.courseLessons.courseId, request.params.id)); + await tx.delete(schemas.courses).where(eq(schemas.courses.id, request.params.id)); }); return reply.code(204).send(); }, diff --git a/src/types/fastify.d.ts b/src/types/fastify.d.ts index d7ce089..29658ba 100644 --- a/src/types/fastify.d.ts +++ b/src/types/fastify.d.ts @@ -1,4 +1,4 @@ -import type { drizzle } from "drizzle-orm/better-sqlite3"; +import type { drizzle } from "drizzle-orm/pglite"; import "@fastify/jwt"; // import type { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'; // import { Type } from '@sinclair/typebox' diff --git a/src/types/index.ts b/src/types/index.ts index cf17486..efdbd37 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,4 +1,4 @@ -import type { drizzle } from "drizzle-orm/better-sqlite3"; +import type { drizzle } from "drizzle-orm/pglite"; import type * as schemas from "../db/schema.ts"; // Таблица, а не «схема»: schema в проекте уже занято под другое. diff --git a/test/db/timestamps.test.ts b/test/db/timestamps.test.ts index 05db610..ce1d968 100644 --- a/test/db/timestamps.test.ts +++ b/test/db/timestamps.test.ts @@ -10,9 +10,8 @@ import * as schemas from "../../src/db/schema.ts"; test("updatedAt moves on update and createdAt stays put", async () => { const app = await build(); - // Отметка ставится заведомо старой, а не через паузу в тесте: хранятся они с - // точностью до секунды, и sleep на секунду ради одного сравнения — плохой - // обмен. + // Отметка ставится заведомо старой, а не через паузу в тесте: ждать, пока + // часы уйдут вперёд, ради одного сравнения — плохой обмен. const past = new Date("2020-01-01T00:00:00Z"); const [user] = await app.db .insert(schemas.users) diff --git a/test/helper.ts b/test/helper.ts index dbc4fce..1ec74ad 100644 --- a/test/helper.ts +++ b/test/helper.ts @@ -12,7 +12,10 @@ import { createClient, createConfig } from "../src/types/handlers/v1/client/inde // any, а покрытие показывало по обработчикам единицы процентов при живых // тестах на них. async function build(): Promise { - const fastify = Fastify({ logger: { level: "error" } }); + // pluginTimeout поднят с дефолтных 10 секунд: drizzle поднимает PGlite — + // postgres в wasm — прогоняет миграции и сиды, и на параллельном прогоне + // десятка файлов старт в десять секунд не укладывается. + const fastify = Fastify({ logger: { level: "error" }, pluginTimeout: 60_000 }); // fp снимает инкапсуляцию, и декораторы приложения (db, jwt) видны снаружи. // В бою так не нужно — это только чтобы тесты могли дотянуться до базы. fastify.register(fp(app)); diff --git a/test/routes/api/conditional.test.ts b/test/routes/api/conditional.test.ts index 2598e8e..f51e749 100644 --- a/test/routes/api/conditional.test.ts +++ b/test/routes/api/conditional.test.ts @@ -1,6 +1,8 @@ import { test } from "vitest"; import * as assert from "node:assert"; +import { eq } from "drizzle-orm"; import { build, getAuthHeader } from "../../helper.ts"; +import * as schemas from "../../../src/db/schema.ts"; // Без условных запросов два одновременных PUT молча перезаписывают друг друга: // второй не знает, что запись изменилась после того, как он её прочитал. @@ -49,7 +51,11 @@ test("a stale If-Match is rejected instead of overwriting", async () => { }); assert.equal(conflicting.statusCode, 412, conflicting.body); - const stored = await app.db.query.courses.findFirst(); + // Перечитывается именно та запись, а не «первая»: без ORDER BY postgres + // порядок строк не обещает, и после UPDATE запись уезжает в конец. + const stored = await app.db.query.courses.findFirst({ + where: eq(schemas.courses.id, course.id), + }); assert.equal(stored?.name, "Changed by someone else"); }); diff --git a/test/routes/api/edge-cases.test.ts b/test/routes/api/edge-cases.test.ts index 792210c..28a108c 100644 --- a/test/routes/api/edge-cases.test.ts +++ b/test/routes/api/edge-cases.test.ts @@ -121,3 +121,19 @@ test("a full name shorter than the response model allows is rejected", async () assert.equal(res.statusCode, 400, res.body); }); + +// Postgres не хранит NUL в text-колонках, и без проверки на входе такой ввод +// доезжал до insert и уходил наружу как 500. Нашёл контрактный прогон. +test("a NUL character in a text field is rejected, not stored", async () => { + const app = await build(); + + const res = await app.inject({ + method: "post", + url: "/users", + body: { email: "nul\u0000@hexlet.io", password: "correct-horse-battery-staple" }, + }); + assert.equal(res.statusCode, 400, res.body); + + const stored = await app.db.query.users.findMany(); + assert.ok(!stored.some((user) => user.email.includes("nul"))); +}); diff --git a/test/routes/api/ownership.test.ts b/test/routes/api/ownership.test.ts index eff85b2..11ca0c2 100644 --- a/test/routes/api/ownership.test.ts +++ b/test/routes/api/ownership.test.ts @@ -1,7 +1,9 @@ import { test } from "vitest"; import * as assert from "node:assert"; +import { eq } from "drizzle-orm"; import { build, getAuthHeader } from "../../helper.ts"; import { buildCourseLesson } from "../../../src/lib/data.ts"; +import * as schemas from "../../../src/db/schema.ts"; // Возвращает курс и пользователя, который его не создавал. async function buildWithOutsider() { @@ -29,7 +31,9 @@ test("a stranger cannot update someone else's course", async () => { }); assert.equal(res.statusCode, 403, res.body); - const unchanged = await app.db.query.courses.findFirst(); + const unchanged = await app.db.query.courses.findFirst({ + where: eq(schemas.courses.id, course.id), + }); assert.equal(unchanged?.name, course.name); }); diff --git a/vitest.config.ts b/vitest.config.ts index 60dad3a..084fbe8 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -40,8 +40,13 @@ export default defineConfig({ RATE_LIMIT_MAX: "100000", // Шеддинг под нагрузкой выключен: тесты гоняются параллельно, event loop // забит по определению, и under-pressure иначе отдаёт 503 на всё. - MAX_EVENT_LOOP_DELAY: "600000", - MAX_EVENT_LOOP_UTILIZATION: "1", + // + // Ноль, а не заведомо большой порог: под нагрузкой гистограмма + // monitorEventLoopDelay возвращает mean = Infinity, и любое конечное + // значение оказывается меньше. Ноль under-pressure понимает как «проверку + // не делать вовсе». + MAX_EVENT_LOOP_DELAY: "0", + MAX_EVENT_LOOP_UTILIZATION: "0", }, }, });