diff --git a/lib/create-contentful-api.ts b/lib/create-contentful-api.ts index 8a2306ce2..0c6c83d23 100644 --- a/lib/create-contentful-api.ts +++ b/lib/create-contentful-api.ts @@ -36,6 +36,7 @@ import { } from './utils/validate-params' import validateSearchParameters from './utils/validate-search-parameters' import validateTimestamp from './utils/validate-timestamp' +import getQuerySelectionSet from './utils/query-selection-set' const ASSET_KEY_MAX_LIFETIME = 48 * 60 * 60 @@ -107,6 +108,15 @@ export default function createContentfulApi( if (areAllowed) { query.includeContentSourceMaps = true + + // Ensure that content source maps and required attributes are selected + if (query.select) { + const selection = getQuerySelectionSet(query) + + selection.add('sys') + + query.select = Array.from(selection).join(',') + } } return query diff --git a/lib/utils/normalize-select.ts b/lib/utils/normalize-select.ts index bd129abfd..7b99a30b5 100644 --- a/lib/utils/normalize-select.ts +++ b/lib/utils/normalize-select.ts @@ -1,3 +1,5 @@ +import getQuerySelectionSet from './query-selection-set' + /* * sdk relies heavily on sys metadata * so we cannot omit the sys property on sdk level entirely @@ -9,13 +11,7 @@ export default function normalizeSelect(query) { return query } - // The selection of fields for the query is limited - // Get the different parts that are listed for selection - const allSelects = Array.isArray(query.select) - ? query.select - : query.select.split(',').map((q) => q.trim()) - // Move the parts into a set for easy access and deduplication - const selectedSet = new Set(allSelects) + const selectedSet = getQuerySelectionSet(query) // If we already select all of `sys` we can just return // since we're anyway fetching everything that is needed diff --git a/lib/utils/query-selection-set.ts b/lib/utils/query-selection-set.ts new file mode 100644 index 000000000..81ac56a4f --- /dev/null +++ b/lib/utils/query-selection-set.ts @@ -0,0 +1,14 @@ +export default function getQuerySelectionSet(query: Record): Set { + if (!query.select) { + return new Set() + } + + // The selection of fields for the query is limited + // Get the different parts that are listed for selection + const allSelects = Array.isArray(query.select) + ? query.select + : query.select.split(',').map((q) => q.trim()) + + // Move the parts into a set for easy access and deduplication + return new Set(allSelects) +} diff --git a/test/integration/getAssets.test.ts b/test/integration/getAssets.test.ts index fe836ff29..2daa4d93c 100644 --- a/test/integration/getAssets.test.ts +++ b/test/integration/getAssets.test.ts @@ -47,32 +47,51 @@ describe('getAssets', () => { await expect(invalidClient.getAssets()).rejects.toThrow(ValidationError) }) - test('preview client', async () => { - const response = await previewClient.getAssets() + describe('preview client', () => { + it('requests content source maps', async () => { + const response = await previewClient.getAssets() - expect(response.items).not.toHaveLength(0) + expect(response.items).not.toHaveLength(0) - response.items.forEach((item) => { - expect(item.sys.type).toEqual('Asset') - expect(item.fields).toBeDefined() - expect(typeof item.fields.title).toBe('string') + response.items.forEach((item) => { + expect(item.sys.type).toEqual('Asset') + expect(item.fields).toBeDefined() + expect(typeof item.fields.title).toBe('string') + }) + + expect(response.sys?.contentSourceMapsLookup).toBeDefined() }) - expect(response.sys?.contentSourceMapsLookup).toBeDefined() - }) + it('enforces selection of sys if query.select is present', async () => { + const response = await previewClient.getAssets({ + select: ['fields.title', 'sys.id', 'sys.type'], + }) - test('preview client withAllLocales modifier', async () => { - const response = await previewClient.withAllLocales.getAssets() + expect(response.items).not.toHaveLength(0) - expect(response.items).not.toHaveLength(0) + response.items.forEach((item) => { + expect(item.sys.type).toEqual('Asset') + expect(item.fields).toBeDefined() + expect(typeof item.fields.title).toBe('string') + expect(item.sys.contentSourceMaps).toBeDefined() + }) - response.items.forEach((item) => { - expect(item.sys.type).toEqual('Asset') - expect(item.fields).toBeDefined() - expect(typeof item.fields.title).toBe('object') + expect(response.sys?.contentSourceMapsLookup).toBeDefined() }) - expect(response.sys?.contentSourceMapsLookup).toBeDefined() + it('works with withAllLocales modifier', async () => { + const response = await previewClient.withAllLocales.getAssets() + + expect(response.items).not.toHaveLength(0) + + response.items.forEach((item) => { + expect(item.sys.type).toEqual('Asset') + expect(item.fields).toBeDefined() + expect(typeof item.fields.title).toBe('object') + }) + + expect(response.sys?.contentSourceMapsLookup).toBeDefined() + }) }) }) }) diff --git a/test/integration/getEntries.test.ts b/test/integration/getEntries.test.ts index 161a1f1ba..dc30f698e 100644 --- a/test/integration/getEntries.test.ts +++ b/test/integration/getEntries.test.ts @@ -392,6 +392,16 @@ describe('getEntries via client chain modifiers', () => { assertCSMEntriesResponse(response) }) + test('enforces entry.sys when query.select is defined', async () => { + const response = await previewClient.getEntries({ + include: 5, + 'sys.id': entryWithResolvableLink, + select: ['fields', 'metadata.tags'], + }) + + assertCSMEntriesResponse(response) + }) + test('withAllLocales modifier', async () => { const response = await previewClient.withAllLocales.getEntries({ include: 5,