mirror of
https://github.com/graphql-hive/console
synced 2026-04-21 14:37:17 +00:00
1463 lines
54 KiB
TypeScript
1463 lines
54 KiB
TypeScript
import { formatISO, subHours } from 'date-fns';
|
|
import { humanId } from 'human-id';
|
|
import z from 'zod';
|
|
import { NoopLogger } from '@hive/api/modules/shared/providers/logger';
|
|
import { createRedisClient } from '@hive/api/modules/shared/providers/redis';
|
|
import { createPostgresDatabasePool, psql } from '@hive/postgres';
|
|
import type { Report } from '../../packages/libraries/core/src/client/usage.js';
|
|
import { authenticate, userEmail } from './auth';
|
|
import {
|
|
CreateCollectionMutation,
|
|
CreateOperationMutation,
|
|
DeleteCollectionMutation,
|
|
DeleteOperationMutation,
|
|
UpdateCollectionMutation,
|
|
UpdateOperationMutation,
|
|
UpdatePreflightScriptMutation,
|
|
} from './collections';
|
|
import { ensureEnv } from './env';
|
|
import {
|
|
addAlert,
|
|
addAlertChannel,
|
|
assignMemberRole,
|
|
checkSchema,
|
|
compareToPreviousVersion,
|
|
createCdnAccess,
|
|
createMemberRole,
|
|
createOrganization,
|
|
createOrganizationAccessToken,
|
|
createProject,
|
|
createTarget,
|
|
createToken,
|
|
deleteMemberRole,
|
|
deleteSchema,
|
|
deleteTokens,
|
|
fetchLatestSchema,
|
|
fetchLatestValidSchema,
|
|
fetchMetadataFromCDN,
|
|
fetchSchemaFromCDN,
|
|
fetchSupergraphFromCDN,
|
|
fetchVersions,
|
|
getOrganization,
|
|
getOrganizationMembers,
|
|
getOrganizationProjects,
|
|
inviteToOrganization,
|
|
joinOrganization,
|
|
pollFor,
|
|
publishSchema,
|
|
readClientStats,
|
|
readOperationBody,
|
|
readOperationsStats,
|
|
readTokenInfo,
|
|
updateBaseSchema,
|
|
updateMemberRole,
|
|
updateTargetValidationSettings,
|
|
} from './flow';
|
|
import * as GraphQLSchema from './gql/graphql';
|
|
import {
|
|
ProjectType,
|
|
SchemaPolicyInput,
|
|
TargetAccessScope,
|
|
UpdateOrgRateLimitDocument,
|
|
} from './gql/graphql';
|
|
import { execute } from './graphql';
|
|
import { createOIDCIntegration } from './oidc-integration.js';
|
|
import {
|
|
CreateSavedFilterMutation,
|
|
DeleteSavedFilterMutation,
|
|
GetSavedFilterQuery,
|
|
GetSavedFiltersQuery,
|
|
TrackSavedFilterViewMutation,
|
|
UpdateSavedFilterMutation,
|
|
} from './saved-filters';
|
|
import { UpdateSchemaPolicyForOrganization, UpdateSchemaPolicyForProject } from './schema-policy';
|
|
import { collect, CollectedOperation, legacyCollect } from './usage';
|
|
import { generateUnique, getServiceHost, pollForEmailVerificationLink } from './utils';
|
|
|
|
function getPGConnectionString() {
|
|
const pg = {
|
|
user: ensureEnv('POSTGRES_USER'),
|
|
password: ensureEnv('POSTGRES_PASSWORD'),
|
|
host: ensureEnv('POSTGRES_HOST'),
|
|
port: ensureEnv('POSTGRES_PORT'),
|
|
db: ensureEnv('POSTGRES_DB'),
|
|
};
|
|
|
|
return `postgres://${pg.user}:${pg.password}@${pg.host}:${pg.port}/${pg.db}?sslmode=disable`;
|
|
}
|
|
|
|
function createConnectionPool() {
|
|
return createPostgresDatabasePool({
|
|
connectionParameters: getPGConnectionString(),
|
|
});
|
|
}
|
|
|
|
async function createDbConnection() {
|
|
const pool = await createConnectionPool();
|
|
return {
|
|
pool,
|
|
[Symbol.asyncDispose]: async () => {
|
|
await pool.end();
|
|
},
|
|
};
|
|
}
|
|
|
|
let sharedDBPoolPromise: ReturnType<typeof createDbConnection>;
|
|
|
|
export function initSeed() {
|
|
function getPool() {
|
|
if (!sharedDBPoolPromise) {
|
|
sharedDBPoolPromise = createDbConnection();
|
|
}
|
|
return sharedDBPoolPromise.then(res => res.pool);
|
|
}
|
|
|
|
async function doAuthenticate(
|
|
email: string,
|
|
opts?: {
|
|
oidcIntegrationId?: string;
|
|
verifyEmail?: boolean;
|
|
},
|
|
) {
|
|
const auth = await authenticate(await getPool(), email, opts?.oidcIntegrationId);
|
|
|
|
if (opts?.verifyEmail ?? true) {
|
|
const pool = await getPool();
|
|
await pool.query(psql`
|
|
INSERT INTO "email_verifications" ("user_identity_id", "email", "verified_at")
|
|
VALUES (${auth.supertokensUserId}, ${email}, NOW())
|
|
`);
|
|
}
|
|
|
|
return auth;
|
|
}
|
|
|
|
async function purgeOrganizationAccessTokenById(id: string) {
|
|
const registryAddress = await getServiceHost('server', 8082);
|
|
const purged: { deleted: boolean } = await fetch(
|
|
'http://' + registryAddress + '/cache/organization-access-token-cache/delete/' + id,
|
|
{
|
|
method: 'POST',
|
|
},
|
|
).then(res => res.json());
|
|
expect(purged.deleted).toBe(true);
|
|
}
|
|
|
|
return {
|
|
pollForEmailVerificationLink,
|
|
getPGConnectionString,
|
|
async purgeOIDCDomains() {
|
|
const pool = await getPool();
|
|
await pool.query(psql`
|
|
TRUNCATE "oidc_integration_domains"
|
|
`);
|
|
},
|
|
async forgeOIDCDNSChallenge(orgSlug: string) {
|
|
const pool = await getPool();
|
|
|
|
const domainChallengeId = await pool
|
|
.oneFirst(
|
|
psql`
|
|
SELECT "oidc_integration_domains"."id"
|
|
FROM "oidc_integration_domains" INNER JOIN "organizations" ON "oidc_integration_domains"."organization_id" = "organizations"."id"
|
|
WHERE "organizations"."clean_id" = ${orgSlug}
|
|
`,
|
|
)
|
|
.then(z.string().parse);
|
|
const key = `hive:oidcDomainChallenge:${domainChallengeId}`;
|
|
|
|
const challenge = {
|
|
id: domainChallengeId,
|
|
recordName: `_hive-challenge`,
|
|
// hardcoded value
|
|
value: 'a894723a5d52a30d73790752b0169835e6f81dd77d2737dba809bee7fde39092',
|
|
};
|
|
|
|
const redis = createRedisClient(
|
|
'',
|
|
{
|
|
host: ensureEnv('REDIS_HOST'),
|
|
password: ensureEnv('REDIS_PASSWORD'),
|
|
port: parseInt(ensureEnv('REDIS_PORT'), 10),
|
|
tlsEnabled: false,
|
|
},
|
|
new NoopLogger(),
|
|
);
|
|
|
|
await redis.set(key, JSON.stringify(challenge));
|
|
await redis.disconnect();
|
|
},
|
|
createDbConnection,
|
|
authenticate: doAuthenticate,
|
|
generateEmail: () => userEmail(generateUnique()),
|
|
purgeOrganizationAccessTokenById,
|
|
async createOwner(verifyEmail: boolean = true) {
|
|
const ownerEmail = userEmail(generateUnique());
|
|
const auth = await doAuthenticate(ownerEmail, {
|
|
verifyEmail,
|
|
});
|
|
|
|
const ownerRefreshToken = auth.refresh_token;
|
|
const ownerToken = auth.access_token;
|
|
|
|
return {
|
|
ownerEmail,
|
|
ownerToken,
|
|
ownerRefreshToken,
|
|
async createOrg() {
|
|
const orgSlug = generateUnique();
|
|
const orgResult = await createOrganization({ slug: orgSlug }, ownerToken).then(r =>
|
|
r.expectNoGraphQLErrors(),
|
|
);
|
|
|
|
const organization =
|
|
orgResult.createOrganization.ok!.createdOrganizationPayload.organization;
|
|
|
|
return {
|
|
organization,
|
|
async overrideOrgPlan(plan: 'PRO' | 'ENTERPRISE' | 'HOBBY') {
|
|
const pool = await createConnectionPool();
|
|
|
|
await pool.query(psql`
|
|
UPDATE organizations SET plan_name = ${plan} WHERE id = ${organization.id}
|
|
`);
|
|
|
|
await pool.end();
|
|
},
|
|
async updateOrgRateLimit(newLimit: number, token = ownerToken) {
|
|
const result = await execute({
|
|
document: UpdateOrgRateLimitDocument,
|
|
variables: {
|
|
selector: {
|
|
organizationSlug: organization.slug,
|
|
},
|
|
monthlyLimits: {
|
|
operations: newLimit,
|
|
},
|
|
},
|
|
authToken: token,
|
|
}).then(r => r.expectNoGraphQLErrors());
|
|
|
|
return result.updateOrgRateLimit;
|
|
},
|
|
async createOrganizationAccessToken(
|
|
args: {
|
|
permissions: Array<string>;
|
|
resources: GraphQLSchema.ResourceAssignmentInput;
|
|
},
|
|
/** Override the used access token. */
|
|
accessToken?: string,
|
|
) {
|
|
const result = await createOrganizationAccessToken(
|
|
{
|
|
title: 'an access token',
|
|
description: 'access token',
|
|
organization: {
|
|
byId: organization.id,
|
|
},
|
|
permissions: args.permissions,
|
|
resources: args.resources,
|
|
},
|
|
accessToken ?? ownerToken,
|
|
).then(r => r.expectNoGraphQLErrors());
|
|
|
|
if (result.createOrganizationAccessToken.error) {
|
|
throw new Error(result.createOrganizationAccessToken.error.message);
|
|
}
|
|
|
|
return result.createOrganizationAccessToken.ok!;
|
|
},
|
|
async setFeatureFlag(name: string, value: boolean | string[]) {
|
|
const pool = await createConnectionPool();
|
|
|
|
await pool.query(psql`
|
|
UPDATE organizations SET feature_flags = ${psql.jsonb({
|
|
[name]: value,
|
|
})}
|
|
WHERE id = ${organization.id}
|
|
`);
|
|
|
|
await pool.end();
|
|
},
|
|
async setDataRetention(days: number) {
|
|
const pool = await createConnectionPool();
|
|
|
|
await pool.query(psql`
|
|
UPDATE organizations SET limit_retention_days = ${days} WHERE id = ${organization.id}
|
|
`);
|
|
|
|
await pool.end();
|
|
},
|
|
async setOrganizationSchemaPolicy(policy: SchemaPolicyInput, allowOverrides: boolean) {
|
|
const result = await execute({
|
|
document: UpdateSchemaPolicyForOrganization,
|
|
variables: {
|
|
allowOverrides,
|
|
selector: {
|
|
organizationSlug: organization.slug,
|
|
},
|
|
policy,
|
|
},
|
|
authToken: ownerToken,
|
|
}).then(r => r.expectNoGraphQLErrors());
|
|
|
|
return result.updateSchemaPolicyForOrganization;
|
|
},
|
|
async fetchOrganizationInfo() {
|
|
const result = await getOrganization(organization.slug, ownerToken).then(r =>
|
|
r.expectNoGraphQLErrors(),
|
|
);
|
|
|
|
return result.organization!;
|
|
},
|
|
async inviteMember(
|
|
email = 'some@email.com',
|
|
inviteToken = ownerToken,
|
|
memberRoleId?: string,
|
|
) {
|
|
const inviteResult = await inviteToOrganization(
|
|
{
|
|
organization: {
|
|
bySelector: {
|
|
organizationSlug: organization.slug,
|
|
},
|
|
},
|
|
email,
|
|
memberRoleId,
|
|
},
|
|
inviteToken,
|
|
).then(r => r.expectNoGraphQLErrors());
|
|
|
|
return inviteResult.inviteToOrganizationByEmail;
|
|
},
|
|
async joinMemberUsingCode(inviteCode: string, memberToken: string) {
|
|
return await joinOrganization(inviteCode, memberToken);
|
|
},
|
|
async members() {
|
|
const membersResult = await getOrganizationMembers(
|
|
{ organizationSlug: organization.slug },
|
|
ownerToken,
|
|
).then(r => r.expectNoGraphQLErrors());
|
|
|
|
const members = membersResult.organization?.members?.edges?.map(edge => edge.node);
|
|
|
|
if (!members) {
|
|
throw new Error(`Could not get members for org ${organization.slug}`);
|
|
}
|
|
|
|
return members;
|
|
},
|
|
/** Expires tokens */
|
|
async forceExpireTokens(tokenIds: string[]) {
|
|
const pool = await createConnectionPool();
|
|
const result = await pool.any(psql`
|
|
UPDATE "organization_access_tokens"
|
|
SET "expires_at" = NOW()
|
|
WHERE id IN (${psql.join(tokenIds, psql`, `)}) AND organization_id = ${organization.id}
|
|
RETURNING
|
|
"id"
|
|
`);
|
|
await pool.end();
|
|
expect(result.length).toBe(tokenIds.length);
|
|
for (const id of tokenIds) {
|
|
await purgeOrganizationAccessTokenById(id);
|
|
}
|
|
},
|
|
async projects(token = ownerToken) {
|
|
const projectsResult = await getOrganizationProjects(
|
|
{ organizationSlug: organization.slug },
|
|
token,
|
|
).then(r => r.expectNoGraphQLErrors());
|
|
|
|
const projects = projectsResult.organization?.projects.edges.map(edge => edge.node);
|
|
|
|
if (!projects) {
|
|
throw new Error(`Could not get projects for org ${organization.slug}`);
|
|
}
|
|
|
|
return projects;
|
|
},
|
|
async createProject(projectType: ProjectType = ProjectType.Single) {
|
|
const projectResult = await createProject(
|
|
{
|
|
organization: {
|
|
bySelector: {
|
|
organizationSlug: organization.slug,
|
|
},
|
|
},
|
|
type: projectType,
|
|
slug: generateUnique(),
|
|
},
|
|
ownerToken,
|
|
).then(r => r.expectNoGraphQLErrors());
|
|
|
|
const targets = projectResult.createProject.ok!.createdTargets;
|
|
const target = targets[0];
|
|
const project = projectResult.createProject.ok!.createdProject;
|
|
|
|
return {
|
|
project,
|
|
targets,
|
|
target,
|
|
async setNativeFederation(enabled: boolean) {
|
|
const pool = await createConnectionPool();
|
|
|
|
await pool.query(psql`
|
|
UPDATE projects SET native_federation = ${enabled} WHERE id = ${project.id}
|
|
`);
|
|
|
|
await pool.end();
|
|
},
|
|
async setProjectSchemaPolicy(policy: SchemaPolicyInput) {
|
|
const result = await execute({
|
|
document: UpdateSchemaPolicyForProject,
|
|
variables: {
|
|
selector: {
|
|
organizationSlug: organization.slug,
|
|
projectSlug: project.slug,
|
|
},
|
|
policy,
|
|
},
|
|
authToken: ownerToken,
|
|
}).then(r => r.expectNoGraphQLErrors());
|
|
|
|
return result.updateSchemaPolicyForProject;
|
|
},
|
|
async removeTokens(tokenIds: string[]) {
|
|
return await deleteTokens(
|
|
{
|
|
organizationSlug: organization.slug,
|
|
projectSlug: project.slug,
|
|
targetSlug: target.slug,
|
|
tokenIds,
|
|
},
|
|
ownerToken,
|
|
)
|
|
.then(r => r.expectNoGraphQLErrors())
|
|
.then(r => r.deleteTokens.deletedTokens);
|
|
},
|
|
async createDocumentCollection({
|
|
name,
|
|
description,
|
|
token = ownerToken,
|
|
}: {
|
|
name: string;
|
|
description: string;
|
|
token?: string;
|
|
}) {
|
|
const result = await execute({
|
|
document: CreateCollectionMutation,
|
|
variables: {
|
|
input: {
|
|
name,
|
|
description,
|
|
},
|
|
selector: {
|
|
organizationSlug: organization.slug,
|
|
projectSlug: project.slug,
|
|
targetSlug: target.slug,
|
|
},
|
|
},
|
|
authToken: token,
|
|
}).then(r => r.expectNoGraphQLErrors());
|
|
|
|
return result.createDocumentCollection;
|
|
},
|
|
async updatePreflightScript({
|
|
sourceCode,
|
|
token = ownerToken,
|
|
}: {
|
|
sourceCode: string;
|
|
token?: string;
|
|
}) {
|
|
const result = await execute({
|
|
document: UpdatePreflightScriptMutation,
|
|
variables: {
|
|
input: {
|
|
selector: {
|
|
organizationSlug: organization.slug,
|
|
projectSlug: project.slug,
|
|
targetSlug: target.slug,
|
|
},
|
|
sourceCode,
|
|
},
|
|
},
|
|
authToken: token,
|
|
}).then(r => r.expectNoGraphQLErrors());
|
|
|
|
return result.updatePreflightScript;
|
|
},
|
|
async updateDocumentCollection({
|
|
collectionId,
|
|
name,
|
|
description,
|
|
token = ownerToken,
|
|
}: {
|
|
collectionId: string;
|
|
name: string;
|
|
description: string;
|
|
token?: string;
|
|
}) {
|
|
const result = await execute({
|
|
document: UpdateCollectionMutation,
|
|
variables: {
|
|
input: {
|
|
collectionId,
|
|
name,
|
|
description,
|
|
},
|
|
selector: {
|
|
organizationSlug: organization.slug,
|
|
projectSlug: project.slug,
|
|
targetSlug: target.slug,
|
|
},
|
|
},
|
|
authToken: token,
|
|
}).then(r => r.expectNoGraphQLErrors());
|
|
|
|
return result.updateDocumentCollection;
|
|
},
|
|
async deleteDocumentCollection({
|
|
collectionId,
|
|
token = ownerToken,
|
|
}: {
|
|
collectionId: string;
|
|
token?: string;
|
|
}) {
|
|
const result = await execute({
|
|
document: DeleteCollectionMutation,
|
|
variables: {
|
|
id: collectionId,
|
|
selector: {
|
|
organizationSlug: organization.slug,
|
|
projectSlug: project.slug,
|
|
targetSlug: target.slug,
|
|
},
|
|
},
|
|
authToken: token,
|
|
}).then(r => r.expectNoGraphQLErrors());
|
|
|
|
return result.deleteDocumentCollection;
|
|
},
|
|
async createOperationInCollection(input: {
|
|
collectionId: string;
|
|
name: string;
|
|
query: string;
|
|
variables?: string;
|
|
headers?: string;
|
|
token?: string;
|
|
}) {
|
|
const result = await execute({
|
|
document: CreateOperationMutation,
|
|
variables: {
|
|
input: {
|
|
collectionId: input.collectionId,
|
|
name: input.name,
|
|
query: input.query,
|
|
headers: input.headers,
|
|
variables: input.variables,
|
|
},
|
|
selector: {
|
|
organizationSlug: organization.slug,
|
|
projectSlug: project.slug,
|
|
targetSlug: target.slug,
|
|
},
|
|
},
|
|
authToken: input.token || ownerToken,
|
|
}).then(r => r.expectNoGraphQLErrors());
|
|
|
|
return result.createOperationInDocumentCollection;
|
|
},
|
|
async deleteOperationInCollection(input: { operationId: string; token?: string }) {
|
|
const result = await execute({
|
|
document: DeleteOperationMutation,
|
|
variables: {
|
|
id: input.operationId,
|
|
selector: {
|
|
organizationSlug: organization.slug,
|
|
projectSlug: project.slug,
|
|
targetSlug: target.slug,
|
|
},
|
|
},
|
|
authToken: input.token || ownerToken,
|
|
}).then(r => r.expectNoGraphQLErrors());
|
|
|
|
return result.deleteOperationInDocumentCollection;
|
|
},
|
|
async updateOperationInCollection(input: {
|
|
operationId: string;
|
|
collectionId: string;
|
|
name: string;
|
|
query: string;
|
|
variables?: string;
|
|
headers?: string;
|
|
token?: string;
|
|
}) {
|
|
const result = await execute({
|
|
document: UpdateOperationMutation,
|
|
variables: {
|
|
input: {
|
|
operationId: input.operationId,
|
|
collectionId: input.collectionId,
|
|
name: input.name,
|
|
query: input.query,
|
|
headers: input.headers,
|
|
variables: input.variables,
|
|
},
|
|
selector: {
|
|
organizationSlug: organization.slug,
|
|
projectSlug: project.slug,
|
|
targetSlug: target.slug,
|
|
},
|
|
},
|
|
authToken: input.token || ownerToken,
|
|
}).then(r => r.expectNoGraphQLErrors());
|
|
|
|
return result.updateOperationInDocumentCollection;
|
|
},
|
|
async getSavedFilter({
|
|
filterId,
|
|
token = ownerToken,
|
|
}: {
|
|
filterId: string;
|
|
token?: string;
|
|
}) {
|
|
const result = await execute({
|
|
document: GetSavedFilterQuery,
|
|
variables: {
|
|
id: filterId,
|
|
selector: {
|
|
organizationSlug: organization.slug,
|
|
projectSlug: project.slug,
|
|
targetSlug: target.slug,
|
|
},
|
|
},
|
|
authToken: token,
|
|
}).then(r => r.expectNoGraphQLErrors());
|
|
|
|
return result.target?.savedFilter;
|
|
},
|
|
async getSavedFilters({
|
|
first = 20,
|
|
after,
|
|
visibility,
|
|
search,
|
|
token = ownerToken,
|
|
}: {
|
|
first?: number;
|
|
after?: string;
|
|
visibility?: GraphQLSchema.SavedFilterVisibilityType;
|
|
search?: string;
|
|
token?: string;
|
|
}) {
|
|
const result = await execute({
|
|
document: GetSavedFiltersQuery,
|
|
variables: {
|
|
first,
|
|
after,
|
|
visibility,
|
|
search,
|
|
selector: {
|
|
organizationSlug: organization.slug,
|
|
projectSlug: project.slug,
|
|
targetSlug: target.slug,
|
|
},
|
|
},
|
|
authToken: token,
|
|
}).then(r => r.expectNoGraphQLErrors());
|
|
|
|
return {
|
|
savedFilters: result.target?.savedFilters,
|
|
viewerCanCreateSavedFilter: result.target?.viewerCanCreateSavedFilter,
|
|
};
|
|
},
|
|
async createSavedFilter({
|
|
name,
|
|
description,
|
|
visibility,
|
|
insightsFilter,
|
|
token = ownerToken,
|
|
}: {
|
|
name: string;
|
|
description?: string;
|
|
visibility: GraphQLSchema.SavedFilterVisibilityType;
|
|
insightsFilter?: GraphQLSchema.InsightsFilterConfigurationInput;
|
|
token?: string;
|
|
}) {
|
|
const result = await execute({
|
|
document: CreateSavedFilterMutation,
|
|
variables: {
|
|
input: {
|
|
target: {
|
|
bySelector: {
|
|
organizationSlug: organization.slug,
|
|
projectSlug: project.slug,
|
|
targetSlug: target.slug,
|
|
},
|
|
},
|
|
name,
|
|
description,
|
|
visibility,
|
|
insightsFilter,
|
|
},
|
|
},
|
|
authToken: token,
|
|
}).then(r => r.expectNoGraphQLErrors());
|
|
|
|
return result.createSavedFilter;
|
|
},
|
|
async updateSavedFilter({
|
|
filterId,
|
|
name,
|
|
description,
|
|
visibility,
|
|
insightsFilter,
|
|
token = ownerToken,
|
|
}: {
|
|
filterId: string;
|
|
name?: string;
|
|
description?: string;
|
|
visibility?: GraphQLSchema.SavedFilterVisibilityType;
|
|
insightsFilter?: GraphQLSchema.InsightsFilterConfigurationInput;
|
|
token?: string;
|
|
}) {
|
|
const result = await execute({
|
|
document: UpdateSavedFilterMutation,
|
|
variables: {
|
|
input: {
|
|
target: {
|
|
bySelector: {
|
|
organizationSlug: organization.slug,
|
|
projectSlug: project.slug,
|
|
targetSlug: target.slug,
|
|
},
|
|
},
|
|
id: filterId,
|
|
name,
|
|
description,
|
|
visibility,
|
|
insightsFilter,
|
|
},
|
|
},
|
|
authToken: token,
|
|
}).then(r => r.expectNoGraphQLErrors());
|
|
|
|
return result.updateSavedFilter;
|
|
},
|
|
async deleteSavedFilter({
|
|
filterId,
|
|
token = ownerToken,
|
|
}: {
|
|
filterId: string;
|
|
token?: string;
|
|
}) {
|
|
const result = await execute({
|
|
document: DeleteSavedFilterMutation,
|
|
variables: {
|
|
input: {
|
|
target: {
|
|
bySelector: {
|
|
organizationSlug: organization.slug,
|
|
projectSlug: project.slug,
|
|
targetSlug: target.slug,
|
|
},
|
|
},
|
|
id: filterId,
|
|
},
|
|
},
|
|
authToken: token,
|
|
}).then(r => r.expectNoGraphQLErrors());
|
|
|
|
return result.deleteSavedFilter;
|
|
},
|
|
async trackSavedFilterView({
|
|
filterId,
|
|
token = ownerToken,
|
|
}: {
|
|
filterId: string;
|
|
token?: string;
|
|
}) {
|
|
const result = await execute({
|
|
document: TrackSavedFilterViewMutation,
|
|
variables: {
|
|
input: {
|
|
target: {
|
|
bySelector: {
|
|
organizationSlug: organization.slug,
|
|
projectSlug: project.slug,
|
|
targetSlug: target.slug,
|
|
},
|
|
},
|
|
id: filterId,
|
|
},
|
|
},
|
|
authToken: token,
|
|
}).then(r => r.expectNoGraphQLErrors());
|
|
|
|
return result.trackSavedFilterView;
|
|
},
|
|
async addAlert(
|
|
input: {
|
|
token?: string;
|
|
} & Parameters<typeof addAlert>[0],
|
|
) {
|
|
const result = await addAlert(input, input.token || ownerToken).then(r =>
|
|
r.expectNoGraphQLErrors(),
|
|
);
|
|
return result.addAlert;
|
|
},
|
|
async addAlertChannel(
|
|
input: { token?: string } & Parameters<typeof addAlertChannel>[0],
|
|
) {
|
|
const result = await addAlertChannel(input, input.token || ownerToken).then(r =>
|
|
r.expectNoGraphQLErrors(),
|
|
);
|
|
return result.addAlertChannel;
|
|
},
|
|
/**
|
|
* Create an access token for a given target.
|
|
* This token can be used for usage reporting and all actions that would be performed by the CLI.
|
|
*/
|
|
async createTargetAccessToken({
|
|
mode = 'readWrite',
|
|
target: forTarget = {
|
|
slug: target.slug,
|
|
id: target.id,
|
|
},
|
|
actorToken = ownerToken,
|
|
}: {
|
|
mode?: 'readWrite' | 'readOnly' | 'noAccess';
|
|
target?: {
|
|
slug: string;
|
|
id: string;
|
|
};
|
|
actorToken?: string;
|
|
}) {
|
|
const target = forTarget;
|
|
|
|
const tokenResult = await createToken(
|
|
{
|
|
name: generateUnique(),
|
|
organizationSlug: organization.slug,
|
|
projectSlug: project.slug,
|
|
targetSlug: target.slug,
|
|
organizationScopes: [],
|
|
projectScopes: [],
|
|
targetScopes:
|
|
mode === 'noAccess'
|
|
? []
|
|
: mode === 'readWrite'
|
|
? [TargetAccessScope.RegistryRead, TargetAccessScope.RegistryWrite]
|
|
: [TargetAccessScope.RegistryRead],
|
|
},
|
|
actorToken,
|
|
).then(r => r.expectNoGraphQLErrors());
|
|
|
|
const secret = tokenResult.createToken.ok!.secret;
|
|
const token = tokenResult.createToken.ok!.createdToken;
|
|
|
|
return {
|
|
token,
|
|
secret,
|
|
async collectLegacyOperations(
|
|
operations: CollectedOperation[],
|
|
headerName: 'x-api-token' | 'authorization' = 'authorization',
|
|
) {
|
|
return await legacyCollect({
|
|
operations,
|
|
token: secret,
|
|
authorizationHeader: headerName,
|
|
});
|
|
},
|
|
collectUsage(report: Report) {
|
|
return collect({
|
|
report,
|
|
accessToken: secret,
|
|
});
|
|
},
|
|
async checkSchema(
|
|
sdl: string,
|
|
service?: string,
|
|
meta?: {
|
|
author: string;
|
|
commit: string;
|
|
},
|
|
contextId?: string,
|
|
schemaProposalId?: string,
|
|
) {
|
|
return await checkSchema(
|
|
{
|
|
sdl,
|
|
service,
|
|
meta,
|
|
contextId,
|
|
schemaProposalId,
|
|
},
|
|
secret,
|
|
);
|
|
},
|
|
async publishSchema(options: {
|
|
sdl: string;
|
|
headerName?: 'x-api-token' | 'authorization';
|
|
author?: string;
|
|
force?: boolean;
|
|
experimental_acceptBreakingChanges?: boolean;
|
|
commit?: string;
|
|
service?: string;
|
|
url?: string;
|
|
metadata?: string;
|
|
/**
|
|
* @deprecated
|
|
*/
|
|
github?: boolean | null;
|
|
}) {
|
|
return await publishSchema(
|
|
{
|
|
author: options.author || 'Kamil',
|
|
commit: options.commit || 'test',
|
|
sdl: options.sdl,
|
|
service: options.service,
|
|
url: options.url,
|
|
force: options.force,
|
|
metadata: options.metadata,
|
|
experimental_acceptBreakingChanges:
|
|
options.experimental_acceptBreakingChanges,
|
|
github: options.github,
|
|
},
|
|
secret,
|
|
options.headerName || 'authorization',
|
|
);
|
|
},
|
|
async deleteSchema(serviceName: string) {
|
|
return await deleteSchema(
|
|
{
|
|
serviceName,
|
|
dryRun: false,
|
|
},
|
|
secret,
|
|
);
|
|
},
|
|
async latestSchema() {
|
|
return (await fetchLatestSchema(secret)).expectNoGraphQLErrors();
|
|
},
|
|
async fetchLatestValidSchema() {
|
|
return (await fetchLatestValidSchema(secret)).expectNoGraphQLErrors();
|
|
},
|
|
async fetchTokenInfo() {
|
|
const tokenInfoResult = await readTokenInfo(secret).then(r =>
|
|
r.expectNoGraphQLErrors(),
|
|
);
|
|
|
|
return tokenInfoResult.tokenInfo;
|
|
},
|
|
};
|
|
},
|
|
async createCdnAccess(ttarget: TargetOverwrite = target) {
|
|
const result = await createCdnAccess(
|
|
{
|
|
organizationSlug: organization.slug,
|
|
projectSlug: project.slug,
|
|
targetSlug: ttarget.slug,
|
|
},
|
|
ownerToken,
|
|
).then(r => r.expectNoGraphQLErrors());
|
|
expect(result.createCdnAccessToken.ok).not.toBeNull();
|
|
|
|
const data = result.createCdnAccessToken.ok!;
|
|
|
|
return {
|
|
secretAccessToken: data.secretAccessToken,
|
|
cdnUrl: data.cdnUrl,
|
|
fetchSchemaFromCDN() {
|
|
return fetchSchemaFromCDN(data.cdnUrl, data.secretAccessToken);
|
|
},
|
|
fetchMetadataFromCDN() {
|
|
return fetchMetadataFromCDN(data.cdnUrl, data.secretAccessToken);
|
|
},
|
|
fetchSupergraphFromCDN() {
|
|
return fetchSupergraphFromCDN(data.cdnUrl, data.secretAccessToken);
|
|
},
|
|
};
|
|
},
|
|
async toggleTargetValidation(
|
|
isEnabled: boolean,
|
|
ttarget: TargetOverwrite = target,
|
|
) {
|
|
const result = await updateTargetValidationSettings(
|
|
{
|
|
target: {
|
|
bySelector: {
|
|
organizationSlug: organization.slug,
|
|
projectSlug: project.slug,
|
|
targetSlug: ttarget.slug,
|
|
},
|
|
},
|
|
conditionalBreakingChangeConfiguration: {
|
|
isEnabled,
|
|
},
|
|
},
|
|
{
|
|
token: ownerToken,
|
|
},
|
|
).then(r => r.expectNoGraphQLErrors());
|
|
|
|
return result.updateTargetConditionalBreakingChangeConfiguration.ok!.target
|
|
.conditionalBreakingChangeConfiguration;
|
|
},
|
|
async updateTargetValidationSettings({
|
|
isEnabled,
|
|
excludedClients,
|
|
excludedAppDeployments,
|
|
percentage,
|
|
target: ttarget = target,
|
|
requestCount,
|
|
breakingChangeFormula,
|
|
}: GraphQLSchema.ConditionalBreakingChangeConfigurationInput & {
|
|
target?: TargetOverwrite;
|
|
}) {
|
|
const result = await updateTargetValidationSettings(
|
|
{
|
|
target: {
|
|
bySelector: {
|
|
organizationSlug: organization.slug,
|
|
projectSlug: project.slug,
|
|
targetSlug: ttarget.slug,
|
|
},
|
|
},
|
|
conditionalBreakingChangeConfiguration: {
|
|
isEnabled,
|
|
excludedClients,
|
|
excludedAppDeployments,
|
|
percentage,
|
|
requestCount,
|
|
breakingChangeFormula,
|
|
period: 2,
|
|
targetIds: [target.id],
|
|
},
|
|
},
|
|
{
|
|
token: ownerToken,
|
|
},
|
|
).then(r => r.expectNoGraphQLErrors());
|
|
|
|
return result.updateTargetConditionalBreakingChangeConfiguration;
|
|
},
|
|
async compareToPreviousVersion(version: string, ttarget: TargetOverwrite = target) {
|
|
return (
|
|
await compareToPreviousVersion(
|
|
{
|
|
organizationSlug: organization.slug,
|
|
projectSlug: project.slug,
|
|
targetSlug: ttarget.slug,
|
|
version,
|
|
},
|
|
ownerToken,
|
|
)
|
|
).expectNoGraphQLErrors();
|
|
},
|
|
async readOperationBody(hash: string, ttarget: TargetOverwrite = target) {
|
|
const operationBodyResult = await readOperationBody(
|
|
{
|
|
organizationSlug: organization.slug,
|
|
projectSlug: project.slug,
|
|
targetSlug: ttarget.slug,
|
|
hash,
|
|
},
|
|
ownerToken,
|
|
).then(r => r.expectNoGraphQLErrors());
|
|
|
|
return operationBodyResult?.target?.operation?.body;
|
|
},
|
|
async waitForOperationsCollected(
|
|
n: number,
|
|
_from?: number,
|
|
_to?: number,
|
|
ttarget: TargetOverwrite = target,
|
|
) {
|
|
const from = formatISO(_from ?? subHours(Date.now(), 1));
|
|
const to = formatISO(_to ?? Date.now());
|
|
const check = async () => {
|
|
const statsResult = await readOperationsStats(
|
|
{
|
|
bySelector: {
|
|
organizationSlug: organization.slug,
|
|
projectSlug: project.slug,
|
|
targetSlug: ttarget.slug,
|
|
},
|
|
},
|
|
{
|
|
from,
|
|
to,
|
|
},
|
|
{},
|
|
ownerToken,
|
|
).then(r => r.expectNoGraphQLErrors());
|
|
return statsResult.target?.operationsStats.totalOperations == n;
|
|
};
|
|
|
|
return pollFor(check);
|
|
},
|
|
async waitForRequestsCollected(
|
|
n: number,
|
|
opts?: {
|
|
from?: number;
|
|
to?: number;
|
|
target?: TargetOverwrite;
|
|
},
|
|
) {
|
|
const from = formatISO(opts?.from ?? subHours(Date.now(), 1));
|
|
const to = formatISO(opts?.to ?? Date.now());
|
|
const check = async () => {
|
|
const statsResult = await readOperationsStats(
|
|
{
|
|
bySelector: {
|
|
organizationSlug: organization.slug,
|
|
projectSlug: project.slug,
|
|
targetSlug: (opts?.target ?? target).slug,
|
|
},
|
|
},
|
|
{
|
|
from,
|
|
to,
|
|
},
|
|
{},
|
|
ownerToken,
|
|
).then(r => r.expectNoGraphQLErrors());
|
|
const totalRequests =
|
|
statsResult.target?.operationsStats.operations.edges.reduce(
|
|
(total, edge) => total + edge.node.count,
|
|
0,
|
|
);
|
|
return totalRequests == n;
|
|
};
|
|
|
|
return pollFor(check);
|
|
},
|
|
async readOperationsStats(
|
|
from: string,
|
|
to: string,
|
|
ttarget: TargetOverwrite = target,
|
|
filter: GraphQLSchema.OperationStatsFilterInput = {},
|
|
) {
|
|
const statsResult = await readOperationsStats(
|
|
{ byId: ttarget.id },
|
|
{
|
|
from,
|
|
to,
|
|
},
|
|
filter,
|
|
ownerToken,
|
|
).then(r => r.expectNoGraphQLErrors());
|
|
|
|
return statsResult.target?.operationsStats!;
|
|
},
|
|
async readClientStats(params: { clientName: string; from: string; to: string }) {
|
|
const statsResult = await readClientStats(
|
|
{
|
|
byId: target.id,
|
|
},
|
|
{
|
|
from: params.from,
|
|
to: params.to,
|
|
},
|
|
params.clientName,
|
|
ownerToken,
|
|
).then(r => r.expectNoGraphQLErrors());
|
|
|
|
return statsResult.target?.clientStats!;
|
|
},
|
|
async updateBaseSchema(newBase: string, ttarget: TargetOverwrite = target) {
|
|
const result = await updateBaseSchema(
|
|
{
|
|
newBase,
|
|
organizationSlug: organization.slug,
|
|
projectSlug: project.slug,
|
|
targetSlug: ttarget.slug,
|
|
},
|
|
ownerToken,
|
|
).then(r => r.expectNoGraphQLErrors());
|
|
|
|
return result.updateBaseSchema;
|
|
},
|
|
async fetchVersions(count: number, ttarget: TargetOverwrite = target) {
|
|
const result = await fetchVersions(
|
|
{
|
|
organizationSlug: organization.slug,
|
|
projectSlug: project.slug,
|
|
targetSlug: ttarget.slug,
|
|
},
|
|
count,
|
|
ownerToken,
|
|
).then(r => r.expectNoGraphQLErrors());
|
|
|
|
if (!result.target) {
|
|
throw new Error('Could not find target');
|
|
}
|
|
|
|
return result.target?.schemaVersions.edges.map(edge => edge.node);
|
|
},
|
|
async createTarget(args?: { slug?: string; accessToken?: string }) {
|
|
return createTarget(
|
|
{
|
|
project: {
|
|
bySelector: {
|
|
organizationSlug: orgSlug,
|
|
projectSlug: project.slug,
|
|
},
|
|
},
|
|
slug: args?.slug ?? generateUnique(),
|
|
},
|
|
args?.accessToken ?? ownerToken,
|
|
);
|
|
},
|
|
};
|
|
},
|
|
async inviteAndJoinMember(options?: {
|
|
inviteToken?: string;
|
|
memberRoleId?: string | undefined;
|
|
oidcIntegrationId?: string | undefined;
|
|
resources?: GraphQLSchema.ResourceAssignmentInput | undefined;
|
|
}) {
|
|
const { inviteToken, memberRoleId, oidcIntegrationId, resources } = Object.assign(
|
|
options ?? {},
|
|
{
|
|
inviteToken: ownerToken,
|
|
},
|
|
);
|
|
const memberEmail = userEmail(generateUnique());
|
|
const memberToken = await doAuthenticate(memberEmail, {
|
|
oidcIntegrationId,
|
|
verifyEmail: true,
|
|
}).then(r => r.access_token);
|
|
|
|
if (!oidcIntegrationId) {
|
|
const invitationResult = await inviteToOrganization(
|
|
{
|
|
organization: {
|
|
bySelector: {
|
|
organizationSlug: organization.slug,
|
|
},
|
|
},
|
|
email: memberEmail,
|
|
memberRoleId,
|
|
resources,
|
|
},
|
|
inviteToken,
|
|
).then(r => r.expectNoGraphQLErrors());
|
|
const code =
|
|
invitationResult.inviteToOrganizationByEmail.ok?.createdOrganizationInvitation
|
|
.code;
|
|
|
|
if (!code) {
|
|
throw new Error(
|
|
`Could not create invitation for ${memberEmail} to join org ${organization.slug}`,
|
|
);
|
|
}
|
|
|
|
const joinResult = await joinOrganization(code, memberToken).then(r =>
|
|
r.expectNoGraphQLErrors(),
|
|
);
|
|
|
|
if (joinResult.joinOrganization.__typename !== 'OrganizationPayload') {
|
|
throw new Error(
|
|
`Member ${memberEmail} could not join organization ${organization.slug}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
const orgAfterJoin = await getOrganization(organization.slug, memberToken).then(r =>
|
|
r.expectNoGraphQLErrors(),
|
|
);
|
|
const member = orgAfterJoin.organization?.me;
|
|
|
|
if (!member) {
|
|
throw new Error(
|
|
`Could not retrieve membership for ${memberEmail} in ${organization.slug} after joining`,
|
|
);
|
|
}
|
|
|
|
return {
|
|
member,
|
|
memberEmail,
|
|
memberToken,
|
|
async assignMemberRole(
|
|
input: {
|
|
roleId: string;
|
|
userId: string;
|
|
resources?: GraphQLSchema.ResourceAssignmentInput;
|
|
},
|
|
options: { useMemberToken?: boolean } = {
|
|
useMemberToken: false,
|
|
},
|
|
) {
|
|
const memberRoleAssignmentResult = await assignMemberRole(
|
|
{
|
|
organization: {
|
|
bySelector: {
|
|
organizationSlug: organization.slug,
|
|
},
|
|
},
|
|
member: {
|
|
byId: input.userId,
|
|
},
|
|
memberRole: {
|
|
byId: input.roleId,
|
|
},
|
|
resources: input.resources ?? {
|
|
mode: GraphQLSchema.ResourceAssignmentModeType.All,
|
|
projects: [],
|
|
},
|
|
},
|
|
options.useMemberToken ? memberToken : ownerToken,
|
|
).then(r => r.expectNoGraphQLErrors());
|
|
|
|
if (memberRoleAssignmentResult.assignMemberRole.error) {
|
|
throw new Error(memberRoleAssignmentResult.assignMemberRole.error.message);
|
|
}
|
|
|
|
return memberRoleAssignmentResult.assignMemberRole.ok?.updatedMember;
|
|
},
|
|
async deleteMemberRole(
|
|
memberRoleId: string,
|
|
options: { useMemberToken?: boolean } = {
|
|
useMemberToken: false,
|
|
},
|
|
) {
|
|
const memberRoleDeletionResult = await deleteMemberRole(
|
|
{
|
|
memberRole: {
|
|
byId: memberRoleId,
|
|
},
|
|
},
|
|
options.useMemberToken ? memberToken : ownerToken,
|
|
).then(r => r.expectNoGraphQLErrors());
|
|
|
|
if (memberRoleDeletionResult.deleteMemberRole.error) {
|
|
throw new Error(memberRoleDeletionResult.deleteMemberRole.error.message);
|
|
}
|
|
|
|
return memberRoleDeletionResult.deleteMemberRole.ok?.updatedOrganization;
|
|
},
|
|
async createMemberRole(
|
|
permissions: Array<string>,
|
|
options: { useMemberToken?: boolean } = {
|
|
useMemberToken: false,
|
|
},
|
|
) {
|
|
const name = humanId({
|
|
separator: '',
|
|
adjectiveCount: 1,
|
|
addAdverb: true,
|
|
capitalize: true,
|
|
});
|
|
const memberRoleCreationResult = await createMemberRole(
|
|
{
|
|
organization: {
|
|
bySelector: {
|
|
organizationSlug: organization.slug,
|
|
},
|
|
},
|
|
name,
|
|
description: 'some description',
|
|
selectedPermissions: permissions,
|
|
},
|
|
options.useMemberToken ? memberToken : ownerToken,
|
|
).then(r => r.expectNoGraphQLErrors());
|
|
|
|
if (memberRoleCreationResult.createMemberRole.error) {
|
|
if (memberRoleCreationResult.createMemberRole.error.inputErrors?.name) {
|
|
throw new Error(
|
|
memberRoleCreationResult.createMemberRole.error.inputErrors.name,
|
|
);
|
|
}
|
|
if (memberRoleCreationResult.createMemberRole.error.inputErrors?.description) {
|
|
throw new Error(
|
|
memberRoleCreationResult.createMemberRole.error.inputErrors.description,
|
|
);
|
|
}
|
|
|
|
throw new Error(memberRoleCreationResult.createMemberRole.error.message);
|
|
}
|
|
|
|
const createdRole =
|
|
memberRoleCreationResult.createMemberRole.ok?.updatedOrganization.memberRoles?.edges.find(
|
|
e => e.node.name === name,
|
|
)?.node;
|
|
|
|
if (!createdRole) {
|
|
throw new Error(
|
|
`Could not find created member role for org ${organization.slug}`,
|
|
);
|
|
}
|
|
|
|
return createdRole;
|
|
},
|
|
async updateMemberRole(
|
|
role: {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
},
|
|
permissions: Array<string>,
|
|
options: { useMemberToken?: boolean } = {
|
|
useMemberToken: false,
|
|
},
|
|
) {
|
|
const memberRoleUpdateResult = await updateMemberRole(
|
|
{
|
|
memberRole: {
|
|
byId: role.id,
|
|
},
|
|
name: role.name,
|
|
description: role.description,
|
|
selectedPermissions: permissions,
|
|
},
|
|
options.useMemberToken ? memberToken : ownerToken,
|
|
).then(r => r.expectNoGraphQLErrors());
|
|
|
|
if (memberRoleUpdateResult.updateMemberRole.error) {
|
|
if (memberRoleUpdateResult.updateMemberRole.error.inputErrors?.name) {
|
|
throw new Error(
|
|
memberRoleUpdateResult.updateMemberRole.error.inputErrors.name,
|
|
);
|
|
}
|
|
if (memberRoleUpdateResult.updateMemberRole.error.inputErrors?.description) {
|
|
throw new Error(
|
|
memberRoleUpdateResult.updateMemberRole.error.inputErrors.description,
|
|
);
|
|
}
|
|
|
|
throw new Error(memberRoleUpdateResult.updateMemberRole.error.message);
|
|
}
|
|
|
|
const updatedRole = memberRoleUpdateResult.updateMemberRole.ok?.updatedRole;
|
|
|
|
if (!updatedRole) {
|
|
throw new Error(
|
|
`Could not find the updated member role for org ${organization.slug}`,
|
|
);
|
|
}
|
|
|
|
return updatedRole;
|
|
},
|
|
};
|
|
},
|
|
createOIDCIntegration() {
|
|
return createOIDCIntegration({
|
|
organizationId: organization.id,
|
|
accessToken: ownerToken,
|
|
getPool: getPool,
|
|
});
|
|
},
|
|
};
|
|
},
|
|
};
|
|
},
|
|
};
|
|
}
|
|
|
|
type TargetOverwrite = {
|
|
__typename?: 'Target';
|
|
id: string;
|
|
slug: string;
|
|
};
|