diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..d6f3562 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +tmp +dist diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..9ead932 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,18 @@ +module.exports = { + extends: 'eslint:recommended', + env: { + node: true + }, + parserOptions: { + ecmaVersion: 8 + }, + rules: { + 'no-console': 0, + 'no-unused-vars': ['error', { 'args': 'none' }], + 'yoda': ["error", "always"], + 'max-len': ["error", { "code": 80 }], + }, + globals: { + Promise: 'readonly' + } +} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..1d1a39d --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,40 @@ +# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions + +name: build + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + timeout-minutes: 6 + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + node-version: [17.x, 16.x, 14.x] + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + # manually install peerdeps for node 12,14 + - run: npm i seneca seneca-entity + - run: npm run build --if-present + - run: npm test + + - name: Coveralls + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + path-to-lcov: ./test/lcov.info diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9a50873 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +lib-cov +*.seed +*.log +*.csv +*.dat +*.out +*.pid +*.gz + +pids +logs +results + +npm-debug.log +node_modules +*~ +.DS_Store +coverage.html + +.history +yarn.lock +package-lock.json +lcov.info +test/coverage.html + + diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..adfb6c3 --- /dev/null +++ b/.npmignore @@ -0,0 +1,3 @@ +*~ +*.off +*-off diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..00fbdb1 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "semi": false, + "singleQuote": true +} \ No newline at end of file diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000..9e98a2e --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,42 @@ +## 6.2.0 - 2021-09-28 + +* Update deps. + + +## 6.0.0 - 2021-05-24 + +* Added upsert support + + +## 5.0.0 - 2020-12-09 + +* Convert to typescript +* Support mongo-style constraints ($gte, $ne, etc.) + + +## 0.6.0 - 2016-08-25 + +* Added Seneca 3 and Node 6 support +* Dropped Node 0.10, 0.12, 5 support +* Updated dependencies + + +## 0.5.1 - 2016-08-09 + +* Updated dependencies + + +## 0.4.0 - 2015-11-25 + +* The memory store follows the specification of seneca stores +* Linted the codebase to folow the seneca styleguide + + +## 0.3.1 - 2015-06-16 + +* Export action responds with object: {json: "..."} + + +## 0.3.0 - 2015-06-16 + +* cmd:import/export no longer uses filesystem, just accepts/provides JSON string. Prep for Seneca 0.6.2. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a30c387 --- /dev/null +++ b/LICENSE @@ -0,0 +1,23 @@ +The MIT License (MIT) + +Copyright (c) 2015-2016, Richard Rodger and other contributors. +Copyright (c) 2010-2014, Richard Rodger. + + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..61a6229 --- /dev/null +++ b/README.md @@ -0,0 +1,152 @@ +![Seneca](http://senecajs.org/files/assets/seneca-logo.png) +> A [Seneca.js][] data storage plugin. + +# seneca-mem-store +[![npm version][npm-badge]][npm-url] +[![Build](https://github.com/senecajs/seneca-mem-store/actions/workflows/build.yml/badge.svg)](https://github.com/senecajs/seneca-mem-store/actions/workflows/build.yml) +[![Dependency Status][david-badge]][david-url] +[![Maintainability](https://api.codeclimate.com/v1/badges/e2cdcc5415161cb378b0/maintainability)](https://codeclimate.com/github/senecajs/seneca-mem-store/maintainability) +[![DeepScan grade](https://deepscan.io/api/teams/5016/projects/17225/branches/388415/badge/grade.svg)](https://deepscan.io/dashboard#view=project&tid=5016&pid=17225&bid=388415) +[![Coveralls][BadgeCoveralls]][Coveralls] + + + +| ![Voxgig](https://www.voxgig.com/res/img/vgt01r.png) | This open source module is sponsored and supported by [Voxgig](https://www.voxgig.com). | +|---|---| + + +## Description + +This module is a plugin for the Seneca framework. It provides an +in-memory storage engine that provides a set of data storage action +patterns. *Data does not persist betweens runs*. This plugin is most +useful for early development and unit testing. It also provides an +example of a document-oriented storage plugin code-base. + +The Seneca framework provides an [ActiveRecord-style data storage API][]. +Each supported database has a plugin, such as this one, that provides +the underlying Seneca plugin actions required for data persistence. + +This plugin is loaded by default by the [seneca-entity][seneca-entity-url] plugin that also needs the [seneca-basic][seneca-basic-url] plugin to function properly. + +If you're using this module, and need help, you can: + +- Post a [github issue][], +- Tweet to [@senecajs][], +- Ask on the [Gitter][gitter-url]. + +If you are new to Seneca in general, please take a look at [senecajs.org][]. We have everything from +tutorials to sample apps to help get you up and running quickly. + + +## Code examples + +For code samples, please see the [tests][mem-store-tests] for this plugin. + +### Seneca compatibility +Supports Seneca versions **2.x** and above + + +### Supported functionality +All Seneca data store supported functionality is implemented in [seneca-store-test](https://github.com/senecajs/seneca-store-test) as a test suite. The tests represent the store functionality specifications. + +## Install + +```sh +npm install seneca +npm install seneca-mem-store +``` + +You'll need the [seneca](http://github.com/senecajs/seneca) toolkit to use this module - it's just a plugin. + +## Quick Example + +```js +var seneca = require('seneca')() + +seneca.use('basic') +.use('entity') + +// Since mem-store is a default plugin, it does not need to be +// added with .use(). You can just go ahead and use it. +seneca.ready(function () { + var apple = seneca.make$('fruit') + apple.name = 'Pink Lady' + apple.price = 0.99 + + apple.save$(function (err, apple) { + console.log("apple.id = " + apple.id) + }) +}) +``` + +## Usage +You don't use this module directly. It provides an underlying data storage engine for the Seneca entity API: + +```js +var entity = seneca.make$('typename') +entity.someproperty = "something" +entity.anotherproperty = 100 + +entity.save$(function (err, entity) { ... }) +entity.load$({id: ... }, function (err, entity) { ... }) +entity.list$({property: ... }, function (err, entity) { ... }) +entity.remove$({id: ... }, function (err, entity) { ... }) +``` + +### Query Support +The standard Seneca query format is supported: + +- `.list$({f1:v1, f2:v2, ...})` implies pseudo-query `f1==v1 AND f2==v2, ...`. + +- `.list$({f1:v1,...}, {sort$:{field1:1}})` means sort by f1, ascending. + +- `.list$({f1:v1,...}, {sort$:{field1:-1}})` means sort by f1, descending. + +- `.list$({f1:v1,...}, {limit$:10})` means only return 10 results. + +- `.list$({f1:v1,...}, {skip$:5})` means skip the first 5. + +- `.list$({f1:v1,...}, {fields$:['fd1','f2']})` means only return the listed fields. + +Note: you can use `sort$`, `limit$`, `skip$` and `fields$` together. + +### Native Driver +This store is an in memory store and as such does not require the need of a native driver. + +## Contributing +The [Senecajs org][] encourages open participation. If you feel you can help in any way, be it with +documentation, examples, extra testing, or new features please get in touch. + +## Test +To run tests, simply use npm: + +```sh +npm run test +``` + +## License +Copyright (c) 2015-2016, Richard Rodger and other contributors. +Copyright (c) 2010-2014, Richard Rodger. +Licensed under [MIT][]. + +[MIT]: ./LICENSE +[npm-badge]: https://badge.fury.io/js/seneca-mem-store.svg +[npm-url]: https://badge.fury.io/js/seneca-mem-store +[Senecajs org]: https://github.com/senecajs/ +[Seneca.js]: https://www.npmjs.com/package/seneca +[@senecajs]: http://twitter.com/senecajs +[senecajs.org]: http://senecajs.org/ +[travis-badge]: https://travis-ci.org/senecajs/seneca-mem-store.svg +[travis-url]: https://travis-ci.org/senecajs/seneca-mem-store +[gitter-badge]: https://badges.gitter.im/Join%20Chat.svg +[gitter-url]: https://gitter.im/senecajs/seneca +[github issue]: https://github.com/senecajs/seneca-mem-store/issues +[ActiveRecord-style data storage API]:http://senecajs.org/tutorials/understanding-data-entities.html +[david-badge]: https://david-dm.org/senecajs/seneca-mem-store.svg +[david-url]: https://david-dm.org/senecajs/seneca-mem-store +[Coveralls]: https://coveralls.io/github/senecajs/seneca-mem-store?branch=master +[BadgeCoveralls]: https://coveralls.io/repos/github/senecajs/seneca-mem-store/badge.svg?branch=master +[seneca-basic-url]: https://github.com/senecajs/seneca-basic +[seneca-entity-url]: https://github.com/senecajs/seneca-entity +[mem-store-tests]: https://github.com/senecajs/seneca-mem-store/tree/master/test diff --git a/dist/intern.d.ts b/dist/intern.d.ts new file mode 100644 index 0000000..13ca293 --- /dev/null +++ b/dist/intern.d.ts @@ -0,0 +1,12 @@ +export declare class intern { + static is_new(ent: any): boolean; + static is_upsert(msg: any): boolean; + static find_mement(entmap: any, base_ent: any, filter: any): any; + static update_mement(entmap: any, base_ent: any, filter: any, new_attrs: any): any; + static should_merge(ent: any, plugin_opts: any): boolean; + static listents(seneca: any, entmap: any, qent: any, q: any, done: any): void; + static clean_array(ary: string[]): string[]; + static is_object(x: any): boolean; + static is_date(x: any): boolean; + static eq_dates(lv: Date, rv: Date): boolean; +} diff --git a/dist/intern.js b/dist/intern.js new file mode 100644 index 0000000..b1bb21c --- /dev/null +++ b/dist/intern.js @@ -0,0 +1,193 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.intern = void 0; +class intern { + static is_new(ent) { + // NOTE: This function is intended for use by the #save method. This + // function returns true when the entity argument is assumed to not yet + // exist in the store. + // + // In terms of code, if client code looks like so: + // ``` + // seneca.make('product') + // .data$({ label, price }) + // .save$(done) + // ``` + // + // - `is_new` will be invoked from the #save method and return + // true, because the product entity is yet to be saved. + // + // The following client code will cause `is_new` to return false, + // when invoked from the #save method, because the user entity already + // exists: + // ``` + // seneca.make('user') + // .load$(user_id, (err, user) => { + // if (err) return done(err) + // + // return user + // .data$({ email, username }) + // .save$(done) + // }) + // ``` + // + return null != ent && null == ent.id; + } + static is_upsert(msg) { + const { ent, q } = msg; + return intern.is_new(ent) && q && Array.isArray(q.upsert$); + } + static find_mement(entmap, base_ent, filter) { + const { base, name } = base_ent.canon$({ object: true }); + const entset = entmap[base] && entmap[base][name]; + if (null == entset) { + return null; + } + let out = null; + for (const ent_id in entset) { + const mement = entset[ent_id]; + if (matches(mement, filter)) { + out = mement; + break; + } + } + return out; + function matches(ent, filter) { + for (const fp in filter) { + if (fp in ent && filter[fp] === ent[fp]) { + continue; + } + return false; + } + return true; + } + } + static update_mement(entmap, base_ent, filter, new_attrs) { + const ent_to_update = intern.find_mement(entmap, base_ent, filter); + if (ent_to_update) { + Object.assign(ent_to_update, new_attrs); + return ent_to_update; + } + return null; + } + static should_merge(ent, plugin_opts) { + return !(false === plugin_opts.merge || false === ent.merge$); + } + // NOTE: Seneca supports a reasonable set of features + // in terms of listing. This function can handle + // sorting, skiping, limiting and general retrieval. + // + static listents(seneca, entmap, qent, q, done) { + let list = []; + let canon = qent.canon$({ object: true }); + let base = canon.base; + let name = canon.name; + let entset = entmap[base] ? entmap[base][name] : null; + let ent; + if (null != entset && null != q) { + if ('string' == typeof q) { + ent = entset[q]; + if (ent) { + list.push(ent); + } + } + else if (Array.isArray(q)) { + q.forEach(function (id) { + let ent = entset[id]; + if (ent) { + ent = qent.make$(ent); + list.push(ent); + } + }); + } + else if ('object' === typeof q) { + let entids = Object.keys(entset); + next_ent: for (let id of entids) { + ent = entset[id]; + for (let p in q) { + let qv = q[p]; // query val + let ev = ent[p]; // ent val + if (-1 === p.indexOf('$')) { + if (Array.isArray(qv)) { + if (-1 === qv.indexOf(ev)) { + continue next_ent; + } + } + else if (intern.is_object(qv)) { + // mongo style constraints + if ((null != qv.$ne && qv.$ne == ev) || + (null != qv.$gte && qv.$gte > ev) || + (null != qv.$gt && qv.$gt >= ev) || + (null != qv.$lt && qv.$lt <= ev) || + (null != qv.$lte && qv.$lte < ev) || + (null != qv.$in && -1 === qv.$in.indexOf(ev)) || + (null != qv.$nin && -1 !== qv.$nin.indexOf(ev)) || + false) { + continue next_ent; + } + } + else { + if (intern.is_date(qv)) { + if (!(intern.is_date(ev) && intern.eq_dates(qv, ev))) { + continue next_ent; + } + } + else if (qv !== ev) { + continue next_ent; + } + } + } + } + ent = qent.make$(ent); + list.push(ent); + } + } + } + // Always sort first, this is the 'expected' behaviour. + if (null != q && q.sort$) { + let sf; + for (sf in q.sort$) { + break; + } + let sd = q.sort$[sf] < 0 ? -1 : 1; + list = list.sort(function (a, b) { + return sd * (a[sf] < b[sf] ? -1 : a[sf] === b[sf] ? 0 : 1); + }); + } + // Skip before limiting. + if (null != q && q.skip$ && q.skip$ > 0) { + list = list.slice(q.skip$); + } + // Limited the possibly sorted and skipped list. + if (null != q && q.limit$ && q.limit$ >= 0) { + list = list.slice(0, q.limit$); + } + // Prune fields + if (null != q && q.fields$) { + for (let i = 0; i < list.length; i++) { + let entfields = list[i].fields$(); + for (let j = 0; j < entfields.length; j++) { + if ('id' !== entfields[j] && -1 == q.fields$.indexOf(entfields[j])) { + delete list[i][entfields[j]]; + } + } + } + } + // Return the resulting list to the caller. + done.call(seneca, null, list); + } + static clean_array(ary) { + return ary.filter((prop) => !prop.includes('$')); + } + static is_object(x) { + return '[object Object]' === toString.call(x); + } + static is_date(x) { + return '[object Date]' === toString.call(x); + } + static eq_dates(lv, rv) { + return lv.getTime() === rv.getTime(); + } +} +exports.intern = intern; +//# sourceMappingURL=intern.js.map \ No newline at end of file diff --git a/dist/intern.js.map b/dist/intern.js.map new file mode 100644 index 0000000..2672fd8 --- /dev/null +++ b/dist/intern.js.map @@ -0,0 +1 @@ +{"version":3,"file":"intern.js","sourceRoot":"","sources":["../src/intern.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/mem-store.d.ts b/dist/mem-store.d.ts new file mode 100644 index 0000000..4c7239a --- /dev/null +++ b/dist/mem-store.d.ts @@ -0,0 +1,28 @@ +type Options = { + prefix?: string; + idlen?: number; + web?: { + dump: boolean; + }; + generate_id?: any; +}; +declare function mem_store(this: any, options: Options): { + name: string; + tag: any; + exportmap: { + native: any; + }; +}; +declare namespace mem_store { + var preload: (this: any) => { + name: string; + exportmap: { + native: () => void; + }; + }; + var defaults: { + 'entity-id-exists': string; + }; + var intern: typeof import("./intern").intern; +} +export default mem_store; diff --git a/dist/mem-store.js b/dist/mem-store.js new file mode 100644 index 0000000..f889aa9 --- /dev/null +++ b/dist/mem-store.js @@ -0,0 +1,273 @@ +/* Copyright (c) 2010-2022 Richard Rodger and other contributors, MIT License */ +'use strict'; +Object.defineProperty(exports, "__esModule", { value: true }); +// TODO: use `undefined` as no-error value consistently +const intern_1 = require("./intern"); +let internals = { + name: 'mem-store', +}; +function mem_store(options) { + let seneca = this; + let init = seneca.export('entity/init'); + // merge default options with any provided by the caller + options = seneca.util.deepextend({ + prefix: '/mem-store', + idlen: 6, + web: { + dump: false, + }, + // TODO: use seneca.export once it allows for null values + generate_id: seneca.root.private$.exports['entity/generate_id'], + }, options); + // The calling Seneca instance will provide + // a description for us on init(), it will + // be used in the logs + let desc; + // Our super awesome in mem database. Please bear in mind + // that this store is meant for fast prototyping, using + // it for production is not advised! + let entmap = {}; + // Define the store using a description object. + // This is a convenience provided by seneca.store.init function. + let store = { + // The name of the plugin, this is what is the name you would + // use in seneca.use(), eg seneca.use('mem-store'). + name: internals.name, + save: function (msg, reply) { + // Take a reference to Seneca + // and the entity to save + let seneca = this; + let ent = msg.ent; + // create our cannon and take a copy of + // the zone, base and name, we will use + // this info further down. + let canon = ent.canon$({ object: true }); + let zone = canon.zone; + let base = canon.base; + let name = canon.name; + // check if we are in create mode, + // if we are do a create, otherwise + // we will do a save instead + // + const is_new = intern_1.intern.is_new(ent); + return is_new ? do_create() : do_save(); + // The actual save logic for saving or + // creating and then saving the entity. + function do_save(id, isnew) { + entmap[base] = entmap[base] || {}; + entmap[base][name] = entmap[base][name] || {}; + // NOTE: It looks like `ent` is stripped of any private fields + // at this point, hence the `ent.data$(true)` does not actually + // leak private fields into saved entities. The line of code in + // the snippet below, for example, does not save the user.psst$ + // field along with the entity: + // + // app.make('user').data$({ psst$: 'private' }).save$() + // + // This can be verified by logging the mement object below. + // + const mement = ent.data$(true, 'string'); + let mement_ptr = null; + let operation = null; + if (intern_1.intern.is_upsert(msg)) { + operation = 'upsert'; + mement_ptr = try_upsert(mement, msg); + } + if (null == mement_ptr) { + operation = intern_1.intern.is_new(msg.ent) ? 'insert' : 'update'; + mement_ptr = complete_save(mement, msg, id, isnew); + } + const result_mement = seneca.util.deep(mement_ptr); + const result_ent = ent.make$(result_mement); + seneca.log.debug('save/' + operation, ent.canon$({ string: 1 }), mement_ptr, desc); + return reply(null, result_ent); + function try_upsert(mement, msg) { + const { q, ent } = msg; + const upsert_on = intern_1.intern.clean_array(q.upsert$); + if (0 < upsert_on.length) { + const has_upsert_fields = upsert_on.every((p) => p in mement); + if (has_upsert_fields) { + const match_by = upsert_on.reduce((h, p) => { + h[p] = mement[p]; + return h; + }, {}); + const updated_ent = intern_1.intern.update_mement(entmap, ent, match_by, mement); + return updated_ent; + } + } + return null; + } + function complete_save(mement, msg, id, isnew) { + const { ent } = msg; + if (null != id) { + mement.id = id; + } + const prev = entmap[base][name][mement.id]; + if (isnew && prev) { + seneca.fail('entity-id-exists', { + type: ent.entity$, + id: mement.id, + }); + return; + } + const should_merge = intern_1.intern.should_merge(ent, options); + if (should_merge) { + mement = Object.assign(prev || {}, mement); + } + entmap[base][name][mement.id] = mement; + return mement; + } + } + // We will still use do_save to save the entity but + // we need a place to handle new entites and id concerns. + function do_create() { + let id; + // Check if we already have an id or if + // we need to generate a new one. + if (null != ent.id$) { + // Take a copy of the existing id and + // delete it from the ent object. Do + // save will handle the id for us. + id = ent.id$; + delete ent.id$; + // Save with the existing id + return do_save(id, true); + } + // Generate a new id + id = options.generate_id ? options.generate_id(ent) : void 0; + if (null == id) { + seneca.fail('generate-invalid-entity-id', { + type: ent.entity$, + id: id, + }); + } + else { + return do_save(id, true); + } + } + }, + load: function (msg, reply) { + let qent = msg.qent; + let q = msg.q || {}; + return intern_1.intern.listents(this, entmap, qent, q, function (err, list) { + let ent = list[0] || null; + this.log.debug('load', q, qent.canon$({ string: 1 }), ent, desc); + reply(err, ent); + }); + }, + list: function (msg, reply) { + let qent = msg.qent; + let q = msg.q || {}; + return intern_1.intern.listents(this, entmap, qent, q, function (err, list) { + this.log.debug('list', q, qent.canon$({ string: 1 }), list.length, list[0], desc); + reply(err, list); + }); + }, + remove: function (msg, reply) { + let seneca = this; + let qent = msg.qent; + let q = msg.q || {}; + let all = q.all$; + // default false + let load = q.load$ === true; + return intern_1.intern.listents(seneca, entmap, qent, q, function (err, list) { + if (err) { + return reply(err); + } + list = list || []; + list = all ? list : list.slice(0, 1); + list.forEach(function (ent) { + let canon = qent.canon$({ + object: true, + }); + delete entmap[canon.base][canon.name][ent.id]; + seneca.log.debug('remove/' + (all ? 'all' : 'one'), q, qent.canon$({ string: 1 }), ent, desc); + }); + let ent = (!all && load && list[0]) || null; + reply(null, ent); + }); + }, + close: function (_msg, reply) { + this.log.debug('close', desc); + reply(); + }, + // .native() is used to handle calls to the underlying driver. Since + // there is no underlying driver for mem-store we simply return the + // default entityMap object. + native: function (_msg, reply) { + reply(null, entmap); + }, + }; + // Init the store using the seneca instance, merged + // options and the store description object above. + let meta = init(seneca, options, store); + //let meta = seneca.store.init(seneca, options, store) + // int() returns some metadata for us, one of these is the + // description, we'll take a copy of that here. + desc = meta.desc; + seneca.add({ role: store.name, cmd: 'dump' }, function (_msg, reply) { + reply(null, entmap); + }); + seneca.add({ role: store.name, cmd: 'export' }, function (_msg, reply) { + let entjson = JSON.stringify(entmap); + reply(null, { json: entjson }); + }); + // TODO: support direct import of literal objects + seneca.add({ role: store.name, cmd: 'import' }, function (msg, reply) { + let imported = JSON.parse(msg.json); + entmap = msg.merge ? this.util.deepextend(entmap, imported) : imported; + reply(); + }); + // Seneca will call init:plugin-name for us. This makes + // this action a great place to do any setup. + //seneca.add('init:mem-store', function (msg, reply) { + seneca.init(function (reply) { + var _a; + if ((_a = options === null || options === void 0 ? void 0 : options.web) === null || _a === void 0 ? void 0 : _a.dump) { + this.act('role:web', { + use: { + prefix: options.prefix, + pin: { role: 'mem-store', cmd: '*' }, + map: { dump: true }, + }, + default$: {}, + }); + } + return reply(); + }); + // We don't return the store itself, it will self load into Seneca via the + // init() function. Instead we return a simple object with the stores name + // and generated meta tag. + return { + name: store.name, + tag: meta.tag, + exportmap: { + native: entmap, + }, + }; +} +mem_store.preload = function () { + let seneca = this; + let meta = { + name: internals.name, + exportmap: { + native: function () { + seneca.export(internals.name + '/native').apply(this, arguments); + }, + }, + }; + return meta; +}; +mem_store.defaults = { + 'entity-id-exists': 'Entity of type <%=type%> with id = <%=id%> already exists.', +}; +/* NOTE: `intern` serves as a namespace for utility functions used by + * the mem store. + */ +mem_store.intern = intern_1.intern; +Object.defineProperty(mem_store, 'name', { value: 'mem-store' }); +exports.default = mem_store; +if ('undefined' !== typeof module) { + module.exports = mem_store; +} +//# sourceMappingURL=mem-store.js.map \ No newline at end of file diff --git a/dist/mem-store.js.map b/dist/mem-store.js.map new file mode 100644 index 0000000..3a0b749 --- /dev/null +++ b/dist/mem-store.js.map @@ -0,0 +1 @@ +{"version":3,"file":"mem-store.js","sourceRoot":"","sources":["../src/mem-store.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..3937de5 --- /dev/null +++ b/package.json @@ -0,0 +1,67 @@ +{ + "name": "seneca-mem-store", + "version": "8.4.0", + "description": "Seneca in-memory data storage plugin.", + "main": "dist/mem-store.js", + "type": "commonjs", + "types": "dist/mem-store.d.ts", + "license": "MIT", + "author": "Richard Rodger (http://richardrodger.com)", + "contributors": [ + "Richard Rodger (http://richardrodger.com)", + "Nicolas Herment (https://github.com/nherment)", + "Dean McDonnell (https://github.com/mcdonnelldean)", + "Maxence Dalmais (https://github.com/maxired)", + "Mihai Dima (https://github.com/mihaidma)", + "Wyatt Preul (https://github.com/geek)", + "Paolo Chiodi (https://github.com/paolochiodi)", + "Shane Lacey (https://github.com/shanel262)", + "Oisín Hennessy (https://github.com/code-jace)", + "Efim Bogdanovsky (https://github.com/lilsweetcaligula)" + ], + "scripts": { + "build": "tsc -d", + "watch": "tsc -w -d", + "test": "lab -v -P test -L -t 90 -r console -o stdout -r html -o test/coverage.html -r lcov -o test/lcov.info --sourcemaps --transform node_modules/lab-transform-typescript", + "test-some": "lab -v -P test --sourcemaps --transform node_modules/lab-transform-typescript -g", + "prettier": "prettier --write --no-semi --single-quote src/**/*.ts test/*.js", + "reset": "npm run clean && npm i && npm run build && npm test", + "clean": "rm -rf node_modules dist package-lock.json yarn.lock", + "repo-tag": "REPO_VERSION=`node -e \"console.log(require('./package').version)\"` && echo TAG: v$REPO_VERSION && git commit -a -m v$REPO_VERSION && git push && git tag v$REPO_VERSION && git push --tags;", + "repo-publish": "npm run clean && npm i --registry=http://registry.npmjs.org && npm run repo-publish-quick", + "repo-publish-quick": "npm run prettier && npm run build && npm test && npm run repo-tag && npm publish --access public --registry=https://registry.npmjs.org" + }, + "repository": { + "type": "git", + "url": "https://github.com/senecajs/seneca-mem-store" + }, + "keywords": [ + "seneca", + "plugin", + "store", + "mem", + "memory" + ], + "peerDependencies": { + "seneca": ">=3", + "seneca-entity": ">=25" + }, + "devDependencies": { + "@hapi/code": "9.0.3", + "@hapi/lab": "25.1.3", + "async": "3.2.5", + "lab-transform-typescript": "3.0.1", + "prettier": "3.1.0", + "seneca-plugin-validator": "0.6.1", + "seneca-promisify": "3.7.0", + "seneca-store-test": "5.2.0", + "typescript": "5.3.2" + }, + "files": [ + "README.md", + "CHANGES.md", + "LICENSE", + "src", + "dist" + ] +} diff --git a/src/intern.ts b/src/intern.ts new file mode 100644 index 0000000..86de683 --- /dev/null +++ b/src/intern.ts @@ -0,0 +1,219 @@ +export class intern { + static is_new(ent: any): boolean { + // NOTE: This function is intended for use by the #save method. This + // function returns true when the entity argument is assumed to not yet + // exist in the store. + // + // In terms of code, if client code looks like so: + // ``` + // seneca.make('product') + // .data$({ label, price }) + // .save$(done) + // ``` + // + // - `is_new` will be invoked from the #save method and return + // true, because the product entity is yet to be saved. + // + // The following client code will cause `is_new` to return false, + // when invoked from the #save method, because the user entity already + // exists: + // ``` + // seneca.make('user') + // .load$(user_id, (err, user) => { + // if (err) return done(err) + // + // return user + // .data$({ email, username }) + // .save$(done) + // }) + // ``` + // + return null != ent && null == ent.id + } + + static is_upsert(msg: any): boolean { + const { ent, q } = msg + return intern.is_new(ent) && q && Array.isArray(q.upsert$) + } + + static find_mement(entmap: any, base_ent: any, filter: any): any { + const { base, name } = base_ent.canon$({ object: true }) + const entset = entmap[base] && entmap[base][name] + + if (null == entset) { + return null + } + + let out = null + + for (const ent_id in entset) { + const mement = entset[ent_id] + + if (matches(mement, filter)) { + out = mement + break + } + } + + return out + + function matches(ent: any, filter: any): boolean { + for (const fp in filter) { + if (fp in ent && filter[fp] === ent[fp]) { + continue + } + + return false + } + + return true + } + } + + static update_mement( + entmap: any, + base_ent: any, + filter: any, + new_attrs: any, + ) { + const ent_to_update = intern.find_mement(entmap, base_ent, filter) + + if (ent_to_update) { + Object.assign(ent_to_update, new_attrs) + return ent_to_update + } + + return null + } + + static should_merge(ent: any, plugin_opts: any): boolean { + return !(false === plugin_opts.merge || false === ent.merge$) + } + + // NOTE: Seneca supports a reasonable set of features + // in terms of listing. This function can handle + // sorting, skiping, limiting and general retrieval. + // + static listents(seneca: any, entmap: any, qent: any, q: any, done: any) { + let list = [] + + let canon = qent.canon$({ object: true }) + let base = canon.base + let name = canon.name + + let entset = entmap[base] ? entmap[base][name] : null + let ent + + if (null != entset && null != q) { + if ('string' == typeof q) { + ent = entset[q] + if (ent) { + list.push(ent) + } + } else if (Array.isArray(q)) { + q.forEach(function (id) { + let ent = entset[id] + if (ent) { + ent = qent.make$(ent) + list.push(ent) + } + }) + } else if ('object' === typeof q) { + let entids = Object.keys(entset) + next_ent: for (let id of entids) { + ent = entset[id] + for (let p in q) { + let qv = q[p] // query val + let ev = ent[p] // ent val + + if (-1 === p.indexOf('$')) { + if (Array.isArray(qv)) { + if (-1 === qv.indexOf(ev)) { + continue next_ent + } + } else if (intern.is_object(qv)) { + // mongo style constraints + if ( + (null != qv.$ne && qv.$ne == ev) || + (null != qv.$gte && qv.$gte > ev) || + (null != qv.$gt && qv.$gt >= ev) || + (null != qv.$lt && qv.$lt <= ev) || + (null != qv.$lte && qv.$lte < ev) || + (null != qv.$in && -1 === qv.$in.indexOf(ev)) || + (null != qv.$nin && -1 !== qv.$nin.indexOf(ev)) || + false + ) { + continue next_ent + } + } else { + if (intern.is_date(qv)) { + if (!(intern.is_date(ev) && intern.eq_dates(qv, ev))) { + continue next_ent + } + } else if (qv !== ev) { + continue next_ent + } + } + } + } + ent = qent.make$(ent) + list.push(ent) + } + } + } + + // Always sort first, this is the 'expected' behaviour. + if (null != q && q.sort$) { + let sf: any + for (sf in q.sort$) { + break + } + + let sd = q.sort$[sf] < 0 ? -1 : 1 + list = list.sort(function (a, b) { + return sd * (a[sf] < b[sf] ? -1 : a[sf] === b[sf] ? 0 : 1) + }) + } + + // Skip before limiting. + if (null != q && q.skip$ && q.skip$ > 0) { + list = list.slice(q.skip$) + } + + // Limited the possibly sorted and skipped list. + if (null != q && q.limit$ && q.limit$ >= 0) { + list = list.slice(0, q.limit$) + } + + // Prune fields + if (null != q && q.fields$) { + for (let i = 0; i < list.length; i++) { + let entfields = list[i].fields$() + for (let j = 0; j < entfields.length; j++) { + if ('id' !== entfields[j] && -1 == q.fields$.indexOf(entfields[j])) { + delete list[i][entfields[j]] + } + } + } + } + + // Return the resulting list to the caller. + done.call(seneca, null, list) + } + + static clean_array(ary: string[]): string[] { + return ary.filter((prop: string) => !prop.includes('$')) + } + + static is_object(x: any): boolean { + return '[object Object]' === toString.call(x) + } + + static is_date(x: any): boolean { + return '[object Date]' === toString.call(x) + } + + static eq_dates(lv: Date, rv: Date): boolean { + return lv.getTime() === rv.getTime() + } +} diff --git a/src/mem-store.ts b/src/mem-store.ts new file mode 100644 index 0000000..3db9d2e --- /dev/null +++ b/src/mem-store.ts @@ -0,0 +1,415 @@ +/* Copyright (c) 2010-2022 Richard Rodger and other contributors, MIT License */ +'use strict' + +// TODO: use `undefined` as no-error value consistently + +import { intern } from './intern' + +let internals = { + name: 'mem-store', +} + +type Options = { + prefix?: string + idlen?: number + web?: { + dump: boolean + } + generate_id?: any +} + +function mem_store(this: any, options: Options) { + let seneca: any = this + + let init = seneca.export('entity/init') + + // merge default options with any provided by the caller + options = seneca.util.deepextend( + { + prefix: '/mem-store', + idlen: 6, + web: { + dump: false, + }, + + // TODO: use seneca.export once it allows for null values + generate_id: seneca.root.private$.exports['entity/generate_id'], + }, + options, + ) + + // The calling Seneca instance will provide + // a description for us on init(), it will + // be used in the logs + let desc: any + + // Our super awesome in mem database. Please bear in mind + // that this store is meant for fast prototyping, using + // it for production is not advised! + let entmap: any = {} + + // Define the store using a description object. + // This is a convenience provided by seneca.store.init function. + let store = { + // The name of the plugin, this is what is the name you would + // use in seneca.use(), eg seneca.use('mem-store'). + name: internals.name, + + save: function (this: any, msg: any, reply: any) { + // Take a reference to Seneca + // and the entity to save + let seneca = this + let ent = msg.ent + + // create our cannon and take a copy of + // the zone, base and name, we will use + // this info further down. + let canon = ent.canon$({ object: true }) + let zone = canon.zone + let base = canon.base + let name = canon.name + + // check if we are in create mode, + // if we are do a create, otherwise + // we will do a save instead + // + const is_new = intern.is_new(ent) + + return is_new ? do_create() : do_save() + + // The actual save logic for saving or + // creating and then saving the entity. + function do_save(id?: any, isnew?: boolean) { + entmap[base] = entmap[base] || {} + entmap[base][name] = entmap[base][name] || {} + + // NOTE: It looks like `ent` is stripped of any private fields + // at this point, hence the `ent.data$(true)` does not actually + // leak private fields into saved entities. The line of code in + // the snippet below, for example, does not save the user.psst$ + // field along with the entity: + // + // app.make('user').data$({ psst$: 'private' }).save$() + // + // This can be verified by logging the mement object below. + // + const mement = ent.data$(true, 'string') + + let mement_ptr: any = null + let operation: string | null = null + + if (intern.is_upsert(msg)) { + operation = 'upsert' + mement_ptr = try_upsert(mement, msg) + } + + if (null == mement_ptr) { + operation = intern.is_new(msg.ent) ? 'insert' : 'update' + mement_ptr = complete_save(mement, msg, id, isnew) + } + + const result_mement = seneca.util.deep(mement_ptr) + const result_ent = ent.make$(result_mement) + + seneca.log.debug( + 'save/' + operation, + ent.canon$({ string: 1 }), + mement_ptr, + desc, + ) + + return reply(null, result_ent) + + function try_upsert(mement: any, msg: any) { + const { q, ent } = msg + const upsert_on = intern.clean_array(q.upsert$) + + if (0 < upsert_on.length) { + const has_upsert_fields = upsert_on.every( + (p: string) => p in mement, + ) + + if (has_upsert_fields) { + const match_by = upsert_on.reduce((h: any, p: string) => { + h[p] = mement[p] + return h + }, {}) + + const updated_ent = intern.update_mement( + entmap, + ent, + match_by, + mement, + ) + + return updated_ent + } + } + + return null + } + + function complete_save( + mement: any, + msg: any, + id?: any, + isnew?: boolean, + ) { + const { ent } = msg + + if (null != id) { + mement.id = id + } + + const prev = entmap[base][name][mement.id] + + if (isnew && prev) { + seneca.fail('entity-id-exists', { + type: ent.entity$, + id: mement.id, + }) + return + } + + const should_merge = intern.should_merge(ent, options) + + if (should_merge) { + mement = Object.assign(prev || {}, mement) + } + + entmap[base][name][mement.id] = mement + + return mement + } + } + + // We will still use do_save to save the entity but + // we need a place to handle new entites and id concerns. + function do_create() { + let id + + // Check if we already have an id or if + // we need to generate a new one. + if (null != ent.id$) { + // Take a copy of the existing id and + // delete it from the ent object. Do + // save will handle the id for us. + id = ent.id$ + delete ent.id$ + + // Save with the existing id + return do_save(id, true) + } + + // Generate a new id + id = options.generate_id ? options.generate_id(ent) : void 0 + + if (null == id) { + seneca.fail('generate-invalid-entity-id', { + type: ent.entity$, + id: id, + }) + } else { + return do_save(id, true) + } + } + }, + + load: function (this: any, msg: any, reply: any) { + let qent = msg.qent + let q = msg.q || {} + + return intern.listents( + this, + entmap, + qent, + q, + function (this: any, err: any, list: any[]) { + let ent = list[0] || null + + this.log.debug('load', q, qent.canon$({ string: 1 }), ent, desc) + + reply(err, ent) + }, + ) + }, + + list: function (msg: any, reply: any) { + let qent = msg.qent + let q = msg.q || {} + + return intern.listents( + this, + entmap, + qent, + q, + function (this: any, err: any, list: any[]) { + this.log.debug( + 'list', + q, + qent.canon$({ string: 1 }), + list.length, + list[0], + desc, + ) + + reply(err, list) + }, + ) + }, + + remove: function (this: any, msg: any, reply: any) { + let seneca = this + let qent = msg.qent + let q = msg.q || {} + let all = q.all$ + + // default false + let load = q.load$ === true + + return intern.listents( + seneca, + entmap, + qent, + q, + function (err: Error, list: any[]) { + if (err) { + return reply(err) + } + + list = list || [] + list = all ? list : list.slice(0, 1) + + list.forEach(function (ent) { + let canon = qent.canon$({ + object: true, + }) + + delete entmap[canon.base][canon.name][ent.id] + + seneca.log.debug( + 'remove/' + (all ? 'all' : 'one'), + q, + qent.canon$({ string: 1 }), + ent, + desc, + ) + }) + + let ent = (!all && load && list[0]) || null + + reply(null, ent) + }, + ) + }, + + close: function (this: any, _msg: any, reply: any) { + this.log.debug('close', desc) + reply() + }, + + // .native() is used to handle calls to the underlying driver. Since + // there is no underlying driver for mem-store we simply return the + // default entityMap object. + native: function (this: any, _msg: any, reply: any) { + reply(null, entmap) + }, + } + + // Init the store using the seneca instance, merged + // options and the store description object above. + let meta = init(seneca, options, store) + //let meta = seneca.store.init(seneca, options, store) + + // int() returns some metadata for us, one of these is the + // description, we'll take a copy of that here. + desc = meta.desc + + seneca.add( + { role: store.name, cmd: 'dump' }, + function (_msg: any, reply: any) { + reply(null, entmap) + }, + ) + + seneca.add( + { role: store.name, cmd: 'export' }, + function (_msg: any, reply: any) { + let entjson = JSON.stringify(entmap) + + reply(null, { json: entjson }) + }, + ) + + // TODO: support direct import of literal objects + seneca.add( + { role: store.name, cmd: 'import' }, + function (this: any, msg: any, reply: any) { + let imported = JSON.parse(msg.json) + entmap = msg.merge ? this.util.deepextend(entmap, imported) : imported + reply() + }, + ) + + // Seneca will call init:plugin-name for us. This makes + // this action a great place to do any setup. + //seneca.add('init:mem-store', function (msg, reply) { + seneca.init(function (this: any, reply: any) { + if (options?.web?.dump) { + this.act('role:web', { + use: { + prefix: options.prefix, + pin: { role: 'mem-store', cmd: '*' }, + map: { dump: true }, + }, + default$: {}, + }) + } + + return reply() + }) + + // We don't return the store itself, it will self load into Seneca via the + // init() function. Instead we return a simple object with the stores name + // and generated meta tag. + return { + name: store.name, + tag: meta.tag, + exportmap: { + native: entmap, + }, + } +} + +mem_store.preload = function (this: any) { + let seneca = this + + let meta = { + name: internals.name, + exportmap: { + native: function () { + seneca.export(internals.name + '/native').apply(this, arguments) + }, + }, + } + + return meta +} + +mem_store.defaults = { + 'entity-id-exists': + 'Entity of type <%=type%> with id = <%=id%> already exists.', +} + +/* NOTE: `intern` serves as a namespace for utility functions used by + * the mem store. + */ +mem_store.intern = intern + +Object.defineProperty(mem_store, 'name', { value: 'mem-store' }) + +export default mem_store + +if ('undefined' !== typeof module) { + module.exports = mem_store +} diff --git a/test/mem.test.js b/test/mem.test.js new file mode 100644 index 0000000..db5c76d --- /dev/null +++ b/test/mem.test.js @@ -0,0 +1,1380 @@ +/* + MIT License, + Copyright (c) 2010-2022, Richard Rodger and other contributors. +*/ + +'use strict' + +const Util = require('util') + +const Assert = require('assert') +const Seneca = require('seneca') +const Shared = require('seneca-store-test') +const MakePluginValidator = require('seneca-plugin-validator') + +const Lab = require('@hapi/lab') +const Code = require('@hapi/code') +const { expect } = Code +const lab = (exports.lab = Lab.script()) +const { describe, before, after } = lab +const it = make_it(lab) + +function makeSenecaForTest(opts = {}) { + const { seneca_opts = {} } = opts + + const seneca = Seneca( + Object.assign( + { + log: 'silent', + default_plugins: { 'mem-store': false }, + }, + seneca_opts, + ), + ) + + const { mem_store_opts = {} } = opts + seneca.use({ name: '..', tag: '1' }, mem_store_opts) + + if ('2.0.0' <= seneca.version) { + seneca.use('entity', { mem_store: false }) + } + + return seneca +} + +const seneca = Seneca({ + log: 'silent', + default_plugins: { 'mem-store': false }, +}) +seneca.use({ name: '..', tag: '1' }) + +const senecaMerge = Seneca({ + log: 'silent', +}) +senecaMerge.use({ name: '..', tag: '1' }, { merge: false }) + +if ('2.0.0' <= seneca.version) { + seneca.use('entity', { mem_store: false }) + senecaMerge.use('entity', { mem_store: false }) +} + +const seneca_test = Seneca() // ({ require }) + .test() + .use('promisify') + .use('entity', { mem_store: false }) +//.use('..') + +const test_opts = { + seneca: seneca_test, + name: 'mem-store', +} + +Shared.test.init(lab, test_opts) +Shared.test.keyvalue(lab, test_opts) + +describe('mem-store', () => { + it('is a plugin', (fin) => { + const validatePlugin = MakePluginValidator(require('..'), module) + + validatePlugin().then(fin).catch(fin) + }) +}) + +describe('mem-store tests', function () { + Shared.basictest({ + seneca: seneca, + senecaMerge: senecaMerge, + script: lab, + }) + + Shared.limitstest({ + seneca: seneca, + script: lab, + }) + + // TODO: does not seem to include ents that are equvalent for sorting + Shared.sorttest({ + seneca: seneca, + script: lab, + }) + + Shared.upserttest({ + seneca: makeSenecaForTest(), + script: lab, + }) + + it('export-native', function (fin) { + Assert.ok( + seneca.export('mem-store$1/native') || + seneca.export('mem-store/1/native'), + ) + fin() + }) + + it('custom-test', function (fin) { + seneca.test(fin) + + var ent = seneca.make('foo', { id$: '0', q: 1 }) + + ent.save$(function (err) { + Assert.ok(null === err) + + seneca.act('role:mem-store, cmd:export', function (err, exported) { + var expected = + '{"undefined":{"foo":{"0":{"entity$":"-/-/foo","q":1,"id":"0"}}}}' + + Assert.ok(null === err) + Assert.equal(exported.json, expected) + + var data = JSON.parse(exported.json) + data['undefined']['foo']['1'] = { entity$: '-/-/foo', val: 2, id: '1' } + + seneca.act( + 'role:mem-store, cmd:import', + { json: JSON.stringify(data) }, + function (err) { + Assert.ok(null === err) + + seneca.make('foo').load$('1', function (err, foo) { + Assert.ok(null === err) + Assert.equal(2, foo.val) + + fin() + }) + }, + ) + }) + }) + }) + + it('import', function (fin) { + seneca.test(fin) + + seneca.act( + 'role:mem-store, cmd:import', + { json: JSON.stringify({ foo: { bar: { aaa: { id: 'aaa', a: 1 } } } }) }, + function (err) { + seneca.make('foo/bar').load$('aaa', function (err, aaa) { + Assert.equal('$-/foo/bar;id=aaa;{a:1}', aaa.toString()) + + seneca.act( + 'role:mem-store, cmd:import, merge:true', + { + json: JSON.stringify({ + foo: { + bar: { + aaa: { id: 'aaa', a: 2 }, + bbb: { id: 'bbb', a: 3 }, + }, + }, + }), + }, + function (err) { + seneca.make('foo/bar').load$('aaa', function (err, aaa) { + Assert.equal('$-/foo/bar;id=aaa;{a:2}', aaa.toString()) + + seneca.make('foo/bar').load$('bbb', function (err, bbb) { + Assert.equal('$-/foo/bar;id=bbb;{a:3}', bbb.toString()) + + seneca.act('role:mem-store, cmd:export', function (err, out) { + Assert.equal( + JSON.stringify({ + foo: { + bar: { + aaa: { id: 'aaa', a: 2 }, + bbb: { id: 'bbb', a: 3 }, + }, + }, + }), + out.json, + ) + fin() + }) + }) + }) + }, + ) + }) + }, + ) + }) + + it('generate_id', function (fin) { + seneca.make$('foo', { a: 1 }).save$(function (err, out) { + if (err) return fin(err) + + Assert(6 === out.id.length) + fin() + }) + }) + + it('fields', function (fin) { + seneca.test(fin) + + var ent = seneca.make('foo', { id$: 'f0', a: 1, b: 2, c: 3 }) + + ent.save$(function (err, foo0) { + foo0.list$({ id: 'f0', fields$: ['a', 'c'] }, function (err, list) { + expect(list[0].toString()).equal('$-/-/foo;id=f0;{a:1,c:3}') + + foo0.load$( + { id: 'f0', fields$: ['a', 'not-a-fields'] }, + function (err, out) { + expect(out.toString()).equal('$-/-/foo;id=f0;{a:1}') + fin() + }, + ) + }) + }) + }) + + it('in-query', function (fin) { + seneca.test(fin) + + seneca.make('zed', { p1: 'a', p2: 10 }).save$() + seneca.make('zed', { p1: 'b', p2: 20 }).save$() + seneca.make('zed', { p1: 'c', p2: 30 }).save$() + seneca.make('zed', { p1: 'a', p2: 40 }).save$() + seneca.ready(function () { + seneca.make('zed').list$({ p1: 'a' }, function (err, list) { + //console.log(err,list) + expect(list.length).equal(2) + + seneca.make('zed').list$({ p1: ['a'] }, function (err, list) { + //console.log(err,list) + expect(list.length).equal(2) + + seneca.make('zed').list$({ p1: ['a', 'b'] }, function (err, list) { + //console.log(err,list) + expect(list.length).equal(3) + fin() + }) + }) + }) + }) + }) + + it('mongo-style-query', function (fin) { + seneca.test(fin) + + seneca.make('mongo', { p1: 'a', p2: 10 }).save$() + seneca.make('mongo', { p1: 'b', p2: 20 }).save$() + seneca.make('mongo', { p1: 'c', p2: 30 }).save$() + seneca.make('mongo', { p1: 'a', p2: 40 }).save$() + + seneca.ready(function () { + let m = seneca.make('mongo') + + m.list$({ p2: { $gte: 20 } }, function (err, list) { + //console.log(err,list) + expect(list.length).equal(3) + + m.list$({ p2: { $gt: 20 } }, function (err, list) { + //console.log(err,list) + expect(list.length).equal(2) + + m.list$({ p2: { $lt: 20 } }, function (err, list) { + //console.log(err,list) + expect(list.length).equal(1) + + m.list$({ p2: { $lte: 20 } }, function (err, list) { + //console.log(err,list) + expect(list.length).equal(2) + + m.list$({ p2: { $ne: 20 } }, function (err, list) { + //console.log(err,list) + expect(list.length).equal(3) + + m.list$({ p1: { $in: ['a', 'b'] } }, function (err, list) { + // console.log(err,list) + expect(list.length).equal(3) + + m.list$({ p1: { $nin: ['a', 'b'] } }, function (err, list) { + // console.log(err,list) + expect(list.length).equal(1) + + // ignore unknown constraints + m.list$( + { p1: { $notaconstraint: 'whatever' } }, + function (err, list) { + // console.log(err,list) + expect(list.length).equal(4) + + fin() + }, + ) + }) + }) + }) + }) + }) + }) + }) + }) + }) + + describe('internal utilities', () => { + const mem_store = seneca.export('mem-store') + const { intern } = mem_store.init + + describe('find_mement', () => { + const ent_base = 'sys' + const ent_name = 'product' + + describe('no such entities exist', () => { + let entmap + + before(async () => { + entmap = {} + }) + + it('cannot match', (fin) => { + const ent = seneca.make('sys', 'product') + const filter = { label: 'lorem ipsum' } + const result = intern.find_mement(entmap, ent, filter) + + expect(result).to.equal(null) + + return fin() + }) + }) + + describe('same entity base, different entity name', () => { + let entmap + + before(async () => { + entmap = { + [ent_base]: { + artist: { + foo: { + id: 'foo', + label: 'lorem ipsum', + }, + }, + }, + } + }) + + it('cannot match', (fin) => { + const ent = seneca.make(ent_base, 'product') + const filter = { label: 'lorem ipsum' } + const result = intern.find_mement(entmap, ent, filter) + + expect(result).to.equal(null) + + return fin() + }) + }) + + describe('filter has more fields than the entity', () => { + let entmap + + before(async () => { + entmap = { + [ent_base]: { + [ent_name]: { + foo: { + id: 'foo', + label: 'lorem ipsum', + }, + }, + }, + } + }) + + it('cannot match', (fin) => { + const ent = seneca.make(ent_base, ent_name) + const filter = { label: 'lorem ipsum', bar: 'baz' } + const result = intern.find_mement(entmap, ent, filter) + + expect(result).to.equal(null) + + return fin() + }) + }) + + describe('some field mismatches', () => { + let entmap + + before(async () => { + entmap = { + [ent_base]: { + [ent_name]: { + foo: { + id: 'foo', + label: 'lorem ipsum', + price: '2.34', + }, + }, + }, + } + }) + + it('cannot match', (fin) => { + const ent = seneca.make(ent_base, ent_name) + const filter = { label: 'lorem ipsum', price: '0.95' } + const result = intern.find_mement(entmap, ent, filter) + + expect(result).to.equal(null) + + return fin() + }) + }) + + describe('all fields and values match', () => { + const some_product = { + id: 'foo', + label: 'lorem ipsum', + price: '2.34', + } + + let entmap + + before(async () => { + entmap = { + sys: { + product: { + foo: some_product, + }, + }, + } + }) + + it('returns the match', (fin) => { + const ent = seneca.make(ent_base, ent_name) + const filter = { label: 'lorem ipsum', price: '2.34' } + const result = intern.find_mement(entmap, ent, filter) + + expect(result).to.equal(some_product) + + return fin() + }) + }) + + describe('when the filter is empty', () => { + const some_product = { + id: 'foo', + label: 'lorem ipsum', + price: '2.34', + } + + let entmap + + before(async () => { + entmap = { + sys: { + product: { + foo: some_product, + }, + }, + } + }) + + it('returns the first document it comes across', (fin) => { + const ent = seneca.make(ent_base, ent_name) + const result = intern.find_mement(entmap, ent, {}) + + expect(result).to.equal(some_product) + + return fin() + }) + }) + }) + + describe('is_new', () => { + describe('passed a null', () => { + it('returns a correct value', (fin) => { + const result = intern.is_new(null) + expect(result).to.equal(false) + + fin() + }) + }) + + describe('passed an entity that has not been saved yet', () => { + let product + + before(() => { + product = seneca.make('product').data$({ label: 'Legions of Rome' }) + }) + + it('returns a correct value', (fin) => { + const result = intern.is_new(product) + expect(result).to.equal(true) + + fin() + }) + }) + + describe('passed an entity that has been saved before', () => { + let product + + before(() => { + return new Promise((resolve, reject) => { + seneca + .make('product') + .data$({ label: 'Legions of Rome' }) + .save$((err, out) => { + if (err) { + return reject(err) + } + + product = out + + return resolve() + }) + }) + }) + + it('returns a correct value', (fin) => { + const result = intern.is_new(product) + expect(result).to.equal(false) + + fin() + }) + }) + + describe('passed a new entity, but also an id arg', () => { + let product + + before(() => { + product = seneca + .make('product') + .data$({ id: 'my_precious', label: 'Legions of Rome' }) + }) + + it('returns a correct value', (fin) => { + const result = intern.is_new(product) + expect(result).to.equal(false) + + fin() + }) + }) + }) + + describe('listents', () => { + describe('when the query argument is a string', () => { + const ent_base = 'sys' + const ent_name = 'product' + + const product_id = 'foobaz' + + const product = { + id: product_id, + label: 'lorem ipsum', + price: '2.34', + } + + describe('when an entity with the same base, name, id exists', () => { + let entmap + + before(async () => { + entmap = { + [ent_base]: { + [ent_name]: { + [product_id]: product, + }, + }, + } + }) + + const product_ent = seneca.make(ent_base, ent_name) + + it('fetches the entity with the matching id', (fin) => { + intern.listents( + seneca, + entmap, + product_ent, + product_id, + (err, out) => { + if (err) { + return fin(err) + } + + expect(out).to.equal([product]) + + return fin() + }, + ) + }) + }) + + describe('entity with the same base, name, but not id exists', () => { + const product_id = 'foobaz' + + const product = { + id: product_id, + label: 'lorem ipsum', + price: '2.34', + } + + let entmap + + before(async () => { + entmap = { + [ent_base]: { + [ent_name]: { + [product_id]: product, + }, + }, + } + }) + + const product_ent = seneca.make(ent_base, ent_name) + + it('cannot match the entity', (fin) => { + intern.listents(seneca, entmap, product_ent, 'quix', (err, out) => { + if (err) { + return fin(err) + } + + expect(out).to.equal([]) + + return fin() + }) + }) + }) + }) + + describe('when the query argument is an array', () => { + const ent_base = 'sys' + const ent_name = 'product' + + const product_id = 'foobaz' + + const product = { + id: product_id, + label: 'lorem ipsum', + price: '2.34', + } + + describe('when an item in the array is null', () => { + let entmap + + before(async () => { + entmap = { + [ent_base]: { + [ent_name]: { + [product_id]: product, + }, + }, + } + }) + + const product_ent = seneca.make(ent_base, ent_name) + + it('ignores the null', (fin) => { + intern.listents(seneca, entmap, product_ent, [null], (err, out) => { + if (err) { + return fin(err) + } + + expect(out).to.equal([]) + + return fin() + }) + }) + }) + }) + + describe('when the query argument is null', () => { + const product_id = 'aaaa' + + const product = { + id: product_id, + label: 'lorem ipsum', + price: '2.34', + } + + const ent_base = 'sys' + const ent_name = 'product' + + let entmap + + before(async () => { + entmap = { + [ent_base]: { + [ent_name]: { + [product_id]: product, + }, + }, + } + }) + + const product_ent = seneca.make(ent_base, ent_name) + + it('returns an empty list', (fin) => { + intern.listents(seneca, entmap, product_ent, null, (err, out) => { + if (err) { + return fin(err) + } + + expect(out).to.equal([]) + + return fin() + }) + }) + }) + }) + }) +}) + +describe('additional mem-store tests', () => { + describe('#save', () => { + describe('when trying to create the entity with the same id', () => { + const seneca = makeSenecaForTest() + + it('crashes', (fin) => { + seneca.test(fin) + + seneca.fail = function (...args) { + expect(0 < args.length).to.equal(true) + expect(args[0]).to.equal('entity-id-exists') + + return fin() + } + + const my_product_id = 'MyPreciousId' + + seneca + .make('sys', 'product') + .data$({ id: my_product_id, label: 'lorem ipsum' }) + .save$((err, _out) => { + if (err) { + return fin(err) + } + + seneca + .make('sys', 'product') + .data$({ id$: my_product_id, label: 'nauta sagittas portat' }) + .save$((err, _out) => { + if (err) { + return fin(err) + } + + return fin(new Error('Expected an error to be thrown')) + }) + }) + }) + }) + + describe('when data.id$ is null', () => { + const seneca = makeSenecaForTest() + + before(() => { + return seneca + .make('sys', 'product') + .data$({ id$: null, label: 'lorem ipsum' }) + .save$() + }) + + it('generates an id and creates a new entity', (fin) => { + seneca.test(fin) + + seneca.make('sys', 'product').load$(null, (err, out) => { + if (err) { + return fin(err) + } + + expect(out).to.be.null() + + return seneca.make('sys', 'product').list$((err, products) => { + if (err) { + return fin(err) + } + + expect(products.length).to.equal(1) + + expect(typeof products[0].id).to.equal('string') + expect(products[0].label).to.equal('lorem ipsum') + + return fin() + }) + }) + }) + }) + + it('generate_id-null', (fin) => { + const seneca = makeSenecaForTest({ + mem_store_opts: { + generate_id(_ent) { + return null + }, + }, + }).quiet() + + seneca + .make('sys', 'product') + .data$({ label: 'lorem ipsum' }) + .save$(function (err, out) { + expect(err.code).equal('generate-invalid-entity-id') + fin() + }) + }) + + describe('the "entity$" field when saving an entity', () => { + const seneca = makeSenecaForTest() + + it('stores the "entity$" field with each entity', (fin) => { + const product_ent = seneca.entity('default_zone', 'sys', 'product') + + product_ent.data$({}).save$((err, saved_product) => { + if (err) { + return fin(err) + } + + if (null == saved_product) { + return fin(new Error('Expected the product to be saved')) + } + + if (null == saved_product.id) { + return fin(new Error('Expected the saved product to have an id')) + } + + try { + expect(saved_product.entity$).to.equal('default_zone/sys/product') + } catch (err) { + return fin(err) + } + + const { id: product_id } = saved_product + + product_ent.load$(product_id, (err, product) => { + if (err) { + return fin(err) + } + + try { + expect(product.entity$).to.equal('default_zone/sys/product') + } catch (err) { + return fin(err) + } + + return fin() + }) + }) + }) + }) + }) + + describe('#load by date', () => { + const millenium = new Date(2000, 0, 1) + const elvis_bday = new Date(1935, 0, 8) + + let seneca + + async function setupTest() { + seneca = makeSenecaForTest() + + await saveEnt(seneca.make('products').data$({ created_at: elvis_bday })) + await saveEnt(seneca.make('products').data$({ created_at: millenium })) + } + + it('can query by date', async (fin) => { + await setupTest() + + return seneca + .make('products') + .load$({ created_at: makeDateSimilarTo(millenium) }, (err, out) => { + if (err) { + return fin(err) + } + + expect(out).to.contain({ + created_at: millenium, + }) + + return fin() + }) + }) + + it('can query by date', async (fin) => { + await setupTest() + + return seneca + .make('products') + .load$({ created_at: makeDateSimilarTo(elvis_bday) }, (err, out) => { + if (err) { + return fin(err) + } + + expect(out).to.contain({ + created_at: elvis_bday, + }) + + return fin() + }) + }) + + it('fails when trying to compare a date field to anything else', (fin) => { + return seneca.make('products').load$({ created_at: 123 }, (err, out) => { + if (err) { + return fin(err) + } + + expect(out).to.equal(null) + + return fin() + }) + }) + }) + + describe('#list by date', () => { + const millenium = new Date(2000, 0, 1) + const elvis_bday = new Date(1935, 0, 8) + + let seneca + + async function setupTest() { + seneca = makeSenecaForTest() + + await saveEnt(seneca.make('products').data$({ created_at: elvis_bday })) + await saveEnt(seneca.make('products').data$({ created_at: millenium })) + } + + it('can query by date', async (fin) => { + await setupTest() + + return seneca + .make('products') + .list$({ created_at: makeDateSimilarTo(millenium) }, (err, out) => { + if (err) { + return fin(err) + } + + expect(out.length).to.equal(1) + + expect(out[0]).to.contain({ + created_at: millenium, + }) + + return fin() + }) + }) + + it('can query by date', async (fin) => { + await setupTest() + + return seneca + .make('products') + .list$({ created_at: makeDateSimilarTo(elvis_bday) }, (err, out) => { + if (err) { + return fin(err) + } + + expect(out.length).to.equal(1) + + expect(out[0]).to.contain({ + created_at: elvis_bday, + }) + + return fin() + }) + }) + }) + + describe('#remove by date', () => { + const millenium = new Date(2000, 0, 1) + const elvis_bday = new Date(1935, 0, 8) + + let seneca + + async function setupTest() { + seneca = makeSenecaForTest() + + await saveEnt(seneca.make('products').data$({ created_at: elvis_bday })) + await saveEnt(seneca.make('products').data$({ created_at: millenium })) + } + + it('can query by date', async (fin) => { + await setupTest() + + return seneca + .make('products') + .remove$({ created_at: makeDateSimilarTo(millenium) }, (err) => { + if (err) { + return fin(err) + } + + return seneca.make('products').list$({}, (err, out) => { + expect(out.length).to.equal(1) + + expect(out[0]).to.contain({ + created_at: elvis_bday, + }) + + return fin() + }) + }) + }) + + it('can query by date', async (fin) => { + await setupTest() + + return seneca + .make('products') + .remove$({ created_at: makeDateSimilarTo(elvis_bday) }, (err, out) => { + if (err) { + return fin(err) + } + + return seneca.make('products').list$({}, (err, out) => { + expect(out.length).to.equal(1) + + expect(out[0]).to.contain({ + created_at: millenium, + }) + + return fin() + }) + }) + }) + }) + + describe('logging', () => { + describe('#save', () => { + const all_logs = [] + + before(() => { + bufferSenecaLogsOnStdout(all_logs) + }) + + after(() => { + // NOTE: Restore the stdout here too, in case an uncaught error + // is thrown. + // + restoreStdout() + }) + + let seneca + + before(() => { + seneca = makeSenecaForTestOfLogging() + }) + + testThatLogging('logs the operation', (fin) => { + seneca.make('products').save$((err, product) => { + if (err) { + return fin(err) + } + + try { + expect(product).to.exist() + + const debug_logs = all_logs.filter( + (log) => 'debug' === log.level_name, + ) + + const save_log = debug_logs.find((log) => { + return ( + Array.isArray(log.data) && + 'string' === typeof log.data[0] && + log.data[0].startsWith('save/') + ) + }) + + expect(save_log).to.exist() + expect(save_log.data.length).to.equal(4) + + const [, log_ent_canon, log_mement, log_desc] = save_log.data + + expect(log_ent_canon).to.equal('-/-/products') + + expect(log_mement).to.contain({ + entity$: '-/-/products', + id: product.id, + }) + + expect(log_desc).to.startWith('mem-store') + + return fin() + } catch (err) { + return fin(err) + } + }) + }) + }) + + describe('#load', () => { + const all_logs = [] + + before(() => { + bufferSenecaLogsOnStdout(all_logs) + }) + + after(() => { + // NOTE: Restore the stdout here too, in case an uncaught error + // is thrown. + // + restoreStdout() + }) + + let seneca + + before(() => { + seneca = makeSenecaForTestOfLogging() + }) + + let product_id + + before( + () => + new Promise((resolve, reject) => { + seneca.make('products').save$((err, product) => { + if (err) { + return reject(err) + } + + product_id = product.id + + return resolve() + }) + }), + ) + + testThatLogging('logs the operation', (fin) => { + seneca.make('products').load$(product_id, (err, product) => { + if (err) { + return fin(err) + } + + try { + const debug_logs = all_logs.filter( + (log) => 'debug' === log.level_name, + ) + + const save_log = debug_logs.find((log) => { + return ( + Array.isArray(log.data) && + 'string' === typeof log.data[0] && + 'load' === log.data[0] + ) + }) + + expect(save_log).to.exist() + expect(save_log.data.length).to.equal(5) + + const [, , log_ent_canon, log_ent, log_desc] = save_log.data + + expect(log_ent_canon).to.equal('-/-/products') + + expect(log_ent).to.contain({ + entity$: '-/-/products', + id: product_id, + }) + + expect(log_desc).to.startWith('mem-store') + + return fin() + } catch (err) { + return fin(err) + } + }) + }) + }) + + describe('#remove', () => { + const all_logs = [] + + before(() => { + bufferSenecaLogsOnStdout(all_logs) + }) + + after(() => { + // NOTE: Restore the stdout here too, in case an uncaught error + // is thrown. + // + restoreStdout() + }) + + let seneca + + before(() => { + seneca = makeSenecaForTestOfLogging() + }) + + let product_id + + before( + () => + new Promise((resolve, reject) => { + seneca.make('products').save$((err, product) => { + if (err) { + return reject(err) + } + + product_id = product.id + + return resolve() + }) + }), + ) + + testThatLogging('logs the operation', (fin) => { + seneca.make('products').remove$(product_id, (err, product) => { + if (err) { + return fin(err) + } + + try { + const debug_logs = all_logs.filter( + (log) => 'debug' === log.level_name, + ) + + const save_log = debug_logs.find((log) => { + return ( + Array.isArray(log.data) && + 'string' === typeof log.data[0] && + log.data[0].startsWith('remove/') + ) + }) + + expect(save_log).to.exist() + expect(save_log.data.length).to.equal(5) + + const [, , log_ent_canon, log_ent, log_desc] = save_log.data + + expect(log_ent_canon).to.equal('-/-/products') + + expect(log_ent).to.contain({ + entity$: '-/-/products', + id: product_id, + }) + + expect(log_desc).to.startWith('mem-store') + + return fin() + } catch (err) { + return fin(err) + } + }) + }) + }) + + describe('#list', () => { + const all_logs = [] + + before(() => { + bufferSenecaLogsOnStdout(all_logs) + }) + + after(() => { + // NOTE: Restore the stdout here too, in case an uncaught error + // is thrown. + // + restoreStdout() + }) + + let seneca + + before(() => { + seneca = makeSenecaForTestOfLogging() + }) + + testThatLogging('logs the operation', (fin) => { + seneca.make('products').list$((err, product) => { + if (err) { + return fin(err) + } + + try { + const debug_logs = all_logs.filter( + (log) => 'debug' === log.level_name, + ) + + const save_log = debug_logs.find((log) => { + return ( + Array.isArray(log.data) && + 'string' === typeof log.data[0] && + 'list' === log.data[0] + ) + }) + + expect(save_log).to.exist() + expect(save_log.data.length).to.equal(6) + + const [, , log_ent_canon, log_length, log_first_ent, log_desc] = + save_log.data + + expect(log_ent_canon).to.equal('-/-/products') + expect(log_length).to.equal(0) + expect(log_first_ent).to.be.null() + expect(log_desc).to.startWith('mem-store') + + return fin() + } catch (err) { + return fin(err) + } + }) + }) + }) + + const writeToStdout = process.stdout.write + + function bufferSenecaLogsOnStdout(out_logs) { + process.stdout.write = (out) => { + const log = (() => { + try { + return JSON.parse(out) + } catch (_err) { + return null + } + })() + + const is_log = null != log && 'level_name' in log + + if (is_log) { + out_logs.push(log) + return + } + + return writeToStdout.call(process.stdout, out) + } + } + + function restoreStdout() { + process.stdout.write = writeToStdout + } + + function testThatLogging(desc, test) { + it(desc, (fin) => { + test((err = null) => { + // NOTE: This allows test output to show after the test ends. + // + restoreStdout() + + return fin(err) + }) + }) + } + + function makeSenecaForTestOfLogging() { + return makeSenecaForTest({ seneca_opts: { log: 'debug' } }) + } + }) +}) + +function make_it(lab) { + return function it(name, opts, func) { + if ('function' === typeof opts) { + func = opts + opts = {} + } + + lab.it( + name, + opts, + Util.promisify(function (x, fin) { + func(fin) + }), + ) + } +} + +function saveEnt(ent, save_opts = {}) { + return Util.promisify(ent.save$).call(ent, save_opts) +} + +function makeDateSimilarTo(date) { + return new Date(date) +} diff --git a/test/quick.js b/test/quick.js new file mode 100644 index 0000000..43a9c5c --- /dev/null +++ b/test/quick.js @@ -0,0 +1,29 @@ +const Seneca = require('seneca') + +run() + +async function run() { + const seneca = Seneca() + .test() + .use('promisify') + .use('entity', { mem_store: false }) + .use('..') + + await seneca.ready() + + console.log(seneca.list()) + console.log(seneca.find('sys:entity,cmd:load')) + + let role_load = seneca.find('role:entity,cmd:load') + console.log(role_load) + console.log(role_load.func.toString()) + + const foo1 = await seneca.entity('foo').data$({ x: 1 }).save$() + console.log(foo1) + + const foo2 = await seneca.entity('foo').data$({ x: 2 }).save$() + console.log(foo2) + + const list = await seneca.entity('foo').list$() + console.log(list) +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..67c6dc9 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "isolatedModules": true, + "module": "commonjs", + "noEmitOnError": true, + "outDir":"dist", + "resolveJsonModule": true, + "rootDir": "src", + "sourceMap": true, + "strict": true, + "target": "ES2019" + }, + "exclude": [ + "dist", + "node_modules", + "test" + ] +} + diff --git a/tsfmt.json b/tsfmt.json new file mode 100644 index 0000000..86d198a --- /dev/null +++ b/tsfmt.json @@ -0,0 +1,4 @@ +{ + "indentSize": 2, + "insertSpaceAfterFunctionKeywordForAnonymousFunctions": true +}