wip: added cache
This commit is contained in:
@@ -9,11 +9,11 @@ import { ConfigModule } from '@nestjs/config';
|
||||
import { PrismaModule } from './prisma/prisma.module';
|
||||
import { APP_FILTER, APP_INTERCEPTOR } from '@nestjs/core';
|
||||
import { ResponseInterceptor } from 'common/interceptors/response.interceptor';
|
||||
import { ExceptionsHandler } from '@nestjs/core/exceptions/exceptions-handler';
|
||||
import { HttpExceptionFilter } from 'common/exceptions/exception-filter';
|
||||
import { OrganizationModule } from './organization/organization.module';
|
||||
import { OrganizationMembershipModule } from './organization-membership/organization-membership.module';
|
||||
import { AuthorizationModule } from './authorization/authorization.module';
|
||||
import { CacheModule } from './cache/cache.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -27,6 +27,7 @@ import { AuthorizationModule } from './authorization/authorization.module';
|
||||
OrganizationModule,
|
||||
OrganizationMembershipModule,
|
||||
AuthorizationModule,
|
||||
CacheModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [
|
||||
@@ -40,6 +41,11 @@ import { AuthorizationModule } from './authorization/authorization.module';
|
||||
provide: APP_FILTER,
|
||||
useClass: HttpExceptionFilter,
|
||||
},
|
||||
// NOTE: Auto cache controller response
|
||||
// {
|
||||
// provide: APP_INTERCEPTOR,
|
||||
// useClass: CacheInterceptor,
|
||||
// },
|
||||
],
|
||||
})
|
||||
export class AppModule implements NestModule {
|
||||
|
||||
32
src/cache/cache.module.ts
vendored
Normal file
32
src/cache/cache.module.ts
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { CacheModule as NestCacheManager } from '@nestjs/cache-manager';
|
||||
import KeyvRedis from '@keyv/redis';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { CacheService } from './cache.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
NestCacheManager.registerAsync({
|
||||
inject: [ConfigService],
|
||||
useFactory: async (configService: ConfigService) => {
|
||||
const redisUrl = configService.get<string>('REDIS_URL');
|
||||
const redisStore = new KeyvRedis(redisUrl, {
|
||||
connectionTimeout: 1000,
|
||||
});
|
||||
|
||||
redisStore.on('error', (err) => {
|
||||
console.error('Redis error:', err.message);
|
||||
});
|
||||
|
||||
return {
|
||||
stores: [redisStore],
|
||||
ttl: 120 * 1000,
|
||||
};
|
||||
},
|
||||
isGlobal: true,
|
||||
}),
|
||||
],
|
||||
providers: [CacheService],
|
||||
exports: [CacheService],
|
||||
})
|
||||
export class CacheModule {}
|
||||
18
src/cache/cache.service.spec.ts
vendored
Normal file
18
src/cache/cache.service.spec.ts
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { CacheService } from './cache.service';
|
||||
|
||||
describe('CacheService', () => {
|
||||
let service: CacheService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [CacheService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<CacheService>(CacheService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
63
src/cache/cache.service.ts
vendored
Normal file
63
src/cache/cache.service.ts
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
import { CACHE_MANAGER, Cache } from '@nestjs/cache-manager';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class CacheService {
|
||||
private redisAvailable = true;
|
||||
|
||||
constructor(@Inject(CACHE_MANAGER) private cache: Cache) {
|
||||
const store = this.cache.stores[0];
|
||||
|
||||
if (store?.on) {
|
||||
store.on('end', () => {
|
||||
this.redisAvailable = false;
|
||||
console.warn('Redis disconnected');
|
||||
});
|
||||
|
||||
store.on('ready', () => {
|
||||
this.redisAvailable = true;
|
||||
console.log('Redis ready');
|
||||
});
|
||||
|
||||
store.on('error', () => {
|
||||
this.redisAvailable = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async getOrSet<T>(
|
||||
key: string,
|
||||
factory: () => Promise<T>,
|
||||
ttl?: number,
|
||||
): Promise<T> {
|
||||
if (this.redisAvailable) {
|
||||
try {
|
||||
const cached = await this.cache.get<T>(key);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
} catch {
|
||||
this.redisAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to DB
|
||||
const fresh = await factory();
|
||||
|
||||
// Try setting cache only if Redis available
|
||||
if (this.redisAvailable) {
|
||||
try {
|
||||
await this.cache.set(key, fresh, ttl);
|
||||
} catch {
|
||||
this.redisAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
return fresh;
|
||||
}
|
||||
|
||||
async deleteKey(key: string) {
|
||||
const a = await this.cache.del(key);
|
||||
console.log(a);
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ export class OrganizationMembershipService {
|
||||
) {
|
||||
const { invitedUserEmail, ...rest } = dto;
|
||||
const [orgExists, invitedUser] = await Promise.all([
|
||||
this.orgService.findById(dto.orgId),
|
||||
this.orgService.organizationExists(dto.orgId),
|
||||
this.userService.findByEmail(invitedUserEmail),
|
||||
]);
|
||||
|
||||
@@ -81,7 +81,7 @@ export class OrganizationMembershipService {
|
||||
|
||||
// TODO: reject, rejectReason
|
||||
async acceptInvitation(userId: string, orgId: string) {
|
||||
const orgExists = await this.orgService.findById(orgId);
|
||||
const orgExists = await this.orgService.organizationExists(orgId);
|
||||
if (!orgExists) throw new NotFoundException('Organization');
|
||||
|
||||
const [userAlreadyPart, isUserInvited] = await Promise.all([
|
||||
@@ -154,7 +154,7 @@ export class OrganizationMembershipService {
|
||||
}
|
||||
|
||||
async getMemebersOfOrganization(orgId: string) {
|
||||
const orgExists = await this.orgService.findById(orgId);
|
||||
const orgExists = await this.orgService.organizationExists(orgId);
|
||||
if (!orgExists) throw new NotFoundException('Organization');
|
||||
|
||||
return await this.prisma.organizationUserJoinTable.findMany({
|
||||
|
||||
@@ -5,6 +5,7 @@ import { PrismaModule } from 'src/prisma/prisma.module';
|
||||
import { RequestContextModule } from 'core/als/request-context.module';
|
||||
import { UserModule } from 'src/user/user.module';
|
||||
import { AuthorizationModule } from 'src/authorization/authorization.module';
|
||||
import { CacheModule } from 'src/cache/cache.module';
|
||||
|
||||
@Module({
|
||||
controllers: [OrganizationController],
|
||||
@@ -14,6 +15,7 @@ import { AuthorizationModule } from 'src/authorization/authorization.module';
|
||||
RequestContextModule,
|
||||
UserModule,
|
||||
AuthorizationModule,
|
||||
CacheModule,
|
||||
],
|
||||
exports: [OrganizationService],
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
ForbiddenException,
|
||||
Inject,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
@@ -8,10 +9,10 @@ import {
|
||||
UpdateOrganizationRequestDTO,
|
||||
} from './dtos';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { RequestContextService } from 'core/als/request-context.service';
|
||||
import { ORG_ROLE } from 'prisma/generated/prisma/enums';
|
||||
import { AuthorizationService } from 'src/authorization/authorization.service';
|
||||
import { USER_ORGANIZATION_OPERATIONS } from 'src/authorization/operations';
|
||||
import { CacheService } from 'src/cache/cache.service';
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationService {
|
||||
@@ -19,6 +20,7 @@ export class OrganizationService {
|
||||
private readonly prisma: PrismaService,
|
||||
// private readonly reqContext: RequestContextService,
|
||||
private readonly authorization: AuthorizationService,
|
||||
private readonly cacheService: CacheService,
|
||||
) {}
|
||||
async createNewOrganization(
|
||||
userId: string,
|
||||
@@ -58,7 +60,7 @@ export class OrganizationService {
|
||||
orgId: string,
|
||||
dto: UpdateOrganizationRequestDTO,
|
||||
) {
|
||||
const orgExists = await this.findById(orgId);
|
||||
const orgExists = await this.organizationExists(orgId);
|
||||
if (!orgExists) throw new NotFoundException('Organization');
|
||||
|
||||
const canUserUpdateOrganization =
|
||||
@@ -81,7 +83,7 @@ export class OrganizationService {
|
||||
|
||||
// TODO: Either empty or choose someone to be owner
|
||||
async deleteAnOrganization(userId: string, orgId: string) {
|
||||
const orgExists = await this.findById(orgId);
|
||||
const orgExists = await this.organizationExists(orgId);
|
||||
if (!orgExists) throw new NotFoundException('Organization');
|
||||
|
||||
const canUserDeleteOrganization =
|
||||
@@ -110,11 +112,28 @@ export class OrganizationService {
|
||||
},
|
||||
});
|
||||
|
||||
await this.cacheService.deleteKey(orgId);
|
||||
|
||||
return deletedOrganization;
|
||||
});
|
||||
}
|
||||
|
||||
async organizationExists(orgId: string) {
|
||||
return await this.prisma.organization.findUnique({
|
||||
where: { id: orgId },
|
||||
select: { id: true },
|
||||
});
|
||||
}
|
||||
|
||||
async findById(orgId: string) {
|
||||
return await this.prisma.organization.findUnique({ where: { id: orgId } });
|
||||
const organization = await this.cacheService.getOrSet(
|
||||
orgId,
|
||||
async () =>
|
||||
await this.prisma.organization.findUnique({
|
||||
where: { id: orgId },
|
||||
}),
|
||||
);
|
||||
|
||||
return organization;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user