mirror of
https://github.com/immich-app/immich.git
synced 2026-10-16 21:05:52 +08:00
feat: server feature level (#32178)
* feat: server feature level * fix: correctly compute feature level
This commit is contained in:
@@ -44,6 +44,7 @@ final Map<String, Map<String, Object?>> openApiPatches = {
|
||||
'MemoriesResponse': {'duration': 5, 'sidebarWeb': false},
|
||||
'PeopleResponse': {'updateStrategy': 'everyone'},
|
||||
'PersonResponseDto': {'otherPeople': const [], 'sharedBy': const [], 'sharedWith': const []},
|
||||
'ServerVersionResponseDto': {'featureLevel': 0},
|
||||
'WorkflowResponseDto': {'logging': false},
|
||||
};
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ void main() {
|
||||
when(() => mockApi.serverInfoApi).thenReturn(mockServerApi);
|
||||
when(
|
||||
() => mockServerApi.getServerVersion(),
|
||||
).thenAnswer((_) async => ServerVersionResponseDto(major: 1, minor: 132, patch_: 0, prerelease: null));
|
||||
).thenAnswer((_) async => ServerVersionResponseDto(major: 1, minor: 132, patch_: 0, prerelease: null, featureLevel: 1));
|
||||
|
||||
when(() => mockSyncStreamRepo.updateUsersV1(any())).thenAnswer(successHandler);
|
||||
when(() => mockSyncStreamRepo.deleteUsersV1(any())).thenAnswer(successHandler);
|
||||
@@ -567,7 +567,7 @@ void main() {
|
||||
await Store.put(StoreKey.syncMigrationStatus, "[]");
|
||||
when(
|
||||
() => mockServerApi.getServerVersion(),
|
||||
).thenAnswer((_) async => ServerVersionResponseDto(major: 2, minor: 4, patch_: 1, prerelease: null));
|
||||
).thenAnswer((_) async => ServerVersionResponseDto(major: 2, minor: 4, patch_: 1, prerelease: null, featureLevel: 1));
|
||||
|
||||
await sut.sync();
|
||||
|
||||
@@ -595,7 +595,7 @@ void main() {
|
||||
await Store.put(StoreKey.syncMigrationStatus, "[]");
|
||||
when(
|
||||
() => mockServerApi.getServerVersion(),
|
||||
).thenAnswer((_) async => ServerVersionResponseDto(major: 2, minor: 5, patch_: 0, prerelease: null));
|
||||
).thenAnswer((_) async => ServerVersionResponseDto(major: 2, minor: 5, patch_: 0, prerelease: null, featureLevel: 1));
|
||||
await sut.sync();
|
||||
|
||||
verifyInOrder([
|
||||
@@ -625,7 +625,7 @@ void main() {
|
||||
|
||||
when(
|
||||
() => mockServerApi.getServerVersion(),
|
||||
).thenAnswer((_) async => ServerVersionResponseDto(major: 2, minor: 4, patch_: 1, prerelease: null));
|
||||
).thenAnswer((_) async => ServerVersionResponseDto(major: 2, minor: 4, patch_: 1, prerelease: null, featureLevel: 1));
|
||||
|
||||
await sut.sync();
|
||||
|
||||
|
||||
@@ -27790,6 +27790,16 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ServerFeatureLevel": {
|
||||
"description": "Server feature level",
|
||||
"enum": [
|
||||
1
|
||||
],
|
||||
"type": "integer",
|
||||
"x-enum-varnames": [
|
||||
"FeatureLevel"
|
||||
]
|
||||
},
|
||||
"ServerFeaturesDto": {
|
||||
"properties": {
|
||||
"configFile": {
|
||||
@@ -28048,6 +28058,18 @@
|
||||
},
|
||||
"ServerVersionResponseDto": {
|
||||
"properties": {
|
||||
"featureLevel": {
|
||||
"description": "Highest feature level supported by the server, which increments independently of the release version",
|
||||
"maximum": 9007199254740991,
|
||||
"minimum": 0,
|
||||
"type": "integer",
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v4.0.0",
|
||||
"state": "Added"
|
||||
}
|
||||
]
|
||||
},
|
||||
"major": {
|
||||
"description": "Major version number",
|
||||
"maximum": 9007199254740991,
|
||||
@@ -28081,6 +28103,7 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"featureLevel",
|
||||
"major",
|
||||
"minor",
|
||||
"patch",
|
||||
|
||||
@@ -2907,6 +2907,8 @@ export type ServerStorageResponseDto = {
|
||||
diskUseRaw: number;
|
||||
};
|
||||
export type ServerVersionResponseDto = {
|
||||
/** Highest feature level supported by the server, which increments independently of the release version */
|
||||
featureLevel: number;
|
||||
/** Major version number */
|
||||
major: number;
|
||||
/** Minor version number */
|
||||
@@ -8632,6 +8634,9 @@ export enum ReleaseType {
|
||||
Prepatch = "prepatch",
|
||||
Prerelease = "prerelease"
|
||||
}
|
||||
export enum ServerFeatureLevel {
|
||||
FeatureLevel = 1
|
||||
}
|
||||
export enum UserMetadataKey {
|
||||
Preferences = "preferences",
|
||||
License = "license",
|
||||
|
||||
@@ -208,6 +208,10 @@ export class HistoryBuilder {
|
||||
return new HistoryBuilder().added('v3.0.0');
|
||||
}
|
||||
|
||||
static v4() {
|
||||
return new HistoryBuilder().added('v4.0.0');
|
||||
}
|
||||
|
||||
added(version: string, description?: string) {
|
||||
return this.push({ version, state: 'Added', description });
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createZodDto } from 'nestjs-zod';
|
||||
import z from 'zod';
|
||||
import type { SemVer } from 'semver';
|
||||
import { ExtraModel, HistoryBuilder } from 'src/decorators.js';
|
||||
import { ServerFeatureLevel, ServerFeatureLevelSchema } from 'src/enum.js';
|
||||
import { isoDatetimeToDate } from 'src/validation.js';
|
||||
|
||||
const ServerPingResponseSchema = z
|
||||
@@ -68,6 +69,11 @@ const ServerVersionResponseSchema = z
|
||||
.nullable()
|
||||
.meta(HistoryBuilder.v3().getExtensions())
|
||||
.describe('Pre-release version number'),
|
||||
featureLevel: z
|
||||
.int()
|
||||
.min(0)
|
||||
.meta(HistoryBuilder.v4().getExtensions())
|
||||
.describe('Highest feature level supported by the server, which increments independently of the release version'),
|
||||
})
|
||||
.meta({ id: 'ServerVersionResponseDto' });
|
||||
|
||||
@@ -175,6 +181,8 @@ export class ServerAboutResponseDto extends createZodDto(ServerAboutResponseSche
|
||||
export class ServerApkLinksDto extends createZodDto(ServerApkLinksSchema) {}
|
||||
export class ServerStorageResponseDto extends createZodDto(ServerStorageResponseSchema) {}
|
||||
|
||||
const serverFeatureLevel = Math.max(...Object.values(ServerFeatureLevel).filter((value) => typeof value === 'number'));
|
||||
|
||||
export class ServerVersionResponseDto extends createZodDto(ServerVersionResponseSchema) {
|
||||
static fromSemVer(value: SemVer): z.infer<typeof ServerVersionResponseSchema> {
|
||||
return {
|
||||
@@ -182,6 +190,7 @@ export class ServerVersionResponseDto extends createZodDto(ServerVersionResponse
|
||||
minor: value.minor,
|
||||
patch: value.patch,
|
||||
prerelease: (value.prerelease[1] as number) ?? null,
|
||||
featureLevel: serverFeatureLevel,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -195,3 +204,6 @@ export class ServerFeaturesDto extends createZodDto(ServerFeaturesSchema) {}
|
||||
|
||||
@ExtraModel()
|
||||
export class ReleaseEventV1 extends createZodDto(ReleaseEventV1Schema) {}
|
||||
|
||||
@ExtraModel()
|
||||
export class ServerFeatureLevelDto extends createZodDto(ServerFeatureLevelSchema as unknown as z.ZodObject) {}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { WorkflowTrigger } from '@immich/plugin-sdk';
|
||||
import z from 'zod';
|
||||
import { asIntegerEnum } from 'src/utils/helpers.js';
|
||||
|
||||
export enum AuthType {
|
||||
Password = 'password',
|
||||
@@ -1325,3 +1326,15 @@ export enum SearchOrderField {
|
||||
}
|
||||
|
||||
export const SearchOrderFieldSchema = z.enum(SearchOrderField).meta({ id: 'SearchOrderField' });
|
||||
|
||||
export enum ServerFeatureLevel {
|
||||
FeatureLevel = 1,
|
||||
}
|
||||
|
||||
export const ServerFeatureLevelSchema = z
|
||||
.enum(ServerFeatureLevel)
|
||||
.describe('Server feature level')
|
||||
.meta({
|
||||
id: 'ServerFeatureLevel',
|
||||
...asIntegerEnum(ServerFeatureLevel),
|
||||
});
|
||||
|
||||
@@ -65,12 +65,15 @@ describe(VersionService.name, () => {
|
||||
|
||||
describe('getVersion', () => {
|
||||
it('should respond the server version', () => {
|
||||
expect(sut.getVersion()).toEqual({
|
||||
const version = sut.getVersion();
|
||||
expect(version).toEqual({
|
||||
major: 3,
|
||||
minor: 0,
|
||||
patch: 0,
|
||||
prerelease: null,
|
||||
featureLevel: expect.any(Number),
|
||||
});
|
||||
expect(Number.isSafeInteger(version.featureLevel)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -175,6 +178,7 @@ describe(VersionService.name, () => {
|
||||
minor: 0,
|
||||
patch: 0,
|
||||
prerelease: null,
|
||||
featureLevel: expect.any(Number),
|
||||
});
|
||||
expect(mocks.websocket.clientSend).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -187,6 +191,7 @@ describe(VersionService.name, () => {
|
||||
minor: 0,
|
||||
patch: 0,
|
||||
prerelease: null,
|
||||
featureLevel: expect.any(Number),
|
||||
});
|
||||
expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_new_release', '42', expect.any(Object));
|
||||
});
|
||||
@@ -199,6 +204,7 @@ describe(VersionService.name, () => {
|
||||
minor: 0,
|
||||
patch: 0,
|
||||
prerelease: null,
|
||||
featureLevel: expect.any(Number),
|
||||
});
|
||||
expect(mocks.websocket.clientSend).not.toHaveBeenCalledWith('on_new_release', '42', expect.any(Object));
|
||||
});
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
type NumericEnum = Record<string, string | number>;
|
||||
|
||||
export const asIntegerEnum = (type: NumericEnum) => {
|
||||
return {
|
||||
type: 'integer',
|
||||
'x-enum-varnames': asIntegerEnumVarNames(type),
|
||||
};
|
||||
};
|
||||
|
||||
const asIntegerEnumVarNames = (type: NumericEnum) => Object.keys(type).filter((key) => Number.isNaN(Number(key)));
|
||||
@@ -166,11 +166,11 @@ describe('utils', () => {
|
||||
});
|
||||
describe('semverToName', () => {
|
||||
it('should not append release candidate tag if prelease is not set', () => {
|
||||
expect(semverToName({ major: 3, minor: 0, patch: 0, prerelease: null })).toEqual('v3.0.0');
|
||||
expect(semverToName({ major: 3, minor: 0, patch: 0, prerelease: null, featureLevel: 1 })).toEqual('v3.0.0');
|
||||
});
|
||||
|
||||
it('should append release candidate if set', () => {
|
||||
expect(semverToName({ major: 3, minor: 0, patch: 0, prerelease: 0 })).toEqual('v3.0.0-rc.0');
|
||||
expect(semverToName({ major: 3, minor: 0, patch: 0, prerelease: 0, featureLevel: 1 })).toEqual('v3.0.0-rc.0');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user