refactor: use core supplied mappings

This commit is contained in:
Wieland Schöbl
2021-09-03 15:17:15 +00:00
committed by Rainer Killinger
parent 614a1b1e9b
commit 43a89ec4f2
22 changed files with 1622 additions and 1762 deletions

View File

@@ -16,118 +16,9 @@
import {SCFacet, SCThingType} from '@openstapps/core';
import {expect} from 'chai';
import {parseAggregations} from '../../../src/storage/elasticsearch/aggregations';
import {AggregationResponse, AggregationSchema} from '../../../src/storage/elasticsearch/common';
import {AggregationResponse} from '../../../src/storage/elasticsearch/types/elasticsearch';
describe('Aggregations', function () {
const schema: AggregationSchema = {
'@all': {
aggs: {
type: {
terms: {
field: 'type.raw',
size: 1000
}
}
},
filter: {
match_all: {}
}
},
'academic event': {
aggs: {
'academicTerms.acronym': {
terms: {
field: 'academicTerms.acronym.raw',
size: 1000
}
},
'catalogs.categories': {
terms: {
field: 'catalogs.categories.raw',
size: 1000
}
},
categories: {
terms: {
field: 'categories.raw',
size: 1000
}
},
'creativeWorks.keywords': {
terms: {
field: 'creativeWorks.keywords.raw',
size: 1000
}
},
majors: {
terms: {
field: 'majors.raw',
size: 1000
}
}
},
filter: {
type: {
value: 'academic event'
}
}
},
catalog: {
aggs: {
'academicTerm.acronym': {
terms: {
field: 'academicTerm.acronym.raw',
size: 1000
}
},
categories: {
terms: {
field: 'categories.raw',
size: 1000
}
},
'superCatalog.categories': {
terms: {
field: 'superCatalog.categories.raw',
size: 1000
}
},
'superCatalogs.categories': {
terms: {
field: 'superCatalogs.categories.raw',
size: 1000
}
}
},
filter: {
type: {
value: 'catalog'
}
}
},
person: {
aggs: {
'homeLocations.categories': {
terms: {
field: 'homeLocations.categories.raw',
size: 1000
}
}
},
filter: {
type: {
value: 'person'
}
}
},
fooType: {
terms: {
field: 'foo',
size: 123,
}
}
};
const aggregations: AggregationResponse = {
catalog: {
doc_count: 4,
@@ -262,19 +153,11 @@ describe('Aggregations', function () {
field: 'categories',
onlyOnType: SCThingType.Catalog,
},
{
buckets: [
{
count: 321,
key: 'foo'
}
],
field: 'fooType'
}
// no fooType as it doesn't appear in the aggregation schema
];
it('should parse the aggregations providing the appropriate facets', function () {
const facets = parseAggregations(schema, aggregations);
const facets = parseAggregations(aggregations);
expect(facets).to.be.eql(expectedFacets);
});

View File

@@ -18,14 +18,16 @@ import {
ESAggTypeFilter,
ESNestedAggregation,
ESTermsFilter
} from '@openstapps/core-tools/lib/mappings/aggregation-definitions';
import { expect } from "chai";
} from '@openstapps/es-mapping-generator/src/types/aggregation';
import {expect} from "chai";
import {
BucketAggregation,
isBucketAggregation, isESAggMatchAllFilter, isESNestedAggregation, isESTermsFilter,
isNestedAggregation,
NestedAggregation
} from '../../../src/storage/elasticsearch/common';
isBucketAggregation,
isESTermsFilter,
isESAggMatchAllFilter,
isESNestedAggregation
} from '../../../lib/storage/elasticsearch/types/guards';
import {BucketAggregation, NestedAggregation} from '../../../src/storage/elasticsearch/types/elasticsearch';
describe('Common', function () {
const bucketAggregation: BucketAggregation = {buckets: []};

View File

@@ -15,8 +15,8 @@
*/
import {ApiResponse, Client} from '@elastic/elasticsearch';
import {SCBook, SCBulkResponse, SCConfigFile, SCMessage, SCSearchQuery, SCThings, SCThingType} from '@openstapps/core';
import {instance as book} from '@openstapps/core/test/resources/Book.1.json';
import {instance as message} from '@openstapps/core/test/resources/Message.1.json';
import {instance as book} from '@openstapps/core/test/resources/indexable/Book.1.json';
import {instance as message} from '@openstapps/core/test/resources/indexable/Message.1.json';
import {Logger} from '@openstapps/logger';
import {SMTP} from '@openstapps/logger/lib/smtp';
import {expect, use} from 'chai';
@@ -26,8 +26,8 @@ import mockedEnv from 'mocked-env';
import sinon from 'sinon';
import {configFile} from '../../../src/common';
import {MailQueue} from '../../../src/notification/mail-queue';
import * as aggregations from '../../../src/storage/elasticsearch/aggregations';
import {ElasticsearchObject} from '../../../src/storage/elasticsearch/common';
import {aggregations} from '../../../src/storage/elasticsearch/templating';
import {ElasticsearchObject} from '../../../src/storage/elasticsearch/types/elasticsearch';
import {Elasticsearch} from '../../../src/storage/elasticsearch/elasticsearch';
import * as Monitoring from '../../../src/storage/elasticsearch/monitoring';
import * as query from '../../../src/storage/elasticsearch/query';
@@ -41,7 +41,6 @@ describe('Elasticsearch', function () {
// increase timeout for the suite
this.timeout(DEFAULT_TEST_TIMEOUT);
const sandbox = sinon.createSandbox();
let checkESTemplateStub: sinon.SinonStub = sandbox.stub(templating, 'checkESTemplate');
before(function () {
console.log('before');
@@ -195,465 +194,481 @@ describe('Elasticsearch', function () {
restore();
});
it('should force mapping update if related process env variable is not set', async function () {
const restore = mockedEnv({
'ES_FORCE_MAPPING_UPDATE': undefined,
});
new Elasticsearch(configFile);
expect(checkESTemplateStub.calledWith(false)).to.be.true;
// restore env variables
restore();
});
it('should force mapping update if related process env variable is set', async function () {
const restore = mockedEnv({
'ES_FORCE_MAPPING_UPDATE': 'true',
});
new Elasticsearch(configFile);
expect(checkESTemplateStub.calledWith(true)).to.be.true;
// restore env variables
restore();
});
});
describe('init', async function () {
const sandbox = sinon.createSandbox();
after(function () {
sandbox.restore();
});
it('should complain (throw an error) if monitoring is set but mail queue is undefined', async function () {
const config: SCConfigFile = {
...configFile,
internal: {
...configFile.internal,
monitoring: {
actions: [],
watchers: [],
}
}
};
const es = new Elasticsearch(config);
return expect(es.init()).to.be.rejected;
});
it('should setup the monitoring if there is monitoring is set and mail queue is defined', function () {
const config: SCConfigFile = {
...configFile,
internal: {
...configFile.internal,
monitoring: {
actions: [],
watchers: [],
}
}
};
const monitoringSetUpStub = sandbox.stub(Monitoring, 'setUp');
const es = new Elasticsearch(config, new MailQueue(getTransport(false) as unknown as SMTP));
es.init();
expect(monitoringSetUpStub.called).to.be.true;
});
});
describe('Operations with bundle/index', async function () {
const sandbox = sinon.createSandbox();
let es: Elasticsearch;
const oldIndex = 'stapps_footype_foosource_oldindex';
beforeEach(function () {
es = new Elasticsearch(configFile);
// @ts-ignore
es.client.indices = {
// @ts-ignore
getAlias: () => Promise.resolve({body: [{[oldIndex]: {aliases: {[SCThingType.Book]: {}}}}]}),
// @ts-ignore
putTemplate: () => Promise.resolve({}),
// @ts-ignore
create: () => Promise.resolve({}),
// @ts-ignore
delete: () => Promise.resolve({}),
// @ts-ignore
exists: () => Promise.resolve({}),
// @ts-ignore
refresh: () => Promise.resolve({}),
// @ts-ignore
updateAliases: () => Promise.resolve({})
};
});
afterEach(function () {
sandbox.restore();
});
describe('bulkCreated', async function () {
it('should reject (throw an error) if the connection to elasticsearch is not established', async function () {
return expect(es.bulkCreated(bulk)).to.be.rejectedWith('elasticsearch');
});
it('should reject (throw an error) if the index name is not valid', async function () {
sandbox.stub(Elasticsearch, 'getIndex').returns(`invalid_${getIndex}`);
sandbox.createStubInstance(Client, {});
await es.init();
return expect(es.bulkCreated(bulk)).to.be.rejectedWith('Index');
});
it('should create a new index', async function () {
const index = getIndex();
sandbox.stub(Elasticsearch, 'getIndex').returns(index);
const putTemplateStub = sandbox.stub(templating, 'putTemplate');
const createStub = sandbox.stub(es.client.indices, 'create');
await es.init();
await es.bulkCreated(bulk);
expect(putTemplateStub.called).to.be.true;
expect(createStub.calledWith({index})).to.be.true;
});
});
describe('bulkExpired', async function () {
describe('init', async function () {
const sandbox = sinon.createSandbox();
after(function () {
sandbox.restore();
});
it('should complain (throw an error) if monitoring is set but mail queue is undefined', async function () {
const config: SCConfigFile = {
...configFile,
internal: {
...configFile.internal,
monitoring: {
actions: [],
watchers: [],
}
}
};
const es = new Elasticsearch(config);
return expect(es.init()).to.be.rejected;
});
it('should setup the monitoring if there is monitoring is set and mail queue is defined', function () {
const config: SCConfigFile = {
...configFile,
internal: {
...configFile.internal,
monitoring: {
actions: [],
watchers: [],
}
}
};
const monitoringSetUpStub = sandbox.stub(Monitoring, 'setUp');
const es = new Elasticsearch(config, new MailQueue(getTransport(false) as unknown as SMTP));
es.init();
expect(monitoringSetUpStub.called).to.be.true;
});
});
describe('Operations with bundle/index', async function () {
const sandbox = sinon.createSandbox();
let es: Elasticsearch;
const oldIndex = 'stapps_footype_foosource_oldindex';
beforeEach(function () {
es = new Elasticsearch(configFile);
// @ts-ignore
es.client.indices = {
// @ts-ignore
getAlias: () => Promise.resolve({body: [{[oldIndex]: {aliases: {[SCThingType.Book]: {}}}}]}),
// @ts-ignore
putTemplate: () => Promise.resolve({}),
// @ts-ignore
create: () => Promise.resolve({}),
// @ts-ignore
delete: () => Promise.resolve({}),
// @ts-ignore
exists: () => Promise.resolve({}),
// @ts-ignore
refresh: () => Promise.resolve({}),
// @ts-ignore
updateAliases: () => Promise.resolve({})
};
});
afterEach(function () {
sandbox.restore();
});
it('should cleanup index in case of the expired bulk for bulk whose index is not in use', async function () {
sandbox.stub(Elasticsearch, 'getIndex').returns(getIndex());
const clientDeleteStub = sandbox.stub(es.client.indices, 'delete');
await es.bulkExpired({...bulk, state: 'in progress'});
describe('bulkCreated', async function () {
it('should reject (throw an error) if the connection to elasticsearch is not established', async function () {
return expect(es.bulkCreated(bulk)).to.be.rejectedWith('elasticsearch');
});
expect(clientDeleteStub.called).to.be.true;
it('should reject (throw an error) if the index name is not valid', async function () {
sandbox.stub(Elasticsearch, 'getIndex').returns(`invalid_${getIndex}`);
sandbox.createStubInstance(Client, {});
await es.init();
return expect(es.bulkCreated(bulk)).to.be.rejectedWith('Index');
});
it('should create a new index', async function () {
const index = getIndex();
sandbox.stub(Elasticsearch, 'getIndex').returns(index);
const putTemplateStub = sandbox.stub(templating, 'putTemplate');
const createStub = sandbox.stub(es.client.indices, 'create');
await es.init();
await es.bulkCreated(bulk);
expect(putTemplateStub.called).to.be.true;
expect(createStub.calledWith({index})).to.be.true;
});
});
it('should not cleanup index in case of the expired bulk for bulk whose index is in use', async function () {
sandbox.stub(Elasticsearch, 'getIndex').returns(getIndex());
const clientDeleteStub = sandbox.stub(es.client.indices, 'delete');
describe('bulkExpired', async function () {
const sandbox = sinon.createSandbox();
afterEach(function () {
sandbox.restore();
});
it('should cleanup index in case of the expired bulk for bulk whose index is not in use', async function () {
sandbox.stub(Elasticsearch, 'getIndex').returns(getIndex());
const clientDeleteStub = sandbox.stub(es.client.indices, 'delete');
await es.bulkExpired({...bulk, state: 'done'});
await es.bulkExpired({...bulk, state: 'in progress'});
expect(clientDeleteStub.called).to.be.false;
expect(clientDeleteStub.called).to.be.true;
});
it('should not cleanup index in case of the expired bulk for bulk whose index is in use', async function () {
sandbox.stub(Elasticsearch, 'getIndex').returns(getIndex());
const clientDeleteStub = sandbox.stub(es.client.indices, 'delete');
await es.bulkExpired({...bulk, state: 'done'});
expect(clientDeleteStub.called).to.be.false;
});
});
describe('bulkUpdated', function () {
it('should reject if the connection to elasticsearch is not established', async function () {
return expect(es.bulkUpdated(bulk)).to.be.rejectedWith('elasticsearch');
});
it('should reject if the index name is not valid', async function () {
sandbox.stub(Elasticsearch, 'getIndex').returns(`invalid_${getIndex()}`);
sandbox.createStubInstance(Client, {});
await es.init();
return expect(es.bulkUpdated(bulk)).to.be.rejectedWith('Index');
});
it('should create a new index', async function () {
const index = getIndex();
const expectedRefreshActions = [
{
add: {index: index, alias: SCThingType.Book},
},
{
remove: {index: oldIndex, alias: SCThingType.Book},
}
];
sandbox.stub(Elasticsearch, 'getIndex').returns(index);
sandbox.stub(es, 'aliasMap').value({
[SCThingType.Book]: {
[bulk.source]: oldIndex,
}
});
const refreshStub = sandbox.stub(es.client.indices, 'refresh');
const updateAliasesStub = sandbox.stub(es.client.indices, 'updateAliases');
const deleteStub = sandbox.stub(es.client.indices, 'delete');
sandbox.stub(templating, 'putTemplate');
await es.init();
await es.bulkUpdated(bulk);
expect(refreshStub.calledWith({index})).to.be.true;
expect(updateAliasesStub.calledWith({
body: {
actions: expectedRefreshActions
}
})
).to.be.true;
expect(deleteStub.called).to.be.true;
});
});
});
describe('bulkUpdated', function () {
it('should reject if the connection to elasticsearch is not established', async function () {
return expect(es.bulkUpdated(bulk)).to.be.rejectedWith('elasticsearch');
describe('get', async function () {
let es: Elasticsearch;
const sandbox = sinon.createSandbox();
before(function () {
es = new Elasticsearch(configFile);
});
it('should reject if the index name is not valid', async function () {
sandbox.stub(Elasticsearch, 'getIndex').returns(`invalid_${getIndex()}`);
sandbox.createStubInstance(Client, {});
await es.init();
return expect(es.bulkUpdated(bulk)).to.be.rejectedWith('Index');
afterEach(function () {
sandbox.restore();
});
it('should create a new index', async function () {
it('should reject if object is not found', async function () {
sandbox.stub(es.client, 'search').resolves({body: {hits: {hits: []}}});
return expect(es.get('123')).to.rejectedWith('found');
});
it('should provide the thing if object is found', async function () {
const foundObject: ElasticsearchObject<SCMessage> = {
_id: '',
_index: '',
_score: 0,
_type: '',
_source: message as SCMessage
};
sandbox.stub(es.client, 'search').resolves({body: {hits: {hits: [foundObject]}}});
return expect(await es.get('123')).to.be.eql(message);
});
});
describe('post', async function () {
let es: Elasticsearch;
const sandbox = sinon.createSandbox();
before(function () {
es = new Elasticsearch(configFile);
});
afterEach(function () {
sandbox.restore();
});
it('should not post if the object already exists in an index which will not be rolled over', async function () {
const index = getIndex();
const expectedRefreshActions = [
{
add: {index: index, alias: SCThingType.Book},
const oldIndex = index.replace('foosource', 'barsource');
const object: ElasticsearchObject<SCMessage> = {
_id: '',
_index: oldIndex,
_score: 0,
_type: '',
_source: message as SCMessage
};
sandbox.stub(es.client, 'search').resolves({body: {hits: {hits: [object]}}});
sandbox.stub(Elasticsearch, 'getIndex').returns(index);
return expect(es.post(object._source, bulk)).to.rejectedWith('exist');
});
it('should not reject if the object already exists but in an index which will be rolled over', async function () {
const object: ElasticsearchObject<SCMessage> = {
_id: '',
_index: getIndex(),
_score: 0,
_type: '',
_source: message as SCMessage
};
sandbox.stub(es.client, 'search').resolves({body: {hits: {hits: [object]}}});
// return index name with different generated UID (see getIndex method)
sandbox.stub(Elasticsearch, 'getIndex').returns(getIndex());
return expect(es.post(object._source, bulk)).to.not.rejectedWith('exist');
});
it('should reject if there is an object creation error on the elasticsearch side', async function () {
sandbox.stub(es.client, 'search').resolves({body: {hits: {hits: []}}});
sandbox.stub(es.client, 'create').resolves({body: {created: false}});
return expect(es.post(message as SCMessage, bulk)).to.rejectedWith('creation');
});
it('should create a new object', async function () {
let caughtParam: any;
sandbox.stub(es.client, 'search').resolves({body: {hits: {hits: []}}});
// @ts-ignore
let createStub = sandbox.stub(es.client, 'create').callsFake((param) => {
caughtParam = param;
return Promise.resolve({body: {created: true}});
});
await es.post(message as SCMessage, bulk);
expect(createStub.called).to.be.true;
expect(caughtParam.body).to.be.eql({...message, creation_date: caughtParam.body.creation_date});
});
});
describe('put', async function () {
let es: Elasticsearch;
const sandbox = sinon.createSandbox();
before(function () {
es = new Elasticsearch(configFile);
});
afterEach(function () {
sandbox.restore();
});
it('should reject to put if the object does not already exist', async function () {
const object: ElasticsearchObject<SCMessage> = {
_id: '',
_index: getIndex(),
_score: 0,
_type: '',
_source: message as SCMessage
};
sandbox.stub(es.client, 'search').resolves({body: {hits: {hits: []}}});
return expect(es.put(object._source)).to.rejectedWith('exist');
});
it('should update the object if it already exists', async function () {
let caughtParam: any;
const object: ElasticsearchObject<SCMessage> = {
_id: '',
_index: getIndex(),
_score: 0,
_type: '',
_source: message as SCMessage
};
sandbox.stub(es.client, 'search').resolves({body: {hits: {hits: [object]}}});
// @ts-ignore
const stubUpdate = sandbox.stub(es.client, 'update').callsFake((params) => {
caughtParam = params;
return Promise.resolve({body: {created: true}});
});
await es.put(object._source);
expect(caughtParam.body.doc).to.be.eql(object._source);
});
});
describe('search', async function () {
let es: Elasticsearch;
const sandbox = sinon.createSandbox();
const objectMessage: ElasticsearchObject<SCMessage> = {
_id: '123',
_index: getIndex(),
_score: 0,
_type: '',
_source: message as SCMessage
};
const objectBook: ElasticsearchObject<SCBook> = {
_id: '321',
_index: getIndex(),
_score: 0,
_type: '',
_source: book as SCBook
};
const fakeEsAggregations = {
'@all': {
doc_count: 17,
type: {
doc_count_error_upper_bound: 0,
sum_other_doc_count: 0,
buckets: [
{
key: 'person',
doc_count: 13
},
{
key: 'catalog',
doc_count: 4
}
]
}
}
};
const fakeSearchResponse: Partial<ApiResponse<SearchResponse<SCThings>>> = {
// @ts-ignore
body: {
took: 12,
timed_out: false,
// @ts-ignore
_shards: {},
// @ts-ignore
hits: {
hits: [
objectMessage,
objectBook,
],
total: 123
},
aggregations: fakeEsAggregations
},
headers: {},
// @ts-ignore
meta: {},
// @ts-ignore
statusCode: {},
// @ts-ignore
warnings: {}
};
let searchStub: sinon.SinonStub;
before(function () {
es = new Elasticsearch(configFile);
});
beforeEach(function () {
searchStub = sandbox.stub(es.client, 'search').resolves(fakeSearchResponse);
});
afterEach(function () {
sandbox.restore();
});
it('should provide appropriate data and facets', async function () {
const fakeFacets = [
{
remove: {index: oldIndex, alias: SCThingType.Book},
buckets: [
{
count: 13,
key: 'person',
},
{
count: 4,
key: 'catalog'
}
],
field: 'type',
}
];
sandbox.stub(Elasticsearch, 'getIndex').returns(index);
sandbox.stub(es, 'aliasMap').value({
[SCThingType.Book]: {
[bulk.source]: oldIndex,
}
const {data, facets} = await es.search({});
expect(data).to.be.eql([objectMessage._source, objectBook._source]);
expect(facets).to.be.eql(fakeFacets);
});
it('should provide pagination from params', async function () {
const from = 30;
const {pagination} = await es.search({from});
expect(pagination).to.be.eql({
count: fakeSearchResponse.body!.hits.hits.length,
offset: from,
total: fakeSearchResponse.body!.hits.total
});
const refreshStub = sandbox.stub(es.client.indices, 'refresh');
const updateAliasesStub = sandbox.stub(es.client.indices, 'updateAliases');
const deleteStub = sandbox.stub(es.client.indices, 'delete');
sandbox.stub(templating, 'putTemplate');
await es.init();
});
await es.bulkUpdated(bulk);
it('should have fallback to zero if from is not given through params', async function () {
const {pagination} = await es.search({});
expect(refreshStub.calledWith({index})).to.be.true;
expect(updateAliasesStub.calledWith({
body: {
actions: expectedRefreshActions
expect(pagination.offset).to.be.equal(0);
});
it('should build the search request properly', async function () {
const params: SCSearchQuery = {
query: 'mathematics',
from: 30,
size: 5,
sort: [
{
type: 'ducet',
order: 'desc',
arguments:
{
field: 'name'
}
}
],
filter: {
type: 'value',
arguments: {
field: 'type',
value: SCThingType.AcademicEvent
}
}
})
).to.be.true;
expect(deleteStub.called).to.be.true;
});
});
});
describe('get', async function () {
let es: Elasticsearch;
const sandbox = sinon.createSandbox();
before(function () {
es = new Elasticsearch(configFile);
});
afterEach(function () {
sandbox.restore();
});
it('should reject if object is not found', async function () {
sandbox.stub(es.client, 'search').resolves({body:{hits: { hits: []}}});
return expect(es.get('123')).to.rejectedWith('found');
});
it('should provide the thing if object is found', async function () {
const foundObject: ElasticsearchObject<SCMessage> = {_id: '', _index: '', _score: 0, _type: '', _source: message as SCMessage};
sandbox.stub(es.client, 'search').resolves({body:{hits: { hits: [foundObject]}}});
return expect(await es.get('123')).to.be.eql(message);
});
});
describe('post', async function () {
let es: Elasticsearch;
const sandbox = sinon.createSandbox();
before(function () {
es = new Elasticsearch(configFile);
});
afterEach(function () {
sandbox.restore();
});
it('should not post if the object already exists in an index which will not be rolled over', async function () {
const index = getIndex();
const oldIndex = index.replace('foosource', 'barsource');
const object: ElasticsearchObject<SCMessage> = {_id: '', _index: oldIndex, _score: 0, _type: '', _source: message as SCMessage};
sandbox.stub(es.client, 'search').resolves({body:{hits: { hits: [object]}}});
sandbox.stub(Elasticsearch, 'getIndex').returns(index);
return expect(es.post(object._source, bulk)).to.rejectedWith('exist');
});
it('should not reject if the object already exists but in an index which will be rolled over', async function () {
const object: ElasticsearchObject<SCMessage> = {_id: '', _index: getIndex(), _score: 0, _type: '', _source: message as SCMessage};
sandbox.stub(es.client, 'search').resolves({body:{hits: { hits: [object]}}});
// return index name with different generated UID (see getIndex method)
sandbox.stub(Elasticsearch, 'getIndex').returns(getIndex());
return expect(es.post(object._source, bulk)).to.not.rejectedWith('exist');
});
it('should reject if there is an object creation error on the elasticsearch side', async function () {
sandbox.stub(es.client, 'search').resolves({body:{hits: { hits: []}}});
sandbox.stub(es.client, 'create').resolves({body: {created: false}});
return expect(es.post(message as SCMessage, bulk)).to.rejectedWith('creation');
});
it('should create a new object', async function () {
let caughtParam: any;
sandbox.stub(es.client, 'search').resolves({body:{hits: { hits: []}}});
// @ts-ignore
let createStub = sandbox.stub(es.client, 'create').callsFake((param) => {
caughtParam = param;
return Promise.resolve({body: { created: true }});
});
await es.post(message as SCMessage, bulk);
expect(createStub.called).to.be.true;
expect(caughtParam.body).to.be.eql({...message, creation_date: caughtParam.body.creation_date});
});
});
describe('put', async function () {
let es: Elasticsearch;
const sandbox = sinon.createSandbox();
before(function () {
es = new Elasticsearch(configFile);
});
afterEach(function () {
sandbox.restore();
});
it('should reject to put if the object does not already exist', async function () {
const object: ElasticsearchObject<SCMessage> = {_id: '', _index: getIndex(), _score: 0, _type: '', _source: message as SCMessage};
sandbox.stub(es.client, 'search').resolves({body:{hits: { hits: []}}});
return expect(es.put(object._source)).to.rejectedWith('exist');
});
it('should update the object if it already exists', async function () {
let caughtParam: any;
const object: ElasticsearchObject<SCMessage> = {_id: '', _index: getIndex(), _score: 0, _type: '', _source: message as SCMessage};
sandbox.stub(es.client, 'search').resolves({body:{hits: { hits: [object]}}});
// @ts-ignore
const stubUpdate = sandbox.stub(es.client, 'update').callsFake((params) => {
caughtParam = params;
return Promise.resolve({body: { created: true }});
});
await es.put(object._source);
expect(caughtParam.body.doc).to.be.eql(object._source);
});
});
describe('search', async function () {
let es: Elasticsearch;
const sandbox = sinon.createSandbox();
const objectMessage: ElasticsearchObject<SCMessage> = {_id: '123', _index: getIndex(), _score: 0, _type: '', _source: message as SCMessage};
const objectBook: ElasticsearchObject<SCBook> = {_id: '321', _index: getIndex(), _score: 0, _type: '', _source: book as SCBook};
const fakeEsAggregations = {
'@all': {
doc_count: 17,
type: {
doc_count_error_upper_bound: 0,
sum_other_doc_count: 0,
buckets: [
{
key: 'person',
doc_count: 13
},
{
key: 'catalog',
doc_count: 4
}
]
}
}
};
const fakeSearchResponse: Partial<ApiResponse<SearchResponse<SCThings>>> = {
// @ts-ignore
body: {
took: 12,
timed_out: false,
};
const fakeResponse = {foo: 'bar'};
const fakeBuildSortResponse = [fakeResponse];
// @ts-ignore
_shards: {},
// @ts-ignore
hits: {
hits: [
objectMessage,
objectBook,
],
total: 123
},
aggregations: fakeEsAggregations
},
headers: {},
// @ts-ignore
meta: {},
// @ts-ignore
statusCode: {},
// @ts-ignore
warnings: {}
};
let searchStub: sinon.SinonStub;
before(function () {
es = new Elasticsearch(configFile);
sandbox.stub(query, 'buildQuery').returns(fakeResponse);
// @ts-ignore
sandbox.stub(query, 'buildSort').returns(fakeBuildSortResponse);
await es.search(params);
sandbox.assert
.calledWithMatch(searchStub,
{
body: {
aggs: aggregations,
query: fakeResponse,
sort: fakeBuildSortResponse
},
from: params.from,
index: Elasticsearch.getListOfAllIndices(),
size: params.size,
}
);
});
});
beforeEach(function () {
searchStub = sandbox.stub(es.client, 'search').resolves(fakeSearchResponse);
});
afterEach(function () {
sandbox.restore();
});
it('should provide appropriate data and facets', async function () {
const fakeFacets = [
{
buckets: [
{
count: 1,
'key': 'foo'
},
{
count: 1,
key: 'bar'
}
],
field: 'type',
}
];
const parseAggregationsStub = sandbox.stub(aggregations, 'parseAggregations').returns(fakeFacets);
const {data, facets} = await es.search({});
expect(data).to.be.eql([objectMessage._source, objectBook._source]);
expect(facets).to.be.eql(fakeFacets);
expect(parseAggregationsStub.calledWith(sinon.match.any, fakeEsAggregations)).to.be.true;
});
it('should provide pagination from params', async function () {
const from = 30;
const {pagination} = await es.search({from});
expect(pagination).to.be.eql({
count: fakeSearchResponse.body!.hits.hits.length,
offset: from,
total: fakeSearchResponse.body!.hits.total
});
});
it('should have fallback to zero if from is not given through params', async function () {
const {pagination} = await es.search({});
expect(pagination.offset).to.be.equal(0);
});
it('should build the search request properly', async function () {
const params: SCSearchQuery = {
query: 'mathematics',
from: 30,
size: 5,
sort: [
{
type: 'ducet',
order: 'desc',
arguments:
{
field: 'name'
}
}
],
filter: {
type: 'value',
arguments: {
field: 'type',
value: SCThingType.AcademicEvent
}
}
};
const fakeResponse = {foo: 'bar'};
const fakeBuildSortResponse = [fakeResponse];
// @ts-ignore
sandbox.stub(query, 'buildQuery').returns(fakeResponse);
// @ts-ignore
sandbox.stub(query, 'buildSort').returns(fakeBuildSortResponse);
await es.search(params);
sandbox.assert
.calledWithMatch(searchStub,
{
body: {
aggs: es.aggregationsSchema,
query: fakeResponse,
sort: fakeBuildSortResponse
},
from: params.from,
index: Elasticsearch.getListOfAllIndices(),
size: params.size,
}
);
});
});
});

View File

@@ -29,7 +29,6 @@ import {getTransport} from '../../common';
import { expect } from 'chai';
import sinon from 'sinon';
import cron from 'node-cron';
import * as templating from '../../../src/storage/elasticsearch/templating';
describe('Monitoring', async function () {
const sandbox = sinon.createSandbox();
@@ -51,7 +50,6 @@ describe('Monitoring', async function () {
transport = getTransport(true);
mailQueue = new MailQueue(transport);
cronScheduleStub = sandbox.stub(cron, 'schedule');
sandbox.stub(templating, 'checkESTemplate');
});
afterEach(async function () {
sandbox.restore();

View File

@@ -22,15 +22,19 @@ import {
SCThingType
} from '@openstapps/core';
import {expect} from 'chai';
import {ESDateRangeFilter, ESRangeFilter} from '../../../src/storage/elasticsearch/common';
import {ESNumericRangeFilter} from '../../../src/storage/elasticsearch/common';
import {configFile} from '../../../src/common';
import {
ElasticsearchConfig, ESBooleanFilter, ESGenericSort, ESGeoDistanceFilter,
ESDateRangeFilter,
ESRangeFilter,
ESNumericRangeFilter,
ElasticsearchConfig,
ESBooleanFilter,
ESGenericSort,
ESGeoDistanceFilter,
ESGeoDistanceSort,
ESTermFilter,
ScriptSort
} from '../../../src/storage/elasticsearch/common';
} from '../../../src/storage/elasticsearch/types/elasticsearch';
import {configFile} from '../../../src/common';
import {buildBooleanFilter, buildFilter, buildQuery, buildSort} from '../../../src/storage/elasticsearch/query';
describe('Query', function () {
@@ -366,7 +370,7 @@ describe('Query', function () {
}
});
it('should default to second scope', function() {
it('should default to second scope', function () {
const filter = buildFilter({
type: 'availability',
arguments: {
@@ -384,7 +388,7 @@ describe('Query', function () {
},
};
expect(filter).to.be.eql(expectedFilter);
})
});
it('should add || to dates', function () {
const filter = buildFilter({

View File

@@ -1,201 +0,0 @@
/*
* Copyright (C) 2020 StApps
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {SCThingType} from '@openstapps/core';
import * as mapping from '@openstapps/core-tools/lib/mapping';
import {ElasticsearchTemplateCollection} from '@openstapps/core-tools/lib/mappings/mapping-definitions';
import {Logger} from '@openstapps/logger';
import {AggregationSchema} from '../../../src/storage/elasticsearch/common';
import {checkESTemplate, refreshAllTemplates} from '../../../src/storage/elasticsearch/templating';
import sinon from "sinon";
import * as path from 'path';
import * as common from '@openstapps/core-tools/lib/common';
import {expect} from 'chai';
import fs from 'fs';
import fsExtra from 'fs-extra';
import {Client} from '@elastic/elasticsearch';
describe('templating', function () {
describe('checkESTemplate', function () {
const sandbox = sinon.createSandbox();
let fakeMap: { aggregations: AggregationSchema, errors: string[], mappings: ElasticsearchTemplateCollection };
beforeEach(function () {
fakeMap = {
aggregations: {
'@all': {
aggs: {
type: {
terms: {
field: 'type.raw',
size: 1000
}
}
},
filter: {
match_all: {}
}
},
},
errors: [],
mappings: {
'template_dish': {
mappings: {
dish: {
// @ts-ignore just mock the mapping
foo: 'mapping'
}
},
settings: {
analysis: {
ducet_sort: {
filter: [
'german_phonebook'
],
tokenizer: 'keyword',
type: 'custom'
},
search_german: {
filter: [
'lowercase',
'german_stop',
'german_stemmer'
],
tokenizer: 'stapps_ngram',
type: 'custom'
}
},
max_result_window: 30000,
},
template: 'stapps_dish*'
},
'template_book': {
mappings: {
book: {
// @ts-ignore just mock the mapping
foo: 'mapping'
}
},
settings: {
analysis: {
ducet_sort: {
filter: [
'german_phonebook'
],
tokenizer: 'keyword',
type: 'custom'
},
search_german: {
filter: [
'lowercase',
'german_stop',
'german_stemmer'
],
tokenizer: 'stapps_ngram',
type: 'custom'
}
},
max_result_window: 30000,
},
template: 'stapps_book*'
}
}
}
});
afterEach(function () {
sandbox.restore();
});
it('should write new templates when "force update" is true', async function () {
sandbox.stub(Logger, 'error').resolves();
sandbox.stub(fs, 'existsSync').returns(true);
sandbox.stub(common, 'getProjectReflection');
let caughtData: any = [];
const writeFileSyncStub = sandbox.stub(fs, 'writeFileSync');
sandbox.stub(path, 'resolve').returns('/foo/bar');
sandbox.stub(mapping, 'generateTemplate').returns(fakeMap);
checkESTemplate(true);
expect(writeFileSyncStub.callCount).to.be.gt(0);
for (let i = 0; i < writeFileSyncStub.callCount; i++) {
caughtData.push(writeFileSyncStub.getCall(i).args[1]);
}
expect(caughtData).to.be.eql([
JSON.stringify(fakeMap.mappings['template_dish'], null, 2),
JSON.stringify(fakeMap.mappings['template_book'], null, 2),
JSON.stringify(fakeMap.aggregations),
]);
});
it('should not write new templates when "force update" is false', async function () {
sandbox.stub(Logger, 'error').resolves();
sandbox.stub(fs, 'existsSync').returns(true);
sandbox.stub(common, 'getProjectReflection');
const writeFileSyncStub = sandbox.stub(fs, 'writeFileSync');
sandbox.stub(path, 'resolve').returns('/foo/bar');
sandbox.stub(mapping, 'generateTemplate').returns(fakeMap);
checkESTemplate(false);
expect(writeFileSyncStub.called).to.be.false;
});
it('should terminate if there are errors in the map', async function () {
const processExitStub = sandbox.stub(process, 'exit');
const fakeMapWithErrors = {
...fakeMap,
errors: ['Foo Error']
};
sandbox.stub(Logger, 'error').resolves();
sandbox.stub(fs, 'existsSync').returns(true);
sandbox.stub(common, 'getProjectReflection');
sandbox.stub(fs, 'writeFileSync');
sandbox.stub(path, 'resolve').returns('/foo/bar');
sandbox.stub(mapping, 'generateTemplate').returns(fakeMapWithErrors);
checkESTemplate(true);
expect(processExitStub.called).to.be.true;
});
});
describe('refreshAllTemplates', async function () {
const sandbox = sinon.createSandbox();
const client = {
indices: {
putTemplate: (_template: any) => {
}
}
}
after(function () {
sandbox.restore();
});
it('should put templates for all types', async function () {
const clientPutTemplateStub = sandbox.stub(client.indices, 'putTemplate');
sandbox.stub(fsExtra, 'readFile').resolves(Buffer.from('{"foo": "file content"}', 'utf8'));
await refreshAllTemplates(client as Client);
for (const type of Object.values(SCThingType)) {
sinon.assert.calledWith(clientPutTemplateStub, {
body: {foo: 'file content'},
name: `template_${type.split(' ').join('_')}`
})
}
});
});
});