From 2b09a9bfeccdf7ef492edef0034fd64f3d7c581d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rebecca=20K=C3=B6nig?= Date: Wed, 31 Jul 2024 22:51:45 +0200 Subject: [PATCH] feat: add support for external references (#2294) --- lib/types/entry.ts | 19 ++- lib/types/resource-link.ts | 4 +- package-lock.json | 14 +- package.json | 2 +- test/integration/parseEntries.test.ts | 89 ++++++++++- test/types/mocks.ts | 7 + test/types/resolved-field.test-d.ts | 217 ++++++++++++++++++++++++++ 7 files changed, 336 insertions(+), 16 deletions(-) diff --git a/lib/types/entry.ts b/lib/types/entry.ts index f726f7227..ce6848a1d 100644 --- a/lib/types/entry.ts +++ b/lib/types/entry.ts @@ -42,13 +42,17 @@ export declare namespace EntryFieldTypes { type: 'EntryResourceLink' entry: EntrySkeleton } + type ExternalResourceLink = { + type: 'ExternalResourceLink' + } type AssetLink = { type: 'AssetLink' } type Array< Item extends | EntryFieldTypes.Symbol | EntryFieldTypes.AssetLink | EntryFieldTypes.EntryLink - | EntryFieldTypes.EntryResourceLink, + | EntryFieldTypes.EntryResourceLink + | EntryFieldTypes.ExternalResourceLink, > = { type: 'Array'; item: Item } type Object = { type: 'Object' @@ -97,11 +101,14 @@ export type EntryFieldType = | EntryFieldTypes.Object | EntryFieldTypes.EntryLink | EntryFieldTypes.EntryResourceLink + | EntryFieldTypes.ExternalResourceLink | EntryFieldTypes.AssetLink | EntryFieldTypes.Array | EntryFieldTypes.Array | EntryFieldTypes.Array> - | EntryFieldTypes.Array> + | EntryFieldTypes.Array< + EntryFieldTypes.EntryResourceLink | EntryFieldTypes.ExternalResourceLink + > /** * All possible values for entry field types @@ -243,9 +250,11 @@ export type ResolvedLink< ? ResolvedEntryLink : Field extends EntryFieldTypes.EntryResourceLink ? ResolvedEntryResourceLink - : Field extends EntryFieldTypes.AssetLink - ? ResolvedAssetLink - : BaseFieldMap + : Field extends EntryFieldTypes.ExternalResourceLink + ? { sys: ResourceLink } + : Field extends EntryFieldTypes.AssetLink + ? ResolvedAssetLink + : BaseFieldMap /** * A collection or single resolved link to another resource diff --git a/lib/types/resource-link.ts b/lib/types/resource-link.ts index 72668c805..c7b3b940b 100644 --- a/lib/types/resource-link.ts +++ b/lib/types/resource-link.ts @@ -2,8 +2,8 @@ * Definition of an external resource link * @category Link */ -export interface ResourceLink { +export interface ResourceLink { type: 'ResourceLink' - linkType: 'Contentful:Entry' + linkType: LinkType urn: string } diff --git a/package-lock.json b/package-lock.json index 76f8ff221..3c284d0f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@contentful/content-source-maps": "^0.6.0", "@contentful/rich-text-types": "^16.0.2", "axios": "~1.6.8", - "contentful-resolve-response": "^1.8.1", + "contentful-resolve-response": "^1.9.0", "contentful-sdk-core": "^8.1.0", "json-stringify-safe": "^5.0.1", "type-fest": "^4.0.0" @@ -6906,9 +6906,9 @@ } }, "node_modules/contentful-resolve-response": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/contentful-resolve-response/-/contentful-resolve-response-1.8.2.tgz", - "integrity": "sha512-F9oLqSkprxcvUVkzp8ZfVy98nfKv8cmdBIZ0RB1HFGU5xbPDyWYNb7D0CzhSeDVcMmwmsfBbM6DuR//Dq6Bmng==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/contentful-resolve-response/-/contentful-resolve-response-1.9.0.tgz", + "integrity": "sha512-LtgPx/eREpHXOX82od48zFZbFhXzYw/NfUoYK4Qf1OaKpLzmYPE4cAY4aD+rxVgnMM5JN/mQaPCsofUlJRYEUA==", "dependencies": { "fast-copy": "^2.1.7" }, @@ -27177,9 +27177,9 @@ "dev": true }, "contentful-resolve-response": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/contentful-resolve-response/-/contentful-resolve-response-1.8.2.tgz", - "integrity": "sha512-F9oLqSkprxcvUVkzp8ZfVy98nfKv8cmdBIZ0RB1HFGU5xbPDyWYNb7D0CzhSeDVcMmwmsfBbM6DuR//Dq6Bmng==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/contentful-resolve-response/-/contentful-resolve-response-1.9.0.tgz", + "integrity": "sha512-LtgPx/eREpHXOX82od48zFZbFhXzYw/NfUoYK4Qf1OaKpLzmYPE4cAY4aD+rxVgnMM5JN/mQaPCsofUlJRYEUA==", "requires": { "fast-copy": "^2.1.7" } diff --git a/package.json b/package.json index 8b0139835..768d1c632 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "@contentful/content-source-maps": "^0.6.0", "@contentful/rich-text-types": "^16.0.2", "axios": "~1.6.8", - "contentful-resolve-response": "^1.8.1", + "contentful-resolve-response": "^1.9.0", "contentful-sdk-core": "^8.1.0", "json-stringify-safe": "^5.0.1", "type-fest": "^4.0.0" diff --git a/test/integration/parseEntries.test.ts b/test/integration/parseEntries.test.ts index 9c063d61d..78993d416 100644 --- a/test/integration/parseEntries.test.ts +++ b/test/integration/parseEntries.test.ts @@ -8,6 +8,9 @@ interface TypeCatFields { likes?: EntryFieldTypes.Array color?: EntryFieldTypes.Symbol bestFriend?: EntryFieldTypes.EntryLink + otherFriends?: EntryFieldTypes.Array< + EntryFieldTypes.EntryResourceLink | EntryFieldTypes.ExternalResourceLink + > birthday?: EntryFieldTypes.Date lifes?: EntryFieldTypes.Integer lives?: EntryFieldTypes.Integer @@ -58,6 +61,7 @@ const resolvedHappyCatEntry = { likes: ['cheezburger'], color: 'gray', bestFriend: { sys: { type: 'Link', linkType: 'Entry', id: 'nyancat' } }, + otherFriends: [], birthday: '2003-10-28T23:00:00+00:00', lives: 1, image: { sys: { type: 'Link', linkType: 'Asset', id: 'happycat' } }, @@ -94,6 +98,9 @@ const resolvedHappyCatEntryAllLocales = { bestFriend: { 'en-US': { sys: { type: 'Link', linkType: 'Entry', id: 'nyancat' } }, }, + otherFriends: { + 'en-US': [], + }, birthday: { 'en-US': '2003-10-28T23:00:00+00:00', }, @@ -250,6 +257,22 @@ beforeEach(() => { likes: ['rainbows', 'fish'], color: 'rainbow', bestFriend: { sys: { type: 'Link', linkType: 'Entry', id: 'happycat' } }, + otherFriends: [ + { + sys: { + type: 'ResourceLink', + linkType: 'Contentful:Entry', + urn: 'crn:contentful:::content:spaces/ezs1swce23xe/entries/happycat', + }, + }, + { + sys: { + type: 'ResourceLink', + linkType: 'AnotherProvider:SomeResourceType', + urn: 'external-id', + }, + }, + ], birthday: '2011-04-04T22:00:00Z', lives: 1337, image: { sys: { type: 'Link', linkType: 'Asset', id: 'nyancat' } }, @@ -303,6 +326,24 @@ beforeEach(() => { }, }, }, + otherFriends: { + 'en-US': [ + { + sys: { + type: 'ResourceLink', + linkType: 'Contentful:Entry', + urn: 'crn:contentful:::content:spaces/ezs1swce23xe/entries/happycat', + }, + }, + { + sys: { + type: 'ResourceLink', + linkType: 'AnotherProvider:SomeResourceType', + urn: 'external-id', + }, + }, + ], + }, birthday: { 'en-US': '2020-07-02T00:00:00Z', }, @@ -353,6 +394,22 @@ beforeEach(() => { id: '6SiPbntBPYYjnVHmipxJBF', }, }, + otherFriends: [ + { + sys: { + type: 'ResourceLink', + linkType: 'Contentful:Entry', + urn: 'crn:contentful:::content:spaces/ezs1swce23xe/entries/happycat', + }, + }, + { + sys: { + type: 'ResourceLink', + linkType: 'AnotherProvider:SomeResourceType', + urn: 'external-id', + }, + }, + ], birthday: '2020-07-02T00:00:00Z', lives: 9, image: { sys: { type: 'Link', linkType: 'Asset', id: 'happycat' } }, @@ -411,6 +468,24 @@ beforeEach(() => { }, }, }, + otherFriends: { + 'en-US': [ + { + sys: { + type: 'ResourceLink', + linkType: 'Contentful:Entry', + urn: 'crn:contentful:::content:spaces/ezs1swce23xe/entries/happycat', + }, + }, + { + sys: { + type: 'ResourceLink', + linkType: 'AnotherProvider:SomeResourceType', + urn: 'external-id', + }, + }, + ], + }, birthday: { 'en-US': '2020-07-02T00:00:00Z', }, @@ -442,6 +517,8 @@ describe('parseEntries via client chain modifiers', () => { expect(response.items[0].fields).toBeDefined() expect(response.items[0].fields.bestFriend?.sys.type).toBe('Entry') + expect(response.items[0].fields.otherFriends?.[0]?.sys.type).toBe('Entry') + expect(response.items[0].fields.otherFriends?.[1]?.sys.type).toBe('ResourceLink') expect(response.items[0].fields.color).toBe('rainbow') expect(response.items[0].fields.color?.['en-US']).not.toBeDefined() }) @@ -453,6 +530,8 @@ describe('parseEntries via client chain modifiers', () => { expect(response.items[0].fields).toBeDefined() expect(response.items[0].fields.bestFriend).toBeUndefined() + expect(response.items[0].fields.otherFriends).toHaveLength(1) + expect(response.items[0].fields.otherFriends?.[0]?.sys.type).toBe('ResourceLink') }) }) @@ -463,7 +542,9 @@ describe('parseEntries via client chain modifiers', () => { expect(response.items[0].fields).toBeDefined() expect(response.items[0].fields.name).toHaveProperty('en-US') expect(response.items[0].fields.name).toHaveProperty('tlh') - expect(response.items[0].fields.bestFriend?.['en-US']?.sys.type).not.toBe('Link') + expect(response.items[0].fields.bestFriend?.['en-US']?.sys.type).toBe('Entry') + expect(response.items[0].fields.otherFriends?.['en-US']?.[0]?.sys.type).toBe('Entry') + expect(response.items[0].fields.otherFriends?.['en-US']?.[1]?.sys.type).toBe('ResourceLink') }) test('client.withAllLocales.withoutLinkResolution', () => { @@ -474,6 +555,8 @@ describe('parseEntries via client chain modifiers', () => { expect(response.items[0].fields.name).toHaveProperty('en-US') expect(response.items[0].fields.name).toHaveProperty('tlh') expect(response.items[0].fields.bestFriend?.['en-US']?.sys.type).toBe('Link') + expect(response.items[0].fields.otherFriends?.['en-US']?.[0]?.sys.type).toBe('ResourceLink') + expect(response.items[0].fields.otherFriends?.['en-US']?.[1]?.sys.type).toBe('ResourceLink') }) test('client.withAllLocales.withoutUnresolvableLinks', () => { @@ -486,6 +569,8 @@ describe('parseEntries via client chain modifiers', () => { expect(response.items[0].fields.name).toHaveProperty('tlh') expect(response.items[0].fields.color).toHaveProperty('en-US') expect(response.items[0].fields.bestFriend).toEqual({}) + expect(response.items[0].fields.otherFriends?.['en-US']).toHaveLength(1) + expect(response.items[0].fields.otherFriends?.['en-US']?.[0]?.sys.type).toBe('ResourceLink') }) }) @@ -495,6 +580,8 @@ describe('parseEntries via client chain modifiers', () => { expect(response.items[0].fields).toBeDefined() expect(response.items[0].fields.bestFriend?.sys.type).toBe('Link') + expect(response.items[0].fields.otherFriends?.[0]?.sys.type).toBe('ResourceLink') + expect(response.items[0].fields.otherFriends?.[1]?.sys.type).toBe('ResourceLink') }) }) }) diff --git a/test/types/mocks.ts b/test/types/mocks.ts index 0da886229..1cb2e6706 100644 --- a/test/types/mocks.ts +++ b/test/types/mocks.ts @@ -48,6 +48,13 @@ export const entryResourceLink: { sys: ResourceLink } = { urn: stringValue, }, } +export const externalResourceLink: { sys: ResourceLink } = { + sys: { + type: 'ResourceLink', + linkType: 'Provider1:ResourceTypeA', + urn: stringValue, + }, +} export const entrySys: EntrySys = { contentType: { sys: { id: stringValue, type: 'Link', linkType: 'ContentType' } }, diff --git a/test/types/resolved-field.test-d.ts b/test/types/resolved-field.test-d.ts index bfd1ee3bc..9f3ff8dee 100644 --- a/test/types/resolved-field.test-d.ts +++ b/test/types/resolved-field.test-d.ts @@ -270,6 +270,223 @@ expectNotAssignable< > >(mocks.entry) +// external resource links + +expectAssignable>( + mocks.externalResourceLink, +) +// assignable because 'Contentful:Entry' is a subtype of string +expectAssignable>( + mocks.entryResourceLink, +) +expectNotAssignable>(mocks.entry) +expectNotAssignable>(undefined) +expectNotAssignable>(mocks.asset) +expectNotAssignable>(mocks.assetLink) +expectAssignable< + ResolvedField, undefined> +>([mocks.externalResourceLink, mocks.entryResourceLink]) +expectNotAssignable< + ResolvedField, undefined> +>([mocks.entry]) +expectNotAssignable< + ResolvedField, undefined> +>([mocks.externalResourceLink, undefined]) +expectNotAssignable< + ResolvedField, undefined> +>([mocks.externalResourceLink, mocks.asset]) +expectNotAssignable< + ResolvedField, undefined> +>([mocks.externalResourceLink, mocks.assetLink]) + +expectAssignable>( + mocks.externalResourceLink, +) +// assignable because 'Contentful:Entry' is a subtype of string +expectAssignable>( + mocks.entryResourceLink, +) +expectNotAssignable< + ResolvedField +>(mocks.entry) +expectNotAssignable< + ResolvedField +>(undefined) +expectNotAssignable< + ResolvedField +>(mocks.asset) +expectNotAssignable< + ResolvedField +>(mocks.assetLink) +expectAssignable< + ResolvedField< + EntryFieldTypes.Array, + 'WITHOUT_UNRESOLVABLE_LINKS' + > +>([mocks.externalResourceLink, mocks.entryResourceLink]) +expectNotAssignable< + ResolvedField< + EntryFieldTypes.Array, + 'WITHOUT_UNRESOLVABLE_LINKS' + > +>([mocks.entry]) +expectNotAssignable< + ResolvedField< + EntryFieldTypes.Array, + 'WITHOUT_UNRESOLVABLE_LINKS' + > +>([mocks.externalResourceLink, undefined]) +expectNotAssignable< + ResolvedField< + EntryFieldTypes.Array, + 'WITHOUT_UNRESOLVABLE_LINKS' + > +>([mocks.externalResourceLink, mocks.asset]) +expectNotAssignable< + ResolvedField< + EntryFieldTypes.Array, + 'WITHOUT_UNRESOLVABLE_LINKS' + > +>([mocks.externalResourceLink, mocks.assetLink]) + +expectAssignable>( + mocks.externalResourceLink, +) +// assignable because 'Contentful:Entry' is a subtype of string +expectAssignable>( + mocks.entryResourceLink, +) +expectNotAssignable>( + mocks.entry, +) +expectNotAssignable>( + undefined, +) +expectNotAssignable>( + mocks.asset, +) +expectNotAssignable>( + mocks.assetLink, +) +expectAssignable< + ResolvedField< + EntryFieldTypes.Array, + 'WITHOUT_LINK_RESOLUTION' + > +>([mocks.externalResourceLink, mocks.entryResourceLink]) +expectNotAssignable< + ResolvedField< + EntryFieldTypes.Array, + 'WITHOUT_LINK_RESOLUTION' + > +>([mocks.entry]) +expectNotAssignable< + ResolvedField< + EntryFieldTypes.Array, + 'WITHOUT_LINK_RESOLUTION' + > +>([undefined]) +expectNotAssignable< + ResolvedField< + EntryFieldTypes.Array, + 'WITHOUT_LINK_RESOLUTION' + > +>([mocks.asset]) +expectNotAssignable< + ResolvedField< + EntryFieldTypes.Array, + 'WITHOUT_LINK_RESOLUTION' + > +>([mocks.assetLink]) + +// mixed resource links + +expectAssignable< + ResolvedField< + | EntryFieldTypes.EntryResourceLink + | EntryFieldTypes.ExternalResourceLink, + undefined + > +>(mocks.entry) +expectAssignable< + ResolvedField< + | EntryFieldTypes.EntryResourceLink + | EntryFieldTypes.ExternalResourceLink, + undefined + > +>(mocks.entryResourceLink) +expectAssignable< + ResolvedField< + | EntryFieldTypes.EntryResourceLink + | EntryFieldTypes.ExternalResourceLink, + undefined + > +>(mocks.externalResourceLink) +expectNotAssignable< + ResolvedField< + | EntryFieldTypes.EntryResourceLink + | EntryFieldTypes.ExternalResourceLink, + undefined + > +>(undefined) +expectAssignable< + ResolvedField< + EntryFieldTypes.Array< + | EntryFieldTypes.EntryResourceLink + | EntryFieldTypes.ExternalResourceLink + >, + undefined + > +>([mocks.entry, mocks.entryResourceLink, mocks.externalResourceLink]) +expectNotAssignable< + ResolvedField< + EntryFieldTypes.Array< + | EntryFieldTypes.EntryResourceLink + | EntryFieldTypes.ExternalResourceLink + >, + undefined + > +>([undefined]) + +expectAssignable< + ResolvedField< + | EntryFieldTypes.EntryResourceLink + | EntryFieldTypes.ExternalResourceLink, + 'WITHOUT_UNRESOLVABLE_LINKS' + > +>(mocks.entry) +// assignable because 'Contentful:Entry' is a subtype of string +expectAssignable< + ResolvedField< + | EntryFieldTypes.EntryResourceLink + | EntryFieldTypes.ExternalResourceLink, + 'WITHOUT_UNRESOLVABLE_LINKS' + > +>(mocks.entryResourceLink) +expectAssignable< + ResolvedField< + | EntryFieldTypes.EntryResourceLink + | EntryFieldTypes.ExternalResourceLink, + 'WITHOUT_UNRESOLVABLE_LINKS' + > +>(mocks.externalResourceLink) +expectAssignable< + ResolvedField< + | EntryFieldTypes.EntryResourceLink + | EntryFieldTypes.ExternalResourceLink, + 'WITHOUT_UNRESOLVABLE_LINKS' + > +>(undefined) +expectAssignable< + ResolvedField< + EntryFieldTypes.Array< + | EntryFieldTypes.EntryResourceLink + | EntryFieldTypes.ExternalResourceLink + >, + 'WITHOUT_UNRESOLVABLE_LINKS' + > +>([mocks.entry, mocks.entryResourceLink, mocks.externalResourceLink, undefined]) + // assets expectAssignable>(mocks.asset)