Skip to content

Commit

Permalink
Get Started checklist (#232)
Browse files Browse the repository at this point in the history
  • Loading branch information
kamilkisiela committed Jul 11, 2022
1 parent f6ccd01 commit 0fd7e9e
Show file tree
Hide file tree
Showing 26 changed files with 843 additions and 142 deletions.
1 change: 1 addition & 0 deletions codegen.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ generates:
BillingPaymentMethod: 'StripeTypes.PaymentMethod.Card'
BillingDetails: 'StripeTypes.PaymentMethod.BillingDetails'
BillingInvoice: 'StripeTypes.Invoice'
OrganizationGetStarted: ../shared/entities#OrganizationGetStarted as OrganizationGetStartedMapper
plugins:
- add:
content: "import { StripeTypes } from '@hive/stripe-billing';"
Expand Down
25 changes: 25 additions & 0 deletions integration-tests/testkit/flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,31 @@ export function createOrganization(input: CreateOrganizationInput, authToken: st
});
}

export function getOrganizationGetStartedProgress(organizationId: string, authToken: string) {
return execute({
document: gql(/* GraphQL */ `
query getOrganizationGetStartedProgress($organizationId: ID!) {
organization(selector: { organization: $organizationId }) {
organization {
getStarted {
creatingProject
publishingSchema
checkingSchema
invitingMembers
reportingOperations
enablingUsageBasedBreakingChanges
}
}
}
}
`),
authToken,
variables: {
organizationId,
},
});
}

export function renameOrganization(input: UpdateOrganizationNameInput, authToken: string) {
return execute({
document: gql(/* GraphQL */ `
Expand Down
229 changes: 229 additions & 0 deletions integration-tests/tests/api/organization/get-started.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import {
createOrganization,
getOrganizationGetStartedProgress,
createProject,
createToken,
publishSchema,
checkSchema,
joinOrganization,
waitFor,
setTargetValidation,
} from '../../../testkit/flow';
import { authenticate } from '../../../testkit/auth';
import { collect } from '../../../testkit/usage';
import { TargetAccessScope, ProjectType, ProjectAccessScope, OrganizationAccessScope } from '@app/gql/graphql';

async function getSteps({ organization, token }: { organization: string; token: string }) {
const result = await getOrganizationGetStartedProgress(organization, token);

expect(result.body.errors).not.toBeDefined();

return result.body.data?.organization?.organization.getStarted;
}

test('freshly created organization has Get Started progress at 0%', async () => {
const { access_token } = await authenticate('main');
const orgResult = await createOrganization(
{
name: 'foo',
},
access_token
);
const org = orgResult.body.data!.createOrganization.ok!.createdOrganizationPayload.organization;

const steps = await getSteps({
organization: org.cleanId,
token: access_token,
});

expect(steps?.creatingProject).toBe(false);
expect(steps?.publishingSchema).toBe(false);
expect(steps?.checkingSchema).toBe(false);
expect(steps?.invitingMembers).toBe(false);
expect(steps?.reportingOperations).toBe(false);
expect(steps?.enablingUsageBasedBreakingChanges).toBe(false);
});

test('completing each step should result in updated Get Started progress', async () => {
const { access_token } = await authenticate('main');
const orgResult = await createOrganization(
{
name: 'foo',
},
access_token
);
const org = orgResult.body.data!.createOrganization.ok!.createdOrganizationPayload.organization;

// Step: creating project

const projectResult = await createProject(
{
organization: org.cleanId,
name: 'foo',
type: ProjectType.Single,
},
access_token
);

let steps = await getSteps({
organization: org.cleanId,
token: access_token,
});

expect(steps?.creatingProject).toBe(true); // modified
expect(steps?.publishingSchema).toBe(false);
expect(steps?.checkingSchema).toBe(false);
expect(steps?.invitingMembers).toBe(false);
expect(steps?.reportingOperations).toBe(false);
expect(steps?.enablingUsageBasedBreakingChanges).toBe(false);

expect(projectResult.body.errors).not.toBeDefined();

const target = projectResult.body.data?.createProject.ok?.createdTargets.find(t => t.name === 'production');
const project = projectResult.body.data?.createProject.ok?.createdProject;

if (!target || !project) {
throw new Error('Failed to create project');
}

const tokenResult = await createToken(
{
name: 'test',
organization: org.cleanId,
project: project.cleanId,
target: target.cleanId,
organizationScopes: [OrganizationAccessScope.Read],
projectScopes: [ProjectAccessScope.Read],
targetScopes: [
TargetAccessScope.Read,
TargetAccessScope.RegistryRead,
TargetAccessScope.RegistryWrite,
TargetAccessScope.Settings,
],
},
access_token
);

expect(tokenResult.body.errors).not.toBeDefined();

const token = tokenResult.body.data!.createToken.ok!.secret;

// Step: publishing schema

await publishSchema(
{
author: 'test',
commit: 'test',
sdl: 'type Query { foo: String }',
},
token
);

steps = await getSteps({
organization: org.cleanId,
token: access_token,
});

expect(steps?.creatingProject).toBe(true);
expect(steps?.publishingSchema).toBe(true); // modified
expect(steps?.checkingSchema).toBe(false);
expect(steps?.invitingMembers).toBe(false);
expect(steps?.reportingOperations).toBe(false);
expect(steps?.enablingUsageBasedBreakingChanges).toBe(false);

// Step: checking schema

await checkSchema(
{
sdl: 'type Query { foo: String bar: String }',
},
token
);

steps = await getSteps({
organization: org.cleanId,
token: access_token,
});

expect(steps?.creatingProject).toBe(true);
expect(steps?.publishingSchema).toBe(true);
expect(steps?.checkingSchema).toBe(true); // modified
expect(steps?.invitingMembers).toBe(false);
expect(steps?.reportingOperations).toBe(false);
expect(steps?.enablingUsageBasedBreakingChanges).toBe(false);

// Step: inviting members

const { access_token: member_access_token } = await authenticate('extra');
await joinOrganization(org.inviteCode, member_access_token);

steps = await getSteps({
organization: org.cleanId,
token: access_token,
});

expect(steps?.creatingProject).toBe(true);
expect(steps?.publishingSchema).toBe(true);
expect(steps?.checkingSchema).toBe(true);
expect(steps?.invitingMembers).toBe(true); // modified
expect(steps?.reportingOperations).toBe(false);
expect(steps?.enablingUsageBasedBreakingChanges).toBe(false);

// Step: reporting operations

await collect({
operations: [
{
operationName: 'foo',
operation: 'query foo { foo }',
fields: ['Query', 'Query.foo'],
execution: {
duration: 2_000_000,
ok: true,
errorsTotal: 0,
},
},
],
token,
authorizationHeader: 'authorization',
});
await waitFor(10_000);

steps = await getSteps({
organization: org.cleanId,
token: access_token,
});

expect(steps?.creatingProject).toBe(true);
expect(steps?.publishingSchema).toBe(true);
expect(steps?.checkingSchema).toBe(true);
expect(steps?.invitingMembers).toBe(true);
expect(steps?.reportingOperations).toBe(true); // modified
expect(steps?.enablingUsageBasedBreakingChanges).toBe(false);

// Step: reporting operations

await setTargetValidation(
{
enabled: true,
target: target.cleanId,
project: project.cleanId,
organization: org.cleanId,
},
{
token,
}
);

steps = await getSteps({
organization: org.cleanId,
token: access_token,
});

expect(steps?.creatingProject).toBe(true);
expect(steps?.publishingSchema).toBe(true);
expect(steps?.checkingSchema).toBe(true);
expect(steps?.invitingMembers).toBe(true);
expect(steps?.reportingOperations).toBe(true);
expect(steps?.enablingUsageBasedBreakingChanges).toBe(true); // modified
});
2 changes: 1 addition & 1 deletion packages/libraries/client/src/version.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const version = '0.16.0';
export const version = '0.17.0';
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,8 @@ export default gql`
duration: Int!
count: SafeInt!
}
extend type OrganizationGetStarted {
reportingOperations: Boolean!
}
`;
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { cache } from '../../../shared/helpers';
import { AuthManager } from '../../auth/providers/auth-manager';
import { TargetAccessScope } from '../../auth/providers/target-access';
import { Logger } from '../../shared/providers/logger';
import type { TargetSelector } from '../../shared/providers/storage';
import type { TargetSelector, OrganizationSelector } from '../../shared/providers/storage';
import { Storage } from '../../shared/providers/storage';
import { OperationsReader } from './operations-reader';

const DAY_IN_MS = 86_400_000;
Expand Down Expand Up @@ -57,7 +58,12 @@ interface ReadFieldStatsOutput {
export class OperationsManager {
private logger: Logger;

constructor(logger: Logger, private authManager: AuthManager, private reader: OperationsReader) {
constructor(
logger: Logger,
private authManager: AuthManager,
private reader: OperationsReader,
private storage: Storage
) {
this.logger = logger.child({ source: 'OperationsManager' });
}

Expand Down Expand Up @@ -435,4 +441,26 @@ export class OperationsManager {
operations,
});
}

async hasOperationsForOrganization(selector: OrganizationSelector): Promise<boolean> {
const targets = await this.storage.getTargetIdsOfOrganization(selector);

if (targets.length === 0) {
return false;
}

const total = await this.reader.countOperationsForTargets({
targets,
});

if (total > 0) {
await this.storage.completeGetStartedStep({
organization: selector.organization,
step: 'reportingOperations',
});
return true;
}

return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,26 @@ export class OperationsReader {
});
}

async countOperationsForTargets({ targets }: { targets: readonly string[] }): Promise<number> {
const result = await this.clickHouse.query<{
total: string;
}>({
query: `SELECT sum(total) as total from operations_new_hourly_mv WHERE target IN ('${targets.join(`', '`)}')`,
queryId: 'count_operations_for_targets',
timeout: 15_000,
});

if (result.data.length === 0) {
return 0;
}

if (result.data.length > 1) {
throw new Error('Too many rows returned, expected 1');
}

return ensureNumber(result.data[0].total);
}

async adminCountOperationsPerTarget({ daysLimit }: { daysLimit: number }) {
const result = await this.clickHouse.query<{
total: string;
Expand Down
11 changes: 11 additions & 0 deletions packages/services/api/src/modules/operations/resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,17 @@ export const resolvers: OperationsModule.Resolvers = {
},
OperationStatsConnection: createConnection(),
ClientStatsConnection: createConnection(),
OrganizationGetStarted: {
reportingOperations(organization, _, { injector }) {
if (organization.reportingOperations === true) {
return organization.reportingOperations;
}

return injector.get(OperationsManager).hasOperationsForOrganization({
organization: organization.id,
});
},
},
};

function transformPercentile(value: number | null): number {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export default gql`
me: Member!
members: MemberConnection!
inviteCode: String!
getStarted: OrganizationGetStarted!
}
type OrganizationConnection {
Expand All @@ -114,4 +115,12 @@ export default gql`
selector: OrganizationSelector!
organization: Organization!
}
type OrganizationGetStarted {
creatingProject: Boolean!
publishingSchema: Boolean!
checkingSchema: Boolean!
invitingMembers: Boolean!
enablingUsageBasedBreakingChanges: Boolean!
}
`;

0 comments on commit 0fd7e9e

Please sign in to comment.