locomotivecms / steam

The rendering stack used by both Wagon and Station (new name of the engine). It includes the rack stack and the liquid drops/filters/tags.
MIT License
38 stars 59 forks source link

ActionJS : handle many to many relationships #181

Open proxygear opened 4 years ago

proxygear commented 4 years ago

Let's say we have to collections authors and books. A book has many authors and an author has many books. Great.

In an action I tried something like this :

allEntries(
  'books', 
  { 'authors': 'an-author-slug' }
)

But it does not works. And I started to wonder how to do the following requests : 'authors.in' : ['a-first-author-slug', 'a-second-author-slug'] or 'authors.ne' : 'a-first-author-slug'

Is it even possible ? From what I checked, I can access to author_ids :

findEntry('books', 'my-book-slug').author_ids

However it returns an array of mongoDB ids. In the other hand I can't access to the mongoDB id of an object :

findEntry('books', 'my-book-slug').id

Some help with this confusion ?

proxygear commented 4 years ago

From what I see, I should start to dug from ActionService. However the actual implementation is in the engine ending in the ContentEntry. So my issue is Engine related and not Steam.

Refreshing my memory, the id method in mongoDB is _id.

So I'm trying something like :

allEntries(
  'books', 
  { 'author_ids': findEntry('authors',  'an-author-slug') }
)

Still without success.

End of this, I'll update the doc :D

proxygear commented 4 years ago

You should actually do something like this in the JS Action :

allEntries(
  'books', 
  {
    'author_ids': {
      '$in': [findEntry('authors',  'an-author-slug')._id]
   }
)

However, _id return a string. To be working with Mongoid you should make it a BsonId Object. Here is what the ruby counter part should be doing :

ContentEntry.where(
  book_ids: {
    '$in' => BSON::ObjectId.from_string(string_id)
  }
).first

If the conversion to BSON::Object is omitted, it will not work.

My first guess would be update steam/lib/locomotive/adapters/mongodb.rb to do some automatic conversion but ... meh ?

proxygear commented 4 years ago

So my idea to fix it is to, within the JS Action to write something like this

var pseudo_id = "BSON(" + findEntry('authors',  'an-author-slug')._id + ")";
allEntries(
  'books', 
  {
    'author_ids': {
      '$in': [pseudo_id]
   }
)

And with a sanitizer, to handle the pseudo ids :

module Locomotive::Steam
  module Adapters
    module Mongodb
      module IdsSanitizer
        PSEUDO_ID_REGEXP = /^BSON\((?<id>[a-z0-9]+)\)$/.freeze

        def self.call(data)
          replace_leaf_strings_within(data, &method(:fix_pseudo_id))
        end

        def self.fix_pseudo_id(string)
          result = PSEUDO_ID_REGEXP.match(string)
          result ? BSON::ObjectId.from_string(result[:id]) : string
        end

        def self.replace_leaf_strings_within(data, &block)
          case data.class
          when Hash
            {}.tap do |h|
              data.each do |key, value|
                h[key] = replace_leaf_strings_within(value, &block)
              end
            end
          when Array
            data.map { |value| replace_leaf_strings_within(value, &block) }
          when String
            block.call(data)
          else
            data
          end
        end
      end
    end
  end
end

It could be used in steam/services/action_service.rb line 99 :

def all_entries_lambda(liquid_context)
    -> (type, conditions) { content_entry_service.all(type, sanitize_conditions(conditions), true) }
end

def sanitize_conditions(conditions)
  Adapters::Mongodb::IdsSanitizer.call(conditions)
end

What do you think @did ?

PS: Using this monkey patch, I was able to solve my problem. I would like to do a pull request, however my solution is quite specific, I would like to discuss with some maintainer to see if something more generic cannot be done. Also, this solution is quite MongoDB specific, it will not work with wagon. But I guess that's another topic.