mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-15 09:06:19 +08:00
feat: user usage init (#5074)
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { PrismaService } from '../../prisma';
|
||||
import { Feature, FeatureKind, FeatureType } from './types';
|
||||
|
||||
@Injectable()
|
||||
export class FeatureService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async getFeaturesVersion() {
|
||||
const features = await this.prisma.features.findMany({
|
||||
where: {
|
||||
type: FeatureKind.Feature,
|
||||
},
|
||||
select: {
|
||||
feature: true,
|
||||
version: true,
|
||||
},
|
||||
});
|
||||
return features.reduce(
|
||||
(acc, feature) => {
|
||||
acc[feature.feature] = feature.version;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>
|
||||
);
|
||||
}
|
||||
|
||||
async getFeature(feature: FeatureType) {
|
||||
return this.prisma.features.findFirst({
|
||||
where: {
|
||||
feature,
|
||||
type: FeatureKind.Feature,
|
||||
},
|
||||
orderBy: {
|
||||
version: 'desc',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async addUserFeature(
|
||||
userId: string,
|
||||
feature: FeatureType,
|
||||
version: number,
|
||||
reason: string,
|
||||
expiredAt?: Date | string
|
||||
) {
|
||||
return this.prisma.$transaction(async tx => {
|
||||
const latestFlag = await tx.userFeatures.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
feature: {
|
||||
feature,
|
||||
type: FeatureKind.Feature,
|
||||
},
|
||||
activated: true,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
if (latestFlag) {
|
||||
return latestFlag.id;
|
||||
} else {
|
||||
return tx.userFeatures
|
||||
.create({
|
||||
data: {
|
||||
reason,
|
||||
expiredAt,
|
||||
activated: true,
|
||||
user: {
|
||||
connect: {
|
||||
id: userId,
|
||||
},
|
||||
},
|
||||
feature: {
|
||||
connect: {
|
||||
feature_version: {
|
||||
feature,
|
||||
version,
|
||||
},
|
||||
type: FeatureKind.Feature,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(r => r.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async removeUserFeature(userId: string, feature: FeatureType) {
|
||||
return this.prisma.userFeatures
|
||||
.updateMany({
|
||||
where: {
|
||||
userId,
|
||||
feature: {
|
||||
feature,
|
||||
type: FeatureKind.Feature,
|
||||
},
|
||||
activated: true,
|
||||
},
|
||||
data: {
|
||||
activated: false,
|
||||
},
|
||||
})
|
||||
.then(r => r.count);
|
||||
}
|
||||
|
||||
async getUserFeatures(userId: string) {
|
||||
const features = await this.prisma.userFeatures.findMany({
|
||||
where: {
|
||||
user: { id: userId },
|
||||
feature: {
|
||||
type: FeatureKind.Feature,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
activated: true,
|
||||
reason: true,
|
||||
createdAt: true,
|
||||
expiredAt: true,
|
||||
feature: {
|
||||
select: {
|
||||
feature: true,
|
||||
configs: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return features as typeof features &
|
||||
{
|
||||
feature: Pick<Feature, 'feature' | 'configs'>;
|
||||
}[];
|
||||
}
|
||||
|
||||
async listFeatureUsers(feature: FeatureType) {
|
||||
return this.prisma.userFeatures
|
||||
.findMany({
|
||||
where: {
|
||||
activated: true,
|
||||
feature: {
|
||||
feature: feature,
|
||||
type: FeatureKind.Feature,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
avatarUrl: true,
|
||||
email: true,
|
||||
emailVerified: true,
|
||||
createdAt: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(users => users.map(user => user.user));
|
||||
}
|
||||
|
||||
async hasFeature(userId: string, feature: FeatureType) {
|
||||
return this.prisma.userFeatures
|
||||
.count({
|
||||
where: {
|
||||
userId,
|
||||
activated: true,
|
||||
feature: {
|
||||
feature,
|
||||
type: FeatureKind.Feature,
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(count => count > 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { Config } from '../../config';
|
||||
import { PrismaService } from '../../prisma';
|
||||
import { FeatureService } from './configure';
|
||||
import { FeatureType } from './types';
|
||||
|
||||
export enum NewFeaturesKind {
|
||||
EarlyAccess,
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class FeatureManagementService {
|
||||
protected logger = new Logger(FeatureManagementService.name);
|
||||
constructor(
|
||||
private readonly feature: FeatureService,
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly config: Config
|
||||
) {}
|
||||
|
||||
isStaff(email: string) {
|
||||
return email.endsWith('@toeverything.info');
|
||||
}
|
||||
|
||||
async addEarlyAccess(userId: string) {
|
||||
return this.feature.addUserFeature(
|
||||
userId,
|
||||
FeatureType.EarlyAccess,
|
||||
1,
|
||||
'Early access user'
|
||||
);
|
||||
}
|
||||
|
||||
async removeEarlyAccess(userId: string) {
|
||||
return this.feature.removeUserFeature(userId, FeatureType.EarlyAccess);
|
||||
}
|
||||
|
||||
async listEarlyAccess() {
|
||||
return this.feature.listFeatureUsers(FeatureType.EarlyAccess);
|
||||
}
|
||||
|
||||
/// check early access by email
|
||||
async canEarlyAccess(email: string) {
|
||||
if (
|
||||
this.config.featureFlags.earlyAccessPreview &&
|
||||
!email.endsWith('@toeverything.info')
|
||||
) {
|
||||
const user = await this.prisma.user.findFirst({
|
||||
where: {
|
||||
email,
|
||||
},
|
||||
});
|
||||
if (user) {
|
||||
const canEarlyAccess = await this.feature
|
||||
.hasFeature(user.id, FeatureType.EarlyAccess)
|
||||
.catch(() => false);
|
||||
if (canEarlyAccess) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: Outdated, switch to feature gates
|
||||
const oldCanEarlyAccess = await this.prisma.newFeaturesWaitingList
|
||||
.findUnique({
|
||||
where: { email, type: NewFeaturesKind.EarlyAccess },
|
||||
})
|
||||
.then(x => !!x)
|
||||
.catch(() => false);
|
||||
if (oldCanEarlyAccess) {
|
||||
this.logger.warn(
|
||||
`User ${email} has early access in old table but not in new table`
|
||||
);
|
||||
}
|
||||
return oldCanEarlyAccess;
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
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.
|
||||
* includes:
|
||||
* - feature query/update/permit
|
||||
* - feature statistics
|
||||
*/
|
||||
@Module({
|
||||
providers: [FeatureService, FeatureManagementService],
|
||||
exports: [FeatureService, FeatureManagementService],
|
||||
})
|
||||
export class FeatureModule {}
|
||||
|
||||
export type { CommonFeature, Feature } from './types';
|
||||
export { FeatureKind, Features, FeatureType } from './types';
|
||||
export {
|
||||
FeatureManagementService,
|
||||
FeatureService,
|
||||
PrismaService,
|
||||
upsertFeature,
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { Prisma } from '@prisma/client';
|
||||
|
||||
export enum FeatureKind {
|
||||
Feature,
|
||||
Quota,
|
||||
}
|
||||
|
||||
export type CommonFeature = {
|
||||
feature: string;
|
||||
type: FeatureKind;
|
||||
version: number;
|
||||
configs: Prisma.InputJsonValue;
|
||||
};
|
||||
|
||||
export type Feature = CommonFeature & {
|
||||
type: FeatureKind.Feature;
|
||||
feature: FeatureType;
|
||||
};
|
||||
|
||||
export enum FeatureType {
|
||||
EarlyAccess = 'early_access',
|
||||
}
|
||||
|
||||
export const Features: Feature[] = [
|
||||
{
|
||||
feature: FeatureType.EarlyAccess,
|
||||
type: FeatureKind.Feature,
|
||||
version: 1,
|
||||
configs: {
|
||||
whitelist: ['@toeverything.info'],
|
||||
},
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user