Commit 75474b49 authored by Daniel Caillibaud's avatar Daniel Caillibaud

Merge branch 'master' into githubMaster

parents 3556f8ed 8d62821f
......@@ -33,8 +33,8 @@ require('./entities')
lassi.component('exemple', ['example-html', 'example-entities', 'example-json'])
// .config(function($cache, $appSettings) {
// var _ = require('lodash');
// $cache.addEngine('', 'memcache', '127.0.0.1:11211');
// var _ = require('lodash');
// $cache.addEngine('', 'memcache', '127.0.0.1:11211');
// })
.bootstrap()
{
"name": "lassi",
"version": "2.4.14",
"version": "2.5.0",
"private": true,
"license": "GPL-3.0",
"main": "./source/Lassi",
......@@ -13,64 +13,47 @@
"prepublish": "npm run snyk-protect"
},
"dependencies": {
"ajv": "^6.1.1",
"ajv-errors": "^1.0.0",
"ajv-i18n": "^3.0.0",
"ajv-keywords": "^3.1.0",
"ajv": "^6.10.0",
"ajv-errors": "^1.0.1",
"ajv-i18n": "^3.4.0",
"ajv-keywords": "^3.4.0",
"an-flow": "git://src.sesamath.net/an-flow#3.0.2",
"an-log": "git://src.sesamath.net/an-log#3.1.6",
"colors": "^1.1.2",
"compression": "^1.7.1",
"connect-redis": "^3.3.2",
"cookie-parser": "^1.4.3",
"colors": "^1.3.3",
"compression": "^1.7.4",
"connect-redis": "^3.4.1",
"cookie-parser": "^1.4.4",
"dustjs-helpers": "^1.7.4",
"dustjs-linkedin": "^2.7.5",
"express": "^4.16.2",
"express-session": "^1.15.6",
"express": "^4.17.1",
"express-session": "^1.16.2",
"lodash": "^4.17.11",
"moment": "^2.19.1",
"mongodb": "^3.1.1",
"moment": "^2.24.0",
"mongodb": "^3.2.7",
"morgan": "^1.9.1",
"progress": "^2.0.3",
"redis": "^2.8.0",
"serve-static": "^1.13.1",
"serve-static": "^1.14.1",
"sesajstools": "git+https://framagit.org/Sesamath/sesajstools.git",
"snyk": "^1.189.0"
},
"devDependencies": {
"chai": "^4.1.2",
"eslint": "^4.15.0",
"eslint-config-standard": "^11.0.0-beta.0",
"eslint-plugin-import": "^2.8.0",
"eslint-plugin-node": "^5.2.1",
"eslint-plugin-promise": "^3.6.0",
"eslint-plugin-standard": "^3.0.1",
"jsdoc": "^3.5.5",
"mocha": "^2.5.3",
"chai": "^4.2.0",
"eslint": "^6.0.0",
"eslint-config-standard": "^12.0.0",
"eslint-plugin-node": "^9.1.0",
"eslint-plugin-promise": "^4.1.1",
"eslint-plugin-standard": "^4.0.0",
"jsdoc": "^3.6.2",
"mocha": "^6.1.4",
"pre-push": "^0.1.1",
"sinon": "^4.0.1",
"sinon-chai": "^2.14.0"
"sinon": "^7.3.2",
"sinon-chai": "^3.3.0"
},
"repository": {
"type": "git",
"url": "git@src.sesamath.net:lassi"
},
"jshintConfig": {
"asi": true,
"strict": false,
"maxlen": 256,
"undef": true,
"unused": true,
"indent": 2,
"loopfunc": false,
"validthis": false,
"jquery": true,
"node": true,
"browser": true,
"globals": {
"lassi": true
}
},
"pre-push": [
"test",
"lint"
......@@ -96,7 +79,10 @@
"no-unused-expressions": "off"
}
}
]
],
"rules": {
"object-curly-spacing": 0
}
},
"snyk": true
}
This diff is collapsed.
......@@ -132,7 +132,6 @@ class Lassi extends EventEmitter {
if (service.postSetup) postSetupable.push(service)
}
// pour mémoriser les services déjà ajoutés
/* global Set */
const added = new Set()
// liste des setup à lancer
const setupables = []
......
......@@ -22,8 +22,6 @@
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
var parse = require('url').parse
var _ = require('lodash')
var Context = require('../Context')
var EventEmitter = require('events').EventEmitter
......@@ -75,7 +73,7 @@ class Controllers extends EventEmitter {
}
// Parsing de l'url
if (!request.parsedUrl) request.parsedUrl = parse(request.url)
if (!request.parsedUrl) request.parsedUrl = new URL(request.url)
// Génération du contexte
var context = new Context(request, response)
......
......@@ -585,6 +585,7 @@ class EntityDefinition {
* @param {simpleCallback} cb
*/
flush (cb) {
if (!this.entities.db) return cb(Error('entities n’a pas été initialisé, flush impossible'))
// Si la collection n'existe pas, getCollection renvoie quand même un objet
// mais "MongoError: ns not found" est renvoyé sur le drop
this.getCollection().drop(function (error) {
......@@ -602,7 +603,7 @@ class EntityDefinition {
* @return {Collection}
*/
getCollection () {
if (!this.entities.db) throw Error('entities n’a pas été initialisé')
if (!this.entities.db) throw Error('entities n’a pas été initialisé, impossible de retourner la collection')
let coll = this.entities.db.collection(this.name)
if (!coll) coll = this.entities.db.createCollection(this.name)
return coll
......
......@@ -260,25 +260,9 @@ function checkIsArray (value) {
* @throws Si _data n'est pas du json valide
*/
function createEntitiesFromDocuments (entityQuery, rows) {
// on veut des objets date à partir de strings qui matchent ce pattern de date.toString()
const dateRegExp = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/
const jsonDateReviver = (key, value) => (typeof value === 'string' && dateRegExp.test(value) && new Date(value)) || value
return rows.map((row) => {
let data = row._data
if (!data) throw new Error(`Données absentes pour ${entityQuery.entity.name}/${row._id}`)
// TODO DATA: enlever ce if-bloc une fois que toutes les entités auront été ré-indexées
// avec un _data en objet mongo plutôt qu'un json
if (typeof data === 'string') {
try {
data = JSON.parse(data, jsonDateReviver)
} catch (error) {
console.error(error, `avec les _data :\n${row._data}`)
// on renvoie un message plus compréhensible
throw new Error(`Données corrompues pour ${entityQuery.entity.name}/${row._id}`)
}
}
data.oid = row._id.toString()
// __deletedAt n'est pas une propriété de _data, c'est un index ajouté seulement quand il existe (par softDelete)
if (row.__deletedAt) data.__deletedAt = row.__deletedAt
......
......@@ -53,11 +53,13 @@ class Entities extends EventEmitter {
* Ferme la connexion ouverte dans initialize
* (reset this.db mais pas this.entities)
*/
close () {
close (next) {
if (this.client) {
this.client.close(false, (error) => {
if (error) console.error(error)
if (next) next(error)
})
// on met ça à null tout de suite, pas très grave si qqun relançait un client alors que close n'avait pas terminé
this.client = null
this.db = null
}
......@@ -85,41 +87,42 @@ class Entities extends EventEmitter {
}
/**
* Initialisation de l'espace de stockage
* Initialisation de l'espace de stockage puis appel de chaque _initialize des entity déjà définies
* @param {simpleCallback} cb
*/
initialize (cb) {
const settings = this.settings.database
const self = this
const {name, host, port, user, password, authSource} = settings
const authMechanism = settings.authMechanism || 'DEFAULT'
// cf http://mongodb.github.io/node-mongodb-native/2.2/api/MongoClient.html#connect pour les options possibles
const options = settings.options || {}
options.useNewUrlParser = true
// pour compatibilité ascendante, poolSize était mis directement dans les settings
if (settings.poolSize && !options.poolSize) {
console.error(`poolsize doit désormais être indiqué dans database.options.poolsize (defaut : ${defaultPoolSize})`)
options.poolSize = settings.poolSize
}
if (!options.poolSize) options.poolSize = defaultPoolSize
// on pourra virer ça dans les prochaines versions de mongodb, avec le driver 3.0 & 3.1
// si on le met pas ça affiche un avertissement
options.useNewUrlParser = true
// construction de l'url de connexion, cf docs.mongodb.org/manual/reference/connection-string/
let url = 'mongodb://'
if (user && password) url += `${encodeURIComponent(user)}:${encodeURIComponent(password)}@`
url += `${host}:${port}/${name}?authMechanism=${authMechanism}`
if (authSource) url += `&authSource=${authSource}`
// on peut connecter
MongoClient.connect(url, options, function (error, mongoClient) {
if (error) return cb(error)
if (self.client) return cb(Error('entities.initialize a déjà été appelé'))
flow().seq(function () {
const settings = self.settings.database
const {name, host, port, user, password, authSource} = settings
const authMechanism = settings.authMechanism || 'DEFAULT'
// cf http://mongodb.github.io/node-mongodb-native/2.2/api/MongoClient.html#connect pour les options possibles
const options = settings.options || {}
options.useNewUrlParser = true
// pour compatibilité ascendante, poolSize était mis directement dans les settings
if (settings.poolSize && !options.poolSize) {
console.error(`poolsize doit désormais être indiqué dans database.options.poolsize (defaut : ${defaultPoolSize})`)
options.poolSize = settings.poolSize
}
if (!options.poolSize) options.poolSize = defaultPoolSize
// on pourra virer ça dans les prochaines versions de mongodb, avec le driver 3.0 & 3.1
// si on le met pas ça affiche un avertissement
options.useNewUrlParser = true
// construction de l'url de connexion, cf docs.mongodb.org/manual/reference/connection-string/
let url = 'mongodb://'
if (user && password) url += `${encodeURIComponent(user)}:${encodeURIComponent(password)}@`
url += `${host}:${port}/${name}?authMechanism=${authMechanism}`
if (authSource) url += `&authSource=${authSource}`
MongoClient.connect(url, options, this)
}).seq(function (mongoClient) {
self.client = mongoClient
self.db = mongoClient.db()
// on passe à l'init de toutes les entities
flow(Object.values(self.entities)).seqEach(function (entityDefinition) {
entityDefinition._initialize(this)
}).done(cb)
})
this(null, Object.values(self.entities))
}).seqEach(function (entityDefinition) {
entityDefinition._initialize(this)
}).empty().done(cb)
}
/**
......
/* eslint-env mocha */
'use strict'
const assert = require('assert')
const expect = require('chai').expect
const {expect} = require('chai')
const flow = require('an-flow')
const moment = require('moment')
const EntitiesCli = require('../source/services/entities-cli')()
......@@ -38,7 +37,7 @@ describe('Test $entities-cli', function () {
}).done(done)
})
after('ferme la connexion', quit)
after('nettoie en sortant', quit)
describe('.purgeDeleted()', () => {
it('Throw Error en cas d’usage incorrect', () => {
......@@ -60,14 +59,14 @@ describe('Test $entities-cli', function () {
flow().seq(function () {
TestEntity.match().onlyDeleted().count(this)
}).seq(function (count) {
assert.equal(count, 3)
expect(count).to.equal(3)
EntitiesCli.commands().purgeDeleted(TestEntity, 13, this)
}).seq(function () {
TestEntity.match().onlyDeleted().sort('i', 'asc').grab(this)
}).seq(function (entities) {
assert.equal(entities.length, 2)
assert.equal(entities[0].i, 1000)
assert.equal(entities[1].i, 1001)
expect(entities.length).to.equal(2)
expect(entities[0].i).to.equal(1000)
expect(entities[1].i).to.equal(1001)
this()
}).done(done)
})
......
......@@ -22,7 +22,7 @@ describe('Entity avec index uniques', function () {
}).catch(done)
})
after('ferme la connexion', quit)
after('nettoie en sortant', quit)
describe('options.unique', () => {
it('empêche d’avoir deux fois la même valeur', (done) => {
......
This diff is collapsed.
/* eslint-env mocha */
'use strict'
const assert = require('assert')
const {expect} = require('chai')
const flow = require('an-flow')
const Entities = require('../source/entities')
const {connectToMongo, quit, setup} = require('./init')
const {initEntities, quit} = require('./init')
const _ = require('lodash')
let entities
let SimpleEntity
/**
* Initialisation des entités
*
* @param {Callback} next
*/
function initEntities (dbSettings, next) {
entities = new Entities({database: dbSettings})
flow().seq(function () {
entities.initialize(this)
}).seq(function () {
SimpleEntity = entities.define('SimpleEntity')
SimpleEntity.flush(this)
}).seq(function () {
SimpleEntity.defineIndex('index1', 'integer')
SimpleEntity.defineIndex('index2', 'string')
SimpleEntity.defineIndex('indexTypeless')
SimpleEntity.defineIndex('indexUnique', {unique: true})
SimpleEntity.defineIndex('indexSparse', {sparse: true})
SimpleEntity.defineIndex('indexUniqueSparse', {unique: true, sparse: true})
SimpleEntity._initialize(this)
}).done(next)
}
describe('Test entities-indexes', function () {
let db
let client
before('Connexion à Mongo et initialisation des entités', function (done) {
// Evite les erreurs de timeout sur une machine lente (ou circleCI)
this.timeout(60000)
let dbSettings
flow().seq(function () {
setup(this)
}).seq(function (TestEntity, _dbSettings) {
dbSettings = _dbSettings
// on laisse tomber le TestEntity pour notre SimpleEntity, mais avant on crée la collection
// si elle n'existe pas pour lui ajouter des index et vérifier qu'ils sont virés ensuite
connectToMongo(this)
}).seq(function (_client) {
client = _client
db = client.db()
db.createCollection('SimpleEntity', this)
initEntities(this)
}).seq(function (_entities) {
entities = _entities
SimpleEntity = entities.define('SimpleEntity')
SimpleEntity.flush(this)
}).seq(function () {
db.createIndex('SimpleEntity', 'indexToDrop', this)
}).seq(function () {
initEntities(dbSettings, this)
}).done(done)
SimpleEntity.defineIndex('index1', 'integer')
SimpleEntity.defineIndex('index2', 'string')
SimpleEntity.defineIndex('indexTypeless')
SimpleEntity.defineIndex('indexUnique', {unique: true})
SimpleEntity.defineIndex('indexSparse', {sparse: true})
SimpleEntity.defineIndex('indexUniqueSparse', {unique: true, sparse: true})
SimpleEntity._initialize(done)
}).catch(done)
})
after('ferme la connexion parallèle (pour check en direct sur mongo)', (done) => {
client.close(done) // notre connexion ouverte dans before
entities.close() // la connexion ouverte par initEntities
quit() // la connexion ouverte par setup
after('Supprime la collection en partant', (done) => {
flow().seq(function () {
if (!SimpleEntity) return this()
SimpleEntity.flush(this)
}).seq(function () {
quit(this)
}).done(done)
})
describe("l'initialisation d'une nouvelle collecion par une Entity - créée dans initEntities", function () {
describe("l'initialisation d'une nouvelle collecion par une Entity - créée dans initSimpleEntity", function () {
it('vire les index non lassi', function (done) {
const db = SimpleEntity.getDb()
const coll = db.collection('SimpleEntity')
coll.listIndexes().toArray((error, indexes) => {
if (error) return done(error)
......@@ -109,7 +82,7 @@ describe('Test entities-indexes', function () {
SimpleEntity.getMongoIndexes(function (error, indexes) {
if (error) return done(error)
const index = _.find(indexes, {name: SimpleEntity.getMongoIndexName('index1')})
assert.equal(index.key.index1, 1)
expect(index.key.index1).to.equal(1)
done()
})
})
......@@ -128,7 +101,7 @@ describe('Test entities-indexes', function () {
SimpleEntity.getMongoIndexes(function (error, indexes) {
if (error) return done(error)
const index = _.find(indexes, {name: SimpleEntity.getMongoIndexName('index1')})
assert.equal(index, undefined)
expect(index).to.equal(undefined)
done()
})
})
......@@ -149,8 +122,8 @@ describe('Test entities-indexes', function () {
if (error) return done(error)
const oldIndex = _.find(indexes, {name: SimpleEntity.getMongoIndexName('index1')})
const newIndex = _.find(indexes, {name: SimpleEntity.getMongoIndexName('anotherIndexedAttribute')})
assert(!!oldIndex)
assert.equal(newIndex.key.anotherIndexedAttribute, 1)
expect(oldIndex).to.be.an('object')
expect(newIndex.key.anotherIndexedAttribute).to.equal(1)
done()
})
})
......@@ -173,10 +146,10 @@ describe('Test entities-indexes', function () {
const oldIndex2 = _.find(indexes, {name: SimpleEntity.getMongoIndexName('index2')})
const newIndex1 = _.find(indexes, {name: SimpleEntity.getMongoIndexName('index1-unique')})
const newIndex2 = _.find(indexes, {name: SimpleEntity.getMongoIndexName('index2-unique-sparse')})
assert(!oldIndex1)
assert(!oldIndex2)
assert(!!newIndex1)
assert(!!newIndex2)
expect(oldIndex1).to.be.undefined
expect(oldIndex2).to.be.undefined
expect(newIndex1).to.be.an('object')
expect(newIndex2).to.be.an('object')
done()
})
......
This diff is collapsed.
......@@ -3,8 +3,7 @@
const expect = require('chai').expect
const flow = require('an-flow')
const Entities = require('../source/entities')
const {quit, setup} = require('./init')
const {initEntities, quit} = require('./init')
let entities
let TestEntity
......@@ -56,21 +55,24 @@ const resetTestEntity = (overrides, done) => {
}
describe('Entity#validateJsonSchema', () => {
before((done) => {
flow()
.seq(function () {
setup(this)
})
.seq(function (Entity, dbSettings) {
entities = new Entities({database: dbSettings})
entities.initialize(done)
})
.catch(done)
before('Connexion à Mongo et initialisation des entités', function (done) {
// Evite les erreurs de timeout sur une machine lente (ou circleCI)
this.timeout(60000)
flow().seq(function () {
initEntities(this)
}).seq(function (_entities) {
entities = _entities
done()
}).catch(done)
})
after(() => {
entities.close()
quit()
after('Supprime la collection en partant', (done) => {
flow().seq(function () {
if (!TestEntity) return this()
TestEntity.flush(this)
}).seq(function () {
quit(this)
}).done(done)
})
describe('rejette un objet invalide', function () {
......
/* eslint-env mocha */
'use strict'
const assert = require('assert')
const {expect} = require('chai')
const flow = require('an-flow')
const Entities = require('../source/entities')
const {quit, setup} = require('./init')
const {initEntities, quit} = require('./init')
let entities
let TestEntity
const testEntities = [
......@@ -21,34 +19,31 @@ const testEntities = [
{ i: 42007, text2: 'baz', type: 'bar' }
]
function initEntities (dbSettings, next) {
entities = new Entities({database: dbSettings})
flow().seq(function () {
entities.initialize(this)
}).seq(function () {
TestEntity = entities.define('TestEntity')
TestEntity.flush(this)
}).seq(function () {
TestEntity.defineIndex('type', 'string')
TestEntity.defineIndex('text1', 'string')
TestEntity.defineTextSearchFields([['text1', 2], 'text2'])
TestEntity._initialize(this)
}).done(next)
}
describe('Test entities-search', function () {
before('Connexion à Mongo et initialisation des entités', function (done) {
this.timeout(60000)
let entities
flow().seq(function () {
setup(this)
}).seq(function (Entity, dbSettings) {
initEntities(dbSettings, this)
initEntities(this)
}).seq(function (_entities) {
entities = _entities
TestEntity = entities.define('TestEntity')
TestEntity.flush(this)
}).seq(function () {
TestEntity.defineIndex('type', 'string')
TestEntity.defineIndex('text1', 'string')
TestEntity.defineTextSearchFields([['text1', 2], 'text2'])
TestEntity._initialize(this)
}).done(done)
})
after(() => {
entities.close()
quit()
after('Supprime la collection en partant', (done) => {
flow().seq(function () {
if (!TestEntity) return this()
TestEntity.flush(this)
}).seq(function () {
quit(this)
}).done(done)
})
describe('.textSearch()', function () {
......@@ -74,24 +69,24 @@ describe('Test entities-search', function () {
it('Fait une recherche sur plusieurs champs (avec respect des poids)', function (done) {
TestEntity.match().textSearch('foo').grab(function (error, results) {
if (error) return done(error)
assert.equal(results.length, 6)
expect(results.length).to.equal(6)
// 42001 arrive premier car il a "foo" dans les champs text1 ET text2
assert.equal(results[0].i, 42001)
expect(results[0].i).to.equal(42001)
// puis 42002 car résultat exact sur text1 > text2
assert.equal(results[1].i, 42002)
expect(results[1].i).to.equal(42002)
// puis 42004 et 42003 / 42006 car text1 > text2 (et 50% mieux que 33% des mots)
assert.equal(results[2].i, 42004)
assert.equal(results[3].i, 42003)
assert.equal(results[4].i, 42006)
expect(results[2].i).to.equal(42004)
expect(results[3].i).to.equal(42003)
expect(results[4].i).to.equal(42006)
// puis 42000
assert.equal(results[5].i, 42000)
expect(results[5].i).to.equal(42000)
done()
})
})
it('Count sur plusieurs champs', function (done) {
TestEntity.match().textSearch('foo').count(function (error, total) {
if (error) return done(error)
assert.equal(total, 6)
expect(total).to.equal(6)
done()
})
})
......@@ -99,13 +94,13 @@ describe('Test entities-search', function () {
it('Fait une recherche sur plusieurs mots', function (done) {
TestEntity.match().textSearch('foo bar').grab(function (error, results) {
if (error) return done(error)
assert.equal(results.length, 7)
expect(results.length).to.equal(7)
// 42004 arrive premier car il a un "foo bar" exact
assert.equal(results[0].i, 42004)
expect(results[0].i).to.equal(42004)
// puis 42004 car "foo bar" en text1
assert.equal(results[1].i, 42003)
expect(results[1].i).to.equal(42003)
// puis 42001 car "foo bar" en text2
assert.equal(results[2].i, 42001)
expect(results[2].i).to.equal(42001)
// et les autres
done()
})
......@@ -114,11 +109,11 @@ describe('Test entities-search', function () {
it('Fait une recherche exacte', function (done) {
TestEntity.match().textSearch('"foo bar"').grab(function (error, results) {
if (error) return done(error)
assert.equal(4, results.length)
assert.equal(results[0].i, 42004)
assert.equal(results[1].i, 42003)
assert.equal(results[2].i, 42001)
assert.equal(results[3].i, 42006)
expect(4).to.equal(results.length)
expect(results[0].i).to.equal(42004)
expect(results[1].i).to.equal(42003)
expect(results[2].i).to.equal(42001)
expect(results[3].i).to.equal(42006)
done()
})
})
......@@ -126,7 +121,7 @@ describe('Test entities-search', function () {
it('Fait une recherche qui ne remonte rien', function (done) {
TestEntity.match().textSearch('rab').grab(function (error, results) {
if (error) return done(error)
assert.equal(0, results.length)
expect(0).to.equal(results.length)
done()
})
})
......@@ -134,9 +129,9 @@ describe('Test entities-search', function () {
it('Fait une recherche text avec un match sur un index non text', function (done) {
TestEntity.match('type').equals('foo').textSearch('foo').grab(function (error, results) {
if (error) return done(error)
assert.equal(2, results.length)
assert.equal(results[0].i, 42002)
assert.equal(results[1].i, 42000)
expect(2).to.equal(results.length)
expect(results[0].i).to.equal(42002)
expect(results[1].i).to.equal(42000)
done()
})
})
......@@ -144,12 +139,12 @@ describe('Test entities-search', function () {
it('Fait une recherche sur plusieurs mots avec un match', function (done) {
TestEntity.match('type').equals('bar').textSearch('foo bar').grab(function (error, results) {
if (error) return done(error)
assert.equal(4, results.length)
expect(4).to.equal(results.length)
// 42004 arrive premier car il a un "foo bar" exact
assert.equal(results[0].i, 42004)
assert.equal(results[1].i, 42001)
assert.equal(results[2].i, 42006)
assert.equal(results[3].i, 42005)
expect(results[0].i).to.equal(42004)