Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
52bd562
Added more properties to search and highlight
JoonLeeNIH Jan 2, 2026
1ed27c7
Added CSV header mapping for new properties
JoonLeeNIH Jan 5, 2026
62def55
Removed highlighting for numerical and date properties
JoonLeeNIH Jan 5, 2026
e241998
Added rollup module to support vitest
JoonLeeNIH Jan 5, 2026
11e6c70
Filter, search, and highlight properties now imported from constants;…
JoonLeeNIH Jan 5, 2026
4a0e530
Revert "Added rollup module to support vitest"
JoonLeeNIH Jan 6, 2026
9e5cced
Added rollup as a dev dependency
JoonLeeNIH Jan 6, 2026
6858b50
Wrote some tests for the dataset query generator
JoonLeeNIH Jan 13, 2026
d9fcdd1
Updated test cases for query generator
JoonLeeNIH Jan 15, 2026
36ebdcb
Added null checks to query generator
JoonLeeNIH Jan 21, 2026
421fc6c
Initial tests for dataset search endpoint
JoonLeeNIH Jan 21, 2026
f58f150
Added tests to check for data and total count in results
JoonLeeNIH Jan 21, 2026
f97a7ff
Added dataset service tests for type
JoonLeeNIH Jan 21, 2026
7f4664c
Update Services/queryGenerator.js
JoonLeeNIH Jan 21, 2026
d852573
Search text is no longer mutated when sanitizing
JoonLeeNIH Jan 26, 2026
7470a3e
Removed unnecessary checks and code from query generator
JoonLeeNIH Jan 26, 2026
f25f9e4
Fixed a wrong parameter for search
JoonLeeNIH Jan 26, 2026
a6eeda4
Fixed usage of nonexistent variable
JoonLeeNIH Jan 26, 2026
efc6c97
Removed redundant checks and unused imports
JoonLeeNIH Jan 26, 2026
45c4552
Preliminary Opensearch mocking for tests
JoonLeeNIH Jan 28, 2026
39ee622
Mocked Opensearch response is copied from a real response.
JoonLeeNIH Feb 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 33 additions & 61 deletions Services/dataset.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,49 @@ const queryGenerator = require('./queryGenerator');
const cacheKeyGenerator = require('./cacheKeyGenerator');
const utils = require('../Utils');

const { DATASET_RETURN_FIELDS } = require('../Utils/datasetFields.js');
const FACET_FILTERS = [
'dataset_source_repo',
'primary_disease',
]

const search = async (searchText, filters, options) => {
let query = null;
let result = {};
searchText = searchText.replace(/[^a-zA-Z0-9]+/g, ' '); // Ignore special characters
let searchableText = utils.getSearchableText(searchText);
let searchableText = null;

// Check searchText type
if (searchText && typeof searchText !== 'string') {
return result;
}

// Check filters type
if (filters && (typeof filters !== 'object' || Array.isArray(filters))) {
return result;
}

// Check options type
if (options && (typeof options !== 'object' || Array.isArray(options))) {
return result;
}

// Format the search text
if (searchText) {
const sanitizedSearchText = searchText.replace(/[^a-zA-Z0-9]+/g, ' '); // Ignore special characters
searchableText = utils.getSearchableText(sanitizedSearchText);
}

query = queryGenerator.getSearchQueryV2(searchableText, filters, options, DATASET_RETURN_FIELDS);

if (query == null) {
return result;
}

if (false && searchableText !== "") {
let aggregationKey = cacheKeyGenerator.getAggregationKey(searchableText);
let aggregation = cache.getValue(aggregationKey);
if (!aggregation) {
let query = queryGenerator.getSearchAggregationQuery(searchText);
let query = queryGenerator.getSearchAggregationQuery(searchableText);
let searchResults = await elasticsearch.searchWithAggregations(config.indexDS, query);
aggregation = searchResults.aggs.myAgg.buckets;
//put in cache for 5 mins
Expand All @@ -31,35 +60,6 @@ const search = async (searchText, filters, options) => {
result.aggs = 'all';
}

const returnFields = [
// 'dataset_uuid',
'dataset_source_repo',
'dataset_title',
'description',
'dataset_source_id',
'dataset_source_url',
'PI_name',
// 'GPA',
'dataset_doc',
'dataset_pmid',
'funding_source',
'release_date',
'limitations_for_reuse',
'assay_method',
'study_type',
'primary_disease',
'participant_count',
'sample_count',
'study_links',
'related_genes',
'related_diseases',
'related_terms',
'dataset_year_enrollment_started',
'dataset_year_enrollment_ended',
'dataset_minimum_age_at_baseline',
'dataset_maximum_age_at_baseline'
];
let query = queryGenerator.getSearchQueryV2(searchText, filters, options, returnFields);
let searchResults = await elasticsearch.searchWithAggregations(config.indexDS, query);
let datasets = searchResults.hits.hits.map((ds) => {
if (ds.inner_hits) {
Expand Down Expand Up @@ -95,35 +95,7 @@ const search = async (searchText, filters, options) => {
};

const export2CSV = async (searchText, filters, options) => {
const returnFields = [
'dataset_uuid',
'dataset_source_repo',
'dataset_title',
'description',
'dataset_source_id',
'dataset_source_url',
'PI_name',
// 'GPA',
'dataset_doc',
'dataset_pmid',
'funding_source',
'release_date',
'limitations_for_reuse',
'assay_method',
'study_type',
'primary_disease',
'participant_count',
'sample_count',
'study_links',
'related_genes',
'related_diseases',
'related_terms',
'dataset_year_enrollment_started',
'dataset_year_enrollment_ended',
'dataset_minimum_age_at_baseline',
'dataset_maximum_age_at_baseline'
];
const query = queryGenerator.getSearchQueryV2(searchText, filters, options, returnFields);
const query = queryGenerator.getSearchQueryV2(searchText, filters, options, DATASET_RETURN_FIELDS);
const searchResults = await elasticsearch.search(config.indexDS, query);
const datasets = searchResults.hits.map((dataset) => dataset._source);

Expand Down
182 changes: 182 additions & 0 deletions Services/dataset.service.test.fixtures.js

Large diffs are not rendered by default.

86 changes: 86 additions & 0 deletions Services/dataset.service.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import {
beforeEach,
describe,
expect,
it,
vi,
} from 'vitest';

// These modules are CommonJS (`require(...)`). Instead of relying on module mocking,
// we `spyOn` the exported functions and replace their implementation, which prevents
// any real OpenSearch calls.
import { createRequire } from "module";
const require = createRequire(import.meta.url);

const elasticsearch = require("../Components/elasticsearch");
const datasetService = require("./dataset.service.js");
import {
inclusiveFilters,
inclusiveOptions,
inclusiveSearchText,
normalFilters,
normalOptions,
normalSearchText,
normalOpensearchResults,
} from './dataset.service.test.fixtures.js';

beforeEach(() => {
vi.restoreAllMocks();

// Default mocked response shape expected by `dataset.service.search()`.
// (It reads `searchResults.hits.hits` and `searchResults.hits.total.value`.)
vi.spyOn(elasticsearch, "searchWithAggregations").mockResolvedValue(normalOpensearchResults);
});

describe('search', () => {
it('should have a "data" key in the results object', async () => {
const result = await datasetService.search(normalSearchText, normalFilters, normalOptions);
expect(elasticsearch.searchWithAggregations).toHaveBeenCalled();
expect(result).toHaveProperty('data');
});

it('should have the correct type for "data" in the results object', async () => {
const result = await datasetService.search(normalSearchText, normalFilters, normalOptions);
expect(Array.isArray(result.data)).toBe(true);
});

it('should have a "total" key in the results object', async () => {
const result = await datasetService.search(normalSearchText, normalFilters, normalOptions);
expect(result).toHaveProperty('total');
});

it('should have the correct type for "total" in the results object', async () => {
const result = await datasetService.search(normalSearchText, normalFilters, normalOptions);
expect(result.total).toBeTypeOf('number');
});

it('should return default results if all parameters are undefined', async () => {
const result = await datasetService.search(undefined, undefined, undefined);
const inclusiveResult = await datasetService.search(inclusiveSearchText, inclusiveFilters, inclusiveOptions);
expect(result).toEqual(inclusiveResult);
});

it('should return default results if all parameters are null', async () => {
const result = await datasetService.search(null, null, null);
const inclusiveResult = await datasetService.search(inclusiveSearchText, inclusiveFilters, inclusiveOptions);
expect(result).toEqual(inclusiveResult);
});

it('should return the same results for null searchText as for empty searchText', async () => {
const resultNull = await datasetService.search(null, normalFilters, normalOptions);
const resultEmpty = await datasetService.search('', normalFilters, normalOptions);
expect(resultNull).toEqual(resultEmpty);
});

it('should return the same results for null filters as for empty filters', async () => {
const resultNull = await datasetService.search(normalSearchText, null, normalOptions);
const resultEmpty = await datasetService.search(normalSearchText, {}, normalOptions);
expect(resultNull).toEqual(resultEmpty);
});

it('should return the same results for null options as for empty options', async () => {
const resultNull = await datasetService.search(normalSearchText, normalFilters, null);
const resultEmpty = await datasetService.search(normalSearchText, normalFilters, {});
expect(resultNull).toEqual(resultEmpty);
});
});
Loading