mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-18 14:56:59 +08:00
feat: user usage gql & test case improve (#5076)
This commit is contained in:
@@ -28,7 +28,6 @@ model User {
|
||||
invoices UserInvoice[]
|
||||
workspacePermissions WorkspaceUserPermission[]
|
||||
pagePermissions WorkspacePageUserPermission[]
|
||||
UserQuotaGates UserQuotaGates[]
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
@@ -158,33 +157,6 @@ model 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 {
|
||||
id String @id @default(cuid())
|
||||
userId String @map("user_id")
|
||||
|
||||
@@ -64,35 +64,8 @@ export class RunCommand extends CommandRunner {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.logger.log(`Running ${migration.name}...`);
|
||||
const record = await this.db.dataMigration.create({
|
||||
data: {
|
||||
name: migration.name,
|
||||
startedAt: new Date(),
|
||||
},
|
||||
});
|
||||
await this.runMigration(migration);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -101,6 +74,56 @@ export class RunCommand extends CommandRunner {
|
||||
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({
|
||||
|
||||
@@ -29,6 +29,7 @@ import { GQLLoggerPlugin } from './graphql/logger-plugin';
|
||||
context: ({ req, res }: { req: Request; res: Response }) => ({
|
||||
req,
|
||||
res,
|
||||
isAdminQuery: false,
|
||||
}),
|
||||
plugins: [new GQLLoggerPlugin()],
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { HttpStatus } from '@nestjs/common';
|
||||
import {
|
||||
Args,
|
||||
Context,
|
||||
Field,
|
||||
Int,
|
||||
Mutation,
|
||||
@@ -254,8 +255,13 @@ export class UserSubscriptionResolver {
|
||||
constructor(private readonly db: PrismaService) {}
|
||||
|
||||
@ResolveField(() => UserSubscriptionType, { nullable: true })
|
||||
async subscription(@CurrentUser() me: User, @Parent() user: User) {
|
||||
if (me.id !== user.id) {
|
||||
async subscription(
|
||||
@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(
|
||||
'You are not allowed to access this subscription',
|
||||
{
|
||||
|
||||
@@ -17,5 +17,4 @@ import { QuotaManagementService } from './storage';
|
||||
export class QuotaModule {}
|
||||
|
||||
export { QuotaManagementService, QuotaService };
|
||||
export { PrismaService } from '../../prisma';
|
||||
export { Quota_FreePlanV1, Quota_ProPlanV1, Quotas, QuotaType } from './types';
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
Args,
|
||||
Context,
|
||||
Field,
|
||||
ID,
|
||||
Int,
|
||||
@@ -267,10 +268,15 @@ export class UserResolver {
|
||||
},
|
||||
})
|
||||
@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)) {
|
||||
throw new ForbiddenException('You are not allowed to do this');
|
||||
}
|
||||
// allow query other user's subscription
|
||||
ctx.isAdminQuery = true;
|
||||
return this.feature.listEarlyAccess();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,7 +269,7 @@ type Query {
|
||||
|
||||
"""Get user by email"""
|
||||
user(email: String!): UserType
|
||||
listEarlyAccess: [UserType!]!
|
||||
earlyAccessUsers: [UserType!]!
|
||||
prices: [SubscriptionPrice!]!
|
||||
}
|
||||
|
||||
|
||||
@@ -126,7 +126,6 @@ test('should find default user', async t => {
|
||||
})
|
||||
.expect(200)
|
||||
.expect(res => {
|
||||
console.log(res.body);
|
||||
t.is(res.body.data.user.email, 'alex.yang@example.org');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,16 +9,13 @@ import ava, { type TestFn } from 'ava';
|
||||
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
|
||||
|
||||
import { AppModule } from '../src/app';
|
||||
import {
|
||||
collectMigrations,
|
||||
RevertCommand,
|
||||
RunCommand,
|
||||
} from '../src/data/commands/run';
|
||||
import { RevertCommand, RunCommand } from '../src/data/commands/run';
|
||||
import { MailService } from '../src/modules/auth/mailer';
|
||||
import { AuthService } from '../src/modules/auth/service';
|
||||
import {
|
||||
changeEmail,
|
||||
createWorkspace,
|
||||
initFeatureConfigs,
|
||||
sendChangeEmail,
|
||||
sendVerifyChangeEmail,
|
||||
signUp,
|
||||
@@ -60,11 +57,7 @@ test.beforeEach(async t => {
|
||||
t.context.mail = mail;
|
||||
|
||||
// init features
|
||||
const run = module.get(RunCommand);
|
||||
const revert = module.get(RevertCommand);
|
||||
const migrations = await collectMigrations();
|
||||
await Promise.allSettled(migrations.map(m => revert.run([m.name])));
|
||||
await run.run();
|
||||
await initFeatureConfigs(module);
|
||||
});
|
||||
|
||||
test.afterEach(async t => {
|
||||
|
||||
@@ -4,11 +4,7 @@ import { PrismaClient } from '@prisma/client';
|
||||
import test from 'ava';
|
||||
|
||||
import { ConfigModule } from '../src/config';
|
||||
import {
|
||||
collectMigrations,
|
||||
RevertCommand,
|
||||
RunCommand,
|
||||
} from '../src/data/commands/run';
|
||||
import { RevertCommand, RunCommand } from '../src/data/commands/run';
|
||||
import { GqlModule } from '../src/graphql.module';
|
||||
import { AuthModule } from '../src/modules/auth';
|
||||
import { AuthResolver } from '../src/modules/auth/resolver';
|
||||
@@ -16,6 +12,7 @@ import { AuthService } from '../src/modules/auth/service';
|
||||
import { PrismaModule } from '../src/prisma';
|
||||
import { mintChallengeResponse, verifyChallengeResponse } from '../src/storage';
|
||||
import { RateLimiterModule } from '../src/throttler';
|
||||
import { initFeatureConfigs } from './utils';
|
||||
|
||||
let authService: AuthService;
|
||||
let authResolver: AuthResolver;
|
||||
@@ -53,11 +50,7 @@ test.beforeEach(async () => {
|
||||
authResolver = module.get(AuthResolver);
|
||||
|
||||
// init features
|
||||
const run = module.get(RunCommand);
|
||||
const revert = module.get(RevertCommand);
|
||||
const migrations = await collectMigrations();
|
||||
await Promise.allSettled(migrations.map(m => revert.run([m.name])));
|
||||
await run.run();
|
||||
await initFeatureConfigs(module);
|
||||
});
|
||||
|
||||
test.afterEach.always(async () => {
|
||||
|
||||
140
packages/backend/server/tests/feature.spec.ts
Normal file
140
packages/backend/server/tests/feature.spec.ts
Normal 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');
|
||||
});
|
||||
@@ -11,16 +11,13 @@ import { PrismaClient } from '@prisma/client';
|
||||
import ava, { type TestFn } from 'ava';
|
||||
|
||||
import { ConfigModule } from '../src/config';
|
||||
import {
|
||||
collectMigrations,
|
||||
RevertCommand,
|
||||
RunCommand,
|
||||
} from '../src/data/commands/run';
|
||||
import { RevertCommand, RunCommand } from '../src/data/commands/run';
|
||||
import { GqlModule } from '../src/graphql.module';
|
||||
import { AuthModule } from '../src/modules/auth';
|
||||
import { AuthService } from '../src/modules/auth/service';
|
||||
import { PrismaModule } from '../src/prisma';
|
||||
import { RateLimiterModule } from '../src/throttler';
|
||||
import { initFeatureConfigs } from './utils';
|
||||
|
||||
const test = ava as TestFn<{
|
||||
auth: AuthService;
|
||||
@@ -55,11 +52,7 @@ test.beforeEach(async t => {
|
||||
t.context.auth = t.context.module.get(AuthService);
|
||||
|
||||
// init features
|
||||
const run = t.context.module.get(RunCommand);
|
||||
const revert = t.context.module.get(RevertCommand);
|
||||
const migrations = await collectMigrations();
|
||||
await Promise.allSettled(migrations.map(m => revert.run([m.name])));
|
||||
await run.run();
|
||||
await initFeatureConfigs(t.context.module);
|
||||
});
|
||||
|
||||
test.afterEach.always(async t => {
|
||||
|
||||
@@ -5,11 +5,7 @@ import { PrismaClient } from '@prisma/client';
|
||||
import ava, { type TestFn } from 'ava';
|
||||
|
||||
import { ConfigModule } from '../src/config';
|
||||
import {
|
||||
collectMigrations,
|
||||
RevertCommand,
|
||||
RunCommand,
|
||||
} from '../src/data/commands/run';
|
||||
import { RevertCommand, RunCommand } from '../src/data/commands/run';
|
||||
import { AuthModule } from '../src/modules/auth';
|
||||
import { AuthService } from '../src/modules/auth/service';
|
||||
import {
|
||||
@@ -22,6 +18,7 @@ import {
|
||||
import { PrismaModule } from '../src/prisma';
|
||||
import { StorageModule } from '../src/storage';
|
||||
import { RateLimiterModule } from '../src/throttler';
|
||||
import { initFeatureConfigs } from './utils';
|
||||
|
||||
const test = ava as TestFn<{
|
||||
auth: AuthService;
|
||||
@@ -70,11 +67,7 @@ test.beforeEach(async t => {
|
||||
t.context.auth = auth;
|
||||
|
||||
// init features
|
||||
const run = module.get(RunCommand);
|
||||
const revert = module.get(RevertCommand);
|
||||
const migrations = await collectMigrations();
|
||||
await Promise.allSettled(migrations.map(m => revert.run([m.name])));
|
||||
await run.run();
|
||||
await initFeatureConfigs(module);
|
||||
});
|
||||
|
||||
test.afterEach.always(async t => {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import { TestingModule } from '@nestjs/testing';
|
||||
import { hashSync } from '@node-rs/argon2';
|
||||
import { PrismaClient, type User } from '@prisma/client';
|
||||
import request from 'supertest';
|
||||
|
||||
import { RevertCommand, RunCommand } from '../src/data/commands/run';
|
||||
import type { TokenType } from '../src/modules/auth';
|
||||
import type { UserType } from '../src/modules/users';
|
||||
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 {
|
||||
acceptInviteById,
|
||||
changeEmail,
|
||||
|
||||
@@ -6,17 +6,14 @@ import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
|
||||
import request from 'supertest';
|
||||
|
||||
import { AppModule } from '../src/app';
|
||||
import {
|
||||
collectMigrations,
|
||||
RevertCommand,
|
||||
RunCommand,
|
||||
} from '../src/data/commands/run';
|
||||
import { RevertCommand, RunCommand } from '../src/data/commands/run';
|
||||
import { QuotaService, QuotaType } from '../src/modules/quota';
|
||||
import {
|
||||
checkBlobSize,
|
||||
collectAllBlobSizes,
|
||||
createWorkspace,
|
||||
getWorkspaceBlobsSize,
|
||||
initFeatureConfigs,
|
||||
listBlobs,
|
||||
setBlob,
|
||||
signUp,
|
||||
@@ -52,11 +49,7 @@ test.beforeEach(async () => {
|
||||
quota = module.get(QuotaService);
|
||||
|
||||
// init features
|
||||
const run = module.get(RunCommand);
|
||||
const revert = module.get(RevertCommand);
|
||||
const migrations = await collectMigrations();
|
||||
await Promise.allSettled(migrations.map(m => revert.run([m.name])));
|
||||
await run.run();
|
||||
await initFeatureConfigs(module);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
@@ -9,17 +9,14 @@ import ava, { type TestFn } from 'ava';
|
||||
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
|
||||
|
||||
import { AppModule } from '../src/app';
|
||||
import {
|
||||
collectMigrations,
|
||||
RevertCommand,
|
||||
RunCommand,
|
||||
} from '../src/data/commands/run';
|
||||
import { RevertCommand, RunCommand } from '../src/data/commands/run';
|
||||
import { MailService } from '../src/modules/auth/mailer';
|
||||
import { AuthService } from '../src/modules/auth/service';
|
||||
import {
|
||||
acceptInviteById,
|
||||
createWorkspace,
|
||||
getWorkspace,
|
||||
initFeatureConfigs,
|
||||
inviteUser,
|
||||
leaveWorkspace,
|
||||
revokeUser,
|
||||
@@ -63,11 +60,7 @@ test.beforeEach(async t => {
|
||||
t.context.mail = mail;
|
||||
|
||||
// init features
|
||||
const run = module.get(RunCommand);
|
||||
const revert = module.get(RevertCommand);
|
||||
const migrations = await collectMigrations();
|
||||
await Promise.allSettled(migrations.map(m => revert.run([m.name])));
|
||||
await run.run();
|
||||
await initFeatureConfigs(module);
|
||||
});
|
||||
|
||||
test.afterEach.always(async t => {
|
||||
|
||||
@@ -6,17 +6,14 @@ import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
|
||||
import request from 'supertest';
|
||||
|
||||
import { AppModule } from '../src/app';
|
||||
import {
|
||||
collectMigrations,
|
||||
RevertCommand,
|
||||
RunCommand,
|
||||
} from '../src/data/commands/run';
|
||||
import { RevertCommand, RunCommand } from '../src/data/commands/run';
|
||||
import {
|
||||
acceptInviteById,
|
||||
createWorkspace,
|
||||
currentUser,
|
||||
getPublicWorkspace,
|
||||
getWorkspacePublicPages,
|
||||
initFeatureConfigs,
|
||||
inviteUser,
|
||||
publishPage,
|
||||
revokePublicPage,
|
||||
@@ -53,11 +50,7 @@ test.beforeEach(async t => {
|
||||
t.context.app = app;
|
||||
|
||||
// init features
|
||||
const run = module.get(RunCommand);
|
||||
const revert = module.get(RevertCommand);
|
||||
const migrations = await collectMigrations();
|
||||
await Promise.allSettled(migrations.map(m => revert.run([m.name])));
|
||||
await run.run();
|
||||
await initFeatureConfigs(module);
|
||||
});
|
||||
|
||||
test.afterEach.always(async t => {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
mutation addToEarlyAccess($email: String!) {
|
||||
addToEarlyAccess(email: $email)
|
||||
}
|
||||
17
packages/frontend/graphql/src/graphql/early-access-list.gql
Normal file
17
packages/frontend/graphql/src/graphql/early-access-list.gql
Normal file
@@ -0,0 +1,17 @@
|
||||
query earlyAccessUsers {
|
||||
earlyAccessUsers {
|
||||
id
|
||||
name
|
||||
email
|
||||
avatarUrl
|
||||
emailVerified
|
||||
createdAt
|
||||
subscription {
|
||||
plan
|
||||
recurring
|
||||
status
|
||||
start
|
||||
end
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
mutation removeEarlyAccess($email: String!) {
|
||||
removeEarlyAccess(email: $email)
|
||||
}
|
||||
@@ -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 = {
|
||||
id: 'getCurrentUserQuery' as const,
|
||||
operationName: 'getCurrentUser',
|
||||
|
||||
@@ -223,6 +223,47 @@ export type DeleteWorkspaceMutation = {
|
||||
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 GetCurrentUserQuery = {
|
||||
@@ -698,6 +739,11 @@ export type Queries =
|
||||
variables: AllBlobSizesQueryVariables;
|
||||
response: AllBlobSizesQuery;
|
||||
}
|
||||
| {
|
||||
name: 'earlyAccessUsersQuery';
|
||||
variables: EarlyAccessUsersQueryVariables;
|
||||
response: EarlyAccessUsersQuery;
|
||||
}
|
||||
| {
|
||||
name: 'getCurrentUserQuery';
|
||||
variables: GetCurrentUserQueryVariables;
|
||||
@@ -835,6 +881,16 @@ export type Mutations =
|
||||
variables: DeleteWorkspaceMutationVariables;
|
||||
response: DeleteWorkspaceMutation;
|
||||
}
|
||||
| {
|
||||
name: 'addToEarlyAccessMutation';
|
||||
variables: AddToEarlyAccessMutationVariables;
|
||||
response: AddToEarlyAccessMutation;
|
||||
}
|
||||
| {
|
||||
name: 'removeEarlyAccessMutation';
|
||||
variables: RemoveEarlyAccessMutationVariables;
|
||||
response: RemoveEarlyAccessMutation;
|
||||
}
|
||||
| {
|
||||
name: 'leaveWorkspaceMutation';
|
||||
variables: LeaveWorkspaceMutationVariables;
|
||||
|
||||
Reference in New Issue
Block a user