mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 12:28:42 +00:00
refactor(server): use feature model (#9932)
This commit is contained in:
@@ -5,16 +5,18 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { once } from 'lodash-es';
|
||||
import { Command, CommandRunner } from 'nest-commander';
|
||||
|
||||
interface Migration {
|
||||
file: string;
|
||||
name: string;
|
||||
always?: boolean;
|
||||
up: (db: PrismaClient, injector: ModuleRef) => Promise<void>;
|
||||
down: (db: PrismaClient, injector: ModuleRef) => Promise<void>;
|
||||
}
|
||||
|
||||
export async function collectMigrations(): Promise<Migration[]> {
|
||||
export const collectMigrations = once(async () => {
|
||||
const folder = join(fileURLToPath(import.meta.url), '../../migrations');
|
||||
|
||||
const migrationFiles = readdirSync(folder)
|
||||
@@ -33,6 +35,7 @@ export async function collectMigrations(): Promise<Migration[]> {
|
||||
return {
|
||||
file,
|
||||
name: migration.name,
|
||||
always: migration.always,
|
||||
up: migration.up,
|
||||
down: migration.down,
|
||||
};
|
||||
@@ -41,7 +44,8 @@ export async function collectMigrations(): Promise<Migration[]> {
|
||||
);
|
||||
|
||||
return migrations;
|
||||
}
|
||||
});
|
||||
|
||||
@Command({
|
||||
name: 'run',
|
||||
description: 'Run all pending data migrations',
|
||||
@@ -65,7 +69,7 @@ export class RunCommand extends CommandRunner {
|
||||
},
|
||||
});
|
||||
|
||||
if (exists) {
|
||||
if (exists && !migration.always) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -100,8 +104,14 @@ export class RunCommand extends CommandRunner {
|
||||
|
||||
private async runMigration(migration: Migration) {
|
||||
this.logger.log(`Running ${migration.name}...`);
|
||||
const record = await this.db.dataMigration.create({
|
||||
data: {
|
||||
const record = await this.db.dataMigration.upsert({
|
||||
where: {
|
||||
name: migration.name,
|
||||
},
|
||||
update: {
|
||||
startedAt: new Date(),
|
||||
},
|
||||
create: {
|
||||
name: migration.name,
|
||||
startedAt: new Date(),
|
||||
},
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
import { FeatureModel } from '../../models';
|
||||
|
||||
export class RefreshFeatures0001 {
|
||||
static always = true;
|
||||
|
||||
// do the migration
|
||||
static async up(_db: PrismaClient, ref: ModuleRef) {
|
||||
await ref.get(FeatureModel, { strict: false }).refreshFeatures();
|
||||
}
|
||||
|
||||
// revert the migration
|
||||
static async down(_db: PrismaClient) {}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
import { Features } from '../../core/features';
|
||||
import { Quotas } from '../../core/quota/schema';
|
||||
import { upsertFeature } from './utils/user-features';
|
||||
|
||||
export class UserFeaturesInit1698652531198 {
|
||||
// do the migration
|
||||
static async up(db: PrismaClient) {
|
||||
// upgrade features from lower version to higher version
|
||||
for (const feature of Features) {
|
||||
await upsertFeature(db, feature);
|
||||
}
|
||||
|
||||
for (const quota of Quotas) {
|
||||
await upsertFeature(db, quota);
|
||||
}
|
||||
}
|
||||
|
||||
// revert the migration
|
||||
static async down(_db: PrismaClient) {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
import { QuotaType } from '../../core/quota/types';
|
||||
export class OldUserFeature1702620653283 {
|
||||
// do the migration
|
||||
static async up(db: PrismaClient) {
|
||||
await db.$transaction(async tx => {
|
||||
const latestFreePlan = await tx.feature.findFirstOrThrow({
|
||||
where: { feature: QuotaType.FreePlanV1 },
|
||||
orderBy: { version: 'desc' },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
// find all users that don't have any features
|
||||
const userIds = await db.user.findMany({
|
||||
where: { NOT: { features: { some: { NOT: { id: { gt: 0 } } } } } },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
await tx.userFeature.createMany({
|
||||
data: userIds.map(({ id: userId }) => ({
|
||||
userId,
|
||||
featureId: latestFreePlan.id,
|
||||
reason: 'old user feature migration',
|
||||
activated: true,
|
||||
})),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// revert the migration
|
||||
// WARN: this will drop all user features
|
||||
static async down(db: PrismaClient) {
|
||||
await db.userFeature.deleteMany({});
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
import { Features } from '../../core/features';
|
||||
import { upsertFeature } from './utils/user-features';
|
||||
|
||||
export class RefreshUserFeatures1704352562369 {
|
||||
// do the migration
|
||||
static async up(db: PrismaClient) {
|
||||
// add early access v2 & copilot feature
|
||||
for (const feature of Features) {
|
||||
await upsertFeature(db, feature);
|
||||
}
|
||||
}
|
||||
|
||||
// revert the migration
|
||||
static async down(_db: PrismaClient) {}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
import { Quotas } from '../../core/quota/schema';
|
||||
import { upgradeQuotaVersion } from './utils/user-quotas';
|
||||
|
||||
export class NewFreePlan1705395933447 {
|
||||
// do the migration
|
||||
static async up(db: PrismaClient) {
|
||||
// free plan 1.0
|
||||
const quota = Quotas[3];
|
||||
await upgradeQuotaVersion(db, quota, 'free plan 1.0 migration');
|
||||
}
|
||||
|
||||
// revert the migration
|
||||
static async down(_db: PrismaClient) {}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
import { Quotas } from '../../core/quota/schema';
|
||||
import { upgradeQuotaVersion } from './utils/user-quotas';
|
||||
|
||||
export class BusinessBlobLimit1706513866287 {
|
||||
// do the migration
|
||||
static async up(db: PrismaClient) {
|
||||
// free plan 1.1
|
||||
const quota = Quotas[4];
|
||||
await upgradeQuotaVersion(db, quota, 'free plan 1.1 migration');
|
||||
}
|
||||
|
||||
// revert the migration
|
||||
static async down(_db: PrismaClient) {}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
import { FeatureType } from '../../core/features';
|
||||
import { upsertLatestFeatureVersion } from './utils/user-features';
|
||||
|
||||
export class RefreshUnlimitedWorkspaceFeature1708321519830 {
|
||||
// do the migration
|
||||
static async up(db: PrismaClient) {
|
||||
// add unlimited workspace feature
|
||||
await upsertLatestFeatureVersion(db, FeatureType.UnlimitedWorkspace);
|
||||
}
|
||||
|
||||
// revert the migration
|
||||
static async down(_db: PrismaClient) {}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
import { QuotaType } from '../../core/quota/types';
|
||||
import { upgradeLatestQuotaVersion } from './utils/user-quotas';
|
||||
|
||||
export class RefreshFreePlan1712224382221 {
|
||||
// do the migration
|
||||
static async up(db: PrismaClient) {
|
||||
// free plan 1.1
|
||||
await upgradeLatestQuotaVersion(
|
||||
db,
|
||||
QuotaType.FreePlanV1,
|
||||
'free plan 1.1 migration'
|
||||
);
|
||||
}
|
||||
|
||||
// revert the migration
|
||||
static async down(_db: PrismaClient) {}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
import { QuotaType } from '../../core/quota/types';
|
||||
import { upgradeLatestQuotaVersion } from './utils/user-quotas';
|
||||
|
||||
export class CopilotFeature1713164714634 {
|
||||
// do the migration
|
||||
static async up(db: PrismaClient) {
|
||||
await upgradeLatestQuotaVersion(
|
||||
db,
|
||||
QuotaType.ProPlanV1,
|
||||
'pro plan 1.1 migration'
|
||||
);
|
||||
await upgradeLatestQuotaVersion(
|
||||
db,
|
||||
QuotaType.RestrictedPlanV1,
|
||||
'restricted plan 1.1 migration'
|
||||
);
|
||||
}
|
||||
|
||||
// revert the migration
|
||||
static async down(_db: PrismaClient) {}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
import { FeatureType } from '../../core/features';
|
||||
import { upsertLatestFeatureVersion } from './utils/user-features';
|
||||
|
||||
export class AiEarlyAccess1713176777814 {
|
||||
// do the migration
|
||||
static async up(db: PrismaClient) {
|
||||
await upsertLatestFeatureVersion(db, FeatureType.AIEarlyAccess);
|
||||
}
|
||||
|
||||
// revert the migration
|
||||
static async down(_db: PrismaClient) {}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
import { FeatureType } from '../../core/features';
|
||||
import { upsertLatestFeatureVersion } from './utils/user-features';
|
||||
|
||||
export class UnlimitedCopilot1713285638427 {
|
||||
// do the migration
|
||||
static async up(db: PrismaClient) {
|
||||
await upsertLatestFeatureVersion(db, FeatureType.UnlimitedCopilot);
|
||||
}
|
||||
|
||||
// revert the migration
|
||||
static async down(_db: PrismaClient) {}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
import { FeatureType } from '../../core/features';
|
||||
import { upsertLatestFeatureVersion } from './utils/user-features';
|
||||
|
||||
export class AdministratorFeature1716195522794 {
|
||||
// do the migration
|
||||
static async up(db: PrismaClient) {
|
||||
await upsertLatestFeatureVersion(db, FeatureType.Admin);
|
||||
}
|
||||
|
||||
// revert the migration
|
||||
static async down(_db: PrismaClient) {}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
import { QuotaType } from '../../core/quota';
|
||||
import { upsertLatestQuotaVersion } from './utils/user-quotas';
|
||||
|
||||
export class LifetimeProQuota1719917815802 {
|
||||
// do the migration
|
||||
static async up(db: PrismaClient) {
|
||||
await upsertLatestQuotaVersion(db, QuotaType.LifetimeProPlanV1);
|
||||
}
|
||||
|
||||
// revert the migration
|
||||
static async down(_db: PrismaClient) {}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
import { QuotaType } from '../../core/quota';
|
||||
import { upsertLatestQuotaVersion } from './utils/user-quotas';
|
||||
|
||||
export class TeamQuota1733804966417 {
|
||||
// do the migration
|
||||
static async up(db: PrismaClient) {
|
||||
await upsertLatestQuotaVersion(db, QuotaType.TeamPlanV1);
|
||||
}
|
||||
|
||||
// revert the migration
|
||||
static async down(_db: PrismaClient) {}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
import { FeatureConfigs, FeatureName, FeatureType } from '../../models';
|
||||
|
||||
export class FeatureRedundant1738590347632 {
|
||||
// do the migration
|
||||
static async up(db: PrismaClient) {
|
||||
const features = await db.feature.findMany();
|
||||
const validFeatures = new Map<
|
||||
number,
|
||||
{
|
||||
name: string;
|
||||
type: FeatureType;
|
||||
}
|
||||
>();
|
||||
|
||||
for (const feature of features) {
|
||||
const def = FeatureConfigs[feature.name as FeatureName];
|
||||
if (!def || def.deprecatedVersion !== feature.deprecatedVersion) {
|
||||
await db.feature.delete({
|
||||
where: { id: feature.id },
|
||||
});
|
||||
} else {
|
||||
validFeatures.set(feature.id, {
|
||||
name: feature.name,
|
||||
type: def.type,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const [id, def] of validFeatures.entries()) {
|
||||
await db.userFeature.updateMany({
|
||||
where: {
|
||||
featureId: id,
|
||||
},
|
||||
data: {
|
||||
name: def.name,
|
||||
type: def.type,
|
||||
},
|
||||
});
|
||||
await db.workspaceFeature.updateMany({
|
||||
where: {
|
||||
featureId: id,
|
||||
},
|
||||
data: {
|
||||
name: def.name,
|
||||
type: def.type,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// revert the migration
|
||||
static async down(_db: PrismaClient) {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
import { Config } from '../../base';
|
||||
import { FeatureManagementService } from '../../core/features';
|
||||
|
||||
export class SelfHostAdmin1 {
|
||||
// do the migration
|
||||
static async up(db: PrismaClient, ref: ModuleRef) {
|
||||
const config = ref.get(Config, { strict: false });
|
||||
if (config.isSelfhosted) {
|
||||
const feature = ref.get(FeatureManagementService, { strict: false });
|
||||
|
||||
const firstUser = await db.user.findFirst({
|
||||
orderBy: {
|
||||
createdAt: 'asc',
|
||||
},
|
||||
});
|
||||
if (firstUser) {
|
||||
await feature.addAdmin(firstUser.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// revert the migration
|
||||
static async down() {
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { Prisma, PrismaClient } from '@prisma/client';
|
||||
|
||||
import { CommonFeature, Features, FeatureType } from '../../../core/features';
|
||||
|
||||
// upgrade features from lower version to higher version
|
||||
export async function upsertFeature(
|
||||
db: PrismaClient,
|
||||
feature: CommonFeature
|
||||
): Promise<void> {
|
||||
const hasEqualOrGreaterVersion =
|
||||
(await db.feature.count({
|
||||
where: {
|
||||
feature: feature.feature,
|
||||
version: {
|
||||
gte: feature.version,
|
||||
},
|
||||
},
|
||||
})) > 0;
|
||||
// will not update exists version
|
||||
if (!hasEqualOrGreaterVersion) {
|
||||
await db.feature.create({
|
||||
data: {
|
||||
feature: feature.feature,
|
||||
type: feature.type,
|
||||
version: feature.version,
|
||||
configs: feature.configs as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function upsertLatestFeatureVersion(
|
||||
db: PrismaClient,
|
||||
type: FeatureType
|
||||
) {
|
||||
const feature = Features.filter(f => f.feature === type);
|
||||
feature.sort((a, b) => b.version - a.version);
|
||||
const latestFeature = feature[0];
|
||||
await upsertFeature(db, latestFeature);
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
import { FeatureKind } from '../../../core/features';
|
||||
import { getLatestQuota } from '../../../core/quota/schema';
|
||||
import { Quota, QuotaType } from '../../../core/quota/types';
|
||||
import { upsertFeature } from './user-features';
|
||||
|
||||
export async function upgradeQuotaVersion(
|
||||
db: PrismaClient,
|
||||
quota: Quota,
|
||||
reason: string
|
||||
) {
|
||||
// add new quota
|
||||
await upsertFeature(db, quota);
|
||||
// migrate all users that using old quota to new quota
|
||||
await db.$transaction(
|
||||
async tx => {
|
||||
const latestQuotaVersion = await tx.feature.findFirstOrThrow({
|
||||
where: { feature: quota.feature },
|
||||
orderBy: { version: 'desc' },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
// find all users that have old free plan
|
||||
const userIds = await tx.user.findMany({
|
||||
where: {
|
||||
features: {
|
||||
some: {
|
||||
feature: {
|
||||
type: FeatureKind.Quota,
|
||||
feature: quota.feature,
|
||||
version: { lt: quota.version },
|
||||
},
|
||||
activated: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
// deactivate all old quota for the user
|
||||
await tx.userFeature.updateMany({
|
||||
where: {
|
||||
id: undefined,
|
||||
userId: {
|
||||
in: userIds.map(({ id }) => id),
|
||||
},
|
||||
feature: {
|
||||
type: FeatureKind.Quota,
|
||||
},
|
||||
activated: true,
|
||||
},
|
||||
data: {
|
||||
activated: false,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.userFeature.createMany({
|
||||
data: userIds.map(({ id: userId }) => ({
|
||||
userId,
|
||||
featureId: latestQuotaVersion.id,
|
||||
reason,
|
||||
activated: true,
|
||||
})),
|
||||
});
|
||||
},
|
||||
{
|
||||
maxWait: 10000,
|
||||
timeout: 20000,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function upsertLatestQuotaVersion(
|
||||
db: PrismaClient,
|
||||
type: QuotaType
|
||||
) {
|
||||
const latestQuota = getLatestQuota(type);
|
||||
await upsertFeature(db, latestQuota);
|
||||
}
|
||||
|
||||
export async function upgradeLatestQuotaVersion(
|
||||
db: PrismaClient,
|
||||
type: QuotaType,
|
||||
reason: string
|
||||
) {
|
||||
const latestQuota = getLatestQuota(type);
|
||||
await upgradeQuotaVersion(db, latestQuota, reason);
|
||||
}
|
||||
Reference in New Issue
Block a user