Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify the delegates #15

Merged
merged 17 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ jobs:
uses: vapor/ci/.github/workflows/run-unit-tests.yml@main
with:
with_linting: true
test_filter: --no-parallel
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ let package = Package(
.library(name: "Orders", targets: ["Orders"]),
],
dependencies: [
.package(url: "https://github.com/vapor/vapor.git", from: "4.106.0"),
.package(url: "https://github.com/vapor/vapor.git", from: "4.106.1"),
.package(url: "https://github.com/vapor/fluent.git", from: "4.12.0"),
.package(url: "https://github.com/vapor/apns.git", from: "4.2.0"),
.package(url: "https://github.com/vapor-community/Zip.git", from: "2.2.3"),
.package(url: "https://github.com/apple/swift-certificates.git", from: "1.5.0"),
.package(url: "https://github.com/apple/swift-certificates.git", from: "1.6.1"),
// used in tests
.package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.8.0"),
],
Expand Down
12 changes: 6 additions & 6 deletions Sources/Orders/Models/Concrete Models/Order.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,17 @@ final public class Order: OrderModel, @unchecked Sendable {
public var updatedAt: Date?

/// An identifier for the order type associated with the order.
@Field(key: Order.FieldKeys.orderTypeIdentifier)
public var orderTypeIdentifier: String
@Field(key: Order.FieldKeys.typeIdentifier)
public var typeIdentifier: String

/// The authentication token supplied to your web service.
@Field(key: Order.FieldKeys.authenticationToken)
public var authenticationToken: String

public required init() {}

public required init(orderTypeIdentifier: String, authenticationToken: String) {
self.orderTypeIdentifier = orderTypeIdentifier
public required init(typeIdentifier: String, authenticationToken: String) {
self.typeIdentifier = typeIdentifier
self.authenticationToken = authenticationToken
}
}
Expand All @@ -49,7 +49,7 @@ extension Order: AsyncMigration {
.id()
.field(Order.FieldKeys.createdAt, .datetime, .required)
.field(Order.FieldKeys.updatedAt, .datetime, .required)
.field(Order.FieldKeys.orderTypeIdentifier, .string, .required)
.field(Order.FieldKeys.typeIdentifier, .string, .required)
.field(Order.FieldKeys.authenticationToken, .string, .required)
.create()
}
Expand All @@ -64,7 +64,7 @@ extension Order {
static let schemaName = "orders"
static let createdAt = FieldKey(stringLiteral: "created_at")
static let updatedAt = FieldKey(stringLiteral: "updated_at")
static let orderTypeIdentifier = FieldKey(stringLiteral: "order_type_identifier")
static let typeIdentifier = FieldKey(stringLiteral: "type_identifier")
static let authenticationToken = FieldKey(stringLiteral: "authentication_token")
}
}
16 changes: 7 additions & 9 deletions Sources/Orders/Models/Concrete Models/OrdersDevice.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ final public class OrdersDevice: DeviceModel, @unchecked Sendable {
public var pushToken: String

/// The identifier Apple Wallet provides for the device.
@Field(key: OrdersDevice.FieldKeys.deviceLibraryIdentifier)
public var deviceLibraryIdentifier: String
@Field(key: OrdersDevice.FieldKeys.libraryIdentifier)
public var libraryIdentifier: String

public init(deviceLibraryIdentifier: String, pushToken: String) {
self.deviceLibraryIdentifier = deviceLibraryIdentifier
public init(libraryIdentifier: String, pushToken: String) {
self.libraryIdentifier = libraryIdentifier
self.pushToken = pushToken
}

Expand All @@ -37,10 +37,8 @@ extension OrdersDevice: AsyncMigration {
try await database.schema(Self.schema)
.field(.id, .int, .identifier(auto: true))
.field(OrdersDevice.FieldKeys.pushToken, .string, .required)
.field(OrdersDevice.FieldKeys.deviceLibraryIdentifier, .string, .required)
.unique(
on: OrdersDevice.FieldKeys.pushToken, OrdersDevice.FieldKeys.deviceLibraryIdentifier
)
.field(OrdersDevice.FieldKeys.libraryIdentifier, .string, .required)
.unique(on: OrdersDevice.FieldKeys.pushToken, OrdersDevice.FieldKeys.libraryIdentifier)
.create()
}

Expand All @@ -53,6 +51,6 @@ extension OrdersDevice {
enum FieldKeys {
static let schemaName = "orders_devices"
static let pushToken = FieldKey(stringLiteral: "push_token")
static let deviceLibraryIdentifier = FieldKey(stringLiteral: "device_library_identifier")
static let libraryIdentifier = FieldKey(stringLiteral: "library_identifier")
}
}
12 changes: 6 additions & 6 deletions Sources/Orders/Models/OrderModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import Foundation
/// Uses a UUID so people can't easily guess order IDs.
public protocol OrderModel: Model where IDValue == UUID {
/// An identifier for the order type associated with the order.
var orderTypeIdentifier: String { get set }
var typeIdentifier: String { get set }

/// The date and time when the customer created the order.
var createdAt: Date? { get set }
Expand All @@ -36,14 +36,14 @@ extension OrderModel {
return id
}

var _$orderTypeIdentifier: Field<String> {
guard let mirror = Mirror(reflecting: self).descendant("_orderTypeIdentifier"),
let orderTypeIdentifier = mirror as? Field<String>
var _$typeIdentifier: Field<String> {
guard let mirror = Mirror(reflecting: self).descendant("_typeIdentifier"),
let typeIdentifier = mirror as? Field<String>
else {
fatalError("orderTypeIdentifier property must be declared using @Field")
fatalError("typeIdentifier property must be declared using @Field")
}

return orderTypeIdentifier
return typeIdentifier
}

var _$updatedAt: Timestamp<DefaultTimestampFormat> {
Expand Down
8 changes: 3 additions & 5 deletions Sources/Orders/Models/OrdersRegistrationModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,13 @@ extension OrdersRegistrationModel {
return order
}

static func `for`(
deviceLibraryIdentifier: String, orderTypeIdentifier: String, on db: any Database
) -> QueryBuilder<Self> {
static func `for`(deviceLibraryIdentifier: String, typeIdentifier: String, on db: any Database) -> QueryBuilder<Self> {
Self.query(on: db)
.join(parent: \._$order)
.join(parent: \._$device)
.with(\._$order)
.with(\._$device)
.filter(OrderType.self, \._$orderTypeIdentifier == orderTypeIdentifier)
.filter(DeviceType.self, \._$deviceLibraryIdentifier == deviceLibraryIdentifier)
.filter(OrderType.self, \._$typeIdentifier == typeIdentifier)
.filter(DeviceType.self, \._$libraryIdentifier == deviceLibraryIdentifier)
}
}
41 changes: 23 additions & 18 deletions Sources/Orders/Orders.docc/GettingStarted.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,6 @@ struct OrderJSONData: OrderJSON.Properties {
### Implement the Delegate

Create a delegate class that implements ``OrdersDelegate``.
In the ``OrdersDelegate/sslSigningFilesDirectory`` you specify there must be the `WWDR.pem`, `ordercertificate.pem` and `orderkey.pem` files.
If they are named like that you're good to go, otherwise you have to specify the custom name.

> Tip: Obtaining the three certificates files could be a bit tricky. You could get some guidance from [this guide](https://github.com/alexandercerutti/passkit-generator/wiki/Generating-Certificates) and [this video](https://www.youtube.com/watch?v=rJZdPoXHtzI). Those guides are for Wallet passes, but the process is similar for Wallet orders.

There are other fields available which have reasonable default values. See ``OrdersDelegate``'s documentation.

Because the files for your order's template and the method of encoding might vary by order type, you'll be provided the ``Order`` for those methods.
In the ``OrdersDelegate/encode(order:db:encoder:)`` method, you'll want to encode a `struct` that conforms to ``OrderJSON``.
Expand All @@ -127,12 +121,8 @@ import Fluent
import Orders

final class OrderDelegate: OrdersDelegate {
let sslSigningFilesDirectory = URL(fileURLWithPath: "Certificates/Orders/", isDirectory: true)

let pemPrivateKeyPassword: String? = Environment.get("ORDERS_PEM_PRIVATE_KEY_PASSWORD")!

func encode<O: OrderModel>(order: O, db: Database, encoder: JSONEncoder) async throws -> Data {
// The specific OrderData class you use here may vary based on the `order.orderTypeIdentifier`
// The specific OrderData class you use here may vary based on the `order.typeIdentifier`
// if you have multiple different types of orders, and thus multiple types of order data.
guard let orderData = try await OrderData.query(on: db)
.filter(\.$order.$id == order.requireID())
Expand All @@ -146,19 +136,21 @@ final class OrderDelegate: OrdersDelegate {
return data
}

func template<O: OrderModel>(for order: O, db: Database) async throws -> URL {
func template<O: OrderModel>(for order: O, db: Database) async throws -> String {
// The location might vary depending on the type of order.
URL(fileURLWithPath: "Templates/Orders/", isDirectory: true)
"Templates/Orders/"
}
}
```

> Important: If you have an encrypted PEM private key, you **must** explicitly declare ``OrdersDelegate/pemPrivateKeyPassword`` as a `String?` or Swift will ignore it as it'll think it's a `String` instead.

### Initialize the Service

Next, initialize the ``OrdersService`` inside the `configure.swift` file.
This will implement all of the routes that Apple Wallet expects to exist on your server.
In the `signingFilesDirectory` you specify there must be the `WWDR.pem`, `certificate.pem` and `key.pem` files.
If they are named like that you're good to go, otherwise you have to specify the custom name.

> Tip: Obtaining the three certificates files could be a bit tricky. You could get some guidance from [this guide](https://github.com/alexandercerutti/passkit-generator/wiki/Generating-Certificates) and [this video](https://www.youtube.com/watch?v=rJZdPoXHtzI). Those guides are for Wallet passes, but the process is similar for Wallet orders.

```swift
import Fluent
Expand All @@ -169,7 +161,11 @@ let orderDelegate = OrderDelegate()

public func configure(_ app: Application) async throws {
...
let ordersService = try OrdersService(app: app, delegate: orderDelegate)
let ordersService = try OrdersService(
app: app,
delegate: orderDelegate,
signingFilesDirectory: "Certificates/Orders/"
)
}
```

Expand Down Expand Up @@ -199,7 +195,16 @@ let orderDelegate = OrderDelegate()

public func configure(_ app: Application) async throws {
...
let ordersService = try OrdersServiceCustom<MyOrderType, MyDeviceType, MyOrdersRegistrationType, MyErrorLogType>(app: app, delegate: orderDelegate)
let ordersService = try OrdersServiceCustom<
MyOrderType,
MyDeviceType,
MyOrdersRegistrationType,
MyErrorLogType
>(
app: app,
delegate: orderDelegate,
signingFilesDirectory: "Certificates/Orders/"
)
}
```

Expand Down Expand Up @@ -234,7 +239,7 @@ struct OrderDataMiddleware: AsyncModelMiddleware {
// Create the `Order` and add it to the `OrderData` automatically at creation
func create(model: OrderData, on db: Database, next: AnyAsyncModelResponder) async throws {
let order = Order(
orderTypeIdentifier: Environment.get("ORDER_TYPE_IDENTIFIER")!,
typeIdentifier: Environment.get("ORDER_TYPE_IDENTIFIER")!,
authenticationToken: Data([UInt8].random(count: 12)).base64EncodedString())
try await order.save(on: db)
model.$order.id = try order.requireID()
Expand Down
64 changes: 5 additions & 59 deletions Sources/Orders/OrdersDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import Foundation

/// The delegate which is responsible for generating the order files.
public protocol OrdersDelegate: AnyObject, Sendable {
/// Should return a `URL` which points to the template data for the order.
/// Should return a URL path which points to the template data for the order.
///
/// The URL should point to a directory containing all the images and localizations for the generated `.order` archive but should *not* contain any of these items:
/// The path should point to a directory containing all the images and localizations for the generated `.order` archive
/// but should *not* contain any of these items:
/// - `manifest.json`
/// - `order.json`
/// - `signature`
Expand All @@ -21,10 +22,8 @@ public protocol OrdersDelegate: AnyObject, Sendable {
/// - order: The order data from the SQL server.
/// - db: The SQL database to query against.
///
/// - Returns: A `URL` which points to the template data for the order.
///
/// > Important: Be sure to use the `URL(fileURLWithPath:isDirectory:)` constructor.
func template<O: OrderModel>(for order: O, db: any Database) async throws -> URL
/// - Returns: A URL path which points to the template data for the order.
func template<O: OrderModel>(for order: O, db: any Database) async throws -> String

/// Generates the SSL `signature` file.
///
Expand All @@ -51,62 +50,9 @@ public protocol OrdersDelegate: AnyObject, Sendable {
func encode<O: OrderModel>(
order: O, db: any Database, encoder: JSONEncoder
) async throws -> Data

/// Should return a `URL` which points to the template data for the order.
///
/// The URL should point to a directory containing the files specified by these keys:
/// - `wwdrCertificate`
/// - `pemCertificate`
/// - `pemPrivateKey`
///
/// > Important: Be sure to use the `URL(fileURLWithPath:isDirectory:)` initializer!
var sslSigningFilesDirectory: URL { get }

/// The location of the `openssl` command as a file URL.
///
/// > Important: Be sure to use the `URL(fileURLWithPath:)` constructor.
var sslBinary: URL { get }

/// The name of Apple's WWDR.pem certificate as contained in `sslSigningFiles` path.
///
/// Defaults to `WWDR.pem`
var wwdrCertificate: String { get }

/// The name of the PEM Certificate for signing the order as contained in `sslSigningFiles` path.
///
/// Defaults to `ordercertificate.pem`
var pemCertificate: String { get }

/// The name of the PEM Certificate's private key for signing the order as contained in `sslSigningFiles` path.
///
/// Defaults to `orderkey.pem`
var pemPrivateKey: String { get }

/// The password to the private key file.
var pemPrivateKeyPassword: String? { get }
}

extension OrdersDelegate {
public var wwdrCertificate: String {
return "WWDR.pem"
}

public var pemCertificate: String {
return "ordercertificate.pem"
}

public var pemPrivateKey: String {
return "orderkey.pem"
}

public var pemPrivateKeyPassword: String? {
return nil
}

public var sslBinary: URL {
return URL(fileURLWithPath: "/usr/bin/openssl")
}

public func generateSignatureFile(in root: URL) -> Bool {
return false
}
Expand Down
39 changes: 30 additions & 9 deletions Sources/Orders/OrdersService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,37 @@ public final class OrdersService: Sendable {
/// - Parameters:
/// - app: The `Vapor.Application` to use in route handlers and APNs.
/// - delegate: The ``OrdersDelegate`` to use for order generation.
/// - signingFilesDirectory: The path of the directory where the signing files (`wwdrCertificate`, `pemCertificate`, `pemPrivateKey`) are located.
/// - wwdrCertificate: The name of Apple's WWDR.pem certificate as contained in `signingFilesDirectory` path. Defaults to `WWDR.pem`.
/// - pemCertificate: The name of the PEM Certificate for signing the pass as contained in `signingFilesDirectory` path. Defaults to `certificate.pem`.
/// - pemPrivateKey: The name of the PEM Certificate's private key for signing the pass as contained in `signingFilesDirectory` path. Defaults to `key.pem`.
/// - pemPrivateKeyPassword: The password to the private key file. If the key is not encrypted it must be `nil`. Defaults to `nil`.
/// - sslBinary: The location of the `openssl` command as a file path.
/// - pushRoutesMiddleware: The `Middleware` to use for push notification routes. If `nil`, push routes will not be registered.
/// - logger: The `Logger` to use.
public init(
app: Application, delegate: any OrdersDelegate,
pushRoutesMiddleware: (any Middleware)? = nil, logger: Logger? = nil
app: Application,
delegate: any OrdersDelegate,
signingFilesDirectory: String,
wwdrCertificate: String = "WWDR.pem",
pemCertificate: String = "certificate.pem",
pemPrivateKey: String = "key.pem",
pemPrivateKeyPassword: String? = nil,
sslBinary: String = "/usr/bin/openssl",
pushRoutesMiddleware: (any Middleware)? = nil,
logger: Logger? = nil
) throws {
service = try .init(
app: app, delegate: delegate, pushRoutesMiddleware: pushRoutesMiddleware, logger: logger
self.service = try .init(
app: app,
delegate: delegate,
signingFilesDirectory: signingFilesDirectory,
wwdrCertificate: wwdrCertificate,
pemCertificate: pemCertificate,
pemPrivateKey: pemPrivateKey,
pemPrivateKeyPassword: pemPrivateKeyPassword,
sslBinary: sslBinary,
pushRoutesMiddleware: pushRoutesMiddleware,
logger: logger
)
}

Expand Down Expand Up @@ -52,12 +75,10 @@ public final class OrdersService: Sendable {
///
/// - Parameters:
/// - id: The `UUID` of the order to send the notifications for.
/// - orderTypeIdentifier: The type identifier of the order.
/// - typeIdentifier: The type identifier of the order.
/// - db: The `Database` to use.
public func sendPushNotificationsForOrder(
id: UUID, of orderTypeIdentifier: String, on db: any Database
) async throws {
try await service.sendPushNotificationsForOrder(id: id, of: orderTypeIdentifier, on: db)
public func sendPushNotificationsForOrder(id: UUID, of typeIdentifier: String, on db: any Database) async throws {
try await service.sendPushNotificationsForOrder(id: id, of: typeIdentifier, on: db)
}

/// Sends push notifications for a given order.
Expand Down
Loading
Loading