Code Monkey home page Code Monkey logo

meteor-peerdb's Introduction

PeerDB

Meteor smart package which provides a reactive database layer with references, generators, triggers, migrations, etc. Meteor provides a great way to code in a reactive style and this package brings reactivity to your database as well. You can now define inside your application along with the rest of your program logic also how your data should be updated on any change and how various aspects of your data should be kept in sync and consistent no matter where the change comes from.

Implemented features are:

  • reactive references between documents
  • reactive reverse references between documents
  • reactive auto-generated fields from other fields
  • reactive triggers
  • migrations

Planned features are:

Adding this package to your Meteor application adds the Document object into the global scope.

Both client and server side.

Installation

meteor add peerlibrary:peerdb

Additional packages

Documents

Instead of Meteor collections with PeerDB you are defining PeerDB documents by extending Document. Internally it defines a Meteor collection, but also all returned documents are then an instance of that PeerDB documents class.

Minimal definition:

class Person extends Document
  @Meta
    name: 'Person'

This would create in your database a MongoDB collection called Persons. name must match the class name. @Meta is used for PeerDB and in addition you can define arbitrary class or object methods for your document which will then be available on documents returned from the database:

class Person extends Document
  # Other fields:
  #   username
  #   displayName
  #   email
  #   homepage

  @Meta
    name: 'Person'

  # Class methods
  @verboseName: ->
    @Meta._name.toLowerCase()

  @verboseNamePlural: ->
    "#{ @verboseName() }s"

  # Instance method
  getDisplayName: ->
    @displayName or @username

You can also wrap existing Meteor collections:

class User extends Document
  @Meta
    name: 'User'
    collection: Meteor.users

And if you need to access the internal or wrapped collection you can do that by:

Person.Meta.collection._ensureIndex
  username: 1

Querying

PeerDB provides an alternative to Meteor collections query methods. You should be using them to access documents. You can access them through the documents property of your document class. For example:

Person.documents.find({}).forEach (person, i, cursor) =>
  console.log person.constructor.verboseName(), person.getDisplayName()

Person.documents.findOne().getDisplayName()

Person.documents.findOne().email

The functions and arguments available are the same as those available for Meteor collections, with the addition of:

  • .documents.exists(query, options) – efficient check if any document matches given query
  • .documents.bulkInsert(arrayOfDocuments, [options], callback) – insert multiple documents in bulk, returning the list of IDs and calling an optional callback

bulkInsert has a special handling of references to minimize issues of loading documents referencing documents which are yet to be inserted. By default, first, all documents are inserted with all optional references delayed. This means, all optional references are first omitted, and then all documents are updated by the second query, setting values for all optional references. Reference fields inside arrays are always delayed. Optional options object accepts field:

  • dontDelay – a list of paths of optional reference fields which should not be delayed

In a similar way we extend the cursor returned from .documents.find(...) with an exists method which operates similar to the count method, only that it is more efficient:

Person.documents.exists({})
Person.documents.find({}).exists()

Person.Meta gives you back document metadata and Person.documents give you access to all documents.

All this is just an easy way to define documents and collections in a unified fashion, but it becomes interesting when you start defining relations between documents.

References

In the traditional SQL world of relational databases you do joins between related documents every time you read them from the database. This makes reading slower, your database management system is redoing the same computation of joins for every read, and also horizontal scaling of a database to many instances is harder because every read might potentially have to talk to other instances.

NoSQL databases like MongoDB remove relations between documents and leave it to users to resolve relations on their own. This often means fetching one document, observing which other documents it references, and fetching those as well. Because each of those documents are stand-alone and static, it is relatively easy and quick for a database management system like MongoDB to find and return them. Such an approach is quick and it scales easily, but the downside is the multiple round trips you have to do in your code to get all documents you are interested in. Those round trips become even worse when those queries are coming over the Internet from Meteor client code, because Internet latency is much higher.

For a general case you can move this fetching of related documents to the server side into Meteor publish functions by using libraries like meteor-related. It provides an easy way to fetch related documents reactively, so when dependencies change, your published documents will be updated accordingly. While latency to your database instances is hopefully better on your server, we did not really improve much from the SQL world: you are effectively recomputing joins and now even in a much less efficient way, especially if you are reading multiple documents at the same time.

Luckily, in many cases we can observe that we are mostly interested only in few fields of a related document, again and again. Instead of recomputing joins every time we read, we could use MongoDB's sub-documents feature to embed those fields along with the reference. Instead of just storing the _id of a related document, we could store also those few often used fields. For example, if you are displaying blog posts, you want to display the author's name together with the blog post. You won't really need only the blog post without the author name. An example blog post document could then look like:

{
  "_id": "frqejWeGWjDTPMj7P",
  "body": "A simple blog post",
  "author": {
    "_id": "yeK7R5Lws6MSeRQad",
    "username": "wesley",
    "displayName": "Wesley Crusher"
  },
  "subscribers": [
    {
      "_id": "k7cgWtxQpPQ3gLgxa"
    },
    {
      "_id": "KMYNwr7TsZvEboXCw"
    },
    {
      "_id": "tMgj8mF2zF3gjCftS"
    }
  ],
  "reviewers": [
    {
      "_id": "tMgj8mF2zF3gjCftS",
      "username": "deanna",
      "displayName": "Deanna Troi"
    }
  ]
}

Great! Now we have to fetch only this one document and we have everything needed to display a blog post. It is easy for us to publish it with Meteor and use it as any other document, with direct access to author's fields.

Now, storing the author's name along with every blog post document brings an issue. What if user changes their name? Then you have to update all those fields in documents referencing the user. So you would have to make sure that anywhere in your code where you are changing the name, you are also updating fields in references. What about changes to the database coming from outside of your code? Here is when PeerDB comes into action. With PeerDB you define those references once and then PeerDB makes sure they stay in sync. It does not matter where the changes come from, it will detect them and update fields in referenced sub-documents accordingly.

If we have two documents:

class Person extends Document
  # Other fields:
  #   username
  #   displayName
  #   email
  #   homepage

  @Meta
    name: 'Person'

class Post extends Document
  # Other fields:
  #   body

  @Meta
    name: 'Post'
    fields: ->
      # We can reference other document
      author: @ReferenceField Person, ['username', 'displayName']
      # Or an array of documents
      subscribers: [@ReferenceField Person]
      reviewers: [@ReferenceField Person, ['username', 'displayName']]

We are using @Meta's fields argument to define references.

In the above definition, the author field will be a subdocument containing _id (always added) and the username and displayName fields. If the displayName field in the referenced Person document is changed, the author field in all related Post documents will be automatically updated with the new value for the displayName field.

Person.documents.update 'tMgj8mF2zF3gjCftS',
  $set:
    displayName: 'Deanna Troi-Riker'

# Returns "Deanna Troi-Riker"
Post.documents.findOne('frqejWeGWjDTPMj7P').reviewers[0].displayName

# Returns "Deanna Troi-Riker", sub-documents are objectified into document instances as well
Post.documents.findOne('frqejWeGWjDTPMj7P').reviewers[0].getDisplayName()

The subscribers field is an array of references to Person documents, where every element in the array will be a subdocument containing only the _id field.

Circular references are possible as well:

class CircularFirst extends Document
  # Other fields:
  #   content

  @Meta
    name: 'CircularFirst'
    fields: ->
      # We can reference circular documents
      second: @ReferenceField CircularSecond, ['content']

class CircularSecond extends Document
  # Other fields:
  #   content

  @Meta
    name: 'CircularSecond'
    fields: ->
      # But of course one should not be required so that we can insert without warnings
      first: @ReferenceField CircularFirst, ['content'], false

If you want to reference the same document recursively, use the string 'self' as an argument to @ReferenceField.

class Recursive extends Document
  # Other fields:
  #   content

  @Meta
    name: 'Recursive'
    fields: ->
      other: @ReferenceField 'self', ['content'], false

All those references between documents can be tricky as you might want to reference documents defined afterwards and JavaScript symbols might not even exist yet in the scope, and PeerDB works hard to still allow you to do that. But to make sure all symbols are correctly resolved you should call Document.defineAll() after all your definitions. The best is to put it in the filename which is loaded last.

One more example to show use of nested objects:

class ACLDocument extends Document
  @Meta
    name: 'ACLDocument'
    fields: ->
      permissions:
        admins: [@ReferenceField User]
        editors: [@ReferenceField User]

You can also do:

class ACLDocument extends Document
  # Each permission object inside "permissions" could have also
  # timestamp and permission type fields.

  @Meta
    name: 'ACLDocument'
    fields: ->
      permissions: [
        user: @ReferenceField User
        grantor: @ReferenceField User, [], false
      ]

ReferenceField accepts the following arguments:

  • targetDocument – target document class, or 'self'
  • fields – list of fields to sync in a reference's sub-document; instead of a field name you can use a MongoDB projection as well, like emails: {$slice: 1}
  • required – should the reference be required (default) or not. If required, when the referenced document is removed, this document will be removed as well. If not required, the reference will be set to null.
  • reverseName – name of a field for a reverse reference; specify to enable a reverse reference
  • reverseFields – list of fields to sync for a reference reference

What are reverse references?

Reverse references

Sometimes you want also to have easy access to information about all the documents referencing a given document. For example, for each author you might want to have a list of all blog posts they wrote, as part of their document.

class Post extends Post
  @Meta
    name: 'Post'
    replaceParent: true
    fields: (fields) ->
      fields.author = @ReferenceField Person, ['username', 'displayName'], true, 'posts'
      fields

We redefine the Post document and replace it with a new definition which enables reverse references for the author field. Now Person.documents.findOne('yeK7R5Lws6MSeRQad') returns:

{
  "_id": "yeK7R5Lws6MSeRQad",
  "username": "wesley",
  "displayName": "Wesley Crusher",
  "email": "[email protected]",
  "homepage": "https://gww.enterprise.starfleet/~wesley/",
  "posts": [
    {
      "_id": "frqejWeGWjDTPMj7P"
    }
  ]
}

Auto-generated fields

Sometimes you need fields in a document which are based on other fields. PeerDB allows you an easy way to define such auto-generated fields:

class Post extends Post
  # Other fields:
  #   title

  @Meta
    name: 'Post'
    replaceParent: true
    generators: (generators) ->
      generators.slug = @GeneratedField 'self', ['title'], (fields) ->
        unless fields.title
          [fields._id, undefined]
        else
          [fields._id, "prefix-#{ fields.title.toLowerCase() }-suffix"]
      generators

The last argument of GeneratedField is a function which receives an object populated with values based on the list of fields you are interested in. In the example above, this is one field named title from the Posts collection. The _id field is always available in fields. Generator function receives just _id when document containing fields is being removed. Otherwise it receives all fields requested. Generator function should return two values, a selector (often just the ID of a document) and a new value. If the value is undefined, the auto-generated field is removed. If the selector is undefined, nothing is done.

You can define auto-generated fields across documents. Furthermore, you can combine reactivity. Maybe you want to also have a count of all posts made by a person?

class Person extends Person
  @Meta
    name: 'Person'
    replaceParent: true
    generators: (generators) ->
      generators.postsCount = @GeneratedField 'self', ['posts'], (fields) ->
        [fields._id, fields.posts?.length or 0]
      generators

Triggers

You can define triggers which are run every time any of the specified fields changes:

class Post extends Post
  # Other fields:
  #   updatedAt

  @Meta
    name: 'Post'
    replaceParent: true
    triggers: ->
      updateUpdatedAt: @Trigger ['title', 'body'], (newDocument, oldDocument) ->
        # Don't do anything when document is removed
        return unless newDocument?._id

        timestamp = new Date()
        Post.documents.update
          _id: newDocument._id
          updatedAt:
            $lt: timestamp
        ,
          $set:
            updatedAt: timestamp

The return value is ignored. newDocument and oldDocument can be null when a document has been removed or added, respectively. Triggers are useful when you want arbitrary code to be run when fields change. This could be implemented directly with observe, but triggers simplify that and provide an alternative API in the PeerDB spirit.

Why we are using a trigger here and not an auto-generated field? The main reason is that we want to ensure updatedAt really just increases, so a more complicated update query is needed. Additionally, reference fields and auto-generated fields should be without side-effects and should be allowed to be called at any time. This is to ensure that we can re-sync any broken references as needed. If you would use an auto-generated field, it could be called again at a later time, updating updatedAt to a later time without any content of a document really changing.

PeerDB does not really re-sync any broken references (made while your Meteor application was not running) automatically. If you believe such references exist (eg., after a hard crash of your application), you can trigger re-syncing by calling Document.updateAll(). All references will be resynced and all auto-generated fields rerun. But not triggers. It is a quite heavy operation.

Abstract documents and replaceParent

You can define abstract documents by setting the abstract Meta flag to true. Such documents will not create a MongoDB collection. They are useful to define common fields and methods you want to reuse in multiple documents.

We skimmed over replaceParent before. You should set it to true when you are defining a document with the same name as a document you are extending (parent). It is a kind of a sanity check that you know what you are doing and that you are promising you are not holding a reference to the extended (and replaced) document somewhere and you expect it to work when using it. How useful replaceParent really is, is a good question, but it allows you to define a common (client and server side) document and then augment it on the server side with server-specific code.

Initialization

If you would like to run some code after Meteor startup, but before observers are enabled, you can use Document.prepare to register a callback. If you would like to run some code after Meteor startup and after observers are enabled, you can use Document.startup to register a callback.

Settings

PEERDB_INSTANCES=1

As your application grows you might want to run specialized Meteor instances just to do PeerDB reactive MongoDB queries. To distribute PeerDB load, configure the number of PeerDB instances using the PEERDB_INSTANCES environment variable. Suggested setting is that your web-facing instances disable PeerDB by setting PEERDB_INSTANCES to 0, and then you have dedicated PeerDB instances.

PEERDB_INSTANCE=0

If you are running multiple PeerDB instances, which instance is this? It is zero-based index so if you configured PEERDB_INSTANCES=2, you have to have two instances, one with PEERDB_INSTANCE=0 and another with PEERDB_INSTANCE=1.

MONGO_OPLOG_URL and MONGO_URL

When running multiple instances you want to connect them all to the same database. You have to configure both normal MongoDB connection and also the oplog connection. You can use your own MongoDB instance or connect to one provided by running Meteor in development mode. In the latter case the recommended way is that one web-facing instance runs MongoDB and all other instances connect to that MongoDB.

MONGO_OPLOG_URL=mongodb://127.0.0.1:3001/local
MONGO_URL=mongodb://127.0.0.1:3001/meteor

Examples

See tests for many examples. See document definitions in PeerLibrary for real-world definitions.

Related projects

  • matb33:collection-hooks – provides an alternative way to attach additional program logic on changes to your data, but it hooks into collection API methods so if a change comes from the outside, hooks are not called; additionally, collection API methods are delayed for the time of all hooks to be executed while in PeerDB hooks run in parallel in or even in a separate process (or processes), allowing your code to return quickly while PeerDB assures that data will be eventually consistent (this has a downside of course as well, so if you do not want that API calls return before all hooks run, matb33:collection-hooks might be more suitable for you)
  • peerlibrary:meteor-related – while PeerDB provides an easy way to embed referenced documents as subdocuments, it requires that those relations are the same for all users; if you want dynamic relations between documents, meteor-related provides an easy way to fetch related documents reactively on the server side, so when dependencies change, your published documents will be updated accordingly
  • herteby:denormalize – it does similar denormalization, but uses matb33:collection-hooks hooks instead reactivity to maintained denormalization, moreover, it looks like herteby:denormalize is much more limited in features than this package, which provides, e.g., also wrapping of documents into JavaScript objects with methods, generators, and reverse fields

meteor-peerdb's People

Contributors

dandv avatar kostko avatar mitar avatar mquandalle avatar wh0 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

meteor-peerdb's Issues

need 0.9 support

When you plan support 0.9? Maybe you'l create some milestones?

Allow and Deny

I would like proper Allow and Deny support. Currently I am using this work around:

Comment._collections.Comments.allow
  insert: (userId, doc) -> userId == doc.user._id #can not create a comment for some other users.
  update: (userId, doc, fieldNames, modifier) -> 
    return true if userId == doc.user._id 
    ...
  remove: (userId, doc) -> false #never delete anything -- apparently

Packaging domain

I put model in package:

/packages/documents/lib/person.coffee

class Person extends AccessDocument

/packages/documents/package.js

api.export(['Person']);

And in server/person.coffee redefine model with replaceParent: true

class @Person extends Person
  @Meta
    name: 'Person'
    replaceParent: true

When run app i get error:

W20150630-13:18:01.097(6)? (STDERR) /Users/boomfly/.meteor/packages/meteor-tool/.1.1.3.1wysac9++os.osx.x86_64+web.browser+web.cordova/mt-os.osx.x86_64/dev_bundle/server-lib/node_modules/fibers/future.js:245
W20150630-13:18:01.097(6)? (STDERR)                                             throw(ex);
W20150630-13:18:01.097(6)? (STDERR)                                                   ^
W20150630-13:18:01.098(6)? (STDERR) Error: Target document not defined (for person from Function.globals.Document.Document.Meta@packages/peerlibrary:peerdb/lib.coffee:716:82)
W20150630-13:18:01.098(6)? (STDERR)     at _ReferenceField.globals.Document.Document._TargetedFieldsObservingField._Class.validate (packages/peerlibrary:peerdb/lib.coffee:210:17)
W20150630-13:18:01.098(6)? (STDERR)     at _ReferenceField.<anonymous> (packages/peerlibrary:peerdb/lib.coffee:1:1)
W20150630-13:18:01.098(6)? (STDERR)     at _ReferenceField.validate (packages/peerlibrary:peerdb/lib.coffee:1:1)
W20150630-13:18:01.098(6)? (STDERR)     at Function.globals.Document.Document._validateFields (packages/peerlibrary:peerdb/lib.coffee:671:15)
W20150630-13:18:01.098(6)? (STDERR)     at Function.globals.Document.Document.validateAll (packages/peerlibrary:peerdb/lib.coffee:761:8)
W20150630-13:18:01.098(6)? (STDERR)     at Function.globals.Document.Document.defineAll (packages/peerlibrary:peerdb/lib.coffee:773:22)
W20150630-13:18:01.099(6)? (STDERR)     at Document (packages/peerlibrary:peerdb/server.coffee:609:20)
W20150630-13:18:01.099(6)? (STDERR)     at /Applications/MAMP/htdocs/imanager/web/.meteor/local/build/programs/server/boot.js:229:5

This mean Person class isnt visible to packages model User defined in packages/documents/lib/accounts.coffee.
Cant understand why this happens.

Required field in arrays

One of the things I struggle most is the required field in reference fields.

I think I understand the reasoning in normal cases, let's say with a post and comments. Comments will have a reference field to post and leave the default required to true. This means that if the post gets deleted the comment documents will get deleted too. This makes sense and I hope I have this correct.

But let's say you set it the other way around (for whatever reason). So this time post has a comments field that is an array reference field to comments. Because I don't want the post to be deleted if a comment gets deleted my intuition is to set required to false:

comments = [@ReferenceField Comment, ['text', 'author'], false]

But this throws an error: Reference field directly in an array cannot be optional.

How is this to be interpreted? My intuition goes from thinking about one comment. If I can only have one comment, I could set this to false. It would mean that if the one comment gets deleted, the comment reference here is set to null (instead of deleting the post). This works OK. But as soon as I want multiple comments, this doesn't hold any more?

The only way I can imagine a consistent interpretation would be that required in this case actually refers to the comment element inside the array - it is required and it will be deleted from the array if the comment gets deleted (as opposed to being replaced in a null placeholder in the array).

Is that true then?

So maybe this is more a suggestion for a better example section of required as it was definitely the hardest to understand as much as I remember my learning experience from the very beginning of my PeerDB use. Since it still boggles me sometimes after 2 or 3 years (like now), it might be a good place to put an extra example in or two.

JavaScript examples

Coffeescript examples are cool, but still a little esoteric. Please consider providing plain JS examples instead.

Dependency on Log

Is there a dependency on some kind of logging package?

I get this message in my console:

Exception in setTimeout callback: ReferenceError: Log is not defined
    at http://localhost:3000/packages/peerlibrary_peerdb.js?dd426f7f54d4401abffc14286f4cc6ef992bc5c3:775:16

Optimization: use query to limit documents for each instance

Instead of observing all changes in all instances and then filtering them in the observe callback, we could provide already a query to the database to limit documents. For example, {_id: {$gte: 'a', $lt: 'c'} returns only documents where _id starts with a or b.

Support repeated values in list of references

Because of current MongoDB limitation, only first occurrence in a list of a reference is synced with referenced document. This means values in the list have to be unique (use $addToSet). Hopefully, we will be able to remove this limitation someday.

Name must match the class name

What is the purpose of collection name having to match the class name?

Right now I'm going around this like this:

class AB.Translation extends AT.RemoteDocument
  @Meta
    abstract: true

and

  class ArtificialBabelTranslation extends AB.Translation
    @Meta
      name: 'ArtificialBabelTranslation'

  AB.Translation = ArtificialBabelTranslation

Am I going to run into trouble because of this? And if no, why do I have to jump through hoops in the first place?

Should we ran observers on the client side as well?

Should we ran observers on the client side as well? This could be useful to populate improve latency compensation. And it would be useful because it would make local (client-only) collections work on the client side as well if they have references.

CRUD many-to-many relatioships between 3 interconnected collections

Hi @mitar

I have 3 collections: Markets, Producers and Organizations.

Markets might have many Producers and vice versa
Producers might have many Organizations and vice versa
Organizations might have many Markets and vice versa

My question is, how should I design the schemas in order to perform atomic inserts, updates and deletes? so all the information is consistent and stays in sync

Thanks in advance for any help

Reverse fields error: Field names cannot contain '.'

I would like to define a reverse field that is a property inside another (plain) object on the document.

class Character extends Document
  # user: the owner of this character
  #   _id
  # name: name of this character
  @Meta
    name: 'Character'
    fields: =>
      user: @ReferenceField User, [] , true, 'userData.characters', ['name']

So that in the user document you get a structure like this:

class User extends Document
  # userData: custom data of the user
  #   characters: list of characters the user has created, reverse of Character.user
  #     _id
  #     name

Right now this throws an Error: Field names cannot contain '.'.

Would this be possible to have or are there other technical problems with allowing this?

What makes exists more efficient?

Would it be possible to rather than keep count of various documents with incrementing in separate collection where variability is fairly high- to extend where you produce like 1/5 more document for each document in original- with exists instead?

No references with collection: Meteor.users

I have three documents, and the Person class hooks into Meteor.users

class @Person extends Document
  @Meta
    name: 'Person'
    collection: Meteor.users

class @Collection extends Document
  @Meta
    name: 'Collection'
    fields:  =>
      projects: [@ReferenceField Project, ['title']]
      creator: @ReferenceField Person, ['username']
      fields

class @Project extends Document
  @Meta
    name: 'Project'
    fields: =>
      collections: [@ReferenceField Collection, ['title']]
      creator: @ReferenceField Person, ['username']

When creating a user, Person is correctly taking him. Also, when I add Project or Collection items and reference them like

Project.documents.insert({title: 'My Project', collections: [{_id:  'Ct2Tn5Y5jQJRNedtc'}]})

it all works well and I can call Project.documents.findOne().collections[0].title

However, when I reference creator in either Project or Collection, only the _id field that I referenced appears.

Collection.documents.insert({title: 'Test', creator: {_id: 'ppqBNxxozyCfQATF6'}})

Also, Document.defineAll() is called after all definitions.

Any ideas why I don't get Collection.documents.findOne().username?

When processing an update of same value for multiple fields in a document, update it in single operation

For example, if a document TestDocument has multiple referencing User collection, and a particular TestDocument document is referencing one document in User, when the latter is removed, we should make only single operation to update all those fields in the TestDocument document.

Currently we do multiple operations, which means that further observe callbacks are triggered but now the document referenced in fields not yet updated does not exist, so an error is shown.

It seems not not all operations can be yet be made so (see https://jira.mongodb.org/browse/SERVER-6566), but ones wher we loop and update each element in an array could since 3.5.2 (see https://jira.mongodb.org/browse/SERVER-1243).

Meteor 1.7 updated MongoDB to 3.6.4 so for those versions we could update our looping updates over array elements to do that in one operation.

If we could do also other operations all together then we should probably at every change do one update to update all fields at once. Current logic setups multiple observes for each field and is updating one by one.

A test for the example above is:

import {Meteor} from 'meteor/meteor';
import {Document} from 'meteor/peerlibrary:peerdb';

import Future from 'fibers/future';

class User extends Document {}

User.Meta({
  name: 'User',
});

class TestDocument extends Document {}

TestDocument.Meta({
  name: 'TestDocument',
  fields(fields) {
    return _.extend(fields, {
      userPermissions: [
        {
          user: Document.ReferenceField(User, {_id: 1}),
          addedBy: Document.ReferenceField(User, {_id: 1}, false),
        },
      ],
    });
  },
});

User.documents.remove({_id: {$in: ['aaaa', 'bbbb']}});
TestDocument.documents.remove({_id: 'cccc'});

const WAIT_FOR_DATABASE_TIMEOUT = 1500; // ms

function waitForDatabase() {
  const future = new Future();

  let timeout = null;
  const newTimeout = function () {
    if (timeout) {
      Meteor.clearTimeout(timeout);
    }
    timeout = Meteor.setTimeout(function () {
      timeout = null;
      if (!future.isResolved()) {
        future.return();
      }
    }, WAIT_FOR_DATABASE_TIMEOUT);
  };

  newTimeout();

  const handles = [];
  for (const document of Document.list) {
    handles.push(document.documents.find({}).observeChanges({
      added(id, fields) {
        newTimeout();
      },
      changed(id, fields) {
        newTimeout();
      },
      removed(id) {
        newTimeout();
      },
    }));
  }

  future.wait();

  for (const handle of handles) {
    handle.stop();
  }
}

Meteor.startup(function () {
  User.documents.insert({
    _id: 'aaaa',
  });
  User.documents.insert({
    _id: 'bbbb',
  });

  TestDocument.documents.insert({
    _id: 'cccc',
    userPermissions: [
      {
        user: {
          _id: 'aaaa',
        },
        addedBy: {
          _id: 'aaaa',
        },
      },
      {
        user: {
          _id: 'bbbb',
        },
        addedBy: {
          _id: 'aaaa',
        },
      },
    ],
  });

  waitForDatabase();

  User.documents.remove({_id: 'aaaa'});
});

When this is ran the following error is seen:

Document 'TestDocument' 'cccc' field 'userPermissions.addedBy' is referencing a nonexistent document 'aaaa'

This is because subdocument with user_.id == 'aaaa' is pulled from the array, which triggers an update with subdocument with addedBy._id === 'aaaa' still there.

_updateSourceField error

I've encountered this issue after manually deleting a document from the database using MongoHub (in case that makes any difference).

E20170929-15:46:44.240(-4) (lib.coffee:1258) PeerDB exception: TypeError: Cannot read property '0' of undefined: []
E20170929-15:46:44.241(-4) (lib.coffee:1259) TypeError: Cannot read property '0' of undefined
    at _Class.globals.Document.Document._GeneratedField._Class._updateSourceField (packages/peerlibrary_peerdb/lib.coffee:565:8)
    at _Class._updateSourceField (lib.coffee.js:3:57)
    at _Class.globals.Document.Document._GeneratedField._Class.updateSource (packages/peerlibrary_peerdb/lib.coffee:638:10)
    at _Class.updateSource (lib.coffee.js:3:57)
    at _Class.globals.Document.Document._GeneratedField._Class.removeSource (packages/peerlibrary_peerdb/lib.coffee:641:8)
    at _Class.removeSource (lib.coffee.js:3:57)
    at packages/peerlibrary_peerdb/lib.coffee:311:12
    at packages/peerlibrary_peerdb/lib.coffee:1254:11
    at runWithEnvironment (packages/meteor.js:1188:24)
    at packages/meteor.js:1201:14
    at packages/mongo/observe_multiplex.js:182:30
    at Array.forEach (native)
    at Function._.each._.forEach (packages/underscore.js:139:11)
    at Object.task (packages/mongo/observe_multiplex.js:176:9)
    at [object Object].SQp._run (packages/meteor.js:819:16)
    at packages/meteor.js:796:12

My server also started running rapidly out of memory around the same time, so I hope this is not connected. I'm trying to identify the source of any corruption in the DB or anything like that to get a hint on what's going on. This is the first exception I can find that's appearing around my issues.

Option to specify the mongo driver

Sometimes you need to connect to multiple mongo databases from the same app. That can be done by specifying the internal mongo driver when creating the collection as show here:

var database = new MongoInternals.RemoteCollectionDriver("<mongo url>");
MyCollection = new Mongo.Collection("collection_name", { _driver: database });

This could work in PeerDB in a similar fashion that you can specify a custom collection to use.

Support nested fields

In the example below, importing.by will not work because it is nested.

class @Publication extends Document
  @Meta =>
    collection: Publications
    fields:
      authors: [@Reference Person, ['slug', 'foreNames', 'lastName']]
      'importing.by': @Reference User

Implement serialization/deserialization of fields

The most typical use case which we are not yet supporting is serialization/deserialization of fields. So that in JavaScript instances fields are represented by some other value than what is stored in the database. For example, one use case is to store timestamps as Date objects, but use momentjs objects in JavaScript instances.

This is something which is traditionally done by providing custom field types, which know how to serialize/deserialize to and from the database. So we could have a DatetimeField, which would store timestamp as Date object into the database, when it would get it as moment value, and when reading from the database, it would convert Date to moment.

Similarly we can then also implement references (which are just a special case of this), so that users can simply assign some referenced object, and PeerDB makes sure to extract _id and store only that (and any configured fields) into the database. And in the other direction (which is currently already supported as a special case.)

bulkInsert removes undeclared subfields

If you have the following document definition:

class Item extends Document
  @Meta
    name: 'Item'
    fields: =>
      foo:
        bar: @ReferenceField AnotherDocument, []

And you perform an Item.documents.bulkInsert operation on data like:

[
  _id: "iBovvBKYBy89buvPC"
  hello: "world"
  foo:
    bar:
      _id: 'JkzTRmQpnR4Y7Qx8Q'
    fieldA: 1
    fieldB: "hello"      
]

Documents which will be inserted will not contain the fields foo.fieldA or foo.fieldB.

Anyway simpleschema, collection2 and peerdb can work together

First of all, I want to say kudos for putting in the work to get a reactive layer on top of mongo (server-side) to handle declarative updates. Both collection2 and simpleschema solve specific issues, same as peerdb. I personally see peerdb more of a better package to replace collection-hooks but I wonder if peerdb api can be made to work with collection2 and simpleschema. I really see 3 concerns here:

Data schema integrity and validation -> SimpleSchema
Collection data schema validation on insert, upsert and update -> Collection2
Provide Collection data hooks on insert, upsert, update and delete -> Collection-hooks || Peerdb

I will personally like to use Peerdb over collection-hooks because of its reactive nature and the ability to handle hooks even if the changes are made by another client. For me peerdb is a clear winner but I cant seem to feel like it will even be much better if it can work in tandem with SimpleSchema and Collection2.

My question is - how do I get the benefit of simpleschema + collection2 + peerdb. Is it be possible to integrate all 3 to work together. Will it be better to integrate peerdb definitions into simpleschema since both of them are in a way defining metadata information. Example, If would be great if we could do something like

//data validation provided by simpleschema 
PostSchema = new SimpleSchema({
    content: {
        type: String,
        optional: false,
    },
   author: {
     type:Object,
     optional: false
   },
   "author.username": {
      type: String
  },
   "author.displayName": {
      type: String
  },
   hits: {
        type: Number
    }
   //etc
});

//peerdb hook through SimpleSchema
PostSchema.References({
    fields: function() {
      return {
        author: Post.ReferenceField(Person, ['username', 'displayName']),
      };
    }
  });

PostSchema.Triggers({
    //trigger definition here ....
  });

Post = new Meteor.Collection('posts');
Post.attachSchema(PostSchema); //provided by collection2

In the approve sample code, simpleschema handles validation (i.e. make sure author's username and displayname is a string), collection2 handle running validation and peerdb extension to simpleschema + collection2 maintains data references and hooks. This is just a thought but I think something along these lines will provide the community with 80% of what its needed for data manipulation. I want to use peerdb now but I also don't want to loose my schema validation checks on my collection (as well as on the client through autoform)

Examples

Hi,
Trying to wrap my head around this to see if I can use. Wanted to know if had any examples or tutorials?

Document.updateAll not updating references

I want to have some fixture data for local development which I’m inserting like so (in server/fixtures.js):

 var questionId0 = Question.documents.insert({
   title: "Title text"
 });

 var testAnswerId0 = Answer.documents.insert({
   question: {
     _id: questionId0
   }
 });

My PeerDB class definition for Answer has the Question field referenced:

       question: Answer.ReferenceField(Question, ['title'])

I see the changes being made to the answer object when I manually update the Question in the Mongo CLI, but running Document.updateAll(); after doing my insertions in fixtures.js does not seem to construct the couple of references I’d want to have while booting up.

Reactive dict/map between documents

It seems a common data structure one would like is the following:

{
  related: {
    <id of a document>: {
      subfield: "Subfield value"
    },
    <id of a document>: {
      ...
    }
  }
}

So a dict/map where keys are IDs and values are a subdocument with few fields of a referenced document. One can do that as a list for now, but I think having a dict/map would be beneficial for easy direct access if key (document id) is known. But current code does not yet support keeping subdocuments in sync for such structure.

Velocity causes warnings

After adding velocity I get: Error loading app files Reuse of a collection without replaceParent set Error: Reuse of a collection without replaceParent set. This is not really surprising given how velocity works. Any ideas on how to make PeerDB Velocity friendly?

PeerDB query in publication rather than always?

My understanding is that it denormalizes all the time even with auto-generators, so if it's for performance than should doing it in publications suffice? If not then auto-generators don't denormalize does the generation occur on client which would then require peerdb query on client (that is if I encoded information for indexing limitations, would rather not have to decode on server all the time)?

Multi-server architecture

I'm trying to make two meteor apps talk to each other. Let's call them Master and Slave.

Each of the apps is a meteor app so it comes with its server and a client. Master.com runs the master server, and pointing the browser to Master.com return you the Master client code. Likewise for Slave server and client.

Slave client can talk to Master server just fine (from what I've tested so far). The problem is when Slave server tries to talk to Master server. Actually, just declaring the Document on the Slave server is the problem, because the server code of peerdb kicks in and tries to ensure index on the collection, but it is not really there (it is coming from a remote connection from Master). So the server throws this error:

W20160107-21:52:55.677(-8)? (STDERR) Error: Can only call _ensureIndex on server collections
W20160107-21:52:55.677(-8)? (STDERR)     at [object Object].Mongo.Collection._ensureIndex (packages/mongo/collection.js:630:1)
W20160107-21:52:55.677(-8)? (STDERR)     at _ReferenceField.globals.Document._TargetedFieldsObservingField._setupTargetObservers (packages/peerlibrary_peerdb/packages/peerlibrary_peerdb.js:1261:1)
W20160107-21:52:55.677(-8)? (STDERR)     at setupTargetObservers (packages/peerlibrary_peerdb/packages/peerlibrary_peerdb.js:1907:1)
W20160107-21:52:55.677(-8)? (STDERR)     at setupObservers (packages/peerlibrary_peerdb/packages/peerlibrary_peerdb.js:1923:1)
W20160107-21:52:55.677(-8)? (STDERR)     at Document (packages/peerlibrary_peerdb/packages/peerlibrary_peerdb.js:1977:1)

At least that is my interpretation after quickly looking into the issue.

The way I create the document is that i send a collection to the @Meta function, and that collection is build with the connection option that points to another server (so even on the server, it is not local to that server).

Let me know if I should be more clear about what I'm doing and if you need a minimal test.

How do you define a mix-in?

Great work! I've been throughly checking out meteor packages for my new big project, and yours is a beauty! I am definitely embarking on peerdb and really hope to see vast progress!

I've got a little question, though. Being not so good programmer, I can't really wrap my head around on how to implement pluggable logic for Documents.

Suppose, I want to add some 'logging' functionality. Or mark documents as deleted instead of really deleting them. It would be great if I'm able to declare some classes something like this: class Thing extends Document mixin LoggingDocument, SoftDeleteDocument

Can I do this with current peerdb and coffeescript? How? If I can not, would you consider adding support for that in the future?

throwing "Document name does not match class name" - if production minified js is used

(Using version 0.6.0 of peer-db with Meteor 0.8.0)

lib.coffee:438

---
throw new Error "Document name does not match class name" if @name and @name isnt meta.name

Receiving this error for:

class @User extends Document
  @Meta
    name: 'User'
    collection: Meteor.users

when running meteor --prodcution because the class gets minified to n instead of User etc.

Background initialization of observers or other optimizations

My app with 40+ document classes and many reference fields between them takes a relatively long time to start.

  • 15s Meteor app startup (including build time)
  • 10s PeerDB Enabling observers

And on code update:

  • 3-4s build time
  • 10s PeerDB Enabling observers

So it takes just long enough that you can't just change the code, switch to browser and see the results almost immediately (under 5s) so you can keep on working. (Instead you go to another tab with Twitter or something and then 15s turns into 5min … but that's another problem.)

Would it be possible that observers would initialize in the background, so that the server would immediately start working, and you can test your code changes (which often enough don't include any document updates, so I'm fine waiting 10 seconds before any computed fields/references are updated).

What I'm worried is that if this initialization is delayed, you'd 'miss' any updates if document changes do happen in those 10 seconds, and not do them at all (which would be a problem). But I don't have an understanding of the architecture if this is indeed a problem or how much effort this would be. So I'd appreciate a discussion on what could be done to mitigate 10s+ restart times due to use of PeerDB.

Would love to have meteor 0.9+ support, need help?

Hi!

I'am currently using peerdb in a project and I like it, however it is keeping us from migrating to meteor 0.9

Is there anything I could do? Any pointers to where the trouble is in migrating to 0.9?

Regards,
Xander

Dedicated PeerDB Instance

I'm a little confused about this section of the documentation:

PEERDB_INSTANCES=1

As your application grows you might want to run specialized Meteor instances just to do PeerDB reactive MongoDB queries. To distribute PeerDB load, configure the number of PeerDB instances using the PEERDB_INSTANCES environment variable. Suggested setting is that your web-facing instances disable PeerDB by setting PEERDB_INSTANCES to 0, and then you have dedicated PeerDB instances.

A few questions...

So it's recommended to setup a separate meteor instance running PeerDB to handle relations?

What happens if you try to do an insert from the server of a public facing meteor project connected to the Mongo database that fails on the separate PeerDB server due to validation? Does it fail on the public facing meteor instance without PeerDB?
I imagine the errors on the PeerDB server can't propagate down to the Meteor server through Mongo (so I must be thinking about something incorrectly).

How does PeerDB manage the collection hooks? Will everything work correctly if the insert/update/deletes aren't done from the meteor server with PeerDB?

My use case is that I have several Meteor servers connected to the same Compose instance, but they all need the same logic taken into account should they try to do an invalid insert/update, and also need the relations managed. Currently I'm doing this by packaging all the database logic (collection-hooks/observers/schemas) in a package and then importing that for every meteor project.
Hoping this is much simpler than that! :)

Make abstract less strict

Currently abstract documents require their parents to be abstract as well. Not sure why this is required? It is pretty common that you might want to have a new document based on the parent document, but that you use some abstract class in between the non-abstract root and your document.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.