mirror of
https://github.com/immich-app/immich.git
synced 2026-04-30 04:58:48 -07:00
Compare commits
2 Commits
debug/back
...
refactor/r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcd23ee043 | ||
|
|
350056dd1a |
@@ -17,7 +17,6 @@
|
||||
A01DD69B2F7F43B40049AB63 /* ImageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A01DD6982F7F43B40049AB63 /* ImageRequest.swift */; };
|
||||
B21E34AA2E5AFD2B0031FDB9 /* BackgroundWorkerApiImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = B21E34A92E5AFD210031FDB9 /* BackgroundWorkerApiImpl.swift */; };
|
||||
B21E34AC2E5B09190031FDB9 /* BackgroundWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */; };
|
||||
B21E34B02E5B09190031FDB9 /* FileLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = B21E34B12E5B09100031FDB9 /* FileLogger.swift */; };
|
||||
B25D377A2E72CA15008B6CA7 /* Connectivity.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B25D37782E72CA15008B6CA7 /* Connectivity.g.swift */; };
|
||||
B25D377C2E72CA26008B6CA7 /* ConnectivityApiImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = B25D377B2E72CA20008B6CA7 /* ConnectivityApiImpl.swift */; };
|
||||
B2BE315F2E5E5229006EEF88 /* BackgroundWorker.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */; };
|
||||
@@ -104,7 +103,6 @@
|
||||
B1FBA9EE014DE20271B0FE77 /* Pods-ShareExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.profile.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
B21E34A92E5AFD210031FDB9 /* BackgroundWorkerApiImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorkerApiImpl.swift; sourceTree = "<group>"; };
|
||||
B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.swift; sourceTree = "<group>"; };
|
||||
B21E34B12E5B09100031FDB9 /* FileLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileLogger.swift; sourceTree = "<group>"; };
|
||||
B25D37782E72CA15008B6CA7 /* Connectivity.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Connectivity.g.swift; sourceTree = "<group>"; };
|
||||
B25D377B2E72CA20008B6CA7 /* ConnectivityApiImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectivityApiImpl.swift; sourceTree = "<group>"; };
|
||||
B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.g.swift; sourceTree = "<group>"; };
|
||||
@@ -306,7 +304,6 @@
|
||||
B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */,
|
||||
B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */,
|
||||
B21E34A92E5AFD210031FDB9 /* BackgroundWorkerApiImpl.swift */,
|
||||
B21E34B12E5B09100031FDB9 /* FileLogger.swift */,
|
||||
);
|
||||
path = Background;
|
||||
sourceTree = "<group>";
|
||||
@@ -617,7 +614,6 @@
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||
A01DD69B2F7F43B40049AB63 /* ImageRequest.swift in Sources */,
|
||||
B21E34AC2E5B09190031FDB9 /* BackgroundWorker.swift in Sources */,
|
||||
B21E34B02E5B09190031FDB9 /* FileLogger.swift in Sources */,
|
||||
FE5499F32F1197D8006016CB /* LocalImages.g.swift in Sources */,
|
||||
FE5499F62F11980E006016CB /* LocalImagesImpl.swift in Sources */,
|
||||
FE5499F42F1197D8006016CB /* RemoteImages.g.swift in Sources */,
|
||||
|
||||
@@ -80,34 +80,29 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
|
||||
* starts the engine, and sets up a timeout timer if specified.
|
||||
*/
|
||||
func run() {
|
||||
FileLogger.log("BackgroundWorker:run Starting Flutter engine for taskType=\(taskType) maxSeconds=\(maxSeconds.map(String.init) ?? "nil")")
|
||||
// Start the Flutter engine with the specified callback as the entry point
|
||||
let isRunning = engine.run(
|
||||
withEntrypoint: "backgroundSyncNativeEntrypoint",
|
||||
libraryURI: "package:immich_mobile/domain/services/background_worker.service.dart"
|
||||
)
|
||||
|
||||
|
||||
// Verify that the Flutter engine started successfully
|
||||
if !isRunning {
|
||||
FileLogger.log("BackgroundWorker:run Flutter engine failed to start, completing with success=false")
|
||||
complete(success: false)
|
||||
return
|
||||
}
|
||||
FileLogger.log("BackgroundWorker:run Flutter engine started")
|
||||
|
||||
|
||||
// Register plugins in the new engine
|
||||
GeneratedPluginRegistrant.register(with: engine)
|
||||
// Register custom plugins
|
||||
AppDelegate.registerPlugins(with: engine, messenger: engine.binaryMessenger)
|
||||
flutterApi = BackgroundWorkerFlutterApi(binaryMessenger: engine.binaryMessenger)
|
||||
BackgroundWorkerBgHostApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: self)
|
||||
FileLogger.log("BackgroundWorker:run Plugins registered, waiting for Flutter onInitialized")
|
||||
|
||||
|
||||
// Set up a timeout timer if maxSeconds was specified to prevent runaway background tasks
|
||||
if maxSeconds != nil {
|
||||
// Schedule a timer to cancel the task after the specified timeout period
|
||||
Timer.scheduledTimer(withTimeInterval: TimeInterval(maxSeconds!), repeats: false) { _ in
|
||||
FileLogger.log("BackgroundWorker:run maxSeconds=\(self.maxSeconds!) timer fired, closing task")
|
||||
self.close()
|
||||
}
|
||||
}
|
||||
@@ -119,7 +114,6 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
|
||||
* This method acts as a bridge between the native iOS background task system and Flutter.
|
||||
*/
|
||||
func onInitialized() throws {
|
||||
FileLogger.log("BackgroundWorker:onInitialized Flutter ready, calling onIosUpload isRefresh=\(self.taskType == .refresh)")
|
||||
flutterApi?.onIosUpload(isRefresh: self.taskType == .refresh, maxSeconds: maxSeconds.map { Int64($0) }, completion: { result in
|
||||
self.handleHostResult(result: result)
|
||||
})
|
||||
@@ -132,22 +126,16 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
|
||||
*/
|
||||
func close() {
|
||||
if isComplete {
|
||||
FileLogger.log("BackgroundWorker:close Already complete, ignoring close()")
|
||||
return
|
||||
}
|
||||
FileLogger.log("BackgroundWorker:close Cancel requested, signaling Flutter (taskType=\(taskType))")
|
||||
|
||||
flutterApi?.cancel { result in
|
||||
FileLogger.log("BackgroundWorker:close Flutter cancel acknowledged")
|
||||
self.complete(success: false)
|
||||
}
|
||||
|
||||
// Fallback safety mechanism: ensure completion is called within 2 seconds
|
||||
// This prevents the background task from hanging indefinitely if Flutter doesn't respond
|
||||
Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { _ in
|
||||
if !self.isComplete {
|
||||
FileLogger.log("BackgroundWorker:close 2s fallback fired, Flutter did not acknowledge cancel")
|
||||
}
|
||||
self.complete(success: false)
|
||||
}
|
||||
}
|
||||
@@ -161,12 +149,8 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
|
||||
*/
|
||||
private func handleHostResult(result: Result<Void, PigeonError>) {
|
||||
switch result {
|
||||
case .success():
|
||||
FileLogger.log("BackgroundWorker:handleHostResult Flutter onIosUpload succeeded (taskType=\(taskType))")
|
||||
self.complete(success: true)
|
||||
case .failure(let error):
|
||||
FileLogger.log("BackgroundWorker:handleHostResult Flutter onIosUpload failed: \(error.localizedDescription) (taskType=\(taskType))")
|
||||
self.close()
|
||||
case .success(): self.complete(success: true)
|
||||
case .failure(_): self.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,8 +166,7 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
|
||||
if(isComplete) {
|
||||
return
|
||||
}
|
||||
|
||||
FileLogger.log("BackgroundWorker:complete Tearing down engine, success=\(success) (taskType=\(taskType))")
|
||||
|
||||
isComplete = true
|
||||
AppDelegate.cancelPlugins(with: engine)
|
||||
engine.destroyContext()
|
||||
|
||||
@@ -5,7 +5,7 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
|
||||
func enable() throws {
|
||||
BackgroundWorkerApiImpl.scheduleRefreshWorker()
|
||||
BackgroundWorkerApiImpl.scheduleProcessingWorker()
|
||||
FileLogger.log("BackgroundWorkerApiImpl:enable Background worker scheduled")
|
||||
print("BackgroundWorkerApiImpl:enable Background worker scheduled")
|
||||
}
|
||||
|
||||
func configure(settings: BackgroundWorkerSettings) throws {
|
||||
@@ -19,7 +19,7 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
|
||||
func disable() throws {
|
||||
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundWorkerApiImpl.refreshTaskID);
|
||||
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundWorkerApiImpl.processingTaskID);
|
||||
FileLogger.log("BackgroundWorkerApiImpl:disableUploadWorker Disabled background workers")
|
||||
print("BackgroundWorkerApiImpl:disableUploadWorker Disabled background workers")
|
||||
}
|
||||
|
||||
private static let refreshTaskID = "app.alextran.immich.background.refreshUpload"
|
||||
@@ -30,7 +30,6 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
|
||||
BGTaskScheduler.shared.register(
|
||||
forTaskWithIdentifier: processingTaskID, using: nil) { task in
|
||||
if task is BGProcessingTask {
|
||||
FileLogger.log("BackgroundWorkerApiImpl:BGProcessingTask Background Processing task received")
|
||||
handleBackgroundProcessing(task: task as! BGProcessingTask)
|
||||
}
|
||||
}
|
||||
@@ -38,11 +37,9 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
|
||||
BGTaskScheduler.shared.register(
|
||||
forTaskWithIdentifier: refreshTaskID, using: nil) { task in
|
||||
if task is BGAppRefreshTask {
|
||||
FileLogger.log("BackgroundWorkerApiImpl:BGAppRefreshTask Background Refresh task received")
|
||||
handleBackgroundRefresh(task: task as! BGAppRefreshTask)
|
||||
}
|
||||
}
|
||||
FileLogger.log("BackgroundWorkerApiImpl:registerBackgroundWorkers Background workers registered")
|
||||
}
|
||||
|
||||
private static func scheduleRefreshWorker() {
|
||||
@@ -51,9 +48,8 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
|
||||
|
||||
do {
|
||||
try BGTaskScheduler.shared.submit(backgroundRefresh)
|
||||
FileLogger.log("BackgroundWorkerApiImpl:scheduleRefreshWorker Scheduled Refresh task")
|
||||
} catch {
|
||||
FileLogger.log("BackgroundWorkerApiImpl:scheduleRefreshWorker Could not schedule the refresh upload task \(error.localizedDescription)")
|
||||
print("Could not schedule the refresh upload task \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,32 +61,25 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
|
||||
|
||||
do {
|
||||
try BGTaskScheduler.shared.submit(backgroundProcessing)
|
||||
FileLogger.log("BackgroundWorkerApiImpl:scheduleProcessingWorker Scheduled Processing task")
|
||||
} catch {
|
||||
FileLogger.log("BackgroundWorkerApiImpl:scheduleProcessingWorker Could not schedule the processing upload task \(error.localizedDescription)")
|
||||
print("Could not schedule the processing upload task \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
private static func handleBackgroundRefresh(task: BGAppRefreshTask) {
|
||||
FileLogger.log("BackgroundWorkerApiImpl:handleBackgroundRefresh Entered, re-queuing next refresh task")
|
||||
scheduleRefreshWorker()
|
||||
// If another task is running, cede the background time back to the OS
|
||||
if taskSemaphore.wait(timeout: .now()) == .success {
|
||||
FileLogger.log("BackgroundWorkerApiImpl:handleBackgroundRefresh Starting background worker")
|
||||
// Restrict the refresh task to run only for a maximum of (maxSeconds) seconds
|
||||
runBackgroundWorker(task: task, taskType: .refresh, maxSeconds: 20)
|
||||
} else {
|
||||
FileLogger.log("BackgroundWorkerApiImpl:handleBackgroundRefresh Processing task is in progress")
|
||||
task.setTaskCompleted(success: true)
|
||||
task.setTaskCompleted(success: false)
|
||||
}
|
||||
}
|
||||
|
||||
private static func handleBackgroundProcessing(task: BGProcessingTask) {
|
||||
FileLogger.log("BackgroundWorkerApiImpl:handleBackgroundProcessing Entered, re-queuing next processing task")
|
||||
scheduleProcessingWorker()
|
||||
FileLogger.log("BackgroundWorkerApiImpl:handleBackgroundProcessing Waiting for taskSemaphore")
|
||||
taskSemaphore.wait()
|
||||
FileLogger.log("BackgroundWorkerApiImpl:handleBackgroundProcessing Semaphore acquired, starting background worker")
|
||||
// There are no restrictions for processing tasks. Although, the OS could signal expiration at any time
|
||||
runBackgroundWorker(task: task, taskType: .processing, maxSeconds: nil)
|
||||
}
|
||||
@@ -116,12 +105,11 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
|
||||
}
|
||||
|
||||
task.expirationHandler = {
|
||||
FileLogger.log("BackgroundWorkerApiImpl:runBackgroundWorker iOS signaled expiration (taskType=\(taskType)), closing worker")
|
||||
DispatchQueue.main.async {
|
||||
backgroundWorker.close()
|
||||
}
|
||||
isSuccess = false
|
||||
|
||||
|
||||
// Schedule a timer to signal the semaphore after 2 seconds
|
||||
Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { _ in
|
||||
semaphore.signal()
|
||||
@@ -134,6 +122,6 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
|
||||
|
||||
semaphore.wait()
|
||||
task.setTaskCompleted(success: isSuccess)
|
||||
FileLogger.log("BackgroundWorkerApiImpl:runBackgroundWorker Background task completed with success: \(isSuccess)")
|
||||
print("Background task completed with success: \(isSuccess)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
enum FileLogger {
|
||||
private static let queue = DispatchQueue(label: "app.alextran.immich.FileLogger")
|
||||
private static let isoFormatter: ISO8601DateFormatter = {
|
||||
let f = ISO8601DateFormatter()
|
||||
f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||
return f
|
||||
}()
|
||||
|
||||
private static var logFileURL: URL? {
|
||||
guard let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
|
||||
else { return nil }
|
||||
return docs.appendingPathComponent("background_log.txt")
|
||||
}
|
||||
|
||||
static func log(_ message: String) {
|
||||
let line = "[\(isoFormatter.string(from: Date()))] \(message)\n"
|
||||
print(line, terminator: "")
|
||||
queue.async {
|
||||
guard let url = logFileURL, let data = line.data(using: .utf8) else { return }
|
||||
if FileManager.default.fileExists(atPath: url.path) {
|
||||
if let handle = try? FileHandle(forWritingTo: url) {
|
||||
defer { try? handle.close() }
|
||||
try? handle.seekToEnd()
|
||||
try? handle.write(contentsOf: data)
|
||||
}
|
||||
} else {
|
||||
try? data.write(to: url, options: .atomic)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,9 +115,7 @@
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
<true/>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<true/>
|
||||
<string>No</string>
|
||||
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
|
||||
30
mobile/openapi/lib/model/album_response_dto.dart
generated
30
mobile/openapi/lib/model/album_response_dto.dart
generated
@@ -157,10 +157,14 @@ class AlbumResponseDto {
|
||||
json[r'albumUsers'] = this.albumUsers;
|
||||
json[r'assetCount'] = this.assetCount;
|
||||
json[r'contributorCounts'] = this.contributorCounts;
|
||||
json[r'createdAt'] = this.createdAt.toUtc().toIso8601String();
|
||||
json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
|
||||
? this.createdAt.millisecondsSinceEpoch
|
||||
: this.createdAt.toUtc().toIso8601String();
|
||||
json[r'description'] = this.description;
|
||||
if (this.endDate != null) {
|
||||
json[r'endDate'] = this.endDate!.toUtc().toIso8601String();
|
||||
json[r'endDate'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
|
||||
? this.endDate!.millisecondsSinceEpoch
|
||||
: this.endDate!.toUtc().toIso8601String();
|
||||
} else {
|
||||
// json[r'endDate'] = null;
|
||||
}
|
||||
@@ -168,7 +172,9 @@ class AlbumResponseDto {
|
||||
json[r'id'] = this.id;
|
||||
json[r'isActivityEnabled'] = this.isActivityEnabled;
|
||||
if (this.lastModifiedAssetTimestamp != null) {
|
||||
json[r'lastModifiedAssetTimestamp'] = this.lastModifiedAssetTimestamp!.toUtc().toIso8601String();
|
||||
json[r'lastModifiedAssetTimestamp'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
|
||||
? this.lastModifiedAssetTimestamp!.millisecondsSinceEpoch
|
||||
: this.lastModifiedAssetTimestamp!.toUtc().toIso8601String();
|
||||
} else {
|
||||
// json[r'lastModifiedAssetTimestamp'] = null;
|
||||
}
|
||||
@@ -179,11 +185,15 @@ class AlbumResponseDto {
|
||||
}
|
||||
json[r'shared'] = this.shared;
|
||||
if (this.startDate != null) {
|
||||
json[r'startDate'] = this.startDate!.toUtc().toIso8601String();
|
||||
json[r'startDate'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
|
||||
? this.startDate!.millisecondsSinceEpoch
|
||||
: this.startDate!.toUtc().toIso8601String();
|
||||
} else {
|
||||
// json[r'startDate'] = null;
|
||||
}
|
||||
json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String();
|
||||
json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
|
||||
? this.updatedAt.millisecondsSinceEpoch
|
||||
: this.updatedAt.toUtc().toIso8601String();
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -201,17 +211,17 @@ class AlbumResponseDto {
|
||||
albumUsers: AlbumUserResponseDto.listFromJson(json[r'albumUsers']),
|
||||
assetCount: mapValueOfType<int>(json, r'assetCount')!,
|
||||
contributorCounts: ContributorCountResponseDto.listFromJson(json[r'contributorCounts']),
|
||||
createdAt: mapDateTime(json, r'createdAt', r'')!,
|
||||
createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!,
|
||||
description: mapValueOfType<String>(json, r'description')!,
|
||||
endDate: mapDateTime(json, r'endDate', r''),
|
||||
endDate: mapDateTime(json, r'endDate', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'),
|
||||
hasSharedLink: mapValueOfType<bool>(json, r'hasSharedLink')!,
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
isActivityEnabled: mapValueOfType<bool>(json, r'isActivityEnabled')!,
|
||||
lastModifiedAssetTimestamp: mapDateTime(json, r'lastModifiedAssetTimestamp', r''),
|
||||
lastModifiedAssetTimestamp: mapDateTime(json, r'lastModifiedAssetTimestamp', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'),
|
||||
order: AssetOrder.fromJson(json[r'order']),
|
||||
shared: mapValueOfType<bool>(json, r'shared')!,
|
||||
startDate: mapDateTime(json, r'startDate', r''),
|
||||
updatedAt: mapDateTime(json, r'updatedAt', r'')!,
|
||||
startDate: mapDateTime(json, r'startDate', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'),
|
||||
updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
||||
30
mobile/openapi/lib/model/asset_response_dto.dart
generated
30
mobile/openapi/lib/model/asset_response_dto.dart
generated
@@ -246,7 +246,9 @@ class AssetResponseDto {
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'checksum'] = this.checksum;
|
||||
json[r'createdAt'] = this.createdAt.toUtc().toIso8601String();
|
||||
json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
|
||||
? this.createdAt.millisecondsSinceEpoch
|
||||
: this.createdAt.toUtc().toIso8601String();
|
||||
if (this.duplicateId != null) {
|
||||
json[r'duplicateId'] = this.duplicateId;
|
||||
} else {
|
||||
@@ -262,8 +264,12 @@ class AssetResponseDto {
|
||||
} else {
|
||||
// json[r'exifInfo'] = null;
|
||||
}
|
||||
json[r'fileCreatedAt'] = this.fileCreatedAt.toUtc().toIso8601String();
|
||||
json[r'fileModifiedAt'] = this.fileModifiedAt.toUtc().toIso8601String();
|
||||
json[r'fileCreatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
|
||||
? this.fileCreatedAt.millisecondsSinceEpoch
|
||||
: this.fileCreatedAt.toUtc().toIso8601String();
|
||||
json[r'fileModifiedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
|
||||
? this.fileModifiedAt.millisecondsSinceEpoch
|
||||
: this.fileModifiedAt.toUtc().toIso8601String();
|
||||
json[r'hasMetadata'] = this.hasMetadata;
|
||||
if (this.height != null) {
|
||||
json[r'height'] = this.height;
|
||||
@@ -286,7 +292,9 @@ class AssetResponseDto {
|
||||
} else {
|
||||
// json[r'livePhotoVideoId'] = null;
|
||||
}
|
||||
json[r'localDateTime'] = this.localDateTime.toUtc().toIso8601String();
|
||||
json[r'localDateTime'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
|
||||
? this.localDateTime.millisecondsSinceEpoch
|
||||
: this.localDateTime.toUtc().toIso8601String();
|
||||
json[r'originalFileName'] = this.originalFileName;
|
||||
if (this.originalMimeType != null) {
|
||||
json[r'originalMimeType'] = this.originalMimeType;
|
||||
@@ -319,7 +327,9 @@ class AssetResponseDto {
|
||||
}
|
||||
json[r'type'] = this.type;
|
||||
json[r'unassignedFaces'] = this.unassignedFaces;
|
||||
json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String();
|
||||
json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
|
||||
? this.updatedAt.millisecondsSinceEpoch
|
||||
: this.updatedAt.toUtc().toIso8601String();
|
||||
json[r'visibility'] = this.visibility;
|
||||
if (this.width != null) {
|
||||
json[r'width'] = this.width;
|
||||
@@ -339,12 +349,12 @@ class AssetResponseDto {
|
||||
|
||||
return AssetResponseDto(
|
||||
checksum: mapValueOfType<String>(json, r'checksum')!,
|
||||
createdAt: mapDateTime(json, r'createdAt', r'')!,
|
||||
createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!,
|
||||
duplicateId: mapValueOfType<String>(json, r'duplicateId'),
|
||||
duration: mapValueOfType<String>(json, r'duration'),
|
||||
exifInfo: ExifResponseDto.fromJson(json[r'exifInfo']),
|
||||
fileCreatedAt: mapDateTime(json, r'fileCreatedAt', r'')!,
|
||||
fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r'')!,
|
||||
fileCreatedAt: mapDateTime(json, r'fileCreatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!,
|
||||
fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!,
|
||||
hasMetadata: mapValueOfType<bool>(json, r'hasMetadata')!,
|
||||
height: json[r'height'] == null
|
||||
? null
|
||||
@@ -357,7 +367,7 @@ class AssetResponseDto {
|
||||
isTrashed: mapValueOfType<bool>(json, r'isTrashed')!,
|
||||
libraryId: mapValueOfType<String>(json, r'libraryId'),
|
||||
livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'),
|
||||
localDateTime: mapDateTime(json, r'localDateTime', r'')!,
|
||||
localDateTime: mapDateTime(json, r'localDateTime', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!,
|
||||
originalFileName: mapValueOfType<String>(json, r'originalFileName')!,
|
||||
originalMimeType: mapValueOfType<String>(json, r'originalMimeType'),
|
||||
originalPath: mapValueOfType<String>(json, r'originalPath')!,
|
||||
@@ -370,7 +380,7 @@ class AssetResponseDto {
|
||||
thumbhash: mapValueOfType<String>(json, r'thumbhash'),
|
||||
type: AssetTypeEnum.fromJson(json[r'type'])!,
|
||||
unassignedFaces: AssetFaceWithoutPersonResponseDto.listFromJson(json[r'unassignedFaces']),
|
||||
updatedAt: mapDateTime(json, r'updatedAt', r'')!,
|
||||
updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!,
|
||||
visibility: AssetVisibility.fromJson(json[r'visibility'])!,
|
||||
width: json[r'width'] == null
|
||||
? null
|
||||
|
||||
12
mobile/openapi/lib/model/exif_response_dto.dart
generated
12
mobile/openapi/lib/model/exif_response_dto.dart
generated
@@ -177,7 +177,9 @@ class ExifResponseDto {
|
||||
// json[r'country'] = null;
|
||||
}
|
||||
if (this.dateTimeOriginal != null) {
|
||||
json[r'dateTimeOriginal'] = this.dateTimeOriginal!.toUtc().toIso8601String();
|
||||
json[r'dateTimeOriginal'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
|
||||
? this.dateTimeOriginal!.millisecondsSinceEpoch
|
||||
: this.dateTimeOriginal!.toUtc().toIso8601String();
|
||||
} else {
|
||||
// json[r'dateTimeOriginal'] = null;
|
||||
}
|
||||
@@ -247,7 +249,9 @@ class ExifResponseDto {
|
||||
// json[r'model'] = null;
|
||||
}
|
||||
if (this.modifyDate != null) {
|
||||
json[r'modifyDate'] = this.modifyDate!.toUtc().toIso8601String();
|
||||
json[r'modifyDate'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
|
||||
? this.modifyDate!.millisecondsSinceEpoch
|
||||
: this.modifyDate!.toUtc().toIso8601String();
|
||||
} else {
|
||||
// json[r'modifyDate'] = null;
|
||||
}
|
||||
@@ -290,7 +294,7 @@ class ExifResponseDto {
|
||||
return ExifResponseDto(
|
||||
city: mapValueOfType<String>(json, r'city'),
|
||||
country: mapValueOfType<String>(json, r'country'),
|
||||
dateTimeOriginal: mapDateTime(json, r'dateTimeOriginal', r''),
|
||||
dateTimeOriginal: mapDateTime(json, r'dateTimeOriginal', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'),
|
||||
description: mapValueOfType<String>(json, r'description'),
|
||||
exifImageHeight: json[r'exifImageHeight'] == null
|
||||
? null
|
||||
@@ -318,7 +322,7 @@ class ExifResponseDto {
|
||||
: num.parse('${json[r'longitude']}'),
|
||||
make: mapValueOfType<String>(json, r'make'),
|
||||
model: mapValueOfType<String>(json, r'model'),
|
||||
modifyDate: mapDateTime(json, r'modifyDate', r''),
|
||||
modifyDate: mapDateTime(json, r'modifyDate', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'),
|
||||
orientation: mapValueOfType<String>(json, r'orientation'),
|
||||
projectionType: mapValueOfType<String>(json, r'projectionType'),
|
||||
rating: json[r'rating'] == null
|
||||
|
||||
@@ -83,7 +83,9 @@ class PartnerResponseDto {
|
||||
// json[r'inTimeline'] = null;
|
||||
}
|
||||
json[r'name'] = this.name;
|
||||
json[r'profileChangedAt'] = this.profileChangedAt.toUtc().toIso8601String();
|
||||
json[r'profileChangedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
|
||||
? this.profileChangedAt.millisecondsSinceEpoch
|
||||
: this.profileChangedAt.toUtc().toIso8601String();
|
||||
json[r'profileImagePath'] = this.profileImagePath;
|
||||
return json;
|
||||
}
|
||||
@@ -102,7 +104,7 @@ class PartnerResponseDto {
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
inTimeline: mapValueOfType<bool>(json, r'inTimeline'),
|
||||
name: mapValueOfType<String>(json, r'name')!,
|
||||
profileChangedAt: mapDateTime(json, r'profileChangedAt', r'')!,
|
||||
profileChangedAt: mapDateTime(json, r'profileChangedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!,
|
||||
profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,
|
||||
);
|
||||
}
|
||||
|
||||
12
mobile/openapi/lib/model/person_response_dto.dart
generated
12
mobile/openapi/lib/model/person_response_dto.dart
generated
@@ -94,7 +94,9 @@ class PersonResponseDto {
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
if (this.birthDate != null) {
|
||||
json[r'birthDate'] = _dateFormatter.format(this.birthDate!.toUtc());
|
||||
json[r'birthDate'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$/')
|
||||
? this.birthDate!.millisecondsSinceEpoch
|
||||
: _dateFormatter.format(this.birthDate!.toUtc());
|
||||
} else {
|
||||
// json[r'birthDate'] = null;
|
||||
}
|
||||
@@ -113,7 +115,9 @@ class PersonResponseDto {
|
||||
json[r'name'] = this.name;
|
||||
json[r'thumbnailPath'] = this.thumbnailPath;
|
||||
if (this.updatedAt != null) {
|
||||
json[r'updatedAt'] = this.updatedAt!.toUtc().toIso8601String();
|
||||
json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
|
||||
? this.updatedAt!.millisecondsSinceEpoch
|
||||
: this.updatedAt!.toUtc().toIso8601String();
|
||||
} else {
|
||||
// json[r'updatedAt'] = null;
|
||||
}
|
||||
@@ -129,14 +133,14 @@ class PersonResponseDto {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return PersonResponseDto(
|
||||
birthDate: mapDateTime(json, r'birthDate', r''),
|
||||
birthDate: mapDateTime(json, r'birthDate', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$/'),
|
||||
color: mapValueOfType<String>(json, r'color'),
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
|
||||
isHidden: mapValueOfType<bool>(json, r'isHidden')!,
|
||||
name: mapValueOfType<String>(json, r'name')!,
|
||||
thumbnailPath: mapValueOfType<String>(json, r'thumbnailPath')!,
|
||||
updatedAt: mapDateTime(json, r'updatedAt', r''),
|
||||
updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -99,7 +99,9 @@ class PersonWithFacesResponseDto {
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
if (this.birthDate != null) {
|
||||
json[r'birthDate'] = _dateFormatter.format(this.birthDate!.toUtc());
|
||||
json[r'birthDate'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$/')
|
||||
? this.birthDate!.millisecondsSinceEpoch
|
||||
: _dateFormatter.format(this.birthDate!.toUtc());
|
||||
} else {
|
||||
// json[r'birthDate'] = null;
|
||||
}
|
||||
@@ -119,7 +121,9 @@ class PersonWithFacesResponseDto {
|
||||
json[r'name'] = this.name;
|
||||
json[r'thumbnailPath'] = this.thumbnailPath;
|
||||
if (this.updatedAt != null) {
|
||||
json[r'updatedAt'] = this.updatedAt!.toUtc().toIso8601String();
|
||||
json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
|
||||
? this.updatedAt!.millisecondsSinceEpoch
|
||||
: this.updatedAt!.toUtc().toIso8601String();
|
||||
} else {
|
||||
// json[r'updatedAt'] = null;
|
||||
}
|
||||
@@ -135,7 +139,7 @@ class PersonWithFacesResponseDto {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return PersonWithFacesResponseDto(
|
||||
birthDate: mapDateTime(json, r'birthDate', r''),
|
||||
birthDate: mapDateTime(json, r'birthDate', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$/'),
|
||||
color: mapValueOfType<String>(json, r'color'),
|
||||
faces: AssetFaceWithoutPersonResponseDto.listFromJson(json[r'faces']),
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
@@ -143,7 +147,7 @@ class PersonWithFacesResponseDto {
|
||||
isHidden: mapValueOfType<bool>(json, r'isHidden')!,
|
||||
name: mapValueOfType<String>(json, r'name')!,
|
||||
thumbnailPath: mapValueOfType<String>(json, r'thumbnailPath')!,
|
||||
updatedAt: mapDateTime(json, r'updatedAt', r''),
|
||||
updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
||||
12
mobile/openapi/lib/model/tag_response_dto.dart
generated
12
mobile/openapi/lib/model/tag_response_dto.dart
generated
@@ -86,7 +86,9 @@ class TagResponseDto {
|
||||
} else {
|
||||
// json[r'color'] = null;
|
||||
}
|
||||
json[r'createdAt'] = this.createdAt.toUtc().toIso8601String();
|
||||
json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
|
||||
? this.createdAt.millisecondsSinceEpoch
|
||||
: this.createdAt.toUtc().toIso8601String();
|
||||
json[r'id'] = this.id;
|
||||
json[r'name'] = this.name;
|
||||
if (this.parentId != null) {
|
||||
@@ -94,7 +96,9 @@ class TagResponseDto {
|
||||
} else {
|
||||
// json[r'parentId'] = null;
|
||||
}
|
||||
json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String();
|
||||
json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
|
||||
? this.updatedAt.millisecondsSinceEpoch
|
||||
: this.updatedAt.toUtc().toIso8601String();
|
||||
json[r'value'] = this.value;
|
||||
return json;
|
||||
}
|
||||
@@ -109,11 +113,11 @@ class TagResponseDto {
|
||||
|
||||
return TagResponseDto(
|
||||
color: mapValueOfType<String>(json, r'color'),
|
||||
createdAt: mapDateTime(json, r'createdAt', r'')!,
|
||||
createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!,
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
name: mapValueOfType<String>(json, r'name')!,
|
||||
parentId: mapValueOfType<String>(json, r'parentId'),
|
||||
updatedAt: mapDateTime(json, r'updatedAt', r'')!,
|
||||
updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!,
|
||||
value: mapValueOfType<String>(json, r'value')!,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -153,7 +153,9 @@ class UserAdminResponseDto {
|
||||
}
|
||||
json[r'name'] = this.name;
|
||||
json[r'oauthId'] = this.oauthId;
|
||||
json[r'profileChangedAt'] = this.profileChangedAt.toUtc().toIso8601String();
|
||||
json[r'profileChangedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
|
||||
? this.profileChangedAt.millisecondsSinceEpoch
|
||||
: this.profileChangedAt.toUtc().toIso8601String();
|
||||
json[r'profileImagePath'] = this.profileImagePath;
|
||||
if (this.quotaSizeInBytes != null) {
|
||||
json[r'quotaSizeInBytes'] = this.quotaSizeInBytes;
|
||||
@@ -196,7 +198,7 @@ class UserAdminResponseDto {
|
||||
license: UserLicense.fromJson(json[r'license']),
|
||||
name: mapValueOfType<String>(json, r'name')!,
|
||||
oauthId: mapValueOfType<String>(json, r'oauthId')!,
|
||||
profileChangedAt: mapDateTime(json, r'profileChangedAt', r'')!,
|
||||
profileChangedAt: mapDateTime(json, r'profileChangedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!,
|
||||
profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,
|
||||
quotaSizeInBytes: mapValueOfType<int>(json, r'quotaSizeInBytes'),
|
||||
quotaUsageInBytes: mapValueOfType<int>(json, r'quotaUsageInBytes'),
|
||||
|
||||
6
mobile/openapi/lib/model/user_response_dto.dart
generated
6
mobile/openapi/lib/model/user_response_dto.dart
generated
@@ -66,7 +66,9 @@ class UserResponseDto {
|
||||
json[r'email'] = this.email;
|
||||
json[r'id'] = this.id;
|
||||
json[r'name'] = this.name;
|
||||
json[r'profileChangedAt'] = this.profileChangedAt.toUtc().toIso8601String();
|
||||
json[r'profileChangedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
|
||||
? this.profileChangedAt.millisecondsSinceEpoch
|
||||
: this.profileChangedAt.toUtc().toIso8601String();
|
||||
json[r'profileImagePath'] = this.profileImagePath;
|
||||
return json;
|
||||
}
|
||||
@@ -84,7 +86,7 @@ class UserResponseDto {
|
||||
email: mapValueOfType<String>(json, r'email')!,
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
name: mapValueOfType<String>(json, r'name')!,
|
||||
profileChangedAt: mapDateTime(json, r'profileChangedAt', r'')!,
|
||||
profileChangedAt: mapDateTime(json, r'profileChangedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!,
|
||||
profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15300,7 +15300,9 @@
|
||||
},
|
||||
"createdAt": {
|
||||
"description": "Creation date",
|
||||
"example": "2024-01-01T00:00:00.000Z",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
@@ -15309,7 +15311,9 @@
|
||||
},
|
||||
"endDate": {
|
||||
"description": "End date (latest asset)",
|
||||
"example": "2024-01-01T00:00:00.000Z",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||
"type": "string"
|
||||
},
|
||||
"hasSharedLink": {
|
||||
@@ -15326,7 +15330,9 @@
|
||||
},
|
||||
"lastModifiedAssetTimestamp": {
|
||||
"description": "Last modified asset timestamp",
|
||||
"example": "2024-01-01T00:00:00.000Z",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||
"type": "string"
|
||||
},
|
||||
"order": {
|
||||
@@ -15338,12 +15344,16 @@
|
||||
},
|
||||
"startDate": {
|
||||
"description": "Start date (earliest asset)",
|
||||
"example": "2024-01-01T00:00:00.000Z",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||
"type": "string"
|
||||
},
|
||||
"updatedAt": {
|
||||
"description": "Last update date",
|
||||
"example": "2024-01-01T00:00:00.000Z",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
@@ -16618,7 +16628,9 @@
|
||||
},
|
||||
"createdAt": {
|
||||
"description": "The UTC timestamp when the asset was originally uploaded to Immich.",
|
||||
"example": "2024-01-01T00:00:00.000Z",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||
"type": "string"
|
||||
},
|
||||
"duplicateId": {
|
||||
@@ -16636,12 +16648,16 @@
|
||||
},
|
||||
"fileCreatedAt": {
|
||||
"description": "The actual UTC timestamp when the file was created/captured, preserving timezone information. This is the authoritative timestamp for chronological sorting within timeline groups. Combined with timezone data, this can be used to determine the exact moment the photo was taken.",
|
||||
"example": "2024-01-01T00:00:00.000Z",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||
"type": "string"
|
||||
},
|
||||
"fileModifiedAt": {
|
||||
"description": "The UTC timestamp when the file was last modified on the filesystem. This reflects the last time the physical file was changed, which may be different from when the photo was originally taken.",
|
||||
"example": "2024-01-01T00:00:00.000Z",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||
"type": "string"
|
||||
},
|
||||
"hasMetadata": {
|
||||
@@ -16714,7 +16730,9 @@
|
||||
},
|
||||
"localDateTime": {
|
||||
"description": "The local date and time when the photo/video was taken, derived from EXIF metadata. This represents the photographer's local time regardless of timezone, stored as a timezone-agnostic timestamp. Used for timeline grouping by \"local\" days and months.",
|
||||
"example": "2024-01-01T00:00:00.000Z",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||
"type": "string"
|
||||
},
|
||||
"originalFileName": {
|
||||
@@ -16787,7 +16805,9 @@
|
||||
},
|
||||
"updatedAt": {
|
||||
"description": "The UTC timestamp when the asset record was last updated in the database. This is automatically maintained by the database and reflects when any field in the asset was last modified.",
|
||||
"example": "2024-01-01T00:00:00.000Z",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||
"type": "string"
|
||||
},
|
||||
"visibility": {
|
||||
@@ -17614,8 +17634,10 @@
|
||||
"dateTimeOriginal": {
|
||||
"default": null,
|
||||
"description": "Original date/time",
|
||||
"example": "2024-01-01T00:00:00.000Z",
|
||||
"format": "date-time",
|
||||
"nullable": true,
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
@@ -17703,8 +17725,10 @@
|
||||
"modifyDate": {
|
||||
"default": null,
|
||||
"description": "Modification date/time",
|
||||
"example": "2024-01-01T00:00:00.000Z",
|
||||
"format": "date-time",
|
||||
"nullable": true,
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||
"type": "string"
|
||||
},
|
||||
"orientation": {
|
||||
@@ -19245,7 +19269,9 @@
|
||||
},
|
||||
"profileChangedAt": {
|
||||
"description": "Profile change date",
|
||||
"example": "2024-01-01T00:00:00.000Z",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||
"type": "string"
|
||||
},
|
||||
"profileImagePath": {
|
||||
@@ -19600,8 +19626,10 @@
|
||||
"properties": {
|
||||
"birthDate": {
|
||||
"description": "Person date of birth",
|
||||
"example": "2024-01-01",
|
||||
"format": "date",
|
||||
"nullable": true,
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$",
|
||||
"type": "string"
|
||||
},
|
||||
"color": {
|
||||
@@ -19652,7 +19680,9 @@
|
||||
},
|
||||
"updatedAt": {
|
||||
"description": "Last update date",
|
||||
"example": "2024-01-01T00:00:00.000Z",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||
"type": "string",
|
||||
"x-immich-history": [
|
||||
{
|
||||
@@ -19729,8 +19759,10 @@
|
||||
"properties": {
|
||||
"birthDate": {
|
||||
"description": "Person date of birth",
|
||||
"example": "2024-01-01",
|
||||
"format": "date",
|
||||
"nullable": true,
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$",
|
||||
"type": "string"
|
||||
},
|
||||
"color": {
|
||||
@@ -19787,7 +19819,9 @@
|
||||
},
|
||||
"updatedAt": {
|
||||
"description": "Last update date",
|
||||
"example": "2024-01-01T00:00:00.000Z",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||
"type": "string",
|
||||
"x-immich-history": [
|
||||
{
|
||||
@@ -24814,7 +24848,9 @@
|
||||
},
|
||||
"createdAt": {
|
||||
"description": "Creation date",
|
||||
"example": "2024-01-01T00:00:00.000Z",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
@@ -24831,7 +24867,9 @@
|
||||
},
|
||||
"updatedAt": {
|
||||
"description": "Last update date",
|
||||
"example": "2024-01-01T00:00:00.000Z",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
@@ -25485,7 +25523,9 @@
|
||||
},
|
||||
"profileChangedAt": {
|
||||
"description": "Profile change date",
|
||||
"example": "2024-01-01T00:00:00.000Z",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||
"type": "string"
|
||||
},
|
||||
"profileImagePath": {
|
||||
@@ -25767,7 +25807,9 @@
|
||||
},
|
||||
"profileChangedAt": {
|
||||
"description": "Profile change date",
|
||||
"example": "2024-01-01T00:00:00.000Z",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||
"type": "string"
|
||||
},
|
||||
"profileImagePath": {
|
||||
|
||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -570,8 +570,8 @@ importers:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.9
|
||||
uuid:
|
||||
specifier: ^11.1.0
|
||||
version: 11.1.0
|
||||
specifier: ^14.0.0
|
||||
version: 14.0.0
|
||||
validator:
|
||||
specifier: ^13.12.0
|
||||
version: 13.15.35
|
||||
@@ -12110,6 +12110,10 @@ packages:
|
||||
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
|
||||
hasBin: true
|
||||
|
||||
uuid@14.0.0:
|
||||
resolution: {integrity: sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==}
|
||||
hasBin: true
|
||||
|
||||
uuid@8.3.2:
|
||||
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
|
||||
hasBin: true
|
||||
@@ -25779,6 +25783,8 @@ snapshots:
|
||||
|
||||
uuid@11.1.0: {}
|
||||
|
||||
uuid@14.0.0: {}
|
||||
|
||||
uuid@8.3.2: {}
|
||||
|
||||
validator@13.15.35: {}
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
"thumbhash": "^0.1.1",
|
||||
"transformation-matrix": "^3.1.0",
|
||||
"ua-parser-js": "^2.0.0",
|
||||
"uuid": "^11.1.0",
|
||||
"uuid": "^14.0.0",
|
||||
"validator": "^13.12.0",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
|
||||
@@ -42,19 +42,19 @@ import { configureUserAgent } from 'src/utils/fetch';
|
||||
|
||||
const common = [...repositories, ...services, GlobalExceptionFilter];
|
||||
|
||||
const configRepository = new ConfigRepository();
|
||||
const { bull, cls, database, otel } = configRepository.getEnv();
|
||||
|
||||
const commonMiddleware = [
|
||||
{ provide: APP_FILTER, useClass: GlobalExceptionFilter },
|
||||
{ provide: APP_PIPE, useClass: ZodValidationPipe },
|
||||
{ provide: APP_INTERCEPTOR, useClass: ZodSerializerInterceptor },
|
||||
...(configRepository.isDev() ? [{ provide: APP_INTERCEPTOR, useClass: ZodSerializerInterceptor }] : []),
|
||||
{ provide: APP_INTERCEPTOR, useClass: LoggingInterceptor },
|
||||
{ provide: APP_INTERCEPTOR, useClass: ErrorInterceptor },
|
||||
];
|
||||
|
||||
const apiMiddleware = [FileUploadInterceptor, ...commonMiddleware, { provide: APP_GUARD, useClass: AuthGuard }];
|
||||
|
||||
const configRepository = new ConfigRepository();
|
||||
const { bull, cls, database, otel } = configRepository.getEnv();
|
||||
|
||||
const commonImports = [
|
||||
ClsModule.forRoot(cls.config),
|
||||
KyselyModule.forRoot(getKyselyConfig(database.config)),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Patch, Post, Put, Query } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { ZodSerializerDto } from 'nestjs-zod';
|
||||
import { Endpoint, HistoryBuilder } from 'src/decorators';
|
||||
import {
|
||||
AddUsersDto,
|
||||
@@ -27,6 +28,7 @@ export class AlbumController {
|
||||
|
||||
@Get()
|
||||
@Authenticated({ permission: Permission.AlbumRead })
|
||||
@ZodSerializerDto([AlbumResponseDto])
|
||||
@Endpoint({
|
||||
summary: 'List all albums',
|
||||
description: 'Retrieve a list of albums available to the authenticated user.',
|
||||
@@ -38,6 +40,7 @@ export class AlbumController {
|
||||
|
||||
@Post()
|
||||
@Authenticated({ permission: Permission.AlbumCreate })
|
||||
@ZodSerializerDto(AlbumResponseDto)
|
||||
@Endpoint({
|
||||
summary: 'Create an album',
|
||||
description: 'Create a new album. The album can also be created with initial users and assets.',
|
||||
@@ -60,6 +63,7 @@ export class AlbumController {
|
||||
|
||||
@Authenticated({ permission: Permission.AlbumRead, sharedLink: true })
|
||||
@Get(':id')
|
||||
@ZodSerializerDto(AlbumResponseDto)
|
||||
@Endpoint({
|
||||
summary: 'Retrieve an album',
|
||||
description: 'Retrieve information about a specific album by its ID.',
|
||||
@@ -71,6 +75,7 @@ export class AlbumController {
|
||||
|
||||
@Patch(':id')
|
||||
@Authenticated({ permission: Permission.AlbumUpdate })
|
||||
@ZodSerializerDto(AlbumResponseDto)
|
||||
@Endpoint({
|
||||
summary: 'Update an album',
|
||||
description:
|
||||
@@ -152,6 +157,7 @@ export class AlbumController {
|
||||
|
||||
@Put(':id/users')
|
||||
@Authenticated({ permission: Permission.AlbumUserCreate })
|
||||
@ZodSerializerDto(AlbumResponseDto)
|
||||
@Endpoint({
|
||||
summary: 'Share album with users',
|
||||
description: 'Share an album with multiple users. Each user can be given a specific role in the album.',
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { AssetController } from 'src/controllers/asset.controller';
|
||||
import { mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AssetMetadataKey } from 'src/enum';
|
||||
import { AssetService } from 'src/services/asset.service';
|
||||
import request from 'supertest';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { getForAsset } from 'test/mappers';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
|
||||
|
||||
@@ -183,6 +186,10 @@ describe(AssetController.name, () => {
|
||||
});
|
||||
|
||||
describe('PUT /assets/:id', () => {
|
||||
beforeEach(() => {
|
||||
service.update.mockResolvedValue(mapAsset(getForAsset(AssetFactory.create())));
|
||||
});
|
||||
|
||||
it('should be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).get(`/assets/123`);
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { ZodSerializerDto } from 'nestjs-zod';
|
||||
import { Endpoint, HistoryBuilder } from 'src/decorators';
|
||||
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||
import {
|
||||
@@ -79,6 +80,7 @@ export class AssetController {
|
||||
|
||||
@Get(':id')
|
||||
@Authenticated({ permission: Permission.AssetRead, sharedLink: true })
|
||||
@ZodSerializerDto(AssetResponseDto)
|
||||
@Endpoint({
|
||||
summary: 'Retrieve an asset',
|
||||
description: 'Retrieve detailed information about a specific asset.',
|
||||
@@ -128,6 +130,7 @@ export class AssetController {
|
||||
|
||||
@Put(':id')
|
||||
@Authenticated({ permission: Permission.AssetUpdate })
|
||||
@ZodSerializerDto(AssetResponseDto)
|
||||
@Endpoint({
|
||||
summary: 'Update an asset',
|
||||
description: 'Update information of a specific asset.',
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { AuthController } from 'src/controllers/auth.controller';
|
||||
import { LoginResponseDto } from 'src/dtos/auth.dto';
|
||||
import { mapUserAdmin } from 'src/dtos/user.dto';
|
||||
import { AuthService } from 'src/services/auth.service';
|
||||
import request from 'supertest';
|
||||
import { UserFactory } from 'test/factories/user.factory';
|
||||
import { mediumFactory } from 'test/medium.factory';
|
||||
import { errorDto } from 'test/medium/responses';
|
||||
import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
|
||||
@@ -53,6 +55,7 @@ describe(AuthController.name, () => {
|
||||
|
||||
it('should transform email to lower case', async () => {
|
||||
service.adminSignUp.mockReset();
|
||||
service.adminSignUp.mockResolvedValue(mapUserAdmin(UserFactory.create()));
|
||||
const { status } = await request(ctx.getHttpServer())
|
||||
.post('/auth/admin-sign-up')
|
||||
.send({ name: 'admin', password: 'password', email: 'aDmIn@IMMICH.cloud' });
|
||||
@@ -61,6 +64,7 @@ describe(AuthController.name, () => {
|
||||
});
|
||||
|
||||
it('should accept an email with a local domain', async () => {
|
||||
service.adminSignUp.mockResolvedValue(mapUserAdmin(UserFactory.create()));
|
||||
const { status } = await request(ctx.getHttpServer())
|
||||
.post('/auth/admin-sign-up')
|
||||
.send({ name: 'admin', password: 'password', email: 'admin@local' });
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Post, Put, Req, Res } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { Request, Response } from 'express';
|
||||
import { ZodSerializerDto } from 'nestjs-zod';
|
||||
import { Endpoint, HistoryBuilder } from 'src/decorators';
|
||||
import {
|
||||
AuthDto,
|
||||
@@ -50,6 +51,7 @@ export class AuthController {
|
||||
}
|
||||
|
||||
@Post('admin-sign-up')
|
||||
@ZodSerializerDto(UserAdminResponseDto)
|
||||
@Endpoint({
|
||||
summary: 'Register admin',
|
||||
description: 'Create the first admin user in the system.',
|
||||
@@ -74,6 +76,7 @@ export class AuthController {
|
||||
@Post('change-password')
|
||||
@Authenticated({ permission: Permission.AuthChangePassword })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ZodSerializerDto(UserAdminResponseDto)
|
||||
@Endpoint({
|
||||
summary: 'Change password',
|
||||
description: 'Change the password of the current user.',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { ZodSerializerDto } from 'nestjs-zod';
|
||||
import { Endpoint, HistoryBuilder } from 'src/decorators';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import {
|
||||
@@ -44,6 +45,7 @@ export class FaceController {
|
||||
|
||||
@Put(':id')
|
||||
@Authenticated({ permission: Permission.FaceUpdate })
|
||||
@ZodSerializerDto(PersonResponseDto)
|
||||
@Endpoint({
|
||||
summary: 'Re-assign a face to another person',
|
||||
description: 'Re-assign the face provided in the body to the person identified by the id in the path parameter.',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Body, Controller, Get, HttpCode, HttpStatus, Post, Redirect, Req, Res } from '@nestjs/common';
|
||||
import { ApiConsumes, ApiTags } from '@nestjs/swagger';
|
||||
import { Request, Response } from 'express';
|
||||
import { ZodSerializerDto } from 'nestjs-zod';
|
||||
import { Endpoint, HistoryBuilder } from 'src/decorators';
|
||||
import {
|
||||
AuthDto,
|
||||
@@ -89,6 +90,7 @@ export class OAuthController {
|
||||
@Post('link')
|
||||
@Authenticated()
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ZodSerializerDto(UserAdminResponseDto)
|
||||
@Endpoint({
|
||||
summary: 'Link OAuth account',
|
||||
description: 'Link an OAuth account to the authenticated user.',
|
||||
@@ -105,6 +107,7 @@ export class OAuthController {
|
||||
@Post('unlink')
|
||||
@Authenticated()
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ZodSerializerDto(UserAdminResponseDto)
|
||||
@Endpoint({
|
||||
summary: 'Unlink OAuth account',
|
||||
description: 'Unlink the OAuth account from the authenticated user.',
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { NextFunction, Response } from 'express';
|
||||
import { ZodSerializerDto } from 'nestjs-zod';
|
||||
import { Endpoint, HistoryBuilder } from 'src/decorators';
|
||||
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
@@ -58,6 +59,7 @@ export class PersonController {
|
||||
|
||||
@Post()
|
||||
@Authenticated({ permission: Permission.PersonCreate })
|
||||
@ZodSerializerDto(PersonResponseDto)
|
||||
@Endpoint({
|
||||
summary: 'Create a person',
|
||||
description: 'Create a new person that can have multiple faces assigned to them.',
|
||||
@@ -92,6 +94,7 @@ export class PersonController {
|
||||
|
||||
@Get(':id')
|
||||
@Authenticated({ permission: Permission.PersonRead })
|
||||
@ZodSerializerDto(PersonResponseDto)
|
||||
@Endpoint({
|
||||
summary: 'Get a person',
|
||||
description: 'Retrieve a person by id.',
|
||||
@@ -103,6 +106,7 @@ export class PersonController {
|
||||
|
||||
@Put(':id')
|
||||
@Authenticated({ permission: Permission.PersonUpdate })
|
||||
@ZodSerializerDto(PersonResponseDto)
|
||||
@Endpoint({
|
||||
summary: 'Update person',
|
||||
description: 'Update an individual person.',
|
||||
@@ -158,6 +162,7 @@ export class PersonController {
|
||||
|
||||
@Put(':id/reassign')
|
||||
@Authenticated({ permission: Permission.PersonReassign })
|
||||
@ZodSerializerDto([PersonResponseDto])
|
||||
@Endpoint({
|
||||
summary: 'Reassign faces',
|
||||
description: 'Bulk reassign a list of faces to a different person.',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Body, Controller, Get, HttpCode, HttpStatus, Post, Query } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { ZodSerializerDto } from 'nestjs-zod';
|
||||
import { Endpoint, HistoryBuilder } from 'src/decorators';
|
||||
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
@@ -54,6 +55,7 @@ export class SearchController {
|
||||
@Post('random')
|
||||
@Authenticated({ permission: Permission.AssetRead })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ZodSerializerDto([AssetResponseDto])
|
||||
@Endpoint({
|
||||
summary: 'Search random assets',
|
||||
description: 'Retrieve a random selection of assets based on the provided criteria.',
|
||||
@@ -66,6 +68,7 @@ export class SearchController {
|
||||
@Post('large-assets')
|
||||
@Authenticated({ permission: Permission.AssetRead })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ZodSerializerDto([AssetResponseDto])
|
||||
@Endpoint({
|
||||
summary: 'Search large assets',
|
||||
description: 'Search for assets that are considered large based on specified criteria.',
|
||||
@@ -100,6 +103,7 @@ export class SearchController {
|
||||
|
||||
@Get('person')
|
||||
@Authenticated({ permission: Permission.PersonRead })
|
||||
@ZodSerializerDto([PersonResponseDto])
|
||||
@Endpoint({
|
||||
summary: 'Search people',
|
||||
description: 'Search for people by name.',
|
||||
@@ -122,6 +126,7 @@ export class SearchController {
|
||||
|
||||
@Get('cities')
|
||||
@Authenticated({ permission: Permission.AssetRead })
|
||||
@ZodSerializerDto([AssetResponseDto])
|
||||
@Endpoint({
|
||||
summary: 'Retrieve assets by city',
|
||||
description:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { ZodSerializerDto } from 'nestjs-zod';
|
||||
import { Endpoint, HistoryBuilder } from 'src/decorators';
|
||||
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
@@ -23,6 +24,7 @@ export class TagController {
|
||||
|
||||
@Post()
|
||||
@Authenticated({ permission: Permission.TagCreate })
|
||||
@ZodSerializerDto(TagResponseDto)
|
||||
@Endpoint({
|
||||
summary: 'Create a tag',
|
||||
description: 'Create a new tag by providing a name and optional color.',
|
||||
@@ -34,6 +36,7 @@ export class TagController {
|
||||
|
||||
@Get()
|
||||
@Authenticated({ permission: Permission.TagRead })
|
||||
@ZodSerializerDto([TagResponseDto])
|
||||
@Endpoint({
|
||||
summary: 'Retrieve tags',
|
||||
description: 'Retrieve a list of all tags.',
|
||||
@@ -45,6 +48,7 @@ export class TagController {
|
||||
|
||||
@Put()
|
||||
@Authenticated({ permission: Permission.TagCreate })
|
||||
@ZodSerializerDto([TagResponseDto])
|
||||
@Endpoint({
|
||||
summary: 'Upsert tags',
|
||||
description: 'Create or update multiple tags in a single request.',
|
||||
@@ -67,6 +71,7 @@ export class TagController {
|
||||
|
||||
@Get(':id')
|
||||
@Authenticated({ permission: Permission.TagRead })
|
||||
@ZodSerializerDto(TagResponseDto)
|
||||
@Endpoint({
|
||||
summary: 'Retrieve a tag',
|
||||
description: 'Retrieve a specific tag by its ID.',
|
||||
@@ -78,6 +83,7 @@ export class TagController {
|
||||
|
||||
@Put(':id')
|
||||
@Authenticated({ permission: Permission.TagUpdate })
|
||||
@ZodSerializerDto(TagResponseDto)
|
||||
@Endpoint({
|
||||
summary: 'Update a tag',
|
||||
description: 'Update an existing tag identified by its ID.',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { ZodSerializerDto } from 'nestjs-zod';
|
||||
import { Endpoint, HistoryBuilder } from 'src/decorators';
|
||||
import { AssetStatsDto, AssetStatsResponseDto } from 'src/dtos/asset.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
@@ -24,6 +25,7 @@ export class UserAdminController {
|
||||
|
||||
@Get()
|
||||
@Authenticated({ permission: Permission.AdminUserRead, admin: true })
|
||||
@ZodSerializerDto([UserAdminResponseDto])
|
||||
@Endpoint({
|
||||
summary: 'Search users',
|
||||
description: 'Search for users.',
|
||||
@@ -35,6 +37,7 @@ export class UserAdminController {
|
||||
|
||||
@Post()
|
||||
@Authenticated({ permission: Permission.AdminUserCreate, admin: true })
|
||||
@ZodSerializerDto(UserAdminResponseDto)
|
||||
@Endpoint({
|
||||
summary: 'Create a user',
|
||||
description: 'Create a new user.',
|
||||
@@ -46,6 +49,7 @@ export class UserAdminController {
|
||||
|
||||
@Get(':id')
|
||||
@Authenticated({ permission: Permission.AdminUserRead, admin: true })
|
||||
@ZodSerializerDto(UserAdminResponseDto)
|
||||
@Endpoint({
|
||||
summary: 'Retrieve a user',
|
||||
description: 'Retrieve a specific user by their ID.',
|
||||
@@ -57,6 +61,7 @@ export class UserAdminController {
|
||||
|
||||
@Put(':id')
|
||||
@Authenticated({ permission: Permission.AdminUserUpdate, admin: true })
|
||||
@ZodSerializerDto(UserAdminResponseDto)
|
||||
@Endpoint({
|
||||
summary: 'Update a user',
|
||||
description: 'Update an existing user.',
|
||||
@@ -72,6 +77,7 @@ export class UserAdminController {
|
||||
|
||||
@Delete(':id')
|
||||
@Authenticated({ permission: Permission.AdminUserDelete, admin: true })
|
||||
@ZodSerializerDto(UserAdminResponseDto)
|
||||
@Endpoint({
|
||||
summary: 'Delete a user',
|
||||
description: 'Delete a user.',
|
||||
@@ -140,6 +146,7 @@ export class UserAdminController {
|
||||
@Post(':id/restore')
|
||||
@Authenticated({ permission: Permission.AdminUserDelete, admin: true })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ZodSerializerDto(UserAdminResponseDto)
|
||||
@Endpoint({
|
||||
summary: 'Restore a deleted user',
|
||||
description: 'Restore a previously deleted user.',
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';
|
||||
import { NextFunction, Response } from 'express';
|
||||
import { ZodSerializerDto } from 'nestjs-zod';
|
||||
import { Endpoint, HistoryBuilder } from 'src/decorators';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
|
||||
@@ -40,6 +41,7 @@ export class UserController {
|
||||
|
||||
@Get()
|
||||
@Authenticated({ permission: Permission.UserRead })
|
||||
@ZodSerializerDto([UserResponseDto])
|
||||
@Endpoint({
|
||||
summary: 'Get all users',
|
||||
description: 'Retrieve a list of all users on the server.',
|
||||
@@ -51,6 +53,7 @@ export class UserController {
|
||||
|
||||
@Get('me')
|
||||
@Authenticated({ permission: Permission.UserRead })
|
||||
@ZodSerializerDto(UserAdminResponseDto)
|
||||
@Endpoint({
|
||||
summary: 'Get current user',
|
||||
description: 'Retrieve information about the user making the API request.',
|
||||
@@ -62,6 +65,7 @@ export class UserController {
|
||||
|
||||
@Put('me')
|
||||
@Authenticated({ permission: Permission.UserUpdate })
|
||||
@ZodSerializerDto(UserAdminResponseDto)
|
||||
@Endpoint({
|
||||
summary: 'Update current user',
|
||||
description: 'Update the current user making the API request.',
|
||||
@@ -166,6 +170,7 @@ export class UserController {
|
||||
|
||||
@Get(':id')
|
||||
@Authenticated({ permission: Permission.UserRead })
|
||||
@ZodSerializerDto(UserResponseDto)
|
||||
@Endpoint({
|
||||
summary: 'Retrieve a user',
|
||||
description: 'Retrieve a specific user by their ID.',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Controller, Get, Query } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { ZodSerializerDto } from 'nestjs-zod';
|
||||
import { Endpoint, HistoryBuilder } from 'src/decorators';
|
||||
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
@@ -25,6 +26,7 @@ export class ViewController {
|
||||
|
||||
@Get('folder')
|
||||
@Authenticated({ permission: Permission.FolderRead })
|
||||
@ZodSerializerDto([AssetResponseDto])
|
||||
@Endpoint({
|
||||
summary: 'Retrieve assets by original path',
|
||||
description: 'Retrieve assets that are children of a specific folder.',
|
||||
|
||||
@@ -11,8 +11,8 @@ describe('mapAlbum', () => {
|
||||
.asset({ localDateTime: startDate }, (builder) => builder.exif())
|
||||
.build();
|
||||
const dto = mapAlbum(getForAlbum(album));
|
||||
expect(dto.startDate).toEqual(startDate.toISOString());
|
||||
expect(dto.endDate).toEqual(endDate.toISOString());
|
||||
expect(dto.startDate).toEqual(startDate);
|
||||
expect(dto.endDate).toEqual(endDate);
|
||||
});
|
||||
|
||||
it('should not set start and end dates for empty assets', () => {
|
||||
|
||||
@@ -6,8 +6,7 @@ import { MapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { UserResponseSchema, mapUser } from 'src/dtos/user.dto';
|
||||
import { AlbumUserRole, AlbumUserRoleSchema, AssetOrder, AssetOrderSchema } from 'src/enum';
|
||||
import { MaybeDehydrated } from 'src/types';
|
||||
import { asDateString } from 'src/utils/date';
|
||||
import { stringToBool } from 'src/validation';
|
||||
import { isoDatetimeToDate, stringToBool } from 'src/validation';
|
||||
import z from 'zod';
|
||||
|
||||
const AlbumUserAddSchema = z
|
||||
@@ -105,10 +104,8 @@ export const AlbumResponseSchema = z
|
||||
id: z.string().describe('Album ID'),
|
||||
albumName: z.string().describe('Album name'),
|
||||
description: z.string().describe('Album description'),
|
||||
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers.
|
||||
createdAt: z.string().meta({ format: 'date-time' }).describe('Creation date'),
|
||||
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers.
|
||||
updatedAt: z.string().meta({ format: 'date-time' }).describe('Last update date'),
|
||||
createdAt: isoDatetimeToDate.describe('Creation date'),
|
||||
updatedAt: isoDatetimeToDate.describe('Last update date'),
|
||||
albumThumbnailAssetId: z.string().nullable().describe('Thumbnail asset ID'),
|
||||
shared: z.boolean().describe('Is shared album'),
|
||||
albumUsers: z
|
||||
@@ -119,16 +116,9 @@ export const AlbumResponseSchema = z
|
||||
),
|
||||
hasSharedLink: z.boolean().describe('Has shared link'),
|
||||
assetCount: z.int().min(0).describe('Number of assets'),
|
||||
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers.
|
||||
lastModifiedAssetTimestamp: z
|
||||
.string()
|
||||
.meta({ format: 'date-time' })
|
||||
.optional()
|
||||
.describe('Last modified asset timestamp'),
|
||||
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers.
|
||||
startDate: z.string().meta({ format: 'date-time' }).optional().describe('Start date (earliest asset)'),
|
||||
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers.
|
||||
endDate: z.string().meta({ format: 'date-time' }).optional().describe('End date (latest asset)'),
|
||||
lastModifiedAssetTimestamp: isoDatetimeToDate.optional().describe('Last modified asset timestamp'),
|
||||
startDate: isoDatetimeToDate.optional().describe('Start date (earliest asset)'),
|
||||
endDate: isoDatetimeToDate.optional().describe('End date (latest asset)'),
|
||||
isActivityEnabled: z.boolean().describe('Activity feed enabled'),
|
||||
order: AssetOrderSchema.optional(),
|
||||
contributorCounts: z.array(ContributorCountResponseSchema).optional(),
|
||||
@@ -144,7 +134,7 @@ export class UpdateAlbumDto extends createZodDto(UpdateAlbumSchema) {}
|
||||
export class GetAlbumsDto extends createZodDto(GetAlbumsSchema) {}
|
||||
export class AlbumStatisticsResponseDto extends createZodDto(AlbumStatisticsResponseSchema) {}
|
||||
export class UpdateAlbumUserDto extends createZodDto(UpdateAlbumUserSchema) {}
|
||||
export class AlbumResponseDto extends createZodDto(AlbumResponseSchema) {}
|
||||
export class AlbumResponseDto extends createZodDto(AlbumResponseSchema, { codec: true }) {}
|
||||
class AlbumUserResponseDto extends createZodDto(AlbumUserResponseSchema) {}
|
||||
|
||||
export type MapAlbumDto = {
|
||||
@@ -190,14 +180,14 @@ export const mapAlbum = (entity: MaybeDehydrated<MapAlbumDto>): AlbumResponseDto
|
||||
albumName: entity.albumName,
|
||||
description: entity.description,
|
||||
albumThumbnailAssetId: entity.albumThumbnailAssetId,
|
||||
createdAt: asDateString(entity.createdAt),
|
||||
updatedAt: asDateString(entity.updatedAt),
|
||||
createdAt: new Date(entity.createdAt),
|
||||
updatedAt: new Date(entity.updatedAt),
|
||||
id: entity.id,
|
||||
albumUsers,
|
||||
shared: hasSharedUser || hasSharedLink,
|
||||
hasSharedLink,
|
||||
startDate: asDateString(startDate),
|
||||
endDate: asDateString(endDate),
|
||||
startDate: startDate ? new Date(startDate) : undefined,
|
||||
endDate: endDate ? new Date(endDate) : undefined,
|
||||
assetCount: entity.assets?.length || 0,
|
||||
isActivityEnabled: entity.isActivityEnabled,
|
||||
order: entity.order,
|
||||
|
||||
@@ -25,8 +25,8 @@ import {
|
||||
import { ImageDimensions, MaybeDehydrated } from 'src/types';
|
||||
import { getDimensions } from 'src/utils/asset.util';
|
||||
import { hexOrBufferToBase64 } from 'src/utils/bytes';
|
||||
import { asDateString } from 'src/utils/date';
|
||||
import { mimeTypes } from 'src/utils/mime-types';
|
||||
import { isoDatetimeToDate } from 'src/validation';
|
||||
import z from 'zod';
|
||||
|
||||
const SanitizedAssetResponseSchema = z
|
||||
@@ -40,13 +40,9 @@ const SanitizedAssetResponseSchema = z
|
||||
)
|
||||
.nullable(),
|
||||
originalMimeType: z.string().optional().describe('Original MIME type'),
|
||||
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers.
|
||||
localDateTime: z
|
||||
.string()
|
||||
.meta({ format: 'date-time' })
|
||||
.describe(
|
||||
'The local date and time when the photo/video was taken, derived from EXIF metadata. This represents the photographer\'s local time regardless of timezone, stored as a timezone-agnostic timestamp. Used for timeline grouping by "local" days and months.',
|
||||
),
|
||||
localDateTime: isoDatetimeToDate.describe(
|
||||
'The local date and time when the photo/video was taken, derived from EXIF metadata. This represents the photographer\'s local time regardless of timezone, stored as a timezone-agnostic timestamp. Used for timeline grouping by "local" days and months.',
|
||||
),
|
||||
duration: z.string().nullable().describe('Video/gif duration in hh:mm:ss.SSS format (null for static images)'),
|
||||
livePhotoVideoId: z.string().nullish().describe('Live photo video ID'),
|
||||
hasMetadata: z.boolean().describe('Whether asset has metadata'),
|
||||
@@ -55,7 +51,7 @@ const SanitizedAssetResponseSchema = z
|
||||
})
|
||||
.meta({ id: 'SanitizedAssetResponseDto' });
|
||||
|
||||
export class SanitizedAssetResponseDto extends createZodDto(SanitizedAssetResponseSchema) {}
|
||||
export class SanitizedAssetResponseDto extends createZodDto(SanitizedAssetResponseSchema, { codec: true }) {}
|
||||
|
||||
const AssetStackResponseSchema = z
|
||||
.object({
|
||||
@@ -67,11 +63,7 @@ const AssetStackResponseSchema = z
|
||||
|
||||
export const AssetResponseSchema = SanitizedAssetResponseSchema.extend(
|
||||
z.object({
|
||||
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers.
|
||||
createdAt: z
|
||||
.string()
|
||||
.meta({ format: 'date-time' })
|
||||
.describe('The UTC timestamp when the asset was originally uploaded to Immich.'),
|
||||
createdAt: isoDatetimeToDate.describe('The UTC timestamp when the asset was originally uploaded to Immich.'),
|
||||
ownerId: z.string().describe('Owner user ID'),
|
||||
owner: UserResponseSchema.optional(),
|
||||
libraryId: z
|
||||
@@ -81,25 +73,15 @@ export const AssetResponseSchema = SanitizedAssetResponseSchema.extend(
|
||||
.meta(new HistoryBuilder().added('v1').deprecated('v1').getExtensions()),
|
||||
originalPath: z.string().describe('Original file path'),
|
||||
originalFileName: z.string().describe('Original file name'),
|
||||
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers.
|
||||
fileCreatedAt: z
|
||||
.string()
|
||||
.meta({ format: 'date-time' })
|
||||
.describe(
|
||||
'The actual UTC timestamp when the file was created/captured, preserving timezone information. This is the authoritative timestamp for chronological sorting within timeline groups. Combined with timezone data, this can be used to determine the exact moment the photo was taken.',
|
||||
),
|
||||
fileModifiedAt: z
|
||||
.string()
|
||||
.meta({ format: 'date-time' })
|
||||
.describe(
|
||||
'The UTC timestamp when the file was last modified on the filesystem. This reflects the last time the physical file was changed, which may be different from when the photo was originally taken.',
|
||||
),
|
||||
updatedAt: z
|
||||
.string()
|
||||
.meta({ format: 'date-time' })
|
||||
.describe(
|
||||
'The UTC timestamp when the asset record was last updated in the database. This is automatically maintained by the database and reflects when any field in the asset was last modified.',
|
||||
),
|
||||
fileCreatedAt: isoDatetimeToDate.describe(
|
||||
'The actual UTC timestamp when the file was created/captured, preserving timezone information. This is the authoritative timestamp for chronological sorting within timeline groups. Combined with timezone data, this can be used to determine the exact moment the photo was taken.',
|
||||
),
|
||||
fileModifiedAt: isoDatetimeToDate.describe(
|
||||
'The UTC timestamp when the file was last modified on the filesystem. This reflects the last time the physical file was changed, which may be different from when the photo was originally taken.',
|
||||
),
|
||||
updatedAt: isoDatetimeToDate.describe(
|
||||
'The UTC timestamp when the asset record was last updated in the database. This is automatically maintained by the database and reflects when any field in the asset was last modified.',
|
||||
),
|
||||
isFavorite: z.boolean().describe('Is favorite'),
|
||||
isArchived: z.boolean().describe('Is archived'),
|
||||
isTrashed: z.boolean().describe('Is trashed'),
|
||||
@@ -124,7 +106,7 @@ export const AssetResponseSchema = SanitizedAssetResponseSchema.extend(
|
||||
}).shape,
|
||||
).meta({ id: 'AssetResponseDto' });
|
||||
|
||||
export class AssetResponseDto extends createZodDto(AssetResponseSchema) {}
|
||||
export class AssetResponseDto extends createZodDto(AssetResponseSchema, { codec: true }) {}
|
||||
|
||||
export type MapAsset = {
|
||||
createdAt: Date;
|
||||
@@ -220,7 +202,7 @@ export function mapAsset(entity: MaybeDehydrated<MapAsset>, options: AssetMapOpt
|
||||
type: entity.type,
|
||||
originalMimeType: mimeTypes.lookup(entity.originalFileName),
|
||||
thumbhash: entity.thumbhash ? hexOrBufferToBase64(entity.thumbhash) : null,
|
||||
localDateTime: asDateString(entity.localDateTime),
|
||||
localDateTime: new Date(entity.localDateTime),
|
||||
duration: entity.duration,
|
||||
livePhotoVideoId: entity.livePhotoVideoId,
|
||||
hasMetadata: false,
|
||||
@@ -234,7 +216,7 @@ export function mapAsset(entity: MaybeDehydrated<MapAsset>, options: AssetMapOpt
|
||||
|
||||
return {
|
||||
id: entity.id,
|
||||
createdAt: asDateString(entity.createdAt),
|
||||
createdAt: new Date(entity.createdAt),
|
||||
ownerId: entity.ownerId,
|
||||
owner: entity.owner ? mapUser(entity.owner) : undefined,
|
||||
libraryId: entity.libraryId,
|
||||
@@ -243,10 +225,10 @@ export function mapAsset(entity: MaybeDehydrated<MapAsset>, options: AssetMapOpt
|
||||
originalFileName: entity.originalFileName,
|
||||
originalMimeType: mimeTypes.lookup(entity.originalFileName),
|
||||
thumbhash: entity.thumbhash ? hexOrBufferToBase64(entity.thumbhash) : null,
|
||||
fileCreatedAt: asDateString(entity.fileCreatedAt),
|
||||
fileModifiedAt: asDateString(entity.fileModifiedAt),
|
||||
localDateTime: asDateString(entity.localDateTime),
|
||||
updatedAt: asDateString(entity.updatedAt),
|
||||
fileCreatedAt: new Date(entity.fileCreatedAt),
|
||||
fileModifiedAt: new Date(entity.fileModifiedAt),
|
||||
localDateTime: new Date(entity.localDateTime),
|
||||
updatedAt: new Date(entity.updatedAt),
|
||||
isFavorite: options.auth?.user.id === entity.ownerId && entity.isFavorite,
|
||||
isArchived: entity.visibility === AssetVisibility.Archive,
|
||||
isTrashed: !!entity.deletedAt,
|
||||
|
||||
@@ -21,7 +21,10 @@ export type AuthDto = {
|
||||
|
||||
const LoginCredentialSchema = z
|
||||
.object({
|
||||
email: toEmail.describe('User email').meta({ example: 'testuser@email.com' }),
|
||||
email: toEmail
|
||||
.transform((val) => val.toLowerCase())
|
||||
.describe('User email')
|
||||
.meta({ example: 'testuser@email.com' }),
|
||||
password: z.string().describe('User password').meta({ example: 'password' }),
|
||||
})
|
||||
.meta({ id: 'LoginCredentialDto' });
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createZodDto } from 'nestjs-zod';
|
||||
import { Exif } from 'src/database';
|
||||
import { MaybeDehydrated } from 'src/types';
|
||||
import { asDateString } from 'src/utils/date';
|
||||
import { isoDatetimeToDate } from 'src/validation';
|
||||
import z from 'zod';
|
||||
|
||||
export const ExifResponseSchema = z
|
||||
@@ -12,10 +12,8 @@ export const ExifResponseSchema = z
|
||||
exifImageHeight: z.number().min(0).nullish().default(null).describe('Image height in pixels'),
|
||||
fileSizeInByte: z.int().min(0).nullish().default(null).describe('File size in bytes'),
|
||||
orientation: z.string().nullish().default(null).describe('Image orientation'),
|
||||
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers.
|
||||
dateTimeOriginal: z.string().meta({ format: 'date-time' }).nullish().default(null).describe('Original date/time'),
|
||||
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers.
|
||||
modifyDate: z.string().meta({ format: 'date-time' }).nullish().default(null).describe('Modification date/time'),
|
||||
dateTimeOriginal: isoDatetimeToDate.nullish().default(null).describe('Original date/time'),
|
||||
modifyDate: isoDatetimeToDate.nullish().default(null).describe('Modification date/time'),
|
||||
timeZone: z.string().nullish().default(null).describe('Time zone'),
|
||||
lensModel: z.string().nullish().default(null).describe('Lens model'),
|
||||
fNumber: z.number().nullish().default(null).describe('F-number (aperture)'),
|
||||
@@ -34,7 +32,7 @@ export const ExifResponseSchema = z
|
||||
.describe('EXIF response')
|
||||
.meta({ id: 'ExifResponseDto' });
|
||||
|
||||
class ExifResponseDto extends createZodDto(ExifResponseSchema) {}
|
||||
class ExifResponseDto extends createZodDto(ExifResponseSchema, { codec: true }) {}
|
||||
|
||||
export function mapExif(entity: MaybeDehydrated<Exif>): ExifResponseDto {
|
||||
return {
|
||||
@@ -44,8 +42,8 @@ export function mapExif(entity: MaybeDehydrated<Exif>): ExifResponseDto {
|
||||
exifImageHeight: entity.exifImageHeight,
|
||||
fileSizeInByte: entity.fileSizeInByte ? Number.parseInt(entity.fileSizeInByte.toString()) : null,
|
||||
orientation: entity.orientation,
|
||||
dateTimeOriginal: asDateString(entity.dateTimeOriginal),
|
||||
modifyDate: asDateString(entity.modifyDate),
|
||||
dateTimeOriginal: entity.dateTimeOriginal ? new Date(entity.dateTimeOriginal) : null,
|
||||
modifyDate: entity.modifyDate ? new Date(entity.modifyDate) : null,
|
||||
timeZone: entity.timeZone,
|
||||
lensModel: entity.lensModel,
|
||||
fNumber: entity.fNumber,
|
||||
|
||||
@@ -7,9 +7,8 @@ import { AssetEditActionItem } from 'src/dtos/editing.dto';
|
||||
import { SourceTypeSchema } from 'src/enum';
|
||||
import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
|
||||
import { ImageDimensions, MaybeDehydrated } from 'src/types';
|
||||
import { asBirthDateString, asDateString } from 'src/utils/date';
|
||||
import { transformFaceBoundingBox } from 'src/utils/transform';
|
||||
import { emptyStringToNull, hexColor, stringToBool } from 'src/validation';
|
||||
import { emptyStringToNull, hexColor, isoDateToDate, isoDatetimeToDate, stringToBool } from 'src/validation';
|
||||
import z from 'zod';
|
||||
|
||||
const PersonCreateSchema = z
|
||||
@@ -60,14 +59,10 @@ const PersonResponseSchema = z
|
||||
.object({
|
||||
id: z.string().describe('Person ID'),
|
||||
name: z.string().describe('Person name'),
|
||||
// TODO: use `isoDateToDate` when using `ZodSerializerDto` on the controllers.
|
||||
birthDate: z.string().meta({ format: 'date' }).describe('Person date of birth').nullable(),
|
||||
birthDate: isoDateToDate.nullable().describe('Person date of birth'),
|
||||
thumbnailPath: z.string().describe('Thumbnail path'),
|
||||
isHidden: z.boolean().describe('Is hidden'),
|
||||
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers.
|
||||
updatedAt: z
|
||||
.string()
|
||||
.meta({ format: 'date-time' })
|
||||
updatedAt: isoDatetimeToDate
|
||||
.optional()
|
||||
.describe('Last update date')
|
||||
.meta(new HistoryBuilder().added('v1.107.0').stable('v2').getExtensions()),
|
||||
@@ -89,7 +84,7 @@ export class PersonUpdateDto extends createZodDto(PersonUpdateSchema) {}
|
||||
export class PeopleUpdateDto extends createZodDto(PeopleUpdateSchema) {}
|
||||
export class MergePersonDto extends createZodDto(MergePersonSchema) {}
|
||||
export class PersonSearchDto extends createZodDto(PersonSearchSchema) {}
|
||||
export class PersonResponseDto extends createZodDto(PersonResponseSchema) {}
|
||||
export class PersonResponseDto extends createZodDto(PersonResponseSchema, { codec: true }) {}
|
||||
|
||||
export const AssetFaceWithoutPersonResponseSchema = z
|
||||
.object({
|
||||
@@ -111,7 +106,7 @@ export const PersonWithFacesResponseSchema = PersonResponseSchema.extend({
|
||||
faces: z.array(AssetFaceWithoutPersonResponseSchema),
|
||||
}).meta({ id: 'PersonWithFacesResponseDto' });
|
||||
|
||||
export class PersonWithFacesResponseDto extends createZodDto(PersonWithFacesResponseSchema) {}
|
||||
export class PersonWithFacesResponseDto extends createZodDto(PersonWithFacesResponseSchema, { codec: true }) {}
|
||||
|
||||
const AssetFaceResponseSchema = AssetFaceWithoutPersonResponseSchema.extend({
|
||||
person: PersonResponseSchema.nullable(),
|
||||
@@ -184,12 +179,12 @@ export function mapPerson(person: MaybeDehydrated<Person>): PersonResponseDto {
|
||||
return {
|
||||
id: person.id,
|
||||
name: person.name,
|
||||
birthDate: asBirthDateString(person.birthDate),
|
||||
birthDate: person.birthDate ? new Date(person.birthDate) : null,
|
||||
thumbnailPath: person.thumbnailPath,
|
||||
isHidden: person.isHidden,
|
||||
isFavorite: person.isFavorite,
|
||||
color: person.color ?? undefined,
|
||||
updatedAt: asDateString(person.updatedAt),
|
||||
updatedAt: person.updatedAt ? new Date(person.updatedAt) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { createZodDto } from 'nestjs-zod';
|
||||
import { Tag } from 'src/database';
|
||||
import { MaybeDehydrated } from 'src/types';
|
||||
import { asDateString } from 'src/utils/date';
|
||||
import { emptyStringToNull, hexColor } from 'src/validation';
|
||||
import { emptyStringToNull, hexColor, isoDatetimeToDate } from 'src/validation';
|
||||
import z from 'zod';
|
||||
|
||||
const TagCreateSchema = z
|
||||
@@ -44,10 +43,8 @@ export const TagResponseSchema = z
|
||||
parentId: z.string().optional().describe('Parent tag ID'),
|
||||
name: z.string().describe('Tag name'),
|
||||
value: z.string().describe('Tag value (full path)'),
|
||||
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers.
|
||||
createdAt: z.string().meta({ format: 'date-time' }).describe('Creation date'),
|
||||
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers.
|
||||
updatedAt: z.string().meta({ format: 'date-time' }).describe('Last update date'),
|
||||
createdAt: isoDatetimeToDate.describe('Creation date'),
|
||||
updatedAt: isoDatetimeToDate.describe('Last update date'),
|
||||
color: z.string().optional().describe('Tag color (hex)'),
|
||||
})
|
||||
.meta({ id: 'TagResponseDto' });
|
||||
@@ -57,7 +54,7 @@ export class TagUpdateDto extends createZodDto(TagUpdateSchema) {}
|
||||
export class TagUpsertDto extends createZodDto(TagUpsertSchema) {}
|
||||
export class TagBulkAssetsDto extends createZodDto(TagBulkAssetsSchema) {}
|
||||
export class TagBulkAssetsResponseDto extends createZodDto(TagBulkAssetsResponseSchema) {}
|
||||
export class TagResponseDto extends createZodDto(TagResponseSchema) {}
|
||||
export class TagResponseDto extends createZodDto(TagResponseSchema, { codec: true }) {}
|
||||
|
||||
export function mapTag(entity: MaybeDehydrated<Tag>): TagResponseDto {
|
||||
return {
|
||||
@@ -65,8 +62,8 @@ export function mapTag(entity: MaybeDehydrated<Tag>): TagResponseDto {
|
||||
parentId: entity.parentId ?? undefined,
|
||||
name: entity.value.split('/').at(-1) as string,
|
||||
value: entity.value,
|
||||
createdAt: asDateString(entity.createdAt),
|
||||
updatedAt: asDateString(entity.updatedAt),
|
||||
createdAt: new Date(entity.createdAt),
|
||||
updatedAt: new Date(entity.updatedAt),
|
||||
color: entity.color ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,13 +3,15 @@ import { User, UserAdmin } from 'src/database';
|
||||
import { pinCodeRegex } from 'src/dtos/auth.dto';
|
||||
import { UserAvatarColor, UserAvatarColorSchema, UserMetadataKey, UserStatusSchema } from 'src/enum';
|
||||
import { MaybeDehydrated, UserMetadataItem } from 'src/types';
|
||||
import { asDateString } from 'src/utils/date';
|
||||
import { emptyStringToNull, isoDatetimeToDate, sanitizeFilename, stringToBool, toEmail } from 'src/validation';
|
||||
import z from 'zod';
|
||||
|
||||
export const UserUpdateMeSchema = z
|
||||
.object({
|
||||
email: toEmail.optional().describe('User email'),
|
||||
email: toEmail
|
||||
.transform((val) => val.toLowerCase())
|
||||
.optional()
|
||||
.describe('User email'),
|
||||
password: z
|
||||
.string()
|
||||
.optional()
|
||||
@@ -29,12 +31,11 @@ export const UserResponseSchema = z
|
||||
email: toEmail.describe('User email'),
|
||||
profileImagePath: z.string().describe('Profile image path'),
|
||||
avatarColor: UserAvatarColorSchema,
|
||||
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers.
|
||||
profileChangedAt: z.string().meta({ format: 'date-time' }).describe('Profile change date'),
|
||||
profileChangedAt: isoDatetimeToDate.describe('Profile change date'),
|
||||
})
|
||||
.meta({ id: 'UserResponseDto' });
|
||||
|
||||
export class UserResponseDto extends createZodDto(UserResponseSchema) {}
|
||||
export class UserResponseDto extends createZodDto(UserResponseSchema, { codec: true }) {}
|
||||
|
||||
const licenseKeyRegex = /^IM(SV|CL)(-[\dA-Za-z]{4}){8}$/;
|
||||
|
||||
@@ -61,7 +62,7 @@ export const mapUser = (entity: MaybeDehydrated<User | UserAdmin>): UserResponse
|
||||
name: entity.name,
|
||||
profileImagePath: entity.profileImagePath,
|
||||
avatarColor: entity.avatarColor ?? emailToAvatarColor(entity.email),
|
||||
profileChangedAt: asDateString(entity.profileChangedAt),
|
||||
profileChangedAt: new Date(entity.profileChangedAt),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -76,7 +77,7 @@ export class UserAdminSearchDto extends createZodDto(UserAdminSearchSchema) {}
|
||||
|
||||
export const UserAdminCreateSchema = z
|
||||
.object({
|
||||
email: toEmail.describe('User email'),
|
||||
email: toEmail.transform((val) => val.toLowerCase()).describe('User email'),
|
||||
password: z.string().describe('User password'),
|
||||
name: z.string().describe('User name'),
|
||||
avatarColor: UserAvatarColorSchema.nullish(),
|
||||
@@ -96,7 +97,10 @@ export class UserAdminCreateDto extends createZodDto(UserAdminCreateSchema) {}
|
||||
|
||||
const UserAdminUpdateSchema = z
|
||||
.object({
|
||||
email: toEmail.optional().describe('User email'),
|
||||
email: toEmail
|
||||
.transform((val) => val.toLowerCase())
|
||||
.optional()
|
||||
.describe('User email'),
|
||||
password: z.string().optional().describe('User password'),
|
||||
pinCode: emptyStringToNull(z.string().regex(pinCodeRegex).nullable())
|
||||
.optional()
|
||||
@@ -135,7 +139,7 @@ const UserAdminResponseSchema = UserResponseSchema.extend({
|
||||
license: UserLicenseSchema.nullable(),
|
||||
}).meta({ id: 'UserAdminResponseDto' });
|
||||
|
||||
export class UserAdminResponseDto extends createZodDto(UserAdminResponseSchema) {}
|
||||
export class UserAdminResponseDto extends createZodDto(UserAdminResponseSchema, { codec: true }) {}
|
||||
|
||||
export function mapUserAdmin(entity: UserAdmin): UserAdminResponseDto {
|
||||
const metadata = entity.metadata || [];
|
||||
|
||||
@@ -19,7 +19,6 @@ import { AlbumUserRole, Permission } from 'src/enum';
|
||||
import { AlbumAssetCount, AlbumInfoOptions } from 'src/repositories/album.repository';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { addAssets, removeAssets } from 'src/utils/asset.util';
|
||||
import { asDateString } from 'src/utils/date';
|
||||
import { getPreferences } from 'src/utils/preferences';
|
||||
|
||||
@Injectable()
|
||||
@@ -63,11 +62,11 @@ export class AlbumService extends BaseService {
|
||||
return albums.map((album) => ({
|
||||
...mapAlbum(album),
|
||||
sharedLinks: undefined,
|
||||
startDate: asDateString(albumMetadata[album.id]?.startDate ?? undefined),
|
||||
endDate: asDateString(albumMetadata[album.id]?.endDate ?? undefined),
|
||||
startDate: albumMetadata[album.id]?.startDate ?? undefined,
|
||||
endDate: albumMetadata[album.id]?.endDate ?? undefined,
|
||||
assetCount: albumMetadata[album.id]?.assetCount ?? 0,
|
||||
// lastModifiedAssetTimestamp is only used in mobile app, please remove if not need
|
||||
lastModifiedAssetTimestamp: asDateString(albumMetadata[album.id]?.lastModifiedAssetTimestamp ?? undefined),
|
||||
lastModifiedAssetTimestamp: albumMetadata[album.id]?.lastModifiedAssetTimestamp ?? undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -83,10 +82,10 @@ export class AlbumService extends BaseService {
|
||||
|
||||
return {
|
||||
...mapAlbum(album),
|
||||
startDate: asDateString(albumMetadataForIds?.startDate ?? undefined),
|
||||
endDate: asDateString(albumMetadataForIds?.endDate ?? undefined),
|
||||
startDate: albumMetadataForIds?.startDate ?? undefined,
|
||||
endDate: albumMetadataForIds?.endDate ?? undefined,
|
||||
assetCount: albumMetadataForIds?.assetCount ?? 0,
|
||||
lastModifiedAssetTimestamp: asDateString(albumMetadataForIds?.lastModifiedAssetTimestamp ?? undefined),
|
||||
lastModifiedAssetTimestamp: albumMetadataForIds?.lastModifiedAssetTimestamp ?? undefined,
|
||||
contributorCounts: isShared ? await this.albumRepository.getContributorCounts(album.id) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -211,11 +211,12 @@ describe(PersonService.name, () => {
|
||||
await expect(sut.update(auth, person.id, { birthDate: '1976-06-30' })).resolves.toEqual({
|
||||
id: person.id,
|
||||
name: person.name,
|
||||
birthDate: '1976-06-30',
|
||||
birthDate: new Date('1976-06-30'),
|
||||
thumbnailPath: person.thumbnailPath,
|
||||
isHidden: false,
|
||||
isFavorite: false,
|
||||
updatedAt: expect.any(String),
|
||||
color: undefined,
|
||||
updatedAt: expect.any(Date),
|
||||
});
|
||||
expect(mocks.person.update).toHaveBeenCalledWith({ id: person.id, birthDate: '1976-06-30' });
|
||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||
@@ -485,10 +486,11 @@ describe(PersonService.name, () => {
|
||||
birthDate: person.birthDate,
|
||||
isHidden: person.isHidden,
|
||||
isFavorite: person.isFavorite,
|
||||
color: undefined,
|
||||
id: person.id,
|
||||
name: person.name,
|
||||
thumbnailPath: person.thumbnailPath,
|
||||
updatedAt: expect.any(String),
|
||||
updatedAt: expect.any(Date),
|
||||
});
|
||||
|
||||
expect(mocks.job.queue).not.toHaveBeenCalledWith();
|
||||
@@ -848,7 +850,7 @@ describe(PersonService.name, () => {
|
||||
facesRecognizedAt: expect.any(Date),
|
||||
});
|
||||
const facesRecognizedAt = mocks.asset.upsertJobStatus.mock.calls[0][0].facesRecognizedAt as Date;
|
||||
expect(facesRecognizedAt.getTime()).toBeGreaterThan(start);
|
||||
expect(facesRecognizedAt.getTime()).toBeGreaterThanOrEqual(start);
|
||||
});
|
||||
|
||||
it('should create a face with no person and queue recognition job', async () => {
|
||||
|
||||
@@ -1,25 +1,5 @@
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
/**
|
||||
* Convert a date to a ISO 8601 datetime string.
|
||||
* @param x - The date to convert.
|
||||
* @returns The ISO 8601 datetime string.
|
||||
* @deprecated Remove this and all references when using `ZodSerializerDto` on the controllers. Then the codec in `isoDatetimeToDate` in validation.ts will handle the conversion instead.
|
||||
*/
|
||||
export const asDateString = <T extends Date | string | undefined | null>(x: T) => {
|
||||
return x instanceof Date ? x.toISOString() : (x as Exclude<T, Date>);
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a date to a date string.
|
||||
* @param x - The date to convert.
|
||||
* @returns The date string.
|
||||
* @deprecated Remove this and all references when using `ZodSerializerDto` on the controllers. Then the codec in `isoDateToDate` in validation.ts will handle the conversion instead.
|
||||
*/
|
||||
export const asBirthDateString = (x: Date | string | null): string | null => {
|
||||
return x instanceof Date ? x.toISOString().split('T')[0] : x;
|
||||
};
|
||||
|
||||
export const extractTimeZone = (dateTimeOriginal?: string | null) => {
|
||||
const extractedTimeZone = dateTimeOriginal ? DateTime.fromISO(dateTimeOriginal, { setZone: true }).zone : undefined;
|
||||
return extractedTimeZone?.type === 'fixed' ? extractedTimeZone : undefined;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { getExifCount, suggestDuplicate, suggestDuplicateKeepAssetIds } from 'sr
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { z } from 'zod';
|
||||
|
||||
type ExifInfoInput = Partial<z.infer<typeof ExifResponseSchema>>;
|
||||
type ExifInfoInput = Partial<z.input<typeof ExifResponseSchema>>;
|
||||
|
||||
const createAsset = (
|
||||
id: string,
|
||||
@@ -15,18 +15,18 @@ const createAsset = (
|
||||
id,
|
||||
type: AssetType.Image,
|
||||
thumbhash: null,
|
||||
localDateTime: new Date().toISOString(),
|
||||
localDateTime: new Date(),
|
||||
duration: '0:00:00.00000',
|
||||
hasMetadata: true,
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
createdAt: new Date().toISOString(),
|
||||
createdAt: new Date(),
|
||||
ownerId: 'owner-1',
|
||||
originalPath: '/path/to/asset',
|
||||
originalFileName: 'asset.jpg',
|
||||
fileCreatedAt: new Date().toISOString(),
|
||||
fileModifiedAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
fileCreatedAt: new Date(),
|
||||
fileModifiedAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
isFavorite: false,
|
||||
isArchived: false,
|
||||
isTrashed: false,
|
||||
|
||||
@@ -135,15 +135,13 @@ export const isValidInteger = (value: number, options: { min?: number; max?: num
|
||||
* Converts email strings to lowercase and validates against HTML5 email regex
|
||||
* @docs https://zod.dev/api?id=email
|
||||
*/
|
||||
export const toEmail = z
|
||||
.email({
|
||||
pattern: z.regexes.html5Email,
|
||||
error: (iss) => `Invalid input: expected email, received ${typeof iss.input}`,
|
||||
})
|
||||
.transform((val) => val.toLowerCase());
|
||||
export const toEmail = z.email({
|
||||
pattern: z.regexes.html5Email,
|
||||
error: (iss) => `Invalid input: expected email, received ${typeof iss.input}`,
|
||||
});
|
||||
|
||||
/**
|
||||
* Parse ISO 8601 datetime strings to Date objects
|
||||
* Parse ISO 8601 datetime strings to Date objects. Requires `{ codec: true }` when using `createZodDto`.
|
||||
* @docs https://zod.dev/api?id=codec
|
||||
*/
|
||||
export const isoDatetimeToDate = z
|
||||
|
||||
8
server/test/fixtures/tag.stub.ts
vendored
8
server/test/fixtures/tag.stub.ts
vendored
@@ -55,15 +55,15 @@ export const tagStub = {
|
||||
export const tagResponseStub = {
|
||||
tag1: Object.freeze<TagResponseDto>({
|
||||
id: 'tag-1',
|
||||
createdAt: '2021-01-01T00:00:00.000Z',
|
||||
updatedAt: '2021-01-01T00:00:00.000Z',
|
||||
createdAt: new Date('2021-01-01T00:00:00.000Z'),
|
||||
updatedAt: new Date('2021-01-01T00:00:00.000Z'),
|
||||
name: 'Tag1',
|
||||
value: 'Tag1',
|
||||
}),
|
||||
color1: Object.freeze<TagResponseDto>({
|
||||
id: 'tag-1',
|
||||
createdAt: '2021-01-01T00:00:00.000Z',
|
||||
updatedAt: '2021-01-01T00:00:00.000Z',
|
||||
createdAt: new Date('2021-01-01T00:00:00.000Z'),
|
||||
updatedAt: new Date('2021-01-01T00:00:00.000Z'),
|
||||
color: '#000000',
|
||||
name: 'Tag1',
|
||||
value: 'Tag1',
|
||||
|
||||
Reference in New Issue
Block a user