mirror of
https://github.com/immich-app/immich.git
synced 2024-12-28 22:51:59 +00:00
chore: lifecycle metadata (#9103)
feat(server): track endpoint lifecycle
This commit is contained in:
parent
6eb5d2e95e
commit
59caf1fce4
22 changed files with 156 additions and 10 deletions
BIN
mobile/openapi/doc/AddUsersDto.md
generated
BIN
mobile/openapi/doc/AddUsersDto.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/AlbumResponseDto.md
generated
BIN
mobile/openapi/doc/AlbumResponseDto.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/MemoryLaneResponseDto.md
generated
BIN
mobile/openapi/doc/MemoryLaneResponseDto.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/MetadataSearchDto.md
generated
BIN
mobile/openapi/doc/MetadataSearchDto.md
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/add_users_dto.dart
generated
BIN
mobile/openapi/lib/model/add_users_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/album_response_dto.dart
generated
BIN
mobile/openapi/lib/model/album_response_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/memory_lane_response_dto.dart
generated
BIN
mobile/openapi/lib/model/memory_lane_response_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/metadata_search_dto.dart
generated
BIN
mobile/openapi/lib/model/metadata_search_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/add_users_dto_test.dart
generated
BIN
mobile/openapi/test/add_users_dto_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/album_response_dto_test.dart
generated
BIN
mobile/openapi/test/album_response_dto_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/memory_lane_response_dto_test.dart
generated
BIN
mobile/openapi/test/memory_lane_response_dto_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/metadata_search_dto_test.dart
generated
BIN
mobile/openapi/test/metadata_search_dto_test.dart
generated
Binary file not shown.
|
@ -6616,7 +6616,7 @@
|
|||
},
|
||||
"sharedUserIds": {
|
||||
"deprecated": true,
|
||||
"description": "Deprecated in favor of albumUsers",
|
||||
"description": "This property was deprecated in v1.102.0",
|
||||
"items": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
|
@ -6721,7 +6721,7 @@
|
|||
},
|
||||
"sharedUsers": {
|
||||
"deprecated": true,
|
||||
"description": "Deprecated in favor of albumUsers",
|
||||
"description": "This property was deprecated in v1.102.0",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/UserResponseDto"
|
||||
},
|
||||
|
@ -8433,6 +8433,7 @@
|
|||
},
|
||||
"title": {
|
||||
"deprecated": true,
|
||||
"description": "This property was deprecated in v1.100.0",
|
||||
"type": "string"
|
||||
},
|
||||
"yearsAgo": {
|
||||
|
@ -8640,6 +8641,7 @@
|
|||
},
|
||||
"resizePath": {
|
||||
"deprecated": true,
|
||||
"description": "This property was deprecated in v1.100.0",
|
||||
"type": "string"
|
||||
},
|
||||
"size": {
|
||||
|
@ -8682,6 +8684,7 @@
|
|||
},
|
||||
"webpPath": {
|
||||
"deprecated": true,
|
||||
"description": "This property was deprecated in v1.100.0",
|
||||
"type": "string"
|
||||
},
|
||||
"withArchived": {
|
||||
|
|
|
@ -162,7 +162,7 @@ export type AlbumResponseDto = {
|
|||
owner: UserResponseDto;
|
||||
ownerId: string;
|
||||
shared: boolean;
|
||||
/** Deprecated in favor of albumUsers */
|
||||
/** This property was deprecated in v1.102.0 */
|
||||
sharedUsers: UserResponseDto[];
|
||||
startDate?: string;
|
||||
updatedAt: string;
|
||||
|
@ -202,7 +202,7 @@ export type AlbumUserAddDto = {
|
|||
};
|
||||
export type AddUsersDto = {
|
||||
albumUsers: AlbumUserAddDto[];
|
||||
/** Deprecated in favor of albumUsers */
|
||||
/** This property was deprecated in v1.102.0 */
|
||||
sharedUserIds?: string[];
|
||||
};
|
||||
export type ApiKeyResponseDto = {
|
||||
|
@ -273,6 +273,7 @@ export type MapMarkerResponseDto = {
|
|||
};
|
||||
export type MemoryLaneResponseDto = {
|
||||
assets: AssetResponseDto[];
|
||||
/** This property was deprecated in v1.100.0 */
|
||||
title: string;
|
||||
yearsAgo: number;
|
||||
};
|
||||
|
@ -637,6 +638,7 @@ export type MetadataSearchDto = {
|
|||
page?: number;
|
||||
personIds?: string[];
|
||||
previewPath?: string;
|
||||
/** This property was deprecated in v1.100.0 */
|
||||
resizePath?: string;
|
||||
size?: number;
|
||||
state?: string;
|
||||
|
@ -648,6 +650,7 @@ export type MetadataSearchDto = {
|
|||
"type"?: AssetTypeEnum;
|
||||
updatedAfter?: string;
|
||||
updatedBefore?: string;
|
||||
/** This property was deprecated in v1.100.0 */
|
||||
webpPath?: string;
|
||||
withArchived?: boolean;
|
||||
withDeleted?: boolean;
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
"test:watch": "vitest --watch",
|
||||
"test:cov": "vitest --coverage",
|
||||
"typeorm": "typeorm",
|
||||
"lifecycle": "node ./dist/utils/lifecycle.js",
|
||||
"typeorm:migrations:create": "typeorm migration:create",
|
||||
"typeorm:migrations:generate": "typeorm migration:generate -d ./dist/database.config.js",
|
||||
"typeorm:migrations:run": "typeorm migration:run -d ./dist/database.config.js",
|
||||
|
|
|
@ -3,6 +3,11 @@ import { readFileSync } from 'node:fs';
|
|||
import { join } from 'node:path';
|
||||
import { Version } from 'src/utils/version';
|
||||
|
||||
export const NEXT_RELEASE = 'NEXT_RELEASE';
|
||||
export const LIFECYCLE_EXTENSION = 'x-immich-lifecycle';
|
||||
export const DEPRECATED_IN_PREFIX = 'This property was deprecated in ';
|
||||
export const ADDED_IN_PREFIX = 'This property was added in ';
|
||||
|
||||
export const SALT_ROUNDS = 10;
|
||||
|
||||
const { version } = JSON.parse(readFileSync('./package.json', 'utf8'));
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { SetMetadata } from '@nestjs/common';
|
||||
import { SetMetadata, applyDecorators } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { OnEventOptions } from '@nestjs/event-emitter/dist/interfaces';
|
||||
import { ApiExtension, ApiOperation, ApiProperty, ApiTags } from '@nestjs/swagger';
|
||||
import _ from 'lodash';
|
||||
import { ADDED_IN_PREFIX, DEPRECATED_IN_PREFIX, LIFECYCLE_EXTENSION } from 'src/constants';
|
||||
import { ServerAsyncEvent, ServerEvent } from 'src/interfaces/event.interface';
|
||||
import { setUnion } from 'src/utils/set';
|
||||
|
||||
|
@ -128,3 +130,31 @@ export const GenerateSql = (...options: GenerateSqlQueries[]) => SetMetadata(GEN
|
|||
|
||||
export const OnServerEvent = (event: ServerEvent | ServerAsyncEvent, options?: OnEventOptions) =>
|
||||
OnEvent(event, { suppressErrors: false, ...options });
|
||||
|
||||
type LifecycleRelease = 'NEXT_RELEASE' | string;
|
||||
type LifecycleMetadata = {
|
||||
addedAt?: LifecycleRelease;
|
||||
deprecatedAt?: LifecycleRelease;
|
||||
};
|
||||
|
||||
export const EndpointLifecycle = ({ addedAt, deprecatedAt }: LifecycleMetadata) => {
|
||||
const decorators: MethodDecorator[] = [ApiExtension(LIFECYCLE_EXTENSION, { addedAt, deprecatedAt })];
|
||||
if (deprecatedAt) {
|
||||
decorators.push(
|
||||
ApiTags('Deprecated'),
|
||||
ApiOperation({ deprecated: true, description: DEPRECATED_IN_PREFIX + deprecatedAt }),
|
||||
);
|
||||
}
|
||||
|
||||
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 }));
|
||||
}
|
||||
|
||||
return applyDecorators(...decorators);
|
||||
};
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ArrayNotEmpty, IsEnum, IsString } from 'class-validator';
|
||||
import _ from 'lodash';
|
||||
import { PropertyLifecycle } from 'src/decorators';
|
||||
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
|
||||
|
@ -25,7 +26,7 @@ export class AlbumUserAddDto {
|
|||
export class AddUsersDto {
|
||||
@ValidateUUID({ each: true, optional: true })
|
||||
@ArrayNotEmpty()
|
||||
@ApiProperty({ deprecated: true, description: 'Deprecated in favor of albumUsers' })
|
||||
@PropertyLifecycle({ deprecatedAt: 'v1.102.0' })
|
||||
sharedUserIds?: string[];
|
||||
|
||||
@ArrayNotEmpty()
|
||||
|
@ -119,7 +120,7 @@ export class AlbumResponseDto {
|
|||
updatedAt!: Date;
|
||||
albumThumbnailAssetId!: string | null;
|
||||
shared!: boolean;
|
||||
@ApiProperty({ deprecated: true, description: 'Deprecated in favor of albumUsers' })
|
||||
@PropertyLifecycle({ deprecatedAt: 'v1.102.0' })
|
||||
sharedUsers!: UserResponseDto[];
|
||||
albumUsers!: AlbumUserResponseDto[];
|
||||
hasSharedLink!: boolean;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { PropertyLifecycle } from 'src/decorators';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { ExifResponseDto, mapExif } from 'src/dtos/exif.dto';
|
||||
import { PersonWithFacesResponseDto, mapFacesWithoutPerson, mapPerson } from 'src/dtos/person.dto';
|
||||
|
@ -131,7 +132,7 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As
|
|||
}
|
||||
|
||||
export class MemoryLaneResponseDto {
|
||||
@ApiProperty({ deprecated: true })
|
||||
@PropertyLifecycle({ deprecatedAt: 'v1.100.0' })
|
||||
title!: string;
|
||||
|
||||
@ApiProperty({ type: 'integer' })
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsEnum, IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator';
|
||||
import { PropertyLifecycle } from 'src/decorators';
|
||||
import { AlbumResponseDto } from 'src/dtos/album.dto';
|
||||
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||
import { AssetOrder } from 'src/entities/album.entity';
|
||||
|
@ -163,13 +164,13 @@ export class MetadataSearchDto extends BaseSearchDto {
|
|||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Optional()
|
||||
@ApiProperty({ deprecated: true })
|
||||
@PropertyLifecycle({ deprecatedAt: 'v1.100.0' })
|
||||
resizePath?: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Optional()
|
||||
@ApiProperty({ deprecated: true })
|
||||
@PropertyLifecycle({ deprecatedAt: 'v1.100.0' })
|
||||
webpPath?: string;
|
||||
|
||||
@IsString()
|
||||
|
|
93
server/src/utils/lifecycle.ts
Normal file
93
server/src/utils/lifecycle.ts
Normal file
|
@ -0,0 +1,93 @@
|
|||
#!/usr/bin/env node
|
||||
import { OpenAPIObject } from '@nestjs/swagger';
|
||||
import { SchemaObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { ADDED_IN_PREFIX, DEPRECATED_IN_PREFIX, LIFECYCLE_EXTENSION, NEXT_RELEASE } from 'src/constants';
|
||||
import { Version } from 'src/utils/version';
|
||||
|
||||
const outputPath = resolve(process.cwd(), '../open-api/immich-openapi-specs.json');
|
||||
const spec = JSON.parse(readFileSync(outputPath).toString()) as OpenAPIObject;
|
||||
|
||||
type Items = {
|
||||
oldEndpoints: Endpoint[];
|
||||
newEndpoints: Endpoint[];
|
||||
oldProperties: Property[];
|
||||
newProperties: Property[];
|
||||
};
|
||||
type Endpoint = { url: string; method: string; endpoint: any };
|
||||
type Property = { schema: string; property: string };
|
||||
|
||||
const metadata: Record<string, Items> = {};
|
||||
const trackVersion = (version: string) => {
|
||||
if (!metadata[version]) {
|
||||
metadata[version] = {
|
||||
oldEndpoints: [],
|
||||
newEndpoints: [],
|
||||
oldProperties: [],
|
||||
newProperties: [],
|
||||
};
|
||||
}
|
||||
return metadata[version];
|
||||
};
|
||||
|
||||
for (const [url, methods] of Object.entries(spec.paths)) {
|
||||
for (const [method, endpoint] of Object.entries(methods) as Array<[string, any]>) {
|
||||
const deprecatedAt = endpoint[LIFECYCLE_EXTENSION]?.deprecatedAt;
|
||||
if (deprecatedAt) {
|
||||
trackVersion(deprecatedAt).oldEndpoints.push({ url, method, endpoint });
|
||||
}
|
||||
|
||||
const addedAt = endpoint[LIFECYCLE_EXTENSION]?.addedAt;
|
||||
if (addedAt) {
|
||||
trackVersion(addedAt).newEndpoints.push({ url, method, endpoint });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [schemaName, schema] of Object.entries(spec.components?.schemas || {})) {
|
||||
for (const [propertyName, property] of Object.entries((schema as SchemaObject).properties || {})) {
|
||||
const propertySchema = property as SchemaObject;
|
||||
if (propertySchema.description?.startsWith(DEPRECATED_IN_PREFIX)) {
|
||||
const deprecatedAt = propertySchema.description.replace(DEPRECATED_IN_PREFIX, '').trim();
|
||||
trackVersion(deprecatedAt).oldProperties.push({ schema: schemaName, property: propertyName });
|
||||
}
|
||||
|
||||
if (propertySchema.description?.startsWith(ADDED_IN_PREFIX)) {
|
||||
const addedAt = propertySchema.description.replace(ADDED_IN_PREFIX, '').trim();
|
||||
trackVersion(addedAt).newProperties.push({ schema: schemaName, property: propertyName });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sortedVersions = Object.keys(metadata).sort((a, b) => {
|
||||
if (a === NEXT_RELEASE) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (b === NEXT_RELEASE) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const versionA = Version.fromString(a);
|
||||
const versionB = Version.fromString(b);
|
||||
return versionB.compareTo(versionA);
|
||||
});
|
||||
|
||||
for (const version of sortedVersions) {
|
||||
const { oldEndpoints, newEndpoints, oldProperties, newProperties } = metadata[version];
|
||||
console.log(`\nChanges in ${version}`);
|
||||
console.log('---------------------');
|
||||
for (const { url, method, endpoint } of oldEndpoints) {
|
||||
console.log(`- Deprecated ${method.toUpperCase()} ${url} (${endpoint.operationId})`);
|
||||
}
|
||||
for (const { url, method, endpoint } of newEndpoints) {
|
||||
console.log(`- Added ${method.toUpperCase()} ${url} (${endpoint.operationId})`);
|
||||
}
|
||||
for (const { schema, property } of oldProperties) {
|
||||
console.log(`- Deprecated ${schema}.${property}`);
|
||||
}
|
||||
for (const { schema, property } of newProperties) {
|
||||
console.log(`- Added ${schema}.${property}`);
|
||||
}
|
||||
}
|
|
@ -61,4 +61,12 @@ export class Version implements IVersion {
|
|||
const [bool, type] = this.compare(version);
|
||||
return bool > 0 ? type : VersionType.EQUAL;
|
||||
}
|
||||
|
||||
compareTo(other: Version) {
|
||||
if (this.isEqual(other)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return this.isNewerThan(other) ? 1 : -1;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue