feat(core): Allow a sending status on instance monitoring reports (no-changelog) (#39923)

This commit is contained in:
Lorent Lempereur 2026-09-30 13:54:16 +00:00 committed by GitHub
parent 5576c785ce
commit 45edf565df
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 126 additions and 3 deletions

View File

@ -19,7 +19,7 @@
| Name | Type | Definition |
| ---- | ---- | ---------- |
| CHK_instance_monitoring_report_status | CHECK | CHECK (((status)::text = ANY ((ARRAY['pending'::character varying, 'delivered'::character varying, 'skipped_after_max_retries'::character varying])::text[]))) |
| CHK_instance_monitoring_report_status | CHECK | CHECK (((status)::text = ANY ((ARRAY['pending'::character varying, 'delivered'::character varying, 'skipped_after_max_retries'::character varying, 'sending'::character varying])::text[]))) |
| PK_4a5cb8aa51c67e4f5a7eb0f9509 | PRIMARY KEY | PRIMARY KEY (id) |
| instance_monitoring_report_attempts_not_null | n | NOT NULL attempts |
| instance_monitoring_report_createdAt_not_null | n | NOT NULL "createdAt" |

View File

@ -6,7 +6,7 @@
<summary><strong>Table Definition</strong></summary>
```sql
CREATE TABLE "instance_monitoring_report" ("id" varchar PRIMARY KEY NOT NULL, "dataPoints" text NOT NULL, "status" varchar(64) NOT NULL DEFAULT ('pending'), "deliveredAt" datetime(3), "attempts" integer NOT NULL DEFAULT (0), "lastAttemptAt" datetime(3), "lastError" text, "createdAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "updatedAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "reportDate" varchar(10), CONSTRAINT "CHK_instance_monitoring_report_status" CHECK (("status" IN ('pending', 'delivered', 'skipped_after_max_retries'))))
CREATE TABLE "instance_monitoring_report" ("id" varchar PRIMARY KEY NOT NULL, "dataPoints" text NOT NULL, "status" varchar(64) NOT NULL DEFAULT ('pending'), "deliveredAt" datetime(3), "attempts" integer NOT NULL DEFAULT (0), "lastAttemptAt" datetime(3), "lastError" text, "createdAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "updatedAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "reportDate" varchar(10), CONSTRAINT "CHK_instance_monitoring_report_status" CHECK ("status" IN ('pending', 'delivered', 'skipped_after_max_retries', 'sending')))
```
</details>
@ -30,7 +30,7 @@ CREATE TABLE "instance_monitoring_report" ("id" varchar PRIMARY KEY NOT NULL, "d
| Name | Type | Definition |
| ---- | ---- | ---------- |
| - | CHECK | CHECK (("status" IN ('pending', 'delivered', 'skipped_after_max_retries'))) |
| - | CHECK | CHECK ("status" IN ('pending', 'delivered', 'skipped_after_max_retries', 'sending')) |
| id | PRIMARY KEY | PRIMARY KEY (id) |
| sqlite_autoindex_instance_monitoring_report_1 | PRIMARY KEY | PRIMARY KEY (id) |

View File

@ -0,0 +1,21 @@
import type { MigrationContext, ReversibleMigration } from '../migration-types';
const TABLE = 'instance_monitoring_report';
const COLUMN = 'status';
const VALUES_BEFORE = ['pending', 'delivered', 'skipped_after_max_retries'];
const VALUES_AFTER = [...VALUES_BEFORE, 'sending'];
export class AllowSendingInstanceReportStatus1790774580617 implements ReversibleMigration {
async up({ schemaBuilder }: MigrationContext) {
await schemaBuilder.dropEnumCheck(TABLE, COLUMN, { recreatesOnSqlite: true });
await schemaBuilder.addEnumCheck(TABLE, COLUMN, VALUES_AFTER, { recreatesOnSqlite: true });
}
async down({ escape, runQuery, schemaBuilder }: MigrationContext) {
await runQuery(
`UPDATE ${escape.tableName(TABLE)} SET ${escape.columnName(COLUMN)} = 'pending' WHERE ${escape.columnName(COLUMN)} = 'sending'`,
);
await schemaBuilder.dropEnumCheck(TABLE, COLUMN, { recreatesOnSqlite: true });
await schemaBuilder.addEnumCheck(TABLE, COLUMN, VALUES_BEFORE, { recreatesOnSqlite: true });
}
}

View File

@ -0,0 +1,102 @@
import {
createTestMigrationContext,
initDbUpToMigration,
runSingleMigration,
undoLastSingleMigration,
type TestMigrationContext,
} from '@n8n/backend-test-utils';
import { DbConnection } from '@n8n/db';
import { Container } from '@n8n/di';
import { DataSource } from '@n8n/typeorm';
import { randomUUID } from 'node:crypto';
const MIGRATION_NAME = 'AllowSendingInstanceReportStatus1790774580617';
const TABLE = 'instance_monitoring_report';
describe('AllowSendingInstanceReportStatus Migration', () => {
let dataSource: DataSource;
beforeAll(async () => {
const dbConnection = Container.get(DbConnection);
await dbConnection.init();
dataSource = Container.get(DataSource);
});
beforeEach(async () => {
const context = createTestMigrationContext(dataSource);
await context.queryRunner.clearDatabase();
await context.queryRunner.release();
await initDbUpToMigration(MIGRATION_NAME);
});
afterAll(async () => {
const dbConnection = Container.get(DbConnection);
await dbConnection.close();
});
async function withContext<T>(run: (context: TestMigrationContext) => Promise<T>): Promise<T> {
const context = createTestMigrationContext(dataSource);
try {
return await run(context);
} finally {
await context.queryRunner.release();
}
}
async function insertReport(context: TestMigrationContext, status: string): Promise<string> {
const id = randomUUID();
await context.runQuery(
`INSERT INTO ${context.escape.tableName(TABLE)}
("id", "dataPoints", "status", "createdAt", "updatedAt")
VALUES (:id, '[]', :status, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
{ id, status },
);
return id;
}
async function statuses(context: TestMigrationContext): Promise<Record<string, string>> {
const rows = (await context.queryRunner.query(
`SELECT "id", "status" FROM ${context.escape.tableName(TABLE)}`,
)) as Array<{ id: string; status: string }>;
return Object.fromEntries(rows.map((row) => [row.id, row.status]));
}
describe('up', () => {
it('accepts a sending report after the migration and still rejects an unknown status', async () => {
await withContext(async (context) => {
await expect(insertReport(context, 'sending')).rejects.toThrow();
});
await runSingleMigration(MIGRATION_NAME);
await withContext(async (context) => {
await expect(insertReport(context, 'sending')).resolves.toBeDefined();
await expect(insertReport(context, 'unknown')).rejects.toThrow();
});
});
});
describe('down', () => {
it('resets sending reports to pending and rejects sending again', async () => {
await runSingleMigration(MIGRATION_NAME);
const ids = await withContext(async (context) => ({
sending: await insertReport(context, 'sending'),
pending: await insertReport(context, 'pending'),
delivered: await insertReport(context, 'delivered'),
skipped: await insertReport(context, 'skipped_after_max_retries'),
}));
await undoLastSingleMigration();
await withContext(async (context) => {
expect(await statuses(context)).toEqual({
[ids.sending]: 'pending',
[ids.pending]: 'pending',
[ids.delivered]: 'delivered',
[ids.skipped]: 'skipped_after_max_retries',
});
await expect(insertReport(context, 'sending')).rejects.toThrow();
});
});
});
});