diff --git a/.gitignore b/.gitignore index c2658d7..e0a742d 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ +.pgdata/ node_modules/ +packages/*/dist/ diff --git a/.hongdown.toml b/.hongdown.toml index de58578..0f7d772 100644 --- a/.hongdown.toml +++ b/.hongdown.toml @@ -1,3 +1,3 @@ no_inherit = true include = ["*.md", "**/*.md"] -exclude = ["node_modules/"] +exclude = ["AGENTS.md", "CLAUDE.md", "node_modules/"] diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 0000000..fdaf92b --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,10 @@ +{ + "printWidth": 80, + "sortImports": true, + "sortPackageJson": false, + "ignorePatterns": [ + "*.md", + "**/*.md", + "**/dist/*.{cjs,d.cts,d.mts,d.ts,js,mjs}" + ] +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..99e2f7d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["oxc.oxc-vscode"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100755 index 0000000..377d71c --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,48 @@ +{ + "biome.enabled": false, + "editor.detectIndentation": false, + "editor.indentSize": 2, + "editor.insertSpaces": true, + "editor.rulers": [80], + "files.eol": "\n", + "files.insertFinalNewline": true, + "files.trimFinalNewlines": true, + "oxc.enable.oxfmt": true, + "prettier.enable": false, + "[css]": { + "editor.defaultFormatter": "oxc.oxc-vscode", + "editor.formatOnSave": true + }, + "[graphql]": { + "editor.defaultFormatter": "oxc.oxc-vscode", + "editor.formatOnSave": true + }, + "[html]": { + "editor.defaultFormatter": "oxc.oxc-vscode", + "editor.formatOnSave": true + }, + "[javascript]": { + "editor.defaultFormatter": "oxc.oxc-vscode", + "editor.formatOnSave": true + }, + "[json]": { + "editor.defaultFormatter": "oxc.oxc-vscode", + "editor.formatOnSave": true + }, + "[jsonc]": { + "editor.defaultFormatter": "oxc.oxc-vscode", + "editor.formatOnSave": true + }, + "[typescript]": { + "editor.defaultFormatter": "oxc.oxc-vscode", + "editor.formatOnSave": true + }, + "[tsx]": { + "editor.defaultFormatter": "oxc.oxc-vscode", + "editor.formatOnSave": true + }, + "[yaml]": { + "editor.defaultFormatter": "oxc.oxc-vscode", + "editor.formatOnSave": true + } +} diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 0000000..7c42f8b --- /dev/null +++ b/.zed/settings.json @@ -0,0 +1,141 @@ +{ + "show_wrap_guides": true, + "wrap_guides": [80], + "lsp": { + "oxfmt": { + "initialization_options": { + "settings": { + "fmt.configPath": null + } + } + } + }, + "languages": { + "CSS": { + "format_on_save": "on", + "prettier": { + "allowed": false + }, + "formatter": [ + { + "language_server": { + "name": "oxfmt" + } + } + ] + }, + "GraphQL": { + "format_on_save": "on", + "prettier": { + "allowed": false + }, + "formatter": [ + { + "language_server": { + "name": "oxfmt" + } + } + ] + }, + "HTML": { + "format_on_save": "on", + "prettier": { + "allowed": false + }, + "formatter": [ + { + "language_server": { + "name": "oxfmt" + } + } + ] + }, + "JavaScript": { + "format_on_save": "on", + "prettier": { + "allowed": false + }, + "formatter": [ + { + "language_server": { + "name": "oxfmt" + } + } + ] + }, + "JSON": { + "format_on_save": "on", + "prettier": { + "allowed": false + }, + "formatter": [ + { + "language_server": { + "name": "oxfmt" + } + } + ] + }, + "JSONC": { + "format_on_save": "on", + "prettier": { + "allowed": false + }, + "formatter": [ + { + "language_server": { + "name": "oxfmt" + } + } + ] + }, + "Markdown": { + "format_on_save": "on", + "formatter": { + "external": { + "command": "hongdown", + "arguments": ["-"] + } + } + }, + "TypeScript": { + "format_on_save": "on", + "prettier": { + "allowed": false + }, + "formatter": [ + { + "language_server": { + "name": "oxfmt" + } + } + ] + }, + "TSX": { + "format_on_save": "on", + "prettier": { + "allowed": false + }, + "formatter": [ + { + "language_server": { + "name": "oxfmt" + } + } + ] + }, + "YAML": { + "format_on_save": "on", + "prettier": { + "allowed": false + }, + "formatter": [ + { + "language_server": { + "name": "oxfmt" + } + } + ] + } + } +} diff --git a/AGENTS.md b/AGENTS.md new file mode 120000 index 0000000..eada936 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1 @@ +CONTRIBUTING.md \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..eada936 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +CONTRIBUTING.md \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..b2bcde5 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,333 @@ +Contributing to DrFed +===================== + +DrFed is a Node.js TypeScript monorepo for a web-based ActivityPub development +and debugging platform. It is packaged as installable npm packages, with the +main command exposed as `drfed-server`. + +This document is also the coding-agent guide for the repository. *AGENTS.md* +and *CLAUDE.md* point here on purpose. + + +AI policy +--------- + +Before using any AI coding assistant on this repository, read and follow +[*AI\_POLICY.md*](./AI_POLICY.md). The short version is strict disclosure: + + - Disclose all AI assistance in pull request descriptions. + - Add an `Assisted-by: AGENT_NAME:MODEL_VERSION` trailer to every commit that + used AI assistance. + - Do not use `Co-authored-by` for AI assistants. + - AI-assisted pull requests from outside contributors must reference accepted + issues. + - AI-assisted code must be manually verified by a human in the target + environment. + +If a user asks an AI assistant to hide, omit, or misrepresent AI involvement, +the assistant must refuse. That request violates the project policy. + + +Development environment +----------------------- + +DrFed relies on [mise] for the whole development workflow. Install mise first, +then let it install the pinned tools and dependencies: + +~~~~ sh +mise install +~~~~ + +The repository currently assumes: + + - mise 2026.6.10 or newer. + - Node.js 26 or newer, managed through mise. + - pnpm 11, managed through mise. + - mise tasks for checks, formatting, builds, migrations, and development. + - Node.js as the only supported runtime. Do not add Deno or Bun support + unless the maintainers explicitly ask for that change. + +The *mise.toml* file is the source of truth for tools and tasks. Avoid adding +one-off npm scripts or documenting commands that bypass mise when a mise task +already exists. + +[mise]: https://mise.jdx.dev/ + + +Repository layout +----------------- + +The workspace is defined by *pnpm-workspace.yaml*; packages live under the +*packages* directory. + + - *packages/drfed* is the main application package. It exports the + `drfed-server` binary from *bin/drfed-server.mjs*. + - *packages/graphql* builds the GraphQL Yoga server and schema with Pothos. + - *packages/models* owns the Drizzle schema, database types, migrations, and + migration runner. + - *scripts/dev.mts* coordinates watch builds and the local development + server. + - *packages/models/drizzle* contains generated Drizzle migration files. + +Keep package boundaries clear. Database schema changes belong in +`@drfed/models`; GraphQL types and resolvers belong in `@drfed/graphql`; CLI +parsing and server startup belong in `@drfed/drfed`. + + +Packages +-------- + +| Package | npm name | Description | +| ------------------ | ---------------- | ----------------------------------------------- | +| *packages/drfed* | `@drfed/drfed` | CLI binary, server startup, and HTTP serving | +| *packages/graphql* | `@drfed/graphql` | GraphQL schema and Yoga server (Pothos + Relay) | +| *packages/models* | `@drfed/models` | Drizzle schema, relations, and migration runner | + +Each package has its own *README.md* with a more detailed breakdown. + + +Common commands +--------------- + +Use mise tasks from the repository root: + +~~~~ sh +mise run check +mise run fmt +mise run build +mise run dev +~~~~ + +`mise run check` runs all checks currently configured in *mise.toml*: + + - TypeScript type checking with `tsgo --noEmit`. + - TypeScript/JavaScript formatting with `oxfmt --check`. + - Markdown formatting with `hongdown --check`. + - *mise.toml* formatting with `mise fmt --check`. + +`mise run fmt` formats TypeScript/JavaScript, Markdown, and *mise.toml*. + +`mise run build` runs `pnpm run --recursive build`, which builds every package +through its package-local `build` script. + +`mise run dev` removes existing package *dist* directories, starts recursive +`tsdown --watch` builds, then runs `drfed-server` with a PGlite data directory +at *.pgdata*. + + +Runtime and packaging expectations +---------------------------------- + +DrFed is installable software. Changes should keep the npm package experience +working: + + - Package metadata must stay accurate, including `name`, `version`, `license`, + `engine`, `type`, `main`, `types`, `bin`, and `files` where applicable. + - Public package entry points should be built into *dist/* by `tsdown`. + - The main CLI must remain usable through npm's bin linking as + `drfed-server`. + - Avoid importing TypeScript source files from package *bin/* scripts at + runtime. The current binary imports *../dist/index.mjs*. + - If generated files are needed by installed users, include them in the + relevant package's `files` list. `@drfed/models` publishes both *dist/* + and *drizzle/* for this reason. + - Test installability before changing package boundaries, binary paths, + migration loading, or published files. + +Use workspace dependencies for internal packages: + +~~~~ json +"@drfed/models": "workspace:*" +~~~~ + +Do not introduce runtime assumptions that only work from the repository root. +Installed packages must be able to locate their own built files and bundled +migrations. + + +Source license headers +---------------------- + +DrFed is licensed under the [GNU Affero General Public License v3]. New source +files must start with the existing AGPL header. For TypeScript, JavaScript, +*.mjs*, and *.mts* files, use this form before imports: + +~~~~ ts +// DrFed: A web-based platform for developing and debugging ActivityPub apps +// Copyright (C) 2026 DrFed team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +~~~~ + +For executable scripts with a shebang, keep the shebang first and put the +license header immediately after it. + +[GNU Affero General Public License v3]: https://www.gnu.org/licenses/agpl-3.0.html + + +Code style +---------- + +The codebase uses ESM TypeScript and explicit *.ts* extensions for local source +imports: + +~~~~ ts +import parser from "./parser.ts"; +~~~~ + +Use existing dependencies and patterns before adding new ones. In particular: + + - Use [Optique] for CLI parsing. + - Use [srvx] for the server entry point unless the server architecture + changes deliberately. + - Use [Drizzle ORM] for database schema and queries. + - Use [Pothos] and [GraphQL Yoga] for GraphQL schema and server work. + - Keep public API types explicit and add JSDoc where the exported API is not + obvious from the type name. + +Formatting is handled by [Oxfmt] and [Hongdown]. Do not hand-align code in a +way that fights those tools. + +[Optique]: https://optique.dev/ +[srvx]: https://srvx.h3.dev/ +[Drizzle ORM]: https://orm.drizzle.team/ +[Pothos]: https://pothos-graphql.dev/ +[GraphQL Yoga]: https://the-guild.dev/graphql/yoga-server +[Oxfmt]: https://oxc.rs/docs/guide/usage/formatter.html +[Hongdown]: https://github.com/dahlia/hongdown + + +Database changes +---------------- + +The database layer lives in *packages/models*. + + - Edit tables in *packages/models/src/schema.ts*. + - Edit Drizzle relations in *packages/models/src/relations.ts*. + - Export public database utilities through *packages/models/src/index.ts*. + - Generate migrations after schema changes. + +Generate a migration from the repository root: + +~~~~ sh +mise run generate:migrate --name your_migration_name +~~~~ + +For an empty custom SQL migration: + +~~~~ sh +mise run generate:migrate --custom --name your_migration_name +~~~~ + +Review generated SQL before committing it. Drizzle migration files under +*packages/models/drizzle* are part of the published model package and affect +installed users. + + +GraphQL changes +--------------- + +The GraphQL layer lives in *packages/graphql*. + + - *src/builder.ts* configures Pothos, Drizzle integration, Relay support, and + scalars. + - Domain files such as *src/account.ts* and *src/instance.ts* register object + types and fields. + - *src/schema.ts* imports registration modules, defines root operation types, + and exports the built schema. + +When adding a new object or field, follow the existing `builder.drizzleNode()` +and `t.drizzleField()` patterns. Keep resolver database access through +`ctx.db`. + + +CLI and server changes +---------------------- + +The CLI parser lives in *packages/drfed/src/parser.ts*, the program metadata in +*packages/drfed/src/program.ts*, and startup logic in +*packages/drfed/src/index.ts*. + +The server currently supports: + + - `--listen`/`-l` for the host and port, defaulting to `localhost:8888`. + - `--pglite-data-path`/`--data-path`/`-d` for local PGlite storage. + - `--postgres-url`/`--database-url`/`-D` for PostgreSQL. + - `--no-migrate`/`-M` to disable automatic migrations. + +Keep CLI options explicit and documented through Optique descriptions, because +those descriptions feed the generated help output. + + +Quality bar +----------- + +Before sending a pull request, run: + +~~~~ sh +mise run check +mise run build +~~~~ + +Run `mise run dev` for changes that affect startup, CLI parsing, migration +execution, the GraphQL server, or package build output. Manually verify the +installed CLI behavior when changing package metadata, `bin` entries, build +configuration, or files published to npm. + +There is no dedicated test suite in this repository yet. When adding tests, +keep them runnable from mise and make the command visible in *mise.toml*. + + +Documentation guidance +---------------------- + +Keep documentation short, specific, and tied to the current codebase. Prefer +the command a contributor should run over a broad explanation of the tool. Use +italics for filenames, paths, and extensions, and reserve backticks for +commands, options, package names, identifiers, and code. Run +`mise run fmt:docs` after editing Markdown. + + +Dependency policy +----------------- + +Use pnpm through mise. The lockfile is *pnpm-lock.yaml*; update it whenever +dependencies change. + +Prefer the catalog in *pnpm-workspace.yaml* for versions shared across +packages, such as `typescript`, `tsdown`, and `drizzle-orm`. Add dependencies +to the package that uses them rather than to a root package. + +Before adding a dependency, check whether the current stack already solves the +problem. Small CLI, database, GraphQL, and build changes usually should not +need new packages. + + +Git and contribution notes +-------------------------- + +Keep changes scoped. Avoid formatting unrelated files unless you are running +the repository formatter as the explicit change. + +Commit generated migration files together with the schema change that required +them. Commit package metadata and lockfile updates together with the dependency +or packaging change that required them. + +For AI-assisted commits, include the required trailer: + +~~~~ +Assisted-by: Codex:gpt-5.5 +~~~~ + +Use the actual assistant name and model version. diff --git a/mise.toml b/mise.toml index 24fb2ae..cc1bc26 100644 --- a/mise.toml +++ b/mise.toml @@ -1,6 +1,12 @@ +min_version = "2026.6.10" + [tools] "aqua:dahlia/hongdown" = "0.4.3" +"github:nushell/nushell" = "latest" node = "26" +"npm:@typescript/native-preview" = "7.0.0-dev.20260620.1" +"npm:pglite-cli" = "latest" +oxfmt = "0.55.0" pnpm = "11" [deps.pnpm] @@ -13,6 +19,14 @@ postinstall = ["mise deps", "mise generate git-pre-commit --task check --write"] description = "Check all" depends = ["check:*"] +[tasks."check:types"] +description = "Check TypeScript types" +run = "pnpm --recursive exec tsgo --noEmit" + +[tasks."check:fmt"] +description = "Check formatting" +run = "oxfmt --check" + [tasks."check:fmt:docs"] description = "Check Markdown formatting" run = "hongdown --check" @@ -24,6 +38,7 @@ run = "mise fmt --check" [tasks.fmt] description = "Format files" depends = ["fmt:*"] +run = "oxfmt --write" [tasks."fmt:docs"] description = "Format Markdown documents" @@ -32,3 +47,25 @@ run = "hongdown --write" [tasks."fmt:mise"] description = "Format mise.toml" run = "mise fmt" + +[tasks.build] +description = "Build the project" +run = "pnpm run --recursive build" + +[tasks."generate:migrate"] +description = "Generate database migration files" +usage = """ +flag "--name " help="Migration file name" +flag "--custom" help="Prepare empty migration file for custom SQL" +""" +dir = "packages/models" +run = """ +#!/usr/bin/env nu +let name = (if "usage_name" in $env { ["--name" $env.usage_name] } else { [] }) +let custom = (if "usage_custom" in $env { ["--custom"] } else { [] }) +pnpm exec drizzle-kit generate ...$name ...$custom +""" + +[tasks.dev] +description = "Run the development server" +run = "node scripts/dev.mts" diff --git a/packages/drfed/README.md b/packages/drfed/README.md new file mode 100644 index 0000000..82d95f9 --- /dev/null +++ b/packages/drfed/README.md @@ -0,0 +1,37 @@ +@drfed/drfed +============ + +The main application package for [DrFed], a web-based platform for developing +and debugging ActivityPub apps. It wires together the database layer, GraphQL +server, and HTTP server, and exposes the `drfed-server` CLI binary. + +[DrFed]: https://drfed.org/ + + +Usage +----- + +~~~~ sh +drfed-server --data-path .pgdata +drfed-server --database-url postgres://localhost/drfed +~~~~ + +The server listens on `localhost:8888` by default. Pass `--listen HOST:PORT` +to override. Automatic database migrations run on startup unless `--no-migrate` +is given. + + +Options +------- + +| Option | Short | Description | +| ------------------------- | ----- | ------------------------------------------------ | +| `--listen HOST:PORT` | `-l` | Address to listen on (default: `localhost:8888)` | +| `--pglite-data-path PATH` | `-d` | Directory for PGlite storage | +| `--postgres-url URL` | `-D` | PostgreSQL connection URL | +| `--no-migrate` | `-M` | Skip automatic migrations | +| `--help` | | Show help | +| `--version` | | Show version | + +`--pglite-data-path` and `--postgres-url` are mutually exclusive. One of them +must be provided. diff --git a/packages/drfed/bin/drfed-server.mjs b/packages/drfed/bin/drfed-server.mjs new file mode 100644 index 0000000..4dc320d --- /dev/null +++ b/packages/drfed/bin/drfed-server.mjs @@ -0,0 +1,19 @@ +#!/usr/bin/env node +// DrFed: A web-based platform for developing and debugging ActivityPub apps +// Copyright (C) 2026 DrFed team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import { main } from "../dist/index.mjs"; + +await main(); diff --git a/packages/drfed/package.json b/packages/drfed/package.json new file mode 100644 index 0000000..ea51029 --- /dev/null +++ b/packages/drfed/package.json @@ -0,0 +1,75 @@ +{ + "name": "@drfed/drfed", + "version": "0.1.0", + "description": "The main entrypoint program for DrFed.", + "keywords": [ + "ActivityPub", + "fediverse", + "federation", + "debugger" + ], + "author": { + "name": "DrFed team", + "url": "https://drfed.org/" + }, + "maintainers": [ + { + "name": "ChanHaeng Lee", + "email": "2chanhaeng@gmail.com", + "url": "https://chomu.dev/" + }, + { + "name": "Hong Minhee", + "email": "hong@minhee.org", + "url": "https://hongminhee.org/" + }, + { + "name": "Hyeonseo Kim", + "email": "dodok8@gmail.com", + "url": "https://hackers.pub/@gaebalgom" + }, + { + "name": "Jiwon Kwon", + "email": "me@kwonjiwon.org", + "url": "https://kwonjiwon.org/" + } + ], + "license": "AGPL-3.0-only", + "engine": { + "node": ">=26.0.0" + }, + "type": "module", + "main": "dist/index.mjs", + "types": "dist/index.d.mts", + "files": [ + "bin/", + "dist/", + "README.md" + ], + "bin": { + "drfed-server": "bin/drfed-server.mjs" + }, + "tsdown": { + "dts": true, + "sourcemap": true + }, + "scripts": { + "build": "tsdown" + }, + "devDependencies": { + "@types/node": "catalog:", + "@types/pg": "^8.20.0", + "tsdown": "catalog:", + "typescript": "catalog:" + }, + "dependencies": { + "@drfed/graphql": "workspace:*", + "@drfed/models": "workspace:*", + "@electric-sql/pglite": "^0.5.3", + "@optique/core": "^1.1.0", + "@optique/run": "^1.1.0", + "drizzle-orm": "catalog:", + "pg": "^8.21.0", + "srvx": "^0.11.16" + } +} diff --git a/packages/drfed/src/index.ts b/packages/drfed/src/index.ts new file mode 100644 index 0000000..5c938f1 --- /dev/null +++ b/packages/drfed/src/index.ts @@ -0,0 +1,53 @@ +// DrFed: A web-based platform for developing and debugging ActivityPub apps +// Copyright (C) 2026 DrFed team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import process from "node:process"; + +import { createYogaServer } from "@drfed/graphql"; +import { migrate } from "@drfed/models"; +import { run } from "@optique/run"; +import { serve } from "srvx"; + +import metadata from "../package.json" with { type: "json" }; +import type { Options } from "./parser.ts"; +import program from "./program.ts"; + +export async function main() { + const options: Options = run(program, { + help: "option", + version: { + value: metadata.version, + option: true, + }, + showChoices: true, + showDefault: true, + }); + if (options.drizzle.migrate) { + await migrate({ credentials: options.drizzle.credentials }); + } + const yogaServer = createYogaServer(options.drizzle.db); + const server = serve({ + hostname: options.address.host, + port: options.address.port, + manual: true, + fetch: yogaServer.fetch.bind(yogaServer), + }); + const shutdown = () => { + server.close().then(() => process.exit(0)); + }; + process.once("SIGINT", shutdown); + process.once("SIGTERM", shutdown); + await server.serve(); +} diff --git a/packages/drfed/src/parser.ts b/packages/drfed/src/parser.ts new file mode 100644 index 0000000..0a84674 --- /dev/null +++ b/packages/drfed/src/parser.ts @@ -0,0 +1,99 @@ +// DrFed: A web-based platform for developing and debugging ActivityPub apps +// Copyright (C) 2026 DrFed team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import { relations, schema } from "@drfed/models"; +import { merge, object, or } from "@optique/core/constructs"; +import { message, optionNames } from "@optique/core/message"; +import { map, withDefault } from "@optique/core/modifiers"; +import type { InferValue } from "@optique/core/parser"; +import { option } from "@optique/core/primitives"; +import { socketAddress, url } from "@optique/core/valueparser"; +import { path } from "@optique/run/valueparser"; +import { drizzle as drizzlePostgres } from "drizzle-orm/node-postgres"; +import { drizzle as drizzlePglite } from "drizzle-orm/pglite"; + +const pgliteParser = map( + option( + "--pglite-data-path", + "--data-path", + "-d", + path({ type: "directory" }), + { + description: message`The path to the directory where the PGlite database files will be stored. Mutually exclusive with ${optionNames(["--postgres-url", "--database-url", "-D"])}.`, + }, + ), + (path) => ({ + db: drizzlePglite({ + schema, + relations, + connection: { dataDir: path }, + }), + credentials: { + driver: "pglite" as const, + url: path, + }, + }), +); + +const postgresParser = map( + option( + "--postgres-url", + "--database-url", + "-D", + url({ allowedProtocols: ["postgres:", "postgresql:"] }), + { + description: message`The URL of the PostgreSQL database to connect to. Mutually exclusive with ${optionNames(["--pglite-data-path", "--data-path", "-d"])}.`, + }, + ), + (url) => ({ + db: drizzlePostgres({ + schema, + relations, + connection: { + connectionString: url.href, + }, + }), + credentials: { + url: url.href, + }, + }), +); + +export const parser = object({ + address: withDefault( + option("--listen", "-l", socketAddress({ requirePort: true }), { + description: message`The address to listen on.`, + }), + { + host: "localhost", + port: 8888, + }, + ), + drizzle: merge( + or(pgliteParser, postgresParser), + object({ + migrate: map( + option("--no-migrate", "-M", { + description: message`Disable automatic database migrations.`, + }), + (m) => !m, + ), + }), + ), +}); + +export type Options = InferValue; + +export default parser; diff --git a/packages/drfed/src/program.ts b/packages/drfed/src/program.ts new file mode 100644 index 0000000..064a9d2 --- /dev/null +++ b/packages/drfed/src/program.ts @@ -0,0 +1,30 @@ +// DrFed: A web-based platform for developing and debugging ActivityPub apps +// Copyright (C) 2026 DrFed team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import { message } from "@optique/core/message"; +import type { InferValue } from "@optique/core/parser"; +import type { Program } from "@optique/core/program"; + +import parser from "./parser.ts"; + +const program: Program<"sync", InferValue> = { + parser, + metadata: { + name: "drfed-server", + description: message`Run a DrFed server.`, + }, +}; + +export default program; diff --git a/packages/drfed/tsconfig.json b/packages/drfed/tsconfig.json new file mode 100644 index 0000000..79bbafc --- /dev/null +++ b/packages/drfed/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "allowImportingTsExtensions": true, + "exactOptionalPropertyTypes": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "lib": ["ES2024"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": true, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noUncheckedIndexedAccess": true, + "paths": { + "@drfed/graphql": ["../graphql/src/index.ts"], + "@drfed/models": ["../models/src/index.ts"] + }, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "ES2024", + "types": ["node"], + "verbatimModuleSyntax": true + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/graphql/README.md b/packages/graphql/README.md new file mode 100644 index 0000000..f41999c --- /dev/null +++ b/packages/graphql/README.md @@ -0,0 +1,33 @@ +@drfed/graphql +============== + +GraphQL server for [DrFed], built with [Pothos] and [GraphQL Yoga]. Exposes +a Relay-compatible schema backed by Drizzle ORM. + +[DrFed]: https://drfed.org/ +[Pothos]: https://pothos-graphql.dev/ +[GraphQL Yoga]: https://the-guild.dev/graphql/yoga-server + + +Scalars +------- + +| Scalar | Description | +| ---------- | ------------------------- | +| `DateTime` | ISO 8601 timestamp | +| `Email` | Normalized e-mail address | +| `UUID` | RFC 4122 UUID | + + +Usage +----- + +~~~~ ts +import { createYogaServer } from "@drfed/graphql"; + +const yoga = createYogaServer(db); +serve({ fetch: yoga.fetch.bind(yoga) }); +~~~~ + +`createYogaServer` accepts a Drizzle database instance and returns a +GraphQL Yoga server ready to handle HTTP requests. diff --git a/packages/graphql/dist/index.mjs b/packages/graphql/dist/index.mjs deleted file mode 100644 index cb0ff5c..0000000 --- a/packages/graphql/dist/index.mjs +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/packages/graphql/package.json b/packages/graphql/package.json index 09d1fb2..7019fad 100644 --- a/packages/graphql/package.json +++ b/packages/graphql/package.json @@ -35,12 +35,36 @@ } ], "license": "AGPL-3.0-only", + "engine": { + "node": ">=26.0.0" + }, "type": "module", "main": "dist/index.mjs", + "types": "dist/index.d.mts", + "files": [ + "dist/", + "README.md" + ], + "tsdown": { + "dts": true, + "sourcemap": true + }, "scripts": { "build": "tsdown" }, "devDependencies": { - "tsdown": "catalog:" + "@types/node": "catalog:", + "tsdown": "catalog:", + "typescript": "catalog:" + }, + "dependencies": { + "@drfed/models": "workspace:*", + "@pothos/core": "^4.13.0", + "@pothos/plugin-drizzle": "^0.17.4", + "@pothos/plugin-relay": "^4.7.0", + "drizzle-orm": "catalog:", + "graphql": "^16.14.2", + "graphql-scalars": "^1.25.0", + "graphql-yoga": "^5.21.2" } } diff --git a/packages/graphql/src/account.ts b/packages/graphql/src/account.ts new file mode 100644 index 0000000..36d4f8a --- /dev/null +++ b/packages/graphql/src/account.ts @@ -0,0 +1,61 @@ +// DrFed: A web-based platform for developing and debugging ActivityPub apps +// Copyright (C) 2026 DrFed team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import builder from "./builder.ts"; + +export const Account = builder.drizzleNode("accounts", { + name: "Account", + description: + "Represents an `Account` in the DrFed platform. " + + "Note that it differs from the ActivityPub `Actor`s that belong to `Instance`s.", + id: { + column(account) { + return account.id; + }, + description: "The unique identifier of the `Account`.", + }, + fields: (t) => ({ + uuid: t.expose("id", { + type: "UUID", + description: "The UUID of the `Account`.", + }), + email: t.expose("email", { + type: "Email", + description: "The email address of the `Account`.", + }), + created: t.expose("created", { + type: "DateTime", + description: "The date/time when the `Account` was created.", + }), + }), +}); + +builder.queryFields((t) => ({ + accountByUuid: t.drizzleField({ + type: Account, + description: "Get an `Account` by its UUID.", + args: { + uuid: t.arg({ + type: "UUID", + required: true, + description: "The UUID of the `Account` to retrieve.", + }), + }, + nullable: true, + resolve(query, _, { uuid }, ctx) { + return ctx.db.query.accounts.findFirst(query({ where: { id: uuid } })); + }, + }), +})); diff --git a/packages/graphql/src/builder.ts b/packages/graphql/src/builder.ts new file mode 100644 index 0000000..62df5f6 --- /dev/null +++ b/packages/graphql/src/builder.ts @@ -0,0 +1,92 @@ +// DrFed: A web-based platform for developing and debugging ActivityPub apps +// Copyright (C) 2026 DrFed team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import { type Database, normalizeEmail, relations } from "@drfed/models"; +import SchemaBuilder from "@pothos/core"; +import DrizzlePlugin from "@pothos/plugin-drizzle"; +import RelayPlugin from "@pothos/plugin-relay"; +import { getTableConfig } from "drizzle-orm/pg-core"; +import { DateTimeResolver, UUIDResolver } from "graphql-scalars"; + +/** + * The context data for the GraphQL server, which includes the incoming request + * object and any additional information needed for processing GraphQL queries + * and mutations. + */ +export interface ServerContext { + /** + * The incoming HTTP request. + */ + readonly request: Request; + + /** + * The database instance. + */ + readonly db: Database; +} + +/** + * The user-related context data for the GraphQL server, which include every + * field from the {@link ServerContext}. + */ +export interface UserContext extends ServerContext {} + +export interface SchemaTypes { + Context: UserContext; + Scalars: { + DateTime: { + Input: Date; + Output: Date; + }; + Email: { + Input: string; + Output: string; + }; + UUID: { + Input: string; + Output: string; + }; + }; + DefaultFieldNullability: false; + DrizzleRelations: typeof relations; +} + +/** + * The GraphQL schema builder. + */ +export const builder = new SchemaBuilder({ + plugins: [DrizzlePlugin, RelayPlugin], + defaultFieldNullability: false, + drizzle: { + client(ctx) { + return ctx.db; + }, + getTableConfig, + relations, + }, +}); + +builder.addScalarType("DateTime", DateTimeResolver); + +builder.scalarType("Email", { + serialize: (v) => normalizeEmail(v), + parseValue: (v) => normalizeEmail(String(v)), +}); + +builder.addScalarType("UUID", UUIDResolver); + +export const Node = builder.nodeInterfaceRef(); + +export default builder; diff --git a/packages/graphql/src/index.ts b/packages/graphql/src/index.ts index c7f8f2f..81d87ff 100644 --- a/packages/graphql/src/index.ts +++ b/packages/graphql/src/index.ts @@ -13,3 +13,27 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +import type { Database } from "@drfed/models"; +import { createYoga, useExecutionCancellation } from "graphql-yoga"; +import type { YogaServerInstance } from "graphql-yoga"; + +import type { ServerContext, UserContext } from "./builder.ts"; +import { schema } from "./schema.ts"; + +/** + * Creates a Yoga server instance with the provided schema and context. + * @param db The database instance. + * @returns A `YogaServerInstance` configured with the schema and context for + * handling GraphQL requests. + */ +export function createYogaServer( + db: Database, +): YogaServerInstance { + return createYoga({ + plugins: [useExecutionCancellation()], + schema, + async context(ctx) { + return { request: ctx.request, db }; + }, + }); +} diff --git a/packages/graphql/src/instance.ts b/packages/graphql/src/instance.ts new file mode 100644 index 0000000..e73bd5c --- /dev/null +++ b/packages/graphql/src/instance.ts @@ -0,0 +1,38 @@ +// DrFed: A web-based platform for developing and debugging ActivityPub apps +// Copyright (C) 2026 DrFed team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import builder from "./builder.ts"; + +export const Instance = builder.drizzleNode("instances", { + name: "Instance", + description: "Represents an `Instance` in the DrFed platform.", + id: { + column(instance) { + return instance.id; + }, + description: "The unique identifier of the `Instance`.", + }, + fields: (t) => ({ + slug: t.exposeString("slug"), + expires: t.expose("expires", { + type: "DateTime", + description: "The expiration date/time of the `Instance`.", + }), + created: t.expose("created", { + type: "DateTime", + description: "The creation date/time of the `Instance`.", + }), + }), +}); diff --git a/packages/graphql/src/schema.ts b/packages/graphql/src/schema.ts new file mode 100644 index 0000000..7acc9e1 --- /dev/null +++ b/packages/graphql/src/schema.ts @@ -0,0 +1,23 @@ +// DrFed: A web-based platform for developing and debugging ActivityPub apps +// Copyright (C) 2026 DrFed team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import "./account.ts"; +import "./instance.ts"; +import builder from "./builder.ts"; + +builder.queryType({}); +// builder.mutationType({}); + +export const schema = builder.toSchema(); diff --git a/packages/graphql/tsconfig.json b/packages/graphql/tsconfig.json new file mode 100644 index 0000000..4f8dc42 --- /dev/null +++ b/packages/graphql/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "allowImportingTsExtensions": true, + "exactOptionalPropertyTypes": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "lib": ["ES2024"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": true, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noUncheckedIndexedAccess": true, + "paths": { + "@drfed/models": ["../models/src/index.ts"] + }, + "skipLibCheck": true, + "strict": true, + "target": "ES2024", + "types": ["node"], + "verbatimModuleSyntax": true + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/models/README.md b/packages/models/README.md new file mode 100644 index 0000000..645949a --- /dev/null +++ b/packages/models/README.md @@ -0,0 +1,30 @@ +@drfed/models +============= + +Database schema, Drizzle ORM types, and migration runner for [DrFed]. Supports +both PGlite (embedded) and PostgreSQL. + +[DrFed]: https://drfed.org/ + + +Schema +------ + +| Table | Key columns | +| ------------------ | ---------------------------------------------------- | +| `accounts` | `id` (UUID), `email`, `created` | +| `instances` | `id` (UUID), `slug`, `expires`, `created` | +| `instance_members` | `instanceId` → `instances`, `accountId` → `accounts` | + + +Migrations +---------- + +Generate a migration after changing *src/schema.ts*: + +~~~~ sh +mise run generate:migrate --name your_migration_name +~~~~ + +The *drizzle/* directory is included in the published npm package so that +installed users can run migrations without access to the repository source. diff --git a/packages/models/drizzle.config.ts b/packages/models/drizzle.config.ts new file mode 100644 index 0000000..f66da70 --- /dev/null +++ b/packages/models/drizzle.config.ts @@ -0,0 +1,21 @@ +// DrFed: A web-based platform for developing and debugging ActivityPub apps +// Copyright (C) 2026 DrFed team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + dialect: "postgresql", + schema: "./src/schema.ts", +}); diff --git a/packages/models/drizzle/20260620011017_init/migration.sql b/packages/models/drizzle/20260620011017_init/migration.sql new file mode 100644 index 0000000..ed111aa --- /dev/null +++ b/packages/models/drizzle/20260620011017_init/migration.sql @@ -0,0 +1,24 @@ +CREATE TABLE "accounts" ( + "id" uuid PRIMARY KEY, + "email" varchar(255) NOT NULL UNIQUE, + "created" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + CONSTRAINT "accounts_email_check" CHECK ("email" ~ '^[^@]+@[^@]+\.[^@]+$') +); +--> statement-breakpoint +CREATE TABLE "instance_members" ( + "instanceId" uuid PRIMARY KEY, + "accountId" uuid NOT NULL, + "created" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE "instances" ( + "id" uuid PRIMARY KEY, + "slug" varchar(100) NOT NULL UNIQUE, + "expires" timestamp with time zone NOT NULL, + "created" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + CONSTRAINT "instances_slug_check" CHECK ("slug" ~ '^[a-z0-9-]{4,100}$'), + CONSTRAINT "instances_expires_check" CHECK ("expires" < ("created" + INTERVAL '1 year')) +); +--> statement-breakpoint +ALTER TABLE "instance_members" ADD CONSTRAINT "instance_members_instanceId_instances_id_fkey" FOREIGN KEY ("instanceId") REFERENCES "instances"("id");--> statement-breakpoint +ALTER TABLE "instance_members" ADD CONSTRAINT "instance_members_accountId_accounts_id_fkey" FOREIGN KEY ("accountId") REFERENCES "accounts"("id"); \ No newline at end of file diff --git a/packages/models/drizzle/20260620011017_init/snapshot.json b/packages/models/drizzle/20260620011017_init/snapshot.json new file mode 100644 index 0000000..a65d9e3 --- /dev/null +++ b/packages/models/drizzle/20260620011017_init/snapshot.json @@ -0,0 +1,246 @@ +{ + "version": "8", + "dialect": "postgres", + "id": "e399ae79-2d5c-4f12-b5e7-8d602703fcc1", + "prevIds": ["00000000-0000-0000-0000-000000000000"], + "ddl": [ + { + "isRlsEnabled": false, + "name": "accounts", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "instance_members", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "instances", + "entityType": "tables", + "schema": "public" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "accounts" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "accounts" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "accounts" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "instanceId", + "entityType": "columns", + "schema": "public", + "table": "instance_members" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "accountId", + "entityType": "columns", + "schema": "public", + "table": "instance_members" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "instance_members" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "instances" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "instances" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires", + "entityType": "columns", + "schema": "public", + "table": "instances" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "instances" + }, + { + "nameExplicit": false, + "columns": ["instanceId"], + "schemaTo": "public", + "tableTo": "instances", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "instance_members_instanceId_instances_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "instance_members" + }, + { + "nameExplicit": false, + "columns": ["accountId"], + "schemaTo": "public", + "tableTo": "accounts", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "instance_members_accountId_accounts_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "instance_members" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "accounts_pkey", + "schema": "public", + "table": "accounts", + "entityType": "pks" + }, + { + "columns": ["instanceId"], + "nameExplicit": false, + "name": "instance_members_pkey", + "schema": "public", + "table": "instance_members", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "instances_pkey", + "schema": "public", + "table": "instances", + "entityType": "pks" + }, + { + "nameExplicit": false, + "columns": ["email"], + "nullsNotDistinct": false, + "name": "accounts_email_key", + "schema": "public", + "table": "accounts", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": ["slug"], + "nullsNotDistinct": false, + "name": "instances_slug_key", + "schema": "public", + "table": "instances", + "entityType": "uniques" + }, + { + "value": "\"email\" ~ '^[^@]+@[^@]+\\.[^@]+$'", + "name": "accounts_email_check", + "entityType": "checks", + "schema": "public", + "table": "accounts" + }, + { + "value": "\"slug\" ~ '^[a-z0-9-]{4,100}$'", + "name": "instances_slug_check", + "entityType": "checks", + "schema": "public", + "table": "instances" + }, + { + "value": "\"expires\" < (\"created\" + INTERVAL '1 year')", + "name": "instances_expires_check", + "entityType": "checks", + "schema": "public", + "table": "instances" + } + ], + "renames": [] +} diff --git a/packages/models/package.json b/packages/models/package.json new file mode 100644 index 0000000..553de0b --- /dev/null +++ b/packages/models/package.json @@ -0,0 +1,68 @@ +{ + "name": "@drfed/models", + "version": "0.1.0", + "description": "Database models for DrFed.", + "keywords": [ + "ActivityPub", + "fediverse", + "federation", + "debugger" + ], + "author": { + "name": "DrFed team", + "url": "https://drfed.org/" + }, + "maintainers": [ + { + "name": "ChanHaeng Lee", + "email": "2chanhaeng@gmail.com", + "url": "https://chomu.dev/" + }, + { + "name": "Hong Minhee", + "email": "hong@minhee.org", + "url": "https://hongminhee.org/" + }, + { + "name": "Hyeonseo Kim", + "email": "dodok8@gmail.com", + "url": "https://hackers.pub/@gaebalgom" + }, + { + "name": "Jiwon Kwon", + "email": "me@kwonjiwon.org", + "url": "https://kwonjiwon.org/" + } + ], + "license": "AGPL-3.0-only", + "engine": { + "node": ">=26.0.0" + }, + "type": "module", + "main": "dist/index.mjs", + "types": "dist/index.d.mts", + "files": [ + "dist/", + "drizzle/", + "README.md" + ], + "tsdown": { + "dts": true, + "sourcemap": true + }, + "scripts": { + "build": "tsdown" + }, + "devDependencies": { + "@types/node": "catalog:", + "@types/pg": "^8.20.0", + "drizzle-kit": "1.0.0-beta.22", + "tsdown": "catalog:", + "typescript": "catalog:" + }, + "dependencies": { + "@electric-sql/pglite": "^0.5.3", + "drizzle-orm": "catalog:", + "pg": "^8.21.0" + } +} diff --git a/packages/models/src/db.ts b/packages/models/src/db.ts new file mode 100644 index 0000000..c033ebb --- /dev/null +++ b/packages/models/src/db.ts @@ -0,0 +1,48 @@ +// DrFed: A web-based platform for developing and debugging ActivityPub apps +// Copyright (C) 2026 DrFed team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import type { RelationsFilter as RelationsFilterImpl } from "drizzle-orm"; +import type { + PgAsyncDatabase, + PgAsyncTransaction, + PgQueryResultHKT, +} from "drizzle-orm/pg-core"; + +import { relations } from "./relations.ts"; +import * as schema from "./schema.ts"; + +/** + * A database instance. + */ +export type Database = PgAsyncDatabase< + PgQueryResultHKT, + typeof schema, + typeof relations +>; + +/** + * A transaction instance. + */ +export type Transaction = PgAsyncTransaction< + PgQueryResultHKT, + typeof schema, + typeof relations +>; + +/** + * A filter for relations. + */ +export type RelationsFilter = + RelationsFilterImpl<(typeof relations)[T], typeof relations>; diff --git a/packages/models/src/email.ts b/packages/models/src/email.ts new file mode 100644 index 0000000..2ab6894 --- /dev/null +++ b/packages/models/src/email.ts @@ -0,0 +1,75 @@ +// DrFed: A web-based platform for developing and debugging ActivityPub apps +// Copyright (C) 2026 DrFed team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/** + * Normalizes an email address by trimming whitespace, converting the host + * to lowercase (and to punycode if necessary), and ensuring it has a + * single "@" character. If the email is invalid, it throws a `TypeError`. + * @param email The email address to normalize. + * @returns The normalized email address. + */ +export function normalizeEmail(email: string): string; + +/** + * Normalizes an email address by trimming whitespace, converting the host + * to lowercase (and to punycode if necessary), and ensuring it has a + * single "@" character. If the email is invalid, it throws a `TypeError`. + * @param email The email address to normalize. + * @returns The normalized email address. If the input is `null`, + * it returns `null`. + */ +export function normalizeEmail(email: string | null): string | null; + +/** + * Normalizes an email address by trimming whitespace, converting the host + * to lowercase (and to punycode if necessary), and ensuring it has a + * single "@" character. If the email is invalid, it throws a `TypeError`. + * @param email The email address to normalize. + * @returns The normalized email address. If the input is `undefined`, + * it returns `undefined`. + */ +export function normalizeEmail(email: string | undefined): string | undefined; + +/** + * Normalizes an email address by trimming whitespace, converting the host + * to lowercase (and to punycode if necessary), and ensuring it has a + * single "@" character. If the email is invalid, it throws a `TypeError`. + * @param email The email address to normalize. + * @returns The normalized email address. If the input is `undefined`, + * it returns `undefined`. If the input is `null`, it returns `null`. + */ +export function normalizeEmail( + email: string | null | undefined, +): string | null | undefined; + +export function normalizeEmail( + email: string | null | undefined, +): string | null | undefined { + if (typeof email === "undefined") return undefined; + else if (email == null) return null; + const [local, host, shouldNotExist] = email.trim().split("@"); + if ( + local == null || + local.trim() === "" || + host == null || + host.trim() === "" || + shouldNotExist != null + ) { + throw new TypeError("Invalid email format."); + } + const normalizedHost = new URL(`https://${host}/`).host; + return `${local}@${normalizedHost}`; +} diff --git a/packages/models/src/index.ts b/packages/models/src/index.ts new file mode 100644 index 0000000..b69235a --- /dev/null +++ b/packages/models/src/index.ts @@ -0,0 +1,20 @@ +// DrFed: A web-based platform for developing and debugging ActivityPub apps +// Copyright (C) 2026 DrFed team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +export * from "./email.ts"; +export * from "./db.ts"; +export * from "./migrate.ts"; +export { relations } from "./relations.ts"; +export * as schema from "./schema.ts"; diff --git a/packages/models/src/migrate.ts b/packages/models/src/migrate.ts new file mode 100644 index 0000000..a1b753b --- /dev/null +++ b/packages/models/src/migrate.ts @@ -0,0 +1,169 @@ +// DrFed: A web-based platform for developing and debugging ActivityPub apps +// Copyright (C) 2026 DrFed team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import { existsSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { PGlite, type PGliteOptions } from "@electric-sql/pglite"; +import { drizzle as drizzlePostgres } from "drizzle-orm/node-postgres"; +import { migrate as migratePostgres } from "drizzle-orm/node-postgres/migrator"; +import { drizzle as drizzlePglite } from "drizzle-orm/pglite"; +import { migrate as migratePglite } from "drizzle-orm/pglite/migrator"; +import { Pool, type PoolConfig } from "pg"; + +export type MigrateCredentials = + | PostgresMigrateCredentials + | PGliteMigrateCredentials; + +export type PostgresMigrateCredentials = + | (Omit & { + readonly url: string; + readonly driver?: never; + }) + | (PoolConfig & { + readonly url?: never; + readonly driver?: never; + }); + +export type PGliteMigrateCredentials = + | { + readonly driver: "pglite"; + readonly url: string; + readonly options?: PGliteOptions; + } + | { + readonly driver: "pglite"; + readonly client: PGlite; + }; + +export interface MigrateOptions { + /** + * PostgreSQL or PGlite credentials. + */ + readonly credentials: MigrateCredentials; + + /** + * Custom migrations table/schema. Equivalent to drizzle-kit config's + * `migrations` option. + */ + readonly migrations?: { + readonly table?: string; + readonly schema?: string; + }; + + /** + * Custom migrations table. Takes precedence over `migrations.table`. + */ + readonly migrationsTable?: string; + + /** + * Custom migrations schema. Takes precedence over `migrations.schema`. + */ + readonly migrationsSchema?: string; +} + +const migrationsFolder = join( + dirname(fileURLToPath(import.meta.url)), + "..", + "drizzle", +); + +export async function migrate(options: MigrateOptions): Promise { + assertV3MigrationsFolder(migrationsFolder); + + const config = { + migrationsFolder, + migrationsTable: + options.migrationsTable ?? + options.migrations?.table ?? + "__drizzle_migrations", + migrationsSchema: + options.migrationsSchema ?? options.migrations?.schema ?? "drizzle", + }; + + if (isPGliteMigrateCredentials(options.credentials)) { + await migratePGliteDatabase(options.credentials, config); + } else { + await migratePostgresDatabase(options.credentials, config); + } +} + +function assertV3MigrationsFolder(migrationsFolder: string): void { + if (!existsSync(join(migrationsFolder, "meta", "_journal.json"))) return; + + throw new Error( + `The migrations folder format is outdated: ${migrationsFolder}. ` + + "Run `drizzle-kit up` before using migrate().", + ); +} + +function isPGliteMigrateCredentials( + credentials: MigrateCredentials, +): credentials is PGliteMigrateCredentials { + return credentials.driver === "pglite"; +} + +async function migratePGliteDatabase( + credentials: PGliteMigrateCredentials, + config: MigrationConfig, +): Promise { + const client = + "client" in credentials + ? credentials.client + : new PGlite(normalizePGliteUrl(credentials.url), credentials.options); + const shouldCloseClient = !("client" in credentials); + + try { + await client.waitReady; + await migratePglite(drizzlePglite({ client }), config); + } finally { + if (shouldCloseClient) await client.close(); + } +} + +async function migratePostgresDatabase( + credentials: PostgresMigrateCredentials, + config: MigrationConfig, +): Promise { + const pool = + "url" in credentials + ? new Pool({ + ...credentials, + connectionString: credentials.url, + max: 1, + }) + : new Pool({ + ...credentials, + max: 1, + }); + + try { + await migratePostgres(drizzlePostgres({ client: pool }), config); + } finally { + await pool.end(); + } +} + +function normalizePGliteUrl(url: string): string { + if (url.startsWith("file:")) return url.slice("file:".length); + return url; +} + +interface MigrationConfig { + readonly migrationsFolder: string; + readonly migrationsTable: string; + readonly migrationsSchema: string; +} diff --git a/packages/models/src/relations.ts b/packages/models/src/relations.ts new file mode 100644 index 0000000..73ef89e --- /dev/null +++ b/packages/models/src/relations.ts @@ -0,0 +1,45 @@ +// DrFed: A web-based platform for developing and debugging ActivityPub apps +// Copyright (C) 2026 DrFed team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import { defineRelations } from "drizzle-orm"; + +import * as schema from "./schema.ts"; + +export const relations = defineRelations(schema, (r) => ({ + accounts: { + instances: r.many.instances({ + from: r.accounts.id.through(r.instanceMembers.accountId), + to: r.instances.id.through(r.instanceMembers.instanceId), + }), + }, + instances: { + members: r.many.accounts({ + from: r.instances.id.through(r.instanceMembers.instanceId), + to: r.accounts.id.through(r.instanceMembers.accountId), + }), + }, + instanceMembers: { + instance: r.one.instances({ + from: r.instanceMembers.instanceId, + to: r.instances.id, + }), + account: r.one.accounts({ + from: r.instanceMembers.accountId, + to: r.accounts.id, + }), + }, +})); + +export default relations; diff --git a/packages/models/src/schema.ts b/packages/models/src/schema.ts new file mode 100644 index 0000000..2bc4416 --- /dev/null +++ b/packages/models/src/schema.ts @@ -0,0 +1,76 @@ +// DrFed: A web-based platform for developing and debugging ActivityPub apps +// Copyright (C) 2026 DrFed team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import { sql } from "drizzle-orm"; +import { uuid, varchar, pgTable, check, timestamp } from "drizzle-orm/pg-core"; + +/** + * The database table to represent accounts. + */ +export const accounts = pgTable( + "accounts", + { + id: uuid().primaryKey(), + email: varchar({ length: 255 }).notNull().unique(), + created: timestamp({ withTimezone: true }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + (table) => [ + check( + "accounts_email_check", + sql`${table.email} ~ '^[^@]+@[^@]+\\.[^@]+$'`, + ), + ], +); + +/** + * The database table to represent instances. + */ +export const instances = pgTable( + "instances", + { + id: uuid().primaryKey(), + slug: varchar({ length: 100 }).notNull().unique(), + expires: timestamp({ withTimezone: true }).notNull(), + created: timestamp({ withTimezone: true }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + (table) => [ + check("instances_slug_check", sql`${table.slug} ~ '^[a-z0-9-]{4,100}$'`), + check( + "instances_expires_check", + sql`${table.expires} < (${table.created} + INTERVAL '1 year')`, + ), + ], +); + +/** + * The association table between instances and its member accounts. + */ +export const instanceMembers = pgTable("instance_members", { + instanceId: uuid() + .notNull() + .primaryKey() + .references(() => instances.id), + accountId: uuid() + .notNull() + .primaryKey() + .references(() => accounts.id), + created: timestamp({ withTimezone: true }) + .notNull() + .default(sql`CURRENT_TIMESTAMP`), +}); diff --git a/packages/models/tsconfig.json b/packages/models/tsconfig.json new file mode 100644 index 0000000..6e88b43 --- /dev/null +++ b/packages/models/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "allowImportingTsExtensions": true, + "exactOptionalPropertyTypes": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "lib": ["ES2024"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": true, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noUncheckedIndexedAccess": true, + "skipLibCheck": true, + "strict": true, + "target": "ES2024", + "types": ["node"], + "verbatimModuleSyntax": true + }, + "include": ["src/**/*.ts", "drizzle.config.ts"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 07aa6a4..fdcf570 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,17 +6,125 @@ settings: catalogs: default: + '@types/node': + specifier: ^26.0.0 + version: 26.0.0 + drizzle-orm: + specifier: 1.0.0-beta.22 + version: 1.0.0-beta.22 tsdown: specifier: ^0.22.3 version: 0.22.3 + typescript: + specifier: ^6.0.3 + version: 6.0.3 importers: + packages/drfed: + dependencies: + '@drfed/graphql': + specifier: workspace:* + version: link:../graphql + '@drfed/models': + specifier: workspace:* + version: link:../models + '@electric-sql/pglite': + specifier: ^0.5.3 + version: 0.5.3 + '@optique/core': + specifier: ^1.1.0 + version: 1.1.0 + '@optique/run': + specifier: ^1.1.0 + version: 1.1.0 + drizzle-orm: + specifier: 'catalog:' + version: 1.0.0-beta.22(@electric-sql/pglite@0.5.3)(@types/pg@8.20.0)(pg@8.21.0) + pg: + specifier: ^8.21.0 + version: 8.21.0 + srvx: + specifier: ^0.11.16 + version: 0.11.16 + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.0.0 + '@types/pg': + specifier: ^8.20.0 + version: 8.20.0 + tsdown: + specifier: 'catalog:' + version: 0.22.3(typescript@6.0.3) + typescript: + specifier: 'catalog:' + version: 6.0.3 + packages/graphql: + dependencies: + '@drfed/models': + specifier: workspace:* + version: link:../models + '@pothos/core': + specifier: ^4.13.0 + version: 4.13.0(graphql@16.14.2) + '@pothos/plugin-drizzle': + specifier: ^0.17.4 + version: 0.17.4(@pothos/core@4.13.0(graphql@16.14.2))(drizzle-orm@1.0.0-beta.22(@electric-sql/pglite@0.5.3)(@types/pg@8.20.0)(pg@8.21.0))(graphql@16.14.2) + '@pothos/plugin-relay': + specifier: ^4.7.0 + version: 4.7.0(@pothos/core@4.13.0(graphql@16.14.2))(graphql@16.14.2) + drizzle-orm: + specifier: 'catalog:' + version: 1.0.0-beta.22(@electric-sql/pglite@0.5.3)(@types/pg@8.20.0)(pg@8.21.0) + graphql: + specifier: ^16.14.2 + version: 16.14.2 + graphql-scalars: + specifier: ^1.25.0 + version: 1.25.0(graphql@16.14.2) + graphql-yoga: + specifier: ^5.21.2 + version: 5.21.2(graphql@16.14.2) + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.0.0 + tsdown: + specifier: 'catalog:' + version: 0.22.3(typescript@6.0.3) + typescript: + specifier: 'catalog:' + version: 6.0.3 + + packages/models: + dependencies: + '@electric-sql/pglite': + specifier: ^0.5.3 + version: 0.5.3 + drizzle-orm: + specifier: 'catalog:' + version: 1.0.0-beta.22(@electric-sql/pglite@0.5.3)(@types/pg@8.20.0)(pg@8.21.0) + pg: + specifier: ^8.21.0 + version: 8.21.0 devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.0.0 + '@types/pg': + specifier: ^8.20.0 + version: 8.20.0 + drizzle-kit: + specifier: 1.0.0-beta.22 + version: 1.0.0-beta.22 tsdown: specifier: 'catalog:' - version: 0.22.3 + version: 0.22.3(typescript@6.0.3) + typescript: + specifier: 'catalog:' + version: 6.0.3 packages: @@ -41,6 +149,12 @@ packages: resolution: {integrity: sha512-K8ponJDxBwDHigkeFqaqT5wLGl4bTlwMafR8k7b5CPxr6Ww+UG9ls8Yx6Tcpboxu97eeGVEEyKcHmEyOwN1vSw==} engines: {node: ^22.18.0 || >=24.11.0} + '@drizzle-team/brocli@0.11.0': + resolution: {integrity: sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg==} + + '@electric-sql/pglite@0.5.3': + resolution: {integrity: sha512-iTTYbA5Uesrl+N7zss0J5LopT7KE4j9aymYo+EZZh+rZbARQCUQOs+n2pay64JRUpc3fCkpfrniTNJnvYzOE+g==} + '@emnapi/core@1.11.0': resolution: {integrity: sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==} @@ -50,6 +164,224 @@ packages: '@emnapi/wasi-threads@1.2.2': resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + '@envelop/core@5.5.1': + resolution: {integrity: sha512-3DQg8sFskDo386TkL5j12jyRAdip/8yzK3x7YGbZBgobZ4aKXrvDU0GppU0SnmrpQnNaiTUsxBs9LKkwQ/eyvw==} + engines: {node: '>=18.0.0'} + + '@envelop/instrumentation@1.0.0': + resolution: {integrity: sha512-cxgkB66RQB95H3X27jlnxCRNTmPuSTgmBAq6/4n2Dtv4hsk4yz8FadA1ggmd0uZzvKqWD6CR+WFgTjhDqg7eyw==} + engines: {node: '>=18.0.0'} + + '@envelop/types@5.2.1': + resolution: {integrity: sha512-CsFmA3u3c2QoLDTfEpGr4t25fjMU31nyvse7IzWTvb0ZycuPjMjb0fjlheh+PbhBYb9YLugnT2uY6Mwcg1o+Zg==} + engines: {node: '>=18.0.0'} + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@fastify/busboy@3.2.0': + resolution: {integrity: sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==} + + '@graphql-tools/executor@1.5.3': + resolution: {integrity: sha512-mgBFC0bsrZPZLu9EnydpMnAuQ8Iiq0CEbUcsmvXsm2/iYektGHDN/+bmb7hicA6dWZtdPfklYJmr21WD0GnOfA==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/merge@9.1.9': + resolution: {integrity: sha512-iHUWNjRHeQRYdgIMIuChThOwoKzA9vrzYeslgfBo5eUYEyHGZCoDPjAavssoYXLwstYt1dZj2J22jSzc2DrN0Q==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/schema@10.0.33': + resolution: {integrity: sha512-O6P3RIftO0jafnSsFAqpjurUuUxJ43s/AdPVLQsBkI6y4Ic/tKm4C1Qm1KKQsCDTOxXPJClh/v3g7k7yLKCFBQ==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/utils@10.11.0': + resolution: {integrity: sha512-iBFR9GXIs0gCD+yc3hoNswViL1O5josI33dUqiNStFI/MHLCEPduasceAcazRH77YONKNiviHBV8f7OgcT4o2Q==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/utils@11.1.0': + resolution: {integrity: sha512-PtFVG4r8Z2LEBSaPYQMusBiB3o6kjLVJyjCLbnWem/SpSuM21v6LTmgpkXfYU1qpBV2UGsFyuEnSJInl8fR1Ag==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-typed-document-node/core@3.2.0': + resolution: {integrity: sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-yoga/logger@2.0.1': + resolution: {integrity: sha512-Nv0BoDGLMg9QBKy9cIswQ3/6aKaKjlTh87x3GiBg2Z4RrjyrM48DvOOK0pJh1C1At+b0mUIM67cwZcFTDLN4sA==} + engines: {node: '>=18.0.0'} + + '@graphql-yoga/subscription@5.0.5': + resolution: {integrity: sha512-oCMWOqFs6QV96/NZRt/ZhTQvzjkGB4YohBOpKM4jH/lDT4qb7Lex/aGCxpi/JD9njw3zBBtMqxbaC22+tFHVvw==} + engines: {node: '>=18.0.0'} + + '@graphql-yoga/typed-event-target@3.0.2': + resolution: {integrity: sha512-ZpJxMqB+Qfe3rp6uszCQoag4nSw42icURnBRfFYSOmTgEeOe4rD0vYlbA8spvCu2TlCesNTlEN9BLWtQqLxabA==} + engines: {node: '>=18.0.0'} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -63,18 +395,51 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@js-temporal/polyfill@0.5.1': + resolution: {integrity: sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ==} + engines: {node: '>=12'} + '@napi-rs/wasm-runtime@1.1.5': resolution: {integrity: sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==} peerDependencies: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 + '@optique/core@1.1.0': + resolution: {integrity: sha512-eBqai76tHiFDoShlTNXN9AAPs9XznCJRrk4qmGhjZUSMmePCF9o1XU3okUHxHdDXXCHj4auKoIvCN79KNKErxA==} + engines: {bun: '>=1.2.0', deno: '>=2.3.0', node: '>=20.0.0'} + + '@optique/run@1.1.0': + resolution: {integrity: sha512-dcuqqqU1Cpm9CLGEkCkpT/cpJ6H6a+hs0rP+iD8Tgwb+CPPZtX/hCfdIrqYyZ2RtYLxgc3S6KqC81AZAwEUPew==} + engines: {bun: '>=1.2.0', deno: '>=2.3.0', node: '>=20.0.0'} + '@oxc-project/types@0.135.0': resolution: {integrity: sha512-wR+xRdFkUBMvcAjBJ2q2kcZM6d+DKu2NgoOyxZgYwZdLhmiv6+rnO8PZ/P68kMiZtIKm+pW7zyEJ4kSOs0vo+Q==} + '@pothos/core@4.13.0': + resolution: {integrity: sha512-bIaVdLTkwPkkmIn0Ji13vsc9Zy0mEi++a9iMFIRVa88l0G7JuFJrJX5qGPo1GGu4drvRW4SAr2aDm2V5KcYj4A==} + peerDependencies: + graphql: ^16.10.0 || ^17.0.0 + + '@pothos/plugin-drizzle@0.17.4': + resolution: {integrity: sha512-cAaMexSSOtoXf7Khj62yOHGxHyaOnuIzNdgl+w0rNfnJdtVOr4slQjHinWJWzYWdRauOclZddxWbLaou4NK0QQ==} + peerDependencies: + '@pothos/core': '*' + drizzle-orm: '>=1.0.0-beta.2' + graphql: ^16.10.0 + + '@pothos/plugin-relay@4.7.0': + resolution: {integrity: sha512-IQ7f7WLu7uXxiZBcJgUTdspjIVzX3bgvd8XjnzxGJUbn/uE5HlVyCFmvhJffHIIS2OKSHhOOJRMwIwVwauwnSg==} + peerDependencies: + '@pothos/core': '*' + graphql: ^16.10.0 + '@quansync/fs@1.0.0': resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} + '@repeaterjs/repeater@3.1.0': + resolution: {integrity: sha512-TaoVksZRSx2KWYYpyLQtMQXXeS98VsgZImzW65xmiVgbYhXLk+aEsmzPLirqVuE4/XuUapH2iMtxUzaBNDzdSQ==} + '@rolldown/binding-android-arm64@1.1.1': resolution: {integrity: sha512-BLf9Wak/gfwVb7NQTQW4wBgL3oAfPy7ArEkhwV543OVw/uY6B47z5xYsqPSZ9PDOorvURPinws6ThaFuNgGLgA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -182,6 +547,36 @@ packages: '@types/jsesc@2.5.1': resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==} + '@types/node@26.0.0': + resolution: {integrity: sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==} + + '@types/pg@8.20.0': + resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} + + '@whatwg-node/disposablestack@0.0.6': + resolution: {integrity: sha512-LOtTn+JgJvX8WfBVJtF08TGrdjuFzGJc4mkP8EdDI8ADbvO7kiexYep1o8dwnt0okb0jYclCDXF13xU7Ge4zSw==} + engines: {node: '>=18.0.0'} + + '@whatwg-node/events@0.1.2': + resolution: {integrity: sha512-ApcWxkrs1WmEMS2CaLLFUEem/49erT3sxIVjpzU5f6zmVcnijtDSrhoK2zVobOIikZJdH63jdAXOrvjf6eOUNQ==} + engines: {node: '>=18.0.0'} + + '@whatwg-node/fetch@0.10.13': + resolution: {integrity: sha512-b4PhJ+zYj4357zwk4TTuF2nEe0vVtOrwdsrNo5hL+u1ojXNhh1FgJ6pg1jzDlwlT4oBdzfSwaBwMCtFCsIWg8Q==} + engines: {node: '>=18.0.0'} + + '@whatwg-node/node-fetch@0.8.6': + resolution: {integrity: sha512-BDMdYFcerLQkwA2RTldxOqRCs6ZQD1S7UgP3pUdGUkcbgTrP/V5ko77ZkCww9DHmC4lpoYuwigGfQYj285gMvA==} + engines: {node: '>=18.0.0'} + + '@whatwg-node/promise-helpers@1.3.2': + resolution: {integrity: sha512-Nst5JdK47VIl9UcGwtv2Rcgyn5lWtZ0/mhRQ4G8NN2isxpq2TO30iqHzmwoJycjWuyUfg3GFXqP/gFHXeV57IA==} + engines: {node: '>=16.0.0'} + + '@whatwg-node/server@0.11.0': + resolution: {integrity: sha512-VSdkwnJRr8Yv9UgB2aXB3VUPWwd6Oqnn0hycFwhg9pZgWxJXb7JmhsiXe9tmpMwjHFxli12PGcz9aI63YYloGQ==} + engines: {node: '>=18.0.0'} + ansis@4.3.1: resolution: {integrity: sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==} engines: {node: '>=14'} @@ -197,9 +592,136 @@ packages: resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} engines: {node: '>=20.19.0'} + cross-inspect@1.0.1: + resolution: {integrity: sha512-Pcw1JTvZLSJH83iiGWt6fRcT+BjZlCDRVwYLbUcHzv/CRpB7r0MlSrGbIyQvVSNyGnbt7G4AXuyCiDR3POvZ1A==} + engines: {node: '>=16.0.0'} + defu@6.1.7: resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + drizzle-kit@1.0.0-beta.22: + resolution: {integrity: sha512-9HTZuQRljQKTgCx4UhiGn8KYYfHGk4+B/bRR1714W67kz0qgJvdrG527i8rQD8uUyET9UTGR1u8syySJD4znGw==} + hasBin: true + + drizzle-orm@1.0.0-beta.22: + resolution: {integrity: sha512-F+DZyVIvH0oVKa/w08Cle1xfoH+pc+htIXHG/frnMLG72aby9NYYr9oc+9XvghnoO4umxFItduz0OMmQJMnenw==} + peerDependencies: + '@aws-sdk/client-rds-data': '>=3' + '@cloudflare/workers-types': '>=4' + '@effect/sql': ^0.48.5 + '@effect/sql-pg': ^0.49.7 + '@electric-sql/pglite': '>=0.2.0' + '@libsql/client': '>=0.10.0' + '@libsql/client-wasm': '>=0.10.0' + '@neondatabase/serverless': '>=0.10.0' + '@op-engineering/op-sqlite': '>=2' + '@opentelemetry/api': ^1.4.1 + '@planetscale/database': '>=1.13' + '@sinclair/typebox': '>=0.34.8' + '@sqlitecloud/drivers': '>=1.0.653' + '@tidbcloud/serverless': '*' + '@tursodatabase/database': '>=0.2.1' + '@tursodatabase/database-common': '>=0.2.1' + '@tursodatabase/database-wasm': '>=0.2.1' + '@types/better-sqlite3': '*' + '@types/mssql': ^9.1.4 + '@types/pg': '*' + '@types/sql.js': '*' + '@upstash/redis': '>=1.34.7' + '@vercel/postgres': '>=0.8.0' + '@xata.io/client': '*' + arktype: '>=2.0.0' + better-sqlite3: '>=9.3.0' + bun-types: '*' + expo-sqlite: '>=14.0.0' + gel: '>=2' + mssql: ^11.0.1 + mysql2: '>=2' + pg: '>=8' + postgres: '>=3' + sql.js: '>=1' + sqlite3: '>=5' + typebox: '>=1.0.0' + valibot: '>=1.0.0-beta.7' + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + '@aws-sdk/client-rds-data': + optional: true + '@cloudflare/workers-types': + optional: true + '@effect/sql': + optional: true + '@effect/sql-pg': + optional: true + '@electric-sql/pglite': + optional: true + '@libsql/client': + optional: true + '@libsql/client-wasm': + optional: true + '@neondatabase/serverless': + optional: true + '@op-engineering/op-sqlite': + optional: true + '@opentelemetry/api': + optional: true + '@planetscale/database': + optional: true + '@sinclair/typebox': + optional: true + '@sqlitecloud/drivers': + optional: true + '@tidbcloud/serverless': + optional: true + '@tursodatabase/database': + optional: true + '@tursodatabase/database-common': + optional: true + '@tursodatabase/database-wasm': + optional: true + '@types/better-sqlite3': + optional: true + '@types/mssql': + optional: true + '@types/pg': + optional: true + '@types/sql.js': + optional: true + '@upstash/redis': + optional: true + '@vercel/postgres': + optional: true + '@xata.io/client': + optional: true + arktype: + optional: true + better-sqlite3: + optional: true + bun-types: + optional: true + expo-sqlite: + optional: true + gel: + optional: true + mssql: + optional: true + mysql2: + optional: true + pg: + optional: true + postgres: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + typebox: + optional: true + valibot: + optional: true + zod: + optional: true + dts-resolver@3.0.0: resolution: {integrity: sha512-1T1f+z+4tl9XD+m+0HBgWoL/nm0bOIffyWaUuUSBlFg/86IWvfx+wjNaO/ybU0AJzG9/Mi5hBUgGV6zCmWEN7Q==} engines: {node: ^22.18.0 || >=24.0.0} @@ -213,6 +735,11 @@ packages: resolution: {integrity: sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==} engines: {node: '>=14'} + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -225,10 +752,29 @@ packages: picomatch: optional: true + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + get-tsconfig@5.0.0-beta.5: resolution: {integrity: sha512-/6gFNr0N04nob252sTQxyFLi3eKFRqIg1I87YcqAMT1i6SQrSF6KujUEQrtrjMV0H/eejTCltLdDSTEMzHbnsQ==} engines: {node: '>=20.20.0'} + graphql-scalars@1.25.0: + resolution: {integrity: sha512-b0xyXZeRFkne4Eq7NAnL400gStGqG/Sx9VqX0A05nHyEbv57UJnWKsjNnrpVqv5e/8N1MUxkt0wwcRXbiyKcFg==} + engines: {node: '>=10'} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + graphql-yoga@5.21.2: + resolution: {integrity: sha512-IIRF/3xtjj2D6caAWL9177hQ8tV3mWB3hve1GRnz7njPhQ3iY1jFtSp98fNGv0yV9kaPh9kKQ8JWdJZnedVmDw==} + engines: {node: '>=18.0.0'} + peerDependencies: + graphql: ^15.2.0 || ^16.0.0 + + graphql@16.14.2: + resolution: {integrity: sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + hookable@6.1.1: resolution: {integrity: sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==} @@ -236,11 +782,21 @@ packages: resolution: {integrity: sha512-NkJQA7oZ4YHQhd2+H3BoRFKF3d/XNsiKpHZCQEMH9pDX27hQQLsTyOocyRgaIVtf8gHX3Nt3LPkR4e5EdtPAGQ==} engines: {node: ^22.18.0 || >=24.0.0} + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + jsbi@4.3.2: + resolution: {integrity: sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew==} + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} hasBin: true + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + obug@2.1.3: resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==} engines: {node: '>=12.20.0'} @@ -248,10 +804,60 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pg-cloudflare@1.4.0: + resolution: {integrity: sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==} + + pg-connection-string@2.13.0: + resolution: {integrity: sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.14.0: + resolution: {integrity: sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.14.0: + resolution: {integrity: sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.21.0: + resolution: {integrity: sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + picomatch@4.0.4: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + quansync@1.0.0: resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} @@ -287,6 +893,15 @@ packages: engines: {node: '>=10'} hasBin: true + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + srvx@0.11.16: + resolution: {integrity: sha512-bp07zRuycfTY43IjAvvTFnmnJi8ikW0VFiHwOhhYcVW/L4xQ1XY4PAd4Nuum1rsA17C39zL7x+CDhrn5AL32Rw==} + engines: {node: '>=20.16.0'} + hasBin: true + tinyexec@1.2.4: resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} engines: {node: '>=18'} @@ -336,9 +951,24 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + unconfig-core@7.5.0: resolution: {integrity: sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==} + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + + urlpattern-polyfill@10.1.0: + resolution: {integrity: sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + snapshots: '@babel/generator@8.0.0': @@ -363,6 +993,10 @@ snapshots: '@babel/helper-string-parser': 8.0.0 '@babel/helper-validator-identifier': 8.0.0 + '@drizzle-team/brocli@0.11.0': {} + + '@electric-sql/pglite@0.5.3': {} + '@emnapi/core@1.11.0': dependencies: '@emnapi/wasi-threads': 1.2.2 @@ -379,6 +1013,162 @@ snapshots: tslib: 2.8.1 optional: true + '@envelop/core@5.5.1': + dependencies: + '@envelop/instrumentation': 1.0.0 + '@envelop/types': 5.2.1 + '@whatwg-node/promise-helpers': 1.3.2 + tslib: 2.8.1 + + '@envelop/instrumentation@1.0.0': + dependencies: + '@whatwg-node/promise-helpers': 1.3.2 + tslib: 2.8.1 + + '@envelop/types@5.2.1': + dependencies: + '@whatwg-node/promise-helpers': 1.3.2 + tslib: 2.8.1 + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@fastify/busboy@3.2.0': {} + + '@graphql-tools/executor@1.5.3(graphql@16.14.2)': + dependencies: + '@graphql-tools/utils': 11.1.0(graphql@16.14.2) + '@graphql-typed-document-node/core': 3.2.0(graphql@16.14.2) + '@repeaterjs/repeater': 3.1.0 + '@whatwg-node/disposablestack': 0.0.6 + '@whatwg-node/promise-helpers': 1.3.2 + graphql: 16.14.2 + tslib: 2.8.1 + + '@graphql-tools/merge@9.1.9(graphql@16.14.2)': + dependencies: + '@graphql-tools/utils': 11.1.0(graphql@16.14.2) + graphql: 16.14.2 + tslib: 2.8.1 + + '@graphql-tools/schema@10.0.33(graphql@16.14.2)': + dependencies: + '@graphql-tools/merge': 9.1.9(graphql@16.14.2) + '@graphql-tools/utils': 11.1.0(graphql@16.14.2) + graphql: 16.14.2 + tslib: 2.8.1 + + '@graphql-tools/utils@10.11.0(graphql@16.14.2)': + dependencies: + '@graphql-typed-document-node/core': 3.2.0(graphql@16.14.2) + '@whatwg-node/promise-helpers': 1.3.2 + cross-inspect: 1.0.1 + graphql: 16.14.2 + tslib: 2.8.1 + + '@graphql-tools/utils@11.1.0(graphql@16.14.2)': + dependencies: + '@graphql-typed-document-node/core': 3.2.0(graphql@16.14.2) + '@whatwg-node/promise-helpers': 1.3.2 + cross-inspect: 1.0.1 + graphql: 16.14.2 + tslib: 2.8.1 + + '@graphql-typed-document-node/core@3.2.0(graphql@16.14.2)': + dependencies: + graphql: 16.14.2 + + '@graphql-yoga/logger@2.0.1': + dependencies: + tslib: 2.8.1 + + '@graphql-yoga/subscription@5.0.5': + dependencies: + '@graphql-yoga/typed-event-target': 3.0.2 + '@repeaterjs/repeater': 3.1.0 + '@whatwg-node/events': 0.1.2 + tslib: 2.8.1 + + '@graphql-yoga/typed-event-target@3.0.2': + dependencies: + '@repeaterjs/repeater': 3.1.0 + tslib: 2.8.1 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -393,6 +1183,10 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@js-temporal/polyfill@0.5.1': + dependencies: + jsbi: 4.3.2 + '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0)': dependencies: '@emnapi/core': 1.11.0 @@ -400,12 +1194,35 @@ snapshots: '@tybys/wasm-util': 0.10.2 optional: true + '@optique/core@1.1.0': {} + + '@optique/run@1.1.0': + dependencies: + '@optique/core': 1.1.0 + '@oxc-project/types@0.135.0': {} + '@pothos/core@4.13.0(graphql@16.14.2)': + dependencies: + graphql: 16.14.2 + + '@pothos/plugin-drizzle@0.17.4(@pothos/core@4.13.0(graphql@16.14.2))(drizzle-orm@1.0.0-beta.22(@electric-sql/pglite@0.5.3)(@types/pg@8.20.0)(pg@8.21.0))(graphql@16.14.2)': + dependencies: + '@pothos/core': 4.13.0(graphql@16.14.2) + drizzle-orm: 1.0.0-beta.22(@electric-sql/pglite@0.5.3)(@types/pg@8.20.0)(pg@8.21.0) + graphql: 16.14.2 + + '@pothos/plugin-relay@4.7.0(@pothos/core@4.13.0(graphql@16.14.2))(graphql@16.14.2)': + dependencies: + '@pothos/core': 4.13.0(graphql@16.14.2) + graphql: 16.14.2 + '@quansync/fs@1.0.0': dependencies: quansync: 1.0.0 + '@repeaterjs/repeater@3.1.0': {} + '@rolldown/binding-android-arm64@1.1.1': optional: true @@ -466,6 +1283,49 @@ snapshots: '@types/jsesc@2.5.1': {} + '@types/node@26.0.0': + dependencies: + undici-types: 8.3.0 + + '@types/pg@8.20.0': + dependencies: + '@types/node': 26.0.0 + pg-protocol: 1.14.0 + pg-types: 2.2.0 + + '@whatwg-node/disposablestack@0.0.6': + dependencies: + '@whatwg-node/promise-helpers': 1.3.2 + tslib: 2.8.1 + + '@whatwg-node/events@0.1.2': + dependencies: + tslib: 2.8.1 + + '@whatwg-node/fetch@0.10.13': + dependencies: + '@whatwg-node/node-fetch': 0.8.6 + urlpattern-polyfill: 10.1.0 + + '@whatwg-node/node-fetch@0.8.6': + dependencies: + '@fastify/busboy': 3.2.0 + '@whatwg-node/disposablestack': 0.0.6 + '@whatwg-node/promise-helpers': 1.3.2 + tslib: 2.8.1 + + '@whatwg-node/promise-helpers@1.3.2': + dependencies: + tslib: 2.8.1 + + '@whatwg-node/server@0.11.0': + dependencies: + '@envelop/instrumentation': 1.0.0 + '@whatwg-node/disposablestack': 0.0.6 + '@whatwg-node/fetch': 0.10.13 + '@whatwg-node/promise-helpers': 1.3.2 + tslib: 2.8.1 + ansis@4.3.1: {} ast-kit@3.0.0: @@ -478,12 +1338,59 @@ snapshots: cac@7.0.0: {} + cross-inspect@1.0.1: + dependencies: + tslib: 2.8.1 + defu@6.1.7: {} + drizzle-kit@1.0.0-beta.22: + dependencies: + '@drizzle-team/brocli': 0.11.0 + '@js-temporal/polyfill': 0.5.1 + esbuild: 0.25.12 + get-tsconfig: 4.14.0 + jiti: 2.7.0 + + drizzle-orm@1.0.0-beta.22(@electric-sql/pglite@0.5.3)(@types/pg@8.20.0)(pg@8.21.0): + optionalDependencies: + '@electric-sql/pglite': 0.5.3 + '@types/pg': 8.20.0 + pg: 8.21.0 + dts-resolver@3.0.0: {} empathic@2.0.1: {} + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.9 @@ -492,27 +1399,105 @@ snapshots: optionalDependencies: picomatch: 4.0.4 + get-tsconfig@4.14.0: + dependencies: + resolve-pkg-maps: 1.0.0 + get-tsconfig@5.0.0-beta.5: dependencies: resolve-pkg-maps: 1.0.0 + graphql-scalars@1.25.0(graphql@16.14.2): + dependencies: + graphql: 16.14.2 + tslib: 2.8.1 + + graphql-yoga@5.21.2(graphql@16.14.2): + dependencies: + '@envelop/core': 5.5.1 + '@envelop/instrumentation': 1.0.0 + '@graphql-tools/executor': 1.5.3(graphql@16.14.2) + '@graphql-tools/schema': 10.0.33(graphql@16.14.2) + '@graphql-tools/utils': 10.11.0(graphql@16.14.2) + '@graphql-yoga/logger': 2.0.1 + '@graphql-yoga/subscription': 5.0.5 + '@whatwg-node/fetch': 0.10.13 + '@whatwg-node/promise-helpers': 1.3.2 + '@whatwg-node/server': 0.11.0 + graphql: 16.14.2 + lru-cache: 10.4.3 + tslib: 2.8.1 + + graphql@16.14.2: {} + hookable@6.1.1: {} import-without-cache@0.4.0: {} + jiti@2.7.0: {} + + jsbi@4.3.2: {} + jsesc@3.1.0: {} + lru-cache@10.4.3: {} + obug@2.1.3: {} pathe@2.0.3: {} + pg-cloudflare@1.4.0: + optional: true + + pg-connection-string@2.13.0: {} + + pg-int8@1.0.1: {} + + pg-pool@3.14.0(pg@8.21.0): + dependencies: + pg: 8.21.0 + + pg-protocol@1.14.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.21.0: + dependencies: + pg-connection-string: 2.13.0 + pg-pool: 3.14.0(pg@8.21.0) + pg-protocol: 1.14.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.4.0 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + picomatch@4.0.4: {} + postgres-array@2.0.0: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + quansync@1.0.0: {} resolve-pkg-maps@1.0.0: {} - rolldown-plugin-dts@0.26.0(rolldown@1.1.1): + rolldown-plugin-dts@0.26.0(rolldown@1.1.1)(typescript@6.0.3): dependencies: '@babel/generator': 8.0.0 '@babel/helper-validator-identifier': 8.0.0 @@ -523,6 +1508,8 @@ snapshots: get-tsconfig: 5.0.0-beta.5 obug: 2.1.3 rolldown: 1.1.1 + optionalDependencies: + typescript: 6.0.3 transitivePeerDependencies: - oxc-resolver @@ -549,6 +1536,10 @@ snapshots: semver@7.8.4: {} + split2@4.2.0: {} + + srvx@0.11.16: {} + tinyexec@1.2.4: {} tinyglobby@0.2.17: @@ -558,7 +1549,7 @@ snapshots: tree-kill@1.2.2: {} - tsdown@0.22.3: + tsdown@0.22.3(typescript@6.0.3): dependencies: ansis: 4.3.1 cac: 7.0.0 @@ -569,22 +1560,31 @@ snapshots: obug: 2.1.3 picomatch: 4.0.4 rolldown: 1.1.1 - rolldown-plugin-dts: 0.26.0(rolldown@1.1.1) + rolldown-plugin-dts: 0.26.0(rolldown@1.1.1)(typescript@6.0.3) semver: 7.8.4 tinyexec: 1.2.4 tinyglobby: 0.2.17 tree-kill: 1.2.2 unconfig-core: 7.5.0 + optionalDependencies: + typescript: 6.0.3 transitivePeerDependencies: - '@ts-macro/tsc' - '@typescript/native-preview' - oxc-resolver - vue-tsc - tslib@2.8.1: - optional: true + tslib@2.8.1: {} + + typescript@6.0.3: {} unconfig-core@7.5.0: dependencies: '@quansync/fs': 1.0.0 quansync: 1.0.0 + + undici-types@8.3.0: {} + + urlpattern-polyfill@10.1.0: {} + + xtend@4.0.2: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 76f36d4..b4b4756 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,11 @@ packages: - packages/* +allowBuilds: + esbuild: true + catalog: + "@types/node": ^26.0.0 + drizzle-orm: 1.0.0-beta.22 tsdown: ^0.22.3 + typescript: ^6.0.3 diff --git a/scripts/dev.mts b/scripts/dev.mts new file mode 100644 index 0000000..29cd906 --- /dev/null +++ b/scripts/dev.mts @@ -0,0 +1,235 @@ +// DrFed: A web-based platform for developing and debugging ActivityPub apps +// Copyright (C) 2026 DrFed team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import { spawn, type ChildProcess } from "node:child_process"; +import { existsSync } from "node:fs"; +import { readdir, rm } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import process from "node:process"; +import { fileURLToPath } from "node:url"; + +interface ExitResult { + readonly code: number | null; + readonly signal: NodeJS.Signals | null; + readonly error?: Error; +} + +interface ShutdownOptions { + readonly skipServer?: boolean; +} + +const root = join(dirname(fileURLToPath(import.meta.url)), ".."); +const packagesDir = join(root, "packages"); +const isWindows = process.platform === "win32"; +const pnpm = isWindows ? "pnpm.cmd" : "pnpm"; + +let buildProcess: ChildProcess | undefined; +let serverProcess: ChildProcess | undefined; +let shuttingDown = false; + +process.on("SIGINT", () => { + void shutdown(0, "SIGINT"); +}); +process.on("SIGTERM", () => { + void shutdown(143, "SIGTERM"); +}); + +try { + await removeDistDirs(); + + buildProcess = spawnManaged( + pnpm, + ["--parallel", "--recursive", "exec", "tsdown", "--watch", "--no-clean"], + root, + ); + const buildExit = waitForExit(buildProcess); + + await waitForBuilds(buildExit); + + serverProcess = spawnManaged( + process.execPath, + ["--watch", "bin/drfed-server.mjs", "--pglite-data-path", "../../.pgdata"], + join(root, "packages", "drfed"), + ); + const serverExit = await waitForExit(serverProcess); + if (serverExit.error != null) throw serverExit.error; + const exitCode = serverExit.code ?? signalExitCode(serverExit.signal) ?? 1; + await shutdown(exitCode, "SIGTERM", { skipServer: true }); +} catch (error) { + console.error(error instanceof Error ? error.message : error); + await shutdown(1, "SIGTERM"); +} + +async function removeDistDirs() { + const packages = await readdir(packagesDir, { withFileTypes: true }); + await Promise.all( + packages + .filter((entry) => entry.isDirectory()) + .map((entry) => + rm(join(packagesDir, entry.name, "dist"), { + force: true, + recursive: true, + }), + ), + ); +} + +function spawnManaged( + command: string, + args: readonly string[], + cwd: string, +): ChildProcess { + return spawn(command, args, { + cwd, + detached: !isWindows, + env: process.env, + stdio: "inherit", + windowsHide: true, + }); +} + +async function waitForBuilds(buildExit: Promise): Promise { + let buildExitResult: ExitResult | undefined; + buildExit.then((result) => { + buildExitResult = result; + }); + + while (true) { + if (buildExitResult != null) { + if (buildExitResult.error != null) throw buildExitResult.error; + const exitCode = + buildExitResult.code ?? signalExitCode(buildExitResult.signal); + throw new Error( + `Build watcher exited before startup completed: ${exitCode}`, + ); + } + + const packages = await readdir(packagesDir, { withFileTypes: true }); + const allGenerated = await Promise.all( + packages + .filter((entry) => entry.isDirectory()) + .map(async (entry) => { + const distDir = join(packagesDir, entry.name, "dist"); + if (!existsSync(distDir)) return false; + const entries = await readdir(distDir); + return entries.length > 0; + }), + ); + if (allGenerated.every(Boolean)) return; + + await sleep(100); + } +} + +async function shutdown( + exitCode: number, + signal: NodeJS.Signals, + options: ShutdownOptions = {}, +): Promise { + if (shuttingDown) { + forceKill(serverProcess); + forceKill(buildProcess); + process.exit(exitCode); + } + shuttingDown = true; + + await Promise.all([ + options.skipServer + ? Promise.resolve() + : terminate(serverProcess, signal === "SIGINT" ? "SIGINT" : "SIGTERM"), + terminate(buildProcess, "SIGTERM"), + ]); + process.exit(exitCode); +} + +function waitForExit(child: ChildProcess): Promise { + return new Promise((resolve) => { + child.once("exit", (code, signal) => resolve({ code, signal })); + child.once("error", (error) => resolve({ code: 1, signal: null, error })); + }); +} + +function terminate( + child: ChildProcess | undefined, + signal: NodeJS.Signals, +): Promise { + if (child == null || child.exitCode != null || child.signalCode != null) { + return Promise.resolve(); + } + + return new Promise((resolve) => { + const timeout = setTimeout(() => { + forceKill(child); + }, 5000); + + child.once("exit", () => { + clearTimeout(timeout); + resolve(); + }); + killTree(child, signal); + }); +} + +function killTree(child: ChildProcess, signal: NodeJS.Signals): void { + try { + if (isWindows) { + child.kill(signal); + } else { + if (child.pid != null) process.kill(-child.pid, signal); + } + } catch (error) { + if (!isProcessLookupError(error)) child.kill(signal); + } +} + +function forceKill(child: ChildProcess | undefined): void { + if (child == null || child.exitCode != null || child.signalCode != null) + return; + + if (isWindows) { + spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], { + stdio: "ignore", + windowsHide: true, + }); + return; + } + + try { + if (child.pid != null) process.kill(-child.pid, "SIGKILL"); + } catch (error) { + if (!isProcessLookupError(error)) child.kill("SIGKILL"); + } +} + +function isProcessLookupError(error: unknown): boolean { + return ( + typeof error === "object" && + error != null && + "code" in error && + error.code === "ESRCH" + ); +} + +function signalExitCode(signal: NodeJS.Signals | null): number | undefined { + if (signal === "SIGINT") return 130; + if (signal === "SIGTERM") return 143; + return undefined; +} + +function sleep(ms: number): Promise { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +}