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 createConnectionPool() { const pg = { user: ensureEnv('POSTGRES_USER'), password: ensureEnv('POSTGRES_PASSWORD'), host: ensureEnv('POSTGRES_HOST'), port: ensureEnv('POSTGRES_PORT'), db: ensureEnv('POSTGRES_DB'), }; return createPostgresDatabasePool({ connectionParameters: `postgres://${pg.user}:${pg.password}@${pg.host}:${pg.port}/${pg.db}?sslmode=disable`, }); } async function createDbConnection() { const pool = await createConnectionPool(); return { pool, [Symbol.asyncDispose]: async () => { await pool.end(); }, }; } let sharedDBPoolPromise: ReturnType; 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, 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; 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[0], ) { const result = await addAlert(input, input.token || ownerToken).then(r => r.expectNoGraphQLErrors(), ); return result.addAlert; }, async addAlertChannel( input: { token?: string } & Parameters[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, 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, 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; };