diff --git a/packages/api/src/datasets/index.integration.test.ts b/packages/api/src/datasets/index.integration.test.ts index 58ff3be64..e8d5981f5 100644 --- a/packages/api/src/datasets/index.integration.test.ts +++ b/packages/api/src/datasets/index.integration.test.ts @@ -30,7 +30,7 @@ describe('search', () => { const result = await datasets.search(); expect(result).toMatchObject({ - totalCount: 14, + totalCount: 15, }); }); }); diff --git a/packages/api/src/datasets/searcher.integration.test.ts b/packages/api/src/datasets/searcher.integration.test.ts index 0fc43ba11..ad00a2229 100644 --- a/packages/api/src/datasets/searcher.integration.test.ts +++ b/packages/api/src/datasets/searcher.integration.test.ts @@ -21,7 +21,7 @@ describe('search', () => { const result = await datasetSearcher.search(); expect(result).toStrictEqual({ - totalCount: 14, + totalCount: 15, offset: 0, limit: 10, sortBy: 'name', @@ -687,6 +687,11 @@ describe('search', () => { id: 'The Museum', name: 'The Museum', }, + { + totalCount: 1, + id: 'NIOD Institute for War, Holocaust and Genocide Studies', + name: 'NIOD Institute for War, Holocaust and Genocide Studies', + }, { totalCount: 1, id: 'Research Organisation', @@ -695,7 +700,7 @@ describe('search', () => { ], licenses: [ { - totalCount: 6, + totalCount: 7, id: 'CC0 1.0 Universal (CC0 1.0) Public Domain Dedication', name: 'CC0 1.0 Universal (CC0 1.0) Public Domain Dedication', }, diff --git a/packages/api/src/definitions.ts b/packages/api/src/definitions.ts index cc8153839..557dd35e6 100644 --- a/packages/api/src/definitions.ts +++ b/packages/api/src/definitions.ts @@ -76,6 +76,11 @@ export type HeritageObject = Thing & { isPartOf?: Dataset; }; +export type Event = { + id: string; + date?: TimeSpan; +}; + export enum ProvenanceEventType { Acquisition = 'acquisition', TransferOfCustody = 'transferOfCustody', diff --git a/packages/api/src/enrichments/searcher-constituents-wikidata.integration.test.ts b/packages/api/src/enrichments/searcher-constituents-wikidata.integration.test.ts index 2acb06872..0e297bbdc 100644 --- a/packages/api/src/enrichments/searcher-constituents-wikidata.integration.test.ts +++ b/packages/api/src/enrichments/searcher-constituents-wikidata.integration.test.ts @@ -25,14 +25,14 @@ describe('search', () => { description: 'uitgeverij uit Utrecht', }, { - id: 'http://www.wikidata.org/entity/Q105964347', + id: 'http://www.wikidata.org/entity/Q131287465', name: 'Rembrandt', - description: 'Metaalwarenfabriek Rembrandt BV', + description: 'fotograaf', }, { - id: 'http://www.wikidata.org/entity/Q17330745', - name: 'Rembrandts vader', - description: 'Nederlands acteur', + id: 'http://www.wikidata.org/entity/Q375926', + name: 'Rembrandt Peale', + description: 'Amerikaans kunstschilder (1778-1860)', }, ], }); @@ -48,9 +48,9 @@ describe('search', () => { expect(result).toStrictEqual({ things: [ { - id: 'http://www.wikidata.org/entity/Q352864', - name: 'Pieter Lastman', - description: 'Dutch painter', + id: 'http://www.wikidata.org/entity/Q29885090', + name: 'Neeltje Willemsdr. Zuytbrouck', + description: "Rembrandt's mother", }, { id: 'http://www.wikidata.org/entity/Q105964347', @@ -60,7 +60,7 @@ describe('search', () => { { id: 'http://www.wikidata.org/entity/Q1300641', name: 'Rembrandt Bugatti', - description: '1884-1916 Italian sculptor', + description: 'Italian sculptor (1884–1916)', }, ], }); diff --git a/packages/api/src/objects/fetcher.ts b/packages/api/src/objects/fetcher.ts index 4484f6cab..ea43d9b8a 100644 --- a/packages/api/src/objects/fetcher.ts +++ b/packages/api/src/objects/fetcher.ts @@ -173,6 +173,7 @@ export class HeritageObjectFetcher { FILTER(LANG(?typeName) = "${options.locale}") } + # For BC: data providers ought to use the same thesauri OPTIONAL { ?type rdfs:label ?typeName FILTER(LANG(?typeName) = "" || LANG(?typeName) = "${options.locale}") @@ -215,6 +216,7 @@ export class HeritageObjectFetcher { FILTER(LANG(?materialName) = "${options.locale}") } + # For BC: data providers ought to use the same thesauri OPTIONAL { ?material rdfs:label ?materialName FILTER(LANG(?materialName) = "" || LANG(?materialName) = "${options.locale}") diff --git a/packages/api/src/objects/searcher.integration.test.ts b/packages/api/src/objects/searcher.integration.test.ts index 19b04cea0..db888f6da 100644 --- a/packages/api/src/objects/searcher.integration.test.ts +++ b/packages/api/src/objects/searcher.integration.test.ts @@ -108,6 +108,25 @@ describe('search', () => { name: 'Vincent van Gogh', }, ]), + types: expect.arrayContaining([ + { + id: expect.stringContaining( + 'https://data.colonialcollections.nl/.well-known/genid/' + ), + }, + ]), + materials: expect.arrayContaining([ + { + id: expect.stringContaining( + 'https://data.colonialcollections.nl/.well-known/genid/' + ), + }, + { + id: expect.stringContaining( + 'https://data.colonialcollections.nl/.well-known/genid/' + ), + }, + ]), dateCreated: { id: expect.stringContaining( 'https://data.colonialcollections.nl/.well-known/genid/' diff --git a/packages/api/src/research-guides/definitions.ts b/packages/api/src/research-guides/definitions.ts index f73a20608..7f7933632 100644 --- a/packages/api/src/research-guides/definitions.ts +++ b/packages/api/src/research-guides/definitions.ts @@ -1,12 +1,13 @@ -import {Place, Term, Thing} from '../definitions'; +import {Event, Place, Term, Thing} from '../definitions'; export type Citation = Thing & {url?: string}; export type ResearchGuide = Thing & { - identifier?: string; + alternateNames?: string[]; abstract?: string; text?: string; encodingFormat?: string; + contentReferenceTimes?: Event[]; contentLocations?: Place[]; keywords?: Term[]; citations?: Citation[]; diff --git a/packages/api/src/research-guides/fetcher.integration.test.ts b/packages/api/src/research-guides/fetcher.integration.test.ts index 288cccf47..9413ee33e 100644 --- a/packages/api/src/research-guides/fetcher.integration.test.ts +++ b/packages/api/src/research-guides/fetcher.integration.test.ts @@ -17,96 +17,150 @@ describe('getTopLevels', () => { // The sorting order is undefined and can change - don't use toStrictEqual() expect(researchGuides).toMatchObject([ { - id: 'https://guides.example.org/top-set', + id: 'https://guides.example.org/topset', name: 'Digital research guide', abstract: 'Research aides for conducting provenance research into colonial collections', text: 'On this page you find various research aides that can assist...', encodingFormat: 'text/markdown', - seeAlso: expect.arrayContaining([ + seeAlso: [ { - id: 'https://guides.example.org/level-3-set', - identifier: '3', - seeAlso: expect.arrayContaining([ + id: 'https://guides.example.org/subset3', + name: '3. Name', + seeAlso: [ { - id: 'https://guides.example.org/level-3c', - name: 'Kunsthandel Van Lier', + id: 'https://guides.example.org/guide6', + name: 'Royal Cabinet of Curiosities', seeAlso: [ { - id: 'https://guides.example.org/level-2a', - name: 'Military and navy', + id: 'https://guides.example.org/guide5', + name: 'Trade', + seeAlso: [ + { + id: 'https://guides.example.org/guide7', + name: 'Kunsthandel Van Lier', + }, + ], }, ], }, { - id: 'https://guides.example.org/level-3a', - name: 'Royal Cabinet of Curiosities', + id: 'https://guides.example.org/guide7', + name: 'Kunsthandel Van Lier', seeAlso: [ { - id: 'https://guides.example.org/level-2c', - name: 'Trade', + id: 'https://guides.example.org/guide4', + name: 'Military and navy', + seeAlso: [ + { + id: 'https://guides.example.org/guide1', + name: 'Doing research', + }, + { + id: 'https://guides.example.org/guide6', + name: 'Royal Cabinet of Curiosities', + }, + ], }, ], }, - ]), + ], }, { - id: 'https://guides.example.org/level-2-set', - identifier: '2', - seeAlso: expect.arrayContaining([ + id: 'https://guides.example.org/subset1', + name: '1. Name', + seeAlso: [ { - id: 'https://guides.example.org/level-2c', - name: 'Trade', + id: 'https://guides.example.org/guide1', + name: 'Doing research', seeAlso: [ { - id: 'https://guides.example.org/level-3c', - name: 'Kunsthandel Van Lier', + id: 'https://guides.example.org/guide4', + name: 'Military and navy', + seeAlso: [ + { + id: 'https://guides.example.org/guide1', + name: 'Doing research', + }, + { + id: 'https://guides.example.org/guide6', + name: 'Royal Cabinet of Curiosities', + }, + ], }, ], }, { - id: 'https://guides.example.org/level-2a', - name: 'Military and navy', + id: 'https://guides.example.org/guide2', + name: 'How can I use the data hub for my research?', + }, + { + id: 'https://guides.example.org/guide3', + name: 'Sources', seeAlso: [ { - id: 'https://guides.example.org/level-3a', - name: 'Royal Cabinet of Curiosities', + id: 'https://guides.example.org/guide5', + name: 'Trade', + seeAlso: [ + { + id: 'https://guides.example.org/guide7', + name: 'Kunsthandel Van Lier', + }, + ], }, ], }, - ]), + ], }, { - id: 'https://guides.example.org/level-1-set', - identifier: '1', - seeAlso: expect.arrayContaining([ + id: 'https://guides.example.org/subset2', + name: '2. Name', + seeAlso: [ { - id: 'https://guides.example.org/level-1b', - name: 'How can I use the data hub for my research?', - }, - { - id: 'https://guides.example.org/level-1c', - name: 'Sources', + id: 'https://guides.example.org/guide4', + name: 'Military and navy', seeAlso: [ { - id: 'https://guides.example.org/level-2c', - name: 'Trade', + id: 'https://guides.example.org/guide1', + name: 'Doing research', + seeAlso: [ + { + id: 'https://guides.example.org/guide4', + name: 'Military and navy', + }, + ], + }, + { + id: 'https://guides.example.org/guide6', + name: 'Royal Cabinet of Curiosities', + seeAlso: [ + { + id: 'https://guides.example.org/guide5', + name: 'Trade', + }, + ], }, ], }, { - id: 'https://guides.example.org/level-1a', - name: 'Doing research', + id: 'https://guides.example.org/guide5', + name: 'Trade', seeAlso: [ { - id: 'https://guides.example.org/level-2a', - name: 'Military and navy', + id: 'https://guides.example.org/guide7', + name: 'Kunsthandel Van Lier', + seeAlso: [ + { + id: 'https://guides.example.org/guide4', + name: 'Military and navy', + }, + ], }, ], }, - ]), + ], }, - ]), + ], }, ]); }); @@ -130,17 +184,17 @@ describe('getByIds', () => { it('returns the research guides that match the IDs', async () => { const researchGuides = await researchGuideFetcher.getByIds({ ids: [ - 'https://guides.example.org/level-2a', - 'https://guides.example.org/level-2c', + 'https://guides.example.org/guide1', + 'https://guides.example.org/guide4', ], }); expect(researchGuides).toMatchObject([ { - id: 'https://guides.example.org/level-2a', + id: 'https://guides.example.org/guide1', }, { - id: 'https://guides.example.org/level-2c', + id: 'https://guides.example.org/guide4', }, ]); }); @@ -165,23 +219,42 @@ describe('getById', () => { it('returns the research guide that matches the ID', async () => { const researchGuide = await researchGuideFetcher.getById({ - id: 'https://guides.example.org/level-2a', + id: 'https://guides.example.org/guide4', }); expect(researchGuide).toStrictEqual({ - id: 'https://guides.example.org/level-2a', + id: 'https://guides.example.org/guide4', name: 'Military and navy', + alternateNames: ['Navy'], abstract: 'Army and Navy personnel who operated in colonized territories collected objects in various ways during the colonial era.', text: 'Dutch authority in the [Dutch East Indies](https://www.geonames.org/1643084/republic-of-indonesia.html), [Suriname](https://www.geonames.org/3382998/republic-of-suriname.html) and on the [Caribbean Islands](https://www.geonames.org/8505032/netherlands-antilles.html) relied heavily on the use of the military...', encodingFormat: 'text/markdown', + contentReferenceTimes: expect.arrayContaining([ + { + id: expect.stringContaining( + 'https://data.colonialcollections.nl/.well-known/genid/' + ), + date: { + id: expect.stringContaining( + 'https://data.colonialcollections.nl/.well-known/genid/' + ), + startDate: new Date('1924-01-01T00:00:00.000Z'), + endDate: new Date('1996-12-31T23:59:59.999Z'), + }, + }, + ]), seeAlso: expect.arrayContaining([ { - id: 'https://guides.example.org/level-3a', + id: 'https://guides.example.org/guide6', name: 'Royal Cabinet of Curiosities', }, + { + id: 'https://guides.example.org/guide1', + name: 'Doing research', + }, ]), - contentLocations: [ + contentLocations: expect.arrayContaining([ { id: expect.stringContaining( 'https://data.colonialcollections.nl/.well-known/genid/' @@ -189,8 +262,8 @@ describe('getById', () => { name: 'Netherlands Antilles', sameAs: 'https://www.geonames.org/8505032/netherlands-antilles.html', }, - ], - keywords: [ + ]), + keywords: expect.arrayContaining([ { id: expect.stringContaining( 'https://data.colonialcollections.nl/.well-known/genid/' @@ -198,8 +271,8 @@ describe('getById', () => { name: 'Midshipman', sameAs: 'https://www.wikidata.org/wiki/Q11141137', }, - ], - citations: [ + ]), + citations: expect.arrayContaining([ { id: expect.stringContaining( 'https://data.colonialcollections.nl/.well-known/genid/' @@ -209,7 +282,7 @@ describe('getById', () => { 'Via Delpher, the editions can be found by selecting the title', url: 'https://www.delpher.nl/', }, - ], + ]), }); }); }); @@ -217,21 +290,26 @@ describe('getById', () => { describe('get with localized names', () => { it('returns a research guide with English names', async () => { const researchGuide = await researchGuideFetcher.getById({ - id: 'https://guides.example.org/level-2a', + id: 'https://guides.example.org/guide4', locale: 'en', }); expect(researchGuide).toMatchObject({ - id: 'https://guides.example.org/level-2a', + id: 'https://guides.example.org/guide4', name: 'Military and navy', + alternateNames: expect.arrayContaining(['Navy']), abstract: 'Army and Navy personnel who operated in colonized territories collected objects in various ways during the colonial era.', text: 'Dutch authority in the [Dutch East Indies](https://www.geonames.org/1643084/republic-of-indonesia.html), [Suriname](https://www.geonames.org/3382998/republic-of-suriname.html) and on the [Caribbean Islands](https://www.geonames.org/8505032/netherlands-antilles.html) relied heavily on the use of the military...', seeAlso: expect.arrayContaining([ { - id: 'https://guides.example.org/level-3a', + id: 'https://guides.example.org/guide6', name: 'Royal Cabinet of Curiosities', }, + { + id: 'https://guides.example.org/guide1', + name: 'Doing research', + }, ]), contentLocations: [ { @@ -255,21 +333,26 @@ describe('get with localized names', () => { it('returns a research guide with Dutch names', async () => { const researchGuide = await researchGuideFetcher.getById({ - id: 'https://guides.example.org/level-2a', + id: 'https://guides.example.org/guide4', locale: 'nl', }); expect(researchGuide).toMatchObject({ - id: 'https://guides.example.org/level-2a', + id: 'https://guides.example.org/guide4', name: 'Leger en Marine', + alternateNames: expect.arrayContaining(['Marine', 'Zeemacht']), abstract: 'Leger- en marinepersoneel dat actief was in gekoloniseerde gebieden, verzamelde op verschillende manieren objecten tijdens het koloniale tijdperk.', text: 'Het Nederlandse gezag in [Nederlands-Indië](https://www.geonames.org/1643084/republic-of-indonesia.html), [Suriname](https://www.geonames.org/3382998/republic-of-suriname.html) en op de [Caribische eilanden](https://www.geonames.org/8505032/netherlands-antilles.html) steunde in belangrijke mate op de inzet van het leger.', seeAlso: expect.arrayContaining([ { - id: 'https://guides.example.org/level-3a', + id: 'https://guides.example.org/guide6', name: 'Koninklijk Kabinet van Zeldzaamheden', }, + { + id: 'https://guides.example.org/guide1', + name: 'Onderzoeken', + }, ]), contentLocations: [ { diff --git a/packages/api/src/research-guides/fetcher.ts b/packages/api/src/research-guides/fetcher.ts index 39ca255cc..d77c6edc6 100644 --- a/packages/api/src/research-guides/fetcher.ts +++ b/packages/api/src/research-guides/fetcher.ts @@ -70,7 +70,7 @@ export class ResearchGuideFetcher { ex:seeAlso ?subSet . ?subSet a ex:CreativeWork ; - ex:identifier ?identifier ; + ex:name ?subSetName ; ex:seeAlso ?guide . ?guide a ex:CreativeWork ; @@ -78,7 +78,7 @@ export class ResearchGuideFetcher { ex:seeAlso ?relatedGuide . ?relatedGuide a ex:CreativeWork ; - ex:name ?subGuideName . + ex:name ?relatedGuideName . } WHERE { VALUES ?topSet { @@ -86,7 +86,7 @@ export class ResearchGuideFetcher { } OPTIONAL { - ?topSet schema:name ?topSetName ; + ?topSet schema:name ?topSetName FILTER(LANG(?topSetName) = "${options.locale}") } @@ -96,7 +96,7 @@ export class ResearchGuideFetcher { } OPTIONAL { - ?topSet schema:text ?topSetText ; + ?topSet schema:text ?topSetText FILTER(LANG(?topSetText) = "${options.locale}") } @@ -106,10 +106,8 @@ export class ResearchGuideFetcher { OPTIONAL { ?topSet la:has_member ?subSet . - - OPTIONAL { - ?subSet crm:P1_is_identified_by/crm:P190_has_symbolic_content ?identifier - } + ?subSet schema:name ?subSetName + FILTER(LANG(?subSetName) = "${options.locale}") # Get a selection of information from member guides, if any OPTIONAL { @@ -171,20 +169,22 @@ export class ResearchGuideFetcher { CONSTRUCT { ?this a ex:CreativeWork ; ex:name ?name ; + ex:alternateName ?alternateName ; ex:abstract ?abstract ; ex:text ?text ; ex:encodingFormat ?encodingFormat ; ex:seeAlso ?relatedGuide ; - ex:contentLocation ?contentLocation ; + ex:contentLocation ?spatial ; ex:keyword ?keyword ; - ex:citation ?citation . + ex:citation ?citation ; + ex:contentReferenceTime ?contentReferenceTime . ?relatedGuide a ex:CreativeWork ; ex:name ?relatedGuideName . - ?contentLocation a ex:Place ; - ex:name ?contentLocationName ; - ex:sameAs ?contentLocationSameAs . + ?spatial a ex:Place ; + ex:name ?spatialName ; + ex:sameAs ?spatialSameAs . ?keyword a ex:DefinedTerm ; ex:name ?keywordName ; @@ -194,6 +194,10 @@ export class ResearchGuideFetcher { ex:name ?citationName ; ex:description ?citationDescription ; ex:url ?citationUrl . + + ?contentReferenceTime a ex:Event ; + ex:startDate ?contentReferenceTimeStartDate ; + ex:endDate ?contentReferenceTimeEndDate . } WHERE { VALUES ?this { @@ -204,22 +208,27 @@ export class ResearchGuideFetcher { schema:additionalType . # "Guides" OPTIONAL { - ?this schema:name ?name . + ?this schema:name ?name FILTER(LANG(?name) = "${options.locale}") } OPTIONAL { - ?this schema:abstract ?abstract . + ?this schema:alternateName ?alternateName + FILTER(LANG(?alternateName) = "${options.locale}") + } + + OPTIONAL { + ?this schema:abstract ?abstract FILTER(LANG(?abstract) = "${options.locale}") } OPTIONAL { - ?this schema:text ?text . + ?this schema:text ?text FILTER(LANG(?text) = "${options.locale}") } OPTIONAL { - ?this schema:encodingFormat ?encodingFormat . + ?this schema:encodingFormat ?encodingFormat } # Get a selection of information from related guides, if any @@ -230,15 +239,15 @@ export class ResearchGuideFetcher { } OPTIONAL { - ?this schema:contentLocation ?contentLocation . + ?this schema:spatial ?spatial . OPTIONAL { - ?contentLocation schema:name ?contentLocationName . - FILTER(LANG(?contentLocationName) = "${options.locale}") + ?spatial schema:name ?spatialName + FILTER(LANG(?spatialName) = "${options.locale}") } OPTIONAL { - ?contentLocation schema:sameAs ?contentLocationSameAs + ?spatial schema:sameAs ?spatialSameAs } } @@ -246,7 +255,7 @@ export class ResearchGuideFetcher { ?this schema:keywords ?keyword . OPTIONAL { - ?keyword schema:name ?keywordName . + ?keyword schema:name ?keywordName FILTER(LANG(?keywordName) = "${options.locale}") } @@ -259,17 +268,29 @@ export class ResearchGuideFetcher { ?this schema:citation ?citation . OPTIONAL { - ?citation schema:name ?citationName . + ?citation schema:name ?citationName FILTER(LANG(?citationName) = "${options.locale}") } OPTIONAL { - ?citation schema:description ?citationDescription . + ?citation schema:description ?citationDescription FILTER(LANG(?citationDescription) = "${options.locale}") } OPTIONAL { - ?citation schema:url ?citationUrl . + ?citation schema:url ?citationUrl + } + } + + OPTIONAL { + ?this schema:contentReferenceTime ?contentReferenceTime . + + OPTIONAL { + ?contentReferenceTime schema:startDate ?contentReferenceTimeStartDate + } + + OPTIONAL { + ?contentReferenceTime schema:endDate ?contentReferenceTimeEndDate } } } diff --git a/packages/api/src/research-guides/index.integration.test.ts b/packages/api/src/research-guides/index.integration.test.ts index bc13d1f10..3d2e3ed61 100644 --- a/packages/api/src/research-guides/index.integration.test.ts +++ b/packages/api/src/research-guides/index.integration.test.ts @@ -26,8 +26,8 @@ describe('getByIds', () => { it('returns the research guides', async () => { const results = await researchGuides.getByIds({ ids: [ - 'https://guides.example.org/level-2a', - 'https://guides.example.org/level-2c', + 'https://guides.example.org/guide1', + 'https://guides.example.org/guide4', ], }); @@ -38,7 +38,7 @@ describe('getByIds', () => { describe('getById', () => { it('returns the research guide', async () => { const researchGuide = await researchGuides.getById({ - id: 'https://guides.example.org/level-2a', + id: 'https://guides.example.org/guide1', }); expect(researchGuide).not.toBeUndefined(); diff --git a/packages/api/src/research-guides/index.ts b/packages/api/src/research-guides/index.ts index d87fc4980..21273b9f4 100644 --- a/packages/api/src/research-guides/index.ts +++ b/packages/api/src/research-guides/index.ts @@ -6,6 +6,9 @@ import { ResearchGuideFetcher, } from './fetcher'; +// Re-export definitions for ease of use in consuming apps +export * from './definitions'; + const constructorOptionsSchema = z.object({ sparqlEndpointUrl: z.string(), }); diff --git a/packages/api/src/research-guides/rdf-helpers.test.ts b/packages/api/src/research-guides/rdf-helpers.test.ts index 88c888956..ef04b277d 100644 --- a/packages/api/src/research-guides/rdf-helpers.test.ts +++ b/packages/api/src/research-guides/rdf-helpers.test.ts @@ -2,7 +2,11 @@ import {describe, expect, it} from '@jest/globals'; import {StreamParser} from 'n3'; import {RdfObjectLoader, Resource} from 'rdf-object'; import streamifyString from 'streamify-string'; -import {createCitations, createResearchGuide} from './rdf-helpers'; +import { + createCitations, + createEvents, + createResearchGuide, +} from './rdf-helpers'; const loader = new RdfObjectLoader({ context: { @@ -19,11 +23,16 @@ beforeAll(async () => { ex:name "Name 1" . ex:researchGuide2 a ex:CreativeWork ; - ex:identifier "1" ; ex:name "Name 2" ; + ex:alternateName "Alternate name 2", "Alternate name 3" ; ex:abstract "Abstract 2" ; ex:text "Text" ; ex:encodingFormat "text/html" ; + ex:contentReferenceTime [ + a ex:Event ; + ex:startDate "1924" ; + ex:endDate "1996" ; + ] ; ex:contentLocation [ ex:name "Content Location" ; ex:sameAs ; @@ -65,6 +74,35 @@ beforeAll(async () => { await loader.import(streamParser); }); +describe('contentReferenceTimes', () => { + let resource: Resource; + + beforeEach(() => { + resource = loader.resources['https://example.org/researchGuide2']; + }); + + it('returns undefined if property does not exist', () => { + const citations = createCitations(resource, 'ex:unknown'); + + expect(citations).toBeUndefined(); + }); + + it('returns events if property exists', () => { + const events = createEvents(resource, 'ex:contentReferenceTime'); + + expect(events).toStrictEqual([ + { + id: expect.any(String), + date: { + id: expect.any(String), + startDate: new Date('1924-01-01T00:00:00.000Z'), + endDate: new Date('1996-12-31T23:59:59.999Z'), + }, + }, + ]); + }); +}); + describe('createCitations', () => { let resource: Resource; @@ -83,7 +121,7 @@ describe('createCitations', () => { expect(citations).toStrictEqual([ { - id: 'n3-2', + id: expect.any(String), name: 'Citation', description: 'Citation Description', url: 'https://example.org/citation', @@ -109,11 +147,21 @@ describe('createResearchGuide', () => { expect(researchGuide).toStrictEqual({ id: 'https://example.org/researchGuide2', - identifier: '1', name: 'Name 2', + alternateNames: ['Alternate name 2', 'Alternate name 3'], abstract: 'Abstract 2', text: 'Text', encodingFormat: 'text/html', + contentReferenceTimes: [ + { + id: expect.any(String), + date: { + id: expect.any(String), + startDate: new Date('1924-01-01T00:00:00.000Z'), + endDate: new Date('1996-12-31T23:59:59.999Z'), + }, + }, + ], seeAlso: [ { id: 'https://example.org/researchGuide3', @@ -136,21 +184,21 @@ describe('createResearchGuide', () => { ], contentLocations: [ { - id: 'n3-0', + id: expect.any(String), name: 'Content Location', sameAs: 'https://example.org/place', }, ], keywords: [ { - id: 'n3-1', + id: expect.any(String), name: 'Keyword', sameAs: 'https://example.org/keyword', }, ], citations: [ { - id: 'n3-2', + id: expect.any(String), name: 'Citation', description: 'Citation Description', url: 'https://example.org/citation', diff --git a/packages/api/src/research-guides/rdf-helpers.ts b/packages/api/src/research-guides/rdf-helpers.ts index f09bdbd25..32e0050bd 100644 --- a/packages/api/src/research-guides/rdf-helpers.ts +++ b/packages/api/src/research-guides/rdf-helpers.ts @@ -1,13 +1,14 @@ import { createPlaces, createThings, + createTimeSpan, getPropertyValues, onlyOne, removeNullish, } from '../rdf-helpers'; import type {Resource} from 'rdf-object'; import {Citation, ResearchGuide} from './definitions'; -import {Term} from '../definitions'; +import {Event, Term} from '../definitions'; function createCitation(citationResource: Resource) { const name = onlyOne(getPropertyValues(citationResource, 'ex:name')); @@ -46,14 +47,33 @@ function createResearchGuides( return researchGuides.length > 0 ? researchGuides : undefined; } +function createEvent(eventResource: Resource) { + const timespan = createTimeSpan(eventResource); + + const event: Event = { + id: eventResource.value, + date: timespan, + }; + + return event; +} + +export function createEvents(resource: Resource, propertyName: string) { + const properties = resource.properties[propertyName]; + const events = properties.map(property => createEvent(property)); + + return events.length > 0 ? events : undefined; +} + export function createResearchGuide( researchGuideResource: Resource, stackSize = 1 ) { - const identifier = onlyOne( - getPropertyValues(researchGuideResource, 'ex:identifier') - ); const name = onlyOne(getPropertyValues(researchGuideResource, 'ex:name')); + const alternateNames = getPropertyValues( + researchGuideResource, + 'ex:alternateName' + ); const abstract = onlyOne( getPropertyValues(researchGuideResource, 'ex:abstract') ); @@ -65,7 +85,7 @@ export function createResearchGuide( let seeAlso: ResearchGuide[] | undefined = undefined; // Prevent infinite recursion - if (stackSize < 4) { + if (stackSize < 5) { seeAlso = createResearchGuides( researchGuideResource, 'ex:seeAlso', @@ -73,6 +93,10 @@ export function createResearchGuide( ); } + const contentReferenceTimes = createEvents( + researchGuideResource, + 'ex:contentReferenceTime' + ); const contentLocations = createPlaces( researchGuideResource, 'ex:contentLocation' @@ -82,11 +106,12 @@ export function createResearchGuide( const researchGuideWithUndefinedValues: ResearchGuide = { id: researchGuideResource.value, - identifier, name, + alternateNames, abstract, text, encodingFormat, + contentReferenceTimes, seeAlso, contentLocations, keywords,