-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
## Now with Macros! Adds a few macros to streamline building with Alchemy. ### Routing Use `@{METHOD}` to generate a route handler that automatically pulls function parameters from incoming requests and converts the return type to a response. Annotating your Application or Controller will automatically register the handlers, or you can access them via `$nameOfTheFunction`. ```swift @Application struct App { @post("/user") func createUser(email: String, password: String) async throws -> User { // create user with parsed `email & `password` } } ``` Automatically validate parameters with `@Validate` and a library (Coming Soon™️) of built in validators. ```swift @controller struct UserController { @patch("/email") func updateEmail(@Validate(.email) email: String) async throws { // email is valid! } } ``` ### Jobs Quickly create a background job with just a function (global or static). Use the `$` prefix to refer to the job. ```swift @job func sendInvoice(email: String) async throws { ... } // run in foreground try await sendInvoice(email: "[email protected]") // run as a background job try await $sendInvoice(email: "[email protected]").dispatch() ``` ### Rune Define models with the `@Model` macro. ```swift @model struct User { var id: UUID var email: String var name: String } ``` This unlocks some major quality of life improvements including type safe, `KeyPath`-based query builders which are Coming Soon™️ #### Relationships Relationships look similar but get some powerful quality of life improvements including better join column inference, CRUD functions, and the ability to be defined in extensions. ```swift struct Todo { var id: Int var name: String var isDone = false @BelongsToMany var tags: [Tag] } extension User { @hasmany var tags: [Tag] } // eager load with `$relationship` User.query().with(\.$todos) // chain relationships for nested loads (this loads todos & tags) User.query().with(\.$todos.$tags) // associates a todo with a user let todo = Todo(name: "do laundry") try await user.$todos.connect(todo) ``` If loaded, relationships are automatically encoded from route handlers. ```swift @get("/todos") func getTodos() -> [Todo] { try await Todo.with(\.$tags).all() } ``` ```json [ { "id": 1, "name": "Build great backends with Alchemy", "tags": [ { "id": 1, "name": "Coding" }, { "id": 2, "name": "Extremely Fun" } ] }, { "id": 2, "name": "Make cookies", "tags": [ { "id": 3, "name": "Cooking" } ] } ] ```
- Loading branch information
1 parent
f6607a6
commit eaa8e94
Showing
132 changed files
with
3,313 additions
and
981 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
@attached(extension, conformances: Application, Controller, names: named(route)) | ||
public macro Application() = #externalMacro(module: "AlchemyPlugin", type: "ApplicationMacro") | ||
|
||
@attached(extension, conformances: Controller, names: named(route)) | ||
public macro Controller() = #externalMacro(module: "AlchemyPlugin", type: "ControllerMacro") | ||
|
||
@attached(peer, names: prefixed(`$`)) | ||
public macro Job() = #externalMacro(module: "AlchemyPlugin", type: "JobMacro") | ||
|
||
// MARK: Rune - Model | ||
|
||
@attached(memberAttribute) | ||
@attached(member, names: named(storage), named(fieldLookup)) | ||
@attached(extension, conformances: Model, Codable, names: named(init), named(fields), named(encode)) | ||
public macro Model() = #externalMacro(module: "AlchemyPlugin", type: "ModelMacro") | ||
|
||
@attached(accessor) | ||
public macro ID() = #externalMacro(module: "AlchemyPlugin", type: "IDMacro") | ||
|
||
// MARK: Rune - Relationships | ||
|
||
@attached(accessor) | ||
@attached(peer, names: prefixed(`$`)) | ||
public macro HasMany(from: String? = nil, to: String? = nil) = #externalMacro(module: "AlchemyPlugin", type: "RelationshipMacro") | ||
|
||
@attached(accessor) | ||
@attached(peer, names: prefixed(`$`)) | ||
public macro HasManyThrough( | ||
_ through: String, | ||
from: String? = nil, | ||
to: String? = nil, | ||
throughFrom: String? = nil, | ||
throughTo: String? = nil | ||
) = #externalMacro(module: "AlchemyPlugin", type: "RelationshipMacro") | ||
|
||
@attached(accessor) | ||
@attached(peer, names: prefixed(`$`)) | ||
public macro HasOne(from: String? = nil, to: String? = nil) = #externalMacro(module: "AlchemyPlugin", type: "RelationshipMacro") | ||
|
||
@attached(accessor) | ||
@attached(peer, names: prefixed(`$`)) | ||
public macro HasOneThrough( | ||
_ through: String, | ||
from: String? = nil, | ||
to: String? = nil, | ||
throughFrom: String? = nil, | ||
throughTo: String? = nil | ||
) = #externalMacro(module: "AlchemyPlugin", type: "RelationshipMacro") | ||
|
||
@attached(accessor) | ||
@attached(peer, names: prefixed(`$`)) | ||
public macro BelongsTo(from: String? = nil, to: String? = nil) = #externalMacro(module: "AlchemyPlugin", type: "RelationshipMacro") | ||
|
||
@attached(accessor) | ||
@attached(peer, names: prefixed(`$`)) | ||
public macro BelongsToThrough( | ||
_ through: String, | ||
from: String? = nil, | ||
to: String? = nil, | ||
throughFrom: String? = nil, | ||
throughTo: String? = nil | ||
) = #externalMacro(module: "AlchemyPlugin", type: "RelationshipMacro") | ||
|
||
@attached(accessor) | ||
@attached(peer, names: prefixed(`$`)) | ||
public macro BelongsToMany( | ||
_ pivot: String? = nil, | ||
from: String? = nil, | ||
to: String? = nil, | ||
pivotFrom: String? = nil, | ||
pivotTo: String? = nil | ||
) = #externalMacro(module: "AlchemyPlugin", type: "RelationshipMacro") | ||
|
||
// MARK: Route Methods | ||
|
||
@attached(peer, names: prefixed(`$`)) public macro HTTP(_ method: String, _ path: String, options: RouteOptions = []) = #externalMacro(module: "AlchemyPlugin", type: "HTTPMethodMacro") | ||
@attached(peer, names: prefixed(`$`)) public macro DELETE(_ path: String, options: RouteOptions = []) = #externalMacro(module: "AlchemyPlugin", type: "HTTPMethodMacro") | ||
@attached(peer, names: prefixed(`$`)) public macro GET(_ path: String, options: RouteOptions = []) = #externalMacro(module: "AlchemyPlugin", type: "HTTPMethodMacro") | ||
@attached(peer, names: prefixed(`$`)) public macro PATCH(_ path: String, options: RouteOptions = []) = #externalMacro(module: "AlchemyPlugin", type: "HTTPMethodMacro") | ||
@attached(peer, names: prefixed(`$`)) public macro POST(_ path: String, options: RouteOptions = []) = #externalMacro(module: "AlchemyPlugin", type: "HTTPMethodMacro") | ||
@attached(peer, names: prefixed(`$`)) public macro PUT(_ path: String, options: RouteOptions = []) = #externalMacro(module: "AlchemyPlugin", type: "HTTPMethodMacro") | ||
@attached(peer, names: prefixed(`$`)) public macro OPTIONS(_ path: String, options: RouteOptions = []) = #externalMacro(module: "AlchemyPlugin", type: "HTTPMethodMacro") | ||
@attached(peer, names: prefixed(`$`)) public macro HEAD(_ path: String, options: RouteOptions = []) = #externalMacro(module: "AlchemyPlugin", type: "HTTPMethodMacro") | ||
@attached(peer, names: prefixed(`$`)) public macro TRACE(_ path: String, options: RouteOptions = []) = #externalMacro(module: "AlchemyPlugin", type: "HTTPMethodMacro") | ||
@attached(peer, names: prefixed(`$`)) public macro CONNECT(_ path: String, options: RouteOptions = []) = #externalMacro(module: "AlchemyPlugin", type: "HTTPMethodMacro") | ||
|
||
// MARK: Route Parameters | ||
|
||
@propertyWrapper public struct Path<L: LosslessStringConvertible> { | ||
public var wrappedValue: L | ||
|
||
public init(wrappedValue: L) { | ||
self.wrappedValue = wrappedValue | ||
} | ||
} | ||
|
||
@propertyWrapper public struct Header<L: LosslessStringConvertible> { | ||
public var wrappedValue: L | ||
|
||
public init(wrappedValue: L) { | ||
self.wrappedValue = wrappedValue | ||
} | ||
} | ||
|
||
@propertyWrapper public struct URLQuery<L: LosslessStringConvertible> { | ||
public var wrappedValue: L | ||
|
||
public init(wrappedValue: L) { | ||
self.wrappedValue = wrappedValue | ||
} | ||
} | ||
|
||
@propertyWrapper public struct Field<C: Codable> { | ||
public var wrappedValue: C | ||
|
||
public init(wrappedValue: C) { | ||
self.wrappedValue = wrappedValue | ||
} | ||
} | ||
|
||
@propertyWrapper public struct Body<C: Codable> { | ||
public var wrappedValue: C | ||
|
||
public init(wrappedValue: C) { | ||
self.wrappedValue = wrappedValue | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
import AlchemyX | ||
|
||
extension Application { | ||
@discardableResult | ||
public func useAlchemyX(db: Database = DB) -> Self { | ||
// 1. users table | ||
|
||
db.migrations.append(UserMigration()) | ||
db.migrations.append(TokenMigration()) | ||
|
||
// 2. users endpoint | ||
|
||
return use(AuthController()) | ||
} | ||
} | ||
|
||
private struct AuthController: Controller, AuthAPI { | ||
func route(_ router: Router) { | ||
registerHandlers(on: router) | ||
} | ||
|
||
func signUp(email: String, password: String) async throws -> AuthResponse { | ||
let password = try await Hash.make(password) | ||
let user = try await User(email: email, password: password).insertReturn() | ||
let token = try await Token(userId: user.id).insertReturn() | ||
return .init(token: token.value, user: user.dto) | ||
} | ||
|
||
func signIn(email: String, password: String) async throws -> AuthResponse { | ||
guard let user = try await User.firstWhere("email" == email) else { | ||
throw HTTPError(.notFound) | ||
} | ||
|
||
guard try await Hash.verify(password, hash: user.password) else { | ||
throw HTTPError(.unauthorized) | ||
} | ||
|
||
let token = try await Token(userId: user.id).insertReturn() | ||
return .init(token: token.value, user: user.dto) | ||
} | ||
|
||
func signOut() async throws { | ||
try await token.delete() | ||
} | ||
|
||
func getUser() async throws -> AlchemyX.User { | ||
try user.dto | ||
} | ||
|
||
func updateUser(email: String?, phone: String?, password: String?) async throws -> AlchemyX.User { | ||
var user = try user | ||
if let email { user.email = email } | ||
if let phone { user.phone = phone } | ||
if let password { user.password = try await Hash.make(password) } | ||
return try await user.save().dto | ||
} | ||
} | ||
|
||
extension Controller { | ||
var req: Request { .current } | ||
fileprivate var user: User { get throws { try req.get() } } | ||
fileprivate var token: Token { get throws { try req.get() } } | ||
} | ||
|
||
@Model | ||
struct Token: TokenAuthable { | ||
var id: UUID | ||
var value: String = UUID().uuidString | ||
let userId: UUID | ||
|
||
@BelongsTo var user: User | ||
} | ||
|
||
@Model | ||
struct User { | ||
var id: UUID | ||
var email: String | ||
var password: String | ||
var phone: String? | ||
|
||
@HasMany var tokens: [Token] | ||
|
||
var dto: AlchemyX.User { | ||
AlchemyX.User( | ||
id: id, | ||
email: email, | ||
phone: phone | ||
) | ||
} | ||
} | ||
|
||
struct TokenMigration: Migration { | ||
func up(db: Database) async throws { | ||
try await db.createTable("tokens") { | ||
$0.uuid("id").primary() | ||
$0.string("value").notNull() | ||
$0.uuid("user_id").references("id", on: "users").notNull() | ||
} | ||
} | ||
|
||
func down(db: Database) async throws { | ||
try await db.dropTable("tokens") | ||
} | ||
} | ||
|
||
struct UserMigration: Migration { | ||
func up(db: Database) async throws { | ||
try await db.createTable("users") { | ||
$0.uuid("id").primary() | ||
$0.string("email").notNull() | ||
$0.string("password").notNull() | ||
$0.string("phone") | ||
} | ||
} | ||
|
||
func down(db: Database) async throws { | ||
try await db.dropTable("users") | ||
} | ||
} |
Oops, something went wrong.