diff --git a/.github/workflows/product-graphql.yml b/.github/workflows/product-graphql.yml new file mode 100644 index 00000000..e4033bab --- /dev/null +++ b/.github/workflows/product-graphql.yml @@ -0,0 +1,23 @@ +name: Product GraphQL +on: + pull_request: + paths: ['applications/product-graphql/**', '.github/workflows/product-graphql.yml'] + push: + paths: ['applications/product-graphql/**', '.github/workflows/product-graphql.yml'] +jobs: + build: + runs-on: ubuntu-latest + defaults: + run: + working-directory: applications/product-graphql + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: '1.4.2' + - run: bun install --frozen-lockfile --ignore-scripts + - run: bun run codegen + - run: git diff --exit-code -- schema.graphql src/client/gql + - run: bun run typecheck + - run: bun run test + - run: bun run build diff --git a/applications/product-graphql/.env.example b/applications/product-graphql/.env.example new file mode 100644 index 00000000..83b62fd9 --- /dev/null +++ b/applications/product-graphql/.env.example @@ -0,0 +1,5 @@ +DATABASE_URL=postgres://catalog_app:YOUR_PASSWORD@YOUR_HOST:5432/postgres +PG_CA_CERT_PATH=.deployment/postgres-ca.pem +APP_ORIGIN=http://127.0.0.1:3001 +PORT=3001 +# DATABASE_LOCAL=true is only for loopback local Postgres. diff --git a/applications/product-graphql/.gitignore b/applications/product-graphql/.gitignore new file mode 100644 index 00000000..b8df62e7 --- /dev/null +++ b/applications/product-graphql/.gitignore @@ -0,0 +1,7 @@ +.env +.deployment/ +node_modules/ +dist/ +test-results/ +playwright-report/ +.clickhouse/ diff --git a/applications/product-graphql/README.md b/applications/product-graphql/README.md new file mode 100644 index 00000000..8aa80eef --- /dev/null +++ b/applications/product-graphql/README.md @@ -0,0 +1,273 @@ +# Collection studio: typed GraphQL and Postgres + +Explore a small design catalogue, inspect variants, and curate an ordered +collection. Save a new collection from your draft, move products up, remove items, +and save again. A stale tab receives a conflict and keeps its draft until reload. + +![Collection studio](docs/product-graphql.png) + +This is a trusted local demo with synthetic products, no accounts or payments. +It binds to loopback and checks Host and write Origin. + +## How the pieces fit + +Pothos constructs Product, Variant and Collection types from typed Drizzle rows +and explicit resolvers. GraphQL Yoga serves the schema through Bun’s Fetch API. +Drizzle queries Postgres and commits collection revisions and item replacements +atomically. A request-scoped DataLoader batches variant lookups. GraphQL Code +Generator emits real typed operation documents from the printed Pothos schema; +urql runs those queries and mutations in React. + +The catalogue has 24 seeded products and three variants each. Pagination accepts +`first` 1–20 and `offset` 0–10,000; the UI shows twelve products per page. +Collections contain at most twelve distinct products with unique ordered +positions 0–11. The switcher lists the first twenty collections by title. This +small local workbench does not expose a public arbitrary-query API. + +Stable Drizzle uses explicit resolvers, avoiding the Pothos Drizzle plugin’s +release-candidate relational API requirement. + +## Get the source and tools + +Run application dependencies inside Linux. On macOS, an isolated +[OrbStack machine](https://docs.orbstack.dev/machines/isolated) keeps dependencies +off the host: + +```sh +orbctl create ubuntu:noble catalog-dev --isolated --arch arm64 --memory 4G --cpus 2 +orbctl run -m catalog-dev -w /home/$USER bash +``` + +Inside Linux: + +```sh +sudo apt-get update +sudo apt-get install -y ca-certificates curl git unzip jq openssl postgresql-client docker.io +sudo systemctl start docker +sudo usermod -aG docker "$USER" +# Reopen your Linux shell so the Docker group applies. +curl -fsSL https://bun.sh/install -o /tmp/bun-install.sh +bash /tmp/bun-install.sh bun-v1.4.2 +export PATH="$HOME/.bun/bin:$HOME/.local/bin:$PATH" +curl -fsSL https://clickhouse.com/cli | sh +git clone https://github.com/ClickHouse/examples.git +cd examples/applications/product-graphql +bun install --frozen-lockfile --ignore-scripts +``` + +The checked-in lockfile pins the library versions. Bun must report `1.4.2`. +No third-party application service is required. Cloud lifecycle commands may +instead run with an existing host `clickhousectl`; copy only the database CA and +step-specific credentials into the VM, keeping Cloud API keys on the host. + +## Choose a database + +### Local Postgres with clickhousectl + +```sh +umask 077 +mkdir -p .deployment +clickhousectl local postgres start --name catalog --version 18 --json \ + > .deployment/local-create.json +export PGHOST=127.0.0.1 +export PGPORT="$(jq -er '.port' .deployment/local-create.json)" +export PGDATABASE="$(jq -er '.database' .deployment/local-create.json)" +export PGADMIN="$(jq -er '.user' .deployment/local-create.json)" +export PGPASSWORD="$(jq -er '.password' .deployment/local-create.json)" +export PGSSLMODE=disable +``` + +Use the actual returned port. This receipt contains a password and belongs in +the ignored private directory. + +### ClickHouse Managed Postgres with clickhousectl + +Create an Admin API key following the +[Cloud API guide](https://clickhouse.com/docs/cloud/manage/openapi), then enter +it interactively: + +```sh +clickhousectl cloud auth login --interactive +clickhousectl cloud auth status +clickhousectl cloud org list +umask 077 +mkdir -p .deployment +``` + +Create `.deployment/resources.env` in an editor with your organization, an +available AWS region and Postgres size: + +```dotenv +CH_ORG_ID=YOUR_ORGANIZATION_UUID +CLOUD_REGION=YOUR_AVAILABLE_REGION +PG_SIZE=YOUR_AVAILABLE_SIZE +``` + +One non-HA Postgres 18 service is enough for this example. Review +[pricing](https://clickhouse.com/pricing) before creating it. Compute, storage, +backups and network usage can incur charges; stopping the application or VM +does not stop the managed service's billing. + +```sh +source .deployment/resources.env +clickhousectl cloud postgres create --org-id "$CH_ORG_ID" \ + --name product-graphql-example --provider aws --region "$CLOUD_REGION" \ + --size "$PG_SIZE" --pg-version 18 --ha-type none \ + --tag project=product-graphql --json > .deployment/postgres-create.json +PG_SERVICE_ID="$(jq -er '.id' .deployment/postgres-create.json)" +``` + +Save the service ID in `.deployment/resources.env`. Preserve the create receipt: +its initial password is returned once. If creation is interrupted, inspect +`clickhousectl cloud postgres list --org-id "$CH_ORG_ID" --json` before retrying. + +Repeat inspection until `state` is `running`: + +```sh +clickhousectl cloud postgres get "$PG_SERVICE_ID" --org-id "$CH_ORG_ID" --json \ + > .deployment/postgres-status.json +jq '{id,state,hostname,username,postgresVersion,size}' .deployment/postgres-status.json +clickhousectl cloud postgres certs get "$PG_SERVICE_ID" --org-id "$CH_ORG_ID" \ + --output .deployment/postgres-ca.pem +``` + +Use the service's actual direct hostname, port, database and administrator. +For a direct service connection, use port 5432 and database `postgres`; check the service response for the actual connection details: + +```sh +export PGHOST="$(jq -er '.hostname' .deployment/postgres-status.json)" +export PGPORT=5432 PGDATABASE=postgres +export PGADMIN="$(jq -er '.username' .deployment/postgres-status.json)" +export PGPASSWORD="$(jq -er '.password' .deployment/postgres-create.json)" +export PGSSLMODE=verify-full +export PGSSLROOTCERT="$PWD/.deployment/postgres-ca.pem" +``` + +## Apply roles, schema and seed + +Run once on your dedicated fresh database. Passwords are read by psql from +environment variables and do not become command arguments. + +```sh +cat > .deployment/passwords.env < .env <=18" }, "optionalPeers": ["@types/node"] }, "sha512-XkOardciF3tow2NUlY6/Qjk1QqYa+6WpmVLTpxXl97Uv4erczDEZ+xg1j6LqBf0ekcRSUiICQA4nQXHHkVKQ9w=="], + + "@inquirer/confirm": ["@inquirer/confirm@6.3.3", "", { "dependencies": { "@inquirer/core": "^12.0.4", "@inquirer/type": "4.2.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-ImOBgpl/sJTFp8naeSkNu4S0Sf8F8pxv3/CUx+LoBAdQvCVqFz6rSB2A/oZYsJge3+yphbEDxEkfaRNYq1OU0A=="], + + "@inquirer/core": ["@inquirer/core@12.0.4", "", { "dependencies": { "@inquirer/ansi": "^2.0.9", "@inquirer/figures": "^2.0.9", "@inquirer/type": "4.2.0", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-M5fB67y7jPObI45vhD82RskWwCSm0scOvenV/I6K8tIi2gfr9DXqQqJ+iw+Wi6SgapikdOiJibYzz1gW47fsDg=="], + + "@inquirer/editor": ["@inquirer/editor@5.3.4", "", { "dependencies": { "@inquirer/core": "^12.0.4", "@inquirer/external-editor": "^3.0.6", "@inquirer/type": "4.2.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-imzxQDfw26NpcJAubMj/dE5RjU9Mp80IcT+FzsD6YoxXmL8L1QYnAFdCNv7ccGWh//73+0YP+1kfhwnExdl2VQ=="], + + "@inquirer/expand": ["@inquirer/expand@5.1.6", "", { "dependencies": { "@inquirer/core": "^12.0.4", "@inquirer/type": "4.2.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-JfiLKi7qGuHmcQO/DqR10UFEp054Y1FXZaszP5ZEfHhgHeuFoT5nNFQIXFhed/5/p07SbsV2tIJFwG/dg5+NmQ=="], + + "@inquirer/external-editor": ["@inquirer/external-editor@3.0.6", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-rWmw6WG8JZ7LMn/srXEylYGhL6EXXJeROE7/4x4GREh6NgzqJBDbIgpNGsYoLPfobeb4VZtEYzPGnT5EFNYhiQ=="], + + "@inquirer/figures": ["@inquirer/figures@2.0.9", "", {}, "sha512-EAWgUTGQ/Umgga51dE3B2PUHbufuXarDfg86uVgoSgNHNNQnyFKcOrQLWVqYMghuSyHh8+2HUH0Js9cTC1WAdg=="], + + "@inquirer/input": ["@inquirer/input@5.1.7", "", { "dependencies": { "@inquirer/core": "^12.0.4", "@inquirer/type": "4.2.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Icrs9tuMBCxGI+lFYNO71VFOnqGO8IIrLIGqnTGh07X8jZ2la9QLAYxXcikHZQjjVhIZUtU2xyH89DVN/pPhVg=="], + + "@inquirer/number": ["@inquirer/number@4.2.4", "", { "dependencies": { "@inquirer/core": "^12.0.4", "@inquirer/type": "4.2.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-C6g80U4U2+w2Vxa1CB0MN3a2WnQIfzLSUG0lduO3Au9GsirJsKATZkOwZPK+vZ1BlAlz+/Z5ParfQCS9E0svmg=="], + + "@inquirer/password": ["@inquirer/password@5.2.3", "", { "dependencies": { "@inquirer/ansi": "^2.0.9", "@inquirer/core": "^12.0.4", "@inquirer/type": "4.2.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-1Uo2qfI2K3wXbfunMpvwAzoZPIUf5iEETwDGqRVbswoorXGqYQJ/bfIJFeLj+pxjq+e2UFPNOmx+fsIl4Urigw=="], + + "@inquirer/prompts": ["@inquirer/prompts@8.7.3", "", { "dependencies": { "@inquirer/checkbox": "^5.2.6", "@inquirer/confirm": "^6.3.3", "@inquirer/editor": "^5.3.4", "@inquirer/expand": "^5.1.6", "@inquirer/input": "^5.1.7", "@inquirer/number": "^4.2.4", "@inquirer/password": "^5.2.3", "@inquirer/rawlist": "^5.3.6", "@inquirer/search": "^4.3.4", "@inquirer/select": "^5.2.6" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-DHK20kO+H5k/XcJQpHQUpVQbMYHHhZchlGWhZVMUV4WIuRuBW5744hipOtriE1vMqPLgnIyXFMHWKkIbTepa4Q=="], + + "@inquirer/rawlist": ["@inquirer/rawlist@5.3.6", "", { "dependencies": { "@inquirer/core": "^12.0.4", "@inquirer/type": "4.2.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-2exExhQ5xTBqfN/rRlZkvM1Gm3b0eiLcWe2NqxWxe7qYmnU4M3j/n9TwzfPUb+Q7dAAz9TmBtDWWdF3jbp4ZLA=="], + + "@inquirer/search": ["@inquirer/search@4.3.4", "", { "dependencies": { "@inquirer/core": "^12.0.4", "@inquirer/figures": "^2.0.9", "@inquirer/type": "4.2.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-xsk0UeFP04jsbEeFMzghjUaH2aHXyt+WykSajQzq/8qyBNHsgGYHea/sFwv5T08kW7Sp+KDX8aF124zPUIs1Ww=="], + + "@inquirer/select": ["@inquirer/select@5.2.6", "", { "dependencies": { "@inquirer/ansi": "^2.0.9", "@inquirer/core": "^12.0.4", "@inquirer/figures": "^2.0.9", "@inquirer/type": "4.2.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-jFu7CFghlr0vpi/326a4wz6BGkJH9X3H/iIu+ahyV2Ja52ktqQE3fC9BYP47JKeSPQYGUv5xquE2Vp9PbepZ3g=="], + + "@inquirer/type": ["@inquirer/type@4.2.0", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-2rnHX8RPo4glJYqnFc/v3nQeIhDcjyRRyaT7MGcfL/8xv/8K03SXYoA2tX06WVSKywWOE/ncbG3dtuzLjR5TCQ=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@oxc-project/types": ["@oxc-project/types@0.152.0", "", {}, "sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw=="], + + "@playwright/test": ["@playwright/test@1.63.0", "", { "dependencies": { "playwright": "1.63.0" }, "bin": { "playwright": "cli.js" } }, "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ=="], + + "@pothos/core": ["@pothos/core@4.15.1", "", { "peerDependencies": { "graphql": "^16.10.0 || ^17.0.0" } }, "sha512-G0riOIbF6zer2SU7V1MmivkSdoKk+gYRf+Ru8JVcy3p9GhyJ8OzQj92RgCfCpEvkTgl6EdH8VueLe1Lm7lY4aA=="], + + "@repeaterjs/repeater": ["@repeaterjs/repeater@3.1.0", "", {}, "sha512-TaoVksZRSx2KWYYpyLQtMQXXeS98VsgZImzW65xmiVgbYhXLk+aEsmzPLirqVuE4/XuUapH2iMtxUzaBNDzdSQ=="], + + "@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.12", "", { "os": "android", "cpu": "arm" }, "sha512-dB/a1214qKfHMXCpgqR4OZT+jS4kTyEXbQGJPqzobt5EwH5rX080pxE37alt3RzvR1bf1Yz/yGqRfrYAxuPw0A=="], + + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.12", "", { "os": "android", "cpu": "arm64" }, "sha512-7KHFgQ5VJxIHcLlrwrc3Xbds7oTNQT7Pgi9gQCJKrd2VGab/UksIOYp6VD8MzCstGxOKMgNamPwUCfxPdP1OHg=="], + + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-3YIhqHD96nA5SaYNRBR16HnGv4oavZvXfD/ayHM+oYZ0WD/8lBAtf6zQua4kEyAvpqrluKXl0lnOBoiNby7x9w=="], + + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-UuuJ35MFw4gmFOrE9pEqIV+K3syIKveph+Qc1/ljHZVdoDW4pz/JHR/eMVom+TZGl/5OOvGJOWaOCVt3ZfqhxA=="], + + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-uMvssit0a4W+/7D8CbHUvG719mH3R2jwXAlh/XcPvuHTE0g++LymF88DCGNX0HM2rBOn0xrzgXktIB6fLSJBTQ=="], + + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.12", "", { "os": "linux", "cpu": "arm" }, "sha512-XcFu0R0xWnwzSf4IQgFH1rJIckPN1pLy2R+4r9IDB7Yfu/ys9cVqfa4pBrMHj7a3gl8mIR4nRNPg0e5IvEVs6g=="], + + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-260UrKgn8tz39ak+SMDOirKzr7V04M9dWPw5llW00SwBivCZoWcRBKV1d8cXnRkUmSZA3BdiUmBHWk7734Ulpw=="], + + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-5YK1I9SqDkbPgc1IA8BgDl34suqUS2q0KWnBrirm0E51YjOs6eo6dV6jbQfNE/argHRSvd0QUGgtpIoYx+WWpw=="], + + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Rkcrmp7eFRg74yL5fXEU91JEWbdEPLevWwGtXpmhbjlD1StScbWTmO94Bhly+Mo+ketKYkdmM1vNUKeWSlx8cQ=="], + + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-qvK4DuAsQc2BSjlx+Xr+IzOIvvxbGZqxFwdWfG6F518Erj0GGISyQbJ6pIappnOxlNPzNHvo/L0BwB30GZ+zVw=="], + + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.12", "", { "os": "linux", "cpu": "x64" }, "sha512-Q9uLBO53Xd4QIq1WOycVQyPP1O4HhraEV2qqb3uTrnVw6QZih9duY4vNXOivL1xoUS1/z+W8eF4NMfl2a8Sdjw=="], + + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.12", "", { "os": "linux", "cpu": "x64" }, "sha512-3IBxWFMjbOZskDPKv8Lf9BCnahlKuHthWkYnyIxOH/QcJrFcS4EmcenthApkwr/5+nEqZlLzeYbxeMaX7A5u4g=="], + + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xtX61xg4LKPkPWilZU1ynKClz5Gj4bf74LML4r3eVLWumKnGjoEr1OSHQhMdbBDoYTi+yjrujvpZe2pUnqCrrA=="], + + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-At7fPB6PCaIjzgIhEZFxuT+BBFqiQibJDT4d3PhiR3f4E7bbMZF4aKblbFfEM3sETRDd1YiQx/+U/g/B/ou5Ew=="], + + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.12", "", { "os": "win32", "cpu": "x64" }, "sha512-WIw2haVKwjuYdXkHaoC0mF8Le71TuCBxjrdKqLbJGctbBABj+ClfmNvtbOnzpq3RokNo5+V1qhtSzJyXorsklQ=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], + + "@types/bun": ["@types/bun@1.4.2", "", { "dependencies": { "bun-types": "1.4.2" } }, "sha512-GimotNn7+ZV0uVArItBbriZsR1oNf0+WTzPkdcFrzShI7k2norL0uzEaJT8T33dWr7O/c9ZDuAFQrctKCi72oQ=="], + + "@types/node": ["@types/node@26.6.4", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg=="], + + "@types/pg": ["@types/pg@8.23.1", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A=="], + + "@types/react": ["@types/react@19.3.0", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg=="], + + "@types/react-dom": ["@types/react-dom@19.3.0", "", { "peerDependencies": { "@types/react": "^19.3.0" } }, "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q=="], + + "@types/ws": ["@types/ws@8.18.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-67MQl+fpWKVTT1NYdnmo3U4sc/xPo/zQBncVnI74qmQa0z/b+1g6iYqNmGCPbxO+zz2aklb08a0oHfegiVd0/w=="], + + "@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="], + + "@typescript/typescript-darwin-arm64": ["@typescript/typescript-darwin-arm64@7.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA=="], + + "@typescript/typescript-darwin-x64": ["@typescript/typescript-darwin-x64@7.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA=="], + + "@typescript/typescript-freebsd-arm64": ["@typescript/typescript-freebsd-arm64@7.0.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ=="], + + "@typescript/typescript-freebsd-x64": ["@typescript/typescript-freebsd-x64@7.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw=="], + + "@typescript/typescript-linux-arm": ["@typescript/typescript-linux-arm@7.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ=="], + + "@typescript/typescript-linux-arm64": ["@typescript/typescript-linux-arm64@7.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ=="], + + "@typescript/typescript-linux-loong64": ["@typescript/typescript-linux-loong64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ=="], + + "@typescript/typescript-linux-mips64el": ["@typescript/typescript-linux-mips64el@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA=="], + + "@typescript/typescript-linux-ppc64": ["@typescript/typescript-linux-ppc64@7.0.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA=="], + + "@typescript/typescript-linux-riscv64": ["@typescript/typescript-linux-riscv64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ=="], + + "@typescript/typescript-linux-s390x": ["@typescript/typescript-linux-s390x@7.0.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw=="], + + "@typescript/typescript-linux-x64": ["@typescript/typescript-linux-x64@7.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A=="], + + "@typescript/typescript-netbsd-arm64": ["@typescript/typescript-netbsd-arm64@7.0.2", "", { "os": "none", "cpu": "arm64" }, "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA=="], + + "@typescript/typescript-netbsd-x64": ["@typescript/typescript-netbsd-x64@7.0.2", "", { "os": "none", "cpu": "x64" }, "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA=="], + + "@typescript/typescript-openbsd-arm64": ["@typescript/typescript-openbsd-arm64@7.0.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ=="], + + "@typescript/typescript-openbsd-x64": ["@typescript/typescript-openbsd-x64@7.0.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg=="], + + "@typescript/typescript-sunos-x64": ["@typescript/typescript-sunos-x64@7.0.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g=="], + + "@typescript/typescript-win32-arm64": ["@typescript/typescript-win32-arm64@7.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ=="], + + "@typescript/typescript-win32-x64": ["@typescript/typescript-win32-x64@7.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g=="], + + "@urql/core": ["@urql/core@6.0.3", "", { "dependencies": { "@0no-co/graphql.web": "^1.0.13", "wonka": "^6.3.2" } }, "sha512-0F39XWR21+IVLJfsqBvLShvoSzxT6h6wWXwrDhN8j9J+IEVOpQIUNHwpgjsp+2ePzn9aCH+rpDonn3HOAUnn9A=="], + + "@vitejs/plugin-react": ["@vitejs/plugin-react@6.1.1", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "oxc-transform-react": "^0.145.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler", "oxc-transform-react"] }, "sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw=="], + + "@whatwg-node/disposablestack": ["@whatwg-node/disposablestack@0.1.0", "", { "dependencies": { "@whatwg-node/promise-helpers": "^2.0.0", "tslib": "^2.6.3" } }, "sha512-ok0A9+D+mdeb3w1GNCpVr+2gVVLRWKo9Ywm/whNsoWSNfNg3jNxS0/wUF6NSUYbfOtFGu2wZgJnLjasbaQEfvA=="], + + "@whatwg-node/events": ["@whatwg-node/events@0.1.2", "", { "dependencies": { "tslib": "^2.6.3" } }, "sha512-ApcWxkrs1WmEMS2CaLLFUEem/49erT3sxIVjpzU5f6zmVcnijtDSrhoK2zVobOIikZJdH63jdAXOrvjf6eOUNQ=="], + + "@whatwg-node/fetch": ["@whatwg-node/fetch@0.10.13", "", { "dependencies": { "@whatwg-node/node-fetch": "^0.8.3", "urlpattern-polyfill": "^10.0.0" } }, "sha512-b4PhJ+zYj4357zwk4TTuF2nEe0vVtOrwdsrNo5hL+u1ojXNhh1FgJ6pg1jzDlwlT4oBdzfSwaBwMCtFCsIWg8Q=="], + + "@whatwg-node/node-fetch": ["@whatwg-node/node-fetch@0.8.6", "", { "dependencies": { "@fastify/busboy": "^3.1.1", "@whatwg-node/disposablestack": "^0.0.6", "@whatwg-node/promise-helpers": "^1.3.2", "tslib": "^2.6.3" } }, "sha512-BDMdYFcerLQkwA2RTldxOqRCs6ZQD1S7UgP3pUdGUkcbgTrP/V5ko77ZkCww9DHmC4lpoYuwigGfQYj285gMvA=="], + + "@whatwg-node/promise-helpers": ["@whatwg-node/promise-helpers@1.3.2", "", { "dependencies": { "tslib": "^2.6.3" } }, "sha512-Nst5JdK47VIl9UcGwtv2Rcgyn5lWtZ0/mhRQ4G8NN2isxpq2TO30iqHzmwoJycjWuyUfg3GFXqP/gFHXeV57IA=="], + + "@whatwg-node/server": ["@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.6.3" } }, "sha512-VSdkwnJRr8Yv9UgB2aXB3VUPWwd6Oqnn0hycFwhg9pZgWxJXb7JmhsiXe9tmpMwjHFxli12PGcz9aI63YYloGQ=="], + + "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], + + "ansi-regex": ["ansi-regex@6.4.0", "", {}, "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw=="], + + "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="], + + "auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="], + + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.11.27", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-ElY12DaROGuan+lMmZ8Cvo/ZUbXPe7Enc/9VU/b1T3Kp4dwytRcNdR8DoSJN5SNJT/CuvcCA0DHDVmMOCePdRQ=="], + + "brace-expansion": ["brace-expansion@5.0.12", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "browserslist": ["browserslist@4.29.3", "", { "dependencies": { "baseline-browser-mapping": "^2.11.26", "caniuse-lite": "^1.0.30001813", "electron-to-chromium": "^1.5.439", "node-releases": "^2.0.57", "update-browserslist-db": "^1.3.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA=="], + + "bun-types": ["bun-types@1.4.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-bxV1FgK7yBIzjRe5zBozIM4Bem11ZJcCXSrjWRG3YWLt8yFDePu4cLjpebO8OvPeIE9trbyPF4fuj3Cia4Fj3w=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001814", "", {}, "sha512-/Uaf1lAzr59XcMpW0o96WoEfr+VXK2OX4U9AgFoiSHsVJ4HppnIFUjtYzsyDH2+tgANaQb2/oxYGwCPapN1FpA=="], + + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "change-case": ["change-case@5.4.4", "", {}, "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w=="], + + "change-case-all": ["change-case-all@2.1.0", "", { "dependencies": { "change-case": "^5.2.0", "sponge-case": "^2.0.2", "swap-case": "^3.0.2", "title-case": "^3.0.3" } }, "sha512-v6b0WWWkZUMHVuYk82l+WROgkUm4qEN2w5hKRNWtEOYwWqUGoi8C6xH0l1RLF1EoWqDFK6MFclmN3od6ws3/uw=="], + + "chardet": ["chardet@2.2.0", "", {}, "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA=="], + + "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], + + "cli-truncate": ["cli-truncate@5.2.0", "", { "dependencies": { "slice-ansi": "^8.0.0", "string-width": "^8.2.0" } }, "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw=="], + + "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], + + "cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="], + + "common-tags": ["common-tags@1.8.2", "", {}, "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "cosmiconfig": ["cosmiconfig@9.0.2", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg=="], + + "cross-inspect": ["cross-inspect@1.0.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Pcw1JTvZLSJH83iiGWt6fRcT+BjZlCDRVwYLbUcHzv/CRpB7r0MlSrGbIyQvVSNyGnbt7G4AXuyCiDR3POvZ1A=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], + + "dataloader": ["dataloader@2.2.3", "", {}, "sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA=="], + + "debounce": ["debounce@3.0.0", "", {}, "sha512-64byRbF0/AirwbuHqB3/ZpMG9/nckDa6ZA0yd6UnaQNwbbemCOwvz2sL5sjXLHhZHADyiwLm0M5qMhltUUx+TA=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "dependency-graph": ["dependency-graph@1.0.0", "", {}, "sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg=="], + + "detect-indent": ["detect-indent@7.0.2", "", {}, "sha512-y+8xyqdGLL+6sh0tVeHcfP/QDd8gUgbasolJJpY7NgeQGSZ739bDtSiaiDgtoicy+mtYB81dKLxO9xRhCyIB3A=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], + + "drizzle-orm": ["drizzle-orm@0.45.3", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@netlify/db": ">=0.4.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "prisma": "*", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@netlify/db", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "prisma", "sql.js", "sqlite3"] }, "sha512-CAloER21cDdcgZ1OmjrZX82EeRNsdP+y0onX/eyoYDaYzki/adrAa91lzM5jWh7zZPK4qUbw/6LGv9J+A+uizA=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.444", "", {}, "sha512-5ss/uJfoDYDHT0lfJzT6FbcskIzROIOPf0BbbFkGcvDzoJU7i//9GDrwwIHQVmIsrAGiF3ihpADBRIsrEFt1rQ=="], + + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + + "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], + + "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], + + "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="], + + "fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="], + + "fast-wrap-ansi": ["fast-wrap-ansi@0.2.2", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q=="], + + "fastq": ["fastq@1.20.3", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-east-asian-width": ["get-east-asian-width@1.7.0", "", {}, "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q=="], + + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], + + "graphql": ["graphql@16.12.0", "", {}, "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ=="], + + "graphql-config": ["graphql-config@5.1.7", "", { "dependencies": { "@graphql-tools/graphql-file-loader": "^8.0.0", "@graphql-tools/json-file-loader": "^8.0.0", "@graphql-tools/load": "^8.1.0", "@graphql-tools/merge": "^9.0.0", "@graphql-tools/url-loader": "^9.0.0", "@graphql-tools/utils": "^11.0.0", "cosmiconfig": "^8.1.0", "jiti": "^2.0.0", "minimatch": "^10.0.0", "string-env-interpolation": "^1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "cosmiconfig-toml-loader": "^1.0.0", "graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" }, "optionalPeers": ["cosmiconfig-toml-loader"] }, "sha512-7/2bD8fvACyXiRkuwgVE5U+WAVyBgcL2h85+FeafAMZRs1mZhZSzioSnJ3pWnh+EA50y8VdQRntF7jX5aznMIg=="], + + "graphql-tag": ["graphql-tag@2.12.7", "", { "dependencies": { "tslib": "^2.1.0" }, "peerDependencies": { "graphql": "^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" } }, "sha512-xnE/NFzy+0eIesvAsREJZ284zTl/wYuBAvpsFSDhRGRdRHdnE90M21Q3xAWyYInb0J756c6x0pIQ62+vtvOs1Q=="], + + "graphql-ws": ["graphql-ws@6.3.0", "", { "peerDependencies": { "@fastify/websocket": "^10 || ^11", "crossws": "~0.3 || ~0.4", "graphql": "^15.10.1 || ^16 || ^17", "ws": "^8" }, "optionalPeers": ["@fastify/websocket", "crossws", "ws"] }, "sha512-qBAXPtFjWHF3QypDyNcBp8Rycd+rmofxUSJsu3bGI9QMMq7pImKBSLSg9hZ2ojIMbJ54T/LPjKBIeOUoJbNBnQ=="], + + "graphql-yoga": ["graphql-yoga@5.24.1", "", { "dependencies": { "@envelop/core": "^5.6.1", "@envelop/instrumentation": "^1.0.1", "@graphql-tools/executor": "^2.0.0", "@graphql-tools/schema": "^10.0.11", "@graphql-tools/utils": "^11.2.0", "@graphql-yoga/logger": "^2.0.2", "@graphql-yoga/subscription": "^5.1.1", "@whatwg-node/fetch": "^0.10.6", "@whatwg-node/promise-helpers": "^1.3.2", "@whatwg-node/server": "^0.11.0", "lru-cache": "^10.0.0", "tslib": "^2.8.1" }, "peerDependencies": { "graphql": "^15.2.0 || ^16.0.0 || ^17.0.0" } }, "sha512-NA2aQ4sZvR/bniCCLOlMG6OSZf5hPg3mUf3Phm33ObqRMZEq6WtZrj2hTcHfT2ZEFVV5FRCbcMb4NB1w7BIKaA=="], + + "iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], + + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "immutable": ["immutable@5.1.9", "", {}, "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "import-from": ["import-from@4.0.0", "", {}, "sha512-P9J71vT5nLlDeV8FHs5nNxaLbrpfAV5cF5srvbZfpwpcJoM/xZR3hiv+q+SAnuSmuGbXMWud063iIMx/V/EWZQ=="], + + "invariant": ["invariant@2.2.4", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA=="], + + "is-absolute": ["is-absolute@1.0.0", "", { "dependencies": { "is-relative": "^1.0.0", "is-windows": "^1.0.1" } }, "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA=="], + + "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-relative": ["is-relative@1.0.0", "", { "dependencies": { "is-unc-path": "^1.0.0" } }, "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA=="], + + "is-unc-path": ["is-unc-path@1.0.0", "", { "dependencies": { "unc-path-regex": "^0.1.2" } }, "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ=="], + + "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], + + "is-windows": ["is-windows@1.0.2", "", {}, "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="], + + "isomorphic-ws": ["isomorphic-ws@5.0.0", "", { "peerDependencies": { "ws": "*" } }, "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw=="], + + "isows": ["isows@1.0.7", "", { "peerDependencies": { "ws": "*" } }, "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg=="], + + "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.3.2", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], + + "json-to-pretty-yaml": ["json-to-pretty-yaml@1.2.2", "", { "dependencies": { "remedial": "^1.0.7", "remove-trailing-spaces": "^1.0.6" } }, "sha512-rvm6hunfCcqegwYaG5T4yKJWxc9FXFgBVrcTZ4XfSVRwa5HA/Xs+vB/Eo9treYYHCeNM0nrSUr82V/M31Urc7A=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "listr2": ["listr2@10.2.2", "", { "dependencies": { "cli-truncate": "^5.2.0", "eventemitter3": "^5.0.4", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^10.0.0" } }, "sha512-JtNtbZj8q5BnDMR7trpwvwk3RIrANtIVzEUm8w7amp6xelLgyuq+4WZoTH913XaQAoH/cNdYhaNzBPA2U3xbDw=="], + + "log-symbols": ["log-symbols@7.0.1", "", { "dependencies": { "is-unicode-supported": "^2.0.0", "yoctocolors": "^2.1.1" } }, "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg=="], + + "log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="], + + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + + "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "map-cache": ["map-cache@0.2.2", "", {}, "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "meros": ["meros@1.3.2", "", { "peerDependencies": { "@types/node": ">=13" }, "optionalPeers": ["@types/node"] }, "sha512-Q3mobPbvEx7XbwhnC1J1r60+5H6EZyNccdzSz0eGexJRwouUtTZxPVRGdqKtxlpD84ScK4+tIGldkqDtCKdI0A=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + + "minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "mute-stream": ["mute-stream@3.0.0", "", {}, "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw=="], + + "nanoid": ["nanoid@3.3.19", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug=="], + + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], + + "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + + "node-releases": ["node-releases@2.0.57", "", {}, "sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw=="], + + "normalize-path": ["normalize-path@2.1.1", "", { "dependencies": { "remove-trailing-separator": "^1.0.1" } }, "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w=="], + + "onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "parse-filepath": ["parse-filepath@1.0.2", "", { "dependencies": { "is-absolute": "^1.0.0", "map-cache": "^0.2.0", "path-root": "^0.1.1" } }, "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q=="], + + "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + + "path-root": ["path-root@0.1.1", "", { "dependencies": { "path-root-regex": "^0.1.0" } }, "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg=="], + + "path-root-regex": ["path-root-regex@0.1.2", "", {}, "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ=="], + + "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], + + "pg": ["pg@8.23.1", "", { "dependencies": { "pg-connection-string": "^2.14.1", "pg-pool": "^3.14.0", "pg-protocol": "^1.16.1", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.4.1" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-aL96AHANtWjPLDOLqnhx+ngp9+UK7ETEU8VJrDCGvsSSi/mGLcWYsS6Herg7lmaBJe4uwrfqsa7gTEFaSizDoQ=="], + + "pg-cloudflare": ["pg-cloudflare@1.4.1", "", {}, "sha512-6PQbsFWZcp9EmJEwy5cGQ2La+AMWpP46lgbb8X+U/XsHIUweYDNCpeuKck5RxL2MdVFi7krbbEi5nX4Zh7JhrQ=="], + + "pg-connection-string": ["pg-connection-string@2.14.1", "", {}, "sha512-qR3kGNPBLpCNtz0evbKA0Y/MRFXwSSdT+pTJvYp/bXTcReZbvX1kzF0IyTc1QnxqF7AZbOeBhNL8R5mYQZV/MA=="], + + "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], + + "pg-pool": ["pg-pool@3.14.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw=="], + + "pg-protocol": ["pg-protocol@1.16.1", "", {}, "sha512-p9VOFMiHB/ZbJATetbg+99PxssTVSQRnyuPSQ67mN1+1KBOjZaZ83ZQzltnxPhJwSsC3nwVjJ10DVJlerbFzLg=="], + + "pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], + + "pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], + + "playwright": ["playwright@1.63.0", "", { "dependencies": { "playwright-core": "1.63.0" }, "bin": { "playwright": "cli.js" } }, "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg=="], + + "playwright-core": ["playwright-core@1.63.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg=="], + + "postcss": ["postcss@8.5.28", "", { "dependencies": { "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A=="], + + "postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], + + "postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="], + + "postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="], + + "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "react": ["react@19.3.0", "", {}, "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog=="], + + "react-dom": ["react-dom@19.3.0", "", { "dependencies": { "scheduler": "^0.28.0" }, "peerDependencies": { "react": "^19.3.0" } }, "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q=="], + + "remedial": ["remedial@1.0.8", "", {}, "sha512-/62tYiOe6DzS5BqVsNpH/nkGlX45C/Sp6V+NtiN6JQNS1Viay7cWkazmRkrQrdFj2eshDe96SIQNIoMxqhzBOg=="], + + "remove-trailing-separator": ["remove-trailing-separator@1.1.0", "", {}, "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw=="], + + "remove-trailing-spaces": ["remove-trailing-spaces@1.0.9", "", {}, "sha512-xzG7w5IRijvIkHIjDk65URsJJ7k4J95wmcArY5PRcmjldIOl7oTvG8+X2Ag690R7SfwiOcHrWZKVc1Pp5WIOzA=="], + + "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], + + "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], + + "rolldown": ["rolldown@1.2.12", "", { "dependencies": { "@oxc-project/types": "=0.152.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.12", "@rolldown/binding-android-arm64": "1.2.12", "@rolldown/binding-darwin-arm64": "1.2.12", "@rolldown/binding-darwin-x64": "1.2.12", "@rolldown/binding-freebsd-x64": "1.2.12", "@rolldown/binding-linux-arm-gnueabihf": "1.2.12", "@rolldown/binding-linux-arm64-gnu": "1.2.12", "@rolldown/binding-linux-arm64-musl": "1.2.12", "@rolldown/binding-linux-ppc64-gnu": "1.2.12", "@rolldown/binding-linux-s390x-gnu": "1.2.12", "@rolldown/binding-linux-x64-gnu": "1.2.12", "@rolldown/binding-linux-x64-musl": "1.2.12", "@rolldown/binding-openharmony-arm64": "1.2.12", "@rolldown/binding-win32-arm64-msvc": "1.2.12", "@rolldown/binding-win32-x64-msvc": "1.2.12" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-8wafseiaG80xmXSfqidUNqZcylTlhmPZZt+za2m+js2sFZ8dTNlhIOV2WcbIPx2hgwPBJpEUGFAMZ9bgBBLTSQ=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "scheduler": ["scheduler@0.28.0", "", {}, "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw=="], + + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "shell-quote": ["shell-quote@1.12.0", "", {}, "sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + + "slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="], + + "source-map-js": ["source-map-js@1.2.2", "", {}, "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw=="], + + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + + "sponge-case": ["sponge-case@2.0.3", "", {}, "sha512-i4h9ZGRfxV6Xw3mpZSFOfbXjf0cQcYmssGWutgNIfFZ2VM+YIWfD71N/kjjwK6X/AAHzBr+rciEcn/L34S8TGw=="], + + "string-env-interpolation": ["string-env-interpolation@1.0.1", "", {}, "sha512-78lwMoCcn0nNu8LszbP1UA7g55OeE4v7rCeWnM5B453rnNr4aq+5it3FEYtZrSEiMvHZOZ9Jlqb0OD0M2VInqg=="], + + "string-width": ["string-width@8.3.0", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ=="], + + "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + + "swap-case": ["swap-case@3.0.3", "", {}, "sha512-6p4op8wE9CQv7uDFzulI6YXUw4lD9n4oQierdbFThEKVWVQcbQcUjdP27W8XE7V4QnWmnq9jueSHceyyQnqQVA=="], + + "sync-fetch": ["sync-fetch@0.7.1", "", { "dependencies": { "node-fetch": "^3.3.2", "timeout-signal": "^2.0.0", "whatwg-mimetype": "^5.0.0" } }, "sha512-tTw2sPVgJb/ZO+Uz5Cs1haWg7aL0cw+DvD8mXos+ws5YaelM3IBZ50M717T1+RD5Qq0TdeZ/EI/wVMnAdJA0Eg=="], + + "timeout-signal": ["timeout-signal@2.0.0", "", {}, "sha512-YBGpG4bWsHoPvofT6y/5iqulfXIiIErl5B0LdtHT1mGXDFTAhhRrbUpTvBgYbovr+3cKblya2WAOcpoy90XguA=="], + + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + + "title-case": ["title-case@3.0.3", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "ts-log": ["ts-log@3.0.3", "", {}, "sha512-4uxVlS/cbNFeSmZbXgBCq+MRkbck1iW7B2Vqd+gziasf//uZP/HFI2IcoHlyYWFgFRl+5G5iqHe4RvY/h/qUxw=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], + + "unc-path-regex": ["unc-path-regex@0.1.2", "", {}, "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg=="], + + "undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="], + + "unixify": ["unixify@1.0.0", "", { "dependencies": { "normalize-path": "^2.1.1" } }, "sha512-6bc58dPYhCMHHuwxldQxO3RRNZ4eCogZ/st++0+fcC1nr0jiGUtAdBJ2qzmLQWSxbtz42pWt4QQMiZ9HvZf5cg=="], + + "update-browserslist-db": ["update-browserslist-db@1.3.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ=="], + + "urlpattern-polyfill": ["urlpattern-polyfill@10.1.0", "", {}, "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw=="], + + "urql": ["urql@5.0.4", "", { "dependencies": { "@urql/core": "^6.0.3", "wonka": "^6.3.2" }, "peerDependencies": { "react": ">= 16.8.0" } }, "sha512-OKNx8hUtt6RPSfsMsTsvas9rvMNN7BPMTvbXCwbiuO/mYa8Y+G4pxTzMJL7Sf1jXwR9gMpQheJ21LQy+nUjMkw=="], + + "vite": ["vite@8.3.2", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", "postcss": "^8.5.28", "rolldown": "~1.2.11", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.7.1", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-SQr1x6W5vVSbROg7vsyXIaxK9b0G7zsT68acdWWRmnBUsgDieLCRG+Rep9WdZgcposvv/GSnr4GUUBqB3vXq6w=="], + + "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], + + "whatwg-mimetype": ["whatwg-mimetype@5.0.0", "", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="], + + "wonka": ["wonka@6.3.6", "", {}, "sha512-MXH+6mDHAZ2GuMpgKS055FR6v0xVP3XwquxIMYXgiW+FejHQlMGlvVRZT4qMCxR+bEo/FCtIdKxwej9WV3YQag=="], + + "wrap-ansi": ["wrap-ansi@10.0.2", "", { "dependencies": { "ansi-styles": "^6.2.3", "string-width": "^8.2.0" } }, "sha512-6OLRNZqRntVGBm26ghZ5eZhCuWWyOkRCSn8XsOIuq5stpCE/tjLbdozd93naILZig0lmD8ANfRwnhn0DLeXGlg=="], + + "ws": ["ws@8.22.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg=="], + + "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "yaml": ["yaml@2.9.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw=="], + + "yargs": ["yargs@18.2.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^8.2.1", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w=="], + + "yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "yoctocolors": ["yoctocolors@2.2.0", "", {}, "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg=="], + + "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "@graphql-tools/apollo-engine-loader/@graphql-tools/utils": ["@graphql-tools/utils@12.0.3", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^2.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-M+04bHvI1SKoug7W2EkwfKjig2mdtgIIlFY8p2jLHmxPNVs5bC/3R+58JrD+7ZXWgPILWTwuqJ9AqTjPlahroA=="], + + "@graphql-tools/apollo-engine-loader/@whatwg-node/fetch": ["@whatwg-node/fetch@0.12.1", "", { "dependencies": { "@whatwg-node/node-fetch": "^0.9.1", "urlpattern-polyfill": "^10.0.0" } }, "sha512-MjSXerFVxhWwPXZMNp4b4Q3WZMEIqzinHVWuH5tF2LNGy4hHypqPJCNpaR6bEZbaGdlH+r5dRAmQ8bxowcetjQ=="], + + "@graphql-tools/batch-execute/@graphql-tools/utils": ["@graphql-tools/utils@12.0.3", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^2.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-M+04bHvI1SKoug7W2EkwfKjig2mdtgIIlFY8p2jLHmxPNVs5bC/3R+58JrD+7ZXWgPILWTwuqJ9AqTjPlahroA=="], + + "@graphql-tools/code-file-loader/@graphql-tools/utils": ["@graphql-tools/utils@12.0.3", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^2.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-M+04bHvI1SKoug7W2EkwfKjig2mdtgIIlFY8p2jLHmxPNVs5bC/3R+58JrD+7ZXWgPILWTwuqJ9AqTjPlahroA=="], + + "@graphql-tools/delegate/@graphql-tools/utils": ["@graphql-tools/utils@12.0.3", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^2.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-M+04bHvI1SKoug7W2EkwfKjig2mdtgIIlFY8p2jLHmxPNVs5bC/3R+58JrD+7ZXWgPILWTwuqJ9AqTjPlahroA=="], + + "@graphql-tools/executor/@graphql-tools/utils": ["@graphql-tools/utils@12.0.3", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^2.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-M+04bHvI1SKoug7W2EkwfKjig2mdtgIIlFY8p2jLHmxPNVs5bC/3R+58JrD+7ZXWgPILWTwuqJ9AqTjPlahroA=="], + + "@graphql-tools/executor/@whatwg-node/promise-helpers": ["@whatwg-node/promise-helpers@2.0.0", "", { "dependencies": { "tslib": "^2.6.3" } }, "sha512-/i9CS0Z96o+cPwaYoAyVxAThDiUX1wV3tDmNWPJbn+vtdR+JftuCDg1NGCXBrzIYVDGkJ8uT6ctwmrBOUzOpKQ=="], + + "@graphql-tools/executor-common/@graphql-tools/utils": ["@graphql-tools/utils@12.0.3", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^2.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-M+04bHvI1SKoug7W2EkwfKjig2mdtgIIlFY8p2jLHmxPNVs5bC/3R+58JrD+7ZXWgPILWTwuqJ9AqTjPlahroA=="], + + "@graphql-tools/executor-graphql-ws/@graphql-tools/utils": ["@graphql-tools/utils@12.0.3", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^2.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-M+04bHvI1SKoug7W2EkwfKjig2mdtgIIlFY8p2jLHmxPNVs5bC/3R+58JrD+7ZXWgPILWTwuqJ9AqTjPlahroA=="], + + "@graphql-tools/executor-graphql-ws/@whatwg-node/disposablestack": ["@whatwg-node/disposablestack@0.0.6", "", { "dependencies": { "@whatwg-node/promise-helpers": "^1.0.0", "tslib": "^2.6.3" } }, "sha512-LOtTn+JgJvX8WfBVJtF08TGrdjuFzGJc4mkP8EdDI8ADbvO7kiexYep1o8dwnt0okb0jYclCDXF13xU7Ge4zSw=="], + + "@graphql-tools/executor-http/@graphql-tools/utils": ["@graphql-tools/utils@12.0.3", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^2.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-M+04bHvI1SKoug7W2EkwfKjig2mdtgIIlFY8p2jLHmxPNVs5bC/3R+58JrD+7ZXWgPILWTwuqJ9AqTjPlahroA=="], + + "@graphql-tools/executor-http/@whatwg-node/disposablestack": ["@whatwg-node/disposablestack@0.0.6", "", { "dependencies": { "@whatwg-node/promise-helpers": "^1.0.0", "tslib": "^2.6.3" } }, "sha512-LOtTn+JgJvX8WfBVJtF08TGrdjuFzGJc4mkP8EdDI8ADbvO7kiexYep1o8dwnt0okb0jYclCDXF13xU7Ge4zSw=="], + + "@graphql-tools/executor-legacy-ws/@graphql-tools/utils": ["@graphql-tools/utils@12.0.3", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^2.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-M+04bHvI1SKoug7W2EkwfKjig2mdtgIIlFY8p2jLHmxPNVs5bC/3R+58JrD+7ZXWgPILWTwuqJ9AqTjPlahroA=="], + + "@graphql-tools/git-loader/@graphql-tools/utils": ["@graphql-tools/utils@12.0.3", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^2.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-M+04bHvI1SKoug7W2EkwfKjig2mdtgIIlFY8p2jLHmxPNVs5bC/3R+58JrD+7ZXWgPILWTwuqJ9AqTjPlahroA=="], + + "@graphql-tools/github-loader/@graphql-tools/utils": ["@graphql-tools/utils@12.0.3", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^2.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-M+04bHvI1SKoug7W2EkwfKjig2mdtgIIlFY8p2jLHmxPNVs5bC/3R+58JrD+7ZXWgPILWTwuqJ9AqTjPlahroA=="], + + "@graphql-tools/github-loader/@whatwg-node/fetch": ["@whatwg-node/fetch@0.12.1", "", { "dependencies": { "@whatwg-node/node-fetch": "^0.9.1", "urlpattern-polyfill": "^10.0.0" } }, "sha512-MjSXerFVxhWwPXZMNp4b4Q3WZMEIqzinHVWuH5tF2LNGy4hHypqPJCNpaR6bEZbaGdlH+r5dRAmQ8bxowcetjQ=="], + + "@graphql-tools/github-loader/@whatwg-node/promise-helpers": ["@whatwg-node/promise-helpers@2.0.0", "", { "dependencies": { "tslib": "^2.6.3" } }, "sha512-/i9CS0Z96o+cPwaYoAyVxAThDiUX1wV3tDmNWPJbn+vtdR+JftuCDg1NGCXBrzIYVDGkJ8uT6ctwmrBOUzOpKQ=="], + + "@graphql-tools/graphql-file-loader/@graphql-tools/utils": ["@graphql-tools/utils@12.0.3", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^2.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-M+04bHvI1SKoug7W2EkwfKjig2mdtgIIlFY8p2jLHmxPNVs5bC/3R+58JrD+7ZXWgPILWTwuqJ9AqTjPlahroA=="], + + "@graphql-tools/graphql-tag-pluck/@graphql-tools/utils": ["@graphql-tools/utils@12.0.3", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^2.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-M+04bHvI1SKoug7W2EkwfKjig2mdtgIIlFY8p2jLHmxPNVs5bC/3R+58JrD+7ZXWgPILWTwuqJ9AqTjPlahroA=="], + + "@graphql-tools/import/@graphql-tools/utils": ["@graphql-tools/utils@12.0.3", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^2.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-M+04bHvI1SKoug7W2EkwfKjig2mdtgIIlFY8p2jLHmxPNVs5bC/3R+58JrD+7ZXWgPILWTwuqJ9AqTjPlahroA=="], + + "@graphql-tools/json-file-loader/@graphql-tools/utils": ["@graphql-tools/utils@12.0.3", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^2.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-M+04bHvI1SKoug7W2EkwfKjig2mdtgIIlFY8p2jLHmxPNVs5bC/3R+58JrD+7ZXWgPILWTwuqJ9AqTjPlahroA=="], + + "@graphql-tools/load/@graphql-tools/utils": ["@graphql-tools/utils@12.0.3", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^2.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-M+04bHvI1SKoug7W2EkwfKjig2mdtgIIlFY8p2jLHmxPNVs5bC/3R+58JrD+7ZXWgPILWTwuqJ9AqTjPlahroA=="], + + "@graphql-tools/merge/@graphql-tools/utils": ["@graphql-tools/utils@12.0.3", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^2.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-M+04bHvI1SKoug7W2EkwfKjig2mdtgIIlFY8p2jLHmxPNVs5bC/3R+58JrD+7ZXWgPILWTwuqJ9AqTjPlahroA=="], + + "@graphql-tools/relay-operation-optimizer/@graphql-tools/utils": ["@graphql-tools/utils@12.0.3", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^2.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-M+04bHvI1SKoug7W2EkwfKjig2mdtgIIlFY8p2jLHmxPNVs5bC/3R+58JrD+7ZXWgPILWTwuqJ9AqTjPlahroA=="], + + "@graphql-tools/schema/@graphql-tools/utils": ["@graphql-tools/utils@12.0.3", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^2.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-M+04bHvI1SKoug7W2EkwfKjig2mdtgIIlFY8p2jLHmxPNVs5bC/3R+58JrD+7ZXWgPILWTwuqJ9AqTjPlahroA=="], + + "@graphql-tools/url-loader/@graphql-tools/utils": ["@graphql-tools/utils@12.0.3", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^2.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-M+04bHvI1SKoug7W2EkwfKjig2mdtgIIlFY8p2jLHmxPNVs5bC/3R+58JrD+7ZXWgPILWTwuqJ9AqTjPlahroA=="], + + "@graphql-tools/url-loader/@whatwg-node/fetch": ["@whatwg-node/fetch@0.12.1", "", { "dependencies": { "@whatwg-node/node-fetch": "^0.9.1", "urlpattern-polyfill": "^10.0.0" } }, "sha512-MjSXerFVxhWwPXZMNp4b4Q3WZMEIqzinHVWuH5tF2LNGy4hHypqPJCNpaR6bEZbaGdlH+r5dRAmQ8bxowcetjQ=="], + + "@graphql-tools/url-loader/@whatwg-node/promise-helpers": ["@whatwg-node/promise-helpers@2.0.0", "", { "dependencies": { "tslib": "^2.6.3" } }, "sha512-/i9CS0Z96o+cPwaYoAyVxAThDiUX1wV3tDmNWPJbn+vtdR+JftuCDg1NGCXBrzIYVDGkJ8uT6ctwmrBOUzOpKQ=="], + + "@graphql-tools/wrap/@graphql-tools/utils": ["@graphql-tools/utils@12.0.3", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^2.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-M+04bHvI1SKoug7W2EkwfKjig2mdtgIIlFY8p2jLHmxPNVs5bC/3R+58JrD+7ZXWgPILWTwuqJ9AqTjPlahroA=="], + + "@whatwg-node/disposablestack/@whatwg-node/promise-helpers": ["@whatwg-node/promise-helpers@2.0.0", "", { "dependencies": { "tslib": "^2.6.3" } }, "sha512-/i9CS0Z96o+cPwaYoAyVxAThDiUX1wV3tDmNWPJbn+vtdR+JftuCDg1NGCXBrzIYVDGkJ8uT6ctwmrBOUzOpKQ=="], + + "@whatwg-node/node-fetch/@whatwg-node/disposablestack": ["@whatwg-node/disposablestack@0.0.6", "", { "dependencies": { "@whatwg-node/promise-helpers": "^1.0.0", "tslib": "^2.6.3" } }, "sha512-LOtTn+JgJvX8WfBVJtF08TGrdjuFzGJc4mkP8EdDI8ADbvO7kiexYep1o8dwnt0okb0jYclCDXF13xU7Ge4zSw=="], + + "@whatwg-node/server/@whatwg-node/disposablestack": ["@whatwg-node/disposablestack@0.0.6", "", { "dependencies": { "@whatwg-node/promise-helpers": "^1.0.0", "tslib": "^2.6.3" } }, "sha512-LOtTn+JgJvX8WfBVJtF08TGrdjuFzGJc4mkP8EdDI8ADbvO7kiexYep1o8dwnt0okb0jYclCDXF13xU7Ge4zSw=="], + + "cliui/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "cliui/wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], + + "graphql-config/cosmiconfig": ["cosmiconfig@8.3.6", "", { "dependencies": { "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0", "path-type": "^4.0.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA=="], + + "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "log-update/slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="], + + "log-update/wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], + + "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + + "@graphql-tools/apollo-engine-loader/@graphql-tools/utils/@whatwg-node/promise-helpers": ["@whatwg-node/promise-helpers@2.0.0", "", { "dependencies": { "tslib": "^2.6.3" } }, "sha512-/i9CS0Z96o+cPwaYoAyVxAThDiUX1wV3tDmNWPJbn+vtdR+JftuCDg1NGCXBrzIYVDGkJ8uT6ctwmrBOUzOpKQ=="], + + "@graphql-tools/apollo-engine-loader/@whatwg-node/fetch/@whatwg-node/node-fetch": ["@whatwg-node/node-fetch@0.9.1", "", { "dependencies": { "@fastify/busboy": "^3.1.1", "@whatwg-node/disposablestack": "^0.1.0", "@whatwg-node/promise-helpers": "^2.0.0", "tslib": "^2.6.3" } }, "sha512-LX/1X2QEHupCz6YXRjefZRoOsycCGOIJdXtQPVVxhzPQ/CpWpWFaXl53ShqLystvLZSvtUDKLhCA/yLveHA/LA=="], + + "@graphql-tools/batch-execute/@graphql-tools/utils/@whatwg-node/promise-helpers": ["@whatwg-node/promise-helpers@2.0.0", "", { "dependencies": { "tslib": "^2.6.3" } }, "sha512-/i9CS0Z96o+cPwaYoAyVxAThDiUX1wV3tDmNWPJbn+vtdR+JftuCDg1NGCXBrzIYVDGkJ8uT6ctwmrBOUzOpKQ=="], + + "@graphql-tools/code-file-loader/@graphql-tools/utils/@whatwg-node/promise-helpers": ["@whatwg-node/promise-helpers@2.0.0", "", { "dependencies": { "tslib": "^2.6.3" } }, "sha512-/i9CS0Z96o+cPwaYoAyVxAThDiUX1wV3tDmNWPJbn+vtdR+JftuCDg1NGCXBrzIYVDGkJ8uT6ctwmrBOUzOpKQ=="], + + "@graphql-tools/delegate/@graphql-tools/utils/@whatwg-node/promise-helpers": ["@whatwg-node/promise-helpers@2.0.0", "", { "dependencies": { "tslib": "^2.6.3" } }, "sha512-/i9CS0Z96o+cPwaYoAyVxAThDiUX1wV3tDmNWPJbn+vtdR+JftuCDg1NGCXBrzIYVDGkJ8uT6ctwmrBOUzOpKQ=="], + + "@graphql-tools/executor-common/@graphql-tools/utils/@whatwg-node/promise-helpers": ["@whatwg-node/promise-helpers@2.0.0", "", { "dependencies": { "tslib": "^2.6.3" } }, "sha512-/i9CS0Z96o+cPwaYoAyVxAThDiUX1wV3tDmNWPJbn+vtdR+JftuCDg1NGCXBrzIYVDGkJ8uT6ctwmrBOUzOpKQ=="], + + "@graphql-tools/executor-graphql-ws/@graphql-tools/utils/@whatwg-node/promise-helpers": ["@whatwg-node/promise-helpers@2.0.0", "", { "dependencies": { "tslib": "^2.6.3" } }, "sha512-/i9CS0Z96o+cPwaYoAyVxAThDiUX1wV3tDmNWPJbn+vtdR+JftuCDg1NGCXBrzIYVDGkJ8uT6ctwmrBOUzOpKQ=="], + + "@graphql-tools/executor-http/@graphql-tools/utils/@whatwg-node/promise-helpers": ["@whatwg-node/promise-helpers@2.0.0", "", { "dependencies": { "tslib": "^2.6.3" } }, "sha512-/i9CS0Z96o+cPwaYoAyVxAThDiUX1wV3tDmNWPJbn+vtdR+JftuCDg1NGCXBrzIYVDGkJ8uT6ctwmrBOUzOpKQ=="], + + "@graphql-tools/executor-legacy-ws/@graphql-tools/utils/@whatwg-node/promise-helpers": ["@whatwg-node/promise-helpers@2.0.0", "", { "dependencies": { "tslib": "^2.6.3" } }, "sha512-/i9CS0Z96o+cPwaYoAyVxAThDiUX1wV3tDmNWPJbn+vtdR+JftuCDg1NGCXBrzIYVDGkJ8uT6ctwmrBOUzOpKQ=="], + + "@graphql-tools/git-loader/@graphql-tools/utils/@whatwg-node/promise-helpers": ["@whatwg-node/promise-helpers@2.0.0", "", { "dependencies": { "tslib": "^2.6.3" } }, "sha512-/i9CS0Z96o+cPwaYoAyVxAThDiUX1wV3tDmNWPJbn+vtdR+JftuCDg1NGCXBrzIYVDGkJ8uT6ctwmrBOUzOpKQ=="], + + "@graphql-tools/github-loader/@whatwg-node/fetch/@whatwg-node/node-fetch": ["@whatwg-node/node-fetch@0.9.1", "", { "dependencies": { "@fastify/busboy": "^3.1.1", "@whatwg-node/disposablestack": "^0.1.0", "@whatwg-node/promise-helpers": "^2.0.0", "tslib": "^2.6.3" } }, "sha512-LX/1X2QEHupCz6YXRjefZRoOsycCGOIJdXtQPVVxhzPQ/CpWpWFaXl53ShqLystvLZSvtUDKLhCA/yLveHA/LA=="], + + "@graphql-tools/graphql-file-loader/@graphql-tools/utils/@whatwg-node/promise-helpers": ["@whatwg-node/promise-helpers@2.0.0", "", { "dependencies": { "tslib": "^2.6.3" } }, "sha512-/i9CS0Z96o+cPwaYoAyVxAThDiUX1wV3tDmNWPJbn+vtdR+JftuCDg1NGCXBrzIYVDGkJ8uT6ctwmrBOUzOpKQ=="], + + "@graphql-tools/graphql-tag-pluck/@graphql-tools/utils/@whatwg-node/promise-helpers": ["@whatwg-node/promise-helpers@2.0.0", "", { "dependencies": { "tslib": "^2.6.3" } }, "sha512-/i9CS0Z96o+cPwaYoAyVxAThDiUX1wV3tDmNWPJbn+vtdR+JftuCDg1NGCXBrzIYVDGkJ8uT6ctwmrBOUzOpKQ=="], + + "@graphql-tools/import/@graphql-tools/utils/@whatwg-node/promise-helpers": ["@whatwg-node/promise-helpers@2.0.0", "", { "dependencies": { "tslib": "^2.6.3" } }, "sha512-/i9CS0Z96o+cPwaYoAyVxAThDiUX1wV3tDmNWPJbn+vtdR+JftuCDg1NGCXBrzIYVDGkJ8uT6ctwmrBOUzOpKQ=="], + + "@graphql-tools/json-file-loader/@graphql-tools/utils/@whatwg-node/promise-helpers": ["@whatwg-node/promise-helpers@2.0.0", "", { "dependencies": { "tslib": "^2.6.3" } }, "sha512-/i9CS0Z96o+cPwaYoAyVxAThDiUX1wV3tDmNWPJbn+vtdR+JftuCDg1NGCXBrzIYVDGkJ8uT6ctwmrBOUzOpKQ=="], + + "@graphql-tools/load/@graphql-tools/utils/@whatwg-node/promise-helpers": ["@whatwg-node/promise-helpers@2.0.0", "", { "dependencies": { "tslib": "^2.6.3" } }, "sha512-/i9CS0Z96o+cPwaYoAyVxAThDiUX1wV3tDmNWPJbn+vtdR+JftuCDg1NGCXBrzIYVDGkJ8uT6ctwmrBOUzOpKQ=="], + + "@graphql-tools/merge/@graphql-tools/utils/@whatwg-node/promise-helpers": ["@whatwg-node/promise-helpers@2.0.0", "", { "dependencies": { "tslib": "^2.6.3" } }, "sha512-/i9CS0Z96o+cPwaYoAyVxAThDiUX1wV3tDmNWPJbn+vtdR+JftuCDg1NGCXBrzIYVDGkJ8uT6ctwmrBOUzOpKQ=="], + + "@graphql-tools/relay-operation-optimizer/@graphql-tools/utils/@whatwg-node/promise-helpers": ["@whatwg-node/promise-helpers@2.0.0", "", { "dependencies": { "tslib": "^2.6.3" } }, "sha512-/i9CS0Z96o+cPwaYoAyVxAThDiUX1wV3tDmNWPJbn+vtdR+JftuCDg1NGCXBrzIYVDGkJ8uT6ctwmrBOUzOpKQ=="], + + "@graphql-tools/schema/@graphql-tools/utils/@whatwg-node/promise-helpers": ["@whatwg-node/promise-helpers@2.0.0", "", { "dependencies": { "tslib": "^2.6.3" } }, "sha512-/i9CS0Z96o+cPwaYoAyVxAThDiUX1wV3tDmNWPJbn+vtdR+JftuCDg1NGCXBrzIYVDGkJ8uT6ctwmrBOUzOpKQ=="], + + "@graphql-tools/url-loader/@whatwg-node/fetch/@whatwg-node/node-fetch": ["@whatwg-node/node-fetch@0.9.1", "", { "dependencies": { "@fastify/busboy": "^3.1.1", "@whatwg-node/disposablestack": "^0.1.0", "@whatwg-node/promise-helpers": "^2.0.0", "tslib": "^2.6.3" } }, "sha512-LX/1X2QEHupCz6YXRjefZRoOsycCGOIJdXtQPVVxhzPQ/CpWpWFaXl53ShqLystvLZSvtUDKLhCA/yLveHA/LA=="], + + "@graphql-tools/wrap/@graphql-tools/utils/@whatwg-node/promise-helpers": ["@whatwg-node/promise-helpers@2.0.0", "", { "dependencies": { "tslib": "^2.6.3" } }, "sha512-/i9CS0Z96o+cPwaYoAyVxAThDiUX1wV3tDmNWPJbn+vtdR+JftuCDg1NGCXBrzIYVDGkJ8uT6ctwmrBOUzOpKQ=="], + + "log-update/wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "@graphql-tools/apollo-engine-loader/@whatwg-node/fetch/@whatwg-node/node-fetch/@whatwg-node/promise-helpers": ["@whatwg-node/promise-helpers@2.0.0", "", { "dependencies": { "tslib": "^2.6.3" } }, "sha512-/i9CS0Z96o+cPwaYoAyVxAThDiUX1wV3tDmNWPJbn+vtdR+JftuCDg1NGCXBrzIYVDGkJ8uT6ctwmrBOUzOpKQ=="], + } +} diff --git a/applications/product-graphql/codegen.ts b/applications/product-graphql/codegen.ts new file mode 100644 index 00000000..4672d760 --- /dev/null +++ b/applications/product-graphql/codegen.ts @@ -0,0 +1,13 @@ +import type { CodegenConfig } from "@graphql-codegen/cli"; +const config: CodegenConfig = { + schema: "schema.graphql", + documents: ["src/client/operations.graphql"], + generates: { + "src/client/gql/": { + preset: "client", + presetConfig: { fragmentMasking: false }, + config: { useTypeImports: true }, + }, + }, +}; +export default config; diff --git a/applications/product-graphql/docs/product-graphql.png b/applications/product-graphql/docs/product-graphql.png new file mode 100644 index 00000000..2d4b823f Binary files /dev/null and b/applications/product-graphql/docs/product-graphql.png differ diff --git a/applications/product-graphql/index.html b/applications/product-graphql/index.html new file mode 100644 index 00000000..81fceb53 --- /dev/null +++ b/applications/product-graphql/index.html @@ -0,0 +1 @@ +Collection studio
diff --git a/applications/product-graphql/package.json b/applications/product-graphql/package.json new file mode 100644 index 00000000..bd86896c --- /dev/null +++ b/applications/product-graphql/package.json @@ -0,0 +1,38 @@ +{ + "name": "product-graphql", + "private": true, + "type": "module", + "scripts": { + "schema": "bun scripts/schema.ts", + "codegen": "bun run schema && graphql-codegen --config codegen.ts", + "typecheck": "tsc --noEmit", + "build": "vite build", + "start": "bun src/server/index.ts", + "test": "bun test tests/validation.test.ts", + "test:integration": "bun test tests/database.test.ts", + "test:browser": "playwright test" + }, + "dependencies": { + "@pothos/core": "4.15.1", + "graphql-yoga": "5.24.1", + "graphql": "16.12.0", + "drizzle-orm": "0.45.3", + "pg": "8.23.1", + "dataloader": "2.2.3", + "urql": "5.0.4", + "react": "19.3.0", + "react-dom": "19.3.0" + }, + "devDependencies": { + "@graphql-codegen/cli": "7.4.3", + "@graphql-codegen/client-preset": "6.2.0", + "@types/pg": "8.23.1", + "@types/bun": "1.4.2", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "@vitejs/plugin-react": "6.1.1", + "vite": "8.3.2", + "typescript": "7.0.2", + "@playwright/test": "1.63.0" + } +} diff --git a/applications/product-graphql/playwright.config.ts b/applications/product-graphql/playwright.config.ts new file mode 100644 index 00000000..5a776e73 --- /dev/null +++ b/applications/product-graphql/playwright.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "@playwright/test"; +export default defineConfig({ + testDir: "tests/browser", + use: { + baseURL: "http://127.0.0.1:3001", + viewport: { width: 1440, height: 1080 }, + }, + workers: 1, + timeout: 30000, +}); diff --git a/applications/product-graphql/schema.graphql b/applications/product-graphql/schema.graphql new file mode 100644 index 00000000..f716becd --- /dev/null +++ b/applications/product-graphql/schema.graphql @@ -0,0 +1,42 @@ +type Collection { + id: ID! + products: [Product!]! + revision: Int! + title: String! +} + +type CollectionSave { + collection: Collection + status: SaveStatus! +} + +type Mutation { + createCollection(productIds: [ID!]!, title: String!): Collection! + replaceCollection(baseRevision: Int!, id: ID!, productIds: [ID!]!): CollectionSave! +} + +type Product { + category: String! + description: String! + id: ID! + name: String! + variants: [Variant!]! +} + +type Query { + collection(id: ID!): Collection + collections: [Collection!]! + products(category: String, first: Int!, offset: Int!, search: String): [Product!]! +} + +enum SaveStatus { + CONFLICT + OK +} + +type Variant { + id: ID! + name: String! + priceCents: Int! + sku: String! +} diff --git a/applications/product-graphql/scripts/schema.ts b/applications/product-graphql/scripts/schema.ts new file mode 100644 index 00000000..6a74d1a3 --- /dev/null +++ b/applications/product-graphql/scripts/schema.ts @@ -0,0 +1,3 @@ +import { printSchema } from "graphql"; +import { schema } from "../src/server/graphql"; +await Bun.write("schema.graphql", printSchema(schema) + "\n"); diff --git a/applications/product-graphql/sql/001_roles.sql b/applications/product-graphql/sql/001_roles.sql new file mode 100644 index 00000000..d7ad0926 --- /dev/null +++ b/applications/product-graphql/sql/001_roles.sql @@ -0,0 +1,10 @@ +\set ON_ERROR_STOP on +\getenv migration_password PG_MIGRATION_PASSWORD +\getenv app_password PG_APP_PASSWORD +CREATE ROLE catalog_owner NOLOGIN; +CREATE ROLE catalog_migration LOGIN PASSWORD :'migration_password'; +CREATE ROLE catalog_app LOGIN PASSWORD :'app_password'; +GRANT catalog_owner TO catalog_migration; +CREATE SCHEMA product_catalog AUTHORIZATION catalog_owner; +REVOKE ALL ON SCHEMA product_catalog FROM PUBLIC; +GRANT USAGE ON SCHEMA product_catalog TO catalog_app; diff --git a/applications/product-graphql/sql/002_schema.sql b/applications/product-graphql/sql/002_schema.sql new file mode 100644 index 00000000..424f7f62 --- /dev/null +++ b/applications/product-graphql/sql/002_schema.sql @@ -0,0 +1,12 @@ +\set ON_ERROR_STOP on +BEGIN; +SET LOCAL ROLE catalog_owner; +CREATE TABLE product_catalog.products(id UUID PRIMARY KEY,name TEXT NOT NULL CHECK(length(name) BETWEEN 1 AND 80),category TEXT NOT NULL CHECK(category IN ('Desk','Travel','Lighting')),description TEXT NOT NULL); +CREATE TABLE product_catalog.variants(id UUID PRIMARY KEY,product_id UUID NOT NULL REFERENCES product_catalog.products(id),name TEXT NOT NULL,sku TEXT NOT NULL UNIQUE,price_cents INTEGER NOT NULL CHECK(price_cents BETWEEN 1 AND 1000000),position INTEGER NOT NULL CHECK(position BETWEEN 0 AND 2),UNIQUE(product_id,position)); +CREATE TABLE product_catalog.collections(id UUID PRIMARY KEY,title TEXT NOT NULL CHECK(length(btrim(title)) BETWEEN 1 AND 60),revision INTEGER NOT NULL CHECK(revision>0)); +CREATE TABLE product_catalog.collection_items(collection_id UUID NOT NULL REFERENCES product_catalog.collections(id) ON DELETE CASCADE,product_id UUID NOT NULL REFERENCES product_catalog.products(id),position INTEGER NOT NULL CHECK(position BETWEEN 0 AND 11),PRIMARY KEY(collection_id,product_id),UNIQUE(collection_id,position)); +GRANT SELECT ON ALL TABLES IN SCHEMA product_catalog TO catalog_app; +GRANT INSERT ON product_catalog.collections,product_catalog.collection_items TO catalog_app; +GRANT UPDATE(revision) ON product_catalog.collections TO catalog_app; +GRANT DELETE ON product_catalog.collection_items TO catalog_app; +COMMIT; diff --git a/applications/product-graphql/sql/003_seed.sql b/applications/product-graphql/sql/003_seed.sql new file mode 100644 index 00000000..887c9fd8 --- /dev/null +++ b/applications/product-graphql/sql/003_seed.sql @@ -0,0 +1,106 @@ +\set ON_ERROR_STOP on +BEGIN; +SET LOCAL ROLE catalog_owner; +INSERT INTO product_catalog.products(id,name,category,description) VALUES +('10000000-0000-4000-8000-000000000001','Arc desk lamp','Lighting','A thoughtful everyday object, made for a slower rhythm.'), +('10000000-0000-4000-8000-000000000002','Canvas weekender','Travel','A thoughtful everyday object, made for a slower rhythm.'), +('10000000-0000-4000-8000-000000000003','Ceramic pen cup','Desk','A thoughtful everyday object, made for a slower rhythm.'), +('10000000-0000-4000-8000-000000000004','Compact field bag','Travel','A thoughtful everyday object, made for a slower rhythm.'), +('10000000-0000-4000-8000-000000000005','Cork desk mat','Desk','A thoughtful everyday object, made for a slower rhythm.'), +('10000000-0000-4000-8000-000000000006','Dawn task light','Lighting','A thoughtful everyday object, made for a slower rhythm.'), +('10000000-0000-4000-8000-000000000007','Felt organiser','Desk','A thoughtful everyday object, made for a slower rhythm.'), +('10000000-0000-4000-8000-000000000008','Fold travel pouch','Travel','A thoughtful everyday object, made for a slower rhythm.'), +('10000000-0000-4000-8000-000000000009','Frame bookend','Desk','A thoughtful everyday object, made for a slower rhythm.'), +('10000000-0000-4000-8000-000000000010','Harbour lantern','Lighting','A thoughtful everyday object, made for a slower rhythm.'), +('10000000-0000-4000-8000-000000000011','Linen cable wrap','Desk','A thoughtful everyday object, made for a slower rhythm.'), +('10000000-0000-4000-8000-000000000012','Loop key case','Travel','A thoughtful everyday object, made for a slower rhythm.'), +('10000000-0000-4000-8000-000000000013','Maple tray','Desk','A thoughtful everyday object, made for a slower rhythm.'), +('10000000-0000-4000-8000-000000000014','Mesh travel kit','Travel','A thoughtful everyday object, made for a slower rhythm.'), +('10000000-0000-4000-8000-000000000015','Orbit reading lamp','Lighting','A thoughtful everyday object, made for a slower rhythm.'), +('10000000-0000-4000-8000-000000000016','Pocket notebook','Travel','A thoughtful everyday object, made for a slower rhythm.'), +('10000000-0000-4000-8000-000000000017','Porcelain task light','Lighting','A thoughtful everyday object, made for a slower rhythm.'), +('10000000-0000-4000-8000-000000000018','Ridge card holder','Travel','A thoughtful everyday object, made for a slower rhythm.'), +('10000000-0000-4000-8000-000000000019','Slate desk stand','Desk','A thoughtful everyday object, made for a slower rhythm.'), +('10000000-0000-4000-8000-000000000020','Soft packing cube','Travel','A thoughtful everyday object, made for a slower rhythm.'), +('10000000-0000-4000-8000-000000000021','Stem wall light','Lighting','A thoughtful everyday object, made for a slower rhythm.'), +('10000000-0000-4000-8000-000000000022','Studio ruler','Desk','A thoughtful everyday object, made for a slower rhythm.'), +('10000000-0000-4000-8000-000000000023','Terra table lamp','Lighting','A thoughtful everyday object, made for a slower rhythm.'), +('10000000-0000-4000-8000-000000000024','Woven document sleeve','Desk','A thoughtful everyday object, made for a slower rhythm.') +ON CONFLICT DO NOTHING; +INSERT INTO product_catalog.variants(id,product_id,name,sku,price_cents,position) VALUES +('20000000-0000-4000-8000-000000000010','10000000-0000-4000-8000-000000000001','Natural','STUDIO-01-0',2625,0), +('20000000-0000-4000-8000-000000000011','10000000-0000-4000-8000-000000000001','Moss','STUDIO-01-1',2825,1), +('20000000-0000-4000-8000-000000000012','10000000-0000-4000-8000-000000000001','Ink','STUDIO-01-2',3025,2), +('20000000-0000-4000-8000-000000000020','10000000-0000-4000-8000-000000000002','Natural','STUDIO-02-0',2750,0), +('20000000-0000-4000-8000-000000000021','10000000-0000-4000-8000-000000000002','Moss','STUDIO-02-1',2950,1), +('20000000-0000-4000-8000-000000000022','10000000-0000-4000-8000-000000000002','Ink','STUDIO-02-2',3150,2), +('20000000-0000-4000-8000-000000000030','10000000-0000-4000-8000-000000000003','Natural','STUDIO-03-0',2875,0), +('20000000-0000-4000-8000-000000000031','10000000-0000-4000-8000-000000000003','Moss','STUDIO-03-1',3075,1), +('20000000-0000-4000-8000-000000000032','10000000-0000-4000-8000-000000000003','Ink','STUDIO-03-2',3275,2), +('20000000-0000-4000-8000-000000000040','10000000-0000-4000-8000-000000000004','Natural','STUDIO-04-0',3000,0), +('20000000-0000-4000-8000-000000000041','10000000-0000-4000-8000-000000000004','Moss','STUDIO-04-1',3200,1), +('20000000-0000-4000-8000-000000000042','10000000-0000-4000-8000-000000000004','Ink','STUDIO-04-2',3400,2), +('20000000-0000-4000-8000-000000000050','10000000-0000-4000-8000-000000000005','Natural','STUDIO-05-0',3125,0), +('20000000-0000-4000-8000-000000000051','10000000-0000-4000-8000-000000000005','Moss','STUDIO-05-1',3325,1), +('20000000-0000-4000-8000-000000000052','10000000-0000-4000-8000-000000000005','Ink','STUDIO-05-2',3525,2), +('20000000-0000-4000-8000-000000000060','10000000-0000-4000-8000-000000000006','Natural','STUDIO-06-0',3250,0), +('20000000-0000-4000-8000-000000000061','10000000-0000-4000-8000-000000000006','Moss','STUDIO-06-1',3450,1), +('20000000-0000-4000-8000-000000000062','10000000-0000-4000-8000-000000000006','Ink','STUDIO-06-2',3650,2), +('20000000-0000-4000-8000-000000000070','10000000-0000-4000-8000-000000000007','Natural','STUDIO-07-0',3375,0), +('20000000-0000-4000-8000-000000000071','10000000-0000-4000-8000-000000000007','Moss','STUDIO-07-1',3575,1), +('20000000-0000-4000-8000-000000000072','10000000-0000-4000-8000-000000000007','Ink','STUDIO-07-2',3775,2), +('20000000-0000-4000-8000-000000000080','10000000-0000-4000-8000-000000000008','Natural','STUDIO-08-0',3500,0), +('20000000-0000-4000-8000-000000000081','10000000-0000-4000-8000-000000000008','Moss','STUDIO-08-1',3700,1), +('20000000-0000-4000-8000-000000000082','10000000-0000-4000-8000-000000000008','Ink','STUDIO-08-2',3900,2), +('20000000-0000-4000-8000-000000000090','10000000-0000-4000-8000-000000000009','Natural','STUDIO-09-0',3625,0), +('20000000-0000-4000-8000-000000000091','10000000-0000-4000-8000-000000000009','Moss','STUDIO-09-1',3825,1), +('20000000-0000-4000-8000-000000000092','10000000-0000-4000-8000-000000000009','Ink','STUDIO-09-2',4025,2), +('20000000-0000-4000-8000-000000000100','10000000-0000-4000-8000-000000000010','Natural','STUDIO-10-0',3750,0), +('20000000-0000-4000-8000-000000000101','10000000-0000-4000-8000-000000000010','Moss','STUDIO-10-1',3950,1), +('20000000-0000-4000-8000-000000000102','10000000-0000-4000-8000-000000000010','Ink','STUDIO-10-2',4150,2), +('20000000-0000-4000-8000-000000000110','10000000-0000-4000-8000-000000000011','Natural','STUDIO-11-0',3875,0), +('20000000-0000-4000-8000-000000000111','10000000-0000-4000-8000-000000000011','Moss','STUDIO-11-1',4075,1), +('20000000-0000-4000-8000-000000000112','10000000-0000-4000-8000-000000000011','Ink','STUDIO-11-2',4275,2), +('20000000-0000-4000-8000-000000000120','10000000-0000-4000-8000-000000000012','Natural','STUDIO-12-0',4000,0), +('20000000-0000-4000-8000-000000000121','10000000-0000-4000-8000-000000000012','Moss','STUDIO-12-1',4200,1), +('20000000-0000-4000-8000-000000000122','10000000-0000-4000-8000-000000000012','Ink','STUDIO-12-2',4400,2), +('20000000-0000-4000-8000-000000000130','10000000-0000-4000-8000-000000000013','Natural','STUDIO-13-0',4125,0), +('20000000-0000-4000-8000-000000000131','10000000-0000-4000-8000-000000000013','Moss','STUDIO-13-1',4325,1), +('20000000-0000-4000-8000-000000000132','10000000-0000-4000-8000-000000000013','Ink','STUDIO-13-2',4525,2), +('20000000-0000-4000-8000-000000000140','10000000-0000-4000-8000-000000000014','Natural','STUDIO-14-0',4250,0), +('20000000-0000-4000-8000-000000000141','10000000-0000-4000-8000-000000000014','Moss','STUDIO-14-1',4450,1), +('20000000-0000-4000-8000-000000000142','10000000-0000-4000-8000-000000000014','Ink','STUDIO-14-2',4650,2), +('20000000-0000-4000-8000-000000000150','10000000-0000-4000-8000-000000000015','Natural','STUDIO-15-0',4375,0), +('20000000-0000-4000-8000-000000000151','10000000-0000-4000-8000-000000000015','Moss','STUDIO-15-1',4575,1), +('20000000-0000-4000-8000-000000000152','10000000-0000-4000-8000-000000000015','Ink','STUDIO-15-2',4775,2), +('20000000-0000-4000-8000-000000000160','10000000-0000-4000-8000-000000000016','Natural','STUDIO-16-0',4500,0), +('20000000-0000-4000-8000-000000000161','10000000-0000-4000-8000-000000000016','Moss','STUDIO-16-1',4700,1), +('20000000-0000-4000-8000-000000000162','10000000-0000-4000-8000-000000000016','Ink','STUDIO-16-2',4900,2), +('20000000-0000-4000-8000-000000000170','10000000-0000-4000-8000-000000000017','Natural','STUDIO-17-0',4625,0), +('20000000-0000-4000-8000-000000000171','10000000-0000-4000-8000-000000000017','Moss','STUDIO-17-1',4825,1), +('20000000-0000-4000-8000-000000000172','10000000-0000-4000-8000-000000000017','Ink','STUDIO-17-2',5025,2), +('20000000-0000-4000-8000-000000000180','10000000-0000-4000-8000-000000000018','Natural','STUDIO-18-0',4750,0), +('20000000-0000-4000-8000-000000000181','10000000-0000-4000-8000-000000000018','Moss','STUDIO-18-1',4950,1), +('20000000-0000-4000-8000-000000000182','10000000-0000-4000-8000-000000000018','Ink','STUDIO-18-2',5150,2), +('20000000-0000-4000-8000-000000000190','10000000-0000-4000-8000-000000000019','Natural','STUDIO-19-0',4875,0), +('20000000-0000-4000-8000-000000000191','10000000-0000-4000-8000-000000000019','Moss','STUDIO-19-1',5075,1), +('20000000-0000-4000-8000-000000000192','10000000-0000-4000-8000-000000000019','Ink','STUDIO-19-2',5275,2), +('20000000-0000-4000-8000-000000000200','10000000-0000-4000-8000-000000000020','Natural','STUDIO-20-0',5000,0), +('20000000-0000-4000-8000-000000000201','10000000-0000-4000-8000-000000000020','Moss','STUDIO-20-1',5200,1), +('20000000-0000-4000-8000-000000000202','10000000-0000-4000-8000-000000000020','Ink','STUDIO-20-2',5400,2), +('20000000-0000-4000-8000-000000000210','10000000-0000-4000-8000-000000000021','Natural','STUDIO-21-0',5125,0), +('20000000-0000-4000-8000-000000000211','10000000-0000-4000-8000-000000000021','Moss','STUDIO-21-1',5325,1), +('20000000-0000-4000-8000-000000000212','10000000-0000-4000-8000-000000000021','Ink','STUDIO-21-2',5525,2), +('20000000-0000-4000-8000-000000000220','10000000-0000-4000-8000-000000000022','Natural','STUDIO-22-0',5250,0), +('20000000-0000-4000-8000-000000000221','10000000-0000-4000-8000-000000000022','Moss','STUDIO-22-1',5450,1), +('20000000-0000-4000-8000-000000000222','10000000-0000-4000-8000-000000000022','Ink','STUDIO-22-2',5650,2), +('20000000-0000-4000-8000-000000000230','10000000-0000-4000-8000-000000000023','Natural','STUDIO-23-0',5375,0), +('20000000-0000-4000-8000-000000000231','10000000-0000-4000-8000-000000000023','Moss','STUDIO-23-1',5575,1), +('20000000-0000-4000-8000-000000000232','10000000-0000-4000-8000-000000000023','Ink','STUDIO-23-2',5775,2), +('20000000-0000-4000-8000-000000000240','10000000-0000-4000-8000-000000000024','Natural','STUDIO-24-0',5500,0), +('20000000-0000-4000-8000-000000000241','10000000-0000-4000-8000-000000000024','Moss','STUDIO-24-1',5700,1), +('20000000-0000-4000-8000-000000000242','10000000-0000-4000-8000-000000000024','Ink','STUDIO-24-2',5900,2) +ON CONFLICT DO NOTHING; +INSERT INTO product_catalog.collections VALUES ('67bc2cf0-5b0f-45c8-bf6a-7b4bb5155a50','Quiet workspace',1) ON CONFLICT DO NOTHING; +INSERT INTO product_catalog.collection_items SELECT '67bc2cf0-5b0f-45c8-bf6a-7b4bb5155a50'::uuid,p.id,row_number() OVER (ORDER BY p.name)-1 FROM product_catalog.products p WHERE p.id IN ('10000000-0000-4000-8000-000000000001','10000000-0000-4000-8000-000000000003','10000000-0000-4000-8000-000000000005') AND EXISTS(SELECT 1 FROM product_catalog.collections WHERE id='67bc2cf0-5b0f-45c8-bf6a-7b4bb5155a50' AND revision=1) ON CONFLICT DO NOTHING; +COMMIT; diff --git a/applications/product-graphql/src/client/env.d.ts b/applications/product-graphql/src/client/env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/applications/product-graphql/src/client/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/applications/product-graphql/src/client/gql/gql.ts b/applications/product-graphql/src/client/gql/gql.ts new file mode 100644 index 00000000..a20f9f5d --- /dev/null +++ b/applications/product-graphql/src/client/gql/gql.ts @@ -0,0 +1,46 @@ +/* eslint-disable */ +import * as types from './graphql'; +import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; + +/** + * Map of all GraphQL operations in the project. + * + * This map has several performance disadvantages: + * 1. It is not tree-shakeable, so it will include all operations in the project. + * 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle. + * 3. It does not support dead code elimination, so it will add unused operations. + * + * Therefore it is highly recommended to use the babel or swc plugin for production. + * Learn more about it here: https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#reducing-bundle-size + */ +type Documents = { + "query Catalog($first: Int!, $offset: Int!, $search: String, $category: String) {\n products(first: $first, offset: $offset, search: $search, category: $category) {\n id\n name\n category\n description\n variants {\n id\n name\n sku\n priceCents\n }\n }\n}\n\nquery CollectionList {\n collections {\n id\n title\n revision\n }\n}\n\nquery CollectionDetail($id: ID!) {\n collection(id: $id) {\n id\n title\n revision\n products {\n id\n name\n category\n description\n variants {\n id\n name\n sku\n priceCents\n }\n }\n }\n}\n\nmutation NewCollection($title: String!, $productIds: [ID!]!) {\n createCollection(title: $title, productIds: $productIds) {\n id\n title\n revision\n products {\n id\n name\n category\n description\n variants {\n id\n name\n sku\n priceCents\n }\n }\n }\n}\n\nmutation SaveCollection($id: ID!, $baseRevision: Int!, $productIds: [ID!]!) {\n replaceCollection(id: $id, baseRevision: $baseRevision, productIds: $productIds) {\n status\n collection {\n id\n title\n revision\n products {\n id\n name\n category\n description\n variants {\n id\n name\n sku\n priceCents\n }\n }\n }\n }\n}": typeof types.CatalogDocument, +}; +const documents: Documents = { + "query Catalog($first: Int!, $offset: Int!, $search: String, $category: String) {\n products(first: $first, offset: $offset, search: $search, category: $category) {\n id\n name\n category\n description\n variants {\n id\n name\n sku\n priceCents\n }\n }\n}\n\nquery CollectionList {\n collections {\n id\n title\n revision\n }\n}\n\nquery CollectionDetail($id: ID!) {\n collection(id: $id) {\n id\n title\n revision\n products {\n id\n name\n category\n description\n variants {\n id\n name\n sku\n priceCents\n }\n }\n }\n}\n\nmutation NewCollection($title: String!, $productIds: [ID!]!) {\n createCollection(title: $title, productIds: $productIds) {\n id\n title\n revision\n products {\n id\n name\n category\n description\n variants {\n id\n name\n sku\n priceCents\n }\n }\n }\n}\n\nmutation SaveCollection($id: ID!, $baseRevision: Int!, $productIds: [ID!]!) {\n replaceCollection(id: $id, baseRevision: $baseRevision, productIds: $productIds) {\n status\n collection {\n id\n title\n revision\n products {\n id\n name\n category\n description\n variants {\n id\n name\n sku\n priceCents\n }\n }\n }\n }\n}": types.CatalogDocument, +}; + +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + * + * + * @example + * ```ts + * const query = graphql(`query GetUser($id: ID!) { user(id: $id) { name } }`); + * ``` + * + * The query argument is unknown! + * Please regenerate the types. + */ +export function graphql(source: string): unknown; + +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "query Catalog($first: Int!, $offset: Int!, $search: String, $category: String) {\n products(first: $first, offset: $offset, search: $search, category: $category) {\n id\n name\n category\n description\n variants {\n id\n name\n sku\n priceCents\n }\n }\n}\n\nquery CollectionList {\n collections {\n id\n title\n revision\n }\n}\n\nquery CollectionDetail($id: ID!) {\n collection(id: $id) {\n id\n title\n revision\n products {\n id\n name\n category\n description\n variants {\n id\n name\n sku\n priceCents\n }\n }\n }\n}\n\nmutation NewCollection($title: String!, $productIds: [ID!]!) {\n createCollection(title: $title, productIds: $productIds) {\n id\n title\n revision\n products {\n id\n name\n category\n description\n variants {\n id\n name\n sku\n priceCents\n }\n }\n }\n}\n\nmutation SaveCollection($id: ID!, $baseRevision: Int!, $productIds: [ID!]!) {\n replaceCollection(id: $id, baseRevision: $baseRevision, productIds: $productIds) {\n status\n collection {\n id\n title\n revision\n products {\n id\n name\n category\n description\n variants {\n id\n name\n sku\n priceCents\n }\n }\n }\n }\n}"): (typeof documents)["query Catalog($first: Int!, $offset: Int!, $search: String, $category: String) {\n products(first: $first, offset: $offset, search: $search, category: $category) {\n id\n name\n category\n description\n variants {\n id\n name\n sku\n priceCents\n }\n }\n}\n\nquery CollectionList {\n collections {\n id\n title\n revision\n }\n}\n\nquery CollectionDetail($id: ID!) {\n collection(id: $id) {\n id\n title\n revision\n products {\n id\n name\n category\n description\n variants {\n id\n name\n sku\n priceCents\n }\n }\n }\n}\n\nmutation NewCollection($title: String!, $productIds: [ID!]!) {\n createCollection(title: $title, productIds: $productIds) {\n id\n title\n revision\n products {\n id\n name\n category\n description\n variants {\n id\n name\n sku\n priceCents\n }\n }\n }\n}\n\nmutation SaveCollection($id: ID!, $baseRevision: Int!, $productIds: [ID!]!) {\n replaceCollection(id: $id, baseRevision: $baseRevision, productIds: $productIds) {\n status\n collection {\n id\n title\n revision\n products {\n id\n name\n category\n description\n variants {\n id\n name\n sku\n priceCents\n }\n }\n }\n }\n}"]; + +export function graphql(source: string) { + return (documents as any)[source] ?? {}; +} + +export type DocumentType> = TDocumentNode extends DocumentNode< infer TType, any> ? TType : never; \ No newline at end of file diff --git a/applications/product-graphql/src/client/gql/graphql.ts b/applications/product-graphql/src/client/gql/graphql.ts new file mode 100644 index 00000000..3f07e939 --- /dev/null +++ b/applications/product-graphql/src/client/gql/graphql.ts @@ -0,0 +1,55 @@ +/* eslint-disable */ +/** Internal type. DO NOT USE DIRECTLY. */ +type Exact = { [K in keyof T]: T[K] }; +/** Internal type. DO NOT USE DIRECTLY. */ +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; +import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; +export type SaveStatus = + | 'CONFLICT' + | 'OK'; + +export type CatalogQueryVariables = Exact<{ + first: number; + offset: number; + search?: string | null | undefined; + category?: string | null | undefined; +}>; + + +export type CatalogQuery = { products: Array<{ id: string, name: string, category: string, description: string, variants: Array<{ id: string, name: string, sku: string, priceCents: number }> }> }; + +export type CollectionListQueryVariables = Exact<{ [key: string]: never; }>; + + +export type CollectionListQuery = { collections: Array<{ id: string, title: string, revision: number }> }; + +export type CollectionDetailQueryVariables = Exact<{ + id: string | number; +}>; + + +export type CollectionDetailQuery = { collection: { id: string, title: string, revision: number, products: Array<{ id: string, name: string, category: string, description: string, variants: Array<{ id: string, name: string, sku: string, priceCents: number }> }> } | null }; + +export type NewCollectionMutationVariables = Exact<{ + title: string; + productIds: Array | string | number; +}>; + + +export type NewCollectionMutation = { createCollection: { id: string, title: string, revision: number, products: Array<{ id: string, name: string, category: string, description: string, variants: Array<{ id: string, name: string, sku: string, priceCents: number }> }> } }; + +export type SaveCollectionMutationVariables = Exact<{ + id: string | number; + baseRevision: number; + productIds: Array | string | number; +}>; + + +export type SaveCollectionMutation = { replaceCollection: { status: SaveStatus, collection: { id: string, title: string, revision: number, products: Array<{ id: string, name: string, category: string, description: string, variants: Array<{ id: string, name: string, sku: string, priceCents: number }> }> } | null } }; + + +export const CatalogDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Catalog"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"offset"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"search"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"category"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"products"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"offset"},"value":{"kind":"Variable","name":{"kind":"Name","value":"offset"}}},{"kind":"Argument","name":{"kind":"Name","value":"search"},"value":{"kind":"Variable","name":{"kind":"Name","value":"search"}}},{"kind":"Argument","name":{"kind":"Name","value":"category"},"value":{"kind":"Variable","name":{"kind":"Name","value":"category"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"variants"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"sku"}},{"kind":"Field","name":{"kind":"Name","value":"priceCents"}}]}}]}}]}}]} as unknown as DocumentNode; +export const CollectionListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"CollectionList"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"collections"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"revision"}}]}}]}}]} as unknown as DocumentNode; +export const CollectionDetailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"CollectionDetail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"collection"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"revision"}},{"kind":"Field","name":{"kind":"Name","value":"products"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"variants"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"sku"}},{"kind":"Field","name":{"kind":"Name","value":"priceCents"}}]}}]}}]}}]}}]} as unknown as DocumentNode; +export const NewCollectionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"NewCollection"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"title"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"productIds"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createCollection"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"title"},"value":{"kind":"Variable","name":{"kind":"Name","value":"title"}}},{"kind":"Argument","name":{"kind":"Name","value":"productIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"productIds"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"revision"}},{"kind":"Field","name":{"kind":"Name","value":"products"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"variants"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"sku"}},{"kind":"Field","name":{"kind":"Name","value":"priceCents"}}]}}]}}]}}]}}]} as unknown as DocumentNode; +export const SaveCollectionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SaveCollection"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"baseRevision"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"productIds"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"replaceCollection"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}},{"kind":"Argument","name":{"kind":"Name","value":"baseRevision"},"value":{"kind":"Variable","name":{"kind":"Name","value":"baseRevision"}}},{"kind":"Argument","name":{"kind":"Name","value":"productIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"productIds"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"collection"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"revision"}},{"kind":"Field","name":{"kind":"Name","value":"products"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"variants"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"sku"}},{"kind":"Field","name":{"kind":"Name","value":"priceCents"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/applications/product-graphql/src/client/gql/index.ts b/applications/product-graphql/src/client/gql/index.ts new file mode 100644 index 00000000..93bcc44c --- /dev/null +++ b/applications/product-graphql/src/client/gql/index.ts @@ -0,0 +1,2 @@ +/* eslint-disable */ +export * from "./gql"; \ No newline at end of file diff --git a/applications/product-graphql/src/client/main.tsx b/applications/product-graphql/src/client/main.tsx new file mode 100644 index 00000000..90b49419 --- /dev/null +++ b/applications/product-graphql/src/client/main.tsx @@ -0,0 +1,394 @@ +import React, { useEffect, useState } from "react"; +import { createRoot } from "react-dom/client"; +import { + Provider, + createClient, + cacheExchange, + fetchExchange, + useQuery, + useMutation, +} from "urql"; +import { + CatalogDocument, + CollectionListDocument, + CollectionDetailDocument, + NewCollectionDocument, + SaveCollectionDocument, + type CatalogQuery, +} from "./gql/graphql"; +import "./style.css"; +type Product = CatalogQuery["products"][number]; +const client = createClient({ + url: window.location.origin + "/graphql", + exchanges: [cacheExchange, fetchExchange], + requestPolicy: "network-only", +}); +const seed = "67bc2cf0-5b0f-45c8-bf6a-7b4bb5155a50"; +function Studio() { + const [searchText, setSearchText] = useState(""); + const [search, setSearch] = useState(""); + const [category, setCategory] = useState(""); + const [offset, setOffset] = useState(0); + const [id, setId] = useState( + new URLSearchParams(window.location.search).get("collection") || seed, + ); + const [catalog] = useQuery({ + query: CatalogDocument, + variables: { + first: 12, + offset, + search: search || null, + category: category || null, + }, + }); + const [list, reloadList] = useQuery({ query: CollectionListDocument }); + const [detail, reload] = useQuery({ + query: CollectionDetailDocument, + variables: { id }, + }); + const [creating, create] = useMutation(NewCollectionDocument); + const [saving, save] = useMutation(SaveCollectionDocument); + const [draft, setDraft] = useState([]); + const [loaded, setLoaded] = useState<{ + title: string; + revision: number; + } | null>(null); + const [dirty, setDirty] = useState(false); + const [message, setMessage] = useState(""); + const [conflict, setConflict] = useState(false); + const [name, setName] = useState("New collection"); + useEffect(() => { + if (detail.data?.collection === null) { + setLoaded(null); + setDraft([]); + setDirty(false); + setConflict(false); + } + if (detail.data?.collection?.id === id) { + setDraft(detail.data.collection.products); + setLoaded(detail.data.collection); + setDirty(false); + setConflict(false); + setMessage(""); + } + }, [detail.data, id]); + const missing = !detail.fetching && detail.data?.collection === null; + const busy = detail.fetching || saving.fetching || creating.fetching; + function edit(next: Product[]) { + setDraft(next); + setDirty(true); + setMessage(""); + } + function activate(next: string) { + const url = new URL(window.location.href); + url.searchParams.set("collection", next); + history.replaceState(null, "", url); + setId(next); + } + function choose(next: string) { + if (next === id) return; + if (dirty && !confirm("Discard unsaved collection changes?")) return; + activate(next); + setLoaded(null); + } + async function persist() { + if (!loaded) return; + const result = await save({ + id, + baseRevision: loaded.revision, + productIds: draft.map((p) => p.id), + }); + if (result.error) { + setMessage(result.error.message); + return; + } + const answer = result.data?.replaceCollection; + if (answer?.status === "CONFLICT") { + setConflict(true); + setMessage( + "Collection changed in another tab. Your draft is still here.", + ); + return; + } + if (answer?.collection) { + setLoaded(answer.collection); + setDraft(answer.collection.products); + setDirty(false); + setMessage("Collection saved"); + reloadList({ requestPolicy: "network-only" }); + } + } + async function newCollection() { + const result = await create({ + title: name, + productIds: draft.map((p) => p.id), + }); + if (result.error) { + setMessage(result.error.message); + return; + } + if (result.data) { + activate(result.data.createCollection.id); + setLoaded(null); + setDirty(false); + reloadList({ requestPolicy: "network-only" }); + } + } + return ( + <> +
+ + ▦{" "} + + Collection studioMake room for good things. + + + Trusted local demo +
+
+
+
+

THE CURATOR’S DESK

+

A considered collection.

+

Explore the catalogue. Choose the pieces. Save the order.

+
+
+ CATALOGUE + 24 + objects · 3 variants each +
+
+
+
+
{ + e.preventDefault(); + setSearch(searchText); + setOffset(0); + }} + > + setSearchText(e.target.value)} + /> + + +
+ {catalog.error &&

{catalog.error.message}

} +
+ {catalog.data?.products.map((product) => ( +
+
+ + {product.category === "Desk" + ? "▱" + : product.category === "Travel" + ? "◈" + : "◉"} + + {product.category} +
+
+

{product.name}

+

{product.description}

+
+ + {product.variants.length} variants · from $ + {( + Math.min( + ...product.variants.map((v) => v.priceCents), + ) / 100 + ).toFixed(2)} + +
    + {product.variants.map((v) => ( +
  • + {v.name} + ${(v.priceCents / 100).toFixed(2)} + {v.sku} +
  • + ))} +
+
+ +
+
+ ))} +
+ +
+ +
+
+ Yoga · Pothos · Drizzle · Code Generator · urql{" "} + ClickHouse Managed Postgres +
+
+ + ); +} +createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/applications/product-graphql/src/client/operations.graphql b/applications/product-graphql/src/client/operations.graphql new file mode 100644 index 00000000..9660bd76 --- /dev/null +++ b/applications/product-graphql/src/client/operations.graphql @@ -0,0 +1,90 @@ +query Catalog($first: Int!, $offset: Int!, $search: String, $category: String) { + products( + first: $first + offset: $offset + search: $search + category: $category + ) { + id + name + category + description + variants { + id + name + sku + priceCents + } + } +} +query CollectionList { + collections { + id + title + revision + } +} +query CollectionDetail($id: ID!) { + collection(id: $id) { + id + title + revision + products { + id + name + category + description + variants { + id + name + sku + priceCents + } + } + } +} +mutation NewCollection($title: String!, $productIds: [ID!]!) { + createCollection(title: $title, productIds: $productIds) { + id + title + revision + products { + id + name + category + description + variants { + id + name + sku + priceCents + } + } + } +} +mutation SaveCollection($id: ID!, $baseRevision: Int!, $productIds: [ID!]!) { + replaceCollection( + id: $id + baseRevision: $baseRevision + productIds: $productIds + ) { + status + collection { + id + title + revision + products { + id + name + category + description + variants { + id + name + sku + priceCents + } + } + } + } +} diff --git a/applications/product-graphql/src/client/style.css b/applications/product-graphql/src/client/style.css new file mode 100644 index 00000000..b0189cbe --- /dev/null +++ b/applications/product-graphql/src/client/style.css @@ -0,0 +1,356 @@ +* { + box-sizing: border-box; +} +body { + margin: 0; + background: #f8f6f0; + color: #263c36; + font-family: system-ui, sans-serif; + font-size: 14px; +} +button, +input, +select { + font: inherit; +} +button { + cursor: pointer; + padding: 10px 14px; + background: #fffdf7; + border: 1px solid #d8dbcd; + border-radius: 5px; + color: #263c36; +} +button:disabled { + opacity: 0.5; + cursor: default; +} +input, +select { + min-width: 0; + padding: 12px; + border: 1px solid #d8dbcd; + border-radius: 5px; + background: #fffdf7; + color: #263c36; +} +header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 24px 42px; + border-bottom: 1px solid #d8dbcd; + background: #fffcf5; +} +.brand { + display: flex; + align-items: center; + gap: 14px; + text-decoration: none; + color: inherit; + font-size: 26px; +} +.brand span { + font-size: 21px; + font-weight: 650; +} +.brand small { + display: block; + font-size: 12px; + font-weight: 400; + margin-top: 4px; + color: #728178; +} +.badge { + font-size: 12px; + color: #74806e; +} +.badge:before { + content: "●"; + margin-right: 9px; + color: #94a871; +} +main { + padding: 34px 42px; +} +.intro { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 32px; +} +.eyebrow { + font-size: 10px; + letter-spacing: 2.6px; + color: #75866b; +} +h1 { + font-family: Georgia, serif; + font-weight: 400; + font-size: 46px; + letter-spacing: -1px; + margin: 12px 0; +} +.intro p:not(.eyebrow) { + color: #758178; + font-size: 16px; +} +.stat { + padding: 18px 30px; + border: 1px solid #d8dbcd; + border-radius: 8px; + display: grid; + grid-template-columns: 1fr auto; + gap: 4px 22px; + align-items: center; +} +.stat small { + letter-spacing: 1.6px; + font-size: 9px; +} +.stat strong { + font-family: Georgia, serif; + font-size: 44px; + font-weight: 400; + grid-column: 2; + grid-row: 1/3; +} +.stat span { + font-size: 11px; + color: #758178; +} +.layout { + display: grid; + grid-template-columns: minmax(0, 1fr) 355px; + gap: 30px; +} +.filters { + display: flex; + gap: 10px; + margin-bottom: 22px; +} +.filters input { + flex: 1; +} +.cards { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 20px; +} +.card { + background: #fffdf7; + border: 1px solid #d8dbcd; + border-radius: 9px; + overflow: hidden; +} +.illustration { + height: 120px; + display: flex; + align-items: center; + justify-content: center; + position: relative; + color: #5a7365; + background: #e5e8d7; +} +.illustration span { + font-size: 85px; + line-height: 1; + transform: rotate(-12deg); +} +.illustration small { + position: absolute; + bottom: 12px; + left: 14px; + font-size: 9px; + text-transform: uppercase; + letter-spacing: 1.5px; +} +.travel { + background: #e5d8bd; + color: #987d51; +} +.lighting { + background: #dce4e4; + color: #708c93; +} +.card-body { + padding: 16px; +} +.card h2 { + font-family: Georgia, serif; + font-size: 21px; + font-weight: 400; + margin: 0 0 8px; +} +.card p { + font-size: 12px; + line-height: 1.5; + color: #7a8275; + margin: 0 0 12px; + min-height: 36px; +} +.card summary { + font-size: 11px; + cursor: pointer; +} +.card ul { + padding: 0; + list-style: none; +} +.card li { + display: grid; + grid-template-columns: 1fr auto; + font-size: 11px; + padding: 7px 0; + border-bottom: 1px solid #e8e7df; +} +.card li small { + grid-column: 1/3; + color: #7a8275; +} +.card button { + margin-top: 14px; + width: 100%; + font-size: 11px; +} +.catalog nav { + display: flex; + justify-content: space-between; + align-items: center; + margin: 24px 0; + color: #7a8275; + font-size: 12px; +} +aside { + background: #eef0e2; + border: 1px solid #d6ddc5; + border-radius: 9px; + padding: 24px; + height: max-content; + position: sticky; + top: 20px; +} +aside > select { + width: 100%; + margin: 8px 0 18px; +} +.collection-heading { + display: flex; + gap: 15px; + align-items: center; + justify-content: space-between; +} +.collection-heading h2 { + font-family: Georgia, serif; + font-size: 26px; + font-weight: 400; + margin: 0; +} +.collection-heading span { + padding: 6px 9px; + background: #dce4cc; + border-radius: 4px; + font-size: 12px; +} +.subtle, +.footnote { + color: #788471; + font-size: 11px; + line-height: 1.6; +} +.selection { + list-style: none; + padding: 0; + margin: 24px 0; +} +.selection li { + display: flex; + align-items: center; + gap: 12px; + border-top: 1px solid #d7ddca; + padding: 14px 0; +} +.number { + font-size: 10px; + color: #839572; +} +.selection strong { + font-size: 12px; + font-weight: 500; +} +.selection small { + display: block; + font-size: 10px; + color: #7a8275; + margin-top: 5px; +} +.item-actions { + display: flex; + margin-left: auto; + gap: 4px; +} +.item-actions button { + padding: 3px 7px; + background: transparent; + font-size: 13px; +} +.status { + font-size: 12px; + line-height: 1.5; +} +.primary, +.reload { + width: 100%; + margin-top: 7px; +} +.primary { + background: #3c5646; + border-color: #3c5646; + color: white; +} +.reload { + background: transparent; + font-size: 11px; +} +.new-collection { + border-top: 1px solid #d7ddca; + margin-top: 24px; + padding-top: 20px; +} +.new-collection label { + font-size: 11px; + display: block; + margin-bottom: 9px; +} +.new-collection input, +.new-collection button { + width: 100%; + margin-bottom: 8px; +} +.empty { + font-family: Georgia, serif; + font-size: 19px; + color: #75866b; + padding: 20px 0; +} +.footnote { + margin-top: 18px; +} +footer { + display: flex; + justify-content: space-between; + color: #89917f; + font-size: 10px; + margin-top: 24px; +} +@media (max-width: 1000px) { + .layout { + grid-template-columns: 1fr; + } + .cards { + grid-template-columns: repeat(2, 1fr); + } + aside { + position: static; + } +} diff --git a/applications/product-graphql/src/server/collections.ts b/applications/product-graphql/src/server/collections.ts new file mode 100644 index 00000000..e6732e47 --- /dev/null +++ b/applications/product-graphql/src/server/collections.ts @@ -0,0 +1,108 @@ +import { and, eq, inArray, asc } from "drizzle-orm"; +import type { Database } from "./database"; +import { + products, + collections, + items, + type Product, + type Collection, +} from "./tables"; +import { contents, uuid, title, revision, bad } from "./validation"; +export type CollectionSnapshot = Collection & { products: Product[] }; +async function known(tx: Pick, ids: string[]) { + const rows = ids.length + ? await tx.select().from(products).where(inArray(products.id, ids)) + : []; + if (rows.length !== ids.length) bad("A chosen product no longer exists"); + return ids.map((id) => rows.find((row) => row.id === id)!); +} +export async function readCollections( + db: Database, + id?: string, +): Promise { + return db.transaction( + async (tx) => { + const headers = await tx + .select() + .from(collections) + .where(id ? eq(collections.id, uuid(id)) : undefined) + .orderBy(asc(collections.title), asc(collections.id)) + .limit(20); + if (!headers.length) return []; + const rows = await tx + .select({ collectionId: items.collectionId, product: products }) + .from(items) + .innerJoin(products, eq(items.productId, products.id)) + .where( + inArray( + items.collectionId, + headers.map((row) => row.id), + ), + ) + .orderBy(asc(items.position)); + return headers.map((header) => ({ + ...header, + products: rows + .filter((row) => row.collectionId === header.id) + .map((row) => row.product), + })); + }, + { isolationLevel: "repeatable read", accessMode: "read only" }, + ); +} +export async function createCollection( + db: Database, + name: string, + ids: string[], +) { + ids = contents(ids); + name = title(name); + return db.transaction(async (tx) => { + const selected = await known(tx, ids); + const [row] = await tx + .insert(collections) + .values({ id: crypto.randomUUID(), title: name, revision: 1 }) + .returning(); + if (ids.length) + await tx.insert(items).values( + ids.map((productId, position) => ({ + collectionId: row.id, + productId, + position, + })), + ); + return { ...row, products: selected }; + }); +} +export async function replaceCollection( + db: Database, + id: string, + base: number, + ids: string[], +) { + uuid(id); + revision(base); + ids = contents(ids); + return db.transaction(async (tx) => { + const [row] = await tx + .update(collections) + .set({ revision: base + 1 }) + .where(and(eq(collections.id, id), eq(collections.revision, base))) + .returning(); + if (!row) return { status: "CONFLICT" as const, collection: null }; + const selected = await known(tx, ids); + await tx.delete(items).where(eq(items.collectionId, id)); + if (ids.length) + await tx.insert(items).values( + ids.map((productId, position) => ({ + collectionId: id, + productId, + position, + })), + ); + return { + status: "OK" as const, + collection: { ...row, products: selected }, + }; + }); +} diff --git a/applications/product-graphql/src/server/context.ts b/applications/product-graphql/src/server/context.ts new file mode 100644 index 00000000..ec4e231b --- /dev/null +++ b/applications/product-graphql/src/server/context.ts @@ -0,0 +1,19 @@ +import DataLoader from "dataloader"; +import { inArray, asc } from "drizzle-orm"; +import type { Pool } from "pg"; +import { database } from "./database"; +import { variants, type Variant } from "./tables"; +export function context(pool: Pool) { + const count = { reads: 0 }; + const db = database(pool, count); + const variantLoader = new DataLoader(async (ids) => { + const rows = await db + .select() + .from(variants) + .where(inArray(variants.productId, [...ids])) + .orderBy(asc(variants.sku)); + return ids.map((id) => rows.filter((row) => row.productId === id)); + }); + return { db, count, variantLoader }; +} +export type Context = ReturnType; diff --git a/applications/product-graphql/src/server/database.ts b/applications/product-graphql/src/server/database.ts new file mode 100644 index 00000000..b0308e3a --- /dev/null +++ b/applications/product-graphql/src/server/database.ts @@ -0,0 +1,89 @@ +import { Pool } from "pg"; +import { drizzle } from "drizzle-orm/node-postgres"; +export async function connectDatabase( + urlText = process.env.DATABASE_URL, + caPath = process.env.PG_CA_CERT_PATH, +) { + if (!urlText) throw new Error("DATABASE_URL is required"); + const url = new URL(urlText); + if ( + !["postgres:", "postgresql:"].includes(url.protocol) || + url.search || + url.hash + ) + throw new Error("Use a plain Postgres URL without query parameters"); + const local = process.env.DATABASE_LOCAL === "true"; + if (local && !["127.0.0.1", "localhost", "[::1]"].includes(url.hostname)) + throw new Error("Local mode requires loopback"); + if (!local && !caPath) throw new Error("PG_CA_CERT_PATH required"); + const ca = local ? undefined : await Bun.file(caPath!).text(); + const certificates = + ca?.match( + /-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g, + ) ?? []; + if (!local && (certificates.length < 1 || certificates.length > 16)) + throw new Error("CA file must contain 1..16 PEM certificates"); + async function connect(trustedCa: string | undefined) { + const pool = new Pool({ + connectionString: url.toString(), + max: 5, + connectionTimeoutMillis: 10000, + idleTimeoutMillis: 30000, + ssl: local + ? false + : { ca: trustedCa, servername: url.hostname, rejectUnauthorized: true }, + }); + try { + await pool.query("SELECT 1"); + return pool; + } catch (error) { + await pool.end(); + throw error; + } + } + try { + return await connect(ca); + } catch (originalError) { + if ( + errorCode(originalError) !== "CERT_SIGNATURE_FAILURE" || + certificates.length < 2 + ) + throw originalError; + // Bun1.4.2 can reject duplicate-subject roots in a rotation bundle. + // Only supplied anchors are retried; certificate and hostname checks remain enabled. + for (const certificate of certificates) { + try { + return await connect(certificate); + } catch (error) { + if (!CA_ERRORS.has(errorCode(error))) throw error; + } + } + throw originalError; + } +} +function errorCode(error: unknown) { + return error !== null && typeof error === "object" && "code" in error + ? String(error.code) + : ""; +} +const CA_ERRORS = new Set([ + "CERT_SIGNATURE_FAILURE", + "UNABLE_TO_GET_ISSUER_CERT", + "UNABLE_TO_GET_ISSUER_CERT_LOCALLY", + "UNABLE_TO_VERIFY_LEAF_SIGNATURE", + "SELF_SIGNED_CERT_IN_CHAIN", + "DEPTH_ZERO_SELF_SIGNED_CERT", + "CERT_HAS_EXPIRED", + "CERT_NOT_YET_VALID", +]); + +export function database(pool: Pool, counter?: { reads: number }) { + return drizzle(pool, { + logger: { + logQuery(query) { + if (/^select\b/i.test(query)) counter && counter.reads++; + }, + }, + }); +} +export type Database = ReturnType; diff --git a/applications/product-graphql/src/server/graphql.ts b/applications/product-graphql/src/server/graphql.ts new file mode 100644 index 00000000..ac648bd0 --- /dev/null +++ b/applications/product-graphql/src/server/graphql.ts @@ -0,0 +1,140 @@ +import SchemaBuilder from "@pothos/core"; +import { asc, eq, ilike, and } from "drizzle-orm"; +import type { Context } from "./context"; +import { products, collections, type Product, type Variant } from "./tables"; +import { + readCollections, + type CollectionSnapshot, + createCollection, + replaceCollection, +} from "./collections"; +import { page, uuid, bad } from "./validation"; +const builder = new SchemaBuilder<{ + Context: Context; + DefaultFieldNullability: false; +}>({ defaultFieldNullability: false }); +const VariantRef = builder.objectRef("Variant").implement({ + fields: (t) => ({ + id: t.exposeID("id"), + name: t.exposeString("name"), + sku: t.exposeString("sku"), + priceCents: t.exposeInt("priceCents"), + }), +}); +const ProductRef = builder.objectRef("Product").implement({ + fields: (t) => ({ + id: t.exposeID("id"), + name: t.exposeString("name"), + category: t.exposeString("category"), + description: t.exposeString("description"), + variants: t.field({ + type: [VariantRef], + resolve: (product, _args, ctx) => ctx.variantLoader.load(product.id), + }), + }), +}); +const CollectionRef = builder + .objectRef("Collection") + .implement({ + fields: (t) => ({ + id: t.exposeID("id"), + title: t.exposeString("title"), + revision: t.exposeInt("revision"), + products: t.field({ + type: [ProductRef], + resolve: (collection) => collection.products, + }), + }), + }); +const Status = builder.enumType("SaveStatus", { + values: ["OK", "CONFLICT"] as const, +}); +const SaveRef = builder + .objectRef<{ + status: "OK" | "CONFLICT"; + collection: CollectionSnapshot | null; + }>("CollectionSave") + .implement({ + fields: (t) => ({ + status: t.field({ type: Status, resolve: (r) => r.status }), + collection: t.field({ + type: CollectionRef, + nullable: true, + resolve: (r) => r.collection, + }), + }), + }); +builder.queryType({ + fields: (t) => ({ + products: t.field({ + type: [ProductRef], + args: { + first: t.arg.int({ required: true }), + offset: t.arg.int({ required: true }), + search: t.arg.string(), + category: t.arg.string(), + }, + resolve: (_root, args, ctx) => { + page(args.first, args.offset); + if ((args.search?.length ?? 0) > 60) bad("Search is too long"); + if ( + args.category && + !["Desk", "Travel", "Lighting"].includes(args.category) + ) + bad("Unknown category"); + return ctx.db + .select() + .from(products) + .where( + and( + args.search + ? ilike( + products.name, + "%" + args.search.replace(/[\\%_]/g, "\\$&") + "%", + ) + : undefined, + args.category ? eq(products.category, args.category) : undefined, + ), + ) + .orderBy(asc(products.name), asc(products.id)) + .limit(args.first) + .offset(args.offset); + }, + }), + collections: t.field({ + type: [CollectionRef], + resolve: (_r, _a, ctx) => readCollections(ctx.db), + }), + collection: t.field({ + type: CollectionRef, + nullable: true, + args: { id: t.arg.id({ required: true }) }, + resolve: async (_r, args, ctx) => + (await readCollections(ctx.db, uuid(args.id)))[0] ?? null, + }), + }), +}); +builder.mutationType({ + fields: (t) => ({ + createCollection: t.field({ + type: CollectionRef, + args: { + title: t.arg.string({ required: true }), + productIds: t.arg.idList({ required: true }), + }, + resolve: (_r, args, ctx) => + createCollection(ctx.db, args.title, args.productIds), + }), + replaceCollection: t.field({ + type: SaveRef, + args: { + id: t.arg.id({ required: true }), + baseRevision: t.arg.int({ required: true }), + productIds: t.arg.idList({ required: true }), + }, + resolve: (_r, args, ctx) => + replaceCollection(ctx.db, args.id, args.baseRevision, args.productIds), + }), + }), +}); +export const schema = builder.toSchema(); diff --git a/applications/product-graphql/src/server/index.ts b/applications/product-graphql/src/server/index.ts new file mode 100644 index 00000000..3d60ee1f --- /dev/null +++ b/applications/product-graphql/src/server/index.ts @@ -0,0 +1,38 @@ +import { createYoga } from "graphql-yoga"; +import { schema } from "./graphql"; +import { context } from "./context"; +import { connectDatabase } from "./database"; +const pool = await connectDatabase(); +const origin = process.env.APP_ORIGIN || "http://127.0.0.1:3001"; +const yoga = createYoga({ + schema, + graphqlEndpoint: "/graphql", + graphiql: false, + logging: false, + cors: false, + context: () => context(pool), +}); +const server = Bun.serve({ + hostname: "127.0.0.1", + port: Number(process.env.PORT || 3001), + async fetch(request) { + const url = new URL(request.url); + if ( + url.host !== new URL(origin).host || + (request.method !== "GET" && request.headers.get("origin") !== origin) + ) + return new Response("Forbidden", { status: 403 }); + if (url.pathname === "/graphql") return yoga.fetch(request); + if (url.pathname === "/") return new Response(Bun.file("dist/index.html")); + if (/^\/assets\/[a-zA-Z0-9_.-]+$/.test(url.pathname)) + return new Response(Bun.file("dist" + url.pathname)); + return new Response("Not found", { status: 404 }); + }, +}); +console.log("Collection studio listening on " + origin); +for (const signal of ["SIGTERM", "SIGINT"] as const) + process.on(signal, async () => { + server.stop(); + await pool.end(); + process.exit(0); + }); diff --git a/applications/product-graphql/src/server/tables.ts b/applications/product-graphql/src/server/tables.ts new file mode 100644 index 00000000..22a565a1 --- /dev/null +++ b/applications/product-graphql/src/server/tables.ts @@ -0,0 +1,48 @@ +import { + pgSchema, + uuid, + text, + integer, + primaryKey, + unique, +} from "drizzle-orm/pg-core"; +const s = pgSchema("product_catalog"); +export const products = s.table("products", { + id: uuid().primaryKey(), + name: text().notNull(), + category: text().notNull(), + description: text().notNull(), +}); +export const variants = s.table("variants", { + id: uuid().primaryKey(), + productId: uuid("product_id") + .notNull() + .references(() => products.id), + name: text().notNull(), + sku: text().notNull(), + priceCents: integer("price_cents").notNull(), +}); +export const collections = s.table("collections", { + id: uuid().primaryKey(), + title: text().notNull(), + revision: integer().notNull(), +}); +export const items = s.table( + "collection_items", + { + collectionId: uuid("collection_id") + .notNull() + .references(() => collections.id, { onDelete: "cascade" }), + productId: uuid("product_id") + .notNull() + .references(() => products.id), + position: integer().notNull(), + }, + (t) => [ + primaryKey({ columns: [t.collectionId, t.productId] }), + unique().on(t.collectionId, t.position), + ], +); +export type Product = typeof products.$inferSelect; +export type Variant = typeof variants.$inferSelect; +export type Collection = typeof collections.$inferSelect; diff --git a/applications/product-graphql/src/server/validation.ts b/applications/product-graphql/src/server/validation.ts new file mode 100644 index 00000000..86c1f309 --- /dev/null +++ b/applications/product-graphql/src/server/validation.ts @@ -0,0 +1,39 @@ +import { GraphQLError } from "graphql"; +export function bad(message: string): never { + throw new GraphQLError(message, { extensions: { code: "BAD_USER_INPUT" } }); +} +export function uuid(value: string) { + if ( + !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test( + value, + ) + ) + bad("Expected a UUID"); + return value; +} +export function contents(ids: string[]) { + if (ids.length > 12 || new Set(ids).size !== ids.length) + bad("Choose up to 12 distinct products"); + return ids.map(uuid); +} +export function page(first: number, offset: number) { + if ( + !Number.isInteger(first) || + first < 1 || + first > 20 || + !Number.isInteger(offset) || + offset < 0 || + offset > 10000 + ) + bad("Use first 1..20 and offset 0..10000"); +} +export function title(value: string) { + const v = value.trim(); + if (v.length < 1 || v.length > 60) bad("Use a title of 1..60 characters"); + return v; +} +export function revision(value: number) { + if (!Number.isInteger(value) || value < 1 || value >= 2147483647) + bad("Invalid revision"); + return value; +} diff --git a/applications/product-graphql/tests/browser/studio.spec.ts b/applications/product-graphql/tests/browser/studio.spec.ts new file mode 100644 index 00000000..bdf6fdde --- /dev/null +++ b/applications/product-graphql/tests/browser/studio.spec.ts @@ -0,0 +1,60 @@ +import { test, expect } from "@playwright/test"; +test("browse nested variants, create, reorder, save and recover conflict", async ({ + page, + context, +}) => { + await page.goto("/"); + await expect(page.getByTestId("revision")).toHaveText(/^r\d+$/); + await page.getByLabel("Category").selectOption("Travel"); + await expect(page.getByTestId("product").first()).toContainText("Travel"); + await page.getByLabel("Category").selectOption(""); + await page.getByLabel("Search products").fill("Canvas"); + await page.getByRole("button", { name: "Search", exact: true }).click(); + await expect(page.getByTestId("product")).toHaveCount(1); + await page.getByText("3 variants · from").click(); + await expect(page.getByText("STUDIO-02-0")).toBeVisible(); + await page + .getByRole("button", { name: "Add Canvas weekender", exact: true }) + .click(); + await page + .getByLabel("Save this edit as a new collection") + .fill("Weekend edit"); + await page + .getByRole("button", { name: "Create collection", exact: true }) + .click(); + await expect( + page.getByRole("heading", { name: "Weekend edit", exact: true }), + ).toBeVisible(); + await expect(page.getByTestId("revision")).toHaveText("r1"); + const other = await context.newPage(); + const selected = new URL(page.url()).searchParams.get("collection")!; + await other.goto("/?collection=" + selected); + await expect( + other.getByRole("heading", { name: "Weekend edit", exact: true }), + ).toBeVisible(); + await expect(other.getByTestId("revision")).toHaveText("r1"); + await page.getByLabel("Move Canvas weekender up").click(); + await page + .getByRole("button", { name: "Save collection", exact: true }) + .click(); + await expect(page.getByTestId("revision")).toHaveText("r2"); + await page.reload(); + await expect(page.locator(".selection li").nth(2)).toContainText( + "Canvas weekender", + ); + await other.getByLabel("Remove Arc desk lamp").click(); + await other + .getByRole("button", { name: "Save collection", exact: true }) + .click(); + await expect(other.getByRole("status")).toContainText("another tab"); + other.once("dialog", (d) => d.accept()); + await other + .getByRole("button", { name: "Reload saved collection", exact: true }) + .click(); + await expect(other.getByTestId("revision")).toHaveText("r2"); + await page.screenshot({ + path: "test-results/product-graphql.png", + fullPage: true, + }); + await other.close(); +}); diff --git a/applications/product-graphql/tests/database.test.ts b/applications/product-graphql/tests/database.test.ts new file mode 100644 index 00000000..ff8e2e4b --- /dev/null +++ b/applications/product-graphql/tests/database.test.ts @@ -0,0 +1,200 @@ +import { + beforeAll, + afterAll, + beforeEach, + afterEach, + test, + expect, +} from "bun:test"; +import { setDefaultTimeout } from "bun:test"; +setDefaultTimeout(30000); +import { graphql, execute } from "graphql"; +import { createYoga } from "graphql-yoga"; +import { connectDatabase } from "../src/server/database"; +import { context } from "../src/server/context"; +import { schema } from "../src/server/graphql"; +import { CatalogDocument } from "../src/client/gql/graphql"; +import type { Pool } from "pg"; +let pool: Pool; +let admin: Pool; +let id: string; +const p1 = "10000000-0000-4000-8000-000000000001", + p2 = "10000000-0000-4000-8000-000000000002"; +beforeAll(async () => { + pool = await connectDatabase(); + if (!process.env.DATABASE_TEST_ADMIN_URL) + throw new Error("Dedicated migration test login required"); + admin = await connectDatabase(process.env.DATABASE_TEST_ADMIN_URL); +}); +afterAll(async () => { + await pool?.end(); + await admin?.end(); +}); +beforeEach(async () => { + id = crypto.randomUUID(); + const c = await admin.connect(); + try { + await c.query("BEGIN"); + await c.query("SET LOCAL ROLE catalog_owner"); + await c.query( + "INSERT INTO product_catalog.collections VALUES($1,'Test collection',1)", + [id], + ); + await c.query( + "INSERT INTO product_catalog.collection_items VALUES($1,$2,0)", + [id, p1], + ); + await c.query("COMMIT"); + } finally { + c.release(); + } +}); +afterEach(async () => { + const c = await admin.connect(); + try { + await c.query("BEGIN"); + await c.query("SET LOCAL ROLE catalog_owner"); + await c.query("DELETE FROM product_catalog.collections WHERE id=$1", [id]); + await c.query("COMMIT"); + } finally { + c.release(); + } +}); +const mutation = `mutation Save($id:ID!,$base:Int!,$ids:[ID!]!){replaceCollection(id:$id,baseRevision:$base,productIds:$ids){status collection{id revision products{id}}}}`; +const run = (source: string, variables: Record = {}) => + graphql({ + schema, + source, + variableValues: variables, + contextValue: context(pool), + }); +const save = (ids: string[], base = 1) => run(mutation, { id, base, ids }); +test("nested catalog is two SQL SELECTs for 1,12 and20 products", async () => { + for (const first of [1, 12, 20]) { + const ctx = context(pool); + const result = await execute({ + schema, + document: CatalogDocument, + variableValues: { first, offset: 0 }, + contextValue: ctx, + }); + expect(result.errors).toBeUndefined(); + expect((result.data?.products as unknown[]).length).toBe(first); + expect(ctx.count.reads).toBe(2); + console.log( + `Catalog ${first} products + variants: ${ctx.count.reads} SQL SELECTs`, + ); + } +}); +test("eight concurrent replacements commit exactly one revision", async () => { + const results = await Promise.all( + Array.from({ length: 8 }, () => save([p2, p1])), + ); + expect(results.every((x) => !x.errors)).toBe(true); + const statuses = results.map( + (x) => (x.data?.replaceCollection as { status: string }).status, + ); + expect(statuses.filter((x) => x === "OK")).toHaveLength(1); + expect(statuses.filter((x) => x === "CONFLICT")).toHaveLength(7); + const row = await pool.query( + "SELECT revision FROM product_catalog.collections WHERE id=$1", + [id], + ); + expect(row.rows[0].revision).toBe(2); + const items = await pool.query( + "SELECT product_id FROM product_catalog.collection_items WHERE collection_id=$1 ORDER BY position", + [id], + ); + expect(items.rows.map((r) => r.product_id)).toEqual([p2, p1]); +}); +test("unknown product rolls version and replacement back", async () => { + const result = await save([p2, crypto.randomUUID()]); + expect(result.errors?.[0].extensions.code).toBe("BAD_USER_INPUT"); + expect( + ( + await pool.query( + "SELECT revision FROM product_catalog.collections WHERE id=$1", + [id], + ) + ).rows[0].revision, + ).toBe(1); + expect( + ( + await pool.query( + "SELECT product_id FROM product_catalog.collection_items WHERE collection_id=$1", + [id], + ) + ).rows[0].product_id, + ).toBe(p1); +}); +test("invalid contents and page return explicit errors", async () => { + expect((await save([p1, p1])).errors?.[0].extensions.code).toBe( + "BAD_USER_INPUT", + ); + expect( + (await save(Array.from({ length: 13 }, () => crypto.randomUUID()))) + .errors?.[0].extensions.code, + ).toBe("BAD_USER_INPUT"); + expect( + (await run("{products(first:21,offset:0){id}}")).errors?.[0].extensions + .code, + ).toBe("BAD_USER_INPUT"); +}); +test("runtime cannot alter catalog, schema or collection title", async () => { + await expect( + pool.query("CREATE TABLE product_catalog.forbidden(id int)"), + ).rejects.toThrow(); + await expect( + pool.query("UPDATE product_catalog.products SET name='Other' WHERE id=$1", [ + p1, + ]), + ).rejects.toThrow(); + await expect( + pool.query( + "UPDATE product_catalog.collections SET title='Other' WHERE id=$1", + [id], + ), + ).rejects.toThrow(); +}); +test("Yoga serves the real generated nested operation", async () => { + const yoga = createYoga({ + schema, + logging: false, + context: () => context(pool), + }); + const response = await yoga.fetch("http://localhost/graphql", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + query: "{products(first:2,offset:0){name variants{sku priceCents}}}", + }), + }); + const result = await response.json(); + expect(result.errors).toBeUndefined(); + expect(result.data.products).toHaveLength(2); + expect(result.data.products[0].variants).toHaveLength(3); +}); + +test("collection reads retain matching revision and ordered items during replacements", async () => { + const writer = (async () => { + for (let base = 1; base <= 12; base++) { + const result = await save([(base + 1) % 2 === 0 ? p2 : p1], base); + expect(result.errors).toBeUndefined(); + } + })(); + for (let n = 0; n < 24; n++) { + const result = await run( + "query Read($id:ID!){collection(id:$id){revision products{id}}}", + { id }, + ); + expect(result.errors).toBeUndefined(); + const collection = result.data?.collection as { + revision: number; + products: { id: string }[]; + }; + expect(collection.products.map((p) => p.id)).toEqual([ + collection.revision % 2 === 0 ? p2 : p1, + ]); + } + await writer; +}); diff --git a/applications/product-graphql/tests/validation.test.ts b/applications/product-graphql/tests/validation.test.ts new file mode 100644 index 00000000..af25c372 --- /dev/null +++ b/applications/product-graphql/tests/validation.test.ts @@ -0,0 +1,15 @@ +import { test, expect } from "bun:test"; +import { contents, page, title, revision } from "../src/server/validation"; +test("pagination bounds and collection bounds", () => { + expect(() => page(21, 0)).toThrow(); + expect(() => page(1, -1)).toThrow(); + expect(() => contents(Array(13).fill("x"))).toThrow(); + expect(() => contents(["x", "x"])).toThrow(); + expect(() => contents(["not-a-uuid"])).toThrow(); +}); +test("title and revision inputs", () => { + expect(title(" Weekend ")).toBe("Weekend"); + expect(() => title(" ")).toThrow(); + expect(() => revision(0)).toThrow(); + expect(() => revision(2147483647)).toThrow(); +}); diff --git a/applications/product-graphql/tsconfig.json b/applications/product-graphql/tsconfig.json new file mode 100644 index 00000000..97796e79 --- /dev/null +++ b/applications/product-graphql/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "jsx": "react-jsx", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "types": [ + "bun" + ], + "noEmit": true + }, + "include": [ + "src", + "scripts", + "tests", + "*.ts" + ] +} diff --git a/applications/product-graphql/vite.config.ts b/applications/product-graphql/vite.config.ts new file mode 100644 index 00000000..1826ab6a --- /dev/null +++ b/applications/product-graphql/vite.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +export default defineConfig({ plugins: [react()] });