feat: user usage gql & test case improve (#5076)

This commit is contained in:
DarkSky
2023-12-14 09:50:41 +00:00
parent ad23ead5e4
commit a93c12e122
22 changed files with 364 additions and 132 deletions

View File

@@ -28,7 +28,6 @@ model User {
invoices UserInvoice[] invoices UserInvoice[]
workspacePermissions WorkspaceUserPermission[] workspacePermissions WorkspaceUserPermission[]
pagePermissions WorkspacePageUserPermission[] pagePermissions WorkspacePageUserPermission[]
UserQuotaGates UserQuotaGates[]
@@map("users") @@map("users")
} }
@@ -158,33 +157,6 @@ model Features {
@@map("features") @@map("features")
} }
// quota gates is a way to enable/disable quotas for a user
// for example, pro plan is a quota that allow some users access to more resources after they pay
model UserQuotaGates {
id String @id @default(uuid()) @db.VarChar
userId String @map("user_id") @db.VarChar
quotaId String? @db.VarChar
reason String @db.VarChar
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
quota UserQuotas? @relation(fields: [quotaId], references: [id])
@@map("user_quota_gates")
}
model UserQuotas {
id String @id @default(uuid()) @db.VarChar
quota String @db.VarChar
configs Json @db.Json
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
UserQuotaGates UserQuotaGates[]
@@map("user_quotas")
}
model Account { model Account {
id String @id @default(cuid()) id String @id @default(cuid())
userId String @map("user_id") userId String @map("user_id")

View File

@@ -64,35 +64,8 @@ export class RunCommand extends CommandRunner {
continue; continue;
} }
this.logger.log(`Running ${migration.name}...`); await this.runMigration(migration);
const record = await this.db.dataMigration.create({
data: {
name: migration.name,
startedAt: new Date(),
},
});
try {
await migration.up(this.db);
} catch (e) {
await this.db.dataMigration.delete({
where: {
id: record.id,
},
});
await migration.down(this.db);
this.logger.error('Failed to run data migration', e);
process.exit(1);
}
await this.db.dataMigration.update({
where: {
id: record.id,
},
data: {
finishedAt: new Date(),
},
});
done.push(migration); done.push(migration);
} }
@@ -101,6 +74,56 @@ export class RunCommand extends CommandRunner {
this.logger.log(`${migration.name}`); this.logger.log(`${migration.name}`);
}); });
} }
async runOne(name: string) {
const migrations = await collectMigrations();
const migration = migrations.find(m => m.name === name);
if (!migration) {
throw new Error(`Unknown migration name: ${name}.`);
}
const exists = await this.db.dataMigration.count({
where: {
name: migration.name,
},
});
if (exists) return;
await this.runMigration(migration);
}
private async runMigration(migration: Migration) {
this.logger.log(`Running ${migration.name}...`);
const record = await this.db.dataMigration.create({
data: {
name: migration.name,
startedAt: new Date(),
},
});
try {
await migration.up(this.db);
} catch (e) {
await this.db.dataMigration.delete({
where: {
id: record.id,
},
});
await migration.down(this.db);
this.logger.error('Failed to run data migration', e);
process.exit(1);
}
await this.db.dataMigration.update({
where: {
id: record.id,
},
data: {
finishedAt: new Date(),
},
});
}
} }
@Command({ @Command({

View File

@@ -29,6 +29,7 @@ import { GQLLoggerPlugin } from './graphql/logger-plugin';
context: ({ req, res }: { req: Request; res: Response }) => ({ context: ({ req, res }: { req: Request; res: Response }) => ({
req, req,
res, res,
isAdminQuery: false,
}), }),
plugins: [new GQLLoggerPlugin()], plugins: [new GQLLoggerPlugin()],
}; };

View File

@@ -1,6 +1,7 @@
import { HttpStatus } from '@nestjs/common'; import { HttpStatus } from '@nestjs/common';
import { import {
Args, Args,
Context,
Field, Field,
Int, Int,
Mutation, Mutation,
@@ -254,8 +255,13 @@ export class UserSubscriptionResolver {
constructor(private readonly db: PrismaService) {} constructor(private readonly db: PrismaService) {}
@ResolveField(() => UserSubscriptionType, { nullable: true }) @ResolveField(() => UserSubscriptionType, { nullable: true })
async subscription(@CurrentUser() me: User, @Parent() user: User) { async subscription(
if (me.id !== user.id) { @Context() ctx: { isAdminQuery: boolean },
@CurrentUser() me: User,
@Parent() user: User
) {
// allow admin to query other user's subscription
if (!ctx.isAdminQuery && me.id !== user.id) {
throw new GraphQLError( throw new GraphQLError(
'You are not allowed to access this subscription', 'You are not allowed to access this subscription',
{ {

View File

@@ -17,5 +17,4 @@ import { QuotaManagementService } from './storage';
export class QuotaModule {} export class QuotaModule {}
export { QuotaManagementService, QuotaService }; export { QuotaManagementService, QuotaService };
export { PrismaService } from '../../prisma';
export { Quota_FreePlanV1, Quota_ProPlanV1, Quotas, QuotaType } from './types'; export { Quota_FreePlanV1, Quota_ProPlanV1, Quotas, QuotaType } from './types';

View File

@@ -6,6 +6,7 @@ import {
} from '@nestjs/common'; } from '@nestjs/common';
import { import {
Args, Args,
Context,
Field, Field,
ID, ID,
Int, Int,
@@ -267,10 +268,15 @@ export class UserResolver {
}, },
}) })
@Query(() => [UserType]) @Query(() => [UserType])
async listEarlyAccess(@CurrentUser() user: UserType): Promise<UserType[]> { async earlyAccessUsers(
@Context() ctx: { isAdminQuery: boolean },
@CurrentUser() user: UserType
): Promise<UserType[]> {
if (!this.feature.isStaff(user.email)) { if (!this.feature.isStaff(user.email)) {
throw new ForbiddenException('You are not allowed to do this'); throw new ForbiddenException('You are not allowed to do this');
} }
// allow query other user's subscription
ctx.isAdminQuery = true;
return this.feature.listEarlyAccess(); return this.feature.listEarlyAccess();
} }
} }

View File

@@ -269,7 +269,7 @@ type Query {
"""Get user by email""" """Get user by email"""
user(email: String!): UserType user(email: String!): UserType
listEarlyAccess: [UserType!]! earlyAccessUsers: [UserType!]!
prices: [SubscriptionPrice!]! prices: [SubscriptionPrice!]!
} }

View File

@@ -126,7 +126,6 @@ test('should find default user', async t => {
}) })
.expect(200) .expect(200)
.expect(res => { .expect(res => {
console.log(res.body);
t.is(res.body.data.user.email, 'alex.yang@example.org'); t.is(res.body.data.user.email, 'alex.yang@example.org');
}); });
}); });

View File

@@ -9,16 +9,13 @@ import ava, { type TestFn } from 'ava';
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'; import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
import { AppModule } from '../src/app'; import { AppModule } from '../src/app';
import { import { RevertCommand, RunCommand } from '../src/data/commands/run';
collectMigrations,
RevertCommand,
RunCommand,
} from '../src/data/commands/run';
import { MailService } from '../src/modules/auth/mailer'; import { MailService } from '../src/modules/auth/mailer';
import { AuthService } from '../src/modules/auth/service'; import { AuthService } from '../src/modules/auth/service';
import { import {
changeEmail, changeEmail,
createWorkspace, createWorkspace,
initFeatureConfigs,
sendChangeEmail, sendChangeEmail,
sendVerifyChangeEmail, sendVerifyChangeEmail,
signUp, signUp,
@@ -60,11 +57,7 @@ test.beforeEach(async t => {
t.context.mail = mail; t.context.mail = mail;
// init features // init features
const run = module.get(RunCommand); await initFeatureConfigs(module);
const revert = module.get(RevertCommand);
const migrations = await collectMigrations();
await Promise.allSettled(migrations.map(m => revert.run([m.name])));
await run.run();
}); });
test.afterEach(async t => { test.afterEach(async t => {

View File

@@ -4,11 +4,7 @@ import { PrismaClient } from '@prisma/client';
import test from 'ava'; import test from 'ava';
import { ConfigModule } from '../src/config'; import { ConfigModule } from '../src/config';
import { import { RevertCommand, RunCommand } from '../src/data/commands/run';
collectMigrations,
RevertCommand,
RunCommand,
} from '../src/data/commands/run';
import { GqlModule } from '../src/graphql.module'; import { GqlModule } from '../src/graphql.module';
import { AuthModule } from '../src/modules/auth'; import { AuthModule } from '../src/modules/auth';
import { AuthResolver } from '../src/modules/auth/resolver'; import { AuthResolver } from '../src/modules/auth/resolver';
@@ -16,6 +12,7 @@ import { AuthService } from '../src/modules/auth/service';
import { PrismaModule } from '../src/prisma'; import { PrismaModule } from '../src/prisma';
import { mintChallengeResponse, verifyChallengeResponse } from '../src/storage'; import { mintChallengeResponse, verifyChallengeResponse } from '../src/storage';
import { RateLimiterModule } from '../src/throttler'; import { RateLimiterModule } from '../src/throttler';
import { initFeatureConfigs } from './utils';
let authService: AuthService; let authService: AuthService;
let authResolver: AuthResolver; let authResolver: AuthResolver;
@@ -53,11 +50,7 @@ test.beforeEach(async () => {
authResolver = module.get(AuthResolver); authResolver = module.get(AuthResolver);
// init features // init features
const run = module.get(RunCommand); await initFeatureConfigs(module);
const revert = module.get(RevertCommand);
const migrations = await collectMigrations();
await Promise.allSettled(migrations.map(m => revert.run([m.name])));
await run.run();
}); });
test.afterEach.always(async () => { test.afterEach.always(async () => {

View File

@@ -0,0 +1,140 @@
/// <reference types="../src/global.d.ts" />
import { Test, TestingModule } from '@nestjs/testing';
import { PrismaClient } from '@prisma/client';
import ava, { type TestFn } from 'ava';
import { ConfigModule } from '../src/config';
import { RevertCommand, RunCommand } from '../src/data/commands/run';
import { AuthModule } from '../src/modules/auth';
import { AuthService } from '../src/modules/auth/service';
import {
FeatureManagementService,
FeatureModule,
FeatureService,
FeatureType,
} from '../src/modules/features';
import { PrismaModule } from '../src/prisma';
import { RateLimiterModule } from '../src/throttler';
import { initFeatureConfigs } from './utils';
const test = ava as TestFn<{
auth: AuthService;
feature: FeatureService;
early_access: FeatureManagementService;
app: TestingModule;
}>;
// cleanup database before each test
test.beforeEach(async () => {
const client = new PrismaClient();
await client.$connect();
await client.user.deleteMany({});
await client.$disconnect();
});
test.beforeEach(async t => {
const module = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
auth: {
accessTokenExpiresIn: 1,
refreshTokenExpiresIn: 1,
leeway: 1,
},
host: 'example.org',
https: true,
featureFlags: {
earlyAccessPreview: true,
},
}),
PrismaModule,
AuthModule,
FeatureModule,
RateLimiterModule,
RevertCommand,
RunCommand,
],
}).compile();
const quota = module.get(FeatureService);
const storageQuota = module.get(FeatureManagementService);
const auth = module.get(AuthService);
t.context.app = module;
t.context.feature = quota;
t.context.early_access = storageQuota;
t.context.auth = auth;
// init features
await initFeatureConfigs(module);
});
test.afterEach.always(async t => {
await t.context.app.close();
});
test('should be able to set feature', async t => {
const { auth, feature } = t.context;
const u1 = await auth.signUp('DarkSky', 'darksky@example.org', '123456');
const f1 = await feature.getUserFeatures(u1.id);
t.is(f1.length, 0, 'should be empty');
await feature.addUserFeature(u1.id, FeatureType.EarlyAccess, 1, 'test');
const f2 = await feature.getUserFeatures(u1.id);
t.is(f2.length, 1, 'should have one feature');
t.is(
f2[0].feature.feature,
FeatureType.EarlyAccess,
'should be early access'
);
});
test('should be able to check early access', async t => {
const { auth, feature, early_access } = t.context;
const u1 = await auth.signUp('DarkSky', 'darksky@example.org', '123456');
const f1 = await early_access.canEarlyAccess(u1.email);
t.false(f1, 'should not have early access');
await early_access.addEarlyAccess(u1.id);
const f2 = await early_access.canEarlyAccess(u1.email);
t.true(f2, 'should have early access');
const f3 = await feature.listFeatureUsers(FeatureType.EarlyAccess);
t.is(f3.length, 1, 'should have one user');
t.is(f3[0].id, u1.id, 'should be the same user');
});
test('should be able revert quota', async t => {
const { auth, feature, early_access } = t.context;
const u1 = await auth.signUp('DarkSky', 'darksky@example.org', '123456');
const f1 = await early_access.canEarlyAccess(u1.email);
t.false(f1, 'should not have early access');
await early_access.addEarlyAccess(u1.id);
const f2 = await early_access.canEarlyAccess(u1.email);
t.true(f2, 'should have early access');
const q1 = await early_access.listEarlyAccess();
t.is(q1.length, 1, 'should have one user');
t.is(q1[0].id, u1.id, 'should be the same user');
await early_access.removeEarlyAccess(u1.id);
const f3 = await early_access.canEarlyAccess(u1.email);
t.false(f3, 'should not have early access');
const q2 = await early_access.listEarlyAccess();
t.is(q2.length, 0, 'should have no user');
const q3 = await feature.getUserFeatures(u1.id);
t.is(q3.length, 1, 'should have 1 feature');
t.is(
q3[0].feature.feature,
FeatureType.EarlyAccess,
'should be early access'
);
t.is(q3[0].activated, false, 'should be deactivated');
});

View File

@@ -11,16 +11,13 @@ import { PrismaClient } from '@prisma/client';
import ava, { type TestFn } from 'ava'; import ava, { type TestFn } from 'ava';
import { ConfigModule } from '../src/config'; import { ConfigModule } from '../src/config';
import { import { RevertCommand, RunCommand } from '../src/data/commands/run';
collectMigrations,
RevertCommand,
RunCommand,
} from '../src/data/commands/run';
import { GqlModule } from '../src/graphql.module'; import { GqlModule } from '../src/graphql.module';
import { AuthModule } from '../src/modules/auth'; import { AuthModule } from '../src/modules/auth';
import { AuthService } from '../src/modules/auth/service'; import { AuthService } from '../src/modules/auth/service';
import { PrismaModule } from '../src/prisma'; import { PrismaModule } from '../src/prisma';
import { RateLimiterModule } from '../src/throttler'; import { RateLimiterModule } from '../src/throttler';
import { initFeatureConfigs } from './utils';
const test = ava as TestFn<{ const test = ava as TestFn<{
auth: AuthService; auth: AuthService;
@@ -55,11 +52,7 @@ test.beforeEach(async t => {
t.context.auth = t.context.module.get(AuthService); t.context.auth = t.context.module.get(AuthService);
// init features // init features
const run = t.context.module.get(RunCommand); await initFeatureConfigs(t.context.module);
const revert = t.context.module.get(RevertCommand);
const migrations = await collectMigrations();
await Promise.allSettled(migrations.map(m => revert.run([m.name])));
await run.run();
}); });
test.afterEach.always(async t => { test.afterEach.always(async t => {

View File

@@ -5,11 +5,7 @@ import { PrismaClient } from '@prisma/client';
import ava, { type TestFn } from 'ava'; import ava, { type TestFn } from 'ava';
import { ConfigModule } from '../src/config'; import { ConfigModule } from '../src/config';
import { import { RevertCommand, RunCommand } from '../src/data/commands/run';
collectMigrations,
RevertCommand,
RunCommand,
} from '../src/data/commands/run';
import { AuthModule } from '../src/modules/auth'; import { AuthModule } from '../src/modules/auth';
import { AuthService } from '../src/modules/auth/service'; import { AuthService } from '../src/modules/auth/service';
import { import {
@@ -22,6 +18,7 @@ import {
import { PrismaModule } from '../src/prisma'; import { PrismaModule } from '../src/prisma';
import { StorageModule } from '../src/storage'; import { StorageModule } from '../src/storage';
import { RateLimiterModule } from '../src/throttler'; import { RateLimiterModule } from '../src/throttler';
import { initFeatureConfigs } from './utils';
const test = ava as TestFn<{ const test = ava as TestFn<{
auth: AuthService; auth: AuthService;
@@ -70,11 +67,7 @@ test.beforeEach(async t => {
t.context.auth = auth; t.context.auth = auth;
// init features // init features
const run = module.get(RunCommand); await initFeatureConfigs(module);
const revert = module.get(RevertCommand);
const migrations = await collectMigrations();
await Promise.allSettled(migrations.map(m => revert.run([m.name])));
await run.run();
}); });
test.afterEach.always(async t => { test.afterEach.always(async t => {

View File

@@ -1,10 +1,12 @@
import { randomUUID } from 'node:crypto'; import { randomUUID } from 'node:crypto';
import type { INestApplication } from '@nestjs/common'; import type { INestApplication } from '@nestjs/common';
import { TestingModule } from '@nestjs/testing';
import { hashSync } from '@node-rs/argon2'; import { hashSync } from '@node-rs/argon2';
import { PrismaClient, type User } from '@prisma/client'; import { PrismaClient, type User } from '@prisma/client';
import request from 'supertest'; import request from 'supertest';
import { RevertCommand, RunCommand } from '../src/data/commands/run';
import type { TokenType } from '../src/modules/auth'; import type { TokenType } from '../src/modules/auth';
import type { UserType } from '../src/modules/users'; import type { UserType } from '../src/modules/users';
import type { InvitationType, WorkspaceType } from '../src/modules/workspaces'; import type { InvitationType, WorkspaceType } from '../src/modules/workspaces';
@@ -561,6 +563,13 @@ export class FakePrisma {
} }
} }
export async function initFeatureConfigs(module: TestingModule) {
const run = module.get(RunCommand);
const revert = module.get(RevertCommand);
await Promise.allSettled([revert.run(['UserFeaturesInit1698652531198'])]);
await run.runOne('UserFeaturesInit1698652531198');
}
export { export {
acceptInviteById, acceptInviteById,
changeEmail, changeEmail,

View File

@@ -6,17 +6,14 @@ import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
import request from 'supertest'; import request from 'supertest';
import { AppModule } from '../src/app'; import { AppModule } from '../src/app';
import { import { RevertCommand, RunCommand } from '../src/data/commands/run';
collectMigrations,
RevertCommand,
RunCommand,
} from '../src/data/commands/run';
import { QuotaService, QuotaType } from '../src/modules/quota'; import { QuotaService, QuotaType } from '../src/modules/quota';
import { import {
checkBlobSize, checkBlobSize,
collectAllBlobSizes, collectAllBlobSizes,
createWorkspace, createWorkspace,
getWorkspaceBlobsSize, getWorkspaceBlobsSize,
initFeatureConfigs,
listBlobs, listBlobs,
setBlob, setBlob,
signUp, signUp,
@@ -52,11 +49,7 @@ test.beforeEach(async () => {
quota = module.get(QuotaService); quota = module.get(QuotaService);
// init features // init features
const run = module.get(RunCommand); await initFeatureConfigs(module);
const revert = module.get(RevertCommand);
const migrations = await collectMigrations();
await Promise.allSettled(migrations.map(m => revert.run([m.name])));
await run.run();
await app.init(); await app.init();
}); });

View File

@@ -9,17 +9,14 @@ import ava, { type TestFn } from 'ava';
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'; import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
import { AppModule } from '../src/app'; import { AppModule } from '../src/app';
import { import { RevertCommand, RunCommand } from '../src/data/commands/run';
collectMigrations,
RevertCommand,
RunCommand,
} from '../src/data/commands/run';
import { MailService } from '../src/modules/auth/mailer'; import { MailService } from '../src/modules/auth/mailer';
import { AuthService } from '../src/modules/auth/service'; import { AuthService } from '../src/modules/auth/service';
import { import {
acceptInviteById, acceptInviteById,
createWorkspace, createWorkspace,
getWorkspace, getWorkspace,
initFeatureConfigs,
inviteUser, inviteUser,
leaveWorkspace, leaveWorkspace,
revokeUser, revokeUser,
@@ -63,11 +60,7 @@ test.beforeEach(async t => {
t.context.mail = mail; t.context.mail = mail;
// init features // init features
const run = module.get(RunCommand); await initFeatureConfigs(module);
const revert = module.get(RevertCommand);
const migrations = await collectMigrations();
await Promise.allSettled(migrations.map(m => revert.run([m.name])));
await run.run();
}); });
test.afterEach.always(async t => { test.afterEach.always(async t => {

View File

@@ -6,17 +6,14 @@ import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
import request from 'supertest'; import request from 'supertest';
import { AppModule } from '../src/app'; import { AppModule } from '../src/app';
import { import { RevertCommand, RunCommand } from '../src/data/commands/run';
collectMigrations,
RevertCommand,
RunCommand,
} from '../src/data/commands/run';
import { import {
acceptInviteById, acceptInviteById,
createWorkspace, createWorkspace,
currentUser, currentUser,
getPublicWorkspace, getPublicWorkspace,
getWorkspacePublicPages, getWorkspacePublicPages,
initFeatureConfigs,
inviteUser, inviteUser,
publishPage, publishPage,
revokePublicPage, revokePublicPage,
@@ -53,11 +50,7 @@ test.beforeEach(async t => {
t.context.app = app; t.context.app = app;
// init features // init features
const run = module.get(RunCommand); await initFeatureConfigs(module);
const revert = module.get(RevertCommand);
const migrations = await collectMigrations();
await Promise.allSettled(migrations.map(m => revert.run([m.name])));
await run.run();
}); });
test.afterEach.always(async t => { test.afterEach.always(async t => {

View File

@@ -0,0 +1,3 @@
mutation addToEarlyAccess($email: String!) {
addToEarlyAccess(email: $email)
}

View File

@@ -0,0 +1,17 @@
query earlyAccessUsers {
earlyAccessUsers {
id
name
email
avatarUrl
emailVerified
createdAt
subscription {
plan
recurring
status
start
end
}
}
}

View File

@@ -0,0 +1,3 @@
mutation removeEarlyAccess($email: String!) {
removeEarlyAccess(email: $email)
}

View File

@@ -188,6 +188,53 @@ mutation deleteWorkspace($id: String!) {
}`, }`,
}; };
export const addToEarlyAccessMutation = {
id: 'addToEarlyAccessMutation' as const,
operationName: 'addToEarlyAccess',
definitionName: 'addToEarlyAccess',
containsFile: false,
query: `
mutation addToEarlyAccess($email: String!) {
addToEarlyAccess(email: $email)
}`,
};
export const earlyAccessUsersQuery = {
id: 'earlyAccessUsersQuery' as const,
operationName: 'earlyAccessUsers',
definitionName: 'earlyAccessUsers',
containsFile: false,
query: `
query earlyAccessUsers {
earlyAccessUsers {
id
name
email
avatarUrl
emailVerified
createdAt
subscription {
plan
recurring
status
start
end
}
}
}`,
};
export const removeEarlyAccessMutation = {
id: 'removeEarlyAccessMutation' as const,
operationName: 'removeEarlyAccess',
definitionName: 'removeEarlyAccess',
containsFile: false,
query: `
mutation removeEarlyAccess($email: String!) {
removeEarlyAccess(email: $email)
}`,
};
export const getCurrentUserQuery = { export const getCurrentUserQuery = {
id: 'getCurrentUserQuery' as const, id: 'getCurrentUserQuery' as const,
operationName: 'getCurrentUser', operationName: 'getCurrentUser',

View File

@@ -223,6 +223,47 @@ export type DeleteWorkspaceMutation = {
deleteWorkspace: boolean; deleteWorkspace: boolean;
}; };
export type AddToEarlyAccessMutationVariables = Exact<{
email: Scalars['String']['input'];
}>;
export type AddToEarlyAccessMutation = {
__typename?: 'Mutation';
addToEarlyAccess: number;
};
export type EarlyAccessUsersQueryVariables = Exact<{ [key: string]: never }>;
export type EarlyAccessUsersQuery = {
__typename?: 'Query';
earlyAccessUsers: Array<{
__typename?: 'UserType';
id: string;
name: string;
email: string;
avatarUrl: string | null;
emailVerified: string | null;
createdAt: string | null;
subscription: {
__typename?: 'UserSubscription';
plan: SubscriptionPlan;
recurring: SubscriptionRecurring;
status: SubscriptionStatus;
start: string;
end: string;
} | null;
}>;
};
export type RemoveEarlyAccessMutationVariables = Exact<{
email: Scalars['String']['input'];
}>;
export type RemoveEarlyAccessMutation = {
__typename?: 'Mutation';
removeEarlyAccess: number;
};
export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never }>; export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never }>;
export type GetCurrentUserQuery = { export type GetCurrentUserQuery = {
@@ -698,6 +739,11 @@ export type Queries =
variables: AllBlobSizesQueryVariables; variables: AllBlobSizesQueryVariables;
response: AllBlobSizesQuery; response: AllBlobSizesQuery;
} }
| {
name: 'earlyAccessUsersQuery';
variables: EarlyAccessUsersQueryVariables;
response: EarlyAccessUsersQuery;
}
| { | {
name: 'getCurrentUserQuery'; name: 'getCurrentUserQuery';
variables: GetCurrentUserQueryVariables; variables: GetCurrentUserQueryVariables;
@@ -835,6 +881,16 @@ export type Mutations =
variables: DeleteWorkspaceMutationVariables; variables: DeleteWorkspaceMutationVariables;
response: DeleteWorkspaceMutation; response: DeleteWorkspaceMutation;
} }
| {
name: 'addToEarlyAccessMutation';
variables: AddToEarlyAccessMutationVariables;
response: AddToEarlyAccessMutation;
}
| {
name: 'removeEarlyAccessMutation';
variables: RemoveEarlyAccessMutationVariables;
response: RemoveEarlyAccessMutation;
}
| { | {
name: 'leaveWorkspaceMutation'; name: 'leaveWorkspaceMutation';
variables: LeaveWorkspaceMutationVariables; variables: LeaveWorkspaceMutationVariables;