mirror of
https://github.com/immich-app/immich.git
synced 2026-05-29 19:12:32 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6bc445a03b |
@@ -1,46 +1,46 @@
|
||||
dev:
|
||||
@printf "This command has been removed. Please use:\n\n mise dev # or mise //:dev from another directory\n\n" >&2 && exit 1
|
||||
@printf "This command has been removed. Please use:\n\n mise dev # or mise //:dev from another directory\n\n"\n\n >&2 && exit 1
|
||||
|
||||
dev-down:
|
||||
@printf "This command has been removed. Please use:\n\n mise dev-down # or mise //:dev-down from another directory\n\n" >&2 && exit 1
|
||||
@printf "This command has been removed. Please use:\n\n mise dev-down # or mise //:dev-down from another directory\n\n"\n\n >&2 && exit 1
|
||||
|
||||
dev-update:
|
||||
@printf "This command has been removed. Please use:\n\n mise dev-update # or mise //:dev-update from another directory\n\n" >&2 && exit 1
|
||||
@printf "This command has been removed. Please use:\n\n mise dev-update # or mise //:dev-update from another directory\n\n"\n\n >&2 && exit 1
|
||||
|
||||
dev-scale:
|
||||
@printf "This command has been removed. Please use:\n\n mise dev-scale # or mise //:dev-scale from another directory\n\n" >&2 && exit 1
|
||||
@printf "This command has been removed. Please use:\n\n mise dev-scale # or mise //:dev-scale from another directory\n\n"\n\n >&2 && exit 1
|
||||
|
||||
dev-docs:
|
||||
npm --prefix docs run start
|
||||
|
||||
.PHONY: e2e
|
||||
e2e:
|
||||
@printf "This command has been removed. Please use:\n\n mise e2e # or mise //:e2e from another directory\n\n" >&2 && exit 1
|
||||
@printf "This command has been removed. Please use:\n\n mise e2e # or mise //:e2e from another directory\n\n"\n\n >&2 && exit 1
|
||||
|
||||
e2e-dev:
|
||||
@printf "This command has been removed. Please use:\n\n mise e2e-dev # or mise //:e2e-dev from another directory\n\n" >&2 && exit 1
|
||||
@printf "This command has been removed. Please use:\n\n mise e2e-dev # or mise //:e2e-dev from another directory\n\n"\n\n >&2 && exit 1
|
||||
|
||||
e2e-update:
|
||||
@printf "This command has been removed. Please use:\n\n mise e2e-update # or mise //:e2e-update from another directory\n\n" >&2 && exit 1
|
||||
@printf "This command has been removed. Please use:\n\n mise e2e-update # or mise //:e2e-update from another directory\n\n"\n\n >&2 && exit 1
|
||||
|
||||
e2e-down:
|
||||
@printf "This command has been removed. Please use:\n\n mise e2e-down # or mise //:e2e-down from another directory\n\n" >&2 && exit 1
|
||||
@printf "This command has been removed. Please use:\n\n mise e2e-down # or mise //:e2e-down from another directory\n\n"\n\n >&2 && exit 1
|
||||
|
||||
prod:
|
||||
@printf "This command has been removed. Please use:\n\n mise prod # or mise //:prod from another directory\n\n" >&2 && exit 1
|
||||
@printf "This command has been removed. Please use:\n\n mise prod # or mise //:prod from another directory\n\n"\n\n >&2 && exit 1
|
||||
|
||||
prod-down:
|
||||
@printf "This command has been removed. Please use:\n\n mise prod-down # or mise //:prod-down from another directory\n\n" >&2 && exit 1
|
||||
@printf "This command has been removed. Please use:\n\n mise prod-down # or mise //:prod-down from another directory\n\n"\n\n >&2 && exit 1
|
||||
|
||||
prod-scale:
|
||||
@printf "This command has been removed. Please use:\n\n mise prod-scale # or mise //:prod-scale from another directory\n\n" >&2 && exit 1
|
||||
@printf "This command has been removed. Please use:\n\n mise prod-scale # or mise //:prod-scale from another directory\n\n"\n\n >&2 && exit 1
|
||||
|
||||
.PHONY: open-api
|
||||
open-api:
|
||||
@printf "This command has been removed. Please use:\n\n mise open-api # or mise //:open-api from another directory\n\n" >&2 && exit 1
|
||||
@printf "This command has been removed. Please use:\n\n mise open-api # or mise //:open-api from another directory\n\n"\n\n >&2 && exit 1
|
||||
|
||||
sql:
|
||||
@printf "This command has been removed. Please use:\n\n mise sql # or mise //:sql from another directory\n\n" >&2 && exit 1
|
||||
@printf "This command has been removed. Please use:\n\n mise sql # or mise //:sql from another directory\n\n"\n\n >&2 && exit 1
|
||||
|
||||
|
||||
renovate:
|
||||
@@ -52,7 +52,16 @@ renovate:
|
||||
MODULES = e2e server web cli sdk docs .github
|
||||
|
||||
test-e2e:
|
||||
@printf "This command has been removed. Please use:\n\n mise //e2e:test # or mise //e2e:test-web for web tests, respectively\n\n" >&2 && exit 1
|
||||
docker compose -f ./e2e/docker-compose.yml build
|
||||
pnpm --filter immich-e2e run test
|
||||
pnpm --filter immich-e2e run test:web
|
||||
|
||||
clean:
|
||||
@printf "This command has been removed. Please use:\n\n mise clean # or mise //:clean from another directory\n\n" >&2 && exit 1
|
||||
find . -name "node_modules" -type d -prune -exec rm -rf {} +
|
||||
find . -name "dist" -type d -prune -exec rm -rf '{}' +
|
||||
find . -name "build" -type d -prune -exec rm -rf '{}' +
|
||||
find . -name ".svelte-kit" -type d -prune -exec rm -rf '{}' +
|
||||
find . -name "coverage" -type d -prune -exec rm -rf '{}' +
|
||||
find . -name ".pnpm-store" -type d -prune -exec rm -rf '{}' +
|
||||
command -v docker >/dev/null 2>&1 && docker compose -f ./docker/docker-compose.dev.yml down -v --remove-orphans || true
|
||||
command -v docker >/dev/null 2>&1 && docker compose -f ./e2e/docker-compose.yml down -v --remove-orphans || true
|
||||
|
||||
+1
-16
@@ -1,21 +1,11 @@
|
||||
[tasks.install]
|
||||
run = "pnpm install --filter immich-e2e --frozen-lockfile"
|
||||
|
||||
[tasks.build]
|
||||
dir = "{{ config_root }}"
|
||||
run = "docker compose build"
|
||||
|
||||
[tasks.test]
|
||||
depends = ["//e2e:build", "//e2e:ci-setup"]
|
||||
env._.path = "./node_modules/.bin"
|
||||
run = "vitest --run"
|
||||
|
||||
[tasks.playwright-install]
|
||||
env._.path = "./node_modules/.bin"
|
||||
run = "playwright install"
|
||||
|
||||
[tasks."test-web"]
|
||||
depends = ["//e2e:build", "//e2e:ci-setup", "//e2e:playwright-install"]
|
||||
env._.path = "./node_modules/.bin"
|
||||
run = "playwright test"
|
||||
|
||||
@@ -40,12 +30,7 @@ run = "tsc --noEmit"
|
||||
|
||||
|
||||
[tasks.ci-setup]
|
||||
depends = [
|
||||
"//:sdk:install",
|
||||
"//:sdk:build",
|
||||
"//packages/cli:install",
|
||||
"//packages/cli:build",
|
||||
]
|
||||
depends = ["//:sdk:install", "//:sdk:build", "//cli:install", "//cli:build"]
|
||||
run = { task = ":install" }
|
||||
|
||||
|
||||
|
||||
@@ -95,7 +95,6 @@ describe('/server', () => {
|
||||
major: expect.any(Number),
|
||||
minor: expect.any(Number),
|
||||
patch: expect.any(Number),
|
||||
prerelease: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,18 +21,18 @@ describe('/system-config', () => {
|
||||
const response1 = await request(app)
|
||||
.put('/system-config')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({ ...config, newVersionCheck: { enabled: false, channel: 'stable' } });
|
||||
.send({ ...config, newVersionCheck: { enabled: false } });
|
||||
|
||||
expect(response1.status).toBe(200);
|
||||
expect(response1.body).toEqual({ ...config, newVersionCheck: { enabled: false, channel: 'stable' } });
|
||||
expect(response1.body).toEqual({ ...config, newVersionCheck: { enabled: false } });
|
||||
|
||||
const response2 = await request(app)
|
||||
.put('/system-config')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({ ...config, newVersionCheck: { enabled: true, channel: 'stable' } });
|
||||
.send({ ...config, newVersionCheck: { enabled: true } });
|
||||
|
||||
expect(response2.status).toBe(200);
|
||||
expect(response2.body).toEqual({ ...config, newVersionCheck: { enabled: true, channel: 'stable' } });
|
||||
expect(response2.body).toEqual({ ...config, newVersionCheck: { enabled: true } });
|
||||
});
|
||||
|
||||
it('should reject an invalid config entry', async () => {
|
||||
|
||||
@@ -305,8 +305,6 @@
|
||||
"refreshing_all_libraries": "Refreshing all libraries",
|
||||
"registration": "Admin Registration",
|
||||
"registration_description": "Since you are the first user on the system, you will be assigned as the Admin and are responsible for administrative tasks, and additional users will be created by you.",
|
||||
"release_channel_release_candidate": "Release candidate",
|
||||
"release_channel_stable": "Stable",
|
||||
"remove_failed_jobs": "Remove failed jobs",
|
||||
"require_password_change_on_login": "Require user to change password on first login",
|
||||
"reset_settings_to_default": "Reset settings to default",
|
||||
@@ -444,8 +442,6 @@
|
||||
"user_settings_description": "Manage user settings",
|
||||
"user_successfully_removed": "User {email} has been successfully removed.",
|
||||
"users_page_description": "Admin users page",
|
||||
"version_check_channel": "Release channel",
|
||||
"version_check_channel_description": "Pick the release channel you want to get version announcements for",
|
||||
"version_check_enabled_description": "Enable version check",
|
||||
"version_check_implications": "The version check feature relies on periodic communication with {server}",
|
||||
"version_check_settings": "Version Check",
|
||||
|
||||
@@ -165,14 +165,3 @@ run = "pnpm format"
|
||||
|
||||
[tasks."i18n:format-fix"]
|
||||
run = "pnpm format:fix"
|
||||
|
||||
[tasks.clean]
|
||||
run = [
|
||||
"find . -name 'node_modules' -type d -prune -exec rm -rf '{}' +",
|
||||
"find . -name 'dist' -type d -prune -exec rm -rf '{}' +",
|
||||
"find . -name 'build' -type d -prune -exec rm -rf '{}' +",
|
||||
"find . -name '.svelte-kit' -type d -prune -exec rm -rf '{}' +",
|
||||
"find . -name 'coverage' -type d -prune -exec rm -rf '{}' +",
|
||||
"find . -name '.pnpm-store' -type d -prune -exec rm -rf '{}' +",
|
||||
{ task = "//:*-down" },
|
||||
]
|
||||
|
||||
@@ -2,12 +2,16 @@ import 'package:immich_mobile/utils/semver.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
class ServerVersion extends SemVer {
|
||||
const ServerVersion({required super.major, required super.minor, required super.patch, super.prerelease});
|
||||
const ServerVersion({required super.major, required super.minor, required super.patch});
|
||||
|
||||
ServerVersion.fromDto(ServerVersionResponseDto dto)
|
||||
: super(major: dto.major, minor: dto.minor, patch: dto.patch_, prerelease: dto.prerelease);
|
||||
@override
|
||||
String toString() {
|
||||
return 'ServerVersion(major: $major, minor: $minor, patch: $patch)';
|
||||
}
|
||||
|
||||
bool isAtLeast({int major = 0, int minor = 0, int patch = 0, int? prerelease}) {
|
||||
return this >= SemVer(major: major, minor: minor, patch: patch, prerelease: prerelease);
|
||||
ServerVersion.fromDto(ServerVersionResponseDto dto) : super(major: dto.major, minor: dto.minor, patch: dto.patch_);
|
||||
|
||||
bool isAtLeast({int major = 0, int minor = 0, int patch = 0}) {
|
||||
return this >= SemVer(major: major, minor: minor, patch: patch);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +1,36 @@
|
||||
enum SemVerType { major, minor, patch, prerelease }
|
||||
enum SemVerType { major, minor, patch }
|
||||
|
||||
class SemVer {
|
||||
final int major;
|
||||
final int minor;
|
||||
final int patch;
|
||||
final int? prerelease;
|
||||
|
||||
const SemVer({required this.major, required this.minor, required this.patch, this.prerelease});
|
||||
const SemVer({required this.major, required this.minor, required this.patch});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '$major.$minor.$patch${prerelease == null ? '' : '-rc.$prerelease'}';
|
||||
return '$major.$minor.$patch';
|
||||
}
|
||||
|
||||
SemVer copyWith({int? major, int? minor, int? patch, int? prerelease}) {
|
||||
return SemVer(
|
||||
major: major ?? this.major,
|
||||
minor: minor ?? this.minor,
|
||||
patch: patch ?? this.patch,
|
||||
prerelease: prerelease ?? this.prerelease,
|
||||
);
|
||||
SemVer copyWith({int? major, int? minor, int? patch}) {
|
||||
return SemVer(major: major ?? this.major, minor: minor ?? this.minor, patch: patch ?? this.patch);
|
||||
}
|
||||
|
||||
static final _pattern = RegExp(r'^v?(\d+)\.(\d+)\.(\d+)(?:-rc\.(\d+))?(?:[-+].*)?$', caseSensitive: false);
|
||||
|
||||
factory SemVer.fromString(String version) {
|
||||
final match = _pattern.firstMatch(version);
|
||||
if (match == null) {
|
||||
if (version.toLowerCase().startsWith("v")) {
|
||||
version = version.substring(1);
|
||||
}
|
||||
|
||||
final parts = version.split("-")[0].split('.');
|
||||
if (parts.length != 3) {
|
||||
throw FormatException('Invalid semantic version string: $version');
|
||||
}
|
||||
|
||||
final prerelease = match.group(4);
|
||||
return SemVer(
|
||||
major: int.parse(match.group(1)!),
|
||||
minor: int.parse(match.group(2)!),
|
||||
patch: int.parse(match.group(3)!),
|
||||
prerelease: prerelease == null ? null : int.parse(prerelease),
|
||||
);
|
||||
try {
|
||||
return SemVer(major: int.parse(parts[0]), minor: int.parse(parts[1]), patch: int.parse(parts[2]));
|
||||
} catch (e) {
|
||||
throw FormatException('Invalid semantic version string: $version');
|
||||
}
|
||||
}
|
||||
|
||||
bool operator >(SemVer other) {
|
||||
@@ -46,10 +40,7 @@ class SemVer {
|
||||
if (minor != other.minor) {
|
||||
return minor > other.minor;
|
||||
}
|
||||
if (patch != other.patch) {
|
||||
return patch > other.patch;
|
||||
}
|
||||
return _comparePrerelease(other) > 0;
|
||||
return patch > other.patch;
|
||||
}
|
||||
|
||||
bool operator <(SemVer other) {
|
||||
@@ -59,23 +50,7 @@ class SemVer {
|
||||
if (minor != other.minor) {
|
||||
return minor < other.minor;
|
||||
}
|
||||
if (patch != other.patch) {
|
||||
return patch < other.patch;
|
||||
}
|
||||
return _comparePrerelease(other) < 0;
|
||||
}
|
||||
|
||||
int _comparePrerelease(SemVer other) {
|
||||
if (prerelease == other.prerelease) {
|
||||
return 0;
|
||||
}
|
||||
if (prerelease == null) {
|
||||
return 1;
|
||||
}
|
||||
if (other.prerelease == null) {
|
||||
return -1;
|
||||
}
|
||||
return prerelease!.compareTo(other.prerelease!);
|
||||
return patch < other.patch;
|
||||
}
|
||||
|
||||
bool operator >=(SemVer other) {
|
||||
@@ -92,11 +67,7 @@ class SemVer {
|
||||
return true;
|
||||
}
|
||||
|
||||
return other is SemVer &&
|
||||
other.major == major &&
|
||||
other.minor == minor &&
|
||||
other.patch == patch &&
|
||||
other.prerelease == prerelease;
|
||||
return other is SemVer && other.major == major && other.minor == minor && other.patch == patch;
|
||||
}
|
||||
|
||||
SemVerType? differenceType(SemVer other) {
|
||||
@@ -109,13 +80,10 @@ class SemVer {
|
||||
if (patch != other.patch) {
|
||||
return SemVerType.patch;
|
||||
}
|
||||
if (prerelease != other.prerelease) {
|
||||
return SemVerType.prerelease;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => major.hashCode ^ minor.hashCode ^ patch.hashCode ^ prerelease.hashCode;
|
||||
int get hashCode => major.hashCode ^ minor.hashCode ^ patch.hashCode;
|
||||
}
|
||||
|
||||
@@ -50,7 +50,9 @@ class AppBarServerInfo extends HookConsumerWidget {
|
||||
divider,
|
||||
_ServerInfoItem(
|
||||
label: "server_version".tr(),
|
||||
text: serverInfoState.serverVersion.major > 0 ? "${serverInfoState.serverVersion}" : "--",
|
||||
text: serverInfoState.serverVersion.major > 0
|
||||
? "${serverInfoState.serverVersion.major}.${serverInfoState.serverVersion.minor}.${serverInfoState.serverVersion.patch}"
|
||||
: "--",
|
||||
),
|
||||
divider,
|
||||
_ServerInfoItem(label: "server_info_box_server_url".tr(), text: getServerUrl() ?? '--', tooltip: true),
|
||||
@@ -58,7 +60,9 @@ class AppBarServerInfo extends HookConsumerWidget {
|
||||
divider,
|
||||
_ServerInfoItem(
|
||||
label: "latest_version".tr(),
|
||||
text: serverInfoState.latestVersion!.major > 0 ? "${serverInfoState.latestVersion!}" : "--",
|
||||
text: serverInfoState.latestVersion!.major > 0
|
||||
? "${serverInfoState.latestVersion!.major}.${serverInfoState.latestVersion!.minor}.${serverInfoState.latestVersion!.patch}"
|
||||
: "--",
|
||||
tooltip: true,
|
||||
icon: serverInfoState.versionStatus == VersionStatus.serverOutOfDate
|
||||
? const Icon(Icons.info, color: Color.fromARGB(255, 243, 188, 106), size: 12)
|
||||
|
||||
Generated
-3
@@ -513,9 +513,6 @@ Class | Method | HTTP request | Description
|
||||
- [RatingsUpdate](doc//RatingsUpdate.md)
|
||||
- [ReactionLevel](doc//ReactionLevel.md)
|
||||
- [ReactionType](doc//ReactionType.md)
|
||||
- [ReleaseChannel](doc//ReleaseChannel.md)
|
||||
- [ReleaseEventV1](doc//ReleaseEventV1.md)
|
||||
- [ReleaseType](doc//ReleaseType.md)
|
||||
- [ReverseGeocodingStateResponseDto](doc//ReverseGeocodingStateResponseDto.md)
|
||||
- [RotateParameters](doc//RotateParameters.md)
|
||||
- [SearchAlbumResponseDto](doc//SearchAlbumResponseDto.md)
|
||||
|
||||
Generated
-3
@@ -258,9 +258,6 @@ part 'model/ratings_response.dart';
|
||||
part 'model/ratings_update.dart';
|
||||
part 'model/reaction_level.dart';
|
||||
part 'model/reaction_type.dart';
|
||||
part 'model/release_channel.dart';
|
||||
part 'model/release_event_v1.dart';
|
||||
part 'model/release_type.dart';
|
||||
part 'model/reverse_geocoding_state_response_dto.dart';
|
||||
part 'model/rotate_parameters.dart';
|
||||
part 'model/search_album_response_dto.dart';
|
||||
|
||||
Generated
-6
@@ -562,12 +562,6 @@ class ApiClient {
|
||||
return ReactionLevelTypeTransformer().decode(value);
|
||||
case 'ReactionType':
|
||||
return ReactionTypeTypeTransformer().decode(value);
|
||||
case 'ReleaseChannel':
|
||||
return ReleaseChannelTypeTransformer().decode(value);
|
||||
case 'ReleaseEventV1':
|
||||
return ReleaseEventV1.fromJson(value);
|
||||
case 'ReleaseType':
|
||||
return ReleaseTypeTypeTransformer().decode(value);
|
||||
case 'ReverseGeocodingStateResponseDto':
|
||||
return ReverseGeocodingStateResponseDto.fromJson(value);
|
||||
case 'RotateParameters':
|
||||
|
||||
Generated
-6
@@ -157,12 +157,6 @@ String parameterToString(dynamic value) {
|
||||
if (value is ReactionType) {
|
||||
return ReactionTypeTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is ReleaseChannel) {
|
||||
return ReleaseChannelTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is ReleaseType) {
|
||||
return ReleaseTypeTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is SearchSuggestionType) {
|
||||
return SearchSuggestionTypeTypeTransformer().encode(value).toString();
|
||||
}
|
||||
|
||||
-85
@@ -1,85 +0,0 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
/// Release channel
|
||||
class ReleaseChannel {
|
||||
/// Instantiate a new enum with the provided [value].
|
||||
const ReleaseChannel._(this.value);
|
||||
|
||||
/// The underlying value of this enum member.
|
||||
final String value;
|
||||
|
||||
@override
|
||||
String toString() => value;
|
||||
|
||||
String toJson() => value;
|
||||
|
||||
static const stable = ReleaseChannel._(r'stable');
|
||||
static const releaseCandidate = ReleaseChannel._(r'releaseCandidate');
|
||||
|
||||
/// List of all possible values in this [enum][ReleaseChannel].
|
||||
static const values = <ReleaseChannel>[
|
||||
stable,
|
||||
releaseCandidate,
|
||||
];
|
||||
|
||||
static ReleaseChannel? fromJson(dynamic value) => ReleaseChannelTypeTransformer().decode(value);
|
||||
|
||||
static List<ReleaseChannel> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <ReleaseChannel>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = ReleaseChannel.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
}
|
||||
|
||||
/// Transformation class that can [encode] an instance of [ReleaseChannel] to String,
|
||||
/// and [decode] dynamic data back to [ReleaseChannel].
|
||||
class ReleaseChannelTypeTransformer {
|
||||
factory ReleaseChannelTypeTransformer() => _instance ??= const ReleaseChannelTypeTransformer._();
|
||||
|
||||
const ReleaseChannelTypeTransformer._();
|
||||
|
||||
String encode(ReleaseChannel data) => data.value;
|
||||
|
||||
/// Decodes a [dynamic value][data] to a ReleaseChannel.
|
||||
///
|
||||
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
||||
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
||||
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
||||
///
|
||||
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
||||
/// and users are still using an old app with the old code.
|
||||
ReleaseChannel? decode(dynamic data, {bool allowNull = true}) {
|
||||
if (data != null) {
|
||||
switch (data) {
|
||||
case r'stable': return ReleaseChannel.stable;
|
||||
case r'releaseCandidate': return ReleaseChannel.releaseCandidate;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Singleton [ReleaseChannelTypeTransformer] instance.
|
||||
static ReleaseChannelTypeTransformer? _instance;
|
||||
}
|
||||
|
||||
-133
@@ -1,133 +0,0 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class ReleaseEventV1 {
|
||||
/// Returns a new [ReleaseEventV1] instance.
|
||||
ReleaseEventV1({
|
||||
required this.checkedAt,
|
||||
required this.isAvailable,
|
||||
required this.releaseVersion,
|
||||
required this.serverVersion,
|
||||
required this.type,
|
||||
});
|
||||
|
||||
/// When the server last checked for a latest version. As an ISO timestamp
|
||||
String checkedAt;
|
||||
|
||||
/// Whether a new version is available
|
||||
bool isAvailable;
|
||||
|
||||
ServerVersionResponseDto releaseVersion;
|
||||
|
||||
ServerVersionResponseDto serverVersion;
|
||||
|
||||
ReleaseType type;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is ReleaseEventV1 &&
|
||||
other.checkedAt == checkedAt &&
|
||||
other.isAvailable == isAvailable &&
|
||||
other.releaseVersion == releaseVersion &&
|
||||
other.serverVersion == serverVersion &&
|
||||
other.type == type;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(checkedAt.hashCode) +
|
||||
(isAvailable.hashCode) +
|
||||
(releaseVersion.hashCode) +
|
||||
(serverVersion.hashCode) +
|
||||
(type.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'ReleaseEventV1[checkedAt=$checkedAt, isAvailable=$isAvailable, releaseVersion=$releaseVersion, serverVersion=$serverVersion, type=$type]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'checkedAt'] = this.checkedAt;
|
||||
json[r'isAvailable'] = this.isAvailable;
|
||||
json[r'releaseVersion'] = this.releaseVersion;
|
||||
json[r'serverVersion'] = this.serverVersion;
|
||||
json[r'type'] = this.type;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [ReleaseEventV1] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static ReleaseEventV1? fromJson(dynamic value) {
|
||||
upgradeDto(value, "ReleaseEventV1");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return ReleaseEventV1(
|
||||
checkedAt: mapValueOfType<String>(json, r'checkedAt')!,
|
||||
isAvailable: mapValueOfType<bool>(json, r'isAvailable')!,
|
||||
releaseVersion: ServerVersionResponseDto.fromJson(json[r'releaseVersion'])!,
|
||||
serverVersion: ServerVersionResponseDto.fromJson(json[r'serverVersion'])!,
|
||||
type: ReleaseType.fromJson(json[r'type'])!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<ReleaseEventV1> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <ReleaseEventV1>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = ReleaseEventV1.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, ReleaseEventV1> mapFromJson(dynamic json) {
|
||||
final map = <String, ReleaseEventV1>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = ReleaseEventV1.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of ReleaseEventV1-objects as value to a dart map
|
||||
static Map<String, List<ReleaseEventV1>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<ReleaseEventV1>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = ReleaseEventV1.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'checkedAt',
|
||||
'isAvailable',
|
||||
'releaseVersion',
|
||||
'serverVersion',
|
||||
'type',
|
||||
};
|
||||
}
|
||||
|
||||
-103
@@ -1,103 +0,0 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
|
||||
class ReleaseType {
|
||||
/// Instantiate a new enum with the provided [value].
|
||||
const ReleaseType._(this.value);
|
||||
|
||||
/// The underlying value of this enum member.
|
||||
final String value;
|
||||
|
||||
@override
|
||||
String toString() => value;
|
||||
|
||||
String toJson() => value;
|
||||
|
||||
static const major = ReleaseType._(r'major');
|
||||
static const premajor = ReleaseType._(r'premajor');
|
||||
static const minor = ReleaseType._(r'minor');
|
||||
static const preminor = ReleaseType._(r'preminor');
|
||||
static const patch_ = ReleaseType._(r'patch');
|
||||
static const prepatch = ReleaseType._(r'prepatch');
|
||||
static const prerelease = ReleaseType._(r'prerelease');
|
||||
static const release = ReleaseType._(r'release');
|
||||
|
||||
/// List of all possible values in this [enum][ReleaseType].
|
||||
static const values = <ReleaseType>[
|
||||
major,
|
||||
premajor,
|
||||
minor,
|
||||
preminor,
|
||||
patch_,
|
||||
prepatch,
|
||||
prerelease,
|
||||
release,
|
||||
];
|
||||
|
||||
static ReleaseType? fromJson(dynamic value) => ReleaseTypeTypeTransformer().decode(value);
|
||||
|
||||
static List<ReleaseType> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <ReleaseType>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = ReleaseType.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
}
|
||||
|
||||
/// Transformation class that can [encode] an instance of [ReleaseType] to String,
|
||||
/// and [decode] dynamic data back to [ReleaseType].
|
||||
class ReleaseTypeTypeTransformer {
|
||||
factory ReleaseTypeTypeTransformer() => _instance ??= const ReleaseTypeTypeTransformer._();
|
||||
|
||||
const ReleaseTypeTypeTransformer._();
|
||||
|
||||
String encode(ReleaseType data) => data.value;
|
||||
|
||||
/// Decodes a [dynamic value][data] to a ReleaseType.
|
||||
///
|
||||
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
||||
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
||||
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
||||
///
|
||||
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
||||
/// and users are still using an old app with the old code.
|
||||
ReleaseType? decode(dynamic data, {bool allowNull = true}) {
|
||||
if (data != null) {
|
||||
switch (data) {
|
||||
case r'major': return ReleaseType.major;
|
||||
case r'premajor': return ReleaseType.premajor;
|
||||
case r'minor': return ReleaseType.minor;
|
||||
case r'preminor': return ReleaseType.preminor;
|
||||
case r'patch': return ReleaseType.patch_;
|
||||
case r'prepatch': return ReleaseType.prepatch;
|
||||
case r'prerelease': return ReleaseType.prerelease;
|
||||
case r'release': return ReleaseType.release;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Singleton [ReleaseTypeTypeTransformer] instance.
|
||||
static ReleaseTypeTypeTransformer? _instance;
|
||||
}
|
||||
|
||||
+6
-22
@@ -16,61 +16,47 @@ class ServerVersionResponseDto {
|
||||
required this.major,
|
||||
required this.minor,
|
||||
required this.patch_,
|
||||
required this.prerelease,
|
||||
});
|
||||
|
||||
/// Major version number
|
||||
///
|
||||
/// Minimum value: 0
|
||||
/// Minimum value: -9007199254740991
|
||||
/// Maximum value: 9007199254740991
|
||||
int major;
|
||||
|
||||
/// Minor version number
|
||||
///
|
||||
/// Minimum value: 0
|
||||
/// Minimum value: -9007199254740991
|
||||
/// Maximum value: 9007199254740991
|
||||
int minor;
|
||||
|
||||
/// Patch version number
|
||||
///
|
||||
/// Minimum value: 0
|
||||
/// Minimum value: -9007199254740991
|
||||
/// Maximum value: 9007199254740991
|
||||
int patch_;
|
||||
|
||||
/// Pre-release version number
|
||||
///
|
||||
/// Minimum value: 0
|
||||
/// Maximum value: 9007199254740991
|
||||
int? prerelease;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is ServerVersionResponseDto &&
|
||||
other.major == major &&
|
||||
other.minor == minor &&
|
||||
other.patch_ == patch_ &&
|
||||
other.prerelease == prerelease;
|
||||
other.patch_ == patch_;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(major.hashCode) +
|
||||
(minor.hashCode) +
|
||||
(patch_.hashCode) +
|
||||
(prerelease == null ? 0 : prerelease!.hashCode);
|
||||
(patch_.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'ServerVersionResponseDto[major=$major, minor=$minor, patch_=$patch_, prerelease=$prerelease]';
|
||||
String toString() => 'ServerVersionResponseDto[major=$major, minor=$minor, patch_=$patch_]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'major'] = this.major;
|
||||
json[r'minor'] = this.minor;
|
||||
json[r'patch'] = this.patch_;
|
||||
if (this.prerelease != null) {
|
||||
json[r'prerelease'] = this.prerelease;
|
||||
} else {
|
||||
// json[r'prerelease'] = null;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -86,7 +72,6 @@ class ServerVersionResponseDto {
|
||||
major: mapValueOfType<int>(json, r'major')!,
|
||||
minor: mapValueOfType<int>(json, r'minor')!,
|
||||
patch_: mapValueOfType<int>(json, r'patch')!,
|
||||
prerelease: mapValueOfType<int>(json, r'prerelease'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
@@ -137,7 +122,6 @@ class ServerVersionResponseDto {
|
||||
'major',
|
||||
'minor',
|
||||
'patch',
|
||||
'prerelease',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -13,32 +13,26 @@ part of openapi.api;
|
||||
class SystemConfigNewVersionCheckDto {
|
||||
/// Returns a new [SystemConfigNewVersionCheckDto] instance.
|
||||
SystemConfigNewVersionCheckDto({
|
||||
required this.channel,
|
||||
required this.enabled,
|
||||
});
|
||||
|
||||
ReleaseChannel channel;
|
||||
|
||||
/// Enabled
|
||||
bool enabled;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is SystemConfigNewVersionCheckDto &&
|
||||
other.channel == channel &&
|
||||
other.enabled == enabled;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(channel.hashCode) +
|
||||
(enabled.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SystemConfigNewVersionCheckDto[channel=$channel, enabled=$enabled]';
|
||||
String toString() => 'SystemConfigNewVersionCheckDto[enabled=$enabled]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'channel'] = this.channel;
|
||||
json[r'enabled'] = this.enabled;
|
||||
return json;
|
||||
}
|
||||
@@ -52,7 +46,6 @@ class SystemConfigNewVersionCheckDto {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return SystemConfigNewVersionCheckDto(
|
||||
channel: ReleaseChannel.fromJson(json[r'channel'])!,
|
||||
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
||||
);
|
||||
}
|
||||
@@ -101,7 +94,6 @@ class SystemConfigNewVersionCheckDto {
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'channel',
|
||||
'enabled',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -116,7 +116,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));
|
||||
|
||||
when(() => mockSyncStreamRepo.updateUsersV1(any())).thenAnswer(successHandler);
|
||||
when(() => mockSyncStreamRepo.deleteUsersV1(any())).thenAnswer(successHandler);
|
||||
@@ -559,7 +559,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));
|
||||
|
||||
await sut.sync();
|
||||
|
||||
@@ -587,7 +587,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));
|
||||
await sut.sync();
|
||||
|
||||
verifyInOrder([
|
||||
@@ -617,7 +617,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));
|
||||
|
||||
await sut.sync();
|
||||
|
||||
|
||||
@@ -88,71 +88,5 @@ void main() {
|
||||
expect(version2.minor, 2);
|
||||
expect(version2.patch, 3);
|
||||
});
|
||||
|
||||
test('Orders later prerelease above earlier prerelease', () {
|
||||
const rc1 = SemVer(major: 1, minor: 151, patch: 0, prerelease: 1);
|
||||
const rc2 = SemVer(major: 1, minor: 151, patch: 0, prerelease: 2);
|
||||
expect(rc2 > rc1, isTrue);
|
||||
expect(rc1 < rc2, isTrue);
|
||||
expect(rc1 == rc2, isFalse);
|
||||
});
|
||||
|
||||
test('Final release outranks its prerelease of the same version', () {
|
||||
const rc = SemVer(major: 1, minor: 151, patch: 0, prerelease: 1);
|
||||
const release = SemVer(major: 1, minor: 151, patch: 0);
|
||||
expect(release > rc, isTrue);
|
||||
expect(rc < release, isTrue);
|
||||
});
|
||||
|
||||
test('Higher major outranks a prerelease regardless of ordinal', () {
|
||||
const rc = SemVer(major: 1, minor: 151, patch: 0, prerelease: 9);
|
||||
const next = SemVer(major: 2, minor: 0, patch: 0);
|
||||
expect(next > rc, isTrue);
|
||||
});
|
||||
|
||||
test('Equal prerelease versions compare as equal', () {
|
||||
const a = SemVer(major: 1, minor: 151, patch: 0, prerelease: 3);
|
||||
const b = SemVer(major: 1, minor: 151, patch: 0, prerelease: 3);
|
||||
expect(a == b, isTrue);
|
||||
expect(a > b, isFalse);
|
||||
expect(a < b, isFalse);
|
||||
});
|
||||
|
||||
test('Reports prerelease difference type', () {
|
||||
const rc1 = SemVer(major: 1, minor: 151, patch: 0, prerelease: 1);
|
||||
const rc2 = SemVer(major: 1, minor: 151, patch: 0, prerelease: 2);
|
||||
expect(rc1.differenceType(rc2), SemVerType.prerelease);
|
||||
});
|
||||
|
||||
test('toString includes prerelease suffix when present', () {
|
||||
const rc = SemVer(major: 1, minor: 151, patch: 0, prerelease: 2);
|
||||
expect(rc.toString(), '1.151.0-rc.2');
|
||||
});
|
||||
|
||||
test('Parses prerelease ordinal from -rc strings', () {
|
||||
final dotted = SemVer.fromString('1.151.0-rc.2');
|
||||
expect(dotted.major, 1);
|
||||
expect(dotted.minor, 151);
|
||||
expect(dotted.patch, 0);
|
||||
expect(dotted.prerelease, 2);
|
||||
|
||||
expect(SemVer.fromString('v1.151.0-rc.3').prerelease, 3);
|
||||
expect(SemVer.fromString('1.2.3-rc.2+build.5').prerelease, 2);
|
||||
});
|
||||
|
||||
test('Plain version string has null prerelease', () {
|
||||
expect(SemVer.fromString('3.0.0').prerelease, isNull);
|
||||
});
|
||||
|
||||
test('Invalid rc suffixes parse without error and have null prerelease', () {
|
||||
final debug = SemVer.fromString('1.2.3-debug');
|
||||
expect(debug.major, 1);
|
||||
expect(debug.minor, 2);
|
||||
expect(debug.patch, 3);
|
||||
expect(debug.prerelease, isNull);
|
||||
|
||||
expect(SemVer.fromString('1.2.3+build.5').prerelease, isNull);
|
||||
expect(SemVer.fromString('1.151.0-rc4').prerelease, isNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -20800,58 +20800,6 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ReleaseChannel": {
|
||||
"description": "Release channel",
|
||||
"enum": [
|
||||
"stable",
|
||||
"releaseCandidate"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ReleaseEventV1": {
|
||||
"properties": {
|
||||
"checkedAt": {
|
||||
"description": "When the server last checked for a latest version. As an ISO timestamp",
|
||||
"type": "string"
|
||||
},
|
||||
"isAvailable": {
|
||||
"description": "Whether a new version is available",
|
||||
"type": "boolean"
|
||||
},
|
||||
"releaseVersion": {
|
||||
"$ref": "#/components/schemas/ServerVersionResponseDto"
|
||||
},
|
||||
"serverVersion": {
|
||||
"$ref": "#/components/schemas/ServerVersionResponseDto"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/components/schemas/ReleaseType",
|
||||
"description": "Release type",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"checkedAt",
|
||||
"isAvailable",
|
||||
"releaseVersion",
|
||||
"serverVersion",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ReleaseType": {
|
||||
"enum": [
|
||||
"major",
|
||||
"premajor",
|
||||
"minor",
|
||||
"preminor",
|
||||
"patch",
|
||||
"prepatch",
|
||||
"prerelease",
|
||||
"release"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ReverseGeocodingStateResponseDto": {
|
||||
"properties": {
|
||||
"lastImportFileName": {
|
||||
@@ -21521,40 +21469,26 @@
|
||||
"major": {
|
||||
"description": "Major version number",
|
||||
"maximum": 9007199254740991,
|
||||
"minimum": 0,
|
||||
"minimum": -9007199254740991,
|
||||
"type": "integer"
|
||||
},
|
||||
"minor": {
|
||||
"description": "Minor version number",
|
||||
"maximum": 9007199254740991,
|
||||
"minimum": 0,
|
||||
"minimum": -9007199254740991,
|
||||
"type": "integer"
|
||||
},
|
||||
"patch": {
|
||||
"description": "Patch version number",
|
||||
"maximum": 9007199254740991,
|
||||
"minimum": 0,
|
||||
"minimum": -9007199254740991,
|
||||
"type": "integer"
|
||||
},
|
||||
"prerelease": {
|
||||
"description": "Pre-release version number",
|
||||
"maximum": 9007199254740991,
|
||||
"minimum": 0,
|
||||
"nullable": true,
|
||||
"type": "integer",
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v3.0.0",
|
||||
"state": "Added"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"major",
|
||||
"minor",
|
||||
"patch",
|
||||
"prerelease"
|
||||
"patch"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
@@ -24575,16 +24509,12 @@
|
||||
},
|
||||
"SystemConfigNewVersionCheckDto": {
|
||||
"properties": {
|
||||
"channel": {
|
||||
"$ref": "#/components/schemas/ReleaseChannel"
|
||||
},
|
||||
"enabled": {
|
||||
"description": "Enabled",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"channel",
|
||||
"enabled"
|
||||
],
|
||||
"type": "object"
|
||||
|
||||
@@ -2074,8 +2074,6 @@ export type ServerVersionResponseDto = {
|
||||
minor: number;
|
||||
/** Patch version number */
|
||||
patch: number;
|
||||
/** Pre-release version number */
|
||||
prerelease: number | null;
|
||||
};
|
||||
export type VersionCheckStateResponseDto = {
|
||||
/** Last check timestamp */
|
||||
@@ -2423,7 +2421,6 @@ export type SystemConfigMetadataDto = {
|
||||
faces: SystemConfigFacesDto;
|
||||
};
|
||||
export type SystemConfigNewVersionCheckDto = {
|
||||
channel: ReleaseChannel;
|
||||
/** Enabled */
|
||||
enabled: boolean;
|
||||
};
|
||||
@@ -2769,16 +2766,6 @@ export type WorkflowShareResponseDto = {
|
||||
trigger: WorkflowTrigger;
|
||||
};
|
||||
export type LicenseResponseDto = UserLicense;
|
||||
export type ReleaseEventV1 = {
|
||||
/** When the server last checked for a latest version. As an ISO timestamp */
|
||||
checkedAt: string;
|
||||
/** Whether a new version is available */
|
||||
isAvailable: boolean;
|
||||
releaseVersion: ServerVersionResponseDto;
|
||||
serverVersion: ServerVersionResponseDto;
|
||||
/** Release type */
|
||||
"type": ReleaseType;
|
||||
};
|
||||
export type SyncAckV1 = {};
|
||||
export type SyncAlbumDeleteV1 = {
|
||||
/** Album ID */
|
||||
@@ -7318,10 +7305,6 @@ export enum LogLevel {
|
||||
Error = "error",
|
||||
Fatal = "fatal"
|
||||
}
|
||||
export enum ReleaseChannel {
|
||||
Stable = "stable",
|
||||
ReleaseCandidate = "releaseCandidate"
|
||||
}
|
||||
export enum OAuthTokenEndpointAuthMethod {
|
||||
ClientSecretPost = "client_secret_post",
|
||||
ClientSecretBasic = "client_secret_basic"
|
||||
@@ -7330,16 +7313,6 @@ export enum AssetOrderBy {
|
||||
TakenAt = "takenAt",
|
||||
CreatedAt = "createdAt"
|
||||
}
|
||||
export enum ReleaseType {
|
||||
Major = "major",
|
||||
Premajor = "premajor",
|
||||
Minor = "minor",
|
||||
Preminor = "preminor",
|
||||
Patch = "patch",
|
||||
Prepatch = "prepatch",
|
||||
Prerelease = "prerelease",
|
||||
Release = "release"
|
||||
}
|
||||
export enum UserMetadataKey {
|
||||
Preferences = "preferences",
|
||||
License = "license",
|
||||
|
||||
Generated
+20
-27
@@ -571,8 +571,8 @@ importers:
|
||||
specifier: ^1.6.3
|
||||
version: 1.6.4
|
||||
semver:
|
||||
specifier: ^7.8.1
|
||||
version: 7.8.1
|
||||
specifier: ^7.6.2
|
||||
version: 7.8.0
|
||||
sharp:
|
||||
specifier: ^0.34.5
|
||||
version: 0.34.5
|
||||
@@ -11243,11 +11243,6 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
semver@7.8.1:
|
||||
resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
send@0.19.2:
|
||||
resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -16305,7 +16300,7 @@ snapshots:
|
||||
nopt: 5.0.0
|
||||
npmlog: 5.0.1
|
||||
rimraf: 3.0.2
|
||||
semver: 7.8.1
|
||||
semver: 7.8.0
|
||||
tar: 6.2.1
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
@@ -17762,7 +17757,7 @@ snapshots:
|
||||
'@testing-library/dom@10.4.1':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.29.0
|
||||
'@babel/runtime': 7.29.7
|
||||
'@babel/runtime': 7.29.2
|
||||
'@types/aria-query': 5.0.4
|
||||
aria-query: 5.3.0
|
||||
dom-accessibility-api: 0.5.16
|
||||
@@ -18466,7 +18461,7 @@ snapshots:
|
||||
'@typescript-eslint/visitor-keys': 8.59.4
|
||||
debug: 4.4.3
|
||||
minimatch: 10.2.5
|
||||
semver: 7.8.1
|
||||
semver: 7.8.0
|
||||
tinyglobby: 0.2.16
|
||||
ts-api-utils: 2.5.0(typescript@6.0.3)
|
||||
typescript: 6.0.3
|
||||
@@ -19566,7 +19561,7 @@ snapshots:
|
||||
dot-prop: 10.1.0
|
||||
env-paths: 3.0.0
|
||||
json-schema-typed: 8.0.2
|
||||
semver: 7.8.1
|
||||
semver: 7.8.0
|
||||
uint8array-extras: 1.5.0
|
||||
|
||||
config-chain@1.1.13:
|
||||
@@ -19738,7 +19733,7 @@ snapshots:
|
||||
postcss-modules-scope: 3.2.1(postcss@8.5.15)
|
||||
postcss-modules-values: 4.0.0(postcss@8.5.15)
|
||||
postcss-value-parser: 4.2.0
|
||||
semver: 7.8.1
|
||||
semver: 7.8.0
|
||||
optionalDependencies:
|
||||
webpack: 5.107.0(postcss@8.5.15)
|
||||
|
||||
@@ -20606,7 +20601,7 @@ snapshots:
|
||||
find-up: 5.0.0
|
||||
globals: 15.15.0
|
||||
lodash.memoize: 4.1.2
|
||||
semver: 7.8.1
|
||||
semver: 7.8.0
|
||||
|
||||
eslint-plugin-prettier@5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.4.0(jiti@2.7.0)))(eslint@10.4.0(jiti@2.7.0))(prettier@3.8.3):
|
||||
dependencies:
|
||||
@@ -20629,7 +20624,7 @@ snapshots:
|
||||
postcss: 8.5.15
|
||||
postcss-load-config: 3.1.4(postcss@8.5.15)
|
||||
postcss-safe-parser: 7.0.1(postcss@8.5.15)
|
||||
semver: 7.8.1
|
||||
semver: 7.8.0
|
||||
svelte-eslint-parser: 1.6.1(svelte@5.55.8(@typescript-eslint/types@8.59.4))
|
||||
optionalDependencies:
|
||||
svelte: 5.55.8(@typescript-eslint/types@8.59.4)
|
||||
@@ -21107,7 +21102,7 @@ snapshots:
|
||||
minimatch: 3.1.5
|
||||
node-abort-controller: 3.1.1
|
||||
schema-utils: 3.3.0
|
||||
semver: 7.8.1
|
||||
semver: 7.8.0
|
||||
tapable: 2.3.3
|
||||
typescript: 5.9.3
|
||||
webpack: 5.106.0(@swc/core@1.15.33(@swc/helpers@0.5.22))(esbuild@0.28.0)(lightningcss@1.32.0)
|
||||
@@ -21543,7 +21538,7 @@ snapshots:
|
||||
|
||||
history@4.10.1:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.7
|
||||
'@babel/runtime': 7.29.2
|
||||
loose-envify: 1.4.0
|
||||
resolve-pathname: 3.0.0
|
||||
tiny-invariant: 1.3.3
|
||||
@@ -22131,7 +22126,7 @@ snapshots:
|
||||
lodash.isstring: 4.0.1
|
||||
lodash.once: 4.1.1
|
||||
ms: 2.1.3
|
||||
semver: 7.8.1
|
||||
semver: 7.8.0
|
||||
|
||||
just-compare@2.3.0: {}
|
||||
|
||||
@@ -22417,7 +22412,7 @@ snapshots:
|
||||
|
||||
make-dir@4.0.0:
|
||||
dependencies:
|
||||
semver: 7.8.1
|
||||
semver: 7.8.0
|
||||
|
||||
maplibre-gl@5.24.0:
|
||||
dependencies:
|
||||
@@ -23252,7 +23247,7 @@ snapshots:
|
||||
|
||||
node-abi@3.92.0:
|
||||
dependencies:
|
||||
semver: 7.8.1
|
||||
semver: 7.8.0
|
||||
optional: true
|
||||
|
||||
node-abort-controller@3.1.1: {}
|
||||
@@ -23293,7 +23288,7 @@ snapshots:
|
||||
graceful-fs: 4.2.11
|
||||
nopt: 9.0.0
|
||||
proc-log: 6.1.0
|
||||
semver: 7.8.1
|
||||
semver: 7.8.0
|
||||
tar: 7.5.15
|
||||
tinyglobby: 0.2.16
|
||||
undici: 6.25.0
|
||||
@@ -23531,7 +23526,7 @@ snapshots:
|
||||
got: 12.6.1
|
||||
registry-auth-token: 5.1.1
|
||||
registry-url: 6.0.1
|
||||
semver: 7.8.1
|
||||
semver: 7.8.0
|
||||
|
||||
package-manager-detector@1.6.0: {}
|
||||
|
||||
@@ -23919,7 +23914,7 @@ snapshots:
|
||||
cosmiconfig: 8.3.6(typescript@6.0.3)
|
||||
jiti: 1.21.7
|
||||
postcss: 8.5.15
|
||||
semver: 7.8.1
|
||||
semver: 7.8.0
|
||||
webpack: 5.107.0(postcss@8.5.15)
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
@@ -24974,14 +24969,12 @@ snapshots:
|
||||
|
||||
semver-diff@4.0.0:
|
||||
dependencies:
|
||||
semver: 7.8.1
|
||||
semver: 7.8.0
|
||||
|
||||
semver@6.3.1: {}
|
||||
|
||||
semver@7.8.0: {}
|
||||
|
||||
semver@7.8.1: {}
|
||||
|
||||
send@0.19.2:
|
||||
dependencies:
|
||||
debug: 2.6.9
|
||||
@@ -25516,7 +25509,7 @@ snapshots:
|
||||
postcss: 8.5.15
|
||||
postcss-scss: 4.0.9(postcss@8.5.15)
|
||||
postcss-selector-parser: 7.1.1
|
||||
semver: 7.8.1
|
||||
semver: 7.8.0
|
||||
optionalDependencies:
|
||||
svelte: 5.55.8(@typescript-eslint/types@8.59.4)
|
||||
|
||||
@@ -26224,7 +26217,7 @@ snapshots:
|
||||
is-yarn-global: 0.4.1
|
||||
latest-version: 7.0.0
|
||||
pupa: 3.3.0
|
||||
semver: 7.8.1
|
||||
semver: 7.8.0
|
||||
semver-diff: 4.0.0
|
||||
xdg-basedir: 5.1.0
|
||||
|
||||
|
||||
+1
-1
@@ -106,7 +106,7 @@
|
||||
"reflect-metadata": "^0.2.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"semver": "^7.8.1",
|
||||
"semver": "^7.6.2",
|
||||
"sharp": "^0.34.5",
|
||||
"sirv": "^3.0.0",
|
||||
"socket.io": "^4.8.1",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { CronExpression } from '@nestjs/schedule';
|
||||
import { ReleaseChannel } from 'src/dtos/system-config.dto';
|
||||
import {
|
||||
AudioCodec,
|
||||
Colorspace,
|
||||
@@ -136,7 +135,6 @@ export type SystemConfig = {
|
||||
};
|
||||
newVersionCheck: {
|
||||
enabled: boolean;
|
||||
channel: ReleaseChannel;
|
||||
};
|
||||
nightlyTasks: {
|
||||
startTime: string;
|
||||
@@ -346,7 +344,6 @@ export const defaults = Object.freeze<SystemConfig>({
|
||||
},
|
||||
newVersionCheck: {
|
||||
enabled: true,
|
||||
channel: ReleaseChannel.Stable,
|
||||
},
|
||||
nightlyTasks: {
|
||||
startTime: '00:00',
|
||||
|
||||
@@ -265,13 +265,3 @@ export class HistoryBuilder {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
export const extraModels: Function[] = [];
|
||||
|
||||
export const ExtraModel = (): ClassDecorator => {
|
||||
// eslint-disable-next-line unicorn/consistent-function-scoping, @typescript-eslint/no-unsafe-function-type
|
||||
return (object: Function) => {
|
||||
extraModels.push(object);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { createZodDto } from 'nestjs-zod';
|
||||
import type { SemVer } from 'semver';
|
||||
import { ExtraModel, HistoryBuilder } from 'src/decorators';
|
||||
import { isoDatetimeToDate } from 'src/validation';
|
||||
import z from 'zod';
|
||||
|
||||
@@ -59,15 +58,9 @@ const ServerStorageResponseSchema = z
|
||||
|
||||
const ServerVersionResponseSchema = z
|
||||
.object({
|
||||
major: z.int().min(0).describe('Major version number'),
|
||||
minor: z.int().min(0).describe('Minor version number'),
|
||||
patch: z.int().min(0).describe('Patch version number'),
|
||||
prerelease: z
|
||||
.int()
|
||||
.min(0)
|
||||
.nullable()
|
||||
.meta(HistoryBuilder.v3().getExtensions())
|
||||
.describe('Pre-release version number'),
|
||||
major: z.int().describe('Major version number'),
|
||||
minor: z.int().describe('Minor version number'),
|
||||
patch: z.int().describe('Patch version number'),
|
||||
})
|
||||
.meta({ id: 'ServerVersionResponseDto' });
|
||||
|
||||
@@ -147,27 +140,6 @@ const ServerFeaturesSchema = z
|
||||
})
|
||||
.meta({ id: 'ServerFeaturesDto' });
|
||||
|
||||
export enum ReleaseType {
|
||||
Major = 'major',
|
||||
Premajor = 'premajor',
|
||||
Minor = 'minor',
|
||||
Preminor = 'preminor',
|
||||
Patch = 'patch',
|
||||
Prepatch = 'prepatch',
|
||||
Prerelease = 'prerelease',
|
||||
Release = 'release',
|
||||
}
|
||||
|
||||
const ReleaseTypeSchema = z.enum(ReleaseType).meta({ id: 'ReleaseType' }).describe('Release type');
|
||||
|
||||
const ReleaseEventV1Schema = z.object({
|
||||
isAvailable: z.boolean().describe('Whether a new version is available'),
|
||||
checkedAt: z.string().describe('When the server last checked for a latest version. As an ISO timestamp'),
|
||||
serverVersion: ServerVersionResponseSchema,
|
||||
releaseVersion: ServerVersionResponseSchema,
|
||||
type: ReleaseTypeSchema.nullable(),
|
||||
});
|
||||
|
||||
export class ServerPingResponse extends createZodDto(ServerPingResponseSchema) {}
|
||||
export class ServerAboutResponseDto extends createZodDto(ServerAboutResponseSchema) {}
|
||||
export class ServerApkLinksDto extends createZodDto(ServerApkLinksSchema) {}
|
||||
@@ -175,12 +147,7 @@ export class ServerStorageResponseDto extends createZodDto(ServerStorageResponse
|
||||
|
||||
export class ServerVersionResponseDto extends createZodDto(ServerVersionResponseSchema) {
|
||||
static fromSemVer(value: SemVer): z.infer<typeof ServerVersionResponseSchema> {
|
||||
return {
|
||||
major: value.major,
|
||||
minor: value.minor,
|
||||
patch: value.patch,
|
||||
prerelease: (value.prerelease[1] as number) ?? null,
|
||||
};
|
||||
return { major: value.major, minor: value.minor, patch: value.patch };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,5 +158,10 @@ export class ServerMediaTypesResponseDto extends createZodDto(ServerMediaTypesRe
|
||||
export class ServerConfigDto extends createZodDto(ServerConfigSchema) {}
|
||||
export class ServerFeaturesDto extends createZodDto(ServerFeaturesSchema) {}
|
||||
|
||||
@ExtraModel()
|
||||
export class ReleaseEventV1 extends createZodDto(ReleaseEventV1Schema) {}
|
||||
export interface ReleaseNotification {
|
||||
isAvailable: boolean;
|
||||
/** ISO8601 */
|
||||
checkedAt: string;
|
||||
serverVersion: ServerVersionResponseDto;
|
||||
releaseVersion: ServerVersionResponseDto;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
|
||||
import { createZodDto } from 'nestjs-zod';
|
||||
import { ExtraModel } from 'src/decorators';
|
||||
import { AssetEditActionSchema } from 'src/dtos/editing.dto';
|
||||
import {
|
||||
AlbumUserRole,
|
||||
@@ -17,6 +17,15 @@ import {
|
||||
import { isoDatetimeToDate } from 'src/validation';
|
||||
import z from 'zod';
|
||||
|
||||
export const extraSyncModels: Function[] = [];
|
||||
|
||||
const ExtraModel = (): ClassDecorator => {
|
||||
// eslint-disable-next-line unicorn/consistent-function-scoping
|
||||
return (object: Function) => {
|
||||
extraSyncModels.push(object);
|
||||
};
|
||||
};
|
||||
|
||||
const SyncUserV1Schema = z
|
||||
.object({
|
||||
id: z.string().describe('User ID'),
|
||||
|
||||
@@ -151,15 +151,8 @@ const SystemConfigMapSchema = z
|
||||
})
|
||||
.meta({ id: 'SystemConfigMapDto' });
|
||||
|
||||
export enum ReleaseChannel {
|
||||
Stable = 'stable',
|
||||
ReleaseCandidate = 'releaseCandidate',
|
||||
}
|
||||
|
||||
const ReleaseChannelSchema = z.enum(ReleaseChannel).describe('Release channel').meta({ id: 'ReleaseChannel' });
|
||||
|
||||
const SystemConfigNewVersionCheckSchema = z
|
||||
.object({ enabled: configBool.describe('Enabled'), channel: ReleaseChannelSchema })
|
||||
.object({ enabled: configBool.describe('Enabled') })
|
||||
.meta({ id: 'SystemConfigNewVersionCheckDto' });
|
||||
|
||||
const SystemConfigNightlyTasksSchema = z
|
||||
|
||||
@@ -4,7 +4,6 @@ import { exec as execCallback } from 'node:child_process';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { promisify } from 'node:util';
|
||||
import sharp from 'sharp';
|
||||
import { ReleaseChannel } from 'src/dtos/system-config.dto';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
|
||||
@@ -65,12 +64,10 @@ export class ServerInfoRepository {
|
||||
this.logger.setContext(ServerInfoRepository.name);
|
||||
}
|
||||
|
||||
async getLatestRelease(channel: ReleaseChannel): Promise<VersionResponse> {
|
||||
async getLatestRelease(): Promise<VersionResponse> {
|
||||
try {
|
||||
const { versionCheck } = this.configRepository.getEnv();
|
||||
const url = new URL(versionCheck.url);
|
||||
url.searchParams.append('channel', channel);
|
||||
const response = await fetch(url);
|
||||
const response = await fetch(versionCheck.url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Version check request failed with status ${response.status}: ${await response.text()}`);
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Server, Socket } from 'socket.io';
|
||||
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { NotificationDto } from 'src/dtos/notification.dto';
|
||||
import { ReleaseEventV1, ServerVersionResponseDto } from 'src/dtos/server.dto';
|
||||
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
|
||||
import { SyncAssetEditV1, SyncAssetExifV1, SyncAssetV2 } from 'src/dtos/sync.dto';
|
||||
import { AppRestartEvent, ArgsOf, EventRepository } from 'src/repositories/event.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
@@ -31,7 +31,7 @@ export interface ClientEventMap {
|
||||
on_person_thumbnail: [string];
|
||||
on_server_version: [ServerVersionResponseDto];
|
||||
on_config_update: [];
|
||||
on_new_release: [ReleaseEventV1];
|
||||
on_new_release: [ReleaseNotification];
|
||||
on_notification: [NotificationDto];
|
||||
on_session_delete: [string];
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { defaults, SystemConfig } from 'src/config';
|
||||
import { ReleaseChannel } from 'src/dtos/system-config.dto';
|
||||
import {
|
||||
AudioCodec,
|
||||
Colorspace,
|
||||
@@ -185,7 +184,6 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
||||
},
|
||||
newVersionCheck: {
|
||||
enabled: true,
|
||||
channel: ReleaseChannel.Stable,
|
||||
},
|
||||
trash: {
|
||||
enabled: true,
|
||||
|
||||
@@ -2,7 +2,6 @@ import { DateTime } from 'luxon';
|
||||
import { SemVer } from 'semver';
|
||||
import { defaults } from 'src/config';
|
||||
import { serverVersion } from 'src/constants';
|
||||
import { ReleaseChannel } from 'src/dtos/system-config.dto';
|
||||
import { CronJob, JobName, JobStatus, SystemMetadataKey } from 'src/enum';
|
||||
import { VersionService } from 'src/services/version.service';
|
||||
import { factory } from 'test/small.factory';
|
||||
@@ -23,17 +22,6 @@ describe(VersionService.name, () => {
|
||||
mocks.cron.update.mockResolvedValue();
|
||||
});
|
||||
|
||||
beforeAll(() => {
|
||||
vitest.mock(import('src/constants.js'), async () => ({
|
||||
...(await vitest.importActual<typeof import('src/constants.js')>('src/constants.js')),
|
||||
serverVersion: new SemVer('v3.0.0'),
|
||||
}));
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vitest.unmock(import('src/constants.js'));
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
expect(sut).toBeDefined();
|
||||
});
|
||||
@@ -78,10 +66,9 @@ describe(VersionService.name, () => {
|
||||
describe('getVersion', () => {
|
||||
it('should respond the server version', () => {
|
||||
expect(sut.getVersion()).toEqual({
|
||||
major: 3,
|
||||
minor: 0,
|
||||
patch: 0,
|
||||
prerelease: null,
|
||||
major: serverVersion.major,
|
||||
minor: serverVersion.minor,
|
||||
patch: serverVersion.patch,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -156,24 +143,24 @@ describe(VersionService.name, () => {
|
||||
describe('onConfigUpdate', () => {
|
||||
it('should queue a version check job when newVersionCheck is enabled', async () => {
|
||||
await sut.onConfigUpdate({
|
||||
oldConfig: { ...defaults, newVersionCheck: { enabled: false, channel: ReleaseChannel.Stable } },
|
||||
newConfig: { ...defaults, newVersionCheck: { enabled: true, channel: ReleaseChannel.Stable } },
|
||||
oldConfig: { ...defaults, newVersionCheck: { enabled: false } },
|
||||
newConfig: { ...defaults, newVersionCheck: { enabled: true } },
|
||||
});
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.VersionCheck, data: {} });
|
||||
});
|
||||
|
||||
it('should not queue a version check job when newVersionCheck is disabled', async () => {
|
||||
await sut.onConfigUpdate({
|
||||
oldConfig: { ...defaults, newVersionCheck: { enabled: true, channel: ReleaseChannel.Stable } },
|
||||
newConfig: { ...defaults, newVersionCheck: { enabled: false, channel: ReleaseChannel.Stable } },
|
||||
oldConfig: { ...defaults, newVersionCheck: { enabled: true } },
|
||||
newConfig: { ...defaults, newVersionCheck: { enabled: false } },
|
||||
});
|
||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not queue a version check job when newVersionCheck was already enabled', async () => {
|
||||
await sut.onConfigUpdate({
|
||||
oldConfig: { ...defaults, newVersionCheck: { enabled: true, channel: ReleaseChannel.Stable } },
|
||||
newConfig: { ...defaults, newVersionCheck: { enabled: true, channel: ReleaseChannel.Stable } },
|
||||
oldConfig: { ...defaults, newVersionCheck: { enabled: true } },
|
||||
newConfig: { ...defaults, newVersionCheck: { enabled: true } },
|
||||
});
|
||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -182,36 +169,21 @@ describe(VersionService.name, () => {
|
||||
describe('onWebsocketConnection', () => {
|
||||
it('should send on_server_version client event', async () => {
|
||||
await sut.onWebsocketConnection({ userId: '42' });
|
||||
expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_server_version', '42', {
|
||||
major: 3,
|
||||
minor: 0,
|
||||
patch: 0,
|
||||
prerelease: null,
|
||||
});
|
||||
expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_server_version', '42', expect.any(SemVer));
|
||||
expect(mocks.websocket.clientSend).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should also send a new release notification', async () => {
|
||||
mocks.systemMetadata.get.mockResolvedValue({ checkedAt: '2024-01-01', releaseVersion: 'v1.42.0' });
|
||||
await sut.onWebsocketConnection({ userId: '42' });
|
||||
expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_server_version', '42', {
|
||||
major: 3,
|
||||
minor: 0,
|
||||
patch: 0,
|
||||
prerelease: null,
|
||||
});
|
||||
expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_server_version', '42', expect.any(SemVer));
|
||||
expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_new_release', '42', expect.any(Object));
|
||||
});
|
||||
|
||||
it('should not send a release notification when the version check is disabled', async () => {
|
||||
mocks.systemMetadata.get.mockResolvedValueOnce({ newVersionCheck: { enabled: false } });
|
||||
await sut.onWebsocketConnection({ userId: '42' });
|
||||
expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_server_version', '42', {
|
||||
major: 3,
|
||||
minor: 0,
|
||||
patch: 0,
|
||||
prerelease: null,
|
||||
});
|
||||
expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_server_version', '42', expect.any(SemVer));
|
||||
expect(mocks.websocket.clientSend).not.toHaveBeenCalledWith('on_new_release', '42', expect.any(Object));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,27 +3,19 @@ import { DateTime } from 'luxon';
|
||||
import semver, { SemVer } from 'semver';
|
||||
import { serverVersion } from 'src/constants';
|
||||
import { OnEvent, OnJob } from 'src/decorators';
|
||||
import { ReleaseEventV1, ReleaseType, ServerVersionResponseDto } from 'src/dtos/server.dto';
|
||||
import { ReleaseChannel } from 'src/dtos/system-config.dto';
|
||||
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
|
||||
import { CronJob, DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName, SystemMetadataKey } from 'src/enum';
|
||||
import { ArgOf } from 'src/repositories/event.repository';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { VersionCheckMetadata } from 'src/types';
|
||||
import { handlePromiseError } from 'src/utils/misc';
|
||||
|
||||
const asNotification = (
|
||||
channel: ReleaseChannel,
|
||||
{ checkedAt, releaseVersion }: VersionCheckMetadata,
|
||||
): ReleaseEventV1 => {
|
||||
const asNotification = ({ checkedAt, releaseVersion }: VersionCheckMetadata): ReleaseNotification => {
|
||||
return {
|
||||
// can't use gt because it's broken for release candidates F https://github.com/npm/node-semver/issues/483
|
||||
isAvailable: semver.intersects(`>${serverVersion}`, releaseVersion.toString(), {
|
||||
includePrerelease: channel === ReleaseChannel.ReleaseCandidate,
|
||||
}),
|
||||
isAvailable: semver.gt(releaseVersion, serverVersion),
|
||||
checkedAt,
|
||||
serverVersion: ServerVersionResponseDto.fromSemVer(serverVersion),
|
||||
releaseVersion: ServerVersionResponseDto.fromSemVer(new SemVer(releaseVersion)),
|
||||
type: semver.diff(serverVersion, releaseVersion) as ReleaseType,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -106,21 +98,14 @@ export class VersionService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
const { version: releaseVersion, published_at: publishedAt } = await this.serverInfoRepository.getLatestRelease(
|
||||
newVersionCheck.channel,
|
||||
);
|
||||
const { version: releaseVersion, published_at: publishedAt } = await this.serverInfoRepository.getLatestRelease();
|
||||
const metadata: VersionCheckMetadata = { checkedAt: DateTime.utc().toISO(), releaseVersion };
|
||||
|
||||
await this.systemMetadataRepository.set(SystemMetadataKey.VersionCheckState, metadata);
|
||||
|
||||
// can't use gt because it's broken for release candidates F https://github.com/npm/node-semver/issues/483
|
||||
if (
|
||||
semver.intersects(`>${serverVersion}`, releaseVersion.toString(), {
|
||||
includePrerelease: newVersionCheck.channel === ReleaseChannel.ReleaseCandidate,
|
||||
})
|
||||
) {
|
||||
if (semver.gt(releaseVersion, serverVersion)) {
|
||||
this.logger.log(`Found ${releaseVersion}, released at ${new Date(publishedAt).toLocaleString()}`);
|
||||
this.websocketRepository.clientBroadcast('on_new_release', asNotification(newVersionCheck.channel, metadata));
|
||||
this.websocketRepository.clientBroadcast('on_new_release', asNotification(metadata));
|
||||
}
|
||||
} catch (error: Error | any) {
|
||||
this.logger.warn(`Unable to run version check: ${error}\n${error?.stack}`);
|
||||
@@ -132,11 +117,7 @@ export class VersionService extends BaseService {
|
||||
|
||||
@OnEvent({ name: 'WebsocketConnect' })
|
||||
async onWebsocketConnection({ userId }: ArgOf<'WebsocketConnect'>) {
|
||||
this.websocketRepository.clientSend(
|
||||
'on_server_version',
|
||||
userId,
|
||||
ServerVersionResponseDto.fromSemVer(serverVersion),
|
||||
);
|
||||
this.websocketRepository.clientSend('on_server_version', userId, serverVersion);
|
||||
|
||||
const { newVersionCheck } = await this.getConfig({ withCache: true });
|
||||
if (!newVersionCheck.enabled) {
|
||||
@@ -145,7 +126,7 @@ export class VersionService extends BaseService {
|
||||
|
||||
const metadata = await this.systemMetadataRepository.get(SystemMetadataKey.VersionCheckState);
|
||||
if (metadata) {
|
||||
this.websocketRepository.clientSend('on_new_release', userId, asNotification(newVersionCheck.channel, metadata));
|
||||
this.websocketRepository.clientSend('on_new_release', userId, asNotification(metadata));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import picomatch from 'picomatch';
|
||||
import parse from 'picomatch/lib/parse';
|
||||
import { SystemConfig } from 'src/config';
|
||||
import { CLIP_MODEL_INFO, endpointTags, serverVersion } from 'src/constants';
|
||||
import { extraModels } from 'src/decorators';
|
||||
import { extraSyncModels } from 'src/dtos/sync.dto';
|
||||
import { ApiCustomExtension, ImmichCookie, ImmichHeader, MetadataKey } from 'src/enum';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
|
||||
@@ -289,7 +289,7 @@ export const useSwagger = (app: INestApplication, { write }: { write: boolean })
|
||||
|
||||
const options: SwaggerDocumentOptions = {
|
||||
operationIdFactory: (controllerKey: string, methodKey: string) => methodKey,
|
||||
extraModels,
|
||||
extraModels: extraSyncModels,
|
||||
ignoreGlobalPrefix: true,
|
||||
};
|
||||
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
import ServerAboutModal from '$lib/modals/ServerAboutModal.svelte';
|
||||
import { userInteraction } from '$lib/stores/user.svelte';
|
||||
import { websocketStore } from '$lib/stores/websocket';
|
||||
import type { ReleaseEvent } from '$lib/types';
|
||||
import { semverToName } from '$lib/utils';
|
||||
import { requestServerInfo } from '$lib/utils/auth';
|
||||
import {
|
||||
getAboutInfo,
|
||||
getVersionHistory,
|
||||
type ReleaseEventV1,
|
||||
type ServerAboutResponseDto,
|
||||
type ServerVersionHistoryResponseDto,
|
||||
} from '@immich/sdk';
|
||||
@@ -35,9 +35,11 @@
|
||||
userInteraction.versions = versions;
|
||||
});
|
||||
let isMain = $derived(info?.sourceRef === 'main' && info.repository === 'immich-app/immich');
|
||||
let version = $derived($serverVersion ? semverToName($serverVersion) : null);
|
||||
let version = $derived(
|
||||
$serverVersion ? `v${$serverVersion.major}.${$serverVersion.minor}.${$serverVersion.patch}` : null,
|
||||
);
|
||||
|
||||
const getReleaseInfo = (release?: ReleaseEventV1) => {
|
||||
const getReleaseInfo = (release?: ReleaseEvent) => {
|
||||
if (!release || !release?.isAvailable || !authManager.user.isAdmin) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -7,13 +7,13 @@ import type {
|
||||
LoginResponseDto,
|
||||
PersonResponseDto,
|
||||
QueueResponseDto,
|
||||
ReleaseEventV1,
|
||||
SharedLinkResponseDto,
|
||||
SystemConfigDto,
|
||||
TagResponseDto,
|
||||
UserAdminResponseDto,
|
||||
WorkflowResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import type { ReleaseEvent } from '$lib/types';
|
||||
import { BaseEventManager } from '$lib/utils/base-event-manager.svelte';
|
||||
import type { TreeNode } from '$lib/utils/tree-utils';
|
||||
|
||||
@@ -86,7 +86,7 @@ export type Events = {
|
||||
WorkflowUpdate: [WorkflowResponseDto];
|
||||
WorkflowDelete: [WorkflowResponseDto];
|
||||
|
||||
ReleaseEvent: [ReleaseEventV1];
|
||||
ReleaseEvent: [ReleaseEvent];
|
||||
|
||||
WebsocketConnect: [];
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { ReleaseEventV1 } from '@immich/sdk';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import { type ReleaseEvent } from '$lib/types';
|
||||
|
||||
class ReleaseManager {
|
||||
value = $state<ReleaseEventV1 | undefined>();
|
||||
value = $state<ReleaseEvent | undefined>();
|
||||
|
||||
constructor() {
|
||||
eventManager.on({
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
type AssetResponseDto,
|
||||
type MaintenanceStatusResponseDto,
|
||||
type NotificationDto,
|
||||
type ReleaseEventV1,
|
||||
type ServerVersionResponseDto,
|
||||
type SyncAssetEditV1,
|
||||
type SyncAssetV2,
|
||||
@@ -16,6 +15,7 @@ import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { maintenanceStore } from '$lib/stores/maintenance.store';
|
||||
import { notificationManager } from '$lib/stores/notification-manager.svelte';
|
||||
import type { ReleaseEvent } from '$lib/types';
|
||||
import { createEventEmitter } from '$lib/utils/eventemitter';
|
||||
|
||||
interface AppRestartEvent {
|
||||
@@ -34,7 +34,7 @@ export interface Events {
|
||||
on_person_thumbnail: (personId: string) => void;
|
||||
on_server_version: (serverVersion: ServerVersionResponseDto) => void;
|
||||
on_config_update: () => void;
|
||||
on_new_release: (event: ReleaseEventV1) => void;
|
||||
on_new_release: (event: ReleaseEvent) => void;
|
||||
on_session_delete: (sessionId: string) => void;
|
||||
on_notification: (notification: NotificationDto) => void;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { QueueResponseDto } from '@immich/sdk';
|
||||
import type { QueueResponseDto, ServerVersionResponseDto } from '@immich/sdk';
|
||||
import type { ActionItem } from '@immich/ui';
|
||||
import type { DateTime } from 'luxon';
|
||||
import type { SvelteSet } from 'svelte/reactivity';
|
||||
@@ -7,6 +7,14 @@ import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
|
||||
export type LatLng = { lng: number; lat: number };
|
||||
|
||||
export interface ReleaseEvent {
|
||||
isAvailable: boolean;
|
||||
/** ISO8601 */
|
||||
checkedAt: string;
|
||||
serverVersion: ServerVersionResponseDto;
|
||||
releaseVersion: ServerVersionResponseDto;
|
||||
}
|
||||
|
||||
export type QueueSnapshot = { timestamp: number; snapshot?: QueueResponseDto[] };
|
||||
|
||||
export type HeaderButtonActionItem = ActionItem & { data?: { title?: string } };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AssetTypeEnum } from '@immich/sdk';
|
||||
import { getAssetUrl } from '$lib/utils';
|
||||
import { getAssetUrl, getReleaseType } from '$lib/utils';
|
||||
import { assetFactory } from '@test-data/factories/asset-factory';
|
||||
import { sharedLinkFactory } from '@test-data/factories/shared-link-factory';
|
||||
|
||||
@@ -161,4 +161,26 @@ describe('utils', () => {
|
||||
expect(url).toContain(asset.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe(getReleaseType.name, () => {
|
||||
it('should return "major" for major version changes', () => {
|
||||
expect(getReleaseType({ major: 1, minor: 0, patch: 0 }, { major: 2, minor: 0, patch: 0 })).toBe('major');
|
||||
expect(getReleaseType({ major: 1, minor: 0, patch: 0 }, { major: 3, minor: 2, patch: 1 })).toBe('major');
|
||||
});
|
||||
|
||||
it('should return "minor" for minor version changes', () => {
|
||||
expect(getReleaseType({ major: 1, minor: 0, patch: 0 }, { major: 1, minor: 1, patch: 0 })).toBe('minor');
|
||||
expect(getReleaseType({ major: 1, minor: 0, patch: 0 }, { major: 1, minor: 2, patch: 1 })).toBe('minor');
|
||||
});
|
||||
|
||||
it('should return "patch" for patch version changes', () => {
|
||||
expect(getReleaseType({ major: 1, minor: 0, patch: 0 }, { major: 1, minor: 0, patch: 1 })).toBe('patch');
|
||||
expect(getReleaseType({ major: 1, minor: 0, patch: 0 }, { major: 1, minor: 0, patch: 5 })).toBe('patch');
|
||||
});
|
||||
|
||||
it('should return "none" for matching versions', () => {
|
||||
expect(getReleaseType({ major: 1, minor: 0, patch: 0 }, { major: 1, minor: 0, patch: 0 })).toBe('none');
|
||||
expect(getReleaseType({ major: 1, minor: 2, patch: 3 }, { major: 1, minor: 2, patch: 3 })).toBe('none');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+20
-2
@@ -411,8 +411,26 @@ export function createDateFormatter(localeCode: string | undefined): DateFormatt
|
||||
};
|
||||
}
|
||||
|
||||
export const semverToName = ({ major, minor, patch, prerelease }: ServerVersionResponseDto) =>
|
||||
`v${major}.${minor}.${patch}${prerelease ? `-rc.${prerelease}` : ''}`;
|
||||
export const getReleaseType = (
|
||||
current: ServerVersionResponseDto,
|
||||
newVersion: ServerVersionResponseDto,
|
||||
): 'major' | 'minor' | 'patch' | 'none' => {
|
||||
if (current.major !== newVersion.major) {
|
||||
return 'major';
|
||||
}
|
||||
|
||||
if (current.minor !== newVersion.minor) {
|
||||
return 'minor';
|
||||
}
|
||||
|
||||
if (current.patch !== newVersion.patch) {
|
||||
return 'patch';
|
||||
}
|
||||
|
||||
return 'none';
|
||||
};
|
||||
|
||||
export const semverToName = ({ major, minor, patch }: ServerVersionResponseDto) => `v${major}.${minor}.${patch}`;
|
||||
|
||||
export const withoutIcons = (actions: ActionItem[]): ActionItem[] =>
|
||||
actions.map((action) => ({ ...action, icon: undefined }));
|
||||
|
||||
@@ -45,9 +45,9 @@
|
||||
import { cloneDeep, isEqual } from 'lodash-es';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { PageData } from './$types';
|
||||
import WorkflowJsonEditor from './WorkflowJsonEditor.svelte';
|
||||
import WorkflowStepCard from './WorkflowStepCard.svelte';
|
||||
import WorkflowSummary from './WorkflowSummary.svelte';
|
||||
import WorkflowStepCard from '$lib/components/workflows/WorkflowStepCard.svelte';
|
||||
import WorkflowJsonEditor from '$lib/components/workflows/WorkflowJsonEditor.svelte';
|
||||
import WorkflowSummary from '$lib/components/workflows/WorkflowSummary.svelte';
|
||||
|
||||
type WorkflowJsonContent = Required<
|
||||
Pick<WorkflowUpdateDto, 'description' | 'enabled' | 'name' | 'steps' | 'trigger'>
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import VersionAnnouncementModal from '$lib/modals/VersionAnnouncementModal.svelte';
|
||||
import { semverToName } from '$lib/utils';
|
||||
import { ReleaseType, type ReleaseEventV1 } from '@immich/sdk';
|
||||
import type { ReleaseEvent } from '$lib/types';
|
||||
import { getReleaseType, semverToName } from '$lib/utils';
|
||||
import { modalManager } from '@immich/ui';
|
||||
|
||||
let modal = $state<{
|
||||
@@ -11,20 +11,16 @@
|
||||
close: () => Promise<void>;
|
||||
}>();
|
||||
|
||||
const onReleaseEvent = async (release: ReleaseEventV1) => {
|
||||
const onReleaseEvent = async (release: ReleaseEvent) => {
|
||||
if (!release.isAvailable || !authManager.user.isAdmin) {
|
||||
return;
|
||||
}
|
||||
|
||||
const releaseVersion = semverToName(release.releaseVersion);
|
||||
const serverVersion = semverToName(release.serverVersion);
|
||||
const type = getReleaseType(release.serverVersion, release.releaseVersion);
|
||||
|
||||
if (
|
||||
!release.type ||
|
||||
release.type === ReleaseType.Patch ||
|
||||
release.type === ReleaseType.Prepatch ||
|
||||
localStorage.getItem('appVersion') === releaseVersion
|
||||
) {
|
||||
if (type === 'none' || type === 'patch' || localStorage.getItem('appVersion') === releaseVersion) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,11 +5,8 @@
|
||||
import { systemConfigManager } from '$lib/managers/system-config-manager.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
import SettingSelect from './SettingSelect.svelte';
|
||||
import { ReleaseChannel } from '@immich/sdk';
|
||||
|
||||
const disabled = $derived(featureFlagsManager.value.configFile);
|
||||
const config = $derived(systemConfigManager.value);
|
||||
let configToEdit = $state(systemConfigManager.cloneValue());
|
||||
</script>
|
||||
|
||||
@@ -23,20 +20,6 @@
|
||||
bind:checked={configToEdit.newVersionCheck.enabled}
|
||||
{disabled}
|
||||
/>
|
||||
<SettingSelect
|
||||
label={$t('admin.version_check_channel')}
|
||||
desc={$t('admin.version_check_channel_description')}
|
||||
bind:value={configToEdit.newVersionCheck.channel}
|
||||
options={[
|
||||
{
|
||||
value: ReleaseChannel.Stable,
|
||||
text: $t('admin.release_channel_stable'),
|
||||
},
|
||||
{ value: ReleaseChannel.ReleaseCandidate, text: $t('admin.release_channel_release_candidate') },
|
||||
]}
|
||||
isEdited={configToEdit.newVersionCheck.channel !== config.newVersionCheck.channel}
|
||||
{disabled}
|
||||
/>
|
||||
<SettingButtonsRow bind:configToEdit keys={['newVersionCheck']} {disabled} />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user