MistKit

MistKit

Swift Package,用于服务器端和命令行访问 CloudKit Web 服务

SwiftPM Twitter GitHub GitHub issues

macOS ubuntu Travis (.com) Bitrise CircleCI

Codecov CodeFactor Grade codebeat badge Code Climate maintainability Code Climate technical debt Code Climate issues Reviewed by Hound

Demonstration of MistKit via Command-Line App mistdemoc

目录

简介

这个 Swift 包使用的不是 CloudKit 框架,而是 CloudKit Web 服务。 为什么?

... 还有更多

在我的例子中,我正在将它用于我的 Apple Watch 应用程序 HeartwitchVapor 后端。这是一个示例代码,展示了如何使用 CloudKit 容器设置和使用 MistKit

演示示例

CloudKit 仪表板 Schema

Sample Schema for Todo List

使用 MistKit 的示例代码

// Example for pulling a todo list from CloudKit
import MistKit
import MistKitNIOHTTP1Token

// setup your connection to CloudKit
let connection = MKDatabaseConnection(
  container: "iCloud.com.brightdigit.MistDemo", 
  apiToken: "****", 
  environment: .development
)

// setup how to manager your user's web authentication token 
let manager = MKTokenManager(storage: MKUserDefaultsStorage(), client: MKNIOHTTP1TokenClient())

// setup your database manager
let database = MKDatabase(
  connection: connection,
  tokenManager: manager
)

// create your request to CloudKit
let query = MKQuery(recordType: TodoListItem.self)

let request = FetchRecordQueryRequest(
  database: .private, 
  query: FetchRecordQuery(query: query))

// handle the result
database.query(request) { result in
  dump(result)
}

// wait for query here...

要同步等待 CloudKit 查询完成,您可以使用 CFRunLoop

...
// handle the result
database.query(request) { result in
  dump(result)

  // nessecary if you need run this synchronously
  CFRunLoopStop(CFRunLoopGetMain())
}

// nessecary if you need run this synchronously
CFRunLoopRun()

特性

以下是该库当前已实现的功能

安装

Swift Package Manager 是 Apple 的去中心化依赖管理器,用于将库集成到您的 Swift 项目中。它现在与 Xcode 11 完全集成。

要使用 SPM 将 MistKit 集成到您的项目中,请在您的 Package.swift 文件中指定它

let package = Package(
  ...
  dependencies: [
    .package(url: "https://github.com/brightdigit/MistKit", from: "0.2.0")
  ],
  targets: [
      .target(
          name: "YourTarget",
          dependencies: ["MistKit", ...]),
      ...
  ]
)

如果您正在构建服务器端实现,还有 SwiftNIO 和 Vapor 的产品

      .target(
          name: "YourTarget",
          dependencies: ["MistKit", 
            .product(name: "MistKitNIO", package: "MistKit"),  // if you are building a server-side application
            .product(name: "MistKitVapor", package: "MistKit") // if you are building a Vapor application
            ...]
      ),

用法

构建 Web 服务请求

MistKit 需要使用以下属性设置连接

这是一个如何设置 MKDatabase 的示例

let connection = MKDatabaseConnection(
  container: options.container, 
  apiToken: options.apiKey, 
  environment: options.environment)

// setup your database manager
let database = MKDatabase(
  connection: connection,
  tokenManager: manager
)

在进行实际请求之前,您应该了解如何为 privateshared 数据库进行身份验证的请求。

设置认证请求

为了访问 privateshared 数据库,Cloud Web Services API 需要一个 Web 身份验证令牌。 为了让 MistKit 获取它,将设置一个 HTTP 服务器来侦听来自 CloudKit 的回调。

因此,当您设置 API 令牌时,请确保为 Sign-In Callback 设置一个 URL

CloudKit Dashboard

设置好之后,您可以设置一个 MKTokenManager

CloudKit Dashboard Callback

管理 Web 身份验证令牌

MKTokenManager 需要一个 MKTokenStorage 用于稍后存储令牌。 您可以使用以下几种实现

可选地,如果需要监听通过 MKTokenClient 进行的 Web 身份验证,MistKit 可以为您设置一个 Web 服务器:您可以使用以下几种实现

这是一个您如何设置 MKDatabase 的示例

let connection = MKDatabaseConnection(
  container: options.container, 
  apiToken: options.apiKey, 
  environment: options.environment
 )

// setup how to manager your user's web authentication token
let manager = MKTokenManager(
  // store the token in UserDefaults
  storage: MKUserDefaultsStorage(), 
  // setup an http server at localhost for port 7000
  client: MKNIOHTTP1TokenClient(bindTo: .ipAddress(host: "127.0.0.1", port: 7000))
)

// setup your database manager
let database = MKDatabase(
  connection: connection,
  tokenManager: manager
)
使用 MKNIOHTTP1TokenClient

如果您没有构建服务器端应用程序,您可以通过将 MistKitNIO 添加到您的包依赖项来使用 MKNIOHTTP1TokenClient

let package = Package(
  ...
  dependencies: [
    .package(url: "https://github.com/brightdigit/MistKit", .branch("main")
  ],
  targets: [
      .target(
          name: "YourTarget",
          dependencies: ["MistKit", "MistKitNIOHTTP1Token", ...]),
      ...
  ]
)

当由于身份验证失败导致请求失败时,MKNIOHTTP1TokenClient 将启动一个 HTTP 服务器来开始侦听 Web 身份验证令牌。 默认情况下,MKNIOHTTP1TokenClient 将仅打印 URL,但您可以覆盖 onRequestURL

public class MKNIOHTTP1TokenClient: MKTokenClient {
  
  public init(bindTo: BindTo, onRedirectURL : ((URL) -> Void)? = nil) {
    self.bindTo = bindTo
    self.onRedirectURL = onRedirectURL ?? {print($0)}
  }
  ...
}

CloudKit 和 Vapor

静态 Web 身份验证令牌

如果您可能已经有 webAuthenticationToken,您可以使用 MKStaticTokenManager。 这是一个 MKTokenManagerProtocol 的只读实现,它接受一个用于 webAuthenticationToken 的只读 String?

这是我在 Vapor 应用程序 Heartwitch 中使用的一些示例代码,用于从我的数据库中提取 webAuthenticationToken 并在创建 MKDatabase 实例时使用该令牌。

import MistKit
import MistKitVapor

extension Application {
  ...
  var cloudKitConnection: MKDatabaseConnection {
    MKDatabaseConnection(
      container: configuration.cloudkitContainer,
      apiToken: configuration.cloudkitAPIKey,
      environment: environment.cloudKitEnvironment
    )
  }

  func cloudKitDatabase(using client: Client, withWebAuthenticationToken webAuthenticationToken: String? = nil) -> MKDatabase<MKVaporClient> {
    MKDatabase(
      connection: cloudKitConnection,
      client: MKVaporClient(client: client),
      tokenManager: MKStaticTokenManager(token: webAuthenticationToken, client: nil)
    )
  }
}

struct DeviceController {

  func fetch(_ request: Request) throws -> EventLoopFuture<MKServerResponse<[DeviceResponseItem]>> {
    let user = try request.auth.require(User.self)
    let userID = try user.requireID()
    let token = user.$appleUsers.query(on: request.db).field(\.$webAuthenticationToken).first().map { $0?.webAuthenticationToken }

    let cloudKitDatabase: EventLoopFuture<MKDatabase> = token.map {
      request.application.cloudKitDatabase(using: request.client, withWebAuthenticationToken: $0)
    }
    
    let cloudKitRequest = FetchRecordQueryRequest(
      database: .private,
      query: FetchRecordQuery(query: query)
    )
    
    let newEntries = cloudKitDatabase.flatMap {
      let cloudKitResult = cloudKitDatabase.query(cloudKitRequest, on: request.eventLoop)
    }

    return newEntries.mistKitResponse()
  }
  
  ...
}

除了静态字符串,您还可以将令牌存储在会话或数据库中。

在数据库和会话中存储 Web 身份验证令牌

mistdemod 演示 Vapor 应用程序中,有一个示例展示了如何基于使用 MKVaporModelStorageMKVaporSessionStorage 的请求创建 MKDatabase

extension MKDatabase where HttpClient == MKVaporClient {
  init(request: Request) {
    let storage: MKTokenStorage
    if let user = request.auth.get(User.self) {
      storage = MKVaporModelStorage(model: user)
    } else {
      storage = MKVaporSessionStorage(session: request.session)
    }
    let manager = MKTokenManager(storage: storage, client: nil)

    let options = MistDemoDefaultConfiguration(apiKey: request.application.cloudKitAPIKey)
    let connection = MKDatabaseConnection(container: options.container, apiToken: options.apiKey, environment: options.environment)

    // use the webAuthenticationToken which is passed
    if let token = options.token {
      manager.webAuthenticationToken = token
    }

    self.init(connection: connection, factory: nil, client: MKVaporClient(client: request.client), tokenManager: manager)
  }
}

在这种情况下,对于 User 模型需要实现 MKModelStorable

final class User: Model, Content {
  ...

  @Field(key: "cloudKitToken")
  var cloudKitToken: String?
}

extension User: MKModelStorable {
  static var tokenKey: KeyPath<User, Field<String?>> = \User.$cloudKitToken
}

MKModelStorable 协议确保 Model 包含存储 Web 身份验证令牌所需的属性。

虽然命令行工具需要一个 MKTokenClient 来监听来自 CloudKit 的回调,但使用服务器端应用程序,您可以只添加一个 API 调用。 这是一个监听 ckWebAuthToken 并将其保存到 User 的示例

struct CloudKitController: RouteCollection {
  func token(_ request: Request) -> EventLoopFuture<HTTPStatus> {
    guard let token: String = request.query["ckWebAuthToken"] else {
      return request.eventLoop.makeSucceededFuture(.notFound)
    }

    guard let user = request.auth.get(User.self) else {
      request.cloudKitAPI.webAuthenticationToken = token
      return request.eventLoop.makeSucceededFuture(.accepted)
    }

    user.cloudKitToken = token
    return user.save(on: request.db).transform(to: .accepted)
  }

  func boot(routes: RoutesBuilder) throws {
    routes.get(["token"], use: token)
  }
}

如果您的应用程序已经使用了 Apple 现有的 CloudKit API,您还可以使用 CKFetchWebAuthTokenOperation 将 webAuthenticationToken 保存到您的数据库

使用查询获取记录 (records/query)

有两种方法来获取记录

设置查询

要获取为 MKAnyRecord,只需使用匹配的 recordType(即 schema name)创建 MKAnyQuery

// create your request to CloudKit
let query = MKAnyQuery(recordType: "TodoListItem")

let request = FetchRecordQueryRequest(
  database: .private,
  query: FetchRecordQuery(query: query)
)

// handle the result
database.perform(request: request) { result in
  do {
    try print(result.get().records.information)
  } catch {
    completed(error)
    return
  }
  completed(nil)
}

这将为您提供包含具有您的值的 fields 属性的 MKAnyRecord

public struct MKAnyRecord: Codable {
  public let recordType: String
  public let recordName: UUID?
  public let recordChangeTag: String?
  public let fields: [String: MKValue]
  ...

MKValue 类型是一个枚举,其中包含字段的类型和值。

强类型查询

为了对请求使用自定义类型,您需要实现 MKQueryRecord。 这是一个包含 title 属性的 todo 项的示例

public class TodoListItem: MKQueryRecord {
  // required property and methods for MKQueryRecord
  public static var recordType: String = "TodoItem"
  public static var desiredKeys: [String]? = ["title"]

  public let recordName: UUID?
  public let recordChangeTag: String?
  
  public required init(record: MKAnyRecord) throws {
    recordName = record.recordName
    recordChangeTag = record.recordChangeTag
    title = try record.string(fromKey: "title")
  }
  
  public var fields: [String: MKValue] {
    return ["title": .string(title)]
  }
  
  // custom fields and methods to `TodoListItem`
  public var title: String
  
  public init(title: String) {
    self.title = title
    recordName = nil
    recordChangeTag = nil
  }
}

现在,您可以使用您的自定义类型创建一个 MKQuery

// create your request to CloudKit
let query = MKQuery(recordType: TodoListItem.self)

let request = FetchRecordQueryRequest(
  database: .private,
  query: FetchRecordQuery(query: query)
)

// handle the result
database.query(request) { result in
  do {
    try print(result.get().information)
  } catch {
    completed(error)
    return
  }
  completed(nil)
}

不要使用 MKDatabase.perform(request:),而使用 MKDatabase.query(_ query:)MKDatabase 会将值解码为您的自定义类型。

过滤器

即将推出

通过记录名称获取记录 (records/lookup)

let recordNames : [UUID] = [...]

let query = LookupRecordQuery(TodoListItem.self, recordNames: recordNames)

let request = LookupRecordQueryRequest(database: .private, query: query)

database.lookup(request) { result in
  try? print(result.get().count)
}

即将推出

获取当前用户身份 (users/caller)

let request = GetCurrentUserIdentityRequest()
database.perform(request: request) { (result) in
  try? print(result.get().userRecordName)
}

即将推出

修改记录 (records/modify)

创建记录

let item = TodoListItem(title: title)

let operation = ModifyOperation(operationType: .create, record: item)

let query = ModifyRecordQuery(operations: [operation])

let request = ModifyRecordQueryRequest(database: .private, query: query)

database.perform(operations: request) { result in
  do {
    try print(result.get().updated.information)
  } catch {
    completed(error)
    return
  }
  completed(nil)
}

删除记录

为了删除和更新记录,您需要已经从 CloudKit 中获取了该对象。 因此,您需要运行 LookupRecordQueryRequestFetchRecordQueryRequest 才能访问该记录。 访问记录后,只需使用您的记录创建一个删除操作

let query = LookupRecordQuery(TodoListItem.self, recordNames: recordNames)

let request = LookupRecordQueryRequest(database: .private, query: query)

database.lookup(request) { result in
  let items: [TodoListItem]
  
  do {
    items = try result.get()
  } catch {
    completed(error)
    return
  }
  
  let operations = items.map { (item) in
    ModifyOperation(operationType: .delete, record: item)
  }

  let query = ModifyRecordQuery(operations: operations)

  let request = ModifyRecordQueryRequest(database: .private, query: query)
  
  database.perform(operations: request) { result in
    do {
      try print("Deleted \(result.get().deleted.count) items.")
    } catch {
      completed(error)
      return
    }
    completed(nil)
  }
}

更新记录

与更新记录类似,您需要已经从 CloudKit 中获取了该对象。 同样,运行 LookupRecordQueryRequestFetchRecordQueryRequest 才能访问该记录。 访问记录后,只需使用您的记录创建一个更新操作

let query = LookupRecordQuery(TodoListItem.self, recordNames: [recordName])

let request = LookupRecordQueryRequest(database: .private, query: query)

database.lookup(request) { result in
  let items: [TodoListItem]
  do {
    items = try result.get()

  } catch {
    completed(error)
    return
  }
  let operations = items.map { (item) -> ModifyOperation<TodoListItem> in
    item.title = self.newTitle
    return ModifyOperation(operationType: .update, record: item)
  }

  let query = ModifyRecordQuery(operations: operations)

  let request = ModifyRecordQueryRequest(database: .private, query: query)
  database.perform(operations: request) { result in
    do {
      try print("Updated \(result.get().updated.count) items.")
    } catch {
      completed(error)
      return
    }
    completed(nil)
  }
}

使用 SwiftNIO

如果您正在构建服务器端应用程序并已经在使用 SwiftNIO,您可能想要利用一些助手,它们将已经使用现有模式和可用的 API。 主要来自 SwiftNIOEventLoops 以及来自 SwiftNIOVapor 的相应 HTTP 客户端

使用 EventLoops

如果您正在 SwiftNIO (或 Vapor) 中构建服务器端应用程序,您可能正在使用 EventLoopsEventLoopFuture 进行异步编程。 EventLoopFutures 本质上是 SwiftNIO 的 Future/Promise 实现。 幸运的是,MistKit 中有一些助手方法提供 EventLoopFutures,类似于它们在 SwiftNIO 中实现的方式。 这些实现增强了已经存在的回调

public extension MKDatabase {
  func query<RecordType>(
    _ query: FetchRecordQueryRequest<MKQuery<RecordType>>,
    on eventLoop: EventLoop
  ) -> EventLoopFuture<[RecordType]>

  func perform<RecordType>(
    operations: ModifyRecordQueryRequest<RecordType>,
    on eventLoop: EventLoop
  ) -> EventLoopFuture<ModifiedRecordQueryResult<RecordType>>
  
  func lookup<RecordType>(
    _ lookup: LookupRecordQueryRequest<RecordType>,
    on eventLoop: EventLoop
  ) -> EventLoopFuture<[RecordType]>

  func perform<RequestType: MKRequest, ResponseType>(
    request: RequestType,
    on eventLoop: EventLoop
  ) -> EventLoopFuture<ResponseType> -> EventLoopFuture<ResponseType>
    where RequestType.Response == ResponseType
}

此外,如果您使用结果作为 Vapor HTTP 响应的 ContentMistKit 提供了一个 MKServerResponse 枚举类型,它区分了身份验证失败(带有重定向 URL)和实际成功。

public enum MKServerResponse<Success>: Codable where Success: Codable {
  public init(attemptRecoveryFrom error: Error) throws

  case failure(URL)
  case success(Success)
}

除了 EventLoopFuture,您还可以使用不同的 HTTP 客户端来调用 CloudKit Web 服务。

选择 HTTP 客户端

默认情况下,MistKit 使用 URLSession 通过 MKURLSessionClient 向 CloudKit Web 服务发出 HTTP 调用

public struct MKURLSessionClient: MKHttpClient {
  public init(session: URLSession) {
    self.session = session
  }

  public func request(withURL url: URL, data: Data?) -> MKURLRequest
}

但是,如果您正在使用 SwiftNIOVapor,使用它们的 HTTP 客户端进行这些调用更有意义

在 mistdemod 示例中,您可以看到如何使用 Vapor RequestRequestclient 属性创建 MKDatabase

extension MKDatabase where HttpClient == MKVaporClient {
  init(request: Request) {
    let manager: MKTokenManager    
    let connection : MKDatabaseConnection
    self.init(
      connection: connection, 
      factory: nil, 
      client: MKVaporClient(client: request.client), 
      tokenManager: manager
    )
  }
}

示例

这里有两个关于如何通过 MistKit 在 CloudKit 中执行基本 CRUD 方法的示例

更多代码文档

文档在这里

路线图

0.1.0

0.2.0

0.4.0

0.6.0

0.8.0

0.9.0

v1.0.0

v1.x.x+

未计划

许可证

此代码根据 MIT 许可证分发。 有关更多信息,请参阅 LICENSE 文件。