Is there a way to handle inheritance using SimpleSchema? - meteor

I have a basic collection called Tasks. I want to create a new Collection called DailyTasks, where a DailyTask extends a Task, and also has a list of Dates on which the DailyTask was completed.
I want Tasks.findAll() to return Tasks and DailyTasks.
I would be willing to have three classes: BaseTask, OneTimeTask (which has a single dateCompleted field) and DailyTask (which has a list of datesCompleted). I would need to know how to configure my schema accordingly.
How can I do this?

You can pass multiple schemas to the SimpleSchema constructor, and they will be combined. Like so:
Tasks = new SimpleSchema({ ... });
DailyTasks = new SimpleSchema([Tasks, {
additionalField: {
type: String
}
}]);

Related

How to combine data model types with document ids?

I'm working with Firestore and Typescript.
For the data models I have types definitions. For example User could be this:
interface User {
name: string;
age: number;
}
The users are stored in the database in the users collection under a unique name/id.
In Firebase when you query a collection, the ids of the documents are available on the document reference, and do not come with the data. In a common use-case for front-end, you want to retrieve an array of records with their ids, because you probably want to interact with them and need to identify each.
So I made a query similar to the code below, where the id is merged into the resulting array:
async function getUsers(): Promise<any[]> {
const query = db.collection("users")
const snapshot = await query.get();
const results = snapshot.docs.map(doc => {
return { ...doc.data(), id: doc.id };
});
}
Now the problem is, that I have a User type, but it can't be used here because it does not contain an id field.
A naïve solution could be to create a new type:
interface UserWithId extends User {
id: string
}
And write the function like:
async function getUsers(): Promise<UserWithId[]> {}
But this doesn't feel right to me, because you would have to potentially do this for many types.
A better solution I think would be to create a generic type:
type DatabaseRecord<T> = {
id: string,
data: T
}
Thus keeping data and ids separate in the returning results:
const results = snapshot.docs.map(doc => {
return { data: doc.data(), id: doc.id };
});
... and use the function signature:
async function getUsers(): Promise<DatabaseRecord<User>[]> {}
I would favour the second over the first solution, because creating new types for each case feels silly. But I am still not sure if that is the best approach.
This seems like such a common scenario but I didn't manage to find any documentation on this. I have seen developers simply write the id in the model data, essentially duplicating the document name in its data, but that to me seems like a big mistake.
I can imagine that if you don't use Typescript (of Flow) that you just don't care about the resulting structure and simply merge the id with the data, but this is one of the reasons I really love using type annotation in JS. It forces you think more about your data and you end up writing cleaner code.

How to modify an already constructed schema

I'm using a third party package that defines a schema like this:
People.schema = new SimpleSchema({
firstName: {
type: String,
optional: false
}
//Many other fields defined...
});
I would like to modify it to have optional: true for the first name without changing the source code for the third party package.
I could use People.schema.pick to get a schema with all of the fields except the firstName, and then combine this with another schema with firstName as optional. But this method would require listing all of the fields in the schema in the pick function, which is tedious.
Is there a better way of accomplishing this?
I can edit the object simple schema creates just like any other object:
People.schema._schema.firstName.optional = true.
To override the field.

Meteor: Publish a subset of another publication

I have a custom publication on my server (which in some way join 2 collections).
This resulting set of this publication is exactly what I need but for performances issues I would like to avoid sending it entirely to the client.
If I did not care about performances, I would only subscribe to the
publication and do something like
theCollection.find({"my":"filter"})
I am therefore trying to find a way to publish a subset of the custom publication so that the filter would be applied on the custom publication on the server side.
Is there a way to chain or filter publications (server side) ?
For the question we can assume the custom publication to look like this and cannot be modified:
Meteor.publish('customPublication', function() {
var sub = this;
var aCursor = Resources.find({type: 'someFilter'});
Mongo.Collection._publishCursor(aCursor, sub, 'customPublication');
sub.ready();
});
if i understand the question right, you are looking for https://atmospherejs.com/reywood/publish-composite
It let's you "publish a set of related documents from various collections using a reactive join. This makes it easy to publish a whole tree of documents at once. The published collections are reactive and will update when additions/changes/deletions are made."
Ok I came to the following workaround. Instead of working on the publication, I simply added a new collection I update according to the other collections. In order to do so I am using the meteor hooks package
function transformDocument(doc)
{
doc.aField = "aValue"; // do what you want here
return doc;
}
ACollection.after.insert(function(userId, doc)
{
var transformedDocument = transformDocument(doc);
AnotherCollection.insert(transformedDocument);
});
ACollection.after.update(function(userId, doc, fieldNames, modifier, options)
{
var transformedDocument = transformDocument(doc);
delete transformedDocument._id;
AnotherCollection.update(doc._id,{$set:transformedDocument});
});
ACollection.after.remove(function(userId, doc)
{
AnotherCollection.remove(doc._id);
});
Then I have the new collection I can publish subsets the regular way
Benefits:
You can filter whatever you want into this db, no need to worry if the field is virtual or real
Only one operation every time a db changes. This avoid having several publication merging the same data
Cave eats:
This requires one more Collection = more space
The 2 db might not be always synchronised, there is few reasons for this:
The client manually changed the data of "AnotherCollection"
You had documents in "ACollection" before you added "AnotherCollection".
The transform function or source collection schema changed at some point
To fix this:
AnotherCollection.allow({
insert: function () {
return Meteor.isServer;
},
update: function () {
return Meteor.isServer;
},
remove: function () {
return Meteor.isServer;
}
});
And to synchronise at meteor startup (i.e. build the collection from scratch). Do this only once for maintenance or after adding this new collection.
Meteor.startup(function()
{
AnotherCollection.remove({});
var documents = ACollection.find({}).fetch();
_.each(documents, function(doc)
{
var transformedDocument = transformDocument(doc);
AnotherCollection.insert(transformedDocument);
});
});

Get Meteor collection by name

Suppose I write:
new Meteor.Collection("foos");
new Meteor.Collection("bars");
Is there an API for accessing those collections by name? Something like Meteor.Collection.get(name), where name is "foos" or "bars"? I know I could write something like
var MyCollections = {
foos: new Meteor.Collection("foos");
bars: new Meteor.Collection("bars");
}
and then use MyCollections[name], but I'd prefer to use an existing API if one exists.
Based on Shane Donelley's mongoinspector
https://github.com/shanedonnelly1/mongoinspector
getCollection = function (string) {
for (var globalObject in window) {
if (window[globalObject] instanceof Meteor.Collection) {
if (globalObject === string) {
return (window[globalObject]);
break;
};
}
}
return undefined; // if none of the collections match
};
I've just found that package : https://github.com/dburles/mongo-collection-instances/
It allow you to
Foo1 = new Mongo.Collection('foo'); // local
Foo2 = new Mongo.Collection('foo', { connection: connection });
Mongo.Collection.get('foo') // returns instance of Foo1
Mongo.Collection.get('foo', { connection: connection });
// returns instance of Foo2
Hope it will help
This feature was added to Meteor in Feb 2016: "Provide a way to access collections from stores on the client"
It works like this:
Meteor.connection._stores['tasks']._getCollection();
And I was using it as follows to test inserts using the javascript console:
Meteor.connection._stores['tasks']._getCollection().insert({text:'test'});
For the insert it required the insecure package to still be installed otherwise got an access denied message.
As far as I can see in the collection.js source there currently is no way in the api to get an existing Collection by name, once it has already been initialized on the server. It probably wouldn't be hard to add that feature.
So, why not fork Meteor and submit a patch or create a smart package and share it I'm sure there are others out there who'd like the same feature.
With https://github.com/dburles/mongo-collection-instances you can use Mongo.Collection.get('collectionname')
Note that the parameter you're inserting is the same one you use when creating the collection. So if you're using const Products = new Mongo.Collection('products') then you should use get('products') (lowercase).
Note that they have a return value, so you can just do
var Patterns = new Meteor.Collection("patterns");
and use Patterns everywhere.
And when you need to subscribe to server updates, provide "patterns" to Meteor.subscribe().
If you have the same code for multiple collections, the chance is high that you're doing something wrong from a software engineering viewpoint; why not use a single collection with a type field (or something else that differentiates the documents) and use that instead of using multiple collections?
Rather than looking, I've just been doing:
Foos = new Meteor.Collection("foos");
or possibly put it inside another object. I haven't really been making a Collections collection object.
It seems there is no way to get at the wrapped Meteor.Collection object without saving it at creation time, as others have mentioned.
But there is at least a way to list all created collections, and actually access the corresponding Mongo LocalCollection object. They are available from any Meteor Collection object, so to keep it generalistic you can create a dummy collection just for this. Use a method as such (CoffeeScript):
dummy = new Meteor.Collection 'dummy'
getCollection = (name) ->
dummy._driver.collections[name]
These objects do have all the find, findOne, update et al methods, and even some that Meteor doesn't seem to expose, like pauseObservers and resumeObservers which seem interesting. But I haven't tried fiddling with this mongo LocalCollection reference directly to knowif it will update the server collection accordingly.
var bars = new Meteor.Collection("foos");
Judging by what the collection.js does, the line we use to instantiate the collection object opens a connection to the database and looks for the collection matching the name we give. So in this case a connection is made and the collection 'foos' is bound to the Meteor.Collection object 'bars'. See collection.js AND remote_collection_driver.js within the mongo-livedata package.
As is the way with MongoDB, whilst you can, you don't have to explicitly create collections. As stated in the MongoDB documentation:
A collection is created when the first document is inserted.
So, I think what you're after is what you already have - unless I've totally misunderstood what you're intentions are.
You can always roll your own automatic collection getter.
Say you have a couple of collections called "Businesses" and "Clients". Put a reference each into some "collections" object and register a Handlebars helper to access those "collections" by collections["name"].
i.e. put something like this on the client-side main.js:
collections = collections || {};
collections.Businesses = Businesses;
collections.Clients = Clients;
Handlebars.registerHelper("getCollection", function(coll) {
return collections[coll].find();
});
Then in your HTML, just refer to the collection by name:
{{#each getCollection 'Businesses'}}
<div> Business: {{_id}} </div>
{{/each}}
{{#each getCollection 'Clients'}}
<div> Client: {{_id}} </div>
{{/each}}
Look ma, no more generic "list all records" boilerplate js required!

How to work with async code in Mongoose virtual properties?

I'm trying to work with associating documents in different collections (not embedded documents) and while there is an issue for that in Mongooose, I'm trying to work around it now by lazy loading the associated document with a virtual property as documented on the Mongoose website.
The problem is that the getter for a virtual takes a function as an argument and uses the return value for the virtual property. This is great when the virtual doesn't require any async calls to calculate it's value, but doesn't work when I need to make an async call to load the other document. Here's the sample code I'm working with:
TransactionSchema.virtual('notebook')
.get( function() { // <-- the return value of this function is used as the property value
Notebook.findById(this.notebookId, function(err, notebook) {
return notebook; // I can't use this value, since the outer function returns before we get to this code
})
// undefined is returned here as the properties value
});
This doesn't work since the function returns before the async call is finished. Is there a way I could use a flow control library to make this work, or could I modify the first function so that I pass the findById call to the getter instead of an anonymous function?
You can define a virtual method, for which you can define a callback.
Using your example:
TransactionSchema.method('getNotebook', function(cb) {
Notebook.findById(this.notebookId, function(err, notebook) {
cb(notebook);
})
});
And while the sole commenter appears to be one of those pedantic types, you also should not be afraid of embedding documents. Its one of mongos strong points from what I understand.
One uses the above code like so:
instance.getNotebook(function(nootebook){
// hey man, I have my notebook and stuff
});
While this addresses the broader problem rather than the specific question, I still thought it was worth submitting:
You can easily load an associated document from another collection (having a nearly identical result as defining a virtual) by using Mongoose's query populate function. Using the above example, this requires specifying the ref of the ObjectID in the Transaction schema (to point to the Notebook collection), then calling populate(NotebookId) while constructing the query. The linked Mongoose documentation addresses this pretty thoroughly.
I'm not familiar with Mongoose's history, but I'm guessing populate did not exist when these earlier answers were submitted.
Josh's approach works great for single document look-ups, but my situation was a little more complex. I needed to do a look-up on a nested property for an entire array of objects. For example, my model looked more like this:
var TransactionSchema = new Schema({
...
, notebooks: {type: [Notebook]}
});
var NotebookSchema = new Schema({
...
, authorName: String // this should not necessarily persist to db because it may get stale
, authorId: String
});
var AuthorSchema = new Schema({
firstName: String
, lastName: String
});
Then, in my application code (I'm using Express), when I get a Transaction, I want all of the notebooks with author last name's:
...
TransactionSchema.findById(someTransactionId, function(err, trans) {
...
if (trans) {
var authorIds = trans.notebooks.map(function(tx) {
return notebook.authorId;
});
Author.find({_id: {$in: authorIds}, [], function(err2, authors) {
for (var a in authors) {
for (var n in trans.notebooks {
if (authors[a].id == trans.notebooks[n].authorId) {
trans.notebooks[n].authorLastName = authors[a].lastName;
break;
}
}
}
...
});
This seems wildly inefficient and hacky, but I could not figure out another way to accomplish this. Lastly, I am new to node.js, mongoose, and stackoverflow so forgive me if this is not the most appropriate place to extend this discussion. It's just that Josh's solution was the most helpful in my eventual "solution."
As this is an old question, I figured it might use an update.
To achieve asynchronous virtual fields, you can use mongoose-fill, as stated in mongoose's github issue: https://github.com/Automattic/mongoose/issues/1894

Resources