diff --git a/OpenLdapSync/src/org/labkey/openldapsync/OpenLdapSyncController.java b/OpenLdapSync/src/org/labkey/openldapsync/OpenLdapSyncController.java index 3922cba86..1d35856fb 100644 --- a/OpenLdapSync/src/org/labkey/openldapsync/OpenLdapSyncController.java +++ b/OpenLdapSync/src/org/labkey/openldapsync/OpenLdapSyncController.java @@ -125,7 +125,7 @@ public ApiResponse execute(LdapForm form, BindException errors) throws Exception } catch (LdapException e) { - _log.error(e); + _log.error(e.getMessage(), e); errors.reject(ERROR_MSG, e.getMessage()); return null; } @@ -528,7 +528,7 @@ public ApiResponse execute(Object form, BindException errors) throws Exception } catch (Exception e) { - _log.error(e); + _log.error(e.getMessage(), e); errors.reject(ERROR_MSG, e.getMessage() == null ? "unable to connect to LDAP server" : e.getMessage()); return null; } diff --git a/SequenceAnalysis/resources/schemas/dbscripts/postgresql/SequenceAnalysis-12.329-12.330.sql b/SequenceAnalysis/resources/schemas/dbscripts/postgresql/SequenceAnalysis-12.329-12.330.sql new file mode 100644 index 000000000..b46a9866d --- /dev/null +++ b/SequenceAnalysis/resources/schemas/dbscripts/postgresql/SequenceAnalysis-12.329-12.330.sql @@ -0,0 +1,12 @@ +CREATE INDEX IDX_haplotypes_name_date ON sequenceanalysis.haplotypes +( + Name ASC, + datedisabled ASC +); + +CREATE INDEX IDX_haplotype_sequences_name_haplotype_type ON sequenceanalysis.haplotype_sequences +( + haplotype ASC, + name ASC, + type ASC +); \ No newline at end of file diff --git a/SequenceAnalysis/resources/schemas/dbscripts/sqlserver/SequenceAnalysis-12.329-12.330.sql b/SequenceAnalysis/resources/schemas/dbscripts/sqlserver/SequenceAnalysis-12.329-12.330.sql new file mode 100644 index 000000000..73397ec1d --- /dev/null +++ b/SequenceAnalysis/resources/schemas/dbscripts/sqlserver/SequenceAnalysis-12.329-12.330.sql @@ -0,0 +1,39 @@ +CREATE NONCLUSTERED INDEX IDX_haplotypes_name_date ON sequenceanalysis.haplotypes +( + Name ASC, + datedisabled ASC +); + +CREATE NONCLUSTERED INDEX IDX_haplotype_sequences_name_haplotype_type ON sequenceanalysis.haplotype_sequences +( + haplotype ASC, + name ASC, + type ASC +); + +CREATE NONCLUSTERED INDEX IDX_alignment_summary_analysis_id_rowid_container_total ON sequenceanalysis.alignment_summary +( + analysis_id ASC, + rowid ASC, + container ASC +) +INCLUDE(total) + +CREATE STATISTICS STAT_ref_nt_sequence_rowid_locus_container ON sequenceanalysis.ref_nt_sequences (RowId, locus, container) +WITH AUTO_DROP = OFF + +CREATE STATISTICS STAT_ref_nt_sequence_locus_container ON sequenceanalysis.ref_nt_sequences (locus, container) +WITH AUTO_DROP = OFF + +CREATE STATISTICS STAT_sequence_analyses_container_readset ON sequenceanalysis.sequence_analyses (Container, readset) +WITH AUTO_DROP = OFF + +CREATE STATISTICS STAT_sequence_readsets_rowid_container ON sequenceanalysis.sequence_readsets (RowId, Container) +WITH AUTO_DROP = OFF + +CREATE STATISTICS STAT_asj_alignmentid_container_ref_nt_id ON sequenceanalysis.alignment_summary_junction (alignment_id, container, ref_nt_id) +WITH AUTO_DROP = OFF + +CREATE STATISTICS STAT_asj_alignmentid_ref_nt_id_status_alignment_id ON sequenceanalysis.alignment_summary_junction (ref_nt_id, status, alignment_id) +WITH AUTO_DROP = OFF + diff --git a/SequenceAnalysis/src/org/labkey/sequenceanalysis/SequenceAnalysisController.java b/SequenceAnalysis/src/org/labkey/sequenceanalysis/SequenceAnalysisController.java index 930f93602..63ce77d1b 100644 --- a/SequenceAnalysis/src/org/labkey/sequenceanalysis/SequenceAnalysisController.java +++ b/SequenceAnalysis/src/org/labkey/sequenceanalysis/SequenceAnalysisController.java @@ -1191,7 +1191,7 @@ public ApiResponse execute(ValidateReadsetImportForm form, BindException errors) { ExceptionUtil.logExceptionToMothership(getViewContext().getRequest(), e); errors.reject(ERROR_MSG, e.getMessage()); - _log.error(e); + _log.error(e.getMessage(), e); return null; } return new ApiSimpleResponse(resultProperties); @@ -4818,7 +4818,7 @@ public ApiResponse execute(ImportTracksForm form, BindException errors) throws E catch (IOException e) { errors.reject(ERROR_MSG, e.getMessage()); - _log.error(e); + _log.error(e.getMessage(), e); return null; } @@ -4834,7 +4834,7 @@ public ApiResponse execute(ImportTracksForm form, BindException errors) throws E catch (PipelineValidationException e) { errors.reject(ERROR_MSG, e.getMessage()); - _log.error(e); + _log.error(e.getMessage(), e); return null; } diff --git a/SequenceAnalysis/src/org/labkey/sequenceanalysis/SequenceAnalysisModule.java b/SequenceAnalysis/src/org/labkey/sequenceanalysis/SequenceAnalysisModule.java index 466c12ac7..3fc642130 100644 --- a/SequenceAnalysis/src/org/labkey/sequenceanalysis/SequenceAnalysisModule.java +++ b/SequenceAnalysis/src/org/labkey/sequenceanalysis/SequenceAnalysisModule.java @@ -211,7 +211,7 @@ public String getName() @Override public Double getSchemaVersion() { - return 12.329; + return 12.330; } @Override diff --git a/SequenceAnalysis/src/org/labkey/sequenceanalysis/query/SequenceTriggerHelper.java b/SequenceAnalysis/src/org/labkey/sequenceanalysis/query/SequenceTriggerHelper.java index bb48598f6..91cf88e07 100644 --- a/SequenceAnalysis/src/org/labkey/sequenceanalysis/query/SequenceTriggerHelper.java +++ b/SequenceAnalysis/src/org/labkey/sequenceanalysis/query/SequenceTriggerHelper.java @@ -136,7 +136,7 @@ public String extractAASequence(int refNtId, List> exons, boolean i } catch (IOException e) { - _log.error(e); + _log.error(e.getMessage(), e); return null; } } diff --git a/Studies/resources/module.xml b/Studies/resources/module.xml index 5a8029b41..266903333 100644 --- a/Studies/resources/module.xml +++ b/Studies/resources/module.xml @@ -2,4 +2,11 @@ + + + This view is used as the default on the demographics table + true + + + diff --git a/Studies/resources/views/manageStudy.html b/Studies/resources/views/manageStudy.html index ba72f5797..c711f1c08 100644 --- a/Studies/resources/views/manageStudy.html +++ b/Studies/resources/views/manageStudy.html @@ -13,13 +13,14 @@ const parentDiv = $('#' + webpart.wrapperDivId) parentDiv.append('
') + parentDiv.append('
') parentDiv.append('
') parentDiv.append('
') LDK.Utils.getBasicQWP({ frame: 'portal', title: 'Cohorts', - name: 'query', + name: 'query-studies', schemaName: 'studies', queryName: 'studyCohorts', filterArray: [LABKEY.Filter.create('studyId',studyId, LABKEY.Filter.Types.EQUAL)], @@ -27,10 +28,42 @@ renderTo: webpart.wrapperDivId + '-studyCohorts' }).render(); + if (LABKEY.getModuleContext('study')?.subject) { + const hasAssignmentDataset = !!LABKEY.getModuleContext('studies')?.hasAssignmentDataset + if (hasAssignmentDataset) { + LABKEY.Query.selectRows({ + schemaName: 'studies', + queryName: 'studies', + columns: 'rowId,studyName', + filterArray: [LABKEY.Filter.create('rowId', studyId)], + failure: LDK.Utils.getErrorCallback(), + success: function(results) { + const studyName = results.rows?.[0].studyName; + LDK.Assert.assertNotEmpty('StudyName was empty in manageStudy.view', studyName); + + const projectFieldName = 'allProjectsPivot/' + studyName + '::lastStartDate'; + const demographicsDefaultView = LABKEY.getModuleProperty('studies', 'demographicsDefaultView') + LDK.Utils.getBasicQWP({ + frame: 'portal', + title: LABKEY.moduleContext.study?.subject.nounPlural, + name: 'query-demographics', + schemaName: 'study', + queryName: 'demographics', + viewName: demographicsDefaultView, + filterArray: [LABKEY.Filter.create(projectFieldName, null, LABKEY.Filter.Types.NONBLANK)], + maxRows: 20, + renderTo: webpart.wrapperDivId + '-demographics' + }).render(); + }, + scope: this + }); + } + } + LDK.Utils.getBasicQWP({ frame: 'portal', title: 'Anchor Events', - name: 'query', + name: 'query-anchorEvents', schemaName: 'studies', queryName: 'anchorEvents', filterArray: [LABKEY.Filter.create('studyId',studyId, LABKEY.Filter.Types.EQUAL)], @@ -41,7 +74,7 @@ LDK.Utils.getBasicQWP({ frame: 'portal', title: 'Expected Timepoints', - name: 'query', + name: 'query-expectedTimepoints', schemaName: 'studies', queryName: 'expectedTimepoints', filterArray: [LABKEY.Filter.create('studyId',studyId, LABKEY.Filter.Types.EQUAL)], @@ -54,6 +87,4 @@ - -PLACEHOLDER: Make a page that accepts a studyId and renders useful UI to manage timepoints, run QC, and show data

\ No newline at end of file diff --git a/Studies/src/org/labkey/studies/StudiesModule.java b/Studies/src/org/labkey/studies/StudiesModule.java index fd7553fc4..be32f3140 100644 --- a/Studies/src/org/labkey/studies/StudiesModule.java +++ b/Studies/src/org/labkey/studies/StudiesModule.java @@ -2,6 +2,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.json.JSONObject; import org.labkey.api.data.Container; import org.labkey.api.laboratory.LaboratoryService; import org.labkey.api.ldk.ExtendedSimpleModule; @@ -11,9 +12,9 @@ import org.labkey.api.query.QuerySchema; import org.labkey.api.security.roles.RoleManager; import org.labkey.api.studies.StudiesService; -import org.labkey.api.util.PageFlowUtil; -import org.labkey.studies.query.StudiesUserSchema; import org.labkey.api.studies.security.StudiesDataAdminRole; +import org.labkey.api.writer.ContainerUser; +import org.labkey.studies.query.StudiesUserSchema; import org.labkey.studies.study.StudiesFilterProvider; import org.labkey.studies.study.StudyEnrollmentEventProvider; @@ -68,6 +69,16 @@ public Set getSchemaNames() return Collections.singleton(StudiesSchema.NAME); } + @Override + public JSONObject getPageContextJson(ContainerUser context) + { + JSONObject json = super.getPageContextJson(context); + + json.put("hasAssignmentDataset", StudiesServiceImpl.get().hasAssignmentDataset(context.getContainer())); + + return json; + } + @Override public void registerSchemas() { diff --git a/Studies/src/org/labkey/studies/StudiesServiceImpl.java b/Studies/src/org/labkey/studies/StudiesServiceImpl.java index 93f35a867..a544d2a10 100644 --- a/Studies/src/org/labkey/studies/StudiesServiceImpl.java +++ b/Studies/src/org/labkey/studies/StudiesServiceImpl.java @@ -21,6 +21,8 @@ import org.labkey.api.security.User; import org.labkey.api.studies.StudiesService; import org.labkey.api.studies.study.EventProvider; +import org.labkey.api.study.Study; +import org.labkey.api.study.StudyService; import org.labkey.api.util.ConfigurationException; import org.labkey.api.util.FileUtil; import org.labkey.api.util.Path; @@ -173,4 +175,17 @@ public TableCustomizer getStudiesTableCustomizer() { return new StudiesTableCustomizer(); } + + public static String ASSIGNMENT_DATASET = "assignment"; + + public boolean hasAssignmentDataset(Container c) + { + Study s = StudyService.get().getStudy(c.isWorkbookOrTab() ? c.getParent() : c); + if (s == null) + { + return false; + } + + return s.getDatasetByName(ASSIGNMENT_DATASET) != null; + } } diff --git a/Studies/src/org/labkey/studies/query/StudiesTableCustomizer.java b/Studies/src/org/labkey/studies/query/StudiesTableCustomizer.java index fdf8f8754..917d512fc 100644 --- a/Studies/src/org/labkey/studies/query/StudiesTableCustomizer.java +++ b/Studies/src/org/labkey/studies/query/StudiesTableCustomizer.java @@ -1,17 +1,32 @@ package org.labkey.studies.query; import org.apache.commons.collections4.MultiValuedMap; -import org.apache.commons.collections4.SetValuedMap; import org.apache.logging.log4j.Logger; import org.labkey.api.collections.CaseInsensitiveHashMap; - import org.labkey.api.collections.CaseInsensitiveKeyedHashSetValuedMap; import org.labkey.api.data.AbstractTableInfo; +import org.labkey.api.data.BaseColumnInfo; +import org.labkey.api.data.ColumnInfo; +import org.labkey.api.data.Container; +import org.labkey.api.data.MutableColumnInfo; import org.labkey.api.data.TableCustomizer; import org.labkey.api.data.TableInfo; import org.labkey.api.ldk.LDKService; +import org.labkey.api.query.ExprColumn; +import org.labkey.api.query.FieldKey; +import org.labkey.api.query.LookupForeignKey; +import org.labkey.api.query.QueryDefinition; +import org.labkey.api.query.QueryException; +import org.labkey.api.query.QueryService; +import org.labkey.api.query.UserSchema; import org.labkey.api.study.DatasetTable; +import org.labkey.api.study.Study; +import org.labkey.api.study.StudyService; import org.labkey.api.util.logging.LogHelper; +import org.labkey.studies.StudiesServiceImpl; + +import java.util.ArrayList; +import java.util.List; public class StudiesTableCustomizer implements TableCustomizer { @@ -61,7 +76,148 @@ else if (tableInfo instanceof DatasetTable ds) private void doCustomize(AbstractTableInfo ati) { // TODO: - // Overlapping studies/cohorts // TimepointLabel + + addProjectAssignmentColumns(ati); + } + + private String getSubjectColName(Container c) + { + Study s = StudyService.get().getStudy(c.isWorkbookOrTab() ? c.getParent() : c); + if (s == null) + { + return null; + } + + return s.getSubjectColumnName(); + } + + private void addProjectAssignmentColumns(AbstractTableInfo ati) + { + final String pivotColName = "allProjectsPivot"; + if (ati.getColumn(pivotColName) != null) + return; + + List pks = ati.getPkColumns(); + ColumnInfo pk; + if (pks.size() == 1) + { + pk = pks.get(0); + } + else + { + if (! (ati instanceof DatasetTable)) + { + _log.error("Table does not have a single PK column: " + ati.getName()); + return; + } + else + { + pk = pks.get(0); + } + } + + if (!StudiesServiceImpl.get().hasAssignmentDataset(ati.getUserSchema().getContainer())) + { + return; + } + + final String subjectSelectName = getSubjectColName(ati.getUserSchema().getContainer()); + if (subjectSelectName == null) + { + _log.error("Unable to find subjectSelectName in StudiesTableCustomizer"); + return; + } + + final String pkColSelectName = pk.getFieldKey().toSQLString(); + + final String lookupName = ati.getName() + "_allProjectsPivot"; + BaseColumnInfo col2 = new ExprColumn(ati, FieldKey.fromString(pivotColName), pk.getValueSql(ExprColumn.STR_TABLE_ALIAS), pk.getJdbcType(), pk); + col2.setLabel("Assignment By Study"); + col2.setName(pivotColName); + col2.setCalculated(true); + col2.setShownInInsertView(false); + col2.setShownInUpdateView(false); + col2.setDescription("Shows groups to which this subject belonged at any point in time."); + col2.setHidden(true); + col2.setReadOnly(true); + col2.setIsUnselectable(true); + col2.setUserEditable(false); + col2.setKeyField(false); + col2.setFk(new LookupForeignKey(){ + @Override + public TableInfo getLookupTableInfo() + { + final UserSchema us = ati.getUserSchema(); + Container target = us.getContainer().isWorkbookOrTab() ? us.getContainer().getParent() : us.getContainer(); + QueryDefinition qd = createQueryDef(us, lookupName); + + qd.setSql(getAssignmentPivotSql(target, ati, pkColSelectName, subjectSelectName)); + qd.setIsTemporary(true); + + List errors = new ArrayList<>(); + TableInfo ti = qd.getTable(errors, true); + + if (!errors.isEmpty()){ + _log.error("Problem with table customizer: " + ati.getPublicName()); + for (QueryException e : errors) + { + _log.error(e.getMessage()); + } + } + + if (ti != null) + { + MutableColumnInfo col = (MutableColumnInfo) ti.getColumn(pk.getName()); + col.setKeyField(true); + col.setHidden(true); + + ((MutableColumnInfo)ti.getColumn("lastStartDate")).setLabel("Most Recent Assignment Date"); + } + + return ti; + } + }); + + ati.addColumn(col2); + } + + private String getAssignmentPivotSql(Container source, final AbstractTableInfo ati, String pkColSelectName, String subjectSelectName) + { + return "SELECT\n" + + "s." + pkColSelectName + ",\n" + + "p.study,\n" + + "max(p.date) as lastStartDate\n" + + "\n" + + "FROM " + ati.getPublicSchemaName() + "." + ati.getPublicName() + " s\n" + + "JOIN \"" + source.getPath() + "\".study.assignment p\n" + + "ON (s." + subjectSelectName + " = p." + subjectSelectName + ")\n" + + "WHERE s." + subjectSelectName + " IS NOT NULL\n" + + "\n" + + "GROUP BY s." + pkColSelectName + ", p.study\n" + + "PIVOT lastStartDate by study IN (select distinct studyName from studies.studies)"; + } + + // TODO: move to parent class + protected QueryDefinition createQueryDef(UserSchema us, String queryName) + { + if (!us.getContainer().isWorkbook()) + { + return QueryService.get().createQueryDef(us.getUser(), us.getContainer(), us, queryName); + } + + // The rationale is that if we are querying from a workbook, preferentially translate to the parent US + // However, there are situations like workbook-scoped lists, where that query might not exist on the parent + UserSchema parentUserSchema = QueryService.get().getUserSchema(us.getUser(), us.getContainer().getParent(), us.getSchemaPath()); + assert parentUserSchema != null; + + if (parentUserSchema.getTableNames().contains(queryName)) + { + return QueryService.get().createQueryDef(parentUserSchema.getUser(), parentUserSchema.getContainer(), parentUserSchema, queryName); + } + else + { + return QueryService.get().createQueryDef(us.getUser(), us.getContainer(), us, queryName); + } } } diff --git a/Studies/src/org/labkey/studies/query/StudiesUserSchema.java b/Studies/src/org/labkey/studies/query/StudiesUserSchema.java index 00401b524..4db90aaee 100644 --- a/Studies/src/org/labkey/studies/query/StudiesUserSchema.java +++ b/Studies/src/org/labkey/studies/query/StudiesUserSchema.java @@ -179,9 +179,17 @@ private TableInfo createCohortsTable(String name, ContainerFilter cf) col2.setLabel("Cohort Name"); col2.setHidden(true); col2.setDescription("This column lists the cohort label, and the name if label is blank"); - ret.addColumn(col2); + SQLFragment coalesce = new SQLFragment("coalesce(" + ExprColumn.STR_TABLE_ALIAS + ".label, " + ExprColumn.STR_TABLE_ALIAS + ".cohortName)"); + SQLFragment select = new SQLFragment("(SELECT studyName FROM " + StudiesSchema.NAME).append(".").append(TABLE_STUDIES).append(" s WHERE s.rowId = ").append(ExprColumn.STR_TABLE_ALIAS).append(".studyId)"); + SQLFragment sql3 = ret.getSqlDialect().concatenate(select, new SQLFragment("': '"), coalesce); + ExprColumn col3 = new ExprColumn(ret, "studyAndCohort", sql3, JdbcType.VARCHAR, ret.getColumn("cohortName"), ret.getColumn("label"), ret.getColumn("rowId")); + col3.setLabel("Study/Cohort"); + col3.setHidden(true); + col3.setDescription("This column lists the study ID and cohort label"); + ret.addColumn(col3); + return ret; } diff --git a/jbrowse/package-lock.json b/jbrowse/package-lock.json index b968b65f6..b50edc098 100644 --- a/jbrowse/package-lock.json +++ b/jbrowse/package-lock.json @@ -24,8 +24,8 @@ "child_process": "^1.0.2", "fs": "^0.0.1-security", "jquery": "^3.7.1", - "jspdf": "^3.0.0", - "jspdf-autotable": "^5.0.2", + "jspdf": "^4.0.0", + "jspdf-autotable": "^5.0.7", "node-polyfill-webpack-plugin": "4.1.0", "path-browserify": "^1.0.1", "react": "^18.3.0", @@ -4987,23 +4987,24 @@ "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" }, "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", "dev": true, + "license": "MIT", "dependencies": { - "bytes": "3.1.2", + "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", "type-is": "~1.6.18", - "unpipe": "1.0.0" + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8", @@ -5019,12 +5020,43 @@ "ms": "2.0.0" } }, + "node_modules/body-parser/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/body-parser/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, + "node_modules/body-parser/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/bonjour-service": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", @@ -6632,39 +6664,40 @@ } }, "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "dev": true, + "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", + "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.13.0", + "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", + "send": "~0.19.0", + "serve-static": "~1.16.2", "setprototypeof": "1.2.0", - "statuses": "2.0.1", + "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" @@ -7587,6 +7620,7 @@ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dev": true, + "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -8092,12 +8126,12 @@ } }, "node_modules/jspdf": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.3.tgz", - "integrity": "sha512-eURjAyz5iX1H8BOYAfzvdPfIKK53V7mCpBTe7Kb16PaM8JSXEcUQNBQaiWMI8wY5RvNOPj4GccMjTlfwRBd+oQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.0.0.tgz", + "integrity": "sha512-w12U97Z6edKd2tXDn3LzTLg7C7QLJlx0BPfM3ecjK2BckUl9/81vZ+r5gK4/3KQdhAcEZhENUxRhtgYBj75MqQ==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.26.9", + "@babel/runtime": "^7.28.4", "fast-png": "^6.2.0", "fflate": "^0.8.1" }, @@ -8109,11 +8143,12 @@ } }, "node_modules/jspdf-autotable": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-5.0.2.tgz", - "integrity": "sha512-YNKeB7qmx3pxOLcNeoqAv3qTS7KuvVwkFe5AduCawpop3NOkBUtqDToxNc225MlNecxT4kP2Zy3z/y/yvGdXUQ==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-5.0.7.tgz", + "integrity": "sha512-2wr7H6liNDBYNwt25hMQwXkEWFOEopgKIvR1Eukuw6Zmprm/ZcnmLTQEjW7Xx3FCbD3v7pflLcnMAv/h1jFDQw==", + "license": "MIT", "peerDependencies": { - "jspdf": "^2 || ^3" + "jspdf": "^2 || ^3 || ^4" } }, "node_modules/keycharm": { @@ -8865,7 +8900,9 @@ } }, "node_modules/object-inspect": { - "version": "1.13.2", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -9523,12 +9560,12 @@ } }, "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -9596,16 +9633,48 @@ } }, "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "dev": true, + "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -10671,13 +10740,69 @@ } }, "node_modules/side-channel": { - "version": "1.0.6", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" }, "engines": { "node": ">= 0.4" diff --git a/jbrowse/package.json b/jbrowse/package.json index b8823a270..402055fef 100644 --- a/jbrowse/package.json +++ b/jbrowse/package.json @@ -30,8 +30,8 @@ "child_process": "^1.0.2", "fs": "^0.0.1-security", "jquery": "^3.7.1", - "jspdf": "^3.0.0", - "jspdf-autotable": "^5.0.2", + "jspdf": "^4.0.0", + "jspdf-autotable": "^5.0.7", "node-polyfill-webpack-plugin": "4.1.0", "path-browserify": "^1.0.1", "react": "^18.3.0", diff --git a/jbrowse/resources/external/fetch-jbrowse-bundle.mjs b/jbrowse/resources/external/fetch-jbrowse-bundle.mjs index 04623fe0b..16a028199 100644 --- a/jbrowse/resources/external/fetch-jbrowse-bundle.mjs +++ b/jbrowse/resources/external/fetch-jbrowse-bundle.mjs @@ -1,5 +1,5 @@ import { execSync } from 'node:child_process'; -import { existsSync, mkdirSync, copyFileSync, rmSync } from 'node:fs'; +import { existsSync, mkdirSync, copyFileSync, rmSync, readFileSync } from 'node:fs'; import { join, resolve } from 'node:path'; const ROOT = resolve('.'); @@ -7,6 +7,44 @@ const BUILD = join(ROOT, 'buildCli'); const OUTDIR = join(ROOT, 'resources', 'external'); const OUTFILE = join(OUTDIR, 'jbrowse.js'); +function getResolvedCoreVersion() { + // Prefer lockfile, fallback to node_modules + const lockPath = join(ROOT, 'package-lock.json'); + if (existsSync(lockPath)) { + const lock = JSON.parse(readFileSync(lockPath, 'utf8')); + const v = + lock?.packages?.['node_modules/@jbrowse/core']?.version || + lock?.dependencies?.['@jbrowse/core']?.version; + if (v) return v; + } + + const corePkg = join(ROOT, 'node_modules', '@jbrowse', 'core', 'package.json'); + if (existsSync(corePkg)) { + return JSON.parse(readFileSync(corePkg, 'utf8')).version; + } + + throw new Error('Could not determine resolved @jbrowse/core version (no lockfile entry and no node_modules).'); +} + +function semverLt(a, b) { + const pa = String(a).match(/(\d+)\.(\d+)\.(\d+)/); + const pb = String(b).match(/(\d+)\.(\d+)\.(\d+)/); + if (!pa || !pb) return false; + for (let i = 1; i <= 3; i++) { + const da = Number(pa[i]); + const db = Number(pb[i]); + if (da !== db) return da < db; + } + return false; +} + +function floorVersion(spec, min) { + const m = String(spec).match(/^(\^|~|>=)?\s*(\d+\.\d+\.\d+)(.*)$/); + if (!m) return spec; + const [, op = '', v, rest = ''] = m; + return semverLt(v, min) ? `${op}${min}${rest}` : spec; +} + async function extractTgz(tgzPath, cwd) { try { const mod = await import('tar').catch(() => null); @@ -20,19 +58,36 @@ async function extractTgz(tgzPath, cwd) { execSync(`tar -xzf "${tgzPath}" -C "${cwd}"`, { stdio: 'inherit', shell: true }); } +function findCliEntrypoint(buildDir) { + const candidates = [ + join(buildDir, 'package', 'bundle', 'index.js'), + join(buildDir, 'package', 'lib', 'index.js') + ]; + + for (const p of candidates) { + if (existsSync(p)) return p; + } + + throw new Error( + `Could not find @jbrowse/cli entrypoint. Tried:\n` + + candidates.map(c => ` - ${c}`).join('\n') + ); +} + async function main() { if (existsSync(BUILD)) rmSync(BUILD, { recursive: true, force: true }); mkdirSync(BUILD, { recursive: true }); mkdirSync(OUTDIR, { recursive: true }); - console.log('Packing @jbrowse/cli (latest)…'); - const out = execSync('npm pack @jbrowse/cli', { + const coreVersion = getResolvedCoreVersion(); + const cliSpec = floorVersion(process.env.JBROWSE_CLI_VERSION || coreVersion, '3.6.0'); + + console.log(`Packing @jbrowse/cli@${cliSpec} (core resolved: ${coreVersion})…`); + const out = execSync(`npm pack @jbrowse/cli@${cliSpec}`, { cwd: BUILD, stdio: ['ignore', 'pipe', 'inherit'], shell: true, - }) - .toString() - .trim(); + }).toString().trim(); const tgz = join(BUILD, out); console.log(`Downloaded: ${tgz}`); @@ -40,12 +95,9 @@ async function main() { console.log('Extracting tarball…'); await extractTgz(tgz, BUILD); - const bundled = join(BUILD, 'package', 'bundle', 'index.js'); - if (!existsSync(bundled)) { - throw new Error(`bundle/index.js not found at ${bundled}`); - } + const entry = findCliEntrypoint(BUILD); - copyFileSync(bundled, OUTFILE); + copyFileSync(entry, OUTFILE); console.log(`Copied bundle to ${OUTFILE}`); rmSync(BUILD, { recursive: true, force: true }); diff --git a/singlecell/resources/chunks/IdentifyAndStoreActiveClonotypes.R b/singlecell/resources/chunks/IdentifyAndStoreActiveClonotypes.R index 0e4d474c2..3efdee1b4 100644 --- a/singlecell/resources/chunks/IdentifyAndStoreActiveClonotypes.R +++ b/singlecell/resources/chunks/IdentifyAndStoreActiveClonotypes.R @@ -11,6 +11,11 @@ for (datasetId in names(seuratObjects)) { printName(datasetId) seuratObj <- readSeuratRDS(seuratObjects[[datasetId]]) + if (! 'TRB_Segments' %in% names(seuratObj@meta.data)) { + print('Re-running AppendTcr to add segment columns') + seuratObj <- Rdiscvr::DownloadAndAppendTcrClonotypes(seuratObj, allowMissing = TRUE) + } + Rdiscvr::IdentifyAndStoreActiveClonotypes(seuratObj, chain = 'TRA', storeStimLevelData = FALSE) Rdiscvr::IdentifyAndStoreActiveClonotypes(seuratObj, chain = 'TRB') diff --git a/singlecell/resources/chunks/PredictTcellActivation.R b/singlecell/resources/chunks/PredictTcellActivation.R index c5374cc76..41bbb6fc5 100644 --- a/singlecell/resources/chunks/PredictTcellActivation.R +++ b/singlecell/resources/chunks/PredictTcellActivation.R @@ -10,6 +10,11 @@ for (datasetId in names(seuratObjects)) { } } + if (! 'TRB_Segments' %in% names(seuratObj@meta.data)) { + print('Re-running AppendTcr to add segment columns') + seuratObj <- Rdiscvr::DownloadAndAppendTcrClonotypes(seuratObj, allowMissing = TRUE) + } + seuratObj <- RIRA::PredictTcellActivation(seuratObj) saveData(seuratObj, datasetId) diff --git a/singlecell/src/org/labkey/singlecell/SingleCellController.java b/singlecell/src/org/labkey/singlecell/SingleCellController.java index 3ad268982..c5a3b9399 100644 --- a/singlecell/src/org/labkey/singlecell/SingleCellController.java +++ b/singlecell/src/org/labkey/singlecell/SingleCellController.java @@ -153,7 +153,7 @@ public void export(OutputFilesForm form, HttpServletResponse response, BindExcep } catch (Exception e) { - _log.error(e); + _log.error(e.getMessage(), e); errors.reject(ERROR_MSG, e.getMessage()); break; } @@ -228,7 +228,7 @@ public Object execute(SimpleApiJsonForm form, BindException errors) throws Excep if (r.get("rowId") != null && StringUtils.trimToNull(r.get("rowId").toString()) != null) { - sampleMap.put((String) r.get("objectId"), (Integer) r.get("rowId")); + sampleMap.put((String) r.get("objectId"), Integer.parseInt(r.get("rowId").toString())); } else { @@ -249,7 +249,7 @@ public Object execute(SimpleApiJsonForm form, BindException errors) throws Excep throw new ApiUsageException("Missing rowId for inserted sample row"); } - sampleMap.put((String) r.get("objectId"), (Integer) r.get("rowId")); + sampleMap.put((String) r.get("objectId"), Integer.parseInt(r.get("rowId").toString())); }); Map sortMap = new HashMap<>(); @@ -262,7 +262,7 @@ public Object execute(SimpleApiJsonForm form, BindException errors) throws Excep if (r.get("rowId") != null && StringUtils.trimToNull(r.get("rowId").toString()) != null) { - sortMap.put((String) r.get("objectId"), (Integer) r.get("rowId")); + sortMap.put((String) r.get("objectId"), Integer.parseInt(r.get("rowId").toString())); } else { @@ -283,7 +283,7 @@ public Object execute(SimpleApiJsonForm form, BindException errors) throws Excep throw new ApiUsageException("Missing objectId for sort row"); } - sortMap.put((String) r.get("objectId"), (Integer) r.get("rowId")); + sortMap.put((String) r.get("objectId"), Integer.parseInt(r.get("rowId").toString())); }); readsetRows = sequenceAnalysis.getTable("sequence_readsets", null).getUpdateService().insertRows(getUser(), getContainer(), readsetRows, bve, null, new HashMap<>()); @@ -292,14 +292,14 @@ public Object execute(SimpleApiJsonForm form, BindException errors) throws Excep throw bve; } - Map readsetMap = new HashMap<>(); + Map readsetMap = new HashMap<>(); readsetRows.forEach(r -> { if (r.get("objectId") == null) { throw new ApiUsageException("Missing objectId for readset row"); } - readsetMap.put((String)r.get("objectId"), (Integer)r.get("rowId")); + readsetMap.put((String)r.get("objectId"), (Long)r.get("rowId")); }); cDNARows.forEach(r -> { @@ -323,7 +323,7 @@ public Object execute(SimpleApiJsonForm form, BindException errors) throws Excep } catch (Exception e) { - _log.error(e); + _log.error(e.getMessage(), e); errors.reject(ERROR_MSG, e.getMessage()); return null;