console/integration-tests/testkit/seed.ts
2026-04-02 12:43:37 +02:00

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.fragment`, `)}) 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;
};