├── .codeclimate.yml ├── .coveralls.yml ├── .eslintignore ├── .eslintrc.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── UPGRADE-2.0.md ├── inch.json ├── package.json ├── packages ├── easy_search │ ├── main.js │ └── package.js ├── easysearch_autosuggest │ ├── README.md │ ├── lib │ │ ├── autosuggest.html │ │ └── autosuggest.js │ ├── package.js │ └── tests │ │ └── autosuggest-tests.js ├── easysearch_components │ ├── README.md │ ├── component_methods.md │ ├── lib │ │ ├── base.js │ │ ├── component-methods.js │ │ ├── core.js │ │ ├── each │ │ │ ├── each.html │ │ │ └── each.js │ │ ├── field-input │ │ │ ├── field-input.html │ │ │ └── field-input.js │ │ ├── if-input-empty │ │ │ ├── if-input-empty.html │ │ │ └── if-input-empty.js │ │ ├── if-no-results │ │ │ ├── if-no-results.html │ │ │ └── if-no-results.js │ │ ├── if-searching │ │ │ ├── if-searching.html │ │ │ └── if-searching.js │ │ ├── input │ │ │ ├── input.html │ │ │ └── input.js │ │ ├── load-more │ │ │ ├── load-more.html │ │ │ └── load-more.js │ │ ├── main.js │ │ ├── pagination │ │ │ ├── pagination.html │ │ │ └── pagination.js │ │ └── single-index.js │ ├── package.js │ └── tests │ │ ├── helpers.js │ │ └── unit │ │ ├── base-tests.js │ │ ├── component-methods-tests.js │ │ ├── core-tests.js │ │ ├── each-tests.js │ │ ├── field-input-tests.js │ │ ├── if-tests.js │ │ ├── input-tests.js │ │ ├── load-more-tests.js │ │ └── pagination-tests.js ├── easysearch_core │ ├── README.md │ ├── lib │ │ ├── core │ │ │ ├── cursor.js │ │ │ ├── data-source.js │ │ │ ├── engine.js │ │ │ ├── index.js │ │ │ ├── reactive-engine.js │ │ │ └── search-collection.js │ │ ├── engines │ │ │ ├── minimongo.js │ │ │ ├── mongo-db.js │ │ │ └── mongo-text-index.js │ │ ├── globals.js │ │ └── main.js │ ├── package.js │ └── tests │ │ ├── functional │ │ ├── minimongo-tests.js │ │ ├── mongo-db-tests.js │ │ └── mongo-text-index-tests.js │ │ ├── helpers.js │ │ └── unit │ │ └── core │ │ ├── cursor-tests.js │ │ ├── engine-tests.js │ │ ├── index-tests.js │ │ └── reactive-engine-tests.js ├── easysearch_elasticsearch │ ├── README.md │ ├── lib │ │ ├── data-syncer.js │ │ ├── engine.js │ │ └── main.js │ ├── package.js │ └── tests │ │ └── engine-tests.js └── matteodem_easy-search │ ├── .coveralls.yml │ ├── .travis.yml │ ├── .versions │ ├── package.js │ └── versions.json └── scripts ├── publish.sh └── test-packages.sh /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | duplication: 4 | enabled: true 5 | config: 6 | languages: 7 | - ruby 8 | - javascript 9 | - python 10 | - php 11 | eslint: 12 | enabled: true 13 | checks: 14 | comma-dangle: 15 | enabled: false 16 | fixme: 17 | enabled: true 18 | ratings: 19 | paths: 20 | - "**.inc" 21 | - "**.js" 22 | - "**.jsx" 23 | - "**.module" 24 | - "**.php" 25 | - "**.py" 26 | - "**.rb" 27 | exclude_paths: [] 28 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-pro 2 | repo_token: kNpehY5zjnoVWbzWSvr33PMEG0m5WOl3y 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/*{.,-}min.js 2 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | parserOptions: 3 | sourceType: module 4 | ecmaFeatures: 5 | jsx: true 6 | 7 | env: 8 | amd: true 9 | browser: true 10 | es6: true 11 | jquery: true 12 | node: true 13 | 14 | # http://eslint.org/docs/rules/ 15 | rules: 16 | # Possible Errors 17 | no-await-in-loop: off 18 | no-cond-assign: error 19 | no-console: off 20 | no-constant-condition: error 21 | no-control-regex: error 22 | no-debugger: error 23 | no-dupe-args: error 24 | no-dupe-keys: error 25 | no-duplicate-case: error 26 | no-empty-character-class: error 27 | no-empty: error 28 | no-ex-assign: error 29 | no-extra-boolean-cast: error 30 | no-extra-parens: off 31 | no-extra-semi: error 32 | no-func-assign: error 33 | no-inner-declarations: 34 | - error 35 | - functions 36 | no-invalid-regexp: error 37 | no-irregular-whitespace: error 38 | no-negated-in-lhs: error 39 | no-obj-calls: error 40 | no-prototype-builtins: off 41 | no-regex-spaces: error 42 | no-sparse-arrays: error 43 | no-template-curly-in-string: off 44 | no-unexpected-multiline: error 45 | no-unreachable: error 46 | no-unsafe-finally: off 47 | no-unsafe-negation: off 48 | use-isnan: error 49 | valid-jsdoc: off 50 | valid-typeof: error 51 | 52 | # Best Practices 53 | accessor-pairs: error 54 | array-callback-return: off 55 | block-scoped-var: off 56 | class-methods-use-this: off 57 | complexity: 58 | - error 59 | - 6 60 | consistent-return: off 61 | curly: off 62 | default-case: off 63 | dot-location: off 64 | dot-notation: off 65 | eqeqeq: error 66 | guard-for-in: error 67 | no-alert: error 68 | no-caller: error 69 | no-case-declarations: error 70 | no-div-regex: error 71 | no-else-return: off 72 | no-empty-function: off 73 | no-empty-pattern: error 74 | no-eq-null: error 75 | no-eval: error 76 | no-extend-native: error 77 | no-extra-bind: error 78 | no-extra-label: off 79 | no-fallthrough: error 80 | no-floating-decimal: off 81 | no-global-assign: off 82 | no-implicit-coercion: off 83 | no-implied-eval: error 84 | no-invalid-this: off 85 | no-iterator: error 86 | no-labels: 87 | - error 88 | - allowLoop: true 89 | allowSwitch: true 90 | no-lone-blocks: error 91 | no-loop-func: error 92 | no-magic-number: off 93 | no-multi-spaces: off 94 | no-multi-str: off 95 | no-native-reassign: error 96 | no-new-func: error 97 | no-new-wrappers: error 98 | no-new: error 99 | no-octal-escape: error 100 | no-octal: error 101 | no-param-reassign: off 102 | no-proto: error 103 | no-redeclare: error 104 | no-restricted-properties: off 105 | no-return-assign: error 106 | no-return-await: off 107 | no-script-url: error 108 | no-self-assign: off 109 | no-self-compare: error 110 | no-sequences: off 111 | no-throw-literal: off 112 | no-unmodified-loop-condition: off 113 | no-unused-expressions: error 114 | no-unused-labels: off 115 | no-useless-call: error 116 | no-useless-concat: error 117 | no-useless-escape: off 118 | no-useless-return: off 119 | no-void: error 120 | no-warning-comments: off 121 | no-with: error 122 | prefer-promise-reject-errors: off 123 | radix: error 124 | require-await: off 125 | vars-on-top: off 126 | wrap-iife: error 127 | yoda: off 128 | 129 | # Strict 130 | strict: off 131 | 132 | # Variables 133 | init-declarations: off 134 | no-catch-shadow: error 135 | no-delete-var: error 136 | no-label-var: error 137 | no-restricted-globals: off 138 | no-shadow-restricted-names: error 139 | no-shadow: off 140 | no-undef-init: error 141 | no-undef: off 142 | no-undefined: off 143 | no-unused-vars: off 144 | no-use-before-define: off 145 | 146 | # Node.js and CommonJS 147 | callback-return: error 148 | global-require: error 149 | handle-callback-err: error 150 | no-mixed-requires: off 151 | no-new-require: off 152 | no-path-concat: error 153 | no-process-env: off 154 | no-process-exit: error 155 | no-restricted-modules: off 156 | no-sync: off 157 | 158 | # Stylistic Issues 159 | array-bracket-spacing: off 160 | block-spacing: off 161 | brace-style: off 162 | camelcase: off 163 | capitalized-comments: off 164 | comma-dangle: 165 | - error 166 | - never 167 | comma-spacing: off 168 | comma-style: off 169 | computed-property-spacing: off 170 | consistent-this: off 171 | eol-last: off 172 | func-call-spacing: off 173 | func-name-matching: off 174 | func-names: off 175 | func-style: off 176 | id-length: off 177 | id-match: off 178 | indent: off 179 | jsx-quotes: off 180 | key-spacing: off 181 | keyword-spacing: off 182 | line-comment-position: off 183 | linebreak-style: off 184 | lines-around-comment: off 185 | lines-around-directive: off 186 | max-depth: off 187 | max-len: off 188 | max-nested-callbacks: off 189 | max-params: off 190 | max-statements-per-line: off 191 | max-statements: 192 | - error 193 | - 30 194 | multiline-ternary: off 195 | new-cap: off 196 | new-parens: off 197 | newline-after-var: off 198 | newline-before-return: off 199 | newline-per-chained-call: off 200 | no-array-constructor: off 201 | no-bitwise: off 202 | no-continue: off 203 | no-inline-comments: off 204 | no-lonely-if: off 205 | no-mixed-operators: off 206 | no-mixed-spaces-and-tabs: off 207 | no-multi-assign: off 208 | no-multiple-empty-lines: off 209 | no-negated-condition: off 210 | no-nested-ternary: off 211 | no-new-object: off 212 | no-plusplus: off 213 | no-restricted-syntax: off 214 | no-spaced-func: off 215 | no-tabs: off 216 | no-ternary: off 217 | no-trailing-spaces: off 218 | no-underscore-dangle: off 219 | no-unneeded-ternary: off 220 | object-curly-newline: off 221 | object-curly-spacing: off 222 | object-property-newline: off 223 | one-var-declaration-per-line: off 224 | one-var: off 225 | operator-assignment: off 226 | operator-linebreak: off 227 | padded-blocks: off 228 | quote-props: off 229 | quotes: off 230 | require-jsdoc: off 231 | semi-spacing: off 232 | semi: off 233 | sort-keys: off 234 | sort-vars: off 235 | space-before-blocks: off 236 | space-before-function-paren: off 237 | space-in-parens: off 238 | space-infix-ops: off 239 | space-unary-ops: off 240 | spaced-comment: off 241 | template-tag-spacing: off 242 | unicode-bom: off 243 | wrap-regex: off 244 | 245 | # ECMAScript 6 246 | arrow-body-style: off 247 | arrow-parens: off 248 | arrow-spacing: off 249 | constructor-super: off 250 | generator-star-spacing: off 251 | no-class-assign: off 252 | no-confusing-arrow: off 253 | no-const-assign: off 254 | no-dupe-class-members: off 255 | no-duplicate-imports: off 256 | no-new-symbol: off 257 | no-restricted-imports: off 258 | no-this-before-super: off 259 | no-useless-computed-key: off 260 | no-useless-constructor: off 261 | no-useless-rename: off 262 | no-var: off 263 | object-shorthand: off 264 | prefer-arrow-callback: off 265 | prefer-const: off 266 | prefer-destructuring: off 267 | prefer-numeric-literals: off 268 | prefer-rest-params: off 269 | prefer-reflect: off 270 | prefer-spread: off 271 | prefer-template: off 272 | require-yield: off 273 | rest-spread-spacing: off 274 | sort-imports: off 275 | symbol-description: off 276 | template-curly-spacing: off 277 | yield-star-spacing: off 278 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .build*/ 2 | .idea/ 3 | .npm*/ 4 | .build* 5 | node_modules/ 6 | _site* 7 | easy-search.iml 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4.8.1" 4 | before_install: 5 | - "curl https://install.meteor.com | /bin/sh" 6 | - export PATH="$HOME/.meteor:$PATH" 7 | - "npm install -g spacejam" 8 | 9 | script: "sh ./scripts/test-packages.sh" 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Matteo De Micheli 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Easy Search [![Build Status](https://travis-ci.org/matteodem/meteor-easy-search.svg?branch=master)](https://travis-ci.org/matteodem/meteor-easy-search) [![Get help on Codementor](https://cdn.codementor.io/badges/get_help_github.svg)](https://www.codementor.io/matteodemicheli?utm_source=github&utm_medium=button&utm_term=matteodemicheli&utm_campaign=github) 2 | ===================== 3 | 4 | Easy Search is a simple and flexible solution for adding search functionality to your Meteor App. Use the Blaze Components + Javascript API to [get started](http://matteodem.github.io/meteor-easy-search/getting-started). 5 | 6 | ```javascript 7 | import { Index, MinimongoEngine } from 'meteor/easy:search' 8 | 9 | // On Client and Server 10 | const Players = new Mongo.Collection('players') 11 | const PlayersIndex = new Index({ 12 | collection: Players, 13 | fields: ['name'], 14 | engine: new MinimongoEngine(), 15 | }) 16 | ``` 17 | 18 | ```javascript 19 | // On Client 20 | Template.searchBox.helpers({ 21 | playersIndex: () => PlayersIndex, 22 | }); 23 | ``` 24 | 25 | ```html 26 | 35 | ``` 36 | 37 | Check out the [searchable leaderboard example](https://github.com/matteodem/easy-search-leaderboard) or have a look at the [current documentation](http://matteodem.github.io/meteor-easy-search/) ([v1 docs](https://github.com/matteodem/meteor-easy-search/tree/gh-pages/_v1docs)) for more information. 38 | 39 | ## How to install 40 | 41 | ```sh 42 | cd /path/to/project 43 | meteor add easy:search 44 | ``` 45 | -------------------------------------------------------------------------------- /UPGRADE-2.0.md: -------------------------------------------------------------------------------- 1 | # Upgrading from 1.* to 2.0 2 | 3 | ## General 4 | 5 | * Instead of `createSearchIndex(name, options)` you now use the class `EasySearch.Index(configuration)` 6 | * Instead of `EasySearch.search(name, searchString, options[, callback])` you now use the instance method `index.search(searchDefinition, configuration)` 7 | * Instead of `EasySearch.createSearcher(name, options)` you create inherited classes, e.g. from `EasySearch.Engine(configuration)` 8 | * `collection.initEasySearch` has been removed in favor of instantiating an `EasySearch.Index` 9 | * All options that were previously passed to `EasySearch.createSearchIndex` are now split up into three levels of configuration: 10 | * Engine level configuration, how does the search behave (e.g. sort) 11 | * Index level configuration, which data is searchable and general configuration (e.g. permission) 12 | * Search level configuration, configuration specific to a search (e.g. limit) 13 | 14 | ```javascript 15 | 16 | let index = new EasySearch.Index({ 17 | // index level configuration 18 | collection: myCollection, 19 | engine: new EasySearch.Minimongo({ 20 | // engine level configuration 21 | sort : () => ['score'] 22 | }) 23 | }); 24 | 25 | index.search('Marie', { 26 | // search level configuration / options 27 | limit: 20, 28 | props: { 29 | 'maxScore': 200 30 | } 31 | }); 32 | ``` 33 | 34 | * ES6 / ES2015 is now used in the package code 35 | * Packages have been split up into several packages 36 | * `easysearch:core`: contains the Javascript API 37 | * `easysearch:components`: contains the Blaze Components 38 | * `easy:search`: Wrapper package for components and core 39 | * `easysearch:elasticsearch`: ElasticSearch engine 40 | * `easysearch:autosuggest`: Autosuggest component 41 | * `matteodem:easy-search` is now deprecated, switch to `easy:search` or one of the sub packages 42 | 43 | ## Index 44 | 45 | * Since there are multiple layers of configuration options most of it has changed places or renamed / removed where it made sense 46 | * `field` => `fields`: index configuratiion, always an array now 47 | * `collection` => `collection`: index configuration 48 | * `limit` => `limit`: search configuration 49 | * `query` => `selector` and `query`: engine configuration (mongo based engines use `selector` now) 50 | * `sort` => `sort`: engine configuration 51 | * `use` => `engine`: index configuration, is now an instanceof Engine 52 | * `convertNumbers` => _removed_: logic should be configured itself 53 | * `useTextIndex` => _removed_: It's own engine now (`EasySearch.MongoTextIndex`) 54 | * `transform` => `transform`: engine configuration, Now always return a document 55 | * `returnFields` => `beforePublish`: engine configuration, A function to return fields that are published 56 | * `changeResults` => _removed_: In favor of `beforePublish` or `transform` 57 | * `props` => `props`: search configuration 58 | * `weights` => `weights`: engine configuration, only for `EasySearch.MongoTextIndex` 59 | * `permission` => `permission`: index configuration 60 | * No `EasySearch.searchMultiple` anymore, use the index instances themselves to search on multiple indexes 61 | * No `changeProperty`, `changeLimit` anymore, use the `props` option while using the `search` method 62 | * No `pagination` anymore, use either the components package or implement the pagination logic by using `skip` and `limit` search configuration 63 | * `search` always returns a `EasySearch.Cursor` that can be used in a reactive context 64 | 65 | ## Components 66 | 67 | * Components are now prefixed with `EasySearch` (e.g. `EasySearch.Input`) 68 | * `EasySearch.getComponentInstance` is now split up into two index methods 69 | * `index.getComponentDict`: Retrieve search values, (e.g. search string, limit) 70 | * `index.getComponentMethods`: Use search component functionaly (e.g. searching, adding / removing props) 71 | 72 | 73 | ## ElasticSearch 74 | 75 | * Not part of core anymore, add `easysearch:elasticsearch` to use 76 | * `body` configuration to change ES body 77 | * ElasticSearch client reachable through `index.config.elasticSearchClient` 78 | 79 | ## Autosuggest 80 | 81 | * Not part of core anymore, add `easysearch:autossugest` to use 82 | * Uses now `jeremy:selectize` for display logic 83 | 84 | There are a lot of new features too, feel free to [check them out](http://matteodem.github.io/meteor-easy-search/). 85 | -------------------------------------------------------------------------------- /inch.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "included": [ 4 | "packages/**/*.js" 5 | ] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "easy-search", 3 | "version": "1.0.0", 4 | "description": "Easy Search [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) [![Build Status](https://travis-ci.org/matteodem/meteor-easy-search.svg?branch=master)](https://travis-ci.org/matteodem/meteor-easy-search) =====================", 5 | "main": "index.js", 6 | "dependencies": { 7 | "conventional-changelog": "^1.0.1", 8 | "cz-conventional-changelog": "^1.1.5" 9 | }, 10 | "devDependencies": { 11 | "cz-conventional-changelog": "^1.1.5" 12 | }, 13 | "scripts": { 14 | "test": "echo \"Error: no test specified\" && exit 1" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/matteodem/meteor-easy-search.git" 19 | }, 20 | "author": "", 21 | "license": "ISC", 22 | "bugs": { 23 | "url": "https://github.com/matteodem/meteor-easy-search/issues" 24 | }, 25 | "homepage": "https://github.com/matteodem/meteor-easy-search#readme", 26 | "config": { 27 | "commitizen": { 28 | "path": "./node_modules/cz-conventional-changelog" 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /packages/easy_search/main.js: -------------------------------------------------------------------------------- 1 | import { 2 | Engine, 3 | ReactiveEngine, 4 | Cursor, 5 | MongoDBEngine, 6 | MinimongoEngine, 7 | MongoTextIndexEngine, 8 | } from 'meteor/easysearch:core' 9 | 10 | import { 11 | Index, // index enhanced with component logic 12 | SingleIndexComponent, 13 | BaseComponent, 14 | FieldInputComponent, 15 | EachComponent, 16 | IfInputEmptyComponent, 17 | IfNoResultsComponent, 18 | IfSearchingComponent, 19 | InputComponent, 20 | LoadMoreComponent, 21 | PaginationComponent, 22 | } from 'meteor/easysearch:components' 23 | 24 | export { 25 | Index, 26 | Engine, 27 | ReactiveEngine, 28 | Cursor, 29 | 30 | MongoDBEngine, 31 | MinimongoEngine, 32 | MongoTextIndexEngine, 33 | 34 | SingleIndexComponent, 35 | BaseComponent, 36 | FieldInputComponent, 37 | EachComponent, 38 | IfInputEmptyComponent, 39 | IfNoResultsComponent, 40 | IfSearchingComponent, 41 | InputComponent, 42 | LoadMoreComponent, 43 | PaginationComponent, 44 | } 45 | -------------------------------------------------------------------------------- /packages/easy_search/package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'easy:search', 3 | summary: "Easy-to-use search with Blaze Components (+ Elastic Search Support)", 4 | version: "2.2.3", 5 | git: "https://github.com/matteodem/meteor-easy-search.git", 6 | documentation: "../../README.md" 7 | }); 8 | 9 | Package.onUse(function(api) { 10 | api.versionsFrom('1.4.2'); 11 | 12 | api.use([ 13 | 'ecmascript', 14 | 'easysearch:core@2.2.3', 15 | 'easysearch:components@2.2.3', 16 | ]); 17 | 18 | api.export('EasySearch'); 19 | 20 | api.mainModule('./main.js'); 21 | }); 22 | -------------------------------------------------------------------------------- /packages/easysearch_autosuggest/README.md: -------------------------------------------------------------------------------- 1 | Easy Search Autosuggest 2 | ===================== 3 | 4 | This package adds an autosuggest blaze component that uses __selectize__ (`jeremy:selectize`) to handle the display logic itself and 5 | __EasySearch__ for the search. Like the [EasySearch Components](#putInLink) it only requires you to specify an index. 6 | 7 | ```html 8 | 12 | ``` 13 | 14 | ## Parameters 15 | 16 | You can pass in following parameters to the `EasySearch.Autosuggest` component. 17 | * __valueField__: String that specifies the document field that contains the autosuggest value, by default "_id" 18 | * __labelField__: String that specifies the search result field to display, by default the first of index `fields` 19 | * __changeConfiguration__: Function to change the configuration that is passed to selectize. 20 | * __renderSuggestion__: String that specifies a custom template to render the autosuggest, by default `EasySearch_Autosuggest_DefaultRenderSuggestion` 21 | * __noDocumentsOnEmpty__: Same as for EasySearch.Input 22 | 23 | ## How to install 24 | 25 | ```sh 26 | cd /path/to/project 27 | meteor add easysearch:autosuggest 28 | ``` 29 | -------------------------------------------------------------------------------- /packages/easysearch_autosuggest/lib/autosuggest.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /packages/easysearch_autosuggest/lib/autosuggest.js: -------------------------------------------------------------------------------- 1 | import { Template } from 'meteor/templating' 2 | import { SingleIndexComponent } from 'meteor/easysearch:components' 3 | 4 | const getDataValue = (scope, val, defaultVal) => scope.getData()[val] || defaultVal 5 | 6 | class AutosuggestComponent extends SingleIndexComponent 7 | { 8 | /** 9 | * Search autosuggest by given string. 10 | * 11 | * @param {String} str 12 | * @returns {Cursor} 13 | */ 14 | search(str) { 15 | const methods = this.index.getComponentMethods(this.name) 16 | 17 | methods.search(str) 18 | 19 | return methods.getCursor() 20 | } 21 | 22 | /** 23 | * Setup autosuggest on rendered 24 | */ 25 | onRendered() { 26 | let handle 27 | let computation 28 | 29 | const valueField = getDataValue(this, 'valueField', '_id') 30 | const labelField = getDataValue(this, 'labelField', this.index.config.fields[0]) 31 | const searchField = getDataValue(this, 'searchField', labelField) 32 | const changeConfiguration = getDataValue(this, 'changeConfiguration', (c) => c) 33 | const suggestionTemplate = Template[ 34 | getDataValue( 35 | this, 36 | 'renderSuggestion', 37 | 'EasySearch_Autosuggest_DefaultRenderSuggestion' 38 | ) 39 | ] 40 | 41 | const select = this.$('select').selectize(changeConfiguration({ 42 | valueField, 43 | labelField, 44 | searchField, 45 | create: false, 46 | preload: true, 47 | render: { 48 | option: (item, escape) => Blaze.toHTMLWithData(suggestionTemplate, { 49 | doc: item, 50 | _id: item._id, 51 | label: _.get(item, labelField) 52 | }) 53 | }, 54 | load: (query, callback) => { 55 | if (computation) { 56 | computation.stop() 57 | } 58 | 59 | computation = Tracker.autorun(() => { 60 | const cursor = this.search(query) 61 | const docs = cursor.fetch() 62 | 63 | if (handle) { 64 | clearTimeout(handle) 65 | } 66 | 67 | handle = setTimeout(() => { 68 | select[0].selectize.clearOptions() 69 | callback(docs) 70 | }, 100) 71 | }) 72 | } 73 | })) 74 | } 75 | } 76 | 77 | AutosuggestComponent.register('EasySearch.Autosuggest') 78 | 79 | export { AutosuggestComponent } 80 | -------------------------------------------------------------------------------- /packages/easysearch_autosuggest/package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'easysearch:autosuggest', 3 | summary: "Selectize Autosuggest Component for EasySearch", 4 | version: "2.2.3", 5 | git: "https://github.com/matteodem/meteor-easy-search.git", 6 | documentation: 'README.md' 7 | }); 8 | 9 | Package.onUse(function(api) { 10 | api.versionsFrom('1.4.2'); 11 | 12 | // Dependencies 13 | api.use(['check', 'ecmascript', 'templating@1.2.15', 'blaze@2.2.0']); 14 | api.use([ 15 | 'easysearch:core@2.2.3', 16 | 'easysearch:components@2.2.3', 17 | 'jeremy:selectize@0.12.1_4', 18 | ]); 19 | 20 | api.use(['erasaur:meteor-lodash@4.0.0']); 21 | 22 | api.addFiles([ 23 | 'lib/autosuggest.html', 24 | ], 'client'); 25 | 26 | api.mainModule('lib/autosuggest.js', 'client'); 27 | }); 28 | 29 | Package.onTest(function(api) { 30 | api.use(['tinytest', 'ecmascript', 'templating']); 31 | api.use('easysearch:autosuggest'); 32 | 33 | api.addFiles(['tests/autosuggest-tests.js'], 'client'); 34 | }); 35 | -------------------------------------------------------------------------------- /packages/easysearch_autosuggest/tests/autosuggest-tests.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matteodem/meteor-easy-search/020d2647a53493e2b8f91784ae1dd2f6552dcb89/packages/easysearch_autosuggest/tests/autosuggest-tests.js -------------------------------------------------------------------------------- /packages/easysearch_components/README.md: -------------------------------------------------------------------------------- 1 | Easy Search Components 2 | ===================== 3 | 4 | The components package adds helpful Blaze Templates to your app that cover a lot of basic functionality with extendibility and customization in mind. The `easy:search` package wraps this package together with `easysearch:core` for convenience. 5 | 6 | ```html 7 | 35 | ``` 36 | 37 | ## How to install 38 | 39 | ```sh 40 | cd /path/to/project 41 | meteor add easysearch:components 42 | ``` 43 | 44 | NB: This package will use the `erasaur:meteor-lodash` package if it is already installed in your application, else it will fallback to the standard Meteor `underscore` package 45 | -------------------------------------------------------------------------------- /packages/easysearch_components/component_methods.md: -------------------------------------------------------------------------------- 1 | # Component methods in EasySearch 2 | 3 | All the EasySearch components use the api that is [defined here](https://github.com/matteodem/meteor-easy-search/blob/master/packages/easysearch:components/lib/component-methods.js#L1). You can do 4 | things such as `search`, `loadMore` or `paginate` and render them by using `EasySearch.Each` (and other components). 5 | 6 | ```js 7 | import { peopleIndex } from './search/people-index' 8 | // instanceof EasySearch Index 9 | 10 | const methods = peopleIndex.getComponentMethods(/* optional name */) 11 | 12 | methods.search('peter') 13 | ``` 14 | 15 | Consider having a look at the [source code](https://github.com/matteodem/meteor-easy-search/blob/master/packages/easysearch:components/lib/component-methods.js#L1) for all the methods. 16 | Don't forget to pass in a name if you have defined one in your blaze components. 17 | -------------------------------------------------------------------------------- /packages/easysearch_components/lib/base.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The BaseComponent holds the base logic for EasySearch Components. 3 | * 4 | * @type {BaseComponent} 5 | */ 6 | BaseComponent = class BaseComponent extends BlazeComponent { 7 | /** 8 | * Return the name of the component. 9 | * 10 | * @returns {String} 11 | */ 12 | get name() { 13 | return this.getData().name; 14 | } 15 | 16 | /** 17 | * Return an array of properties that are reserved to the base component. 18 | * 19 | * @returns {String[]} 20 | */ 21 | static get reserveredProperties() { 22 | return ['index', 'indexes', 'name', 'attributes']; 23 | } 24 | 25 | /** 26 | * Setup component on created. 27 | */ 28 | onCreated() { 29 | this.autorun(() => this.initializeBase()); 30 | } 31 | 32 | initializeBase() { 33 | let index = this.getData().index, 34 | indexes = [index]; 35 | 36 | if (!index) { 37 | indexes = this.getData().indexes; 38 | } 39 | 40 | if (_.isEmpty(indexes)) { 41 | throw new Meteor.Error('no-index', 'Please provide an index for your component'); 42 | } 43 | 44 | if (indexes.filter((index) => index instanceof EasySearch.Index).length !== indexes.length) { 45 | throw new Meteor.Error( 46 | 'invalid-configuration', 47 | `Did not receive an index or an array of indexes: "${indexes.toString()}"` 48 | ); 49 | } 50 | 51 | this.indexes = indexes; 52 | this.options = _.defaults({}, _.omit(this.getData(), ...BaseComponent.reserveredProperties), this.defaultOptions); 53 | 54 | check(this.name, Match.Optional(String)); 55 | check(this.options, Object); 56 | 57 | this.eachIndex(function (index, name) { 58 | if (!index.getComponentDict(name)) { 59 | index.registerComponent(name); 60 | } 61 | }); 62 | } 63 | 64 | /** 65 | * Return the default options. 66 | * 67 | * @returns {Object} 68 | */ 69 | get defaultOptions () { 70 | return {}; 71 | } 72 | 73 | /** 74 | * @param {String} searchStr 75 | * 76 | * @returns {Boolean} 77 | */ 78 | shouldShowDocuments(searchStr) { 79 | return !this.getData().noDocumentsOnEmpty || 0 < searchStr.length; 80 | } 81 | 82 | /** 83 | * Search the component. 84 | * 85 | * @param {String} searchString String to search for 86 | */ 87 | search(searchString) { 88 | check(searchString, String); 89 | 90 | const showDocuments = this.shouldShowDocuments(searchString); 91 | 92 | this.eachIndex(function (index, name) { 93 | index.getComponentDict(name).set('showDocuments', showDocuments); 94 | 95 | if (showDocuments) { 96 | index.getComponentMethods(name).search(searchString); 97 | } 98 | }); 99 | } 100 | 101 | /** 102 | * Return the data. 103 | * 104 | * @returns {Object} 105 | */ 106 | getData() { 107 | return (this.data() || {}); 108 | } 109 | 110 | /** 111 | * Return the dictionaries. 112 | * 113 | * @returns {Object} 114 | */ 115 | get dicts() { 116 | return this.eachIndex((index, name) => { 117 | return index.getComponentDict(name); 118 | }, 'map'); 119 | } 120 | 121 | /** 122 | * Loop through each index and apply a function 123 | * 124 | * @param {Function} func Function to run 125 | * @param {String} method Lodash method name 126 | * 127 | * @return mixed 128 | */ 129 | eachIndex(func, method = 'each') { 130 | let componentScope = this, 131 | logic = this.getData().logic; 132 | 133 | if (!_.isEmpty(logic)) { 134 | method = 'OR' === logic ? 'some' : 'every'; 135 | } 136 | 137 | return _[method](this.indexes, function (index) { 138 | return func.apply(this, [index, componentScope.name]); 139 | }); 140 | } 141 | }; 142 | 143 | EasySearch.BaseComponent = BaseComponent; 144 | -------------------------------------------------------------------------------- /packages/easysearch_components/lib/component-methods.js: -------------------------------------------------------------------------------- 1 | EasySearch._getComponentMethods = function (dict, index) { 2 | return { 3 | /** 4 | * Search a component for the given search string. 5 | * 6 | * @param {Object|String} searchDefinition Search definition 7 | */ 8 | search: (searchDefinition) => { 9 | dict.set('searchOptions', { 10 | props: (dict.get('searchOptions') || {}).props 11 | }); 12 | 13 | dict.set('searchDefinition', searchDefinition); 14 | dict.set('stopPublication', true); 15 | }, 16 | /** 17 | * Return the EasySearch.Cursor for the current search. 18 | * 19 | * @returns {Cursor} 20 | */ 21 | getCursor: () => { 22 | const searchDefinition = dict.get('searchDefinition') || '', 23 | options = dict.get('searchOptions') || {}, 24 | showDocuments = dict.get('showDocuments'); 25 | 26 | check(options, Match.Optional(Object)); 27 | 28 | if (false === showDocuments) { 29 | dict.set('count', 0); 30 | dict.set('searching', false); 31 | dict.set('limit', 0); 32 | dict.set('skip', 0); 33 | dict.set('currentCount', 0); 34 | dict.set('stopPublication', false); 35 | 36 | return EasySearch.Cursor.emptyCursor; 37 | } 38 | 39 | const cursor = index.search(searchDefinition, options), 40 | searchOptions = index._getSearchOptions(options); 41 | 42 | dict.set('count', cursor.count()); 43 | dict.set('searching', !cursor.isReady()); 44 | dict.set('limit', searchOptions.limit); 45 | dict.set('skip', searchOptions.skip); 46 | dict.set('currentCount', cursor.mongoCursor.count()); 47 | dict.set('stopPublication', false); 48 | 49 | return cursor; 50 | }, 51 | /** 52 | * Return true if the current search string is empty. 53 | * 54 | * @returns {boolean} 55 | */ 56 | searchIsEmpty: () => { 57 | let searchDefinition = dict.get('searchDefinition'); 58 | 59 | return !searchDefinition || (_.isString(searchDefinition) && 0 === searchDefinition.trim().length); 60 | }, 61 | /** 62 | * Return true if the component has no results. 63 | * 64 | * @returns {boolean} 65 | */ 66 | hasNoResults: () => { 67 | let count = dict.get('count'), 68 | showDocuments = dict.get('showDocuments'); 69 | 70 | return false !== showDocuments 71 | && !dict.get('searching') 72 | && (!_.isNumber(count) || 0 === count); 73 | }, 74 | /** 75 | * Return true if the component is being searched. 76 | * 77 | * @returns {boolean} 78 | */ 79 | isSearching: () => { 80 | return !!dict.get('searching'); 81 | }, 82 | /** 83 | * Return true if the component has more documents than displayed right now. 84 | * 85 | * @returns {boolean} 86 | */ 87 | hasMoreDocuments: () => { 88 | return dict.get('currentCount') < dict.get('count'); 89 | }, 90 | /** 91 | * Load more documents for the component. 92 | * 93 | * @param {Number} count Count of docs 94 | */ 95 | loadMore: (count) => { 96 | check(count, Number); 97 | 98 | let currentCount = dict.get('currentCount'), 99 | options = dict.get('searchOptions') || {}; 100 | 101 | options.limit = currentCount + count; 102 | dict.set('searchOptions', options); 103 | }, 104 | /** 105 | * Paginate through documents for the given page. 106 | * 107 | * @param {Number} page Page number 108 | */ 109 | paginate: (page) => { 110 | check(page, Number); 111 | 112 | let options = dict.get('searchOptions') || {}, 113 | limit = dict.get('limit'); 114 | 115 | options.skip = limit * (page - 1); 116 | dict.set('currentPage', page); 117 | dict.set('searchOptions', options); 118 | dict.set('stopPublication', true); 119 | }, 120 | /** 121 | * Add custom properties for search. 122 | */ 123 | addProps(...args) { 124 | let options = dict.get('searchOptions') || {}; 125 | 126 | options.props = options.props || {}; 127 | 128 | if (_.isObject(args[0])) { 129 | options.props = _.extend(options.props, args[0]); 130 | } else if (_.isString(args[0])) { 131 | options.props[args[0]] = args[1]; 132 | } 133 | 134 | dict.set('searchOptions', options); 135 | this.paginate(1); 136 | }, 137 | /** 138 | * Remove custom properties for search. 139 | */ 140 | removeProps(...args) { 141 | let options = dict.get('searchOptions') || {}; 142 | 143 | if (!_.isEmpty(args)) { 144 | options.props = _.omit(options.props, args) || {}; 145 | } else { 146 | options.props = {}; 147 | } 148 | 149 | dict.set('searchOptions', options); 150 | this.paginate(1); 151 | }, 152 | /** 153 | * Reset the search. 154 | */ 155 | reset() { 156 | this.search(''); 157 | this.paginate(1); 158 | dict.set('searchOptions', {}); 159 | }, 160 | }; 161 | }; 162 | -------------------------------------------------------------------------------- /packages/easysearch_components/lib/core.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Extend EasySearch.Index with component functionality. 3 | * 4 | * @type {Index} 5 | */ 6 | EasySearch.Index = class Index extends EasySearch.Index { 7 | /** 8 | * Constructor. 9 | */ 10 | constructor() { 11 | super(...arguments); 12 | this.components = {}; 13 | } 14 | 15 | /** 16 | * Return static default name for components. 17 | * 18 | * @returns {String} 19 | */ 20 | static get COMPONENT_DEFAULT_NAME() { 21 | return'__default'; 22 | } 23 | 24 | /** 25 | * Register a component on the index. 26 | * 27 | * @param {String} componentName Optional name of the component 28 | */ 29 | registerComponent(componentName = EasySearch.Index.COMPONENT_DEFAULT_NAME) { 30 | this.components[componentName] = new ReactiveDict( 31 | `easySearchComponent_${this.config.name}_${componentName}_${Random.id()}` 32 | ); 33 | } 34 | 35 | /** 36 | * Get the reactive dictionary for a component. 37 | * 38 | * @param {String} componentName Optional name of the component 39 | */ 40 | getComponentDict(componentName = EasySearch.Index.COMPONENT_DEFAULT_NAME) { 41 | return this.components[componentName]; 42 | } 43 | 44 | /** 45 | * Get component methods that are useful for implementing search behaviour. 46 | * 47 | * @param componentName 48 | */ 49 | getComponentMethods(componentName = EasySearch.Index.COMPONENT_DEFAULT_NAME) { 50 | let dict = this.getComponentDict(componentName); 51 | 52 | if (!dict) { 53 | throw new Meteor.Error('no-component', `Component with name '${componentName}' not found`); 54 | } 55 | 56 | return EasySearch._getComponentMethods(dict, this); 57 | } 58 | }; 59 | 60 | /** 61 | * Return true if the current page is valid. 62 | * 63 | * @param {Number} totalPagesLength Count of all pages available 64 | * @param {Number} currentPage Current page to check 65 | * 66 | * @returns {boolean} 67 | */ 68 | function isValidPage(totalPagesLength, currentPage) { 69 | return currentPage <= totalPagesLength && currentPage > 0; 70 | } 71 | 72 | /** 73 | * Helper method to get the pages for pagination as an array. 74 | * 75 | * @param totalCount Total count of results 76 | * @param pageCount Count of results per page 77 | * @param currentPage Current page 78 | * @param prevAndNext True if Next and Previous buttons should appear 79 | * @param maxPages Maximum count of pages to show 80 | * 81 | * @private 82 | * 83 | * @returns {Array} 84 | */ 85 | EasySearch._getPagesForPagination = function ({totalCount, pageCount, currentPage, prevAndNext, maxPages}) { 86 | let pages = _.range(1, Math.ceil(totalCount / pageCount) + 1), 87 | pagesLength = pages.length; 88 | 89 | if (!isValidPage(pagesLength, currentPage)) { 90 | throw new Meteor.Error('invalid-page', 'Current page is not in valid range'); 91 | } 92 | 93 | if (maxPages) { 94 | let startSlice = (currentPage > (maxPages / 2) ? (currentPage - 1) - Math.floor(maxPages / 2) : 0), 95 | endSlice = startSlice + maxPages; 96 | 97 | if (endSlice > pagesLength) { 98 | pages = pages.slice(-maxPages); 99 | } else { 100 | pages = pages.slice(startSlice, startSlice + maxPages); 101 | } 102 | } 103 | 104 | let pageData = _.map(pages, function (page) { 105 | let isCurrentPage = page === currentPage; 106 | return { page, content: page.toString(), current: isCurrentPage, disabled: isCurrentPage }; 107 | }); 108 | 109 | if (prevAndNext) { 110 | // Previous 111 | let prevPage = isValidPage(pagesLength, currentPage - 1) ? currentPage - 1 : null; 112 | pageData.unshift({ page: prevPage, content: 'Prev', current: false, disabled: 1 === currentPage }); 113 | // Next 114 | let nextPage = isValidPage(pagesLength, currentPage + 1) ? currentPage + 1 : null; 115 | pageData.push( 116 | { page: nextPage, content: 'Next', current: false, disabled: null == nextPage || pagesLength + 1 === currentPage } 117 | ); 118 | } 119 | 120 | return pageData; 121 | }; 122 | -------------------------------------------------------------------------------- /packages/easysearch_components/lib/each/each.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /packages/easysearch_components/lib/each/each.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The EachComponent allows to loop through the search results found. 3 | * 4 | * @type {EachComponent} 5 | */ 6 | EasySearch.EachComponent = class EachComponent extends SingleIndexComponent { 7 | /** 8 | * Return the mongo cursor for the search. 9 | * 10 | * @returns {Mongo.Cursor} 11 | */ 12 | doc() { 13 | const stopPublication = this.index 14 | .getComponentDict(this.name) 15 | .get('stopPublication') 16 | ; 17 | 18 | this.cursor && stopPublication && this.cursor.stop(); 19 | 20 | this.cursor = this.index 21 | .getComponentMethods(this.name) 22 | .getCursor() 23 | ; 24 | 25 | return this.cursor.mongoCursor; 26 | } 27 | 28 | /** 29 | * Return the datascope for each document. 30 | * 31 | * @param {Object} scope 32 | * @param {Number} index 33 | * 34 | * @returns {Object} 35 | */ 36 | dataScope(scope, index) { 37 | scope['@index'] = index 38 | 39 | return scope 40 | } 41 | }; 42 | 43 | EasySearch.EachComponent.register('EasySearch.Each'); 44 | -------------------------------------------------------------------------------- /packages/easysearch_components/lib/field-input/field-input.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /packages/easysearch_components/lib/field-input/field-input.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The FieldInputComponent lets you search through configured indexes for a specified fild. 3 | * 4 | * @type {FieldInputComponent} 5 | */ 6 | EasySearch.FieldInputComponent = class FieldInputComponent extends EasySearch.InputComponent { 7 | /** 8 | * Setup component on created. 9 | */ 10 | onCreated() { 11 | super.onCreated(); 12 | 13 | if (_.isEmpty(this.getData().field)) { 14 | throw new Meteor.Error('no-field', 'Please provide a field for your field input component'); 15 | } 16 | } 17 | 18 | /** 19 | * Search the component. 20 | * 21 | * @param {String} searchString String to search for 22 | */ 23 | search(searchString) { 24 | check(searchString, String); 25 | 26 | this.eachIndex((index, name) => { 27 | let searchDefinition = index.getComponentDict(name).get('searchDefinition') || {}; 28 | 29 | if (_.isString(searchDefinition)) { 30 | throw new Meteor.Error('You can either EasySearch.FieldInput or EasySearch.Input'); 31 | } 32 | 33 | if (this.options.field) { 34 | searchDefinition[this.options.field] = searchString; 35 | index.getComponentMethods(name).search(searchDefinition); 36 | } 37 | }); 38 | } 39 | }; 40 | 41 | EasySearch.FieldInputComponent.register('EasySearch.FieldInput'); 42 | -------------------------------------------------------------------------------- /packages/easysearch_components/lib/if-input-empty/if-input-empty.html: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /packages/easysearch_components/lib/if-input-empty/if-input-empty.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The IfInputEmptyComponent lets you display content when the input is empty. 3 | * 4 | * @type {IfInputEmptyComponent} 5 | */ 6 | EasySearch.IfInputEmptyComponent = class IfInputEmptyComponent extends BaseComponent { 7 | /** 8 | * Return true if the input is empty. 9 | * 10 | * @returns {boolean} 11 | */ 12 | inputEmpty() { 13 | return !!this.eachIndex(function (index, name) { 14 | return index.getComponentMethods(name).searchIsEmpty(); 15 | }, 'every'); 16 | } 17 | }; 18 | 19 | EasySearch.IfInputEmptyComponent.register('EasySearch.IfInputEmpty'); 20 | -------------------------------------------------------------------------------- /packages/easysearch_components/lib/if-no-results/if-no-results.html: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /packages/easysearch_components/lib/if-no-results/if-no-results.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The IfNoResultsComponent lets you display content when there are no results. 3 | * 4 | * @type {IfNoResultsComponent} 5 | */ 6 | EasySearch.IfNoResultsComponent = class IfNoResultsComponent extends BaseComponent { 7 | /** 8 | * Return true if there are no results. 9 | * 10 | * @returns {boolean} 11 | */ 12 | noResults() { 13 | return !!this.eachIndex(function (index, name) { 14 | return index.getComponentMethods(name).hasNoResults(); 15 | }, 'every'); 16 | } 17 | }; 18 | 19 | EasySearch.IfNoResultsComponent.register('EasySearch.IfNoResults'); 20 | -------------------------------------------------------------------------------- /packages/easysearch_components/lib/if-searching/if-searching.html: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /packages/easysearch_components/lib/if-searching/if-searching.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The IfSearchingComponent lets you display content when the component is being searched. 3 | * 4 | * @type {IfSearchingComponent} 5 | */ 6 | EasySearch.IfSearchingComponent = class IfSearchingComponent extends BaseComponent { 7 | /** 8 | * Return true if the component is being searched. 9 | * 10 | * @returns {boolean} 11 | */ 12 | searching() { 13 | return !!this.eachIndex(function (index, name) { 14 | return index.getComponentMethods(name).isSearching(); 15 | }, 'every'); 16 | } 17 | }; 18 | 19 | EasySearch.IfSearchingComponent.register('EasySearch.IfSearching'); 20 | -------------------------------------------------------------------------------- /packages/easysearch_components/lib/input/input.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /packages/easysearch_components/lib/input/input.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The InputComponent lets you search through configured indexes. 3 | * 4 | * @type {InputComponent} 5 | */ 6 | EasySearch.InputComponent = class InputComponent extends BaseComponent { 7 | /** 8 | * Setup input onCreated. 9 | */ 10 | onCreated() { 11 | super.onCreated(...arguments); 12 | 13 | this.search(this.inputAttributes().value); 14 | 15 | // create a reactive dependency to the cursor 16 | this.debouncedSearch = _.debounce((searchString) => { 17 | searchString = searchString.trim(); 18 | 19 | if (this.searchString !== searchString) { 20 | this.searchString = searchString; 21 | 22 | this.eachIndex((index, name) => { 23 | index.getComponentDict(name).set('currentPage', 1); 24 | }); 25 | 26 | this.search(searchString); 27 | } 28 | 29 | }, this.options.timeout); 30 | } 31 | 32 | /** 33 | * Event map. 34 | * 35 | * @returns {Object} 36 | */ 37 | events() { 38 | return [{ 39 | 'keyup input' : function (e) { 40 | if ('enter' == this.getData().event && e.keyCode != 13) { 41 | return; 42 | } 43 | 44 | const value = $(e.target).val(); 45 | 46 | if (value.length >= this.options.charLimit) { 47 | this.debouncedSearch($(e.target).val()); 48 | } 49 | } 50 | }]; 51 | } 52 | 53 | /** 54 | * Return the attributes to set on the input (class, id). 55 | * 56 | * @returns {Object} 57 | */ 58 | inputAttributes() { 59 | return _.defaults({}, this.getData().attributes, InputComponent.defaultAttributes); 60 | } 61 | 62 | /** 63 | * Return the default attributes. 64 | * 65 | * @returns {Object} 66 | */ 67 | static get defaultAttributes() { 68 | return { 69 | type: 'text', 70 | value: '' 71 | }; 72 | } 73 | 74 | /** 75 | * Return the default options. 76 | * 77 | * @returns {Object} 78 | */ 79 | get defaultOptions() { 80 | return { 81 | timeout: 50, 82 | charLimit: 0 83 | }; 84 | } 85 | }; 86 | 87 | EasySearch.InputComponent.register('EasySearch.Input'); 88 | -------------------------------------------------------------------------------- /packages/easysearch_components/lib/load-more/load-more.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /packages/easysearch_components/lib/load-more/load-more.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The LoadMoreComponent lets you load more documents through a button. 3 | * 4 | * @type {LoadMoreComponent} 5 | */ 6 | EasySearch.LoadMoreComponent = class LoadMoreComponent extends SingleIndexComponent { 7 | /** 8 | * Load more documents. 9 | */ 10 | loadMore() { 11 | this.index 12 | .getComponentMethods(this.name) 13 | .loadMore(this.options.count) 14 | ; 15 | } 16 | 17 | /** 18 | * Content of the component. 19 | * 20 | * @returns string 21 | */ 22 | content() { 23 | return this.options.content; 24 | } 25 | 26 | /** 27 | * Attributes of the component. 28 | * 29 | * @returns string 30 | */ 31 | attributes() { 32 | return this.getData().attributes || {}; 33 | } 34 | 35 | /** 36 | * Return true if there are more documents to load. 37 | * 38 | * @returns {Boolean} 39 | */ 40 | moreDocuments() { 41 | return this.index.getComponentMethods(this.name).hasMoreDocuments(); 42 | } 43 | 44 | /** 45 | * Event map. 46 | * 47 | * @returns {Object} 48 | */ 49 | events() { 50 | return [{ 51 | 'click button' : function () { 52 | this.loadMore(); 53 | } 54 | }]; 55 | } 56 | 57 | /** 58 | * Return the default options. 59 | * 60 | * @returns {Object} 61 | */ 62 | get defaultOptions() { 63 | return { 64 | content: 'Load more', 65 | count: 10 66 | }; 67 | } 68 | }; 69 | 70 | EasySearch.LoadMoreComponent.register('EasySearch.LoadMore'); 71 | -------------------------------------------------------------------------------- /packages/easysearch_components/lib/main.js: -------------------------------------------------------------------------------- 1 | const { 2 | Index, 3 | SingleIndexComponent, 4 | BaseComponent, 5 | FieldInputComponent, 6 | EachComponent, 7 | IfInputEmptyComponent, 8 | IfNoResultsComponent, 9 | IfSearchingComponent, 10 | InputComponent, 11 | LoadMoreComponent, 12 | PaginationComponent, 13 | } = EasySearch; 14 | 15 | export { 16 | Index, 17 | SingleIndexComponent, 18 | BaseComponent, 19 | FieldInputComponent, 20 | EachComponent, 21 | IfInputEmptyComponent, 22 | IfNoResultsComponent, 23 | IfSearchingComponent, 24 | InputComponent, 25 | LoadMoreComponent, 26 | PaginationComponent, 27 | }; 28 | -------------------------------------------------------------------------------- /packages/easysearch_components/lib/pagination/pagination.html: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /packages/easysearch_components/lib/pagination/pagination.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The PaginationComponent lets you paginate through documents. 3 | * 4 | * @type {PaginationComponent} 5 | */ 6 | EasySearch.PaginationComponent = class PaginationComponent extends SingleIndexComponent { 7 | /** 8 | * Setup component on created. 9 | */ 10 | onCreated() { 11 | super.onCreated(); 12 | this.index.getComponentMethods(this.name).paginate(1); 13 | } 14 | 15 | /** 16 | * Get pages for displaying the pagination. 17 | * 18 | * @returns {Array} 19 | */ 20 | page() { 21 | let totalCount = this.dict.get('count'), 22 | pageCount = this.dict.get('limit'), 23 | currentPage = this.dict.get('currentPage'), 24 | maxPages = this.options.maxPages, 25 | prevAndNext = this.options.prevAndNext; 26 | 27 | if (!pageCount || !totalCount) { 28 | return []; 29 | } 30 | 31 | return this.options.transformPages( 32 | EasySearch._getPagesForPagination( 33 | { 34 | totalCount, 35 | pageCount, 36 | currentPage, 37 | maxPages, 38 | prevAndNext 39 | } 40 | )); 41 | } 42 | 43 | customRenderPagination() { 44 | return this.getData().customRenderPagination; 45 | } 46 | 47 | /** 48 | * Paginate documents. 49 | */ 50 | paginate(page) { 51 | check(page, Number); 52 | 53 | this.index.getComponentMethods(this.name).paginate(page); 54 | } 55 | 56 | /** 57 | * Return page classes. 58 | * 59 | * @param {Object} data Data for the current page 60 | * 61 | * @returns {String} 62 | */ 63 | pageClasses(data) { 64 | return `${(data.disabled ? 'disabled' : '' )} ${(data.current ? 'current' : '' )}`.trim(); 65 | } 66 | 67 | /** 68 | * Return true if there are more documents to load. 69 | * 70 | * @returns {Boolean} 71 | */ 72 | moreDocuments() { 73 | return this.index.getComponentMethods(this.name).hasMoreDocuments(); 74 | } 75 | 76 | /** 77 | * Event map. 78 | * 79 | * @returns {Object} 80 | */ 81 | events() { 82 | return [{ 83 | 'click .page:not(.disabled)' : function (e) { 84 | let currentPage = this.currentData().page; 85 | this.dict.set('currentPage', currentPage); 86 | this.paginate(currentPage); 87 | 88 | e.preventDefault(); 89 | } 90 | }]; 91 | } 92 | 93 | /** 94 | * Return the default options. 95 | * 96 | * @returns {Object} 97 | */ 98 | get defaultOptions() { 99 | return { 100 | prevAndNext: true, 101 | maxPages: null, 102 | transformPages: (pages) => pages 103 | }; 104 | } 105 | }; 106 | 107 | EasySearch.PaginationComponent.register('EasySearch.Pagination'); 108 | -------------------------------------------------------------------------------- /packages/easysearch_components/lib/single-index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The SingleIndexComponent holds logic for components that only can use one index. 3 | * 4 | * @type {SingleIndexComponent} 5 | */ 6 | SingleIndexComponent = class SingleIndexComponent extends BaseComponent { 7 | /** 8 | * Setup component on created. 9 | */ 10 | onCreated() { 11 | super.onCreated(); 12 | 13 | if (this.indexes.length > 1) { 14 | throw new Meteor.Error('only-single-index', 'Can only specify one index'); 15 | } 16 | } 17 | 18 | /** 19 | * Return the index 20 | * 21 | * @returns {Index} 22 | */ 23 | get index() { 24 | return _.first(this.indexes); 25 | } 26 | 27 | /** 28 | * Return the dictionary. 29 | * 30 | * @returns {Object} 31 | */ 32 | get dict() { 33 | return _.first(this.dicts); 34 | } 35 | }; 36 | 37 | EasySearch.SingleIndexComponent = SingleIndexComponent; 38 | -------------------------------------------------------------------------------- /packages/easysearch_components/package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'easysearch:components', 3 | summary: "Blaze Components for EasySearch", 4 | version: "2.2.3", 5 | git: "https://github.com/matteodem/meteor-easy-search.git", 6 | documentation: 'README.md' 7 | }); 8 | 9 | Package.onUse(function(api) { 10 | api.versionsFrom('1.4.2'); 11 | 12 | // Dependencies 13 | api.use(['check', 'reactive-dict', 'ecmascript', 'random', 'underscore', 'tracker', 'mongo']); 14 | api.use(['peerlibrary:blaze-components@0.16.0', 'easysearch:core@2.2.3']); 15 | api.use(['erasaur:meteor-lodash@4.0.0'], { weak: true }); 16 | 17 | // Base Component 18 | api.addFiles(['lib/base.js', 'lib/single-index.js', 'lib/component-methods.js', 'lib/core.js'], 'client'); 19 | 20 | // Input and Each 21 | api.addFiles(['lib/input/input.html', 'lib/input/input.js', 'lib/field-input/field-input.html', 'lib/field-input/field-input.js'], 'client'); 22 | api.addFiles(['lib/each/each.html', 'lib/each/each.js'], 'client'); 23 | 24 | // If Components 25 | api.addFiles(['lib/if-input-empty/if-input-empty.html', 'lib/if-input-empty/if-input-empty.js'], 'client'); 26 | api.addFiles(['lib/if-no-results/if-no-results.html', 'lib/if-no-results/if-no-results.js'], 'client'); 27 | api.addFiles(['lib/if-searching/if-searching.html', 'lib/if-searching/if-searching.js'], 'client'); 28 | 29 | // Loading More Components 30 | api.addFiles([ 31 | 'lib/load-more/load-more.html', 'lib/load-more/load-more.js', 'lib/pagination/pagination.html', 'lib/pagination/pagination.js' 32 | ], 'client'); 33 | 34 | api.export('EasySearch'); 35 | 36 | api.mainModule('lib/main.js'); 37 | }); 38 | 39 | Package.onTest(function(api) { 40 | api.use(['tinytest', 'ecmascript', 'tracker', 'underscore', 'mongo']); 41 | api.use('easysearch:components'); 42 | 43 | // Test Helpers 44 | api.addFiles(['tests/helpers.js']); 45 | 46 | // Unit tests 47 | api.addFiles([ 48 | 'tests/unit/input-tests.js', 49 | 'tests/unit/field-input-tests.js', 50 | 'tests/unit/each-tests.js', 51 | 'tests/unit/if-tests.js', 52 | 'tests/unit/base-tests.js', 53 | 'tests/unit/load-more-tests.js', 54 | 'tests/unit/core-tests.js', 55 | 'tests/unit/pagination-tests.js', 56 | 'tests/unit/component-methods-tests.js' 57 | ], 'client'); 58 | }); 59 | -------------------------------------------------------------------------------- /packages/easysearch_components/tests/helpers.js: -------------------------------------------------------------------------------- 1 | TestHelpers = { 2 | createComponent: function(component, data) { 3 | var c = new component(); 4 | 5 | c.data = function () { return data; }; 6 | c.autorun = (f) => f(); 7 | 8 | return c; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /packages/easysearch_components/tests/unit/base-tests.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | Tinytest.add('EasySearch Components - Unit - Base', function (test) { 4 | var component = TestHelpers.createComponent(EasySearch.BaseComponent, { 5 | name: 'customName', 6 | index: new EasySearch.Index({ 7 | collection: new Mongo.Collection('baseIndexCollection'), 8 | engine: new EasySearch.Minimongo(), 9 | fields: ['test'] 10 | }), 11 | fooTest: 'barTest' 12 | }); 13 | 14 | component.onCreated(); 15 | component.search('testString'); 16 | 17 | test.throws(function () { 18 | component.search({ tryto: 'hack' }); 19 | }); 20 | 21 | test.equal(component.name, 'customName'); 22 | test.equal(component.options, { fooTest: 'barTest' }); 23 | test.equal(component.defaultOptions, {}); 24 | test.instanceOf(_.first(component.indexes), EasySearch.Index); 25 | test.equal(_.first(component.dicts).get('searchDefinition'), 'testString'); 26 | }); 27 | 28 | Tinytest.add('EasySearch Components - Unit - Base without index', function (test) { 29 | var component = TestHelpers.createComponent(EasySearch.BaseComponent, { 30 | attributes: { type: 'number' }, 31 | name: 'customName', 32 | fooTest: 'barTest' 33 | }); 34 | 35 | test.throws(function () { 36 | component.onCreated(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /packages/easysearch_components/tests/unit/component-methods-tests.js: -------------------------------------------------------------------------------- 1 | Tinytest.add('EasySearch Components - Unit - Component Methods - addProps / removeProps', function (test) { 2 | let index = new EasySearch.Index({ 3 | collection: new Mongo.Collection('setPropsCollection'), 4 | engine: new EasySearch.Minimongo(), 5 | fields: ['test'] 6 | }); 7 | 8 | index.registerComponent(); 9 | 10 | let componentMethods = index.getComponentMethods(); 11 | 12 | componentMethods.addProps('customProp', 'customValue'); 13 | 14 | console.log(index.getComponentDict().get('searchOptions')); 15 | test.equal(index.getComponentDict().get('searchOptions'), { 16 | props: { 17 | customProp: 'customValue' 18 | }, 19 | skip: NaN, 20 | }); 21 | 22 | componentMethods.addProps({ 23 | what: 'test', 24 | aha: 'yeah' 25 | }); 26 | 27 | test.equal(index.getComponentDict().get('searchOptions'), { 28 | props: { 29 | what: 'test', 30 | aha: 'yeah', 31 | customProp: 'customValue' 32 | }, 33 | skip: NaN, 34 | }); 35 | 36 | componentMethods.addProps({ 37 | aha: 'yeah new', 38 | anArray: ['i', 'am', 'complex'] 39 | }); 40 | 41 | test.equal(index.getComponentDict().get('searchOptions').props.aha, 'yeah new'); 42 | test.equal(index.getComponentDict().get('searchOptions').props.anArray, ['i', 'am', 'complex']); 43 | 44 | componentMethods.removeProps('aha', 'customProp', 'anArray'); 45 | 46 | test.equal(index.getComponentDict().get('searchOptions'), { 47 | props: { 48 | what: 'test' 49 | }, 50 | skip: NaN, 51 | }); 52 | 53 | componentMethods.removeProps(); 54 | 55 | test.equal(index.getComponentDict().get('searchOptions'), { 56 | props: {}, 57 | skip: NaN, 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /packages/easysearch_components/tests/unit/core-tests.js: -------------------------------------------------------------------------------- 1 | var collection = new Mongo.Collection(null), 2 | index = new EasySearch.Index({ 3 | collection: collection, 4 | engine: new EasySearch.Minimongo(), 5 | fields: ['test'] 6 | }); 7 | 8 | collection.insert({ 'test': 'foo2' }); 9 | collection.insert({ 'test': 'fo2' }); 10 | collection.insert({ 'test': '1111 foo' }); 11 | 12 | index.registerComponent('testName'); 13 | 14 | Tinytest.add('EasySearch Components - Unit - Core - registerComponent', function (test) { 15 | index.getComponentDict('testName').set('testValue', 'testBar'); 16 | test.equal(index.getComponentDict('testName').get('testValue'), 'testBar'); 17 | }); 18 | 19 | Tinytest.add('EasySearch Components - Unit - Core - search', function (test) { 20 | index.getComponentDict('testName').set('searchOptions', { foo: 'bar' }); 21 | 22 | index.getComponentMethods('testName').search(''); 23 | 24 | test.equal(index.getComponentDict('testName').get('searchDefinition'), ''); 25 | test.equal(index.getComponentDict('testName').get('searchOptions'), {}); 26 | 27 | index.getComponentMethods('testName').search('foo'); 28 | 29 | test.equal(index.getComponentDict('testName').get('searchDefinition'), 'foo'); 30 | test.equal(index.getComponentDict('testName').get('searchOptions'), {}); 31 | }); 32 | 33 | Tinytest.addAsync('EasySearch Components - Unit - Core - getCursor', function (test, done) { 34 | Tracker.autorun(function (c) { 35 | var docs = index.getComponentMethods('testName').getCursor().mongoCursor.fetch(); 36 | 37 | if (2 == docs.length) { 38 | done(); 39 | c.stop(); 40 | } 41 | }); 42 | 43 | index.getComponentMethods('testName').search('foo'); 44 | }); 45 | 46 | Tinytest.add('EasySearch Components - Unit - Core - searchIsEmpty', function (test) { 47 | index.getComponentMethods('testName').search(''); 48 | test.equal(index.getComponentMethods('testName').searchIsEmpty(), true); 49 | index.getComponentMethods('testName').search('foo2'); 50 | test.equal(index.getComponentMethods('testName').searchIsEmpty(), false); 51 | }); 52 | 53 | Tinytest.add('EasySearch Components - Unit - Core - hasNoResults', function (test) { 54 | index.getComponentDict('testName').set('count', 0); 55 | test.equal(index.getComponentMethods('testName').hasNoResults(), true); 56 | index.getComponentDict('testName').set('count', 10); 57 | test.equal(index.getComponentMethods('testName').hasNoResults(), false); 58 | }); 59 | 60 | Tinytest.add('EasySearch Components - Unit - Core - isSearching', function (test) { 61 | index.getComponentDict('testName').set('searching', false); 62 | test.equal(index.getComponentMethods('testName').isSearching(), false); 63 | 64 | index.getComponentDict('testName').set('searching', true); 65 | test.equal(index.getComponentMethods('testName').isSearching(), true); 66 | }); 67 | -------------------------------------------------------------------------------- /packages/easysearch_components/tests/unit/each-tests.js: -------------------------------------------------------------------------------- 1 | Tinytest.add('EasySearch Components - Unit - Each', function (test) { 2 | var component = TestHelpers.createComponent(EasySearch.EachComponent, { 3 | index: new EasySearch.Index({ 4 | collection: new Mongo.Collection(null), 5 | engine: new EasySearch.Minimongo(), 6 | fields: ['test'] 7 | }) 8 | }); 9 | 10 | component.onCreated(); 11 | 12 | _.first(component.indexes).search = function (searchDefinition) { 13 | test.equal(searchDefinition, 'hans'); 14 | return new EasySearch.Cursor({ 15 | fetch: function () { return [{ foo: 'bar' }]; }, 16 | count: function () { return 10; } 17 | }, 200); 18 | }; 19 | 20 | component.dict.set('searchDefinition', 'hans'); 21 | component.dict.set('searchOptions', {}); 22 | 23 | var cursor = component.doc(); 24 | 25 | test.equal(component.dict.get('count'), 200); 26 | test.equal(component.dict.get('currentCount'), 10); 27 | test.equal(component.dict.get('searching'), false); 28 | test.equal(cursor.fetch(), [{ foo: 'bar' }]); 29 | 30 | test.throws(function () { 31 | TestHelpers.createComponent(EasySearch.EachComponent, { 32 | indexes: [new EasySearch.Index({ 33 | collection: new Mongo.Collection(null), 34 | engine: new EasySearch.Minimongo(), 35 | fields: ['test'] 36 | })] 37 | }); 38 | }()); 39 | }); 40 | -------------------------------------------------------------------------------- /packages/easysearch_components/tests/unit/field-input-tests.js: -------------------------------------------------------------------------------- 1 | Tinytest.addAsync('EasySearch Components - Unit - FieldInput', function (test, done) { 2 | let index = new EasySearch.Index({ 3 | collection: new Mongo.Collection(null), 4 | engine: new EasySearch.Minimongo(), 5 | fields: ['test'], 6 | allowedFields: ['test', 'name', 'score'] 7 | }); 8 | 9 | let component = TestHelpers.createComponent(EasySearch.FieldInputComponent, { 10 | field: 'name', 11 | indexes: [index] 12 | }); 13 | 14 | let componentTwo = TestHelpers.createComponent(EasySearch.FieldInputComponent, { 15 | field: 'score', 16 | indexes: [index] 17 | }); 18 | 19 | component.onCreated(); 20 | 21 | test.equal(EasySearch.InputComponent.defaultAttributes, { type: 'text', value: '' }); 22 | test.equal(component.inputAttributes(), { type: 'text', value: '' }); 23 | test.equal(component.options, { timeout: 50, field: 'name', charLimit: 0 }); 24 | test.equal(_.first(component.dicts).get('searchDefinition'), { name: '' }); 25 | test.isFalse(_.first(component.dicts).get('searching')); 26 | 27 | component.debouncedSearch('Peter'); 28 | component.debouncedSearch('Hans'); 29 | 30 | test.equal(_.first(component.dicts).get('searchDefinition'), { name: '' }); 31 | 32 | componentTwo.onCreated(); 33 | componentTwo.debouncedSearch('200'); 34 | 35 | Meteor.setTimeout(function () { 36 | test.equal(_.first(component.dicts).get('searchDefinition'), { name: 'Hans', score: '200' }); 37 | done(); 38 | }, 300); 39 | 40 | 41 | test.throws(function () { 42 | let component = TestHelpers.createComponent(EasySearch.FieldInputComponent, { 43 | indexes: [index] 44 | }); 45 | 46 | component.onCreated(); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /packages/easysearch_components/tests/unit/if-tests.js: -------------------------------------------------------------------------------- 1 | Tinytest.add('EasySearch Components - Unit - IfInputEmpty', function (test) { 2 | var component = TestHelpers.createComponent(EasySearch.IfInputEmptyComponent, { 3 | index: new EasySearch.Index({ 4 | collection: new Mongo.Collection(null), 5 | engine: new EasySearch.Minimongo(), 6 | fields: ['test'] 7 | }) 8 | }); 9 | 10 | component.onCreated(); 11 | 12 | _.first(component.dicts).set('searchDefinition', null); 13 | test.isTrue(component.inputEmpty()); 14 | 15 | _.first(component.dicts).set('searchDefinition', ''); 16 | test.isTrue(component.inputEmpty()); 17 | 18 | _.first(component.dicts).set('searchDefinition', ' '); 19 | test.isTrue(component.inputEmpty()); 20 | 21 | _.first(component.dicts).set('searchDefinition', 'test'); 22 | test.isFalse(component.inputEmpty()); 23 | }); 24 | 25 | Tinytest.add('EasySearch Components - Unit - IfNoResults', function (test) { 26 | var component = TestHelpers.createComponent(EasySearch.IfNoResultsComponent, { 27 | index: new EasySearch.Index({ 28 | collection: new Mongo.Collection(null), 29 | engine: new EasySearch.Minimongo(), 30 | fields: ['test'] 31 | }) 32 | }); 33 | 34 | component.onCreated(); 35 | 36 | _.first(component.dicts).set('count', null); 37 | test.isTrue(component.noResults()); 38 | 39 | _.first(component.dicts).set('count', 0); 40 | test.isTrue(component.noResults()); 41 | 42 | _.first(component.dicts).set('count', 1); 43 | test.isFalse(component.noResults()); 44 | 45 | _.first(component.dicts).set('count', 120); 46 | test.isFalse(component.noResults()); 47 | }); 48 | 49 | Tinytest.add('EasySearch Components - Unit - IfSearching', function (test) { 50 | var component = TestHelpers.createComponent(EasySearch.IfSearchingComponent, { 51 | index: new EasySearch.Index({ 52 | collection: new Mongo.Collection(null), 53 | engine: new EasySearch.Minimongo(), 54 | fields: ['test'] 55 | }) 56 | }); 57 | 58 | component.onCreated(); 59 | 60 | _.first(component.dicts).set('searching', true); 61 | test.isTrue(component.searching()); 62 | 63 | _.first(component.dicts).set('searching', false); 64 | test.isFalse(component.searching()); 65 | }); 66 | -------------------------------------------------------------------------------- /packages/easysearch_components/tests/unit/input-tests.js: -------------------------------------------------------------------------------- 1 | Tinytest.addAsync('EasySearch Components - Unit - Input', function (test, done) { 2 | var component = TestHelpers.createComponent(EasySearch.InputComponent, { 3 | attributes: { type: 'number' }, 4 | indexes: [new EasySearch.Index({ 5 | collection: new Mongo.Collection(null), 6 | engine: new EasySearch.Minimongo(), 7 | fields: ['test'] 8 | })] 9 | }); 10 | 11 | component.onCreated(); 12 | 13 | test.equal(EasySearch.InputComponent.defaultAttributes, { type: 'text', value: '' }); 14 | test.equal(component.inputAttributes(), { type: 'number', value: '' }); 15 | test.equal(component.options, { timeout: 50, charLimit: 0 }); 16 | test.equal(_.first(component.dicts).get('searchDefinition'), ''); 17 | test.isFalse(_.first(component.dicts).get('searching')); 18 | 19 | component.debouncedSearch('Peter'); 20 | component.debouncedSearch('Hans'); 21 | 22 | test.equal(_.first(component.dicts).get('searchDefinition'), ''); 23 | 24 | Meteor.setTimeout(function () { 25 | test.equal(_.first(component.dicts).get('searchDefinition'), 'Hans'); 26 | done(); 27 | }, 100); 28 | }); 29 | -------------------------------------------------------------------------------- /packages/easysearch_components/tests/unit/load-more-tests.js: -------------------------------------------------------------------------------- 1 | Tinytest.add('EasySearch Components - Unit - LoadMore', function (test) { 2 | var component = TestHelpers.createComponent(EasySearch.LoadMoreComponent, { 3 | count: 15, 4 | attributes: { 'class': 'whatsup' }, 5 | indexes: [new EasySearch.Index({ 6 | collection: new Mongo.Collection(null), 7 | engine: new EasySearch.Minimongo(), 8 | fields: ['test'] 9 | })] 10 | }); 11 | 12 | component.onCreated(); 13 | 14 | test.equal(component.options, { 15 | content: 'Load more', 16 | count: 15 17 | }); 18 | 19 | test.equal(component.defaultOptions, { 20 | content: 'Load more', 21 | count: 10 22 | }); 23 | 24 | test.equal(component.attributes(), { 'class': 'whatsup' }); 25 | test.equal(component.content(), 'Load more'); 26 | 27 | component.dict.set('currentCount', 20); 28 | component.dict.set('count', 30); 29 | 30 | test.equal(component.moreDocuments(), true); 31 | 32 | component.dict.set('currentCount', 20); 33 | component.dict.set('count', 19); 34 | 35 | test.equal(component.moreDocuments(), false); 36 | 37 | component.loadMore(); 38 | test.equal(component.dict.get('searchOptions').limit, 35); 39 | }); 40 | -------------------------------------------------------------------------------- /packages/easysearch_components/tests/unit/pagination-tests.js: -------------------------------------------------------------------------------- 1 | Tinytest.add('EasySearch Components - Unit - Pagination - _getPagesForPagination', function (test) { 2 | var pages = EasySearch._getPagesForPagination({ totalCount: 39, pageCount: 10, currentPage: 1 }); 3 | 4 | test.equal(pages[0], { page: 1, content: "1", current: true, disabled: true }); 5 | test.equal(pages[1], { page: 2, content: "2", current: false, disabled: false }); 6 | test.equal(pages[2], { page: 3, content: "3", current: false, disabled: false }); 7 | test.equal(pages[3], { page: 4, content: "4", current: false, disabled: false }); 8 | test.isUndefined(pages[4]); 9 | 10 | test.throws(function () { 11 | EasySearch._getPagesForPagination({ totalCount: 39, pageCount: 10, currentPage: 0 }); 12 | }); 13 | 14 | test.throws(function () { 15 | EasySearch._getPagesForPagination({ totalCount: 39, pageCount: 10, currentPage: 5 }); 16 | }); 17 | 18 | pages = EasySearch._getPagesForPagination({ totalCount: 39, pageCount: 10, currentPage: 4, prevAndNext: true }); 19 | 20 | test.equal(pages[0], { page: 3, content: "Prev", current: false, disabled: false }); 21 | test.equal(pages[1], { page: 1, content: "1", current: false, disabled: false }); 22 | test.equal(pages[2], { page: 2, content: "2", current: false, disabled: false }); 23 | test.equal(pages[3], { page: 3, content: "3", current: false, disabled: false }); 24 | test.equal(pages[4], { page: 4, content: "4", current: true, disabled: true }); 25 | test.equal(pages[5], { page: null, content: "Next", current: false, disabled: true }); 26 | test.isUndefined(pages[6]); 27 | 28 | pages = EasySearch._getPagesForPagination({ totalCount: 50, pageCount: 5, currentPage: 1, prevAndNext: true, maxPages: 3 }); 29 | 30 | test.equal(pages[0], { page: null, content: "Prev", current: false, disabled: true }); 31 | test.equal(pages[1], { page: 1, content: "1", current: true, disabled: true }); 32 | test.equal(pages[2], { page: 2, content: "2", current: false, disabled: false }); 33 | test.equal(pages[3], { page: 3, content: "3", current: false, disabled: false }); 34 | test.equal(pages[4], { page: 2, content: "Next", current: false, disabled: false }); 35 | 36 | pages = EasySearch._getPagesForPagination({ totalCount: 50, pageCount: 5, currentPage: 6, prevAndNext: true, maxPages: 3 }); 37 | 38 | test.equal(pages[0], { page: 5, content: "Prev", current: false, disabled: false }); 39 | test.equal(pages[1], { page: 5, content: "5", current: false, disabled: false }); 40 | test.equal(pages[2], { page: 6, content: "6", current: true, disabled: true }); 41 | test.equal(pages[3], { page: 7, content: "7", current: false, disabled: false }); 42 | test.equal(pages[4], { page: 7, content: "Next", current: false, disabled: false }); 43 | 44 | pages = EasySearch._getPagesForPagination({ totalCount: 50, pageCount: 5, currentPage: 9, prevAndNext: true, maxPages: 5 }); 45 | 46 | test.equal(pages[0], { page: 8, content: "Prev", current: false, disabled: false }); 47 | test.equal(pages[1], { page: 6, content: "6", current: false, disabled: false }); 48 | test.equal(pages[2], { page: 7, content: "7", current: false, disabled: false }); 49 | test.equal(pages[3], { page: 8, content: "8", current: false, disabled: false }); 50 | test.equal(pages[4], { page: 9, content: "9", current: true, disabled: true }); 51 | test.equal(pages[5], { page: 10, content: "10", current: false, disabled: false }); 52 | test.equal(pages[6], { page: 10, content: "Next", current: false, disabled: false }); 53 | 54 | pages = EasySearch._getPagesForPagination({ totalCount: 200, pageCount: 10, currentPage: 8, prevAndNext: true, maxPages: 10 }); 55 | 56 | test.equal(pages[0], { page: 7, content: "Prev", current: false, disabled: false }); 57 | test.equal(pages[1], { page: 3, content: "3", current: false, disabled: false }); 58 | test.equal(pages[2], { page: 4, content: "4", current: false, disabled: false }); 59 | test.equal(pages[3], { page: 5, content: "5", current: false, disabled: false }); 60 | test.equal(pages[4], { page: 6, content: "6", current: false, disabled: false }); 61 | test.equal(pages[5], { page: 7, content: "7", current: false, disabled: false }); 62 | test.equal(pages[6], { page: 8, content: "8", current: true, disabled: true }); 63 | test.equal(pages[7], { page: 9, content: "9", current: false, disabled: false }); 64 | test.equal(pages[8], { page: 10, content: "10", current: false, disabled: false }); 65 | test.equal(pages[9], { page: 11, content: "11", current: false, disabled: false }); 66 | test.equal(pages[10], { page: 12, content: "12", current: false, disabled: false }); 67 | test.equal(pages[11], { page: 9, content: "Next", current: false, disabled: false }); 68 | 69 | pages = EasySearch._getPagesForPagination({ totalCount: 200, pageCount: 10, currentPage: 11, prevAndNext: true, maxPages: 10 }); 70 | 71 | test.equal(pages[0], { page: 10, content: "Prev", current: false, disabled: false }); 72 | test.equal(pages[1], { page: 6, content: "6", current: false, disabled: false }); 73 | test.equal(pages[2], { page: 7, content: "7", current: false, disabled: false }); 74 | test.equal(pages[3], { page: 8, content: "8", current: false, disabled: false }); 75 | test.equal(pages[4], { page: 9, content: "9", current: false, disabled: false }); 76 | test.equal(pages[5], { page: 10, content: "10", current: false, disabled: false }); 77 | test.equal(pages[6], { page: 11, content: "11", current: true, disabled: true }); 78 | test.equal(pages[7], { page: 12, content: "12", current: false, disabled: false }); 79 | test.equal(pages[8], { page: 13, content: "13", current: false, disabled: false }); 80 | test.equal(pages[9], { page: 14, content: "14", current: false, disabled: false }); 81 | test.equal(pages[10], { page: 15, content: "15", current: false, disabled: false }); 82 | test.equal(pages[11], { page: 12, content: "Next", current: false, disabled: false }); 83 | }); 84 | 85 | Tinytest.add('EasySearch Components - Unit - Pagination', function (test) { 86 | var component = TestHelpers.createComponent(EasySearch.PaginationComponent, { 87 | index: new EasySearch.Index({ 88 | collection: new Mongo.Collection(null), 89 | engine: new EasySearch.Minimongo(), 90 | fields: ['test'] 91 | }) 92 | }); 93 | 94 | component.onCreated(); 95 | 96 | test.isNull(component.options.maxPages); 97 | test.isTrue(component.options.prevAndNext); 98 | test.equal(component.options.transformPages([1, 2, 3]), [1, 2, 3]); 99 | 100 | test.equal(component.pageClasses({ disabled: true }), 'disabled'); 101 | test.equal(component.pageClasses({ current: true }), 'current'); 102 | }); 103 | 104 | -------------------------------------------------------------------------------- /packages/easysearch_core/README.md: -------------------------------------------------------------------------------- 1 | Easy Search Core 2 | ===================== 3 | 4 | The core package allows you to search indexes with configured engines through the Javascript API. The `easy:search` package wraps this package together with `easysearch:components` for convenience. 5 | 6 | ```javascript 7 | import { Index, MongoDBEngine } from 'meteor/easysearch:core' 8 | 9 | // On Client and Server 10 | const Players = new Mongo.Collection('players') 11 | const PlayersIndex = new Index({ 12 | collection: Players, 13 | fields: ['name'], 14 | engine: new MongoDBEngine(), 15 | }) 16 | 17 | Tracker.autorun(() => { 18 | const cursor = PlayersIndex.search('Peter') 19 | 20 | console.log(cursor.fetch()) // logs the documents 21 | console.log(cursor.count()) // logs the count of all matched documents 22 | }) 23 | ``` 24 | 25 | ## How to install 26 | 27 | ```sh 28 | cd /path/to/project 29 | meteor add easysearch:core 30 | ``` 31 | 32 | NB: This package will use the `erasaur:meteor-lodash` package if it is already installed in your application, else it will fallback to the standard Meteor `underscore` package. 33 | -------------------------------------------------------------------------------- /packages/easysearch_core/lib/core/cursor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A Cursor represents a pointer to the search results. Since it's specific 3 | * to EasySearch it can also be used to check for valid return values. 4 | * 5 | * @type {Cursor} 6 | */ 7 | class Cursor { 8 | /** 9 | * Constructor 10 | * 11 | * @param {Mongo.Cursor} mongoCursor Referenced mongo cursor 12 | * @param {Number} count Count of all documents found 13 | * @param {Boolean} isReady Cursor is ready 14 | * @param {Object} publishHandle Publish handle to stop if on client 15 | * 16 | * @constructor 17 | * 18 | */ 19 | constructor(mongoCursor, count, isReady = true, publishHandle = null) { 20 | check(mongoCursor.fetch, Function); 21 | check(count, Number); 22 | check(isReady, Match.Optional(Boolean)); 23 | check(publishHandle, Match.OneOf(null, Object)); 24 | 25 | this._mongoCursor = mongoCursor; 26 | this._count = count; 27 | this._isReady = isReady; 28 | this._publishHandle = publishHandle; 29 | } 30 | 31 | /** 32 | * Fetch the search results. 33 | * 34 | * @returns {[Object]} 35 | */ 36 | fetch() { 37 | return this._mongoCursor.fetch(); 38 | } 39 | 40 | /** 41 | * Stop the subscription handle associated with the cursor. 42 | */ 43 | stop() { 44 | if (this._publishHandle) { 45 | return this._publishHandle.stop(); 46 | } 47 | } 48 | 49 | /** 50 | * Return count of all documents found 51 | * 52 | * @returns {Number} 53 | */ 54 | count() { 55 | return this._count; 56 | } 57 | 58 | /** 59 | * Return if the cursor is ready. 60 | * 61 | * @returns {Boolean} 62 | */ 63 | isReady() { 64 | return this._isReady; 65 | } 66 | 67 | /** 68 | * Return the raw mongo cursor. 69 | * 70 | * @returns {Mongo.Cursor} 71 | */ 72 | get mongoCursor() { 73 | return this._mongoCursor; 74 | } 75 | 76 | /** 77 | * Return a fake empty cursor, without data. 78 | * 79 | * @returns {Object} 80 | */ 81 | static get emptyCursor() { 82 | return { fetch: () => [], observe: () => { return { stop: () => null }; }, stop: () => {} }; 83 | } 84 | } 85 | 86 | export default Cursor; 87 | -------------------------------------------------------------------------------- /packages/easysearch_core/lib/core/data-source.js: -------------------------------------------------------------------------------- 1 | // TODO: turn collections into more general data source instances 2 | -------------------------------------------------------------------------------- /packages/easysearch_core/lib/core/engine.js: -------------------------------------------------------------------------------- 1 | /** 2 | * An Engine is the technology used for searching with EasySearch, with 3 | * customizable configuration to how it interacts with the data from the Index. 4 | * 5 | * @type {Engine} 6 | */ 7 | class Engine { 8 | /** 9 | * Constructor 10 | * 11 | * @param {Object} config configuration 12 | * 13 | * @constructor 14 | */ 15 | constructor(config = {}) { 16 | if (this.constructor === Engine) { 17 | throw new Error('Cannot initialize instance of Engine'); 18 | } 19 | 20 | if (!_.isFunction(this.search)) { 21 | throw new Error('Engine needs to implement search method'); 22 | } 23 | 24 | this.config = _.defaults({}, config, this.defaultConfiguration()); 25 | } 26 | 27 | /** 28 | * Return default configuration. 29 | * 30 | * @returns {Object} 31 | */ 32 | defaultConfiguration() { 33 | return {}; 34 | } 35 | 36 | /** 37 | * Call a configuration method with the engine scope. 38 | * 39 | * @param {String} methodName Method name 40 | * @param {Object} args Arguments for the method 41 | * 42 | * @returns {*} 43 | */ 44 | callConfigMethod(methodName, ...args) { 45 | check(methodName, String); 46 | 47 | let func = this.config[methodName]; 48 | 49 | if (func) { 50 | return func.apply(this, args); 51 | } 52 | } 53 | 54 | /** 55 | * Check the given search parameter for validity 56 | * 57 | * @param search 58 | */ 59 | checkSearchParam(search) { 60 | check(search, String); 61 | } 62 | 63 | /** 64 | *Code to run on index creation 65 | * 66 | * @param {Object} indexConfig Index configuraction 67 | */ 68 | onIndexCreate(indexConfig) { 69 | if (!indexConfig.allowedFields) { 70 | indexConfig.allowedFields = indexConfig.fields; 71 | } 72 | } 73 | } 74 | 75 | export default Engine; 76 | -------------------------------------------------------------------------------- /packages/easysearch_core/lib/core/index.js: -------------------------------------------------------------------------------- 1 | import { Mongo } from 'meteor/mongo' 2 | import Engine from './engine' 3 | 4 | /** 5 | * An Index represents the main entry point for searching with EasySearch. It relies on 6 | * the given engine to have the search functionality and defines the data that should be searchable. 7 | * 8 | * @type {Index} 9 | */ 10 | class Index { 11 | /** 12 | * Constructor 13 | * 14 | * @param {Object} config Configuration 15 | * 16 | * @constructor 17 | */ 18 | constructor(config) { 19 | check(config, Object); 20 | check(config.fields, [String]); 21 | if(!config.ignoreCollectionCheck) check(config.collection, Mongo.Collection); 22 | 23 | if (!(config.engine instanceof Engine)) { 24 | throw new Meteor.Error('invalid-engine', 'engine needs to be instanceof Engine'); 25 | } 26 | 27 | if (!config.name) 28 | config.name = (config.collection._name || '').toLowerCase(); 29 | 30 | this.config = _.extend(Index.defaultConfiguration, config); 31 | this.defaultSearchOptions = _.defaults( 32 | {}, 33 | this.config.defaultSearchOptions, 34 | { limit: 10, skip: 0, props: {} }, 35 | ); 36 | 37 | // Engine specific code on index creation 38 | config.engine.onIndexCreate(this.config); 39 | } 40 | 41 | /** 42 | * Default configuration for an index. 43 | * 44 | * @returns {Object} 45 | */ 46 | static get defaultConfiguration() { 47 | return { 48 | permission: () => true, 49 | defaultSearchOptions: {}, 50 | countUpdateIntervalMs: 2000, 51 | }; 52 | } 53 | 54 | /** 55 | * Search the index. 56 | * 57 | * @param {Object|String} searchDefinition Search definition 58 | * @param {Object} options Options 59 | * 60 | * @returns {Cursor} 61 | */ 62 | search(searchDefinition, options = {}) { 63 | this.config.engine.checkSearchParam(searchDefinition, this.config); 64 | 65 | check(options, { 66 | limit: Match.Optional(Number), 67 | skip: Match.Optional(Number), 68 | props: Match.Optional(Object), 69 | userId: Match.Optional(Match.OneOf(String, null)), 70 | }); 71 | 72 | options = { 73 | search: this._getSearchOptions(options), 74 | index: this.config, 75 | }; 76 | 77 | if (!this.config.permission(options.search)) { 78 | throw new Meteor.Error('not-allowed', "Not allowed to search this index!"); 79 | } 80 | 81 | return this.config.engine.search(searchDefinition, options); 82 | } 83 | 84 | /** 85 | * Returns the search options based on the given options. 86 | * 87 | * @param {Object} options Options to use 88 | * 89 | * @returns {Object} 90 | */ 91 | _getSearchOptions(options) { 92 | if (!Meteor.isServer) { 93 | delete options.userId; 94 | } 95 | 96 | if (typeof options.userId === "undefined" && Meteor.userId) { 97 | options.userId = Meteor.userId(); 98 | } 99 | 100 | return _.defaults(options, this.defaultSearchOptions); 101 | } 102 | } 103 | 104 | export default Index; 105 | -------------------------------------------------------------------------------- /packages/easysearch_core/lib/core/reactive-engine.js: -------------------------------------------------------------------------------- 1 | import SearchCollection from './search-collection' 2 | import Engine from './engine' 3 | 4 | /** 5 | * A ReactiveEngine handles the reactive logic, such as subscribing 6 | * and publishing documents into a self contained collection. 7 | * 8 | * @type {ReactiveEngine} 9 | */ 10 | class ReactiveEngine extends Engine { 11 | /** 12 | * Constructor. 13 | * 14 | * @param {Object} config Configuration 15 | * 16 | * @constructor 17 | */ 18 | constructor(config) { 19 | super(config); 20 | 21 | if (this === this.constructor) { 22 | throw new Error('Cannot initialize instance of ReactiveEngine'); 23 | } 24 | 25 | if (!_.isFunction(this.getSearchCursor)) { 26 | throw new Error('Reactive engine needs to implement getSearchCursor method'); 27 | } 28 | } 29 | 30 | /** 31 | * Return default configuration. 32 | * 33 | * @returns {Object} 34 | */ 35 | defaultConfiguration() { 36 | return _.defaults({}, { 37 | transform: (doc) => doc, 38 | beforePublish: (event, doc) => doc 39 | }, super.defaultConfiguration()); 40 | } 41 | 42 | /** 43 | * Code to run on index creation 44 | * 45 | * @param {Object} indexConfig Index configuration 46 | */ 47 | onIndexCreate(indexConfig) { 48 | super.onIndexCreate(indexConfig); 49 | indexConfig.searchCollection = new SearchCollection(indexConfig, this); 50 | indexConfig.mongoCollection = indexConfig.searchCollection._collection; 51 | } 52 | 53 | /** 54 | * Transform the search definition. 55 | * 56 | * @param {String|Object} searchDefinition Search definition 57 | * @param {Object} options Search and index options 58 | * 59 | * @returns {Object} 60 | */ 61 | transformSearchDefinition(searchDefinition, options) { 62 | if (_.isString(searchDefinition)) { 63 | let obj = {}; 64 | 65 | _.each(options.index.fields, function (field) { 66 | obj[field] = searchDefinition; 67 | }); 68 | 69 | searchDefinition = obj; 70 | } 71 | 72 | return searchDefinition; 73 | } 74 | 75 | /** 76 | * Check the given search parameter for validity 77 | * 78 | * @param search 79 | * @param indexOptions 80 | */ 81 | checkSearchParam(search, indexOptions) { 82 | check(search, Match.OneOf(String, Object)); 83 | 84 | if (_.isObject(search)) { 85 | _.each(search, function (val, field) { 86 | check(val, String); 87 | 88 | if (-1 === _.indexOf(indexOptions.allowedFields, field)) { 89 | throw new Meteor.Error(`Not allowed to search over field "${field}"`); 90 | } 91 | }); 92 | } 93 | } 94 | 95 | /** 96 | * Reactively search on the collection. 97 | * 98 | * @param {Object} searchDefinition Search definition 99 | * @param {Object} options Options 100 | * 101 | * @returns {Cursor} 102 | */ 103 | search(searchDefinition, options) { 104 | if (Meteor.isClient) { 105 | return options.index.searchCollection.find(searchDefinition, options.search); 106 | } else { 107 | return this.getSearchCursor( 108 | this.transformSearchDefinition(searchDefinition, options), 109 | options 110 | ); 111 | } 112 | } 113 | } 114 | 115 | export default ReactiveEngine; 116 | -------------------------------------------------------------------------------- /packages/easysearch_core/lib/core/search-collection.js: -------------------------------------------------------------------------------- 1 | import { Mongo } from 'meteor/mongo' 2 | import Cursor from './cursor' 3 | import ReactiveEngine from './reactive-engine' 4 | 5 | /** 6 | * A search collection represents a reactive collection on the client, 7 | * which is used by the ReactiveEngine for searching. 8 | * 9 | * @type {SearchCollection} 10 | */ 11 | class SearchCollection { 12 | /** 13 | * Constructor 14 | * 15 | * @param {Object} indexConfiguration Index configuration 16 | * @param {ReactiveEngine} engine Reactive Engine 17 | * 18 | * @constructor 19 | */ 20 | constructor(indexConfiguration, engine) { 21 | check(indexConfiguration, Object); 22 | check(indexConfiguration.name, Match.OneOf(String, null)); 23 | 24 | if (!(engine instanceof ReactiveEngine)) { 25 | throw new Meteor.Error('invalid-engine', 'engine needs to be instanceof ReactiveEngine'); 26 | } 27 | 28 | this._indexConfiguration = indexConfiguration; 29 | this._name = `${indexConfiguration.name}/easySearch`; 30 | this._engine = engine; 31 | 32 | if (Meteor.isClient) { 33 | this._collection = new Mongo.Collection(this._name); 34 | } else if (Meteor.isServer) { 35 | this._setUpPublication(); 36 | } 37 | } 38 | 39 | /** 40 | * Get name 41 | * 42 | * @returns {String} 43 | */ 44 | get name() { 45 | return this._name; 46 | } 47 | 48 | /** 49 | * Get engine 50 | * 51 | * @returns {ReactiveEngine} 52 | */ 53 | get engine() { 54 | return this._engine; 55 | } 56 | 57 | /** 58 | * Find documents on the client. 59 | * 60 | * @param {Object} searchDefinition Search definition 61 | * @param {Object} options Options 62 | * 63 | * @returns {Cursor} 64 | */ 65 | find(searchDefinition, options) { 66 | if (!Meteor.isClient) { 67 | throw new Error('find can only be used on client'); 68 | } 69 | 70 | let publishHandle = Meteor.subscribe(this.name, searchDefinition, options); 71 | 72 | let count = this._getCount(searchDefinition); 73 | let mongoCursor = this._getMongoCursor(searchDefinition, options); 74 | 75 | if (!_.isNumber(count)) { 76 | return new Cursor(mongoCursor, 0, false); 77 | } 78 | 79 | return new Cursor(mongoCursor, count, true, publishHandle); 80 | } 81 | 82 | /** 83 | * Get the count of the cursor. 84 | * 85 | * @params {Object} searchDefinition Search definition 86 | * 87 | * @returns {Cursor.count} 88 | * 89 | * @private 90 | */ 91 | _getCount(searchDefinition) { 92 | let countDoc = this._collection.findOne('searchCount' + JSON.stringify(searchDefinition)); 93 | 94 | if (countDoc) { 95 | return countDoc.count; 96 | } 97 | } 98 | 99 | /** 100 | * Get the mongo cursor on the client. 101 | * 102 | * @param {Object} searchDefinition Search definition 103 | * @param {Object} options Search options 104 | * 105 | * @returns {Cursor} 106 | * @private 107 | */ 108 | _getMongoCursor(searchDefinition, options) { 109 | const clientSort = this.engine.callConfigMethod('clientSort', searchDefinition, options); 110 | 111 | return this._collection.find( 112 | { __searchDefinition: JSON.stringify(searchDefinition), __searchOptions: JSON.stringify(options.props) }, 113 | { 114 | transform: (doc) => { 115 | delete doc.__searchDefinition; 116 | delete doc.__searchOptions; 117 | delete doc.__sortPosition; 118 | 119 | doc = this.engine.config.transform(doc); 120 | 121 | return doc; 122 | }, 123 | sort: (clientSort ? clientSort : ['__sortPosition']) 124 | } 125 | ); 126 | } 127 | 128 | /** 129 | * Return a unique document id for publication. 130 | * 131 | * @param {Document} doc 132 | * 133 | * @returns string 134 | */ 135 | generateId(doc) { 136 | return doc._id + doc.__searchDefinition + doc.__searchOptions; 137 | } 138 | 139 | /** 140 | * Add custom fields to the given document 141 | * 142 | * @param {Document} doc 143 | * @param {Object} data 144 | * @returns {*} 145 | */ 146 | addCustomFields(doc, data) { 147 | _.forEach(data, function (val, key) { 148 | doc['__' + key] = val; 149 | }); 150 | 151 | return doc; 152 | } 153 | 154 | /** 155 | * Set up publication. 156 | * 157 | * @private 158 | */ 159 | _setUpPublication() { 160 | var collectionScope = this, 161 | collectionName = this.name; 162 | 163 | Meteor.publish(collectionName, function (searchDefinition, options) { 164 | check(searchDefinition, Match.OneOf(String, Object)); 165 | check(options, Object); 166 | 167 | let definitionString = JSON.stringify(searchDefinition), 168 | optionsString = JSON.stringify(options.props); 169 | 170 | options.userId = this.userId; 171 | options.publicationScope = this; 172 | 173 | if (!collectionScope._indexConfiguration.permission(options)) { 174 | throw new Meteor.Error('not-allowed', "You're not allowed to search this index!"); 175 | } 176 | 177 | collectionScope.engine.checkSearchParam(searchDefinition, collectionScope._indexConfiguration); 178 | 179 | let cursor = collectionScope.engine.search(searchDefinition, { 180 | search: options, 181 | index: collectionScope._indexConfiguration 182 | }); 183 | 184 | const count = cursor.count(); 185 | 186 | this.added(collectionName, 'searchCount' + definitionString, { count }); 187 | 188 | let intervalID; 189 | 190 | if (collectionScope._indexConfiguration.countUpdateIntervalMs) { 191 | intervalID = Meteor.setInterval( 192 | () => this.changed( 193 | collectionName, 194 | 'searchCount' + definitionString, 195 | { count: cursor.mongoCursor.count && cursor.mongoCursor.count() || 0 } 196 | ), 197 | collectionScope._indexConfiguration.countUpdateIntervalMs 198 | ); 199 | } 200 | 201 | let resultsHandle; 202 | this.onStop(function () { 203 | intervalID && Meteor.clearInterval(intervalID); 204 | resultsHandle && resultsHandle.stop(); 205 | }); 206 | 207 | let observedDocs = []; 208 | 209 | const updateDocWithCustomFields = (doc, sortPosition) => collectionScope 210 | .addCustomFields(doc, { 211 | originalId: doc._id, 212 | sortPosition, 213 | searchDefinition: definitionString, 214 | searchOptions: optionsString, 215 | }); 216 | 217 | 218 | resultsHandle = cursor.mongoCursor.observe({ 219 | addedAt: (doc, atIndex, before) => { 220 | doc = collectionScope.engine.config.beforePublish('addedAt', doc, atIndex, before); 221 | doc = updateDocWithCustomFields(doc, atIndex); 222 | 223 | this.added(collectionName, collectionScope.generateId(doc), doc); 224 | 225 | /* 226 | * Reorder all observed docs to keep valid sorting. Here we adjust the 227 | * sortPosition number field to give space for the newly added doc 228 | */ 229 | if (observedDocs.map(d => d.__sortPosition).includes(atIndex)) { 230 | observedDocs = observedDocs.map((doc, docIndex) => { 231 | if (doc.__sortPosition >= atIndex) { 232 | doc = collectionScope.addCustomFields(doc, { 233 | sortPosition: doc.__sortPosition + 1, 234 | }); 235 | 236 | // do not throw changed event on last doc as it will be removed from cursor 237 | if (docIndex < observedDocs.length) { 238 | this.changed( 239 | collectionName, 240 | collectionScope.generateId(doc), 241 | doc 242 | ); 243 | } 244 | } 245 | 246 | return doc; 247 | }); 248 | } 249 | 250 | observedDocs = [...observedDocs , doc]; 251 | }, 252 | changedAt: (doc, oldDoc, atIndex) => { 253 | doc = collectionScope.engine.config.beforePublish('changedAt', doc, oldDoc, atIndex); 254 | doc = collectionScope.addCustomFields(doc, { 255 | searchDefinition: definitionString, 256 | searchOptions: optionsString, 257 | sortPosition: atIndex, 258 | originalId: doc._id 259 | }); 260 | 261 | this.changed(collectionName, collectionScope.generateId(doc), doc); 262 | }, 263 | movedTo: (doc, fromIndex, toIndex, before) => { 264 | doc = collectionScope.engine.config.beforePublish('movedTo', doc, fromIndex, toIndex, before); 265 | doc = updateDocWithCustomFields(doc, toIndex); 266 | 267 | let beforeDoc = collectionScope._indexConfiguration.collection.findOne(before); 268 | 269 | if (beforeDoc) { 270 | beforeDoc = collectionScope.addCustomFields(beforeDoc, { 271 | searchDefinition: definitionString, 272 | searchOptions: optionsString, 273 | sortPosition: fromIndex 274 | }); 275 | this.changed(collectionName, collectionScope.generateId(beforeDoc), beforeDoc); 276 | } 277 | 278 | this.changed(collectionName, collectionScope.generateId(doc), doc); 279 | }, 280 | removedAt: (doc, atIndex) => { 281 | doc = collectionScope.engine.config.beforePublish('removedAt', doc, atIndex); 282 | doc = collectionScope.addCustomFields( 283 | doc, 284 | { 285 | searchDefinition: definitionString, 286 | searchOptions: optionsString 287 | }); 288 | this.removed(collectionName, collectionScope.generateId(doc)); 289 | 290 | /* 291 | * Adjust sort position for all docs after the removed doc and 292 | * remove the doc from the observed docs array 293 | */ 294 | observedDocs = observedDocs.map(doc => { 295 | if (doc.__sortPosition > atIndex) { 296 | doc.__sortPosition -= 1; 297 | } 298 | 299 | return doc; 300 | }).filter( 301 | d => collectionScope.generateId(d) !== collectionScope.generateId(doc) 302 | ); 303 | } 304 | }); 305 | 306 | this.onStop(function () { 307 | resultsHandle.stop(); 308 | }); 309 | 310 | this.ready(); 311 | }); 312 | } 313 | } 314 | 315 | export default SearchCollection; 316 | -------------------------------------------------------------------------------- /packages/easysearch_core/lib/engines/minimongo.js: -------------------------------------------------------------------------------- 1 | import Engine from '../core/engine'; 2 | import ReactiveEngine from '../core/reactive-engine'; 3 | import MongoDBEngine from './mongo-db'; 4 | 5 | /** 6 | * The MinimongEngine lets you search the index on the client-side. 7 | * 8 | * @type {MinimongoEngine} 9 | */ 10 | class MinimongoEngine extends Engine { 11 | /** 12 | * Return default configuration. 13 | * 14 | * @returns {Object} 15 | */ 16 | defaultConfiguration() { 17 | return _.defaults({}, MongoDBEngine.defaultMongoConfiguration(this), super.defaultConfiguration()); 18 | } 19 | 20 | /** 21 | * Search the index. 22 | * 23 | * @param {Object} searchDefinition Search definition 24 | * @param {Object} options Object of options 25 | * 26 | * @returns {cursor} 27 | */ 28 | search(searchDefinition, options) { 29 | if (!Meteor.isClient) { 30 | throw new Meteor.Error('only-client', 'Minimongo can only be used on the client'); 31 | } 32 | 33 | searchDefinition = this.transformSearchDefinition(searchDefinition, options); 34 | 35 | // check() calls are in getSearchCursor method 36 | return MongoDBEngine.prototype.getSearchCursor.apply(this, [searchDefinition, options]); 37 | } 38 | } 39 | 40 | MinimongoEngine.prototype.checkSearchParam = ReactiveEngine.prototype.checkSearchParam; 41 | MinimongoEngine.prototype.transformSearchDefinition = ReactiveEngine.prototype.transformSearchDefinition; 42 | 43 | MinimongoEngine.prototype.getFindOptions = function (...args) { 44 | let findOptions = MongoDBEngine.prototype.getFindOptions.apply(this, args); 45 | 46 | findOptions.transform = this.config.transform; 47 | 48 | return findOptions; 49 | }; 50 | 51 | export default MinimongoEngine; 52 | -------------------------------------------------------------------------------- /packages/easysearch_core/lib/engines/mongo-db.js: -------------------------------------------------------------------------------- 1 | import Cursor from '../core/cursor'; 2 | import ReactiveEngine from '../core/reactive-engine'; 3 | 4 | /** 5 | * The MongoDBEngine lets you search the index on the server side with MongoDB. Subscriptions and publications 6 | * are handled within the Engine. 7 | * 8 | * @type {MongoDBEngine} 9 | */ 10 | class MongoDBEngine extends ReactiveEngine { 11 | /** 12 | * Return default configuration. 13 | * 14 | * @returns {Object} 15 | */ 16 | defaultConfiguration() { 17 | return _.defaults({}, MongoDBEngine.defaultMongoConfiguration(this), super.defaultConfiguration()); 18 | } 19 | 20 | /** 21 | * Default mongo configuration, used in constructor and MinimongoEngine to get the configuration. 22 | * 23 | * @param {Object} engineScope Scope of the engine 24 | * 25 | * @returns {Object} 26 | */ 27 | static defaultMongoConfiguration(engineScope) { 28 | return { 29 | aggregation: '$or', 30 | selector(searchObject, options, aggregation) { 31 | const selector = {}; 32 | 33 | selector[aggregation] = []; 34 | 35 | _.each(searchObject, (searchString, field) => { 36 | const fieldSelector = engineScope.callConfigMethod( 37 | 'selectorPerField', field, searchString, options 38 | ); 39 | 40 | if (fieldSelector) { 41 | selector[aggregation].push(fieldSelector); 42 | } 43 | }); 44 | 45 | return selector; 46 | }, 47 | selectorPerField(field, searchString) { 48 | const selector = {}; 49 | 50 | searchString = searchString.replace(/(\W{1})/g, '\\$1'); 51 | selector[field] = { '$regex' : `.*${searchString}.*`, '$options' : 'i'}; 52 | 53 | return selector; 54 | }, 55 | sort(searchObject, options) { 56 | return options.index.fields; 57 | } 58 | }; 59 | } 60 | 61 | /** 62 | * Return the find options for the mongo find query. 63 | * 64 | * @param {String} searchDefinition Search definition 65 | * @param {Object} options Search and index options 66 | */ 67 | getFindOptions(searchDefinition, options) { 68 | return { 69 | skip: options.search.skip, 70 | limit: options.search.limit, 71 | disableOplog: this.config.disableOplog, 72 | pollingIntervalMs: this.config.pollingIntervalMs, 73 | pollingThrottleMs: this.config.pollingThrottleMs, 74 | sort: this.callConfigMethod('sort', searchDefinition, options), 75 | fields: this.callConfigMethod('fields', searchDefinition, options) 76 | }; 77 | } 78 | 79 | /** 80 | * Return the reactive search cursor. 81 | * 82 | * @param {String} searchDefinition Search definition 83 | * @param {Object} options Search and index options 84 | */ 85 | getSearchCursor(searchDefinition, options) { 86 | const selector = this.callConfigMethod( 87 | 'selector', 88 | searchDefinition, 89 | options, 90 | this.config.aggregation 91 | ), 92 | findOptions = this.getFindOptions(searchDefinition, options), 93 | collection = options.index.collection; 94 | 95 | check(options, Object); 96 | check(selector, Object); 97 | check(findOptions, Object); 98 | 99 | return new Cursor( 100 | collection.find(selector, findOptions), 101 | collection.find(selector).count() 102 | ); 103 | } 104 | } 105 | 106 | export default MongoDBEngine; 107 | -------------------------------------------------------------------------------- /packages/easysearch_core/lib/engines/mongo-text-index.js: -------------------------------------------------------------------------------- 1 | import ReactiveEngine from '../core/reactive-engine'; 2 | import MongoDBEngine from './mongo-db'; 3 | 4 | /** 5 | * The MongoTextIndexEngine lets you search the index with Mongo text indexes. 6 | * 7 | * @type {MongoTextIndexEngine} 8 | */ 9 | class MongoTextIndexEngine extends ReactiveEngine { 10 | /** 11 | * Return default configuration. 12 | * 13 | * @returns {Object} 14 | */ 15 | defaultConfiguration() { 16 | let mongoConfiguration = MongoDBEngine.defaultMongoConfiguration(this); 17 | 18 | mongoConfiguration.selector = function (searchString) { 19 | if (searchString.trim()) { 20 | return { $text: { $search: searchString } }; 21 | } 22 | 23 | return {}; 24 | }; 25 | 26 | return _.defaults({}, mongoConfiguration, super.defaultConfiguration()); 27 | } 28 | 29 | /** 30 | * Setup the index on creation. 31 | * 32 | * @param {Object} indexConfig Index configuration 33 | */ 34 | onIndexCreate(indexConfig) { 35 | super.onIndexCreate(indexConfig); 36 | 37 | if (Meteor.isServer) { 38 | let textIndexesConfig = {}; 39 | 40 | _.each(indexConfig.fields, function (field) { 41 | textIndexesConfig[field] = 'text'; 42 | }); 43 | 44 | if (indexConfig.weights) { 45 | textIndexesConfig.weights = options.weights(); 46 | } 47 | 48 | indexConfig.collection._ensureIndex(textIndexesConfig); 49 | } 50 | } 51 | 52 | /** 53 | * Transform the search definition. 54 | * 55 | * @param {String|Object} searchDefinition Search definition 56 | * @param {Object} options Search and index options 57 | * 58 | * @returns {Object} 59 | */ 60 | transformSearchDefinition(searchDefinition, options) { 61 | return searchDefinition; 62 | } 63 | 64 | /** 65 | * Check the given search parameter for validity 66 | * 67 | * @param search 68 | */ 69 | checkSearchParam(search) { 70 | check(search, String); 71 | } 72 | } 73 | 74 | // Explicitely inherit getSearchCursor method functionality 75 | MongoTextIndexEngine.prototype.getSearchCursor = MongoDBEngine.prototype.getSearchCursor; 76 | MongoTextIndexEngine.prototype.getFindOptions = MongoDBEngine.prototype.getFindOptions; 77 | 78 | export default MongoTextIndexEngine; 79 | -------------------------------------------------------------------------------- /packages/easysearch_core/lib/globals.js: -------------------------------------------------------------------------------- 1 | import { 2 | Index, 3 | Engine, 4 | ReactiveEngine, 5 | Cursor, 6 | MongoDBEngine, 7 | MinimongoEngine, 8 | MongoTextIndexEngine 9 | } from './main'; 10 | 11 | EasySearch = { 12 | // Core 13 | Index, 14 | Engine, 15 | ReactiveEngine, 16 | Cursor, 17 | // Engines 18 | MongoDB: MongoDBEngine, 19 | Minimongo: MinimongoEngine, 20 | MongoTextIndex: MongoTextIndexEngine 21 | }; 22 | -------------------------------------------------------------------------------- /packages/easysearch_core/lib/main.js: -------------------------------------------------------------------------------- 1 | import Index from './core/index'; 2 | import Engine from './core/engine'; 3 | import ReactiveEngine from './core/reactive-engine'; 4 | import Cursor from './core/cursor'; 5 | import MongoDBEngine from './engines/mongo-db'; 6 | import MinimongoEngine from './engines/minimongo'; 7 | import MongoTextIndexEngine from './engines/mongo-text-index'; 8 | 9 | export { 10 | Index, 11 | Engine, 12 | ReactiveEngine, 13 | Cursor, 14 | MongoDBEngine, 15 | MinimongoEngine, 16 | MongoTextIndexEngine 17 | }; 18 | -------------------------------------------------------------------------------- /packages/easysearch_core/package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'easysearch:core', 3 | summary: "Javascript Core for EasySearch", 4 | version: "2.2.3", 5 | git: "https://github.com/matteodem/meteor-easy-search.git", 6 | documentation: 'README.md' 7 | }); 8 | 9 | Package.onUse(function(api) { 10 | api.versionsFrom('1.4.2'); 11 | 12 | // Dependencies 13 | api.use(['check', 'ecmascript', 'mongo', 'underscore']); 14 | api.use(['erasaur:meteor-lodash@4.0.0'], { weak: true }); 15 | 16 | // Core packages 17 | api.addFiles([ 18 | 'lib/core/index.js', 19 | 'lib/core/engine.js', 20 | 'lib/core/reactive-engine.js', 21 | 'lib/core/cursor.js', 22 | 'lib/core/search-collection.js' 23 | ]); 24 | 25 | // Engines 26 | api.addFiles([ 27 | 'lib/engines/mongo-db.js', 28 | 'lib/engines/minimongo.js', 29 | 'lib/engines/mongo-text-index.js' 30 | ]); 31 | 32 | // Global 33 | api.addFiles(['lib/globals.js']); 34 | api.export('EasySearch'); 35 | 36 | api.mainModule('lib/main.js'); 37 | }); 38 | 39 | Package.onTest(function(api) { 40 | api.use(['tinytest', 'mongo', 'tracker', 'ecmascript', 'audit-argument-checks', 'underscore']); 41 | api.use('easysearch:core'); 42 | 43 | // Test Helpers 44 | api.addFiles(['tests/helpers.js']); 45 | 46 | // Unit tests 47 | api.addFiles([ 48 | 'tests/unit/core/cursor-tests.js', 49 | 'tests/unit/core/engine-tests.js', 50 | 'tests/unit/core/reactive-engine-tests.js', 51 | 'tests/unit/core/index-tests.js' 52 | ]); 53 | 54 | // Functional tests 55 | api.addFiles([ 56 | 'tests/functional/mongo-db-tests.js', 57 | 'tests/functional/mongo-text-index-tests.js', 58 | 'tests/functional/minimongo-tests.js' 59 | ]); 60 | }); 61 | -------------------------------------------------------------------------------- /packages/easysearch_core/tests/functional/minimongo-tests.js: -------------------------------------------------------------------------------- 1 | var collection = new Mongo.Collection('minimongoCollection'); 2 | 3 | if (Meteor.isServer) { 4 | collection.remove({ }); 5 | collection.insert({ _id: 'testId', name: 'testName' }); 6 | 7 | for (var i = 0; i < 100; i += 1) { 8 | collection.insert({ _id: 'testId' + i, sortField: i, name: 'name sup what' }); 9 | } 10 | } 11 | 12 | var index = new EasySearch.Index({ 13 | engine: new EasySearch.Minimongo({ 14 | sort() { 15 | return ['sortField']; 16 | }, 17 | transform(doc) { 18 | doc.what = 'what'; 19 | 20 | return doc; 21 | } 22 | }), 23 | collection: collection, 24 | fields: ['name'] 25 | }); 26 | 27 | if (Meteor.isServer) { 28 | Meteor.publish('testCollection', function () { 29 | return collection.find(); 30 | }); 31 | 32 | Tinytest.add('EasySearch - Functional - Minimongo - Error on Server', function (test) { 33 | try { 34 | index.search('test'); 35 | } catch (e) { 36 | test.instanceOf(e, Meteor.Error); 37 | return; 38 | } 39 | 40 | test.fail(); 41 | }); 42 | } else if (Meteor.isClient) { 43 | Meteor.subscribe('testCollection'); 44 | 45 | Tinytest.addAsync('EasySearch - Functional - Minimongo - prefix search', function (test, done) { 46 | Tracker.autorun(function (c) { 47 | var docs = index.search('test').fetch(); 48 | 49 | if (docs.length === 1) { 50 | test.equal(docs, [{ _id: 'testId', name: 'testName', what: 'what' }]); 51 | test.equal(index.search('test').count(), 1); 52 | done(); 53 | c.stop(); 54 | } 55 | }); 56 | }); 57 | 58 | Tinytest.addAsync('EasySearch - Functional - Minimongo - suffixed search', function (test, done) { 59 | function getExpectedDocs() { 60 | var docs = []; 61 | 62 | for (var i = 0; i < 10; i += 1) { 63 | docs.push({ _id: 'testId' + i, sortField: i, name: 'name sup what', what: 'what' }); 64 | } 65 | 66 | return docs; 67 | } 68 | 69 | Tracker.autorun(function (c) { 70 | var docs = index.search({ name: 'what' }).fetch(); 71 | 72 | if (docs.length === 10) { 73 | test.equal(docs, getExpectedDocs()); 74 | test.equal(index.search('what').count(), 100); 75 | done(); 76 | c.stop(); 77 | } 78 | }); 79 | }); 80 | 81 | Tinytest.add('EasySearch - Functional - Minimongo - failing searches', function (test) { 82 | test.throws(function () { 83 | index.search(100); 84 | }); 85 | }); 86 | } 87 | -------------------------------------------------------------------------------- /packages/easysearch_core/tests/functional/mongo-db-tests.js: -------------------------------------------------------------------------------- 1 | var collection = new Mongo.Collection(null); 2 | 3 | if (Meteor.isServer) { 4 | collection.insert({ _id: 'testId', name: 'testName' }); 5 | collection.insert({ _id: 'beforePublishDoc', sortField: -1, name: 'publishdoc' }); 6 | 7 | for (var i = 0; i < 100; i += 1) { 8 | collection.insert({ _id: 'testId' + i, sortField: i, name: 'name sup what', otherName: (i == 1 ? 'what' : '') }); 9 | } 10 | } 11 | 12 | var index = new EasySearch.Index({ 13 | engine: new EasySearch.MongoDB({ 14 | sort: function () { 15 | return ['sortField']; 16 | }, 17 | selectorPerField: function (field, searchString, options) { 18 | if ('testName' === searchString) { 19 | var selector = {}; 20 | 21 | selector[field] = 'name sup what'; 22 | 23 | if (options.search.props.returnNone) { 24 | selector[field] = ''; 25 | } 26 | 27 | return selector; 28 | } 29 | 30 | return this.defaultConfiguration().selectorPerField(field, searchString); 31 | }, 32 | beforePublish: function (event, doc) { 33 | if ('addedAt' == event && 'beforePublishDoc' == doc._id) { 34 | doc.newAwesomeProperty = doc.name + ' awesome property'; 35 | } 36 | 37 | return doc; 38 | } 39 | }), 40 | collection: collection, 41 | fields: ['name'], 42 | allowedFields: ['name', 'otherName'] 43 | }); 44 | 45 | var customDocObjectIndex = new EasySearch.Index({ 46 | name: 'secondIndex', 47 | engine: new EasySearch.MongoDB({ 48 | transform(doc) { 49 | return new (function (data) { 50 | this.getData = function () { 51 | return data; 52 | }; 53 | })(doc); 54 | } 55 | }), 56 | collection: collection, 57 | fields: ['name'] 58 | }); 59 | 60 | function getExpectedDocs(count) { 61 | var docs = []; 62 | 63 | for (var i = 0; i < count; i += 1) { 64 | docs.push({ _id: 'testId' + i, sortField: i, name: 'name sup what', otherName: (i == 1 ? 'what' : '') }); 65 | } 66 | 67 | return docs; 68 | } 69 | 70 | Tinytest.addAsync('EasySearch - Functional - MongoDB - prefix search', function (test, done) { 71 | Tracker.autorun(function (c) { 72 | var docs = index.search('test').fetch(); 73 | 74 | if (docs.length === 1) { 75 | test.equal(docs[0].name, 'testName'); 76 | test.equal(index.search('test').count(), 1); 77 | done(); 78 | c.stop(); 79 | } 80 | }); 81 | }); 82 | 83 | Tinytest.addAsync('EasySearch - Functional - MongoDB - suffixed search', function (test, done) { 84 | Tracker.autorun(function (c) { 85 | var cursor = index.search('what'), 86 | docs = cursor.fetch(); 87 | 88 | if (docs.length === 10) { 89 | test.equal(cursor.count(), 100); 90 | test.equal(docs[0].name, 'name sup what'); 91 | test.equal(docs[0].sortField, 0); 92 | test.equal(docs[5].name, 'name sup what'); 93 | test.equal(docs[6].sortField, 6); 94 | test.equal(index.search('what').count(), 100); 95 | done(); 96 | c.stop(); 97 | } 98 | }); 99 | }); 100 | 101 | Tinytest.addAsync('EasySearch - Functional - MongoDB - custom selector', function (test, done) { 102 | Tracker.autorun(function (c) { 103 | var cursor = index.search('testName', { limit: 20 }), 104 | docs = cursor.fetch(); 105 | 106 | if (docs.length === 20) { 107 | test.equal(cursor.count(), 100); 108 | test.equal(docs[11].sortField, 11); 109 | test.equal(docs[17].sortField, 17); 110 | 111 | test.equal(index.search('testName').count(), 100); 112 | done(); 113 | c.stop(); 114 | } 115 | }); 116 | }); 117 | 118 | Tinytest.addAsync('EasySearch - Functional - MongoDB - custom property', function (test, done) { 119 | Tracker.autorun(function (c) { 120 | var cursor = index.search('should not match anything', { limit: 10, props: { returnNone: true } }), 121 | docs = cursor.fetch(); 122 | 123 | if (docs.length === 0) { 124 | test.equal(cursor.count(), 0); 125 | done(); 126 | c.stop(); 127 | } 128 | }); 129 | }); 130 | 131 | Tinytest.addAsync('EasySearch - Functional - MongoDB - per field search', function (test, done) { 132 | Tracker.autorun(function (c) { 133 | var cursor = index.search({ otherName: 'what' }, { limit: 20 }), 134 | docs = cursor.fetch(); 135 | 136 | if (cursor.count() === 1) { 137 | test.equal(docs[0].sortField, 1); 138 | test.equal(docs[0].name, 'name sup what'); 139 | test.equal(docs[0].otherName, 'what'); 140 | done(); 141 | c.stop(); 142 | } 143 | }); 144 | }); 145 | 146 | Tinytest.addAsync('EasySearch - Functional - MongoDB - beforePublish', function (test, done) { 147 | Tracker.autorun(function (c) { 148 | var docs = index.search('publish').fetch(); 149 | 150 | if (docs.length === 1) { 151 | var expectedDocs = [{ _id: 'beforePublishDoc', sortField: -1, name: 'publishdoc' }]; 152 | 153 | Meteor.setTimeout(function () { 154 | test.equal(docs[0].sortField, -1); 155 | test.equal(docs[0].name, 'publishdoc'); 156 | 157 | if (Meteor.isClient) { 158 | test.equal(docs[0].newAwesomeProperty, 'publishdoc awesome property'); 159 | } 160 | 161 | test.equal(index.search('publish').count(), 1); 162 | done(); 163 | c.stop(); 164 | }, 100); 165 | } 166 | }); 167 | }); 168 | 169 | Tinytest.add('EasySearch - Functional - MongoDB - failing searches', function (test) { 170 | test.throws(function () { 171 | index.search(100); 172 | }); 173 | 174 | test.throws(function () { 175 | index.search({ score: 10 }); 176 | }); 177 | }); 178 | 179 | if (Meteor.isClient) { 180 | Tinytest.addAsync('EasySearch - Functional - MongoDB - Transform custom object instances', function (test, done) { 181 | Tracker.autorun(function (c) { 182 | var docs = customDocObjectIndex.search('publish').fetch(); 183 | 184 | if (docs.length === 1) { 185 | var expectedDoc = { _id: 'beforePublishDoc', sortField: -1, name: 'publishdoc' }, 186 | firstActualDoc = docs[0]; 187 | 188 | test.isTrue("function" == typeof firstActualDoc.getData); 189 | test.equal(firstActualDoc.getData().sortField, -1); 190 | test.equal(firstActualDoc.getData().name, 'publishdoc'); 191 | 192 | done(); 193 | c.stop(); 194 | } 195 | }); 196 | }); 197 | } 198 | -------------------------------------------------------------------------------- /packages/easysearch_core/tests/functional/mongo-text-index-tests.js: -------------------------------------------------------------------------------- 1 | var collection = new Mongo.Collection('easysearch_testcollection'); 2 | 3 | if (Meteor.isServer) { 4 | collection.remove({ }); 5 | collection.insert({ _id: 'testId', name: 'testName with some weirdtokens in it' }); 6 | collection.insert({ _id: 'testId2', name: 'test what with some other tokens in it' }); 7 | } 8 | 9 | var index = new EasySearch.Index({ 10 | engine: new EasySearch.MongoTextIndex(), 11 | collection: collection, 12 | fields: ['name'], 13 | allowedFields: ['name', 'otherName'] 14 | }); 15 | 16 | Tinytest.addAsync('EasySearch - Functional - MongoTextIndex - search', function (test, done) { 17 | // TODO: fix the error that this test throws 18 | /* 19 | Tracker.autorun(function (c) { 20 | var cursor = index.search('weirdtokens testName'), 21 | docs = cursor.fetch(); 22 | 23 | if (docs.length === 1) { 24 | test.equal(docs, [{ _id: 'testId', name: 'testName with some weirdtokens in it' }]); 25 | test.equal(cursor.count(), 1); 26 | done(); 27 | c.stop(); 28 | } 29 | }); 30 | */ 31 | 32 | done(); 33 | }); 34 | 35 | Tinytest.add('EasySearch - Functional - MongoTextIndex - per field search', function (test) { 36 | test.throws(function () { 37 | index.search({ 'name': 'test' }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /packages/easysearch_core/tests/helpers.js: -------------------------------------------------------------------------------- 1 | TestHelpers = { 2 | createEngine: function (methods, defaultConf = {}) { 3 | let e = class engine extends EasySearch.Engine { 4 | defaultConfiguration() { return defaultConf; } 5 | }; 6 | 7 | _.each(methods, function (method, key) { 8 | e.prototype[key] = method; 9 | }); 10 | 11 | return e; 12 | }, 13 | createReactiveEngine: function (methods, defaultConf = {}) { 14 | let e = class engine extends EasySearch.ReactiveEngine { 15 | defaultConfiguration() { return defaultConf; } 16 | }; 17 | 18 | _.each(methods, function (method, key) { 19 | e.prototype[key] = method; 20 | }); 21 | 22 | return e; 23 | }, 24 | createIndex: function () { 25 | return new EasySearch.Index({ 26 | collection: new Mongo.Collection(null), 27 | fields: ['testField'], 28 | engine: new (TestHelpers.createEngine({ search: function () {} }))() 29 | }); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /packages/easysearch_core/tests/unit/core/cursor-tests.js: -------------------------------------------------------------------------------- 1 | Tinytest.add('EasySearch - Unit - Core - Cursor', function (test) { 2 | test.throws(function () { 3 | new EasySearch.Cursor(); 4 | }); 5 | 6 | test.throws(function () { 7 | new EasySearch.Cursor(new Mongo.Cursor(), null); 8 | }); 9 | 10 | var mongoCursor = new Mongo.Cursor(), 11 | cursor = new EasySearch.Cursor(mongoCursor, 200), 12 | notReadyCursor = new EasySearch.Cursor(mongoCursor, 0, false); 13 | 14 | mongoCursor.fetch = function () { 15 | return [{ _id: 'testId', name: 'whatever' }, { _id: 'testId2', name: 'whatever2' }]; 16 | }; 17 | 18 | test.equal(cursor.fetch(), [{ _id: 'testId', name: 'whatever' }, { _id: 'testId2', name: 'whatever2' }]); 19 | test.equal(cursor.count(), 200); 20 | test.equal(cursor.mongoCursor, mongoCursor); 21 | test.equal(cursor.isReady(), true); 22 | test.equal(notReadyCursor.isReady(), false); 23 | }); 24 | -------------------------------------------------------------------------------- /packages/easysearch_core/tests/unit/core/engine-tests.js: -------------------------------------------------------------------------------- 1 | Tinytest.add('EasySearch - Unit - Core - Engine', function (test) { 2 | var CustomNoSearchEngine = TestHelpers.createEngine({}), 3 | CustomSearchEngine = TestHelpers.createEngine({ 4 | search: function () { 5 | var cursor = new Mongo.Cursor(); 6 | 7 | cursor.fetch = function () { return []; }; 8 | 9 | return new EasySearch.Cursor(cursor, 200); 10 | } 11 | }); 12 | 13 | test.throws(function () { 14 | new EasySearch.Engine(); 15 | }); 16 | 17 | test.throws(function () { 18 | new CustomNoSearchEngine(); 19 | }); 20 | 21 | var engineInstance = new CustomSearchEngine(); 22 | 23 | test.equal(engineInstance.search().fetch(), []); 24 | test.equal(engineInstance.search().count(), 200); 25 | }); 26 | 27 | 28 | Tinytest.add('EasySearch - Unit - Core - Engine - custom configuration', function (test) { 29 | var CustomSearchEngine = TestHelpers.createEngine({ 30 | search: function () { 31 | var cursor = new Mongo.Cursor(); 32 | 33 | cursor.fetch = function () { return []; }; 34 | 35 | return new EasySearch.Cursor(cursor, 200); 36 | } 37 | }, { otherMethod: function () { 38 | return 'otherString'; 39 | }}); 40 | 41 | var nonOverwritingEngineInstance = new CustomSearchEngine({ 42 | customMethod: function () { 43 | return 'someString'; 44 | } 45 | }); 46 | 47 | var overwritingEngineInstance = new CustomSearchEngine({ 48 | otherMethod: function () { 49 | return 'anotherString'; 50 | } 51 | }); 52 | 53 | test.equal(nonOverwritingEngineInstance.config.customMethod(), 'someString'); 54 | test.equal(nonOverwritingEngineInstance.config.otherMethod(), 'otherString'); 55 | test.isUndefined(overwritingEngineInstance.config.customMethod); 56 | test.equal(overwritingEngineInstance.config.otherMethod(), 'anotherString'); 57 | }); 58 | -------------------------------------------------------------------------------- /packages/easysearch_core/tests/unit/core/index-tests.js: -------------------------------------------------------------------------------- 1 | var index = TestHelpers.createIndex(); 2 | 3 | Tinytest.addAsync('EasySearch - Unit - Core - Index', function (test, done) { 4 | test.throws(function () { 5 | new EasySearch.Index(); 6 | }); 7 | 8 | test.throws(function () { 9 | index.search(); 10 | }); 11 | 12 | test.throws(function () { 13 | index.search(true); 14 | }); 15 | 16 | index.config.engine.search = function (searchDefinition, options) { 17 | test.equal(searchDefinition, 'testSearch'); 18 | test.equal(options.search, { limit: 200, skip: 20, props: {} }) 19 | }; 20 | 21 | index.search('testSearch', { limit: 200, skip: 20 }); 22 | 23 | index.config.engine.search = function (searchDefinition, options) { 24 | test.equal(searchDefinition, 'testSearch'); 25 | test.equal(options.search, { props: { custom: 'property' }, limit: 10, skip: 0 }) 26 | done(); 27 | }; 28 | 29 | index.search('testSearch', { props: { custom: 'property' } }); 30 | }); 31 | 32 | Tinytest.add('EasySearch - Unit - Core - Index - Error handling', function (test) { 33 | test.throws(function () { 34 | index.search('testSearch', { foo: 'bar' }); 35 | }); 36 | 37 | test.throws(function () { 38 | index.search('testSearch', { limit: { foo: 'bar' } }); 39 | }); 40 | }); 41 | 42 | Tinytest.add('EasySearch - Unit - Core - Index - default configuration', function (test) { 43 | test.equal(EasySearch.Index.defaultConfiguration.permission(), true); 44 | }); 45 | -------------------------------------------------------------------------------- /packages/easysearch_core/tests/unit/core/reactive-engine-tests.js: -------------------------------------------------------------------------------- 1 | Tinytest.add('EasySearch - Unit - Core - ReactiveEngine', function (test) { 2 | var CustomReactiveEngine = TestHelpers.createReactiveEngine({ 3 | getSearchCursor: function (s, o) { 4 | test.equal(s, { name: 'testCursor' }); 5 | test.equal(o.foo, 'bar'); 6 | 7 | return new EasySearch.Cursor(new Mongo.Cursor(), 155); 8 | } 9 | }); 10 | 11 | test.throws(function () { 12 | new EasySearch.ReactiveEngine(); 13 | }); 14 | 15 | var engineInstance = new CustomReactiveEngine(), 16 | indexConfig = { name: 'testIndex' }; 17 | 18 | engineInstance.onIndexCreate(indexConfig); 19 | 20 | if (Meteor.isClient) { 21 | var counter = engineInstance.search('test', { index: { fields: ['name'], searchCollection: { find: function (d, o) { 22 | test.equal(d, 'test'); 23 | test.equal(o, { limit: 9 }); 24 | 25 | return new EasySearch.Cursor(new Mongo.Cursor(), 777); 26 | } } }, search: { limit: 9 } }); 27 | 28 | test.equal(counter.count(), 777); 29 | } else if (Meteor.isServer) { 30 | var cursor = engineInstance.search('testCursor', { foo: 'bar', index: { fields: ['name'] } }); 31 | test.instanceOf(cursor, EasySearch.Cursor); 32 | test.equal(cursor.count(), 155); 33 | } 34 | }); 35 | -------------------------------------------------------------------------------- /packages/easysearch_elasticsearch/README.md: -------------------------------------------------------------------------------- 1 | Easy Search Elasticsearch 2 | ===================== 3 | 4 | This package adds an `EasySearch.ElasticSearch` engine to EasySearch. EasySearch synchronizes documents to an index called 5 | __easysearch__, with types based on the collection name. 6 | 7 | ```javascript 8 | import { Index } from 'meteor/easy:search' 9 | import { ElasticSearchEngine } from 'meteor/easysearch:elasticsearch' 10 | 11 | // On Client and Server 12 | const Players = new Mongo.Collection('players') 13 | const PlayersIndex = new Index({ 14 | collection: Players, 15 | fields: ['name'], 16 | engine: new ElasticSearchEngine({ 17 | body: () => { ... } // modify the body that's sent when searching 18 | }), 19 | }) 20 | ``` 21 | 22 | ## Configuration 23 | 24 | The client doesn't require any configuration if ElasticSearch runs locally on port `9200`. 25 | The configuration options that can be passed to `EasSearch.ElasticSearch` as an object are following. 26 | 27 | * __client__: Object of [client configuration](https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/quick-start.html) (such as the `host` and so on) 28 | * __fieldsToIndex__: Array of document fields to index, by default all fields 29 | * __query(searchObject, options)__: Function that returns the query, by default a `fuzzy_like_this` query 30 | * __sort(searchObject, options)__: Function that returns the sort parameter, by default the index `fields` 31 | * __getElasticSearchDoc(doc, fields)__: Function that returns the document to index, fieldsToIndex by default 32 | * __body(body)__: Function that returns the ElasticSearch body to send when searching 33 | * __indexName__: String for the elasticsearch index name 34 | * __indexType__: String for the elasticsearch index type 35 | 36 | ## Mapping, Analyzers and so on 37 | 38 | To make changes to the mapping you can use the mapping setting which will set the mapping when creating a new index. 39 | 40 | ```javascript 41 | const PlayersIndex = new Index({ 42 | collection: Players, 43 | name: 'players', 44 | fields: ['name'], 45 | mapping: { 46 | players: { 47 | properties: { 48 | name: { 49 | type: 'string' 50 | } 51 | } 52 | } 53 | } 54 | ... 55 | }) 56 | ``` 57 | 58 | ## How to run ElasticSearch 59 | 60 | ```sh 61 | 62 | # Install Elastic Search through brew. 63 | brew install elasticsearch 64 | # Start the service, runs on http://localhost:9200 by default. 65 | elasticsearch -f -D es.config=/usr/local/opt/elasticsearch/config/elasticsearch.yml 66 | ``` 67 | 68 | ## How to install 69 | 70 | ```sh 71 | cd /path/to/project 72 | meteor add easysearch:elasticsearch 73 | ``` 74 | -------------------------------------------------------------------------------- /packages/easysearch_elasticsearch/lib/data-syncer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The ElasticSearchDataSyncer syncs data between a collection and a specified index. 3 | * 4 | * @type {ElasticSearchDataSyncer} 5 | */ 6 | class ElasticSearchDataSyncer { 7 | /** 8 | * Constructor. 9 | * 10 | * @param indexName {String} Index name 11 | * @param indexType {String} Index type 12 | * @param collection {Object} Mongo Collection 13 | * @param client {Object} ElasticSearch client 14 | * @param beforeIndex {Function} Change document before indexing 15 | */ 16 | constructor({indexName, indexType, collection, client, beforeIndex}) { 17 | this.indexName = indexName; 18 | this.indexType = indexType; 19 | this.collection = collection; 20 | this.client = client; 21 | 22 | const removeId = (obj) => { 23 | let { _id, ...rest } = obj 24 | return rest; 25 | } 26 | 27 | this.collection.find().observeChanges({ 28 | added: (id, fields) => { 29 | this.writeToIndex(removeId(beforeIndex(fields)), id); 30 | }, 31 | changed: (id) => { 32 | this.writeToIndex(removeId(beforeIndex(collection.findOne(id))), id); 33 | }, 34 | removed: (id) => { 35 | this.client.delete({ 36 | index: this.indexName, 37 | type: this.indexType, 38 | id: id 39 | }); 40 | } 41 | }); 42 | } 43 | 44 | /** 45 | * Write a document to a specified index. 46 | * 47 | * @param {Object} doc Document to write into the index 48 | * @param {String} id ID of the document 49 | */ 50 | writeToIndex(doc, id) { 51 | this.client.index({ 52 | index : this.indexName, 53 | type : this.indexType, 54 | id : id, 55 | body : doc 56 | }); 57 | } 58 | } 59 | 60 | export default ElasticSearchDataSyncer 61 | -------------------------------------------------------------------------------- /packages/easysearch_elasticsearch/lib/engine.js: -------------------------------------------------------------------------------- 1 | import ElasticSearchDataSyncer from './data-syncer' 2 | 3 | if (Meteor.isServer) { 4 | var Future = Npm.require('fibers/future'), 5 | elasticsearch = Npm.require('elasticsearch'); 6 | } 7 | 8 | /** 9 | * The ElasticsearchEngine lets you search documents through an Elasticsearch Index. 10 | * 11 | * @type {ElasticSearchEngine} 12 | */ 13 | class ElasticSearchEngine extends EasySearch.ReactiveEngine { 14 | /** 15 | * Constructor. 16 | */ 17 | constructor() { 18 | super(...arguments); 19 | } 20 | 21 | /** 22 | * Return default configuration. 23 | * 24 | * @returns {Object} 25 | */ 26 | defaultConfiguration() { 27 | return _.defaults({}, ElasticSearchEngine.defaultElasticsearchConfiguration(), super.defaultConfiguration()); 28 | } 29 | 30 | /** 31 | * Default configuration. 32 | * 33 | * @returns {Object} 34 | */ 35 | static defaultElasticsearchConfiguration() { 36 | return { 37 | indexName: 'easysearch', 38 | /** 39 | * Return the fields to index in ElasticSearch. 40 | * 41 | * @param {Object} options Index options 42 | * 43 | * @returns {null|Array} 44 | */ 45 | fieldsToIndex(options) { 46 | return null; 47 | }, 48 | /** 49 | * The ES query object used for searching the results. 50 | * 51 | * @param {Object} searchObject Search object 52 | * @param {Object} options Search options 53 | * 54 | * @return array 55 | */ 56 | query(searchObject, options) { 57 | let query = { 58 | bool: { 59 | should: [] 60 | } 61 | }; 62 | 63 | _.each(searchObject, function (searchString, field) { 64 | query.bool.should.push({ 65 | match: { 66 | [field]: { 67 | query: searchString, 68 | fuzziness: 'AUTO', 69 | operator: 'or' 70 | } 71 | } 72 | }); 73 | }); 74 | 75 | return query; 76 | }, 77 | /** 78 | * The ES sorting method used for sorting the results. 79 | * 80 | * @param {Object} searchObject Search object 81 | * @param {Object} options Search options 82 | * 83 | * @return array 84 | */ 85 | sort(searchObject, options) { 86 | return options.index.fields; 87 | }, 88 | /** 89 | * Return the ElasticSearch document to index. 90 | * 91 | * @param {Object} doc Document to index 92 | * @param {Array} fields Array of document fields 93 | */ 94 | getElasticSearchDoc(doc, fields) { 95 | if (null === fields) { 96 | return doc; 97 | } 98 | 99 | let partialDoc = {}; 100 | 101 | _.each(fields, function (field) { 102 | if (_.has(doc, field)) { 103 | partialDoc[field] = _.get(doc, field); 104 | } 105 | }); 106 | 107 | return partialDoc; 108 | }, 109 | /** 110 | * Return the elastic search body. 111 | * 112 | * @param {Object} body Existing ES body 113 | * 114 | * @return {Object} 115 | */ 116 | body: (body) => body, 117 | /** 118 | * Default ES client configuration. 119 | */ 120 | client: { 121 | host: 'localhost:9200', 122 | }, 123 | }; 124 | } 125 | 126 | /** 127 | * Put mapping according to mapping field provided when creating an EasySearch index 128 | * 129 | * @param {Object} indexConfig Index configuration 130 | * @param {Function} cb callback on finished mapping 131 | */ 132 | putMapping(indexConfig = {}, cb) { 133 | const { 134 | mapping: body, 135 | elasticSearchClient, 136 | } = indexConfig; 137 | 138 | if (!body) { 139 | return cb(); 140 | } 141 | 142 | const { indexName } = this.config 143 | const type = this.getIndexType(indexConfig) 144 | 145 | elasticSearchClient.indices.create({ 146 | updateAllTypes: false, 147 | index: indexName, 148 | }, Meteor.bindEnvironment(() => { 149 | elasticSearchClient.indices.getMapping({ 150 | index: indexName, 151 | type 152 | }, Meteor.bindEnvironment((err, res) => { 153 | const isEmpty = Object.keys(res).length === 0 && res.constructor === Object; 154 | if (!isEmpty) { 155 | return cb(); 156 | } 157 | 158 | elasticSearchClient.indices.putMapping({ 159 | updateAllTypes: false, 160 | index: indexName, 161 | type, 162 | body 163 | }, cb); 164 | })); 165 | })); 166 | } 167 | 168 | /** 169 | * @returns {String} 170 | */ 171 | getIndexType(indexConfig) { 172 | return this.config.indexType || indexConfig.name; 173 | } 174 | 175 | /** 176 | * Act on index creation. 177 | * 178 | * @param {Object} indexConfig Index configuration 179 | */ 180 | onIndexCreate(indexConfig) { 181 | super.onIndexCreate(indexConfig); 182 | 183 | if (Meteor.isServer) { 184 | indexConfig.elasticSearchClient = new elasticsearch.Client(this.config.client); 185 | this.putMapping(indexConfig, Meteor.bindEnvironment(() => { 186 | indexConfig.elasticSearchSyncer = new ElasticSearchDataSyncer({ 187 | indexName: this.config.indexName, 188 | indexType: this.getIndexType(indexConfig), 189 | collection: indexConfig.collection, 190 | client: indexConfig.elasticSearchClient, 191 | beforeIndex: (doc) => this.callConfigMethod('getElasticSearchDoc', doc, this.callConfigMethod('fieldsToIndex', indexConfig)) 192 | }); 193 | })); 194 | } 195 | } 196 | 197 | /** 198 | * Return the reactive search cursor. 199 | * 200 | * @param {Object} searchDefinition Search definition 201 | * @param {Object} options Search and index options 202 | */ 203 | getSearchCursor(searchDefinition, options) { 204 | let fut = new Future(), 205 | body = {}; 206 | 207 | searchDefinition = EasySearch.MongoDB.prototype.transformSearchDefinition(searchDefinition, options); 208 | 209 | body.query = this.callConfigMethod('query', searchDefinition, options); 210 | body.sort = this.callConfigMethod('sort', searchDefinition, options); 211 | body.fields = ['_id']; 212 | 213 | body = this.callConfigMethod('body', body, options); 214 | 215 | options.index.elasticSearchClient.search({ 216 | index: this.config.indexName, 217 | type: this.getIndexType(options.index), 218 | body: body, 219 | size: options.search.limit, 220 | from: options.search.skip 221 | }, Meteor.bindEnvironment((error, data) => { 222 | if (error) { 223 | console.log('Had an error while searching!'); 224 | console.log(error); 225 | return; 226 | } 227 | 228 | let { total, ids } = this.getCursorData(data), 229 | cursor; 230 | 231 | if (ids.length > 0) { 232 | cursor = options.index.collection.find({ 233 | $or: _.map(ids, (_id) => { 234 | return { _id }; 235 | }) 236 | }, { limit: options.search.limit, sort: options.search.sort || {} }); 237 | } else { 238 | cursor = EasySearch.Cursor.emptyCursor; 239 | } 240 | 241 | fut['return'](new EasySearch.Cursor(cursor, total)); 242 | })); 243 | 244 | return fut.wait(); 245 | } 246 | 247 | /** 248 | * Get data for the cursor from the elastic search response. 249 | * 250 | * @param {Object} data ElasticSearch data 251 | * 252 | * @returns {Object} 253 | */ 254 | getCursorData(data) { 255 | return { 256 | ids : _.map(data.hits.hits, (resultSet) => resultSet._id), 257 | total: data.hits.total 258 | }; 259 | } 260 | } 261 | 262 | EasySearch.ElasticSearch = ElasticSearchEngine 263 | 264 | export default ElasticSearchEngine 265 | -------------------------------------------------------------------------------- /packages/easysearch_elasticsearch/lib/main.js: -------------------------------------------------------------------------------- 1 | import ElasticSearchEngine from './engine' 2 | 3 | export { 4 | ElasticSearchEngine, 5 | } 6 | -------------------------------------------------------------------------------- /packages/easysearch_elasticsearch/package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'easysearch:elasticsearch', 3 | summary: "Elasticsearch Engine for EasySearch", 4 | version: "2.2.3", 5 | git: "https://github.com/matteodem/meteor-easy-search.git", 6 | documentation: 'README.md' 7 | }); 8 | 9 | Npm.depends({ 10 | 'elasticsearch': '13.0.0' 11 | }); 12 | 13 | Package.onUse(function(api) { 14 | api.versionsFrom('1.4.2'); 15 | 16 | // Dependencies 17 | api.use(['check', 'ecmascript']); 18 | api.use(['easysearch:core@2.2.3', 'erasaur:meteor-lodash@4.0.0']); 19 | 20 | api.addFiles([ 21 | 'lib/data-syncer.js', 22 | 'lib/engine.js', 23 | ]); 24 | 25 | api.export('EasySearch'); 26 | api.mainModule('./lib/main.js'); 27 | }); 28 | 29 | Package.onTest(function(api) { 30 | api.use(['tinytest', 'ecmascript']); 31 | api.use('easysearch:elasticsearch'); 32 | 33 | api.addFiles(['tests/engine-tests.js']); 34 | }); 35 | -------------------------------------------------------------------------------- /packages/easysearch_elasticsearch/tests/engine-tests.js: -------------------------------------------------------------------------------- 1 | Tinytest.add('EasySearch ElasticSearch - Unit - Configuration', function (test) { 2 | var defaultConfig = EasySearch.ElasticSearch.defaultElasticsearchConfiguration(), 3 | doc = { 4 | name: 'test name', 5 | score: 12, 6 | nested: { field: '200' } 7 | }; 8 | 9 | test.equal(defaultConfig.fieldsToIndex(), null); 10 | test.equal(defaultConfig.query({ name: 'testString' }, { index: { fields: ['name'] } }), { 11 | bool: { 12 | should: [{ fuzzy_like_this: { 13 | 'fields': ['name'], 14 | 'like_text': 'testString' 15 | }}] 16 | } 17 | }); 18 | 19 | test.equal(defaultConfig.sort('testString', { index: { fields: ['name'] } }), ['name']); 20 | 21 | test.equal(defaultConfig.getElasticSearchDoc(doc, null), doc); 22 | test.equal(defaultConfig.getElasticSearchDoc(doc, ['score', 'nested.field']),{ score: 12, 'nested.field' : '200' }); 23 | 24 | test.equal(defaultConfig.client, { host: 'localhost:9200' }); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/matteodem_easy-search/.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-pro 2 | repo_token: kNpehY5zjnoVWbzWSvr33PMEG0m5WOl3y 3 | -------------------------------------------------------------------------------- /packages/matteodem_easy-search/.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: node_js 3 | node_js: 4 | - "0.12" 5 | before_install: 6 | - "curl -L https://raw.githubusercontent.com/arunoda/travis-ci-meteor-packages/master/configure.sh | /bin/sh" 7 | services: 8 | - mongodb 9 | -------------------------------------------------------------------------------- /packages/matteodem_easy-search/.versions: -------------------------------------------------------------------------------- 1 | babel-compiler@5.8.24_1 2 | babel-runtime@0.1.4 3 | base64@1.0.4 4 | binary-heap@1.0.4 5 | blaze@2.1.3 6 | blaze-tools@1.0.4 7 | boilerplate-generator@1.0.4 8 | caching-compiler@1.0.0 9 | caching-html-compiler@1.0.2 10 | callback-hook@1.0.4 11 | check@1.0.6 12 | coffeescript@1.0.10 13 | ddp@1.2.2 14 | ddp-client@1.2.1 15 | ddp-common@1.2.1 16 | ddp-server@1.2.1 17 | deps@1.0.9 18 | diff-sequence@1.0.1 19 | easy:search@2.0.0 20 | easysearch:components@2.0.0 21 | easysearch:core@2.0.0 22 | ecmascript@0.1.5 23 | ecmascript-collections@0.1.6 24 | ejson@1.0.7 25 | erasaur:meteor-lodash@3.10.1_1 26 | geojson-utils@1.0.4 27 | html-tools@1.0.5 28 | htmljs@1.0.5 29 | id-map@1.0.4 30 | jquery@1.11.4 31 | logging@1.0.8 32 | matteodem:easy-search@2.0.0 33 | meteor@1.1.9 34 | minifiers@1.1.7 35 | minimongo@1.0.10 36 | mongo@1.1.2 37 | mongo-id@1.0.1 38 | npm-mongo@1.4.39_1 39 | observe-sequence@1.0.7 40 | ordered-dict@1.0.4 41 | peerlibrary:assert@0.2.5 42 | peerlibrary:base-component@0.10.0 43 | peerlibrary:blaze-components@0.13.0 44 | promise@0.5.0 45 | random@1.0.4 46 | reactive-dict@1.1.2 47 | reactive-var@1.0.6 48 | retry@1.0.4 49 | routepolicy@1.0.6 50 | spacebars@1.0.7 51 | spacebars-compiler@1.0.7 52 | templating@1.1.4 53 | templating-tools@1.0.0 54 | tracker@1.0.9 55 | ui@1.0.8 56 | underscore@1.0.4 57 | webapp@1.2.2 58 | webapp-hashing@1.0.5 59 | -------------------------------------------------------------------------------- /packages/matteodem_easy-search/package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'matteodem:easy-search', 3 | summary: "Easy-to-use search with Blaze Components (+ Elastic Search support)", 4 | version: "2.0.0", 5 | git: "https://github.com/matteodem/meteor-easy-search.git", 6 | documentation: "../../README.md" 7 | }); 8 | 9 | Package.on_use(function (api) { 10 | if (api.versionsFrom) { 11 | api.versionsFrom('1.2.0.1'); 12 | } 13 | 14 | api.use('easy:search@2.0.0'); 15 | api.export('EasySearch'); 16 | 17 | console.log('matteodem:easy-search is deprecated (use easy:search instead)'); 18 | }); 19 | -------------------------------------------------------------------------------- /packages/matteodem_easy-search/versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | [ 4 | "application-configuration", 5 | "1.0.3" 6 | ], 7 | [ 8 | "autoupdate", 9 | "1.1.3" 10 | ], 11 | [ 12 | "base64", 13 | "1.0.1" 14 | ], 15 | [ 16 | "binary-heap", 17 | "1.0.1" 18 | ], 19 | [ 20 | "blaze", 21 | "2.0.3" 22 | ], 23 | [ 24 | "blaze-tools", 25 | "1.0.1" 26 | ], 27 | [ 28 | "boilerplate-generator", 29 | "1.0.1" 30 | ], 31 | [ 32 | "callback-hook", 33 | "1.0.1" 34 | ], 35 | [ 36 | "check", 37 | "1.0.2" 38 | ], 39 | [ 40 | "ddp", 41 | "1.0.12" 42 | ], 43 | [ 44 | "deps", 45 | "1.0.5" 46 | ], 47 | [ 48 | "ejson", 49 | "1.0.4" 50 | ], 51 | [ 52 | "fastclick", 53 | "1.0.1" 54 | ], 55 | [ 56 | "follower-livedata", 57 | "1.0.2" 58 | ], 59 | [ 60 | "geojson-utils", 61 | "1.0.1" 62 | ], 63 | [ 64 | "html-tools", 65 | "1.0.2" 66 | ], 67 | [ 68 | "htmljs", 69 | "1.0.2" 70 | ], 71 | [ 72 | "http", 73 | "1.0.8" 74 | ], 75 | [ 76 | "id-map", 77 | "1.0.1" 78 | ], 79 | [ 80 | "jquery", 81 | "1.0.1" 82 | ], 83 | [ 84 | "json", 85 | "1.0.1" 86 | ], 87 | [ 88 | "launch-screen", 89 | "1.0.0" 90 | ], 91 | [ 92 | "less", 93 | "1.0.11" 94 | ], 95 | [ 96 | "livedata", 97 | "1.0.11" 98 | ], 99 | [ 100 | "logging", 101 | "1.0.5" 102 | ], 103 | [ 104 | "meteor", 105 | "1.1.3" 106 | ], 107 | [ 108 | "meteor-platform", 109 | "1.2.0" 110 | ], 111 | [ 112 | "minifiers", 113 | "1.1.2" 114 | ], 115 | [ 116 | "minimongo", 117 | "1.0.5" 118 | ], 119 | [ 120 | "mobile-status-bar", 121 | "1.0.1" 122 | ], 123 | [ 124 | "mongo", 125 | "1.0.9" 126 | ], 127 | [ 128 | "mongo-livedata", 129 | "1.0.6" 130 | ], 131 | [ 132 | "observe-sequence", 133 | "1.0.3" 134 | ], 135 | [ 136 | "ordered-dict", 137 | "1.0.1" 138 | ], 139 | [ 140 | "random", 141 | "1.0.1" 142 | ], 143 | [ 144 | "reactive-dict", 145 | "1.0.4" 146 | ], 147 | [ 148 | "reactive-var", 149 | "1.0.3" 150 | ], 151 | [ 152 | "reload", 153 | "1.1.1" 154 | ], 155 | [ 156 | "retry", 157 | "1.0.1" 158 | ], 159 | [ 160 | "routepolicy", 161 | "1.0.2" 162 | ], 163 | [ 164 | "session", 165 | "1.0.4" 166 | ], 167 | [ 168 | "spacebars", 169 | "1.0.3" 170 | ], 171 | [ 172 | "spacebars-compiler", 173 | "1.0.3" 174 | ], 175 | [ 176 | "standard-app-packages", 177 | "1.0.3" 178 | ], 179 | [ 180 | "templating", 181 | "1.0.9" 182 | ], 183 | [ 184 | "tracker", 185 | "1.0.3" 186 | ], 187 | [ 188 | "ui", 189 | "1.0.4" 190 | ], 191 | [ 192 | "url", 193 | "1.0.2" 194 | ], 195 | [ 196 | "webapp", 197 | "1.1.4" 198 | ], 199 | [ 200 | "webapp-hashing", 201 | "1.0.1" 202 | ], 203 | [ 204 | "meteorhacks:aggregate", 205 | "1.2.1" 206 | ] 207 | ], 208 | "pluginDependencies": [], 209 | "toolVersion": "meteor-tool@1.0.36", 210 | "format": "1.0" 211 | } 212 | -------------------------------------------------------------------------------- /scripts/publish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ -d "packages/easy_search" ]; then 4 | cd packages/easysearch_core 5 | meteor publish 6 | cd ../easysearch_components 7 | meteor publish 8 | cd ../easy:search 9 | meteor publish 10 | cd ../easysearch_autosuggest 11 | meteor publish 12 | cd ../easysearch_elasticsearch 13 | meteor publish 14 | cd .. 15 | else 16 | echo "Execute in root folder" 17 | fi 18 | -------------------------------------------------------------------------------- /scripts/test-packages.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cd ./packages && spacejam test-packages ./easysearch_core ./easysearch_components 4 | --------------------------------------------------------------------------------