diff --git a/OutlineKit/Sources/OutlineKit/Caching/CachingOutlineAPIClient.swift b/OutlineKit/Sources/OutlineKit/Caching/CachingOutlineAPIClient.swift index 9ec88a5..4f59bf3 100644 --- a/OutlineKit/Sources/OutlineKit/Caching/CachingOutlineAPIClient.swift +++ b/OutlineKit/Sources/OutlineKit/Caching/CachingOutlineAPIClient.swift @@ -343,10 +343,18 @@ public actor CachingOutlineAPIClient: OutlineAPIClient { try await live.uploadAttachmentFile(result, fileData: fileData) } + public func deleteAttachment(id: String) async throws { + try await live.deleteAttachment(id: id) + } + public func updateUserAvatar(_ request: UpdateUserAvatarRequest) async throws -> OutlineUser { try await live.updateUserAvatar(request) } + public func updateUserName(_ request: UpdateUserNameRequest) async throws -> OutlineUser { + try await live.updateUserName(request) + } + // MARK: - Sync management (Settings surface) public func pendingOperations() async -> [PendingOperationSummary] { diff --git a/OutlineKit/Sources/OutlineKit/Core/OutlineAPIClient.swift b/OutlineKit/Sources/OutlineKit/Core/OutlineAPIClient.swift index 53307ca..21f18d9 100644 --- a/OutlineKit/Sources/OutlineKit/Core/OutlineAPIClient.swift +++ b/OutlineKit/Sources/OutlineKit/Core/OutlineAPIClient.swift @@ -75,6 +75,12 @@ public protocol OutlineAPIClient: Sendable { /// target. See `OutlineAttachment`/`CreateAttachmentResult`. func createAttachment(_ request: CreateAttachmentRequest) async throws -> CreateAttachmentResult func uploadAttachmentFile(_ result: CreateAttachmentResult, fileData: Data) async throws + /// Best-effort — matches the shape every other simple `id`-only delete + /// in this API uses (`pins.delete`, `stars.delete`, …), not confirmed + /// against a live server specifically for attachments yet. + func deleteAttachment(id: String) async throws /// `users.update`, avatar only. See `UpdateUserAvatarRequest`. func updateUserAvatar(_ request: UpdateUserAvatarRequest) async throws -> OutlineUser + /// `users.update`, name only. See `UpdateUserNameRequest`. + func updateUserName(_ request: UpdateUserNameRequest) async throws -> OutlineUser } diff --git a/OutlineKit/Sources/OutlineKit/LiveOutlineAPIClient.swift b/OutlineKit/Sources/OutlineKit/LiveOutlineAPIClient.swift index 579d744..444a11c 100644 --- a/OutlineKit/Sources/OutlineKit/LiveOutlineAPIClient.swift +++ b/OutlineKit/Sources/OutlineKit/LiveOutlineAPIClient.swift @@ -278,10 +278,18 @@ public actor LiveOutlineAPIClient: OutlineAPIClient { } } + public func deleteAttachment(id: String) async throws { + try await postForSuccess("attachments.delete", body: StarIDParams(id: id)) + } + public func updateUserAvatar(_ request: UpdateUserAvatarRequest) async throws -> OutlineUser { try await post("users.update", body: request) } + public func updateUserName(_ request: UpdateUserNameRequest) async throws -> OutlineUser { + try await post("users.update", body: request) + } + private func post(_ path: String, body: Body) async throws -> Response { guard let token = try? tokenStore.token() else { throw OutlineAPIError.tokenUnavailable diff --git a/OutlineKit/Sources/OutlineKit/Requests/UpdateUserNameRequest.swift b/OutlineKit/Sources/OutlineKit/Requests/UpdateUserNameRequest.swift new file mode 100644 index 0000000..5857409 --- /dev/null +++ b/OutlineKit/Sources/OutlineKit/Requests/UpdateUserNameRequest.swift @@ -0,0 +1,12 @@ +import Foundation + +/// `users.update`, name only. +public struct UpdateUserNameRequest: Encodable, Sendable { + public let id: String + public let name: String + + public init(id: String, name: String) { + self.id = id + self.name = name + } +} diff --git a/OutlineKit/Tests/OutlineKitTests/CachingOutlineAPIClientTests.swift b/OutlineKit/Tests/OutlineKitTests/CachingOutlineAPIClientTests.swift index 339efa8..8256a1d 100644 --- a/OutlineKit/Tests/OutlineKitTests/CachingOutlineAPIClientTests.swift +++ b/OutlineKit/Tests/OutlineKitTests/CachingOutlineAPIClientTests.swift @@ -91,7 +91,9 @@ private final class StubOutlineAPIClient: OutlineAPIClient, @unchecked Sendable func currentUser() async throws -> OutlineUser { throw NotStubbed() } func createAttachment(_ request: CreateAttachmentRequest) async throws -> CreateAttachmentResult { throw NotStubbed() } func uploadAttachmentFile(_ result: CreateAttachmentResult, fileData: Data) async throws { throw NotStubbed() } + func deleteAttachment(id: String) async throws { throw NotStubbed() } func updateUserAvatar(_ request: UpdateUserAvatarRequest) async throws -> OutlineUser { throw NotStubbed() } + func updateUserName(_ request: UpdateUserNameRequest) async throws -> OutlineUser { throw NotStubbed() } } private struct StubTransportError: Error {} diff --git a/OutlineKit/Tests/OutlineKitTests/LiveOutlineAPIClientTests.swift b/OutlineKit/Tests/OutlineKitTests/LiveOutlineAPIClientTests.swift index 74a6aad..3627531 100644 --- a/OutlineKit/Tests/OutlineKitTests/LiveOutlineAPIClientTests.swift +++ b/OutlineKit/Tests/OutlineKitTests/LiveOutlineAPIClientTests.swift @@ -1051,6 +1051,46 @@ final class LiveOutlineAPIClientTests: XCTestCase { XCTAssertTrue(sentJSON.contains("\"id\":\"user-1\"")) } + func testUpdateUserNameSendsRequestAndDecodesResult() async throws { + let httpClient = MockHTTPClient() + httpClient.responseData = """ + { + "data": { + "id": "user-1", + "name": "New Name" + } + } + """.data(using: .utf8)! + + let client = LiveOutlineAPIClient( + configuration: OutlineConfiguration(baseURL: URL(string: "https://outline.example.com")!), + tokenStore: StaticTokenStore(), + httpClient: httpClient + ) + + let user = try await client.updateUserName(UpdateUserNameRequest(id: "user-1", name: "New Name")) + + XCTAssertEqual(user.name, "New Name") + XCTAssertEqual(httpClient.lastRequest?.url?.path, "/api/users.update") + } + + func testDeleteAttachmentSendsRequest() async throws { + let httpClient = MockHTTPClient() + httpClient.responseData = """ + { "success": true } + """.data(using: .utf8)! + + let client = LiveOutlineAPIClient( + configuration: OutlineConfiguration(baseURL: URL(string: "https://outline.example.com")!), + tokenStore: StaticTokenStore(), + httpClient: httpClient + ) + + try await client.deleteAttachment(id: "attach-1") + + XCTAssertEqual(httpClient.lastRequest?.url?.path, "/api/attachments.delete") + } + func testCreateAttachmentDecodesUploadTargetAndFormFields() async throws { let httpClient = MockHTTPClient() httpClient.responseData = """