用 Swift 编写 Parse Cloud Code!
什么是 Cloud Code?对于复杂的应用程序,有时您只需要不在移动设备上运行的逻辑。Cloud Code 使这成为可能。ParseServerSwift
中的 Cloud Code 易于使用,因为它使用 Parse-SwiftOG 和 Vapor 构建。与传统的在 Node.js parse-server 上运行的 基于 JS 的 Cloud Code 相比,ParseServerSwift
提供了许多额外的优势
从技术上讲,可以使用 ParseServerSwift
编写完整的应用程序,唯一的区别是此代码在您的 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 函数和触发器添加路由。
以下环境变量可用,可以直接配置,也可以通过 .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
应与 Parse Server 上的 webhookKey 匹配。
上述环境变量自动配置 Parse-SwiftOG SDK。如果您需要更自定义的配置,请参阅 文档。
要利用上述环境变量,您应该修改项目中的 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 数据库。
当 ParseSwift
未在 Apple 平台上构建时,它依赖于 FoundationNetworking
。在构建您自己的 ParseServerSwift
项目时,请务必将以下行添加到您的 Dockerfile 发布阶段。
ParseServerSwift
文件夹docker-compose up
parse
,密码:1234
Parse-Hipaa
应用程序,单击 Webhooks
,您将看到所有注册为 webook 的示例 Cloud Code要启动服务器,请在项目根目录的终端中键入 swift run
。
Apple 的 WWDC User Xcode for server-side development 建议创建 Swift 包(15:26 标记)来存放您的模型,并在服务器和客户端应用程序之间共享它们,以减少代码重复。为了最大限度地利用 Parse-Swift,建议不仅将您的模型添加到共享包中,还要添加所有查询(服务器和客户端)。原因如下
POST
调用要了解有关共享模型的更多信息,请阅读 SwiftLee 博客。
如果您尚未为您的模型创建共享包,建议将您的所有 ParseObject
添加到名为 Models
的文件夹中,类似于 ParseServerSwift/Sources/ParseServerSwift/Models。
请注意,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?
}
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
}
}
为 ParseHooks
添加路由就像添加 Vapor 中的路由 一样简单。ParseServerSwift
提供了一些添加到路由的附加方法,以轻松创建和注册 Hook 函数 和 Hook 触发器。所有路由都应添加到项目中的 routes.swift
文件中。示例 ParseServerSwift
路由可以在 ParseServerSwift/Sources/ParseServerSwift/routes.swift 中找到。
由于 ParseServerSwift
是 Vapor 服务器,因此可以配置多种不同的方式来满足您的需求。请务必通读 vapor 文档。您可能想要利用的一些重要功能在下面突出显示
要了解有关创建组和集合的更多信息,请查看此博客。
请务必将 import ParseSwift
和 import ParseServerSwift
添加到 routes.swift 的顶部
有时您需要通过发送错误响应给 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.
Parse-Swift 有许多 Swift Playgrounds 来演示如何使用 SDK。以下是一些值得注意的专门用于 Cloud Code 的 Playgrounds,可以直接在 ParseServerSwift
中使用
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!")
}
// 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)
}