mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-07 09:21:24 +08:00
feat(server): cluster level event system (#9884)
This commit is contained in:
@@ -0,0 +1,62 @@
|
|||||||
|
import { INestApplication } from '@nestjs/common';
|
||||||
|
import ava, { TestFn } from 'ava';
|
||||||
|
import Sinon from 'sinon';
|
||||||
|
|
||||||
|
import { EventBus } from '../../base';
|
||||||
|
import { SocketIoAdapter } from '../../base/websocket';
|
||||||
|
import { createTestingModule } from '../utils';
|
||||||
|
import { Listeners } from './provider';
|
||||||
|
|
||||||
|
const test = ava as TestFn<{
|
||||||
|
app1: INestApplication;
|
||||||
|
app2: INestApplication;
|
||||||
|
}>;
|
||||||
|
async function createApp() {
|
||||||
|
const m = await createTestingModule(
|
||||||
|
{
|
||||||
|
providers: [Listeners],
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
const app = m.createNestApplication({ logger: false });
|
||||||
|
|
||||||
|
app.useWebSocketAdapter(new SocketIoAdapter(app));
|
||||||
|
await app.init();
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
test.before(async t => {
|
||||||
|
t.context.app1 = await createApp();
|
||||||
|
t.context.app2 = await createApp();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after(async t => {
|
||||||
|
await t.context.app1.close();
|
||||||
|
await t.context.app2.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should broadcast event to cluster instances', async t => {
|
||||||
|
const { app1, app2 } = t.context;
|
||||||
|
|
||||||
|
// app 1 for listening
|
||||||
|
const eventbus1 = app1.get(EventBus);
|
||||||
|
|
||||||
|
const listener = Sinon.spy(app1.get(Listeners), 'onTestEvent');
|
||||||
|
const runtimeListener = Sinon.stub().returns({ count: 2 });
|
||||||
|
const off = eventbus1.on('__test__.event', runtimeListener);
|
||||||
|
|
||||||
|
// app 2 for broadcasting
|
||||||
|
const eventbus2 = app2.get(EventBus);
|
||||||
|
eventbus2.broadcast('__test__.event', { count: 0 });
|
||||||
|
|
||||||
|
// cause the cross instances broadcasting is asynchronization calling
|
||||||
|
// we should wait for the event's arriving before asserting
|
||||||
|
await eventbus1.waitFor('__test__.event');
|
||||||
|
|
||||||
|
t.true(listener.calledOnceWith({ count: 0 }));
|
||||||
|
t.true(runtimeListener.calledOnceWith({ count: 0 }));
|
||||||
|
|
||||||
|
off();
|
||||||
|
});
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { TestingModule } from '@nestjs/testing';
|
||||||
|
import ava, { TestFn } from 'ava';
|
||||||
|
import Sinon from 'sinon';
|
||||||
|
|
||||||
|
import { EventBus, metrics } from '../../base';
|
||||||
|
import { createTestingModule } from '../utils';
|
||||||
|
import { Listeners } from './provider';
|
||||||
|
|
||||||
|
export const test = ava as TestFn<{
|
||||||
|
module: TestingModule;
|
||||||
|
eventbus: EventBus;
|
||||||
|
listener: Sinon.SinonSpy;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
test.before(async t => {
|
||||||
|
const m = await createTestingModule({
|
||||||
|
providers: [Listeners],
|
||||||
|
});
|
||||||
|
const eventbus = m.get(EventBus);
|
||||||
|
t.context.module = m;
|
||||||
|
t.context.eventbus = eventbus;
|
||||||
|
t.context.listener = Sinon.spy(m.get(Listeners), 'onTestEvent');
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterEach(() => {
|
||||||
|
Sinon.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after(async t => {
|
||||||
|
await t.context.module.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should register event listener', t => {
|
||||||
|
const { eventbus } = t.context;
|
||||||
|
|
||||||
|
// @ts-expect-error private member
|
||||||
|
t.true(eventbus.emitter.eventNames().includes('__test__.event'));
|
||||||
|
|
||||||
|
eventbus.on('__test__.event2', () => {});
|
||||||
|
// @ts-expect-error private member
|
||||||
|
t.true(eventbus.emitter.eventNames().includes('__test__.event2'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should dispatch event listener', t => {
|
||||||
|
const { eventbus, listener } = t.context;
|
||||||
|
|
||||||
|
const runtimeListener = Sinon.stub();
|
||||||
|
const off = eventbus.on('__test__.event', runtimeListener);
|
||||||
|
|
||||||
|
const payload = { count: 0 };
|
||||||
|
eventbus.emit('__test__.event', payload);
|
||||||
|
|
||||||
|
t.true(listener.calledOnceWithExactly(payload));
|
||||||
|
t.true(runtimeListener.calledOnceWithExactly(payload));
|
||||||
|
|
||||||
|
off();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should dispatch async event listener', async t => {
|
||||||
|
const { eventbus, listener } = t.context;
|
||||||
|
|
||||||
|
const runtimeListener = Sinon.stub().returns({ count: 2 });
|
||||||
|
const off = eventbus.on('__test__.event', runtimeListener);
|
||||||
|
|
||||||
|
const payload = { count: 0 };
|
||||||
|
const returns = await eventbus.emitAsync('__test__.event', payload);
|
||||||
|
|
||||||
|
t.true(listener.calledOnceWithExactly(payload));
|
||||||
|
t.true(runtimeListener.calledOnceWithExactly(payload));
|
||||||
|
|
||||||
|
t.deepEqual(returns, [{ count: 1 }, { count: 2 }]);
|
||||||
|
|
||||||
|
off();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should record event handler call metrics', async t => {
|
||||||
|
const { eventbus } = t.context;
|
||||||
|
const timerStub = Sinon.stub(
|
||||||
|
metrics.event.histogram('function_timer'),
|
||||||
|
'record'
|
||||||
|
);
|
||||||
|
const counterStub = Sinon.stub(
|
||||||
|
metrics.event.counter('function_calls'),
|
||||||
|
'add'
|
||||||
|
);
|
||||||
|
|
||||||
|
await eventbus.emitAsync('__test__.event', { count: 0 });
|
||||||
|
|
||||||
|
t.deepEqual(timerStub.getCall(0).args[1], {
|
||||||
|
name: 'event_handler',
|
||||||
|
event: '__test__.event',
|
||||||
|
namespace: '__test__',
|
||||||
|
error: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
t.deepEqual(counterStub.getCall(0).args[1], {
|
||||||
|
event: '__test__.event',
|
||||||
|
namespace: '__test__',
|
||||||
|
});
|
||||||
|
|
||||||
|
Sinon.reset();
|
||||||
|
|
||||||
|
await eventbus.emitAsync('__test__.throw', { count: 0 });
|
||||||
|
|
||||||
|
t.deepEqual(timerStub.getCall(0).args[1], {
|
||||||
|
name: 'event_handler',
|
||||||
|
event: '__test__.throw',
|
||||||
|
namespace: '__test__',
|
||||||
|
error: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { OnEvent } from '../../base';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Events {
|
||||||
|
'__test__.event': { count: number };
|
||||||
|
'__test__.event2': { count: number };
|
||||||
|
'__test__.throw': { count: number };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class Listeners {
|
||||||
|
@OnEvent('__test__.event')
|
||||||
|
onTestEvent({ count }: Events['__test__.event']) {
|
||||||
|
return {
|
||||||
|
count: count + 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnEvent('__test__.throw')
|
||||||
|
onThrow() {
|
||||||
|
throw new Error('Error in event handler');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
|
||||||
import { TestingModule } from '@nestjs/testing';
|
import { TestingModule } from '@nestjs/testing';
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
import ava, { TestFn } from 'ava';
|
import ava, { TestFn } from 'ava';
|
||||||
import Sinon from 'sinon';
|
import Sinon from 'sinon';
|
||||||
|
|
||||||
import { EmailAlreadyUsed } from '../../base';
|
import { EmailAlreadyUsed, EventBus } from '../../base';
|
||||||
import { Permission } from '../../models/common';
|
import { Permission } from '../../models/common';
|
||||||
import { UserModel } from '../../models/user';
|
import { UserModel } from '../../models/user';
|
||||||
import { WorkspaceMemberStatus } from '../../models/workspace';
|
import { WorkspaceMemberStatus } from '../../models/workspace';
|
||||||
@@ -46,7 +45,7 @@ test('should create a new user', async t => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should trigger user.created event', async t => {
|
test('should trigger user.created event', async t => {
|
||||||
const event = t.context.module.get(EventEmitter2);
|
const event = t.context.module.get(EventBus);
|
||||||
const spy = Sinon.spy();
|
const spy = Sinon.spy();
|
||||||
event.on('user.created', spy);
|
event.on('user.created', spy);
|
||||||
|
|
||||||
@@ -117,7 +116,7 @@ test('should not update email to an existing one', async t => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should trigger user.updated event', async t => {
|
test('should trigger user.updated event', async t => {
|
||||||
const event = t.context.module.get(EventEmitter2);
|
const event = t.context.module.get(EventBus);
|
||||||
const spy = Sinon.spy();
|
const spy = Sinon.spy();
|
||||||
event.on('user.updated', spy);
|
event.on('user.updated', spy);
|
||||||
|
|
||||||
@@ -217,7 +216,7 @@ test('should fulfill user', async t => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should trigger user.updated event when fulfilling user', async t => {
|
test('should trigger user.updated event when fulfilling user', async t => {
|
||||||
const event = t.context.module.get(EventEmitter2);
|
const event = t.context.module.get(EventBus);
|
||||||
const createSpy = Sinon.spy();
|
const createSpy = Sinon.spy();
|
||||||
const updateSpy = Sinon.spy();
|
const updateSpy = Sinon.spy();
|
||||||
event.on('user.created', createSpy);
|
event.on('user.created', createSpy);
|
||||||
@@ -250,7 +249,7 @@ test('should delete user', async t => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should trigger user.deleted event', async t => {
|
test('should trigger user.deleted event', async t => {
|
||||||
const event = t.context.module.get(EventEmitter2);
|
const event = t.context.module.get(EventBus);
|
||||||
const spy = Sinon.spy();
|
const spy = Sinon.spy();
|
||||||
event.on('user.deleted', spy);
|
event.on('user.deleted', spy);
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
|
||||||
import { TestingModule } from '@nestjs/testing';
|
import { TestingModule } from '@nestjs/testing';
|
||||||
import { PrismaClient, WorkspaceMemberStatus } from '@prisma/client';
|
import { PrismaClient, WorkspaceMemberStatus } from '@prisma/client';
|
||||||
import ava, { TestFn } from 'ava';
|
import ava, { TestFn } from 'ava';
|
||||||
import Sinon from 'sinon';
|
import Sinon from 'sinon';
|
||||||
|
|
||||||
import { Config } from '../../base/config';
|
import { Config, EventBus } from '../../base';
|
||||||
import { Permission } from '../../models/common';
|
import { Permission } from '../../models/common';
|
||||||
import { UserModel } from '../../models/user';
|
import { UserModel } from '../../models/user';
|
||||||
import { WorkspaceModel } from '../../models/workspace';
|
import { WorkspaceModel } from '../../models/workspace';
|
||||||
@@ -305,7 +304,7 @@ test('should grant member with read permission and Pending status by default', a
|
|||||||
email: 'test2@affine.pro',
|
email: 'test2@affine.pro',
|
||||||
});
|
});
|
||||||
|
|
||||||
const event = t.context.module.get(EventEmitter2);
|
const event = t.context.module.get(EventBus);
|
||||||
const updatedSpy = Sinon.spy();
|
const updatedSpy = Sinon.spy();
|
||||||
event.on('workspace.members.updated', updatedSpy);
|
event.on('workspace.members.updated', updatedSpy);
|
||||||
const member1 = await t.context.workspace.grantMember(
|
const member1 = await t.context.workspace.grantMember(
|
||||||
@@ -829,7 +828,7 @@ test('should delete workspace member in Pending, Accepted status', async t => {
|
|||||||
);
|
);
|
||||||
t.is(member.status, WorkspaceMemberStatus.Pending);
|
t.is(member.status, WorkspaceMemberStatus.Pending);
|
||||||
|
|
||||||
const event = t.context.module.get(EventEmitter2);
|
const event = t.context.module.get(EventBus);
|
||||||
const updatedSpy = Sinon.spy();
|
const updatedSpy = Sinon.spy();
|
||||||
event.on('workspace.members.updated', updatedSpy);
|
event.on('workspace.members.updated', updatedSpy);
|
||||||
let success = await t.context.workspace.deleteMember(
|
let success = await t.context.workspace.deleteMember(
|
||||||
@@ -880,7 +879,7 @@ test('should trigger workspace.members.requestDeclined event when delete workspa
|
|||||||
);
|
);
|
||||||
t.is(member.status, WorkspaceMemberStatus.UnderReview);
|
t.is(member.status, WorkspaceMemberStatus.UnderReview);
|
||||||
|
|
||||||
const event = t.context.module.get(EventEmitter2);
|
const event = t.context.module.get(EventBus);
|
||||||
const updatedSpy = Sinon.spy();
|
const updatedSpy = Sinon.spy();
|
||||||
const requestDeclinedSpy = Sinon.spy();
|
const requestDeclinedSpy = Sinon.spy();
|
||||||
event.on('workspace.members.updated', updatedSpy);
|
event.on('workspace.members.updated', updatedSpy);
|
||||||
@@ -925,7 +924,7 @@ test('should trigger workspace.members.requestDeclined event when delete workspa
|
|||||||
);
|
);
|
||||||
t.is(member.status, WorkspaceMemberStatus.NeedMoreSeatAndReview);
|
t.is(member.status, WorkspaceMemberStatus.NeedMoreSeatAndReview);
|
||||||
|
|
||||||
const event = t.context.module.get(EventEmitter2);
|
const event = t.context.module.get(EventBus);
|
||||||
const updatedSpy = Sinon.spy();
|
const updatedSpy = Sinon.spy();
|
||||||
const requestDeclinedSpy = Sinon.spy();
|
const requestDeclinedSpy = Sinon.spy();
|
||||||
event.on('workspace.members.updated', updatedSpy);
|
event.on('workspace.members.updated', updatedSpy);
|
||||||
|
|||||||
@@ -105,18 +105,21 @@ function gql(app: INestApplication, query: string) {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
}
|
}
|
||||||
|
|
||||||
test.beforeEach(async ({ context }) => {
|
test.before(async ({ context }) => {
|
||||||
const { app } = await createTestingApp({
|
const { app } = await createTestingApp({
|
||||||
providers: [TestResolver, TestGateway],
|
providers: [TestResolver, TestGateway],
|
||||||
controllers: [TestController],
|
controllers: [TestController],
|
||||||
});
|
});
|
||||||
|
|
||||||
context.logger = Sinon.stub(new Logger().localInstance);
|
context.logger = Sinon.stub(new Logger().localInstance);
|
||||||
|
|
||||||
context.app = app;
|
context.app = app;
|
||||||
});
|
});
|
||||||
|
|
||||||
test.afterEach.always(async ctx => {
|
test.beforeEach(() => {
|
||||||
|
Sinon.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after.always(async ctx => {
|
||||||
await ctx.context.app.close();
|
await ctx.context.app.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -131,6 +134,7 @@ test('should be able to handle known user error in graphql query', async t => {
|
|||||||
t.is(err.message, 'You do not have permission to access this resource.');
|
t.is(err.message, 'You do not have permission to access this resource.');
|
||||||
t.is(err.extensions.status, HttpStatus.FORBIDDEN);
|
t.is(err.extensions.status, HttpStatus.FORBIDDEN);
|
||||||
t.is(err.extensions.name, 'ACCESS_DENIED');
|
t.is(err.extensions.name, 'ACCESS_DENIED');
|
||||||
|
// console.log(t.context.logger.error.getCalls());
|
||||||
t.true(t.context.logger.error.notCalled);
|
t.true(t.context.logger.error.notCalled);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import Sinon from 'sinon';
|
|||||||
import Stripe from 'stripe';
|
import Stripe from 'stripe';
|
||||||
|
|
||||||
import { AppModule } from '../../app.module';
|
import { AppModule } from '../../app.module';
|
||||||
import { EventEmitter, Runtime } from '../../base';
|
import { EventBus, Runtime } from '../../base';
|
||||||
import { ConfigModule } from '../../base/config';
|
import { ConfigModule } from '../../base/config';
|
||||||
import { CurrentUser } from '../../core/auth';
|
import { CurrentUser } from '../../core/auth';
|
||||||
import { AuthService } from '../../core/auth/service';
|
import { AuthService } from '../../core/auth/service';
|
||||||
@@ -158,7 +158,7 @@ const test = ava as TestFn<{
|
|||||||
db: PrismaClient;
|
db: PrismaClient;
|
||||||
app: INestApplication;
|
app: INestApplication;
|
||||||
service: SubscriptionService;
|
service: SubscriptionService;
|
||||||
event: Sinon.SinonStubbedInstance<EventEmitter>;
|
event: Sinon.SinonStubbedInstance<EventBus>;
|
||||||
feature: Sinon.SinonStubbedInstance<FeatureManagementService>;
|
feature: Sinon.SinonStubbedInstance<FeatureManagementService>;
|
||||||
runtime: Sinon.SinonStubbedInstance<Runtime>;
|
runtime: Sinon.SinonStubbedInstance<Runtime>;
|
||||||
stripe: {
|
stripe: {
|
||||||
@@ -203,14 +203,12 @@ test.before(async t => {
|
|||||||
m.overrideProvider(FeatureManagementService).useValue(
|
m.overrideProvider(FeatureManagementService).useValue(
|
||||||
Sinon.createStubInstance(FeatureManagementService)
|
Sinon.createStubInstance(FeatureManagementService)
|
||||||
);
|
);
|
||||||
m.overrideProvider(EventEmitter).useValue(
|
m.overrideProvider(EventBus).useValue(Sinon.createStubInstance(EventBus));
|
||||||
Sinon.createStubInstance(EventEmitter)
|
|
||||||
);
|
|
||||||
m.overrideProvider(Runtime).useValue(Sinon.createStubInstance(Runtime));
|
m.overrideProvider(Runtime).useValue(Sinon.createStubInstance(Runtime));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
t.context.event = app.get(EventEmitter);
|
t.context.event = app.get(EventBus);
|
||||||
t.context.service = app.get(SubscriptionService);
|
t.context.service = app.get(SubscriptionService);
|
||||||
t.context.feature = app.get(FeatureManagementService);
|
t.context.feature = app.get(FeatureManagementService);
|
||||||
t.context.runtime = app.get(Runtime);
|
t.context.runtime = app.get(Runtime);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import ava from 'ava';
|
|||||||
import Sinon from 'sinon';
|
import Sinon from 'sinon';
|
||||||
|
|
||||||
import { AppModule } from '../app.module';
|
import { AppModule } from '../app.module';
|
||||||
import { EventEmitter } from '../base';
|
import { EventBus } from '../base';
|
||||||
import { AuthService } from '../core/auth';
|
import { AuthService } from '../core/auth';
|
||||||
import { DocContentService } from '../core/doc-renderer';
|
import { DocContentService } from '../core/doc-renderer';
|
||||||
import { Permission, PermissionService } from '../core/permission';
|
import { Permission, PermissionService } from '../core/permission';
|
||||||
@@ -41,7 +41,7 @@ import {
|
|||||||
const test = ava as TestFn<{
|
const test = ava as TestFn<{
|
||||||
app: INestApplication;
|
app: INestApplication;
|
||||||
auth: AuthService;
|
auth: AuthService;
|
||||||
event: Sinon.SinonStubbedInstance<EventEmitter>;
|
event: Sinon.SinonStubbedInstance<EventBus>;
|
||||||
quota: QuotaService;
|
quota: QuotaService;
|
||||||
quotaManager: QuotaManagementService;
|
quotaManager: QuotaManagementService;
|
||||||
permissions: PermissionService;
|
permissions: PermissionService;
|
||||||
@@ -52,8 +52,8 @@ test.beforeEach(async t => {
|
|||||||
imports: [AppModule],
|
imports: [AppModule],
|
||||||
tapModule: module => {
|
tapModule: module => {
|
||||||
module
|
module
|
||||||
.overrideProvider(EventEmitter)
|
.overrideProvider(EventBus)
|
||||||
.useValue(Sinon.createStubInstance(EventEmitter));
|
.useValue(Sinon.createStubInstance(EventBus));
|
||||||
module.overrideProvider(DocContentService).useValue({
|
module.overrideProvider(DocContentService).useValue({
|
||||||
getWorkspaceContent() {
|
getWorkspaceContent() {
|
||||||
return {
|
return {
|
||||||
@@ -67,7 +67,7 @@ test.beforeEach(async t => {
|
|||||||
|
|
||||||
t.context.app = app;
|
t.context.app = app;
|
||||||
t.context.auth = app.get(AuthService);
|
t.context.auth = app.get(AuthService);
|
||||||
t.context.event = app.get(EventEmitter);
|
t.context.event = app.get(EventBus);
|
||||||
t.context.quota = app.get(QuotaService);
|
t.context.quota = app.get(QuotaService);
|
||||||
t.context.quotaManager = app.get(QuotaManagementService);
|
t.context.quotaManager = app.get(QuotaManagementService);
|
||||||
t.context.permissions = app.get(PermissionService);
|
t.context.permissions = app.get(PermissionService);
|
||||||
|
|||||||
@@ -112,7 +112,12 @@ export async function createTestingModule(
|
|||||||
|
|
||||||
if (init) {
|
if (init) {
|
||||||
await m.init();
|
await m.init();
|
||||||
|
// we got a lot smoking tests try to break nestjs
|
||||||
|
// can't tolerate the noisy logs
|
||||||
|
// @ts-expect-error private
|
||||||
|
m.applyLogger({
|
||||||
|
logger: ['fatal'],
|
||||||
|
});
|
||||||
const runtime = m.get(Runtime);
|
const runtime = m.get(Runtime);
|
||||||
// by pass password min length validation
|
// by pass password min length validation
|
||||||
await runtime.set('auth/password.min', 1);
|
await runtime.set('auth/password.min', 1);
|
||||||
@@ -128,7 +133,7 @@ export async function createTestingApp(moduleDef: TestingModuleMeatdata = {}) {
|
|||||||
cors: true,
|
cors: true,
|
||||||
bodyParser: true,
|
bodyParser: true,
|
||||||
rawBody: true,
|
rawBody: true,
|
||||||
logger: ['warn'],
|
logger: ['fatal'],
|
||||||
});
|
});
|
||||||
|
|
||||||
app.useGlobalFilters(new GlobalExceptionFilter(app.getHttpAdapter()));
|
app.useGlobalFilters(new GlobalExceptionFilter(app.getHttpAdapter()));
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ export const FunctionalityModules = [
|
|||||||
HelpersModule,
|
HelpersModule,
|
||||||
ErrorModule,
|
ErrorModule,
|
||||||
LoggerModule,
|
LoggerModule,
|
||||||
|
WebSocketModule,
|
||||||
];
|
];
|
||||||
|
|
||||||
function filterOptionalModule(
|
function filterOptionalModule(
|
||||||
@@ -197,7 +198,6 @@ export function buildAppModule() {
|
|||||||
// basic
|
// basic
|
||||||
.use(...FunctionalityModules)
|
.use(...FunctionalityModules)
|
||||||
.use(ModelsModule)
|
.use(ModelsModule)
|
||||||
.useIf(config => config.flavor.sync, WebSocketModule)
|
|
||||||
|
|
||||||
// auth
|
// auth
|
||||||
.use(UserModule, AuthModule, PermissionModule)
|
.use(UserModule, AuthModule, PermissionModule)
|
||||||
|
|||||||
@@ -45,10 +45,8 @@ export async function createApp() {
|
|||||||
app.useGlobalFilters(new GlobalExceptionFilter(app.getHttpAdapter()));
|
app.useGlobalFilters(new GlobalExceptionFilter(app.getHttpAdapter()));
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
|
|
||||||
if (AFFiNE.flavor.sync) {
|
const adapter = new SocketIoAdapter(app);
|
||||||
const adapter = new SocketIoAdapter(app);
|
app.useWebSocketAdapter(adapter);
|
||||||
app.useWebSocketAdapter(adapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (AFFiNE.isSelfhosted && AFFiNE.metrics.telemetry.enabled) {
|
if (AFFiNE.isSelfhosted && AFFiNE.metrics.telemetry.enabled) {
|
||||||
const mixpanel = await import('mixpanel');
|
const mixpanel = await import('mixpanel');
|
||||||
|
|||||||
@@ -1,71 +1,18 @@
|
|||||||
import type { Snapshot, User, Workspace } from '@prisma/client';
|
declare global {
|
||||||
|
/**
|
||||||
import { Flatten, Payload } from './types';
|
* Event definitions can be extended by
|
||||||
|
*
|
||||||
export interface WorkspaceEvents {
|
* @example
|
||||||
members: {
|
*
|
||||||
reviewRequested: Payload<{ inviteId: string }>;
|
* declare global {
|
||||||
requestDeclined: Payload<{
|
* interface Events {
|
||||||
userId: User['id'];
|
* 'user.subscription.created': {
|
||||||
workspaceId: Workspace['id'];
|
* userId: User['id'];
|
||||||
}>;
|
* }
|
||||||
requestApproved: Payload<{ inviteId: string }>;
|
* }
|
||||||
roleChanged: Payload<{
|
* }
|
||||||
userId: User['id'];
|
*/
|
||||||
workspaceId: Workspace['id'];
|
interface Events {}
|
||||||
permission: number;
|
|
||||||
}>;
|
|
||||||
ownershipTransferred: Payload<{
|
|
||||||
from: User['id'];
|
|
||||||
to: User['id'];
|
|
||||||
workspaceId: Workspace['id'];
|
|
||||||
}>;
|
|
||||||
ownershipReceived: Payload<{ workspaceId: Workspace['id'] }>;
|
|
||||||
updated: Payload<{ workspaceId: Workspace['id']; count: number }>;
|
|
||||||
leave: Payload<{
|
|
||||||
user: Pick<User, 'id' | 'email'>;
|
|
||||||
workspaceId: Workspace['id'];
|
|
||||||
}>;
|
|
||||||
removed: Payload<{ workspaceId: Workspace['id']; userId: User['id'] }>;
|
|
||||||
};
|
|
||||||
deleted: Payload<Workspace['id']>;
|
|
||||||
blob: {
|
|
||||||
deleted: Payload<{
|
|
||||||
workspaceId: Workspace['id'];
|
|
||||||
key: string;
|
|
||||||
}>;
|
|
||||||
sync: Payload<{
|
|
||||||
workspaceId: Workspace['id'];
|
|
||||||
key: string;
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DocEvents {
|
export type EventName = keyof Events;
|
||||||
deleted: Payload<Pick<Snapshot, 'id' | 'workspaceId'>>;
|
|
||||||
updated: Payload<Pick<Snapshot, 'id' | 'workspaceId'>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Event definitions can be extended by
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
*
|
|
||||||
* declare module './event/def' {
|
|
||||||
* interface UserEvents {
|
|
||||||
* created: Payload<User>;
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* assert<Event, 'user.created'>()
|
|
||||||
*/
|
|
||||||
export interface EventDefinitions {
|
|
||||||
workspace: WorkspaceEvents;
|
|
||||||
snapshot: DocEvents;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type EventKV = Flatten<EventDefinitions>;
|
|
||||||
|
|
||||||
export type Event = keyof EventKV;
|
|
||||||
export type EventPayload<E extends Event> = EventKV[E];
|
|
||||||
export type { Payload };
|
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import {
|
||||||
|
applyDecorators,
|
||||||
|
Injectable,
|
||||||
|
Logger,
|
||||||
|
OnApplicationBootstrap,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
EventEmitter2,
|
||||||
|
EventEmitterReadinessWatcher,
|
||||||
|
OnEvent as RawOnEvent,
|
||||||
|
OnEventMetadata,
|
||||||
|
} from '@nestjs/event-emitter';
|
||||||
|
import {
|
||||||
|
OnGatewayConnection,
|
||||||
|
WebSocketGateway,
|
||||||
|
WebSocketServer,
|
||||||
|
} from '@nestjs/websockets';
|
||||||
|
import type { Server, Socket } from 'socket.io';
|
||||||
|
|
||||||
|
import { CallMetric } from '../metrics';
|
||||||
|
import type { EventName } from './def';
|
||||||
|
|
||||||
|
const EventHandlerWrapper = (event: EventName): MethodDecorator => {
|
||||||
|
// @ts-expect-error allow
|
||||||
|
return (
|
||||||
|
_target,
|
||||||
|
key,
|
||||||
|
desc: TypedPropertyDescriptor<(...args: any[]) => any>
|
||||||
|
) => {
|
||||||
|
const originalMethod = desc.value;
|
||||||
|
if (!originalMethod) {
|
||||||
|
return desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
desc.value = function (...args: any[]) {
|
||||||
|
new Logger(EventBus.name).log(
|
||||||
|
`Event handler: ${event} (${key.toString()})`
|
||||||
|
);
|
||||||
|
return originalMethod.apply(this, args);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OnEvent = (
|
||||||
|
event: EventName,
|
||||||
|
opts?: OnEventMetadata['options']
|
||||||
|
) => {
|
||||||
|
const namespace = event.split('.')[0];
|
||||||
|
|
||||||
|
return applyDecorators(
|
||||||
|
EventHandlerWrapper(event),
|
||||||
|
CallMetric('event', 'event_handler', undefined, { event, namespace }),
|
||||||
|
RawOnEvent(event, opts)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We use socket.io system to auto pub/sub on server to server broadcast events
|
||||||
|
*/
|
||||||
|
@WebSocketGateway({
|
||||||
|
namespace: 's2s',
|
||||||
|
})
|
||||||
|
@Injectable()
|
||||||
|
export class EventBus implements OnGatewayConnection, OnApplicationBootstrap {
|
||||||
|
private readonly logger = new Logger(EventBus.name);
|
||||||
|
|
||||||
|
@WebSocketServer()
|
||||||
|
private readonly server?: Server;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly emitter: EventEmitter2,
|
||||||
|
private readonly watcher: EventEmitterReadinessWatcher
|
||||||
|
) {}
|
||||||
|
|
||||||
|
handleConnection(client: Socket) {
|
||||||
|
// for internal usage only, disallow any connection from client
|
||||||
|
this.logger.warn(
|
||||||
|
`EventBus get suspicious connection from client ${client.id}, disconnecting...`
|
||||||
|
);
|
||||||
|
client.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
async onApplicationBootstrap() {
|
||||||
|
this.watcher
|
||||||
|
.waitUntilReady()
|
||||||
|
.then(() => {
|
||||||
|
const events = this.emitter.eventNames() as EventName[];
|
||||||
|
events.forEach(event => {
|
||||||
|
// Proxy all events received from server(trigger by `server.serverSideEmit`)
|
||||||
|
// to internal event system
|
||||||
|
this.server?.on(event, payload => {
|
||||||
|
this.logger.log(`Server Event: ${event} (Received)`);
|
||||||
|
this.emit(event, payload);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// startup time promise, never throw at runtime
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit event to trigger all listeners on current instance
|
||||||
|
*/
|
||||||
|
async emitAsync<T extends EventName>(event: T, payload: Events[T]) {
|
||||||
|
this.logger.log(`Dispatch event: ${event} (async)`);
|
||||||
|
return await this.emitter.emitAsync(event, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit event to trigger all listeners on current instance
|
||||||
|
*/
|
||||||
|
emit<T extends EventName>(event: T, payload: Events[T]) {
|
||||||
|
this.logger.log(`Dispatch event: ${event}`);
|
||||||
|
return this.emitter.emit(event, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcast event to trigger all listeners on all instance in cluster
|
||||||
|
*/
|
||||||
|
broadcast<T extends EventName>(event: T, payload: Events[T]) {
|
||||||
|
this.logger.log(`Server Event: ${event} (Send)`);
|
||||||
|
this.server?.serverSideEmit(event, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
on<T extends EventName>(
|
||||||
|
event: T,
|
||||||
|
listener: (payload: Events[T]) => void | Promise<any>,
|
||||||
|
opts?: OnEventMetadata['options']
|
||||||
|
) {
|
||||||
|
this.emitter.on(event, listener as any, opts);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
this.emitter.off(event, listener as any);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
waitFor<T extends EventName>(name: T, timeout?: number) {
|
||||||
|
return this.emitter.waitFor(name, timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,43 +1,14 @@
|
|||||||
import { Global, Injectable, Module } from '@nestjs/common';
|
import { Global, Module } from '@nestjs/common';
|
||||||
import {
|
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||||
EventEmitter2,
|
|
||||||
EventEmitterModule,
|
|
||||||
OnEvent as RawOnEvent,
|
|
||||||
} from '@nestjs/event-emitter';
|
|
||||||
|
|
||||||
import type { Event, EventPayload } from './def';
|
import { EventBus, OnEvent } from './eventbus';
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class EventEmitter {
|
|
||||||
constructor(private readonly emitter: EventEmitter2) {}
|
|
||||||
|
|
||||||
emit<E extends Event>(event: E, payload: EventPayload<E>) {
|
|
||||||
return this.emitter.emit(event, payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
emitAsync<E extends Event>(event: E, payload: EventPayload<E>) {
|
|
||||||
return this.emitter.emitAsync(event, payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
on<E extends Event>(event: E, handler: (payload: EventPayload<E>) => void) {
|
|
||||||
return this.emitter.on(event, handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
once<E extends Event>(event: E, handler: (payload: EventPayload<E>) => void) {
|
|
||||||
return this.emitter.once(event, handler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const OnEvent = RawOnEvent as (
|
|
||||||
event: Event,
|
|
||||||
opts?: Parameters<typeof RawOnEvent>[1]
|
|
||||||
) => MethodDecorator;
|
|
||||||
|
|
||||||
@Global()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
imports: [EventEmitterModule.forRoot()],
|
imports: [EventEmitterModule.forRoot({ global: false })],
|
||||||
providers: [EventEmitter],
|
providers: [EventBus],
|
||||||
exports: [EventEmitter],
|
exports: [EventBus],
|
||||||
})
|
})
|
||||||
export class EventModule {}
|
export class EventModule {}
|
||||||
export { Event, EventPayload };
|
|
||||||
|
export { EventBus, OnEvent };
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
import type { Join, PathType } from '../utils/types';
|
|
||||||
|
|
||||||
export type Payload<T> = {
|
|
||||||
__payload: true;
|
|
||||||
data: T;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Leaves<T, P extends string = ''> =
|
|
||||||
T extends Record<string, any>
|
|
||||||
? {
|
|
||||||
[K in keyof T]: K extends string
|
|
||||||
? T[K] extends Payload<any>
|
|
||||||
? K
|
|
||||||
: Join<K, Leaves<T[K], P>>
|
|
||||||
: never;
|
|
||||||
}[keyof T]
|
|
||||||
: never;
|
|
||||||
|
|
||||||
export type Flatten<T extends Record<string, any>> = {
|
|
||||||
// @ts-expect-error allow
|
|
||||||
[K in Leaves<T>]: PathType<T, K> extends Payload<infer U> ? U : never;
|
|
||||||
};
|
|
||||||
@@ -14,7 +14,7 @@ export {
|
|||||||
getAFFiNEConfigModifier,
|
getAFFiNEConfigModifier,
|
||||||
} from './config';
|
} from './config';
|
||||||
export * from './error';
|
export * from './error';
|
||||||
export { EventEmitter, type EventPayload, OnEvent } from './event';
|
export { EventBus, OnEvent } from './event';
|
||||||
export type { GraphqlContext } from './graphql';
|
export type { GraphqlContext } from './graphql';
|
||||||
export * from './guard';
|
export * from './guard';
|
||||||
export { CryptoHelper, URLHelper } from './helpers';
|
export { CryptoHelper, URLHelper } from './helpers';
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ export type KnownMetricScopes =
|
|||||||
| 'doc'
|
| 'doc'
|
||||||
| 'sse'
|
| 'sse'
|
||||||
| 'mail'
|
| 'mail'
|
||||||
| 'ai';
|
| 'ai'
|
||||||
|
| 'event';
|
||||||
|
|
||||||
const metricCreators: MetricCreators = {
|
const metricCreators: MetricCreators = {
|
||||||
counter(meter: Meter, name: string, opts?: MetricOptions) {
|
counter(meter: Meter, name: string, opts?: MetricOptions) {
|
||||||
|
|||||||
@@ -1,16 +1,36 @@
|
|||||||
import { Injectable, OnModuleDestroy } from '@nestjs/common';
|
import {
|
||||||
|
Injectable,
|
||||||
|
Logger,
|
||||||
|
OnModuleDestroy,
|
||||||
|
OnModuleInit,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { Redis as IORedis, RedisOptions } from 'ioredis';
|
import { Redis as IORedis, RedisOptions } from 'ioredis';
|
||||||
|
|
||||||
import { Config } from '../../base/config';
|
import { Config } from '../../base/config';
|
||||||
|
|
||||||
class Redis extends IORedis implements OnModuleDestroy {
|
class Redis extends IORedis implements OnModuleInit, OnModuleDestroy {
|
||||||
|
private readonly logger = new Logger(this.constructor.name);
|
||||||
constructor(opts: RedisOptions) {
|
constructor(opts: RedisOptions) {
|
||||||
super(opts);
|
super(opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
errorHandler = (err: Error) => {
|
||||||
|
this.logger.error(err);
|
||||||
|
};
|
||||||
|
|
||||||
|
onModuleInit() {
|
||||||
|
this.on('error', this.errorHandler);
|
||||||
|
}
|
||||||
|
|
||||||
onModuleDestroy() {
|
onModuleDestroy() {
|
||||||
this.disconnect();
|
this.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override duplicate(override?: Partial<RedisOptions>): IORedis {
|
||||||
|
const client = super.duplicate(override);
|
||||||
|
client.on('error', this.errorHandler);
|
||||||
|
return client;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|||||||
@@ -1,22 +1,7 @@
|
|||||||
import { FlattenedAppRuntimeConfig } from '../config/types';
|
import { FlattenedAppRuntimeConfig } from '../config/types';
|
||||||
import { OnEvent } from '../event';
|
|
||||||
import { Payload } from '../event/def';
|
|
||||||
|
|
||||||
declare module '../event/def' {
|
declare global {
|
||||||
interface EventDefinitions {
|
interface Events {
|
||||||
runtime: {
|
'runtime.changed__NOT_IMPLEMENTED__': Partial<FlattenedAppRuntimeConfig>;
|
||||||
[K in keyof FlattenedAppRuntimeConfig]: {
|
|
||||||
changed: Payload<FlattenedAppRuntimeConfig[K]>;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* not implemented yet
|
|
||||||
*/
|
|
||||||
export const OnRuntimeConfigChange_DO_NOT_USE = (
|
|
||||||
nameWithModule: keyof FlattenedAppRuntimeConfig
|
|
||||||
) => {
|
|
||||||
return OnEvent(`runtime.${nameWithModule}.changed`);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -39,17 +39,18 @@ export class SocketIoAdapter extends IoAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pubClient = this.app.get(SocketIoRedis);
|
const pubClient = this.app.get(SocketIoRedis);
|
||||||
|
|
||||||
pubClient.on('error', err => {
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
|
|
||||||
const subClient = pubClient.duplicate();
|
const subClient = pubClient.duplicate();
|
||||||
subClient.on('error', err => {
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
|
|
||||||
server.adapter(createAdapter(pubClient, subClient));
|
server.adapter(createAdapter(pubClient, subClient));
|
||||||
|
const close = server.close;
|
||||||
|
|
||||||
|
server.close = async fn => {
|
||||||
|
await close.call(server, fn);
|
||||||
|
// NOTE(@forehalo):
|
||||||
|
// the lifecycle of duplicated redis client will not be controlled by nestjs lifecycle
|
||||||
|
// we've got to manually disconnect it
|
||||||
|
subClient.disconnect();
|
||||||
|
};
|
||||||
|
|
||||||
return server;
|
return server;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { applyUpdate, Doc } from 'yjs';
|
import { applyUpdate, Doc } from 'yjs';
|
||||||
|
|
||||||
import { Cache, type EventPayload, OnEvent } from '../../base';
|
import { Cache, OnEvent } from '../../base';
|
||||||
import { PgWorkspaceDocStorageAdapter } from '../doc';
|
import { PgWorkspaceDocStorageAdapter } from '../doc';
|
||||||
import {
|
import {
|
||||||
type PageDocContent,
|
type PageDocContent,
|
||||||
@@ -78,15 +78,15 @@ export class DocContentService {
|
|||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnEvent('snapshot.updated')
|
@OnEvent('doc.snapshot.updated')
|
||||||
async markDocContentCacheStale({
|
async markDocContentCacheStale({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
id,
|
docId,
|
||||||
}: EventPayload<'snapshot.updated'>) {
|
}: Events['doc.snapshot.updated']) {
|
||||||
const key =
|
const key =
|
||||||
workspaceId === id
|
workspaceId === docId
|
||||||
? `workspace:${workspaceId}:content`
|
? `workspace:${workspaceId}:content`
|
||||||
: `workspace:${workspaceId}:doc:${id}:content`;
|
: `workspace:${workspaceId}:doc:${docId}:content`;
|
||||||
await this.cache.delete(key);
|
await this.cache.delete(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
Cache,
|
Cache,
|
||||||
DocHistoryNotFound,
|
DocHistoryNotFound,
|
||||||
DocNotFound,
|
DocNotFound,
|
||||||
EventEmitter,
|
EventBus,
|
||||||
FailedToSaveUpdates,
|
FailedToSaveUpdates,
|
||||||
FailedToUpsertSnapshot,
|
FailedToUpsertSnapshot,
|
||||||
metrics,
|
metrics,
|
||||||
@@ -22,7 +22,18 @@ import {
|
|||||||
} from '../storage';
|
} from '../storage';
|
||||||
|
|
||||||
const UPDATES_QUEUE_CACHE_KEY = 'doc:manager:updates';
|
const UPDATES_QUEUE_CACHE_KEY = 'doc:manager:updates';
|
||||||
|
declare global {
|
||||||
|
interface Events {
|
||||||
|
'doc.snapshot.deleted': {
|
||||||
|
workspaceId: string;
|
||||||
|
docId: string;
|
||||||
|
};
|
||||||
|
'doc.snapshot.updated': {
|
||||||
|
workspaceId: string;
|
||||||
|
docId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PgWorkspaceDocStorageAdapter extends DocStorageAdapter {
|
export class PgWorkspaceDocStorageAdapter extends DocStorageAdapter {
|
||||||
private readonly logger = new Logger(PgWorkspaceDocStorageAdapter.name);
|
private readonly logger = new Logger(PgWorkspaceDocStorageAdapter.name);
|
||||||
@@ -31,7 +42,7 @@ export class PgWorkspaceDocStorageAdapter extends DocStorageAdapter {
|
|||||||
private readonly db: PrismaClient,
|
private readonly db: PrismaClient,
|
||||||
private readonly mutex: Mutex,
|
private readonly mutex: Mutex,
|
||||||
private readonly cache: Cache,
|
private readonly cache: Cache,
|
||||||
private readonly event: EventEmitter,
|
private readonly event: EventBus,
|
||||||
protected override readonly options: DocStorageOptions
|
protected override readonly options: DocStorageOptions
|
||||||
) {
|
) {
|
||||||
super(options);
|
super(options);
|
||||||
@@ -470,9 +481,9 @@ export class PgWorkspaceDocStorageAdapter extends DocStorageAdapter {
|
|||||||
const updatedSnapshot = result.at(0);
|
const updatedSnapshot = result.at(0);
|
||||||
|
|
||||||
if (updatedSnapshot) {
|
if (updatedSnapshot) {
|
||||||
this.event.emit('snapshot.updated', {
|
this.event.emit('doc.snapshot.updated', {
|
||||||
workspaceId: snapshot.spaceId,
|
workspaceId: snapshot.spaceId,
|
||||||
id: snapshot.docId,
|
docId: snapshot.docId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,7 @@ import { Injectable, Logger, OnModuleInit, Optional } from '@nestjs/common';
|
|||||||
import { Cron, CronExpression, SchedulerRegistry } from '@nestjs/schedule';
|
import { Cron, CronExpression, SchedulerRegistry } from '@nestjs/schedule';
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
import {
|
import { CallMetric, Config, metrics, OnEvent } from '../../base';
|
||||||
CallMetric,
|
|
||||||
Config,
|
|
||||||
type EventPayload,
|
|
||||||
metrics,
|
|
||||||
OnEvent,
|
|
||||||
} from '../../base';
|
|
||||||
import { PgWorkspaceDocStorageAdapter } from './adapters/workspace';
|
import { PgWorkspaceDocStorageAdapter } from './adapters/workspace';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -81,7 +75,7 @@ export class DocStorageCronJob implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@OnEvent('user.deleted')
|
@OnEvent('user.deleted')
|
||||||
async clearUserWorkspaces(payload: EventPayload<'user.deleted'>) {
|
async clearUserWorkspaces(payload: Events['user.deleted']) {
|
||||||
for (const workspace of payload.ownedWorkspaces) {
|
for (const workspace of payload.ownedWorkspaces) {
|
||||||
await this.workspace.deleteSpace(workspace);
|
await this.workspace.deleteSpace(workspace);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
|
||||||
import { type EventPayload, OnEvent, Runtime } from '../../base';
|
import { Runtime } from '../../base';
|
||||||
import { Models } from '../../models';
|
import { Models } from '../../models';
|
||||||
import { FeatureService } from './service';
|
import { FeatureService } from './service';
|
||||||
import { FeatureType } from './types';
|
import { FeatureType } from './types';
|
||||||
@@ -167,9 +167,4 @@ export class FeatureManagementService {
|
|||||||
async listFeatureWorkspaces(feature: FeatureType) {
|
async listFeatureWorkspaces(feature: FeatureType) {
|
||||||
return this.feature.listWorkspacesByFeature(feature);
|
return this.feature.listWorkspacesByFeature(feature);
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnEvent('user.admin.created')
|
|
||||||
async onAdminUserCreated({ id }: EventPayload<'user.admin.created'>) {
|
|
||||||
await this.addAdmin(id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { groupBy } from 'lodash-es';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
DocAccessDenied,
|
DocAccessDenied,
|
||||||
EventEmitter,
|
EventBus,
|
||||||
SpaceAccessDenied,
|
SpaceAccessDenied,
|
||||||
SpaceOwnerNotFound,
|
SpaceOwnerNotFound,
|
||||||
} from '../../base';
|
} from '../../base';
|
||||||
@@ -15,7 +15,7 @@ import { Permission, PublicPageMode } from './types';
|
|||||||
export class PermissionService {
|
export class PermissionService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly prisma: PrismaClient,
|
private readonly prisma: PrismaClient,
|
||||||
private readonly event: EventEmitter
|
private readonly event: EventBus
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private get acceptedCondition() {
|
private get acceptedCondition() {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import type { Request, Response } from 'express';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
ActionForbidden,
|
ActionForbidden,
|
||||||
EventEmitter,
|
|
||||||
InternalServerError,
|
InternalServerError,
|
||||||
Mutex,
|
Mutex,
|
||||||
PasswordRequired,
|
PasswordRequired,
|
||||||
@@ -24,7 +23,6 @@ export class CustomSetupController {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly models: Models,
|
private readonly models: Models,
|
||||||
private readonly auth: AuthService,
|
private readonly auth: AuthService,
|
||||||
private readonly event: EventEmitter,
|
|
||||||
private readonly mutex: Mutex,
|
private readonly mutex: Mutex,
|
||||||
private readonly server: ServerService,
|
private readonly server: ServerService,
|
||||||
private readonly runtime: Runtime
|
private readonly runtime: Runtime
|
||||||
@@ -62,7 +60,6 @@ export class CustomSetupController {
|
|||||||
if (!lock) {
|
if (!lock) {
|
||||||
throw new InternalServerError();
|
throw new InternalServerError();
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await this.models.user.create({
|
const user = await this.models.user.create({
|
||||||
email: input.email,
|
email: input.email,
|
||||||
password: input.password,
|
password: input.password,
|
||||||
@@ -70,7 +67,12 @@ export class CustomSetupController {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.event.emitAsync('user.admin.created', user);
|
await this.models.userFeature.add(
|
||||||
|
user.id,
|
||||||
|
'administrator',
|
||||||
|
'selfhost setup'
|
||||||
|
);
|
||||||
|
|
||||||
await this.auth.setCookies(req, res, user.id);
|
await this.auth.setCookies(req, res, user.id);
|
||||||
res.send({ id: user.id, email: user.email, name: user.name });
|
res.send({ id: user.id, email: user.email, name: user.name });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common';
|
|||||||
|
|
||||||
import type {
|
import type {
|
||||||
BlobInputType,
|
BlobInputType,
|
||||||
EventPayload,
|
|
||||||
PutObjectMetadata,
|
PutObjectMetadata,
|
||||||
StorageProvider,
|
StorageProvider,
|
||||||
} from '../../../base';
|
} from '../../../base';
|
||||||
@@ -47,7 +46,7 @@ export class AvatarStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@OnEvent('user.deleted')
|
@OnEvent('user.deleted')
|
||||||
async onUserDeleted(user: EventPayload<'user.deleted'>) {
|
async onUserDeleted(user: Events['user.deleted']) {
|
||||||
if (user.avatarUrl) {
|
if (user.avatarUrl) {
|
||||||
await this.delete(user.avatarUrl);
|
await this.delete(user.avatarUrl);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import { PrismaClient } from '@prisma/client';
|
|||||||
import {
|
import {
|
||||||
autoMetadata,
|
autoMetadata,
|
||||||
Config,
|
Config,
|
||||||
EventEmitter,
|
EventBus,
|
||||||
type EventPayload,
|
|
||||||
type GetObjectMetadata,
|
type GetObjectMetadata,
|
||||||
ListObjectsMetadata,
|
ListObjectsMetadata,
|
||||||
OnEvent,
|
OnEvent,
|
||||||
@@ -21,7 +20,7 @@ export class WorkspaceBlobStorage {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly config: Config,
|
private readonly config: Config,
|
||||||
private readonly event: EventEmitter,
|
private readonly event: EventBus,
|
||||||
private readonly storageFactory: StorageProviderFactory,
|
private readonly storageFactory: StorageProviderFactory,
|
||||||
private readonly db: PrismaClient
|
private readonly db: PrismaClient
|
||||||
) {
|
) {
|
||||||
@@ -105,7 +104,7 @@ export class WorkspaceBlobStorage {
|
|||||||
});
|
});
|
||||||
|
|
||||||
deletedBlobs.forEach(blob => {
|
deletedBlobs.forEach(blob => {
|
||||||
this.event.emit('workspace.blob.deleted', {
|
this.event.emit('workspace.blob.delete', {
|
||||||
workspaceId: workspaceId,
|
workspaceId: workspaceId,
|
||||||
key: blob.key,
|
key: blob.key,
|
||||||
});
|
});
|
||||||
@@ -152,10 +151,7 @@ export class WorkspaceBlobStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@OnEvent('workspace.blob.sync')
|
@OnEvent('workspace.blob.sync')
|
||||||
async syncBlobMeta({
|
async syncBlobMeta({ workspaceId, key }: Events['workspace.blob.sync']) {
|
||||||
workspaceId,
|
|
||||||
key,
|
|
||||||
}: EventPayload<'workspace.blob.sync'>) {
|
|
||||||
try {
|
try {
|
||||||
const meta = await this.provider.head(`${workspaceId}/${key}`);
|
const meta = await this.provider.head(`${workspaceId}/${key}`);
|
||||||
|
|
||||||
@@ -176,23 +172,23 @@ export class WorkspaceBlobStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@OnEvent('workspace.deleted')
|
@OnEvent('workspace.deleted')
|
||||||
async onWorkspaceDeleted(workspaceId: EventPayload<'workspace.deleted'>) {
|
async onWorkspaceDeleted({ id }: Events['workspace.deleted']) {
|
||||||
const blobs = await this.list(workspaceId);
|
const blobs = await this.list(id);
|
||||||
|
|
||||||
// to reduce cpu time holding
|
// to reduce cpu time holding
|
||||||
blobs.forEach(blob => {
|
blobs.forEach(blob => {
|
||||||
this.event.emit('workspace.blob.deleted', {
|
this.event.emit('workspace.blob.delete', {
|
||||||
workspaceId: workspaceId,
|
workspaceId: id,
|
||||||
key: blob.key,
|
key: blob.key,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnEvent('workspace.blob.deleted')
|
@OnEvent('workspace.blob.delete')
|
||||||
async onDeleteWorkspaceBlob({
|
async onDeleteWorkspaceBlob({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
key,
|
key,
|
||||||
}: EventPayload<'workspace.blob.deleted'>) {
|
}: Events['workspace.blob.delete']) {
|
||||||
await this.delete(workspaceId, key, true);
|
await this.delete(workspaceId, key, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
|
||||||
import { Config, type EventPayload, OnEvent } from '../../base';
|
import { Config, OnEvent } from '../../base';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserEventsListener {
|
export class UserEventsListener {
|
||||||
@@ -9,7 +9,7 @@ export class UserEventsListener {
|
|||||||
constructor(private readonly config: Config) {}
|
constructor(private readonly config: Config) {}
|
||||||
|
|
||||||
@OnEvent('user.updated')
|
@OnEvent('user.updated')
|
||||||
async onUserUpdated(user: EventPayload<'user.updated'>) {
|
async onUserUpdated(user: Events['user.updated']) {
|
||||||
const { enabled, customerIo } = this.config.metrics;
|
const { enabled, customerIo } = this.config.metrics;
|
||||||
if (enabled && customerIo?.token) {
|
if (enabled && customerIo?.token) {
|
||||||
const payload = {
|
const payload = {
|
||||||
@@ -33,7 +33,7 @@ export class UserEventsListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@OnEvent('user.deleted')
|
@OnEvent('user.deleted')
|
||||||
async onUserDeleted(user: EventPayload<'user.deleted'>) {
|
async onUserDeleted(user: Events['user.deleted']) {
|
||||||
const { enabled, customerIo } = this.config.metrics;
|
const { enabled, customerIo } = this.config.metrics;
|
||||||
if (enabled && customerIo?.token) {
|
if (enabled && customerIo?.token) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
} from '@nestjs/graphql';
|
} from '@nestjs/graphql';
|
||||||
import type { User } from '@prisma/client';
|
import type { User } from '@prisma/client';
|
||||||
|
|
||||||
import type { Payload } from '../../base/event/def';
|
|
||||||
import { type CurrentUser } from '../auth/session';
|
import { type CurrentUser } from '../auth/session';
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
@@ -91,11 +90,3 @@ export class ManageUserInput {
|
|||||||
@Field({ description: 'User name', nullable: true })
|
@Field({ description: 'User name', nullable: true })
|
||||||
name?: string;
|
name?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '../../base/event/def' {
|
|
||||||
interface UserEvents {
|
|
||||||
admin: {
|
|
||||||
created: Payload<{ id: string }>;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { getStreamAsBuffer } from 'get-stream';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
Cache,
|
Cache,
|
||||||
type EventPayload,
|
|
||||||
MailService,
|
MailService,
|
||||||
OnEvent,
|
OnEvent,
|
||||||
URLHelper,
|
URLHelper,
|
||||||
@@ -256,7 +255,7 @@ export class WorkspaceService {
|
|||||||
async onMemberLeave({
|
async onMemberLeave({
|
||||||
user,
|
user,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
}: EventPayload<'workspace.members.leave'>) {
|
}: Events['workspace.members.leave']) {
|
||||||
const workspace = await this.getWorkspaceInfo(workspaceId);
|
const workspace = await this.getWorkspaceInfo(workspaceId);
|
||||||
const owner = await this.permission.getWorkspaceOwner(workspaceId);
|
const owner = await this.permission.getWorkspaceOwner(workspaceId);
|
||||||
await this.mailer.sendMemberLeaveEmail(owner.email, {
|
await this.mailer.sendMemberLeaveEmail(owner.email, {
|
||||||
@@ -269,7 +268,7 @@ export class WorkspaceService {
|
|||||||
async onMemberRemoved({
|
async onMemberRemoved({
|
||||||
userId,
|
userId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
}: EventPayload<'workspace.members.requestDeclined'>) {
|
}: Events['workspace.members.requestDeclined']) {
|
||||||
const user = await this.models.user.get(userId);
|
const user = await this.models.user.get(userId);
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ import { nanoid } from 'nanoid';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
Cache,
|
Cache,
|
||||||
EventEmitter,
|
EventBus,
|
||||||
type EventPayload,
|
|
||||||
MemberNotFoundInSpace,
|
MemberNotFoundInSpace,
|
||||||
OnEvent,
|
OnEvent,
|
||||||
RequestMutex,
|
RequestMutex,
|
||||||
@@ -43,7 +42,7 @@ export class TeamWorkspaceResolver {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly cache: Cache,
|
private readonly cache: Cache,
|
||||||
private readonly event: EventEmitter,
|
private readonly event: EventBus,
|
||||||
private readonly url: URLHelper,
|
private readonly url: URLHelper,
|
||||||
private readonly prisma: PrismaClient,
|
private readonly prisma: PrismaClient,
|
||||||
private readonly permissions: PermissionService,
|
private readonly permissions: PermissionService,
|
||||||
@@ -344,7 +343,7 @@ export class TeamWorkspaceResolver {
|
|||||||
@OnEvent('workspace.members.reviewRequested')
|
@OnEvent('workspace.members.reviewRequested')
|
||||||
async onReviewRequested({
|
async onReviewRequested({
|
||||||
inviteId,
|
inviteId,
|
||||||
}: EventPayload<'workspace.members.reviewRequested'>) {
|
}: Events['workspace.members.reviewRequested']) {
|
||||||
// send review request mail to owner and admin
|
// send review request mail to owner and admin
|
||||||
await this.workspaceService.sendReviewRequestedEmail(inviteId);
|
await this.workspaceService.sendReviewRequestedEmail(inviteId);
|
||||||
}
|
}
|
||||||
@@ -352,7 +351,7 @@ export class TeamWorkspaceResolver {
|
|||||||
@OnEvent('workspace.members.requestApproved')
|
@OnEvent('workspace.members.requestApproved')
|
||||||
async onApproveRequest({
|
async onApproveRequest({
|
||||||
inviteId,
|
inviteId,
|
||||||
}: EventPayload<'workspace.members.requestApproved'>) {
|
}: Events['workspace.members.requestApproved']) {
|
||||||
// send approve mail
|
// send approve mail
|
||||||
await this.workspaceService.sendReviewApproveEmail(inviteId);
|
await this.workspaceService.sendReviewApproveEmail(inviteId);
|
||||||
}
|
}
|
||||||
@@ -361,7 +360,7 @@ export class TeamWorkspaceResolver {
|
|||||||
async onDeclineRequest({
|
async onDeclineRequest({
|
||||||
userId,
|
userId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
}: EventPayload<'workspace.members.requestDeclined'>) {
|
}: Events['workspace.members.requestDeclined']) {
|
||||||
const user = await this.models.user.getPublicUser(userId);
|
const user = await this.models.user.getPublicUser(userId);
|
||||||
// send decline mail
|
// send decline mail
|
||||||
await this.workspaceService.sendReviewDeclinedEmail(
|
await this.workspaceService.sendReviewDeclinedEmail(
|
||||||
@@ -375,7 +374,7 @@ export class TeamWorkspaceResolver {
|
|||||||
userId,
|
userId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
permission,
|
permission,
|
||||||
}: EventPayload<'workspace.members.roleChanged'>) {
|
}: Events['workspace.members.roleChanged']) {
|
||||||
// send role changed mail
|
// send role changed mail
|
||||||
await this.workspaceService.sendRoleChangedEmail(userId, {
|
await this.workspaceService.sendRoleChangedEmail(userId, {
|
||||||
id: workspaceId,
|
id: workspaceId,
|
||||||
@@ -388,7 +387,7 @@ export class TeamWorkspaceResolver {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
}: EventPayload<'workspace.members.ownershipTransferred'>) {
|
}: Events['workspace.members.ownershipTransferred']) {
|
||||||
// send ownership transferred mail
|
// send ownership transferred mail
|
||||||
const fromUser = await this.models.user.getPublicUser(from);
|
const fromUser = await this.models.user.getPublicUser(from);
|
||||||
const toUser = await this.models.user.getPublicUser(to);
|
const toUser = await this.models.user.getPublicUser(to);
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
AlreadyInSpace,
|
AlreadyInSpace,
|
||||||
Cache,
|
Cache,
|
||||||
DocNotFound,
|
DocNotFound,
|
||||||
EventEmitter,
|
EventBus,
|
||||||
InternalServerError,
|
InternalServerError,
|
||||||
MemberQuotaExceeded,
|
MemberQuotaExceeded,
|
||||||
QueryTooLong,
|
QueryTooLong,
|
||||||
@@ -83,7 +83,7 @@ export class WorkspaceResolver {
|
|||||||
private readonly permissions: PermissionService,
|
private readonly permissions: PermissionService,
|
||||||
private readonly quota: QuotaManagementService,
|
private readonly quota: QuotaManagementService,
|
||||||
private readonly models: Models,
|
private readonly models: Models,
|
||||||
private readonly event: EventEmitter,
|
private readonly event: EventBus,
|
||||||
private readonly mutex: RequestMutex,
|
private readonly mutex: RequestMutex,
|
||||||
private readonly workspaceService: WorkspaceService,
|
private readonly workspaceService: WorkspaceService,
|
||||||
private readonly workspaceStorage: PgWorkspaceDocStorageAdapter
|
private readonly workspaceStorage: PgWorkspaceDocStorageAdapter
|
||||||
@@ -389,7 +389,7 @@ export class WorkspaceResolver {
|
|||||||
});
|
});
|
||||||
await this.workspaceStorage.deleteSpace(id);
|
await this.workspaceStorage.deleteSpace(id);
|
||||||
|
|
||||||
this.event.emit('workspace.deleted', id);
|
this.event.emit('workspace.deleted', { id });
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,10 @@ import { pick } from 'lodash-es';
|
|||||||
import {
|
import {
|
||||||
CryptoHelper,
|
CryptoHelper,
|
||||||
EmailAlreadyUsed,
|
EmailAlreadyUsed,
|
||||||
EventEmitter,
|
EventBus,
|
||||||
WrongSignInCredentials,
|
WrongSignInCredentials,
|
||||||
WrongSignInMethod,
|
WrongSignInMethod,
|
||||||
} from '../base';
|
} from '../base';
|
||||||
import type { Payload } from '../base/event/def';
|
|
||||||
import { Quota_FreePlanV1_1 } from '../core/quota/schema';
|
import { Quota_FreePlanV1_1 } from '../core/quota/schema';
|
||||||
import { BaseModel } from './base';
|
import { BaseModel } from './base';
|
||||||
import type { Workspace } from './workspace';
|
import type { Workspace } from './workspace';
|
||||||
@@ -40,21 +39,15 @@ const defaultUserCreatingData = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
declare module '../base/event/def' {
|
declare global {
|
||||||
interface UserEvents {
|
interface Events {
|
||||||
created: Payload<User>;
|
'user.created': User;
|
||||||
updated: Payload<User>;
|
'user.updated': User;
|
||||||
deleted: Payload<
|
'user.deleted': User & {
|
||||||
User & {
|
// TODO(@forehalo): unlink foreign key constraint on [WorkspaceUserPermission] to delegate
|
||||||
// TODO(@forehalo): unlink foreign key constraint on [WorkspaceUserPermission] to delegate
|
// dealing of owned workspaces of deleted users to workspace model
|
||||||
// dealing of owned workspaces of deleted users to workspace model
|
ownedWorkspaces: Workspace['id'][];
|
||||||
ownedWorkspaces: Workspace['id'][];
|
};
|
||||||
}
|
|
||||||
>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EventDefinitions {
|
|
||||||
user: UserEvents;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +58,7 @@ export type { User };
|
|||||||
export class UserModel extends BaseModel {
|
export class UserModel extends BaseModel {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly crypto: CryptoHelper,
|
private readonly crypto: CryptoHelper,
|
||||||
private readonly event: EventEmitter
|
private readonly event: EventBus
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,57 @@ import {
|
|||||||
} from '@prisma/client';
|
} from '@prisma/client';
|
||||||
import { groupBy } from 'lodash-es';
|
import { groupBy } from 'lodash-es';
|
||||||
|
|
||||||
import { EventEmitter } from '../base';
|
import { EventBus } from '../base';
|
||||||
import { BaseModel } from './base';
|
import { BaseModel } from './base';
|
||||||
import { Permission } from './common';
|
import { Permission } from './common';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Events {
|
||||||
|
'workspace.members.reviewRequested': { inviteId: string };
|
||||||
|
'workspace.members.requestDeclined': {
|
||||||
|
userId: string;
|
||||||
|
workspaceId: string;
|
||||||
|
};
|
||||||
|
'workspace.members.requestApproved': { inviteId: string };
|
||||||
|
'workspace.members.roleChanged': {
|
||||||
|
userId: string;
|
||||||
|
workspaceId: string;
|
||||||
|
permission: number;
|
||||||
|
};
|
||||||
|
'workspace.members.ownershipTransferred': {
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
workspaceId: string;
|
||||||
|
};
|
||||||
|
'workspace.members.updated': {
|
||||||
|
workspaceId: string;
|
||||||
|
count: number;
|
||||||
|
};
|
||||||
|
'workspace.members.leave': {
|
||||||
|
user: {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
workspaceId: string;
|
||||||
|
};
|
||||||
|
'workspace.members.removed': {
|
||||||
|
workspaceId: string;
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
'workspace.deleted': {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
'workspace.blob.delete': {
|
||||||
|
workspaceId: string;
|
||||||
|
key: string;
|
||||||
|
};
|
||||||
|
'workspace.blob.sync': {
|
||||||
|
workspaceId: string;
|
||||||
|
key: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export { WorkspaceMemberStatus };
|
export { WorkspaceMemberStatus };
|
||||||
export type { Workspace };
|
export type { Workspace };
|
||||||
export type UpdateWorkspaceInput = Pick<
|
export type UpdateWorkspaceInput = Pick<
|
||||||
@@ -28,7 +75,7 @@ export interface FindWorkspaceMembersOptions {
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WorkspaceModel extends BaseModel {
|
export class WorkspaceModel extends BaseModel {
|
||||||
constructor(private readonly event: EventEmitter) {
|
constructor(private readonly event: EventBus) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { Cron, CronExpression } from '@nestjs/schedule';
|
|||||||
import { InstalledLicense, PrismaClient } from '@prisma/client';
|
import { InstalledLicense, PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
EventEmitter,
|
Config,
|
||||||
type EventPayload,
|
EventBus,
|
||||||
InternalServerError,
|
InternalServerError,
|
||||||
LicenseNotFound,
|
LicenseNotFound,
|
||||||
OnEvent,
|
OnEvent,
|
||||||
@@ -27,9 +27,10 @@ export class LicenseService {
|
|||||||
private readonly logger = new Logger(LicenseService.name);
|
private readonly logger = new Logger(LicenseService.name);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
private readonly config: Config,
|
||||||
private readonly db: PrismaClient,
|
private readonly db: PrismaClient,
|
||||||
private readonly quota: QuotaManagementService,
|
private readonly quota: QuotaManagementService,
|
||||||
private readonly event: EventEmitter,
|
private readonly event: EventBus,
|
||||||
private readonly permission: PermissionService
|
private readonly permission: PermissionService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -151,7 +152,11 @@ export class LicenseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@OnEvent('workspace.members.updated')
|
@OnEvent('workspace.members.updated')
|
||||||
async updateTeamSeats(payload: EventPayload<'workspace.members.updated'>) {
|
async updateTeamSeats(payload: Events['workspace.members.updated']) {
|
||||||
|
if (!this.config.isSelfhosted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { workspaceId, count } = payload;
|
const { workspaceId, count } = payload;
|
||||||
|
|
||||||
const license = await this.db.installedLicense.findUnique({
|
const license = await this.db.installedLicense.findUnique({
|
||||||
@@ -308,7 +313,7 @@ export class LicenseService {
|
|||||||
plan,
|
plan,
|
||||||
recurring,
|
recurring,
|
||||||
quantity,
|
quantity,
|
||||||
}: EventPayload<'workspace.subscription.activated'>) {
|
}: Events['workspace.subscription.activated']) {
|
||||||
switch (plan) {
|
switch (plan) {
|
||||||
case SubscriptionPlan.SelfHostedTeam:
|
case SubscriptionPlan.SelfHostedTeam:
|
||||||
await this.quota.addTeamWorkspace(
|
await this.quota.addTeamWorkspace(
|
||||||
@@ -331,7 +336,7 @@ export class LicenseService {
|
|||||||
async onWorkspaceSubscriptionCanceled({
|
async onWorkspaceSubscriptionCanceled({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
plan,
|
plan,
|
||||||
}: EventPayload<'workspace.subscription.canceled'>) {
|
}: Events['workspace.subscription.canceled']) {
|
||||||
switch (plan) {
|
switch (plan) {
|
||||||
case SubscriptionPlan.SelfHostedTeam:
|
case SubscriptionPlan.SelfHostedTeam:
|
||||||
await this.quota.removeTeamWorkspace(workspaceId);
|
await this.quota.removeTeamWorkspace(workspaceId);
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ import assert from 'node:assert';
|
|||||||
|
|
||||||
import type { RawBodyRequest } from '@nestjs/common';
|
import type { RawBodyRequest } from '@nestjs/common';
|
||||||
import { Controller, Logger, Post, Req } from '@nestjs/common';
|
import { Controller, Logger, Post, Req } from '@nestjs/common';
|
||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
|
||||||
import type { Request } from 'express';
|
import type { Request } from 'express';
|
||||||
import Stripe from 'stripe';
|
import Stripe from 'stripe';
|
||||||
|
|
||||||
import { Config, InternalServerError } from '../../base';
|
import { Config, EventBus, InternalServerError } from '../../base';
|
||||||
import { Public } from '../../core/auth';
|
import { Public } from '../../core/auth';
|
||||||
|
|
||||||
@Controller('/api/stripe')
|
@Controller('/api/stripe')
|
||||||
@@ -17,7 +16,7 @@ export class StripeWebhookController {
|
|||||||
constructor(
|
constructor(
|
||||||
config: Config,
|
config: Config,
|
||||||
private readonly stripe: Stripe,
|
private readonly stripe: Stripe,
|
||||||
private readonly event: EventEmitter2
|
private readonly event: EventBus
|
||||||
) {
|
) {
|
||||||
assert(config.plugins.payment.stripe);
|
assert(config.plugins.payment.stripe);
|
||||||
this.webhookKey = config.plugins.payment.stripe.keys.webhookKey;
|
this.webhookKey = config.plugins.payment.stripe.keys.webhookKey;
|
||||||
@@ -41,7 +40,7 @@ export class StripeWebhookController {
|
|||||||
|
|
||||||
// Stripe requires responseing webhook immediately and handle event asynchronously.
|
// Stripe requires responseing webhook immediately and handle event asynchronously.
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
this.event.emitAsync(`stripe:${event.type}`, event).catch(e => {
|
this.event.emitAsync(`stripe.${event.type}` as any, event).catch(e => {
|
||||||
this.logger.error('Failed to handle Stripe Webhook event.', e);
|
this.logger.error('Failed to handle Stripe Webhook event.', e);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { OnEvent } from '@nestjs/event-emitter';
|
|||||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
import { EventEmitter, type EventPayload } from '../../base';
|
import { EventBus } from '../../base';
|
||||||
import {
|
import {
|
||||||
SubscriptionPlan,
|
SubscriptionPlan,
|
||||||
SubscriptionRecurring,
|
SubscriptionRecurring,
|
||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
export class SubscriptionCronJobs {
|
export class SubscriptionCronJobs {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly db: PrismaClient,
|
private readonly db: PrismaClient,
|
||||||
private readonly event: EventEmitter
|
private readonly event: EventBus
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private getDateRange(after: number, base: number | Date = Date.now()) {
|
private getDateRange(after: number, base: number | Date = Date.now()) {
|
||||||
@@ -77,14 +77,14 @@ export class SubscriptionCronJobs {
|
|||||||
// should not reach here
|
// should not reach here
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
this.event.emit('workspace.subscription.notify', {
|
|
||||||
workspaceId: subscription.targetId,
|
if (!subscription.nextBillAt) {
|
||||||
expirationDate: end,
|
this.event.emit('workspace.subscription.notify', {
|
||||||
deletionDate:
|
workspaceId: subscription.targetId,
|
||||||
subscription.status === 'canceled'
|
expirationDate: end,
|
||||||
? this.getDateRange(180, end).end
|
deletionDate: this.getDateRange(180, end).end,
|
||||||
: undefined,
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ export class SubscriptionCronJobs {
|
|||||||
async handleUserSubscriptionCanceled({
|
async handleUserSubscriptionCanceled({
|
||||||
userId,
|
userId,
|
||||||
plan,
|
plan,
|
||||||
}: EventPayload<'user.subscription.canceled'>) {
|
}: Events['user.subscription.canceled']) {
|
||||||
await this.db.subscription.delete({
|
await this.db.subscription.delete({
|
||||||
where: {
|
where: {
|
||||||
targetId_plan: {
|
targetId_plan: {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import Stripe from 'stripe';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
EventEmitter,
|
EventBus,
|
||||||
InternalServerError,
|
InternalServerError,
|
||||||
InvalidCheckoutParameters,
|
InvalidCheckoutParameters,
|
||||||
Runtime,
|
Runtime,
|
||||||
@@ -58,7 +58,7 @@ export class UserSubscriptionManager extends SubscriptionManager {
|
|||||||
db: PrismaClient,
|
db: PrismaClient,
|
||||||
private readonly runtime: Runtime,
|
private readonly runtime: Runtime,
|
||||||
private readonly feature: FeatureManagementService,
|
private readonly feature: FeatureManagementService,
|
||||||
private readonly event: EventEmitter,
|
private readonly event: EventBus,
|
||||||
private readonly url: URLHelper
|
private readonly url: URLHelper
|
||||||
) {
|
) {
|
||||||
super(stripe, db);
|
super(stripe, db);
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import Stripe from 'stripe';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
EventEmitter,
|
EventBus,
|
||||||
type EventPayload,
|
|
||||||
OnEvent,
|
OnEvent,
|
||||||
SubscriptionAlreadyExists,
|
SubscriptionAlreadyExists,
|
||||||
SubscriptionPlanNotFound,
|
SubscriptionPlanNotFound,
|
||||||
@@ -49,7 +48,7 @@ export class WorkspaceSubscriptionManager extends SubscriptionManager {
|
|||||||
stripe: Stripe,
|
stripe: Stripe,
|
||||||
db: PrismaClient,
|
db: PrismaClient,
|
||||||
private readonly url: URLHelper,
|
private readonly url: URLHelper,
|
||||||
private readonly event: EventEmitter
|
private readonly event: EventBus
|
||||||
) {
|
) {
|
||||||
super(stripe, db);
|
super(stripe, db);
|
||||||
}
|
}
|
||||||
@@ -269,7 +268,7 @@ export class WorkspaceSubscriptionManager extends SubscriptionManager {
|
|||||||
async onMembersUpdated({
|
async onMembersUpdated({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
count,
|
count,
|
||||||
}: EventPayload<'workspace.members.updated'>) {
|
}: Events['workspace.members.updated']) {
|
||||||
const subscription = await this.getSubscription({
|
const subscription = await this.getSubscription({
|
||||||
plan: SubscriptionPlan.Team,
|
plan: SubscriptionPlan.Team,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { OnEvent } from '@nestjs/event-emitter';
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
|
|
||||||
import type { EventPayload } from '../../base';
|
|
||||||
import { FeatureManagementService } from '../../core/features';
|
import { FeatureManagementService } from '../../core/features';
|
||||||
import { PermissionService } from '../../core/permission';
|
import { PermissionService } from '../../core/permission';
|
||||||
import {
|
import {
|
||||||
@@ -29,7 +28,7 @@ export class QuotaOverride {
|
|||||||
plan,
|
plan,
|
||||||
recurring,
|
recurring,
|
||||||
quantity,
|
quantity,
|
||||||
}: EventPayload<'workspace.subscription.activated'>) {
|
}: Events['workspace.subscription.activated']) {
|
||||||
switch (plan) {
|
switch (plan) {
|
||||||
case 'team': {
|
case 'team': {
|
||||||
const hasTeamWorkspace = await this.quota.hasWorkspaceQuota(
|
const hasTeamWorkspace = await this.quota.hasWorkspaceQuota(
|
||||||
@@ -62,7 +61,7 @@ export class QuotaOverride {
|
|||||||
async onWorkspaceSubscriptionCanceled({
|
async onWorkspaceSubscriptionCanceled({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
plan,
|
plan,
|
||||||
}: EventPayload<'workspace.subscription.canceled'>) {
|
}: Events['workspace.subscription.canceled']) {
|
||||||
switch (plan) {
|
switch (plan) {
|
||||||
case SubscriptionPlan.Team:
|
case SubscriptionPlan.Team:
|
||||||
await this.manager.removeTeamWorkspace(workspaceId);
|
await this.manager.removeTeamWorkspace(workspaceId);
|
||||||
@@ -77,7 +76,7 @@ export class QuotaOverride {
|
|||||||
userId,
|
userId,
|
||||||
plan,
|
plan,
|
||||||
recurring,
|
recurring,
|
||||||
}: EventPayload<'user.subscription.activated'>) {
|
}: Events['user.subscription.activated']) {
|
||||||
switch (plan) {
|
switch (plan) {
|
||||||
case SubscriptionPlan.AI:
|
case SubscriptionPlan.AI:
|
||||||
await this.feature.addCopilot(userId, 'subscription activated');
|
await this.feature.addCopilot(userId, 'subscription activated');
|
||||||
@@ -100,7 +99,7 @@ export class QuotaOverride {
|
|||||||
async onUserSubscriptionCanceled({
|
async onUserSubscriptionCanceled({
|
||||||
userId,
|
userId,
|
||||||
plan,
|
plan,
|
||||||
}: EventPayload<'user.subscription.canceled'>) {
|
}: Events['user.subscription.canceled']) {
|
||||||
switch (plan) {
|
switch (plan) {
|
||||||
case SubscriptionPlan.AI:
|
case SubscriptionPlan.AI:
|
||||||
await this.feature.removeCopilot(userId);
|
await this.feature.removeCopilot(userId);
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import type { User, Workspace } from '@prisma/client';
|
import type { User, Workspace } from '@prisma/client';
|
||||||
import Stripe from 'stripe';
|
import Stripe from 'stripe';
|
||||||
|
|
||||||
import type { Payload } from '../../base/event/def';
|
|
||||||
|
|
||||||
export enum SubscriptionRecurring {
|
export enum SubscriptionRecurring {
|
||||||
Monthly = 'monthly',
|
Monthly = 'monthly',
|
||||||
Yearly = 'yearly',
|
Yearly = 'yearly',
|
||||||
@@ -50,41 +48,44 @@ export enum CouponType {
|
|||||||
ProEarlyAccessAIOneYearFree = 'ai_pro_ea_one_year_free',
|
ProEarlyAccessAIOneYearFree = 'ai_pro_ea_one_year_free',
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '../../base/event/def' {
|
declare global {
|
||||||
interface UserEvents {
|
interface Events {
|
||||||
subscription: {
|
'user.subscription.activated': {
|
||||||
activated: Payload<{
|
userId: User['id'];
|
||||||
userId: User['id'];
|
plan: SubscriptionPlan;
|
||||||
plan: SubscriptionPlan;
|
recurring: SubscriptionRecurring;
|
||||||
recurring: SubscriptionRecurring;
|
};
|
||||||
}>;
|
'user.subscription.canceled': {
|
||||||
canceled: Payload<{
|
userId: User['id'];
|
||||||
userId: User['id'];
|
plan: SubscriptionPlan;
|
||||||
plan: SubscriptionPlan;
|
recurring: SubscriptionRecurring;
|
||||||
recurring: SubscriptionRecurring;
|
|
||||||
}>;
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
interface WorkspaceEvents {
|
'workspace.subscription.activated': {
|
||||||
subscription: {
|
workspaceId: Workspace['id'];
|
||||||
activated: Payload<{
|
plan: SubscriptionPlan;
|
||||||
workspaceId: Workspace['id'];
|
recurring: SubscriptionRecurring;
|
||||||
plan: SubscriptionPlan;
|
quantity: number;
|
||||||
recurring: SubscriptionRecurring;
|
|
||||||
quantity: number;
|
|
||||||
}>;
|
|
||||||
canceled: Payload<{
|
|
||||||
workspaceId: Workspace['id'];
|
|
||||||
plan: SubscriptionPlan;
|
|
||||||
recurring: SubscriptionRecurring;
|
|
||||||
}>;
|
|
||||||
notify: Payload<{
|
|
||||||
workspaceId: Workspace['id'];
|
|
||||||
expirationDate: Date;
|
|
||||||
deletionDate: Date | undefined;
|
|
||||||
}>;
|
|
||||||
};
|
};
|
||||||
|
'workspace.subscription.canceled': {
|
||||||
|
workspaceId: Workspace['id'];
|
||||||
|
plan: SubscriptionPlan;
|
||||||
|
recurring: SubscriptionRecurring;
|
||||||
|
};
|
||||||
|
'workspace.subscription.notify': {
|
||||||
|
workspaceId: Workspace['id'];
|
||||||
|
expirationDate: Date;
|
||||||
|
deletionDate: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
'stripe.invoice.created': Stripe.InvoiceCreatedEvent;
|
||||||
|
'stripe.invoice.updated': Stripe.InvoiceUpdatedEvent;
|
||||||
|
'stripe.invoice.finalization_failed': Stripe.InvoiceFinalizationFailedEvent;
|
||||||
|
'stripe.invoice.payment_failed': Stripe.InvoicePaymentFailedEvent;
|
||||||
|
'stripe.invoice.paid': Stripe.InvoicePaidEvent;
|
||||||
|
'stripe.customer.subscription.created': Stripe.CustomerSubscriptionCreatedEvent;
|
||||||
|
'stripe.customer.subscription.updated': Stripe.CustomerSubscriptionUpdatedEvent;
|
||||||
|
'stripe.customer.subscription.deleted': Stripe.CustomerSubscriptionDeletedEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { OnEvent } from '@nestjs/event-emitter';
|
|
||||||
import Stripe from 'stripe';
|
import Stripe from 'stripe';
|
||||||
|
|
||||||
|
import { OnEvent } from '../../base';
|
||||||
import { SubscriptionService } from './service';
|
import { SubscriptionService } from './service';
|
||||||
|
|
||||||
const OnStripeEvent = (
|
|
||||||
event: Stripe.Event.Type,
|
|
||||||
opts?: Parameters<typeof OnEvent>[1]
|
|
||||||
) => OnEvent(`stripe:${event}`, opts);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stripe webhook events sent in random order, and may be even sent more than once.
|
* Stripe webhook events sent in random order, and may be even sent more than once.
|
||||||
*
|
*
|
||||||
@@ -22,11 +17,11 @@ export class StripeWebhook {
|
|||||||
private readonly stripe: Stripe
|
private readonly stripe: Stripe
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@OnStripeEvent('invoice.created')
|
@OnEvent('stripe.invoice.created')
|
||||||
@OnStripeEvent('invoice.updated')
|
@OnEvent('stripe.invoice.updated')
|
||||||
@OnStripeEvent('invoice.finalization_failed')
|
@OnEvent('stripe.invoice.finalization_failed')
|
||||||
@OnStripeEvent('invoice.payment_failed')
|
@OnEvent('stripe.invoice.payment_failed')
|
||||||
@OnStripeEvent('invoice.paid')
|
@OnEvent('stripe.invoice.paid')
|
||||||
async onInvoiceUpdated(
|
async onInvoiceUpdated(
|
||||||
event:
|
event:
|
||||||
| Stripe.InvoiceCreatedEvent
|
| Stripe.InvoiceCreatedEvent
|
||||||
@@ -39,8 +34,8 @@ export class StripeWebhook {
|
|||||||
await this.service.saveStripeInvoice(invoice);
|
await this.service.saveStripeInvoice(invoice);
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnStripeEvent('customer.subscription.created')
|
@OnEvent('stripe.customer.subscription.created')
|
||||||
@OnStripeEvent('customer.subscription.updated')
|
@OnEvent('stripe.customer.subscription.updated')
|
||||||
async onSubscriptionChanges(
|
async onSubscriptionChanges(
|
||||||
event:
|
event:
|
||||||
| Stripe.CustomerSubscriptionUpdatedEvent
|
| Stripe.CustomerSubscriptionUpdatedEvent
|
||||||
@@ -56,7 +51,7 @@ export class StripeWebhook {
|
|||||||
await this.service.saveStripeSubscription(subscription);
|
await this.service.saveStripeSubscription(subscription);
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnStripeEvent('customer.subscription.deleted')
|
@OnEvent('stripe.customer.subscription.deleted')
|
||||||
async onSubscriptionDeleted(event: Stripe.CustomerSubscriptionDeletedEvent) {
|
async onSubscriptionDeleted(event: Stripe.CustomerSubscriptionDeletedEvent) {
|
||||||
await this.service.deleteStripeSubscription(event.data.object);
|
await this.service.deleteStripeSubscription(event.data.object);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user