From 6e24253e1505782483d23699be6f031f296b64c7 Mon Sep 17 00:00:00 2001 From: LiamStokingerContentful Date: Thu, 5 Dec 2024 15:59:24 +0100 Subject: [PATCH] feat: [GROOT-1552] implement taxonomy methods in CDA --- lib/create-contentful-api.ts | 102 +++++++++++++++++++++ lib/types/client.ts | 80 ++++++++++++++++ lib/types/concept-scheme.ts | 40 ++++++++ lib/types/concept.ts | 76 +++++++++++++++ lib/types/index.ts | 2 + lib/types/query/order.ts | 4 + lib/types/query/query.ts | 12 +++ test/integration/getConcept.test.ts | 17 ++++ test/integration/getConceptScheme.test.ts | 17 ++++ test/integration/getConceptSchemes.test.ts | 54 +++++++++++ test/integration/getConcepts.test.ts | 62 +++++++++++++ 11 files changed, 466 insertions(+) create mode 100644 lib/types/concept-scheme.ts create mode 100644 lib/types/concept.ts create mode 100644 test/integration/getConcept.test.ts create mode 100644 test/integration/getConceptScheme.test.ts create mode 100644 test/integration/getConceptSchemes.test.ts create mode 100644 test/integration/getConcepts.test.ts diff --git a/lib/create-contentful-api.ts b/lib/create-contentful-api.ts index 345259205..c11d56226 100644 --- a/lib/create-contentful-api.ts +++ b/lib/create-contentful-api.ts @@ -24,6 +24,10 @@ import type { SyncOptions, EntrySkeletonType, LocaleCode, + ConceptCollection, + Concept, + ConceptScheme, + ConceptSchemeCollection, } from './types/index.js' import normalizeSearchParameters from './utils/normalize-search-parameters.js' import normalizeSelect from './utils/normalize-select.js' @@ -467,6 +471,98 @@ export default function createContentfulApi( }) } + function getConceptScheme( + id: string, + query: Record = {}, + ): Promise> { + return internalGetConceptScheme(id, query) + } + + async function internalGetConceptScheme( + id: string, + query: Record = {}, + ): Promise> { + try { + return get({ + context: 'environment', + path: `taxonomy/concept-schemes/${id}`, + config: createRequestConfig({ + query: normalizeSearchParameters(normalizeSelect(query)), + }), + }) + } catch (error) { + errorHandler(error) + } + } + + function getConceptSchemes( + query: Record = {}, + ): Promise> { + return internalGetConceptSchemes(query) + } + + async function internalGetConceptSchemes( + query: Record = {}, + ): Promise> { + try { + return get({ + context: 'environment', + path: 'taxonomy/concept-schemes', + config: createRequestConfig({ + query: normalizeSearchParameters(normalizeSelect(query)), + }), + }) + } catch (error) { + errorHandler(error) + } + } + + function getConcept( + id: string, + query: Record = {}, + ): Promise> { + return internalGetConcept(id, query) + } + + async function internalGetConcept( + id: string, + query: Record = {}, + ): Promise> { + try { + return get({ + context: 'environment', + path: `taxonomy/concepts/${id}`, + config: createRequestConfig({ + query: normalizeSearchParameters(normalizeSelect(query)), + }), + }) + } catch (error) { + errorHandler(error) + } + } + + function getConcepts( + query: Record = {}, + ): Promise> { + return internalGetConcepts(query) + } + + async function internalGetConcepts( + query: Record = {}, + ): Promise> { + try { + return get({ + context: 'environment', + path: 'taxonomy/concepts', + config: createRequestConfig({ + query: normalizeSearchParameters(normalizeSelect(query)), + }), + }) + } catch (error) { + errorHandler(error) + } + } + /* * Switches BaseURL to use /environments path * */ @@ -495,6 +591,12 @@ export default function createContentfulApi( getEntry, getEntries, + getConceptScheme, + getConceptSchemes, + + getConcept, + getConcepts, + createAssetKey, } as unknown as ContentfulClientApi } diff --git a/lib/types/client.ts b/lib/types/client.ts index 14192d2cd..d58799038 100644 --- a/lib/types/client.ts +++ b/lib/types/client.ts @@ -4,6 +4,8 @@ import { LocaleCode, LocaleCollection } from './locale.js' import { AssetQueries, AssetsQueries, + ConceptSchemesQueries, + ConceptsQueries, EntriesQueries, EntryQueries, EntrySkeletonType, @@ -14,6 +16,8 @@ import { Tag, TagCollection } from './tag.js' import { AssetKey } from './asset-key.js' import { Entry, EntryCollection } from './entry.js' import { Asset, AssetCollection, AssetFields } from './asset.js' +import { Concept, ConceptCollection } from './concept.js' +import { ConceptScheme, ConceptSchemeCollection } from './concept-scheme.js' /** * Client chain modifiers used in all types that depend on the client configuration. @@ -202,6 +206,82 @@ export interface ContentfulClientApi { */ getTags(query?: TagQueries): Promise + /** + * Fetches a Concept + * @param id - The concept’s ID + * @returns Promise for a concept + * @example + * ```typescript + * const contentful = require('contentful') + * + * const client = contentful.createClient({ + * space: '', + * accessToken: '' + * }) + * + * const concept = await client.getConcept('') + * console.log(concept) + * ``` + */ + getConcept(id: string): Promise> + + /** + * Fetches a collection of Concepts + * @param query - Object with search parameters + * @returns Promise for a collection of Concepts + * @example + * ```typescript + * const contentful = require('contentful') + * + * const client = contentful.createClient({ + * space: '', + * accessToken: '' + * }) + * + * const response = await client.getConcepts() + * console.log(response.items) + * ``` + */ + getConcepts(query?: ConceptsQueries): Promise> + + /** + * Fetches a Concept Scheme + * @param id - The concept scheme's ID + * @returns Promise for a concept scheme + * @example + * ```typescript + * const contentful = require('contentful') + * + * const client = contentful.createClient({ + * space: '', + * accessToken: '' + * }) + * + * const conceptScheme = await client.getConceptScheme('') + * console.log(conceptScheme) + * ``` + */ + getConceptScheme(id: string): Promise> + + /** + * Fetches a collection of Concept Schemes + * @param query - Object with search parameters + * @returns Promise for a collection of Concept Schemes + * @example + * ```typescript + * const contentful = require('contentful') + * + * const client = contentful.createClient({ + * space: '', + * accessToken: '' + * }) + * + * const response = await client.getConceptSchemes() + * console.log(response.items) + * ``` + */ + getConceptSchemes(query?: ConceptSchemesQueries): Promise> + /** * Creates an asset key for signing asset URLs (Embargoed Assets) * @returns Promise for an asset key diff --git a/lib/types/concept-scheme.ts b/lib/types/concept-scheme.ts new file mode 100644 index 000000000..ac32e06f5 --- /dev/null +++ b/lib/types/concept-scheme.ts @@ -0,0 +1,40 @@ +import { Link } from './link' +import { LocaleCode } from './locale' + +type ISODateString = string + +export type ConceptSchemeSys = { + id: string + type: 'TaxonomyConceptScheme' + createdAt: ISODateString + updatedAt: ISODateString + version: number +} + +export interface ConceptScheme { + sys: ConceptSchemeSys + uri?: string + prefLabel: { + [locale in Locales]: string + } + definition?: + | { + [locale in Locales]: string + } + | null + topConcepts: Link<'TaxonomyConcept'>[] + concepts: Link<'TaxonomyConcept'>[] + totalConcepts: number +} + +export type ConceptSchemeCollection = { + sys: { + type: 'Array' + } + items: ConceptScheme[] + limit: number + pages?: { + prev?: string + next?: string + } +} diff --git a/lib/types/concept.ts b/lib/types/concept.ts new file mode 100644 index 000000000..45691be84 --- /dev/null +++ b/lib/types/concept.ts @@ -0,0 +1,76 @@ +import { Link } from './link' +import { LocaleCode } from './locale' + +type ISODateString = string + +export type ConceptSys = { + id: string + type: 'TaxonomyConcept' + createdAt: ISODateString + updatedAt: ISODateString + version: number +} + +export interface Concept { + sys: ConceptSys + uri?: string + prefLabel: { + [locale in Locales]: string + } + altLabels?: { + [locale in Locales]: string[] + } + hiddenLabels?: { + [locale in Locales]: string[] + } + note?: + | { + [locale in Locales]: string + } + | null + changeNote?: + | { + [locale in Locales]: string + } + | null + definition?: + | { + [locale in Locales]: string + } + | null + editorialNote?: + | { + [locale in Locales]: string + } + | null + example?: + | { + [locale in Locales]: string + } + | null + historyNote?: + | { + [locale in Locales]: string + } + | null + scopeNote?: + | { + [locale in Locales]: string + } + | null + notations?: string[] + broader?: Link<'TaxonomyConcept'>[] + related?: Link<'TaxonomyConcept'>[] +} + +export type ConceptCollection = { + sys: { + type: 'Array' + } + items: Concept[] + limit: number + pages?: { + prev?: string + next?: string + } +} diff --git a/lib/types/index.ts b/lib/types/index.ts index 56f619c81..3c009bcb5 100644 --- a/lib/types/index.ts +++ b/lib/types/index.ts @@ -2,6 +2,8 @@ export * from './asset.js' export * from './asset-key.js' export { AddChainModifier, ChainModifiers, ContentfulClientApi } from './client.js' export * from './collection.js' +export * from './concept-scheme.js' +export * from './concept.js' export * from './content-type.js' export * from './entry.js' export * from './link.js' diff --git a/lib/types/query/order.ts b/lib/types/query/order.ts index f44a5c27f..4bf90b7c6 100644 --- a/lib/types/query/order.ts +++ b/lib/types/query/order.ts @@ -90,3 +90,7 @@ export type AssetOrderFilter = { export type TagOrderFilter = { order?: (OrderFilterPaths | 'name' | '-name')[] } + +export type TaxonomyOrderFilter = { + order?: ('sys.createdAt' | 'sys.updatedAt' | 'prefLabel')[] +} diff --git a/lib/types/query/query.ts b/lib/types/query/query.ts index 9c819a2ff..c2172c5e6 100644 --- a/lib/types/query/query.ts +++ b/lib/types/query/query.ts @@ -14,6 +14,7 @@ import { EntryFieldsExistenceFilter, ExistenceFilter } from './existence.js' import { LocationSearchFilters } from './location.js' import { AssetOrderFilter, + TaxonomyOrderFilter, EntryOrderFilter, EntryOrderFilterWithFields, TagOrderFilter, @@ -219,3 +220,14 @@ export type TagQueries = TagNameFilters & SysQueries> & TagOrderFilter & FixedPagedOptions + +type CursorPaginationOptions = { + limit?: number + prevPage?: string + nextPage?: string +} + +export type ConceptsQueries = CursorPaginationOptions & + TaxonomyOrderFilter & { concept_scheme?: string } + +export type ConceptSchemesQueries = CursorPaginationOptions & TaxonomyOrderFilter diff --git a/test/integration/getConcept.test.ts b/test/integration/getConcept.test.ts new file mode 100644 index 000000000..b7191fcff --- /dev/null +++ b/test/integration/getConcept.test.ts @@ -0,0 +1,17 @@ +import * as contentful from '../../lib/contentful' +import { params } from './utils' + +if (process.env.API_INTEGRATION_TESTS) { + params.host = '127.0.0.1:5000' + params.insecure = true +} + +const client = contentful.createClient(params) + +describe('getConcept', () => { + it('returns a single concept', async () => { + const response = await client.getConcept('3eXhEIEzcZqwHyYWHbzSoS') + + expect(response.sys.type).toBe('TaxonomyConcept') + }) +}) diff --git a/test/integration/getConceptScheme.test.ts b/test/integration/getConceptScheme.test.ts new file mode 100644 index 000000000..855a13992 --- /dev/null +++ b/test/integration/getConceptScheme.test.ts @@ -0,0 +1,17 @@ +import * as contentful from '../../lib/contentful' +import { params } from './utils' + +if (process.env.API_INTEGRATION_TESTS) { + params.host = '127.0.0.1:5000' + params.insecure = true +} + +const client = contentful.createClient(params) + +describe('getConceptScheme', () => { + it('returns a single concept scheme', async () => { + const response = await client.getConceptScheme('7lcOh0M5JAu5xvEwWzs00H') + + expect(response.sys.type).toBe('TaxonomyConceptScheme') + }) +}) diff --git a/test/integration/getConceptSchemes.test.ts b/test/integration/getConceptSchemes.test.ts new file mode 100644 index 000000000..3df6e4889 --- /dev/null +++ b/test/integration/getConceptSchemes.test.ts @@ -0,0 +1,54 @@ +import * as contentful from '../../lib/contentful' +import { params } from './utils' + +if (process.env.API_INTEGRATION_TESTS) { + params.host = '127.0.0.1:5000' + params.insecure = true +} + +const client = contentful.createClient(params) + +describe('getConceptSchemes', () => { + it('returns all concept schemes when no filters are available', async () => { + const response = await client.getConceptSchemes() + + expect(response.items[0].sys.type).toBe('TaxonomyConceptScheme') + }) + + describe('order', () => { + it('orders the concept schemes by createdAt', async () => { + const response = await client.getConceptSchemes({ order: ['sys.createdAt'] }) + + expect(new Date(response.items[0].sys.createdAt).getTime()).toBeLessThan( + new Date(response.items[1].sys.createdAt).getTime(), + ) + }) + + it('orders the concept schemes by updatedAt', async () => { + const response = await client.getConceptSchemes({ order: ['sys.updatedAt'] }) + + expect(new Date(response.items[0].sys.updatedAt).getTime()).toBeLessThan( + new Date(response.items[1].sys.updatedAt).getTime(), + ) + }) + + it('orders the concept schemes by prefLabel', async () => { + const response = await client.getConceptSchemes({ order: ['prefLabel'] }) + + expect( + response.items[0].prefLabel['en-US'].localeCompare(response.items[1].prefLabel['en-US']), + ).toBeLessThan(0) + }) + }) + + describe('pagination', () => { + it('returns limit and next page cursor', async () => { + const response = await client.getConceptSchemes({ limit: 1 }) + + expect(response.items).toHaveLength(1) + expect(response.limit).toBe(1) + expect(response.sys.type).toBe('Array') + expect(response.pages?.next).toBeDefined() + }) + }) +}) diff --git a/test/integration/getConcepts.test.ts b/test/integration/getConcepts.test.ts new file mode 100644 index 000000000..07cd13901 --- /dev/null +++ b/test/integration/getConcepts.test.ts @@ -0,0 +1,62 @@ +import * as contentful from '../../lib/contentful' +import { params } from './utils' + +if (process.env.API_INTEGRATION_TESTS) { + params.host = '127.0.0.1:5000' + params.insecure = true +} + +const client = contentful.createClient(params) + +describe('getConcepts', () => { + it('returns all concepts when no filters are available', async () => { + const response = await client.getConcepts() + + expect(response.items[0].sys.type).toBe('TaxonomyConcept') + }) + + describe('order', () => { + it('orders the concepts by createdAt', async () => { + const response = await client.getConcepts({ order: ['sys.createdAt'] }) + + expect(new Date(response.items[0].sys.createdAt).getTime()).toBeLessThan( + new Date(response.items[1].sys.createdAt).getTime(), + ) + }) + + it('orders the concepts by updatedAt', async () => { + const response = await client.getConcepts({ order: ['sys.updatedAt'] }) + + expect(new Date(response.items[0].sys.updatedAt).getTime()).toBeLessThan( + new Date(response.items[1].sys.updatedAt).getTime(), + ) + }) + + it('orders the concepts by prefLabel', async () => { + const response = await client.getConcepts({ order: ['prefLabel'] }) + + expect( + response.items[0].prefLabel['en-US'].localeCompare(response.items[1].prefLabel['en-US']), + ).toBeLessThan(0) + }) + }) + + describe('pagination', () => { + it('returns limit and next page cursor', async () => { + const response = await client.getConcepts({ limit: 1 }) + + expect(response.items).toHaveLength(1) + expect(response.limit).toBe(1) + expect(response.sys.type).toBe('Array') + expect(response.pages?.next).toBeDefined() + }) + }) + + describe('Concept Scheme', () => { + it('filters by Concept Scheme', async () => { + const response = await client.getConcepts({ concept_scheme: '7lcOh0M5JAu5xvEwWzs00H' }) + + expect(response.items).toHaveLength(2) + }) + }) +})