mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-09-17 14:37:18 +03:00
feat: integrate user usage into apis (#5075)
This commit is contained in:
parent
63de73a815
commit
ad23ead5e4
8
.github/workflows/build-test.yml
vendored
8
.github/workflows/build-test.yml
vendored
@ -374,7 +374,9 @@ jobs:
|
||||
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
|
||||
|
||||
- name: Run init-db script
|
||||
run: yarn workspace @affine/server exec node --loader ts-node/esm/transpile-only ./scripts/init-db.ts
|
||||
run: |
|
||||
yarn workspace @affine/server data-migration run
|
||||
yarn workspace @affine/server exec node --loader ts-node/esm/transpile-only ./scripts/init-db.ts
|
||||
env:
|
||||
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
|
||||
|
||||
@ -464,7 +466,9 @@ jobs:
|
||||
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
|
||||
|
||||
- name: Run init-db script
|
||||
run: yarn workspace @affine/server exec node --loader ts-node/esm/transpile-only ./scripts/init-db.ts
|
||||
run: |
|
||||
yarn workspace @affine/server data-migration run
|
||||
yarn workspace @affine/server exec node --loader ts-node/esm/transpile-only ./scripts/init-db.ts
|
||||
- name: Download storage.node
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
|
@ -135,7 +135,8 @@
|
||||
"ENABLE_LOCAL_EMAIL": "true",
|
||||
"OAUTH_EMAIL_LOGIN": "noreply@toeverything.info",
|
||||
"OAUTH_EMAIL_PASSWORD": "affine",
|
||||
"OAUTH_EMAIL_SENDER": "noreply@toeverything.info"
|
||||
"OAUTH_EMAIL_SENDER": "noreply@toeverything.info",
|
||||
"FEATURES_EARLY_ACCESS_PREVIEW": "false"
|
||||
}
|
||||
},
|
||||
"nodemonConfig": {
|
||||
|
@ -28,6 +28,7 @@ model User {
|
||||
invoices UserInvoice[]
|
||||
workspacePermissions WorkspaceUserPermission[]
|
||||
pagePermissions WorkspacePageUserPermission[]
|
||||
UserQuotaGates UserQuotaGates[]
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
@ -157,6 +158,33 @@ 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")
|
||||
|
@ -188,11 +188,6 @@ export interface AFFiNEConfig {
|
||||
fs: {
|
||||
path: string;
|
||||
};
|
||||
/**
|
||||
* default storage quota
|
||||
* @default 10 * 1024 * 1024 * 1024 (10GB)
|
||||
*/
|
||||
quota: number;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -58,7 +58,6 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => {
|
||||
AFFINE_SERVER_HOST: 'host',
|
||||
AFFINE_SERVER_SUB_PATH: 'path',
|
||||
AFFINE_ENV: 'affineEnv',
|
||||
AFFINE_FREE_USER_QUOTA: 'objectStorage.quota',
|
||||
DATABASE_URL: 'db.url',
|
||||
ENABLE_R2_OBJECT_STORAGE: ['objectStorage.r2.enabled', 'boolean'],
|
||||
R2_OBJECT_STORAGE_ACCOUNT_ID: 'objectStorage.r2.accountId',
|
||||
@ -192,8 +191,6 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => {
|
||||
fs: {
|
||||
path: join(homedir(), '.affine-storage'),
|
||||
},
|
||||
// 10GB
|
||||
quota: 10 * 1024 * 1024 * 1024,
|
||||
},
|
||||
rateLimiter: {
|
||||
ttl: 60,
|
||||
|
@ -1,12 +1,39 @@
|
||||
import {
|
||||
CommonFeature,
|
||||
FeatureKind,
|
||||
Features,
|
||||
FeatureType,
|
||||
upsertFeature,
|
||||
} from '../../modules/features';
|
||||
import { Quotas } from '../../modules/quota';
|
||||
} from '../../modules/features/types';
|
||||
import { Quotas } from '../../modules/quota/types';
|
||||
import { PrismaService } from '../../prisma';
|
||||
|
||||
// upgrade features from lower version to higher version
|
||||
async function upsertFeature(
|
||||
db: PrismaService,
|
||||
feature: CommonFeature
|
||||
): Promise<void> {
|
||||
const hasEqualOrGreaterVersion =
|
||||
(await db.features.count({
|
||||
where: {
|
||||
feature: feature.feature,
|
||||
version: {
|
||||
gte: feature.version,
|
||||
},
|
||||
},
|
||||
})) > 0;
|
||||
// will not update exists version
|
||||
if (!hasEqualOrGreaterVersion) {
|
||||
await db.features.create({
|
||||
data: {
|
||||
feature: feature.feature,
|
||||
type: feature.type,
|
||||
version: feature.version,
|
||||
configs: feature.configs,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class UserFeaturesInit1698652531198 {
|
||||
// do the migration
|
||||
static async up(db: PrismaService) {
|
||||
|
@ -11,8 +11,8 @@ import Google from 'next-auth/providers/google';
|
||||
import { Config } from '../../config';
|
||||
import { PrismaService } from '../../prisma';
|
||||
import { SessionService } from '../../session';
|
||||
import { NewFeaturesKind } from '../users/types';
|
||||
import { isStaff } from '../users/utils';
|
||||
import { FeatureType } from '../features';
|
||||
import { Quota_FreePlanV1 } from '../quota';
|
||||
import { MailService } from './mailer';
|
||||
import {
|
||||
decode,
|
||||
@ -44,6 +44,17 @@ export const NextAuthOptionsProvider: FactoryProvider<NextAuthOptions> = {
|
||||
email: data.email,
|
||||
avatarUrl: '',
|
||||
emailVerified: data.emailVerified,
|
||||
features: {
|
||||
create: {
|
||||
reason: 'created by email sign up',
|
||||
activated: true,
|
||||
feature: {
|
||||
connect: {
|
||||
feature_version: Quota_FreePlanV1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
if (data.email && !data.name) {
|
||||
userData.name = data.email.split('@')[0];
|
||||
@ -223,18 +234,23 @@ export const NextAuthOptionsProvider: FactoryProvider<NextAuthOptions> = {
|
||||
}
|
||||
const email = profile?.email ?? user.email;
|
||||
if (email) {
|
||||
if (isStaff(email)) {
|
||||
return true;
|
||||
}
|
||||
return prisma.newFeaturesWaitingList
|
||||
.findUnique({
|
||||
// FIXME: cannot inject FeatureManagementService here
|
||||
// it will cause prisma.account to be undefined
|
||||
// then prismaAdapter.getUserByAccount will throw error
|
||||
if (email.endsWith('@toeverything.info')) return true;
|
||||
return prisma.userFeatures
|
||||
.count({
|
||||
where: {
|
||||
email,
|
||||
type: NewFeaturesKind.EarlyAccess,
|
||||
user: {
|
||||
email,
|
||||
},
|
||||
feature: {
|
||||
feature: FeatureType.EarlyAccess,
|
||||
},
|
||||
activated: true,
|
||||
},
|
||||
})
|
||||
.then(user => !!user)
|
||||
.catch(() => false);
|
||||
.then(count => count > 0);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
@ -14,6 +14,7 @@ import { nanoid } from 'nanoid';
|
||||
import { Config } from '../../config';
|
||||
import { PrismaService } from '../../prisma';
|
||||
import { verifyChallengeResponse } from '../../storage';
|
||||
import { Quota_FreePlanV1 } from '../quota';
|
||||
import { MailService } from './mailer';
|
||||
|
||||
export type UserClaim = Pick<
|
||||
@ -190,6 +191,17 @@ export class AuthService {
|
||||
name,
|
||||
email,
|
||||
password: hashedPassword,
|
||||
features: {
|
||||
create: {
|
||||
reason: 'created by api sign up',
|
||||
activated: true,
|
||||
feature: {
|
||||
connect: {
|
||||
feature_version: Quota_FreePlanV1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -209,6 +221,17 @@ export class AuthService {
|
||||
data: {
|
||||
name: 'Unnamed',
|
||||
email,
|
||||
features: {
|
||||
create: {
|
||||
reason: 'created by invite sign up',
|
||||
activated: true,
|
||||
feature: {
|
||||
connect: {
|
||||
feature_version: Quota_FreePlanV1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -258,6 +281,7 @@ export class AuthService {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async changeEmail(id: string, newEmail: string): Promise<User> {
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: {
|
||||
|
@ -3,34 +3,6 @@ import { Module } from '@nestjs/common';
|
||||
import { PrismaService } from '../../prisma';
|
||||
import { FeatureService } from './configure';
|
||||
import { FeatureManagementService } from './feature';
|
||||
import type { CommonFeature } from './types';
|
||||
|
||||
// upgrade features from lower version to higher version
|
||||
async function upsertFeature(
|
||||
db: PrismaService,
|
||||
feature: CommonFeature
|
||||
): Promise<void> {
|
||||
const hasEqualOrGreaterVersion =
|
||||
(await db.features.count({
|
||||
where: {
|
||||
feature: feature.feature,
|
||||
version: {
|
||||
gte: feature.version,
|
||||
},
|
||||
},
|
||||
})) > 0;
|
||||
// will not update exists version
|
||||
if (!hasEqualOrGreaterVersion) {
|
||||
await db.features.create({
|
||||
data: {
|
||||
feature: feature.feature,
|
||||
type: feature.type,
|
||||
version: feature.version,
|
||||
configs: feature.configs,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Feature module provider pre-user feature flag management.
|
||||
@ -46,9 +18,4 @@ export class FeatureModule {}
|
||||
|
||||
export type { CommonFeature, Feature } from './types';
|
||||
export { FeatureKind, Features, FeatureType } from './types';
|
||||
export {
|
||||
FeatureManagementService,
|
||||
FeatureService,
|
||||
PrismaService,
|
||||
upsertFeature,
|
||||
};
|
||||
export { FeatureManagementService, FeatureService, PrismaService };
|
||||
|
@ -6,6 +6,7 @@ import { GqlModule } from '../graphql.module';
|
||||
import { ServerConfigModule } from './config';
|
||||
import { DocModule } from './doc';
|
||||
import { PaymentModule } from './payment';
|
||||
import { QuotaModule } from './quota';
|
||||
import { SelfHostedModule } from './self-hosted';
|
||||
import { SyncModule } from './sync';
|
||||
import { UsersModule } from './users';
|
||||
@ -37,7 +38,8 @@ switch (SERVER_FLAVOR) {
|
||||
WorkspaceModule,
|
||||
UsersModule,
|
||||
DocModule,
|
||||
PaymentModule
|
||||
PaymentModule,
|
||||
QuotaModule
|
||||
);
|
||||
break;
|
||||
case 'allinone':
|
||||
@ -48,6 +50,7 @@ switch (SERVER_FLAVOR) {
|
||||
GqlModule,
|
||||
WorkspaceModule,
|
||||
UsersModule,
|
||||
QuotaModule,
|
||||
SyncModule,
|
||||
DocModule,
|
||||
PaymentModule
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { UsersModule } from '../users';
|
||||
import { FeatureModule } from '../features';
|
||||
import { SubscriptionResolver, UserSubscriptionResolver } from './resolver';
|
||||
import { ScheduleManager } from './schedule';
|
||||
import { SubscriptionService } from './service';
|
||||
@ -8,7 +8,7 @@ import { StripeProvider } from './stripe';
|
||||
import { StripeWebhook } from './webhook';
|
||||
|
||||
@Module({
|
||||
imports: [UsersModule],
|
||||
imports: [FeatureModule],
|
||||
providers: [
|
||||
ScheduleManager,
|
||||
StripeProvider,
|
||||
|
@ -11,7 +11,7 @@ import Stripe from 'stripe';
|
||||
|
||||
import { Config } from '../../config';
|
||||
import { PrismaService } from '../../prisma';
|
||||
import { UsersService } from '../users';
|
||||
import { FeatureManagementService } from '../features';
|
||||
import { ScheduleManager } from './schedule';
|
||||
|
||||
const OnEvent = (
|
||||
@ -82,8 +82,8 @@ export class SubscriptionService {
|
||||
config: Config,
|
||||
private readonly stripe: Stripe,
|
||||
private readonly db: PrismaService,
|
||||
private readonly user: UsersService,
|
||||
private readonly scheduleManager: ScheduleManager
|
||||
private readonly scheduleManager: ScheduleManager,
|
||||
private readonly features: FeatureManagementService
|
||||
) {
|
||||
this.paymentConfig = config.payment;
|
||||
|
||||
@ -658,7 +658,7 @@ export class SubscriptionService {
|
||||
user: User,
|
||||
couponType: CouponType
|
||||
): Promise<string | null> {
|
||||
const earlyAccess = await this.user.isEarlyAccessUser(user.email);
|
||||
const earlyAccess = await this.features.canEarlyAccess(user.email);
|
||||
if (earlyAccess) {
|
||||
try {
|
||||
const coupon = await this.stripe.coupons.retrieve(couponType);
|
||||
|
@ -1,42 +0,0 @@
|
||||
type FeatureEarlyAccessPreview = {
|
||||
whitelist: RegExp[];
|
||||
};
|
||||
|
||||
type FeatureStorageLimit = {
|
||||
storageQuota: number;
|
||||
};
|
||||
|
||||
type UserFeatureGate = {
|
||||
earlyAccessPreview: FeatureEarlyAccessPreview;
|
||||
freeUser: FeatureStorageLimit;
|
||||
proUser: FeatureStorageLimit;
|
||||
};
|
||||
|
||||
const UserLevel = {
|
||||
freeUser: {
|
||||
storageQuota: 10 * 1024 * 1024 * 1024,
|
||||
},
|
||||
proUser: {
|
||||
storageQuota: 100 * 1024 * 1024 * 1024,
|
||||
},
|
||||
} satisfies Pick<UserFeatureGate, 'freeUser' | 'proUser'>;
|
||||
|
||||
export function getStorageQuota(features: string[]) {
|
||||
for (const feature of features) {
|
||||
if (feature in UserLevel) {
|
||||
return UserLevel[feature as keyof typeof UserLevel].storageQuota;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const UserType = {
|
||||
earlyAccessPreview: {
|
||||
whitelist: [/@toeverything\.info$/],
|
||||
},
|
||||
} satisfies Pick<UserFeatureGate, 'earlyAccessPreview'>;
|
||||
|
||||
export const FeatureGates = {
|
||||
...UserType,
|
||||
...UserLevel,
|
||||
} satisfies UserFeatureGate;
|
@ -1,11 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { FeatureModule } from '../features';
|
||||
import { StorageModule } from '../storage';
|
||||
import { UserResolver } from './resolver';
|
||||
import { UsersService } from './users';
|
||||
|
||||
@Module({
|
||||
imports: [StorageModule],
|
||||
imports: [StorageModule, FeatureModule],
|
||||
providers: [UserResolver, UsersService],
|
||||
exports: [UsersService],
|
||||
})
|
||||
|
@ -12,7 +12,6 @@ import {
|
||||
Mutation,
|
||||
ObjectType,
|
||||
Query,
|
||||
registerEnumType,
|
||||
ResolveField,
|
||||
Resolver,
|
||||
} from '@nestjs/graphql';
|
||||
@ -24,14 +23,10 @@ import { PrismaService } from '../../prisma/service';
|
||||
import { CloudThrottlerGuard, Throttle } from '../../throttler';
|
||||
import type { FileUpload } from '../../types';
|
||||
import { Auth, CurrentUser, Public, Publicable } from '../auth/guard';
|
||||
import { AuthService } from '../auth/service';
|
||||
import { FeatureManagementService } from '../features';
|
||||
import { StorageService } from '../storage/storage.service';
|
||||
import { NewFeaturesKind } from './types';
|
||||
import { UsersService } from './users';
|
||||
import { isStaff } from './utils';
|
||||
|
||||
registerEnumType(NewFeaturesKind, {
|
||||
name: 'NewFeaturesKind',
|
||||
});
|
||||
|
||||
@ObjectType()
|
||||
export class UserType implements Partial<User> {
|
||||
@ -71,14 +66,6 @@ export class RemoveAvatar {
|
||||
success!: boolean;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class AddToNewFeaturesWaitingList {
|
||||
@Field()
|
||||
email!: string;
|
||||
@Field(() => NewFeaturesKind, { description: 'New features kind' })
|
||||
type!: NewFeaturesKind;
|
||||
}
|
||||
|
||||
/**
|
||||
* User resolver
|
||||
* All op rate limit: 10 req/m
|
||||
@ -88,9 +75,11 @@ export class AddToNewFeaturesWaitingList {
|
||||
@Resolver(() => UserType)
|
||||
export class UserResolver {
|
||||
constructor(
|
||||
private readonly auth: AuthService,
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly storage: StorageService,
|
||||
private readonly users: UsersService
|
||||
private readonly users: UsersService,
|
||||
private readonly feature: FeatureManagementService
|
||||
) {}
|
||||
|
||||
@Throttle({
|
||||
@ -138,7 +127,7 @@ export class UserResolver {
|
||||
})
|
||||
@Public()
|
||||
async user(@Args('email') email: string) {
|
||||
if (!(await this.users.canEarlyAccess(email))) {
|
||||
if (!(await this.feature.canEarlyAccess(email))) {
|
||||
return new GraphQLError(
|
||||
`You don't have early access permission\nVisit https://community.affine.pro/c/insider-general/ for more information`,
|
||||
{
|
||||
@ -233,27 +222,55 @@ export class UserResolver {
|
||||
ttl: 60,
|
||||
},
|
||||
})
|
||||
@Mutation(() => AddToNewFeaturesWaitingList)
|
||||
async addToNewFeaturesWaitingList(
|
||||
@CurrentUser() user: UserType,
|
||||
@Args('type', {
|
||||
type: () => NewFeaturesKind,
|
||||
})
|
||||
type: NewFeaturesKind,
|
||||
@Mutation(() => Int)
|
||||
async addToEarlyAccess(
|
||||
@CurrentUser() currentUser: UserType,
|
||||
@Args('email') email: string
|
||||
): Promise<AddToNewFeaturesWaitingList> {
|
||||
if (!isStaff(user.email)) {
|
||||
): Promise<number> {
|
||||
if (!this.feature.isStaff(currentUser.email)) {
|
||||
throw new ForbiddenException('You are not allowed to do this');
|
||||
}
|
||||
await this.prisma.newFeaturesWaitingList.create({
|
||||
data: {
|
||||
email,
|
||||
type,
|
||||
},
|
||||
});
|
||||
return {
|
||||
email,
|
||||
type,
|
||||
};
|
||||
const user = await this.users.findUserByEmail(email);
|
||||
if (user) {
|
||||
return this.feature.addEarlyAccess(user.id);
|
||||
} else {
|
||||
const user = await this.auth.createAnonymousUser(email);
|
||||
return this.feature.addEarlyAccess(user.id);
|
||||
}
|
||||
}
|
||||
|
||||
@Throttle({
|
||||
default: {
|
||||
limit: 10,
|
||||
ttl: 60,
|
||||
},
|
||||
})
|
||||
@Mutation(() => Int)
|
||||
async removeEarlyAccess(
|
||||
@CurrentUser() currentUser: UserType,
|
||||
@Args('email') email: string
|
||||
): Promise<number> {
|
||||
if (!this.feature.isStaff(currentUser.email)) {
|
||||
throw new ForbiddenException('You are not allowed to do this');
|
||||
}
|
||||
const user = await this.users.findUserByEmail(email);
|
||||
if (!user) {
|
||||
throw new BadRequestException(`User ${email} not found`);
|
||||
}
|
||||
return this.feature.removeEarlyAccess(user.id);
|
||||
}
|
||||
|
||||
@Throttle({
|
||||
default: {
|
||||
limit: 10,
|
||||
ttl: 60,
|
||||
},
|
||||
})
|
||||
@Query(() => [UserType])
|
||||
async listEarlyAccess(@CurrentUser() user: UserType): Promise<UserType[]> {
|
||||
if (!this.feature.isStaff(user.email)) {
|
||||
throw new ForbiddenException('You are not allowed to do this');
|
||||
}
|
||||
return this.feature.listEarlyAccess();
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +0,0 @@
|
||||
export enum NewFeaturesKind {
|
||||
EarlyAccess,
|
||||
}
|
@ -1,54 +1,10 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { Config } from '../../config';
|
||||
import { PrismaService } from '../../prisma';
|
||||
import { getStorageQuota } from './gates';
|
||||
import { NewFeaturesKind } from './types';
|
||||
import { isStaff } from './utils';
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly config: Config
|
||||
) {}
|
||||
|
||||
async canEarlyAccess(email: string) {
|
||||
if (this.config.featureFlags.earlyAccessPreview && !isStaff(email)) {
|
||||
return this.isEarlyAccessUser(email);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
async isEarlyAccessUser(email: string) {
|
||||
return this.prisma.newFeaturesWaitingList
|
||||
.count({
|
||||
where: { email, type: NewFeaturesKind.EarlyAccess },
|
||||
})
|
||||
.then(count => count > 0)
|
||||
.catch(() => false);
|
||||
}
|
||||
|
||||
async getStorageQuotaById(id: string) {
|
||||
const features = await this.prisma.user
|
||||
.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
features: {
|
||||
select: {
|
||||
feature: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(user => user?.features.map(f => f.feature) ?? []);
|
||||
|
||||
return (
|
||||
getStorageQuota(features.map(f => f.feature)) ||
|
||||
this.config.objectStorage.quota
|
||||
);
|
||||
}
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async findUserByEmail(email: string) {
|
||||
return this.prisma.user
|
||||
|
@ -1,3 +0,0 @@
|
||||
export function isStaff(email: string) {
|
||||
return email.endsWith('@toeverything.info');
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { DocModule } from '../doc';
|
||||
import { QuotaModule } from '../quota';
|
||||
import { UsersService } from '../users';
|
||||
import { WorkspacesController } from './controller';
|
||||
import { DocHistoryResolver } from './history.resolver';
|
||||
@ -8,7 +9,7 @@ import { PermissionService } from './permission';
|
||||
import { PagePermissionResolver, WorkspaceResolver } from './resolver';
|
||||
|
||||
@Module({
|
||||
imports: [DocModule],
|
||||
imports: [DocModule, QuotaModule],
|
||||
controllers: [WorkspacesController],
|
||||
providers: [
|
||||
WorkspaceResolver,
|
||||
|
@ -42,6 +42,7 @@ import { DocID } from '../../utils/doc';
|
||||
import { Auth, CurrentUser, Public } from '../auth';
|
||||
import { MailService } from '../auth/mailer';
|
||||
import { AuthService } from '../auth/service';
|
||||
import { QuotaManagementService } from '../quota';
|
||||
import { UsersService } from '../users';
|
||||
import { UserType } from '../users/resolver';
|
||||
import { PermissionService, PublicPageMode } from './permission';
|
||||
@ -148,6 +149,7 @@ export class WorkspaceResolver {
|
||||
private readonly permissions: PermissionService,
|
||||
private readonly users: UsersService,
|
||||
private readonly event: EventEmitter,
|
||||
private readonly quota: QuotaManagementService,
|
||||
@Inject(StorageProvide) private readonly storage: Storage
|
||||
) {}
|
||||
|
||||
@ -233,6 +235,14 @@ export class WorkspaceResolver {
|
||||
}));
|
||||
}
|
||||
|
||||
@ResolveField(() => Int, {
|
||||
description: 'Blobs size of workspace',
|
||||
complexity: 2,
|
||||
})
|
||||
async blobsSize(@Parent() workspace: WorkspaceType) {
|
||||
return this.storage.blobsSize([workspace.id]);
|
||||
}
|
||||
|
||||
@Query(() => Boolean, {
|
||||
description: 'Get is owner of workspace',
|
||||
complexity: 2,
|
||||
@ -656,36 +666,9 @@ export class WorkspaceResolver {
|
||||
return this.storage.listBlobs(workspaceId);
|
||||
}
|
||||
|
||||
@Query(() => WorkspaceBlobSizes)
|
||||
async collectBlobSizes(
|
||||
@CurrentUser() user: UserType,
|
||||
@Args('workspaceId') workspaceId: string
|
||||
) {
|
||||
await this.permissions.checkWorkspace(workspaceId, user.id);
|
||||
|
||||
return this.storage.blobsSize([workspaceId]).then(size => ({ size }));
|
||||
}
|
||||
|
||||
@Query(() => WorkspaceBlobSizes)
|
||||
async collectAllBlobSizes(@CurrentUser() user: UserType) {
|
||||
const workspaces = await this.prisma.workspaceUserPermission
|
||||
.findMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
accepted: true,
|
||||
type: Permission.Owner,
|
||||
},
|
||||
select: {
|
||||
workspace: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(data => data.map(({ workspace }) => workspace.id));
|
||||
|
||||
const size = await this.storage.blobsSize(workspaces);
|
||||
const size = await this.quota.getUserUsage(user.id);
|
||||
return { size };
|
||||
}
|
||||
|
||||
@ -693,7 +676,7 @@ export class WorkspaceResolver {
|
||||
async checkBlobSize(
|
||||
@CurrentUser() user: UserType,
|
||||
@Args('workspaceId') workspaceId: string,
|
||||
@Args('size', { type: () => Float }) size: number
|
||||
@Args('size', { type: () => Float }) blobSize: number
|
||||
) {
|
||||
const canWrite = await this.permissions.tryCheckWorkspace(
|
||||
workspaceId,
|
||||
@ -701,13 +684,8 @@ export class WorkspaceResolver {
|
||||
Permission.Write
|
||||
);
|
||||
if (canWrite) {
|
||||
const { user } = await this.permissions.getWorkspaceOwner(workspaceId);
|
||||
if (user) {
|
||||
const quota = await this.users.getStorageQuotaById(user.id);
|
||||
const { size: currentSize } = await this.collectAllBlobSizes(user);
|
||||
|
||||
return { size: quota - (size + currentSize) };
|
||||
}
|
||||
const size = await this.quota.checkBlobQuota(workspaceId, blobSize);
|
||||
return { size };
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@ -725,14 +703,12 @@ export class WorkspaceResolver {
|
||||
Permission.Write
|
||||
);
|
||||
|
||||
// quota was apply to owner's account
|
||||
const { user: owner } =
|
||||
await this.permissions.getWorkspaceOwner(workspaceId);
|
||||
if (!owner) return new NotFoundException('Workspace owner not found');
|
||||
const quota = await this.users.getStorageQuotaById(owner.id);
|
||||
const { size } = await this.collectAllBlobSizes(owner);
|
||||
const { quota, size } = await this.quota.getWorkspaceUsage(workspaceId);
|
||||
|
||||
const checkExceeded = (recvSize: number) => {
|
||||
if (!quota) {
|
||||
throw new ForbiddenException('cannot find user quota');
|
||||
}
|
||||
if (size + recvSize > quota) {
|
||||
this.logger.log(
|
||||
`storage size limit exceeded: ${size + recvSize} > ${quota}`
|
||||
|
@ -51,17 +51,6 @@ type RemoveAvatar {
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type AddToNewFeaturesWaitingList {
|
||||
email: String!
|
||||
|
||||
"""New features kind"""
|
||||
type: NewFeaturesKind!
|
||||
}
|
||||
|
||||
enum NewFeaturesKind {
|
||||
EarlyAccess
|
||||
}
|
||||
|
||||
type TokenType {
|
||||
token: String!
|
||||
refresh: String!
|
||||
@ -196,6 +185,9 @@ type WorkspaceType {
|
||||
"""Owner of workspace"""
|
||||
owner: UserType!
|
||||
|
||||
"""Blobs size of workspace"""
|
||||
blobsSize: Int!
|
||||
|
||||
"""Shared pages of workspace"""
|
||||
sharedPages: [String!]! @deprecated(reason: "use WorkspaceType.publicPages")
|
||||
|
||||
@ -269,7 +261,6 @@ type Query {
|
||||
|
||||
"""List blobs of workspace"""
|
||||
listBlobs(workspaceId: String!): [String!]!
|
||||
collectBlobSizes(workspaceId: String!): WorkspaceBlobSizes!
|
||||
collectAllBlobSizes: WorkspaceBlobSizes!
|
||||
checkBlobSize(workspaceId: String!, size: Float!): WorkspaceBlobSizes!
|
||||
|
||||
@ -278,6 +269,7 @@ type Query {
|
||||
|
||||
"""Get user by email"""
|
||||
user(email: String!): UserType
|
||||
listEarlyAccess: [UserType!]!
|
||||
prices: [SubscriptionPrice!]!
|
||||
}
|
||||
|
||||
@ -315,7 +307,8 @@ type Mutation {
|
||||
"""Remove user avatar"""
|
||||
removeAvatar: RemoveAvatar!
|
||||
deleteAccount: DeleteAccount!
|
||||
addToNewFeaturesWaitingList(type: NewFeaturesKind!, email: String!): AddToNewFeaturesWaitingList!
|
||||
addToEarlyAccess(email: String!): Int!
|
||||
removeEarlyAccess(email: String!): Int!
|
||||
|
||||
"""Create a subscription checkout link of stripe"""
|
||||
checkout(recurring: SubscriptionRecurring!, idempotencyKey: String!): String!
|
||||
|
@ -45,6 +45,13 @@ class FakePrisma {
|
||||
},
|
||||
};
|
||||
}
|
||||
get newFeaturesWaitingList() {
|
||||
return {
|
||||
async findUnique() {
|
||||
return null;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
test.beforeEach(async t => {
|
||||
@ -119,6 +126,7 @@ 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,6 +9,11 @@ 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 { MailService } from '../src/modules/auth/mailer';
|
||||
import { AuthService } from '../src/modules/auth/service';
|
||||
import {
|
||||
@ -37,6 +42,7 @@ test.beforeEach(async t => {
|
||||
await client.$disconnect();
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
providers: [RevertCommand, RunCommand],
|
||||
}).compile();
|
||||
const app = module.createNestApplication();
|
||||
app.use(
|
||||
@ -52,6 +58,13 @@ test.beforeEach(async t => {
|
||||
t.context.app = app;
|
||||
t.context.auth = auth;
|
||||
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();
|
||||
});
|
||||
|
||||
test.afterEach(async t => {
|
||||
|
@ -4,6 +4,11 @@ import { PrismaClient } from '@prisma/client';
|
||||
import test from 'ava';
|
||||
|
||||
import { ConfigModule } from '../src/config';
|
||||
import {
|
||||
collectMigrations,
|
||||
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';
|
||||
@ -40,10 +45,19 @@ test.beforeEach(async () => {
|
||||
GqlModule,
|
||||
AuthModule,
|
||||
RateLimiterModule,
|
||||
RevertCommand,
|
||||
RunCommand,
|
||||
],
|
||||
}).compile();
|
||||
authService = module.get(AuthService);
|
||||
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();
|
||||
});
|
||||
|
||||
test.afterEach.always(async () => {
|
||||
|
@ -11,6 +11,11 @@ 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 { GqlModule } from '../src/graphql.module';
|
||||
import { AuthModule } from '../src/modules/auth';
|
||||
import { AuthService } from '../src/modules/auth/service';
|
||||
@ -45,8 +50,16 @@ test.beforeEach(async t => {
|
||||
AuthModule,
|
||||
RateLimiterModule,
|
||||
],
|
||||
providers: [RevertCommand, RunCommand],
|
||||
}).compile();
|
||||
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();
|
||||
});
|
||||
|
||||
test.afterEach.always(async t => {
|
||||
|
153
packages/backend/server/tests/quota.spec.ts
Normal file
153
packages/backend/server/tests/quota.spec.ts
Normal file
@ -0,0 +1,153 @@
|
||||
/// <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 {
|
||||
collectMigrations,
|
||||
RevertCommand,
|
||||
RunCommand,
|
||||
} from '../src/data/commands/run';
|
||||
import { AuthModule } from '../src/modules/auth';
|
||||
import { AuthService } from '../src/modules/auth/service';
|
||||
import {
|
||||
QuotaManagementService,
|
||||
QuotaModule,
|
||||
Quotas,
|
||||
QuotaService,
|
||||
QuotaType,
|
||||
} from '../src/modules/quota';
|
||||
import { PrismaModule } from '../src/prisma';
|
||||
import { StorageModule } from '../src/storage';
|
||||
import { RateLimiterModule } from '../src/throttler';
|
||||
|
||||
const test = ava as TestFn<{
|
||||
auth: AuthService;
|
||||
quota: QuotaService;
|
||||
storageQuota: QuotaManagementService;
|
||||
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,
|
||||
}),
|
||||
StorageModule.forRoot(),
|
||||
PrismaModule,
|
||||
AuthModule,
|
||||
QuotaModule,
|
||||
RateLimiterModule,
|
||||
RevertCommand,
|
||||
RunCommand,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
const quota = module.get(QuotaService);
|
||||
const storageQuota = module.get(QuotaManagementService);
|
||||
const auth = module.get(AuthService);
|
||||
|
||||
t.context.app = module;
|
||||
t.context.quota = quota;
|
||||
t.context.storageQuota = storageQuota;
|
||||
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();
|
||||
});
|
||||
|
||||
test.afterEach.always(async t => {
|
||||
await t.context.app.close();
|
||||
});
|
||||
|
||||
test('should be able to set quota', async t => {
|
||||
const { auth, quota } = t.context;
|
||||
|
||||
const u1 = await auth.signUp('DarkSky', 'darksky@example.org', '123456');
|
||||
|
||||
const q1 = await quota.getUserQuota(u1.id);
|
||||
t.truthy(q1, 'should have quota');
|
||||
t.is(q1?.feature.feature, QuotaType.Quota_FreePlanV1, 'should be free plan');
|
||||
|
||||
await quota.switchUserQuota(u1.id, QuotaType.Quota_ProPlanV1);
|
||||
|
||||
const q2 = await quota.getUserQuota(u1.id);
|
||||
t.is(q2?.feature.feature, QuotaType.Quota_ProPlanV1, 'should be pro plan');
|
||||
|
||||
const fail = quota.switchUserQuota(u1.id, 'not_exists_plan_v1' as QuotaType);
|
||||
await t.throwsAsync(fail, { instanceOf: Error }, 'should throw error');
|
||||
});
|
||||
|
||||
test('should be able to check storage quota', async t => {
|
||||
const { auth, quota, storageQuota } = t.context;
|
||||
const u1 = await auth.signUp('DarkSky', 'darksky@example.org', '123456');
|
||||
|
||||
const q1 = await storageQuota.getUserQuota(u1.id);
|
||||
t.is(q1?.blobLimit, Quotas[0].configs.blobLimit, 'should be free plan');
|
||||
t.is(q1?.storageQuota, Quotas[0].configs.storageQuota, 'should be free plan');
|
||||
|
||||
await quota.switchUserQuota(u1.id, QuotaType.Quota_ProPlanV1);
|
||||
const q2 = await storageQuota.getUserQuota(u1.id);
|
||||
t.is(q2?.blobLimit, Quotas[1].configs.blobLimit, 'should be pro plan');
|
||||
t.is(q2?.storageQuota, Quotas[1].configs.storageQuota, 'should be pro plan');
|
||||
});
|
||||
|
||||
test('should be able revert quota', async t => {
|
||||
const { auth, quota, storageQuota } = t.context;
|
||||
const u1 = await auth.signUp('DarkSky', 'darksky@example.org', '123456');
|
||||
|
||||
const q1 = await storageQuota.getUserQuota(u1.id);
|
||||
t.is(q1?.blobLimit, Quotas[0].configs.blobLimit, 'should be free plan');
|
||||
t.is(q1?.storageQuota, Quotas[0].configs.storageQuota, 'should be free plan');
|
||||
|
||||
await quota.switchUserQuota(u1.id, QuotaType.Quota_ProPlanV1);
|
||||
const q2 = await storageQuota.getUserQuota(u1.id);
|
||||
t.is(q2?.blobLimit, Quotas[1].configs.blobLimit, 'should be pro plan');
|
||||
t.is(q2?.storageQuota, Quotas[1].configs.storageQuota, 'should be pro plan');
|
||||
|
||||
await quota.switchUserQuota(u1.id, QuotaType.Quota_FreePlanV1);
|
||||
const q3 = await storageQuota.getUserQuota(u1.id);
|
||||
t.is(q3?.blobLimit, Quotas[0].configs.blobLimit, 'should be free plan');
|
||||
|
||||
const quotas = await quota.getUserQuotas(u1.id);
|
||||
t.is(quotas.length, 3, 'should have 3 quotas');
|
||||
t.is(
|
||||
quotas[0].feature.feature,
|
||||
QuotaType.Quota_FreePlanV1,
|
||||
'should be free plan'
|
||||
);
|
||||
t.is(
|
||||
quotas[1].feature.feature,
|
||||
QuotaType.Quota_ProPlanV1,
|
||||
'should be pro plan'
|
||||
);
|
||||
t.is(
|
||||
quotas[2].feature.feature,
|
||||
QuotaType.Quota_FreePlanV1,
|
||||
'should be free plan'
|
||||
);
|
||||
t.is(quotas[0].activated, false, 'should be activated');
|
||||
t.is(quotas[1].activated, false, 'should be activated');
|
||||
t.is(quotas[2].activated, true, 'should be activated');
|
||||
});
|
@ -324,7 +324,7 @@ async function listBlobs(
|
||||
return res.body.data.listBlobs;
|
||||
}
|
||||
|
||||
async function collectBlobSizes(
|
||||
async function getWorkspaceBlobsSize(
|
||||
app: INestApplication,
|
||||
token: string,
|
||||
workspaceId: string
|
||||
@ -335,14 +335,14 @@ async function collectBlobSizes(
|
||||
.send({
|
||||
query: `
|
||||
query {
|
||||
collectBlobSizes(workspaceId: "${workspaceId}") {
|
||||
size
|
||||
workspace(id: "${workspaceId}") {
|
||||
blobsSize
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200);
|
||||
return res.body.data.collectBlobSizes.size;
|
||||
return res.body.data.workspace.blobsSize;
|
||||
}
|
||||
|
||||
async function collectAllBlobSizes(
|
||||
@ -566,13 +566,13 @@ export {
|
||||
changeEmail,
|
||||
checkBlobSize,
|
||||
collectAllBlobSizes,
|
||||
collectBlobSizes,
|
||||
createWorkspace,
|
||||
currentUser,
|
||||
flushDB,
|
||||
getInviteInfo,
|
||||
getPublicWorkspace,
|
||||
getWorkspace,
|
||||
getWorkspaceBlobsSize,
|
||||
inviteUser,
|
||||
leaveWorkspace,
|
||||
listBlobs,
|
||||
|
@ -6,17 +6,24 @@ 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 { QuotaService, QuotaType } from '../src/modules/quota';
|
||||
import {
|
||||
checkBlobSize,
|
||||
collectAllBlobSizes,
|
||||
collectBlobSizes,
|
||||
createWorkspace,
|
||||
getWorkspaceBlobsSize,
|
||||
listBlobs,
|
||||
setBlob,
|
||||
signUp,
|
||||
} from './utils';
|
||||
|
||||
let app: INestApplication;
|
||||
let quota: QuotaService;
|
||||
|
||||
const client = new PrismaClient();
|
||||
|
||||
@ -33,6 +40,7 @@ test.beforeEach(async () => {
|
||||
test.beforeEach(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
providers: [RevertCommand, RunCommand],
|
||||
}).compile();
|
||||
app = module.createNestApplication();
|
||||
app.use(
|
||||
@ -41,6 +49,15 @@ test.beforeEach(async () => {
|
||||
maxFiles: 5,
|
||||
})
|
||||
);
|
||||
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 app.init();
|
||||
});
|
||||
|
||||
@ -103,7 +120,7 @@ test('should calc blobs size', async t => {
|
||||
const buffer2 = Buffer.from([0, 1]);
|
||||
await setBlob(app, u1.token.token, workspace.id, buffer2);
|
||||
|
||||
const size = await collectBlobSizes(app, u1.token.token, workspace.id);
|
||||
const size = await getWorkspaceBlobsSize(app, u1.token.token, workspace.id);
|
||||
t.is(size, 4, 'failed to collect blob sizes');
|
||||
});
|
||||
|
||||
@ -143,3 +160,39 @@ test('should calc all blobs size', async t => {
|
||||
);
|
||||
t.is(size2, -1, 'failed to check blob size');
|
||||
});
|
||||
|
||||
test('should be able calc quota after switch plan', async t => {
|
||||
const u1 = await signUp(app, 'darksky', 'darksky@affine.pro', '1');
|
||||
|
||||
const workspace1 = await createWorkspace(app, u1.token.token);
|
||||
|
||||
const buffer1 = Buffer.from([0, 0]);
|
||||
await setBlob(app, u1.token.token, workspace1.id, buffer1);
|
||||
const buffer2 = Buffer.from([0, 1]);
|
||||
await setBlob(app, u1.token.token, workspace1.id, buffer2);
|
||||
|
||||
const workspace2 = await createWorkspace(app, u1.token.token);
|
||||
|
||||
const buffer3 = Buffer.from([0, 0]);
|
||||
await setBlob(app, u1.token.token, workspace2.id, buffer3);
|
||||
const buffer4 = Buffer.from([0, 1]);
|
||||
await setBlob(app, u1.token.token, workspace2.id, buffer4);
|
||||
|
||||
const size1 = await checkBlobSize(
|
||||
app,
|
||||
u1.token.token,
|
||||
workspace1.id,
|
||||
10 * 1024 * 1024 * 1024 - 8
|
||||
);
|
||||
t.is(size1, 0, 'failed to check free plan blob size');
|
||||
|
||||
quota.switchUserQuota(u1.id, QuotaType.Quota_ProPlanV1);
|
||||
|
||||
const size2 = await checkBlobSize(
|
||||
app,
|
||||
u1.token.token,
|
||||
workspace1.id,
|
||||
100 * 1024 * 1024 * 1024 - 8
|
||||
);
|
||||
t.is(size2, 0, 'failed to check pro plan blob size');
|
||||
});
|
||||
|
@ -9,6 +9,11 @@ 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 { MailService } from '../src/modules/auth/mailer';
|
||||
import { AuthService } from '../src/modules/auth/service';
|
||||
import {
|
||||
@ -39,6 +44,7 @@ test.beforeEach(async t => {
|
||||
await client.$disconnect();
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
providers: [RevertCommand, RunCommand],
|
||||
}).compile();
|
||||
const app = module.createNestApplication();
|
||||
app.use(
|
||||
@ -51,9 +57,17 @@ test.beforeEach(async t => {
|
||||
|
||||
const auth = module.get(AuthService);
|
||||
const mail = module.get(MailService);
|
||||
|
||||
t.context.app = app;
|
||||
t.context.auth = auth;
|
||||
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();
|
||||
});
|
||||
|
||||
test.afterEach.always(async t => {
|
||||
|
@ -4,6 +4,7 @@ import ava, { type TestFn } from 'ava';
|
||||
import { stub } from 'sinon';
|
||||
|
||||
import { AppModule } from '../src/app';
|
||||
import { Quotas } from '../src/modules/quota';
|
||||
import { UsersService } from '../src/modules/users';
|
||||
import { PermissionService } from '../src/modules/workspaces/permission';
|
||||
import { WorkspaceResolver } from '../src/modules/workspaces/resolver';
|
||||
@ -20,6 +21,9 @@ class FakePermission {
|
||||
user: new FakePrisma().fakeUser,
|
||||
};
|
||||
}
|
||||
async getOwnedWorkspaces() {
|
||||
return [''];
|
||||
}
|
||||
}
|
||||
|
||||
const fakeUserService = {
|
||||
@ -42,6 +46,19 @@ test.beforeEach(async t => {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
userFeatures: {
|
||||
async count() {
|
||||
return 1;
|
||||
},
|
||||
async findFirst() {
|
||||
return {
|
||||
createdAt: new Date(),
|
||||
expiredAt: new Date(),
|
||||
reason: '',
|
||||
feature: Quotas[0],
|
||||
};
|
||||
},
|
||||
},
|
||||
})
|
||||
.overrideProvider(PermissionService)
|
||||
.useClass(FakePermission)
|
||||
|
@ -6,6 +6,11 @@ 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 {
|
||||
acceptInviteById,
|
||||
createWorkspace,
|
||||
@ -34,6 +39,7 @@ test.beforeEach(async t => {
|
||||
await client.$disconnect();
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
providers: [RevertCommand, RunCommand],
|
||||
}).compile();
|
||||
const app = module.createNestApplication();
|
||||
app.use(
|
||||
@ -45,6 +51,13 @@ test.beforeEach(async t => {
|
||||
await app.init();
|
||||
t.context.client = client;
|
||||
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();
|
||||
});
|
||||
|
||||
test.afterEach.always(async t => {
|
||||
|
@ -1,5 +1,5 @@
|
||||
query blobSizes($workspaceId: String!) {
|
||||
collectBlobSizes(workspaceId: $workspaceId) {
|
||||
size
|
||||
workspace(id: $workspaceId) {
|
||||
blobsSize
|
||||
}
|
||||
}
|
||||
|
@ -56,12 +56,12 @@ mutation setBlob($workspaceId: String!, $blob: Upload!) {
|
||||
export const blobSizesQuery = {
|
||||
id: 'blobSizesQuery' as const,
|
||||
operationName: 'blobSizes',
|
||||
definitionName: 'collectBlobSizes',
|
||||
definitionName: 'workspace',
|
||||
containsFile: false,
|
||||
query: `
|
||||
query blobSizes($workspaceId: String!) {
|
||||
collectBlobSizes(workspaceId: $workspaceId) {
|
||||
size
|
||||
workspace(id: $workspaceId) {
|
||||
blobsSize
|
||||
}
|
||||
}`,
|
||||
};
|
||||
|
@ -40,10 +40,6 @@ export enum InvoiceStatus {
|
||||
Void = 'Void',
|
||||
}
|
||||
|
||||
export enum NewFeaturesKind {
|
||||
EarlyAccess = 'EarlyAccess',
|
||||
}
|
||||
|
||||
/** User permission in workspace */
|
||||
export enum Permission {
|
||||
Admin = 'Admin',
|
||||
@ -127,7 +123,7 @@ export type BlobSizesQueryVariables = Exact<{
|
||||
|
||||
export type BlobSizesQuery = {
|
||||
__typename?: 'Query';
|
||||
collectBlobSizes: { __typename?: 'WorkspaceBlobSizes'; size: number };
|
||||
workspace: { __typename?: 'WorkspaceType'; blobsSize: number };
|
||||
};
|
||||
|
||||
export type AllBlobSizesQueryVariables = Exact<{ [key: string]: never }>;
|
||||
|
@ -107,6 +107,20 @@ export async function createRandomUser(): Promise<{
|
||||
...user,
|
||||
emailVerified: new Date(),
|
||||
password: await hash(user.password),
|
||||
features: {
|
||||
create: {
|
||||
reason: 'created by test case',
|
||||
activated: true,
|
||||
feature: {
|
||||
connect: {
|
||||
feature_version: {
|
||||
feature: 'free_plan_v1',
|
||||
version: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
462
yarn.lock
462
yarn.lock
@ -2487,7 +2487,30 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.13.16, @babel/core@npm:^7.14.0, @babel/core@npm:^7.18.9, @babel/core@npm:^7.20.12, @babel/core@npm:^7.20.7, @babel/core@npm:^7.21.3, @babel/core@npm:^7.22.5, @babel/core@npm:^7.22.9, @babel/core@npm:^7.23.3, @babel/core@npm:^7.7.5":
|
||||
"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.13.16, @babel/core@npm:^7.14.0, @babel/core@npm:^7.20.12, @babel/core@npm:^7.20.7, @babel/core@npm:^7.21.3, @babel/core@npm:^7.22.5, @babel/core@npm:^7.22.9, @babel/core@npm:^7.7.5":
|
||||
version: 7.23.2
|
||||
resolution: "@babel/core@npm:7.23.2"
|
||||
dependencies:
|
||||
"@ampproject/remapping": "npm:^2.2.0"
|
||||
"@babel/code-frame": "npm:^7.22.13"
|
||||
"@babel/generator": "npm:^7.23.0"
|
||||
"@babel/helper-compilation-targets": "npm:^7.22.15"
|
||||
"@babel/helper-module-transforms": "npm:^7.23.0"
|
||||
"@babel/helpers": "npm:^7.23.2"
|
||||
"@babel/parser": "npm:^7.23.0"
|
||||
"@babel/template": "npm:^7.22.15"
|
||||
"@babel/traverse": "npm:^7.23.2"
|
||||
"@babel/types": "npm:^7.23.0"
|
||||
convert-source-map: "npm:^2.0.0"
|
||||
debug: "npm:^4.1.0"
|
||||
gensync: "npm:^1.0.0-beta.2"
|
||||
json5: "npm:^2.2.3"
|
||||
semver: "npm:^6.3.1"
|
||||
checksum: b69d7008695b2ac7a3a2db83c5c712fbb79f7031c4480f6351cde327930e38873003d1d021059b729a1d0cb48093f1d384c64269b78f6189f50051fe4f64dc2d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/core@npm:^7.18.9, @babel/core@npm:^7.23.3":
|
||||
version: 7.23.3
|
||||
resolution: "@babel/core@npm:7.23.3"
|
||||
dependencies:
|
||||
@ -2522,6 +2545,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/generator@npm:^7.23.0":
|
||||
version: 7.23.0
|
||||
resolution: "@babel/generator@npm:7.23.0"
|
||||
dependencies:
|
||||
"@babel/types": "npm:^7.23.0"
|
||||
"@jridgewell/gen-mapping": "npm:^0.3.2"
|
||||
"@jridgewell/trace-mapping": "npm:^0.3.17"
|
||||
jsesc: "npm:^2.5.1"
|
||||
checksum: bd1598bd356756065d90ce26968dd464ac2b915c67623f6f071fb487da5f9eb454031a380e20e7c9a7ce5c4a49d23be6cb9efde404952b0b3f3c0c3a9b73d68a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/generator@npm:^7.23.6":
|
||||
version: 7.23.6
|
||||
resolution: "@babel/generator@npm:7.23.6"
|
||||
@ -2656,6 +2691,21 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-module-transforms@npm:^7.23.0":
|
||||
version: 7.23.0
|
||||
resolution: "@babel/helper-module-transforms@npm:7.23.0"
|
||||
dependencies:
|
||||
"@babel/helper-environment-visitor": "npm:^7.22.20"
|
||||
"@babel/helper-module-imports": "npm:^7.22.15"
|
||||
"@babel/helper-simple-access": "npm:^7.22.5"
|
||||
"@babel/helper-split-export-declaration": "npm:^7.22.6"
|
||||
"@babel/helper-validator-identifier": "npm:^7.22.20"
|
||||
peerDependencies:
|
||||
"@babel/core": ^7.0.0
|
||||
checksum: d72fe444f7b6c5aadaac8f393298d603eedd48e5dead67273a48e5c83a677cbccbd8a12a06c5bf5d97924666083279158a4bd0e799d28b86cbbfacba9e41f598
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-module-transforms@npm:^7.23.3":
|
||||
version: 7.23.3
|
||||
resolution: "@babel/helper-module-transforms@npm:7.23.3"
|
||||
@ -2740,6 +2790,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-string-parser@npm:^7.22.5":
|
||||
version: 7.22.5
|
||||
resolution: "@babel/helper-string-parser@npm:7.22.5"
|
||||
checksum: 7f275a7f1a9504da06afc33441e219796352a4a3d0288a961bc14d1e30e06833a71621b33c3e60ee3ac1ff3c502d55e392bcbc0665f6f9d2629809696fab7cdd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-string-parser@npm:^7.23.4":
|
||||
version: 7.23.4
|
||||
resolution: "@babel/helper-string-parser@npm:7.23.4"
|
||||
@ -4089,6 +4146,24 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/traverse@npm:^7.23.2":
|
||||
version: 7.23.2
|
||||
resolution: "@babel/traverse@npm:7.23.2"
|
||||
dependencies:
|
||||
"@babel/code-frame": "npm:^7.22.13"
|
||||
"@babel/generator": "npm:^7.23.0"
|
||||
"@babel/helper-environment-visitor": "npm:^7.22.20"
|
||||
"@babel/helper-function-name": "npm:^7.23.0"
|
||||
"@babel/helper-hoist-variables": "npm:^7.22.5"
|
||||
"@babel/helper-split-export-declaration": "npm:^7.22.6"
|
||||
"@babel/parser": "npm:^7.23.0"
|
||||
"@babel/types": "npm:^7.23.0"
|
||||
debug: "npm:^4.1.0"
|
||||
globals: "npm:^11.1.0"
|
||||
checksum: e4fcb8f8395804956df4ae1301230a14b6eb35b74a7058a0e0b40f6f4be7281e619e6dafe400e833d4512da5d61cf17ea177d04b00a8f7cf3d8d69aff83ca3d8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/traverse@npm:^7.23.6":
|
||||
version: 7.23.6
|
||||
resolution: "@babel/traverse@npm:7.23.6"
|
||||
@ -4107,7 +4182,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/types@npm:^7.0.0, @babel/types@npm:^7.16.8, @babel/types@npm:^7.17.0, @babel/types@npm:^7.18.13, @babel/types@npm:^7.18.9, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.3, @babel/types@npm:^7.23.4, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3":
|
||||
"@babel/types@npm:^7.0.0, @babel/types@npm:^7.16.8, @babel/types@npm:^7.17.0, @babel/types@npm:^7.18.13, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3":
|
||||
version: 7.23.0
|
||||
resolution: "@babel/types@npm:7.23.0"
|
||||
dependencies:
|
||||
"@babel/helper-string-parser": "npm:^7.22.5"
|
||||
"@babel/helper-validator-identifier": "npm:^7.22.20"
|
||||
to-fast-properties: "npm:^2.0.0"
|
||||
checksum: ca5b896a26c91c5672254725c4c892a35567d2122afc47bd5331d1611a7f9230c19fc9ef591a5a6f80bf0d80737e104a9ac205c96447c74bee01d4319db58001
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/types@npm:^7.18.9, @babel/types@npm:^7.23.3, @babel/types@npm:^7.23.4":
|
||||
version: 7.23.4
|
||||
resolution: "@babel/types@npm:7.23.4"
|
||||
dependencies:
|
||||
@ -5318,6 +5404,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/android-arm64@npm:0.19.7":
|
||||
version: 0.19.7
|
||||
resolution: "@esbuild/android-arm64@npm:0.19.7"
|
||||
conditions: os=android & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/android-arm64@npm:0.19.8":
|
||||
version: 0.19.8
|
||||
resolution: "@esbuild/android-arm64@npm:0.19.8"
|
||||
@ -5346,6 +5439,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/android-arm@npm:0.19.7":
|
||||
version: 0.19.7
|
||||
resolution: "@esbuild/android-arm@npm:0.19.7"
|
||||
conditions: os=android & cpu=arm
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/android-arm@npm:0.19.8":
|
||||
version: 0.19.8
|
||||
resolution: "@esbuild/android-arm@npm:0.19.8"
|
||||
@ -5374,6 +5474,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/android-x64@npm:0.19.7":
|
||||
version: 0.19.7
|
||||
resolution: "@esbuild/android-x64@npm:0.19.7"
|
||||
conditions: os=android & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/android-x64@npm:0.19.8":
|
||||
version: 0.19.8
|
||||
resolution: "@esbuild/android-x64@npm:0.19.8"
|
||||
@ -5402,6 +5509,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/darwin-arm64@npm:0.19.7":
|
||||
version: 0.19.7
|
||||
resolution: "@esbuild/darwin-arm64@npm:0.19.7"
|
||||
conditions: os=darwin & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/darwin-arm64@npm:0.19.8":
|
||||
version: 0.19.8
|
||||
resolution: "@esbuild/darwin-arm64@npm:0.19.8"
|
||||
@ -5430,6 +5544,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/darwin-x64@npm:0.19.7":
|
||||
version: 0.19.7
|
||||
resolution: "@esbuild/darwin-x64@npm:0.19.7"
|
||||
conditions: os=darwin & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/darwin-x64@npm:0.19.8":
|
||||
version: 0.19.8
|
||||
resolution: "@esbuild/darwin-x64@npm:0.19.8"
|
||||
@ -5458,6 +5579,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/freebsd-arm64@npm:0.19.7":
|
||||
version: 0.19.7
|
||||
resolution: "@esbuild/freebsd-arm64@npm:0.19.7"
|
||||
conditions: os=freebsd & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/freebsd-arm64@npm:0.19.8":
|
||||
version: 0.19.8
|
||||
resolution: "@esbuild/freebsd-arm64@npm:0.19.8"
|
||||
@ -5486,6 +5614,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/freebsd-x64@npm:0.19.7":
|
||||
version: 0.19.7
|
||||
resolution: "@esbuild/freebsd-x64@npm:0.19.7"
|
||||
conditions: os=freebsd & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/freebsd-x64@npm:0.19.8":
|
||||
version: 0.19.8
|
||||
resolution: "@esbuild/freebsd-x64@npm:0.19.8"
|
||||
@ -5514,6 +5649,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-arm64@npm:0.19.7":
|
||||
version: 0.19.7
|
||||
resolution: "@esbuild/linux-arm64@npm:0.19.7"
|
||||
conditions: os=linux & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-arm64@npm:0.19.8":
|
||||
version: 0.19.8
|
||||
resolution: "@esbuild/linux-arm64@npm:0.19.8"
|
||||
@ -5542,6 +5684,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-arm@npm:0.19.7":
|
||||
version: 0.19.7
|
||||
resolution: "@esbuild/linux-arm@npm:0.19.7"
|
||||
conditions: os=linux & cpu=arm
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-arm@npm:0.19.8":
|
||||
version: 0.19.8
|
||||
resolution: "@esbuild/linux-arm@npm:0.19.8"
|
||||
@ -5570,6 +5719,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-ia32@npm:0.19.7":
|
||||
version: 0.19.7
|
||||
resolution: "@esbuild/linux-ia32@npm:0.19.7"
|
||||
conditions: os=linux & cpu=ia32
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-ia32@npm:0.19.8":
|
||||
version: 0.19.8
|
||||
resolution: "@esbuild/linux-ia32@npm:0.19.8"
|
||||
@ -5598,6 +5754,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-loong64@npm:0.19.7":
|
||||
version: 0.19.7
|
||||
resolution: "@esbuild/linux-loong64@npm:0.19.7"
|
||||
conditions: os=linux & cpu=loong64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-loong64@npm:0.19.8":
|
||||
version: 0.19.8
|
||||
resolution: "@esbuild/linux-loong64@npm:0.19.8"
|
||||
@ -5626,6 +5789,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-mips64el@npm:0.19.7":
|
||||
version: 0.19.7
|
||||
resolution: "@esbuild/linux-mips64el@npm:0.19.7"
|
||||
conditions: os=linux & cpu=mips64el
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-mips64el@npm:0.19.8":
|
||||
version: 0.19.8
|
||||
resolution: "@esbuild/linux-mips64el@npm:0.19.8"
|
||||
@ -5654,6 +5824,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-ppc64@npm:0.19.7":
|
||||
version: 0.19.7
|
||||
resolution: "@esbuild/linux-ppc64@npm:0.19.7"
|
||||
conditions: os=linux & cpu=ppc64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-ppc64@npm:0.19.8":
|
||||
version: 0.19.8
|
||||
resolution: "@esbuild/linux-ppc64@npm:0.19.8"
|
||||
@ -5682,6 +5859,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-riscv64@npm:0.19.7":
|
||||
version: 0.19.7
|
||||
resolution: "@esbuild/linux-riscv64@npm:0.19.7"
|
||||
conditions: os=linux & cpu=riscv64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-riscv64@npm:0.19.8":
|
||||
version: 0.19.8
|
||||
resolution: "@esbuild/linux-riscv64@npm:0.19.8"
|
||||
@ -5710,6 +5894,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-s390x@npm:0.19.7":
|
||||
version: 0.19.7
|
||||
resolution: "@esbuild/linux-s390x@npm:0.19.7"
|
||||
conditions: os=linux & cpu=s390x
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-s390x@npm:0.19.8":
|
||||
version: 0.19.8
|
||||
resolution: "@esbuild/linux-s390x@npm:0.19.8"
|
||||
@ -5738,6 +5929,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-x64@npm:0.19.7":
|
||||
version: 0.19.7
|
||||
resolution: "@esbuild/linux-x64@npm:0.19.7"
|
||||
conditions: os=linux & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-x64@npm:0.19.8":
|
||||
version: 0.19.8
|
||||
resolution: "@esbuild/linux-x64@npm:0.19.8"
|
||||
@ -5766,6 +5964,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/netbsd-x64@npm:0.19.7":
|
||||
version: 0.19.7
|
||||
resolution: "@esbuild/netbsd-x64@npm:0.19.7"
|
||||
conditions: os=netbsd & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/netbsd-x64@npm:0.19.8":
|
||||
version: 0.19.8
|
||||
resolution: "@esbuild/netbsd-x64@npm:0.19.8"
|
||||
@ -5794,6 +5999,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/openbsd-x64@npm:0.19.7":
|
||||
version: 0.19.7
|
||||
resolution: "@esbuild/openbsd-x64@npm:0.19.7"
|
||||
conditions: os=openbsd & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/openbsd-x64@npm:0.19.8":
|
||||
version: 0.19.8
|
||||
resolution: "@esbuild/openbsd-x64@npm:0.19.8"
|
||||
@ -5822,6 +6034,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/sunos-x64@npm:0.19.7":
|
||||
version: 0.19.7
|
||||
resolution: "@esbuild/sunos-x64@npm:0.19.7"
|
||||
conditions: os=sunos & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/sunos-x64@npm:0.19.8":
|
||||
version: 0.19.8
|
||||
resolution: "@esbuild/sunos-x64@npm:0.19.8"
|
||||
@ -5850,6 +6069,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/win32-arm64@npm:0.19.7":
|
||||
version: 0.19.7
|
||||
resolution: "@esbuild/win32-arm64@npm:0.19.7"
|
||||
conditions: os=win32 & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/win32-arm64@npm:0.19.8":
|
||||
version: 0.19.8
|
||||
resolution: "@esbuild/win32-arm64@npm:0.19.8"
|
||||
@ -5878,6 +6104,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/win32-ia32@npm:0.19.7":
|
||||
version: 0.19.7
|
||||
resolution: "@esbuild/win32-ia32@npm:0.19.7"
|
||||
conditions: os=win32 & cpu=ia32
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/win32-ia32@npm:0.19.8":
|
||||
version: 0.19.8
|
||||
resolution: "@esbuild/win32-ia32@npm:0.19.8"
|
||||
@ -5906,6 +6139,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/win32-x64@npm:0.19.7":
|
||||
version: 0.19.7
|
||||
resolution: "@esbuild/win32-x64@npm:0.19.7"
|
||||
conditions: os=win32 & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/win32-x64@npm:0.19.8":
|
||||
version: 0.19.8
|
||||
resolution: "@esbuild/win32-x64@npm:0.19.8"
|
||||
@ -6692,7 +6932,20 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@graphql-tools/utils@npm:^10.0.0, @graphql-tools/utils@npm:^10.0.10, @graphql-tools/utils@npm:^10.0.11, @graphql-tools/utils@npm:^10.0.2, @graphql-tools/utils@npm:^10.0.5, @graphql-tools/utils@npm:^10.0.8":
|
||||
"@graphql-tools/utils@npm:^10.0.0, @graphql-tools/utils@npm:^10.0.2, @graphql-tools/utils@npm:^10.0.5":
|
||||
version: 10.0.7
|
||||
resolution: "@graphql-tools/utils@npm:10.0.7"
|
||||
dependencies:
|
||||
"@graphql-typed-document-node/core": "npm:^3.1.1"
|
||||
dset: "npm:^3.1.2"
|
||||
tslib: "npm:^2.4.0"
|
||||
peerDependencies:
|
||||
graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0
|
||||
checksum: 50383782f7e2667f44891f060a0b91f1c551becccf919f041e0ce70bafd42021bf8b446273ce2b3efcd2de53d0b59b99f954c2f4094041fba86d478f616a30ea
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@graphql-tools/utils@npm:^10.0.10, @graphql-tools/utils@npm:^10.0.11, @graphql-tools/utils@npm:^10.0.8":
|
||||
version: 10.0.11
|
||||
resolution: "@graphql-tools/utils@npm:10.0.11"
|
||||
dependencies:
|
||||
@ -11799,6 +12052,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@smithy/abort-controller@npm:^2.0.12":
|
||||
version: 2.0.12
|
||||
resolution: "@smithy/abort-controller@npm:2.0.12"
|
||||
dependencies:
|
||||
"@smithy/types": "npm:^2.4.0"
|
||||
tslib: "npm:^2.5.0"
|
||||
checksum: ade23e7e6d3b30615cb376e2578b7f9545a2e0c1ab67f570a76ce5dde3547c6dde0964976e3e914f4844df0dd0ddf9ddc38820ba69f61eed2fffe6e563d0c4e4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@smithy/abort-controller@npm:^2.0.14":
|
||||
version: 2.0.14
|
||||
resolution: "@smithy/abort-controller@npm:2.0.14"
|
||||
@ -12061,7 +12324,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@smithy/node-http-handler@npm:^2.1.10, @smithy/node-http-handler@npm:^2.1.8, @smithy/node-http-handler@npm:^2.1.9":
|
||||
"@smithy/node-http-handler@npm:^2.1.10, @smithy/node-http-handler@npm:^2.1.9":
|
||||
version: 2.1.10
|
||||
resolution: "@smithy/node-http-handler@npm:2.1.10"
|
||||
dependencies:
|
||||
@ -12074,6 +12337,19 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@smithy/node-http-handler@npm:^2.1.8":
|
||||
version: 2.1.8
|
||||
resolution: "@smithy/node-http-handler@npm:2.1.8"
|
||||
dependencies:
|
||||
"@smithy/abort-controller": "npm:^2.0.12"
|
||||
"@smithy/protocol-http": "npm:^3.0.8"
|
||||
"@smithy/querystring-builder": "npm:^2.0.12"
|
||||
"@smithy/types": "npm:^2.4.0"
|
||||
tslib: "npm:^2.5.0"
|
||||
checksum: aca079234edc6d8946df0408949af3eee0f862225e6ebafcd72123b96f087213e2a4f7bb71d6d6a21eebc78dae636f5c999c91700f7577c6ba61998f05b070ae
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@smithy/property-provider@npm:^2.0.0, @smithy/property-provider@npm:^2.0.15":
|
||||
version: 2.0.15
|
||||
resolution: "@smithy/property-provider@npm:2.0.15"
|
||||
@ -12084,7 +12360,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@smithy/protocol-http@npm:^3.0.10, @smithy/protocol-http@npm:^3.0.8, @smithy/protocol-http@npm:^3.0.9":
|
||||
"@smithy/protocol-http@npm:^3.0.10, @smithy/protocol-http@npm:^3.0.9":
|
||||
version: 3.0.10
|
||||
resolution: "@smithy/protocol-http@npm:3.0.10"
|
||||
dependencies:
|
||||
@ -12094,6 +12370,27 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@smithy/protocol-http@npm:^3.0.8":
|
||||
version: 3.0.8
|
||||
resolution: "@smithy/protocol-http@npm:3.0.8"
|
||||
dependencies:
|
||||
"@smithy/types": "npm:^2.4.0"
|
||||
tslib: "npm:^2.5.0"
|
||||
checksum: 014df5fe50231434b5227b8359f31d925de77c581d576170b4d62fdd64cb3c24b35aeec636f229aba3cd303f32a12e0c1be3355af883dbe73f995e4b975ac0f7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@smithy/querystring-builder@npm:^2.0.12":
|
||||
version: 2.0.12
|
||||
resolution: "@smithy/querystring-builder@npm:2.0.12"
|
||||
dependencies:
|
||||
"@smithy/types": "npm:^2.4.0"
|
||||
"@smithy/util-uri-escape": "npm:^2.0.0"
|
||||
tslib: "npm:^2.5.0"
|
||||
checksum: e3ba93e7195b6240b052ff88833685f926ee14191880214bf7c073aae5315e4956b57762a96745e2bd2f1d2bc7f2fa66f797400a739fdde7c13bed83d2c56cdf
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@smithy/querystring-builder@npm:^2.0.14":
|
||||
version: 2.0.14
|
||||
resolution: "@smithy/querystring-builder@npm:2.0.14"
|
||||
@ -14604,7 +14901,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/lodash-es@npm:^4.17.11, @types/lodash-es@npm:^4.17.6, @types/lodash-es@npm:^4.17.9":
|
||||
"@types/lodash-es@npm:^4.17.11":
|
||||
version: 4.17.12
|
||||
resolution: "@types/lodash-es@npm:4.17.12"
|
||||
dependencies:
|
||||
@ -14613,6 +14910,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/lodash-es@npm:^4.17.6, @types/lodash-es@npm:^4.17.9":
|
||||
version: 4.17.9
|
||||
resolution: "@types/lodash-es@npm:4.17.9"
|
||||
dependencies:
|
||||
"@types/lodash": "npm:*"
|
||||
checksum: 5e3a8a74134e67c37f1b8eb4a2897c88038f1b1bd7f508feec9e5561b52787d7efcc30c18981e9c6edec2b894f127b60312a431d98b84e12e785bea9cb5d1d40
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/lodash.debounce@npm:^4.0.7":
|
||||
version: 4.0.9
|
||||
resolution: "@types/lodash.debounce@npm:4.0.9"
|
||||
@ -14747,7 +15053,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:^18.0.0, @types/node@npm:^18.11.18, @types/node@npm:^18.11.9":
|
||||
"@types/node@npm:^18.0.0, @types/node@npm:^18.11.9":
|
||||
version: 18.18.13
|
||||
resolution: "@types/node@npm:18.18.13"
|
||||
dependencies:
|
||||
@ -14756,6 +15062,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:^18.11.18":
|
||||
version: 18.18.5
|
||||
resolution: "@types/node@npm:18.18.5"
|
||||
checksum: a7363aab9f402290799d3e2696fbc70c76a8a65e2354f72b8f399c38edc346f600066f8ac59dde985cfc64160cfeb63ed7fc917aecdfe7ec469345d3ce029bda
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/nodemailer@npm:^6.4.14":
|
||||
version: 6.4.14
|
||||
resolution: "@types/nodemailer@npm:6.4.14"
|
||||
@ -15077,7 +15390,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/ws@npm:^8.0.0, @types/ws@npm:^8.5.10, @types/ws@npm:^8.5.5, @types/ws@npm:^8.5.7":
|
||||
"@types/ws@npm:^8.0.0, @types/ws@npm:^8.5.5, @types/ws@npm:^8.5.7":
|
||||
version: 8.5.7
|
||||
resolution: "@types/ws@npm:8.5.7"
|
||||
dependencies:
|
||||
"@types/node": "npm:*"
|
||||
checksum: 48e426be74d6bdc176c06f98cc96f7fc91dba10aaf88c87108b57e1dba588f4607dcd062d7a83686a3857dc7af09fdd420d8a816c0306cb0362ece2f0e37983c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/ws@npm:^8.5.10":
|
||||
version: 8.5.10
|
||||
resolution: "@types/ws@npm:8.5.10"
|
||||
dependencies:
|
||||
@ -16128,7 +16450,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"acorn@npm:^8.10.0, acorn@npm:^8.11.2, acorn@npm:^8.4.1, acorn@npm:^8.6.0, acorn@npm:^8.7.1, acorn@npm:^8.8.0, acorn@npm:^8.8.1, acorn@npm:^8.8.2, acorn@npm:^8.9.0":
|
||||
"acorn@npm:^8.10.0, acorn@npm:^8.4.1, acorn@npm:^8.7.1, acorn@npm:^8.8.0, acorn@npm:^8.8.1, acorn@npm:^8.8.2, acorn@npm:^8.9.0":
|
||||
version: 8.10.0
|
||||
resolution: "acorn@npm:8.10.0"
|
||||
bin:
|
||||
acorn: bin/acorn
|
||||
checksum: 522310c20fdc3c271caed3caf0f06c51d61cb42267279566edd1d58e83dbc12eebdafaab666a0f0be1b7ad04af9c6bc2a6f478690a9e6391c3c8b165ada917dd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"acorn@npm:^8.11.2, acorn@npm:^8.6.0":
|
||||
version: 8.11.2
|
||||
resolution: "acorn@npm:8.11.2"
|
||||
bin:
|
||||
@ -20758,7 +21089,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"esbuild@npm:^0.19.3, esbuild@npm:^0.19.7":
|
||||
"esbuild@npm:^0.19.3":
|
||||
version: 0.19.8
|
||||
resolution: "esbuild@npm:0.19.8"
|
||||
dependencies:
|
||||
@ -20835,6 +21166,83 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"esbuild@npm:^0.19.7":
|
||||
version: 0.19.7
|
||||
resolution: "esbuild@npm:0.19.7"
|
||||
dependencies:
|
||||
"@esbuild/android-arm": "npm:0.19.7"
|
||||
"@esbuild/android-arm64": "npm:0.19.7"
|
||||
"@esbuild/android-x64": "npm:0.19.7"
|
||||
"@esbuild/darwin-arm64": "npm:0.19.7"
|
||||
"@esbuild/darwin-x64": "npm:0.19.7"
|
||||
"@esbuild/freebsd-arm64": "npm:0.19.7"
|
||||
"@esbuild/freebsd-x64": "npm:0.19.7"
|
||||
"@esbuild/linux-arm": "npm:0.19.7"
|
||||
"@esbuild/linux-arm64": "npm:0.19.7"
|
||||
"@esbuild/linux-ia32": "npm:0.19.7"
|
||||
"@esbuild/linux-loong64": "npm:0.19.7"
|
||||
"@esbuild/linux-mips64el": "npm:0.19.7"
|
||||
"@esbuild/linux-ppc64": "npm:0.19.7"
|
||||
"@esbuild/linux-riscv64": "npm:0.19.7"
|
||||
"@esbuild/linux-s390x": "npm:0.19.7"
|
||||
"@esbuild/linux-x64": "npm:0.19.7"
|
||||
"@esbuild/netbsd-x64": "npm:0.19.7"
|
||||
"@esbuild/openbsd-x64": "npm:0.19.7"
|
||||
"@esbuild/sunos-x64": "npm:0.19.7"
|
||||
"@esbuild/win32-arm64": "npm:0.19.7"
|
||||
"@esbuild/win32-ia32": "npm:0.19.7"
|
||||
"@esbuild/win32-x64": "npm:0.19.7"
|
||||
dependenciesMeta:
|
||||
"@esbuild/android-arm":
|
||||
optional: true
|
||||
"@esbuild/android-arm64":
|
||||
optional: true
|
||||
"@esbuild/android-x64":
|
||||
optional: true
|
||||
"@esbuild/darwin-arm64":
|
||||
optional: true
|
||||
"@esbuild/darwin-x64":
|
||||
optional: true
|
||||
"@esbuild/freebsd-arm64":
|
||||
optional: true
|
||||
"@esbuild/freebsd-x64":
|
||||
optional: true
|
||||
"@esbuild/linux-arm":
|
||||
optional: true
|
||||
"@esbuild/linux-arm64":
|
||||
optional: true
|
||||
"@esbuild/linux-ia32":
|
||||
optional: true
|
||||
"@esbuild/linux-loong64":
|
||||
optional: true
|
||||
"@esbuild/linux-mips64el":
|
||||
optional: true
|
||||
"@esbuild/linux-ppc64":
|
||||
optional: true
|
||||
"@esbuild/linux-riscv64":
|
||||
optional: true
|
||||
"@esbuild/linux-s390x":
|
||||
optional: true
|
||||
"@esbuild/linux-x64":
|
||||
optional: true
|
||||
"@esbuild/netbsd-x64":
|
||||
optional: true
|
||||
"@esbuild/openbsd-x64":
|
||||
optional: true
|
||||
"@esbuild/sunos-x64":
|
||||
optional: true
|
||||
"@esbuild/win32-arm64":
|
||||
optional: true
|
||||
"@esbuild/win32-ia32":
|
||||
optional: true
|
||||
"@esbuild/win32-x64":
|
||||
optional: true
|
||||
bin:
|
||||
esbuild: bin/esbuild
|
||||
checksum: 326b9d98a77c5f2fb9a535b292bdc67c88bdfb4a19d29a221d65fd69f4800faea1f34947e8e6bc25ca3bd5db01f61c6968fec91f8c335e21e29b50330d90bd89
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"escalade@npm:^3.1.1":
|
||||
version: 3.1.1
|
||||
resolution: "escalade@npm:3.1.1"
|
||||
@ -23081,7 +23489,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"graphql-ws@npm:5.14.2, graphql-ws@npm:^5.14.0":
|
||||
"graphql-ws@npm:5.14.2":
|
||||
version: 5.14.2
|
||||
resolution: "graphql-ws@npm:5.14.2"
|
||||
peerDependencies:
|
||||
@ -23090,6 +23498,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"graphql-ws@npm:^5.14.0":
|
||||
version: 5.14.1
|
||||
resolution: "graphql-ws@npm:5.14.1"
|
||||
peerDependencies:
|
||||
graphql: ">=0.11 <=16"
|
||||
checksum: d3b0917df3ae20aa65b5193527f7005cdce35d7c59856adc3aad2ff128952b9f6f207c0cc4f92bb5d7b5210d458243a93fb3e58339253ed830a5b17619d21ea8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"graphql@npm:0.13.1 - 16, graphql@npm:^16.0.0, graphql@npm:^16.8.1":
|
||||
version: 16.8.1
|
||||
resolution: "graphql@npm:16.8.1"
|
||||
@ -26223,7 +26640,19 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lib0@npm:^0.2.74, lib0@npm:^0.2.85, lib0@npm:^0.2.86, lib0@npm:^0.2.87, lib0@npm:^0.2.88":
|
||||
"lib0@npm:^0.2.74, lib0@npm:^0.2.85, lib0@npm:^0.2.87":
|
||||
version: 0.2.87
|
||||
resolution: "lib0@npm:0.2.87"
|
||||
dependencies:
|
||||
isomorphic.js: "npm:^0.2.4"
|
||||
bin:
|
||||
0gentesthtml: bin/gentesthtml.js
|
||||
0serve: bin/0serve.js
|
||||
checksum: 078a55d1a6eb85a6fe836cf8c1268fa4761e475679db7f31c4993acd8a3a15e16c20e4481da9239402ff8a02a3718be9572767b3d6759e23a3518bcc0cc6b520
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lib0@npm:^0.2.86, lib0@npm:^0.2.88":
|
||||
version: 0.2.88
|
||||
resolution: "lib0@npm:0.2.88"
|
||||
dependencies:
|
||||
@ -37089,7 +37518,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"yaml@npm:2.3.4, yaml@npm:^2.2.1, yaml@npm:^2.3.1, yaml@npm:^2.3.4":
|
||||
"yaml@npm:2.3.4, yaml@npm:^2.3.4":
|
||||
version: 2.3.4
|
||||
resolution: "yaml@npm:2.3.4"
|
||||
checksum: f8207ce43065a22268a2806ea6a0fa3974c6fde92b4b2fa0082357e487bc333e85dc518910007e7ac001b532c7c84bd3eccb6c7757e94182b564028b0008f44b
|
||||
@ -37103,6 +37532,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"yaml@npm:^2.2.1, yaml@npm:^2.3.1":
|
||||
version: 2.3.3
|
||||
resolution: "yaml@npm:2.3.3"
|
||||
checksum: 3b1a974b9d3672c671d47099a41c0de77b7ff978d0849aa55a095587486e82cd072321d19f2b4c791a367f766310b5a82dff098839b0f4ddcbbbe477f82dfb07
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"yargs-parser@npm:21.1.1, yargs-parser@npm:>=21.1.1, yargs-parser@npm:^21.1.1":
|
||||
version: 21.1.1
|
||||
resolution: "yargs-parser@npm:21.1.1"
|
||||
|
Loading…
Reference in New Issue
Block a user