feat: 增加QQBot插件定时任务持久化接口

This commit is contained in:
sunlei 2026-06-17 09:19:12 +08:00
parent 250bb654f0
commit 7775fd580d
13 changed files with 619 additions and 2 deletions

View File

@ -875,6 +875,53 @@ CREATE TABLE IF NOT EXISTS qqbot_plugin_runtime_event (
KEY idx_qqbot_plugin_runtime_event_plugin (plugin_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS qqbot_plugin_task (
id BIGINT NOT NULL PRIMARY KEY,
plugin_id BIGINT NOT NULL,
installation_id BIGINT NOT NULL,
task_key VARCHAR(128) NOT NULL,
task_name VARCHAR(128) NOT NULL,
handler_name VARCHAR(128) NOT NULL,
description TEXT NULL,
default_cron VARCHAR(64) NOT NULL,
cron_expression VARCHAR(64) NOT NULL,
enabled TINYINT NOT NULL DEFAULT 1,
timeout_ms INT NOT NULL,
runtime_status VARCHAR(32) NOT NULL DEFAULT 'idle',
last_run_id BIGINT NULL,
last_run_at DATETIME NULL,
last_status VARCHAR(32) NULL,
last_error TEXT NULL,
last_duration_ms INT NULL,
next_run_at DATETIME NULL,
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_qqbot_plugin_task (installation_id, task_key),
KEY idx_qqbot_plugin_task_plugin (plugin_id),
KEY idx_qqbot_plugin_task_enabled (enabled),
KEY idx_qqbot_plugin_task_status (runtime_status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS qqbot_plugin_task_run (
id BIGINT NOT NULL PRIMARY KEY,
task_id BIGINT NOT NULL,
plugin_id BIGINT NOT NULL,
installation_id BIGINT NOT NULL,
task_key VARCHAR(128) NOT NULL,
trigger_type VARCHAR(32) NOT NULL,
status VARCHAR(32) NOT NULL,
job_id VARCHAR(191) NULL,
started_at DATETIME NULL,
finished_at DATETIME NULL,
duration_ms INT NULL,
safe_summary JSON NULL,
error_message TEXT NULL,
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY idx_qqbot_plugin_task_run_task_time (task_id, create_time),
KEY idx_qqbot_plugin_task_run_plugin_time (plugin_id, create_time),
KEY idx_qqbot_plugin_task_run_status_time (status, create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS napcat_container (
id BIGINT NOT NULL PRIMARY KEY,
account_id BIGINT NULL,

View File

@ -11,6 +11,8 @@ SELECT 'napcat_account_binding' AS table_name, COUNT(*) AS row_count FROM napcat
SELECT 'napcat_login_session' AS table_name, COUNT(*) AS row_count FROM napcat_login_session;
SELECT 'napcat_login_challenge' AS table_name, COUNT(*) AS row_count FROM napcat_login_challenge;
SELECT 'napcat_runtime_cleanup' AS table_name, COUNT(*) AS row_count FROM napcat_runtime_cleanup;
SELECT 'qqbot_plugin_task' AS table_name, COUNT(*) AS row_count FROM qqbot_plugin_task;
SELECT 'qqbot_plugin_task_run' AS table_name, COUNT(*) AS row_count FROM qqbot_plugin_task_run;
SELECT 'seed_admin_user' AS check_name, COUNT(*) AS matched_rows
FROM admin_user

View File

@ -1,2 +1,4 @@
export * from './qqbot-plugin-task-cron.validator';
export * from './qqbot-plugin-task-manifest.synchronizer';
export * from './qqbot-plugin-task.service';
export * from './qqbot-plugin-task.types';

View File

@ -0,0 +1,49 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import type { QqbotPluginTaskManifest } from '../../domain/manifest';
import { QqbotPluginTask } from '../../infrastructure/persistence';
export type SyncPluginManifestTasksInput = {
installationId: string;
manifestTasks: QqbotPluginTaskManifest[];
pluginId: string;
};
@Injectable()
export class QqbotPluginTaskManifestSynchronizer {
constructor(
@InjectRepository(QqbotPluginTask)
private readonly taskRepository: Repository<QqbotPluginTask>,
) {}
async syncManifestTasks(input: SyncPluginManifestTasksInput) {
const tasks: QqbotPluginTask[] = [];
for (const manifestTask of input.manifestTasks) {
const existing = await this.taskRepository.findOne({
where: {
installationId: input.installationId,
taskKey: manifestTask.key,
},
});
const task = this.taskRepository.create({
...(existing || {}),
cronExpression: existing?.cronExpression || manifestTask.defaultCron,
defaultCron: manifestTask.defaultCron,
description: manifestTask.description || null,
enabled: existing?.enabled ?? manifestTask.enabled,
handlerName: manifestTask.handlerName,
installationId: input.installationId,
pluginId: input.pluginId,
runtimeStatus:
existing?.runtimeStatus ||
(manifestTask.enabled ? 'scheduled' : 'disabled'),
taskKey: manifestTask.key,
taskName: manifestTask.name,
timeoutMs: manifestTask.timeoutMs,
});
tasks.push(await this.taskRepository.save(task));
}
return tasks;
}
}

View File

@ -0,0 +1,138 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import {
Between,
FindOptionsWhere,
LessThanOrEqual,
MoreThanOrEqual,
Repository,
} from 'typeorm';
import { throwVbenError, ToolsService } from '@/common';
import {
QqbotPlugin,
QqbotPluginTask,
QqbotPluginTaskRun,
} from '../../infrastructure/persistence';
import { requireQqbotPluginTaskCron } from './qqbot-plugin-task-cron.validator';
import type {
QqbotPluginTaskPageQuery,
QqbotPluginTaskRunPageQuery,
} from './qqbot-plugin-task.types';
@Injectable()
export class QqbotPluginTaskService {
constructor(
@InjectRepository(QqbotPluginTask)
private readonly taskRepository: Repository<QqbotPluginTask>,
@InjectRepository(QqbotPluginTaskRun)
private readonly runRepository: Repository<QqbotPluginTaskRun>,
@InjectRepository(QqbotPlugin)
private readonly pluginRepository: Repository<QqbotPlugin>,
private readonly toolsService: ToolsService,
) {}
async pageTasks(query: QqbotPluginTaskPageQuery) {
const { pageNo, pageSize, skip } = this.toolsService.getPageParams(query);
const where: FindOptionsWhere<QqbotPluginTask> = {};
const pluginId = await this.resolvePluginIdFilter(query);
if (pluginId) where.pluginId = pluginId;
if (query.taskKey) where.taskKey = query.taskKey;
if (query.status) where.runtimeStatus = query.status;
if (query.enabled !== undefined) {
where.enabled = this.toolsService.normalizeBoolean(query.enabled);
}
const [list, total] = await this.taskRepository.findAndCount({
order: { createTime: 'DESC' },
skip,
take: pageSize,
where,
});
return { list, pageNo, pageSize, total };
}
async getTaskDetail(id: string) {
const task = await this.taskRepository.findOne({ where: { id } });
if (!task) throwVbenError('插件定时任务不存在');
return task;
}
async enableTask(id: string) {
await this.taskRepository.update(
{ id },
{ enabled: true, runtimeStatus: 'scheduled' },
);
return this.getTaskDetail(id);
}
async disableTask(id: string) {
await this.taskRepository.update(
{ id },
{ enabled: false, runtimeStatus: 'disabled' },
);
return this.getTaskDetail(id);
}
async updateTaskCron(id: string, body: { cronExpression?: string }) {
const cronExpression = requireQqbotPluginTaskCron(body.cronExpression);
await this.taskRepository.update({ id }, { cronExpression });
return this.getTaskDetail(id);
}
async runTaskOnce(id: string, body: { input?: Record<string, unknown> }) {
void body;
const task = await this.getTaskDetail(id);
const run = await this.runRepository.save({
installationId: task.installationId,
pluginId: task.pluginId,
status: 'running',
taskId: task.id,
taskKey: task.taskKey,
triggerType: 'manual',
});
return { jobId: run.jobId || `${run.id || ''}`, taskId: task.id };
}
async pageTaskRuns(id: string, query: QqbotPluginTaskRunPageQuery) {
const { pageNo, pageSize, skip } = this.toolsService.getPageParams(query);
const where: FindOptionsWhere<QqbotPluginTaskRun> = { taskId: id };
if (query.status) where.status = query.status;
if (query.triggerType) where.triggerType = query.triggerType;
Object.assign(where, this.buildRunTimeFilter(query));
const [list, total] = await this.runRepository.findAndCount({
order: { createTime: 'DESC' },
skip,
take: pageSize,
where,
});
return { list, pageNo, pageSize, total };
}
private async resolvePluginIdFilter(query: QqbotPluginTaskPageQuery) {
if (query.pluginId) return query.pluginId;
if (!query.pluginKey) return undefined;
const plugin = await this.pluginRepository.findOne({
where: { pluginKey: query.pluginKey },
});
return plugin?.id || '__missing_plugin__';
}
private buildRunTimeFilter(query: QqbotPluginTaskRunPageQuery) {
if (query.startTime && query.endTime) {
return {
createTime: Between(query.startTime, query.endTime),
};
}
if (query.startTime) {
return {
createTime: MoreThanOrEqual(query.startTime),
};
}
if (query.endTime) {
return {
createTime: LessThanOrEqual(query.endTime),
};
}
return {};
}
}

View File

@ -0,0 +1,74 @@
import {
Body,
Controller,
Get,
HttpCode,
HttpStatus,
Param,
Post,
Query,
UseGuards,
} from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { vbenSuccess } from '@/common';
import { JwtAuthGuard } from '@/modules/admin/identity/auth/jwt-auth.guard';
import { QqbotPluginTaskService } from '../application/task';
@ApiTags('QQBot - 插件定时任务')
@Controller('qqbot/plugin-platform/tasks')
@UseGuards(JwtAuthGuard)
export class QqbotPluginPlatformTaskController {
constructor(private readonly service: QqbotPluginTaskService) {}
@Get('page')
@ApiOperation({ summary: '插件定时任务分页' })
async page(@Query() query: Record<string, unknown>) {
return vbenSuccess(await this.service.pageTasks(query));
}
@Get(':id')
@ApiOperation({ summary: '插件定时任务详情' })
async detail(@Param('id') id: string) {
return vbenSuccess(await this.service.getTaskDetail(id));
}
@Post(':id/enable')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '启用插件定时任务' })
async enable(@Param('id') id: string) {
return vbenSuccess(await this.service.enableTask(id));
}
@Post(':id/disable')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '停用插件定时任务' })
async disable(@Param('id') id: string) {
return vbenSuccess(await this.service.disableTask(id));
}
@Post(':id/cron')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '更新插件定时任务 cron' })
async updateCron(
@Param('id') id: string,
@Body() body: { cronExpression?: string },
) {
return vbenSuccess(await this.service.updateTaskCron(id, body));
}
@Post(':id/run')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '手动运行插件定时任务' })
async run(
@Param('id') id: string,
@Body() body: { input?: Record<string, unknown> },
) {
return vbenSuccess(await this.service.runTaskOnce(id, body));
}
@Get(':id/runs')
@ApiOperation({ summary: '插件定时任务运行记录分页' })
async runs(@Param('id') id: string, @Query() query: Record<string, unknown>) {
return vbenSuccess(await this.service.pageTaskRuns(id, query));
}
}

View File

@ -4,6 +4,7 @@ export const QQBOT_PLUGIN_PLATFORM_DOMAIN_CONTRACT = {
installLocal: '/qqbot/plugin-platform/install-local',
installations: '/qqbot/plugin-platform/installations',
runtimeEvents: '/qqbot/plugin-platform/runtime-events',
tasks: '/qqbot/plugin-platform/tasks',
validate: '/qqbot/plugin-platform/validate',
},
tables: [
@ -16,5 +17,7 @@ export const QQBOT_PLUGIN_PLATFORM_DOMAIN_CONTRACT = {
'qqbot_plugin_config',
'qqbot_plugin_asset',
'qqbot_plugin_runtime_event',
'qqbot_plugin_task',
'qqbot_plugin_task_run',
],
} as const;

View File

@ -3,6 +3,7 @@ import {
ensureSnowflakeId,
KtCreateDateColumn,
KtDateTime,
KtDateTimeColumn,
KtUpdateDateColumn,
} from '@/common';
@ -25,6 +26,21 @@ export type QqbotPluginRuntimeStatus =
export type QqbotPluginRuntimeEventLevel = 'error' | 'info' | 'warn';
export type QqbotPluginTaskRuntimeStatus =
| 'disabled'
| 'failed'
| 'idle'
| 'running'
| 'scheduled';
export type QqbotPluginTaskRunStatus =
| 'failed'
| 'running'
| 'skipped'
| 'success';
export type QqbotPluginTaskTriggerType = 'bootstrap' | 'manual' | 'schedule';
@Entity('qqbot_plugin')
@Index('uk_qqbot_plugin_key', ['pluginKey'], { unique: true })
export class QqbotPlugin {
@ -291,6 +307,133 @@ export class QqbotPluginRuntimeEvent {
}
}
@Entity('qqbot_plugin_task')
@Index('uk_qqbot_plugin_task', ['installationId', 'taskKey'], {
unique: true,
})
@Index('idx_qqbot_plugin_task_plugin', ['pluginId'])
@Index('idx_qqbot_plugin_task_enabled', ['enabled'])
@Index('idx_qqbot_plugin_task_status', ['runtimeStatus'])
export class QqbotPluginTask {
@PrimaryColumn({ type: 'bigint' })
id: string;
@Column({ name: 'plugin_id', type: 'bigint' })
pluginId: string;
@Column({ name: 'installation_id', type: 'bigint' })
installationId: string;
@Column({ length: 128, name: 'task_key' })
taskKey: string;
@Column({ length: 128, name: 'task_name' })
taskName: string;
@Column({ length: 128, name: 'handler_name' })
handlerName: string;
@Column({ name: 'description', nullable: true, type: 'text' })
description: null | string;
@Column({ length: 64, name: 'default_cron' })
defaultCron: string;
@Column({ length: 64, name: 'cron_expression' })
cronExpression: string;
@Column({ default: true })
enabled: boolean;
@Column({ name: 'timeout_ms', type: 'int' })
timeoutMs: number;
@Column({ length: 32, name: 'runtime_status' })
runtimeStatus: QqbotPluginTaskRuntimeStatus;
@Column({ name: 'last_run_id', nullable: true, type: 'bigint' })
lastRunId: null | string;
@KtDateTimeColumn({ name: 'last_run_at', nullable: true })
lastRunAt: null | KtDateTime;
@Column({ length: 32, name: 'last_status', nullable: true })
lastStatus: null | QqbotPluginTaskRunStatus;
@Column({ name: 'last_error', nullable: true, type: 'text' })
lastError: null | string;
@Column({ name: 'last_duration_ms', nullable: true, type: 'int' })
lastDurationMs: null | number;
@KtDateTimeColumn({ name: 'next_run_at', nullable: true })
nextRunAt: null | KtDateTime;
@KtCreateDateColumn({ name: 'create_time' })
createTime: KtDateTime;
@KtUpdateDateColumn({ name: 'update_time' })
updateTime: KtDateTime;
@BeforeInsert()
createId() {
ensureSnowflakeId(this);
}
}
@Entity('qqbot_plugin_task_run')
@Index('idx_qqbot_plugin_task_run_task_time', ['taskId', 'createTime'])
@Index('idx_qqbot_plugin_task_run_plugin_time', ['pluginId', 'createTime'])
@Index('idx_qqbot_plugin_task_run_status_time', ['status', 'createTime'])
export class QqbotPluginTaskRun {
@PrimaryColumn({ type: 'bigint' })
id: string;
@Column({ name: 'task_id', type: 'bigint' })
taskId: string;
@Column({ name: 'plugin_id', type: 'bigint' })
pluginId: string;
@Column({ name: 'installation_id', type: 'bigint' })
installationId: string;
@Column({ length: 128, name: 'task_key' })
taskKey: string;
@Column({ length: 32, name: 'trigger_type' })
triggerType: QqbotPluginTaskTriggerType;
@Column({ length: 32 })
status: QqbotPluginTaskRunStatus;
@Column({ length: 191, name: 'job_id', nullable: true })
jobId: null | string;
@KtDateTimeColumn({ name: 'started_at', nullable: true })
startedAt: null | KtDateTime;
@KtDateTimeColumn({ name: 'finished_at', nullable: true })
finishedAt: null | KtDateTime;
@Column({ name: 'duration_ms', nullable: true, type: 'int' })
durationMs: null | number;
@Column({ name: 'safe_summary', nullable: true, type: 'simple-json' })
safeSummary: null | Record<string, unknown>;
@Column({ name: 'error_message', nullable: true, type: 'text' })
errorMessage: null | string;
@KtCreateDateColumn({ name: 'create_time' })
createTime: KtDateTime;
@BeforeInsert()
createId() {
ensureSnowflakeId(this);
}
}
export const QQBOT_PLUGIN_PLATFORM_ENTITIES = [
QqbotPlugin,
QqbotPluginVersion,
@ -301,4 +444,6 @@ export const QQBOT_PLUGIN_PLATFORM_ENTITIES = [
QqbotPluginConfig,
QqbotPluginAsset,
QqbotPluginRuntimeEvent,
QqbotPluginTask,
QqbotPluginTaskRun,
] as const;

View File

@ -11,6 +11,11 @@ import { QqbotEventPluginRegistryService } from './application/registry/qqbot-ev
import { QqbotPluginRegistryService } from './application/registry/qqbot-plugin-registry.service';
import { QqbotPluginExecutionAdapter } from './application/plugin-execution.adapter';
import { QqbotPluginPlatformService } from './application/plugin-platform.service';
import {
QqbotPluginTaskManifestSynchronizer,
QqbotPluginTaskService,
} from './application/task';
import { QqbotPluginPlatformTaskController } from './contract/plugin-platform-task.controller';
import { QqbotPluginPlatformController } from './contract/plugin-platform.controller';
import { QqbotPluginController } from './contract/qqbot-plugin.controller';
import { QQBOT_PLUGIN_PLATFORM_ENTITIES } from './infrastructure/persistence';
@ -25,7 +30,11 @@ import {
import { QQBOT_PLUGIN_RUNTIME_FACTORY } from './application/plugin-platform.service';
@Module({
controllers: [QqbotPluginController, QqbotPluginPlatformController],
controllers: [
QqbotPluginController,
QqbotPluginPlatformController,
QqbotPluginPlatformTaskController,
],
exports: [
QQBOT_PLUGIN_EXECUTION_PORT,
QqbotPluginHttpClientService,
@ -53,6 +62,8 @@ import { QQBOT_PLUGIN_RUNTIME_FACTORY } from './application/plugin-platform.serv
QqbotBuiltinPluginPackageLoaderService,
QqbotBuiltinPluginWorkerRuntimeFactoryService,
QqbotPluginPackageReaderService,
QqbotPluginTaskManifestSynchronizer,
QqbotPluginTaskService,
{
provide: QQBOT_PLUGIN_EXECUTION_PORT,
useExisting: QqbotPluginExecutionAdapter,

View File

@ -33,6 +33,8 @@ describe('QQBot plugin platform persistence contract', () => {
'qqbot_plugin_config',
'qqbot_plugin_asset',
'qqbot_plugin_runtime_event',
'qqbot_plugin_task',
'qqbot_plugin_task_run',
]);
for (const table of QQBOT_PLUGIN_PLATFORM_DOMAIN_CONTRACT.tables) {
@ -52,6 +54,8 @@ describe('QQBot plugin platform persistence contract', () => {
'QqbotPluginConfig',
'QqbotPluginAsset',
'QqbotPluginRuntimeEvent',
'QqbotPluginTask',
'QqbotPluginTaskRun',
],
);

View File

@ -273,7 +273,7 @@ describe('QQBot plugin platform API contract', () => {
it('keeps TypeORM entity registration aligned with the persistence contract', () => {
expect(QqbotPluginPlatformModule).toBeDefined();
expect(QQBOT_PLUGIN_PLATFORM_ENTITIES).toHaveLength(9);
expect(QQBOT_PLUGIN_PLATFORM_ENTITIES).toHaveLength(11);
});
it('passes runtime-event filters to persistence', async () => {

View File

@ -0,0 +1,105 @@
import { QqbotPluginPlatformTaskController } from '../../../../src/modules/qqbot/plugin-platform/contract/plugin-platform-task.controller';
import { ToolsService } from '../../../../src/common';
import { QqbotPluginTaskService } from '../../../../src/modules/qqbot/plugin-platform/application/task';
import {
collectControllerRoutes,
routeKey,
} from '../../../helpers/controller-route.helper';
const createRepositoryMock = () => ({
findAndCount: jest.fn(async () => [[], 0]),
findOne: jest.fn(async () => null),
save: jest.fn(async (value) => value),
update: jest.fn(async () => ({ affected: 1 })),
});
describe('QQBot plugin task API contract', () => {
it('exposes task management routes under plugin-platform ownership', () => {
expect(
collectControllerRoutes([QqbotPluginPlatformTaskController]).map(
routeKey,
),
).toEqual(
expect.arrayContaining([
'GET /qqbot/plugin-platform/tasks/page',
'GET /qqbot/plugin-platform/tasks/:id',
'POST /qqbot/plugin-platform/tasks/:id/enable',
'POST /qqbot/plugin-platform/tasks/:id/disable',
'POST /qqbot/plugin-platform/tasks/:id/cron',
'POST /qqbot/plugin-platform/tasks/:id/run',
'GET /qqbot/plugin-platform/tasks/:id/runs',
]),
);
});
it('applies task page filters and safe pagination defaults', async () => {
const taskRepository = createRepositoryMock();
const runRepository = createRepositoryMock();
const pluginRepository = createRepositoryMock();
pluginRepository.findOne.mockResolvedValue({ id: 'plugin-1' });
const service = new QqbotPluginTaskService(
taskRepository as any,
runRepository as any,
pluginRepository as any,
new ToolsService(),
);
const page = await service.pageTasks({
enabled: 'true',
pageNo: 'bad',
pageSize: 'also-bad',
pluginKey: 'bangdream',
status: 'scheduled',
taskKey: 'bangdream.bestdori.sync-main-data',
});
expect(pluginRepository.findOne).toHaveBeenCalledWith({
where: { pluginKey: 'bangdream' },
});
expect(taskRepository.findAndCount).toHaveBeenCalledWith({
order: { createTime: 'DESC' },
skip: 0,
take: 10,
where: {
enabled: true,
pluginId: 'plugin-1',
runtimeStatus: 'scheduled',
taskKey: 'bangdream.bestdori.sync-main-data',
},
});
expect(page).toMatchObject({ pageNo: 1, pageSize: 10, total: 0 });
});
it('applies task run status, trigger, and time-range filters', async () => {
const taskRepository = createRepositoryMock();
const runRepository = createRepositoryMock();
const pluginRepository = createRepositoryMock();
const service = new QqbotPluginTaskService(
taskRepository as any,
runRepository as any,
pluginRepository as any,
new ToolsService(),
);
await service.pageTaskRuns('task-1', {
endTime: '2026-06-17 23:59:59',
pageNo: 2,
pageSize: 20,
startTime: '2026-06-17 00:00:00',
status: 'success',
triggerType: 'schedule',
});
expect(runRepository.findAndCount).toHaveBeenCalledWith({
order: { createTime: 'DESC' },
skip: 20,
take: 20,
where: {
createTime: expect.any(Object),
status: 'success',
taskId: 'task-1',
triggerType: 'schedule',
},
});
});
});

View File

@ -0,0 +1,37 @@
import { getMetadataArgsStorage } from 'typeorm';
import {
QQBOT_PLUGIN_PLATFORM_DOMAIN_CONTRACT,
QQBOT_PLUGIN_PLATFORM_ENTITIES,
QqbotPluginTask,
QqbotPluginTaskRun,
} from '../../../../src/modules/qqbot/plugin-platform/infrastructure/persistence';
import { readRefactorV3SqlSchema } from '../../../helpers/sql-schema.helper';
describe('QQBot plugin task persistence contract', () => {
const schema = readRefactorV3SqlSchema();
it('declares task tables in SQL and entity registry', () => {
expect(QQBOT_PLUGIN_PLATFORM_DOMAIN_CONTRACT.tables).toEqual(
expect.arrayContaining(['qqbot_plugin_task', 'qqbot_plugin_task_run']),
);
expect(QQBOT_PLUGIN_PLATFORM_ENTITIES).toEqual(
expect.arrayContaining([QqbotPluginTask, QqbotPluginTaskRun]),
);
expect(schema.hasTable('qqbot_plugin_task')).toBe(true);
expect(schema.hasTable('qqbot_plugin_task_run')).toBe(true);
});
it('maps task entity columns to SQL schema', () => {
for (const entity of [QqbotPluginTask, QqbotPluginTaskRun]) {
const tableName = getMetadataArgsStorage().tables.find(
(table) => table.target === entity,
)?.name;
const columns = getMetadataArgsStorage()
.columns.filter((column) => column.target === entity)
.map((column) => `${column.options.name || column.propertyName}`);
expect(tableName).toBeTruthy();
schema.expectTableColumns(tableName || '', columns);
}
});
});