mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
feat: user usage gql & test case improve (#5076)
This commit is contained in:
@@ -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")
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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()],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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',
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 => {
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
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 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 => {
|
||||||
|
|||||||
@@ -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 => {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 => {
|
||||||
|
|||||||
@@ -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 => {
|
||||||
|
|||||||
@@ -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 = {
|
export const getCurrentUserQuery = {
|
||||||
id: 'getCurrentUserQuery' as const,
|
id: 'getCurrentUserQuery' as const,
|
||||||
operationName: 'getCurrentUser',
|
operationName: 'getCurrentUser',
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user