mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
feat: endpoint versioning (#23858)
This commit is contained in:
+112
-30
@@ -1,8 +1,7 @@
|
||||
import { SetMetadata, applyDecorators } from '@nestjs/common';
|
||||
import { ApiExtension, ApiOperation, ApiOperationOptions, ApiProperty, ApiTags } from '@nestjs/swagger';
|
||||
import { ApiOperation, ApiOperationOptions, ApiProperty, ApiPropertyOptions, ApiTags } from '@nestjs/swagger';
|
||||
import _ from 'lodash';
|
||||
import { ADDED_IN_PREFIX, DEPRECATED_IN_PREFIX, LIFECYCLE_EXTENSION } from 'src/constants';
|
||||
import { ApiTag, ImmichWorker, JobName, MetadataKey, QueueName } from 'src/enum';
|
||||
import { ApiCustomExtension, ApiTag, ImmichWorker, JobName, MetadataKey, QueueName } from 'src/enum';
|
||||
import { EmitEvent } from 'src/repositories/event.repository';
|
||||
import { immich_uuid_v7, updated_at } from 'src/schema/functions';
|
||||
import { BeforeUpdateTrigger, Column, ColumnOptions } from 'src/sql-tools';
|
||||
@@ -153,39 +152,122 @@ export type JobConfig = {
|
||||
};
|
||||
export const OnJob = (config: JobConfig) => SetMetadata(MetadataKey.JobConfig, config);
|
||||
|
||||
type LifecycleRelease = 'NEXT_RELEASE' | string;
|
||||
type LifecycleMetadata = {
|
||||
addedAt?: LifecycleRelease;
|
||||
deprecatedAt?: LifecycleRelease;
|
||||
};
|
||||
type EndpointOptions = ApiOperationOptions & { history?: HistoryBuilder };
|
||||
export const Endpoint = ({ history, ...options }: EndpointOptions) => {
|
||||
const decorators: MethodDecorator[] = [];
|
||||
const extensions = history?.getExtensions() ?? {};
|
||||
|
||||
export const EndpointLifecycle = ({
|
||||
addedAt,
|
||||
deprecatedAt,
|
||||
description,
|
||||
...options
|
||||
}: LifecycleMetadata & ApiOperationOptions) => {
|
||||
const decorators: MethodDecorator[] = [ApiExtension(LIFECYCLE_EXTENSION, { addedAt, deprecatedAt })];
|
||||
if (deprecatedAt) {
|
||||
decorators.push(
|
||||
ApiTags(ApiTag.Deprecated),
|
||||
ApiOperation({
|
||||
deprecated: true,
|
||||
description: DEPRECATED_IN_PREFIX + deprecatedAt + (description ? `. ${description}` : ''),
|
||||
...options,
|
||||
}),
|
||||
);
|
||||
if (!extensions[ApiCustomExtension.History]) {
|
||||
console.log(`Missing history for endpoint: ${options.summary}`);
|
||||
}
|
||||
|
||||
if (history?.isDeprecated()) {
|
||||
options.deprecated = true;
|
||||
decorators.push(ApiTags(ApiTag.Deprecated));
|
||||
}
|
||||
|
||||
decorators.push(ApiOperation({ ...options, ...extensions }));
|
||||
|
||||
return applyDecorators(...decorators);
|
||||
};
|
||||
|
||||
export const PropertyLifecycle = ({ addedAt, deprecatedAt }: LifecycleMetadata) => {
|
||||
const decorators: PropertyDecorator[] = [];
|
||||
decorators.push(ApiProperty({ description: ADDED_IN_PREFIX + addedAt }));
|
||||
if (deprecatedAt) {
|
||||
decorators.push(ApiProperty({ deprecated: true, description: DEPRECATED_IN_PREFIX + deprecatedAt }));
|
||||
type PropertyOptions = ApiPropertyOptions & { history?: HistoryBuilder };
|
||||
export const Property = ({ history, ...options }: PropertyOptions) => {
|
||||
const extensions = history?.getExtensions() ?? {};
|
||||
|
||||
if (history?.isDeprecated()) {
|
||||
options.deprecated = true;
|
||||
}
|
||||
|
||||
return applyDecorators(...decorators);
|
||||
return ApiProperty({ ...options, ...extensions });
|
||||
};
|
||||
|
||||
type HistoryEntry = {
|
||||
version: string;
|
||||
state: ApiState | 'Added' | 'Updated';
|
||||
description?: string;
|
||||
replacementId?: string;
|
||||
};
|
||||
|
||||
type DeprecatedOptions = {
|
||||
/** replacement operationId */
|
||||
replacementId?: string;
|
||||
};
|
||||
|
||||
type CustomExtensions = {
|
||||
[ApiCustomExtension.State]?: ApiState;
|
||||
[ApiCustomExtension.History]?: HistoryEntry[];
|
||||
};
|
||||
|
||||
enum ApiState {
|
||||
'Stable' = 'Stable',
|
||||
'Alpha' = 'Alpha',
|
||||
'Beta' = 'Beta',
|
||||
'Internal' = 'Internal',
|
||||
'Deprecated' = 'Deprecated',
|
||||
}
|
||||
export class HistoryBuilder {
|
||||
private hasDeprecated = false;
|
||||
private items: HistoryEntry[] = [];
|
||||
|
||||
added(version: string, description?: string) {
|
||||
return this.push({ version, state: 'Added', description });
|
||||
}
|
||||
|
||||
updated(version: string, description: string) {
|
||||
return this.push({ version, state: 'Updated', description });
|
||||
}
|
||||
|
||||
alpha(version: string) {
|
||||
return this.push({ version, state: ApiState.Alpha });
|
||||
}
|
||||
|
||||
beta(version: string) {
|
||||
return this.push({ version, state: ApiState.Beta });
|
||||
}
|
||||
|
||||
internal(version: string) {
|
||||
return this.push({ version, state: ApiState.Internal });
|
||||
}
|
||||
|
||||
stable(version: string) {
|
||||
return this.push({ version, state: ApiState.Stable });
|
||||
}
|
||||
|
||||
deprecated(version: string, options?: DeprecatedOptions) {
|
||||
const { replacementId } = options || {};
|
||||
this.hasDeprecated = true;
|
||||
return this.push({ version, state: ApiState.Deprecated, replacementId });
|
||||
}
|
||||
|
||||
isDeprecated(): boolean {
|
||||
return this.hasDeprecated;
|
||||
}
|
||||
|
||||
getExtensions() {
|
||||
const extensions: CustomExtensions = {};
|
||||
|
||||
if (this.items.length > 0) {
|
||||
extensions[ApiCustomExtension.History] = this.items;
|
||||
}
|
||||
|
||||
for (const item of this.items.toReversed()) {
|
||||
if (item.state === 'Added' || item.state === 'Updated') {
|
||||
continue;
|
||||
}
|
||||
|
||||
extensions[ApiCustomExtension.State] = item.state;
|
||||
break;
|
||||
}
|
||||
|
||||
return extensions;
|
||||
}
|
||||
|
||||
private push(item: HistoryEntry) {
|
||||
if (!item.version.startsWith('v')) {
|
||||
throw new Error(`Version string must start with 'v': received '${JSON.stringify(item)}'`);
|
||||
}
|
||||
this.items.push(item);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user