feat: create job with data dto

This commit is contained in:
izzy
2026-01-12 12:32:55 +00:00
parent d4ad523eb3
commit b49ee6d90d
11 changed files with 323 additions and 3 deletions

View File

@@ -210,6 +210,7 @@ Class | Method | HTTP request | Description
*QueuesApi* | [**getQueue**](doc//QueuesApi.md#getqueue) | **GET** /queues/{name} | Retrieve a queue
*QueuesApi* | [**getQueueJobs**](doc//QueuesApi.md#getqueuejobs) | **GET** /queues/{name}/jobs | Retrieve queue jobs
*QueuesApi* | [**getQueues**](doc//QueuesApi.md#getqueues) | **GET** /queues | List all queues
*QueuesApi* | [**queueJob**](doc//QueuesApi.md#queuejob) | **POST** /queues/job | Create a manual job
*QueuesApi* | [**updateQueue**](doc//QueuesApi.md#updatequeue) | **PUT** /queues/{name} | Update a queue
*SearchApi* | [**getAssetsByCity**](doc//SearchApi.md#getassetsbycity) | **GET** /search/cities | Retrieve assets by city
*SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore | Retrieve explore data
@@ -495,6 +496,7 @@ Class | Method | HTTP request | Description
- [QueueCommand](doc//QueueCommand.md)
- [QueueCommandDto](doc//QueueCommandDto.md)
- [QueueDeleteDto](doc//QueueDeleteDto.md)
- [QueueJobCreateDto](doc//QueueJobCreateDto.md)
- [QueueJobResponseDto](doc//QueueJobResponseDto.md)
- [QueueJobStatus](doc//QueueJobStatus.md)
- [QueueName](doc//QueueName.md)

View File

@@ -241,6 +241,7 @@ part 'model/purchase_update.dart';
part 'model/queue_command.dart';
part 'model/queue_command_dto.dart';
part 'model/queue_delete_dto.dart';
part 'model/queue_job_create_dto.dart';
part 'model/queue_job_response_dto.dart';
part 'model/queue_job_status.dart';
part 'model/queue_name.dart';

View File

@@ -245,6 +245,54 @@ class QueuesApi {
return null;
}
/// Create a manual job
///
/// Run a specific job. Most jobs are queued automatically, but this endpoint allows for manual creation of a handful of jobs, including various cleanup tasks, as well as creating a new database backup.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [QueueJobCreateDto] queueJobCreateDto (required):
Future<Response> queueJobWithHttpInfo(QueueJobCreateDto queueJobCreateDto,) async {
// ignore: prefer_const_declarations
final apiPath = r'/queues/job';
// ignore: prefer_final_locals
Object? postBody = queueJobCreateDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
apiPath,
'POST',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Create a manual job
///
/// Run a specific job. Most jobs are queued automatically, but this endpoint allows for manual creation of a handful of jobs, including various cleanup tasks, as well as creating a new database backup.
///
/// Parameters:
///
/// * [QueueJobCreateDto] queueJobCreateDto (required):
Future<void> queueJob(QueueJobCreateDto queueJobCreateDto,) async {
final response = await queueJobWithHttpInfo(queueJobCreateDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
/// Update a queue
///
/// Change the paused status of a specific queue.

View File

@@ -530,6 +530,8 @@ class ApiClient {
return QueueCommandDto.fromJson(value);
case 'QueueDeleteDto':
return QueueDeleteDto.fromJson(value);
case 'QueueJobCreateDto':
return QueueJobCreateDto.fromJson(value);
case 'QueueJobResponseDto':
return QueueJobResponseDto.fromJson(value);
case 'QueueJobStatus':

View File

@@ -0,0 +1,99 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class QueueJobCreateDto {
/// Returns a new [QueueJobCreateDto] instance.
QueueJobCreateDto({
required this.job,
});
Object job;
@override
bool operator ==(Object other) => identical(this, other) || other is QueueJobCreateDto &&
other.job == job;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(job.hashCode);
@override
String toString() => 'QueueJobCreateDto[job=$job]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'job'] = this.job;
return json;
}
/// Returns a new [QueueJobCreateDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static QueueJobCreateDto? fromJson(dynamic value) {
upgradeDto(value, "QueueJobCreateDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return QueueJobCreateDto(
job: mapValueOfType<Object>(json, r'job')!,
);
}
return null;
}
static List<QueueJobCreateDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <QueueJobCreateDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = QueueJobCreateDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, QueueJobCreateDto> mapFromJson(dynamic json) {
final map = <String, QueueJobCreateDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = QueueJobCreateDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of QueueJobCreateDto-objects as value to a dart map
static Map<String, List<QueueJobCreateDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<QueueJobCreateDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = QueueJobCreateDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'job',
};
}

View File

@@ -8476,6 +8476,56 @@
"x-immich-state": "Alpha"
}
},
"/queues/job": {
"post": {
"description": "Run a specific job. Most jobs are queued automatically, but this endpoint allows for manual creation of a handful of jobs, including various cleanup tasks, as well as creating a new database backup.",
"operationId": "queueJob",
"parameters": [],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/QueueJobCreateDto"
}
}
},
"required": true
},
"responses": {
"204": {
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"summary": "Create a manual job",
"tags": [
"Queues"
],
"x-immich-admin-only": true,
"x-immich-history": [
{
"version": "v2.5.0",
"state": "Added"
},
{
"version": "v2.5.0",
"state": "Alpha"
}
],
"x-immich-permission": "job.create",
"x-immich-state": "Alpha"
}
},
"/queues/{name}": {
"get": {
"description": "Retrieves a specific queue by its name.",
@@ -19135,6 +19185,17 @@
},
"type": "object"
},
"QueueJobCreateDto": {
"properties": {
"job": {
"type": "object"
}
},
"required": [
"job"
],
"type": "object"
},
"QueueJobResponseDto": {
"properties": {
"data": {

View File

@@ -1038,6 +1038,9 @@ export type QueueResponseDto = {
name: QueueName;
statistics: QueueStatisticsDto;
};
export type QueueJobCreateDto = {
job: object;
};
export type QueueUpdateDto = {
isPaused?: boolean;
};
@@ -3834,6 +3837,18 @@ export function getQueues(opts?: Oazapfts.RequestOpts) {
...opts
}));
}
/**
* Create a manual job
*/
export function queueJob({ queueJobCreateDto }: {
queueJobCreateDto: QueueJobCreateDto;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchText("/queues/job", oazapfts.json({
...opts,
method: "POST",
body: queueJobCreateDto
})));
}
/**
* Retrieve a queue
*/

View File

@@ -1,9 +1,10 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Put, Query } from '@nestjs/common';
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Endpoint, HistoryBuilder } from 'src/decorators';
import { AuthDto } from 'src/dtos/auth.dto';
import {
QueueDeleteDto,
QueueJobCreateDto,
QueueJobResponseDto,
QueueJobSearchDto,
QueueNameParamDto,
@@ -19,6 +20,19 @@ import { QueueService } from 'src/services/queue.service';
export class QueueController {
constructor(private service: QueueService) {}
@Post('job')
@Authenticated({ permission: Permission.JobCreate, admin: true })
@HttpCode(HttpStatus.NO_CONTENT)
@Endpoint({
summary: 'Create a manual job',
description:
'Run a specific job. Most jobs are queued automatically, but this endpoint allows for manual creation of a handful of jobs, including various cleanup tasks, as well as creating a new database backup.',
history: new HistoryBuilder().added('v2.5.0').alpha('v2.5.0'),
})
queueJob(@Body() dto: QueueJobCreateDto): Promise<void> {
return this.service.createJob(dto);
}
@Get()
@Authenticated({ permission: Permission.QueueRead, admin: true })
@Endpoint({

View File

@@ -1,7 +1,75 @@
import { ApiProperty } from '@nestjs/swagger';
import { ClassConstructor, Transform, Type } from 'class-transformer';
import { Equals, IsBoolean, IsDefined, IsOptional, ValidateNested } from 'class-validator';
import { HistoryBuilder, Property } from 'src/decorators';
import { JobName, QueueCommand, QueueJobStatus, QueueName } from 'src/enum';
import { ValidateBoolean, ValidateEnum } from 'src/validation';
import { transformToOneOf, ValidateBoolean, ValidateEnum } from 'src/validation';
class BaseJobData {
@IsOptional()
@IsBoolean()
force?: boolean;
}
class BaseJob {
@ValidateNested()
@Type(() => BaseJobData)
data!: BaseJobData;
}
class JobTagCleanup extends BaseJob {
@Equals(JobName.TagCleanup)
name!: JobName.TagCleanup;
}
class JobPersonCleanup extends BaseJob {
@Equals(JobName.PersonCleanup)
name!: JobName.PersonCleanup;
}
class JobUserDeleteCheck extends BaseJob {
@Equals(JobName.UserDeleteCheck)
name!: JobName.UserDeleteCheck;
}
class JobMemoryCleanup extends BaseJob {
@Equals(JobName.MemoryCleanup)
name!: JobName.MemoryCleanup;
}
class JobMemoryGenerate extends BaseJob {
@Equals(JobName.MemoryGenerate)
name!: JobName.MemoryGenerate;
}
class JobDatabaseBackup extends BaseJob {
@Equals(JobName.DatabaseBackup)
name!: JobName.DatabaseBackup;
}
const JOB_MAP: Record<string, ClassConstructor<object>> = {
[JobName.TagCleanup]: JobTagCleanup,
[JobName.PersonCleanup]: JobPersonCleanup,
[JobName.UserDeleteCheck]: JobUserDeleteCheck,
[JobName.MemoryCleanup]: JobMemoryCleanup,
[JobName.MemoryGenerate]: JobMemoryGenerate,
[JobName.DatabaseBackup]: JobDatabaseBackup,
};
export class QueueJobCreateDto {
@ValidateNested()
@Transform(transformToOneOf(JOB_MAP))
@IsDefined({
message: `job.name must be one of ${Object.keys(JOB_MAP)}`,
})
job!:
| JobTagCleanup
| JobPersonCleanup
| JobUserDeleteCheck
| JobMemoryCleanup
| JobMemoryGenerate
| JobDatabaseBackup;
}
export class QueueNameParamDto {
@ValidateEnum({ enum: QueueName, name: 'QueueName' })

View File

@@ -12,6 +12,7 @@ import {
import {
QueueCommandDto,
QueueDeleteDto,
QueueJobCreateDto,
QueueJobResponseDto,
QueueJobSearchDto,
QueueResponseDto,
@@ -100,6 +101,10 @@ export class QueueService extends BaseService {
this.services = services;
}
createJob(dto: QueueJobCreateDto): Promise<void> {
return this.jobRepository.queue(dto.job);
}
async runCommandLegacy(name: QueueName, dto: QueueCommandDto): Promise<QueueResponseLegacyDto> {
this.logger.debug(`Handling command: queue=${name},command=${dto.command},force=${dto.force}`);

View File

@@ -7,7 +7,7 @@ import {
applyDecorators,
} from '@nestjs/common';
import { ApiProperty, ApiPropertyOptions } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { ClassConstructor, Transform, TransformFnParams, plainToInstance } from 'class-transformer';
import {
IsArray,
IsBoolean,
@@ -417,3 +417,8 @@ export function IsIPRange(options: IsIPRangeOptions, validationOptions?: Validat
validationOptions,
);
}
export const transformToOneOf =
<T extends Record<string, ClassConstructor<object>>>(map: T) =>
({ value }: TransformFnParams) =>
value && typeof value === 'object' && map[value.name] ? plainToInstance(map[value.name], value) : null;