ParseServerSwift

Documentation Tutorial Build Status CI release codecov License


用 Swift 编写 Parse Cloud Code!

什么是 Cloud Code?对于复杂的应用程序,有时您只需要不在移动设备上运行的逻辑。Cloud Code 使这成为可能。ParseServerSwift 中的 Cloud Code 易于使用,因为它使用 Parse-SwiftOGVapor 构建。与传统的在 Node.js parse-server 上运行的 基于 JS 的 Cloud Code 相比,ParseServerSwift 提供了许多额外的优势

从技术上讲,可以使用 ParseServerSwift 编写完整的应用程序,唯一的区别是此代码在您的 ParseServerSwift 中运行,而不是在用户的移动设备上运行。当您更新 Cloud Code 时,它会立即对所有移动环境可用。您不必等待应用程序的新版本发布。这使您可以动态更改应用程序行为并更快地添加新功能。

使用 ParseServerSwift 创建您的 Cloud Code 应用程序

按照 说明设置 Vapor 项目,以在 macOS 或 Linux 上安装和设置您的项目。

然后将 ParseServerSwift 添加到 Package.swift 文件中的 dependencies

// swift-tools-version:5.6
import PackageDescription

let package = Package(
    name: "YOUR_PROJECT_NAME",
    platforms: [
        .iOS(.v13),
        .macCatalyst(.v13),
        .macOS(.v10_15),
        .tvOS(.v13),
        .watchOS(.v6)
    ],
    products: [
            .library(name: "YOUR_PROJECT_NAME", targets: ["YOUR_PROJECT_NAME"])
    ],
    dependencies: [
        .package(url: "https://github.com/netreconlab/ParseServerSwift", .upToNextMajor(from: "0.8.4")),
        .package(url: "https://github.com/vapor/vapor.git", .upToNextMajor(from: "4.76.2")),
        .package(url: "https://github.com/netreconlab/Parse-Swift.git", .upToNextMajor(from: "5.7.0"))
    ]
    ...
    targets: [
        .target(
            name: "YOUR_PROJECT_NAME",
            dependencies: [
                .product(name: "Vapor", package: "vapor"),
                .product(name: "ParseSwift", package: "Parse-Swift"),
                .product(name: "ParseServerSwift", package: "ParseServerSwift"),
            ]
        ),
        .executableTarget(name: "App",
                          dependencies: [.target(name: "YOUR_PROJECT_NAME")],
                          swiftSettings: [
                              // Enable better optimizations when building in Release configuration. Despite the use of
                              // the `.unsafeFlags` construct required by SwiftPM, this flag is recommended for Release
                              // builds. See <https://github.com/swift-server/guides/blob/main/docs/building.md#building-for-production> for details.
                              .unsafeFlags(["-cross-module-optimization"], .when(configuration: .release))
                          ]),
        .testTarget(name: "YOUR_PROJECT_NAMETests", dependencies: [
            .target(name: "YOUR_PROJECT_NAME"),
            .product(name: "XCTVapor", package: "vapor"),
        ])
    ]
)

添加 ParseServerSwift 将允许您快速为 Parse Cloud Hook 函数和触发器添加路由。

配置 ParseServerSwift 以连接到您的 Parse 服务器

环境变量

以下环境变量可用,可以直接配置,也可以通过 .env.env.production 等配置。有关更多详细信息,请参阅 Vapor 文档

PARSE_SERVER_SWIFT_HOST_NAME: cloud-code # The name of your host. If you are running in Docker it should be same name as the docker service
PARSE_SERVER_SWIFT_PORT: # This is the default port on the docker image
PARSE_SERVER_SWIFT_DEFAULT_MAX_BODY_SIZE: 500kb # Set the default size for bodies that are collected into memory before calling your handlers (See Vapor docs for more details)
PARSE_SERVER_SWIFT_URLS: http://parse:1337/parse # (Required) Specify one of your Parse Servers to connect to. Can connect to multiple by seperating URLs with commas
PARSE_SERVER_SWIFT_APPLICATION_ID: appId # (Required) The application id of your Parse Server
PARSE_SERVER_SWIFT_PRIMARY_KEY: primaryKey # (Required) The master key of your Parse Server 
PARSE_SERVER_SWIFT_WEBHOOK_KEY: webookKey # The webhookKey of your Parse Server

WebhookKey

webhookKey 应与 Parse Server 上的 webhookKey 匹配。

Parse-SwiftOG SDK

上述环境变量自动配置 Parse-SwiftOG SDK。如果您需要更自定义的配置,请参阅 文档

初始化 ParseSwiftServer

要利用上述环境变量,您应该修改项目中的 entrypoint.swift,使其看起来类似于下面

import Vapor
import Dispatch
import Logging
import NIOCore
import NIOPosix
import ParseServerSwift

@main
enum Entrypoint {
    static func main() async throws {
        var env = try Environment.detect()
        try LoggingSystem.bootstrap(from: &env)

        let app = try await Application.make(env)

        // This attempts to install NIO as the Swift Concurrency global executor.
        // You should not call any async functions before this point.
        let executorTakeoverSuccess = NIOSingletons.unsafeTryInstallSingletonPosixEventLoopGroupAsConcurrencyGlobalExecutor()
        app.logger.debug("Running with \(executorTakeoverSuccess ? "SwiftNIO" : "standard") Swift Concurrency default executor")

        try await parseServerSwiftConfigure(
            app,
            using: exampleRoutes
        )
        try await app.execute()
        try await app.asyncShutdown()
    }
}

如果您想以编程方式传递配置参数,您可以将 configure 方法添加到 configure.swift,使其看起来类似于下面

public func configure(_ app: Application) async throws {
    // Initialize ParseServerSwift
    let configuration = try ParseServerConfiguration(app: app,
                                                     hostName: "hostName",
                                                     port: 8081,
                                                     applicationId: "applicationId",
                                                     primaryKey: "primaryKey",
                                                     webhookKey: hookKey,
                                                     parseServerURLString: "primaryKey")
    try await ParseServerSwift.initialize(configuration, app: app)
    
    // Add any additional code to configure your server here...
    
    // register routes
    try routes(app)
}

启动服务器

ParseServerSwift 经过优化,可在 Docker 容器中运行。示例 docker-compose.yml 演示了如何快速启动一个 (1) ParseServerSwift 服务器、一个 (1) parse-hipaa 服务器和 (1) hipaa-postgres 数据库。

在 Docker 中

ParseSwift 未在 Apple 平台上构建时,它依赖于 FoundationNetworking。在构建您自己的 ParseServerSwift 项目时,请务必将以下行添加到您的 Dockerfile 发布阶段。

  1. Fork 此仓库
  2. 在您的终端中,将目录更改为 ParseServerSwift 文件夹
  3. 键入 docker-compose up
  4. 访问您的容器

image

在 macOS 上

要启动服务器,请在项目根目录的终端中键入 swift run

编写 Cloud Code

共享服务器-客户端代码

Apple 的 WWDC User Xcode for server-side development 建议创建 Swift 包(15:26 标记)来存放您的模型,并在服务器和客户端应用程序之间共享它们,以减少代码重复。为了最大限度地利用 Parse-Swift,建议不仅将您的模型添加到共享包中,还要添加所有查询(服务器和客户端)。原因如下

  1. 默认情况下,客户端上的 Parse-Swift 查询会被缓存;允许基于 Parse-Swift 的应用程序利用缓存来构建更快速的体验
  2. 当在 ParseServerSwift 中利用您的共享查询时;它们永远不会访问本地服务器缓存,因为它们始终从 Node.js Parse Server 请求最新数据
  3. 从客户端调用 Cloud-Code 函数永远不会访问本地缓存,因为这些是对 Node.js Parse Server 的 POST 调用

要了解有关共享模型的更多信息,请阅读 SwiftLee 博客

创建 ParseObject

如果您尚未为您的模型创建共享包,建议将您的所有 ParseObject 添加到名为 Models 的文件夹中,类似于 ParseServerSwift/Sources/ParseServerSwift/Models

ParseUser 模型

请注意,ParseServerSwift 中的 ParseUser 应符合 ParseCloudUser。这是因为 ParseCloudUser 在服务器端包含一些附加属性。在客户端,您应始终使用 ParseUser 而不是 ParseCloudUser。此外,请确保将 _User 类中的所有附加属性添加到 User 模型中。下面是一个 User 模型示例

/**
 An example `ParseUser`. You will want to add custom
 properties to reflect the `ParseUser` on your Parse Server.
 */
struct User: ParseCloudUser {

    var authData: [String: [String: String]?]?
    var username: String?
    var email: String?
    var emailVerified: Bool?
    var password: String?
    var objectId: String?
    var createdAt: Date?
    var updatedAt: Date?
    var ACL: ParseACL?
    var originalData: Data?
    var sessionToken: String?
    var _failed_login_count: Int?
    var _account_lockout_expires_at: Date?
}

一个 ParseObject 模型示例

GameScore 模型如下所示

import Foundation
import ParseSwift

/**
 An example `ParseObject`. This is for testing. You can
 remove when creating your application.
 */
struct GameScore: ParseObject {
    // These are required by ParseObject.
    var objectId: String?
    var createdAt: Date?
    var updatedAt: Date?
    var ACL: ParseACL?
    var originalData: Data?

    // Your own properties.
    var points: Int?

    // Implement your own version of merge.
    func merge(with object: Self) throws -> Self {
        var updated = try mergeParse(with: object)
        if updated.shouldRestoreKey(\.points,
                                     original: object) {
            updated.points = object.points
        }
        return updated
    }
}

创建新的 Cloud Code 路由

ParseHooks 添加路由就像添加 Vapor 中的路由 一样简单。ParseServerSwift 提供了一些添加到路由的附加方法,以轻松创建和注册 Hook 函数Hook 触发器。所有路由都应添加到项目中的 routes.swift 文件中。示例 ParseServerSwift 路由可以在 ParseServerSwift/Sources/ParseServerSwift/routes.swift 中找到。

路由器组和集合

由于 ParseServerSwift 是 Vapor 服务器,因此可以配置多种不同的方式来满足您的需求。请务必通读 vapor 文档。您可能想要利用的一些重要功能在下面突出显示

要了解有关创建组和集合的更多信息,请查看此博客

请务必将 import ParseSwiftimport ParseServerSwift 添加到 routes.swift 的顶部

从 Cloud Code 路由发送错误

有时您需要通过发送错误响应给 Node.js Parse Server 以传播到客户端。可以通过发送 ParseHookResponse 来完成发送错误。以下是发送错误的两个示例

// Note: `T` is the type to be returned if there is no error thrown.

// Standard Parse error with your own unique message
let standardError = ParseError(code: .missingObjectId, message: "This object requires an objectId")
return ParseHookResponse<T>(error: standardError) // Be sure to "return" the ParseHookResponse in your route, DO NOT "throw" the error.

// Custom error with your own unique code and message
let customError = ParseError(otherCode: 1001, message: "My custom error")
return ParseHookResponse<T>(error: customError) // Be sure to "return" ParseHookResponse in your route, DO NOT "throw" the error.

Cloud Code 示例

Parse-Swift 有许多 Swift Playgrounds 来演示如何使用 SDK。以下是一些值得注意的专门用于 Cloud Code 的 Playgrounds,可以直接在 ParseServerSwift 中使用

Cloud Code 函数

Cloud Code 函数也可以接受参数。建议将所有参数放在 ParseServerSwift/Sources/ParseServerSwift/Models/Parameters

// A simple Parse Hook Function route that returns "Hello World".
app.post("hello",
         name: "hello") { req async throws -> ParseHookResponse<String> in
    // Note that `ParseHookResponse<String>` means a "successful"
    // response will return a "String" type.
    if let error: ParseHookResponse<String> = checkHeaders(req) {
        return error
    }
    var parseRequest = try req.content
        .decode(ParseHookFunctionRequest<User, FooParameters>.self)
    
    // If a User made the request, fetch the complete user.
    if parseRequest.user != nil {
        parseRequest = try await parseRequest.hydrateUser(request: req)
    }
    
    // To query using the User's credentials who called this function,
    // use the options() method from the parseRequest
    let options = try parseRequest.options(req)
    let scores = try await GameScore.query.findAll(options: options)
    req.logger.info("Scores this user can access: \(scores)")
    return ParseHookResponse(success: "Hello world!")
}

Cloud Code 触发器

// A Parse Hook Trigger route.
app.post("score", "save", "before",
         object: GameScore.self,
         trigger: .beforeSave) { req async throws -> ParseHookResponse<GameScore> in
    // Note that `ParseHookResponse<GameScore>` means a "successful"
    // response will return a "GameScore" type.
    if let error: ParseHookResponse<GameScore> = checkHeaders(req) {
        return error
    }
    var parseRequest = try req.content
        .decode(ParseHookTriggerObjectRequest<User, GameScore>.self)

    // If a User made the request, fetch the complete user.
    if parseRequest.user != nil {
        parseRequest = try await parseRequest.hydrateUser(request: req)
    }

    guard let object = parseRequest.object else {
        return ParseHookResponse(error: .init(code: .missingObjectId,
                                              message: "Object not sent in request."))
    }
    // To query using the primaryKey pass the `usePrimaryKey` option
    // to ther query.
    let scores = try await GameScore.query.findAll(options: [.usePrimaryKey])
    req.logger.info("Before save is being made. Showing all scores before saving new ones: \(scores)")
    return ParseHookResponse(success: object)
}

// Another Parse Hook Trigger route.
app.post("score", "find", "before",
         object: GameScore.self,
         trigger: .beforeFind) { req async throws -> ParseHookResponse<[GameScore]> in
    // Note that `ParseHookResponse<[GameScore]>` means a "successful"
    // response will return a "[GameScore]" type.
    if let error: ParseHookResponse<[GameScore]> = checkHeaders(req) {
        return error
    }
    let parseRequest = try req.content
        .decode(ParseHookTriggerObjectRequest<User, GameScore>.self)
    req.logger.info("A query is being made: \(parseRequest)")

    // Return two custom scores instead.
    let score1 = GameScore(objectId: "yolo",
                           createdAt: Date(),
                           points: 50)
    let score2 = GameScore(objectId: "nolo",
                           createdAt: Date(),
                           points: 60)
    req.logger.info("""
        Returning custom objects to the user from Cloud Code instead of querying:
        \(score1); \(score2)
    """)
    return ParseHookResponse(success: [score1, score2])
}

// Another Parse Hook Trigger route.
app.post("user", "login", "after",
         object: User.self,
         trigger: .afterLogin) { req async throws -> ParseHookResponse<Bool> in
    // Note that `ParseHookResponse<Bool>` means a "successful"
    // response will return a "Bool" type. Bool is the standard response with
    // a "true" response meaning everything is okay or continue.
    if let error: ParseHookResponse<Bool> = checkHeaders(req) {
        return error
    }
    let parseRequest = try req.content
        .decode(ParseHookTriggerObjectRequest<User, GameScore>.self)

    req.logger.info("A user has logged in: \(parseRequest)")
    return ParseHookResponse(success: true)
}

// A Parse Hook Trigger route for `ParseFile`.
app.on("file", "save", "before",
       trigger: .beforeSave) { req async throws -> ParseHookResponse<Bool> in
    // Note that `ParseHookResponse<Bool>` means a "successful"
    // response will return a "Bool" type. Bool is the standard response with
    // a "true" response meaning everything is okay or continue. Sending "false"
    // in this case will reject saving the file.
    if let error: ParseHookResponse<Bool> = checkHeaders(req) {
        return error
    }
    let parseRequest = try req.content
        .decode(ParseHookTriggerRequest<User>.self)

    req.logger.info("A ParseFile is being saved: \(parseRequest)")
    return ParseHookResponse(success: true)
}

// Another Parse Hook Trigger route for `ParseFile`.
app.post("file", "delete", "before",
         trigger: .beforeDelete) { req async throws -> ParseHookResponse<Bool> in
    // Note that `ParseHookResponse<Bool>` means a "successful"
    // response will return a "Bool" type. Bool is the standard response with
    // a "true" response meaning everything is okay or continue.
    if let error: ParseHookResponse<Bool> = checkHeaders(req) {
        return error
    }
    let parseRequest = try req.content
        .decode(ParseHookTriggerRequest<User>.self)

    req.logger.info("A ParseFile is being deleted: \(parseRequest)")
    return ParseHookResponse(success: true)
}

// A Parse Hook Trigger route for `ParseLiveQuery`.
app.post("connect", "before",
         trigger: .beforeConnect) { req async throws -> ParseHookResponse<Bool> in
    // Note that `ParseHookResponse<Bool>` means a "successful"
    // response will return a "Bool" type. Bool is the standard response with
    // a "true" response meaning everything is okay or continue.
    if let error: ParseHookResponse<Bool> = checkHeaders(req) {
        return error
    }
    let parseRequest = try req.content
        .decode(ParseHookTriggerRequest<User>.self)

    req.logger.info("A LiveQuery connection is being made: \(parseRequest)")
    return ParseHookResponse(success: true)
}

// Another Parse Hook Trigger route for `ParseLiveQuery`.
app.post("score", "subscribe", "before",
         object: GameScore.self,
         trigger: .beforeSubscribe) { req async throws -> ParseHookResponse<Bool> in
    // Note that `ParseHookResponse<Bool>` means a "successful"
    // response will return a "Bool" type. Bool is the standard response with
    // a "true" response meaning everything is okay or continue.
    if let error: ParseHookResponse<Bool> = checkHeaders(req) {
        return error
    }
    let parseRequest = try req.content
        .decode(ParseHookTriggerObjectRequest<User, GameScore>.self)

    req.logger.info("A LiveQuery subscription is being made: \(parseRequest)")
    return ParseHookResponse(success: true)
}

// Another Parse Hook Trigger route for `ParseLiveQuery`.
app.post("score", "event", "after",
         object: GameScore.self,
         trigger: .afterEvent) { req async throws -> ParseHookResponse<Bool> in
    // Note that `ParseHookResponse<Bool>` means a "successful"
    // response will return a "Bool" type. Bool is the standard response with
    // a "true" response meaning everything is okay or continue.
    if let error: ParseHookResponse<Bool> = checkHeaders(req) {
        return error
    }
    let parseRequest = try req.content
        .decode(ParseHookTriggerObjectRequest<User, GameScore>.self)

    req.logger.info("A LiveQuery event occured: \(parseRequest)")
    return ParseHookResponse(success: true)
}