mirror of
https://github.com/immich-app/immich.git
synced 2024-12-23 02:06:15 +02:00
chore: lifecycle metadata (#9103)
feat(server): track endpoint lifecycle
This commit is contained in:
parent
6eb5d2e95e
commit
59caf1fce4
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": {
|
"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": {
|
||||||
|
@ -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;
|
||||||
|
@ -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",
|
||||||
|
@ -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'));
|
||||||
|
@ -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);
|
||||||
|
};
|
||||||
|
@ -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;
|
||||||
|
@ -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' })
|
||||||
|
@ -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()
|
||||||
|
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);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user