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
|
||||
what this change introduces for their self-hosted Hive instance, without going into too much
|
||||
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
|
||||
|
||||
## 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
|
||||
|
||||
### Patch Changes
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "hive",
|
||||
"version": "11.0.4",
|
||||
"version": "11.1.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"generate": "tsx generate.ts",
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
"prettier": "3.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-hive/gateway": "^2.1.19",
|
||||
"@graphql-hive/gateway": "2.7.2",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/mime-types": "2.1.4",
|
||||
"@types/node": "24.12.2",
|
||||
|
|
|
|||
|
|
@ -120,6 +120,12 @@ export class Redis {
|
|||
args: [
|
||||
'/opt/bitnami/scripts/redis/run.sh',
|
||||
`--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
|
||||
'--save ""',
|
||||
],
|
||||
|
|
|
|||
|
|
@ -203,6 +203,8 @@ services:
|
|||
ENCRYPTION_SECRET: '${HIVE_ENCRYPTION_SECRET}'
|
||||
WEB_APP_URL: '${HIVE_APP_BASE_URL}'
|
||||
PORT: 3001
|
||||
SERVER_HOST: '${SERVER_HOST:-::}'
|
||||
SERVER_HOST_IPV6_ONLY: '${SERVER_HOST_IPV6_ONLY:-0}'
|
||||
S3_ENDPOINT: 'http://s3:9000'
|
||||
S3_ACCESS_KEY_ID: ${MINIO_ROOT_USER}
|
||||
S3_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD}
|
||||
|
|
@ -234,6 +236,8 @@ services:
|
|||
environment:
|
||||
NODE_ENV: production
|
||||
PORT: 3012
|
||||
SERVER_HOST: '${SERVER_HOST:-::}'
|
||||
SERVER_HOST_IPV6_ONLY: '${SERVER_HOST_IPV6_ONLY:-0}'
|
||||
LOG_LEVEL: '${LOG_LEVEL:-debug}'
|
||||
OPENTELEMETRY_COLLECTOR_ENDPOINT: '${OPENTELEMETRY_COLLECTOR_ENDPOINT:-}'
|
||||
SENTRY: '${SENTRY:-0}'
|
||||
|
|
@ -250,6 +254,8 @@ services:
|
|||
environment:
|
||||
NODE_ENV: production
|
||||
PORT: 3002
|
||||
SERVER_HOST: '${SERVER_HOST:-::}'
|
||||
SERVER_HOST_IPV6_ONLY: '${SERVER_HOST_IPV6_ONLY:-0}'
|
||||
REDIS_HOST: redis
|
||||
REDIS_PORT: 6379
|
||||
REDIS_PASSWORD: '${REDIS_PASSWORD}'
|
||||
|
|
@ -278,6 +284,8 @@ services:
|
|||
REDIS_PORT: 6379
|
||||
REDIS_PASSWORD: '${REDIS_PASSWORD}'
|
||||
PORT: 3003
|
||||
SERVER_HOST: '${SERVER_HOST:-::}'
|
||||
SERVER_HOST_IPV6_ONLY: '${SERVER_HOST_IPV6_ONLY:-0}'
|
||||
LOG_LEVEL: '${LOG_LEVEL:-debug}'
|
||||
OPENTELEMETRY_COLLECTOR_ENDPOINT: '${OPENTELEMETRY_COLLECTOR_ENDPOINT:-}'
|
||||
SENTRY: '${SENTRY:-0}'
|
||||
|
|
@ -294,6 +302,8 @@ services:
|
|||
environment:
|
||||
NODE_ENV: production
|
||||
PORT: 3014
|
||||
SERVER_HOST: '${SERVER_HOST:-::}'
|
||||
SERVER_HOST_IPV6_ONLY: '${SERVER_HOST_IPV6_ONLY:-0}'
|
||||
POSTGRES_HOST: db
|
||||
POSTGRES_PORT: 5432
|
||||
POSTGRES_DB: '${POSTGRES_DB}'
|
||||
|
|
@ -340,6 +350,8 @@ services:
|
|||
REDIS_PORT: 6379
|
||||
REDIS_PASSWORD: '${REDIS_PASSWORD}'
|
||||
PORT: 3006
|
||||
SERVER_HOST: '${SERVER_HOST:-::}'
|
||||
SERVER_HOST_IPV6_ONLY: '${SERVER_HOST_IPV6_ONLY:-0}'
|
||||
LOG_LEVEL: '${LOG_LEVEL:-debug}'
|
||||
SENTRY: '${SENTRY:-0}'
|
||||
SENTRY_DSN: '${SENTRY_DSN:-}'
|
||||
|
|
@ -367,6 +379,8 @@ services:
|
|||
CLICKHOUSE_USERNAME: '${CLICKHOUSE_USER}'
|
||||
CLICKHOUSE_PASSWORD: '${CLICKHOUSE_PASSWORD}'
|
||||
PORT: 3007
|
||||
SERVER_HOST: '${SERVER_HOST:-::}'
|
||||
SERVER_HOST_IPV6_ONLY: '${SERVER_HOST_IPV6_ONLY:-0}'
|
||||
LOG_LEVEL: '${LOG_LEVEL:-debug}'
|
||||
SENTRY: '${SENTRY:-0}'
|
||||
SENTRY_DSN: '${SENTRY_DSN:-}'
|
||||
|
|
@ -380,6 +394,8 @@ services:
|
|||
- 'stack'
|
||||
environment:
|
||||
PORT: 3000
|
||||
SERVER_HOST: '${SERVER_HOST:-::}'
|
||||
SERVER_HOST_IPV6_ONLY: '${SERVER_HOST_IPV6_ONLY:-0}'
|
||||
NODE_ENV: production
|
||||
APP_BASE_URL: '${HIVE_APP_BASE_URL}'
|
||||
GRAPHQL_PUBLIC_ENDPOINT: http://localhost:8082/graphql
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
|
||||
Developing Hive locally requires you to have the following software installed locally:
|
||||
|
||||
- Node.js >=22 (or `nvm` or `fnm`)
|
||||
- pnpm >=10.16.0
|
||||
- Node.js (or `nvm` or `fnm`): check the `package.json` `engines` entry for the correct version
|
||||
- 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)
|
||||
- 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
|
||||
- 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:
|
||||
|
||||
```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
|
||||
you only need to run GraphQL Codegen)
|
||||
- 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
|
||||
- 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 { clickHouseInsert } from '../../testkit/clickhouse';
|
||||
import { graphql } from '../../testkit/gql';
|
||||
import { ResourceAssignmentModeType } from '../../testkit/gql/graphql';
|
||||
import { execute } from '../../testkit/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 () => {
|
||||
const { createOrg, ownerToken } = await initSeed().createOwner();
|
||||
const { createProject, setFeatureFlag, organization } = await createOrg();
|
||||
const { createOrg } = await initSeed().createOwner();
|
||||
const { createProject, setFeatureFlag, organization, createOrganizationAccessToken } =
|
||||
await createOrg();
|
||||
await setFeatureFlag('appDeployments', true);
|
||||
const { createTargetAccessToken, project, target } = await createProject();
|
||||
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({
|
||||
document: AddDocumentsToAppDeployment,
|
||||
variables: {
|
||||
input: {
|
||||
appName: 'app-name',
|
||||
appVersion: 'app-version',
|
||||
target: {
|
||||
byId: target.id,
|
||||
},
|
||||
documents: [
|
||||
{
|
||||
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());
|
||||
expect(addDocumentsToAppDeployment.error).toBeNull();
|
||||
|
||||
|
|
@ -1812,7 +1825,7 @@ test('get app deployment documents via GraphQL API', async () => {
|
|||
appDeploymentName: 'app-name',
|
||||
appDeploymentVersion: 'app-version',
|
||||
},
|
||||
authToken: ownerToken,
|
||||
authToken: token.secret,
|
||||
}).then(res => res.expectNoGraphQLErrors());
|
||||
expect(result.target).toMatchObject({
|
||||
appDeployment: {
|
||||
|
|
@ -2157,6 +2170,137 @@ test('app deployment usage reporting', async () => {
|
|||
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 () => {
|
||||
const { createOrg, ownerToken } = await initSeed().createOwner();
|
||||
const { createProject, setFeatureFlag, organization } = await createOrg();
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import {
|
|||
ResourceAssignmentModeType,
|
||||
RuleInstanceSeverityLevel,
|
||||
} from 'testkit/gql/graphql';
|
||||
import { SchemaVersionStore } from '@hive/api/modules/schema/providers/schema-version-store';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { createStorage } from '@hive/storage';
|
||||
import { graphql } from '../../../testkit/gql';
|
||||
|
|
@ -2103,7 +2104,8 @@ test.concurrent(
|
|||
|
||||
const conn = connectionString();
|
||||
const storage = await createStorage(conn, 2);
|
||||
await storage.createVersion({
|
||||
const schemaVersions = new SchemaVersionStore(storage.pool);
|
||||
await schemaVersions.createSchemaVersion({
|
||||
schema: brokenSdl,
|
||||
author: 'Jochen',
|
||||
async actionFn() {},
|
||||
|
|
@ -2113,7 +2115,6 @@ test.concurrent(
|
|||
compositeSchemaSDL: null,
|
||||
conditionalBreakingChangeMetadata: null,
|
||||
contracts: null,
|
||||
coordinatesDiff: null,
|
||||
diffSchemaVersionId: null,
|
||||
github: null,
|
||||
metadata: null,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { ProjectType } from 'testkit/gql/graphql';
|
|||
import { initSeed } from 'testkit/seed';
|
||||
import { assertNonNull, getServiceHost } from 'testkit/utils';
|
||||
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 { sortSDL } from '@theguild/federation-composition';
|
||||
|
||||
|
|
@ -132,6 +133,7 @@ test.concurrent(
|
|||
|
||||
try {
|
||||
storage = await createStorage(connectionString(), 1);
|
||||
const schemaVersions = new SchemaVersionStore(storage.pool);
|
||||
const { createOrg } = await initSeed().createOwner();
|
||||
const { createProject, organization } = await createOrg();
|
||||
const { createTargetAccessToken, project, target } = await createProject(
|
||||
|
|
@ -171,11 +173,7 @@ test.concurrent(
|
|||
.then(r => r.expectNoGraphQLErrors());
|
||||
expect(deleteServiceResult.schemaDelete.__typename).toBe('SchemaDeleteSuccess');
|
||||
|
||||
const latestVersion = await storage.getMaybeLatestVersion({
|
||||
targetId: target.id,
|
||||
projectId: project.id,
|
||||
organizationId: organization.id,
|
||||
});
|
||||
const latestVersion = await schemaVersions.getMaybeLatestSchemaVersionForTargetId(target.id);
|
||||
assertNonNull(latestVersion);
|
||||
|
||||
expect(latestVersion.compositeSchemaSDL).toMatchInlineSnapshot(`
|
||||
|
|
@ -187,9 +185,7 @@ test.concurrent(
|
|||
expect(latestVersion.hasPersistedSchemaChanges).toEqual(true);
|
||||
expect(latestVersion.isComposable).toEqual(true);
|
||||
|
||||
const changes = await storage.getSchemaChangesForVersion({
|
||||
versionId: latestVersion.id,
|
||||
});
|
||||
const changes = await schemaVersions.getSchemaSchangesForSchemaVersion(latestVersion);
|
||||
|
||||
if (Array.isArray(changes) === false) {
|
||||
throw new Error('No changes were persisted in the database.');
|
||||
|
|
@ -229,6 +225,7 @@ test.concurrent(
|
|||
|
||||
try {
|
||||
storage = await createStorage(connectionString(), 1);
|
||||
const schemaVersions = new SchemaVersionStore(storage.pool);
|
||||
const { createOrg, ownerToken } = await initSeed().createOwner();
|
||||
const { createProject, organization } = await createOrg();
|
||||
const { createTargetAccessToken, project, target, setNativeFederation } = await createProject(
|
||||
|
|
@ -311,11 +308,7 @@ test.concurrent(
|
|||
.then(r => r.expectNoGraphQLErrors());
|
||||
expect(deleteServiceResult.schemaDelete.__typename).toBe('SchemaDeleteSuccess');
|
||||
|
||||
const latestVersion = await storage.getMaybeLatestVersion({
|
||||
targetId: target.id,
|
||||
projectId: project.id,
|
||||
organizationId: organization.id,
|
||||
});
|
||||
const latestVersion = await schemaVersions.getMaybeLatestSchemaVersionForTargetId(target.id);
|
||||
assertNonNull(latestVersion);
|
||||
|
||||
expect(latestVersion.compositeSchemaSDL).toEqual(null);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { ProjectType } from 'testkit/gql/graphql';
|
|||
import { execute } from 'testkit/graphql';
|
||||
import { assertNonNull, getServiceHost } from 'testkit/utils';
|
||||
import z from 'zod';
|
||||
import { SchemaVersionStore } from '@hive/api/modules/schema/providers/schema-version-store';
|
||||
import { createPostgresDatabasePool, psql } from '@hive/postgres';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { createStorage } from '@hive/storage';
|
||||
|
|
@ -627,16 +628,12 @@ describe('schema publishing changes are persisted', () => {
|
|||
return;
|
||||
}
|
||||
|
||||
const latestVersion = await storage.getMaybeLatestVersion({
|
||||
targetId: target.id,
|
||||
projectId: project.id,
|
||||
organizationId: organization.id,
|
||||
});
|
||||
const schemaVersions = new SchemaVersionStore(storage.pool);
|
||||
|
||||
const latestVersion = await schemaVersions.getMaybeLatestSchemaVersionForTargetId(target.id);
|
||||
assertNonNull(latestVersion);
|
||||
|
||||
const changes = await storage.getSchemaChangesForVersion({
|
||||
versionId: latestVersion.id,
|
||||
});
|
||||
const changes = await schemaVersions.getSchemaSchangesForSchemaVersion(latestVersion);
|
||||
|
||||
if (!Array.isArray(changes)) {
|
||||
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 () => {
|
||||
const storage = await createStorage(connectionString(), 1);
|
||||
const schemaVersions = new SchemaVersionStore(storage.pool);
|
||||
|
||||
try {
|
||||
const serviceName = {
|
||||
|
|
@ -3305,11 +3303,7 @@ test('Target.schemaVersion: result is read from the database', async () => {
|
|||
return;
|
||||
}
|
||||
|
||||
const latestVersion = await storage.getMaybeLatestVersion({
|
||||
targetId: target.id,
|
||||
projectId: project.id,
|
||||
organizationId: organization.id,
|
||||
});
|
||||
const latestVersion = await schemaVersions.getMaybeLatestSchemaVersionForTargetId(target.id);
|
||||
assertNonNull(latestVersion);
|
||||
|
||||
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 () => {
|
||||
const storage = await createStorage(connectionString(), 1);
|
||||
const schemaVersions = new SchemaVersionStore(storage.pool);
|
||||
const serviceAddress = await getServiceHost('composition_federation_2', 3069, false);
|
||||
|
||||
try {
|
||||
|
|
@ -3444,11 +3439,7 @@ test('Composition Error (Federation 2) can be served from the database', async (
|
|||
return;
|
||||
}
|
||||
|
||||
const latestVersion = await storage.getMaybeLatestVersion({
|
||||
targetId: target.id,
|
||||
projectId: project.id,
|
||||
organizationId: organization.id,
|
||||
});
|
||||
const latestVersion = await schemaVersions.getMaybeLatestSchemaVersionForTargetId(target.id);
|
||||
assertNonNull(latestVersion);
|
||||
|
||||
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 () => {
|
||||
const storage = await createStorage(connectionString(), 1);
|
||||
const schemaVersions = new SchemaVersionStore(storage.pool);
|
||||
const serviceAddress = await getServiceHost('composition_federation_2', 3069, false);
|
||||
|
||||
try {
|
||||
|
|
@ -3610,11 +3602,7 @@ test('Composition Network Failure (Federation 2)', async () => {
|
|||
return;
|
||||
}
|
||||
|
||||
const latestVersion = await storage.getMaybeLatestVersion({
|
||||
targetId: target.id,
|
||||
projectId: project.id,
|
||||
organizationId: organization.id,
|
||||
});
|
||||
const latestVersion = await schemaVersions.getMaybeLatestSchemaVersionForTargetId(target.id);
|
||||
assertNonNull(latestVersion);
|
||||
|
||||
const result = await execute({
|
||||
|
|
@ -4543,7 +4531,8 @@ test.concurrent(
|
|||
|
||||
const conn = connectionString();
|
||||
const storage = await createStorage(conn, 2);
|
||||
await storage.createVersion({
|
||||
const schemaVersions = new SchemaVersionStore(storage.pool);
|
||||
await schemaVersions.createSchemaVersion({
|
||||
schema: brokenSdl,
|
||||
author: 'Jochen',
|
||||
async actionFn() {},
|
||||
|
|
@ -4553,7 +4542,6 @@ test.concurrent(
|
|||
compositeSchemaSDL: null,
|
||||
conditionalBreakingChangeMetadata: null,
|
||||
contracts: null,
|
||||
coordinatesDiff: null,
|
||||
diffSchemaVersionId: null,
|
||||
github: null,
|
||||
metadata: null,
|
||||
|
|
|
|||
11
package.json
11
package.json
|
|
@ -12,10 +12,10 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.18.3+sha512.bbd16e6d7286fd7e01f6b3c0b3c932cda2965c06a908328f74663f10a9aea51f1129eea615134bf992831b009eabe167ecb7008b597f40ff9bc75946aadfb08d",
|
||||
"packageManager": "pnpm@10.33.2+sha512.a90faf6feeab71ad6c6e57f94e0fe1a12f5dcc22cd754db40ae9593eb6a3e0b6b12e3540218bb37ae083404b1f2ce6db2a4121e979829b4aff94b99f49da1cf8",
|
||||
"engines": {
|
||||
"node": ">=24.14.1",
|
||||
"pnpm": ">=10.16.0"
|
||||
"pnpm": ">=10.33.2"
|
||||
},
|
||||
"scripts": {
|
||||
"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-eslint/eslint-plugin": "3.20.1",
|
||||
"@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-tools/load": "8.1.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.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.fast-uri@2.x.x": "address https://github.com/graphql-hive/console/security/dependabot/683",
|
||||
"overrides": {
|
||||
"esbuild": "0.25.9",
|
||||
"csstype": "3.1.2",
|
||||
|
|
@ -163,7 +164,9 @@
|
|||
"ajv@8.x.x": "^8.18.0",
|
||||
"yauzl@2.x.x": "^3.2.1",
|
||||
"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": {
|
||||
"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);
|
||||
}
|
||||
|
||||
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": {
|
||||
"@graphql-hive/core": "workspace:*",
|
||||
"@graphql-hive/logger": "^1.0.9"
|
||||
"@graphql-hive/logger": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@apollo/server": "5.5.0",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
# @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
|
||||
|
||||
### Patch Changes
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ DESCRIPTION
|
|||
```
|
||||
|
||||
_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`
|
||||
|
||||
|
|
@ -108,7 +108,7 @@ DESCRIPTION
|
|||
```
|
||||
|
||||
_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`
|
||||
|
||||
|
|
@ -136,7 +136,7 @@ DESCRIPTION
|
|||
```
|
||||
|
||||
_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`
|
||||
|
||||
|
|
@ -160,7 +160,7 @@ DESCRIPTION
|
|||
```
|
||||
|
||||
_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`
|
||||
|
||||
|
|
@ -203,7 +203,7 @@ DESCRIPTION
|
|||
```
|
||||
|
||||
_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]`
|
||||
|
||||
|
|
@ -249,7 +249,7 @@ DESCRIPTION
|
|||
```
|
||||
|
||||
_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`
|
||||
|
||||
|
|
@ -308,7 +308,7 @@ DESCRIPTION
|
|||
```
|
||||
|
||||
_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`
|
||||
|
||||
|
|
@ -353,7 +353,7 @@ DESCRIPTION
|
|||
```
|
||||
|
||||
_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`
|
||||
|
||||
|
|
@ -385,7 +385,7 @@ DESCRIPTION
|
|||
```
|
||||
|
||||
_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]`
|
||||
|
||||
|
|
@ -418,7 +418,7 @@ DESCRIPTION
|
|||
```
|
||||
|
||||
_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`
|
||||
|
||||
|
|
@ -462,7 +462,7 @@ DESCRIPTION
|
|||
```
|
||||
|
||||
_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]`
|
||||
|
||||
|
|
@ -525,7 +525,7 @@ DESCRIPTION
|
|||
```
|
||||
|
||||
_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 -->
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@graphql-hive/cli",
|
||||
"version": "0.59.1",
|
||||
"version": "0.59.2",
|
||||
"description": "A CLI util to manage and control your GraphQL Hive",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -49,7 +49,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@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/graphql-file-loader": "~8.1.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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@graphql-hive/logger": "^1.0.9",
|
||||
"@graphql-hive/logger": "^1.1.0",
|
||||
"@graphql-hive/signal": "^2.0.0",
|
||||
"@graphql-tools/utils": "^10.0.0",
|
||||
"@whatwg-node/fetch": "^0.10.13",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
# @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
|
||||
|
||||
### Patch Changes
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@graphql-hive/laboratory",
|
||||
"version": "0.1.7",
|
||||
"version": "0.1.8",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"main": "./dist/hive-laboratory.cjs.js",
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
ListTreeIcon,
|
||||
RotateCcwIcon,
|
||||
SearchIcon,
|
||||
SettingsIcon,
|
||||
TextAlignStartIcon,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
|
@ -759,7 +760,17 @@ export const Builder = (props: {
|
|||
operationName?: string | null;
|
||||
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 [searchValue, setSearchValue] = useState<string>('');
|
||||
|
|
@ -845,23 +856,45 @@ export const Builder = (props: {
|
|||
|
||||
return (
|
||||
<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>
|
||||
<div className="ml-auto 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 className="ml-auto flex items-center gap-3">
|
||||
{shouldPollSchema && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
const tab =
|
||||
tabs.find(t => t.type === 'settings') ??
|
||||
addTab({
|
||||
type: 'settings',
|
||||
data: {},
|
||||
});
|
||||
|
||||
setActiveTab(tab);
|
||||
}}
|
||||
variant="ghost"
|
||||
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 className="px-3 pt-3">
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { OperationDefinitionNode, parse } from 'graphql';
|
|||
import * as monaco from 'monaco-editor';
|
||||
import { MonacoGraphQLAPI } from 'monaco-graphql/esm/api.js';
|
||||
import { initializeMode } from 'monaco-graphql/initializeMode';
|
||||
import { cn } from '@/lib/utils';
|
||||
import MonacoEditor, { loader } from '@monaco-editor/react';
|
||||
import { useLaboratory } from './context';
|
||||
|
||||
|
|
@ -84,7 +85,7 @@ const darkTheme: monaco.editor.IStandaloneThemeData = {
|
|||
],
|
||||
colors: {
|
||||
'editor.foreground': '#f6f8fa',
|
||||
'editor.background': '#0f1214',
|
||||
'editor.background': '#0f121400',
|
||||
'editor.selectionBackground': '#2A2F34',
|
||||
'editor.inactiveSelectionBackground': '#2A2F34',
|
||||
'editor.lineHighlightBackground': '#2A2F34',
|
||||
|
|
@ -354,10 +355,10 @@ const EditorInner = forwardRef<EditorHandle, EditorProps>((props, ref) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="size-full overflow-hidden">
|
||||
<div className={cn('size-full overflow-hidden', props.className)}>
|
||||
<MonacoEditor
|
||||
className="size-full"
|
||||
{...props}
|
||||
className="size-full"
|
||||
theme={theme === 'dark' ? 'hive-laboratory-dark' : 'hive-laboratory-light'}
|
||||
onMount={handleMount}
|
||||
loading={null}
|
||||
|
|
|
|||
|
|
@ -514,15 +514,10 @@ export const Laboratory = (
|
|||
const pluginsApi = usePlugins(props);
|
||||
const testsApi = useTests(props);
|
||||
const tabsApi = useTabs(props);
|
||||
const endpointApi = useEndpoint({
|
||||
...props,
|
||||
settingsApi,
|
||||
});
|
||||
const collectionsApi = useCollections({
|
||||
...props,
|
||||
tabsApi,
|
||||
});
|
||||
|
||||
const operationsApi = useOperations({
|
||||
...props,
|
||||
collectionsApi,
|
||||
|
|
@ -533,6 +528,14 @@ export const Laboratory = (
|
|||
pluginsApi,
|
||||
checkPermissions,
|
||||
});
|
||||
const endpointApi = useEndpoint({
|
||||
...props,
|
||||
settingsApi,
|
||||
operationsApi,
|
||||
envApi,
|
||||
pluginsApi,
|
||||
preflightApi,
|
||||
});
|
||||
|
||||
const historyApi = useHistory(props);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { useEffect } from 'react';
|
||||
import { z } from 'zod';
|
||||
import { Editor } from '@/components/laboratory/editor';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { useForm } from '@tanstack/react-form';
|
||||
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 { useLaboratory } from './context';
|
||||
|
||||
|
|
@ -20,6 +22,8 @@ const settingsFormSchema = z.object({
|
|||
introspection: z.object({
|
||||
method: z.enum(['GET', 'POST']).optional(),
|
||||
schemaDescription: z.boolean().optional(),
|
||||
headers: z.string().optional(),
|
||||
includeActiveOperationHeaders: z.boolean().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
@ -31,19 +35,17 @@ export const Settings = () => {
|
|||
validators: {
|
||||
onSubmit: settingsFormSchema,
|
||||
},
|
||||
onSubmit: ({ value }) => {
|
||||
setSettings(value as typeof settings);
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
form.store.subscribe(state => {
|
||||
setSettings(state.currentVal.values);
|
||||
});
|
||||
}, [setSettings]);
|
||||
|
||||
return (
|
||||
<div className="bg-card size-full overflow-y-auto p-3">
|
||||
<form
|
||||
id="settings-form"
|
||||
onSubmit={form.handleSubmit}
|
||||
onChange={form.handleSubmit}
|
||||
className="mx-auto flex max-w-2xl flex-col gap-4"
|
||||
>
|
||||
<form id="settings-form" className="mx-auto flex max-w-2xl flex-col gap-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Fetch</CardTitle>
|
||||
|
|
@ -220,6 +222,43 @@ export const Settings = () => {
|
|||
);
|
||||
}}
|
||||
</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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -93,6 +93,10 @@
|
|||
--destructive-foreground: var(--hive-laboratory-destructive-foreground, var(--color-neutral-1));
|
||||
|
||||
--ring: var(--hive-laboratory-ring, var(--color-ring));
|
||||
|
||||
& .monaco-editor {
|
||||
--vscode-focusBorder: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
.hive-laboratory.dark {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,17 @@
|
|||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
buildClientSchema,
|
||||
GraphQLSchema,
|
||||
introspectionFromSchema,
|
||||
type IntrospectionQuery,
|
||||
} from 'graphql';
|
||||
import { debounce } from 'lodash';
|
||||
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 { SubscriptionProtocol, UrlLoader } from '@graphql-tools/url-loader';
|
||||
import type { LaboratorySettingsActions, LaboratorySettingsState } from './settings';
|
||||
|
|
@ -16,6 +21,7 @@ export interface LaboratoryEndpointState {
|
|||
schema: GraphQLSchema | null;
|
||||
introspection: IntrospectionQuery | null;
|
||||
defaultEndpoint: string | null;
|
||||
shouldPollSchema: boolean;
|
||||
}
|
||||
|
||||
export interface LaboratoryEndpointActions {
|
||||
|
|
@ -24,11 +30,17 @@ export interface LaboratoryEndpointActions {
|
|||
restoreDefaultEndpoint: () => void;
|
||||
}
|
||||
|
||||
export const EXPECTED_ERROR_REASON = 'Expected error reason';
|
||||
|
||||
export const useEndpoint = (props: {
|
||||
defaultEndpoint?: string | null;
|
||||
onEndpointChange?: (endpoint: string | null) => void;
|
||||
defaultSchemaIntrospection?: IntrospectionQuery | null;
|
||||
settingsApi?: LaboratorySettingsState & LaboratorySettingsActions;
|
||||
operationsApi?: LaboratoryOperationsState & LaboratoryOperationsActions;
|
||||
envApi?: LaboratoryEnvState & LaboratoryEnvActions;
|
||||
pluginsApi?: LaboratoryPluginsState & LaboratoryPluginsActions;
|
||||
preflightApi?: LaboratoryPreflightState & LaboratoryPreflightActions;
|
||||
}): LaboratoryEndpointState & LaboratoryEndpointActions => {
|
||||
const [endpoint, _setEndpoint] = useState<string | null>(props.defaultEndpoint ?? null);
|
||||
const [introspection, setIntrospection] = useState<IntrospectionQuery | null>(null);
|
||||
|
|
@ -47,75 +59,173 @@ export const useEndpoint = (props: {
|
|||
|
||||
const loader = useMemo(() => new UrlLoader(), []);
|
||||
|
||||
const fetchSchema = useCallback(
|
||||
async (signal?: AbortSignal) => {
|
||||
if (endpoint === props.defaultEndpoint && props.defaultSchemaIntrospection) {
|
||||
setIntrospection(props.defaultSchemaIntrospection);
|
||||
return;
|
||||
}
|
||||
const activeOperationHeadersRef = useRef<string | null | undefined>(
|
||||
props.operationsApi?.activeOperation?.headers,
|
||||
);
|
||||
const envVariablesRef = useRef<LaboratoryEnv['variables'] | undefined>(
|
||||
props.envApi?.env?.variables,
|
||||
);
|
||||
const pluginsStateRef = useRef<Record<string, any> | undefined>(props.pluginsApi?.pluginsState);
|
||||
|
||||
if (!endpoint) {
|
||||
setIntrospection(null);
|
||||
return;
|
||||
}
|
||||
activeOperationHeadersRef.current = props.operationsApi?.activeOperation?.headers;
|
||||
envVariablesRef.current = props.envApi?.env?.variables;
|
||||
pluginsStateRef.current = props.pluginsApi?.pluginsState;
|
||||
|
||||
try {
|
||||
const result = await loader.load(endpoint, {
|
||||
subscriptionsEndpoint: endpoint,
|
||||
subscriptionsProtocol:
|
||||
(props.settingsApi?.settings.subscriptions.protocol as SubscriptionProtocol) ??
|
||||
SubscriptionProtocol.GRAPHQL_SSE,
|
||||
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,
|
||||
}),
|
||||
});
|
||||
const fetchSchema = useMemo(
|
||||
() =>
|
||||
debounce(
|
||||
async (
|
||||
signal?: AbortSignal,
|
||||
options?: {
|
||||
env?: LaboratoryEnv;
|
||||
pluginsState?: Record<string, any>;
|
||||
},
|
||||
) => {
|
||||
if (endpoint === props.defaultEndpoint && props.defaultSchemaIntrospection) {
|
||||
setIntrospection(props.defaultSchemaIntrospection);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.length === 0) {
|
||||
throw new Error('Failed to fetch schema');
|
||||
}
|
||||
if (!endpoint) {
|
||||
setIntrospection(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result[0].schema) {
|
||||
throw new Error('Failed to fetch schema');
|
||||
}
|
||||
try {
|
||||
let env = options?.env?.variables ?? envVariablesRef.current ?? {};
|
||||
let plugins = options?.pluginsState ?? pluginsStateRef.current ?? {};
|
||||
|
||||
setIntrospection(introspectionFromSchema(result[0].schema));
|
||||
} 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');
|
||||
}
|
||||
let sourceHeaders: Record<string, string> = {};
|
||||
|
||||
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,
|
||||
props.settingsApi?.settings.fetch.timeout,
|
||||
props.settingsApi?.settings.introspection.method,
|
||||
props.settingsApi?.settings.introspection.schemaDescription,
|
||||
props.settingsApi?.settings.introspection.headers,
|
||||
props.settingsApi?.settings.introspection.includeActiveOperationHeaders,
|
||||
],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
fetchSchema.cancel();
|
||||
};
|
||||
}, [fetchSchema]);
|
||||
|
||||
const shouldPollSchema = useMemo(() => {
|
||||
return endpoint !== props.defaultEndpoint || !props.defaultSchemaIntrospection;
|
||||
}, [endpoint, props.defaultEndpoint, props.defaultSchemaIntrospection]);
|
||||
|
|
@ -132,7 +242,7 @@ export const useEndpoint = (props: {
|
|||
try {
|
||||
await fetchSchema(intervalController.signal);
|
||||
} catch {
|
||||
intervalController.abort('Polling schema failed');
|
||||
intervalController.abort(new Error('Aborted because of schema polling error'));
|
||||
}
|
||||
},
|
||||
5000,
|
||||
|
|
@ -140,7 +250,7 @@ export const useEndpoint = (props: {
|
|||
);
|
||||
|
||||
return () => {
|
||||
intervalController.abort('Polling schema aborted');
|
||||
intervalController.abort(new Error(EXPECTED_ERROR_REASON));
|
||||
};
|
||||
}, [shouldPollSchema, fetchSchema]);
|
||||
|
||||
|
|
@ -152,10 +262,38 @@ export const useEndpoint = (props: {
|
|||
|
||||
useEffect(() => {
|
||||
if (endpoint && !shouldPollSchema) {
|
||||
void fetchSchema();
|
||||
const abortController = new AbortController();
|
||||
|
||||
void fetchSchema(abortController.signal);
|
||||
|
||||
return () => {
|
||||
abortController.abort(new Error(EXPECTED_ERROR_REASON));
|
||||
};
|
||||
}
|
||||
}, [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 {
|
||||
endpoint,
|
||||
setEndpoint,
|
||||
|
|
@ -164,5 +302,6 @@ export const useEndpoint = (props: {
|
|||
fetchSchema,
|
||||
restoreDefaultEndpoint,
|
||||
defaultEndpoint: props.defaultEndpoint ?? null,
|
||||
shouldPollSchema,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ export type LaboratorySettings = {
|
|||
introspection: {
|
||||
method?: 'GET' | 'POST';
|
||||
schemaDescription?: boolean;
|
||||
headers?: string;
|
||||
includeActiveOperationHeaders?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -29,6 +31,8 @@ export const defaultLaboratorySettings: LaboratorySettings = {
|
|||
introspection: {
|
||||
method: 'POST',
|
||||
schemaDescription: false,
|
||||
headers: '',
|
||||
includeActiveOperationHeaders: false,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -50,6 +54,10 @@ export const normalizeLaboratorySettings = (
|
|||
schemaDescription:
|
||||
settings?.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
|
||||
|
||||
## 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
|
||||
|
||||
### Patch Changes
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@graphql-hive/render-laboratory",
|
||||
"version": "0.1.7",
|
||||
"version": "0.1.8",
|
||||
"type": "module",
|
||||
"description": "",
|
||||
"repository": {
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@graphql-hive/core": "workspace:*",
|
||||
"@graphql-hive/logger": "^1.0.9",
|
||||
"@graphql-hive/logger": "^1.1.0",
|
||||
"@graphql-yoga/plugin-persisted-operations": "^3.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@
|
|||
"./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/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": {
|
||||
"graphql": "^16.0.0",
|
||||
|
|
@ -23,7 +24,7 @@
|
|||
"@date-fns/utc": "2.1.1",
|
||||
"@graphql-hive/core": "workspace:*",
|
||||
"@graphql-hive/signal": "1.0.0",
|
||||
"@graphql-inspector/core": "7.1.2",
|
||||
"@graphql-inspector/core": "7.1.3",
|
||||
"@graphql-tools/merge": "9.1.1",
|
||||
"@hive/cdn-script": "workspace:*",
|
||||
"@hive/postgres": "workspace:*",
|
||||
|
|
|
|||
|
|
@ -124,10 +124,10 @@ export class AppDeploymentsManager {
|
|||
},
|
||||
});
|
||||
|
||||
const target = await this.targetManager.getTargetById(selector);
|
||||
|
||||
return await this.appDeployments.addDocumentsToAppDeployment({
|
||||
organizationId: selector.organizationId,
|
||||
projectId: selector.projectId,
|
||||
targetId: selector.targetId,
|
||||
target,
|
||||
appDeployment: args.appDeployment,
|
||||
operations: args.documents,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import { differenceInCalendarDays, startOfDay, subDays } from 'date-fns';
|
||||
import { Inject, Injectable, Scope } from 'graphql-modules';
|
||||
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 {
|
||||
PostgresDatabasePool,
|
||||
psql,
|
||||
|
|
@ -16,8 +20,10 @@ import {
|
|||
encodeCreatedAtAndUUIDIdBasedCursor,
|
||||
encodeHashBasedCursor,
|
||||
} from '@hive/storage';
|
||||
import type { Target } from '../../../shared/entities';
|
||||
import { ClickHouse, sql as cSql } from '../../operations/providers/clickhouse-client';
|
||||
import { SchemaVersionHelper } from '../../schema/providers/schema-version-helper';
|
||||
import { SchemaVersionStore } from '../../schema/providers/schema-version-store';
|
||||
import { Logger } from '../../shared/providers/logger';
|
||||
import { S3_CONFIG, type S3Config } from '../../shared/providers/s3-config';
|
||||
import { Storage } from '../../shared/providers/storage';
|
||||
|
|
@ -55,6 +61,7 @@ export class AppDeployments {
|
|||
private storage: Storage,
|
||||
private schemaVersionHelper: SchemaVersionHelper,
|
||||
private persistedDocumentScheduler: PersistedDocumentScheduler,
|
||||
private schemaVersions: SchemaVersionStore,
|
||||
@Inject(APP_DEPLOYMENTS_ENABLED) private appDeploymentsEnabled: boolean,
|
||||
) {
|
||||
this.logger = logger.child({ source: 'AppDeployments' });
|
||||
|
|
@ -249,9 +256,7 @@ export class AppDeployments {
|
|||
}
|
||||
|
||||
async addDocumentsToAppDeployment(args: {
|
||||
organizationId: string;
|
||||
projectId: string;
|
||||
targetId: string;
|
||||
target: Target;
|
||||
appDeployment: {
|
||||
name: string;
|
||||
version: string;
|
||||
|
|
@ -263,7 +268,7 @@ export class AppDeployments {
|
|||
}) {
|
||||
if (this.appDeploymentsEnabled === false) {
|
||||
const organization = await this.storage.getOrganization({
|
||||
organizationId: args.organizationId,
|
||||
organizationId: args.target.orgId,
|
||||
});
|
||||
if (organization.featureFlags.appDeployments === false) {
|
||||
this.logger.debug(
|
||||
|
|
@ -283,7 +288,7 @@ export class AppDeployments {
|
|||
// todo: validate input
|
||||
|
||||
const appDeployment = await this.findAppDeployment({
|
||||
targetId: args.targetId,
|
||||
targetId: args.target.id,
|
||||
name: args.appDeployment.name,
|
||||
version: args.appDeployment.version,
|
||||
});
|
||||
|
|
@ -309,9 +314,9 @@ export class AppDeployments {
|
|||
}
|
||||
|
||||
if (args.operations.length !== 0) {
|
||||
const latestSchemaVersion = await this.storage.getMaybeLatestValidVersion({
|
||||
targetId: args.targetId,
|
||||
});
|
||||
const latestSchemaVersion = await this.schemaVersions.getMaybeLatestValidSchemaVersion(
|
||||
args.target,
|
||||
);
|
||||
|
||||
if (latestSchemaVersion === null) {
|
||||
return {
|
||||
|
|
@ -326,9 +331,9 @@ export class AppDeployments {
|
|||
|
||||
const compositeSchemaSdl = await this.schemaVersionHelper.getCompositeSchemaSdl({
|
||||
...latestSchemaVersion,
|
||||
organizationId: args.organizationId,
|
||||
projectId: args.projectId,
|
||||
targetId: args.targetId,
|
||||
organizationId: args.target.orgId,
|
||||
projectId: args.target.projectId,
|
||||
targetId: args.target.id,
|
||||
});
|
||||
if (compositeSchemaSdl === null) {
|
||||
// No valid schema found.
|
||||
|
|
@ -343,7 +348,7 @@ export class AppDeployments {
|
|||
|
||||
const result = await this.persistedDocumentScheduler.processBatch({
|
||||
schemaSdl: compositeSchemaSdl,
|
||||
targetId: args.targetId,
|
||||
targetId: args.target.id,
|
||||
appDeployment: {
|
||||
id: appDeployment.id,
|
||||
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: {
|
||||
organizationId: string;
|
||||
targetId: string;
|
||||
|
|
@ -431,8 +457,11 @@ export class AppDeployments {
|
|||
};
|
||||
}
|
||||
|
||||
const appDeploymentDocumentHashes =
|
||||
await this._getAllDocumentHashesForAppDeployment(appDeployment);
|
||||
|
||||
for (const s3 of this.s3) {
|
||||
const result = await s3.client.fetch(
|
||||
let result = await s3.client.fetch(
|
||||
[
|
||||
s3.endpoint,
|
||||
s3.bucket,
|
||||
|
|
@ -457,6 +486,38 @@ export class AppDeployments {
|
|||
if (result.statusCode !== 200) {
|
||||
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
|
||||
|
|
@ -690,8 +751,11 @@ export class AppDeployments {
|
|||
}
|
||||
}
|
||||
|
||||
const appDeploymentDocumentHashes =
|
||||
await this._getAllDocumentHashesForAppDeployment(appDeployment);
|
||||
|
||||
for (const s3 of this.s3) {
|
||||
const result = await s3.client.fetch(
|
||||
let result = await s3.client.fetch(
|
||||
[
|
||||
s3.endpoint,
|
||||
s3.bucket,
|
||||
|
|
@ -722,6 +786,38 @@ export class AppDeployments {
|
|||
`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({
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ const SharedOIDCIntegrationDomainFieldsModel = z.object({
|
|||
id: z.string().uuid(),
|
||||
organizationId: z.string().uuid(),
|
||||
oidcIntegrationId: z.string().uuid(),
|
||||
domainName: z.string(),
|
||||
domainName: z.string().transform(name => name.toLowerCase()),
|
||||
createdAt: z.string(),
|
||||
});
|
||||
|
||||
|
|
@ -86,7 +86,7 @@ export class OIDCIntegrationStore {
|
|||
) VALUES (
|
||||
${organizationId}
|
||||
, ${oidcIntegrationId}
|
||||
, ${domainName}
|
||||
, ${domainName.toLowerCase()}
|
||||
)
|
||||
ON CONFLICT ("oidc_integration_id", "domain_name")
|
||||
DO NOTHING
|
||||
|
|
@ -131,7 +131,7 @@ export class OIDCIntegrationStore {
|
|||
FROM
|
||||
"oidc_integration_domains"
|
||||
WHERE
|
||||
"domain_name" = ${domainName}
|
||||
"domain_name" = ${domainName.toLowerCase()}
|
||||
AND "verified_at" IS NOT NULL
|
||||
`;
|
||||
|
||||
|
|
@ -149,7 +149,7 @@ export class OIDCIntegrationStore {
|
|||
"oidc_integration_domains"
|
||||
WHERE
|
||||
"oidc_integration_id" = ${oidcIntegrationId}
|
||||
AND "domain_name" = ${domainName}
|
||||
AND "domain_name" = ${domainName.toLowerCase()}
|
||||
AND "verified_at" IS NOT NULL
|
||||
`;
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import * as GraphQLSchema from '../../../__generated__/types';
|
|||
import { Organization, ProjectType } from '../../../shared/entities';
|
||||
import { AccessError } from '../../../shared/errors';
|
||||
import { Session } from '../../auth/lib/authz';
|
||||
import { SchemaVersionStore } from '../../schema/providers/schema-version-store';
|
||||
import { Storage } from '../../shared/providers/storage';
|
||||
|
||||
/**
|
||||
|
|
@ -19,6 +20,7 @@ export class ResourceSelector {
|
|||
constructor(
|
||||
private storage: Storage,
|
||||
private session: Session,
|
||||
private schemaVersions: SchemaVersionStore,
|
||||
) {}
|
||||
|
||||
private async _assertResourceSelectorAdminPermissions(organizationId: string) {
|
||||
|
|
@ -163,7 +165,9 @@ export class ResourceSelector {
|
|||
if (target.type === GraphQLSchema.ProjectType.SINGLE) {
|
||||
return null;
|
||||
}
|
||||
const latest = await this.storage.getMaybeLatestValidVersion({ targetId: target.targetId });
|
||||
const latest = await this.schemaVersions.getMaybeLatestSchemaVersionForTargetId(
|
||||
target.targetId,
|
||||
);
|
||||
if (latest) {
|
||||
return await this.storage.pool
|
||||
.anyFirst(
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { SchemaHelper } from './providers/schema-helper';
|
|||
import { SchemaManager } from './providers/schema-manager';
|
||||
import { SchemaPublisher } from './providers/schema-publisher';
|
||||
import { SchemaVersionHelper } from './providers/schema-version-helper';
|
||||
import { SchemaVersionStore } from './providers/schema-version-store';
|
||||
import { resolvers } from './resolvers.generated';
|
||||
import typeDefs from './module.graphql';
|
||||
|
||||
|
|
@ -21,6 +22,7 @@ export const schemaModule = createModule({
|
|||
typeDefs,
|
||||
resolvers,
|
||||
providers: [
|
||||
SchemaVersionStore,
|
||||
SchemaManager,
|
||||
SchemaPublisher,
|
||||
Inspector,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,5 @@
|
|||
import type { DocumentNode, GraphQLSchema, Kind } from 'graphql';
|
||||
import type {
|
||||
SchemaChangeType,
|
||||
SchemaCheck,
|
||||
SchemaCheckApprovalMetadata,
|
||||
SchemaVersion,
|
||||
} from '@hive/storage';
|
||||
import type { SchemaChangeType, SchemaCheck, SchemaCheckApprovalMetadata } from '@hive/storage';
|
||||
import type { SchemaError } from '../../__generated__/types';
|
||||
import type { DateRange, PushedCompositeSchema, SingleSchema } from '../../shared/entities';
|
||||
import type { PromiseOrValue } from '../../shared/helpers';
|
||||
|
|
@ -16,6 +11,7 @@ import type {
|
|||
PaginatedContractConnection,
|
||||
} from './providers/contracts';
|
||||
import type { SchemaCheckWarning } from './providers/models/shared';
|
||||
import type { SchemaVersion } from './providers/schema-version-store';
|
||||
|
||||
export type SchemaChangeConnectionMapper = ReadonlyArray<SchemaChangeMapper>;
|
||||
export type SchemaChangeMapper = SchemaChangeType;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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 type { Target } from '../../../shared/entities';
|
||||
import { cache } from '../../../shared/helpers';
|
||||
|
|
@ -15,6 +15,7 @@ import {
|
|||
type ContractVersion,
|
||||
type CreateContractInput,
|
||||
} from './contracts';
|
||||
import type { SchemaVersion } from './schema-version-store';
|
||||
|
||||
@Injectable({
|
||||
scope: Scope.Operation,
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ import type {
|
|||
SuccessfulSchemaCheckMapper,
|
||||
} from '../module.graphql.mappers';
|
||||
import { Injectable, Scope } from 'graphql-modules';
|
||||
import { Storage } from '../../shared/providers/storage';
|
||||
import { formatNumber } from '../lib/number-formatting';
|
||||
import { SchemaManager } from './schema-manager';
|
||||
import { SchemaVersionStore } from './schema-version-store';
|
||||
|
||||
type SchemaCheck = FailedSchemaCheckMapper | SuccessfulSchemaCheckMapper;
|
||||
|
||||
|
|
@ -16,7 +16,7 @@ type SchemaCheck = FailedSchemaCheckMapper | SuccessfulSchemaCheckMapper;
|
|||
export class SchemaCheckManager {
|
||||
constructor(
|
||||
private schemaManager: SchemaManager,
|
||||
private storage: Storage,
|
||||
private schemaVersions: SchemaVersionStore,
|
||||
) {}
|
||||
|
||||
getHasSchemaCompositionErrors(schemaCheck: SchemaCheck) {
|
||||
|
|
@ -63,7 +63,7 @@ export class SchemaCheckManager {
|
|||
if (schemaCheck.schemaVersionId === null) {
|
||||
return null;
|
||||
}
|
||||
return this.schemaManager.getSchemaVersion({
|
||||
return this.schemaManager.getSchemaVersionBySelector({
|
||||
organizationId: schemaCheck.selector.organizationId,
|
||||
projectId: schemaCheck.selector.projectId,
|
||||
targetId: schemaCheck.targetId,
|
||||
|
|
@ -76,10 +76,10 @@ export class SchemaCheckManager {
|
|||
return null;
|
||||
}
|
||||
|
||||
const service = await this.storage.getSchemaByNameOfVersion({
|
||||
versionId: schemaCheck.schemaVersionId,
|
||||
serviceName: schemaCheck.serviceName,
|
||||
});
|
||||
const service = await this.schemaVersions.getSchemaForSchemaVersionIdAndName(
|
||||
schemaCheck.schemaVersionId,
|
||||
schemaCheck.serviceName,
|
||||
);
|
||||
|
||||
return service?.sdl ?? null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,9 +43,9 @@ import { TargetManager } from '../../target/providers/target-manager';
|
|||
import { BreakingSchemaChangeUsageHelper } from './breaking-schema-changes-helper';
|
||||
import { SCHEMA_MODULE_CONFIG, type SchemaModuleConfig } from './config';
|
||||
import { Contracts } from './contracts';
|
||||
import type { SchemaCoordinatesDiffResult } from './inspector';
|
||||
import { CompositionOrchestrator } from './orchestrator/composition-orchestrator';
|
||||
import { ensureCompositeSchemas, removeDescriptions, SchemaHelper } from './schema-helper';
|
||||
import { SchemaVersionStore } from './schema-version-store';
|
||||
|
||||
const ENABLE_EXTERNAL_COMPOSITION_SCHEMA = z.object({
|
||||
endpoint: z.string().url().nonempty(),
|
||||
|
|
@ -84,6 +84,7 @@ export class SchemaManager {
|
|||
private contracts: Contracts,
|
||||
private breakingSchemaChangeUsageHelper: BreakingSchemaChangeUsageHelper,
|
||||
private idTranslator: IdTranslator,
|
||||
private schemaVersions: SchemaVersionStore,
|
||||
@Inject(SCHEMA_MODULE_CONFIG) private schemaModuleConfig: SchemaModuleConfig,
|
||||
) {
|
||||
this.logger = logger.child({ source: 'SchemaManager' });
|
||||
|
|
@ -92,7 +93,9 @@ export class SchemaManager {
|
|||
return Promise.all(
|
||||
selectors.map(async selector => {
|
||||
return {
|
||||
...(await this.storage.getLatestValidVersion(selector)),
|
||||
...(await this.schemaVersions.getLatestValidSchemaVersionForTargetId(
|
||||
selector.targetId,
|
||||
)),
|
||||
projectId: selector.projectId,
|
||||
targetId: selector.targetId,
|
||||
organizationId: selector.organizationId,
|
||||
|
|
@ -110,11 +113,7 @@ export class SchemaManager {
|
|||
|
||||
async hasSchema(target: Target) {
|
||||
this.logger.debug('Checking if schema is available (targetId=%s)', target.id);
|
||||
return this.storage.hasSchema({
|
||||
organizationId: target.orgId,
|
||||
projectId: target.projectId,
|
||||
targetId: target.id,
|
||||
});
|
||||
return this.schemaVersions.anyVersionExistsForTarget(target);
|
||||
}
|
||||
|
||||
@traceFn('SchemaManager.compose', {
|
||||
|
|
@ -255,7 +254,7 @@ export class SchemaManager {
|
|||
} & TargetSelector,
|
||||
) {
|
||||
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) {
|
||||
throw new HiveError('No schemas found for this version.');
|
||||
|
|
@ -275,19 +274,17 @@ export class SchemaManager {
|
|||
@atomic(stringifySelector)
|
||||
async getMaybeSchemasOfVersion(schemaVersion: SchemaVersion) {
|
||||
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 }) {
|
||||
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) {
|
||||
this.logger.debug('Fetching maybe latest valid version (targetId=%o)', target.id);
|
||||
const version = await this.storage.getMaybeLatestValidVersion({
|
||||
targetId: target.id,
|
||||
});
|
||||
const version = await this.schemaVersions.getMaybeLatestValidSchemaVersion(target);
|
||||
|
||||
if (!version) {
|
||||
return null;
|
||||
|
|
@ -308,11 +305,7 @@ export class SchemaManager {
|
|||
|
||||
async getMaybeLatestVersion(target: Target) {
|
||||
this.logger.debug('Fetching maybe latest version (targetId=%o)', target.id);
|
||||
const latest = await this.storage.getMaybeLatestVersion({
|
||||
targetId: target.id,
|
||||
projectId: target.projectId,
|
||||
organizationId: target.orgId,
|
||||
});
|
||||
const latest = await this.schemaVersions.getMaybeLatestSchemaVersionForTargetId(target.id);
|
||||
|
||||
if (!latest) {
|
||||
return null;
|
||||
|
|
@ -326,7 +319,7 @@ export class SchemaManager {
|
|||
};
|
||||
}
|
||||
|
||||
async getSchemaVersion(
|
||||
async getSchemaVersionBySelector(
|
||||
selector: TargetSelector & { versionId: string },
|
||||
): Promise<SchemaVersion | null> {
|
||||
this.logger.debug('Fetching single schema version (selector=%o)', selector);
|
||||
|
|
@ -336,17 +329,20 @@ export class SchemaManager {
|
|||
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 {
|
||||
projectId: selector.projectId,
|
||||
targetId: selector.targetId,
|
||||
organizationId: selector.organizationId,
|
||||
...result,
|
||||
...version,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -362,10 +358,7 @@ export class SchemaManager {
|
|||
return null;
|
||||
}
|
||||
|
||||
const schemas = await this.storage.getSchemasOfVersion({
|
||||
versionId: schemaVersion.id,
|
||||
includeMetadata: true,
|
||||
});
|
||||
const schemas = await this.schemaVersions.getSchemasBySchemaVersionId(schemaVersion.id);
|
||||
|
||||
return {
|
||||
version: schemaVersion,
|
||||
|
|
@ -373,14 +366,14 @@ export class SchemaManager {
|
|||
};
|
||||
}
|
||||
|
||||
async getPaginatedSchemaVersionsForTargetId(args: {
|
||||
targetId: string;
|
||||
organizationId: string;
|
||||
projectId: string;
|
||||
first: number | null;
|
||||
cursor: null | string;
|
||||
}) {
|
||||
const connection = await this.storage.getPaginatedSchemaVersionsForTargetId(args);
|
||||
async getPaginatedSchemaVersionsForTargetId(
|
||||
target: Target,
|
||||
args: {
|
||||
first: number | null;
|
||||
cursor: null | string;
|
||||
},
|
||||
) {
|
||||
const connection = await this.schemaVersions.getPaginatedSchemaVersionsForTarget(target, args);
|
||||
|
||||
return {
|
||||
...connection,
|
||||
|
|
@ -388,9 +381,9 @@ export class SchemaManager {
|
|||
...edge,
|
||||
node: {
|
||||
...edge.node,
|
||||
organizationId: args.organizationId,
|
||||
projectId: args.projectId,
|
||||
targetId: args.targetId,
|
||||
organizationId: target.orgId,
|
||||
projectId: target.projectId,
|
||||
targetId: target.id,
|
||||
},
|
||||
})),
|
||||
};
|
||||
|
|
@ -411,7 +404,7 @@ export class SchemaManager {
|
|||
|
||||
async getSchemaLog(selector: { commit: string } & TargetSelector) {
|
||||
this.logger.debug('Fetching schema log (selector=%o)', selector);
|
||||
return this.storage.getSchemaLog({
|
||||
return this.schemaVersions.getSchemLog({
|
||||
commit: selector.commit,
|
||||
targetId: selector.targetId,
|
||||
});
|
||||
|
|
@ -438,10 +431,8 @@ export class SchemaManager {
|
|||
url?: string | null;
|
||||
base_schema: string | null;
|
||||
metadata: string | null;
|
||||
projectType: ProjectType;
|
||||
actionFn(versionId: string): Promise<void>;
|
||||
changes: Array<SchemaChangeType>;
|
||||
coordinatesDiff: SchemaCoordinatesDiffResult | null;
|
||||
previousSchemaVersion: string | null;
|
||||
diffSchemaVersionId: string | null;
|
||||
github: null | {
|
||||
|
|
@ -489,7 +480,6 @@ export class SchemaManager {
|
|||
'service',
|
||||
'logIds',
|
||||
'url',
|
||||
'projectType',
|
||||
'previousSchemaVersion',
|
||||
'diffSchemaVersionId',
|
||||
'github',
|
||||
|
|
@ -497,7 +487,7 @@ export class SchemaManager {
|
|||
]),
|
||||
);
|
||||
|
||||
return this.storage.createVersion({
|
||||
return this.schemaVersions.createSchemaVersion({
|
||||
...input,
|
||||
logIds: input.logIds,
|
||||
});
|
||||
|
|
@ -589,22 +579,14 @@ export class SchemaManager {
|
|||
await this.storage.updateBaseSchema(selector, newBaseSchema);
|
||||
}
|
||||
|
||||
countSchemaVersionsOfProject(
|
||||
selector: ProjectSelector & {
|
||||
period: DateRange | null;
|
||||
},
|
||||
): Promise<number> {
|
||||
this.logger.debug('Fetching schema versions count of project (selector=%o)', selector);
|
||||
return this.storage.countSchemaVersionsOfProject(selector);
|
||||
countSchemaVersionsOfProject(project: Project, period: DateRange | null): Promise<number> {
|
||||
this.logger.debug('Fetching schema versions count of project (projectId=%s)', project.id);
|
||||
return this.schemaVersions.countSchemaVersionsOfProject(project, period);
|
||||
}
|
||||
|
||||
countSchemaVersionsOfTarget(
|
||||
selector: TargetSelector & {
|
||||
period: DateRange | null;
|
||||
},
|
||||
): Promise<number> {
|
||||
this.logger.debug('Fetching schema versions count of target (selector=%o)', selector);
|
||||
return this.storage.countSchemaVersionsOfTarget(selector);
|
||||
countSchemaVersionsOfTarget(target: Target, period: DateRange | null): Promise<number> {
|
||||
this.logger.debug('Fetching schema versions count of target (targetId=%s)', target.id);
|
||||
return this.schemaVersions.countSchemaVersionsOfTarget(target, period);
|
||||
}
|
||||
|
||||
async completeGetStartedCheck(
|
||||
|
|
@ -1084,11 +1066,8 @@ export class SchemaManager {
|
|||
},
|
||||
});
|
||||
|
||||
const record = await this.storage.getSchemaVersionByCommit({
|
||||
projectId: selector.projectId,
|
||||
targetId: selector.targetId,
|
||||
commit: args.commit,
|
||||
});
|
||||
const target = await this.targetManager.getTargetById({ targetId: selector.targetId });
|
||||
const record = await this.schemaVersions.getSchemaVersionForTargetByCommit(target, args.commit);
|
||||
|
||||
if (!record) {
|
||||
return null;
|
||||
|
|
@ -1102,47 +1081,31 @@ export class SchemaManager {
|
|||
};
|
||||
}
|
||||
|
||||
async getComposableVersionBeforeVersionId(args: {
|
||||
organization: string;
|
||||
project: string;
|
||||
target: string;
|
||||
beforeVersionId: string;
|
||||
beforeVersionCreatedAt: string;
|
||||
}) {
|
||||
this.logger.debug('Fetch version before version id. (args=%o)', args);
|
||||
async getComposableVersionBeforeVersionId(schemaVersion: SchemaVersion) {
|
||||
this.logger.debug('Fetch version before version id. (schemaVersionId=%s)', schemaVersion.id);
|
||||
|
||||
const schemaVersion = await this.storage.getVersionBeforeVersionId({
|
||||
targetId: args.target,
|
||||
beforeVersionId: args.beforeVersionId,
|
||||
beforeVersionCreatedAt: args.beforeVersionCreatedAt,
|
||||
onlyComposable: true,
|
||||
});
|
||||
const previousSchemaVersion = await this.schemaVersions.getSchemaVersionBeforeSchemaVersion(
|
||||
schemaVersion,
|
||||
true,
|
||||
);
|
||||
|
||||
if (!schemaVersion) {
|
||||
if (!previousSchemaVersion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...schemaVersion,
|
||||
organizationId: args.organization,
|
||||
projectId: args.project,
|
||||
targetId: args.target,
|
||||
...previousSchemaVersion,
|
||||
organizationId: schemaVersion.organizationId,
|
||||
projectId: schemaVersion.projectId,
|
||||
targetId: schemaVersion.targetId,
|
||||
};
|
||||
}
|
||||
|
||||
async getFirstComposableSchemaVersionBeforeVersionId(args: {
|
||||
organization: string;
|
||||
project: string;
|
||||
target: string;
|
||||
beforeVersionId: string;
|
||||
beforeVersionCreatedAt: string;
|
||||
}) {
|
||||
const schemaVersion = await this.storage.getVersionBeforeVersionId({
|
||||
targetId: args.target,
|
||||
beforeVersionId: args.beforeVersionId,
|
||||
beforeVersionCreatedAt: args.beforeVersionCreatedAt,
|
||||
onlyComposable: true,
|
||||
});
|
||||
async getFirstComposableSchemaVersionBeforeSchemaVersion(previousSchemaVersion: SchemaVersion) {
|
||||
const schemaVersion = await this.schemaVersions.getSchemaVersionBeforeSchemaVersion(
|
||||
previousSchemaVersion,
|
||||
true,
|
||||
);
|
||||
|
||||
if (!schemaVersion) {
|
||||
return null;
|
||||
|
|
@ -1150,9 +1113,9 @@ export class SchemaManager {
|
|||
|
||||
return {
|
||||
...schemaVersion,
|
||||
organizationId: args.organization,
|
||||
projectId: args.project,
|
||||
targetId: args.target,
|
||||
organizationId: previousSchemaVersion.organizationId,
|
||||
projectId: previousSchemaVersion.projectId,
|
||||
targetId: previousSchemaVersion.targetId,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ import {
|
|||
} from './schema-helper';
|
||||
import { SchemaManager } from './schema-manager';
|
||||
import { SchemaVersionHelper } from './schema-version-helper';
|
||||
import { SchemaVersionStore } from './schema-version-store';
|
||||
|
||||
const schemaCheckCount = new promClient.Counter({
|
||||
name: 'registry_check_count',
|
||||
|
|
@ -165,6 +166,7 @@ export class SchemaPublisher {
|
|||
private schemaVersionHelper: SchemaVersionHelper,
|
||||
private operationsReader: OperationsReader,
|
||||
private idTranslator: IdTranslator,
|
||||
private schemaVersions: SchemaVersionStore,
|
||||
@Inject(SCHEMA_MODULE_CONFIG) private schemaModuleConfig: SchemaModuleConfig,
|
||||
singleModel: SingleModel,
|
||||
compositeModel: CompositeModel,
|
||||
|
|
@ -1493,15 +1495,11 @@ export class SchemaPublisher {
|
|||
if (deleteResult.conclusion === SchemaDeleteConclusion.Accept) {
|
||||
this.logger.debug('Delete accepted');
|
||||
if (input.dryRun !== true) {
|
||||
const schemaVersion = await this.storage.deleteSchema({
|
||||
organizationId: selector.organizationId,
|
||||
projectId: selector.projectId,
|
||||
targetId: selector.targetId,
|
||||
const schemaVersion = await this.schemaVersions.deleteSubgraphFromTarget(target, {
|
||||
serviceName: input.serviceName,
|
||||
composable: deleteResult.state.composable,
|
||||
diffSchemaVersionId: latestComposableVersion?.version.id ?? null,
|
||||
changes: deleteResult.state.changes,
|
||||
coordinatesDiff: deleteResult.state.coordinatesDiff,
|
||||
contracts:
|
||||
deleteResult.state.contracts?.map(contract => ({
|
||||
contractId: contract.contractId,
|
||||
|
|
@ -2027,7 +2025,6 @@ export class SchemaPublisher {
|
|||
url: serviceUrl,
|
||||
base_schema: baseSchema,
|
||||
metadata: input.metadata ?? null,
|
||||
projectType: project.type,
|
||||
github,
|
||||
actionFn: async (versionId: string) => {
|
||||
if (composable && fullSchemaSdl) {
|
||||
|
|
@ -2054,7 +2051,6 @@ export class SchemaPublisher {
|
|||
}
|
||||
},
|
||||
changes,
|
||||
coordinatesDiff: publishResult.state.coordinatesDiff,
|
||||
diffSchemaVersionId: latestComposable?.version.id ?? null,
|
||||
previousSchemaVersion: latestVersion?.version.id ?? null,
|
||||
conditionalBreakingChangeMetadata: await this.getConditionalBreakingChangeMetadata({
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { CompositionOrchestrator } from './orchestrator/composition-orchestrator
|
|||
import { RegistryChecks } from './registry-checks';
|
||||
import { ensureCompositeSchemas, SchemaHelper, toCompositeSchemaInput } from './schema-helper';
|
||||
import { SchemaManager } from './schema-manager';
|
||||
import { SchemaVersionStore } from './schema-version-store';
|
||||
|
||||
@Injectable({
|
||||
scope: Scope.Operation,
|
||||
|
|
@ -38,6 +39,7 @@ export class SchemaVersionHelper {
|
|||
private storage: Storage,
|
||||
private logger: Logger,
|
||||
private compositionOrchestrator: CompositionOrchestrator,
|
||||
private schemaVersions: SchemaVersionStore,
|
||||
) {}
|
||||
|
||||
@traceFn('SchemaVersionHelper.composeSchemaVersion', {
|
||||
|
|
@ -51,9 +53,7 @@ export class SchemaVersionHelper {
|
|||
@cache<SchemaVersion>(version => version.id)
|
||||
private async composeSchemaVersion(schemaVersion: SchemaVersion) {
|
||||
const [schemas, project, organization] = await Promise.all([
|
||||
this.storage.getSchemasOfVersion({
|
||||
versionId: schemaVersion.id,
|
||||
}),
|
||||
this.schemaVersions.getSchemasBySchemaVersionId(schemaVersion.id),
|
||||
this.projectManager.getProject({
|
||||
organizationId: schemaVersion.organizationId,
|
||||
projectId: schemaVersion.projectId,
|
||||
|
|
@ -186,11 +186,8 @@ export class SchemaVersionHelper {
|
|||
}
|
||||
|
||||
if (schemaVersion.hasPersistedSchemaChanges) {
|
||||
const changes: null | Array<SchemaChangeType> = await this.storage.getSchemaChangesForVersion(
|
||||
{
|
||||
versionId: schemaVersion.id,
|
||||
},
|
||||
);
|
||||
const changes: null | Array<SchemaChangeType> =
|
||||
await this.schemaVersions.getSchemaSchangesForSchemaVersion(schemaVersion);
|
||||
|
||||
const safeChanges: Array<SchemaChangeType> = [];
|
||||
const breakingChanges: Array<SchemaChangeType> = [];
|
||||
|
|
@ -220,12 +217,8 @@ export class SchemaVersionHelper {
|
|||
const incomingSdl = await this.getCompositeSchemaSdl(schemaVersion);
|
||||
|
||||
const [schemaBefore, schemasAfter] = await Promise.all([
|
||||
this.storage.getSchemasOfVersion({
|
||||
versionId: schemaVersion.id,
|
||||
}),
|
||||
this.storage.getSchemasOfVersion({
|
||||
versionId: previousVersion.id,
|
||||
}),
|
||||
this.schemaVersions.getSchemasBySchemaVersionId(schemaVersion.id),
|
||||
this.schemaVersions.getSchemasBySchemaVersionId(previousVersion.id),
|
||||
]);
|
||||
|
||||
if (!existingSdl || !incomingSdl) {
|
||||
|
|
@ -271,7 +264,7 @@ export class SchemaVersionHelper {
|
|||
): Promise<SchemaVersion | null> {
|
||||
if (schemaVersion.recordVersion === '2024-01-10') {
|
||||
if (schemaVersion.diffSchemaVersionId) {
|
||||
return await this.schemaManager.getSchemaVersion({
|
||||
return await this.schemaManager.getSchemaVersionBySelector({
|
||||
organizationId: schemaVersion.organizationId,
|
||||
projectId: schemaVersion.projectId,
|
||||
targetId: schemaVersion.targetId,
|
||||
|
|
@ -281,13 +274,7 @@ export class SchemaVersionHelper {
|
|||
return null;
|
||||
}
|
||||
|
||||
return await this.schemaManager.getComposableVersionBeforeVersionId({
|
||||
organization: schemaVersion.organizationId,
|
||||
project: schemaVersion.projectId,
|
||||
target: schemaVersion.targetId,
|
||||
beforeVersionId: schemaVersion.id,
|
||||
beforeVersionCreatedAt: schemaVersion.createdAt,
|
||||
});
|
||||
return await this.schemaManager.getComposableVersionBeforeVersionId(schemaVersion);
|
||||
}
|
||||
|
||||
async getBreakingSchemaChanges(schemaVersion: SchemaVersion) {
|
||||
|
|
@ -327,13 +314,7 @@ export class SchemaVersionHelper {
|
|||
}
|
||||
|
||||
const composableVersion =
|
||||
await this.schemaManager.getFirstComposableSchemaVersionBeforeVersionId({
|
||||
organization: schemaVersion.organizationId,
|
||||
project: schemaVersion.projectId,
|
||||
target: schemaVersion.targetId,
|
||||
beforeVersionId: schemaVersion.id,
|
||||
beforeVersionCreatedAt: schemaVersion.createdAt,
|
||||
});
|
||||
await this.schemaManager.getFirstComposableSchemaVersionBeforeSchemaVersion(schemaVersion);
|
||||
|
||||
return !composableVersion;
|
||||
}
|
||||
|
|
@ -353,10 +334,10 @@ export class SchemaVersionHelper {
|
|||
return null;
|
||||
}
|
||||
|
||||
const schemaLog = await this.storage.getServiceSchemaOfVersion({
|
||||
schemaVersionId: previousVersion.id,
|
||||
const schemaLog = await this.schemaVersions.getServiceSchemaOfVersion(
|
||||
schemaVersion,
|
||||
serviceName,
|
||||
});
|
||||
);
|
||||
|
||||
return schemaLog?.sdl ?? null;
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -19,11 +19,9 @@ export const Project: Pick<
|
|||
return null;
|
||||
},
|
||||
schemaVersionsCount: (project, { period }, { injector }) => {
|
||||
return injector.get(SchemaManager).countSchemaVersionsOfProject({
|
||||
organizationId: project.orgId,
|
||||
projectId: project.id,
|
||||
period: period ? parseDateRangeInput(period) : null,
|
||||
});
|
||||
return injector
|
||||
.get(SchemaManager)
|
||||
.countSchemaVersionsOfProject(project, period ? parseDateRangeInput(period) : null);
|
||||
},
|
||||
isNativeFederationEnabled: project => {
|
||||
return project.nativeFederation === true;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { parseDateRangeInput } from '../../../shared/helpers';
|
|||
import { OperationsManager } from '../../operations/providers/operations-manager';
|
||||
import { ContractsManager } from '../providers/contracts-manager';
|
||||
import { SchemaManager } from '../providers/schema-manager';
|
||||
import { SchemaVersionStore } from '../providers/schema-version-store';
|
||||
import { toGraphQLSchemaCheck, toGraphQLSchemaCheckCurry } from '../to-graphql-schema-check';
|
||||
import type { TargetResolvers } from './../../../__generated__/types';
|
||||
|
||||
|
|
@ -21,32 +22,18 @@ export const Target: Pick<
|
|||
| 'schemaVersionsCount'
|
||||
> = {
|
||||
schemaVersions: async (target, args, { injector }) => {
|
||||
return injector.get(SchemaManager).getPaginatedSchemaVersionsForTargetId({
|
||||
targetId: target.id,
|
||||
organizationId: target.orgId,
|
||||
projectId: target.projectId,
|
||||
return injector.get(SchemaManager).getPaginatedSchemaVersionsForTargetId(target, {
|
||||
cursor: args.after ?? null,
|
||||
first: args.first ?? null,
|
||||
});
|
||||
},
|
||||
schemaVersion: async (target, args, { injector }) => {
|
||||
const schemaVersion = await injector.get(SchemaManager).getSchemaVersion({
|
||||
return await injector.get(SchemaManager).getSchemaVersionBySelector({
|
||||
organizationId: target.orgId,
|
||||
projectId: target.projectId,
|
||||
targetId: target.id,
|
||||
versionId: args.id,
|
||||
});
|
||||
|
||||
if (schemaVersion === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...schemaVersion,
|
||||
organizationId: target.orgId,
|
||||
projectId: target.projectId,
|
||||
targetId: target.id,
|
||||
};
|
||||
},
|
||||
latestSchemaVersion: (target, _, { injector }) => {
|
||||
return injector.get(SchemaManager).getMaybeLatestVersion(target);
|
||||
|
|
@ -58,7 +45,7 @@ export const Target: Pick<
|
|||
return injector.get(SchemaManager).getBaseSchemaForTarget(target);
|
||||
},
|
||||
hasSchema: (target, _, { injector }) => {
|
||||
return injector.get(SchemaManager).hasSchema(target);
|
||||
return injector.get(SchemaVersionStore).anyVersionExistsForTarget(target);
|
||||
},
|
||||
schemaCheck: async (target, args, { injector }) => {
|
||||
const schemaCheck = await injector.get(SchemaManager).findSchemaCheckForTarget(target, args.id);
|
||||
|
|
@ -92,12 +79,9 @@ export const Target: Pick<
|
|||
};
|
||||
},
|
||||
schemaVersionsCount: (target, { period }, { injector }) => {
|
||||
return injector.get(SchemaManager).countSchemaVersionsOfTarget({
|
||||
organizationId: target.orgId,
|
||||
projectId: target.projectId,
|
||||
targetId: target.id,
|
||||
period: period ? parseDateRangeInput(period) : null,
|
||||
});
|
||||
return injector
|
||||
.get(SchemaManager)
|
||||
.countSchemaVersionsOfTarget(target, period ? parseDateRangeInput(period) : null);
|
||||
},
|
||||
contracts: async (target, args, { injector }) => {
|
||||
return await injector.get(ContractsManager).getPaginatedContractsForTarget({
|
||||
|
|
|
|||
|
|
@ -2,14 +2,10 @@ import { Injectable } from 'graphql-modules';
|
|||
import type { PolicyConfigurationObject } from '@hive/policy';
|
||||
import { PostgresDatabasePool } from '@hive/postgres';
|
||||
import type {
|
||||
ConditionalBreakingChangeMetadata,
|
||||
PaginatedOrganizationInvitationConnection,
|
||||
PaginatedSchemaVersionConnection,
|
||||
SchemaChangeType,
|
||||
SchemaCheck,
|
||||
SchemaCheckInput,
|
||||
SchemaCompositionError,
|
||||
SchemaVersion,
|
||||
TargetBreadcrumb,
|
||||
} from '@hive/storage';
|
||||
import type { SchemaChecksFilter } from '../../../__generated__/types';
|
||||
|
|
@ -17,7 +13,6 @@ import type {
|
|||
Alert,
|
||||
AlertChannel,
|
||||
CDNAccessToken,
|
||||
DeletedCompositeSchema,
|
||||
DocumentCollection,
|
||||
DocumentCollectionOperation,
|
||||
Member,
|
||||
|
|
@ -28,8 +23,6 @@ import type {
|
|||
PaginatedDocumentCollectionOperations,
|
||||
PaginatedDocumentCollections,
|
||||
Project,
|
||||
Schema,
|
||||
SchemaLog,
|
||||
SchemaPolicy,
|
||||
Target,
|
||||
TargetSettings,
|
||||
|
|
@ -42,7 +35,6 @@ import type {
|
|||
} from '../../auth/providers/scopes';
|
||||
import type { ResourceAssignmentGroup } from '../../organization/lib/resource-assignment-model';
|
||||
import type { Contracts } from '../../schema/providers/contracts';
|
||||
import type { SchemaCoordinatesDiffResult } from '../../schema/providers/inspector';
|
||||
|
||||
export interface OrganizationSelector {
|
||||
organizationId: string;
|
||||
|
|
@ -56,15 +48,6 @@ export interface TargetSelector extends ProjectSelector {
|
|||
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
|
||||
export interface Storage {
|
||||
pool: PostgresDatabasePool;
|
||||
|
|
@ -351,69 +334,6 @@ export interface Storage {
|
|||
_: Pick<TargetSelector, 'targetId' | 'projectId'> &
|
||||
Partial<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<
|
||||
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>;
|
||||
|
||||
deleteSlackIntegration(_: OrganizationSelector): Promise<void>;
|
||||
|
|
@ -881,4 +711,3 @@ export interface Storage {
|
|||
@Injectable()
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
|
||||
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 type { AvailableRulesResponse, PolicyConfigurationObject } from '@hive/policy';
|
||||
import type { CompositionFailureError } from '@hive/schema';
|
||||
import type {
|
||||
CompositeDeletedSchemaLog,
|
||||
CompositePushSchemaLog,
|
||||
schema_policy_resource,
|
||||
SinglePushSchemaLog,
|
||||
} from '@hive/storage';
|
||||
import type { schema_policy_resource } from '@hive/storage';
|
||||
import type {
|
||||
AlertChannelType,
|
||||
AlertType,
|
||||
|
|
@ -18,6 +13,11 @@ import type {
|
|||
TargetAccessScope,
|
||||
} from '../__generated__/types';
|
||||
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';
|
||||
|
||||
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": {
|
||||
"@cloudflare/workers-types": "4.20250913.0",
|
||||
"@hive/service-common": "workspace:*",
|
||||
"@types/service-worker-mock": "2.0.4",
|
||||
"@whatwg-node/server": "0.10.17",
|
||||
"esbuild": "0.25.9",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { createServer } from 'http';
|
||||
import { Router } from 'itty-router';
|
||||
import { resolveServerListenOptions } from '@hive/service-common/listen-options';
|
||||
import { createServerAdapter } from '@whatwg-node/server';
|
||||
import { createSignatureValidator } from './auth';
|
||||
import { env } from './dev-polyfill';
|
||||
|
|
@ -7,6 +8,12 @@ import { handleRequest } from './handler';
|
|||
|
||||
// eslint-disable-next-line no-process-env
|
||||
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);
|
||||
|
||||
function main() {
|
||||
|
|
@ -32,7 +39,14 @@ function main() {
|
|||
const server = createServer(app);
|
||||
|
||||
return new Promise<void>(resolve => {
|
||||
server.listen(PORT, '::', resolve);
|
||||
server.listen(
|
||||
{
|
||||
port: PORT,
|
||||
host: listenOptions.host,
|
||||
ipv6Only: listenOptions.ipv6Only,
|
||||
},
|
||||
resolve,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
S3_ENDPOINT=http://localhost:9000
|
||||
S3_ACCESS_KEY_ID="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": {
|
||||
"@cloudflare/workers-types": "4.20250913.0",
|
||||
"@hive/service-common": "workspace:*",
|
||||
"@types/service-worker-mock": "2.0.4",
|
||||
"@whatwg-node/server": "0.10.17",
|
||||
"bcryptjs": "2.4.3",
|
||||
|
|
|
|||
|
|
@ -61,7 +61,8 @@ type Event =
|
|||
| 'GET cdn-legacy-keys'
|
||||
| 'GET cdn-access-token'
|
||||
| 'GET persistedOperation'
|
||||
| 'HEAD appDeploymentIsEnabled';
|
||||
| 'HEAD appDeploymentIsEnabled'
|
||||
| 'GET appDeploymentManifest';
|
||||
// Either 3 digit status code or error code e.g. timeout, http error etc.
|
||||
statusCodeOrErrCode: number | string;
|
||||
/** duration in milliseconds */
|
||||
|
|
|
|||
|
|
@ -69,6 +69,12 @@ const VersionedParamsModel = zod.object({
|
|||
.transform(value => value ?? null),
|
||||
});
|
||||
|
||||
export const AppDeploymentsManifestParamsModel = zod.object({
|
||||
targetId: zod.string(),
|
||||
appName: zod.string(),
|
||||
appVersion: zod.string(),
|
||||
});
|
||||
|
||||
const PersistedOperationParamsModel = zod.object({
|
||||
targetId: zod.string(),
|
||||
appName: zod.string(),
|
||||
|
|
@ -461,9 +467,6 @@ export const createArtifactRequestHandler = (deps: ArtifactRequestHandler) => {
|
|||
const response = createResponse(
|
||||
analytics,
|
||||
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 },
|
||||
params.targetId,
|
||||
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 router.handle(request, captureException);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,14 @@ import type { Analytics } from './analytics';
|
|||
import { AwsClient } from './aws';
|
||||
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(
|
||||
targetId: string,
|
||||
artifactType: string,
|
||||
|
|
@ -50,6 +58,7 @@ const AppDeploymentIsEnabledKeyModel = zod.tuple([
|
|||
/**
|
||||
* 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.
|
||||
*
|
||||
**/
|
||||
export function buildAppDeploymentIsEnabledKey(
|
||||
...args: [targetId: string, appName: string, appVersion: string]
|
||||
|
|
@ -57,6 +66,12 @@ export function buildAppDeploymentIsEnabledKey(
|
|||
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.
|
||||
*/
|
||||
|
|
@ -353,6 +368,49 @@ export class ArtifactStorageReader {
|
|||
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(
|
||||
targetId: string,
|
||||
appName: string,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { createServer } from 'http';
|
||||
import * as itty from 'itty-router';
|
||||
import { resolveServerListenOptions } from '@hive/service-common/listen-options';
|
||||
import { createServerAdapter } from '@whatwg-node/server';
|
||||
import { createArtifactRequestHandler } from './artifact-handler';
|
||||
import { ArtifactStorageReader } from './artifact-storage-reader';
|
||||
|
|
@ -22,6 +23,12 @@ const s3 = {
|
|||
|
||||
// eslint-disable-next-line no-process-env
|
||||
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);
|
||||
|
||||
|
|
@ -93,7 +100,14 @@ function main() {
|
|||
const server = createServer(app);
|
||||
|
||||
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
|
||||
SERVER_HOST=::
|
||||
SERVER_HOST_IPV6_ONLY=0
|
||||
OPENTELEMETRY_COLLECTOR_ENDPOINT="<sync>"
|
||||
CLICKHOUSE_PROTOCOL="http"
|
||||
CLICKHOUSE_HOST="localhost"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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;
|
||||
|
||||
|
|
@ -22,6 +22,8 @@ export const emptyString = <T extends zod.ZodType>(input: T) => {
|
|||
|
||||
const EnvironmentModel = zod.object({
|
||||
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()),
|
||||
RELEASE: emptyString(zod.string().optional()),
|
||||
});
|
||||
|
|
@ -141,6 +143,10 @@ export const env = {
|
|||
release: base.RELEASE ?? 'local',
|
||||
http: {
|
||||
port: base.PORT ?? 4012,
|
||||
...resolveServerListenOptions({
|
||||
serverHost: base.SERVER_HOST,
|
||||
serverHostIpv6Only: base.SERVER_HOST_IPV6_ONLY,
|
||||
}),
|
||||
},
|
||||
tracing: {
|
||||
enabled: !!tracing.OPENTELEMETRY_COLLECTOR_ENDPOINT,
|
||||
|
|
|
|||
|
|
@ -137,11 +137,16 @@ async function main() {
|
|||
});
|
||||
|
||||
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({
|
||||
port: env.http.port,
|
||||
host: '::',
|
||||
host: env.http.host,
|
||||
ipv6Only: env.http.ipv6Only,
|
||||
});
|
||||
await Promise.all([usageEstimator.start(), rateLimiter.start(), stripeBilling.start()]);
|
||||
} 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/federation-internals": "2.13.2",
|
||||
"@graphql-hive/external-composition": "workspace:*",
|
||||
"@hive/service-common": "workspace:*",
|
||||
"@whatwg-node/server": "0.10.17",
|
||||
"dotenv": "16.4.7",
|
||||
"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 { resolveServerListenOptions } from '@hive/service-common';
|
||||
|
||||
function extractConfig<Input, Output>(config: zod.SafeParseReturnType<Input, Output>): Output {
|
||||
if (!config.success) {
|
||||
|
|
@ -15,6 +16,8 @@ const BaseSchema = zod.object({
|
|||
.number()
|
||||
.transform(port => port || 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(),
|
||||
});
|
||||
|
||||
|
|
@ -44,6 +47,10 @@ export function resolveEnv(env: Record<string, string | undefined>) {
|
|||
release: base.RELEASE ?? 'local',
|
||||
http: {
|
||||
port: base.PORT,
|
||||
...resolveServerListenOptions({
|
||||
serverHost: base.SERVER_HOST,
|
||||
serverHostIpv6Only: base.SERVER_HOST_IPV6_ONLY,
|
||||
}),
|
||||
},
|
||||
secret: base.SECRET,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,9 +7,20 @@ import { createRequestListener } from './server';
|
|||
const env = resolveEnv(process.env);
|
||||
const server = createServer(createRequestListener(env));
|
||||
|
||||
server.listen(env.http.port, '::', () => {
|
||||
console.log(`Listening on http://localhost:${env.http.port}`);
|
||||
});
|
||||
function formatListenAddress(host: string, port: number) {
|
||||
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', () => {
|
||||
server.close(err => {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
ENVIRONMENT=development
|
||||
LOG_LEVEL=debug
|
||||
SERVER_HOST=::
|
||||
SERVER_HOST_IPV6_ONLY=0
|
||||
OPENTELEMETRY_COLLECTOR_ENDPOINT="<sync>"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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;
|
||||
|
||||
|
|
@ -20,6 +20,8 @@ const emptyString = <T extends zod.ZodType>(input: T) => {
|
|||
|
||||
const EnvironmentModel = zod.object({
|
||||
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()),
|
||||
RELEASE: emptyString(zod.string().optional()),
|
||||
});
|
||||
|
|
@ -107,6 +109,10 @@ export const env = {
|
|||
},
|
||||
http: {
|
||||
port: base.PORT ?? 6600,
|
||||
...resolveServerListenOptions({
|
||||
serverHost: base.SERVER_HOST,
|
||||
serverHostIpv6Only: base.SERVER_HOST_IPV6_ONLY,
|
||||
}),
|
||||
},
|
||||
sentry: sentry.SENTRY === '1' ? { dsn: sentry.SENTRY_DSN } : null,
|
||||
log: {
|
||||
|
|
|
|||
|
|
@ -83,11 +83,16 @@ async function main() {
|
|||
|
||||
await server.listen({
|
||||
port: env.http.port,
|
||||
host: '::',
|
||||
host: env.http.host,
|
||||
ipv6Only: env.http.ipv6Only,
|
||||
});
|
||||
|
||||
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) {
|
||||
server.log.fatal(error);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
REDIS_HOST="localhost"
|
||||
REDIS_PORT="6379"
|
||||
REDIS_PASSWORD=""
|
||||
SERVER_HOST=::
|
||||
SERVER_HOST_IPV6_ONLY=0
|
||||
ENCRYPTION_SECRET="97e4094d2463e71a981913cca4e56788"
|
||||
SCHEMA_CACHE_TTL_MS=5000
|
||||
SCHEMA_CACHE_SUCCESS_TTL_MS=5000
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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;
|
||||
|
||||
|
|
@ -21,6 +21,8 @@ const emptyString = <T extends zod.ZodType>(input: T) => {
|
|||
|
||||
const EnvironmentModel = zod.object({
|
||||
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),
|
||||
ENVIRONMENT: emptyString(zod.string().optional()),
|
||||
RELEASE: emptyString(zod.string().optional()),
|
||||
|
|
@ -144,6 +146,10 @@ export const env = {
|
|||
encryptionSecret: base.ENCRYPTION_SECRET,
|
||||
http: {
|
||||
port: base.PORT ?? 6500,
|
||||
...resolveServerListenOptions({
|
||||
serverHost: base.SERVER_HOST,
|
||||
serverHostIpv6Only: base.SERVER_HOST_IPV6_ONLY,
|
||||
}),
|
||||
bodyLimit: base.BODY_LIMIT,
|
||||
},
|
||||
tracing: {
|
||||
|
|
|
|||
|
|
@ -146,11 +146,16 @@ async function main() {
|
|||
|
||||
await server.listen({
|
||||
port: env.http.port,
|
||||
host: '::',
|
||||
host: env.http.host,
|
||||
ipv6Only: env.http.ipv6Only,
|
||||
});
|
||||
|
||||
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) {
|
||||
server.log.fatal(error);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
ENVIRONMENT=development
|
||||
LOG_LEVEL=debug
|
||||
SERVER_HOST=::
|
||||
SERVER_HOST_IPV6_ONLY=0
|
||||
POSTGRES_USER=postgres
|
||||
POSTGRES_PASSWORD=postgres
|
||||
POSTGRES_HOST=localhost
|
||||
|
|
|
|||
|
|
@ -13,9 +13,8 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@envelop/core": "5.5.1",
|
||||
"@envelop/graphql-jit": "8.0.3",
|
||||
"@envelop/graphql-modules": "9.1.0",
|
||||
"@envelop/opentelemetry": "6.3.1",
|
||||
"@envelop/graphql-jit": "11.1.1",
|
||||
"@envelop/graphql-modules": "9.1.1",
|
||||
"@envelop/types": "5.0.0",
|
||||
"@escape.tech/graphql-armor-max-aliases": "2.6.2",
|
||||
"@escape.tech/graphql-armor-max-depth": "2.4.2",
|
||||
|
|
@ -24,7 +23,7 @@
|
|||
"@fastify/cookie": "11.0.2",
|
||||
"@fastify/cors": "11.2.0",
|
||||
"@fastify/formbody": "8.0.2",
|
||||
"@graphql-hive/plugin-opentelemetry": "1.3.0",
|
||||
"@graphql-hive/plugin-opentelemetry": "1.4.26",
|
||||
"@graphql-hive/yoga": "workspace:*",
|
||||
"@graphql-tools/merge": "9.1.1",
|
||||
"@graphql-yoga/plugin-response-cache": "3.15.4",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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;
|
||||
|
||||
|
|
@ -20,6 +20,8 @@ const emptyString = <T extends zod.ZodType>(input: T) => {
|
|||
|
||||
const EnvironmentModel = zod.object({
|
||||
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()),
|
||||
RELEASE: emptyString(zod.string().optional()),
|
||||
ENCRYPTION_SECRET: emptyString(zod.string()),
|
||||
|
|
@ -417,6 +419,10 @@ export const env = {
|
|||
},
|
||||
http: {
|
||||
port: base.PORT ?? 3001,
|
||||
...resolveServerListenOptions({
|
||||
serverHost: base.SERVER_HOST,
|
||||
serverHostIpv6Only: base.SERVER_HOST_IPV6_ONLY,
|
||||
}),
|
||||
},
|
||||
postgres: {
|
||||
host: postgres.POSTGRES_HOST,
|
||||
|
|
|
|||
|
|
@ -610,12 +610,17 @@ export async function main() {
|
|||
}
|
||||
|
||||
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({
|
||||
port: env.http.port,
|
||||
host: '::',
|
||||
host: env.http.host,
|
||||
ipv6Only: env.http.ipv6Only,
|
||||
});
|
||||
} catch (error) {
|
||||
server.log.fatal(error);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@
|
|||
"license": "MIT",
|
||||
"private": true,
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
".": "./src/index.ts",
|
||||
"./listen-options": "./src/listen-options.ts"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@sentry/node": "^7.0.0",
|
||||
|
|
@ -13,18 +14,18 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@fastify/cors": "11.2.0",
|
||||
"@graphql-hive/logger": "1.0.9",
|
||||
"@graphql-hive/plugin-opentelemetry": "1.3.0",
|
||||
"@opentelemetry/api": "1.9.0",
|
||||
"@opentelemetry/auto-instrumentations-node": "0.67.2",
|
||||
"@opentelemetry/context-async-hooks": "2.2.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "0.208.0",
|
||||
"@opentelemetry/instrumentation": "0.208.0",
|
||||
"@opentelemetry/resources": "2.2.0",
|
||||
"@opentelemetry/sdk-node": "0.208.0",
|
||||
"@opentelemetry/sdk-trace-base": "2.2.0",
|
||||
"@opentelemetry/sdk-trace-node": "2.2.0",
|
||||
"@opentelemetry/semantic-conventions": "1.38.0",
|
||||
"@graphql-hive/logger": "1.1.0",
|
||||
"@graphql-hive/plugin-opentelemetry": "1.4.26",
|
||||
"@opentelemetry/api": "1.9.1",
|
||||
"@opentelemetry/auto-instrumentations-node": "0.75.0",
|
||||
"@opentelemetry/context-async-hooks": "2.7.1",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "0.217.0",
|
||||
"@opentelemetry/instrumentation": "0.217.0",
|
||||
"@opentelemetry/resources": "2.7.1",
|
||||
"@opentelemetry/sdk-node": "0.217.0",
|
||||
"@opentelemetry/sdk-trace-base": "2.7.1",
|
||||
"@opentelemetry/sdk-trace-node": "2.7.1",
|
||||
"@opentelemetry/semantic-conventions": "1.40.0",
|
||||
"@sentry/integrations": "7.114.0",
|
||||
"@sentry/node": "7.120.2",
|
||||
"@sentry/types": "7.120.2",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ export * from './metrics';
|
|||
export * from './heartbeats';
|
||||
export * from './trpc';
|
||||
export * from './tracing';
|
||||
export { resolveServerListenOptions } from './listen-options';
|
||||
export { registerShutdown } from './graceful-shutdown';
|
||||
export { cleanRequestId, maskToken } from './helpers';
|
||||
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);
|
||||
}
|
||||
|
||||
type MetricsListenOptions = {
|
||||
port?: number;
|
||||
host?: string;
|
||||
ipv6Only?: boolean;
|
||||
};
|
||||
|
||||
export async function startMetrics(
|
||||
instanceLabel: string | undefined,
|
||||
port = 10_254,
|
||||
options: MetricsListenOptions = {},
|
||||
): Promise<() => Promise<void>> {
|
||||
const { port = 10_254, host = '::', ipv6Only = false } = options;
|
||||
|
||||
promClient.collectDefaultMetrics({
|
||||
labels: { instance: instanceLabel },
|
||||
});
|
||||
|
|
@ -45,7 +53,8 @@ export async function startMetrics(
|
|||
|
||||
await server.listen({
|
||||
port,
|
||||
host: '::',
|
||||
host,
|
||||
ipv6Only,
|
||||
});
|
||||
|
||||
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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-inspector/core": "7.1.2",
|
||||
"@graphql-inspector/core": "7.1.3",
|
||||
"@hive/postgres": "workspace:*",
|
||||
"@hive/service-common": "workspace:*",
|
||||
"@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_PASSWORD=""
|
||||
PORT=6001
|
||||
SERVER_HOST=::
|
||||
SERVER_HOST_IPV6_ONLY=0
|
||||
OPENTELEMETRY_COLLECTOR_ENDPOINT="<sync>"
|
||||
LOG_LEVEL="debug"
|
||||
OPENTELEMETRY_TRACE_USAGE_REQUESTS=1
|
||||
OPENTELEMETRY_TRACE_USAGE_REQUESTS=1
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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;
|
||||
|
||||
|
|
@ -20,6 +20,8 @@ const emptyString = <T extends zod.ZodType>(input: T) => {
|
|||
|
||||
const EnvironmentModel = zod.object({
|
||||
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()),
|
||||
RELEASE: emptyString(zod.string().optional()),
|
||||
HEARTBEAT_ENDPOINT: emptyString(zod.string().url().optional()),
|
||||
|
|
@ -131,6 +133,10 @@ export const env = {
|
|||
release: base.RELEASE ?? 'local',
|
||||
http: {
|
||||
port: base.PORT ?? 6001,
|
||||
...resolveServerListenOptions({
|
||||
serverHost: base.SERVER_HOST,
|
||||
serverHostIpv6Only: base.SERVER_HOST_IPV6_ONLY,
|
||||
}),
|
||||
},
|
||||
tracing: {
|
||||
enabled: !!tracing.OPENTELEMETRY_COLLECTOR_ENDPOINT,
|
||||
|
|
|
|||
|
|
@ -169,12 +169,17 @@ export async function main() {
|
|||
});
|
||||
|
||||
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({
|
||||
port: env.http.port,
|
||||
host: '::',
|
||||
host: env.http.host,
|
||||
ipv6Only: env.http.ipv6Only,
|
||||
});
|
||||
} catch (error) {
|
||||
server.log.fatal(error);
|
||||
|
|
|
|||
|
|
@ -10,4 +10,6 @@ CLICKHOUSE_USERNAME="test"
|
|||
CLICKHOUSE_PASSWORD="test"
|
||||
CLICKHOUSE_ASYNC_INSERT_BUSY_TIMEOUT_MS="500"
|
||||
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 zod from 'zod';
|
||||
import { resolveServerListenOptions } from '@hive/service-common';
|
||||
|
||||
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({
|
||||
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()),
|
||||
RELEASE: emptyString(zod.string().optional()),
|
||||
HEARTBEAT_ENDPOINT: emptyString(zod.string().url().optional()),
|
||||
|
|
@ -162,6 +165,10 @@ export const env = {
|
|||
release: base.RELEASE ?? 'local',
|
||||
http: {
|
||||
port: base.PORT ?? 5000,
|
||||
...resolveServerListenOptions({
|
||||
serverHost: base.SERVER_HOST,
|
||||
serverHostIpv6Only: base.SERVER_HOST_IPV6_ONLY,
|
||||
}),
|
||||
},
|
||||
kafka: {
|
||||
concurrency: kafka.KAFKA_CONCURRENCY,
|
||||
|
|
|
|||
|
|
@ -80,11 +80,16 @@ async function main() {
|
|||
});
|
||||
|
||||
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({
|
||||
port: env.http.port,
|
||||
host: '::',
|
||||
host: env.http.host,
|
||||
ipv6Only: env.http.ipv6Only,
|
||||
});
|
||||
await start();
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -17,5 +17,7 @@ REDIS_PORT="6379"
|
|||
REDIS_PASSWORD=""
|
||||
|
||||
PORT=4001
|
||||
SERVER_HOST=::
|
||||
SERVER_HOST_IPV6_ONLY=0
|
||||
COMMERCE_ENDPOINT="http://localhost:4013"
|
||||
OPENTELEMETRY_COLLECTOR_ENDPOINT="<sync>"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import * as fs from 'fs';
|
||||
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;
|
||||
|
||||
|
|
@ -21,6 +21,8 @@ const emptyString = <T extends zod.ZodType>(input: T) => {
|
|||
|
||||
const EnvironmentModel = zod.object({
|
||||
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(),
|
||||
COMMERCE_ENDPOINT: emptyString(zod.string().url().optional()),
|
||||
RATE_LIMIT_TTL: emptyString(NumberFromString.optional()).default(30_000),
|
||||
|
|
@ -152,6 +154,10 @@ export const env = {
|
|||
release: base.RELEASE ?? 'local',
|
||||
http: {
|
||||
port: base.PORT ?? 5000,
|
||||
...resolveServerListenOptions({
|
||||
serverHost: base.SERVER_HOST,
|
||||
serverHostIpv6Only: base.SERVER_HOST_IPV6_ONLY,
|
||||
}),
|
||||
},
|
||||
tracing: {
|
||||
enabled: !!tracing.OPENTELEMETRY_COLLECTOR_ENDPOINT,
|
||||
|
|
|
|||
|
|
@ -193,12 +193,17 @@ async function main() {
|
|||
});
|
||||
|
||||
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({
|
||||
port: env.http.port,
|
||||
host: '::',
|
||||
host: env.http.host,
|
||||
ipv6Only: env.http.ipv6Only,
|
||||
});
|
||||
await usage.start();
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -8,4 +8,6 @@ EMAIL_PROVIDER=mock
|
|||
SCHEMA_ENDPOINT=http://localhost:6500
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
REDIS_PASSWORD=
|
||||
SERVER_HOST=::
|
||||
SERVER_HOST_IPV6_ONLY=0
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@
|
|||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-hive/logger": "1.0.9",
|
||||
"@graphql-inspector/core": "7.1.2",
|
||||
"@graphql-hive/logger": "1.1.0",
|
||||
"@graphql-inspector/core": "7.1.3",
|
||||
"@graphql-inspector/patch": "0.1.3",
|
||||
"@graphql-yoga/redis-event-target": "3.0.3",
|
||||
"@hive/postgres": "workspace:*",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import zod from 'zod';
|
||||
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';
|
||||
|
||||
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({
|
||||
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()),
|
||||
RELEASE: emptyString(zod.string().optional()),
|
||||
HEARTBEAT_ENDPOINT: emptyString(zod.string().url().optional()),
|
||||
|
|
@ -205,6 +207,10 @@ export const env = {
|
|||
release: base.RELEASE ?? 'local',
|
||||
http: {
|
||||
port: base.PORT ?? 6260,
|
||||
...resolveServerListenOptions({
|
||||
serverHost: base.SERVER_HOST,
|
||||
serverHostIpv6Only: base.SERVER_HOST_IPV6_ONLY,
|
||||
}),
|
||||
},
|
||||
tracing: {
|
||||
enabled: !!tracing.OPENTELEMETRY_COLLECTOR_ENDPOINT,
|
||||
|
|
|
|||
|
|
@ -134,11 +134,16 @@ if (context.email.id === 'mock') {
|
|||
|
||||
await server.listen({
|
||||
port: env.http.port,
|
||||
host: '::',
|
||||
host: env.http.host,
|
||||
ipv6Only: env.http.ipv6Only,
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
const runner = await run({
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
APP_BASE_URL="http://localhost:3000"
|
||||
ENVIRONMENT="development"
|
||||
SERVER_HOST=::
|
||||
SERVER_HOST_IPV6_ONLY=0
|
||||
|
||||
# Public GraphQL endpoint
|
||||
GRAPHQL_PUBLIC_ENDPOINT="http://localhost:3001/graphql"
|
||||
|
|
|
|||
|
|
@ -25,11 +25,12 @@
|
|||
"@graphiql/toolkit": "0.9.1",
|
||||
"@graphql-codegen/client-preset-swc-plugin": "0.2.0",
|
||||
"@graphql-hive/laboratory": "workspace:*",
|
||||
"@graphql-inspector/core": "7.1.2",
|
||||
"@graphql-inspector/core": "7.1.3",
|
||||
"@graphql-inspector/patch": "0.1.3",
|
||||
"@graphql-tools/mock": "9.0.25",
|
||||
"@graphql-typed-document-node/core": "3.2.0",
|
||||
"@headlessui/react": "2.2.0",
|
||||
"@hive/service-common": "workspace:*",
|
||||
"@hookform/resolvers": "3.10.0",
|
||||
"@ladle/react": "5.1.1",
|
||||
"@monaco-editor/react": "4.8.0-rc.2",
|
||||
|
|
|
|||
|
|
@ -138,7 +138,6 @@ export const ChangesBlock_SchemaChangeFragment = graphql(`
|
|||
export function ChangesBlock(
|
||||
props: {
|
||||
title: string | React.ReactElement;
|
||||
severityLevel: SeverityLevelType;
|
||||
organizationSlug: string;
|
||||
projectSlug: 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 { resolveServerListenOptions } from '@hive/service-common/listen-options';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { ALLOWED_ENVIRONMENT_VARIABLES } from './frontend-public-variables';
|
||||
|
||||
|
|
@ -41,6 +42,8 @@ const BaseSchema = zod.object({
|
|||
NODE_ENV: zod.string().default('development'),
|
||||
ENVIRONMENT: zod.string(),
|
||||
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(),
|
||||
GRAPHQL_PUBLIC_ENDPOINT: zod.string().url(),
|
||||
GRAPHQL_PUBLIC_SUBSCRIPTION_ENDPOINT: zod.string().url(),
|
||||
|
|
@ -172,6 +175,10 @@ function buildConfig() {
|
|||
|
||||
const config = {
|
||||
port: base.PORT ?? 3000,
|
||||
...resolveServerListenOptions({
|
||||
serverHost: base.SERVER_HOST,
|
||||
serverHostIpv6Only: base.SERVER_HOST_IPV6_ONLY,
|
||||
}),
|
||||
release: base.RELEASE ?? 'local',
|
||||
nodeEnv: base.NODE_ENV,
|
||||
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 { DiffEditor } from '@/components/v2/diff-editor';
|
||||
import { FragmentType, graphql, useFragment } from '@/gql';
|
||||
import { ProjectType, SeverityLevelType } from '@/gql/graphql';
|
||||
import { ProjectType } from '@/gql/graphql';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
CheckIcon,
|
||||
|
|
@ -304,7 +304,7 @@ function ConditionalBreakingChangesMetadataSection(props: {
|
|||
Get more out of schema checks by enabling conditional breaking changes based on usage data.
|
||||
<br />
|
||||
<DocsLink
|
||||
href="/management/targets#conditional-breaking-changes"
|
||||
href="/schema-registry/management/targets#conditional-breaking-changes"
|
||||
className="text-neutral-10 hover:text-neutral-11"
|
||||
>
|
||||
Learn more about conditional breaking changes.
|
||||
|
|
@ -387,7 +387,7 @@ function ConditionalBreakingChangesMetadataSection(props: {
|
|||
).
|
||||
<br />
|
||||
<DocsLink
|
||||
href="/management/targets#conditional-breaking-changes"
|
||||
href="/schema-registry/management/targets#conditional-breaking-changes"
|
||||
className="text-neutral-10 hover:text-neutral-11"
|
||||
>
|
||||
Learn more about conditional breaking changes.
|
||||
|
|
@ -564,7 +564,6 @@ function DefaultSchemaView(props: {
|
|||
targetSlug={props.targetSlug}
|
||||
schemaCheckId={schemaCheck.id}
|
||||
title={<BreakingChangesTitle />}
|
||||
severityLevel={SeverityLevelType.Breaking}
|
||||
changesWithUsage={schemaCheck.breakingSchemaChanges.edges.map(edge => edge.node)}
|
||||
conditionBreakingChangeMetadata={schemaCheck.conditionalBreakingChangeMetadata}
|
||||
/>
|
||||
|
|
@ -578,7 +577,6 @@ function DefaultSchemaView(props: {
|
|||
targetSlug={props.targetSlug}
|
||||
schemaCheckId={schemaCheck.id}
|
||||
title="Safe Changes"
|
||||
severityLevel={SeverityLevelType.Safe}
|
||||
changes={schemaCheck.safeSchemaChanges.edges.map(edge => edge.node)}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -768,7 +766,6 @@ function ContractCheckView(props: {
|
|||
targetSlug={props.targetSlug}
|
||||
schemaCheckId={schemaCheck.id}
|
||||
title={<BreakingChangesTitle />}
|
||||
severityLevel={SeverityLevelType.Breaking}
|
||||
changesWithUsage={contractCheck.breakingSchemaChanges.edges.map(
|
||||
edge => edge.node,
|
||||
)}
|
||||
|
|
@ -784,7 +781,6 @@ function ContractCheckView(props: {
|
|||
targetSlug={props.targetSlug}
|
||||
schemaCheckId={schemaCheck.id}
|
||||
title="Safe Changes"
|
||||
severityLevel={SeverityLevelType.Safe}
|
||||
changes={contractCheck.safeSchemaChanges.edges.map(edge => edge.node)}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue