Understanding Migrations in Vapor-Fluent (Server side Swift) - swift

I'm writing a web service in Swift using Vapor framework.
I have model named Item. Intially it has only name and id properties.
typealias VaporModel = Content & PostgreSQLModel & Parameter
final class Item: VaporModel {
var id: Int?
var name: String
}
After I configure a controller for the model and add the routes, when I hit the post Item request, I get the error as Model.defaultDatabase is required to use as DatabaseConnectable. I think the error is because I have not added Item to Migrations in configure.swift and I do the same after conforming Item to PostgreSQLMigration.
var migrations = MigrationConfig()
migrations.add(model: Item.self, database: .psql)
services.register(migrations)
Now, I am able to hit the post request and create items in the database.
So I understand that Migration protocol creates the default schema for a model and adds a new table to the database with the model's properties as columns.
Now I want to add a property such as price to my Item class. Now when I hit the post request, I get the error as column "price" of relation "Item" does not exist.
I assume the Migration protocol will be able to identify the schema changes and the column to my table (that's what I was used to in while using Realm for my iOS apps). But I am wrong and I read through the Migration docs and implement the prepare and revert methods in migration like below.
extension Item: PostgreSQLMigration {
static func prepare(on conn: PostgreSQLConnection) -> Future<Void> {
return Database.create(self, on: conn) { creator in
creator.field(for: \.price)
}
}
static func revert(on connection: PostgreSQLConnection) -> EventLoopFuture<Void> {
return Future.map(on: connection) { }
}
}
I'm still struck with the same error column "price" of relation "Item" does not exist. What am I missing here? Is my migration code correct?
Also, I understand that if am not making any changes to the Model, I can comment out the migration config, because they need not run every time I run the service. Is that correct?

With your code you haven't added a new migration. You have implemented a manual initial migration, but the initial migration has run already as requested (migrations.add(model: Item.self, database: .psql). To create a new migration you would need sth like:
struct ItemAddPriceMigration: Migration {
typealias Database = PostgreSQLDatabase
static func prepare(on conn: PostgreSQLConnection) -> EventLoopFuture<Void> {
return Database.update(Item.self, on: conn) { builder in
builder.field(for: \.price)
}
}
static func revert(on conn: PostgreSQLConnection) -> EventLoopFuture<Void> {
return conn.future()
}
}
And then you need to add it in configure:
migrations.add(migration: ItemAddPriceMigration.self, database: .psql)

Related

Linking my Fluent model to a pre-existing database table

I've been trying to link up my PostgreSQL database to a Swift Vapor project so I create routes to it. The first table I want to access is a table in my_database calls users. It has the properties user_id (primary integer key) and created_on (timestamp with time zone).
I've linked my Vapor project to my_database so that I can create new models. However, what if I want access to the pre-existing table users. Here's what I've cobbled together from the documentation and a few tutorials:
My initial model:
final class Users: Model {
static let name = "users"
typealias ID = Int
typealias Database = PostgreSQLDatabase
static let idKey: WritableKeyPath<Users, Int?> = \.user_id
var user_id: Int?
var created_on: Date
}
extension Users: Content { }
My migration (which I think is just a 'blank' migration, just to hook the project to the table?):
struct FirstMigration: PostgreSQLMigration {
static func prepare(on conn: PostgreSQLConnection) -> EventLoopFuture<Void> {
return conn.future()
}
static func revert(on conn: PostgreSQLConnection) -> EventLoopFuture<Void> {
return Future<Void>.done(on: conn)
}
}
And my configuration:
var migrations = MigrationConfig()
migrations.add(migration: FirstMigration.self, database: .psql)
services.register(migrations)
Any guidance much appreciated!
You don't need the migration. You either need to make your model confirm to Migration and add the model as a migration or set the static defaultDatabase property. Then you should be good to go.
PS - I think you want entity not name to tell it what the table name is.

How do I call a new database connection from within a read-only database connection in GRDB?

The function below returns a Ledgers record. Most of the time, it will find it in the optional _currentReceipt variable, or by searching the database, No writing needed there. I'd like to use a read-only GRDB database connection. Read-only database connections can run in parallel on different threads.
In the rare case, the first two steps fail, I can create a default Ledger. Calling try FoodyDataStack.thisDataStack.dbPool.write { writeDB in ... will throw a fatal error, Database connections are not reentrant. I'm looking for a way to save that default Ledger without having to wrap the whole function in a read-write connection.
Can I call an NSOperation on a separate queue from within a GRDB .read block?
class func getCurrentReceipt(db: Database) throws -> Ledgers {
if let cr = FoodyDataStack.thisDataStack._currentReceipt {
return cr
}
// Fall through
do {
if let cr = try Ledgers.filter(Ledgers.Columns.receiptClosed == ReceiptStatus.receiptOpen.rawValue).order(Ledgers.Columns.dateModified.desc).fetchOne(db) {
FoodyDataStack.thisDataStack._currentReceipt = cr
return cr
} else {
throw FoodyDataStack.myGRDBerrors.couldNotFindCurrentReceipt
}
} catch FoodyDataStack.myGRDBerrors.couldNotFindCurrentReceipt {
// Create new receipt with default store
let newReceipt = Ledgers()
newReceipt.dateCreated = Date()
newReceipt.dateModified = Date()
newReceipt.receiptStatus = .receiptOpen
newReceipt.receiptUrgency = .immediate
newReceipt.dateLedger = Date()
newReceipt.uuidStore = Stores.defaultStore(db).uuidKey
FoodyDataStack.thisDataStack._currentReceipt = newReceipt
return newReceipt
} catch {
NSLog("WARNING: Unhandled error in Ledgers.getCurrentReceipt() \(error.localizedDescription)")
}
}
Edit: I'm leaving this question here, but I think I may be going for premature optimization. I'm going to try dbQueue instead of dbPool and see what the performance is. I'll be back to dbPool if speed requires it.
GRDB database access methods are not reentrant (The read and write methods of DatabaseQueue and DatabasePool).
To help you solve your issue, try to split your database access methods in two levels.
The first level is not exposed to the rest of the application. Its methods all take a db: Database argument.
class MyStack {
private var dbQueue: DatabaseQueue
private func fetchFoo(_ db: Database, id: Int64) throws -> Foo? {
return try Foo.fetchOne(db, key: id)
}
private func setBar(_ db: Database, foo: Foo) throws {
try foo.updateChanges(db) {
$0.bar = true
}
}
}
Methods from the second level are exposed to the rest of the application. They wrap first-level methods in read and write database access methods:
class MyStack {
func fetchFoo(id: Int64) throws -> Foo? {
return try dbQueue.read { db in
try fetchFoo(db, id: id)
}
}
func setBar(id: Int64) throws {
try dbQueue.write { db in
guard let foo = try fetchFoo(db) else {
throw fooNotFound
}
try setBar(foo: foo)
}
}
}
Methods of the first level can be as low-level as needed, and can be composed.
Methods of the second level are high level, and are not composable: they can't call each other because of the "Database connections are not reentrant" fatal error. They provide the thread-safe database methods that guarantee database consistency.
See the Concurrency Guide for more information.

Populating objects from cloud records (or another external source) using a generic function

I'm building a generic API for my Swift applications. I use CoreData for local storage and CloudKit for cloud synchronization.
in order to be able to work with my data objects in generic functions I have organized them as follows (brief summary):
Objects that go in the CoreData Database are NSManagedObject instances that conform to a protocol called ManagedObjectProtocol, which enables conversion to DataObject instances
NSManagedObjects that need to be cloud synced conform to a protocol called CloudObject which allows populating objects from records and vice-versa
Objects I use in the graphic layer of my apps are NSObject classes that conform to the DataObject protocol which allows for conversion to NSManagedObject instances
an object of a specific class. What I would like this code to look like is this:
for record in records {
let context = self.persistentContainer.newBackgroundContext()
//classForEntityName is a function in a custom extension that returns an NSManagedObject for the entityName provided.
//I assume here that recordType == entityName
if let managed = self.persistentContainer.classForEntityName(record!.recordType) {
if let cloud = managed as? CloudObject {
cloud.populateManagedObject(from: record!, in: context)
}
}
}
However, this gives me several errors:
Protocol 'CloudObject' can only be used as a generic constraint because it has Self or associated type requirements
Member 'populateManagedObject' cannot be used on value of protocol type 'CloudObject'; use a generic constraint instead
The CloudObject protocol looks as follows:
protocol CloudObject {
associatedtype CloudManagedObject: NSManagedObject, ManagedObjectProtocol
var recordID: CKRecordID? { get }
var recordType: String { get }
func populateManagedObject(from record: CKRecord, in context: NSManagedObjectContext) -> Promise<CloudManagedObject>
func populateCKRecord() -> CKRecord
}
Somehow I need to find a way that allows me to get the specific class conforming to CloudObject based on the recordType I receive. How would I Best go about this?
Any help would be much appreciated!
As the data formats of CoreData and CloudKit are not related you need a way to efficiently identify CoreData objects from a CloudKit record and vice versa.
My suggestion is to use the same name for CloudKit record type and CoreData entity and to use a custom record name (string) with format <Entity>.<identifer>. Entity is the record type / class name and identifier is a CoreData attribute with unique values. For example if there are two entities named Person and Event the record name is "Person.JohnDoe" or "Event.E71F87E3-E381-409E-9732-7E670D2DC11C". If there are CoreData relationships add more dot separated components to identify those
For convenience you could use a helper enum Entity to create the appropriate entity from a record
enum Entity : String {
case person = "Person"
case event = "Event"
init?(record : CKRecord) {
let components = record.recordID.recordName.components(separatedBy: ".")
self.init(rawValue: components.first!)
}
}
and an extension of CKRecord to create a record for specific record type from a Entity (in my example CloudManager is a singleton to manage the CloudKit stuff e.g. the zones)
extension CKRecord {
convenience init(entity : Entity) {
self.init(recordType: entity.rawValue, zoneID: CloudManager.shared.zoneID)
}
convenience init(entity : Entity, recordID : CKRecordID) {
self.init(recordType: entity.rawValue, recordID: recordID)
}
}
When you receive Cloud records extract the entity and the unique identifier. Then try to fetch the corresponding CoreData object. If the object exists update it, if not create a new one. On the other hand create a new record from a CoreData object with the unique record name. Your CloudObject protocol widely fits this pattern, the associated type is not needed (by the way deleting it gets rid of the error) but add a requirement recordName
var recordName : String { get set }
and an extension to get the recordID from the record name
extension CloudObject where Self : NSManagedObject {
var recordID : CKRecordID {
return CKRecordID(recordName: self.recordName, zoneID: CloudManager.shared.zoneID)
}
}
Swift is not Java, Swift is like C++, associatedType is a way of writing a generic protocol, and generics in Swift means C++ template.
In Java, ArrayList<String> is the same type as ArrayList<Integer>!!
In Swift (and C++) , Array<String> is NOT the same type as Array<Int>
So, you can't take an array of Arrays for example, you MUST make it an array of Array<SpecificType>
What did Apple do to make you able to make a "type-erased" array for example?
They made Array<T> extend Array<Any>.
If you want to immitate this in your code, how?
protocol CloudObject {
// Omitted the associatedtype (like you already done as in the replies)
//associatedtype CloudManagedObject: NSManagedObject, ManagedObjectProtocol
var recordID: CKRecordID? { get }
var recordType: String { get }
func populateManagedObject(from record: CKRecord, in context: NSManagedObjectContext) -> Promise<NSManagedObject & ManagedObjectProtocol>
func populateCKRecord() -> CKRecord
}
Then make the "generic protocol", this would be useful in safely and performance programming when the resolving of protocol is known at compile time
protocol CloudObjectGeneric: CloudObject {
// Generify it
associatedtype CloudManagedObject: NSManagedObject, ManagedObjectProtocol
// You don't need to redefine those, those are not changed in generic form
//var recordID: CKRecordID? { get }
//var recordType: String { get }
//func populateCKRecord() -> CKRecord
// You need a new function, which is the generic one
func populateManagedObject(from record: CKRecord, in context: NSManagedObjectContext) -> Promise<CloudObject>
}
Then make the generic protocol conform to the non-generic one, not to need writing 2 populateManagedObject functions in each implementation
extension CloudObjectGeneric {
// Function like this if the generic was a parameter, would be
// straightforward, just pass it with a cast to indicate you
// are NOT CALLING THE SAME FUNCTION, you are calling it from
// the generic one, but here the generic is in the return, so
// you will need a cast in the result.
func populateManagedObject(from record: CKRecord, in context: NSManagedObjectContext) -> Promise<CloudObject> {
let generic = populateManagedObject(from record: CKRecord, in context: NSManagedObjectContext)
return generic as! Promise<CloudObject> // In Promises I think this
// will NOT work, and you need .map({$0 as! CloudObject})
}
}

How to add a field to the Moya.Response JSON that wasn't in the real payload from the http response

If I have:
import Moya
import RxSwift
import ObjectMapper
import Moya_ObjectMapper
provider.request(.callApi(id: id))
.mapObject(Thing.self)
.subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background))
.observeOn(MainScheduler.instance)
...
struct Thing: Mappable, Equatable {
var id: String?
init?(map: Map) {
}
mutating func mapping(map: Map) {
id <- map["id"]
}
Making an http api call and getting back json like {"id: "123"} and it's all working great. A new Thing struct is made with the right id. But what if I want to add "flavor" to Thing and hard code {"id: "123", "flavor": "something"}.
i.e. let's just modify the actual http response body and add "flavor": "something" before it gets to the .mapObject method. Where is the right place to tap into that?
And it's not just adding it to the mapping func in Thing because "something" is different for each id. Might be flavor: "something1" and then flavor: "something2". I have this value in the same scope as callApi(id: id) so something like:
provider.request(.callApi(id: id))
.addJSON("flavor", flavor)
.mapObject(Thing.self)
.subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background))
.observeOn(MainScheduler.instance)
But .addJSON is something I just made up. It doesn't exist. But there must be some simple solution for this?
Trying to modify the actual JSON feels dirty and at two low-level. But I've been there so no judging from me if it works. :)
I'd approach it by creating a special version of the Moya.Response extension methods available from Moya_ObjectMapper.
public extension Response {
/// Maps data received from the signal into an object which implements the Mappable protocol.
/// If the conversion fails, the signal errors.
public func mapObject<T: BaseMappable>(_ type: T.Type, context: MapContext? = nil) throws -> T {
guard let object = Mapper<T>(context: context).map(JSONObject: try mapJSON()) else {
throw MoyaError.jsonMapping(self)
}
return object
}
I'd add a similar method but with an additional parameter closure (T) -> (T). So it would essentially return the mapped object after doing another map which would add any necessary information you need into it.

Two same parents in one entity

I making chat backend and I need messages history table which will contain two users.
Is it some way to do something like that right way?
static func prepare(_ database: Database) throws {
try database.create("historys") { history in
history.id()
history.parent(User.self, optional: false)
history.parent(User.self, optional: false)
}
}
Now I'm getting an error of multiple user_id fields.
It should really be possible to set the field name in your preparation; this would be a useful enhancement.
In the meantime, though, you can get the same effect by creating an int field.
static func prepare(_ database: Database) throws {
try database.create("historys") { history in
history.id()
history.int("sender_user_id", optional: false)
history.int("recipient_user_id", optional: false)
}
}
In your model, you'll have properties senderUserId: Node and recipientUserId: Node, and you'll initialise them as e.g. senderUserId = try Node.extract("sender_user_id").
You can then fetch each relation using the following convenience methods on the model:
func sender() throws -> Parent<User> {
return try parent(senderUserId)
}
func recipient() throws -> Parent<User> {
return try parent(recipientUserId)
}