feat(server): add model base (#9734)

This commit is contained in:
forehalo
2025-01-17 02:03:53 +00:00
parent c2149f218b
commit 64335b9e0c
12 changed files with 105 additions and 65 deletions

View File

@@ -13,9 +13,7 @@ interface Context {
const test = ava as TestFn<Context>; const test = ava as TestFn<Context>;
test.before(async t => { test.before(async t => {
const module = await createTestingModule({ const module = await createTestingModule({});
providers: [FeatureModel],
});
t.context.feature = module.get(FeatureModel); t.context.feature = module.get(FeatureModel);
t.context.module = module; t.context.module = module;

View File

@@ -18,9 +18,7 @@ interface Context {
const test = ava as TestFn<Context>; const test = ava as TestFn<Context>;
test.before(async t => { test.before(async t => {
const module = await createTestingModule({ const module = await createTestingModule({});
providers: [SessionModel],
});
t.context.session = module.get(SessionModel); t.context.session = module.get(SessionModel);
t.context.user = module.get(UserModel); t.context.user = module.get(UserModel);

View File

@@ -17,9 +17,7 @@ interface Context {
const test = ava as TestFn<Context>; const test = ava as TestFn<Context>;
test.before(async t => { test.before(async t => {
const module = await createTestingModule({ const module = await createTestingModule({});
providers: [UserModel],
});
t.context.user = module.get(UserModel); t.context.user = module.get(UserModel);
t.context.module = module; t.context.module = module;

View File

@@ -17,9 +17,7 @@ interface Context {
const test = ava as TestFn<Context>; const test = ava as TestFn<Context>;
test.before(async t => { test.before(async t => {
const module = await createTestingModule({ const module = await createTestingModule({});
providers: [VerificationTokenModel],
});
t.context.verificationToken = module.get(VerificationTokenModel); t.context.verificationToken = module.get(VerificationTokenModel);
t.context.db = module.get(PrismaClient); t.context.db = module.get(PrismaClient);

View File

@@ -41,4 +41,4 @@ export {
getRequestResponseFromHost, getRequestResponseFromHost,
parseCookies, parseCookies,
} from './utils/request'; } from './utils/request';
export type * from './utils/types'; export * from './utils/types';

View File

@@ -0,0 +1,19 @@
import { Inject, Logger } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { Config } from '../base';
import type { Models } from '.';
import { MODELS_SYMBOL } from './provider';
export class BaseModel {
protected readonly logger = new Logger(this.constructor.name);
@Inject(MODELS_SYMBOL)
protected readonly models!: Models;
@Inject(Config)
protected readonly config!: Config;
@Inject(PrismaClient)
protected readonly db!: PrismaClient;
}

View File

@@ -1,8 +1,9 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Feature, PrismaClient } from '@prisma/client'; import { Feature } from '@prisma/client';
import { z } from 'zod'; import { z } from 'zod';
import { PrismaTransaction } from '../base'; import { PrismaTransaction } from '../base';
import { BaseModel } from './base';
import { Features, FeatureType } from './common'; import { Features, FeatureType } from './common';
type FeatureNames = keyof typeof Features; type FeatureNames = keyof typeof Features;
@@ -17,11 +18,7 @@ type FeatureConfigs<T extends FeatureNames> = z.infer<
// We have to manually update all the users and workspaces binding to the latest version, which are thousands of handreds. // We have to manually update all the users and workspaces binding to the latest version, which are thousands of handreds.
// This is a huge burden for us and we should remove it. // This is a huge burden for us and we should remove it.
@Injectable() @Injectable()
export class FeatureModel { export class FeatureModel extends BaseModel {
private readonly logger = new Logger(FeatureModel.name);
constructor(private readonly db: PrismaClient) {}
async get<T extends FeatureNames>(name: T) { async get<T extends FeatureNames>(name: T) {
const feature = await this.getLatest(this.db, name); const feature = await this.getLatest(this.db, name);

View File

@@ -1,31 +1,71 @@
import { Global, Injectable, Module } from '@nestjs/common'; import {
ExistingProvider,
FactoryProvider,
Global,
Module,
} from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { ApplyType } from '../base';
import { FeatureModel } from './feature'; import { FeatureModel } from './feature';
import { MODELS_SYMBOL } from './provider';
import { SessionModel } from './session'; import { SessionModel } from './session';
import { UserModel } from './user'; import { UserModel } from './user';
import { VerificationTokenModel } from './verification-token'; import { VerificationTokenModel } from './verification-token';
const models = [ const MODELS = {
UserModel, user: UserModel,
SessionModel, session: SessionModel,
VerificationTokenModel, verificationToken: VerificationTokenModel,
FeatureModel, feature: FeatureModel,
] as const; };
@Injectable() type ModelsType = {
export class Models { [K in keyof typeof MODELS]: InstanceType<(typeof MODELS)[K]>;
constructor( };
public readonly user: UserModel,
public readonly session: SessionModel, export class Models extends ApplyType<ModelsType>() {}
public readonly verificationToken: VerificationTokenModel,
public readonly feature: FeatureModel const ModelsProvider: FactoryProvider = {
) {} provide: Models,
useFactory: (ref: ModuleRef) => {
return new Proxy({} as any, {
get: (target, prop) => {
// cache
if (prop in target) {
return target[prop];
} }
// find the model instance
// @ts-expect-error null detection happens right after
const Model = MODELS[prop];
if (!Model) {
return undefined;
}
const model = ref.get(Model);
if (!model) {
throw new Error(`Failed to initialize model ${Model.name}`);
}
target[prop] = model;
return model;
},
});
},
inject: [ModuleRef],
};
const ModelsSymbolProvider: ExistingProvider = {
provide: MODELS_SYMBOL,
useExisting: Models,
};
@Global() @Global()
@Module({ @Module({
providers: [...models, Models], providers: [...Object.values(MODELS), ModelsProvider, ModelsSymbolProvider],
exports: [Models], exports: [ModelsProvider],
}) })
export class ModelModules {} export class ModelModules {}

View File

@@ -0,0 +1 @@
export const MODELS_SYMBOL = Symbol('AFFINE_MODELS');

View File

@@ -1,25 +1,18 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { import {
Prisma, Prisma,
PrismaClient,
type Session, type Session,
type User, type User,
type UserSession, type UserSession,
} from '@prisma/client'; } from '@prisma/client';
import { Config } from '../base'; import { BaseModel } from './base';
export type { Session, UserSession }; export type { Session, UserSession };
export type UserSessionWithUser = UserSession & { user: User }; export type UserSessionWithUser = UserSession & { user: User };
@Injectable() @Injectable()
export class SessionModel { export class SessionModel extends BaseModel {
private readonly logger = new Logger(SessionModel.name);
constructor(
private readonly db: PrismaClient,
private readonly config: Config
) {}
async createSession() { async createSession() {
return await this.db.session.create({ return await this.db.session.create({
data: {}, data: {},

View File

@@ -1,9 +1,8 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Prisma, PrismaClient, type User, Workspace } from '@prisma/client'; import { Prisma, type User, Workspace } from '@prisma/client';
import { pick } from 'lodash-es'; import { pick } from 'lodash-es';
import { import {
Config,
CryptoHelper, CryptoHelper,
EmailAlreadyUsed, EmailAlreadyUsed,
EventEmitter, EventEmitter,
@@ -15,6 +14,7 @@ import {
import type { Payload } from '../base/event/def'; import type { Payload } from '../base/event/def';
import { Permission } from '../core/permission'; import { Permission } from '../core/permission';
import { Quota_FreePlanV1_1 } from '../core/quota/schema'; import { Quota_FreePlanV1_1 } from '../core/quota/schema';
import { BaseModel } from './base';
const publicUserSelect = { const publicUserSelect = {
id: true, id: true,
@@ -64,14 +64,13 @@ export type PublicUser = Pick<User, keyof typeof publicUserSelect>;
export type { User }; export type { User };
@Injectable() @Injectable()
export class UserModel { export class UserModel extends BaseModel {
private readonly logger = new Logger(UserModel.name);
constructor( constructor(
private readonly db: PrismaClient,
private readonly crypto: CryptoHelper, private readonly crypto: CryptoHelper,
private readonly event: EventEmitter, private readonly event: EventEmitter
private readonly config: Config ) {
) {} super();
}
async get(id: string) { async get(id: string) {
return this.db.user.findUnique({ return this.db.user.findUnique({

View File

@@ -1,9 +1,10 @@
import { randomUUID } from 'node:crypto'; import { randomUUID } from 'node:crypto';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { PrismaClient, type VerificationToken } from '@prisma/client'; import { type VerificationToken } from '@prisma/client';
import { CryptoHelper } from '../base/helpers'; import { CryptoHelper } from '../base/helpers';
import { BaseModel } from './base';
export type { VerificationToken }; export type { VerificationToken };
@@ -16,12 +17,10 @@ export enum TokenType {
} }
@Injectable() @Injectable()
export class VerificationTokenModel { export class VerificationTokenModel extends BaseModel {
private readonly logger = new Logger(VerificationTokenModel.name); constructor(private readonly crypto: CryptoHelper) {
constructor( super();
private readonly db: PrismaClient, }
private readonly crypto: CryptoHelper
) {}
/** /**
* create token by type and credential (optional) with ttl in seconds (default 30 minutes) * create token by type and credential (optional) with ttl in seconds (default 30 minutes)