Vapor 4: how to include siblings including extra properties from the pivot table? - swift

I am struggling a lot with how to return a model that contains a many-to-many relationship via a pivot table that contains extra fields. Basically, I want to return the full pivot table with the extra fields, but I can't figure how to do this.
Let's consider the following 3 models: Course, User, and the pivot between them, a Student. The Student model contains the extra field progress.
final class Course: Model, Content {
static let schema = "courses"
#ID(key: .id)
var id: UUID?
#Field(key: "name")
var name: String
init() { }
}
final class Student: Model {
static let schema = "students"
#ID(key: .id)
var id: UUID?
#Parent(key: "course_id")
var course: Course
#Parent(key: "user_id")
var user: User
#Field(key: "progress")
var progress: Int
init() { }
}
final class User: Model, Content {
static let schema = "users"
#ID(key: .id)
var id: UUID?
#Field(key: "name")
var name: String
#Field(key: "private")
var somePrivateField: String
init() { }
}
I have a route like this, which returns an array of courses:
func list(req: Request) throws -> EventLoopFuture<[Course]> {
return Course
.query(on: req.db)
.all()
.get()
}
The resulting JSON looks something like this:
[
{
"id": 1,
"name": "Course 1"
}
]
How can I also include the array of students, so that the end result is something like this?
[
{
"id": 1,
"name": "Course 1",
"students": [
{
"user": {
"id": 1,
"name": "User 1"
},
"progress": 0
},
{
"user": {
"id": 2,
"name": "User 2"
},
"progress": 100
},
]
]
I can add the users to the Course model like this:
#Siblings(through: Student.self, from: \.$course, to: \.$user)
public var users: [User]
And then change my route like this:
func list(req: Request) throws -> EventLoopFuture<[Course]> {
return Course
.query(on: req.db)
.with(\.$user)
.all()
.get()
}
But that only adds the user info to the result, NOT the extra properties on the pivot table (namely, progress). It kinda seems to me that even though pivot tables can have extra properties and the docs even specifically point that out, there are no good ways of actually dealing with this scenario since #Siblings don't point to the pivot at all.
Bonus question: I'd want the User model be mapped to a PublicUser model, so that private/internal fields are not part of the JSON result. See this question for what I mean. I want to do that same thing, but with the Student pivot's User model. Complicated, I know 😬

I encountered the same issue about accessing additional fields in the pivot table, but there is a fairly tidy way of accomplishing this. In addition to your siblings relationship, define a #Children relation from Course to Student. Then, in your query, do a nested with.
Put this in your Course model:
#Children(for:\.$course) var students: [Student]
Query:
let query = Course.query(on: req.db).with(\.$students){ $0.with(\.$user) }.all()
The first with gets the additional fields of the pivot table and then the nested with get the User model.

You can query and use the extra properties on the pivot table directly using the pivots property on the Sibling relation which eagerly loads the pivot table objects through the sibling relation for easy access.
Eg:
Course.query(on: db)
.with(\.$user)
.with(\.$user.$pivots).all().map { course in
// you can now access the loaded students using:
let students = course.$user.pivots
...
}

Related

`find_one` does not give ObjectId in proper format

As the title states, here is the following code.
let users_coll = db
.database("foo")
.collection::<bson::oid::ObjectId>("users");
let user_id = users_coll
.find_one(
doc! { "email": &account.email },
mongodb::options::FindOneOptions::builder()
.projection(doc! { "_id": 1i32 })
.build(),
)
.await?
.unwrap();
But it fails at ? operator with the following mongodb::error::Error,
Error { kind: BsonDeserialization(DeserializationError { message: "expected map containing extended-JSON formatted ObjectId, instead found { \"_id\": ObjectId(\"62af199df4a16d3ea6056536\") }" }), labels: {}, wire_version: None, source: None }
And it is right. Given ObjectId should be in this format,
{
"_id": {
"$oid": "62af199df4a16d3ea6056536"
}
}
But I do not know how to handle this. Any help is appreciated.
Have a good day!
Your users collection isn't a collection of ObjectIds, it's actually a collection of documents which each contain an ObjectId. To let Rust know what to do with those, you should create a struct which represents the document, or at least the parts which you care about getting back from your query, and tell your collection to de-serialize into that struct:
use mongodb::bson::oid::ObjectId;
use serde::{Serialize, Deserialize};
#[derive(Debug, Default, Serialize, Deserialize)]
struct User {
_id: ObjectId,
}
#[tokio::main]
async fn main() {
let users_coll = db
.database("foo")
.collection::<User>("users");
let user_id: ObjectId = users_coll
.find_one(
doc! { "email": &account.email },
mongodb::options::FindOneOptions::builder()
.projection(doc! { "_id": 1i32 })
.build(),
)
.await?
.unwrap()
._id;
}
By default, the BSON fields have to match the struct fields exactly (_id in this case), but I'm pretty sure serde has a way to change that if you don't like the leading underscore.

SwiftUI: Trying to set my CoreData model for decoded JSON

I have a small app that doesn't have persistence. I have JSON file of 100+ countries as part of the app:
Countries.json (only included 1 for brevity)
[
{
"id": 0,
"display_name": "Algeria",
"searchable_names": [
"algeria"
],
"image": "algeria",
"latitude": 36.753889,
"longitude": 3.058889,
"favorite": false,
"extended": false
}
]
and my country model looks like this:
Country.swift
import Foundation
struct Country: Codable, Identifiable {
var id: Int
let display_name: String
let searchable_names: [String]
let image: String
let latitude: Double
let longitude: Double
var favorite: Bool
var extended: Bool
}
and I'd decode the JSON like this:
#State var countries: [Country] = Bundle.main.decode("Countries.json")
Now, I am wanting to add a way to persist data so that if a user togglers favorite, that country will stay marked as a favorite on subsequent app visits.
I am having trouble in how to translate this over to use CoreData. I tried making a CoreData model called Country and added each property in but I feel like that is not correct. I basically need a way to add this decoded JSON to CoreData so that I could manipulate a single countries favorite value. I am bit lost at this point in how to accomplish this with a CoreData model.

Retrieving records on a Reference List (hierarchical records) - Swift Concurrency & CloudKit

I am using CloudKit (the default public database) to store information about matches between two teams. I am trying to use the Swift Concurrent approach with async/await to retrieve the match and team data.
The matches are stored as “Match” RecordType with some metadata, like the year the match was played (“matchYear”). The “Match” Record Type also contains a Reference List to a RecordType called “Team” (two teams for each match). The team names are stored in the “Team” Record Type (field “teamName”)
Rough description of my CloudKit Schema
“Match” Record Type
Fields:
matchYear: Int64
teams: Reference (List)
…
“Team” Record Type
Fields:
teamName: String
…
With the following code I am able to read all “Match” records from the CloudKit and store that list to an array of MatchRecords, which I can then use to display a list of matches (using SwiftUI, if that makes any difference). The resulting array also contains references to the teams for each match.
struct MatchRecord {
var recordID: CKRecord.ID
var matchYear: Int
var teams: [CKRecord.Reference]
…
}
…
private lazy var container = CKContainer.default()
private lazy var database = container.publicCloudDatabase
…
#MainActor func loadMatchList() async throws -> [MatchRecord] {
let predicate = NSPredicate(value: true)
let sortDescriptors = [NSSortDescriptor(key: “matchYear", ascending: false),NSSortDescriptor(key: "___createTime", ascending: false)]
let query = CKQuery(recordType: “Match”, predicate: predicate)
query.sortDescriptors = sortDescriptors
let (matchResults, _) = try await database.records(matching: query)
let allMatches: [MatchRecord] = matchResults
.compactMap { _, result in try? result.get() }
.compactMap {
let match = MatchRecord(
recordID: $0.recordID,
matchYear: $0[“matchYear"] as! Int,
teams: $0["teams"] as! [CKRecord.Reference])
// Potentially team record retrieval goes here…
return match
}
return allMatches
}
How do I retrieve the Team records as part of this async function so that I will have also the names of the teams available for the list view?
(I could potentially first fetch the list of matches and then loop through that array and retrieve the detail data for each match but that seems wasteful. I am guessing there should be a way to insert this in the compactMap closure marked down in the code sample above, but my map/reduce and async/await skills fail me…)
The data structure could be something along as described below.
struct MatchRecord {
var recordID: CKRecord.ID
var matchYear: Int
var teams: [TeamRecord]
…
}
struct TeamRecord {
var recordID: CKRecord.ID
var teamName: String
…
}
FWIW I know that as there are only two teams for each game, I could also store the team names as part of the Match record, but In the future I am planning to also include the roster information for each team, so I need to come up with a clean and scalable method to retrieve this type of hierarchical data from CloudKit…
Using CoreData with CloudKit is not an option here.
After some thinking I came up with one somewhat ugly solution using async properties. The Match and Team struct definitions are roughly these
struct MatchRecord {
var recordID: CKRecord.ID
var matchYear: Int
var teams: [TeamRecord]
…
}
struct TeamRecord {
var referenceID: CKRecord.Reference
var name: String {
get async {
try! await CKContainer.default().publicCloudDatabase.record(for: referenceID.recordID)["teamName"] as! String
}
}
}
The compactMap gets small for loop in there to populate the teams structure (only storing the ID as the names are retrieved only later)
.compactMap {
let match = MatchRecord(
recordID: $0.recordID,
matchYear: $0[“matchYear"] as! Int,
teams: [])
for item in ($0["teams"] as! [CKRecord.Reference]) {
match.teams.append(TeamRecord(referenceID: item))
}
return match
}
And when displaying the list, the list rows are defined in a separate view, where the async properties are pulled in using a task. Along these lines
struct MatchRow: View {
...
#State var team0: String = ""
#State var team1: String = ""
var body: some View {
...
Text(verbatim: "\(match.matchYear): \(team0) - \(team2)")
.task{
guard team0.isEmpty else { return }
let (team0Result, team1Result) = await (match.teams[0].name, match.teams[1].name)
team0 = team0Result
team1 = team1Result
}
}
}
}
I am sure there is a more elegant solution for this. I especially do not like the idea of adding the task in the list row view as that splits the logic in many places...

Realm filter results based on values in child object list

This is how my Realm objects look:
class Restaurant: Object {
#objc dynamic var name: String? = nil
let meals = List<Meal>()
}
class Meal: Object {
#objc dynamic var mealName: String? = nil
let tag = RealmOptional<Int>()
}
I'm trying to fetch all meals that have some tags (I know I can filter all Realm objects of type Meal for specific tags), but the goal is to fetch all Restaurant objects and filter it's Meal child objects based on tag values.
I tried filtering like this:
restaurants = realm.objects(Restaurant.self).filter("meals.#tags IN %#", selectedTags)
but this won't work. Is there a way to filter results based on values in child object list?
To clarify the question, this is an example how filtering should work
for selectedTags = [1, 2, 3]
This is the whole Restaurant model that is saved in Realm.
[Restaurant {
name = "Foo"
meals = [
Meal {
mealName = "Meal 1"
tag = 1
},
Meal {
mealName = "Meal 2"
tag = 2
},
Meal {
mealName = "Meal 7"
tag = 7
}
]
}]
Filtering should return this:
[Restaurant {
name = "Foo"
meals = [
Meal {
mealName = "Meal 1"
tag = 1
},
Meal {
mealName = "Meal 2"
tag = 2
}
]
}]
Here's one possible solution - add a reverse refererence to the restaurant for each meal object
class Restaurant: Object {
#objc dynamic var name: String? = nil
let meals = List<Meal>()
}
class Meal: Object {
#objc dynamic var mealName: String? = nil
let tag = RealmOptional<Int>()
#objc dynamic var restaurant: Restaurant? //Add this
}
then query the meals for that restaurant with the tags you want.
let results = realm.objects(Meal.self).filter("restaurant.name == %# AND tag IN %#", "Foo", [1,2])
LinkingObjects could also be leveraged but it depends on what kind of queries will be needed and what the relationships are between Restaurants and Meals - I am assuming 1-Many in this case.
if you want ALL restaurants, then LinkingObjects is the way to go.
Edit:
Thought of another solution. This will work without adding a reference or an inverse relationship and will return an array of restaurants that have meals with the selected tags.
let selectedTags = [1,2]
let results = realm.objects(Restaurant.self).filter( {
for meal in $0.meals {
if let thisTag = meal.tag.value { //optional so safely unwrap it
if selectedTags.contains(thisTag) {
return true //the tag for this meal was in the list, return true
}
} else {
return false //tag was nil so return false
}
}
return false
})
In short, you cannot do what you are asking. Not within a Realm query (and therefore benefit from update notifications if that is important) at least. No doubt you can make some kind of structure containing what you want though via non-Realm filtering.
To better answer, let's first consider what you're trying to produce as a query result. As you say, your attempt above won't work. But you're trying to filter Restaurants by having some Meals matching some criteria; this is probably achievable, but your resulting query on Restaurant type would then produce a list of Restaurants. Each restaurant would still have a natural property of all its Meals, and would require the same filter applied again to the meals.
It makes sense though to add a function (if you need the search criteria to be dynamic, use a computed property if the filter is always the same tags) to the Restaurant class that produces a view of its Meals matching your criteria.
e.g.
extension Restaurant
{
var importantMeals : Results<Meal>
{
return meals.filter(...)
}
}
So I think there are two options.
Iterate through all Restaurant objects, and add it to a data structure of your own (Set or array) if its importantMeals property is not empty. Then use the same property to produce the meal list when needed. Or you could use a non-Realm filter to produce that query for you. E.g. realm.objects(Restaurant.self).compactMap {$0}.filter { !$0.importantMeals.isEmpty }
Alternatively, filter all Meals according to your criteria (realm.objects(Meal.self).filter(...)). You could then add a LinkingObjects property to your Meal class to make the Set of Restaurants with relevant Meals.
The correct approach will depend on how you want to use the results, but I'd suggest approach 1 will see you right. Note that you might want to sort the results produced by queries before using if order is of any importance to you (e.g. for displaying in UITableView) as there is no guarantee that the order of objects will be the same for each query performed.

Realm query problem with sorted localized data

Consider the following Realm models:
class Fruit: Object {
#objc dynamic var name = ""
let localizations = List<Localization>()
/**
Returns the localized name of the fruit matching the preferred language of the app
or self.name if the fruit does not have a localization matching the user preferred language codes.
*/
var localizedName: String? {
guard !Locale.isPreferredLanguageDefaultAppLanguage else { return self.name }
let preferredLanguagesCodes = Locale.preferredLanguagesCodes
let localizations = preferredLanguagesCodes.compactMap({ languageCode in Array(self.localizations).filter({ $0.languageCode == languageCode }).first })
return localizations.first?.localizedName ?? self.name
}
}
class Localization: Object {
#objc dynamic var localizedName: String = ""
#objc dynamic var languageCode: String = ""
}
Let's say I have 2 fruits in my database (represented in JSON format for the sake of simplicity):
[
{
"name": "Apple",
"localizations": [
{
"localizedName": "Pomme",
"languageCode": "fr"
}
]
},
{
"name": "Banana",
"localizations": [
{
"localizedName": "Banane",
"languageCode": "fr"
}
]
}
]
Now I want to get all the fruits in my database, and sort them alphabetically by their localizedName.
var localizedNameSortingBlock: ((Fruit, Fruit) -> Bool) = {
guard let localizedNameA = $0.localizedName, let localizedNameB = $1.localizedName else { return false }
return localizedNameA.diacriticInsensitive < localizedNameB.diacriticInsensitive
}
let sortedFruits = Array(Realm().objects(Fruit.self)).sorted(by: localizedNameSortingBlock)
If the first preferred language of my device is "English", I get this:
Apple
Banana
If it's set to "French":
Banane
Pomme
It's quite simple, but this solution has a major inconvenient:
By casting the Results<Fruit> collection into an array, I'm loosing the ability to get live updates via Realm's notification token system.
The problem is, I can't sort using NSPredicate directly on the Results collection, because the localizedName property is a computed property, thus is ignored by Realm.
I thought about writing the localizedName value directly into the name property of the Fruit object, but doing so requires to loop through all fruits and change their name whenever the user change of preferred language. There must be a better way.
So my question is:
Is there a way to retrieve all the fruits in my database, get them sorted by their localizedName, without loosing the ability to receive batch updates from Realm?