How to implement a selector in easy: search for Meteor, using React instead of Blaze

社会主义新天地 提交于 2020-01-05 08:11:06

问题


I'm trying to follow the documentation and examples to add a server-side selector to a search function in my Meteor app, implemented using the Easy Search plugin. The end goal is to ensure that only documents the user has permission to see are returned by searching.

I can see a selector working in the Leaderboard example, but I can't get it to work in my code.

Versions:

Meteor 1.7.0.1
easy:search@2.2.1
easysearch:components@2.2.2
easysearch:core@2.2.2

I modified the Meteor 'todos' example app to demonstrate the problem, and my demo code is in a repo.

NOTE! to demonstrate the problem, you need to create an account in the demo app, then create a list and make it private. This add the 'userId' field to the list.

Then you can search for the name of the list, by typing in the search box near the top of the main section; search results are written to the browser console.

The first problem is that if I copy the code from the example in the documentation, I see a server error 'searchObject is not defined:

copied from docs, causes an error: imports/api/lists/lists.js

export const MyIndex = new Index({
    'collection': Lists,
    'fields': ['name'],
    engine: new MongoDBEngine({
    selector(searchDefinition, options, aggregation) {
      // retrieve the default selector
      const selector = this.defaultConfiguration()
        .selector(searchObject, options, aggregation)

      // options.search.userId contains the userId of the logged in user
      selector.userId = options.search.userId

      return selector
    },
  }),
});

It seems there is an error in the docs.

Working instead from the leaderboard example, the code below runs but intermittently returns no results. For example if I have a list called "My list", and I type the search term 's', sometimes the list is returned from the search and sometimes it is not. If I use the MiniMongo engine it all works perfectly.

index selector {"$or":[{"name":{"$regex":".*my.*","$options":"i"}}],"userId":"Wtrr5FRHhkKuAcrLZ"}

client and server: imports/api/lists/lists.js

export const MyIndex = new Index({
  'collection': Lists,
  'fields': ['name'],
  'engine': new MongoDBEngine({
    selector: function (searchObject, options, aggregation) {
      let selector = this.defaultConfiguration().selector(searchObject, options, aggregation);

      selector.userId = options.search.userId;
      console.log('index selector', JSON.stringify(selector));
      return selector;
    }
  }),
  permission: () => {
    return true;
  },
});

client: imports/ui/components/lists-show.js

Template.Lists_show.events({
'keyup #search'(event) {
    console.log('search for ', event.target.value);

    const cursor = MyIndex.search(event.target.value);
    console.log('count',cursor.count());
    console.log('results', cursor.fetch());
  },
});

client: imports/ui/components/lists-show.html

<input id="search" type="text" placeholder="search..." />

Edit: I think the problem is that while the Minimongo engine runs on the client, the MongoDBEngine runs on the server and there are timing issues with the results. The docs show using Tracker.autorun, but that's not a natural fit with my React / Redux app. I'll post an answer if I manage to figure something out - I can't be the only person trying to do something like this.


回答1:


I got it working in my React / Redux / Meteor app. Things to note:

  1. The cursor MyIndex.search(searchTerm) is a reactive data source - you can't just use it as a return value. When searching on the client with MiniMongo this isn't an issue, but it's important when you use MongoDBEngine to search on the server, because it's asynchronous. In React you can wrap the cursor in withTracker to pass data to the component reactively. In Blaze you would use autorun.tracker. This is shown in the docs but not explained, and it took me a while to understand what was happening.

  2. The docs have an error in the selector example, easily corrected but it's confusing if you have other problems in your code.

  3. With MongoDBEngine, 'permission' must be specified - it does not default to 'true'. Without it, you will see no results.

  4. Writing out the default selector object to the console let me see how it's constructed, and then create a new selector that returns MyDocs that are either public or created by the user.

My code is below. In case it helps anybody else, I've shown how to search on tags also, which are objects with a name property stored in a collection Tags. Each MyDoc has a 'tags' property which is an array of tag ids. The selector first searches the Tags collection to find tags whose name matches the search term, then selects docs in MyDocs with the ids of those tags in their doc.tags array.

There may be a better way to find the search term, or to structure the Tags search, but this is what I could get working.

On server and client:

import { Index, MongoDBEngine } from 'meteor/easy:search';

export const MyDocs = new Mongo.Collection('mydocs');
export const Tags = new Mongo.Collection('tags');

export const MyIndex = new Index({
    'collection': MyDocs,
    'fields': ['name'],
    'engine': new MongoDBEngine({
        'selector': function (searchObject, options, aggregation) {
            const selector = this.defaultConfiguration().selector(searchObject, options, aggregation);

            console.log('default selector', selector); // this searches on name only

            // find docs by tag as well as by name
            const searchTerm = searchObject.name;
            const matchingTags = Tags.find({ 'name': { '$regex': searchTerm } }).fetch();
            const matchingTagIds = matchingTags.map((tag) => tag._id);
            selector.$or.push({ 'tags': { '$in': matchingTagIds } });

            const newSelector = {
                '$and': [
                    {
                        '$or': [
                            { 'isPublic': { '$eq': true } },
                            { 'createdBy': options.search.userId },
                        ],
                    },
                    {
                        '$or': selector.$or,
                    },
                ],
            };

            return newSelector;
        },
        'fields': (searchObject, options) => ({
            '_id': 1,
            'createdBy': 1,
            'name': 1,
        }),
        'sort': () => ({ 'name': 1 }),
    }),
    'permission': () => true,
});

React component in client only code:

import React from 'react';
import { connect } from 'react-redux';
import { withTracker } from 'meteor/react-meteor-data';
import PropTypes from 'prop-types';
import store from '../modules/store';
import {
    getSearchTerm,
    searchStart,
} from '../modules/search'; // contains Redux actions and partial store for search
import { MyIndex } from '../../modules/collection';

function Search(props) {
// functional React component that contains the search box
...
const onChange = (value) => {
    clearTimeout(global.searchTimeout);

    if (value.length >= 2) {
        // user has entered a search term
        // of at least 2 characters
        // wait until they stop typing
        global.searchTimeout = setTimeout(() => {
            dispatch(searchStart(value)); // Redux action which sets the searchTerm in Redux state
        }, 500);
    }
};
...
// the component returns html which calls onChange when the user types in the search input
// and a list which displays the search results, accessed in props.searchResults
}

const Tracker = withTracker(({ dispatch }) => {
    // searchTerm is saved in Redux state.
    const state = store.getState();
    const searchTerm = getSearchTerm(state); // Redux function to get searchTerm out of Redux state

    let results = [];

    if (searchTerm) {
        const cursor = MyIndex.search(searchTerm); // search is a reactive data source

        results = cursor.fetch();
        console.log('*** cursor count', cursor.count());

    return {
        'searchResults': results,
    };
})(Search);

export default connect()(Tracker);


来源:https://stackoverflow.com/questions/59560457/how-to-implement-a-selector-in-easy-search-for-meteor-using-react-instead-of-b

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!