diff --git a/__tests__/api-atlases-create.test.ts b/__tests__/api-atlases-create.test.ts index 208edb95..71427984 100644 --- a/__tests__/api-atlases-create.test.ts +++ b/__tests__/api-atlases-create.test.ts @@ -25,7 +25,10 @@ jest.mock("../app/services/cellxgene"); jest.mock("../app/utils/pg-app-connect-config"); const NEW_ATLAS_DATA: NewAtlasData = { + cellxgeneAtlasCollection: "7a223dd3-a422-4f4b-a437-90b9a3b00ba8", + codeLinks: [{ url: "https://example.com/new-atlas-foo" }], description: "foo bar baz baz foo bar", + highlights: "bar foo baz baz baz foo", integrationLead: [], network: "eye", shortName: "test", @@ -260,6 +263,51 @@ describe("/api/atlases/create", () => { ).toEqual(400); }); + it("returns error 400 when cellxgene id is not a uuid", async () => { + expect( + ( + await doCreateTest( + USER_CONTENT_ADMIN, + { + ...NEW_ATLAS_DATA, + cellxgeneAtlasCollection: "not-a-uuid", + }, + true + ) + )._getStatusCode() + ).toEqual(400); + }); + + it("returns error 400 when link url is not a url", async () => { + expect( + ( + await doCreateTest( + USER_CONTENT_ADMIN, + { + ...NEW_ATLAS_DATA, + codeLinks: [{ url: "not-a-url" }], + }, + true + ) + )._getStatusCode() + ).toEqual(400); + }); + + it("returns error 400 when highlights are too long", async () => { + expect( + ( + await doCreateTest( + USER_CONTENT_ADMIN, + { + ...NEW_ATLAS_DATA, + highlights: "x".repeat(10001), + }, + true + ) + )._getStatusCode() + ).toEqual(400); + }); + it("creates and returns atlas entry with no integration leads", async () => { await testSuccessfulCreate(NEW_ATLAS_DATA); }); @@ -291,9 +339,16 @@ async function testSuccessfulCreate(atlasData: NewAtlasData): Promise { expect(newAtlasFromDb.target_completion).toEqual( atlasData.targetCompletion ? new Date(atlasData.targetCompletion) : null ); + expect(newAtlasFromDb.overview.cellxgeneAtlasCollection).toEqual( + atlasData.cellxgeneAtlasCollection ?? null + ); + expect(newAtlasFromDb.overview.codeLinks).toEqual(atlasData.codeLinks ?? []); expect(newAtlasFromDb.overview.description).toEqual( atlasData.description ?? "" ); + expect(newAtlasFromDb.overview.highlights).toEqual( + atlasData.highlights ?? "" + ); expect(newAtlasFromDb.overview.integrationLead).toEqual( atlasData.integrationLead ); diff --git a/__tests__/api-atlases-id.test.ts b/__tests__/api-atlases-id.test.ts index 169a4dcb..b0bd0786 100644 --- a/__tests__/api-atlases-id.test.ts +++ b/__tests__/api-atlases-id.test.ts @@ -82,8 +82,10 @@ const ATLAS_DRAFT_EDIT: AtlasEditData = { wave: "3", }; -const ATLAS_PUBLIC_EDIT_NO_TARGET_COMPLETION: AtlasEditData = { +const ATLAS_PUBLIC_EDIT_NO_TARGET_COMPLETION_OR_CELLXGENE: AtlasEditData = { + codeLinks: ATLAS_DRAFT.codeLinks, description: ATLAS_DRAFT.description, + highlights: ATLAS_DRAFT.highlights, integrationLead: ATLAS_DRAFT.integrationLead, network: ATLAS_DRAFT.network, shortName: ATLAS_DRAFT.shortName, @@ -92,6 +94,8 @@ const ATLAS_PUBLIC_EDIT_NO_TARGET_COMPLETION: AtlasEditData = { }; const ATLAS_WITH_MISC_SOURCE_STUDIES_EDIT: AtlasEditData = { + cellxgeneAtlasCollection: + ATLAS_WITH_MISC_SOURCE_STUDIES.cellxgeneAtlasCollection, integrationLead: ATLAS_WITH_MISC_SOURCE_STUDIES.integrationLead, network: ATLAS_WITH_MISC_SOURCE_STUDIES.network, shortName: ATLAS_WITH_MISC_SOURCE_STUDIES.shortName, @@ -388,16 +392,16 @@ describe(TEST_ROUTE, () => { await testSuccessfulEdit(ATLAS_DRAFT, ATLAS_DRAFT_EDIT, 2); }); - it("PUT updates and returns atlas entry with target completion removed", async () => { + it("PUT updates and returns atlas entry with target completion and CELLxGENE collection removed", async () => { const updatedAtlas = await testSuccessfulEdit( ATLAS_PUBLIC, - ATLAS_PUBLIC_EDIT_NO_TARGET_COMPLETION, + ATLAS_PUBLIC_EDIT_NO_TARGET_COMPLETION_OR_CELLXGENE, 0 ); expect(updatedAtlas.target_completion).toBeNull(); }); - it("PUT updates and returns atlas entry with description removed", async () => { + it("PUT updates and returns atlas entry with description, code links, and highlights removed", async () => { const updatedAtlas = await testSuccessfulEdit( ATLAS_WITH_MISC_SOURCE_STUDIES, ATLAS_WITH_MISC_SOURCE_STUDIES_EDIT, @@ -430,7 +434,12 @@ async function testSuccessfulEdit( const updatedOverview = updatedAtlasFromDb.overview; + expect(updatedOverview.cellxgeneAtlasCollection).toEqual( + editData.cellxgeneAtlasCollection ?? null + ); + expect(updatedOverview.codeLinks).toEqual(editData.codeLinks ?? []); expect(updatedOverview.description).toEqual(editData.description ?? ""); + expect(updatedOverview.highlights).toEqual(editData.highlights ?? ""); expect(updatedOverview.integrationLead).toEqual(editData.integrationLead); expect(updatedOverview.network).toEqual(editData.network); expect(updatedOverview.shortName).toEqual(editData.shortName); diff --git a/app/apis/catalog/hca-atlas-tracker/common/entities.ts b/app/apis/catalog/hca-atlas-tracker/common/entities.ts index 8694508d..652c15bd 100644 --- a/app/apis/catalog/hca-atlas-tracker/common/entities.ts +++ b/app/apis/catalog/hca-atlas-tracker/common/entities.ts @@ -6,9 +6,12 @@ export type APIValue = (typeof API)[APIKey]; export interface HCAAtlasTrackerListAtlas { bioNetwork: NetworkKey; + cellxgeneAtlasCollection: string | null; + codeLinks: LinkInfo[]; completedTaskCount: number; componentAtlasCount: number; description: string; + highlights: string; id: string; integrationLeadEmail: IntegrationLead["email"][]; integrationLeadName: IntegrationLead["name"][]; @@ -27,9 +30,12 @@ export interface HCAAtlasTrackerListAtlas { export interface HCAAtlasTrackerAtlas { bioNetwork: NetworkKey; + cellxgeneAtlasCollection: string | null; + codeLinks: LinkInfo[]; completedTaskCount: number; componentAtlasCount: number; description: string; + highlights: string; id: string; integrationLead: IntegrationLead[]; publication: { @@ -217,8 +223,11 @@ export interface HCAAtlasTrackerDBAtlasWithComponentAtlases } export interface HCAAtlasTrackerDBAtlasOverview { + cellxgeneAtlasCollection: string | null; + codeLinks: LinkInfo[]; completedTaskCount: number; description: string; + highlights: string; integrationLead: IntegrationLead[]; network: NetworkKey; shortName: string; @@ -455,6 +464,11 @@ export interface IntegrationLead { name: string; } +export interface LinkInfo { + label?: string; + url: string; +} + export type SourceDatasetId = string; export type SourceStudyId = string; diff --git a/app/apis/catalog/hca-atlas-tracker/common/schema.ts b/app/apis/catalog/hca-atlas-tracker/common/schema.ts index 19c95bf8..28ed5104 100644 --- a/app/apis/catalog/hca-atlas-tracker/common/schema.ts +++ b/app/apis/catalog/hca-atlas-tracker/common/schema.ts @@ -16,7 +16,15 @@ import { ROLE } from "./entities"; * Schema for data used to create a new atlas. */ export const newAtlasSchema = object({ + cellxgeneAtlasCollection: string().uuid().nullable(), + codeLinks: array().of( + object({ + label: string(), + url: string().url().required(), + }).required() + ), description: string().max(10000), + highlights: string().max(10000), integrationLead: array() .of( object({ diff --git a/app/apis/catalog/hca-atlas-tracker/common/utils.ts b/app/apis/catalog/hca-atlas-tracker/common/utils.ts index dd57c4bb..cf01ff9d 100644 --- a/app/apis/catalog/hca-atlas-tracker/common/utils.ts +++ b/app/apis/catalog/hca-atlas-tracker/common/utils.ts @@ -43,9 +43,12 @@ export function atlasInputMapper( ): HCAAtlasTrackerListAtlas { return { bioNetwork: apiAtlas.bioNetwork, + cellxgeneAtlasCollection: apiAtlas.cellxgeneAtlasCollection, + codeLinks: apiAtlas.codeLinks, completedTaskCount: apiAtlas.completedTaskCount, componentAtlasCount: apiAtlas.componentAtlasCount, description: apiAtlas.description, + highlights: apiAtlas.highlights, id: apiAtlas.id, integrationLeadEmail: apiAtlas.integrationLead.map(({ email }) => email), integrationLeadName: apiAtlas.integrationLead.map(({ name }) => name), @@ -68,9 +71,12 @@ export function dbAtlasToApiAtlas( ): HCAAtlasTrackerAtlas { return { bioNetwork: dbAtlas.overview.network, + cellxgeneAtlasCollection: dbAtlas.overview.cellxgeneAtlasCollection, + codeLinks: dbAtlas.overview.codeLinks, completedTaskCount: dbAtlas.overview.completedTaskCount, componentAtlasCount: dbAtlas.component_atlas_count, description: dbAtlas.overview.description, + highlights: dbAtlas.overview.highlights, id: dbAtlas.id, integrationLead: dbAtlas.overview.integrationLead, publication: { diff --git a/app/services/atlases.ts b/app/services/atlases.ts index a6e7ca01..27bd8491 100644 --- a/app/services/atlases.ts +++ b/app/services/atlases.ts @@ -83,7 +83,10 @@ export async function atlasInputDataToDbData( ): Promise { return { overviewData: { + cellxgeneAtlasCollection: inputData.cellxgeneAtlasCollection ?? null, + codeLinks: inputData.codeLinks ?? [], description: inputData.description ?? "", + highlights: inputData.highlights ?? "", integrationLead: inputData.integrationLead, network: inputData.network, shortName: inputData.shortName, diff --git a/migrations/1723321794859_atlas-highlights-code-cellxgene.ts b/migrations/1723321794859_atlas-highlights-code-cellxgene.ts new file mode 100644 index 00000000..db9a1c54 --- /dev/null +++ b/migrations/1723321794859_atlas-highlights-code-cellxgene.ts @@ -0,0 +1,13 @@ +import { MigrationBuilder } from "node-pg-migrate"; + +export function up(pgm: MigrationBuilder): void { + pgm.sql( + `UPDATE hat.atlases SET overview=overview||'{"cellxgeneAtlasCollection":null,"codeLinks":[],"highlights":""}'` + ); +} + +export function down(pgm: MigrationBuilder): void { + pgm.sql( + `UPDATE hat.atlases SET overview=overview-'cellxgeneAtlasCollection'-'codeLinks'-'highlights'` + ); +} diff --git a/testing/constants.ts b/testing/constants.ts index d0bcfa06..a083a63f 100644 --- a/testing/constants.ts +++ b/testing/constants.ts @@ -1537,7 +1537,10 @@ export const INTEGRATION_LEAD_BAZ_BAZ = { }; export const ATLAS_DRAFT: TestAtlas = { + cellxgeneAtlasCollection: null, + codeLinks: [], description: "bar baz baz foo baz", + highlights: "", id: ATLAS_ID_DRAFT, integrationLead: [ { @@ -1562,7 +1565,10 @@ export const ATLAS_DRAFT: TestAtlas = { }; export const ATLAS_PUBLIC: TestAtlas = { + cellxgeneAtlasCollection: "354564bb-52cb-4dea-8e2e-d3d707ca3b87", + codeLinks: [{ label: "foo", url: "https://example.com/atlas-public-foo" }], description: "foo foo bar bar foo", + highlights: "bar foo baz foo foo bar baz", id: ATLAS_ID_PUBLIC, integrationLead: [ { @@ -1584,7 +1590,10 @@ export const ATLAS_PUBLIC: TestAtlas = { }; export const ATLAS_WITH_IL: TestAtlas = { + cellxgeneAtlasCollection: null, + codeLinks: [], description: "foo baz bar baz foo baz", + highlights: "", id: "798b563d-16ff-438a-8e15-77be05b1f8ec", integrationLead: [INTEGRATION_LEAD_BAZ], network: "heart", @@ -1596,7 +1605,12 @@ export const ATLAS_WITH_IL: TestAtlas = { }; export const ATLAS_WITH_MISC_SOURCE_STUDIES: TestAtlas = { + cellxgeneAtlasCollection: "5aa910ee-23d7-419e-b2a4-8362dc058426", + codeLinks: [ + { url: "https://example.com/atlas-with-misc-source-studies-foo" }, + ], description: "bar foo bar bar foo baz", + highlights: "foo foo foo foo bar foo bar", id: ATLAS_ID_WITH_MISC_SOURCE_STUDIES, integrationLead: [INTEGRATION_LEAD_BAZ_BAZ], network: "adipose", @@ -1623,7 +1637,10 @@ export const ATLAS_WITH_MISC_SOURCE_STUDIES: TestAtlas = { }; export const ATLAS_WITH_SOURCE_STUDY_VALIDATIONS_A: TestAtlas = { + cellxgeneAtlasCollection: null, + codeLinks: [], description: "foo baz baz bar foo bar", + highlights: "", id: "7ce0814d-606c-475b-942a-0f72ff8c5c0b", integrationLead: [], network: "organoid", @@ -1641,7 +1658,10 @@ export const ATLAS_WITH_SOURCE_STUDY_VALIDATIONS_A: TestAtlas = { }; export const ATLAS_WITH_SOURCE_STUDY_VALIDATIONS_B: TestAtlas = { + cellxgeneAtlasCollection: null, + codeLinks: [], description: "baz foo baz foo bar bar foo", + highlights: "", id: "9766683a-3c8d-4ec8-b8b5-3fceb8fe0d31", integrationLead: [], network: "gut", diff --git a/testing/entities.ts b/testing/entities.ts index 419c1c53..7236f500 100644 --- a/testing/entities.ts +++ b/testing/entities.ts @@ -3,6 +3,7 @@ import { DOI_STATUS, HCAAtlasTrackerDBUnpublishedSourceStudyInfo, IntegrationLead, + LinkInfo, NetworkKey, PublicationInfo, ROLE, @@ -20,7 +21,10 @@ export interface TestUser { } export interface TestAtlas { + cellxgeneAtlasCollection: string | null; + codeLinks: LinkInfo[]; description: string; + highlights: string; id: string; integrationLead: IntegrationLead[]; network: NetworkKey; diff --git a/testing/utils.ts b/testing/utils.ts index 66a91876..01fb0519 100644 --- a/testing/utils.ts +++ b/testing/utils.ts @@ -55,8 +55,11 @@ export function makeTestAtlasOverview( atlas: TestAtlas ): HCAAtlasTrackerDBAtlasOverview { return { + cellxgeneAtlasCollection: atlas.cellxgeneAtlasCollection, + codeLinks: atlas.codeLinks, completedTaskCount: 0, description: atlas.description, + highlights: atlas.highlights, integrationLead: atlas.integrationLead, network: atlas.network, shortName: atlas.shortName, @@ -258,7 +261,12 @@ export function expectApiAtlasToMatchTest( apiAtlas: HCAAtlasTrackerAtlas, testAtlas: TestAtlas ): void { + expect(apiAtlas.cellxgeneAtlasCollection).toEqual( + testAtlas.cellxgeneAtlasCollection + ); + expect(apiAtlas.codeLinks).toEqual(testAtlas.codeLinks); expect(apiAtlas.description).toEqual(testAtlas.description); + expect(apiAtlas.highlights).toEqual(testAtlas.highlights); expect(apiAtlas.id).toEqual(testAtlas.id); expect(apiAtlas.integrationLead).toEqual(testAtlas.integrationLead); expect(apiAtlas.bioNetwork).toEqual(testAtlas.network);