feat: unique flag for apps - one instance per session

This commit is contained in:
devlikepro committed 2026-08-31 10:44:22 +07:00
1 parent dbda9c0457
commit 16b50c672b
2 files changed
+59 -40

No files matched your search

+27
View File
@@ -11,6 +11,8 @@ export interface AppDefinition {
migrations: boolean;
// If adding, updating, or removing this app requires a session restart
restartOnChange: boolean;
// If only one instance of this app is allowed per session
unique: boolean;
}
// All Apps
@@ -21,6 +23,7 @@ export const APPS: Record<AppName, AppDefinition> = {
queue: false,
migrations: false,
restartOnChange: true,
unique: true,
},
[AppName.chatwoot]: {
name: AppName.chatwoot,
@@ -28,6 +31,7 @@ export const APPS: Record<AppName, AppDefinition> = {
queue: true,
migrations: true,
restartOnChange: true,
unique: true,
},
[AppName.mcp]: {
name: AppName.mcp,
@@ -35,6 +39,7 @@ export const APPS: Record<AppName, AppDefinition> = {
queue: false,
migrations: false,
restartOnChange: false,
unique: false,
},
[AppName.brazilianPhoneNumbers]: {
name: AppName.brazilianPhoneNumbers,
@@ -42,5 +47,27 @@ export const APPS: Record<AppName, AppDefinition> = {
queue: false,
migrations: true,
restartOnChange: true,
unique: true,
},
};
export function isUniqueApp(name: AppName): boolean {
return APPS[name]?.unique === true;
}
// Returns the first unique AppName that appears more than once in the list, or null
export function findDuplicateUniqueApp(
apps: Array<{ app: AppName }>,
): AppName | null {
const seen = new Set<AppName>();
for (const app of apps) {
if (!isUniqueApp(app.app)) {
continue;
}
if (seen.has(app.app)) {
return app.app;
}
seen.add(app.app);
}
return null;
}
+32 -40
View File
@@ -22,6 +22,10 @@ import { App } from '../dto/app.dto';
import { AppRepository } from '../storage/AppRepository';
import { AppName } from '@waha/apps/app_sdk/apps/name';
import { AppRuntimeConfig } from '@waha/apps/app_sdk/apps/AppRuntime';
import {
findDuplicateUniqueApp,
isUniqueApp,
} from '@waha/apps/app_sdk/apps/definition';
export class AppDisableError extends UnprocessableEntityException {
constructor(app: string) {
@@ -31,6 +35,15 @@ export class AppDisableError extends UnprocessableEntityException {
}
}
export class AppUniquePerSessionError extends UnprocessableEntityException {
constructor(app: string, session: string, existingAppId: string) {
super(
`Only one '${app}' app is allowed per session. ` +
`Session '${session}' already has a '${app}' app with ID '${existingAppId}'.`,
);
}
}
@Injectable()
export class AppsEnabledService implements IAppsService {
constructor(
@@ -67,47 +80,17 @@ export class AppsEnabledService implements IAppsService {
throw new Error(`App with ID '${app.id}' already exists.`);
}
let existingApps: App[] = [];
if (
app.app === AppName.chatwoot ||
app.app === AppName.calls ||
app.app === AppName.brazilianPhoneNumbers
) {
existingApps = await repo.getAllBySession(app.session);
}
// Validate only one Chatwoot app per session
if (app.app === AppName.chatwoot) {
const existingChatwootApp = existingApps.find(
(existingApp) => existingApp.app === AppName.chatwoot,
// Validate only one instance of a unique app per session
if (isUniqueApp(app.app)) {
const existingApps = await repo.getAllBySession(app.session);
const duplicateApp = existingApps.find(
(existingApp) => existingApp.app === app.app,
);
if (existingChatwootApp) {
throw new Error(
`Only one Chatwoot app is allowed per session. Session '${app.session}' already has a Chatwoot app with ID '${existingChatwootApp.id}'.`,
);
}
}
// Validate only one Calls app per session
if (app.app === AppName.calls) {
const existingCallsApp = existingApps.find(
(existingApp) => existingApp.app === AppName.calls,
);
if (existingCallsApp) {
throw new Error(
`Only one Calls app is allowed per session. Session '${app.session}' already has a Calls app with ID '${existingCallsApp.id}'.`,
);
}
}
// Validate only one Brazilian Phone Numbers app per session
if (app.app === AppName.brazilianPhoneNumbers) {
const existingBrazilianPhoneNumbersApp = existingApps.find(
(existingApp) => existingApp.app === AppName.brazilianPhoneNumbers,
);
if (existingBrazilianPhoneNumbersApp) {
throw new Error(
`Only one Brazilian Phone Numbers app is allowed per session. Session '${app.session}' already has a Brazilian Phone Numbers app with ID '${existingBrazilianPhoneNumbersApp.id}'.`,
if (duplicateApp) {
throw new AppUniquePerSessionError(
app.app,
app.session,
duplicateApp.id,
);
}
}
@@ -257,6 +240,15 @@ export class AppsEnabledService implements IAppsService {
session: string,
apps: App[],
): Promise<void> {
// Reject duplicate unique apps in the payload before any writes,
// otherwise the by-type matching below binds them to the same app
const duplicateUniqueApp = findDuplicateUniqueApp(apps);
if (duplicateUniqueApp !== null) {
throw new UnprocessableEntityException(
`Only one '${duplicateUniqueApp}' app is allowed per session - remove duplicate entries from 'apps'.`,
);
}
const existing = await this.list(manager, session);
const ids = new Set<string>();