chore: lifecycle metadata (#9103)

feat(server): track endpoint lifecycle
This commit is contained in:
Jason Rasmussen 2024-04-29 09:48:28 -04:00 committed by GitHub
parent 6eb5d2e95e
commit 59caf1fce4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 171 additions and 19 deletions

View File

@ -9,7 +9,7 @@ import 'package:openapi/api.dart';
Name | Type | Description | Notes Name | Type | Description | Notes
------------ | ------------- | ------------- | ------------- ------------ | ------------- | ------------- | -------------
**albumUsers** | [**List<AlbumUserAddDto>**](AlbumUserAddDto.md) | | [default to const []] **albumUsers** | [**List<AlbumUserAddDto>**](AlbumUserAddDto.md) | | [default to const []]
**sharedUserIds** | **List<String>** | Deprecated in favor of albumUsers | [optional] [default to const []] **sharedUserIds** | **List<String>** | This property was deprecated in v1.102.0 | [optional] [default to const []]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@ -24,7 +24,7 @@ Name | Type | Description | Notes
**owner** | [**UserResponseDto**](UserResponseDto.md) | | **owner** | [**UserResponseDto**](UserResponseDto.md) | |
**ownerId** | **String** | | **ownerId** | **String** | |
**shared** | **bool** | | **shared** | **bool** | |
**sharedUsers** | [**List<UserResponseDto>**](UserResponseDto.md) | Deprecated in favor of albumUsers | [default to const []] **sharedUsers** | [**List<UserResponseDto>**](UserResponseDto.md) | This property was deprecated in v1.102.0 | [default to const []]
**startDate** | [**DateTime**](DateTime.md) | | [optional] **startDate** | [**DateTime**](DateTime.md) | | [optional]
**updatedAt** | [**DateTime**](DateTime.md) | | **updatedAt** | [**DateTime**](DateTime.md) | |

View File

@ -9,7 +9,7 @@ import 'package:openapi/api.dart';
Name | Type | Description | Notes Name | Type | Description | Notes
------------ | ------------- | ------------- | ------------- ------------ | ------------- | ------------- | -------------
**assets** | [**List<AssetResponseDto>**](AssetResponseDto.md) | | [default to const []] **assets** | [**List<AssetResponseDto>**](AssetResponseDto.md) | | [default to const []]
**title** | **String** | | **title** | **String** | This property was deprecated in v1.100.0 |
**yearsAgo** | **int** | | **yearsAgo** | **int** | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@ -36,7 +36,7 @@ Name | Type | Description | Notes
**page** | **num** | | [optional] **page** | **num** | | [optional]
**personIds** | **List<String>** | | [optional] [default to const []] **personIds** | **List<String>** | | [optional] [default to const []]
**previewPath** | **String** | | [optional] **previewPath** | **String** | | [optional]
**resizePath** | **String** | | [optional] **resizePath** | **String** | This property was deprecated in v1.100.0 | [optional]
**size** | **num** | | [optional] **size** | **num** | | [optional]
**state** | **String** | | [optional] **state** | **String** | | [optional]
**takenAfter** | [**DateTime**](DateTime.md) | | [optional] **takenAfter** | [**DateTime**](DateTime.md) | | [optional]
@ -47,7 +47,7 @@ Name | Type | Description | Notes
**type** | [**AssetTypeEnum**](AssetTypeEnum.md) | | [optional] **type** | [**AssetTypeEnum**](AssetTypeEnum.md) | | [optional]
**updatedAfter** | [**DateTime**](DateTime.md) | | [optional] **updatedAfter** | [**DateTime**](DateTime.md) | | [optional]
**updatedBefore** | [**DateTime**](DateTime.md) | | [optional] **updatedBefore** | [**DateTime**](DateTime.md) | | [optional]
**webpPath** | **String** | | [optional] **webpPath** | **String** | This property was deprecated in v1.100.0 | [optional]
**withArchived** | **bool** | | [optional] [default to false] **withArchived** | **bool** | | [optional] [default to false]
**withDeleted** | **bool** | | [optional] **withDeleted** | **bool** | | [optional]
**withExif** | **bool** | | [optional] **withExif** | **bool** | | [optional]

View File

@ -19,7 +19,7 @@ class AddUsersDto {
List<AlbumUserAddDto> albumUsers; List<AlbumUserAddDto> albumUsers;
/// Deprecated in favor of albumUsers /// This property was deprecated in v1.102.0
List<String> sharedUserIds; List<String> sharedUserIds;
@override @override

View File

@ -84,7 +84,7 @@ class AlbumResponseDto {
bool shared; bool shared;
/// Deprecated in favor of albumUsers /// This property was deprecated in v1.102.0
List<UserResponseDto> sharedUsers; List<UserResponseDto> sharedUsers;
/// ///

View File

@ -20,6 +20,7 @@ class MemoryLaneResponseDto {
List<AssetResponseDto> assets; List<AssetResponseDto> assets;
/// This property was deprecated in v1.100.0
String title; String title;
int yearsAgo; int yearsAgo;

View File

@ -279,6 +279,7 @@ class MetadataSearchDto {
/// ///
String? previewPath; String? previewPath;
/// This property was deprecated in v1.100.0
/// ///
/// Please note: This property should have been non-nullable! Since the specification file /// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated /// does not include a default value (using the "default:" property), however, the generated
@ -369,6 +370,7 @@ class MetadataSearchDto {
/// ///
DateTime? updatedBefore; DateTime? updatedBefore;
/// This property was deprecated in v1.100.0
/// ///
/// Please note: This property should have been non-nullable! Since the specification file /// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated /// does not include a default value (using the "default:" property), however, the generated

View File

@ -21,7 +21,7 @@ void main() {
// TODO // TODO
}); });
// Deprecated in favor of albumUsers // This property was deprecated in v1.102.0
// List<String> sharedUserIds (default value: const []) // List<String> sharedUserIds (default value: const [])
test('to test the property `sharedUserIds`', () async { test('to test the property `sharedUserIds`', () async {
// TODO // TODO

View File

@ -96,7 +96,7 @@ void main() {
// TODO // TODO
}); });
// Deprecated in favor of albumUsers // This property was deprecated in v1.102.0
// List<UserResponseDto> sharedUsers (default value: const []) // List<UserResponseDto> sharedUsers (default value: const [])
test('to test the property `sharedUsers`', () async { test('to test the property `sharedUsers`', () async {
// TODO // TODO

View File

@ -21,6 +21,7 @@ void main() {
// TODO // TODO
}); });
// This property was deprecated in v1.100.0
// String title // String title
test('to test the property `title`', () async { test('to test the property `title`', () async {
// TODO // TODO

View File

@ -156,6 +156,7 @@ void main() {
// TODO // TODO
}); });
// This property was deprecated in v1.100.0
// String resizePath // String resizePath
test('to test the property `resizePath`', () async { test('to test the property `resizePath`', () async {
// TODO // TODO
@ -211,6 +212,7 @@ void main() {
// TODO // TODO
}); });
// This property was deprecated in v1.100.0
// String webpPath // String webpPath
test('to test the property `webpPath`', () async { test('to test the property `webpPath`', () async {
// TODO // TODO

View File

@ -6616,7 +6616,7 @@
}, },
"sharedUserIds": { "sharedUserIds": {
"deprecated": true, "deprecated": true,
"description": "Deprecated in favor of albumUsers", "description": "This property was deprecated in v1.102.0",
"items": { "items": {
"format": "uuid", "format": "uuid",
"type": "string" "type": "string"
@ -6721,7 +6721,7 @@
}, },
"sharedUsers": { "sharedUsers": {
"deprecated": true, "deprecated": true,
"description": "Deprecated in favor of albumUsers", "description": "This property was deprecated in v1.102.0",
"items": { "items": {
"$ref": "#/components/schemas/UserResponseDto" "$ref": "#/components/schemas/UserResponseDto"
}, },
@ -8433,6 +8433,7 @@
}, },
"title": { "title": {
"deprecated": true, "deprecated": true,
"description": "This property was deprecated in v1.100.0",
"type": "string" "type": "string"
}, },
"yearsAgo": { "yearsAgo": {
@ -8640,6 +8641,7 @@
}, },
"resizePath": { "resizePath": {
"deprecated": true, "deprecated": true,
"description": "This property was deprecated in v1.100.0",
"type": "string" "type": "string"
}, },
"size": { "size": {
@ -8682,6 +8684,7 @@
}, },
"webpPath": { "webpPath": {
"deprecated": true, "deprecated": true,
"description": "This property was deprecated in v1.100.0",
"type": "string" "type": "string"
}, },
"withArchived": { "withArchived": {

View File

@ -162,7 +162,7 @@ export type AlbumResponseDto = {
owner: UserResponseDto; owner: UserResponseDto;
ownerId: string; ownerId: string;
shared: boolean; shared: boolean;
/** Deprecated in favor of albumUsers */ /** This property was deprecated in v1.102.0 */
sharedUsers: UserResponseDto[]; sharedUsers: UserResponseDto[];
startDate?: string; startDate?: string;
updatedAt: string; updatedAt: string;
@ -202,7 +202,7 @@ export type AlbumUserAddDto = {
}; };
export type AddUsersDto = { export type AddUsersDto = {
albumUsers: AlbumUserAddDto[]; albumUsers: AlbumUserAddDto[];
/** Deprecated in favor of albumUsers */ /** This property was deprecated in v1.102.0 */
sharedUserIds?: string[]; sharedUserIds?: string[];
}; };
export type ApiKeyResponseDto = { export type ApiKeyResponseDto = {
@ -273,6 +273,7 @@ export type MapMarkerResponseDto = {
}; };
export type MemoryLaneResponseDto = { export type MemoryLaneResponseDto = {
assets: AssetResponseDto[]; assets: AssetResponseDto[];
/** This property was deprecated in v1.100.0 */
title: string; title: string;
yearsAgo: number; yearsAgo: number;
}; };
@ -637,6 +638,7 @@ export type MetadataSearchDto = {
page?: number; page?: number;
personIds?: string[]; personIds?: string[];
previewPath?: string; previewPath?: string;
/** This property was deprecated in v1.100.0 */
resizePath?: string; resizePath?: string;
size?: number; size?: number;
state?: string; state?: string;
@ -648,6 +650,7 @@ export type MetadataSearchDto = {
"type"?: AssetTypeEnum; "type"?: AssetTypeEnum;
updatedAfter?: string; updatedAfter?: string;
updatedBefore?: string; updatedBefore?: string;
/** This property was deprecated in v1.100.0 */
webpPath?: string; webpPath?: string;
withArchived?: boolean; withArchived?: boolean;
withDeleted?: boolean; withDeleted?: boolean;

View File

@ -22,6 +22,7 @@
"test:watch": "vitest --watch", "test:watch": "vitest --watch",
"test:cov": "vitest --coverage", "test:cov": "vitest --coverage",
"typeorm": "typeorm", "typeorm": "typeorm",
"lifecycle": "node ./dist/utils/lifecycle.js",
"typeorm:migrations:create": "typeorm migration:create", "typeorm:migrations:create": "typeorm migration:create",
"typeorm:migrations:generate": "typeorm migration:generate -d ./dist/database.config.js", "typeorm:migrations:generate": "typeorm migration:generate -d ./dist/database.config.js",
"typeorm:migrations:run": "typeorm migration:run -d ./dist/database.config.js", "typeorm:migrations:run": "typeorm migration:run -d ./dist/database.config.js",

View File

@ -3,6 +3,11 @@ import { readFileSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
import { Version } from 'src/utils/version'; 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; export const SALT_ROUNDS = 10;
const { version } = JSON.parse(readFileSync('./package.json', 'utf8')); const { version } = JSON.parse(readFileSync('./package.json', 'utf8'));

View File

@ -1,7 +1,9 @@
import { SetMetadata } from '@nestjs/common'; import { SetMetadata, applyDecorators } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter'; import { OnEvent } from '@nestjs/event-emitter';
import { OnEventOptions } from '@nestjs/event-emitter/dist/interfaces'; import { OnEventOptions } from '@nestjs/event-emitter/dist/interfaces';
import { ApiExtension, ApiOperation, ApiProperty, ApiTags } from '@nestjs/swagger';
import _ from 'lodash'; import _ from 'lodash';
import { ADDED_IN_PREFIX, DEPRECATED_IN_PREFIX, LIFECYCLE_EXTENSION } from 'src/constants';
import { ServerAsyncEvent, ServerEvent } from 'src/interfaces/event.interface'; import { ServerAsyncEvent, ServerEvent } from 'src/interfaces/event.interface';
import { setUnion } from 'src/utils/set'; 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) => export const OnServerEvent = (event: ServerEvent | ServerAsyncEvent, options?: OnEventOptions) =>
OnEvent(event, { suppressErrors: false, ...options }); 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);
};

View File

@ -1,6 +1,7 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { ArrayNotEmpty, IsEnum, IsString } from 'class-validator'; import { ArrayNotEmpty, IsEnum, IsString } from 'class-validator';
import _ from 'lodash'; import _ from 'lodash';
import { PropertyLifecycle } from 'src/decorators';
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
@ -25,7 +26,7 @@ export class AlbumUserAddDto {
export class AddUsersDto { export class AddUsersDto {
@ValidateUUID({ each: true, optional: true }) @ValidateUUID({ each: true, optional: true })
@ArrayNotEmpty() @ArrayNotEmpty()
@ApiProperty({ deprecated: true, description: 'Deprecated in favor of albumUsers' }) @PropertyLifecycle({ deprecatedAt: 'v1.102.0' })
sharedUserIds?: string[]; sharedUserIds?: string[];
@ArrayNotEmpty() @ArrayNotEmpty()
@ -119,7 +120,7 @@ export class AlbumResponseDto {
updatedAt!: Date; updatedAt!: Date;
albumThumbnailAssetId!: string | null; albumThumbnailAssetId!: string | null;
shared!: boolean; shared!: boolean;
@ApiProperty({ deprecated: true, description: 'Deprecated in favor of albumUsers' }) @PropertyLifecycle({ deprecatedAt: 'v1.102.0' })
sharedUsers!: UserResponseDto[]; sharedUsers!: UserResponseDto[];
albumUsers!: AlbumUserResponseDto[]; albumUsers!: AlbumUserResponseDto[];
hasSharedLink!: boolean; hasSharedLink!: boolean;

View File

@ -1,4 +1,5 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { PropertyLifecycle } from 'src/decorators';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { ExifResponseDto, mapExif } from 'src/dtos/exif.dto'; import { ExifResponseDto, mapExif } from 'src/dtos/exif.dto';
import { PersonWithFacesResponseDto, mapFacesWithoutPerson, mapPerson } from 'src/dtos/person.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 { export class MemoryLaneResponseDto {
@ApiProperty({ deprecated: true }) @PropertyLifecycle({ deprecatedAt: 'v1.100.0' })
title!: string; title!: string;
@ApiProperty({ type: 'integer' }) @ApiProperty({ type: 'integer' })

View File

@ -1,6 +1,7 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { IsEnum, IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator'; import { IsEnum, IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator';
import { PropertyLifecycle } from 'src/decorators';
import { AlbumResponseDto } from 'src/dtos/album.dto'; import { AlbumResponseDto } from 'src/dtos/album.dto';
import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { AssetResponseDto } from 'src/dtos/asset-response.dto';
import { AssetOrder } from 'src/entities/album.entity'; import { AssetOrder } from 'src/entities/album.entity';
@ -163,13 +164,13 @@ export class MetadataSearchDto extends BaseSearchDto {
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
@Optional() @Optional()
@ApiProperty({ deprecated: true }) @PropertyLifecycle({ deprecatedAt: 'v1.100.0' })
resizePath?: string; resizePath?: string;
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
@Optional() @Optional()
@ApiProperty({ deprecated: true }) @PropertyLifecycle({ deprecatedAt: 'v1.100.0' })
webpPath?: string; webpPath?: string;
@IsString() @IsString()

View 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}`);
}
}

View File

@ -61,4 +61,12 @@ export class Version implements IVersion {
const [bool, type] = this.compare(version); const [bool, type] = this.compare(version);
return bool > 0 ? type : VersionType.EQUAL; return bool > 0 ? type : VersionType.EQUAL;
} }
compareTo(other: Version) {
if (this.isEqual(other)) {
return 0;
}
return this.isNewerThan(other) ? 1 : -1;
}
} }