diff --git a/.swiftlint.yml b/.swiftlint.yml index 6767d1a4..d763bfca 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -3,3 +3,5 @@ disabled_rules: - multiple_closures_with_trailing_closure - trailing_whitespace - type_body_length + - cyclomatic_complexity + - function_body_length diff --git a/DevLog/App/Assembler/DataAssembler.swift b/DevLog/App/Assembler/DataAssembler.swift index 33feab2f..b193137a 100644 --- a/DevLog/App/Assembler/DataAssembler.swift +++ b/DevLog/App/Assembler/DataAssembler.swift @@ -28,7 +28,14 @@ final class DataAssembler: Assembler { container.register(TodoRepository.self) { TodoRepositoryImpl( - todoService: container.resolve(TodoService.self) + todoService: container.resolve(TodoService.self), + todoCategoryService: container.resolve(TodoCategoryService.self) + ) + } + + container.register(TodoCategoryRepository.self) { + TodoCategoryRepositoryImpl( + todoCategoryService: container.resolve(TodoCategoryService.self) ) } @@ -67,7 +74,10 @@ final class DataAssembler: Assembler { } container.register(PushNotificationRepository.self) { - PushNotificationRepositoryImpl(pushNotificationService: container.resolve(PushNotificationService.self)) + PushNotificationRepositoryImpl( + pushNotificationService: container.resolve(PushNotificationService.self), + todoCategoryService: container.resolve(TodoCategoryService.self) + ) } container.register(WebPageRepository.self) { diff --git a/DevLog/App/Assembler/DomainAssembler.swift b/DevLog/App/Assembler/DomainAssembler.swift index d9f3da61..da962efc 100644 --- a/DevLog/App/Assembler/DomainAssembler.swift +++ b/DevLog/App/Assembler/DomainAssembler.swift @@ -11,6 +11,7 @@ final class DomainAssembler: Assembler { registerConnectivityUseCases(container) registerAuthProviderUseCases(container) registerTodoUseCases(container) + registerTodoCategoryUseCases(container) registerUserDataUseCases(container) registerPushNotificationUseCases(container) registerWebPageUseCases(container) @@ -85,6 +86,20 @@ private extension DomainAssembler { } } + func registerTodoCategoryUseCases(_ container: DIContainer) { + container.register(FetchTodoCategoryPreferencesUseCase.self) { + FetchTodoCategoryPreferencesUseCaseImpl( + container.resolve(TodoCategoryRepository.self) + ) + } + + container.register(UpdateTodoCategoryPreferencesUseCase.self) { + UpdateTodoCategoryPreferencesUseCaseImpl( + container.resolve(TodoCategoryRepository.self) + ) + } + } + func registerUserDataUseCases(_ container: DIContainer) { container.register(FetchUserDataUseCase.self) { FetchUserDataUseCaseImpl(container.resolve(UserDataRepository.self)) diff --git a/DevLog/App/Assembler/InfraAssembler.swift b/DevLog/App/Assembler/InfraAssembler.swift index 514dae6e..33fcce01 100644 --- a/DevLog/App/Assembler/InfraAssembler.swift +++ b/DevLog/App/Assembler/InfraAssembler.swift @@ -36,6 +36,10 @@ final class InfraAssembler: Assembler { TodoService() } + container.register(TodoCategoryService.self) { + TodoCategoryService() + } + container.register(UserService.self) { UserService() } diff --git a/DevLog/Data/DTO/PushNotificationResponse.swift b/DevLog/Data/DTO/PushNotificationResponse.swift index 9fd5a1ac..f72e9d51 100644 --- a/DevLog/Data/DTO/PushNotificationResponse.swift +++ b/DevLog/Data/DTO/PushNotificationResponse.swift @@ -14,5 +14,5 @@ struct PushNotificationResponse { let receivedAt: Date let isRead: Bool let todoId: String - let todoCategory: String + let todoCategory: TodoCategoryResponse } diff --git a/DevLog/Data/DTO/TodoCategoryResponse.swift b/DevLog/Data/DTO/TodoCategoryResponse.swift new file mode 100644 index 00000000..75b841f1 --- /dev/null +++ b/DevLog/Data/DTO/TodoCategoryResponse.swift @@ -0,0 +1,13 @@ +// +// TodoCategoryResponse.swift +// DevLog +// +// Created by opfic on 3/30/26. +// + +import Foundation + +enum TodoCategoryResponse { + case raw(String) + case decoded(TodoCategory) +} diff --git a/DevLog/Data/DTO/TodoDTO.swift b/DevLog/Data/DTO/TodoDTO.swift index 723f58a9..2d8e16e8 100644 --- a/DevLog/Data/DTO/TodoDTO.swift +++ b/DevLog/Data/DTO/TodoDTO.swift @@ -20,7 +20,7 @@ struct TodoRequest: Encodable { let completedAt: Date? let dueDate: Date? let tags: [String] - let category: TodoCategory + let category: String } struct TodoResponse { @@ -36,5 +36,5 @@ struct TodoResponse { let completedAt: Date? let dueDate: Date? let tags: [String] - let category: String + let category: TodoCategoryResponse } diff --git a/DevLog/Data/DTO/TodoReferenceResponse.swift b/DevLog/Data/DTO/TodoReferenceResponse.swift new file mode 100644 index 00000000..3f60dd2e --- /dev/null +++ b/DevLog/Data/DTO/TodoReferenceResponse.swift @@ -0,0 +1,15 @@ +// +// TodoReferenceResponse.swift +// DevLog +// +// Created by opfic on 3/30/26. +// + +import Foundation + +struct TodoReferenceResponse { + let id: String + let number: Int + let title: String + let category: TodoCategoryResponse +} diff --git a/DevLog/Data/Mapper/PushNotificationMapping.swift b/DevLog/Data/Mapper/PushNotificationMapping.swift index d0bcc7c8..c717d170 100644 --- a/DevLog/Data/Mapper/PushNotificationMapping.swift +++ b/DevLog/Data/Mapper/PushNotificationMapping.swift @@ -7,8 +7,15 @@ extension PushNotificationResponse { func toDomain() throws -> PushNotification { - guard let todoCategory = TodoCategory(rawValue: self.todoCategory) else { - throw DataError.invalidData("PushNotificationResponse.todoCategory is invalid: \(self.todoCategory)") + let todoCategory: TodoCategory + + switch self.todoCategory { + case .decoded(let category): + todoCategory = category + case .raw(let category): + throw DataError.invalidData( + "PushNotificationResponse.todoCategory must be resolved before toDomain(): \(category)" + ) } return PushNotification( diff --git a/DevLog/Data/Mapper/TodoMapping.swift b/DevLog/Data/Mapper/TodoMapping.swift index 5a4b9790..e879a1e7 100644 --- a/DevLog/Data/Mapper/TodoMapping.swift +++ b/DevLog/Data/Mapper/TodoMapping.swift @@ -20,15 +20,20 @@ extension TodoRequest { completedAt: entity.completedAt, dueDate: entity.dueDate, tags: entity.tags, - category: entity.category + category: entity.category.storageValue ) } } extension TodoResponse { func toDomain() throws -> Todo { - guard let category = TodoCategory(rawValue: self.category) else { - throw DataError.invalidData("TodoResponse.category is invalid: \(self.category)") + let todoCategory: TodoCategory + + switch category { + case .decoded(let category): + todoCategory = category + case .raw(let category): + throw DataError.invalidData("TodoResponse.category must be resolved before toDomain(): \(category)") } return Todo( @@ -44,7 +49,7 @@ extension TodoResponse { completedAt: self.completedAt, dueDate: self.dueDate, tags: self.tags, - category: category + category: todoCategory ) } } diff --git a/DevLog/Data/Repository/PushNotificationRepositoryImpl.swift b/DevLog/Data/Repository/PushNotificationRepositoryImpl.swift index 34826bac..030cc0db 100644 --- a/DevLog/Data/Repository/PushNotificationRepositoryImpl.swift +++ b/DevLog/Data/Repository/PushNotificationRepositoryImpl.swift @@ -9,25 +9,30 @@ import Foundation import Combine final class PushNotificationRepositoryImpl: PushNotificationRepository { - private let service: PushNotificationService + private let pushNotificationService: PushNotificationService + private let todoCategoryService: TodoCategoryService - init(pushNotificationService: PushNotificationService) { - self.service = pushNotificationService + init( + pushNotificationService: PushNotificationService, + todoCategoryService: TodoCategoryService + ) { + self.pushNotificationService = pushNotificationService + self.todoCategoryService = todoCategoryService } /// 푸시 알림 On/Off 설정 func fetchPushNotificationEnabled() async throws -> Bool { - return try await service.fetchPushNotificationEnabled() + return try await pushNotificationService.fetchPushNotificationEnabled() } /// 푸시 알림 시간 설정 func fetchPushNotificationTime() async throws -> DateComponents { - return try await service.fetchPushNotificationTime() + return try await pushNotificationService.fetchPushNotificationTime() } /// 푸시 알림 설정 업데이트 func updatePushNotificationSettings(_ settings: PushNotificationSettings) async throws { - try await service.updatePushNotificationSettings( + try await pushNotificationService.updatePushNotificationSettings( isEnabled: settings.isEnabled, components: settings.scheduledTime ) } @@ -38,35 +43,125 @@ final class PushNotificationRepositoryImpl: PushNotificationRepository { cursor: PushNotificationCursor? ) async throws -> PushNotificationPage { let cursorDTO = cursor.map { PushNotificationCursorDTO.fromDomain($0) } - let response = try await service.requestNotifications(query, cursor: cursorDTO) - return try response.toDomain() + async let responseTask = pushNotificationService.requestNotifications(query, cursor: cursorDTO) + async let preferencesTask = todoCategoryService.fetchPreferences() + + let (response, preferences) = try await (responseTask, preferencesTask) + return try resolvePage(from: response, with: preferences) } func observeNotifications( _ query: PushNotificationQuery, limit: Int ) throws -> AnyPublisher { - try service.observeNotifications(query, limit: limit) - .tryMap { try $0.toDomain() } + let subject = PassthroughSubject() + var cancellable: AnyCancellable? + + cancellable = try pushNotificationService.observeNotifications(query, limit: limit) + .sink( + receiveCompletion: { completion in + switch completion { + case .finished: + subject.send(completion: .finished) + case .failure(let error): + subject.send(completion: .failure(error)) + } + }, + receiveValue: { [weak self] response in + guard let self else { return } + + Task { + do { + let preferences = try await self.todoCategoryService.fetchPreferences() + let page = try self.resolvePage(from: response, with: preferences) + subject.send(page) + } catch { + subject.send(completion: .failure(error)) + } + } + } + ) + + return subject + .handleEvents(receiveCancel: { cancellable?.cancel() }) .eraseToAnyPublisher() } func observeUnreadPushCount() throws -> AnyPublisher { - try service.observeUnreadPushCount() + try pushNotificationService.observeUnreadPushCount() .eraseToAnyPublisher() } // 푸시 알림 기록 삭제 func deleteNotification(_ notificationID: String) async throws { - try await service.deleteNotification(notificationID) + try await pushNotificationService.deleteNotification(notificationID) } func undoDeleteNotification(_ notificationID: String) async throws { - try await service.undoDeleteNotification(notificationID) + try await pushNotificationService.undoDeleteNotification(notificationID) } // 푸시 알림 읽음/안읽음 토글 func toggleNotificationRead(_ todoId: String) async throws { - try await service.toggleNotificationRead(todoId) + try await pushNotificationService.toggleNotificationRead(todoId) + } +} + +private extension PushNotificationRepositoryImpl { + func resolvePage( + from response: PushNotificationPageResponse, + with preferences: [TodoCategoryPreference] + ) throws -> PushNotificationPage { + let userTodoCategories: [UserTodoCategory] = preferences.compactMap { preference in + guard case .user(let userTodoCategory) = preference.category else { + return nil + } + + return userTodoCategory + } + + let responses = try response.items.map { + try resolve($0, userTodoCategories: userTodoCategories) + } + + return try PushNotificationPageResponse( + items: responses, + nextCursor: response.nextCursor + ).toDomain() + } + + // resolvePage() 메서드에서만 사용됨 + private func resolve( + _ response: PushNotificationResponse, + userTodoCategories: [UserTodoCategory] + ) throws -> PushNotificationResponse { + let id: String + switch response.todoCategory { + case .raw(let rawValue): + id = rawValue + case .decoded: + return response + } + + let todoCategory: TodoCategory + if let systemTodoCategory = SystemTodoCategory(rawValue: id) { + todoCategory = .system(systemTodoCategory) + } else if let userTodoCategory = userTodoCategories.first(where: { + $0.id == id + }) { + todoCategory = .user(userTodoCategory) + } else { + throw DataError.invalidData("PushNotificationResponse.todoCategory is invalid: \(id)") + } + + return PushNotificationResponse( + id: response.id, + title: response.title, + body: response.body, + receivedAt: response.receivedAt, + isRead: response.isRead, + todoId: response.todoId, + todoCategory: .decoded(todoCategory) + ) } } diff --git a/DevLog/Data/Repository/TodoCategoryRepositoryImpl.swift b/DevLog/Data/Repository/TodoCategoryRepositoryImpl.swift new file mode 100644 index 00000000..1689b9cb --- /dev/null +++ b/DevLog/Data/Repository/TodoCategoryRepositoryImpl.swift @@ -0,0 +1,22 @@ +// +// TodoCategoryRepositoryImpl.swift +// DevLog +// +// Created by opfic on 3/30/26. +// + +final class TodoCategoryRepositoryImpl: TodoCategoryRepository { + private let todoCategoryService: TodoCategoryService + + init(todoCategoryService: TodoCategoryService) { + self.todoCategoryService = todoCategoryService + } + + func fetchPreferences() async throws -> [TodoCategoryPreference] { + try await todoCategoryService.fetchPreferences() + } + + func updatePreferences(_ preferences: [TodoCategoryPreference]) async throws { + try await todoCategoryService.updatePreferences(preferences) + } +} diff --git a/DevLog/Data/Repository/TodoRepositoryImpl.swift b/DevLog/Data/Repository/TodoRepositoryImpl.swift index 0d63c573..f53affdb 100644 --- a/DevLog/Data/Repository/TodoRepositoryImpl.swift +++ b/DevLog/Data/Repository/TodoRepositoryImpl.swift @@ -9,24 +9,81 @@ import Foundation final class TodoRepositoryImpl: TodoRepository { private let todoService: TodoService + private let todoCategoryService: TodoCategoryService - init(todoService: TodoService) { + init( + todoService: TodoService, + todoCategoryService: TodoCategoryService + ) { self.todoService = todoService + self.todoCategoryService = todoCategoryService } func fetchTodos(_ query: TodoQuery, cursor: TodoCursor?) async throws -> TodoPage { let responseCursor = cursor.map { TodoCursorDTO.fromDomain($0) } - let response = try await todoService.fetchTodos(query, cursor: responseCursor) - return try response.toDomain() + async let response = todoService.fetchTodos(query, cursor: responseCursor) + async let preferences = todoCategoryService.fetchPreferences() + + let (todoResponse, todoPreferences) = try await (response, preferences) + let userTodoCategories: [UserTodoCategory] = todoPreferences.compactMap { preference in + guard case .user(let category) = preference.category else { + return nil + } + + return category + } + + let resolvedTodoResponses = try todoResponse.items.map { + try resolve($0, userTodoCategories: userTodoCategories) + } + + return try TodoPageResponse( + items: resolvedTodoResponses, + nextCursor: todoResponse.nextCursor + ).toDomain() } func fetchTodo(_ todoId: String) async throws -> Todo { - let response = try await todoService.fetchTodo(todoId: todoId) - return try response.toDomain() + async let response = todoService.fetchTodo(todoId: todoId) + async let preferences = todoCategoryService.fetchPreferences() + + let (todoResponse, todoPreferences) = try await (response, preferences) + let userTodoCategories: [UserTodoCategory] = todoPreferences.compactMap { preference in + guard case .user(let category) = preference.category else { + return nil + } + + return category + } + + return try resolve(todoResponse, userTodoCategories: userTodoCategories).toDomain() } - func fetchReferenceItems(_ numbers: [Int]) async throws -> [Int: TodoReferenceItem] { - try await todoService.fetchReferenceItems(numbers) + func fetchReferences(_ numbers: [Int]) async throws -> [Int: TodoReference] { + async let responseTask = todoService.fetchReferences(numbers) + async let preferencesTask = todoCategoryService.fetchPreferences() + + let (responses, preferences) = try await (responseTask, preferencesTask) + let userTodoCategories: [UserTodoCategory] = preferences.compactMap { preference in + guard case .user(let category) = preference.category else { + return nil + } + + return category + } + + return try responses.reduce(into: [Int: TodoReference]()) { partialResult, pair in + let response = try resolve(pair.value, userTodoCategories: userTodoCategories) + guard case let .decoded(category) = response.category else { + throw DataError.invalidData("TodoReferenceResponse.category must be resolved before use") + } + + partialResult[pair.key] = TodoReference( + id: response.id, + title: response.title, + category: category + ) + } } func upsertTodo(_ todo: Todo) async throws { @@ -42,3 +99,76 @@ final class TodoRepositoryImpl: TodoRepository { try await todoService.undoDeleteTodo(todoId: todoId) } } + +private extension TodoRepositoryImpl { + func resolve( + _ response: TodoResponse, + userTodoCategories: [UserTodoCategory] + ) throws -> TodoResponse { + let id: String + switch response.category { + case .raw(let value): + id = value + case .decoded: + return response + } + + let category: TodoCategory + if let systemTodoCategory = SystemTodoCategory(rawValue: id) { + category = .system(systemTodoCategory) + } else if let userTodoCategory = userTodoCategories.first(where: { + $0.id == id + }) { + category = .user(userTodoCategory) + } else { + throw DataError.invalidData("TodoResponse.category is invalid: \(id)") + } + + return TodoResponse( + id: response.id, + isPinned: response.isPinned, + isCompleted: response.isCompleted, + isChecked: response.isChecked, + number: response.number, + title: response.title, + content: response.content, + createdAt: response.createdAt, + updatedAt: response.updatedAt, + completedAt: response.completedAt, + dueDate: response.dueDate, + tags: response.tags, + category: .decoded(category) + ) + } + + func resolve( + _ response: TodoReferenceResponse, + userTodoCategories: [UserTodoCategory] + ) throws -> TodoReferenceResponse { + let categoryID: String + switch response.category { + case .raw(let value): + categoryID = value + case .decoded: + return response + } + + let category: TodoCategory + if let systemTodoCategory = SystemTodoCategory(rawValue: categoryID) { + category = .system(systemTodoCategory) + } else if let userTodoCategory = userTodoCategories.first(where: { + $0.id == categoryID + }) { + category = .user(userTodoCategory) + } else { + throw DataError.invalidData("TodoReferenceResponse.category is invalid: \(categoryID)") + } + + return TodoReferenceResponse( + id: response.id, + number: response.number, + title: response.title, + category: .decoded(category) + ) + } +} diff --git a/DevLog/Domain/Entity/SystemTodoCategory.swift b/DevLog/Domain/Entity/SystemTodoCategory.swift new file mode 100644 index 00000000..e49fdd52 --- /dev/null +++ b/DevLog/Domain/Entity/SystemTodoCategory.swift @@ -0,0 +1,19 @@ +// +// SystemTodoCategory.swift +// DevLog +// +// Created by opfic on 3/29/26. +// + +import Foundation + +enum SystemTodoCategory: String, CaseIterable { + case issue // 이슈 + case feature // 신규 기능 + case improvement // 개선/리팩터링 + case review // 코드/문서 리뷰 + case test // 테스트/QA + case doc // 문서화 + case research // 리서치/학습 + case etc // 기타 +} diff --git a/DevLog/Domain/Entity/Todo.swift b/DevLog/Domain/Entity/Todo.swift index 00f59d7a..e34da02c 100644 --- a/DevLog/Domain/Entity/Todo.swift +++ b/DevLog/Domain/Entity/Todo.swift @@ -7,7 +7,7 @@ import Foundation -struct Todo: Identifiable, Hashable { +struct Todo: Equatable { let id: String var isPinned: Bool // 해당 할 일이 상단에 고정되어 있는지 여부 var isCompleted: Bool // 해당 할 일의 완료 여부 diff --git a/DevLog/Domain/Entity/TodoCategory.swift b/DevLog/Domain/Entity/TodoCategory.swift new file mode 100644 index 00000000..a914869a --- /dev/null +++ b/DevLog/Domain/Entity/TodoCategory.swift @@ -0,0 +1,22 @@ +// +// TodoCategory.swift +// DevLog +// +// Created by opfic on 3/29/26. +// + +import Foundation + +enum TodoCategory: Equatable { + case system(SystemTodoCategory) + case user(UserTodoCategory) + + var storageValue: String { + switch self { + case .system(let category): + return category.rawValue + case .user(let category): + return category.id + } + } +} diff --git a/DevLog/Domain/Entity/TodoCategoryPreference.swift b/DevLog/Domain/Entity/TodoCategoryPreference.swift new file mode 100644 index 00000000..1b3f337d --- /dev/null +++ b/DevLog/Domain/Entity/TodoCategoryPreference.swift @@ -0,0 +1,13 @@ +// +// TodoCategoryPreference.swift +// DevLog +// +// Created by opfic on 3/30/26. +// + +import Foundation + +struct TodoCategoryPreference: Equatable { + let category: TodoCategory + var isVisible: Bool +} diff --git a/DevLog/Domain/Entity/TodoReferenceItem.swift b/DevLog/Domain/Entity/TodoReference.swift similarity index 64% rename from DevLog/Domain/Entity/TodoReferenceItem.swift rename to DevLog/Domain/Entity/TodoReference.swift index d3cc572d..3b8a7470 100644 --- a/DevLog/Domain/Entity/TodoReferenceItem.swift +++ b/DevLog/Domain/Entity/TodoReference.swift @@ -1,5 +1,5 @@ // -// TodoReferenceItem.swift +// TodoReference.swift // DevLog // // Created by opfic on 3/25/26. @@ -7,7 +7,7 @@ import Foundation -struct TodoReferenceItem: Identifiable, Equatable { +struct TodoReference { let id: String let title: String let category: TodoCategory diff --git a/DevLog/Domain/Entity/UserTodoCategory.swift b/DevLog/Domain/Entity/UserTodoCategory.swift new file mode 100644 index 00000000..f35ff6e0 --- /dev/null +++ b/DevLog/Domain/Entity/UserTodoCategory.swift @@ -0,0 +1,14 @@ +// +// UserTodoCategory.swift +// DevLog +// +// Created by opfic on 3/29/26. +// + +import Foundation + +struct UserTodoCategory: Equatable { + var id: String + var name: String + var colorHex: String +} diff --git a/DevLog/Domain/Protocol/TodoCategoryRepository.swift b/DevLog/Domain/Protocol/TodoCategoryRepository.swift new file mode 100644 index 00000000..485bc0c5 --- /dev/null +++ b/DevLog/Domain/Protocol/TodoCategoryRepository.swift @@ -0,0 +1,11 @@ +// +// TodoCategoryRepository.swift +// DevLog +// +// Created by opfic on 3/30/26. +// + +protocol TodoCategoryRepository { + func fetchPreferences() async throws -> [TodoCategoryPreference] + func updatePreferences(_ preferences: [TodoCategoryPreference]) async throws +} diff --git a/DevLog/Domain/Protocol/TodoRepository.swift b/DevLog/Domain/Protocol/TodoRepository.swift index 925236ea..0961ec9b 100644 --- a/DevLog/Domain/Protocol/TodoRepository.swift +++ b/DevLog/Domain/Protocol/TodoRepository.swift @@ -10,7 +10,7 @@ import Foundation protocol TodoRepository { func fetchTodos(_ query: TodoQuery, cursor: TodoCursor?) async throws -> TodoPage func fetchTodo(_ todoId: String) async throws -> Todo - func fetchReferenceItems(_ numbers: [Int]) async throws -> [Int: TodoReferenceItem] + func fetchReferences(_ numbers: [Int]) async throws -> [Int: TodoReference] func upsertTodo(_ todo: Todo) async throws func deleteTodo(_ todoId: String) async throws func undoDeleteTodo(_ todoId: String) async throws diff --git a/DevLog/Domain/UseCase/Todo/Fetch/FetchReferenceItemsUseCase.swift b/DevLog/Domain/UseCase/Todo/Fetch/FetchReferenceItemsUseCase.swift index f2323acc..62c43ffd 100644 --- a/DevLog/Domain/UseCase/Todo/Fetch/FetchReferenceItemsUseCase.swift +++ b/DevLog/Domain/UseCase/Todo/Fetch/FetchReferenceItemsUseCase.swift @@ -6,5 +6,5 @@ // protocol FetchReferenceItemsUseCase { - func execute(_ numbers: [Int]) async throws -> [Int: TodoReferenceItem] + func execute(_ numbers: [Int]) async throws -> [Int: TodoReference] } diff --git a/DevLog/Domain/UseCase/Todo/Fetch/FetchReferenceItemsUseCaseImpl.swift b/DevLog/Domain/UseCase/Todo/Fetch/FetchReferenceItemsUseCaseImpl.swift index c297a1d8..866c1c29 100644 --- a/DevLog/Domain/UseCase/Todo/Fetch/FetchReferenceItemsUseCaseImpl.swift +++ b/DevLog/Domain/UseCase/Todo/Fetch/FetchReferenceItemsUseCaseImpl.swift @@ -12,7 +12,7 @@ final class FetchReferenceItemsUseCaseImpl: FetchReferenceItemsUseCase { self.repository = repository } - func execute(_ numbers: [Int]) async throws -> [Int: TodoReferenceItem] { - try await repository.fetchReferenceItems(numbers) + func execute(_ numbers: [Int]) async throws -> [Int: TodoReference] { + try await repository.fetchReferences(numbers) } } diff --git a/DevLog/Domain/UseCase/TodoCategory/Fetch/FetchTodoCategoryPreferencesUseCase.swift b/DevLog/Domain/UseCase/TodoCategory/Fetch/FetchTodoCategoryPreferencesUseCase.swift new file mode 100644 index 00000000..d7ee10d9 --- /dev/null +++ b/DevLog/Domain/UseCase/TodoCategory/Fetch/FetchTodoCategoryPreferencesUseCase.swift @@ -0,0 +1,10 @@ +// +// FetchTodoCategoryPreferencesUseCase.swift +// DevLog +// +// Created by opfic on 3/30/26. +// + +protocol FetchTodoCategoryPreferencesUseCase { + func execute() async throws -> [TodoCategoryPreference] +} diff --git a/DevLog/Domain/UseCase/TodoCategory/Fetch/FetchTodoCategoryPreferencesUseCaseImpl.swift b/DevLog/Domain/UseCase/TodoCategory/Fetch/FetchTodoCategoryPreferencesUseCaseImpl.swift new file mode 100644 index 00000000..56c51adc --- /dev/null +++ b/DevLog/Domain/UseCase/TodoCategory/Fetch/FetchTodoCategoryPreferencesUseCaseImpl.swift @@ -0,0 +1,18 @@ +// +// FetchTodoCategoryPreferencesUseCaseImpl.swift +// DevLog +// +// Created by opfic on 3/30/26. +// + +final class FetchTodoCategoryPreferencesUseCaseImpl: FetchTodoCategoryPreferencesUseCase { + private let todoCategoryRepository: TodoCategoryRepository + + init(_ todoCategoryRepository: TodoCategoryRepository) { + self.todoCategoryRepository = todoCategoryRepository + } + + func execute() async throws -> [TodoCategoryPreference] { + try await todoCategoryRepository.fetchPreferences() + } +} diff --git a/DevLog/Domain/UseCase/TodoCategory/Update/UpdateTodoCategoryPreferencesUseCase.swift b/DevLog/Domain/UseCase/TodoCategory/Update/UpdateTodoCategoryPreferencesUseCase.swift new file mode 100644 index 00000000..359f47c4 --- /dev/null +++ b/DevLog/Domain/UseCase/TodoCategory/Update/UpdateTodoCategoryPreferencesUseCase.swift @@ -0,0 +1,10 @@ +// +// UpdateTodoCategoryPreferencesUseCase.swift +// DevLog +// +// Created by opfic on 3/30/26. +// + +protocol UpdateTodoCategoryPreferencesUseCase { + func execute(_ preferences: [TodoCategoryPreference]) async throws +} diff --git a/DevLog/Domain/UseCase/TodoCategory/Update/UpdateTodoCategoryPreferencesUseCaseImpl.swift b/DevLog/Domain/UseCase/TodoCategory/Update/UpdateTodoCategoryPreferencesUseCaseImpl.swift new file mode 100644 index 00000000..a5e04993 --- /dev/null +++ b/DevLog/Domain/UseCase/TodoCategory/Update/UpdateTodoCategoryPreferencesUseCaseImpl.swift @@ -0,0 +1,18 @@ +// +// UpdateTodoCategoryPreferencesUseCaseImpl.swift +// DevLog +// +// Created by opfic on 3/30/26. +// + +final class UpdateTodoCategoryPreferencesUseCaseImpl: UpdateTodoCategoryPreferencesUseCase { + private let todoCategoryRepository: TodoCategoryRepository + + init(_ todoCategoryRepository: TodoCategoryRepository) { + self.todoCategoryRepository = todoCategoryRepository + } + + func execute(_ preferences: [TodoCategoryPreference]) async throws { + try await todoCategoryRepository.updatePreferences(preferences) + } +} diff --git a/DevLog/Infra/Common/FirestorePath.swift b/DevLog/Infra/Common/FirestorePath.swift index cad439bd..60b4b66e 100644 --- a/DevLog/Infra/Common/FirestorePath.swift +++ b/DevLog/Infra/Common/FirestorePath.swift @@ -19,6 +19,7 @@ enum FirestorePath { case info case tokens case settings + case categories } enum Counter: String { diff --git a/DevLog/Infra/Service/PushNotificationService.swift b/DevLog/Infra/Service/PushNotificationService.swift index f8da35cf..c3c74d68 100644 --- a/DevLog/Infra/Service/PushNotificationService.swift +++ b/DevLog/Infra/Service/PushNotificationService.swift @@ -318,7 +318,7 @@ private extension PushNotificationService { receivedAt: receivedAt.dateValue(), isRead: isRead, todoId: todoId, - todoCategory: todoCategory + todoCategory: .raw(todoCategory) ) } diff --git a/DevLog/Infra/Service/TodoCategoryService.swift b/DevLog/Infra/Service/TodoCategoryService.swift new file mode 100644 index 00000000..ba8930cc --- /dev/null +++ b/DevLog/Infra/Service/TodoCategoryService.swift @@ -0,0 +1,180 @@ +// +// TodoCategoryService.swift +// DevLog +// +// Created by opfic on 3/30/26. +// + +import FirebaseAuth +import FirebaseFirestore + +final class TodoCategoryService { + private enum Field: String { + case items + case kind + case id + case systemCategory + case name + case colorHex + case isVisible + } + + private enum Kind: String { + case system + case user + } + + private let store = Firestore.firestore() + private let logger = Logger(category: "TodoCategoryService") + + func fetchPreferences() async throws -> [TodoCategoryPreference] { + guard let uid = Auth.auth().currentUser?.uid else { + logger.error("User not authenticated") + throw AuthError.notAuthenticated + } + + logger.info("Fetching todo category preferences") + + do { + let snapshot = try await store.document( + FirestorePath.userData(uid, document: .categories) + ).getDocument() + + guard let items = snapshot.data()?[Field.items.rawValue] as? [[String: Any]] else { + logger.info("Todo category preferences not found, using defaults") + return SystemTodoCategory.allCases.map { + TodoCategoryPreference(category: .system($0), isVisible: true) + } + } + + let preferences = items.compactMap { makePreference($0) } + if preferences.isEmpty { + logger.info("Todo category preferences empty, using defaults") + return SystemTodoCategory.allCases.map { + TodoCategoryPreference(category: .system($0), isVisible: true) + } + } + + let mergedPreferences = mergedPreferences(preferences) + logger.info("Successfully fetched todo category preferences") + return mergedPreferences + } catch { + logger.error("Failed to fetch todo category preferences", error: error) + throw error + } + } + + func updatePreferences(_ preferences: [TodoCategoryPreference]) async throws { + guard let uid = Auth.auth().currentUser?.uid else { + logger.error("User not authenticated") + throw AuthError.notAuthenticated + } + + logger.info("Updating todo category preferences") + + do { + try await store.document( + FirestorePath.userData(uid, document: .categories) + ).setData( + [Field.items.rawValue: preferences.map(toDictionary)], + merge: true + ) + logger.info("Successfully updated todo category preferences") + } catch { + logger.error("Failed to update todo category preferences", error: error) + throw error + } + } +} + +private extension TodoCategoryService { + func mergedPreferences( + _ preferences: [TodoCategoryPreference] + ) -> [TodoCategoryPreference] { + var mergedPreferences = preferences + + for systemTodoCategory in SystemTodoCategory.allCases { + let containsSystemTodoCategory = preferences.contains { preference in + guard case .system(let currentSystemTodoCategory) = preference.category else { + return false + } + + return currentSystemTodoCategory == systemTodoCategory + } + + if containsSystemTodoCategory { continue } + + mergedPreferences.append( + TodoCategoryPreference( + category: .system(systemTodoCategory), + isVisible: true + ) + ) + } + + return mergedPreferences + } + + func makePreference(_ items: [String: Any]) -> TodoCategoryPreference? { + guard + let kindString = items[Field.kind.rawValue] as? String, + let kind = Kind(rawValue: kindString), + let isVisible = items[Field.isVisible.rawValue] as? Bool + else { + return nil + } + + switch kind { + case .system: + guard + let systemCategoryString = items[Field.systemCategory.rawValue] as? String, + let systemTodoCategory = SystemTodoCategory(rawValue: systemCategoryString) + else { + return nil + } + + return TodoCategoryPreference( + category: .system(systemTodoCategory), + isVisible: isVisible + ) + case .user: + guard + let id = items[Field.id.rawValue] as? String, + let name = items[Field.name.rawValue] as? String, + let colorHex = items[Field.colorHex.rawValue] as? String + else { + return nil + } + + return TodoCategoryPreference( + category: .user( + UserTodoCategory( + id: id, + name: name, + colorHex: colorHex + ) + ), + isVisible: isVisible + ) + } + } + + func toDictionary(_ preference: TodoCategoryPreference) -> [String: Any] { + switch preference.category { + case .system(let systemTodoCategory): + return [ + Field.kind.rawValue: Kind.system.rawValue, + Field.systemCategory.rawValue: systemTodoCategory.rawValue, + Field.isVisible.rawValue: preference.isVisible + ] + case .user(let userTodoCategory): + return [ + Field.kind.rawValue: Kind.user.rawValue, + Field.id.rawValue: userTodoCategory.id, + Field.name.rawValue: userTodoCategory.name, + Field.colorHex.rawValue: userTodoCategory.colorHex, + Field.isVisible.rawValue: preference.isVisible + ] + } + } +} diff --git a/DevLog/Infra/Service/TodoService.swift b/DevLog/Infra/Service/TodoService.swift index c077e849..4ad84cb0 100644 --- a/DevLog/Infra/Service/TodoService.swift +++ b/DevLog/Infra/Service/TodoService.swift @@ -32,7 +32,7 @@ final class TodoService { "sortTarget=\(query.sortTarget.fieldName)", "sortOrder=\(query.sortOrder == .latest ? "latest" : "oldest")", query.keyword != nil ? "keywordLength=\(trimmedKeyword.count)" : nil, - query.category != nil ? "category=\(query.category!.rawValue)" : nil, + query.category != nil ? "category=\(query.category!.storageValue)" : nil, query.isPinned != nil ? "pinned=\(query.isPinned!)" : nil, query.completionFilter.isCompletedValue != nil ? "completed=\(query.completionFilter.isCompletedValue!)" : nil, query.dueDateFilter != .all ? "dueDateFilter=\(query.dueDateFilter)" : nil, @@ -49,7 +49,7 @@ final class TodoService { if let category = query.category { firestoreQuery = firestoreQuery.whereField( TodoFieldKey.category.rawValue, - isEqualTo: category.rawValue + isEqualTo: category.storageValue ) } @@ -241,7 +241,7 @@ final class TodoService { } } - func fetchReferenceItems(_ numbers: [Int]) async throws -> [Int: TodoReferenceItem] { + func fetchReferences(_ numbers: [Int]) async throws -> [Int: TodoReferenceResponse] { guard let uid = Auth.auth().currentUser?.uid else { throw AuthError.notAuthenticated } let uniqueNumbers = Array(Set(numbers)).sorted() @@ -265,20 +265,20 @@ final class TodoService { return documents } - return snapshots.reduce(into: [Int: TodoReferenceItem]()) { partialResult, document in + return snapshots.reduce(into: [Int: TodoReferenceResponse]()) { partialResult, document in let data = document.data() guard !(data[TodoFieldKey.deletingAt.rawValue] is Timestamp), - let response = makeResponse(from: document), - let category = TodoCategory(rawValue: response.category) + let response = makeResponse(from: document) else { return } - partialResult[response.number] = TodoReferenceItem( + partialResult[response.number] = TodoReferenceResponse( id: response.id, + number: response.number, title: response.title, - category: category + category: response.category ) } } @@ -469,7 +469,7 @@ private extension TodoService { completedAt: completedAt, dueDate: dueDate, tags: tags, - category: category + category: .raw(category) ) } diff --git a/DevLog/Presentation/Extension/Color+Hex.swift b/DevLog/Presentation/Extension/Color+Hex.swift new file mode 100644 index 00000000..766cc2b6 --- /dev/null +++ b/DevLog/Presentation/Extension/Color+Hex.swift @@ -0,0 +1,52 @@ +// +// Color+Hex.swift +// DevLog +// +// Created by opfic on 3/30/26. +// + +import SwiftUI + +extension Color { + static var randomValue: Color { + Color( + red: Double(Int.random(in: 0...255)) / 255, + green: Double(Int.random(in: 0...255)) / 255, + blue: Double(Int.random(in: 0...255)) / 255 + ) + } + + init?(hexString: String) { + let trimmedHex = hexString.trimmingCharacters(in: .whitespacesAndNewlines) + let sanitizedHex = trimmedHex.hasPrefix("#") ? String(trimmedHex.dropFirst()) : trimmedHex + + guard sanitizedHex.count == 6, + let hexValue = Int(sanitizedHex, radix: 16) else { + return nil + } + + let red = Double((hexValue >> 16) & 0xFF) / 255 + let green = Double((hexValue >> 8) & 0xFF) / 255 + let blue = Double(hexValue & 0xFF) / 255 + + self.init(red: red, green: green, blue: blue) + } + + var hexValue: String? { + let uiColor = UIColor(self) + var red: CGFloat = 0 + var green: CGFloat = 0 + var blue: CGFloat = 0 + var alpha: CGFloat = 0 + + guard uiColor.getRed(&red, green: &green, blue: &blue, alpha: &alpha) else { + return nil + } + + let redValue = Int(round(red * 255)) + let greenValue = Int(round(green * 255)) + let blueValue = Int(round(blue * 255)) + + return String(format: "#%02X%02X%02X", redValue, greenValue, blueValue) + } +} diff --git a/DevLog/Presentation/Structure/Profile/ProfileSelectedDayActivity.swift b/DevLog/Presentation/Structure/Profile/ProfileSelectedDayActivity.swift index fb56c40e..9ad52abf 100644 --- a/DevLog/Presentation/Structure/Profile/ProfileSelectedDayActivity.swift +++ b/DevLog/Presentation/Structure/Profile/ProfileSelectedDayActivity.swift @@ -20,4 +20,16 @@ struct ProfileSelectedDayActivity: Identifiable, Hashable { } return showsCreated ? "생성" : "완료" } + + static func == (lhs: Self, rhs: Self) -> Bool { + lhs.todo.id == rhs.todo.id + && lhs.showsCreated == rhs.showsCreated + && lhs.showsCompleted == rhs.showsCompleted + } + + func hash(into hasher: inout Hasher) { + hasher.combine(todo.id) + hasher.combine(showsCreated) + hasher.combine(showsCompleted) + } } diff --git a/DevLog/Presentation/Structure/PushNotificationItem.swift b/DevLog/Presentation/Structure/PushNotificationItem.swift index dd8273fb..cce0d540 100644 --- a/DevLog/Presentation/Structure/PushNotificationItem.swift +++ b/DevLog/Presentation/Structure/PushNotificationItem.swift @@ -25,4 +25,12 @@ struct PushNotificationItem: Identifiable, Hashable { self.todoId = notification.todoId self.todoCategory = notification.todoCategory } + + static func == (lhs: PushNotificationItem, rhs: PushNotificationItem) -> Bool { + lhs.id == rhs.id + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } } diff --git a/DevLog/Presentation/Structure/RecentTodoItem.swift b/DevLog/Presentation/Structure/RecentTodoItem.swift index 77a10744..0d972555 100644 --- a/DevLog/Presentation/Structure/RecentTodoItem.swift +++ b/DevLog/Presentation/Structure/RecentTodoItem.swift @@ -14,7 +14,7 @@ struct RecentTodoItem: Identifiable, Hashable { let isPinned: Bool let updatedAt: Date let tags: [String] - let category: TodoCategory + var category: TodoCategory init(from todo: Todo) { self.id = todo.id @@ -25,4 +25,12 @@ struct RecentTodoItem: Identifiable, Hashable { self.tags = todo.tags self.category = todo.category } + + static func == (lhs: RecentTodoItem, rhs: RecentTodoItem) -> Bool { + lhs.id == rhs.id + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } } diff --git a/DevLog/Presentation/Enum/TodoCategory.swift b/DevLog/Presentation/Structure/SystemTodoCategoryItem.swift similarity index 61% rename from DevLog/Presentation/Enum/TodoCategory.swift rename to DevLog/Presentation/Structure/SystemTodoCategoryItem.swift index 50ddb8d3..0a9da6c6 100644 --- a/DevLog/Presentation/Enum/TodoCategory.swift +++ b/DevLog/Presentation/Structure/SystemTodoCategoryItem.swift @@ -1,26 +1,23 @@ // -// TodoCategory.swift +// SystemTodoCategoryItem.swift // DevLog // -// Created by opfic on 5/29/25. +// Created by opfic on 3/30/26. // import SwiftUI -enum TodoCategory: String, Identifiable, CaseIterable, Codable { - case issue // 이슈 - case feature // 신규 기능 - case improvement // 개선/리팩터링 - case review // 코드/문서 리뷰 - case test // 테스트/QA - case doc // 문서화 - case research // 리서치/학습 - case etc // 기타 +struct SystemTodoCategoryItem: Identifiable, Hashable { + let systemTodoCategory: SystemTodoCategory - var id: String { rawValue } + init(from systemTodoCategory: SystemTodoCategory) { + self.systemTodoCategory = systemTodoCategory + } + + var id: String { systemTodoCategory.rawValue } var symbolName: String { - switch self { + switch systemTodoCategory { case .issue: return "exclamationmark.triangle" case .feature: return "sparkles" case .improvement: return "arrow.triangle.2.circlepath" @@ -31,9 +28,9 @@ enum TodoCategory: String, Identifiable, CaseIterable, Codable { case .etc: return "ellipsis" } } - + var localizedName: String { - switch self { + switch systemTodoCategory { case .issue: return NSLocalizedString("todo_category_issue", comment: "Todo category: Issue") case .feature: return NSLocalizedString("todo_category_feature", comment: "Todo category: Feature") case .improvement: return NSLocalizedString("todo_category_improvement", comment: "Todo category: Improvement") @@ -44,17 +41,17 @@ enum TodoCategory: String, Identifiable, CaseIterable, Codable { case .etc: return NSLocalizedString("todo_category_etc", comment: "Todo category: Etc") } } - + var color: Color { - switch self { - case .issue: return Color.red - case .feature: return Color.green - case .improvement: return Color.cyan - case .review: return Color.orange - case .test: return Color.purple - case .doc: return Color.yellow - case .research: return Color.teal - case .etc: return Color.gray + switch systemTodoCategory { + case .issue: return .red + case .feature: return .green + case .improvement: return .cyan + case .review: return .orange + case .test: return .purple + case .doc: return .yellow + case .research: return .teal + case .etc: return .gray } } } diff --git a/DevLog/Presentation/Structure/TodayTodoItem.swift b/DevLog/Presentation/Structure/TodayTodoItem.swift index 7afd16cc..7e583bbc 100644 --- a/DevLog/Presentation/Structure/TodayTodoItem.swift +++ b/DevLog/Presentation/Structure/TodayTodoItem.swift @@ -27,4 +27,12 @@ struct TodayTodoItem: Identifiable, Hashable { self.dueDate = todo.dueDate self.category = todo.category } + + static func == (lhs: TodayTodoItem, rhs: TodayTodoItem) -> Bool { + lhs.id == rhs.id + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } } diff --git a/DevLog/Presentation/Structure/TodoCategoryItem.swift b/DevLog/Presentation/Structure/TodoCategoryItem.swift new file mode 100644 index 00000000..4c3c4374 --- /dev/null +++ b/DevLog/Presentation/Structure/TodoCategoryItem.swift @@ -0,0 +1,90 @@ +// +// TodoCategoryItem.swift +// DevLog +// +// Created by opfic on 3/30/26. +// + +import SwiftUI + +struct TodoCategoryItem: Identifiable, Hashable { + var category: TodoCategory + var isVisible: Bool + + init(from preference: TodoCategoryPreference) { + self.category = preference.category + self.isVisible = preference.isVisible + } + + init( + from category: TodoCategory, + isVisible: Bool = true + ) { + self.category = category + self.isVisible = isVisible + } + + var id: String { category.storageValue } + + var todoCategory: TodoCategory { category } + + var preference: TodoCategoryPreference { + TodoCategoryPreference( + category: category, + isVisible: isVisible + ) + } + + var isUserCategory: Bool { + if case .user = category { + return true + } + + return false + } + + var symbolName: String { + switch category { + case .system(let systemTodoCategory): + return SystemTodoCategoryItem(from: systemTodoCategory).symbolName + case .user(let userTodoCategory): + return UserTodoCategoryItem(from: userTodoCategory).symbolName + } + } + + var localizedName: String { + switch category { + case .system(let systemTodoCategory): + return SystemTodoCategoryItem(from: systemTodoCategory).localizedName + case .user(let userTodoCategory): + return UserTodoCategoryItem(from: userTodoCategory).localizedName + } + } + + var color: Color { + switch category { + case .system(let systemTodoCategory): + return SystemTodoCategoryItem(from: systemTodoCategory).color + case .user(let userTodoCategory): + return UserTodoCategoryItem(from: userTodoCategory).color + } + } + + static func == (lhs: TodoCategoryItem, rhs: TodoCategoryItem) -> Bool { + lhs.category == rhs.category && lhs.isVisible == rhs.isVisible + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + + switch category { + case .system(let systemTodoCategory): + hasher.combine(systemTodoCategory.rawValue) + case .user(let userTodoCategory): + hasher.combine(userTodoCategory.name) + hasher.combine(userTodoCategory.colorHex) + } + + hasher.combine(isVisible) + } +} diff --git a/DevLog/Presentation/Structure/TodoCategoryPreference.swift b/DevLog/Presentation/Structure/TodoCategoryPreference.swift deleted file mode 100644 index 0b3264f2..00000000 --- a/DevLog/Presentation/Structure/TodoCategoryPreference.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// TodoCategoryPreference.swift -// DevLog -// -// Created by 최윤진 on 1/2/26. -// - -struct TodoCategoryPreference: Identifiable, Equatable { - var id: String { category.id } - let category: TodoCategory - var isVisible: Bool -} diff --git a/DevLog/Presentation/Structure/TodoReferenceItem.swift b/DevLog/Presentation/Structure/TodoReferenceItem.swift new file mode 100644 index 00000000..39440d02 --- /dev/null +++ b/DevLog/Presentation/Structure/TodoReferenceItem.swift @@ -0,0 +1,20 @@ +// +// TodoReferenceItem.swift +// DevLog +// +// Created by opfic on 3/30/26. +// + +import Foundation + +struct TodoReferenceItem: Equatable { + let id: String + let title: String + let category: TodoCategoryItem + + init(from todoReference: TodoReference) { + self.id = todoReference.id + self.title = todoReference.title + self.category = TodoCategoryItem(from: todoReference.category) + } +} diff --git a/DevLog/Presentation/Structure/UserTodoCategoryItem.swift b/DevLog/Presentation/Structure/UserTodoCategoryItem.swift new file mode 100644 index 00000000..cabd7b82 --- /dev/null +++ b/DevLog/Presentation/Structure/UserTodoCategoryItem.swift @@ -0,0 +1,32 @@ +// +// UserTodoCategoryItem.swift +// DevLog +// +// Created by opfic on 3/30/26. +// + +import SwiftUI + +struct UserTodoCategoryItem: Identifiable, Hashable { + let userTodoCategory: UserTodoCategory + + init(from userTodoCategory: UserTodoCategory) { + self.userTodoCategory = userTodoCategory + } + + var id: String { userTodoCategory.id } + + var symbolName: String { "tray.fill" } + + var localizedName: String { userTodoCategory.name } + + var color: Color { Color(hexString: userTodoCategory.colorHex) ?? .gray } + + static func == (lhs: UserTodoCategoryItem, rhs: UserTodoCategoryItem) -> Bool { + lhs.id == rhs.id + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} diff --git a/DevLog/Presentation/ViewModel/HomeViewModel.swift b/DevLog/Presentation/ViewModel/HomeViewModel.swift index 809c8745..a906c21a 100644 --- a/DevLog/Presentation/ViewModel/HomeViewModel.swift +++ b/DevLog/Presentation/ViewModel/HomeViewModel.swift @@ -11,9 +11,7 @@ import Combine @Observable final class HomeViewModel: Store { struct State: Equatable { - var todoCategoryPreferences = TodoCategory.allCases.map { - TodoCategoryPreference(category: $0, isVisible: true) - } + var preferences: [TodoCategoryItem] = [] var recentTodos: [RecentTodoItem] = [] var webPages: [WebPageItem] = [] var isNetworkConnected: Bool = true @@ -23,6 +21,7 @@ final class HomeViewModel: Store { var webPageURLInput: String = "https://" var selectedTodoCategory: TodoCategory? var reorderTodo: Bool = false + var isPreferencesLoading: Bool = false var isRecentTodosLoading: Bool = false var isWebPageLoading: Bool = false var isAppending: Bool = false @@ -43,7 +42,8 @@ final class HomeViewModel: Store { case setToast(isPresented: Bool, type: ToastType? = nil) case setLoading(LoadingTarget, Bool) case tapTodoCategory(TodoCategory) - case orderTodoCategoryPreferences([TodoCategoryPreference]) + case orderTodoCategory([TodoCategoryItem]) + case setTodoCategory([TodoCategoryItem]) case addTodo(Todo) case updateRecentTodos([RecentTodoItem]) case updateWebPageURLInput(String) @@ -59,6 +59,8 @@ final class HomeViewModel: Store { case addWebPage(String) case deleteWebPage(WebPageItem, Int) case undoDeleteWebPage(String) + case fetchTodoCategoryPreferences + case updateTodoCategoryPreferences([TodoCategoryItem]) case fetchRecentTodos case fetchWebPages case showModalAfterDelay(ModalType) @@ -87,12 +89,15 @@ final class HomeViewModel: Store { } enum LoadingTarget: Hashable { + case preferences case recentTodos case webPage case overlay } private(set) var state = State() + private let fetchPreferencesUseCase: FetchTodoCategoryPreferencesUseCase + private let updatePreferencesUseCase: UpdateTodoCategoryPreferencesUseCase private let upsertTodoUseCase: UpsertTodoUseCase private let addWebPageUseCase: AddWebPageUseCase private let deleteWebPageUseCase: DeleteWebPageUseCase @@ -105,6 +110,8 @@ final class HomeViewModel: Store { private var cancellables = Set() init( + fetchPreferencesUseCase: FetchTodoCategoryPreferencesUseCase, + updatePreferencesUseCase: UpdateTodoCategoryPreferencesUseCase, addWebPageUseCase: AddWebPageUseCase, deleteWebPageUseCase: DeleteWebPageUseCase, undoDeleteWebPageUseCase: UndoDeleteWebPageUseCase, @@ -113,6 +120,8 @@ final class HomeViewModel: Store { fetchWebPagesUseCase: FetchWebPagesUseCase, networkConnectivityUseCase: ObserveNetworkConnectivityUseCase ) { + self.fetchPreferencesUseCase = fetchPreferencesUseCase + self.updatePreferencesUseCase = updatePreferencesUseCase self.addWebPageUseCase = addWebPageUseCase self.deleteWebPageUseCase = deleteWebPageUseCase self.undoDeleteWebPageUseCase = undoDeleteWebPageUseCase @@ -132,11 +141,12 @@ final class HomeViewModel: Store { case .networkStatusChanged(let isConnected): state.isNetworkConnected = isConnected case .onAppear, .setPresentation, .setAlert, .setToast, .tapTodoCategory, - .orderTodoCategoryPreferences, .addTodo, .updateWebPageURLInput, + .orderTodoCategory, .addTodo, .updateWebPageURLInput, .addWebPage, .deleteWebPage, .undoDeleteWebPage: effects = reduceByView(action, state: &state) - case .setLoading, .updateRecentTodos, .updateWebPages, .restoreWebPage: + case .setLoading, .setTodoCategory, .updateRecentTodos, + .updateWebPages, .restoreWebPage: effects = reduceByRun(action, state: &state) } @@ -146,6 +156,25 @@ final class HomeViewModel: Store { func run(_ effect: SideEffect) { switch effect { + case .fetchTodoCategoryPreferences: + beginLoading(for: .preferences, mode: .immediate) + Task { + do { + defer { endLoading(for: .preferences, mode: .immediate) } + let preferences = try await fetchPreferencesUseCase.execute() + send(.setTodoCategory(preferences.map(TodoCategoryItem.init(from:)))) + } catch { + send(.setAlert(isPresented: true, type: .error)) + } + } + case .updateTodoCategoryPreferences(let items): + Task { + do { + try await updatePreferencesUseCase.execute(items.map(\.preference)) + } catch { + send(.setAlert(isPresented: true, type: .error)) + } + } case .addTodo(let todo): beginLoading(for: .overlay, mode: .delayed) Task { @@ -255,7 +284,7 @@ private extension HomeViewModel { func reduceByView(_ action: Action, state: inout State) -> [SideEffect] { switch action { case .onAppear: - return [.fetchRecentTodos, .fetchWebPages] + return [.fetchTodoCategoryPreferences, .fetchRecentTodos, .fetchWebPages] case .setPresentation(let presentation, let isPresented): setPresentation(&state, presentation: presentation, isPresented: isPresented) case .setAlert(let presented, let type): @@ -273,8 +302,10 @@ private extension HomeViewModel { state.selectedTodoCategory = category state.showContentPicker = false return [.showModalAfterDelay(.todoEditor)] - case .orderTodoCategoryPreferences(let preferences): - state.todoCategoryPreferences = preferences + case .orderTodoCategory(let preferences): + state.preferences = preferences + state.recentTodos = syncRecentTodos(state.recentTodos, preferences: preferences) + return [.updateTodoCategoryPreferences(preferences)] case .addTodo(let todo): return [.addTodo(todo)] case .updateWebPageURLInput(let text): @@ -308,6 +339,9 @@ private extension HomeViewModel { switch action { case .setLoading(let loadingTarget, let isLoading): setLoading(&state, loadingTarget: loadingTarget, isLoading: isLoading) + case .setTodoCategory(let preferences): + state.preferences = preferences + state.recentTodos = syncRecentTodos(state.recentTodos, preferences: preferences) case .updateRecentTodos(let todos): state.recentTodos = todos case .updateWebPages(let pages): @@ -394,6 +428,8 @@ private extension HomeViewModel { isLoading: Bool ) { switch loadingTarget { + case .preferences: + state.isPreferencesLoading = isLoading case .recentTodos: state.isRecentTodosLoading = isLoading case .webPage: @@ -403,6 +439,23 @@ private extension HomeViewModel { } } + func syncRecentTodos( + _ recentTodos: [RecentTodoItem], + preferences: [TodoCategoryItem] + ) -> [RecentTodoItem] { + recentTodos.map { recentTodo in + guard let item = preferences.first(where: { + $0.category.storageValue == recentTodo.category.storageValue + }) else { + return recentTodo + } + + var recentTodo = recentTodo + recentTodo.category = item.category + return recentTodo + } + } + func normalizedWebPageURL(_ input: String) -> String? { let trimmed = input.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { return nil } diff --git a/DevLog/Presentation/ViewModel/TodoDetailViewModel.swift b/DevLog/Presentation/ViewModel/TodoDetailViewModel.swift index dc95c781..cfd9a71c 100644 --- a/DevLog/Presentation/ViewModel/TodoDetailViewModel.swift +++ b/DevLog/Presentation/ViewModel/TodoDetailViewModel.swift @@ -113,6 +113,7 @@ final class TodoDetailViewModel: Store { if !numbers.isEmpty { do { referenceItems = try await fetchReferenceItemsUseCase.execute(numbers) + .mapValues(TodoReferenceItem.init(from:)) } catch { referenceItems = [:] } diff --git a/DevLog/Presentation/ViewModel/TodoEditorViewModel.swift b/DevLog/Presentation/ViewModel/TodoEditorViewModel.swift index 813607f7..26cca445 100644 --- a/DevLog/Presentation/ViewModel/TodoEditorViewModel.swift +++ b/DevLog/Presentation/ViewModel/TodoEditorViewModel.swift @@ -39,7 +39,7 @@ final class TodoEditorViewModel: Store { self.content = state.content self.dueDate = state.dueDate self.tags = Array(state.tags) - self.category = state.category + self.category = state.category.category } } @@ -57,7 +57,8 @@ final class TodoEditorViewModel: Store { var tagText: String = "" var focusOnEditor: Bool = false var tabViewTag: Tag = .editor - var category: TodoCategory = .etc + var categories: [TodoCategoryItem] = [] + var category = TodoCategoryItem(from: .system(.etc)) var isValidToSave: Bool { !title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } @@ -68,27 +69,31 @@ final class TodoEditorViewModel: Store { } enum Action { + case onAppear case addTag(String) case removeTag(String) case setContent(String) case setCompleted(Bool) case setDueDate(Date?) - case setCategory(TodoCategory) + case setCategory(TodoCategoryItem) case setPinned(Bool) case setShowInfo(Bool) case setSelectedTodoId(TodoIdItem?) case setTabViewTag(Tag) case setTagText(String) case setTitle(String) + case setCategories([TodoCategoryItem]) case setReferenceItems([Int: TodoReferenceItem]) } enum SideEffect { + case fetchCategories case resolveMarkdown(String) } private(set) var state = State() private let calendar = Calendar.current + private let fetchPreferencesUseCase: FetchTodoCategoryPreferencesUseCase private let fetchReferenceItemsUseCase: FetchReferenceItemsUseCase private let id: String private let isCompleted: Bool @@ -117,8 +122,10 @@ final class TodoEditorViewModel: Store { // 새로운 Todo 생성용 생성자 init( category: TodoCategory, + fetchPreferencesUseCase: FetchTodoCategoryPreferencesUseCase, fetchReferenceItemsUseCase: FetchReferenceItemsUseCase ) { + self.fetchPreferencesUseCase = fetchPreferencesUseCase self.fetchReferenceItemsUseCase = fetchReferenceItemsUseCase self.id = UUID().uuidString self.isCompleted = false @@ -126,14 +133,17 @@ final class TodoEditorViewModel: Store { self.number = nil self.createdAt = nil self.originalDraft = nil - state.category = category + state.category = TodoCategoryItem(from: category) + state.categories = [TodoCategoryItem(from: category)] } // 기존 Todo 편집용 생성자 init( todo: Todo, + fetchPreferencesUseCase: FetchTodoCategoryPreferencesUseCase, fetchReferenceItemsUseCase: FetchReferenceItemsUseCase ) { + self.fetchPreferencesUseCase = fetchPreferencesUseCase self.fetchReferenceItemsUseCase = fetchReferenceItemsUseCase self.id = todo.id self.isCompleted = todo.isCompleted @@ -148,7 +158,7 @@ final class TodoEditorViewModel: Store { state.content = todo.content state.dueDate = todo.dueDate state.tags = OrderedSet(todo.tags) - state.category = todo.category + state.category = TodoCategoryItem(from: todo.category) } func reduce(with action: Action) -> [SideEffect] { @@ -156,6 +166,8 @@ final class TodoEditorViewModel: Store { var effects: [SideEffect] = [] switch action { + case .onAppear: + effects = [.fetchCategories] case .addTag(let tag): if !tag.isEmpty { state.tags.append(tag) @@ -180,8 +192,8 @@ final class TodoEditorViewModel: Store { state.completedAt = isCompleted ? Date() : nil } state.isCompleted = isCompleted - case .setCategory(let todoCategory): - state.category = todoCategory + case .setCategory(let todoCategoryItem): + state.category = todoCategoryItem case .setPinned(let isPinned): state.isPinned = isPinned case .setShowInfo(let isPresented): @@ -193,6 +205,8 @@ final class TodoEditorViewModel: Store { if tag == .preview { effects = [.resolveMarkdown(state.content)] } + case .setCategories(let categories): + state.categories = categories case .setReferenceItems(let items): state.referenceItems = items } @@ -203,6 +217,13 @@ final class TodoEditorViewModel: Store { func run(_ effect: SideEffect) { switch effect { + case .fetchCategories: + Task { + do { + let preferences = try await fetchPreferencesUseCase.execute() + send(.setCategories(preferences.map(TodoCategoryItem.init(from:)))) + } catch { } + } case .resolveMarkdown(let content): Task { let numbers = content.todoReferenceNumbers @@ -211,6 +232,7 @@ final class TodoEditorViewModel: Store { if !numbers.isEmpty { do { referenceItems = try await fetchReferenceItemsUseCase.execute(numbers) + .mapValues(TodoReferenceItem.init(from:)) } catch { referenceItems = [:] } @@ -255,7 +277,7 @@ extension TodoEditorViewModel { completedAt: state.completedAt, dueDate: state.dueDate, tags: state.tags.map { $0 }, - category: state.category + category: state.category.category ) } } diff --git a/DevLog/Presentation/ViewModel/TodoManageViewModel.swift b/DevLog/Presentation/ViewModel/TodoManageViewModel.swift index 0067a042..4feffe8d 100644 --- a/DevLog/Presentation/ViewModel/TodoManageViewModel.swift +++ b/DevLog/Presentation/ViewModel/TodoManageViewModel.swift @@ -5,37 +5,222 @@ // Created by 최윤진 on 11/30/25. // -import Foundation +import SwiftUI @Observable final class TodoManageViewModel: Store { struct State: Equatable { - var todoCategoryPreferences: [TodoCategoryPreference] + var preferences: [TodoCategoryItem] + var category: TodoCategoryItem? + var showSheet: Bool = false + var showAlert: Bool = false } enum Action { + case tapAddUserCategory case moveItem(from: IndexSet, target: Int) - case tapItem(_ item: TodoCategory) + case tapItem(TodoCategoryItem) + case tapEditUserCategory(TodoCategoryItem) + case tapDeleteUserCategory(TodoCategoryItem) + case confirmDeleteUserCategory + case setShowSheet(Bool) + case setShowAlert(Bool) + case setCategoryName(String) + case setCategoryColor(Color) + case setRandomCategoryColor + case saveUserCategory } enum SideEffect { } private(set) var state: State - init(_ todoCategoryPreferences: [TodoCategoryPreference]) { - self.state = State(todoCategoryPreferences: todoCategoryPreferences) + var isEditing: Bool { + guard let categoryItem = state.category else { + return false + } + + return state.preferences.contains { $0.id == categoryItem.id } + } + + var navigationTitle: String { + isEditing ? "카테고리 수정" : "카테고리 추가" + } + + var submitTitle: String { + isEditing ? "저장" : "추가" + } + + var placeholder: String { + guard + let categoryItem = state.category, + case .user(let userTodoCategory) = categoryItem.category + else { + return "이름" + } + + return userTodoCategory.name + } + + var categoryNameCountText: String { + guard + let item = state.category, + case .user(let category) = item.category + else { + return "0/20" + } + + return "\(category.name.count)/20" + } + + var canSubmitUserCategory: Bool { + guard + let item = state.category, + case .user(let category) = item.category + else { + return false + } + + let trimmedCategoryName = category.name.trimmingCharacters(in: .whitespacesAndNewlines) + if trimmedCategoryName.isEmpty { + return false + } + + if SystemTodoCategory.allCases.contains(where: { + $0.rawValue.caseInsensitiveCompare(trimmedCategoryName) == .orderedSame + }) { + return false + } + + if state.preferences.contains(where: { item in + guard case .user(let userTodoCategory) = item.category, + userTodoCategory.id != category.id else { + return false + } + + return userTodoCategory.name.caseInsensitiveCompare(trimmedCategoryName) == .orderedSame + }) { + return false + } + + return true + } + + init(_ preferences: [TodoCategoryItem]) { + self.state = State(preferences: preferences) } func reduce(with action: Action) -> [SideEffect] { var state = self.state switch action { + case .tapAddUserCategory: + guard let randomHexValue = Color.randomValue.hexValue else { + break + } + + state.category = TodoCategoryItem( + from: .user( + UserTodoCategory( + id: UUID().uuidString.lowercased(), + name: "", + colorHex: randomHexValue + ) + ) + ) + state.showSheet = true case .moveItem(let from, let target): - state.todoCategoryPreferences.move(fromOffsets: from, toOffset: target) + state.preferences.move(fromOffsets: from, toOffset: target) case .tapItem(let item): - if let index = state.todoCategoryPreferences.firstIndex(where: { $0.category == item }) { - state.todoCategoryPreferences[index].isVisible.toggle() + if let index = state.preferences.firstIndex(where: { $0.id == item.id }) { + state.preferences[index].isVisible.toggle() + } + case .tapEditUserCategory(let item): + guard item.isUserCategory else { + break + } + + state.category = item + state.showSheet = true + case .tapDeleteUserCategory(let item): + guard item.isUserCategory else { + break + } + + state.category = item + state.showAlert = true + case .confirmDeleteUserCategory: + guard let categoryItem = state.category else { + break + } + + if let index = state.preferences.firstIndex(where: { $0.id == categoryItem.id }) { + state.preferences.remove(at: index) + } + state.showAlert = false + state.category = nil + case .setShowSheet(let isPresented): + state.showSheet = isPresented + if !isPresented { + state.category = nil } + case .setShowAlert(let isPresented): + state.showAlert = isPresented + if !isPresented { + state.category = nil + } + case .setCategoryName(let name): + guard var item = state.category, + case .user(var category) = item.category else { + break + } + + category.name = String(name.prefix(20)) + item.category = .user(category) + state.category = item + case .setCategoryColor(let color): + guard var item = state.category, + case .user(var category) = item.category, + let hexValue = color.hexValue else { + break + } + + category.colorHex = hexValue + item.category = .user(category) + state.category = item + case .setRandomCategoryColor: + guard var item = state.category, + case .user(var category) = item.category, + let randomHexValue = Color.randomValue.hexValue else { + break + } + + category.colorHex = randomHexValue + item.category = .user(category) + state.category = item + case .saveUserCategory: + guard var item = state.category, + case .user(let category) = item.category else { + break + } + + item.category = .user( + UserTodoCategory( + id: category.id, + name: category.name.trimmingCharacters(in: .whitespacesAndNewlines), + colorHex: category.colorHex + ) + ) + + if let index = state.preferences.firstIndex(where: { $0.id == item.id }) { + item.isVisible = state.preferences[index].isVisible + state.preferences[index] = item + } else { + state.preferences.append(item) + } + + state.showSheet = false + state.category = nil } if self.state != state { self.state = state } diff --git a/DevLog/Resource/Localizable.xcstrings b/DevLog/Resource/Localizable.xcstrings index 867a75d6..0891522a 100644 --- a/DevLog/Resource/Localizable.xcstrings +++ b/DevLog/Resource/Localizable.xcstrings @@ -154,6 +154,12 @@ } } } + }, + "TODO" : { + + }, + "TODO 편집" : { + }, "todo_category_doc" : { "comment" : "Todo category: Documentation", @@ -242,12 +248,6 @@ } } } - }, - "TODO" : { - - }, - "TODO 편집" : { - }, "Todos" : { @@ -380,6 +380,9 @@ }, "이 분기에는 선택한 활동이 없어요" : { + }, + "이 카테고리를 삭제하면 해당하던 TODO는 기타 카테고리로 처리됩니다.\n정말 삭제하시겠습니까?" : { + }, "읽지 않음" : { @@ -450,6 +453,9 @@ }, "카테고리" : { + }, + "카테고리 삭제" : { + }, "컨텐츠" : { @@ -504,4 +510,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/DevLog/UI/Common/MainView.swift b/DevLog/UI/Common/MainView.swift index daab8800..089dab58 100644 --- a/DevLog/UI/Common/MainView.swift +++ b/DevLog/UI/Common/MainView.swift @@ -14,6 +14,8 @@ struct MainView: View { var body: some View { TabView { HomeView(viewModel: HomeViewModel( + fetchPreferencesUseCase: container.resolve(FetchTodoCategoryPreferencesUseCase.self), + updatePreferencesUseCase: container.resolve(UpdateTodoCategoryPreferencesUseCase.self), addWebPageUseCase: container.resolve(AddWebPageUseCase.self), deleteWebPageUseCase: container.resolve(DeleteWebPageUseCase.self), undoDeleteWebPageUseCase: container.resolve(UndoDeleteWebPageUseCase.self), diff --git a/DevLog/UI/Home/HomeView.swift b/DevLog/UI/Home/HomeView.swift index 5303971a..968bbf37 100644 --- a/DevLog/UI/Home/HomeView.swift +++ b/DevLog/UI/Home/HomeView.swift @@ -24,14 +24,14 @@ struct HomeView: View { .navigationTitle("홈") .navigationDestination(for: Path.self) { path in switch path { - case .category(let todoCategory): + case .category(let item): TodoListView(viewModel: TodoListViewModel( fetchTodosUseCase: container.resolve(FetchTodosUseCase.self), fetchTodoByIdUseCase: container.resolve(FetchTodoByIdUseCase.self), upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self), deleteTodoUseCase: container.resolve(DeleteTodoUseCase.self), undoDeleteTodoUseCase: container.resolve(UndoDeleteTodoUseCase.self), - category: todoCategory + category: item.todoCategory )) .environment(router) case .detail(let todoId): @@ -60,11 +60,11 @@ struct HomeView: View { set: { viewModel.send(.setPresentation(.reorderTodo, $0)) } )) { TodoManageView( - viewModel: TodoManageViewModel(viewModel.state.todoCategoryPreferences), + viewModel: TodoManageViewModel(viewModel.state.preferences), onDismiss: { array in viewModel.send(.setPresentation(.reorderTodo, false)) withAnimation { - viewModel.send(.orderTodoCategoryPreferences(array)) + viewModel.send(.orderTodoCategory(array)) } } ) @@ -83,6 +83,7 @@ struct HomeView: View { TodoEditorView( viewModel: TodoEditorViewModel( category: selectedCategory, + fetchPreferencesUseCase: container.resolve(FetchTodoCategoryPreferencesUseCase.self), fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self) ), onSubmit: { viewModel.send(.addTodo($0)) } @@ -162,15 +163,18 @@ struct HomeView: View { private var todoSection: some View { Section(content: { - let preferences = viewModel.state.todoCategoryPreferences - ForEach(preferences.filter { $0.isVisible }, id: \.id) { preference in - let category = preference.category - NavigationLink(value: Path.category(category)) { - labelImage( - text: category.localizedName, - systemName: category.symbolName, - imageColor: category.color - ) + if viewModel.state.isPreferencesLoading { + LoadingView() + } else { + let preferences = viewModel.state.preferences + ForEach(preferences.filter { $0.isVisible }, id: \.id) { item in + NavigationLink(value: Path.category(item)) { + labelImage( + text: item.localizedName, + systemName: item.symbolName, + imageColor: item.color + ) + } } } }, header: { @@ -289,19 +293,22 @@ struct HomeView: View { NavigationStack { List { Section { - let preferences = viewModel.state.todoCategoryPreferences.filter(\.isVisible) - ForEach(preferences, id: \.id) { preference in - let category = preference.category - Button { - DispatchQueue.main.async { - viewModel.send(.tapTodoCategory(category)) + if viewModel.state.isPreferencesLoading { + LoadingView() + } else { + let preferences = viewModel.state.preferences.filter(\.isVisible) + ForEach(preferences, id: \.id) { item in + Button { + DispatchQueue.main.async { + viewModel.send(.tapTodoCategory(item.category)) + } + } label: { + labelImage( + text: item.localizedName, + systemName: item.symbolName, + imageColor: item.color + ) } - } label: { - labelImage( - text: category.localizedName, - systemName: category.symbolName, - imageColor: category.color - ) } } } header: { @@ -363,7 +370,7 @@ struct HomeView: View { } private enum Path: Hashable { - case category(TodoCategory) + case category(TodoCategoryItem) case detail(String) case web(WebPageItem) } @@ -374,12 +381,13 @@ private struct RecentTodoRow: View { let sceneWidth: CGFloat var body: some View { + let category = TodoCategoryItem(from: todo.category) HStack(alignment: .top, spacing: 12) { RoundedRectangle(cornerRadius: 8) - .fill(todo.category.color) + .fill(category.color) .frame(width: sceneWidth * 0.08, height: sceneWidth * 0.08) .overlay { - Image(systemName: todo.category.symbolName) + Image(systemName: category.symbolName) .foregroundStyle(Color.white) .font(.title3) } @@ -404,9 +412,9 @@ private struct RecentTodoRow: View { } HStack(spacing: 6) { - Text(todo.category.localizedName) + Text(category.localizedName) .font(.caption.weight(.semibold)) - .foregroundStyle(todo.category.color) + .foregroundStyle(category.color) RelativeTimeText(date: todo.updatedAt) } diff --git a/DevLog/UI/Home/TodoDetailView.swift b/DevLog/UI/Home/TodoDetailView.swift index 91020b0e..709eed2a 100644 --- a/DevLog/UI/Home/TodoDetailView.swift +++ b/DevLog/UI/Home/TodoDetailView.swift @@ -63,6 +63,7 @@ struct TodoDetailView: View { TodoEditorView( viewModel: TodoEditorViewModel( todo: todo, + fetchPreferencesUseCase: container.resolve(FetchTodoCategoryPreferencesUseCase.self), fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self) ), onSubmit: { viewModel.send(.upsertTodo($0)) } @@ -117,7 +118,7 @@ private struct TodoDetailInfoSheetView: View { HStack { Text("카테고리") Spacer() - Text(todo.category.localizedName) + Text(TodoCategoryItem(from: todo.category).localizedName) .foregroundStyle(.secondary) } diff --git a/DevLog/UI/Home/TodoEditorView.swift b/DevLog/UI/Home/TodoEditorView.swift index 58927a5e..703341be 100644 --- a/DevLog/UI/Home/TodoEditorView.swift +++ b/DevLog/UI/Home/TodoEditorView.swift @@ -38,6 +38,7 @@ struct TodoEditorView: View { .onTapGesture { field = .content } + .onAppear { viewModel.send(.onAppear) } .navigationTitle(viewModel.navigationTitle) .navigationBarTitleDisplayMode(.inline) .toolbarBackground(.background, for: .navigationBar) @@ -211,13 +212,21 @@ private struct TodoEditorInfoSheetView: View { Picker( "카테고리", selection: Binding( - get: { viewModel.state.category }, - set: { viewModel.send(.setCategory($0)) } + get: { viewModel.state.category.id }, + set: { categoryID in + guard let item = viewModel.state.categories.first(where: { + $0.id == categoryID + }) else { + return + } + + viewModel.send(.setCategory(item)) + } ) ) { - ForEach(TodoCategory.allCases) { category in - Text(category.localizedName) - .tag(category) + ForEach(viewModel.state.categories, id: \.id) { item in + Text(item.localizedName) + .tag(item.id) } } diff --git a/DevLog/UI/Home/TodoListView.swift b/DevLog/UI/Home/TodoListView.swift index 3bcd904b..7601b366 100644 --- a/DevLog/UI/Home/TodoListView.swift +++ b/DevLog/UI/Home/TodoListView.swift @@ -41,7 +41,7 @@ struct TodoListView: View { set: { viewModel.send(.setIsSearching($0)) } ), placement: .navigationBarDrawer(displayMode: .always), - prompt: "\(viewModel.state.category.localizedName) 검색" + prompt: "\(TodoCategoryItem(from: viewModel.state.category).localizedName) 검색" ) } } @@ -76,7 +76,7 @@ struct TodoListView: View { ) { Label(viewModel.state.toastMessage, systemImage: "arrow.uturn.left") } - .navigationTitle(viewModel.state.category.localizedName) + .navigationTitle(TodoCategoryItem(from: viewModel.state.category).localizedName) .fullScreenCover(isPresented: Binding( get: { viewModel.state.showEditor }, set: { viewModel.send(.setShowEditor($0)) } @@ -84,6 +84,7 @@ struct TodoListView: View { TodoEditorView( viewModel: TodoEditorViewModel( category: viewModel.state.category, + fetchPreferencesUseCase: container.resolve(FetchTodoCategoryPreferencesUseCase.self), fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self) ), onSubmit: { viewModel.send(.upsertTodo($0)) } @@ -221,7 +222,7 @@ struct TodoListView: View { set: { viewModel.send(.setIsSearching($0)) } ), placement: .navigationBarDrawer(displayMode: .always), - prompt: "\(viewModel.state.category.localizedName) 검색" + prompt: "\(TodoCategoryItem(from: viewModel.state.category).localizedName) 검색" ) } @@ -234,7 +235,7 @@ struct TodoListView: View { : Array(searchResults.prefix(limit)) if viewModel.state.searchText.isEmpty { - Text("\(viewModel.state.category.localizedName)의 제목이나 내용을 검색해 보세요.") + Text("\(TodoCategoryItem(from: viewModel.state.category).localizedName)의 제목이나 내용을 검색해 보세요.") .foregroundStyle(Color.gray) .frame(maxWidth: .infinity) } else if viewModel.state.isLoading { diff --git a/DevLog/UI/Home/TodoManageView.swift b/DevLog/UI/Home/TodoManageView.swift index 529801e7..99237bf3 100644 --- a/DevLog/UI/Home/TodoManageView.swift +++ b/DevLog/UI/Home/TodoManageView.swift @@ -9,42 +9,178 @@ import SwiftUI struct TodoManageView: View { @State var viewModel: TodoManageViewModel - var onDismiss: (([TodoCategoryPreference]) -> Void)? + @State private var tmpText = "" + var onDismiss: (([TodoCategoryItem]) -> Void)? var body: some View { NavigationStack { List { - ForEach(viewModel.state.todoCategoryPreferences, id: \.id) { preference in - let category = preference.category + ForEach(viewModel.state.preferences, id: \.id) { item in HStack(spacing: 0) { - CheckBox(isChecked: preference.isVisible, font: .title3) + CheckBox(isChecked: item.isVisible, font: .title3) .padding(.horizontal) .onTapGesture { - viewModel.send(.tapItem(category)) + viewModel.send(.tapItem(item)) } - Text(category.localizedName) + Text(item.localizedName) + .lineLimit(1) + Spacer() + if item.isUserCategory { + Button { + viewModel.send(.tapEditUserCategory(item)) + } label: { + Image(systemName: "slider.horizontal.3") + } + .buttonStyle(.borderless) + .padding(.trailing, 8) + + Button(role: .destructive) { + viewModel.send(.tapDeleteUserCategory(item)) + } label: { + Image(systemName: "trash") + } + .buttonStyle(.borderless) + .padding(.trailing) + } } } - .onMove { (source: IndexSet, destination: Int) in + .onMove { source, destination in viewModel.send(.moveItem(from: source, target: destination)) } .listRowInsets(EdgeInsets()) } - // 편집 모드 활성화 - // row 우측에 line.3.horizontal 추가됨 - .environment(\.editMode, .constant(EditMode.active)) + .environment(\.editMode, .constant(.active)) .navigationTitle("TODO 편집") .navigationBarTitleDisplayMode(.inline) .navigationBarBackButtonHidden() + .sheet(isPresented: Binding( + get: { viewModel.state.showSheet }, + set: { viewModel.send(.setShowSheet($0)) } + )) { + categorySheet + } + .alert( + "카테고리 삭제", + isPresented: Binding( + get: { viewModel.state.showAlert }, + set: { viewModel.send(.setShowAlert($0)) } + ) + ) { + Button("취소", role: .cancel) { + viewModel.send(.setShowAlert(false)) + } + Button("삭제", role: .destructive) { + viewModel.send(.confirmDeleteUserCategory) + } + } message: { + Text("이 카테고리를 삭제하면 해당하던 TODO는 기타 카테고리로 처리됩니다.\n정말 삭제하시겠습니까?") + .multilineTextAlignment(.leading) + } .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button { + viewModel.send(.tapAddUserCategory) + } label: { + Image(systemName: "plus") + } + } + ToolbarItem(placement: .navigationBarTrailing) { - Button(action: { - onDismiss?(viewModel.state.todoCategoryPreferences) - }) { + Button { + onDismiss?(viewModel.state.preferences) + } label: { Text("완료") } } } } + .presentationDragIndicator(.visible) + } + + private var categorySheet: some View { + NavigationStack { + Form { + Section { + HStack(spacing: 8) { + TextField( + "", + text: $tmpText, + prompt: Text(viewModel.placeholder).foregroundStyle(.secondary) + ) + .frame(height: UIFont.preferredFont(forTextStyle: .body).lineHeight) + .onAppear { + tmpText = currentCategoryName + } + .onChange(of: tmpText) { _, value in + viewModel.send(.setCategoryName(value)) + tmpText = currentCategoryName + } + + Text(viewModel.categoryNameCountText) + .font(.footnote) + .foregroundStyle(.secondary) + .monospacedDigit() + } + } + + Section { + let color = Color(hexString: currentCategoryColorHex) ?? .randomValue + ColorPicker(selection: Binding( + get: { color }, + set: { viewModel.send(.setCategoryColor($0)) } + ), supportsOpacity: false) { + Text(currentCategoryColorHex.isEmpty ? "#" : currentCategoryColorHex) + .overlay(alignment: .bottom) { + Rectangle() + .frame(height: 1) + .offset(y: 1) + } + .foregroundStyle(color) + .onTapGesture { + viewModel.send(.setRandomCategoryColor) + } + } + .pickerStyle(.palette) + } + } + .navigationTitle(viewModel.navigationTitle) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button("취소") { + viewModel.send(.setShowSheet(false)) + } + } + + ToolbarItem(placement: .navigationBarTrailing) { + Button(viewModel.submitTitle) { + viewModel.send(.saveUserCategory) + } + .disabled(!viewModel.canSubmitUserCategory) + } + } + } + } + + private var currentCategoryName: String { + guard + let categoryItem = viewModel.state.category, + case .user(let userTodoCategory) = categoryItem.category + else { + return "" + } + + return userTodoCategory.name + } + + private var currentCategoryColorHex: String { + guard + let categoryItem = viewModel.state.category, + case .user(let userTodoCategory) = categoryItem.category + else { + return "" + } + + return userTodoCategory.colorHex } } diff --git a/DevLog/UI/Profile/ProfileView.swift b/DevLog/UI/Profile/ProfileView.swift index c116f47c..a7872ac6 100644 --- a/DevLog/UI/Profile/ProfileView.swift +++ b/DevLog/UI/Profile/ProfileView.swift @@ -332,9 +332,10 @@ struct ProfileView: View { Button { router.push(Path.activity(activity)) } label: { + let todoCategoryItem = TodoCategoryItem(from: activity.todo.category) HStack(spacing: 8) { - Image(systemName: activity.todo.category.symbolName) - .foregroundStyle(activity.todo.category.color) + Image(systemName: todoCategoryItem.symbolName) + .foregroundStyle(todoCategoryItem.color) .frame(width: 20) Text(activity.todo.title) .font(.caption) diff --git a/DevLog/UI/PushNotification/PushNotificationListView.swift b/DevLog/UI/PushNotification/PushNotificationListView.swift index 7bff88b3..d83ed018 100644 --- a/DevLog/UI/PushNotification/PushNotificationListView.swift +++ b/DevLog/UI/PushNotification/PushNotificationListView.swift @@ -246,12 +246,12 @@ struct PushNotificationListView: View { private func notificationRow(_ item: PushNotificationItem) -> some View { HStack { VStack { - let todoCategory = item.todoCategory + let todoCategoryItem = TodoCategoryItem(from: item.todoCategory) RoundedRectangle(cornerRadius: 8) - .fill(todoCategory.color) + .fill(todoCategoryItem.color) .frame(width: sceneWidth * 0.08, height: sceneWidth * 0.08) .overlay { - Image(systemName: todoCategory.symbolName) + Image(systemName: todoCategoryItem.symbolName) .foregroundStyle(Color.white) .font(.title3) } diff --git a/DevLog/UI/Today/TodayView.swift b/DevLog/UI/Today/TodayView.swift index 2f65bad8..1765db8b 100644 --- a/DevLog/UI/Today/TodayView.swift +++ b/DevLog/UI/Today/TodayView.swift @@ -289,10 +289,11 @@ private struct TodayTodoRow: View { let item: TodayTodoItem var body: some View { + let todoCategoryItem = TodoCategoryItem(from: item.category) VStack(alignment: .leading, spacing: 8) { HStack(spacing: 8) { - Image(systemName: item.category.symbolName) - .foregroundStyle(item.category.color) + Image(systemName: todoCategoryItem.symbolName) + .foregroundStyle(todoCategoryItem.color) .frame(width: 18) Text(item.title) .font(.headline) @@ -308,9 +309,9 @@ private struct TodayTodoRow: View { } HStack(spacing: 8) { - Text(item.category.localizedName) + Text(todoCategoryItem.localizedName) .font(.caption.weight(.semibold)) - .foregroundStyle(item.category.color) + .foregroundStyle(todoCategoryItem.color) if let dueDate { Text(dueDate.text) diff --git a/Firebase/functions/src/fcm/notification.ts b/Firebase/functions/src/fcm/notification.ts index 167d82a7..279da8ac 100644 --- a/Firebase/functions/src/fcm/notification.ts +++ b/Firebase/functions/src/fcm/notification.ts @@ -42,7 +42,7 @@ export const sendPushNotification = onTaskDispatched({ logger.warn("notificationTask 문서 형식이 올바르지 않습니다.", { taskId }); return; } - const { userId, todoId, todoCategory, dueDateKey, title, body } = parsed; + const { userId, todoId, dueDateKey, title, body } = parsed; const settingsDocRef = admin.firestore().doc(`users/${userId}/userData/settings`); const todoDocRef = admin.firestore().doc(`users/${userId}/todoLists/${todoId}`); @@ -56,6 +56,8 @@ export const sendPushNotification = onTaskDispatched({ const todoData = todoDoc.data(); if (!todoDoc.exists || !todoData || todoData.isCompleted === true) { return; } + const todoCategory = typeof todoData.category === "string" ? todoData.category.trim() : ""; + if (!todoCategory) { return; } const timeZone = resolveTimeZone(settingsData); diff --git a/Firebase/functions/src/index.ts b/Firebase/functions/src/index.ts index fb12553b..bc543223 100644 --- a/Firebase/functions/src/index.ts +++ b/Firebase/functions/src/index.ts @@ -34,9 +34,18 @@ import { import { removeTodoNotificationDocuments, - removeCompletedTodoReceipts, - removeStaleTodoReceipts -} from "./todo/remove"; + removeCompletedTodoNotificationRecords, + cleanupUnusedTodoNotificationRecords +} from "./todo/cleanup"; + +import { + syncTodoNotificationCategory +} from "./todo/update"; + +import { + requestMoveRemovedCategoryTodosToEtc, + completeMoveRemovedCategoryTodosToEtc +} from "./todoCategory/update"; import { requestTodoDeletion, @@ -94,8 +103,11 @@ export { export { removeTodoNotificationDocuments, - removeCompletedTodoReceipts, - removeStaleTodoReceipts, + removeCompletedTodoNotificationRecords, + cleanupUnusedTodoNotificationRecords, + syncTodoNotificationCategory, + requestMoveRemovedCategoryTodosToEtc, + completeMoveRemovedCategoryTodosToEtc, requestTodoDeletion, undoTodoDeletion, completeTodoDeletion, diff --git a/Firebase/functions/src/todo/remove.ts b/Firebase/functions/src/todo/cleanup.ts similarity index 92% rename from Firebase/functions/src/todo/remove.ts rename to Firebase/functions/src/todo/cleanup.ts index 45745ded..881a0173 100644 --- a/Firebase/functions/src/todo/remove.ts +++ b/Firebase/functions/src/todo/cleanup.ts @@ -28,7 +28,7 @@ export const removeTodoNotificationDocuments = onDocumentDeleted({ } ); -export const removeCompletedTodoReceipts = onDocumentUpdated({ +export const removeCompletedTodoNotificationRecords = onDocumentUpdated({ document: "users/{userId}/todoLists/{todoId}", region: LOCATION }, @@ -54,7 +54,7 @@ export const removeCompletedTodoReceipts = onDocumentUpdated({ try { await deleteByTodoId(userId, "notificationReceipts", todoId); } catch (error) { - logger.error("완료된 todo의 notificationReceipts 정리 실패", { + logger.error("완료된 todo의 notification record 정리 실패", { userId, todoId, error @@ -63,7 +63,7 @@ export const removeCompletedTodoReceipts = onDocumentUpdated({ } ); -export const removeStaleTodoReceipts = onSchedule({ +export const cleanupUnusedTodoNotificationRecords = onSchedule({ region: LOCATION, schedule: "0 * * * *", timeZone: "UTC" @@ -99,7 +99,7 @@ export const removeStaleTodoReceipts = onSchedule({ lastExpiredCompletedTodo = snapshot.docs[snapshot.docs.length - 1]; } } catch (error) { - logger.error("지난 마감일의 완료된 todo receipt 정리 실패", { error }); + logger.error("지난 마감일의 완료된 todo notification record 정리 실패", { error }); } try { @@ -131,7 +131,7 @@ export const removeStaleTodoReceipts = onSchedule({ lastTodoWithoutDueDate = snapshot.docs[snapshot.docs.length - 1]; } } catch (error) { - logger.error("마감일이 없는 todo receipt 정리 실패", { error }); + logger.error("마감일이 없는 todo notification record 정리 실패", { error }); } } ); diff --git a/Firebase/functions/src/todo/update.ts b/Firebase/functions/src/todo/update.ts new file mode 100644 index 00000000..930869be --- /dev/null +++ b/Firebase/functions/src/todo/update.ts @@ -0,0 +1,131 @@ +import { onDocumentUpdated } from "firebase-functions/v2/firestore"; +import * as admin from "firebase-admin"; +import * as logger from "firebase-functions/logger"; +import { normalizeError } from "../common/error"; + +const LOCATION = "asia-northeast3"; +const BATCH_SIZE = 200; + +export const syncTodoNotificationCategory = onDocumentUpdated({ + document: "users/{userId}/todoLists/{todoId}", + region: LOCATION + }, + async (event) => { + const beforeData = event.data?.before.data(); + const afterData = event.data?.after.data(); + const userId = event.params.userId; + const todoId = event.params.todoId; + + const beforeCategory = typeof beforeData?.category === "string" ? beforeData.category.trim() : ""; + const afterCategory = typeof afterData?.category === "string" ? afterData.category.trim() : ""; + + if (!beforeCategory || !afterCategory || beforeCategory == afterCategory) { + return; + } + + try { + await Promise.all([ + updateNotifications(userId, todoId, afterCategory), + updateNotificationTasks(userId, todoId, afterCategory) + ]); + } catch (error) { + logger.error("todo 카테고리 변경 후 알림 데이터 동기화 실패", { + userId, + todoId, + beforeCategory, + afterCategory, + error: normalizeError(error) + }); + throw error; + } + } +); + +async function updateNotifications( + userId: string, + todoId: string, + todoCategory: string +): Promise { + await updateNotificationBatch(userId, todoId, todoCategory) +} + +async function updateNotificationTasks( + userId: string, + todoId: string, + todoCategory: string +): Promise { + await updateNotificationTaskBatch(userId, todoId, todoCategory) +} + +async function updateNotificationBatch( + userId: string, + todoId: string, + todoCategory: string, + lastDocument?: + FirebaseFirestore.QueryDocumentSnapshot +): Promise { + let query = admin.firestore() + .collection(`users/${userId}/notifications`) + .where("todoId", "==", todoId) + .orderBy(admin.firestore.FieldPath.documentId()) + .limit(BATCH_SIZE); + + if (lastDocument) { + query = query.startAfter(lastDocument); + } + + const snapshot = await query.get(); + if (snapshot.empty) { return; } + + const batch = admin.firestore().batch(); + snapshot.docs.forEach((document) => { + batch.update(document.ref, { todoCategory }); + }); + await batch.commit(); + + if (snapshot.size < BATCH_SIZE) { return; } + + await updateNotificationBatch( + userId, + todoId, + todoCategory, + snapshot.docs[snapshot.docs.length - 1] + ); +} + +async function updateNotificationTaskBatch( + userId: string, + todoId: string, + todoCategory: string, + lastDocument?: + FirebaseFirestore.QueryDocumentSnapshot +): Promise { + let query = admin.firestore() + .collection("notificationTasks") + .where("userId", "==", userId) + .where("todoId", "==", todoId) + .orderBy(admin.firestore.FieldPath.documentId()) + .limit(BATCH_SIZE); + + if (lastDocument) { + query = query.startAfter(lastDocument); + } + + const snapshot = await query.get(); + if (snapshot.empty) { return; } + + const batch = admin.firestore().batch(); + snapshot.docs.forEach((document) => { + batch.update(document.ref, { todoCategory }); + }); + await batch.commit(); + + if (snapshot.size < BATCH_SIZE) { return; } + + await updateNotificationTaskBatch( + userId, + todoId, + todoCategory, + snapshot.docs[snapshot.docs.length - 1] + ); +} diff --git a/Firebase/functions/src/todoCategory/update.ts b/Firebase/functions/src/todoCategory/update.ts new file mode 100644 index 00000000..1aee0eb6 --- /dev/null +++ b/Firebase/functions/src/todoCategory/update.ts @@ -0,0 +1,183 @@ +import { onDocumentUpdated } from "firebase-functions/v2/firestore"; +import { onTaskDispatched } from "firebase-functions/v2/tasks"; +import { getFunctions } from "firebase-admin/functions"; +import * as admin from "firebase-admin"; +import * as logger from "firebase-functions/logger"; +import { normalizeError } from "../common/error"; + +const LOCATION = "asia-northeast3"; +const BATCH_SIZE = 200; +const ETC_CATEGORY = "etc"; + +type CategoryItem = { + kind?: unknown; + id?: unknown; +}; + +type TodoCategoryUpdateTaskData = { + userId: string; + id: string; + createdAt?: FirebaseFirestore.Timestamp | Date | null; +}; + +export const requestMoveRemovedCategoryTodosToEtc = onDocumentUpdated({ + document: "users/{userId}/userData/categories", + region: LOCATION + }, + async (event) => { + const userId = event.params.userId; + const beforeData = event.data?.before.data(); + const afterData = event.data?.after.data(); + + if (!beforeData || !afterData) { return; } + + const beforeItems = Array.isArray(beforeData.items) ? beforeData.items as CategoryItem[] : []; + const afterItems = Array.isArray(afterData.items) ? afterData.items as CategoryItem[] : []; + const removedIDs = getRemovedIDs(beforeItems, afterItems); + + if (removedIDs.length === 0) { return; } + + try { + const queue = getFunctions().taskQueue( + `locations/${LOCATION}/functions/completeMoveRemovedCategoryTodosToEtc` + ); + + for (const id of removedIDs) { + const taskRef = admin.firestore().collection("todoCategoryUpdateTasks").doc(); + const taskData = { + userId, + id, + createdAt: admin.firestore.FieldValue.serverTimestamp() + }; + + try { + await taskRef.set(taskData); + await queue.enqueue({ taskId: taskRef.id }); + } catch (error) { + try { + await taskRef.delete(); + } catch (cleanupError) { + logger.warn("todoCategoryUpdateTasks 정리 실패", { + userId, + id, + taskId: taskRef.id, + error: normalizeError(cleanupError) + }); + } + + throw error; + } + } + } catch (error) { + logger.error("삭제된 사용자 카테고리 todo 정리 요청 실패", { + userId, + removedIDs, + error: normalizeError(error) + }); + throw error; + } + } +); + +export const completeMoveRemovedCategoryTodosToEtc = onTaskDispatched({ + region: LOCATION, + retryConfig: { maxAttempts: 3, minBackoffSeconds: 5 }, + rateLimits: { maxDispatchesPerSecond: 20 }, + }, + async (request) => { + const taskId = typeof request.data?.taskId === "string" ? request.data.taskId.trim() : ""; + if (!taskId) { + logger.warn("유효하지 않은 카테고리 정리 payload", request.data); + return; + } + + const taskRef = admin.firestore().collection("todoCategoryUpdateTasks").doc(taskId); + const taskSnapshot = await taskRef.get(); + if (!taskSnapshot.exists) { return; } + + const taskData = taskSnapshot.data() as TodoCategoryUpdateTaskData | undefined; + const userId = typeof taskData?.userId === "string" ? taskData.userId : ""; + const id = typeof taskData?.id === "string" ? taskData.id : ""; + + if (!userId || !id) { + logger.warn("todoCategoryUpdateTasks 문서 형식이 올바르지 않습니다.", { taskId }); + return; + } + + try { + await updateTodos(userId, id); + await taskRef.delete(); + } catch (error) { + logger.error("삭제된 사용자 카테고리 todo 정리 실패", { + userId, + id, + taskId, + error: normalizeError(error) + }); + throw error; + } + } +); + +function getRemovedIDs( + beforeItems: CategoryItem[], + afterItems: CategoryItem[] +): string[] { + const beforeIDs = new Set( + beforeItems.flatMap((item) => { + if (item.kind !== "user") { return []; } + return typeof item.id === "string" ? [item.id] : []; + }) + ); + const afterIDs = new Set( + afterItems.flatMap((item) => { + if (item.kind !== "user") { return []; } + return typeof item.id === "string" ? [item.id] : []; + }) + ); + + return Array.from(beforeIDs).filter((id) => !afterIDs.has(id)); +} + +async function updateTodos( + userId: string, + id: string +): Promise { + await updateTodoBatch(userId, id) +} + +async function updateTodoBatch( + userId: string, + id: string, + lastDocument?: + FirebaseFirestore.QueryDocumentSnapshot +): Promise { + let query = admin.firestore() + .collection(`users/${userId}/todoLists`) + .where("category", "==", id) + .orderBy(admin.firestore.FieldPath.documentId()) + .limit(BATCH_SIZE); + + if (lastDocument) { + query = query.startAfter(lastDocument); + } + + const snapshot = await query.get(); + if (snapshot.empty) { return; } + + const batch = admin.firestore().batch(); + snapshot.docs.forEach((document) => { + batch.update(document.ref, { + category: ETC_CATEGORY + }); + }); + await batch.commit(); + + if (snapshot.size < BATCH_SIZE) { return; } + + await updateTodoBatch( + userId, + id, + snapshot.docs[snapshot.docs.length - 1] + ); +} diff --git a/Firebase/package-lock.json b/Firebase/package-lock.json new file mode 100644 index 00000000..931089a8 --- /dev/null +++ b/Firebase/package-lock.json @@ -0,0 +1,3129 @@ +{ + "name": "Firebase", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "firebase-functions": "^7.2.2" + } + }, + "node_modules/@fastify/busboy": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.2.0.tgz", + "integrity": "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==", + "license": "MIT", + "peer": true + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz", + "integrity": "sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/@firebase/app-types": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz", + "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz", + "integrity": "sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/@firebase/component": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.2.tgz", + "integrity": "sha512-iyVDGc6Vjx7Rm0cAdccLH/NG6fADsgJak/XW9IA2lPf8AjIlsemOpFGKczYyPHxm4rnKdR8z6sK4+KEC7NwmEg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@firebase/util": "1.15.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.1.2.tgz", + "integrity": "sha512-lP96CMjMPy/+d1d9qaaHjHHdzdwvEOuyyLq9ehX89e2XMKwS1jHNzYBO+42bdSumuj5ukPbmnFtViZu8YOMT+w==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.2", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.15.0", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.1.2.tgz", + "integrity": "sha512-j4A6IhVZbgxAzT6gJJC2PfOxYCK9SrDrUO7nTM4EscTYtKkAkzsbKoCnDdjFapQfnsncvPWjqVTr/0PffUwg3g==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@firebase/component": "0.7.2", + "@firebase/database": "1.1.2", + "@firebase/database-types": "1.0.18", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.15.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "1.0.18", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.18.tgz", + "integrity": "sha512-yOY8IC2go9lfbVDMiy2ATun4EB2AFwocPaQADwMN/RHRUAZSM4rlAV7PGbWPSG/YhkJ2A9xQAiAENgSua9G5Fg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@firebase/app-types": "0.9.3", + "@firebase/util": "1.15.0" + } + }, + "node_modules/@firebase/logger": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.5.0.tgz", + "integrity": "sha512-cGskaAvkrnh42b3BA3doDWeBmuHFO/Mx5A83rbRDYakPjO9bJtRL3dX7javzc2Rr/JHZf4HlterTW2lUkfeN4g==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/util": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.15.0.tgz", + "integrity": "sha512-AmWf3cHAOMbrCPG4xdPKQaj5iHnyYfyLKZxwz+Xf55bqKbpAmcYifB4jQinT2W9XhDRHISOoPyBOariJpCG6FA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@google-cloud/firestore": { + "version": "7.11.6", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-7.11.6.tgz", + "integrity": "sha512-EW/O8ktzwLfyWBOsNuhRoMi8lrC3clHM5LVFhGvO1HCsLozCOOXRAlHrYBoE6HL42Sc8yYMuCb2XqcnJ4OOEpw==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@opentelemetry/api": "^1.3.0", + "fast-deep-equal": "^3.1.1", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^4.3.3", + "protobufjs": "^7.2.6" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/paginator": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.2.tgz", + "integrity": "sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/projectify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz", + "integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/promisify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.0.0.tgz", + "integrity": "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.19.0.tgz", + "integrity": "sha512-n2FjE7NAOYyshogdc7KQOl/VZb4sneqPjWouSyia9CMDdMhRX5+RIbqalNmC7LOLzuLAN89VlF2HvG8na9G+zQ==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@google-cloud/paginator": "^5.0.0", + "@google-cloud/projectify": "^4.0.0", + "@google-cloud/promisify": "<4.1.0", + "abort-controller": "^3.0.0", + "async-retry": "^1.3.3", + "duplexify": "^4.1.3", + "fast-xml-parser": "^5.3.4", + "gaxios": "^6.0.2", + "google-auth-library": "^9.6.3", + "html-entities": "^2.5.2", + "mime": "^3.0.0", + "p-limit": "^3.0.1", + "retry-request": "^7.0.0", + "teeny-request": "^9.0.0", + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage/node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage/node_modules/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage/node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", + "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", + "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.3", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "optional": true, + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/caseless": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", + "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT" + }, + "node_modules/@types/request": { + "version": "2.48.13", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.13.tgz", + "integrity": "sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.5" + } + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "retry": "0.13.1" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT", + "peer": true + }, + "node_modules/farmhash-modern": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/farmhash-modern/-/farmhash-modern-1.1.0.tgz", + "integrity": "sha512-6ypT4XfgqJk/F3Yuv4SX26I3doUjt0GTG4a+JgWxXQpxXzTBq8fPUeGHfcYMMDPHJHm3yPOSjaeBwBGAHWXCdA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT", + "peer": true + }, + "node_modules/fast-xml-builder": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", + "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "path-expression-matcher": "^1.1.3" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.5.9", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.9.tgz", + "integrity": "sha512-jldvxr1MC6rtiZKgrFnDSvT8xuH+eJqxqOBThUVjYrxssYTo1avZLGql5l0a0BAERR01CadYzZ83kVEkbyDg+g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "fast-xml-builder": "^1.1.4", + "path-expression-matcher": "^1.2.0", + "strnum": "^2.2.2" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-admin": { + "version": "13.7.0", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-13.7.0.tgz", + "integrity": "sha512-o3qS8zCJbApe7aKzkO2Pa380t9cHISqeSd3blqYTtOuUUUua3qZTLwNWgGUOss3td6wbzrZhiHIj3c8+fC046Q==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@fastify/busboy": "^3.0.0", + "@firebase/database-compat": "^2.0.0", + "@firebase/database-types": "^1.0.6", + "farmhash-modern": "^1.1.0", + "fast-deep-equal": "^3.1.1", + "google-auth-library": "^10.6.1", + "jsonwebtoken": "^9.0.0", + "jwks-rsa": "^3.1.0", + "node-forge": "^1.3.1", + "uuid": "^11.0.2" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@google-cloud/firestore": "^7.11.0", + "@google-cloud/storage": "^7.19.0" + } + }, + "node_modules/firebase-functions": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-7.2.2.tgz", + "integrity": "sha512-fWFVI+4weuaat+Fp+4xYY1T+omiTvya8fW79+edgLWCOaDEBSBNlfhstnt+K1esblscZlJf8v+IA0LsCG8Uf1Q==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.5", + "@types/express": "^4.17.21", + "cors": "^2.8.5", + "express": "^4.21.0", + "protobufjs": "^7.2.2" + }, + "bin": { + "firebase-functions": "lib/bin/firebase-functions.js" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@apollo/server": "^5.2.0", + "@as-integrations/express4": "^1.1.2", + "firebase-admin": "^11.10.0 || ^12.0.0 || ^13.0.0", + "graphql": "^16.12.0" + }, + "peerDependenciesMeta": { + "@apollo/server": { + "optional": true + }, + "@as-integrations/express4": { + "optional": true + }, + "graphql": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", + "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "peer": true, + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gaxios/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata/node_modules/gaxios": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", + "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "peer": true, + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "optional": true, + "peer": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/google-auth-library": { + "version": "10.6.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", + "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-auth-library/node_modules/gaxios": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", + "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-auth-library/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "peer": true, + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/google-gax": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.6.1.tgz", + "integrity": "sha512-V6eky/xz2mcKfAd1Ioxyd6nmA61gao3n01C+YeuIwu3vzM9EDR6wcVzMSIbLMDXWeoi9SHYctXuKYC5uJUT3eQ==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@grpc/grpc-js": "^1.10.9", + "@grpc/proto-loader": "^0.7.13", + "@types/long": "^4.0.0", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "google-auth-library": "^9.3.0", + "node-fetch": "^2.7.0", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^2.0.2", + "protobufjs": "^7.3.2", + "retry-request": "^7.0.0", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax/node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax/node_modules/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax/node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "license": "MIT", + "peer": true + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "peer": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "peer": true + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "peer": true, + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "peer": true + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "peer": true, + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jwks-rsa": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.2.2.tgz", + "integrity": "sha512-BqTyEDV+lS8F2trk3A+qJnxV5Q9EqKCBJOPti3W97r7qTympCZjb7h2X6f2kc+0K3rsSTY1/6YG2eaXKoj497w==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/jsonwebtoken": "^9.0.4", + "debug": "^4.3.4", + "jose": "^4.15.4", + "limiter": "^1.1.5", + "lru-memoizer": "^2.2.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/jwks-rsa/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/jwks-rsa/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "peer": true + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "peer": true, + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==", + "peer": true + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "license": "MIT", + "peer": true + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT", + "peer": true + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT", + "peer": true + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT", + "peer": true + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT", + "peer": true + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT", + "peer": true + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT", + "peer": true + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT", + "peer": true + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "peer": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lru-memoizer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz", + "integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==", + "license": "MIT", + "peer": true, + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "6.0.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-forge": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", + "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", + "license": "(BSD-3-Clause OR GPL-2.0)", + "peer": true, + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-expression-matcher": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.2.0.tgz", + "integrity": "sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/proto3-json-serializer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz", + "integrity": "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "protobufjs": "^7.2.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/retry-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz", + "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@types/request": "^2.48.8", + "extend": "^3.0.2", + "teeny-request": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strnum": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.2.tgz", + "integrity": "sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/teeny-request": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", + "integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.9", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/teeny-request/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/teeny-request/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/teeny-request/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/teeny-request/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/teeny-request/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "peer": true + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "peer": true, + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause", + "optional": true, + "peer": true + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC", + "optional": true, + "peer": true + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "optional": true, + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "peer": true + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "optional": true, + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/Firebase/package.json b/Firebase/package.json new file mode 100644 index 00000000..7bea15f5 --- /dev/null +++ b/Firebase/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "firebase-functions": "^7.2.2" + } +}