mirror of
https://github.com/graphql-hive/console
synced 2026-05-24 09:38:26 +00:00
Compare commits
19 commits
@graphql-h
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19d3822973 | ||
|
|
cb68b50534 | ||
|
|
0e3ce40070 | ||
|
|
aa38edc106 | ||
|
|
3fad965894 | ||
|
|
ce4d445f3c | ||
|
|
24652f0847 | ||
|
|
a3ba6ccaed | ||
|
|
63e682791f | ||
|
|
931c3274bf | ||
|
|
0b091375b5 | ||
|
|
9dc61ac48b | ||
|
|
fee9fed0a1 | ||
|
|
0cd6cc5606 | ||
|
|
67ee116a20 | ||
|
|
c98625c47b | ||
|
|
80b76004da | ||
|
|
a989647908 | ||
|
|
51e5baa0dd |
105 changed files with 4788 additions and 3671 deletions
5
.changeset/brave-laws-cut.md
Normal file
5
.changeset/brave-laws-cut.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'hive': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
add eviction policy to redis
|
||||||
|
|
@ -80,3 +80,56 @@ We use changesets for versioning.
|
||||||
- These changesets should have the right amount of informational content for someone to understand
|
- These changesets should have the right amount of informational content for someone to understand
|
||||||
what this change introduces for their self-hosted Hive instance, without going into too much
|
what this change introduces for their self-hosted Hive instance, without going into too much
|
||||||
internal technical detail.
|
internal technical detail.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pull Request Reviewing
|
||||||
|
|
||||||
|
### What to Look For
|
||||||
|
|
||||||
|
Code that looks wrong in isolation may be correct given surrounding logic—and vice versa. Read the
|
||||||
|
full file to understand existing patterns, control flow, and error handling.
|
||||||
|
|
||||||
|
**Bugs** - Your primary focus.
|
||||||
|
|
||||||
|
- Logic errors, off-by-one mistakes, incorrect conditionals
|
||||||
|
- If-else guards: missing guards, incorrect branching, unreachable code paths
|
||||||
|
- Edge cases: null/empty/undefined inputs, error conditions, race conditions
|
||||||
|
- Security issues: injection, auth bypass, data exposure
|
||||||
|
- Broken error handling that swallows failures, throws unexpectedly or returns error types that are
|
||||||
|
not caught.
|
||||||
|
|
||||||
|
**Structure** - Does the code fit the codebase?
|
||||||
|
|
||||||
|
- Does it follow existing patterns and conventions?
|
||||||
|
- Are there established abstractions it should use but doesn't?
|
||||||
|
- Excessive nesting that could be flattened with early returns or extraction
|
||||||
|
|
||||||
|
**Performance** - Only flag if obviously problematic.
|
||||||
|
|
||||||
|
- O(n²) on unbounded data, N+1 queries, blocking I/O on hot paths
|
||||||
|
|
||||||
|
**Behavior Changes** - If a behavioral change is introduced, raise it (especially if it's possibly
|
||||||
|
unintentional).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Before You Flag Something
|
||||||
|
|
||||||
|
**Be certain.** If you're going to call something a bug, you need to be confident it actually is
|
||||||
|
one.
|
||||||
|
|
||||||
|
- Only review the changes - do not review pre-existing code that wasn't modified
|
||||||
|
- Don't flag something as a bug if you're unsure - investigate first
|
||||||
|
- Don't invent hypothetical problems - if an edge case matters, explain the realistic scenario where
|
||||||
|
it breaks
|
||||||
|
- If you need more context to be sure, use the tools below to get it
|
||||||
|
|
||||||
|
**Don't be a zealot about style.** When checking code against conventions:
|
||||||
|
|
||||||
|
- Verify the code is _actually_ in violation. Don't complain about else statements if early returns
|
||||||
|
are already being used correctly.
|
||||||
|
- Some "violations" are acceptable when they're the simplest option.
|
||||||
|
- Excessive nesting is a legitimate concern regardless of other style choices.
|
||||||
|
- Don't flag style preferences as issues unless they clearly violate established project
|
||||||
|
conventions.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,87 @@
|
||||||
# hive
|
# hive
|
||||||
|
|
||||||
|
## 11.1.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#8044](https://github.com/graphql-hive/console/pull/8044)
|
||||||
|
[`ce4d445`](https://github.com/graphql-hive/console/commit/ce4d445f3c2a2d214b7c47e35c9a38f4de7c3f0e)
|
||||||
|
Thanks [@n1ru4l](https://github.com/n1ru4l)! - Address vulnerability
|
||||||
|
[GHSA-q6x5-8v7m-xcrf](https://github.com/advisories/GHSA-q6x5-8v7m-xcrf),
|
||||||
|
[GHSA-jvwf-75h9-cwgg](https://github.com/advisories/GHSA-jvwf-75h9-cwgg),
|
||||||
|
[GHSA-75px-5xx7-5xc7](https://github.com/advisories/GHSA-75px-5xx7-5xc7),
|
||||||
|
[GHSA-66ff-xgx4-vchm](https://github.com/advisories/GHSA-66ff-xgx4-vchm),
|
||||||
|
[GHSA-685m-2w69-288q](https://github.com/advisories/GHSA-685m-2w69-288q),
|
||||||
|
[GHSA-2pr8-phx7-x9h3](https://github.com/advisories/GHSA-2pr8-phx7-x9h3) and
|
||||||
|
[GHSA-fx83-v9x8-x52w](https://github.com/advisories/GHSA-fx83-v9x8-x52w).
|
||||||
|
|
||||||
|
## 11.1.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- [#8015](https://github.com/graphql-hive/console/pull/8015)
|
||||||
|
[`80b7600`](https://github.com/graphql-hive/console/commit/80b76004da822f9f526fef3160b51c834865d266)
|
||||||
|
Thanks [@n1ru4l](https://github.com/n1ru4l)! - Write app deployment manifest to CDN upon app
|
||||||
|
activation and retirement. The app deployment manifest can be used to discover what hashes belong
|
||||||
|
to an app deployment version.
|
||||||
|
|
||||||
|
- [#7982](https://github.com/graphql-hive/console/pull/7982)
|
||||||
|
[`63e6827`](https://github.com/graphql-hive/console/commit/63e682791fbb3c53f05e34329043c8fc8b705189)
|
||||||
|
Thanks [@jetocotoje](https://github.com/jetocotoje)! - Add configuration for specifying services
|
||||||
|
listening host. It is now possible to specify on which host the services are listening.
|
||||||
|
Furthermore, the services can be configured to only listing on IPv6.
|
||||||
|
|
||||||
|
The behaviour can be configrued via the two new environment variables `SERVER_HOST` and
|
||||||
|
`SERVER_HOST_IPV6_ONLY` for each service.
|
||||||
|
|
||||||
|
```
|
||||||
|
SERVER_HOST="::"
|
||||||
|
SERVER_HOST_IPV6_ONLY="0"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#8020](https://github.com/graphql-hive/console/pull/8020)
|
||||||
|
[`a989647`](https://github.com/graphql-hive/console/commit/a989647908ad7635831d7bd753076fb12f98dbe8)
|
||||||
|
Thanks [@jdolle](https://github.com/jdolle)! - OIDC verification domains are now inserted and
|
||||||
|
compared against lowercase domains. Any existing domains that use an uppercase letter must be
|
||||||
|
converted to lowercase. This is to avoid an unnecessary convert to LOWER() in the SQL statements.
|
||||||
|
|
||||||
|
To convert existing domains, run the SQL query:
|
||||||
|
|
||||||
|
```
|
||||||
|
UPDATE oidc_integration_domains
|
||||||
|
SET domain_name=LOWER(domain_name)
|
||||||
|
WHERE domain_name != LOWER(domain_name)
|
||||||
|
;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [#8040](https://github.com/graphql-hive/console/pull/8040)
|
||||||
|
[`931c327`](https://github.com/graphql-hive/console/commit/931c3274bf69984d00a3db3e6e20adcfcd39ad4b)
|
||||||
|
Thanks [@n1ru4l](https://github.com/n1ru4l)! - Address vulnerabilityies
|
||||||
|
[GHSA-q7rr-3cgh-j5r3](https://github.com/advisories/GHSA-q7rr-3cgh-j5r3).
|
||||||
|
|
||||||
|
- [#8035](https://github.com/graphql-hive/console/pull/8035)
|
||||||
|
[`0cd6cc5`](https://github.com/graphql-hive/console/commit/0cd6cc5606e8cf3c952583feec956c8f024ee615)
|
||||||
|
Thanks [@n1ru4l](https://github.com/n1ru4l)! - Address vulnerability
|
||||||
|
[GHSA-pf86-5x62-jrwf](https://github.com/advisories/GHSA-pf86-5x62-jrwf)
|
||||||
|
|
||||||
|
- [#8021](https://github.com/graphql-hive/console/pull/8021)
|
||||||
|
[`51e5baa`](https://github.com/graphql-hive/console/commit/51e5baa0dd42b9a5fcd499e60f03baa0c45c8da9)
|
||||||
|
Thanks [@jdolle](https://github.com/jdolle)! - "INPUT_FIELD_ADDED" is now classified as Dangerous
|
||||||
|
(was NonBreaking) when the added field has a default value, since rolling deploys can expose
|
||||||
|
consumers to the default before producers are ready.
|
||||||
|
|
||||||
|
- [#8035](https://github.com/graphql-hive/console/pull/8035)
|
||||||
|
[`0cd6cc5`](https://github.com/graphql-hive/console/commit/0cd6cc5606e8cf3c952583feec956c8f024ee615)
|
||||||
|
Thanks [@n1ru4l](https://github.com/n1ru4l)! - Address vulnerability
|
||||||
|
[GHSA-5wm8-gmm8-39j9](https://github.com/advisories/GHSA-5wm8-gmm8-39j9)
|
||||||
|
|
||||||
|
- [#8035](https://github.com/graphql-hive/console/pull/8035)
|
||||||
|
[`0cd6cc5`](https://github.com/graphql-hive/console/commit/0cd6cc5606e8cf3c952583feec956c8f024ee615)
|
||||||
|
Thanks [@n1ru4l](https://github.com/n1ru4l)! - Address vulnerability
|
||||||
|
[GHSA-q3j6-qgpj-74h6](https://github.com/advisories/GHSA-q3j6-qgpj-74h6)
|
||||||
|
|
||||||
## 11.0.4
|
## 11.0.4
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "hive",
|
"name": "hive",
|
||||||
"version": "11.0.4",
|
"version": "11.1.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"generate": "tsx generate.ts",
|
"generate": "tsx generate.ts",
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
"prettier": "3.4.2"
|
"prettier": "3.4.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@graphql-hive/gateway": "^2.1.19",
|
"@graphql-hive/gateway": "2.7.2",
|
||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
"@types/mime-types": "2.1.4",
|
"@types/mime-types": "2.1.4",
|
||||||
"@types/node": "24.12.2",
|
"@types/node": "24.12.2",
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,12 @@ export class Redis {
|
||||||
args: [
|
args: [
|
||||||
'/opt/bitnami/scripts/redis/run.sh',
|
'/opt/bitnami/scripts/redis/run.sh',
|
||||||
`--maxmemory ${memoryInMegabytes}mb`,
|
`--maxmemory ${memoryInMegabytes}mb`,
|
||||||
|
// Once Redis reaches maxmemory (90% of the container limit, see above),
|
||||||
|
// evict the least-recently-used keys instead of failing writes with OOM
|
||||||
|
// errors (the default `noeviction` behaviour). Everything Hive stores in
|
||||||
|
// this Redis is a cache / lock / rate-limit entry with a TTL, so evicting
|
||||||
|
// cold keys is safe and LRU keeps hot keys (locks, fresh counters) around.
|
||||||
|
'--maxmemory-policy allkeys-lru',
|
||||||
// This disables snapshotting to save cpu and reduce latency spikes
|
// This disables snapshotting to save cpu and reduce latency spikes
|
||||||
'--save ""',
|
'--save ""',
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -203,6 +203,8 @@ services:
|
||||||
ENCRYPTION_SECRET: '${HIVE_ENCRYPTION_SECRET}'
|
ENCRYPTION_SECRET: '${HIVE_ENCRYPTION_SECRET}'
|
||||||
WEB_APP_URL: '${HIVE_APP_BASE_URL}'
|
WEB_APP_URL: '${HIVE_APP_BASE_URL}'
|
||||||
PORT: 3001
|
PORT: 3001
|
||||||
|
SERVER_HOST: '${SERVER_HOST:-::}'
|
||||||
|
SERVER_HOST_IPV6_ONLY: '${SERVER_HOST_IPV6_ONLY:-0}'
|
||||||
S3_ENDPOINT: 'http://s3:9000'
|
S3_ENDPOINT: 'http://s3:9000'
|
||||||
S3_ACCESS_KEY_ID: ${MINIO_ROOT_USER}
|
S3_ACCESS_KEY_ID: ${MINIO_ROOT_USER}
|
||||||
S3_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD}
|
S3_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD}
|
||||||
|
|
@ -234,6 +236,8 @@ services:
|
||||||
environment:
|
environment:
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
PORT: 3012
|
PORT: 3012
|
||||||
|
SERVER_HOST: '${SERVER_HOST:-::}'
|
||||||
|
SERVER_HOST_IPV6_ONLY: '${SERVER_HOST_IPV6_ONLY:-0}'
|
||||||
LOG_LEVEL: '${LOG_LEVEL:-debug}'
|
LOG_LEVEL: '${LOG_LEVEL:-debug}'
|
||||||
OPENTELEMETRY_COLLECTOR_ENDPOINT: '${OPENTELEMETRY_COLLECTOR_ENDPOINT:-}'
|
OPENTELEMETRY_COLLECTOR_ENDPOINT: '${OPENTELEMETRY_COLLECTOR_ENDPOINT:-}'
|
||||||
SENTRY: '${SENTRY:-0}'
|
SENTRY: '${SENTRY:-0}'
|
||||||
|
|
@ -250,6 +254,8 @@ services:
|
||||||
environment:
|
environment:
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
PORT: 3002
|
PORT: 3002
|
||||||
|
SERVER_HOST: '${SERVER_HOST:-::}'
|
||||||
|
SERVER_HOST_IPV6_ONLY: '${SERVER_HOST_IPV6_ONLY:-0}'
|
||||||
REDIS_HOST: redis
|
REDIS_HOST: redis
|
||||||
REDIS_PORT: 6379
|
REDIS_PORT: 6379
|
||||||
REDIS_PASSWORD: '${REDIS_PASSWORD}'
|
REDIS_PASSWORD: '${REDIS_PASSWORD}'
|
||||||
|
|
@ -278,6 +284,8 @@ services:
|
||||||
REDIS_PORT: 6379
|
REDIS_PORT: 6379
|
||||||
REDIS_PASSWORD: '${REDIS_PASSWORD}'
|
REDIS_PASSWORD: '${REDIS_PASSWORD}'
|
||||||
PORT: 3003
|
PORT: 3003
|
||||||
|
SERVER_HOST: '${SERVER_HOST:-::}'
|
||||||
|
SERVER_HOST_IPV6_ONLY: '${SERVER_HOST_IPV6_ONLY:-0}'
|
||||||
LOG_LEVEL: '${LOG_LEVEL:-debug}'
|
LOG_LEVEL: '${LOG_LEVEL:-debug}'
|
||||||
OPENTELEMETRY_COLLECTOR_ENDPOINT: '${OPENTELEMETRY_COLLECTOR_ENDPOINT:-}'
|
OPENTELEMETRY_COLLECTOR_ENDPOINT: '${OPENTELEMETRY_COLLECTOR_ENDPOINT:-}'
|
||||||
SENTRY: '${SENTRY:-0}'
|
SENTRY: '${SENTRY:-0}'
|
||||||
|
|
@ -294,6 +302,8 @@ services:
|
||||||
environment:
|
environment:
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
PORT: 3014
|
PORT: 3014
|
||||||
|
SERVER_HOST: '${SERVER_HOST:-::}'
|
||||||
|
SERVER_HOST_IPV6_ONLY: '${SERVER_HOST_IPV6_ONLY:-0}'
|
||||||
POSTGRES_HOST: db
|
POSTGRES_HOST: db
|
||||||
POSTGRES_PORT: 5432
|
POSTGRES_PORT: 5432
|
||||||
POSTGRES_DB: '${POSTGRES_DB}'
|
POSTGRES_DB: '${POSTGRES_DB}'
|
||||||
|
|
@ -340,6 +350,8 @@ services:
|
||||||
REDIS_PORT: 6379
|
REDIS_PORT: 6379
|
||||||
REDIS_PASSWORD: '${REDIS_PASSWORD}'
|
REDIS_PASSWORD: '${REDIS_PASSWORD}'
|
||||||
PORT: 3006
|
PORT: 3006
|
||||||
|
SERVER_HOST: '${SERVER_HOST:-::}'
|
||||||
|
SERVER_HOST_IPV6_ONLY: '${SERVER_HOST_IPV6_ONLY:-0}'
|
||||||
LOG_LEVEL: '${LOG_LEVEL:-debug}'
|
LOG_LEVEL: '${LOG_LEVEL:-debug}'
|
||||||
SENTRY: '${SENTRY:-0}'
|
SENTRY: '${SENTRY:-0}'
|
||||||
SENTRY_DSN: '${SENTRY_DSN:-}'
|
SENTRY_DSN: '${SENTRY_DSN:-}'
|
||||||
|
|
@ -367,6 +379,8 @@ services:
|
||||||
CLICKHOUSE_USERNAME: '${CLICKHOUSE_USER}'
|
CLICKHOUSE_USERNAME: '${CLICKHOUSE_USER}'
|
||||||
CLICKHOUSE_PASSWORD: '${CLICKHOUSE_PASSWORD}'
|
CLICKHOUSE_PASSWORD: '${CLICKHOUSE_PASSWORD}'
|
||||||
PORT: 3007
|
PORT: 3007
|
||||||
|
SERVER_HOST: '${SERVER_HOST:-::}'
|
||||||
|
SERVER_HOST_IPV6_ONLY: '${SERVER_HOST_IPV6_ONLY:-0}'
|
||||||
LOG_LEVEL: '${LOG_LEVEL:-debug}'
|
LOG_LEVEL: '${LOG_LEVEL:-debug}'
|
||||||
SENTRY: '${SENTRY:-0}'
|
SENTRY: '${SENTRY:-0}'
|
||||||
SENTRY_DSN: '${SENTRY_DSN:-}'
|
SENTRY_DSN: '${SENTRY_DSN:-}'
|
||||||
|
|
@ -380,6 +394,8 @@ services:
|
||||||
- 'stack'
|
- 'stack'
|
||||||
environment:
|
environment:
|
||||||
PORT: 3000
|
PORT: 3000
|
||||||
|
SERVER_HOST: '${SERVER_HOST:-::}'
|
||||||
|
SERVER_HOST_IPV6_ONLY: '${SERVER_HOST_IPV6_ONLY:-0}'
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
APP_BASE_URL: '${HIVE_APP_BASE_URL}'
|
APP_BASE_URL: '${HIVE_APP_BASE_URL}'
|
||||||
GRAPHQL_PUBLIC_ENDPOINT: http://localhost:8082/graphql
|
GRAPHQL_PUBLIC_ENDPOINT: http://localhost:8082/graphql
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@
|
||||||
|
|
||||||
Developing Hive locally requires you to have the following software installed locally:
|
Developing Hive locally requires you to have the following software installed locally:
|
||||||
|
|
||||||
- Node.js >=22 (or `nvm` or `fnm`)
|
- Node.js (or `nvm` or `fnm`): check the `package.json` `engines` entry for the correct version
|
||||||
- pnpm >=10.16.0
|
- pnpm: check the `package.json` `engines` entry for the correct version
|
||||||
- Docker version 26.1.1 or later(previous versions will not work correctly on arm64)
|
- Docker version 26.1.1 or later(previous versions will not work correctly on arm64)
|
||||||
- make sure these ports are free: 5432, 6379, 9000, 9001, 8123, 9092, 8081, 8082, 9644, 3567, 7043
|
- make sure these ports are free: 5432, 6379, 9000, 9001, 8123, 9092, 8081, 8082, 9644, 3567, 7043
|
||||||
|
|
||||||
|
|
@ -13,7 +13,7 @@ Developing Hive locally requires you to have the following software installed lo
|
||||||
|
|
||||||
- Clone the repository locally
|
- Clone the repository locally
|
||||||
- Make sure to install the recommended VSCode extensions (defined in `.vscode/extensions.json`)
|
- Make sure to install the recommended VSCode extensions (defined in `.vscode/extensions.json`)
|
||||||
- In the root of the repo, run `nvm use` to use the same version of node as mentioned above
|
- In the root of the repo, run `nvm use` (or `fnm use`) to use the required Node.js version
|
||||||
- Create `.env` file in the root, and use the following:
|
- Create `.env` file in the root, and use the following:
|
||||||
|
|
||||||
```dotenv
|
```dotenv
|
||||||
|
|
@ -42,7 +42,8 @@ Add "user" field to ./docker/docker-compose.dev.yml
|
||||||
- Run `pnpm generate` to generate the typings from the graphql files (use `pnpm graphql:generate` if
|
- Run `pnpm generate` to generate the typings from the graphql files (use `pnpm graphql:generate` if
|
||||||
you only need to run GraphQL Codegen)
|
you only need to run GraphQL Codegen)
|
||||||
- Run `pnpm build` to build all services
|
- Run `pnpm build` to build all services
|
||||||
- Click on `Start Hive` in the bottom bar of VSCode
|
- Click on `Start Hive` in the bottom bar of VSCode (alternatively you can manually start the
|
||||||
|
services you need)
|
||||||
- Open the UI (`http://localhost:3000` by default) and Sign in with any of the identity provider
|
- Open the UI (`http://localhost:3000` by default) and Sign in with any of the identity provider
|
||||||
- Once this is done, you should be able to log in and use the project
|
- Once this is done, you should be able to log in and use the project
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { createHive } from '@graphql-hive/core';
|
||||||
import { psql } from '@hive/postgres';
|
import { psql } from '@hive/postgres';
|
||||||
import { clickHouseInsert } from '../../testkit/clickhouse';
|
import { clickHouseInsert } from '../../testkit/clickhouse';
|
||||||
import { graphql } from '../../testkit/gql';
|
import { graphql } from '../../testkit/gql';
|
||||||
|
import { ResourceAssignmentModeType } from '../../testkit/gql/graphql';
|
||||||
import { execute } from '../../testkit/graphql';
|
import { execute } from '../../testkit/graphql';
|
||||||
|
|
||||||
const CreateAppDeployment = graphql(`
|
const CreateAppDeployment = graphql(`
|
||||||
|
|
@ -1742,8 +1743,9 @@ test('retire app deployments fails without feature flag enabled for organization
|
||||||
});
|
});
|
||||||
|
|
||||||
test('get app deployment documents via GraphQL API', async () => {
|
test('get app deployment documents via GraphQL API', async () => {
|
||||||
const { createOrg, ownerToken } = await initSeed().createOwner();
|
const { createOrg } = await initSeed().createOwner();
|
||||||
const { createProject, setFeatureFlag, organization } = await createOrg();
|
const { createProject, setFeatureFlag, organization, createOrganizationAccessToken } =
|
||||||
|
await createOrg();
|
||||||
await setFeatureFlag('appDeployments', true);
|
await setFeatureFlag('appDeployments', true);
|
||||||
const { createTargetAccessToken, project, target } = await createProject();
|
const { createTargetAccessToken, project, target } = await createProject();
|
||||||
const token = await createTargetAccessToken({});
|
const token = await createTargetAccessToken({});
|
||||||
|
|
@ -1771,12 +1773,23 @@ test('get app deployment documents via GraphQL API', async () => {
|
||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Ensure this is possible with the minimal available permissions.
|
||||||
|
const organizationAccessToken = await createOrganizationAccessToken({
|
||||||
|
permissions: ['appDeployment:create'],
|
||||||
|
resources: {
|
||||||
|
mode: ResourceAssignmentModeType.All,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const { addDocumentsToAppDeployment } = await execute({
|
const { addDocumentsToAppDeployment } = await execute({
|
||||||
document: AddDocumentsToAppDeployment,
|
document: AddDocumentsToAppDeployment,
|
||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
appName: 'app-name',
|
appName: 'app-name',
|
||||||
appVersion: 'app-version',
|
appVersion: 'app-version',
|
||||||
|
target: {
|
||||||
|
byId: target.id,
|
||||||
|
},
|
||||||
documents: [
|
documents: [
|
||||||
{
|
{
|
||||||
hash: 'aaa',
|
hash: 'aaa',
|
||||||
|
|
@ -1797,7 +1810,7 @@ test('get app deployment documents via GraphQL API', async () => {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
authToken: token.secret,
|
authToken: organizationAccessToken.privateAccessKey,
|
||||||
}).then(res => res.expectNoGraphQLErrors());
|
}).then(res => res.expectNoGraphQLErrors());
|
||||||
expect(addDocumentsToAppDeployment.error).toBeNull();
|
expect(addDocumentsToAppDeployment.error).toBeNull();
|
||||||
|
|
||||||
|
|
@ -1812,7 +1825,7 @@ test('get app deployment documents via GraphQL API', async () => {
|
||||||
appDeploymentName: 'app-name',
|
appDeploymentName: 'app-name',
|
||||||
appDeploymentVersion: 'app-version',
|
appDeploymentVersion: 'app-version',
|
||||||
},
|
},
|
||||||
authToken: ownerToken,
|
authToken: token.secret,
|
||||||
}).then(res => res.expectNoGraphQLErrors());
|
}).then(res => res.expectNoGraphQLErrors());
|
||||||
expect(result.target).toMatchObject({
|
expect(result.target).toMatchObject({
|
||||||
appDeployment: {
|
appDeployment: {
|
||||||
|
|
@ -2157,6 +2170,137 @@ test('app deployment usage reporting', async () => {
|
||||||
expect(data.target?.appDeployment?.lastUsed).toEqual(expect.any(String));
|
expect(data.target?.appDeployment?.lastUsed).toEqual(expect.any(String));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('app deployment manifest is written to and accessible via CDN', async () => {
|
||||||
|
const { createOrg } = await initSeed().createOwner();
|
||||||
|
const { createProject, setFeatureFlag } = await createOrg();
|
||||||
|
await setFeatureFlag('appDeployments', true);
|
||||||
|
const { createTargetAccessToken, createCdnAccess } = await createProject();
|
||||||
|
const token = await createTargetAccessToken({});
|
||||||
|
|
||||||
|
const { createAppDeployment } = await execute({
|
||||||
|
document: CreateAppDeployment,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
appName: 'app-name',
|
||||||
|
appVersion: 'app-version',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
authToken: token.secret,
|
||||||
|
}).then(res => res.expectNoGraphQLErrors());
|
||||||
|
expect(createAppDeployment.error).toBeNull();
|
||||||
|
|
||||||
|
await token.publishSchema({
|
||||||
|
sdl: /* GraphQL */ `
|
||||||
|
type Query {
|
||||||
|
a: String
|
||||||
|
b: String
|
||||||
|
c: String
|
||||||
|
d: String
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { addDocumentsToAppDeployment } = await execute({
|
||||||
|
document: AddDocumentsToAppDeployment,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
appName: 'app-name',
|
||||||
|
appVersion: 'app-version',
|
||||||
|
documents: [
|
||||||
|
{
|
||||||
|
hash: 'aaa',
|
||||||
|
body: 'query { a }',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hash: 'bbb',
|
||||||
|
body: 'query { b }',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hash: 'ccc',
|
||||||
|
body: 'query { c }',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hash: 'ddd',
|
||||||
|
body: 'query { d }',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
authToken: token.secret,
|
||||||
|
}).then(res => res.expectNoGraphQLErrors());
|
||||||
|
expect(addDocumentsToAppDeployment.error).toBeNull();
|
||||||
|
|
||||||
|
const cdnAccess = await createCdnAccess();
|
||||||
|
const persistedOperationUrl = `${cdnAccess.cdnUrl}/apps/app-name/app-version`;
|
||||||
|
|
||||||
|
let response = await fetch(persistedOperationUrl, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'X-Hive-CDN-Key': cdnAccess.secretAccessToken,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// before the app deployment is activated it shall not exist.
|
||||||
|
expect(response.status).toEqual(404);
|
||||||
|
|
||||||
|
const { activateAppDeployment } = await execute({
|
||||||
|
document: ActivateAppDeployment,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
appName: 'app-name',
|
||||||
|
appVersion: 'app-version',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
authToken: token.secret,
|
||||||
|
}).then(res => res.expectNoGraphQLErrors());
|
||||||
|
expect(activateAppDeployment.error).toBeNull();
|
||||||
|
|
||||||
|
response = await fetch(persistedOperationUrl, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'X-Hive-CDN-Key': cdnAccess.secretAccessToken,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(response.status).toEqual(200);
|
||||||
|
let manifest = await response.json();
|
||||||
|
expect(manifest).toMatchObject({
|
||||||
|
appName: 'app-name',
|
||||||
|
appVersion: 'app-version',
|
||||||
|
documentHashes: ['aaa', 'bbb', 'ccc', 'ddd'],
|
||||||
|
id: expect.any(String),
|
||||||
|
isActive: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Retire flow.
|
||||||
|
|
||||||
|
const { retireAppDeployment } = await execute({
|
||||||
|
document: RetireAppDeployment,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
appName: 'app-name',
|
||||||
|
appVersion: 'app-version',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
authToken: token.secret,
|
||||||
|
}).then(res => res.expectNoGraphQLErrors());
|
||||||
|
expect(retireAppDeployment.error).toBeNull();
|
||||||
|
|
||||||
|
response = await fetch(persistedOperationUrl, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'X-Hive-CDN-Key': cdnAccess.secretAccessToken,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(response.status).toEqual(200);
|
||||||
|
manifest = await response.json();
|
||||||
|
expect(manifest).toMatchObject({
|
||||||
|
appName: 'app-name',
|
||||||
|
appVersion: 'app-version',
|
||||||
|
documentHashes: ['aaa', 'bbb', 'ccc', 'ddd'],
|
||||||
|
id: expect.any(String),
|
||||||
|
isActive: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('activeAppDeployments returns empty list when no active deployments exist', async () => {
|
test('activeAppDeployments returns empty list when no active deployments exist', async () => {
|
||||||
const { createOrg, ownerToken } = await initSeed().createOwner();
|
const { createOrg, ownerToken } = await initSeed().createOwner();
|
||||||
const { createProject, setFeatureFlag, organization } = await createOrg();
|
const { createProject, setFeatureFlag, organization } = await createOrg();
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import {
|
||||||
ResourceAssignmentModeType,
|
ResourceAssignmentModeType,
|
||||||
RuleInstanceSeverityLevel,
|
RuleInstanceSeverityLevel,
|
||||||
} from 'testkit/gql/graphql';
|
} from 'testkit/gql/graphql';
|
||||||
|
import { SchemaVersionStore } from '@hive/api/modules/schema/providers/schema-version-store';
|
||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
import { createStorage } from '@hive/storage';
|
import { createStorage } from '@hive/storage';
|
||||||
import { graphql } from '../../../testkit/gql';
|
import { graphql } from '../../../testkit/gql';
|
||||||
|
|
@ -2103,7 +2104,8 @@ test.concurrent(
|
||||||
|
|
||||||
const conn = connectionString();
|
const conn = connectionString();
|
||||||
const storage = await createStorage(conn, 2);
|
const storage = await createStorage(conn, 2);
|
||||||
await storage.createVersion({
|
const schemaVersions = new SchemaVersionStore(storage.pool);
|
||||||
|
await schemaVersions.createSchemaVersion({
|
||||||
schema: brokenSdl,
|
schema: brokenSdl,
|
||||||
author: 'Jochen',
|
author: 'Jochen',
|
||||||
async actionFn() {},
|
async actionFn() {},
|
||||||
|
|
@ -2113,7 +2115,6 @@ test.concurrent(
|
||||||
compositeSchemaSDL: null,
|
compositeSchemaSDL: null,
|
||||||
conditionalBreakingChangeMetadata: null,
|
conditionalBreakingChangeMetadata: null,
|
||||||
contracts: null,
|
contracts: null,
|
||||||
coordinatesDiff: null,
|
|
||||||
diffSchemaVersionId: null,
|
diffSchemaVersionId: null,
|
||||||
github: null,
|
github: null,
|
||||||
metadata: null,
|
metadata: null,
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { ProjectType } from 'testkit/gql/graphql';
|
||||||
import { initSeed } from 'testkit/seed';
|
import { initSeed } from 'testkit/seed';
|
||||||
import { assertNonNull, getServiceHost } from 'testkit/utils';
|
import { assertNonNull, getServiceHost } from 'testkit/utils';
|
||||||
import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
|
import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
|
||||||
|
import { SchemaVersionStore } from '@hive/api/modules/schema/providers/schema-version-store';
|
||||||
import { createStorage } from '@hive/storage';
|
import { createStorage } from '@hive/storage';
|
||||||
import { sortSDL } from '@theguild/federation-composition';
|
import { sortSDL } from '@theguild/federation-composition';
|
||||||
|
|
||||||
|
|
@ -132,6 +133,7 @@ test.concurrent(
|
||||||
|
|
||||||
try {
|
try {
|
||||||
storage = await createStorage(connectionString(), 1);
|
storage = await createStorage(connectionString(), 1);
|
||||||
|
const schemaVersions = new SchemaVersionStore(storage.pool);
|
||||||
const { createOrg } = await initSeed().createOwner();
|
const { createOrg } = await initSeed().createOwner();
|
||||||
const { createProject, organization } = await createOrg();
|
const { createProject, organization } = await createOrg();
|
||||||
const { createTargetAccessToken, project, target } = await createProject(
|
const { createTargetAccessToken, project, target } = await createProject(
|
||||||
|
|
@ -171,11 +173,7 @@ test.concurrent(
|
||||||
.then(r => r.expectNoGraphQLErrors());
|
.then(r => r.expectNoGraphQLErrors());
|
||||||
expect(deleteServiceResult.schemaDelete.__typename).toBe('SchemaDeleteSuccess');
|
expect(deleteServiceResult.schemaDelete.__typename).toBe('SchemaDeleteSuccess');
|
||||||
|
|
||||||
const latestVersion = await storage.getMaybeLatestVersion({
|
const latestVersion = await schemaVersions.getMaybeLatestSchemaVersionForTargetId(target.id);
|
||||||
targetId: target.id,
|
|
||||||
projectId: project.id,
|
|
||||||
organizationId: organization.id,
|
|
||||||
});
|
|
||||||
assertNonNull(latestVersion);
|
assertNonNull(latestVersion);
|
||||||
|
|
||||||
expect(latestVersion.compositeSchemaSDL).toMatchInlineSnapshot(`
|
expect(latestVersion.compositeSchemaSDL).toMatchInlineSnapshot(`
|
||||||
|
|
@ -187,9 +185,7 @@ test.concurrent(
|
||||||
expect(latestVersion.hasPersistedSchemaChanges).toEqual(true);
|
expect(latestVersion.hasPersistedSchemaChanges).toEqual(true);
|
||||||
expect(latestVersion.isComposable).toEqual(true);
|
expect(latestVersion.isComposable).toEqual(true);
|
||||||
|
|
||||||
const changes = await storage.getSchemaChangesForVersion({
|
const changes = await schemaVersions.getSchemaSchangesForSchemaVersion(latestVersion);
|
||||||
versionId: latestVersion.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (Array.isArray(changes) === false) {
|
if (Array.isArray(changes) === false) {
|
||||||
throw new Error('No changes were persisted in the database.');
|
throw new Error('No changes were persisted in the database.');
|
||||||
|
|
@ -229,6 +225,7 @@ test.concurrent(
|
||||||
|
|
||||||
try {
|
try {
|
||||||
storage = await createStorage(connectionString(), 1);
|
storage = await createStorage(connectionString(), 1);
|
||||||
|
const schemaVersions = new SchemaVersionStore(storage.pool);
|
||||||
const { createOrg, ownerToken } = await initSeed().createOwner();
|
const { createOrg, ownerToken } = await initSeed().createOwner();
|
||||||
const { createProject, organization } = await createOrg();
|
const { createProject, organization } = await createOrg();
|
||||||
const { createTargetAccessToken, project, target, setNativeFederation } = await createProject(
|
const { createTargetAccessToken, project, target, setNativeFederation } = await createProject(
|
||||||
|
|
@ -311,11 +308,7 @@ test.concurrent(
|
||||||
.then(r => r.expectNoGraphQLErrors());
|
.then(r => r.expectNoGraphQLErrors());
|
||||||
expect(deleteServiceResult.schemaDelete.__typename).toBe('SchemaDeleteSuccess');
|
expect(deleteServiceResult.schemaDelete.__typename).toBe('SchemaDeleteSuccess');
|
||||||
|
|
||||||
const latestVersion = await storage.getMaybeLatestVersion({
|
const latestVersion = await schemaVersions.getMaybeLatestSchemaVersionForTargetId(target.id);
|
||||||
targetId: target.id,
|
|
||||||
projectId: project.id,
|
|
||||||
organizationId: organization.id,
|
|
||||||
});
|
|
||||||
assertNonNull(latestVersion);
|
assertNonNull(latestVersion);
|
||||||
|
|
||||||
expect(latestVersion.compositeSchemaSDL).toEqual(null);
|
expect(latestVersion.compositeSchemaSDL).toEqual(null);
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { ProjectType } from 'testkit/gql/graphql';
|
||||||
import { execute } from 'testkit/graphql';
|
import { execute } from 'testkit/graphql';
|
||||||
import { assertNonNull, getServiceHost } from 'testkit/utils';
|
import { assertNonNull, getServiceHost } from 'testkit/utils';
|
||||||
import z from 'zod';
|
import z from 'zod';
|
||||||
|
import { SchemaVersionStore } from '@hive/api/modules/schema/providers/schema-version-store';
|
||||||
import { createPostgresDatabasePool, psql } from '@hive/postgres';
|
import { createPostgresDatabasePool, psql } from '@hive/postgres';
|
||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
import { createStorage } from '@hive/storage';
|
import { createStorage } from '@hive/storage';
|
||||||
|
|
@ -627,16 +628,12 @@ describe('schema publishing changes are persisted', () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const latestVersion = await storage.getMaybeLatestVersion({
|
const schemaVersions = new SchemaVersionStore(storage.pool);
|
||||||
targetId: target.id,
|
|
||||||
projectId: project.id,
|
const latestVersion = await schemaVersions.getMaybeLatestSchemaVersionForTargetId(target.id);
|
||||||
organizationId: organization.id,
|
|
||||||
});
|
|
||||||
assertNonNull(latestVersion);
|
assertNonNull(latestVersion);
|
||||||
|
|
||||||
const changes = await storage.getSchemaChangesForVersion({
|
const changes = await schemaVersions.getSchemaSchangesForSchemaVersion(latestVersion);
|
||||||
versionId: latestVersion.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!Array.isArray(changes)) {
|
if (!Array.isArray(changes)) {
|
||||||
throw new Error('Expected changes to be an array');
|
throw new Error('Expected changes to be an array');
|
||||||
|
|
@ -3263,6 +3260,7 @@ const SchemaCompareToPreviousVersionQuery = graphql(`
|
||||||
|
|
||||||
test('Target.schemaVersion: result is read from the database', async () => {
|
test('Target.schemaVersion: result is read from the database', async () => {
|
||||||
const storage = await createStorage(connectionString(), 1);
|
const storage = await createStorage(connectionString(), 1);
|
||||||
|
const schemaVersions = new SchemaVersionStore(storage.pool);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const serviceName = {
|
const serviceName = {
|
||||||
|
|
@ -3305,11 +3303,7 @@ test('Target.schemaVersion: result is read from the database', async () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const latestVersion = await storage.getMaybeLatestVersion({
|
const latestVersion = await schemaVersions.getMaybeLatestSchemaVersionForTargetId(target.id);
|
||||||
targetId: target.id,
|
|
||||||
projectId: project.id,
|
|
||||||
organizationId: organization.id,
|
|
||||||
});
|
|
||||||
assertNonNull(latestVersion);
|
assertNonNull(latestVersion);
|
||||||
|
|
||||||
const result = await execute({
|
const result = await execute({
|
||||||
|
|
@ -3346,6 +3340,7 @@ test('Target.schemaVersion: result is read from the database', async () => {
|
||||||
|
|
||||||
test('Composition Error (Federation 2) can be served from the database', async () => {
|
test('Composition Error (Federation 2) can be served from the database', async () => {
|
||||||
const storage = await createStorage(connectionString(), 1);
|
const storage = await createStorage(connectionString(), 1);
|
||||||
|
const schemaVersions = new SchemaVersionStore(storage.pool);
|
||||||
const serviceAddress = await getServiceHost('composition_federation_2', 3069, false);
|
const serviceAddress = await getServiceHost('composition_federation_2', 3069, false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -3444,11 +3439,7 @@ test('Composition Error (Federation 2) can be served from the database', async (
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const latestVersion = await storage.getMaybeLatestVersion({
|
const latestVersion = await schemaVersions.getMaybeLatestSchemaVersionForTargetId(target.id);
|
||||||
targetId: target.id,
|
|
||||||
projectId: project.id,
|
|
||||||
organizationId: organization.id,
|
|
||||||
});
|
|
||||||
assertNonNull(latestVersion);
|
assertNonNull(latestVersion);
|
||||||
|
|
||||||
const result = await execute({
|
const result = await execute({
|
||||||
|
|
@ -3476,6 +3467,7 @@ test('Composition Error (Federation 2) can be served from the database', async (
|
||||||
|
|
||||||
test('Composition Network Failure (Federation 2)', async () => {
|
test('Composition Network Failure (Federation 2)', async () => {
|
||||||
const storage = await createStorage(connectionString(), 1);
|
const storage = await createStorage(connectionString(), 1);
|
||||||
|
const schemaVersions = new SchemaVersionStore(storage.pool);
|
||||||
const serviceAddress = await getServiceHost('composition_federation_2', 3069, false);
|
const serviceAddress = await getServiceHost('composition_federation_2', 3069, false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -3610,11 +3602,7 @@ test('Composition Network Failure (Federation 2)', async () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const latestVersion = await storage.getMaybeLatestVersion({
|
const latestVersion = await schemaVersions.getMaybeLatestSchemaVersionForTargetId(target.id);
|
||||||
targetId: target.id,
|
|
||||||
projectId: project.id,
|
|
||||||
organizationId: organization.id,
|
|
||||||
});
|
|
||||||
assertNonNull(latestVersion);
|
assertNonNull(latestVersion);
|
||||||
|
|
||||||
const result = await execute({
|
const result = await execute({
|
||||||
|
|
@ -4543,7 +4531,8 @@ test.concurrent(
|
||||||
|
|
||||||
const conn = connectionString();
|
const conn = connectionString();
|
||||||
const storage = await createStorage(conn, 2);
|
const storage = await createStorage(conn, 2);
|
||||||
await storage.createVersion({
|
const schemaVersions = new SchemaVersionStore(storage.pool);
|
||||||
|
await schemaVersions.createSchemaVersion({
|
||||||
schema: brokenSdl,
|
schema: brokenSdl,
|
||||||
author: 'Jochen',
|
author: 'Jochen',
|
||||||
async actionFn() {},
|
async actionFn() {},
|
||||||
|
|
@ -4553,7 +4542,6 @@ test.concurrent(
|
||||||
compositeSchemaSDL: null,
|
compositeSchemaSDL: null,
|
||||||
conditionalBreakingChangeMetadata: null,
|
conditionalBreakingChangeMetadata: null,
|
||||||
contracts: null,
|
contracts: null,
|
||||||
coordinatesDiff: null,
|
|
||||||
diffSchemaVersionId: null,
|
diffSchemaVersionId: null,
|
||||||
github: null,
|
github: null,
|
||||||
metadata: null,
|
metadata: null,
|
||||||
|
|
|
||||||
11
package.json
11
package.json
|
|
@ -12,10 +12,10 @@
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "pnpm@10.18.3+sha512.bbd16e6d7286fd7e01f6b3c0b3c932cda2965c06a908328f74663f10a9aea51f1129eea615134bf992831b009eabe167ecb7008b597f40ff9bc75946aadfb08d",
|
"packageManager": "pnpm@10.33.2+sha512.a90faf6feeab71ad6c6e57f94e0fe1a12f5dcc22cd754db40ae9593eb6a3e0b6b12e3540218bb37ae083404b1f2ce6db2a4121e979829b4aff94b99f49da1cf8",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=24.14.1",
|
"node": ">=24.14.1",
|
||||||
"pnpm": ">=10.16.0"
|
"pnpm": ">=10.33.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "pnpm turbo build --filter='@hive/app' --concurrency=1 --color && pnpm turbo build --concurrency=4 --color --filter '!@hive/app'",
|
"build": "pnpm turbo build --filter='@hive/app' --concurrency=1 --color && pnpm turbo build --concurrency=4 --color --filter '!@hive/app'",
|
||||||
|
|
@ -73,7 +73,7 @@
|
||||||
"@graphql-codegen/urql-introspection": "3.0.1",
|
"@graphql-codegen/urql-introspection": "3.0.1",
|
||||||
"@graphql-eslint/eslint-plugin": "3.20.1",
|
"@graphql-eslint/eslint-plugin": "3.20.1",
|
||||||
"@graphql-inspector/cli": "6.0.6",
|
"@graphql-inspector/cli": "6.0.6",
|
||||||
"@graphql-inspector/core": "7.1.2",
|
"@graphql-inspector/core": "7.1.3",
|
||||||
"@graphql-inspector/patch": "0.1.3",
|
"@graphql-inspector/patch": "0.1.3",
|
||||||
"@graphql-tools/load": "8.1.2",
|
"@graphql-tools/load": "8.1.2",
|
||||||
"@manypkg/get-packages": "2.2.2",
|
"@manypkg/get-packages": "2.2.2",
|
||||||
|
|
@ -128,6 +128,7 @@
|
||||||
"overrides.ajv@8.x.x": "address https://github.com/graphql-hive/console/security/dependabot/507",
|
"overrides.ajv@8.x.x": "address https://github.com/graphql-hive/console/security/dependabot/507",
|
||||||
"overrides.yauzl@2.x.x": "address https://github.com/graphql-hive/console/security/dependabot/542",
|
"overrides.yauzl@2.x.x": "address https://github.com/graphql-hive/console/security/dependabot/542",
|
||||||
"overrides.path-to-regexp@0.x.x": "address https://github.com/graphql-hive/console/security/dependabot/619",
|
"overrides.path-to-regexp@0.x.x": "address https://github.com/graphql-hive/console/security/dependabot/619",
|
||||||
|
"overrides.fast-uri@2.x.x": "address https://github.com/graphql-hive/console/security/dependabot/683",
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"esbuild": "0.25.9",
|
"esbuild": "0.25.9",
|
||||||
"csstype": "3.1.2",
|
"csstype": "3.1.2",
|
||||||
|
|
@ -163,7 +164,9 @@
|
||||||
"ajv@8.x.x": "^8.18.0",
|
"ajv@8.x.x": "^8.18.0",
|
||||||
"yauzl@2.x.x": "^3.2.1",
|
"yauzl@2.x.x": "^3.2.1",
|
||||||
"glob@10.x.x": "^10.5.0",
|
"glob@10.x.x": "^10.5.0",
|
||||||
"path-to-regexp@0.x.x": "^0.1.13"
|
"path-to-regexp@0.x.x": "^0.1.13",
|
||||||
|
"fast-uri@2.x.x": "3.x.x",
|
||||||
|
"protobufjs@8.x.x": "^8.0.2"
|
||||||
},
|
},
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"mjml-core@4.14.0": "patches/mjml-core@4.14.0.patch",
|
"mjml-core@4.14.0": "patches/mjml-core@4.14.0.patch",
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,20 @@ function psqlFn(template: TemplateStringsArray, ...values: ValueExpression[]) {
|
||||||
return tag.type(z.unknown())(template, ...values);
|
return tag.type(z.unknown())(template, ...values);
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.assign(psqlFn, createSqlTag());
|
/**
|
||||||
|
* Small helper utility for jsonifying a nullable object.
|
||||||
|
*/
|
||||||
|
function jsonbOrNull<T>(obj: T | null | undefined) {
|
||||||
|
if (obj == null) return null;
|
||||||
|
return psqlFn`${JSON.stringify(obj)}::jsonb`;
|
||||||
|
}
|
||||||
|
|
||||||
export const psql = psqlFn as any as SqlTag<any> & CallableTag;
|
Object.assign(psqlFn, tag, {
|
||||||
|
jsonbOrNull,
|
||||||
|
});
|
||||||
|
|
||||||
|
type UtilityExtensions = {
|
||||||
|
jsonbOrNull: typeof jsonbOrNull;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const psql = psqlFn as any as SqlTag<any> & CallableTag & UtilityExtensions;
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@graphql-hive/core": "workspace:*",
|
"@graphql-hive/core": "workspace:*",
|
||||||
"@graphql-hive/logger": "^1.0.9"
|
"@graphql-hive/logger": "^1.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@apollo/server": "5.5.0",
|
"@apollo/server": "5.5.0",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,14 @@
|
||||||
# @graphql-hive/cli
|
# @graphql-hive/cli
|
||||||
|
|
||||||
|
## 0.59.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#8035](https://github.com/graphql-hive/console/pull/8035)
|
||||||
|
[`0cd6cc5`](https://github.com/graphql-hive/console/commit/0cd6cc5606e8cf3c952583feec956c8f024ee615)
|
||||||
|
Thanks [@n1ru4l](https://github.com/n1ru4l)! - Address vulnerability
|
||||||
|
[GHSA-rpmf-866q-6p89](https://github.com/advisories/GHSA-rpmf-866q-6p89)
|
||||||
|
|
||||||
## 0.59.1
|
## 0.59.1
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ DESCRIPTION
|
||||||
```
|
```
|
||||||
|
|
||||||
_See code:
|
_See code:
|
||||||
[src/commands/app/create.ts](https://github.com/graphql-hive/platform/blob/v0.59.1/src/commands/app/create.ts)_
|
[src/commands/app/create.ts](https://github.com/graphql-hive/platform/blob/v0.59.2/src/commands/app/create.ts)_
|
||||||
|
|
||||||
## `hive app:publish`
|
## `hive app:publish`
|
||||||
|
|
||||||
|
|
@ -108,7 +108,7 @@ DESCRIPTION
|
||||||
```
|
```
|
||||||
|
|
||||||
_See code:
|
_See code:
|
||||||
[src/commands/app/publish.ts](https://github.com/graphql-hive/platform/blob/v0.59.1/src/commands/app/publish.ts)_
|
[src/commands/app/publish.ts](https://github.com/graphql-hive/platform/blob/v0.59.2/src/commands/app/publish.ts)_
|
||||||
|
|
||||||
## `hive app:retire`
|
## `hive app:retire`
|
||||||
|
|
||||||
|
|
@ -136,7 +136,7 @@ DESCRIPTION
|
||||||
```
|
```
|
||||||
|
|
||||||
_See code:
|
_See code:
|
||||||
[src/commands/app/retire.ts](https://github.com/graphql-hive/platform/blob/v0.59.1/src/commands/app/retire.ts)_
|
[src/commands/app/retire.ts](https://github.com/graphql-hive/platform/blob/v0.59.2/src/commands/app/retire.ts)_
|
||||||
|
|
||||||
## `hive artifact:fetch`
|
## `hive artifact:fetch`
|
||||||
|
|
||||||
|
|
@ -160,7 +160,7 @@ DESCRIPTION
|
||||||
```
|
```
|
||||||
|
|
||||||
_See code:
|
_See code:
|
||||||
[src/commands/artifact/fetch.ts](https://github.com/graphql-hive/platform/blob/v0.59.1/src/commands/artifact/fetch.ts)_
|
[src/commands/artifact/fetch.ts](https://github.com/graphql-hive/platform/blob/v0.59.2/src/commands/artifact/fetch.ts)_
|
||||||
|
|
||||||
## `hive dev`
|
## `hive dev`
|
||||||
|
|
||||||
|
|
@ -203,7 +203,7 @@ DESCRIPTION
|
||||||
```
|
```
|
||||||
|
|
||||||
_See code:
|
_See code:
|
||||||
[src/commands/dev.ts](https://github.com/graphql-hive/platform/blob/v0.59.1/src/commands/dev.ts)_
|
[src/commands/dev.ts](https://github.com/graphql-hive/platform/blob/v0.59.2/src/commands/dev.ts)_
|
||||||
|
|
||||||
## `hive help [COMMAND]`
|
## `hive help [COMMAND]`
|
||||||
|
|
||||||
|
|
@ -249,7 +249,7 @@ DESCRIPTION
|
||||||
```
|
```
|
||||||
|
|
||||||
_See code:
|
_See code:
|
||||||
[src/commands/introspect.ts](https://github.com/graphql-hive/platform/blob/v0.59.1/src/commands/introspect.ts)_
|
[src/commands/introspect.ts](https://github.com/graphql-hive/platform/blob/v0.59.2/src/commands/introspect.ts)_
|
||||||
|
|
||||||
## `hive operations:check FILE`
|
## `hive operations:check FILE`
|
||||||
|
|
||||||
|
|
@ -308,7 +308,7 @@ DESCRIPTION
|
||||||
```
|
```
|
||||||
|
|
||||||
_See code:
|
_See code:
|
||||||
[src/commands/operations/check.ts](https://github.com/graphql-hive/platform/blob/v0.59.1/src/commands/operations/check.ts)_
|
[src/commands/operations/check.ts](https://github.com/graphql-hive/platform/blob/v0.59.2/src/commands/operations/check.ts)_
|
||||||
|
|
||||||
## `hive schema:check FILE`
|
## `hive schema:check FILE`
|
||||||
|
|
||||||
|
|
@ -353,7 +353,7 @@ DESCRIPTION
|
||||||
```
|
```
|
||||||
|
|
||||||
_See code:
|
_See code:
|
||||||
[src/commands/schema/check.ts](https://github.com/graphql-hive/platform/blob/v0.59.1/src/commands/schema/check.ts)_
|
[src/commands/schema/check.ts](https://github.com/graphql-hive/platform/blob/v0.59.2/src/commands/schema/check.ts)_
|
||||||
|
|
||||||
## `hive schema:delete SERVICE`
|
## `hive schema:delete SERVICE`
|
||||||
|
|
||||||
|
|
@ -385,7 +385,7 @@ DESCRIPTION
|
||||||
```
|
```
|
||||||
|
|
||||||
_See code:
|
_See code:
|
||||||
[src/commands/schema/delete.ts](https://github.com/graphql-hive/platform/blob/v0.59.1/src/commands/schema/delete.ts)_
|
[src/commands/schema/delete.ts](https://github.com/graphql-hive/platform/blob/v0.59.2/src/commands/schema/delete.ts)_
|
||||||
|
|
||||||
## `hive schema:fetch [COMMIT]`
|
## `hive schema:fetch [COMMIT]`
|
||||||
|
|
||||||
|
|
@ -418,7 +418,7 @@ DESCRIPTION
|
||||||
```
|
```
|
||||||
|
|
||||||
_See code:
|
_See code:
|
||||||
[src/commands/schema/fetch.ts](https://github.com/graphql-hive/platform/blob/v0.59.1/src/commands/schema/fetch.ts)_
|
[src/commands/schema/fetch.ts](https://github.com/graphql-hive/platform/blob/v0.59.2/src/commands/schema/fetch.ts)_
|
||||||
|
|
||||||
## `hive schema:publish FILE`
|
## `hive schema:publish FILE`
|
||||||
|
|
||||||
|
|
@ -462,7 +462,7 @@ DESCRIPTION
|
||||||
```
|
```
|
||||||
|
|
||||||
_See code:
|
_See code:
|
||||||
[src/commands/schema/publish.ts](https://github.com/graphql-hive/platform/blob/v0.59.1/src/commands/schema/publish.ts)_
|
[src/commands/schema/publish.ts](https://github.com/graphql-hive/platform/blob/v0.59.2/src/commands/schema/publish.ts)_
|
||||||
|
|
||||||
## `hive update [CHANNEL]`
|
## `hive update [CHANNEL]`
|
||||||
|
|
||||||
|
|
@ -525,7 +525,7 @@ DESCRIPTION
|
||||||
```
|
```
|
||||||
|
|
||||||
_See code:
|
_See code:
|
||||||
[src/commands/whoami.ts](https://github.com/graphql-hive/platform/blob/v0.59.1/src/commands/whoami.ts)_
|
[src/commands/whoami.ts](https://github.com/graphql-hive/platform/blob/v0.59.2/src/commands/whoami.ts)_
|
||||||
|
|
||||||
<!-- commandsstop -->
|
<!-- commandsstop -->
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@graphql-hive/cli",
|
"name": "@graphql-hive/cli",
|
||||||
"version": "0.59.1",
|
"version": "0.59.2",
|
||||||
"description": "A CLI util to manage and control your GraphQL Hive",
|
"description": "A CLI util to manage and control your GraphQL Hive",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -49,7 +49,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@graphql-hive/core": "workspace:*",
|
"@graphql-hive/core": "workspace:*",
|
||||||
"@graphql-inspector/core": "7.1.2",
|
"@graphql-inspector/core": "7.1.3",
|
||||||
"@graphql-tools/code-file-loader": "~8.1.0",
|
"@graphql-tools/code-file-loader": "~8.1.0",
|
||||||
"@graphql-tools/graphql-file-loader": "~8.1.0",
|
"@graphql-tools/graphql-file-loader": "~8.1.0",
|
||||||
"@graphql-tools/json-file-loader": "~8.0.0",
|
"@graphql-tools/json-file-loader": "~8.0.0",
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@
|
||||||
"graphql": "^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0"
|
"graphql": "^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@graphql-hive/logger": "^1.0.9",
|
"@graphql-hive/logger": "^1.1.0",
|
||||||
"@graphql-hive/signal": "^2.0.0",
|
"@graphql-hive/signal": "^2.0.0",
|
||||||
"@graphql-tools/utils": "^10.0.0",
|
"@graphql-tools/utils": "^10.0.0",
|
||||||
"@whatwg-node/fetch": "^0.10.13",
|
"@whatwg-node/fetch": "^0.10.13",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,14 @@
|
||||||
# @graphql-hive/laboratory
|
# @graphql-hive/laboratory
|
||||||
|
|
||||||
|
## 0.1.8
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#8024](https://github.com/graphql-hive/console/pull/8024)
|
||||||
|
[`0e3ce40`](https://github.com/graphql-hive/console/commit/0e3ce400706c625925161f8d59cc5691380cef07)
|
||||||
|
Thanks [@mskorokhodov](https://github.com/mskorokhodov)! - Hive laboratory introspection query to
|
||||||
|
include active tab headers
|
||||||
|
|
||||||
## 0.1.7
|
## 0.1.7
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@graphql-hive/laboratory",
|
"name": "@graphql-hive/laboratory",
|
||||||
"version": "0.1.7",
|
"version": "0.1.8",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "./dist/hive-laboratory.cjs.js",
|
"main": "./dist/hive-laboratory.cjs.js",
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import {
|
||||||
ListTreeIcon,
|
ListTreeIcon,
|
||||||
RotateCcwIcon,
|
RotateCcwIcon,
|
||||||
SearchIcon,
|
SearchIcon,
|
||||||
|
SettingsIcon,
|
||||||
TextAlignStartIcon,
|
TextAlignStartIcon,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
@ -759,7 +760,17 @@ export const Builder = (props: {
|
||||||
operationName?: string | null;
|
operationName?: string | null;
|
||||||
isReadOnly?: boolean;
|
isReadOnly?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { schema, activeOperation, endpoint, setEndpoint, defaultEndpoint } = useLaboratory();
|
const {
|
||||||
|
schema,
|
||||||
|
activeOperation,
|
||||||
|
endpoint,
|
||||||
|
setEndpoint,
|
||||||
|
defaultEndpoint,
|
||||||
|
tabs,
|
||||||
|
addTab,
|
||||||
|
setActiveTab,
|
||||||
|
shouldPollSchema,
|
||||||
|
} = useLaboratory();
|
||||||
|
|
||||||
const [endpointValue, setEndpointValue] = useState<string>(endpoint ?? '');
|
const [endpointValue, setEndpointValue] = useState<string>(endpoint ?? '');
|
||||||
const [searchValue, setSearchValue] = useState<string>('');
|
const [searchValue, setSearchValue] = useState<string>('');
|
||||||
|
|
@ -845,23 +856,45 @@ export const Builder = (props: {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-card flex size-full flex-col overflow-hidden">
|
<div className="bg-card flex size-full flex-col overflow-hidden">
|
||||||
<div className="flex items-center px-3 pt-3">
|
<div className="flex items-center gap-3 px-3 pt-3">
|
||||||
<span className="text-base font-medium">Builder</span>
|
<span className="text-base font-medium">Builder</span>
|
||||||
<div className="ml-auto flex items-center">
|
<div className="ml-auto flex items-center gap-3">
|
||||||
<Tooltip>
|
{shouldPollSchema && (
|
||||||
<TooltipTrigger asChild>
|
<Button
|
||||||
<Button
|
onClick={() => {
|
||||||
onClick={() => setOpenPaths([])}
|
const tab =
|
||||||
variant="ghost"
|
tabs.find(t => t.type === 'settings') ??
|
||||||
size="icon-sm"
|
addTab({
|
||||||
className="p-1! size-6 rounded-sm"
|
type: 'settings',
|
||||||
disabled={openPaths.length === 0}
|
data: {},
|
||||||
>
|
});
|
||||||
<CopyMinusIcon className="text-muted-foreground size-4" />
|
|
||||||
</Button>
|
setActiveTab(tab);
|
||||||
</TooltipTrigger>
|
}}
|
||||||
<TooltipContent>Collapse all</TooltipContent>
|
variant="ghost"
|
||||||
</Tooltip>
|
size="sm"
|
||||||
|
className="p-1! h-6 rounded-sm !px-1.5"
|
||||||
|
>
|
||||||
|
<SettingsIcon className="size-4" />
|
||||||
|
Introspection settings
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
onClick={() => setOpenPaths([])}
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-sm"
|
||||||
|
className="p-1! size-6 rounded-sm"
|
||||||
|
disabled={openPaths.length === 0}
|
||||||
|
>
|
||||||
|
<CopyMinusIcon className="text-muted-foreground size-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Collapse all</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-3 pt-3">
|
<div className="px-3 pt-3">
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import { OperationDefinitionNode, parse } from 'graphql';
|
||||||
import * as monaco from 'monaco-editor';
|
import * as monaco from 'monaco-editor';
|
||||||
import { MonacoGraphQLAPI } from 'monaco-graphql/esm/api.js';
|
import { MonacoGraphQLAPI } from 'monaco-graphql/esm/api.js';
|
||||||
import { initializeMode } from 'monaco-graphql/initializeMode';
|
import { initializeMode } from 'monaco-graphql/initializeMode';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
import MonacoEditor, { loader } from '@monaco-editor/react';
|
import MonacoEditor, { loader } from '@monaco-editor/react';
|
||||||
import { useLaboratory } from './context';
|
import { useLaboratory } from './context';
|
||||||
|
|
||||||
|
|
@ -84,7 +85,7 @@ const darkTheme: monaco.editor.IStandaloneThemeData = {
|
||||||
],
|
],
|
||||||
colors: {
|
colors: {
|
||||||
'editor.foreground': '#f6f8fa',
|
'editor.foreground': '#f6f8fa',
|
||||||
'editor.background': '#0f1214',
|
'editor.background': '#0f121400',
|
||||||
'editor.selectionBackground': '#2A2F34',
|
'editor.selectionBackground': '#2A2F34',
|
||||||
'editor.inactiveSelectionBackground': '#2A2F34',
|
'editor.inactiveSelectionBackground': '#2A2F34',
|
||||||
'editor.lineHighlightBackground': '#2A2F34',
|
'editor.lineHighlightBackground': '#2A2F34',
|
||||||
|
|
@ -354,10 +355,10 @@ const EditorInner = forwardRef<EditorHandle, EditorProps>((props, ref) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="size-full overflow-hidden">
|
<div className={cn('size-full overflow-hidden', props.className)}>
|
||||||
<MonacoEditor
|
<MonacoEditor
|
||||||
className="size-full"
|
|
||||||
{...props}
|
{...props}
|
||||||
|
className="size-full"
|
||||||
theme={theme === 'dark' ? 'hive-laboratory-dark' : 'hive-laboratory-light'}
|
theme={theme === 'dark' ? 'hive-laboratory-dark' : 'hive-laboratory-light'}
|
||||||
onMount={handleMount}
|
onMount={handleMount}
|
||||||
loading={null}
|
loading={null}
|
||||||
|
|
|
||||||
|
|
@ -514,15 +514,10 @@ export const Laboratory = (
|
||||||
const pluginsApi = usePlugins(props);
|
const pluginsApi = usePlugins(props);
|
||||||
const testsApi = useTests(props);
|
const testsApi = useTests(props);
|
||||||
const tabsApi = useTabs(props);
|
const tabsApi = useTabs(props);
|
||||||
const endpointApi = useEndpoint({
|
|
||||||
...props,
|
|
||||||
settingsApi,
|
|
||||||
});
|
|
||||||
const collectionsApi = useCollections({
|
const collectionsApi = useCollections({
|
||||||
...props,
|
...props,
|
||||||
tabsApi,
|
tabsApi,
|
||||||
});
|
});
|
||||||
|
|
||||||
const operationsApi = useOperations({
|
const operationsApi = useOperations({
|
||||||
...props,
|
...props,
|
||||||
collectionsApi,
|
collectionsApi,
|
||||||
|
|
@ -533,6 +528,14 @@ export const Laboratory = (
|
||||||
pluginsApi,
|
pluginsApi,
|
||||||
checkPermissions,
|
checkPermissions,
|
||||||
});
|
});
|
||||||
|
const endpointApi = useEndpoint({
|
||||||
|
...props,
|
||||||
|
settingsApi,
|
||||||
|
operationsApi,
|
||||||
|
envApi,
|
||||||
|
pluginsApi,
|
||||||
|
preflightApi,
|
||||||
|
});
|
||||||
|
|
||||||
const historyApi = useHistory(props);
|
const historyApi = useHistory(props);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
|
import { useEffect } from 'react';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { Editor } from '@/components/laboratory/editor';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { useForm } from '@tanstack/react-form';
|
import { useForm } from '@tanstack/react-form';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
|
||||||
import { Field, FieldGroup, FieldLabel } from '../ui/field';
|
import { Field, FieldDescription, FieldGroup, FieldLabel } from '../ui/field';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
|
||||||
import { useLaboratory } from './context';
|
import { useLaboratory } from './context';
|
||||||
|
|
||||||
|
|
@ -20,6 +22,8 @@ const settingsFormSchema = z.object({
|
||||||
introspection: z.object({
|
introspection: z.object({
|
||||||
method: z.enum(['GET', 'POST']).optional(),
|
method: z.enum(['GET', 'POST']).optional(),
|
||||||
schemaDescription: z.boolean().optional(),
|
schemaDescription: z.boolean().optional(),
|
||||||
|
headers: z.string().optional(),
|
||||||
|
includeActiveOperationHeaders: z.boolean().optional(),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -31,19 +35,17 @@ export const Settings = () => {
|
||||||
validators: {
|
validators: {
|
||||||
onSubmit: settingsFormSchema,
|
onSubmit: settingsFormSchema,
|
||||||
},
|
},
|
||||||
onSubmit: ({ value }) => {
|
|
||||||
setSettings(value as typeof settings);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.store.subscribe(state => {
|
||||||
|
setSettings(state.currentVal.values);
|
||||||
|
});
|
||||||
|
}, [setSettings]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-card size-full overflow-y-auto p-3">
|
<div className="bg-card size-full overflow-y-auto p-3">
|
||||||
<form
|
<form id="settings-form" className="mx-auto flex max-w-2xl flex-col gap-4">
|
||||||
id="settings-form"
|
|
||||||
onSubmit={form.handleSubmit}
|
|
||||||
onChange={form.handleSubmit}
|
|
||||||
className="mx-auto flex max-w-2xl flex-col gap-4"
|
|
||||||
>
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Fetch</CardTitle>
|
<CardTitle>Fetch</CardTitle>
|
||||||
|
|
@ -220,6 +222,43 @@ export const Settings = () => {
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</form.Field>
|
</form.Field>
|
||||||
|
<form.Field name="introspection.headers">
|
||||||
|
{field => {
|
||||||
|
return (
|
||||||
|
<Field>
|
||||||
|
<FieldLabel htmlFor={field.name}>Headers</FieldLabel>
|
||||||
|
<Editor
|
||||||
|
value={field.state.value ?? '{}'}
|
||||||
|
onChange={field.handleChange}
|
||||||
|
defaultLanguage="json"
|
||||||
|
theme="hive-laboratory"
|
||||||
|
className="bg-input/30 border-input focus-within:border-ring focus-within:ring-ring/50 h-64 rounded rounded-md border focus-within:ring-[3px]"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</form.Field>
|
||||||
|
<form.Field name="introspection.includeActiveOperationHeaders">
|
||||||
|
{field => {
|
||||||
|
return (
|
||||||
|
<Field className="flex-row items-start">
|
||||||
|
<Switch
|
||||||
|
className="mt-0.5 !w-8"
|
||||||
|
checked={field.state.value ?? false}
|
||||||
|
onCheckedChange={field.handleChange}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<FieldLabel htmlFor={field.name}>
|
||||||
|
Include active operation headers
|
||||||
|
</FieldLabel>
|
||||||
|
<FieldDescription>
|
||||||
|
Active operation (tab) headers will be included in the introspection query
|
||||||
|
</FieldDescription>
|
||||||
|
</div>
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</form.Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,10 @@
|
||||||
--destructive-foreground: var(--hive-laboratory-destructive-foreground, var(--color-neutral-1));
|
--destructive-foreground: var(--hive-laboratory-destructive-foreground, var(--color-neutral-1));
|
||||||
|
|
||||||
--ring: var(--hive-laboratory-ring, var(--color-ring));
|
--ring: var(--hive-laboratory-ring, var(--color-ring));
|
||||||
|
|
||||||
|
& .monaco-editor {
|
||||||
|
--vscode-focusBorder: transparent !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.hive-laboratory.dark {
|
.hive-laboratory.dark {
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,17 @@
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
buildClientSchema,
|
buildClientSchema,
|
||||||
GraphQLSchema,
|
GraphQLSchema,
|
||||||
introspectionFromSchema,
|
introspectionFromSchema,
|
||||||
type IntrospectionQuery,
|
type IntrospectionQuery,
|
||||||
} from 'graphql';
|
} from 'graphql';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
// import z from 'zod';
|
import { LaboratoryEnv, LaboratoryEnvActions, LaboratoryEnvState } from '@/lib/env';
|
||||||
|
import { LaboratoryOperationsActions, LaboratoryOperationsState } from '@/lib/operations';
|
||||||
|
import { handleTemplate } from '@/lib/operations.utils';
|
||||||
|
import { LaboratoryPluginsActions, LaboratoryPluginsState } from '@/lib/plugins';
|
||||||
|
import { LaboratoryPreflightActions, LaboratoryPreflightState } from '@/lib/preflight';
|
||||||
import { asyncInterval } from '@/lib/utils';
|
import { asyncInterval } from '@/lib/utils';
|
||||||
import { SubscriptionProtocol, UrlLoader } from '@graphql-tools/url-loader';
|
import { SubscriptionProtocol, UrlLoader } from '@graphql-tools/url-loader';
|
||||||
import type { LaboratorySettingsActions, LaboratorySettingsState } from './settings';
|
import type { LaboratorySettingsActions, LaboratorySettingsState } from './settings';
|
||||||
|
|
@ -16,6 +21,7 @@ export interface LaboratoryEndpointState {
|
||||||
schema: GraphQLSchema | null;
|
schema: GraphQLSchema | null;
|
||||||
introspection: IntrospectionQuery | null;
|
introspection: IntrospectionQuery | null;
|
||||||
defaultEndpoint: string | null;
|
defaultEndpoint: string | null;
|
||||||
|
shouldPollSchema: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LaboratoryEndpointActions {
|
export interface LaboratoryEndpointActions {
|
||||||
|
|
@ -24,11 +30,17 @@ export interface LaboratoryEndpointActions {
|
||||||
restoreDefaultEndpoint: () => void;
|
restoreDefaultEndpoint: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const EXPECTED_ERROR_REASON = 'Expected error reason';
|
||||||
|
|
||||||
export const useEndpoint = (props: {
|
export const useEndpoint = (props: {
|
||||||
defaultEndpoint?: string | null;
|
defaultEndpoint?: string | null;
|
||||||
onEndpointChange?: (endpoint: string | null) => void;
|
onEndpointChange?: (endpoint: string | null) => void;
|
||||||
defaultSchemaIntrospection?: IntrospectionQuery | null;
|
defaultSchemaIntrospection?: IntrospectionQuery | null;
|
||||||
settingsApi?: LaboratorySettingsState & LaboratorySettingsActions;
|
settingsApi?: LaboratorySettingsState & LaboratorySettingsActions;
|
||||||
|
operationsApi?: LaboratoryOperationsState & LaboratoryOperationsActions;
|
||||||
|
envApi?: LaboratoryEnvState & LaboratoryEnvActions;
|
||||||
|
pluginsApi?: LaboratoryPluginsState & LaboratoryPluginsActions;
|
||||||
|
preflightApi?: LaboratoryPreflightState & LaboratoryPreflightActions;
|
||||||
}): LaboratoryEndpointState & LaboratoryEndpointActions => {
|
}): LaboratoryEndpointState & LaboratoryEndpointActions => {
|
||||||
const [endpoint, _setEndpoint] = useState<string | null>(props.defaultEndpoint ?? null);
|
const [endpoint, _setEndpoint] = useState<string | null>(props.defaultEndpoint ?? null);
|
||||||
const [introspection, setIntrospection] = useState<IntrospectionQuery | null>(null);
|
const [introspection, setIntrospection] = useState<IntrospectionQuery | null>(null);
|
||||||
|
|
@ -47,75 +59,173 @@ export const useEndpoint = (props: {
|
||||||
|
|
||||||
const loader = useMemo(() => new UrlLoader(), []);
|
const loader = useMemo(() => new UrlLoader(), []);
|
||||||
|
|
||||||
const fetchSchema = useCallback(
|
const activeOperationHeadersRef = useRef<string | null | undefined>(
|
||||||
async (signal?: AbortSignal) => {
|
props.operationsApi?.activeOperation?.headers,
|
||||||
if (endpoint === props.defaultEndpoint && props.defaultSchemaIntrospection) {
|
);
|
||||||
setIntrospection(props.defaultSchemaIntrospection);
|
const envVariablesRef = useRef<LaboratoryEnv['variables'] | undefined>(
|
||||||
return;
|
props.envApi?.env?.variables,
|
||||||
}
|
);
|
||||||
|
const pluginsStateRef = useRef<Record<string, any> | undefined>(props.pluginsApi?.pluginsState);
|
||||||
|
|
||||||
if (!endpoint) {
|
activeOperationHeadersRef.current = props.operationsApi?.activeOperation?.headers;
|
||||||
setIntrospection(null);
|
envVariablesRef.current = props.envApi?.env?.variables;
|
||||||
return;
|
pluginsStateRef.current = props.pluginsApi?.pluginsState;
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
const fetchSchema = useMemo(
|
||||||
const result = await loader.load(endpoint, {
|
() =>
|
||||||
subscriptionsEndpoint: endpoint,
|
debounce(
|
||||||
subscriptionsProtocol:
|
async (
|
||||||
(props.settingsApi?.settings.subscriptions.protocol as SubscriptionProtocol) ??
|
signal?: AbortSignal,
|
||||||
SubscriptionProtocol.GRAPHQL_SSE,
|
options?: {
|
||||||
credentials: props.settingsApi?.settings.fetch.credentials,
|
env?: LaboratoryEnv;
|
||||||
specifiedByUrl: true,
|
pluginsState?: Record<string, any>;
|
||||||
directiveIsRepeatable: true,
|
},
|
||||||
inputValueDeprecation: true,
|
) => {
|
||||||
retry: props.settingsApi?.settings.fetch.retry,
|
if (endpoint === props.defaultEndpoint && props.defaultSchemaIntrospection) {
|
||||||
timeout: props.settingsApi?.settings.fetch.timeout,
|
setIntrospection(props.defaultSchemaIntrospection);
|
||||||
useGETForQueries: props.settingsApi?.settings.fetch.useGETForQueries,
|
return;
|
||||||
exposeHTTPDetailsInExtensions: true,
|
}
|
||||||
descriptions: props.settingsApi?.settings.introspection.schemaDescription ?? false,
|
|
||||||
method: props.settingsApi?.settings.introspection.method ?? 'POST',
|
|
||||||
fetch: (input: string | URL | Request, init?: RequestInit) =>
|
|
||||||
fetch(input, {
|
|
||||||
...init,
|
|
||||||
signal,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.length === 0) {
|
if (!endpoint) {
|
||||||
throw new Error('Failed to fetch schema');
|
setIntrospection(null);
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!result[0].schema) {
|
try {
|
||||||
throw new Error('Failed to fetch schema');
|
let env = options?.env?.variables ?? envVariablesRef.current ?? {};
|
||||||
}
|
let plugins = options?.pluginsState ?? pluginsStateRef.current ?? {};
|
||||||
|
|
||||||
setIntrospection(introspectionFromSchema(result[0].schema));
|
let sourceHeaders: Record<string, string> = {};
|
||||||
} catch (error: unknown) {
|
|
||||||
if (
|
|
||||||
error &&
|
|
||||||
typeof error === 'object' &&
|
|
||||||
'message' in error &&
|
|
||||||
typeof error.message === 'string'
|
|
||||||
) {
|
|
||||||
toast.error(error.message);
|
|
||||||
} else {
|
|
||||||
toast.error('Failed to fetch schema');
|
|
||||||
}
|
|
||||||
|
|
||||||
setIntrospection(null);
|
if (props.settingsApi?.settings.introspection.headers) {
|
||||||
|
try {
|
||||||
|
sourceHeaders = JSON.parse(props.settingsApi?.settings.introspection.headers);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
throw error;
|
if (
|
||||||
}
|
props.settingsApi?.settings.introspection.includeActiveOperationHeaders &&
|
||||||
},
|
activeOperationHeadersRef.current
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
sourceHeaders = {
|
||||||
|
...sourceHeaders,
|
||||||
|
...JSON.parse(activeOperationHeadersRef.current),
|
||||||
|
};
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let stringifiedHeaders = JSON.stringify(sourceHeaders);
|
||||||
|
|
||||||
|
if (stringifiedHeaders.includes('{{')) {
|
||||||
|
try {
|
||||||
|
const preflightResult = await props.preflightApi?.runPreflight?.(
|
||||||
|
props.pluginsApi?.plugins ?? [],
|
||||||
|
props.pluginsApi?.pluginsState ?? {},
|
||||||
|
);
|
||||||
|
|
||||||
|
props?.envApi?.setEnv(preflightResult?.env ?? { variables: {} });
|
||||||
|
props?.pluginsApi?.setPluginsState(preflightResult?.pluginsState ?? {});
|
||||||
|
|
||||||
|
env = preflightResult?.env?.variables ?? {};
|
||||||
|
plugins = preflightResult?.pluginsState ?? {};
|
||||||
|
|
||||||
|
if (preflightResult?.headers) {
|
||||||
|
stringifiedHeaders = JSON.stringify({
|
||||||
|
...sourceHeaders,
|
||||||
|
...preflightResult?.headers,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
toast.error('Failed to run preflight');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsedHeaders: Record<string, string> = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
parsedHeaders = JSON.parse(
|
||||||
|
handleTemplate(stringifiedHeaders, {
|
||||||
|
...env,
|
||||||
|
plugins,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
toast.error('Failed to parse headers');
|
||||||
|
parsedHeaders = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await loader.load(endpoint, {
|
||||||
|
subscriptionsEndpoint: endpoint,
|
||||||
|
subscriptionsProtocol:
|
||||||
|
(props.settingsApi?.settings.subscriptions.protocol as SubscriptionProtocol) ??
|
||||||
|
SubscriptionProtocol.GRAPHQL_SSE,
|
||||||
|
headers: parsedHeaders,
|
||||||
|
credentials: props.settingsApi?.settings.fetch.credentials,
|
||||||
|
specifiedByUrl: true,
|
||||||
|
directiveIsRepeatable: true,
|
||||||
|
inputValueDeprecation: true,
|
||||||
|
retry: props.settingsApi?.settings.fetch.retry,
|
||||||
|
timeout: props.settingsApi?.settings.fetch.timeout,
|
||||||
|
useGETForQueries: props.settingsApi?.settings.fetch.useGETForQueries,
|
||||||
|
exposeHTTPDetailsInExtensions: true,
|
||||||
|
descriptions: props.settingsApi?.settings.introspection.schemaDescription ?? false,
|
||||||
|
method: props.settingsApi?.settings.introspection.method ?? 'POST',
|
||||||
|
fetch: (input: string | URL | Request, init?: RequestInit) =>
|
||||||
|
fetch(input, {
|
||||||
|
...init,
|
||||||
|
signal,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.length === 0) {
|
||||||
|
throw new Error('Failed to fetch schema');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result[0].schema) {
|
||||||
|
throw new Error('Failed to fetch schema');
|
||||||
|
}
|
||||||
|
|
||||||
|
setIntrospection(introspectionFromSchema(result[0].schema));
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (
|
||||||
|
error &&
|
||||||
|
typeof error === 'object' &&
|
||||||
|
'message' in error &&
|
||||||
|
typeof error.message === 'string'
|
||||||
|
) {
|
||||||
|
if (error.message === EXPECTED_ERROR_REASON) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.error(error.message);
|
||||||
|
} else {
|
||||||
|
toast.error('Failed to fetch schema');
|
||||||
|
}
|
||||||
|
|
||||||
|
setIntrospection(null);
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
500,
|
||||||
|
),
|
||||||
[
|
[
|
||||||
endpoint,
|
endpoint,
|
||||||
props.settingsApi?.settings.fetch.timeout,
|
props.settingsApi?.settings.fetch.timeout,
|
||||||
props.settingsApi?.settings.introspection.method,
|
props.settingsApi?.settings.introspection.method,
|
||||||
props.settingsApi?.settings.introspection.schemaDescription,
|
props.settingsApi?.settings.introspection.schemaDescription,
|
||||||
|
props.settingsApi?.settings.introspection.headers,
|
||||||
|
props.settingsApi?.settings.introspection.includeActiveOperationHeaders,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
fetchSchema.cancel();
|
||||||
|
};
|
||||||
|
}, [fetchSchema]);
|
||||||
|
|
||||||
const shouldPollSchema = useMemo(() => {
|
const shouldPollSchema = useMemo(() => {
|
||||||
return endpoint !== props.defaultEndpoint || !props.defaultSchemaIntrospection;
|
return endpoint !== props.defaultEndpoint || !props.defaultSchemaIntrospection;
|
||||||
}, [endpoint, props.defaultEndpoint, props.defaultSchemaIntrospection]);
|
}, [endpoint, props.defaultEndpoint, props.defaultSchemaIntrospection]);
|
||||||
|
|
@ -132,7 +242,7 @@ export const useEndpoint = (props: {
|
||||||
try {
|
try {
|
||||||
await fetchSchema(intervalController.signal);
|
await fetchSchema(intervalController.signal);
|
||||||
} catch {
|
} catch {
|
||||||
intervalController.abort('Polling schema failed');
|
intervalController.abort(new Error('Aborted because of schema polling error'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
5000,
|
5000,
|
||||||
|
|
@ -140,7 +250,7 @@ export const useEndpoint = (props: {
|
||||||
);
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
intervalController.abort('Polling schema aborted');
|
intervalController.abort(new Error(EXPECTED_ERROR_REASON));
|
||||||
};
|
};
|
||||||
}, [shouldPollSchema, fetchSchema]);
|
}, [shouldPollSchema, fetchSchema]);
|
||||||
|
|
||||||
|
|
@ -152,10 +262,38 @@ export const useEndpoint = (props: {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (endpoint && !shouldPollSchema) {
|
if (endpoint && !shouldPollSchema) {
|
||||||
void fetchSchema();
|
const abortController = new AbortController();
|
||||||
|
|
||||||
|
void fetchSchema(abortController.signal);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
abortController.abort(new Error(EXPECTED_ERROR_REASON));
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}, [endpoint, fetchSchema, shouldPollSchema]);
|
}, [endpoint, fetchSchema, shouldPollSchema]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!endpoint || !shouldPollSchema) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const abortController = new AbortController();
|
||||||
|
void fetchSchema(abortController.signal, {
|
||||||
|
env: props.envApi?.env ?? undefined,
|
||||||
|
pluginsState: props.pluginsApi?.pluginsState,
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
abortController.abort(new Error(EXPECTED_ERROR_REASON));
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
endpoint,
|
||||||
|
shouldPollSchema,
|
||||||
|
fetchSchema,
|
||||||
|
props.settingsApi?.settings.introspection.headers,
|
||||||
|
props.settingsApi?.settings.introspection.includeActiveOperationHeaders,
|
||||||
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
endpoint,
|
endpoint,
|
||||||
setEndpoint,
|
setEndpoint,
|
||||||
|
|
@ -164,5 +302,6 @@ export const useEndpoint = (props: {
|
||||||
fetchSchema,
|
fetchSchema,
|
||||||
restoreDefaultEndpoint,
|
restoreDefaultEndpoint,
|
||||||
defaultEndpoint: props.defaultEndpoint ?? null,
|
defaultEndpoint: props.defaultEndpoint ?? null,
|
||||||
|
shouldPollSchema,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ export type LaboratorySettings = {
|
||||||
introspection: {
|
introspection: {
|
||||||
method?: 'GET' | 'POST';
|
method?: 'GET' | 'POST';
|
||||||
schemaDescription?: boolean;
|
schemaDescription?: boolean;
|
||||||
|
headers?: string;
|
||||||
|
includeActiveOperationHeaders?: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -29,6 +31,8 @@ export const defaultLaboratorySettings: LaboratorySettings = {
|
||||||
introspection: {
|
introspection: {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
schemaDescription: false,
|
schemaDescription: false,
|
||||||
|
headers: '',
|
||||||
|
includeActiveOperationHeaders: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -50,6 +54,10 @@ export const normalizeLaboratorySettings = (
|
||||||
schemaDescription:
|
schemaDescription:
|
||||||
settings?.introspection?.schemaDescription ??
|
settings?.introspection?.schemaDescription ??
|
||||||
defaultLaboratorySettings.introspection.schemaDescription,
|
defaultLaboratorySettings.introspection.schemaDescription,
|
||||||
|
headers: settings?.introspection?.headers ?? defaultLaboratorySettings.introspection.headers,
|
||||||
|
includeActiveOperationHeaders:
|
||||||
|
settings?.introspection?.includeActiveOperationHeaders ??
|
||||||
|
defaultLaboratorySettings.introspection.includeActiveOperationHeaders,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,18 @@
|
||||||
# @graphql-yoga/render-graphiql
|
# @graphql-yoga/render-graphiql
|
||||||
|
|
||||||
|
## 0.1.8
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#8024](https://github.com/graphql-hive/console/pull/8024)
|
||||||
|
[`0e3ce40`](https://github.com/graphql-hive/console/commit/0e3ce400706c625925161f8d59cc5691380cef07)
|
||||||
|
Thanks [@mskorokhodov](https://github.com/mskorokhodov)! - Hive laboratory introspection query to
|
||||||
|
include active tab headers
|
||||||
|
|
||||||
|
- Updated dependencies
|
||||||
|
[[`0e3ce40`](https://github.com/graphql-hive/console/commit/0e3ce400706c625925161f8d59cc5691380cef07)]:
|
||||||
|
- @graphql-hive/laboratory@0.1.8
|
||||||
|
|
||||||
## 0.1.7
|
## 0.1.7
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@graphql-hive/render-laboratory",
|
"name": "@graphql-hive/render-laboratory",
|
||||||
"version": "0.1.7",
|
"version": "0.1.8",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "",
|
"description": "",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@graphql-hive/core": "workspace:*",
|
"@graphql-hive/core": "workspace:*",
|
||||||
"@graphql-hive/logger": "^1.0.9",
|
"@graphql-hive/logger": "^1.1.0",
|
||||||
"@graphql-yoga/plugin-persisted-operations": "^3.9.0"
|
"@graphql-yoga/plugin-persisted-operations": "^3.9.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,8 @@
|
||||||
"./modules/auth/lib/supertokens-at-home/crypto": "./src/modules/auth/lib/supertokens-at-home/crypto.ts",
|
"./modules/auth/lib/supertokens-at-home/crypto": "./src/modules/auth/lib/supertokens-at-home/crypto.ts",
|
||||||
"./modules/auth/providers/supertokens-store": "./src/modules/auth/providers/supertokens-store.ts",
|
"./modules/auth/providers/supertokens-store": "./src/modules/auth/providers/supertokens-store.ts",
|
||||||
"./modules/shared/providers/logger": "./src/modules/shared/providers/logger.ts",
|
"./modules/shared/providers/logger": "./src/modules/shared/providers/logger.ts",
|
||||||
"./modules/shared/providers/redis": "./src/modules/shared/providers/redis.ts"
|
"./modules/shared/providers/redis": "./src/modules/shared/providers/redis.ts",
|
||||||
|
"./modules/schema/providers/schema-version-store": "./src/modules/schema/providers/schema-version-store.ts"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"graphql": "^16.0.0",
|
"graphql": "^16.0.0",
|
||||||
|
|
@ -23,7 +24,7 @@
|
||||||
"@date-fns/utc": "2.1.1",
|
"@date-fns/utc": "2.1.1",
|
||||||
"@graphql-hive/core": "workspace:*",
|
"@graphql-hive/core": "workspace:*",
|
||||||
"@graphql-hive/signal": "1.0.0",
|
"@graphql-hive/signal": "1.0.0",
|
||||||
"@graphql-inspector/core": "7.1.2",
|
"@graphql-inspector/core": "7.1.3",
|
||||||
"@graphql-tools/merge": "9.1.1",
|
"@graphql-tools/merge": "9.1.1",
|
||||||
"@hive/cdn-script": "workspace:*",
|
"@hive/cdn-script": "workspace:*",
|
||||||
"@hive/postgres": "workspace:*",
|
"@hive/postgres": "workspace:*",
|
||||||
|
|
|
||||||
|
|
@ -124,10 +124,10 @@ export class AppDeploymentsManager {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const target = await this.targetManager.getTargetById(selector);
|
||||||
|
|
||||||
return await this.appDeployments.addDocumentsToAppDeployment({
|
return await this.appDeployments.addDocumentsToAppDeployment({
|
||||||
organizationId: selector.organizationId,
|
target,
|
||||||
projectId: selector.projectId,
|
|
||||||
targetId: selector.targetId,
|
|
||||||
appDeployment: args.appDeployment,
|
appDeployment: args.appDeployment,
|
||||||
operations: args.documents,
|
operations: args.documents,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
import { differenceInCalendarDays, startOfDay, subDays } from 'date-fns';
|
import { differenceInCalendarDays, startOfDay, subDays } from 'date-fns';
|
||||||
import { Inject, Injectable, Scope } from 'graphql-modules';
|
import { Inject, Injectable, Scope } from 'graphql-modules';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { buildAppDeploymentIsEnabledKey } from '@hive/cdn-script/artifact-storage-reader';
|
import {
|
||||||
|
AppDeploymentManifestModel,
|
||||||
|
buildAppDeploymentIsEnabledKey,
|
||||||
|
buildAppDeploymentManifestKey,
|
||||||
|
} from '@hive/cdn-script/artifact-storage-reader';
|
||||||
import {
|
import {
|
||||||
PostgresDatabasePool,
|
PostgresDatabasePool,
|
||||||
psql,
|
psql,
|
||||||
|
|
@ -16,8 +20,10 @@ import {
|
||||||
encodeCreatedAtAndUUIDIdBasedCursor,
|
encodeCreatedAtAndUUIDIdBasedCursor,
|
||||||
encodeHashBasedCursor,
|
encodeHashBasedCursor,
|
||||||
} from '@hive/storage';
|
} from '@hive/storage';
|
||||||
|
import type { Target } from '../../../shared/entities';
|
||||||
import { ClickHouse, sql as cSql } from '../../operations/providers/clickhouse-client';
|
import { ClickHouse, sql as cSql } from '../../operations/providers/clickhouse-client';
|
||||||
import { SchemaVersionHelper } from '../../schema/providers/schema-version-helper';
|
import { SchemaVersionHelper } from '../../schema/providers/schema-version-helper';
|
||||||
|
import { SchemaVersionStore } from '../../schema/providers/schema-version-store';
|
||||||
import { Logger } from '../../shared/providers/logger';
|
import { Logger } from '../../shared/providers/logger';
|
||||||
import { S3_CONFIG, type S3Config } from '../../shared/providers/s3-config';
|
import { S3_CONFIG, type S3Config } from '../../shared/providers/s3-config';
|
||||||
import { Storage } from '../../shared/providers/storage';
|
import { Storage } from '../../shared/providers/storage';
|
||||||
|
|
@ -55,6 +61,7 @@ export class AppDeployments {
|
||||||
private storage: Storage,
|
private storage: Storage,
|
||||||
private schemaVersionHelper: SchemaVersionHelper,
|
private schemaVersionHelper: SchemaVersionHelper,
|
||||||
private persistedDocumentScheduler: PersistedDocumentScheduler,
|
private persistedDocumentScheduler: PersistedDocumentScheduler,
|
||||||
|
private schemaVersions: SchemaVersionStore,
|
||||||
@Inject(APP_DEPLOYMENTS_ENABLED) private appDeploymentsEnabled: boolean,
|
@Inject(APP_DEPLOYMENTS_ENABLED) private appDeploymentsEnabled: boolean,
|
||||||
) {
|
) {
|
||||||
this.logger = logger.child({ source: 'AppDeployments' });
|
this.logger = logger.child({ source: 'AppDeployments' });
|
||||||
|
|
@ -249,9 +256,7 @@ export class AppDeployments {
|
||||||
}
|
}
|
||||||
|
|
||||||
async addDocumentsToAppDeployment(args: {
|
async addDocumentsToAppDeployment(args: {
|
||||||
organizationId: string;
|
target: Target;
|
||||||
projectId: string;
|
|
||||||
targetId: string;
|
|
||||||
appDeployment: {
|
appDeployment: {
|
||||||
name: string;
|
name: string;
|
||||||
version: string;
|
version: string;
|
||||||
|
|
@ -263,7 +268,7 @@ export class AppDeployments {
|
||||||
}) {
|
}) {
|
||||||
if (this.appDeploymentsEnabled === false) {
|
if (this.appDeploymentsEnabled === false) {
|
||||||
const organization = await this.storage.getOrganization({
|
const organization = await this.storage.getOrganization({
|
||||||
organizationId: args.organizationId,
|
organizationId: args.target.orgId,
|
||||||
});
|
});
|
||||||
if (organization.featureFlags.appDeployments === false) {
|
if (organization.featureFlags.appDeployments === false) {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
|
|
@ -283,7 +288,7 @@ export class AppDeployments {
|
||||||
// todo: validate input
|
// todo: validate input
|
||||||
|
|
||||||
const appDeployment = await this.findAppDeployment({
|
const appDeployment = await this.findAppDeployment({
|
||||||
targetId: args.targetId,
|
targetId: args.target.id,
|
||||||
name: args.appDeployment.name,
|
name: args.appDeployment.name,
|
||||||
version: args.appDeployment.version,
|
version: args.appDeployment.version,
|
||||||
});
|
});
|
||||||
|
|
@ -309,9 +314,9 @@ export class AppDeployments {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.operations.length !== 0) {
|
if (args.operations.length !== 0) {
|
||||||
const latestSchemaVersion = await this.storage.getMaybeLatestValidVersion({
|
const latestSchemaVersion = await this.schemaVersions.getMaybeLatestValidSchemaVersion(
|
||||||
targetId: args.targetId,
|
args.target,
|
||||||
});
|
);
|
||||||
|
|
||||||
if (latestSchemaVersion === null) {
|
if (latestSchemaVersion === null) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -326,9 +331,9 @@ export class AppDeployments {
|
||||||
|
|
||||||
const compositeSchemaSdl = await this.schemaVersionHelper.getCompositeSchemaSdl({
|
const compositeSchemaSdl = await this.schemaVersionHelper.getCompositeSchemaSdl({
|
||||||
...latestSchemaVersion,
|
...latestSchemaVersion,
|
||||||
organizationId: args.organizationId,
|
organizationId: args.target.orgId,
|
||||||
projectId: args.projectId,
|
projectId: args.target.projectId,
|
||||||
targetId: args.targetId,
|
targetId: args.target.id,
|
||||||
});
|
});
|
||||||
if (compositeSchemaSdl === null) {
|
if (compositeSchemaSdl === null) {
|
||||||
// No valid schema found.
|
// No valid schema found.
|
||||||
|
|
@ -343,7 +348,7 @@ export class AppDeployments {
|
||||||
|
|
||||||
const result = await this.persistedDocumentScheduler.processBatch({
|
const result = await this.persistedDocumentScheduler.processBatch({
|
||||||
schemaSdl: compositeSchemaSdl,
|
schemaSdl: compositeSchemaSdl,
|
||||||
targetId: args.targetId,
|
targetId: args.target.id,
|
||||||
appDeployment: {
|
appDeployment: {
|
||||||
id: appDeployment.id,
|
id: appDeployment.id,
|
||||||
name: args.appDeployment.name,
|
name: args.appDeployment.name,
|
||||||
|
|
@ -366,6 +371,27 @@ export class AppDeployments {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async _getAllDocumentHashesForAppDeployment(
|
||||||
|
appDeployment: AppDeploymentRecord,
|
||||||
|
): Promise<Array<string>> {
|
||||||
|
return await this.clickhouse
|
||||||
|
.query({
|
||||||
|
query: cSql`
|
||||||
|
SELECT
|
||||||
|
DISTINCT "document_hash" AS "hash"
|
||||||
|
FROM
|
||||||
|
"app_deployment_documents"
|
||||||
|
WHERE
|
||||||
|
"app_deployment_id" = ${appDeployment.id}
|
||||||
|
`,
|
||||||
|
queryId: 'app-deployment-document-ids',
|
||||||
|
timeout: 10_000,
|
||||||
|
})
|
||||||
|
.then(res =>
|
||||||
|
z.array(z.object({ hash: z.string() }).transform(row => row.hash)).parse(res.data),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async activateAppDeployment(args: {
|
async activateAppDeployment(args: {
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
targetId: string;
|
targetId: string;
|
||||||
|
|
@ -431,8 +457,11 @@ export class AppDeployments {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const appDeploymentDocumentHashes =
|
||||||
|
await this._getAllDocumentHashesForAppDeployment(appDeployment);
|
||||||
|
|
||||||
for (const s3 of this.s3) {
|
for (const s3 of this.s3) {
|
||||||
const result = await s3.client.fetch(
|
let result = await s3.client.fetch(
|
||||||
[
|
[
|
||||||
s3.endpoint,
|
s3.endpoint,
|
||||||
s3.bucket,
|
s3.bucket,
|
||||||
|
|
@ -457,6 +486,38 @@ export class AppDeployments {
|
||||||
if (result.statusCode !== 200) {
|
if (result.statusCode !== 200) {
|
||||||
throw new Error(`Failed to enable app deployment: ${result.statusMessage}`);
|
throw new Error(`Failed to enable app deployment: ${result.statusMessage}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result = await s3.client.fetch(
|
||||||
|
[
|
||||||
|
s3.endpoint,
|
||||||
|
s3.bucket,
|
||||||
|
buildAppDeploymentManifestKey(
|
||||||
|
appDeployment.targetId,
|
||||||
|
appDeployment.name,
|
||||||
|
appDeployment.version,
|
||||||
|
),
|
||||||
|
].join('/'),
|
||||||
|
{
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: appDeployment.id,
|
||||||
|
appName: appDeployment.name,
|
||||||
|
appVersion: appDeployment.version,
|
||||||
|
documentHashes: appDeploymentDocumentHashes.sort(),
|
||||||
|
isActive: true,
|
||||||
|
} satisfies z.TypeOf<typeof AppDeploymentManifestModel>),
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
},
|
||||||
|
aws: {
|
||||||
|
signQuery: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.statusCode !== 200) {
|
||||||
|
throw new Error(`Failed to write app manifest: ${result.statusMessage}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedAppDeployment = await this.pool
|
const updatedAppDeployment = await this.pool
|
||||||
|
|
@ -690,8 +751,11 @@ export class AppDeployments {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const appDeploymentDocumentHashes =
|
||||||
|
await this._getAllDocumentHashesForAppDeployment(appDeployment);
|
||||||
|
|
||||||
for (const s3 of this.s3) {
|
for (const s3 of this.s3) {
|
||||||
const result = await s3.client.fetch(
|
let result = await s3.client.fetch(
|
||||||
[
|
[
|
||||||
s3.endpoint,
|
s3.endpoint,
|
||||||
s3.bucket,
|
s3.bucket,
|
||||||
|
|
@ -722,6 +786,38 @@ export class AppDeployments {
|
||||||
`Failed to disable app deployment. Request failed with status code "${result.statusMessage}".`,
|
`Failed to disable app deployment. Request failed with status code "${result.statusMessage}".`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result = await s3.client.fetch(
|
||||||
|
[
|
||||||
|
s3.endpoint,
|
||||||
|
s3.bucket,
|
||||||
|
buildAppDeploymentManifestKey(
|
||||||
|
appDeployment.targetId,
|
||||||
|
appDeployment.name,
|
||||||
|
appDeployment.version,
|
||||||
|
),
|
||||||
|
].join('/'),
|
||||||
|
{
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: appDeployment.id,
|
||||||
|
appName: appDeployment.name,
|
||||||
|
appVersion: appDeployment.version,
|
||||||
|
documentHashes: appDeploymentDocumentHashes.sort(),
|
||||||
|
isActive: false,
|
||||||
|
} satisfies z.TypeOf<typeof AppDeploymentManifestModel>),
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
},
|
||||||
|
aws: {
|
||||||
|
signQuery: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.statusCode !== 200) {
|
||||||
|
throw new Error(`Failed to write app manifest: ${result.statusMessage}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.clickhouse.query({
|
await this.clickhouse.query({
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ const SharedOIDCIntegrationDomainFieldsModel = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
organizationId: z.string().uuid(),
|
organizationId: z.string().uuid(),
|
||||||
oidcIntegrationId: z.string().uuid(),
|
oidcIntegrationId: z.string().uuid(),
|
||||||
domainName: z.string(),
|
domainName: z.string().transform(name => name.toLowerCase()),
|
||||||
createdAt: z.string(),
|
createdAt: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -86,7 +86,7 @@ export class OIDCIntegrationStore {
|
||||||
) VALUES (
|
) VALUES (
|
||||||
${organizationId}
|
${organizationId}
|
||||||
, ${oidcIntegrationId}
|
, ${oidcIntegrationId}
|
||||||
, ${domainName}
|
, ${domainName.toLowerCase()}
|
||||||
)
|
)
|
||||||
ON CONFLICT ("oidc_integration_id", "domain_name")
|
ON CONFLICT ("oidc_integration_id", "domain_name")
|
||||||
DO NOTHING
|
DO NOTHING
|
||||||
|
|
@ -131,7 +131,7 @@ export class OIDCIntegrationStore {
|
||||||
FROM
|
FROM
|
||||||
"oidc_integration_domains"
|
"oidc_integration_domains"
|
||||||
WHERE
|
WHERE
|
||||||
"domain_name" = ${domainName}
|
"domain_name" = ${domainName.toLowerCase()}
|
||||||
AND "verified_at" IS NOT NULL
|
AND "verified_at" IS NOT NULL
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
@ -149,7 +149,7 @@ export class OIDCIntegrationStore {
|
||||||
"oidc_integration_domains"
|
"oidc_integration_domains"
|
||||||
WHERE
|
WHERE
|
||||||
"oidc_integration_id" = ${oidcIntegrationId}
|
"oidc_integration_id" = ${oidcIntegrationId}
|
||||||
AND "domain_name" = ${domainName}
|
AND "domain_name" = ${domainName.toLowerCase()}
|
||||||
AND "verified_at" IS NOT NULL
|
AND "verified_at" IS NOT NULL
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import * as GraphQLSchema from '../../../__generated__/types';
|
||||||
import { Organization, ProjectType } from '../../../shared/entities';
|
import { Organization, ProjectType } from '../../../shared/entities';
|
||||||
import { AccessError } from '../../../shared/errors';
|
import { AccessError } from '../../../shared/errors';
|
||||||
import { Session } from '../../auth/lib/authz';
|
import { Session } from '../../auth/lib/authz';
|
||||||
|
import { SchemaVersionStore } from '../../schema/providers/schema-version-store';
|
||||||
import { Storage } from '../../shared/providers/storage';
|
import { Storage } from '../../shared/providers/storage';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -19,6 +20,7 @@ export class ResourceSelector {
|
||||||
constructor(
|
constructor(
|
||||||
private storage: Storage,
|
private storage: Storage,
|
||||||
private session: Session,
|
private session: Session,
|
||||||
|
private schemaVersions: SchemaVersionStore,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private async _assertResourceSelectorAdminPermissions(organizationId: string) {
|
private async _assertResourceSelectorAdminPermissions(organizationId: string) {
|
||||||
|
|
@ -163,7 +165,9 @@ export class ResourceSelector {
|
||||||
if (target.type === GraphQLSchema.ProjectType.SINGLE) {
|
if (target.type === GraphQLSchema.ProjectType.SINGLE) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const latest = await this.storage.getMaybeLatestValidVersion({ targetId: target.targetId });
|
const latest = await this.schemaVersions.getMaybeLatestSchemaVersionForTargetId(
|
||||||
|
target.targetId,
|
||||||
|
);
|
||||||
if (latest) {
|
if (latest) {
|
||||||
return await this.storage.pool
|
return await this.storage.pool
|
||||||
.anyFirst(
|
.anyFirst(
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import { SchemaHelper } from './providers/schema-helper';
|
||||||
import { SchemaManager } from './providers/schema-manager';
|
import { SchemaManager } from './providers/schema-manager';
|
||||||
import { SchemaPublisher } from './providers/schema-publisher';
|
import { SchemaPublisher } from './providers/schema-publisher';
|
||||||
import { SchemaVersionHelper } from './providers/schema-version-helper';
|
import { SchemaVersionHelper } from './providers/schema-version-helper';
|
||||||
|
import { SchemaVersionStore } from './providers/schema-version-store';
|
||||||
import { resolvers } from './resolvers.generated';
|
import { resolvers } from './resolvers.generated';
|
||||||
import typeDefs from './module.graphql';
|
import typeDefs from './module.graphql';
|
||||||
|
|
||||||
|
|
@ -21,6 +22,7 @@ export const schemaModule = createModule({
|
||||||
typeDefs,
|
typeDefs,
|
||||||
resolvers,
|
resolvers,
|
||||||
providers: [
|
providers: [
|
||||||
|
SchemaVersionStore,
|
||||||
SchemaManager,
|
SchemaManager,
|
||||||
SchemaPublisher,
|
SchemaPublisher,
|
||||||
Inspector,
|
Inspector,
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,5 @@
|
||||||
import type { DocumentNode, GraphQLSchema, Kind } from 'graphql';
|
import type { DocumentNode, GraphQLSchema, Kind } from 'graphql';
|
||||||
import type {
|
import type { SchemaChangeType, SchemaCheck, SchemaCheckApprovalMetadata } from '@hive/storage';
|
||||||
SchemaChangeType,
|
|
||||||
SchemaCheck,
|
|
||||||
SchemaCheckApprovalMetadata,
|
|
||||||
SchemaVersion,
|
|
||||||
} from '@hive/storage';
|
|
||||||
import type { SchemaError } from '../../__generated__/types';
|
import type { SchemaError } from '../../__generated__/types';
|
||||||
import type { DateRange, PushedCompositeSchema, SingleSchema } from '../../shared/entities';
|
import type { DateRange, PushedCompositeSchema, SingleSchema } from '../../shared/entities';
|
||||||
import type { PromiseOrValue } from '../../shared/helpers';
|
import type { PromiseOrValue } from '../../shared/helpers';
|
||||||
|
|
@ -16,6 +11,7 @@ import type {
|
||||||
PaginatedContractConnection,
|
PaginatedContractConnection,
|
||||||
} from './providers/contracts';
|
} from './providers/contracts';
|
||||||
import type { SchemaCheckWarning } from './providers/models/shared';
|
import type { SchemaCheckWarning } from './providers/models/shared';
|
||||||
|
import type { SchemaVersion } from './providers/schema-version-store';
|
||||||
|
|
||||||
export type SchemaChangeConnectionMapper = ReadonlyArray<SchemaChangeMapper>;
|
export type SchemaChangeConnectionMapper = ReadonlyArray<SchemaChangeMapper>;
|
||||||
export type SchemaChangeMapper = SchemaChangeType;
|
export type SchemaChangeMapper = SchemaChangeType;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Injectable, Scope } from 'graphql-modules';
|
import { Injectable, Scope } from 'graphql-modules';
|
||||||
import type { SchemaCheck, SchemaVersion } from '@hive/storage';
|
import type { SchemaCheck } from '@hive/storage';
|
||||||
import * as GraphQLSchema from '../../../__generated__/types';
|
import * as GraphQLSchema from '../../../__generated__/types';
|
||||||
import type { Target } from '../../../shared/entities';
|
import type { Target } from '../../../shared/entities';
|
||||||
import { cache } from '../../../shared/helpers';
|
import { cache } from '../../../shared/helpers';
|
||||||
|
|
@ -15,6 +15,7 @@ import {
|
||||||
type ContractVersion,
|
type ContractVersion,
|
||||||
type CreateContractInput,
|
type CreateContractInput,
|
||||||
} from './contracts';
|
} from './contracts';
|
||||||
|
import type { SchemaVersion } from './schema-version-store';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
scope: Scope.Operation,
|
scope: Scope.Operation,
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@ import type {
|
||||||
SuccessfulSchemaCheckMapper,
|
SuccessfulSchemaCheckMapper,
|
||||||
} from '../module.graphql.mappers';
|
} from '../module.graphql.mappers';
|
||||||
import { Injectable, Scope } from 'graphql-modules';
|
import { Injectable, Scope } from 'graphql-modules';
|
||||||
import { Storage } from '../../shared/providers/storage';
|
|
||||||
import { formatNumber } from '../lib/number-formatting';
|
import { formatNumber } from '../lib/number-formatting';
|
||||||
import { SchemaManager } from './schema-manager';
|
import { SchemaManager } from './schema-manager';
|
||||||
|
import { SchemaVersionStore } from './schema-version-store';
|
||||||
|
|
||||||
type SchemaCheck = FailedSchemaCheckMapper | SuccessfulSchemaCheckMapper;
|
type SchemaCheck = FailedSchemaCheckMapper | SuccessfulSchemaCheckMapper;
|
||||||
|
|
||||||
|
|
@ -16,7 +16,7 @@ type SchemaCheck = FailedSchemaCheckMapper | SuccessfulSchemaCheckMapper;
|
||||||
export class SchemaCheckManager {
|
export class SchemaCheckManager {
|
||||||
constructor(
|
constructor(
|
||||||
private schemaManager: SchemaManager,
|
private schemaManager: SchemaManager,
|
||||||
private storage: Storage,
|
private schemaVersions: SchemaVersionStore,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
getHasSchemaCompositionErrors(schemaCheck: SchemaCheck) {
|
getHasSchemaCompositionErrors(schemaCheck: SchemaCheck) {
|
||||||
|
|
@ -63,7 +63,7 @@ export class SchemaCheckManager {
|
||||||
if (schemaCheck.schemaVersionId === null) {
|
if (schemaCheck.schemaVersionId === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return this.schemaManager.getSchemaVersion({
|
return this.schemaManager.getSchemaVersionBySelector({
|
||||||
organizationId: schemaCheck.selector.organizationId,
|
organizationId: schemaCheck.selector.organizationId,
|
||||||
projectId: schemaCheck.selector.projectId,
|
projectId: schemaCheck.selector.projectId,
|
||||||
targetId: schemaCheck.targetId,
|
targetId: schemaCheck.targetId,
|
||||||
|
|
@ -76,10 +76,10 @@ export class SchemaCheckManager {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const service = await this.storage.getSchemaByNameOfVersion({
|
const service = await this.schemaVersions.getSchemaForSchemaVersionIdAndName(
|
||||||
versionId: schemaCheck.schemaVersionId,
|
schemaCheck.schemaVersionId,
|
||||||
serviceName: schemaCheck.serviceName,
|
schemaCheck.serviceName,
|
||||||
});
|
);
|
||||||
|
|
||||||
return service?.sdl ?? null;
|
return service?.sdl ?? null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,9 +43,9 @@ import { TargetManager } from '../../target/providers/target-manager';
|
||||||
import { BreakingSchemaChangeUsageHelper } from './breaking-schema-changes-helper';
|
import { BreakingSchemaChangeUsageHelper } from './breaking-schema-changes-helper';
|
||||||
import { SCHEMA_MODULE_CONFIG, type SchemaModuleConfig } from './config';
|
import { SCHEMA_MODULE_CONFIG, type SchemaModuleConfig } from './config';
|
||||||
import { Contracts } from './contracts';
|
import { Contracts } from './contracts';
|
||||||
import type { SchemaCoordinatesDiffResult } from './inspector';
|
|
||||||
import { CompositionOrchestrator } from './orchestrator/composition-orchestrator';
|
import { CompositionOrchestrator } from './orchestrator/composition-orchestrator';
|
||||||
import { ensureCompositeSchemas, removeDescriptions, SchemaHelper } from './schema-helper';
|
import { ensureCompositeSchemas, removeDescriptions, SchemaHelper } from './schema-helper';
|
||||||
|
import { SchemaVersionStore } from './schema-version-store';
|
||||||
|
|
||||||
const ENABLE_EXTERNAL_COMPOSITION_SCHEMA = z.object({
|
const ENABLE_EXTERNAL_COMPOSITION_SCHEMA = z.object({
|
||||||
endpoint: z.string().url().nonempty(),
|
endpoint: z.string().url().nonempty(),
|
||||||
|
|
@ -84,6 +84,7 @@ export class SchemaManager {
|
||||||
private contracts: Contracts,
|
private contracts: Contracts,
|
||||||
private breakingSchemaChangeUsageHelper: BreakingSchemaChangeUsageHelper,
|
private breakingSchemaChangeUsageHelper: BreakingSchemaChangeUsageHelper,
|
||||||
private idTranslator: IdTranslator,
|
private idTranslator: IdTranslator,
|
||||||
|
private schemaVersions: SchemaVersionStore,
|
||||||
@Inject(SCHEMA_MODULE_CONFIG) private schemaModuleConfig: SchemaModuleConfig,
|
@Inject(SCHEMA_MODULE_CONFIG) private schemaModuleConfig: SchemaModuleConfig,
|
||||||
) {
|
) {
|
||||||
this.logger = logger.child({ source: 'SchemaManager' });
|
this.logger = logger.child({ source: 'SchemaManager' });
|
||||||
|
|
@ -92,7 +93,9 @@ export class SchemaManager {
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
selectors.map(async selector => {
|
selectors.map(async selector => {
|
||||||
return {
|
return {
|
||||||
...(await this.storage.getLatestValidVersion(selector)),
|
...(await this.schemaVersions.getLatestValidSchemaVersionForTargetId(
|
||||||
|
selector.targetId,
|
||||||
|
)),
|
||||||
projectId: selector.projectId,
|
projectId: selector.projectId,
|
||||||
targetId: selector.targetId,
|
targetId: selector.targetId,
|
||||||
organizationId: selector.organizationId,
|
organizationId: selector.organizationId,
|
||||||
|
|
@ -110,11 +113,7 @@ export class SchemaManager {
|
||||||
|
|
||||||
async hasSchema(target: Target) {
|
async hasSchema(target: Target) {
|
||||||
this.logger.debug('Checking if schema is available (targetId=%s)', target.id);
|
this.logger.debug('Checking if schema is available (targetId=%s)', target.id);
|
||||||
return this.storage.hasSchema({
|
return this.schemaVersions.anyVersionExistsForTarget(target);
|
||||||
organizationId: target.orgId,
|
|
||||||
projectId: target.projectId,
|
|
||||||
targetId: target.id,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@traceFn('SchemaManager.compose', {
|
@traceFn('SchemaManager.compose', {
|
||||||
|
|
@ -255,7 +254,7 @@ export class SchemaManager {
|
||||||
} & TargetSelector,
|
} & TargetSelector,
|
||||||
) {
|
) {
|
||||||
this.logger.debug('Fetching non-empty list of schemas (selector=%o)', selector);
|
this.logger.debug('Fetching non-empty list of schemas (selector=%o)', selector);
|
||||||
const schemas = await this.storage.getSchemasOfVersion(selector);
|
const schemas = await this.schemaVersions.getSchemasBySchemaVersionId(selector.versionId);
|
||||||
|
|
||||||
if (schemas.length === 0) {
|
if (schemas.length === 0) {
|
||||||
throw new HiveError('No schemas found for this version.');
|
throw new HiveError('No schemas found for this version.');
|
||||||
|
|
@ -275,19 +274,17 @@ export class SchemaManager {
|
||||||
@atomic(stringifySelector)
|
@atomic(stringifySelector)
|
||||||
async getMaybeSchemasOfVersion(schemaVersion: SchemaVersion) {
|
async getMaybeSchemasOfVersion(schemaVersion: SchemaVersion) {
|
||||||
this.logger.debug('Fetching schemas (schemaVersionId=%s)', schemaVersion.id);
|
this.logger.debug('Fetching schemas (schemaVersionId=%s)', schemaVersion.id);
|
||||||
return this.storage.getSchemasOfVersion({ versionId: schemaVersion.id });
|
return this.schemaVersions.getSchemasBySchemaVersionId(schemaVersion.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMatchingServiceSchemaOfVersions(versions: { before: string | null; after: string }) {
|
async getMatchingServiceSchemaOfVersions(versions: { before: string | null; after: string }) {
|
||||||
this.logger.debug('Fetching service schema of versions (selector=%o)', versions);
|
this.logger.debug('Fetching service schema of versions (selector=%o)', versions);
|
||||||
return this.storage.getMatchingServiceSchemaOfVersions(versions);
|
return this.schemaVersions.getMatchingServiceSchemaOfVersions(versions);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMaybeLatestValidVersion(target: Target) {
|
async getMaybeLatestValidVersion(target: Target) {
|
||||||
this.logger.debug('Fetching maybe latest valid version (targetId=%o)', target.id);
|
this.logger.debug('Fetching maybe latest valid version (targetId=%o)', target.id);
|
||||||
const version = await this.storage.getMaybeLatestValidVersion({
|
const version = await this.schemaVersions.getMaybeLatestValidSchemaVersion(target);
|
||||||
targetId: target.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!version) {
|
if (!version) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -308,11 +305,7 @@ export class SchemaManager {
|
||||||
|
|
||||||
async getMaybeLatestVersion(target: Target) {
|
async getMaybeLatestVersion(target: Target) {
|
||||||
this.logger.debug('Fetching maybe latest version (targetId=%o)', target.id);
|
this.logger.debug('Fetching maybe latest version (targetId=%o)', target.id);
|
||||||
const latest = await this.storage.getMaybeLatestVersion({
|
const latest = await this.schemaVersions.getMaybeLatestSchemaVersionForTargetId(target.id);
|
||||||
targetId: target.id,
|
|
||||||
projectId: target.projectId,
|
|
||||||
organizationId: target.orgId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!latest) {
|
if (!latest) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -326,7 +319,7 @@ export class SchemaManager {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSchemaVersion(
|
async getSchemaVersionBySelector(
|
||||||
selector: TargetSelector & { versionId: string },
|
selector: TargetSelector & { versionId: string },
|
||||||
): Promise<SchemaVersion | null> {
|
): Promise<SchemaVersion | null> {
|
||||||
this.logger.debug('Fetching single schema version (selector=%o)', selector);
|
this.logger.debug('Fetching single schema version (selector=%o)', selector);
|
||||||
|
|
@ -336,17 +329,20 @@ export class SchemaManager {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await this.storage.getMaybeVersion(selector);
|
const version = await this.schemaVersions.getSchemaVersionById(selector.versionId);
|
||||||
|
|
||||||
if (!result) {
|
if (!version) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version.targetId !== selector.targetId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
projectId: selector.projectId,
|
projectId: selector.projectId,
|
||||||
targetId: selector.targetId,
|
|
||||||
organizationId: selector.organizationId,
|
organizationId: selector.organizationId,
|
||||||
...result,
|
...version,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -362,10 +358,7 @@ export class SchemaManager {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const schemas = await this.storage.getSchemasOfVersion({
|
const schemas = await this.schemaVersions.getSchemasBySchemaVersionId(schemaVersion.id);
|
||||||
versionId: schemaVersion.id,
|
|
||||||
includeMetadata: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
version: schemaVersion,
|
version: schemaVersion,
|
||||||
|
|
@ -373,14 +366,14 @@ export class SchemaManager {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPaginatedSchemaVersionsForTargetId(args: {
|
async getPaginatedSchemaVersionsForTargetId(
|
||||||
targetId: string;
|
target: Target,
|
||||||
organizationId: string;
|
args: {
|
||||||
projectId: string;
|
first: number | null;
|
||||||
first: number | null;
|
cursor: null | string;
|
||||||
cursor: null | string;
|
},
|
||||||
}) {
|
) {
|
||||||
const connection = await this.storage.getPaginatedSchemaVersionsForTargetId(args);
|
const connection = await this.schemaVersions.getPaginatedSchemaVersionsForTarget(target, args);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...connection,
|
...connection,
|
||||||
|
|
@ -388,9 +381,9 @@ export class SchemaManager {
|
||||||
...edge,
|
...edge,
|
||||||
node: {
|
node: {
|
||||||
...edge.node,
|
...edge.node,
|
||||||
organizationId: args.organizationId,
|
organizationId: target.orgId,
|
||||||
projectId: args.projectId,
|
projectId: target.projectId,
|
||||||
targetId: args.targetId,
|
targetId: target.id,
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|
@ -411,7 +404,7 @@ export class SchemaManager {
|
||||||
|
|
||||||
async getSchemaLog(selector: { commit: string } & TargetSelector) {
|
async getSchemaLog(selector: { commit: string } & TargetSelector) {
|
||||||
this.logger.debug('Fetching schema log (selector=%o)', selector);
|
this.logger.debug('Fetching schema log (selector=%o)', selector);
|
||||||
return this.storage.getSchemaLog({
|
return this.schemaVersions.getSchemLog({
|
||||||
commit: selector.commit,
|
commit: selector.commit,
|
||||||
targetId: selector.targetId,
|
targetId: selector.targetId,
|
||||||
});
|
});
|
||||||
|
|
@ -438,10 +431,8 @@ export class SchemaManager {
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
base_schema: string | null;
|
base_schema: string | null;
|
||||||
metadata: string | null;
|
metadata: string | null;
|
||||||
projectType: ProjectType;
|
|
||||||
actionFn(versionId: string): Promise<void>;
|
actionFn(versionId: string): Promise<void>;
|
||||||
changes: Array<SchemaChangeType>;
|
changes: Array<SchemaChangeType>;
|
||||||
coordinatesDiff: SchemaCoordinatesDiffResult | null;
|
|
||||||
previousSchemaVersion: string | null;
|
previousSchemaVersion: string | null;
|
||||||
diffSchemaVersionId: string | null;
|
diffSchemaVersionId: string | null;
|
||||||
github: null | {
|
github: null | {
|
||||||
|
|
@ -489,7 +480,6 @@ export class SchemaManager {
|
||||||
'service',
|
'service',
|
||||||
'logIds',
|
'logIds',
|
||||||
'url',
|
'url',
|
||||||
'projectType',
|
|
||||||
'previousSchemaVersion',
|
'previousSchemaVersion',
|
||||||
'diffSchemaVersionId',
|
'diffSchemaVersionId',
|
||||||
'github',
|
'github',
|
||||||
|
|
@ -497,7 +487,7 @@ export class SchemaManager {
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
|
||||||
return this.storage.createVersion({
|
return this.schemaVersions.createSchemaVersion({
|
||||||
...input,
|
...input,
|
||||||
logIds: input.logIds,
|
logIds: input.logIds,
|
||||||
});
|
});
|
||||||
|
|
@ -589,22 +579,14 @@ export class SchemaManager {
|
||||||
await this.storage.updateBaseSchema(selector, newBaseSchema);
|
await this.storage.updateBaseSchema(selector, newBaseSchema);
|
||||||
}
|
}
|
||||||
|
|
||||||
countSchemaVersionsOfProject(
|
countSchemaVersionsOfProject(project: Project, period: DateRange | null): Promise<number> {
|
||||||
selector: ProjectSelector & {
|
this.logger.debug('Fetching schema versions count of project (projectId=%s)', project.id);
|
||||||
period: DateRange | null;
|
return this.schemaVersions.countSchemaVersionsOfProject(project, period);
|
||||||
},
|
|
||||||
): Promise<number> {
|
|
||||||
this.logger.debug('Fetching schema versions count of project (selector=%o)', selector);
|
|
||||||
return this.storage.countSchemaVersionsOfProject(selector);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
countSchemaVersionsOfTarget(
|
countSchemaVersionsOfTarget(target: Target, period: DateRange | null): Promise<number> {
|
||||||
selector: TargetSelector & {
|
this.logger.debug('Fetching schema versions count of target (targetId=%s)', target.id);
|
||||||
period: DateRange | null;
|
return this.schemaVersions.countSchemaVersionsOfTarget(target, period);
|
||||||
},
|
|
||||||
): Promise<number> {
|
|
||||||
this.logger.debug('Fetching schema versions count of target (selector=%o)', selector);
|
|
||||||
return this.storage.countSchemaVersionsOfTarget(selector);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async completeGetStartedCheck(
|
async completeGetStartedCheck(
|
||||||
|
|
@ -1084,11 +1066,8 @@ export class SchemaManager {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const record = await this.storage.getSchemaVersionByCommit({
|
const target = await this.targetManager.getTargetById({ targetId: selector.targetId });
|
||||||
projectId: selector.projectId,
|
const record = await this.schemaVersions.getSchemaVersionForTargetByCommit(target, args.commit);
|
||||||
targetId: selector.targetId,
|
|
||||||
commit: args.commit,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!record) {
|
if (!record) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -1102,47 +1081,31 @@ export class SchemaManager {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getComposableVersionBeforeVersionId(args: {
|
async getComposableVersionBeforeVersionId(schemaVersion: SchemaVersion) {
|
||||||
organization: string;
|
this.logger.debug('Fetch version before version id. (schemaVersionId=%s)', schemaVersion.id);
|
||||||
project: string;
|
|
||||||
target: string;
|
|
||||||
beforeVersionId: string;
|
|
||||||
beforeVersionCreatedAt: string;
|
|
||||||
}) {
|
|
||||||
this.logger.debug('Fetch version before version id. (args=%o)', args);
|
|
||||||
|
|
||||||
const schemaVersion = await this.storage.getVersionBeforeVersionId({
|
const previousSchemaVersion = await this.schemaVersions.getSchemaVersionBeforeSchemaVersion(
|
||||||
targetId: args.target,
|
schemaVersion,
|
||||||
beforeVersionId: args.beforeVersionId,
|
true,
|
||||||
beforeVersionCreatedAt: args.beforeVersionCreatedAt,
|
);
|
||||||
onlyComposable: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!schemaVersion) {
|
if (!previousSchemaVersion) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...schemaVersion,
|
...previousSchemaVersion,
|
||||||
organizationId: args.organization,
|
organizationId: schemaVersion.organizationId,
|
||||||
projectId: args.project,
|
projectId: schemaVersion.projectId,
|
||||||
targetId: args.target,
|
targetId: schemaVersion.targetId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFirstComposableSchemaVersionBeforeVersionId(args: {
|
async getFirstComposableSchemaVersionBeforeSchemaVersion(previousSchemaVersion: SchemaVersion) {
|
||||||
organization: string;
|
const schemaVersion = await this.schemaVersions.getSchemaVersionBeforeSchemaVersion(
|
||||||
project: string;
|
previousSchemaVersion,
|
||||||
target: string;
|
true,
|
||||||
beforeVersionId: string;
|
);
|
||||||
beforeVersionCreatedAt: string;
|
|
||||||
}) {
|
|
||||||
const schemaVersion = await this.storage.getVersionBeforeVersionId({
|
|
||||||
targetId: args.target,
|
|
||||||
beforeVersionId: args.beforeVersionId,
|
|
||||||
beforeVersionCreatedAt: args.beforeVersionCreatedAt,
|
|
||||||
onlyComposable: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!schemaVersion) {
|
if (!schemaVersion) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -1150,9 +1113,9 @@ export class SchemaManager {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...schemaVersion,
|
...schemaVersion,
|
||||||
organizationId: args.organization,
|
organizationId: previousSchemaVersion.organizationId,
|
||||||
projectId: args.project,
|
projectId: previousSchemaVersion.projectId,
|
||||||
targetId: args.target,
|
targetId: previousSchemaVersion.targetId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ import {
|
||||||
} from './schema-helper';
|
} from './schema-helper';
|
||||||
import { SchemaManager } from './schema-manager';
|
import { SchemaManager } from './schema-manager';
|
||||||
import { SchemaVersionHelper } from './schema-version-helper';
|
import { SchemaVersionHelper } from './schema-version-helper';
|
||||||
|
import { SchemaVersionStore } from './schema-version-store';
|
||||||
|
|
||||||
const schemaCheckCount = new promClient.Counter({
|
const schemaCheckCount = new promClient.Counter({
|
||||||
name: 'registry_check_count',
|
name: 'registry_check_count',
|
||||||
|
|
@ -165,6 +166,7 @@ export class SchemaPublisher {
|
||||||
private schemaVersionHelper: SchemaVersionHelper,
|
private schemaVersionHelper: SchemaVersionHelper,
|
||||||
private operationsReader: OperationsReader,
|
private operationsReader: OperationsReader,
|
||||||
private idTranslator: IdTranslator,
|
private idTranslator: IdTranslator,
|
||||||
|
private schemaVersions: SchemaVersionStore,
|
||||||
@Inject(SCHEMA_MODULE_CONFIG) private schemaModuleConfig: SchemaModuleConfig,
|
@Inject(SCHEMA_MODULE_CONFIG) private schemaModuleConfig: SchemaModuleConfig,
|
||||||
singleModel: SingleModel,
|
singleModel: SingleModel,
|
||||||
compositeModel: CompositeModel,
|
compositeModel: CompositeModel,
|
||||||
|
|
@ -1493,15 +1495,11 @@ export class SchemaPublisher {
|
||||||
if (deleteResult.conclusion === SchemaDeleteConclusion.Accept) {
|
if (deleteResult.conclusion === SchemaDeleteConclusion.Accept) {
|
||||||
this.logger.debug('Delete accepted');
|
this.logger.debug('Delete accepted');
|
||||||
if (input.dryRun !== true) {
|
if (input.dryRun !== true) {
|
||||||
const schemaVersion = await this.storage.deleteSchema({
|
const schemaVersion = await this.schemaVersions.deleteSubgraphFromTarget(target, {
|
||||||
organizationId: selector.organizationId,
|
|
||||||
projectId: selector.projectId,
|
|
||||||
targetId: selector.targetId,
|
|
||||||
serviceName: input.serviceName,
|
serviceName: input.serviceName,
|
||||||
composable: deleteResult.state.composable,
|
composable: deleteResult.state.composable,
|
||||||
diffSchemaVersionId: latestComposableVersion?.version.id ?? null,
|
diffSchemaVersionId: latestComposableVersion?.version.id ?? null,
|
||||||
changes: deleteResult.state.changes,
|
changes: deleteResult.state.changes,
|
||||||
coordinatesDiff: deleteResult.state.coordinatesDiff,
|
|
||||||
contracts:
|
contracts:
|
||||||
deleteResult.state.contracts?.map(contract => ({
|
deleteResult.state.contracts?.map(contract => ({
|
||||||
contractId: contract.contractId,
|
contractId: contract.contractId,
|
||||||
|
|
@ -2027,7 +2025,6 @@ export class SchemaPublisher {
|
||||||
url: serviceUrl,
|
url: serviceUrl,
|
||||||
base_schema: baseSchema,
|
base_schema: baseSchema,
|
||||||
metadata: input.metadata ?? null,
|
metadata: input.metadata ?? null,
|
||||||
projectType: project.type,
|
|
||||||
github,
|
github,
|
||||||
actionFn: async (versionId: string) => {
|
actionFn: async (versionId: string) => {
|
||||||
if (composable && fullSchemaSdl) {
|
if (composable && fullSchemaSdl) {
|
||||||
|
|
@ -2054,7 +2051,6 @@ export class SchemaPublisher {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
changes,
|
changes,
|
||||||
coordinatesDiff: publishResult.state.coordinatesDiff,
|
|
||||||
diffSchemaVersionId: latestComposable?.version.id ?? null,
|
diffSchemaVersionId: latestComposable?.version.id ?? null,
|
||||||
previousSchemaVersion: latestVersion?.version.id ?? null,
|
previousSchemaVersion: latestVersion?.version.id ?? null,
|
||||||
conditionalBreakingChangeMetadata: await this.getConditionalBreakingChangeMetadata({
|
conditionalBreakingChangeMetadata: await this.getConditionalBreakingChangeMetadata({
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import { CompositionOrchestrator } from './orchestrator/composition-orchestrator
|
||||||
import { RegistryChecks } from './registry-checks';
|
import { RegistryChecks } from './registry-checks';
|
||||||
import { ensureCompositeSchemas, SchemaHelper, toCompositeSchemaInput } from './schema-helper';
|
import { ensureCompositeSchemas, SchemaHelper, toCompositeSchemaInput } from './schema-helper';
|
||||||
import { SchemaManager } from './schema-manager';
|
import { SchemaManager } from './schema-manager';
|
||||||
|
import { SchemaVersionStore } from './schema-version-store';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
scope: Scope.Operation,
|
scope: Scope.Operation,
|
||||||
|
|
@ -38,6 +39,7 @@ export class SchemaVersionHelper {
|
||||||
private storage: Storage,
|
private storage: Storage,
|
||||||
private logger: Logger,
|
private logger: Logger,
|
||||||
private compositionOrchestrator: CompositionOrchestrator,
|
private compositionOrchestrator: CompositionOrchestrator,
|
||||||
|
private schemaVersions: SchemaVersionStore,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@traceFn('SchemaVersionHelper.composeSchemaVersion', {
|
@traceFn('SchemaVersionHelper.composeSchemaVersion', {
|
||||||
|
|
@ -51,9 +53,7 @@ export class SchemaVersionHelper {
|
||||||
@cache<SchemaVersion>(version => version.id)
|
@cache<SchemaVersion>(version => version.id)
|
||||||
private async composeSchemaVersion(schemaVersion: SchemaVersion) {
|
private async composeSchemaVersion(schemaVersion: SchemaVersion) {
|
||||||
const [schemas, project, organization] = await Promise.all([
|
const [schemas, project, organization] = await Promise.all([
|
||||||
this.storage.getSchemasOfVersion({
|
this.schemaVersions.getSchemasBySchemaVersionId(schemaVersion.id),
|
||||||
versionId: schemaVersion.id,
|
|
||||||
}),
|
|
||||||
this.projectManager.getProject({
|
this.projectManager.getProject({
|
||||||
organizationId: schemaVersion.organizationId,
|
organizationId: schemaVersion.organizationId,
|
||||||
projectId: schemaVersion.projectId,
|
projectId: schemaVersion.projectId,
|
||||||
|
|
@ -186,11 +186,8 @@ export class SchemaVersionHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (schemaVersion.hasPersistedSchemaChanges) {
|
if (schemaVersion.hasPersistedSchemaChanges) {
|
||||||
const changes: null | Array<SchemaChangeType> = await this.storage.getSchemaChangesForVersion(
|
const changes: null | Array<SchemaChangeType> =
|
||||||
{
|
await this.schemaVersions.getSchemaSchangesForSchemaVersion(schemaVersion);
|
||||||
versionId: schemaVersion.id,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const safeChanges: Array<SchemaChangeType> = [];
|
const safeChanges: Array<SchemaChangeType> = [];
|
||||||
const breakingChanges: Array<SchemaChangeType> = [];
|
const breakingChanges: Array<SchemaChangeType> = [];
|
||||||
|
|
@ -220,12 +217,8 @@ export class SchemaVersionHelper {
|
||||||
const incomingSdl = await this.getCompositeSchemaSdl(schemaVersion);
|
const incomingSdl = await this.getCompositeSchemaSdl(schemaVersion);
|
||||||
|
|
||||||
const [schemaBefore, schemasAfter] = await Promise.all([
|
const [schemaBefore, schemasAfter] = await Promise.all([
|
||||||
this.storage.getSchemasOfVersion({
|
this.schemaVersions.getSchemasBySchemaVersionId(schemaVersion.id),
|
||||||
versionId: schemaVersion.id,
|
this.schemaVersions.getSchemasBySchemaVersionId(previousVersion.id),
|
||||||
}),
|
|
||||||
this.storage.getSchemasOfVersion({
|
|
||||||
versionId: previousVersion.id,
|
|
||||||
}),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!existingSdl || !incomingSdl) {
|
if (!existingSdl || !incomingSdl) {
|
||||||
|
|
@ -271,7 +264,7 @@ export class SchemaVersionHelper {
|
||||||
): Promise<SchemaVersion | null> {
|
): Promise<SchemaVersion | null> {
|
||||||
if (schemaVersion.recordVersion === '2024-01-10') {
|
if (schemaVersion.recordVersion === '2024-01-10') {
|
||||||
if (schemaVersion.diffSchemaVersionId) {
|
if (schemaVersion.diffSchemaVersionId) {
|
||||||
return await this.schemaManager.getSchemaVersion({
|
return await this.schemaManager.getSchemaVersionBySelector({
|
||||||
organizationId: schemaVersion.organizationId,
|
organizationId: schemaVersion.organizationId,
|
||||||
projectId: schemaVersion.projectId,
|
projectId: schemaVersion.projectId,
|
||||||
targetId: schemaVersion.targetId,
|
targetId: schemaVersion.targetId,
|
||||||
|
|
@ -281,13 +274,7 @@ export class SchemaVersionHelper {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.schemaManager.getComposableVersionBeforeVersionId({
|
return await this.schemaManager.getComposableVersionBeforeVersionId(schemaVersion);
|
||||||
organization: schemaVersion.organizationId,
|
|
||||||
project: schemaVersion.projectId,
|
|
||||||
target: schemaVersion.targetId,
|
|
||||||
beforeVersionId: schemaVersion.id,
|
|
||||||
beforeVersionCreatedAt: schemaVersion.createdAt,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getBreakingSchemaChanges(schemaVersion: SchemaVersion) {
|
async getBreakingSchemaChanges(schemaVersion: SchemaVersion) {
|
||||||
|
|
@ -327,13 +314,7 @@ export class SchemaVersionHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
const composableVersion =
|
const composableVersion =
|
||||||
await this.schemaManager.getFirstComposableSchemaVersionBeforeVersionId({
|
await this.schemaManager.getFirstComposableSchemaVersionBeforeSchemaVersion(schemaVersion);
|
||||||
organization: schemaVersion.organizationId,
|
|
||||||
project: schemaVersion.projectId,
|
|
||||||
target: schemaVersion.targetId,
|
|
||||||
beforeVersionId: schemaVersion.id,
|
|
||||||
beforeVersionCreatedAt: schemaVersion.createdAt,
|
|
||||||
});
|
|
||||||
|
|
||||||
return !composableVersion;
|
return !composableVersion;
|
||||||
}
|
}
|
||||||
|
|
@ -353,10 +334,10 @@ export class SchemaVersionHelper {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const schemaLog = await this.storage.getServiceSchemaOfVersion({
|
const schemaLog = await this.schemaVersions.getServiceSchemaOfVersion(
|
||||||
schemaVersionId: previousVersion.id,
|
schemaVersion,
|
||||||
serviceName,
|
serviceName,
|
||||||
});
|
);
|
||||||
|
|
||||||
return schemaLog?.sdl ?? null;
|
return schemaLog?.sdl ?? null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -19,11 +19,9 @@ export const Project: Pick<
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
schemaVersionsCount: (project, { period }, { injector }) => {
|
schemaVersionsCount: (project, { period }, { injector }) => {
|
||||||
return injector.get(SchemaManager).countSchemaVersionsOfProject({
|
return injector
|
||||||
organizationId: project.orgId,
|
.get(SchemaManager)
|
||||||
projectId: project.id,
|
.countSchemaVersionsOfProject(project, period ? parseDateRangeInput(period) : null);
|
||||||
period: period ? parseDateRangeInput(period) : null,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
isNativeFederationEnabled: project => {
|
isNativeFederationEnabled: project => {
|
||||||
return project.nativeFederation === true;
|
return project.nativeFederation === true;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { parseDateRangeInput } from '../../../shared/helpers';
|
||||||
import { OperationsManager } from '../../operations/providers/operations-manager';
|
import { OperationsManager } from '../../operations/providers/operations-manager';
|
||||||
import { ContractsManager } from '../providers/contracts-manager';
|
import { ContractsManager } from '../providers/contracts-manager';
|
||||||
import { SchemaManager } from '../providers/schema-manager';
|
import { SchemaManager } from '../providers/schema-manager';
|
||||||
|
import { SchemaVersionStore } from '../providers/schema-version-store';
|
||||||
import { toGraphQLSchemaCheck, toGraphQLSchemaCheckCurry } from '../to-graphql-schema-check';
|
import { toGraphQLSchemaCheck, toGraphQLSchemaCheckCurry } from '../to-graphql-schema-check';
|
||||||
import type { TargetResolvers } from './../../../__generated__/types';
|
import type { TargetResolvers } from './../../../__generated__/types';
|
||||||
|
|
||||||
|
|
@ -21,32 +22,18 @@ export const Target: Pick<
|
||||||
| 'schemaVersionsCount'
|
| 'schemaVersionsCount'
|
||||||
> = {
|
> = {
|
||||||
schemaVersions: async (target, args, { injector }) => {
|
schemaVersions: async (target, args, { injector }) => {
|
||||||
return injector.get(SchemaManager).getPaginatedSchemaVersionsForTargetId({
|
return injector.get(SchemaManager).getPaginatedSchemaVersionsForTargetId(target, {
|
||||||
targetId: target.id,
|
|
||||||
organizationId: target.orgId,
|
|
||||||
projectId: target.projectId,
|
|
||||||
cursor: args.after ?? null,
|
cursor: args.after ?? null,
|
||||||
first: args.first ?? null,
|
first: args.first ?? null,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
schemaVersion: async (target, args, { injector }) => {
|
schemaVersion: async (target, args, { injector }) => {
|
||||||
const schemaVersion = await injector.get(SchemaManager).getSchemaVersion({
|
return await injector.get(SchemaManager).getSchemaVersionBySelector({
|
||||||
organizationId: target.orgId,
|
organizationId: target.orgId,
|
||||||
projectId: target.projectId,
|
projectId: target.projectId,
|
||||||
targetId: target.id,
|
targetId: target.id,
|
||||||
versionId: args.id,
|
versionId: args.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (schemaVersion === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...schemaVersion,
|
|
||||||
organizationId: target.orgId,
|
|
||||||
projectId: target.projectId,
|
|
||||||
targetId: target.id,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
latestSchemaVersion: (target, _, { injector }) => {
|
latestSchemaVersion: (target, _, { injector }) => {
|
||||||
return injector.get(SchemaManager).getMaybeLatestVersion(target);
|
return injector.get(SchemaManager).getMaybeLatestVersion(target);
|
||||||
|
|
@ -58,7 +45,7 @@ export const Target: Pick<
|
||||||
return injector.get(SchemaManager).getBaseSchemaForTarget(target);
|
return injector.get(SchemaManager).getBaseSchemaForTarget(target);
|
||||||
},
|
},
|
||||||
hasSchema: (target, _, { injector }) => {
|
hasSchema: (target, _, { injector }) => {
|
||||||
return injector.get(SchemaManager).hasSchema(target);
|
return injector.get(SchemaVersionStore).anyVersionExistsForTarget(target);
|
||||||
},
|
},
|
||||||
schemaCheck: async (target, args, { injector }) => {
|
schemaCheck: async (target, args, { injector }) => {
|
||||||
const schemaCheck = await injector.get(SchemaManager).findSchemaCheckForTarget(target, args.id);
|
const schemaCheck = await injector.get(SchemaManager).findSchemaCheckForTarget(target, args.id);
|
||||||
|
|
@ -92,12 +79,9 @@ export const Target: Pick<
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
schemaVersionsCount: (target, { period }, { injector }) => {
|
schemaVersionsCount: (target, { period }, { injector }) => {
|
||||||
return injector.get(SchemaManager).countSchemaVersionsOfTarget({
|
return injector
|
||||||
organizationId: target.orgId,
|
.get(SchemaManager)
|
||||||
projectId: target.projectId,
|
.countSchemaVersionsOfTarget(target, period ? parseDateRangeInput(period) : null);
|
||||||
targetId: target.id,
|
|
||||||
period: period ? parseDateRangeInput(period) : null,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
contracts: async (target, args, { injector }) => {
|
contracts: async (target, args, { injector }) => {
|
||||||
return await injector.get(ContractsManager).getPaginatedContractsForTarget({
|
return await injector.get(ContractsManager).getPaginatedContractsForTarget({
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,10 @@ import { Injectable } from 'graphql-modules';
|
||||||
import type { PolicyConfigurationObject } from '@hive/policy';
|
import type { PolicyConfigurationObject } from '@hive/policy';
|
||||||
import { PostgresDatabasePool } from '@hive/postgres';
|
import { PostgresDatabasePool } from '@hive/postgres';
|
||||||
import type {
|
import type {
|
||||||
ConditionalBreakingChangeMetadata,
|
|
||||||
PaginatedOrganizationInvitationConnection,
|
PaginatedOrganizationInvitationConnection,
|
||||||
PaginatedSchemaVersionConnection,
|
|
||||||
SchemaChangeType,
|
SchemaChangeType,
|
||||||
SchemaCheck,
|
SchemaCheck,
|
||||||
SchemaCheckInput,
|
SchemaCheckInput,
|
||||||
SchemaCompositionError,
|
|
||||||
SchemaVersion,
|
|
||||||
TargetBreadcrumb,
|
TargetBreadcrumb,
|
||||||
} from '@hive/storage';
|
} from '@hive/storage';
|
||||||
import type { SchemaChecksFilter } from '../../../__generated__/types';
|
import type { SchemaChecksFilter } from '../../../__generated__/types';
|
||||||
|
|
@ -17,7 +13,6 @@ import type {
|
||||||
Alert,
|
Alert,
|
||||||
AlertChannel,
|
AlertChannel,
|
||||||
CDNAccessToken,
|
CDNAccessToken,
|
||||||
DeletedCompositeSchema,
|
|
||||||
DocumentCollection,
|
DocumentCollection,
|
||||||
DocumentCollectionOperation,
|
DocumentCollectionOperation,
|
||||||
Member,
|
Member,
|
||||||
|
|
@ -28,8 +23,6 @@ import type {
|
||||||
PaginatedDocumentCollectionOperations,
|
PaginatedDocumentCollectionOperations,
|
||||||
PaginatedDocumentCollections,
|
PaginatedDocumentCollections,
|
||||||
Project,
|
Project,
|
||||||
Schema,
|
|
||||||
SchemaLog,
|
|
||||||
SchemaPolicy,
|
SchemaPolicy,
|
||||||
Target,
|
Target,
|
||||||
TargetSettings,
|
TargetSettings,
|
||||||
|
|
@ -42,7 +35,6 @@ import type {
|
||||||
} from '../../auth/providers/scopes';
|
} from '../../auth/providers/scopes';
|
||||||
import type { ResourceAssignmentGroup } from '../../organization/lib/resource-assignment-model';
|
import type { ResourceAssignmentGroup } from '../../organization/lib/resource-assignment-model';
|
||||||
import type { Contracts } from '../../schema/providers/contracts';
|
import type { Contracts } from '../../schema/providers/contracts';
|
||||||
import type { SchemaCoordinatesDiffResult } from '../../schema/providers/inspector';
|
|
||||||
|
|
||||||
export interface OrganizationSelector {
|
export interface OrganizationSelector {
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
|
|
@ -56,15 +48,6 @@ export interface TargetSelector extends ProjectSelector {
|
||||||
targetId: string;
|
targetId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateContractVersionInput = {
|
|
||||||
contractId: string;
|
|
||||||
contractName: string;
|
|
||||||
compositeSchemaSDL: string | null;
|
|
||||||
supergraphSDL: string | null;
|
|
||||||
schemaCompositionErrors: Array<SchemaCompositionError> | null;
|
|
||||||
changes: null | Array<SchemaChangeType>;
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
|
||||||
export interface Storage {
|
export interface Storage {
|
||||||
pool: PostgresDatabasePool;
|
pool: PostgresDatabasePool;
|
||||||
|
|
@ -351,69 +334,6 @@ export interface Storage {
|
||||||
_: Pick<TargetSelector, 'targetId' | 'projectId'> &
|
_: Pick<TargetSelector, 'targetId' | 'projectId'> &
|
||||||
Partial<TargetSettings['appDeploymentProtection']>,
|
Partial<TargetSettings['appDeploymentProtection']>,
|
||||||
): Promise<TargetSettings['appDeploymentProtection']>;
|
): Promise<TargetSettings['appDeploymentProtection']>;
|
||||||
|
|
||||||
countSchemaVersionsOfProject(
|
|
||||||
_: ProjectSelector & {
|
|
||||||
period: {
|
|
||||||
from: Date;
|
|
||||||
to: Date;
|
|
||||||
} | null;
|
|
||||||
},
|
|
||||||
): Promise<number>;
|
|
||||||
countSchemaVersionsOfTarget(
|
|
||||||
_: TargetSelector & {
|
|
||||||
period: {
|
|
||||||
from: Date;
|
|
||||||
to: Date;
|
|
||||||
} | null;
|
|
||||||
},
|
|
||||||
): Promise<number>;
|
|
||||||
|
|
||||||
hasSchema(_: TargetSelector): Promise<boolean>;
|
|
||||||
|
|
||||||
getLatestValidVersion(_: { targetId: string }): Promise<SchemaVersion | never>;
|
|
||||||
|
|
||||||
getMaybeLatestValidVersion(_: { targetId: string }): Promise<SchemaVersion | null | never>;
|
|
||||||
|
|
||||||
getMaybeLatestVersion(_: TargetSelector): Promise<SchemaVersion | null>;
|
|
||||||
|
|
||||||
/** Find the version before a schema version */
|
|
||||||
getVersionBeforeVersionId(_: {
|
|
||||||
targetId: string;
|
|
||||||
beforeVersionId: string;
|
|
||||||
beforeVersionCreatedAt: string;
|
|
||||||
onlyComposable: boolean;
|
|
||||||
}): Promise<SchemaVersion | null>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find a specific schema version via it's action id.
|
|
||||||
* The action id is the id of the action that created the schema version, it is user provided.
|
|
||||||
* Multiple entries with the same action ID can exist. In that case the latest one is returned.
|
|
||||||
*/
|
|
||||||
getSchemaVersionByCommit(_: {
|
|
||||||
targetId: string;
|
|
||||||
projectId: string;
|
|
||||||
commit: string;
|
|
||||||
}): Promise<SchemaVersion | null>;
|
|
||||||
getMatchingServiceSchemaOfVersions(versions: {
|
|
||||||
before: string | null;
|
|
||||||
after: string;
|
|
||||||
}): Promise<null | {
|
|
||||||
serviceName: string;
|
|
||||||
before: string | null;
|
|
||||||
after: string | null;
|
|
||||||
}>;
|
|
||||||
getSchemasOfVersion(_: { versionId: string; includeMetadata?: boolean }): Promise<Schema[]>;
|
|
||||||
getSchemaByNameOfVersion(_: { versionId: string; serviceName: string }): Promise<Schema | null>;
|
|
||||||
getServiceSchemaOfVersion(args: {
|
|
||||||
schemaVersionId: string;
|
|
||||||
serviceName: string;
|
|
||||||
}): Promise<Schema | null>;
|
|
||||||
getPaginatedSchemaVersionsForTargetId(args: {
|
|
||||||
targetId: string;
|
|
||||||
first: number | null;
|
|
||||||
cursor: null | string;
|
|
||||||
}): Promise<PaginatedSchemaVersionConnection>;
|
|
||||||
getPaginatedSchemaChecksForSchemaProposal<
|
getPaginatedSchemaChecksForSchemaProposal<
|
||||||
TransformedSchemaCheck extends SchemaCheck = SchemaCheck,
|
TransformedSchemaCheck extends SchemaCheck = SchemaCheck,
|
||||||
>(_: {
|
>(_: {
|
||||||
|
|
@ -438,96 +358,6 @@ export interface Storage {
|
||||||
}>;
|
}>;
|
||||||
}>
|
}>
|
||||||
>;
|
>;
|
||||||
getMaybeVersion(_: TargetSelector & { versionId: string }): Promise<SchemaVersion | null>;
|
|
||||||
deleteSchema(
|
|
||||||
_: {
|
|
||||||
serviceName: string;
|
|
||||||
composable: boolean;
|
|
||||||
actionFn(versionId: string): Promise<void>;
|
|
||||||
changes: Array<SchemaChangeType> | null;
|
|
||||||
diffSchemaVersionId: string | null;
|
|
||||||
conditionalBreakingChangeMetadata: null | ConditionalBreakingChangeMetadata;
|
|
||||||
contracts: null | Array<CreateContractVersionInput>;
|
|
||||||
coordinatesDiff: SchemaCoordinatesDiffResult | null;
|
|
||||||
} & TargetSelector &
|
|
||||||
(
|
|
||||||
| {
|
|
||||||
compositeSchemaSDL: null;
|
|
||||||
supergraphSDL: null;
|
|
||||||
schemaCompositionErrors: Array<SchemaCompositionError>;
|
|
||||||
tags: null;
|
|
||||||
schemaMetadata: null;
|
|
||||||
metadataAttributes: null;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
compositeSchemaSDL: string;
|
|
||||||
supergraphSDL: string | null;
|
|
||||||
schemaCompositionErrors: null;
|
|
||||||
tags: null | Array<string>;
|
|
||||||
schemaMetadata: null | Record<
|
|
||||||
string,
|
|
||||||
Array<{ name: string; content: string; source: string | null }>
|
|
||||||
>;
|
|
||||||
metadataAttributes: null | Record<string, string[]>;
|
|
||||||
}
|
|
||||||
),
|
|
||||||
): Promise<DeletedCompositeSchema & { versionId: string }>;
|
|
||||||
|
|
||||||
createVersion(
|
|
||||||
_: ({
|
|
||||||
schema: string;
|
|
||||||
author: string;
|
|
||||||
service?: string | null;
|
|
||||||
metadata: string | null;
|
|
||||||
valid: boolean;
|
|
||||||
url?: string | null;
|
|
||||||
commit: string;
|
|
||||||
logIds: string[];
|
|
||||||
base_schema: string | null;
|
|
||||||
actionFn(versionId: string): Promise<void>;
|
|
||||||
changes: Array<SchemaChangeType>;
|
|
||||||
previousSchemaVersion: null | string;
|
|
||||||
diffSchemaVersionId: null | string;
|
|
||||||
github: null | {
|
|
||||||
repository: string;
|
|
||||||
sha: string;
|
|
||||||
};
|
|
||||||
contracts: null | Array<CreateContractVersionInput>;
|
|
||||||
conditionalBreakingChangeMetadata: null | ConditionalBreakingChangeMetadata;
|
|
||||||
coordinatesDiff: SchemaCoordinatesDiffResult | null;
|
|
||||||
} & TargetSelector) &
|
|
||||||
(
|
|
||||||
| {
|
|
||||||
compositeSchemaSDL: null;
|
|
||||||
supergraphSDL: null;
|
|
||||||
schemaCompositionErrors: Array<SchemaCompositionError>;
|
|
||||||
tags: null;
|
|
||||||
schemaMetadata: null;
|
|
||||||
metadataAttributes: null;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
compositeSchemaSDL: string;
|
|
||||||
supergraphSDL: string | null;
|
|
||||||
schemaCompositionErrors: null;
|
|
||||||
tags: null | Array<string>;
|
|
||||||
schemaMetadata: null | Record<
|
|
||||||
string,
|
|
||||||
Array<{ name: string; content: string; source: string | null }>
|
|
||||||
>;
|
|
||||||
metadataAttributes: null | Record<string, string[]>;
|
|
||||||
}
|
|
||||||
),
|
|
||||||
): Promise<SchemaVersion | never>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the changes between the given version and the previous version.
|
|
||||||
* If it return `null` the schema version does not have any changes persisted.
|
|
||||||
* This can happen if the schema version was created before we introduced persisting changes.
|
|
||||||
*/
|
|
||||||
getSchemaChangesForVersion(_: { versionId: string }): Promise<null | Array<SchemaChangeType>>;
|
|
||||||
|
|
||||||
getSchemaLog(_: { commit: string; targetId: string }): Promise<SchemaLog>;
|
|
||||||
|
|
||||||
addSlackIntegration(_: OrganizationSelector & { token: string }): Promise<void>;
|
addSlackIntegration(_: OrganizationSelector & { token: string }): Promise<void>;
|
||||||
|
|
||||||
deleteSlackIntegration(_: OrganizationSelector): Promise<void>;
|
deleteSlackIntegration(_: OrganizationSelector): Promise<void>;
|
||||||
|
|
@ -881,4 +711,3 @@ export interface Storage {
|
||||||
@Injectable()
|
@Injectable()
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
|
||||||
export class Storage implements Storage {}
|
export class Storage implements Storage {}
|
||||||
export type { PaginatedSchemaVersionConnection };
|
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,7 @@ import { DocumentNode, GraphQLError, parse, print, SourceLocation } from 'graphq
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import type { AvailableRulesResponse, PolicyConfigurationObject } from '@hive/policy';
|
import type { AvailableRulesResponse, PolicyConfigurationObject } from '@hive/policy';
|
||||||
import type { CompositionFailureError } from '@hive/schema';
|
import type { CompositionFailureError } from '@hive/schema';
|
||||||
import type {
|
import type { schema_policy_resource } from '@hive/storage';
|
||||||
CompositeDeletedSchemaLog,
|
|
||||||
CompositePushSchemaLog,
|
|
||||||
schema_policy_resource,
|
|
||||||
SinglePushSchemaLog,
|
|
||||||
} from '@hive/storage';
|
|
||||||
import type {
|
import type {
|
||||||
AlertChannelType,
|
AlertChannelType,
|
||||||
AlertType,
|
AlertType,
|
||||||
|
|
@ -18,6 +13,11 @@ import type {
|
||||||
TargetAccessScope,
|
TargetAccessScope,
|
||||||
} from '../__generated__/types';
|
} from '../__generated__/types';
|
||||||
import type { ResourceAssignmentGroup } from '../modules/organization/lib/resource-assignment-model';
|
import type { ResourceAssignmentGroup } from '../modules/organization/lib/resource-assignment-model';
|
||||||
|
import type {
|
||||||
|
CompositeDeletedSchemaLog,
|
||||||
|
CompositePushSchemaLog,
|
||||||
|
SinglePushSchemaLog,
|
||||||
|
} from '../modules/schema/providers/schema-version-store';
|
||||||
import { parseGraphQLSource, sortDocumentNode } from './schema';
|
import { parseGraphQLSource, sortDocumentNode } from './schema';
|
||||||
|
|
||||||
export const NameModel = z
|
export const NameModel = z
|
||||||
|
|
|
||||||
4
packages/services/broker-worker/.env.template
Normal file
4
packages/services/broker-worker/.env.template
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
CF_BROKER_SIGNATURE=dev-secret
|
||||||
|
PORT=4010
|
||||||
|
SERVER_HOST=::
|
||||||
|
SERVER_HOST_IPV6_ONLY=0
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cloudflare/workers-types": "4.20250913.0",
|
"@cloudflare/workers-types": "4.20250913.0",
|
||||||
|
"@hive/service-common": "workspace:*",
|
||||||
"@types/service-worker-mock": "2.0.4",
|
"@types/service-worker-mock": "2.0.4",
|
||||||
"@whatwg-node/server": "0.10.17",
|
"@whatwg-node/server": "0.10.17",
|
||||||
"esbuild": "0.25.9",
|
"esbuild": "0.25.9",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { createServer } from 'http';
|
import { createServer } from 'http';
|
||||||
import { Router } from 'itty-router';
|
import { Router } from 'itty-router';
|
||||||
|
import { resolveServerListenOptions } from '@hive/service-common/listen-options';
|
||||||
import { createServerAdapter } from '@whatwg-node/server';
|
import { createServerAdapter } from '@whatwg-node/server';
|
||||||
import { createSignatureValidator } from './auth';
|
import { createSignatureValidator } from './auth';
|
||||||
import { env } from './dev-polyfill';
|
import { env } from './dev-polyfill';
|
||||||
|
|
@ -7,6 +8,12 @@ import { handleRequest } from './handler';
|
||||||
|
|
||||||
// eslint-disable-next-line no-process-env
|
// eslint-disable-next-line no-process-env
|
||||||
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 4010;
|
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 4010;
|
||||||
|
const listenOptions = resolveServerListenOptions({
|
||||||
|
// eslint-disable-next-line no-process-env
|
||||||
|
serverHost: process.env.SERVER_HOST,
|
||||||
|
// eslint-disable-next-line no-process-env
|
||||||
|
serverHostIpv6Only: process.env.SERVER_HOST_IPV6_ONLY === '1' ? '1' : '0',
|
||||||
|
});
|
||||||
const isSignatureValid = createSignatureValidator(env.SIGNATURE);
|
const isSignatureValid = createSignatureValidator(env.SIGNATURE);
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
|
|
@ -32,7 +39,14 @@ function main() {
|
||||||
const server = createServer(app);
|
const server = createServer(app);
|
||||||
|
|
||||||
return new Promise<void>(resolve => {
|
return new Promise<void>(resolve => {
|
||||||
server.listen(PORT, '::', resolve);
|
server.listen(
|
||||||
|
{
|
||||||
|
port: PORT,
|
||||||
|
host: listenOptions.host,
|
||||||
|
ipv6Only: listenOptions.ipv6Only,
|
||||||
|
},
|
||||||
|
resolve,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,3 +2,6 @@ S3_ENDPOINT=http://localhost:9000
|
||||||
S3_ACCESS_KEY_ID="minioadmin"
|
S3_ACCESS_KEY_ID="minioadmin"
|
||||||
S3_SECRET_ACCESS_KEY="minioadmin"
|
S3_SECRET_ACCESS_KEY="minioadmin"
|
||||||
S3_BUCKET_NAME="artifacts"
|
S3_BUCKET_NAME="artifacts"
|
||||||
|
PORT=4010
|
||||||
|
SERVER_HOST=::
|
||||||
|
SERVER_HOST_IPV6_ONLY=0
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cloudflare/workers-types": "4.20250913.0",
|
"@cloudflare/workers-types": "4.20250913.0",
|
||||||
|
"@hive/service-common": "workspace:*",
|
||||||
"@types/service-worker-mock": "2.0.4",
|
"@types/service-worker-mock": "2.0.4",
|
||||||
"@whatwg-node/server": "0.10.17",
|
"@whatwg-node/server": "0.10.17",
|
||||||
"bcryptjs": "2.4.3",
|
"bcryptjs": "2.4.3",
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,8 @@ type Event =
|
||||||
| 'GET cdn-legacy-keys'
|
| 'GET cdn-legacy-keys'
|
||||||
| 'GET cdn-access-token'
|
| 'GET cdn-access-token'
|
||||||
| 'GET persistedOperation'
|
| 'GET persistedOperation'
|
||||||
| 'HEAD appDeploymentIsEnabled';
|
| 'HEAD appDeploymentIsEnabled'
|
||||||
|
| 'GET appDeploymentManifest';
|
||||||
// Either 3 digit status code or error code e.g. timeout, http error etc.
|
// Either 3 digit status code or error code e.g. timeout, http error etc.
|
||||||
statusCodeOrErrCode: number | string;
|
statusCodeOrErrCode: number | string;
|
||||||
/** duration in milliseconds */
|
/** duration in milliseconds */
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,12 @@ const VersionedParamsModel = zod.object({
|
||||||
.transform(value => value ?? null),
|
.transform(value => value ?? null),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const AppDeploymentsManifestParamsModel = zod.object({
|
||||||
|
targetId: zod.string(),
|
||||||
|
appName: zod.string(),
|
||||||
|
appVersion: zod.string(),
|
||||||
|
});
|
||||||
|
|
||||||
const PersistedOperationParamsModel = zod.object({
|
const PersistedOperationParamsModel = zod.object({
|
||||||
targetId: zod.string(),
|
targetId: zod.string(),
|
||||||
appName: zod.string(),
|
appName: zod.string(),
|
||||||
|
|
@ -461,9 +467,6 @@ export const createArtifactRequestHandler = (deps: ArtifactRequestHandler) => {
|
||||||
const response = createResponse(
|
const response = createResponse(
|
||||||
analytics,
|
analytics,
|
||||||
result.body,
|
result.body,
|
||||||
// We're using here a public location, because we expose the Location to the end user and
|
|
||||||
// the public S3 endpoint may differ from the internal S3 endpoint. E.g. within a docker network.
|
|
||||||
// If they are the same, private and public locations will be the same.
|
|
||||||
{ status: 200 },
|
{ status: 200 },
|
||||||
params.targetId,
|
params.targetId,
|
||||||
request,
|
request,
|
||||||
|
|
@ -474,6 +477,70 @@ export const createArtifactRequestHandler = (deps: ArtifactRequestHandler) => {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
router.get(
|
||||||
|
'/artifacts/v1/:targetId/apps/:appName/:appVersion',
|
||||||
|
async function AppDeploymentManifestHandler(request) {
|
||||||
|
const parseResult = AppDeploymentsManifestParamsModel.safeParse(request.params);
|
||||||
|
|
||||||
|
if (parseResult.success === false) {
|
||||||
|
analytics.track(
|
||||||
|
{ type: 'error', value: ['invalid-params'] },
|
||||||
|
request.params?.targetId ?? 'unknown',
|
||||||
|
);
|
||||||
|
|
||||||
|
return createResponse(
|
||||||
|
analytics,
|
||||||
|
'Not found.',
|
||||||
|
{
|
||||||
|
status: 404,
|
||||||
|
},
|
||||||
|
request.params?.targetId ?? 'unknown',
|
||||||
|
request,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = parseResult.data;
|
||||||
|
|
||||||
|
const maybeResponse = await authenticate(request, params.targetId);
|
||||||
|
|
||||||
|
if (maybeResponse !== null) {
|
||||||
|
return maybeResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
const manifest = await deps.artifactStorageReader.readAppDeploymentManifest(
|
||||||
|
params.targetId,
|
||||||
|
params.appName,
|
||||||
|
params.appVersion,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (manifest === null) {
|
||||||
|
return createResponse(
|
||||||
|
analytics,
|
||||||
|
'Not found.',
|
||||||
|
{
|
||||||
|
status: 404,
|
||||||
|
},
|
||||||
|
request.params?.targetId ?? 'unknown',
|
||||||
|
request,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = createResponse(
|
||||||
|
analytics,
|
||||||
|
JSON.stringify(manifest),
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params.targetId,
|
||||||
|
request,
|
||||||
|
);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return async (request: Request, captureException?: (error: unknown) => void) => {
|
return async (request: Request, captureException?: (error: unknown) => void) => {
|
||||||
return router.handle(request, captureException);
|
return router.handle(request, captureException);
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,14 @@ import type { Analytics } from './analytics';
|
||||||
import { AwsClient } from './aws';
|
import { AwsClient } from './aws';
|
||||||
import type { Breadcrumb } from './breadcrumbs';
|
import type { Breadcrumb } from './breadcrumbs';
|
||||||
|
|
||||||
|
export const AppDeploymentManifestModel = zod.object({
|
||||||
|
id: zod.string().uuid(),
|
||||||
|
appName: zod.string(),
|
||||||
|
appVersion: zod.string(),
|
||||||
|
isActive: zod.boolean(),
|
||||||
|
documentHashes: zod.array(zod.string()),
|
||||||
|
});
|
||||||
|
|
||||||
export function buildArtifactStorageKey(
|
export function buildArtifactStorageKey(
|
||||||
targetId: string,
|
targetId: string,
|
||||||
artifactType: string,
|
artifactType: string,
|
||||||
|
|
@ -50,6 +58,7 @@ const AppDeploymentIsEnabledKeyModel = zod.tuple([
|
||||||
/**
|
/**
|
||||||
* S3 key for determining whether app deployment is enabled or not.
|
* S3 key for determining whether app deployment is enabled or not.
|
||||||
* Note: we validate to avoid invalid keys / collisions that could be caused by type errors.
|
* Note: we validate to avoid invalid keys / collisions that could be caused by type errors.
|
||||||
|
*
|
||||||
**/
|
**/
|
||||||
export function buildAppDeploymentIsEnabledKey(
|
export function buildAppDeploymentIsEnabledKey(
|
||||||
...args: [targetId: string, appName: string, appVersion: string]
|
...args: [targetId: string, appName: string, appVersion: string]
|
||||||
|
|
@ -57,6 +66,12 @@ export function buildAppDeploymentIsEnabledKey(
|
||||||
return ['apps-enabled', ...AppDeploymentIsEnabledKeyModel.parse(args)].join('/');
|
return ['apps-enabled', ...AppDeploymentIsEnabledKeyModel.parse(args)].join('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildAppDeploymentManifestKey(
|
||||||
|
...args: [targetId: string, appName: string, appVersion: string]
|
||||||
|
) {
|
||||||
|
return ['apps-manifest', ...AppDeploymentIsEnabledKeyModel.parse(args)].join('/');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read an artifact/app deployment operation from S3.
|
* Read an artifact/app deployment operation from S3.
|
||||||
*/
|
*/
|
||||||
|
|
@ -353,6 +368,49 @@ export class ArtifactStorageReader {
|
||||||
return response.status === 200;
|
return response.status === 200;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async readAppDeploymentManifest(targetId: string, appName: string, appVersion: string) {
|
||||||
|
const key = buildAppDeploymentManifestKey(targetId, appName, appVersion);
|
||||||
|
|
||||||
|
const response = await this.request({
|
||||||
|
key,
|
||||||
|
method: 'GET',
|
||||||
|
onAttempt: args => {
|
||||||
|
if (args.result.type === 'error') {
|
||||||
|
this.breadcrumb(
|
||||||
|
`Fetch attempt failed (source=${args.isMirror ? 'mirror' : 'primary'}, attempt=${args.attempt} duration=${args.duration}, result=${args.result.type}, key=${key}, message=${args.result.error.message})`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.breadcrumb(
|
||||||
|
`Fetch attempt succeeded (source=${args.isMirror ? 'mirror' : 'primary'}, attempt=${args.attempt} duration=${args.duration}, result=${args.result.type}, key=${key})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.analytics?.track(
|
||||||
|
{
|
||||||
|
type: args.isMirror ? 's3' : 'r2',
|
||||||
|
statusCodeOrErrCode:
|
||||||
|
args.result.type === 'error'
|
||||||
|
? String(args.result.error.name ?? 'unknown')
|
||||||
|
: args.result.response.status,
|
||||||
|
action: 'GET appDeploymentManifest',
|
||||||
|
duration: args.duration,
|
||||||
|
},
|
||||||
|
targetId,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 404) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new Error('Failed to retrieve app deployment manifest');
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await response.json();
|
||||||
|
return AppDeploymentManifestModel.parse(body);
|
||||||
|
}
|
||||||
|
|
||||||
async loadAppDeploymentPersistedOperation(
|
async loadAppDeploymentPersistedOperation(
|
||||||
targetId: string,
|
targetId: string,
|
||||||
appName: string,
|
appName: string,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { createServer } from 'http';
|
import { createServer } from 'http';
|
||||||
import * as itty from 'itty-router';
|
import * as itty from 'itty-router';
|
||||||
|
import { resolveServerListenOptions } from '@hive/service-common/listen-options';
|
||||||
import { createServerAdapter } from '@whatwg-node/server';
|
import { createServerAdapter } from '@whatwg-node/server';
|
||||||
import { createArtifactRequestHandler } from './artifact-handler';
|
import { createArtifactRequestHandler } from './artifact-handler';
|
||||||
import { ArtifactStorageReader } from './artifact-storage-reader';
|
import { ArtifactStorageReader } from './artifact-storage-reader';
|
||||||
|
|
@ -22,6 +23,12 @@ const s3 = {
|
||||||
|
|
||||||
// eslint-disable-next-line no-process-env
|
// eslint-disable-next-line no-process-env
|
||||||
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 4010;
|
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 4010;
|
||||||
|
const listenOptions = resolveServerListenOptions({
|
||||||
|
// eslint-disable-next-line no-process-env
|
||||||
|
serverHost: process.env.SERVER_HOST,
|
||||||
|
// eslint-disable-next-line no-process-env
|
||||||
|
serverHostIpv6Only: process.env.SERVER_HOST_IPV6_ONLY === '1' ? '1' : '0',
|
||||||
|
});
|
||||||
|
|
||||||
const artifactStorageReader = new ArtifactStorageReader(s3, null, null, null);
|
const artifactStorageReader = new ArtifactStorageReader(s3, null, null, null);
|
||||||
|
|
||||||
|
|
@ -93,7 +100,14 @@ function main() {
|
||||||
const server = createServer(app);
|
const server = createServer(app);
|
||||||
|
|
||||||
return new Promise<void>(resolve => {
|
return new Promise<void>(resolve => {
|
||||||
server.listen(PORT, '::', resolve);
|
server.listen(
|
||||||
|
{
|
||||||
|
port: PORT,
|
||||||
|
host: listenOptions.host,
|
||||||
|
ipv6Only: listenOptions.ipv6Only,
|
||||||
|
},
|
||||||
|
resolve,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
PORT=4013
|
PORT=4013
|
||||||
|
SERVER_HOST=::
|
||||||
|
SERVER_HOST_IPV6_ONLY=0
|
||||||
OPENTELEMETRY_COLLECTOR_ENDPOINT="<sync>"
|
OPENTELEMETRY_COLLECTOR_ENDPOINT="<sync>"
|
||||||
CLICKHOUSE_PROTOCOL="http"
|
CLICKHOUSE_PROTOCOL="http"
|
||||||
CLICKHOUSE_HOST="localhost"
|
CLICKHOUSE_HOST="localhost"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import zod from 'zod';
|
import zod from 'zod';
|
||||||
import { OpenTelemetryConfigurationModel } from '@hive/service-common';
|
import { OpenTelemetryConfigurationModel, resolveServerListenOptions } from '@hive/service-common';
|
||||||
|
|
||||||
const isNumberString = (input: unknown) => zod.string().regex(/^\d+$/).safeParse(input).success;
|
const isNumberString = (input: unknown) => zod.string().regex(/^\d+$/).safeParse(input).success;
|
||||||
|
|
||||||
|
|
@ -22,6 +22,8 @@ export const emptyString = <T extends zod.ZodType>(input: T) => {
|
||||||
|
|
||||||
const EnvironmentModel = zod.object({
|
const EnvironmentModel = zod.object({
|
||||||
PORT: emptyString(NumberFromString.optional()),
|
PORT: emptyString(NumberFromString.optional()),
|
||||||
|
SERVER_HOST: emptyString(zod.string().optional()),
|
||||||
|
SERVER_HOST_IPV6_ONLY: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()),
|
||||||
ENVIRONMENT: emptyString(zod.string().optional()),
|
ENVIRONMENT: emptyString(zod.string().optional()),
|
||||||
RELEASE: emptyString(zod.string().optional()),
|
RELEASE: emptyString(zod.string().optional()),
|
||||||
});
|
});
|
||||||
|
|
@ -141,6 +143,10 @@ export const env = {
|
||||||
release: base.RELEASE ?? 'local',
|
release: base.RELEASE ?? 'local',
|
||||||
http: {
|
http: {
|
||||||
port: base.PORT ?? 4012,
|
port: base.PORT ?? 4012,
|
||||||
|
...resolveServerListenOptions({
|
||||||
|
serverHost: base.SERVER_HOST,
|
||||||
|
serverHostIpv6Only: base.SERVER_HOST_IPV6_ONLY,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
tracing: {
|
tracing: {
|
||||||
enabled: !!tracing.OPENTELEMETRY_COLLECTOR_ENDPOINT,
|
enabled: !!tracing.OPENTELEMETRY_COLLECTOR_ENDPOINT,
|
||||||
|
|
|
||||||
|
|
@ -137,11 +137,16 @@ async function main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (env.prometheus) {
|
if (env.prometheus) {
|
||||||
await startMetrics(env.prometheus.labels.instance, env.prometheus.port);
|
await startMetrics(env.prometheus.labels.instance, {
|
||||||
|
port: env.prometheus.port,
|
||||||
|
host: env.http.host,
|
||||||
|
ipv6Only: env.http.ipv6Only,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
await server.listen({
|
await server.listen({
|
||||||
port: env.http.port,
|
port: env.http.port,
|
||||||
host: '::',
|
host: env.http.host,
|
||||||
|
ipv6Only: env.http.ipv6Only,
|
||||||
});
|
});
|
||||||
await Promise.all([usageEstimator.start(), rateLimiter.start(), stripeBilling.start()]);
|
await Promise.all([usageEstimator.start(), rateLimiter.start(), stripeBilling.start()]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -1 +1,4 @@
|
||||||
SECRET=secretsecret
|
SECRET=secretsecret
|
||||||
|
PORT=3069
|
||||||
|
SERVER_HOST=::
|
||||||
|
SERVER_HOST_IPV6_ONLY=0
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
"@apollo/composition": "2.13.2",
|
"@apollo/composition": "2.13.2",
|
||||||
"@apollo/federation-internals": "2.13.2",
|
"@apollo/federation-internals": "2.13.2",
|
||||||
"@graphql-hive/external-composition": "workspace:*",
|
"@graphql-hive/external-composition": "workspace:*",
|
||||||
|
"@hive/service-common": "workspace:*",
|
||||||
"@whatwg-node/server": "0.10.17",
|
"@whatwg-node/server": "0.10.17",
|
||||||
"dotenv": "16.4.7",
|
"dotenv": "16.4.7",
|
||||||
"graphql": "16.9.0",
|
"graphql": "16.9.0",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { resolveEnv } from './environment';
|
||||||
|
|
||||||
|
describe('resolveEnv', () => {
|
||||||
|
// eslint-disable-next-line no-restricted-syntax -- explicit IPv4 literal required for validation coverage
|
||||||
|
const ipv4Host = '0.0.0.0';
|
||||||
|
|
||||||
|
test('uses host and ipv6 defaults when not provided', () => {
|
||||||
|
const env = resolveEnv({
|
||||||
|
SECRET: 'secretsecret',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(env.http).toEqual({
|
||||||
|
port: 3069,
|
||||||
|
host: '::',
|
||||||
|
ipv6Only: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('honors explicit host and ipv6-only values', () => {
|
||||||
|
const env = resolveEnv({
|
||||||
|
SECRET: 'secretsecret',
|
||||||
|
PORT: '4000',
|
||||||
|
SERVER_HOST: '::1',
|
||||||
|
SERVER_HOST_IPV6_ONLY: '1',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(env.http).toEqual({
|
||||||
|
port: 4000,
|
||||||
|
host: '::1',
|
||||||
|
ipv6Only: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects ipv6-only combined with an IPv4 literal host', () => {
|
||||||
|
expect(() =>
|
||||||
|
resolveEnv({
|
||||||
|
SECRET: 'secretsecret',
|
||||||
|
SERVER_HOST: ipv4Host,
|
||||||
|
SERVER_HOST_IPV6_ONLY: '1',
|
||||||
|
}),
|
||||||
|
).toThrow(/SERVER_HOST_IPV6_ONLY=1 is incompatible with IPv4 host "0\.0\.0\.0"/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import zod from 'zod';
|
import zod from 'zod';
|
||||||
|
import { resolveServerListenOptions } from '@hive/service-common';
|
||||||
|
|
||||||
function extractConfig<Input, Output>(config: zod.SafeParseReturnType<Input, Output>): Output {
|
function extractConfig<Input, Output>(config: zod.SafeParseReturnType<Input, Output>): Output {
|
||||||
if (!config.success) {
|
if (!config.success) {
|
||||||
|
|
@ -15,6 +16,8 @@ const BaseSchema = zod.object({
|
||||||
.number()
|
.number()
|
||||||
.transform(port => port || 3069)
|
.transform(port => port || 3069)
|
||||||
.default(3069),
|
.default(3069),
|
||||||
|
SERVER_HOST: zod.string().default('::'),
|
||||||
|
SERVER_HOST_IPV6_ONLY: zod.union([zod.literal('1'), zod.literal('0')]).default('0'),
|
||||||
SECRET: zod.string(),
|
SECRET: zod.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -44,6 +47,10 @@ export function resolveEnv(env: Record<string, string | undefined>) {
|
||||||
release: base.RELEASE ?? 'local',
|
release: base.RELEASE ?? 'local',
|
||||||
http: {
|
http: {
|
||||||
port: base.PORT,
|
port: base.PORT,
|
||||||
|
...resolveServerListenOptions({
|
||||||
|
serverHost: base.SERVER_HOST,
|
||||||
|
serverHostIpv6Only: base.SERVER_HOST_IPV6_ONLY,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
secret: base.SECRET,
|
secret: base.SECRET,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,20 @@ import { createRequestListener } from './server';
|
||||||
const env = resolveEnv(process.env);
|
const env = resolveEnv(process.env);
|
||||||
const server = createServer(createRequestListener(env));
|
const server = createServer(createRequestListener(env));
|
||||||
|
|
||||||
server.listen(env.http.port, '::', () => {
|
function formatListenAddress(host: string, port: number) {
|
||||||
console.log(`Listening on http://localhost:${env.http.port}`);
|
return host.includes(':') ? `[${host}]:${port}` : `${host}:${port}`;
|
||||||
});
|
}
|
||||||
|
|
||||||
|
server.listen(
|
||||||
|
{
|
||||||
|
port: env.http.port,
|
||||||
|
host: env.http.host,
|
||||||
|
ipv6Only: env.http.ipv6Only,
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
console.log(`Listening on ${formatListenAddress(env.http.host, env.http.port)}`);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
process.on('SIGINT', () => {
|
process.on('SIGINT', () => {
|
||||||
server.close(err => {
|
server.close(err => {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
ENVIRONMENT=development
|
ENVIRONMENT=development
|
||||||
LOG_LEVEL=debug
|
LOG_LEVEL=debug
|
||||||
|
SERVER_HOST=::
|
||||||
|
SERVER_HOST_IPV6_ONLY=0
|
||||||
OPENTELEMETRY_COLLECTOR_ENDPOINT="<sync>"
|
OPENTELEMETRY_COLLECTOR_ENDPOINT="<sync>"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import zod from 'zod';
|
import zod from 'zod';
|
||||||
import { OpenTelemetryConfigurationModel } from '@hive/service-common';
|
import { OpenTelemetryConfigurationModel, resolveServerListenOptions } from '@hive/service-common';
|
||||||
|
|
||||||
const isNumberString = (input: unknown) => zod.string().regex(/^\d+$/).safeParse(input).success;
|
const isNumberString = (input: unknown) => zod.string().regex(/^\d+$/).safeParse(input).success;
|
||||||
|
|
||||||
|
|
@ -20,6 +20,8 @@ const emptyString = <T extends zod.ZodType>(input: T) => {
|
||||||
|
|
||||||
const EnvironmentModel = zod.object({
|
const EnvironmentModel = zod.object({
|
||||||
PORT: emptyString(NumberFromString.optional()),
|
PORT: emptyString(NumberFromString.optional()),
|
||||||
|
SERVER_HOST: emptyString(zod.string().optional()),
|
||||||
|
SERVER_HOST_IPV6_ONLY: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()),
|
||||||
ENVIRONMENT: emptyString(zod.string().optional()),
|
ENVIRONMENT: emptyString(zod.string().optional()),
|
||||||
RELEASE: emptyString(zod.string().optional()),
|
RELEASE: emptyString(zod.string().optional()),
|
||||||
});
|
});
|
||||||
|
|
@ -107,6 +109,10 @@ export const env = {
|
||||||
},
|
},
|
||||||
http: {
|
http: {
|
||||||
port: base.PORT ?? 6600,
|
port: base.PORT ?? 6600,
|
||||||
|
...resolveServerListenOptions({
|
||||||
|
serverHost: base.SERVER_HOST,
|
||||||
|
serverHostIpv6Only: base.SERVER_HOST_IPV6_ONLY,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
sentry: sentry.SENTRY === '1' ? { dsn: sentry.SENTRY_DSN } : null,
|
sentry: sentry.SENTRY === '1' ? { dsn: sentry.SENTRY_DSN } : null,
|
||||||
log: {
|
log: {
|
||||||
|
|
|
||||||
|
|
@ -83,11 +83,16 @@ async function main() {
|
||||||
|
|
||||||
await server.listen({
|
await server.listen({
|
||||||
port: env.http.port,
|
port: env.http.port,
|
||||||
host: '::',
|
host: env.http.host,
|
||||||
|
ipv6Only: env.http.ipv6Only,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (env.prometheus) {
|
if (env.prometheus) {
|
||||||
await startMetrics(env.prometheus.labels.instance, env.prometheus.port);
|
await startMetrics(env.prometheus.labels.instance, {
|
||||||
|
port: env.prometheus.port,
|
||||||
|
host: env.http.host,
|
||||||
|
ipv6Only: env.http.ipv6Only,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
server.log.fatal(error);
|
server.log.fatal(error);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
REDIS_HOST="localhost"
|
REDIS_HOST="localhost"
|
||||||
REDIS_PORT="6379"
|
REDIS_PORT="6379"
|
||||||
REDIS_PASSWORD=""
|
REDIS_PASSWORD=""
|
||||||
|
SERVER_HOST=::
|
||||||
|
SERVER_HOST_IPV6_ONLY=0
|
||||||
ENCRYPTION_SECRET="97e4094d2463e71a981913cca4e56788"
|
ENCRYPTION_SECRET="97e4094d2463e71a981913cca4e56788"
|
||||||
SCHEMA_CACHE_TTL_MS=5000
|
SCHEMA_CACHE_TTL_MS=5000
|
||||||
SCHEMA_CACHE_SUCCESS_TTL_MS=5000
|
SCHEMA_CACHE_SUCCESS_TTL_MS=5000
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import zod from 'zod';
|
import zod from 'zod';
|
||||||
import { OpenTelemetryConfigurationModel } from '@hive/service-common';
|
import { OpenTelemetryConfigurationModel, resolveServerListenOptions } from '@hive/service-common';
|
||||||
|
|
||||||
const isNumberString = (input: unknown) => zod.string().regex(/^\d+$/).safeParse(input).success;
|
const isNumberString = (input: unknown) => zod.string().regex(/^\d+$/).safeParse(input).success;
|
||||||
|
|
||||||
|
|
@ -21,6 +21,8 @@ const emptyString = <T extends zod.ZodType>(input: T) => {
|
||||||
|
|
||||||
const EnvironmentModel = zod.object({
|
const EnvironmentModel = zod.object({
|
||||||
PORT: emptyString(NumberFromString().optional()),
|
PORT: emptyString(NumberFromString().optional()),
|
||||||
|
SERVER_HOST: emptyString(zod.string().optional()),
|
||||||
|
SERVER_HOST_IPV6_ONLY: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()),
|
||||||
BODY_LIMIT: NumberFromString().optional().default(/* 15mb in bytes */ 15e6),
|
BODY_LIMIT: NumberFromString().optional().default(/* 15mb in bytes */ 15e6),
|
||||||
ENVIRONMENT: emptyString(zod.string().optional()),
|
ENVIRONMENT: emptyString(zod.string().optional()),
|
||||||
RELEASE: emptyString(zod.string().optional()),
|
RELEASE: emptyString(zod.string().optional()),
|
||||||
|
|
@ -144,6 +146,10 @@ export const env = {
|
||||||
encryptionSecret: base.ENCRYPTION_SECRET,
|
encryptionSecret: base.ENCRYPTION_SECRET,
|
||||||
http: {
|
http: {
|
||||||
port: base.PORT ?? 6500,
|
port: base.PORT ?? 6500,
|
||||||
|
...resolveServerListenOptions({
|
||||||
|
serverHost: base.SERVER_HOST,
|
||||||
|
serverHostIpv6Only: base.SERVER_HOST_IPV6_ONLY,
|
||||||
|
}),
|
||||||
bodyLimit: base.BODY_LIMIT,
|
bodyLimit: base.BODY_LIMIT,
|
||||||
},
|
},
|
||||||
tracing: {
|
tracing: {
|
||||||
|
|
|
||||||
|
|
@ -146,11 +146,16 @@ async function main() {
|
||||||
|
|
||||||
await server.listen({
|
await server.listen({
|
||||||
port: env.http.port,
|
port: env.http.port,
|
||||||
host: '::',
|
host: env.http.host,
|
||||||
|
ipv6Only: env.http.ipv6Only,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (env.prometheus) {
|
if (env.prometheus) {
|
||||||
await startMetrics(env.prometheus.labels.instance, env.prometheus.port);
|
await startMetrics(env.prometheus.labels.instance, {
|
||||||
|
port: env.prometheus.port,
|
||||||
|
host: env.http.host,
|
||||||
|
ipv6Only: env.http.ipv6Only,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
server.log.fatal(error);
|
server.log.fatal(error);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
ENVIRONMENT=development
|
ENVIRONMENT=development
|
||||||
LOG_LEVEL=debug
|
LOG_LEVEL=debug
|
||||||
|
SERVER_HOST=::
|
||||||
|
SERVER_HOST_IPV6_ONLY=0
|
||||||
POSTGRES_USER=postgres
|
POSTGRES_USER=postgres
|
||||||
POSTGRES_PASSWORD=postgres
|
POSTGRES_PASSWORD=postgres
|
||||||
POSTGRES_HOST=localhost
|
POSTGRES_HOST=localhost
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,8 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@envelop/core": "5.5.1",
|
"@envelop/core": "5.5.1",
|
||||||
"@envelop/graphql-jit": "8.0.3",
|
"@envelop/graphql-jit": "11.1.1",
|
||||||
"@envelop/graphql-modules": "9.1.0",
|
"@envelop/graphql-modules": "9.1.1",
|
||||||
"@envelop/opentelemetry": "6.3.1",
|
|
||||||
"@envelop/types": "5.0.0",
|
"@envelop/types": "5.0.0",
|
||||||
"@escape.tech/graphql-armor-max-aliases": "2.6.2",
|
"@escape.tech/graphql-armor-max-aliases": "2.6.2",
|
||||||
"@escape.tech/graphql-armor-max-depth": "2.4.2",
|
"@escape.tech/graphql-armor-max-depth": "2.4.2",
|
||||||
|
|
@ -24,7 +23,7 @@
|
||||||
"@fastify/cookie": "11.0.2",
|
"@fastify/cookie": "11.0.2",
|
||||||
"@fastify/cors": "11.2.0",
|
"@fastify/cors": "11.2.0",
|
||||||
"@fastify/formbody": "8.0.2",
|
"@fastify/formbody": "8.0.2",
|
||||||
"@graphql-hive/plugin-opentelemetry": "1.3.0",
|
"@graphql-hive/plugin-opentelemetry": "1.4.26",
|
||||||
"@graphql-hive/yoga": "workspace:*",
|
"@graphql-hive/yoga": "workspace:*",
|
||||||
"@graphql-tools/merge": "9.1.1",
|
"@graphql-tools/merge": "9.1.1",
|
||||||
"@graphql-yoga/plugin-response-cache": "3.15.4",
|
"@graphql-yoga/plugin-response-cache": "3.15.4",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import zod from 'zod';
|
import zod from 'zod';
|
||||||
import { OpenTelemetryConfigurationModel } from '@hive/service-common';
|
import { OpenTelemetryConfigurationModel, resolveServerListenOptions } from '@hive/service-common';
|
||||||
|
|
||||||
const isNumberString = (input: unknown) => zod.string().regex(/^\d+$/).safeParse(input).success;
|
const isNumberString = (input: unknown) => zod.string().regex(/^\d+$/).safeParse(input).success;
|
||||||
|
|
||||||
|
|
@ -20,6 +20,8 @@ const emptyString = <T extends zod.ZodType>(input: T) => {
|
||||||
|
|
||||||
const EnvironmentModel = zod.object({
|
const EnvironmentModel = zod.object({
|
||||||
PORT: emptyString(NumberFromString.optional()),
|
PORT: emptyString(NumberFromString.optional()),
|
||||||
|
SERVER_HOST: emptyString(zod.string().optional()),
|
||||||
|
SERVER_HOST_IPV6_ONLY: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()),
|
||||||
ENVIRONMENT: emptyString(zod.string().optional()),
|
ENVIRONMENT: emptyString(zod.string().optional()),
|
||||||
RELEASE: emptyString(zod.string().optional()),
|
RELEASE: emptyString(zod.string().optional()),
|
||||||
ENCRYPTION_SECRET: emptyString(zod.string()),
|
ENCRYPTION_SECRET: emptyString(zod.string()),
|
||||||
|
|
@ -417,6 +419,10 @@ export const env = {
|
||||||
},
|
},
|
||||||
http: {
|
http: {
|
||||||
port: base.PORT ?? 3001,
|
port: base.PORT ?? 3001,
|
||||||
|
...resolveServerListenOptions({
|
||||||
|
serverHost: base.SERVER_HOST,
|
||||||
|
serverHostIpv6Only: base.SERVER_HOST_IPV6_ONLY,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
postgres: {
|
postgres: {
|
||||||
host: postgres.POSTGRES_HOST,
|
host: postgres.POSTGRES_HOST,
|
||||||
|
|
|
||||||
|
|
@ -610,12 +610,17 @@ export async function main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (env.prometheus) {
|
if (env.prometheus) {
|
||||||
await startMetrics(env.prometheus.labels.instance, env.prometheus.port);
|
await startMetrics(env.prometheus.labels.instance, {
|
||||||
|
port: env.prometheus.port,
|
||||||
|
host: env.http.host,
|
||||||
|
ipv6Only: env.http.ipv6Only,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await server.listen({
|
await server.listen({
|
||||||
port: env.http.port,
|
port: env.http.port,
|
||||||
host: '::',
|
host: env.http.host,
|
||||||
|
ipv6Only: env.http.ipv6Only,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
server.log.fatal(error);
|
server.log.fatal(error);
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"private": true,
|
"private": true,
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts"
|
".": "./src/index.ts",
|
||||||
|
"./listen-options": "./src/listen-options.ts"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@sentry/node": "^7.0.0",
|
"@sentry/node": "^7.0.0",
|
||||||
|
|
@ -13,18 +14,18 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@fastify/cors": "11.2.0",
|
"@fastify/cors": "11.2.0",
|
||||||
"@graphql-hive/logger": "1.0.9",
|
"@graphql-hive/logger": "1.1.0",
|
||||||
"@graphql-hive/plugin-opentelemetry": "1.3.0",
|
"@graphql-hive/plugin-opentelemetry": "1.4.26",
|
||||||
"@opentelemetry/api": "1.9.0",
|
"@opentelemetry/api": "1.9.1",
|
||||||
"@opentelemetry/auto-instrumentations-node": "0.67.2",
|
"@opentelemetry/auto-instrumentations-node": "0.75.0",
|
||||||
"@opentelemetry/context-async-hooks": "2.2.0",
|
"@opentelemetry/context-async-hooks": "2.7.1",
|
||||||
"@opentelemetry/exporter-trace-otlp-http": "0.208.0",
|
"@opentelemetry/exporter-trace-otlp-http": "0.217.0",
|
||||||
"@opentelemetry/instrumentation": "0.208.0",
|
"@opentelemetry/instrumentation": "0.217.0",
|
||||||
"@opentelemetry/resources": "2.2.0",
|
"@opentelemetry/resources": "2.7.1",
|
||||||
"@opentelemetry/sdk-node": "0.208.0",
|
"@opentelemetry/sdk-node": "0.217.0",
|
||||||
"@opentelemetry/sdk-trace-base": "2.2.0",
|
"@opentelemetry/sdk-trace-base": "2.7.1",
|
||||||
"@opentelemetry/sdk-trace-node": "2.2.0",
|
"@opentelemetry/sdk-trace-node": "2.7.1",
|
||||||
"@opentelemetry/semantic-conventions": "1.38.0",
|
"@opentelemetry/semantic-conventions": "1.40.0",
|
||||||
"@sentry/integrations": "7.114.0",
|
"@sentry/integrations": "7.114.0",
|
||||||
"@sentry/node": "7.120.2",
|
"@sentry/node": "7.120.2",
|
||||||
"@sentry/types": "7.120.2",
|
"@sentry/types": "7.120.2",
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ export * from './metrics';
|
||||||
export * from './heartbeats';
|
export * from './heartbeats';
|
||||||
export * from './trpc';
|
export * from './trpc';
|
||||||
export * from './tracing';
|
export * from './tracing';
|
||||||
|
export { resolveServerListenOptions } from './listen-options';
|
||||||
export { registerShutdown } from './graceful-shutdown';
|
export { registerShutdown } from './graceful-shutdown';
|
||||||
export { cleanRequestId, maskToken } from './helpers';
|
export { cleanRequestId, maskToken } from './helpers';
|
||||||
export { sentryInit } from './sentry';
|
export { sentryInit } from './sentry';
|
||||||
|
|
|
||||||
46
packages/services/service-common/src/listen-options.spec.ts
Normal file
46
packages/services/service-common/src/listen-options.spec.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { resolveServerListenOptions } from './listen-options';
|
||||||
|
|
||||||
|
describe('resolveServerListenOptions', () => {
|
||||||
|
// eslint-disable-next-line no-restricted-syntax -- explicit IPv4 literal required for validation coverage
|
||||||
|
const ipv4Host = '0.0.0.0';
|
||||||
|
|
||||||
|
test('defaults to dual-stack host', () => {
|
||||||
|
expect(resolveServerListenOptions({})).toEqual({
|
||||||
|
host: '::',
|
||||||
|
ipv6Only: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('supports ipv4-only host', () => {
|
||||||
|
expect(
|
||||||
|
resolveServerListenOptions({
|
||||||
|
serverHost: ipv4Host,
|
||||||
|
serverHostIpv6Only: '0',
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
host: ipv4Host,
|
||||||
|
ipv6Only: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('supports ipv6-only wildcard host', () => {
|
||||||
|
expect(
|
||||||
|
resolveServerListenOptions({
|
||||||
|
serverHost: '::',
|
||||||
|
serverHostIpv6Only: '1',
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
host: '::',
|
||||||
|
ipv6Only: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects ipv6-only combined with an IPv4 literal host', () => {
|
||||||
|
expect(() =>
|
||||||
|
resolveServerListenOptions({
|
||||||
|
serverHost: ipv4Host,
|
||||||
|
serverHostIpv6Only: '1',
|
||||||
|
}),
|
||||||
|
).toThrow(/SERVER_HOST_IPV6_ONLY=1 is incompatible with IPv4 host "0\.0\.0\.0"/);
|
||||||
|
});
|
||||||
|
});
|
||||||
17
packages/services/service-common/src/listen-options.ts
Normal file
17
packages/services/service-common/src/listen-options.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
const IPV4_LITERAL = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
|
||||||
|
|
||||||
|
export function resolveServerListenOptions(input: {
|
||||||
|
serverHost?: string;
|
||||||
|
serverHostIpv6Only?: '0' | '1';
|
||||||
|
}) {
|
||||||
|
const host = input.serverHost ?? '::';
|
||||||
|
const ipv6Only = input.serverHostIpv6Only === '1';
|
||||||
|
|
||||||
|
if (ipv6Only && IPV4_LITERAL.test(host)) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid listen options: SERVER_HOST_IPV6_ONLY=1 is incompatible with IPv4 host "${host}".`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { host, ipv6Only } as const;
|
||||||
|
}
|
||||||
|
|
@ -13,10 +13,18 @@ export function reportReadiness(isReady: boolean) {
|
||||||
readiness.set(isReady ? 1 : 0);
|
readiness.set(isReady ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MetricsListenOptions = {
|
||||||
|
port?: number;
|
||||||
|
host?: string;
|
||||||
|
ipv6Only?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export async function startMetrics(
|
export async function startMetrics(
|
||||||
instanceLabel: string | undefined,
|
instanceLabel: string | undefined,
|
||||||
port = 10_254,
|
options: MetricsListenOptions = {},
|
||||||
): Promise<() => Promise<void>> {
|
): Promise<() => Promise<void>> {
|
||||||
|
const { port = 10_254, host = '::', ipv6Only = false } = options;
|
||||||
|
|
||||||
promClient.collectDefaultMetrics({
|
promClient.collectDefaultMetrics({
|
||||||
labels: { instance: instanceLabel },
|
labels: { instance: instanceLabel },
|
||||||
});
|
});
|
||||||
|
|
@ -45,7 +53,8 @@ export async function startMetrics(
|
||||||
|
|
||||||
await server.listen({
|
await server.listen({
|
||||||
port,
|
port,
|
||||||
host: '::',
|
host,
|
||||||
|
ipv6Only,
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => server.close();
|
return () => server.close();
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
"db:generate": "schemats generate --config schemats.cjs -o src/db/types.ts && prettier --write src/db/types.ts"
|
"db:generate": "schemats generate --config schemats.cjs -o src/db/types.ts && prettier --write src/db/types.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@graphql-inspector/core": "7.1.2",
|
"@graphql-inspector/core": "7.1.3",
|
||||||
"@hive/postgres": "workspace:*",
|
"@hive/postgres": "workspace:*",
|
||||||
"@hive/service-common": "workspace:*",
|
"@hive/service-common": "workspace:*",
|
||||||
"@sentry/node": "7.120.2",
|
"@sentry/node": "7.120.2",
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -7,6 +7,8 @@ REDIS_HOST="localhost"
|
||||||
REDIS_PORT="6379"
|
REDIS_PORT="6379"
|
||||||
REDIS_PASSWORD=""
|
REDIS_PASSWORD=""
|
||||||
PORT=6001
|
PORT=6001
|
||||||
|
SERVER_HOST=::
|
||||||
|
SERVER_HOST_IPV6_ONLY=0
|
||||||
OPENTELEMETRY_COLLECTOR_ENDPOINT="<sync>"
|
OPENTELEMETRY_COLLECTOR_ENDPOINT="<sync>"
|
||||||
LOG_LEVEL="debug"
|
LOG_LEVEL="debug"
|
||||||
OPENTELEMETRY_TRACE_USAGE_REQUESTS=1
|
OPENTELEMETRY_TRACE_USAGE_REQUESTS=1
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import zod from 'zod';
|
import zod from 'zod';
|
||||||
import { OpenTelemetryConfigurationModel } from '@hive/service-common';
|
import { OpenTelemetryConfigurationModel, resolveServerListenOptions } from '@hive/service-common';
|
||||||
|
|
||||||
const isNumberString = (input: unknown) => zod.string().regex(/^\d+$/).safeParse(input).success;
|
const isNumberString = (input: unknown) => zod.string().regex(/^\d+$/).safeParse(input).success;
|
||||||
|
|
||||||
|
|
@ -20,6 +20,8 @@ const emptyString = <T extends zod.ZodType>(input: T) => {
|
||||||
|
|
||||||
const EnvironmentModel = zod.object({
|
const EnvironmentModel = zod.object({
|
||||||
PORT: emptyString(NumberFromString.optional()),
|
PORT: emptyString(NumberFromString.optional()),
|
||||||
|
SERVER_HOST: emptyString(zod.string().optional()),
|
||||||
|
SERVER_HOST_IPV6_ONLY: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()),
|
||||||
ENVIRONMENT: emptyString(zod.string().optional()),
|
ENVIRONMENT: emptyString(zod.string().optional()),
|
||||||
RELEASE: emptyString(zod.string().optional()),
|
RELEASE: emptyString(zod.string().optional()),
|
||||||
HEARTBEAT_ENDPOINT: emptyString(zod.string().url().optional()),
|
HEARTBEAT_ENDPOINT: emptyString(zod.string().url().optional()),
|
||||||
|
|
@ -131,6 +133,10 @@ export const env = {
|
||||||
release: base.RELEASE ?? 'local',
|
release: base.RELEASE ?? 'local',
|
||||||
http: {
|
http: {
|
||||||
port: base.PORT ?? 6001,
|
port: base.PORT ?? 6001,
|
||||||
|
...resolveServerListenOptions({
|
||||||
|
serverHost: base.SERVER_HOST,
|
||||||
|
serverHostIpv6Only: base.SERVER_HOST_IPV6_ONLY,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
tracing: {
|
tracing: {
|
||||||
enabled: !!tracing.OPENTELEMETRY_COLLECTOR_ENDPOINT,
|
enabled: !!tracing.OPENTELEMETRY_COLLECTOR_ENDPOINT,
|
||||||
|
|
|
||||||
|
|
@ -169,12 +169,17 @@ export async function main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (env.prometheus) {
|
if (env.prometheus) {
|
||||||
await startMetrics(env.prometheus.labels.instance, env.prometheus.port);
|
await startMetrics(env.prometheus.labels.instance, {
|
||||||
|
port: env.prometheus.port,
|
||||||
|
host: env.http.host,
|
||||||
|
ipv6Only: env.http.ipv6Only,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await server.listen({
|
await server.listen({
|
||||||
port: env.http.port,
|
port: env.http.port,
|
||||||
host: '::',
|
host: env.http.host,
|
||||||
|
ipv6Only: env.http.ipv6Only,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
server.log.fatal(error);
|
server.log.fatal(error);
|
||||||
|
|
|
||||||
|
|
@ -11,3 +11,5 @@ CLICKHOUSE_PASSWORD="test"
|
||||||
CLICKHOUSE_ASYNC_INSERT_BUSY_TIMEOUT_MS="500"
|
CLICKHOUSE_ASYNC_INSERT_BUSY_TIMEOUT_MS="500"
|
||||||
CLICKHOUSE_ASYNC_INSERT_MAX_DATA_SIZE="1000"
|
CLICKHOUSE_ASYNC_INSERT_MAX_DATA_SIZE="1000"
|
||||||
PORT=4002
|
PORT=4002
|
||||||
|
SERVER_HOST=::
|
||||||
|
SERVER_HOST_IPV6_ONLY=0
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import zod from 'zod';
|
import zod from 'zod';
|
||||||
|
import { resolveServerListenOptions } from '@hive/service-common';
|
||||||
|
|
||||||
const isNumberString = (input: unknown) => zod.string().regex(/^\d+$/).safeParse(input).success;
|
const isNumberString = (input: unknown) => zod.string().regex(/^\d+$/).safeParse(input).success;
|
||||||
|
|
||||||
|
|
@ -20,6 +21,8 @@ const emptyString = <T extends zod.ZodType>(input: T) => {
|
||||||
|
|
||||||
const EnvironmentModel = zod.object({
|
const EnvironmentModel = zod.object({
|
||||||
PORT: emptyString(NumberFromString.optional()),
|
PORT: emptyString(NumberFromString.optional()),
|
||||||
|
SERVER_HOST: emptyString(zod.string().optional()),
|
||||||
|
SERVER_HOST_IPV6_ONLY: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()),
|
||||||
ENVIRONMENT: emptyString(zod.string().optional()),
|
ENVIRONMENT: emptyString(zod.string().optional()),
|
||||||
RELEASE: emptyString(zod.string().optional()),
|
RELEASE: emptyString(zod.string().optional()),
|
||||||
HEARTBEAT_ENDPOINT: emptyString(zod.string().url().optional()),
|
HEARTBEAT_ENDPOINT: emptyString(zod.string().url().optional()),
|
||||||
|
|
@ -162,6 +165,10 @@ export const env = {
|
||||||
release: base.RELEASE ?? 'local',
|
release: base.RELEASE ?? 'local',
|
||||||
http: {
|
http: {
|
||||||
port: base.PORT ?? 5000,
|
port: base.PORT ?? 5000,
|
||||||
|
...resolveServerListenOptions({
|
||||||
|
serverHost: base.SERVER_HOST,
|
||||||
|
serverHostIpv6Only: base.SERVER_HOST_IPV6_ONLY,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
kafka: {
|
kafka: {
|
||||||
concurrency: kafka.KAFKA_CONCURRENCY,
|
concurrency: kafka.KAFKA_CONCURRENCY,
|
||||||
|
|
|
||||||
|
|
@ -80,11 +80,16 @@ async function main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (env.prometheus) {
|
if (env.prometheus) {
|
||||||
await startMetrics(env.prometheus.labels.instance, env.prometheus.port);
|
await startMetrics(env.prometheus.labels.instance, {
|
||||||
|
port: env.prometheus.port,
|
||||||
|
host: env.http.host,
|
||||||
|
ipv6Only: env.http.ipv6Only,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
await server.listen({
|
await server.listen({
|
||||||
port: env.http.port,
|
port: env.http.port,
|
||||||
host: '::',
|
host: env.http.host,
|
||||||
|
ipv6Only: env.http.ipv6Only,
|
||||||
});
|
});
|
||||||
await start();
|
await start();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -17,5 +17,7 @@ REDIS_PORT="6379"
|
||||||
REDIS_PASSWORD=""
|
REDIS_PASSWORD=""
|
||||||
|
|
||||||
PORT=4001
|
PORT=4001
|
||||||
|
SERVER_HOST=::
|
||||||
|
SERVER_HOST_IPV6_ONLY=0
|
||||||
COMMERCE_ENDPOINT="http://localhost:4013"
|
COMMERCE_ENDPOINT="http://localhost:4013"
|
||||||
OPENTELEMETRY_COLLECTOR_ENDPOINT="<sync>"
|
OPENTELEMETRY_COLLECTOR_ENDPOINT="<sync>"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import zod from 'zod';
|
import zod from 'zod';
|
||||||
import { OpenTelemetryConfigurationModel } from '@hive/service-common';
|
import { OpenTelemetryConfigurationModel, resolveServerListenOptions } from '@hive/service-common';
|
||||||
|
|
||||||
const isNumberString = (input: unknown) => zod.string().regex(/^\d+$/).safeParse(input).success;
|
const isNumberString = (input: unknown) => zod.string().regex(/^\d+$/).safeParse(input).success;
|
||||||
|
|
||||||
|
|
@ -21,6 +21,8 @@ const emptyString = <T extends zod.ZodType>(input: T) => {
|
||||||
|
|
||||||
const EnvironmentModel = zod.object({
|
const EnvironmentModel = zod.object({
|
||||||
PORT: emptyString(NumberFromString.optional()),
|
PORT: emptyString(NumberFromString.optional()),
|
||||||
|
SERVER_HOST: emptyString(zod.string().optional()),
|
||||||
|
SERVER_HOST_IPV6_ONLY: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()),
|
||||||
TOKENS_ENDPOINT: zod.string().url(),
|
TOKENS_ENDPOINT: zod.string().url(),
|
||||||
COMMERCE_ENDPOINT: emptyString(zod.string().url().optional()),
|
COMMERCE_ENDPOINT: emptyString(zod.string().url().optional()),
|
||||||
RATE_LIMIT_TTL: emptyString(NumberFromString.optional()).default(30_000),
|
RATE_LIMIT_TTL: emptyString(NumberFromString.optional()).default(30_000),
|
||||||
|
|
@ -152,6 +154,10 @@ export const env = {
|
||||||
release: base.RELEASE ?? 'local',
|
release: base.RELEASE ?? 'local',
|
||||||
http: {
|
http: {
|
||||||
port: base.PORT ?? 5000,
|
port: base.PORT ?? 5000,
|
||||||
|
...resolveServerListenOptions({
|
||||||
|
serverHost: base.SERVER_HOST,
|
||||||
|
serverHostIpv6Only: base.SERVER_HOST_IPV6_ONLY,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
tracing: {
|
tracing: {
|
||||||
enabled: !!tracing.OPENTELEMETRY_COLLECTOR_ENDPOINT,
|
enabled: !!tracing.OPENTELEMETRY_COLLECTOR_ENDPOINT,
|
||||||
|
|
|
||||||
|
|
@ -193,12 +193,17 @@ async function main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (env.prometheus) {
|
if (env.prometheus) {
|
||||||
await startMetrics(env.prometheus.labels.instance, env.prometheus.port);
|
await startMetrics(env.prometheus.labels.instance, {
|
||||||
|
port: env.prometheus.port,
|
||||||
|
host: env.http.host,
|
||||||
|
ipv6Only: env.http.ipv6Only,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await server.listen({
|
await server.listen({
|
||||||
port: env.http.port,
|
port: env.http.port,
|
||||||
host: '::',
|
host: env.http.host,
|
||||||
|
ipv6Only: env.http.ipv6Only,
|
||||||
});
|
});
|
||||||
await usage.start();
|
await usage.start();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -9,3 +9,5 @@ SCHEMA_ENDPOINT=http://localhost:6500
|
||||||
REDIS_HOST=localhost
|
REDIS_HOST=localhost
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
REDIS_PASSWORD=
|
REDIS_PASSWORD=
|
||||||
|
SERVER_HOST=::
|
||||||
|
SERVER_HOST_IPV6_ONLY=0
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@graphql-hive/logger": "1.0.9",
|
"@graphql-hive/logger": "1.1.0",
|
||||||
"@graphql-inspector/core": "7.1.2",
|
"@graphql-inspector/core": "7.1.3",
|
||||||
"@graphql-inspector/patch": "0.1.3",
|
"@graphql-inspector/patch": "0.1.3",
|
||||||
"@graphql-yoga/redis-event-target": "3.0.3",
|
"@graphql-yoga/redis-event-target": "3.0.3",
|
||||||
"@hive/postgres": "workspace:*",
|
"@hive/postgres": "workspace:*",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import zod from 'zod';
|
import zod from 'zod';
|
||||||
import { PostgresConnectionParamaters } from '@hive/postgres';
|
import { PostgresConnectionParamaters } from '@hive/postgres';
|
||||||
import { OpenTelemetryConfigurationModel } from '@hive/service-common';
|
import { OpenTelemetryConfigurationModel, resolveServerListenOptions } from '@hive/service-common';
|
||||||
import { RequestBroker } from './lib/webhooks/send-webhook.js';
|
import { RequestBroker } from './lib/webhooks/send-webhook.js';
|
||||||
|
|
||||||
const isNumberString = (input: unknown) => zod.string().regex(/^\d+$/).safeParse(input).success;
|
const isNumberString = (input: unknown) => zod.string().regex(/^\d+$/).safeParse(input).success;
|
||||||
|
|
@ -22,6 +22,8 @@ const emptyString = <T extends zod.ZodType>(input: T) => {
|
||||||
|
|
||||||
const EnvironmentModel = zod.object({
|
const EnvironmentModel = zod.object({
|
||||||
PORT: emptyString(NumberFromString.optional()).default(3014),
|
PORT: emptyString(NumberFromString.optional()).default(3014),
|
||||||
|
SERVER_HOST: emptyString(zod.string().optional()),
|
||||||
|
SERVER_HOST_IPV6_ONLY: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()),
|
||||||
ENVIRONMENT: emptyString(zod.string().optional()),
|
ENVIRONMENT: emptyString(zod.string().optional()),
|
||||||
RELEASE: emptyString(zod.string().optional()),
|
RELEASE: emptyString(zod.string().optional()),
|
||||||
HEARTBEAT_ENDPOINT: emptyString(zod.string().url().optional()),
|
HEARTBEAT_ENDPOINT: emptyString(zod.string().url().optional()),
|
||||||
|
|
@ -205,6 +207,10 @@ export const env = {
|
||||||
release: base.RELEASE ?? 'local',
|
release: base.RELEASE ?? 'local',
|
||||||
http: {
|
http: {
|
||||||
port: base.PORT ?? 6260,
|
port: base.PORT ?? 6260,
|
||||||
|
...resolveServerListenOptions({
|
||||||
|
serverHost: base.SERVER_HOST,
|
||||||
|
serverHostIpv6Only: base.SERVER_HOST_IPV6_ONLY,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
tracing: {
|
tracing: {
|
||||||
enabled: !!tracing.OPENTELEMETRY_COLLECTOR_ENDPOINT,
|
enabled: !!tracing.OPENTELEMETRY_COLLECTOR_ENDPOINT,
|
||||||
|
|
|
||||||
|
|
@ -134,11 +134,16 @@ if (context.email.id === 'mock') {
|
||||||
|
|
||||||
await server.listen({
|
await server.listen({
|
||||||
port: env.http.port,
|
port: env.http.port,
|
||||||
host: '::',
|
host: env.http.host,
|
||||||
|
ipv6Only: env.http.ipv6Only,
|
||||||
});
|
});
|
||||||
|
|
||||||
const shutdownMetrics = env.prometheus
|
const shutdownMetrics = env.prometheus
|
||||||
? await startMetrics(env.prometheus.labels.instance, env.prometheus.port)
|
? await startMetrics(env.prometheus.labels.instance, {
|
||||||
|
port: env.prometheus.port,
|
||||||
|
host: env.http.host,
|
||||||
|
ipv6Only: env.http.ipv6Only,
|
||||||
|
})
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const runner = await run({
|
const runner = await run({
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
APP_BASE_URL="http://localhost:3000"
|
APP_BASE_URL="http://localhost:3000"
|
||||||
ENVIRONMENT="development"
|
ENVIRONMENT="development"
|
||||||
|
SERVER_HOST=::
|
||||||
|
SERVER_HOST_IPV6_ONLY=0
|
||||||
|
|
||||||
# Public GraphQL endpoint
|
# Public GraphQL endpoint
|
||||||
GRAPHQL_PUBLIC_ENDPOINT="http://localhost:3001/graphql"
|
GRAPHQL_PUBLIC_ENDPOINT="http://localhost:3001/graphql"
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,12 @@
|
||||||
"@graphiql/toolkit": "0.9.1",
|
"@graphiql/toolkit": "0.9.1",
|
||||||
"@graphql-codegen/client-preset-swc-plugin": "0.2.0",
|
"@graphql-codegen/client-preset-swc-plugin": "0.2.0",
|
||||||
"@graphql-hive/laboratory": "workspace:*",
|
"@graphql-hive/laboratory": "workspace:*",
|
||||||
"@graphql-inspector/core": "7.1.2",
|
"@graphql-inspector/core": "7.1.3",
|
||||||
"@graphql-inspector/patch": "0.1.3",
|
"@graphql-inspector/patch": "0.1.3",
|
||||||
"@graphql-tools/mock": "9.0.25",
|
"@graphql-tools/mock": "9.0.25",
|
||||||
"@graphql-typed-document-node/core": "3.2.0",
|
"@graphql-typed-document-node/core": "3.2.0",
|
||||||
"@headlessui/react": "2.2.0",
|
"@headlessui/react": "2.2.0",
|
||||||
|
"@hive/service-common": "workspace:*",
|
||||||
"@hookform/resolvers": "3.10.0",
|
"@hookform/resolvers": "3.10.0",
|
||||||
"@ladle/react": "5.1.1",
|
"@ladle/react": "5.1.1",
|
||||||
"@monaco-editor/react": "4.8.0-rc.2",
|
"@monaco-editor/react": "4.8.0-rc.2",
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,6 @@ export const ChangesBlock_SchemaChangeFragment = graphql(`
|
||||||
export function ChangesBlock(
|
export function ChangesBlock(
|
||||||
props: {
|
props: {
|
||||||
title: string | React.ReactElement;
|
title: string | React.ReactElement;
|
||||||
severityLevel: SeverityLevelType;
|
|
||||||
organizationSlug: string;
|
organizationSlug: string;
|
||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
targetSlug: string;
|
targetSlug: string;
|
||||||
|
|
|
||||||
7
packages/web/app/src/env/backend.ts
vendored
7
packages/web/app/src/env/backend.ts
vendored
|
|
@ -1,4 +1,5 @@
|
||||||
import zod from 'zod';
|
import zod from 'zod';
|
||||||
|
import { resolveServerListenOptions } from '@hive/service-common/listen-options';
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { ALLOWED_ENVIRONMENT_VARIABLES } from './frontend-public-variables';
|
import { ALLOWED_ENVIRONMENT_VARIABLES } from './frontend-public-variables';
|
||||||
|
|
||||||
|
|
@ -41,6 +42,8 @@ const BaseSchema = zod.object({
|
||||||
NODE_ENV: zod.string().default('development'),
|
NODE_ENV: zod.string().default('development'),
|
||||||
ENVIRONMENT: zod.string(),
|
ENVIRONMENT: zod.string(),
|
||||||
PORT: emptyString(NumberFromString().optional()),
|
PORT: emptyString(NumberFromString().optional()),
|
||||||
|
SERVER_HOST: emptyString(zod.string().optional()),
|
||||||
|
SERVER_HOST_IPV6_ONLY: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()),
|
||||||
APP_BASE_URL: zod.string().url(),
|
APP_BASE_URL: zod.string().url(),
|
||||||
GRAPHQL_PUBLIC_ENDPOINT: zod.string().url(),
|
GRAPHQL_PUBLIC_ENDPOINT: zod.string().url(),
|
||||||
GRAPHQL_PUBLIC_SUBSCRIPTION_ENDPOINT: zod.string().url(),
|
GRAPHQL_PUBLIC_SUBSCRIPTION_ENDPOINT: zod.string().url(),
|
||||||
|
|
@ -172,6 +175,10 @@ function buildConfig() {
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
port: base.PORT ?? 3000,
|
port: base.PORT ?? 3000,
|
||||||
|
...resolveServerListenOptions({
|
||||||
|
serverHost: base.SERVER_HOST,
|
||||||
|
serverHostIpv6Only: base.SERVER_HOST_IPV6_ONLY,
|
||||||
|
}),
|
||||||
release: base.RELEASE ?? 'local',
|
release: base.RELEASE ?? 'local',
|
||||||
nodeEnv: base.NODE_ENV,
|
nodeEnv: base.NODE_ENV,
|
||||||
environment: base.ENVIRONMENT,
|
environment: base.ENVIRONMENT,
|
||||||
|
|
|
||||||
46
packages/web/app/src/env/listen-options.spec.ts
vendored
Normal file
46
packages/web/app/src/env/listen-options.spec.ts
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { resolveServerListenOptions } from '@hive/service-common/listen-options';
|
||||||
|
|
||||||
|
describe('resolveServerListenOptions', () => {
|
||||||
|
// eslint-disable-next-line no-restricted-syntax -- explicit IPv4 literal required for validation coverage
|
||||||
|
const ipv4Host = '0.0.0.0';
|
||||||
|
|
||||||
|
test('defaults to dual-stack host and ipv4 fallback enabled', () => {
|
||||||
|
expect(resolveServerListenOptions({})).toEqual({
|
||||||
|
host: '::',
|
||||||
|
ipv6Only: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('supports ipv6-only wildcard mode', () => {
|
||||||
|
expect(
|
||||||
|
resolveServerListenOptions({
|
||||||
|
serverHost: '::',
|
||||||
|
serverHostIpv6Only: '1',
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
host: '::',
|
||||||
|
ipv6Only: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('supports ipv4-only host values', () => {
|
||||||
|
expect(
|
||||||
|
resolveServerListenOptions({
|
||||||
|
serverHost: ipv4Host,
|
||||||
|
serverHostIpv6Only: '0',
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
host: ipv4Host,
|
||||||
|
ipv6Only: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects ipv6-only combined with an IPv4 literal host', () => {
|
||||||
|
expect(() =>
|
||||||
|
resolveServerListenOptions({
|
||||||
|
serverHost: ipv4Host,
|
||||||
|
serverHostIpv6Only: '1',
|
||||||
|
}),
|
||||||
|
).toThrow(/SERVER_HOST_IPV6_ONLY=1 is incompatible with IPv4 host "0\.0\.0\.0"/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -35,7 +35,7 @@ import { TimeAgo } from '@/components/ui/time-ago';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
import { DiffEditor } from '@/components/v2/diff-editor';
|
import { DiffEditor } from '@/components/v2/diff-editor';
|
||||||
import { FragmentType, graphql, useFragment } from '@/gql';
|
import { FragmentType, graphql, useFragment } from '@/gql';
|
||||||
import { ProjectType, SeverityLevelType } from '@/gql/graphql';
|
import { ProjectType } from '@/gql/graphql';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import {
|
import {
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
|
|
@ -304,7 +304,7 @@ function ConditionalBreakingChangesMetadataSection(props: {
|
||||||
Get more out of schema checks by enabling conditional breaking changes based on usage data.
|
Get more out of schema checks by enabling conditional breaking changes based on usage data.
|
||||||
<br />
|
<br />
|
||||||
<DocsLink
|
<DocsLink
|
||||||
href="/management/targets#conditional-breaking-changes"
|
href="/schema-registry/management/targets#conditional-breaking-changes"
|
||||||
className="text-neutral-10 hover:text-neutral-11"
|
className="text-neutral-10 hover:text-neutral-11"
|
||||||
>
|
>
|
||||||
Learn more about conditional breaking changes.
|
Learn more about conditional breaking changes.
|
||||||
|
|
@ -387,7 +387,7 @@ function ConditionalBreakingChangesMetadataSection(props: {
|
||||||
).
|
).
|
||||||
<br />
|
<br />
|
||||||
<DocsLink
|
<DocsLink
|
||||||
href="/management/targets#conditional-breaking-changes"
|
href="/schema-registry/management/targets#conditional-breaking-changes"
|
||||||
className="text-neutral-10 hover:text-neutral-11"
|
className="text-neutral-10 hover:text-neutral-11"
|
||||||
>
|
>
|
||||||
Learn more about conditional breaking changes.
|
Learn more about conditional breaking changes.
|
||||||
|
|
@ -564,7 +564,6 @@ function DefaultSchemaView(props: {
|
||||||
targetSlug={props.targetSlug}
|
targetSlug={props.targetSlug}
|
||||||
schemaCheckId={schemaCheck.id}
|
schemaCheckId={schemaCheck.id}
|
||||||
title={<BreakingChangesTitle />}
|
title={<BreakingChangesTitle />}
|
||||||
severityLevel={SeverityLevelType.Breaking}
|
|
||||||
changesWithUsage={schemaCheck.breakingSchemaChanges.edges.map(edge => edge.node)}
|
changesWithUsage={schemaCheck.breakingSchemaChanges.edges.map(edge => edge.node)}
|
||||||
conditionBreakingChangeMetadata={schemaCheck.conditionalBreakingChangeMetadata}
|
conditionBreakingChangeMetadata={schemaCheck.conditionalBreakingChangeMetadata}
|
||||||
/>
|
/>
|
||||||
|
|
@ -578,7 +577,6 @@ function DefaultSchemaView(props: {
|
||||||
targetSlug={props.targetSlug}
|
targetSlug={props.targetSlug}
|
||||||
schemaCheckId={schemaCheck.id}
|
schemaCheckId={schemaCheck.id}
|
||||||
title="Safe Changes"
|
title="Safe Changes"
|
||||||
severityLevel={SeverityLevelType.Safe}
|
|
||||||
changes={schemaCheck.safeSchemaChanges.edges.map(edge => edge.node)}
|
changes={schemaCheck.safeSchemaChanges.edges.map(edge => edge.node)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -768,7 +766,6 @@ function ContractCheckView(props: {
|
||||||
targetSlug={props.targetSlug}
|
targetSlug={props.targetSlug}
|
||||||
schemaCheckId={schemaCheck.id}
|
schemaCheckId={schemaCheck.id}
|
||||||
title={<BreakingChangesTitle />}
|
title={<BreakingChangesTitle />}
|
||||||
severityLevel={SeverityLevelType.Breaking}
|
|
||||||
changesWithUsage={contractCheck.breakingSchemaChanges.edges.map(
|
changesWithUsage={contractCheck.breakingSchemaChanges.edges.map(
|
||||||
edge => edge.node,
|
edge => edge.node,
|
||||||
)}
|
)}
|
||||||
|
|
@ -784,7 +781,6 @@ function ContractCheckView(props: {
|
||||||
targetSlug={props.targetSlug}
|
targetSlug={props.targetSlug}
|
||||||
schemaCheckId={schemaCheck.id}
|
schemaCheckId={schemaCheck.id}
|
||||||
title="Safe Changes"
|
title="Safe Changes"
|
||||||
severityLevel={SeverityLevelType.Safe}
|
|
||||||
changes={contractCheck.safeSchemaChanges.edges.map(edge => edge.node)}
|
changes={contractCheck.safeSchemaChanges.edges.map(edge => edge.node)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue