diff --git a/Services/dataset.service.js b/Services/dataset.service.js index b72ac44..fee79c5 100644 --- a/Services/dataset.service.js +++ b/Services/dataset.service.js @@ -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 @@ -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) { @@ -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); diff --git a/Services/dataset.service.test.fixtures.js b/Services/dataset.service.test.fixtures.js new file mode 100644 index 0000000..c5407ce --- /dev/null +++ b/Services/dataset.service.test.fixtures.js @@ -0,0 +1,182 @@ +/** + * Fixtures for the dataset.service.test.js file + */ + +export const inclusiveSearchText = ''; // Search text that matches all datasets +export const normalSearchText = 'multiple myeloma'; +export const inclusiveFilters = {}; // Filters that match all datasets +export const normalFilters = { + primary_disease: [ + "Melanoma", + "Multiple Cancer Types", + ], + dataset_source_repo: [ + "CEDCD", + "dbGaP", + ], +}; +export const inclusiveOptions = {}; // Options that match all datasets +export const normalOptions = { + pageInfo: { + page: 1, + pageSize: 10, + }, + sort: { + name: "Dataset", + k: "dataset_title_sort", + v: "asc", + }, +}; + +// Copied from the actual Opensearch response received inside the search() function +// Truncated to only 3 results +export const normalOpensearchResults = { + hits: { + total: { + value: 105, + relation: "eq", + }, + max_score: null, + hits: [ + { + _index: "datasets", + _id: "wgKss5sBxNS2-GapIa8v", + _score: null, + _source: { + dataset_maximum_age_at_baseline: null, + related_terms: "Breast Carcinoma In Situ;Carcinoma, Ductal, Breast;Multifactorial Inheritance;LCIS - lobular carcinoma in situ;LCIS, Lobular Carcinoma In Situ;Lobular Carcinoma In Situ", + description: "

We evaluate the validity of repurposing archival tissue specimens for germline genetic studies. We performed lc-WGS and imputed genotypes on 10 pairs of matching blood and tumor tissue and benchmarked the accuracy of genome-wide genotypes, HLA haplotypes, and several polygenic risk scores (PRSs). The reported results indicate the high accuracy of germline genotypes and haplotypes obtained from archival tissue DNA. Using this methodology, we estimate breast cancer PRS in 36 Ductal carcinoma in situ (DCIS) patients and demonstrate its association with breast cancer subsequent event (BCSE).

A description of this work is available in medRxiv: https://www.medrxiv.org/content/10.1101/2022.03.31.22273116v1

", + dataset_year_enrollment_ended: null, + study_links: "", + dataset_source_id: "phs002865", + limitations_for_reuse: "GRU", + funding_source: "", + related_genes: "", + experimental_approaches: null, + dataset_title: "Accurate Genome-Wide Germline DNA Profiling from Decade-Old Archival Tissue Specimens", + dataset_pmid: "", + dataset_doc: "Office of Data Sharing (ODS)", + related_diseases: "Multifactorial Inheritance;Carcinoma, Ductal, Breast;Breast Neoplasms;Breast Carcinoma In Situ;Sequence Analysis;High-Throughput Nucleotide Sequencing;Genotyping Techniques;Prognosis", + PI_name: "Olivier Harismendy", + dataset_source_repo: "dbGaP", + participant_count: 50, + study_type: "Sequencing", + release_date: "2022-08-25", + dataset_minimum_age_at_baseline: null, + primary_disease: "Multiple Cancer Types", + sample_count: 50, + dataset_source_url: "https://www.ncbi.nlm.nih.gov/projects/gap/cgi-bin/study.cgi?study_id=phs002865", + dataset_year_enrollment_started: null, + institute: null, + assay_method: "WGS", + }, + highlight: { + "description.search": [ + "

We evaluate the validity of repurposing archival tissue specimens for germline genetic studies. We performed lc-WGS and imputed genotypes on 10 pairs of matching blood and tumor tissue and benchmarked the accuracy of genome-wide genotypes, HLA haplotypes, and several polygenic risk scores (PRSs). The reported results indicate the high accuracy of germline genotypes and haplotypes obtained from archival tissue DNA. Using this methodology, we estimate breast cancer PRS in 36 Ductal carcinoma in situ (DCIS) patients and demonstrate its association with breast cancer subsequent event (BCSE).

A description of this work is available in medRxiv: https://www.medrxiv.org/content/10.1101/2022.03.31.22273116v1

", + ], + "primary_disease.search": [ + "Multiple Cancer Types", + ], + }, + sort: [ + "accurategenomewidegermlinednaprofilingfromdecadeoldarchivaltissuespecimens", + ], + }, + { + _index: "datasets", + _id: "-AKss5sBxNS2-GapMLLe", + _score: null, + _source: { + dataset_maximum_age_at_baseline: 69, + related_terms: "Buffy Coat and/or Lymphocytes, Feces, Saliva and/or Buccal, Serum and/or Plasma, Tumor Tissue FFPE, Tumor Tissue Fresh/Frozen, Urine", + description: "The Black Women's Health Study (BWHS) is an ongoing follow-up study for cancer and other serious illnesses in U.S. Black women. The study began in 1995 when 59,000 women (median age, 38) from across the U.S. enrolled by completing health questionnaires. The BWHS has successfully followed participants with biennial questionnaires for data on incident disease and medical, reproductive, behavioral, psychosocial, and socioeconomic factors. Follow-up is also conducted through 24 cancer registries and the National Death Index. Participants' addresses have been linked to U.S. census data and to air pollution data. Cancer diagnoses are validated by pathology data from hospitals and cancer registries. A DNA biorepository was established through collection of cheek cell samples from 26,800 participants. Additionally, approximately 13,000 participants have provided blood samples and 900 have provided tumor tissue. The top priority of the BWHS is to conduct research on diseases that disproportionately affect Black Americans, including cancers, stroke, heart disease, type 2 diabetes, chronic kidney disease, lupus, sarcoidosis, and others.", + dataset_year_enrollment_ended: "1995", + study_links: "http://www.bu.edu/bwhs/", + dataset_source_id: "82", + limitations_for_reuse: "", + funding_source: "", + related_genes: "", + experimental_approaches: null, + dataset_title: "A Follow-up Study for Causes of Cancer in Black Women: Black Women's Health Study", + dataset_pmid: "", + dataset_doc: "Division of Cancer Control and Population Sciences (DCCPS)", + related_diseases: "All Other Cancers, Bladder, Bone, Brain, Cervical carcinoma in situ (CIN II/III, CIS, AIS), Cervix (Squamous cell carcinoma, Adenocarcinoma), Colon, Corpus, body of uterus, Ductal carcinoma in situ of breast, Esophagus, Gall bladder and extrahepatic bile ducts, Hodgkin Lymphoma, Invasive Breast Cancer, Kidney and other unspecified urinary organs, Leukemia, Liver and intrahepatic bile ducts, Lung and bronchus, Melanoma (excluding mucosal sites), Myeloma, No Cancer, Non-Hodgkin Lymphoma, Oropharyngeal, Ovary, fallopian tube, broad ligament, Pancreas, Prostate, Rectum and anus, Small intestine, Stomach, Thyroid", + PI_name: "Julie Palmer, ScD, Lynn Rosenberg, ScD", + dataset_source_repo: "CEDCD", + participant_count: 59000, + study_type: "Etiology", + release_date: null, + dataset_minimum_age_at_baseline: 21, + primary_disease: "Multiple Cancer Types", + sample_count: null, + dataset_source_url: "https://cedcd.nci.nih.gov/cohort?id=82", + dataset_year_enrollment_started: "1995", + institute: null, + assay_method: "", + }, + highlight: { + "description.search": [ + "The Black Women's Health Study (BWHS) is an ongoing follow-up study for cancer and other serious illnesses in U.S. Black women. The study began in 1995 when 59,000 women (median age, 38) from across the U.S. enrolled by completing health questionnaires. The BWHS has successfully followed participants with biennial questionnaires for data on incident disease and medical, reproductive, behavioral, psychosocial, and socioeconomic factors. Follow-up is also conducted through 24 cancer registries and the National Death Index. Participants' addresses have been linked to U.S. census data and to air pollution data. Cancer diagnoses are validated by pathology data from hospitals and cancer registries. A DNA biorepository was established through collection of cheek cell samples from 26,800 participants. Additionally, approximately 13,000 participants have provided blood samples and 900 have provided tumor tissue. The top priority of the BWHS is to conduct research on diseases that disproportionately affect Black Americans, including cancers, stroke, heart disease, type 2 diabetes, chronic kidney disease, lupus, sarcoidosis, and others.", + ], + "primary_disease.search": [ + "Multiple Cancer Types", + ], + "related_diseases.search": [ + "All Other Cancers, Bladder, Bone, Brain, Cervical carcinoma in situ (CIN II/III, CIS, AIS), Cervix (Squamous cell carcinoma, Adenocarcinoma), Colon, Corpus, body of uterus, Ductal carcinoma in situ of breast, Esophagus, Gall bladder and extrahepatic bile ducts, Hodgkin Lymphoma, Invasive Breast Cancer, Kidney and other unspecified urinary organs, Leukemia, Liver and intrahepatic bile ducts, Lung and bronchus, Melanoma (excluding mucosal sites), Myeloma, No Cancer, Non-Hodgkin Lymphoma, Oropharyngeal, Ovary, fallopian tube, broad ligament, Pancreas, Prostate, Rectum and anus, Small intestine, Stomach, Thyroid", + ], + }, + sort: [ + "afollowupstudyforcausesofcancerinblackwomenblackwomenshealthstudy", + ], + }, + { + _index: "datasets", + _id: "7QKss5sBxNS2-GapMLLe", + _score: null, + _source: { + dataset_maximum_age_at_baseline: 64, + related_terms: "Buffy Coat and/or Lymphocytes, Feces, Saliva and/or Buccal, Serum and/or Plasma, Tumor Tissue FFPE, Tumor Tissue Fresh/Frozen, Urine", + description: "This study explores potential causes of cancer and other diseases among farmers and their families and among commercial pesticide applicators. Current medical research suggests that while agricultural workers are generally healthier than the general U.S. population, they may have higher rates of some cancers, including leukemia, myeloma, non-Hodgkin's lymphoma, and cancers of the lip, stomach, skin, brain, and prostate. Other conditions, like asthma, neurologic disease, and adverse reproductive outcomes may also be related to agricultural exposures. The Agricultural Health Study is designed to identify occupational, lifestyle, and genetic factors that may affect the rate of diseases in farming populations. The Agricultural Health Study began in 1994, and will continue to gather information for a number of years about the health of pesticide applicators and their families, details on occupational practices, and information on lifestyle and diet on a periodic basis. The complete set of questionnaires External Web Site Policy may be viewed. Personal identifying information on participants is kept confidential and used only by research staff. Names are not included in any reports. The study results are reported as statistical summaries only. North Carolina and Iowa were selected for this important study based on a nationwide competition. Both states have strong agricultural sectors with diverse production methods, commodities, and products. Information we learn from these two states will be helpful to farmers throughout the United States and other countries using modern agricultural technologies.", + dataset_year_enrollment_ended: "1997", + study_links: "http://aghealth.nih.gov/", + dataset_source_id: "109", + limitations_for_reuse: "", + funding_source: "", + related_genes: "", + experimental_approaches: null, + dataset_title: "Agricultural Health Study", + dataset_pmid: "", + dataset_doc: "Division of Cancer Control and Population Sciences (DCCPS)", + related_diseases: "All Other Cancers, Bladder, Bone, Brain, Cervix (Squamous cell carcinoma, Adenocarcinoma), Colon, Corpus, body of uterus, Esophagus, Gall bladder and extrahepatic bile ducts, Hodgkin Lymphoma, Invasive Breast Cancer, Kidney and other unspecified urinary organs, Leukemia, Liver and intrahepatic bile ducts, Lung and bronchus, Melanoma (excluding mucosal sites), Myeloma, Non-Hodgkin Lymphoma, Oropharyngeal, Ovary, fallopian tube, broad ligament, Pancreas, Prostate, Rectum and anus, Small intestine, Stomach, Thyroid", + PI_name: "Jonathan Hofmann", + dataset_source_repo: "CEDCD", + participant_count: 89656, + study_type: "Etiology", + release_date: null, + dataset_minimum_age_at_baseline: 30, + primary_disease: "Multiple Cancer Types", + sample_count: null, + dataset_source_url: "https://cedcd.nci.nih.gov/cohort?id=109", + dataset_year_enrollment_started: "1993", + institute: null, + assay_method: "", + }, + highlight: { + "description.search": [ + "This study explores potential causes of cancer and other diseases among farmers and their families and among commercial pesticide applicators. Current medical research suggests that while agricultural workers are generally healthier than the general U.S. population, they may have higher rates of some cancers, including leukemia, myeloma, non-Hodgkin's lymphoma, and cancers of the lip, stomach, skin, brain, and prostate. Other conditions, like asthma, neurologic disease, and adverse reproductive outcomes may also be related to agricultural exposures. The Agricultural Health Study is designed to identify occupational, lifestyle, and genetic factors that may affect the rate of diseases in farming populations. The Agricultural Health Study began in 1994, and will continue to gather information for a number of years about the health of pesticide applicators and their families, details on occupational practices, and information on lifestyle and diet on a periodic basis. The complete set of questionnaires External Web Site Policy may be viewed. Personal identifying information on participants is kept confidential and used only by research staff. Names are not included in any reports. The study results are reported as statistical summaries only. North Carolina and Iowa were selected for this important study based on a nationwide competition. Both states have strong agricultural sectors with diverse production methods, commodities, and products. Information we learn from these two states will be helpful to farmers throughout the United States and other countries using modern agricultural technologies.", + ], + "primary_disease.search": [ + "Multiple Cancer Types", + ], + "related_diseases.search": [ + "All Other Cancers, Bladder, Bone, Brain, Cervix (Squamous cell carcinoma, Adenocarcinoma), Colon, Corpus, body of uterus, Esophagus, Gall bladder and extrahepatic bile ducts, Hodgkin Lymphoma, Invasive Breast Cancer, Kidney and other unspecified urinary organs, Leukemia, Liver and intrahepatic bile ducts, Lung and bronchus, Melanoma (excluding mucosal sites), Myeloma, Non-Hodgkin Lymphoma, Oropharyngeal, Ovary, fallopian tube, broad ligament, Pancreas, Prostate, Rectum and anus, Small intestine, Stomach, Thyroid", + ], + }, + sort: [ + "agriculturalhealthstudy", + ], + }, + ], + }, + aggs: undefined, +}; diff --git a/Services/dataset.service.test.js b/Services/dataset.service.test.js new file mode 100644 index 0000000..4aa0e4e --- /dev/null +++ b/Services/dataset.service.test.js @@ -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); + }); +}); diff --git a/Services/queryGenerator.js b/Services/queryGenerator.js index 5cf3f53..d14ab34 100644 --- a/Services/queryGenerator.js +++ b/Services/queryGenerator.js @@ -1,34 +1,4 @@ -const { query } = require("winston"); -const config = require("../Config"); -const { values } = require("lodash"); -const DATASET_FIELDS = [ - // 'dataset_uuid', - 'dataset_title', - 'description', - // 'dataset_maximum_age_at_baseline', - // 'dataset_minimum_age_at_baseline', - 'dataset_source_id', - 'dataset_source_repo', - 'dataset_source_url', - // 'dataset_year_enrollment_ended', - // 'dataset_year_enrollment_started', - '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', -]; +const { DATASET_SEARCH_FIELDS, DATASET_HIGHLIGHT_FIELDS } = require('../Utils/datasetFields.js'); let queryGenerator = {}; @@ -150,6 +120,15 @@ queryGenerator.getSearchAggregationQuery = (searchText) => { }; queryGenerator.getFiltersClause = (filters) => { + // Handle null or invalid parameter + if ( + !filters || + typeof filters !== 'object' || + Array.isArray(filters) + ) { + return null; + } + // Ignore filters with no values selected const cleanedFilters = Object.fromEntries( Object.entries(filters).filter(([field, values]) => values.length > 0) @@ -169,7 +148,52 @@ queryGenerator.getFiltersClause = (filters) => { return clause; } +queryGenerator.getHighlightClause = () => { + const fieldsMap = DATASET_HIGHLIGHT_FIELDS.reduce((acc, field) => { + acc[field] = { number_of_fragments: 0 }; + return acc; + }, {}); + + return { + pre_tags: [""], + post_tags: [""], + fields: fieldsMap, + }; +}; + +queryGenerator.getSortClause = (options) => { + // Handle null or wrong type + if (!options || typeof options !== 'object' || Array.isArray(options)) { + return null; + } + + // Check whether a sort object exists + if (!options.sort || typeof options.sort !== 'object' || Array.isArray(options.sort)) { + return null; + } + + // Check whether the sort object has a 'k' and 'v' property + if (!options.sort.k || !options.sort.v) { + return null; + } + + // Check whether the sort property and direction are strings + if (typeof options.sort.k !== 'string' || typeof options.sort.v !== 'string') { + return null; + } + + // Return the sort clause + return { + [options.sort.k]: options.sort.v, + }; +}; + queryGenerator.getTextSearchConditions = (searchText) => { + // Handle null parameter + if (!searchText || typeof searchText !== 'string') { + return null; + } + const conditions = []; const searchTerms = searchText.trim().split(' ').map( term => term.trim() @@ -180,12 +204,17 @@ queryGenerator.getTextSearchConditions = (searchText) => { return searchTerms.indexOf(term) === idx; }); + // Check again that actual search terms exist + if (uniqueSearchTerms.length <= 0) { + return null; + } + // Add a search condition for finding each term in any of the dataset fields uniqueSearchTerms.forEach((term) => { const dsl = { 'multi_match': { 'query': term, - 'fields': DATASET_FIELDS.map((field) => `${field}.search`), + 'fields': DATASET_SEARCH_FIELDS, } }; @@ -195,21 +224,69 @@ queryGenerator.getTextSearchConditions = (searchText) => { return conditions; }; +/** + * Constructs a search query for the datasets index + * @param {String} searchText The text to search for + * @param {Object} filters The filters to apply + * @param {Object} options Sort and pagination options + * @param {Array} returnFields The fields to return + * @returns {Object|null} The OpenSearch query body object, or null if validation fails. + */ queryGenerator.getSearchQueryV2 = (searchText, filters, options, returnFields) => { - const body = {}; + const body = { + from: 0, + size: 10, + }; const compoundQuery = { 'bool': { - 'must': [], }, }; - const filtersClause = queryGenerator.getFiltersClause(filters); - const textSearchClause = queryGenerator.getTextSearchConditions(searchText); + let filtersClause; + let sortClause; + let textSearchClause; + + // Check searchText type + // Loose null equality treats undefined as null + if (searchText != null && typeof searchText !== 'string') { + return null; + } + + // Check filters type + if (filters != null && (typeof filters !== 'object' || Array.isArray(filters))) { + return null; + } + + // Check options type + if (options != null && (typeof options !== 'object' || Array.isArray(options))) { + return null; + } + + // Check returnFields type and length + if (!Array.isArray(returnFields) || returnFields.length <= 0) { + return null; + } - body['_source'] = returnFields; + filtersClause = queryGenerator.getFiltersClause(filters); + textSearchClause = queryGenerator.getTextSearchConditions(searchText); + body['_source'] = returnFields ?? false; + + // We already verified that options is the right type if it's not null + // We must still verify that options is truthy if (options) { - body.size = options.pageInfo.pageSize; - body.from = (options.pageInfo.page - 1 ) * options.pageInfo.pageSize; + if (options.pageInfo?.pageSize > 0) { + body.size = options.pageInfo.pageSize; + } + + if (options.pageInfo?.page > 0) { + body.from = body.size * (options.pageInfo.page - 1); + } + } + + sortClause = queryGenerator.getSortClause(options); + + if (sortClause != null) { + body.sort = [sortClause]; } if (filtersClause != null) { @@ -220,57 +297,12 @@ queryGenerator.getSearchQueryV2 = (searchText, filters, options, returnFields) = compoundQuery.bool.must = textSearchClause; } - if (compoundQuery.bool.must.length > 0 || compoundQuery.bool.filter) { + if (compoundQuery.bool.must?.length > 0 || compoundQuery.bool.filter) { body.query = compoundQuery; } - let agg = {}; - agg.myAgg = {}; - agg.myAgg.terms = {}; - agg.myAgg.terms.field = "dbGaP_phs"; - agg.myAgg.terms.size = 1000; - - // body.aggs = agg; - // Add sort parameters - if (options?.sort) { - body.sort = []; // Initialize a list of sort clauses - const sortClause = {}; - sortClause[options.sort.k] = options.sort.v; // In our API, "k" is the property name, and "v" is the direction - body.sort.push(sortClause); - } + body.highlight = queryGenerator.getHighlightClause(); - body.highlight = { - pre_tags: [""], - post_tags: [""], - fields: { - // 'dataset_uuid': { number_of_fragments: 0 }, - 'dataset_title.search': { number_of_fragments: 0 }, - 'description.search': { number_of_fragments: 0 }, - 'dataset_maximum_age_at_baseline.search': { number_of_fragments: 0 }, - 'dataset_minimum_age_at_baseline.search': { number_of_fragments: 0 }, - 'dataset_source_id.search': { number_of_fragments: 0 }, - 'dataset_source_repo.search': { number_of_fragments: 0 }, - 'dataset_source_url.search': { number_of_fragments: 0 }, - 'dataset_year_enrollment_ended.search': { number_of_fragments: 0 }, - 'dataset_year_enrollment_started.search': { number_of_fragments: 0 }, - 'PI_name.search': { number_of_fragments: 0 }, - // 'GPA': { number_of_fragments: 0 }, - 'dataset_doc.search': { number_of_fragments: 0 }, - 'dataset_pmid.search': { number_of_fragments: 0 }, - 'funding_source.search': { number_of_fragments: 0 }, - // 'release_date': { number_of_fragments: 0 }, - 'limitations_for_reuse.search': { number_of_fragments: 0 }, - 'assay_method.search': { number_of_fragments: 0 }, - 'study_type.search': { number_of_fragments: 0 }, - 'primary_disease.search': { number_of_fragments: 0 }, - // 'participant_count': { number_of_fragments: 0 }, - // 'sample_count': { number_of_fragments: 0 }, - 'study_links.search': { number_of_fragments: 0 }, - 'related_genes.search': { number_of_fragments: 0 }, - 'related_diseases.search': { number_of_fragments: 0 }, - 'related_terms.search': { number_of_fragments: 0 }, - }, - }; return body; }; diff --git a/Services/queryGenerator.test.fixtures.js b/Services/queryGenerator.test.fixtures.js new file mode 100644 index 0000000..5b75b81 --- /dev/null +++ b/Services/queryGenerator.test.fixtures.js @@ -0,0 +1,80 @@ +/** + * Fixtures for the queryGenerator.test.js file + */ +const { DATASET_SEARCH_FIELDS } = require('../Utils/datasetFields.js'); +const queryGenerator = require('./queryGenerator.js'); + +// Input parameters +export const normalSearchText = 'multiple myeloma'; +export const normalFilters = { + primary_disease: [ + "Melanoma", + "Multiple Cancer Types", + ], + dataset_source_repo: [ + "CEDCD", + "dbGaP", + ], +}; +export const normalReturnFields = ['dataset_title', 'description']; +export const normalOptions = { + pageInfo: { + page: 1, + pageSize: 10, + }, + sort: { + name: "Dataset", + k: "dataset_title_sort", + v: "asc", + }, +}; + +// Reference queries +export const oSHighlightClause = queryGenerator.getHighlightClause(); +export const normalOSQuery = { + _source: ['dataset_title', 'description'], + size: 10, + from: 0, + query: { + bool: { + must: [ + { + multi_match: { + query: "multiple", + fields: DATASET_SEARCH_FIELDS, + }, + }, + { + multi_match: { + query: "myeloma", + fields: DATASET_SEARCH_FIELDS, + }, + }, + ], + filter: [ + { + terms: { + primary_disease: [ + "Melanoma", + "Multiple Cancer Types", + ], + }, + }, + { + terms: { + dataset_source_repo: [ + "CEDCD", + "dbGaP", + ], + }, + }, + ], + }, + }, + sort: [ + { + dataset_title_sort: "asc", + }, + ], + highlight: oSHighlightClause, +}; diff --git a/Services/queryGenerator.test.js b/Services/queryGenerator.test.js new file mode 100644 index 0000000..9d5fd2f --- /dev/null +++ b/Services/queryGenerator.test.js @@ -0,0 +1,136 @@ +import { describe, it, expect } from 'vitest'; +const queryGenerator = require('./queryGenerator.js'); +const { DATASET_SEARCH_FIELDS } = require('../Utils/datasetFields.js'); +import { normalSearchText, normalFilters, normalReturnFields, normalOptions } from './queryGenerator.test.fixtures.js'; + +// Opensearch +import { normalOSQuery, oSHighlightClause } from './queryGenerator.test.fixtures.js'; + +describe('getSearchQueryV2', () => { + it('should handle undefined parameters', () => { // 1 + // Should be null, mainly because returnFields is undefined + const result = queryGenerator.getSearchQueryV2(undefined, undefined, undefined, undefined); + expect(result).toBeNull(); + + // Undefined searchText is ok + const querySearchText = JSON.parse(JSON.stringify(normalOSQuery)); + delete querySearchText.query.bool.must; + const resultSearchText = queryGenerator.getSearchQueryV2(undefined, normalFilters, normalOptions, normalReturnFields); + expect(resultSearchText).toStrictEqual(querySearchText); + + // TODO: Add tests showing that undefined filters and options are ok + }); + + it('should handle null parameters', () => { // 2 + // Should be null, mainly because returnFields is null + const result = queryGenerator.getSearchQueryV2(null, null, null, null); + expect(result).toBeNull(); + + // TODO: Add tests showing that null searchText, filters and options are ok + }); + + it('should correctly handle empty parameters', () => { // 3 + // Should be null, mainly because returnFields is empty + const result = queryGenerator.getSearchQueryV2('', {}, {}, []); + expect(result).toBeNull(); + + // Empty searchText is ok + const querySearchText = JSON.parse(JSON.stringify(normalOSQuery)); + delete querySearchText.query.bool.must; + const resultSearchText = queryGenerator.getSearchQueryV2('', normalFilters, normalOptions, normalReturnFields); + expect(resultSearchText).toStrictEqual(querySearchText); + + // TODO: Add tests showing that empty filters and options are ok + }); + + it('should handle parameters being the wrong type', () => { // 4 + // Search text being the wrong type + const resultSearchText = queryGenerator.getSearchQueryV2(12345, normalFilters, normalOptions, normalReturnFields); + expect(resultSearchText).toBeNull(); + + // Filters not being an object + const resultFilters = queryGenerator.getSearchQueryV2(normalSearchText, 'not an object', normalOptions, normalReturnFields); + expect(resultFilters).toBeNull(); + + // Filters being an array + const resultFiltersArray = queryGenerator.getSearchQueryV2(normalSearchText, ['a', 'b'], normalOptions, normalReturnFields); + expect(resultFiltersArray).toBeNull(); + + // Options being the wrong type + const resultOptions = queryGenerator.getSearchQueryV2(normalSearchText, normalFilters, 'not an object', normalReturnFields); + expect(resultOptions).toBeNull(); + + // Options being an array + const resultOptionsArray = queryGenerator.getSearchQueryV2(normalSearchText, normalFilters, [1,2,3], normalReturnFields); + expect(resultOptionsArray).toBeNull(); + + // Return fields not being an array + const resultReturnFields = queryGenerator.getSearchQueryV2(normalSearchText, normalFilters, normalOptions, 'not an array'); + expect(resultReturnFields).toBeNull(); + }); + + it('should handle whitespace-only searchText', () => { // 5 + const querySearchText = JSON.parse(JSON.stringify(normalOSQuery)); + delete querySearchText.query.bool.must; + const result = queryGenerator.getSearchQueryV2(' ', normalFilters, normalOptions, normalReturnFields); + expect(result).toStrictEqual(querySearchText); + }); + + it('should form a correct query when all parameters are provided', () => { // 6 + const result = queryGenerator.getSearchQueryV2(normalSearchText, normalFilters, normalOptions, normalReturnFields); + expect(result).toStrictEqual(normalOSQuery); + }); + + it('should handle page being nonpositive', () => { // 7 + const optionsWithZeroPage = { + ...normalOptions, + pageInfo: { + ...normalOptions.pageInfo, + page: 0, + }, + }; + const optionsWithNegativePage = { + ...normalOptions, + pageInfo: { + ...normalOptions.pageInfo, + page: -1, + }, + }; + const result = queryGenerator.getSearchQueryV2(normalSearchText, normalFilters, optionsWithZeroPage, normalReturnFields); + expect(result).toStrictEqual(normalOSQuery); + const resultWithNegativePage = queryGenerator.getSearchQueryV2(normalSearchText, normalFilters, optionsWithNegativePage, normalReturnFields); + expect(resultWithNegativePage).toStrictEqual(normalOSQuery); + }); + + it('should ignore page size being nonpositive', () => { // 8 + const optionsWithZeroPageSize = { + ...normalOptions, + pageInfo: { + ...normalOptions.pageInfo, + pageSize: 0, + }, + }; + const optionsWithNegativePageSize = { + ...normalOptions, + pageInfo: { + ...normalOptions.pageInfo, + pageSize: -1, + }, + }; + const resultZero = queryGenerator.getSearchQueryV2( + normalSearchText, + normalFilters, + optionsWithZeroPageSize, + normalReturnFields + ); + expect(resultZero).toStrictEqual(normalOSQuery); + + const resultNegative = queryGenerator.getSearchQueryV2( + normalSearchText, + normalFilters, + optionsWithNegativePageSize, + normalReturnFields + ); + expect(resultNegative).toStrictEqual(normalOSQuery); + }); +}); diff --git a/Utils/datasetFields.js b/Utils/datasetFields.js index 080b928..4596224 100644 --- a/Utils/datasetFields.js +++ b/Utils/datasetFields.js @@ -1,11 +1,78 @@ // Maps Dataset natural field names to property names +const DATASET_SEARCH_FIELDS = [ + // 'dataset_uuid', + 'dataset_source_repo.search', + 'dataset_title.search', + 'description.search', + 'experimental_approaches.search', + 'dataset_source_id.search', + 'dataset_source_url.search', + 'institute.search', + 'PI_name.search', + // 'GPA', + 'dataset_doc.search', + // POC_name, + // POC_email, + // 'dataset_maximum_age_at_baseline', + // 'dataset_minimum_age_at_baseline', + 'dataset_pmid.search', + // 'dataset_year_enrollment_ended', + // 'dataset_year_enrollment_started', + 'funding_source.search', + // 'release_date', + 'limitations_for_reuse.search', + 'assay_method.search', + 'study_type.search', + 'primary_disease.search', + // 'participant_count', + // 'sample_count', + 'study_links.search', + 'related_genes.search', + 'related_diseases.search', + 'related_terms.search', +]; +const DATASET_HIGHLIGHT_FIELDS = DATASET_SEARCH_FIELDS; +const DATASET_RETURN_FIELDS = [ + // 'dataset_uuid', + 'dataset_source_repo', + 'dataset_title', + 'description', + 'experimental_approaches', + 'dataset_source_id', + 'dataset_source_url', + 'institute', + 'PI_name', + // 'GPA', + 'dataset_doc', + // POC_name, + // POC_email, + 'dataset_maximum_age_at_baseline', + 'dataset_minimum_age_at_baseline', + 'dataset_pmid', + 'dataset_year_enrollment_ended', + 'dataset_year_enrollment_started', + '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', +]; const datasetFields = { 'Dataset UUID': 'dataset_uuid', 'Dataset Title': 'dataset_title', 'Description': 'description', + 'Experimental Approaches': 'experimental_approaches', 'Dataset Source ID': 'dataset_source_id', 'Dataset Source Repository': 'dataset_source_repo', 'Dataset Source URL': 'dataset_source_url', + 'Institute': 'institute', 'Principal Investigator(s)': 'PI_name', // Specifically exclude GPA, because we don't display it anywhere // 'Grant Program Administrator': 'GPA', @@ -26,6 +93,9 @@ const datasetFields = { }; module.exports = { + DATASET_SEARCH_FIELDS, + DATASET_HIGHLIGHT_FIELDS, + DATASET_RETURN_FIELDS, datasetFields, }; diff --git a/package-lock.json b/package-lock.json index 08ba19b..d614837 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ }, "devDependencies": { "@vitest/coverage-v8": "^4.0.15", + "rollup": "^4.55.1", "vitest": "^4.0.15" } }, @@ -610,9 +611,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", - "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", + "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", "cpu": [ "arm" ], @@ -624,9 +625,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", - "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", + "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", "cpu": [ "arm64" ], @@ -638,9 +639,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", - "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", + "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", "cpu": [ "arm64" ], @@ -652,9 +653,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", - "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", + "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", "cpu": [ "x64" ], @@ -666,9 +667,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", - "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", + "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", "cpu": [ "arm64" ], @@ -680,9 +681,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", - "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", + "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", "cpu": [ "x64" ], @@ -694,9 +695,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", - "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", + "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", "cpu": [ "arm" ], @@ -708,9 +709,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", - "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", + "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", "cpu": [ "arm" ], @@ -722,9 +723,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", - "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", + "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", "cpu": [ "arm64" ], @@ -736,9 +737,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", - "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", + "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", "cpu": [ "arm64" ], @@ -750,9 +751,23 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", - "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", + "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", + "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", "cpu": [ "loong64" ], @@ -764,9 +779,23 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", - "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", + "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", + "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", "cpu": [ "ppc64" ], @@ -778,9 +807,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", - "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", + "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", "cpu": [ "riscv64" ], @@ -792,9 +821,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", - "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", + "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", "cpu": [ "riscv64" ], @@ -806,9 +835,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", - "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", + "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", "cpu": [ "s390x" ], @@ -820,9 +849,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", - "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", + "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", "cpu": [ "x64" ], @@ -834,9 +863,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", - "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", + "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", "cpu": [ "x64" ], @@ -847,10 +876,24 @@ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", + "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", - "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", + "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", "cpu": [ "arm64" ], @@ -862,9 +905,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", - "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", + "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", "cpu": [ "arm64" ], @@ -876,9 +919,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", - "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", + "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", "cpu": [ "ia32" ], @@ -890,9 +933,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", - "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", + "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", "cpu": [ "x64" ], @@ -904,9 +947,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", - "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", + "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", "cpu": [ "x64" ], @@ -2718,9 +2761,9 @@ } }, "node_modules/rollup": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", - "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", + "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", "dev": true, "license": "MIT", "dependencies": { @@ -2734,28 +2777,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.53.3", - "@rollup/rollup-android-arm64": "4.53.3", - "@rollup/rollup-darwin-arm64": "4.53.3", - "@rollup/rollup-darwin-x64": "4.53.3", - "@rollup/rollup-freebsd-arm64": "4.53.3", - "@rollup/rollup-freebsd-x64": "4.53.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", - "@rollup/rollup-linux-arm-musleabihf": "4.53.3", - "@rollup/rollup-linux-arm64-gnu": "4.53.3", - "@rollup/rollup-linux-arm64-musl": "4.53.3", - "@rollup/rollup-linux-loong64-gnu": "4.53.3", - "@rollup/rollup-linux-ppc64-gnu": "4.53.3", - "@rollup/rollup-linux-riscv64-gnu": "4.53.3", - "@rollup/rollup-linux-riscv64-musl": "4.53.3", - "@rollup/rollup-linux-s390x-gnu": "4.53.3", - "@rollup/rollup-linux-x64-gnu": "4.53.3", - "@rollup/rollup-linux-x64-musl": "4.53.3", - "@rollup/rollup-openharmony-arm64": "4.53.3", - "@rollup/rollup-win32-arm64-msvc": "4.53.3", - "@rollup/rollup-win32-ia32-msvc": "4.53.3", - "@rollup/rollup-win32-x64-gnu": "4.53.3", - "@rollup/rollup-win32-x64-msvc": "4.53.3", + "@rollup/rollup-android-arm-eabi": "4.55.1", + "@rollup/rollup-android-arm64": "4.55.1", + "@rollup/rollup-darwin-arm64": "4.55.1", + "@rollup/rollup-darwin-x64": "4.55.1", + "@rollup/rollup-freebsd-arm64": "4.55.1", + "@rollup/rollup-freebsd-x64": "4.55.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", + "@rollup/rollup-linux-arm-musleabihf": "4.55.1", + "@rollup/rollup-linux-arm64-gnu": "4.55.1", + "@rollup/rollup-linux-arm64-musl": "4.55.1", + "@rollup/rollup-linux-loong64-gnu": "4.55.1", + "@rollup/rollup-linux-loong64-musl": "4.55.1", + "@rollup/rollup-linux-ppc64-gnu": "4.55.1", + "@rollup/rollup-linux-ppc64-musl": "4.55.1", + "@rollup/rollup-linux-riscv64-gnu": "4.55.1", + "@rollup/rollup-linux-riscv64-musl": "4.55.1", + "@rollup/rollup-linux-s390x-gnu": "4.55.1", + "@rollup/rollup-linux-x64-gnu": "4.55.1", + "@rollup/rollup-linux-x64-musl": "4.55.1", + "@rollup/rollup-openbsd-x64": "4.55.1", + "@rollup/rollup-openharmony-arm64": "4.55.1", + "@rollup/rollup-win32-arm64-msvc": "4.55.1", + "@rollup/rollup-win32-ia32-msvc": "4.55.1", + "@rollup/rollup-win32-x64-gnu": "4.55.1", + "@rollup/rollup-win32-x64-msvc": "4.55.1", "fsevents": "~2.3.2" } }, diff --git a/package.json b/package.json index 9f77038..f3b2fd6 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ }, "devDependencies": { "@vitest/coverage-v8": "^4.0.15", + "rollup": "^4.55.1", "vitest": "^4.0.15" } }