0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-02-04 01:09:14 -05:00

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
------------ | ------------- | ------------- | -------------
**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)

View file

@ -24,7 +24,7 @@ Name | Type | Description | Notes
**owner** | [**UserResponseDto**](UserResponseDto.md) | |
**ownerId** | **String** | |
**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]
**updatedAt** | [**DateTime**](DateTime.md) | |

View file

@ -9,7 +9,7 @@ import 'package:openapi/api.dart';
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**assets** | [**List<AssetResponseDto>**](AssetResponseDto.md) | | [default to const []]
**title** | **String** | |
**title** | **String** | This property was deprecated in v1.100.0 |
**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)

View file

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

View file

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

View file

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

View file

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

View file

@ -279,6 +279,7 @@ class MetadataSearchDto {
///
String? previewPath;
/// This property was deprecated in v1.100.0
///
/// 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
@ -369,6 +370,7 @@ class MetadataSearchDto {
///
DateTime? updatedBefore;
/// This property was deprecated in v1.100.0
///
/// 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

View file

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

View file

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

View file

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

View file

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

View file

@ -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": {

View file

@ -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;

View file

@ -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",

View file

@ -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'));

View file

@ -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);
};

View file

@ -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;

View file

@ -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' })

View file

@ -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()

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);
return bool > 0 ? type : VersionType.EQUAL;
}
compareTo(other: Version) {
if (this.isEqual(other)) {
return 0;
}
return this.isNewerThan(other) ? 1 : -1;
}
}