From e6ce074d542f860db92e595632f08d4bfb9c3f9d Mon Sep 17 00:00:00 2001 From: Abhishek Ezhava Date: Tue, 4 Aug 2026 15:12:00 +0530 Subject: [PATCH 1/3] fix: correct interface types for nested global fields, add taxonomy field support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nested global fields (a global field referencing another global field, whether inside a plain field, a group, or a modular block) got the wrong GraphQL interface type. The interface field name was derived by blindly string-replacing the parent type name onto every child field, which only works when the child's type name is literally built from that parent name. A nested global field's type name is built from its own reference_to instead, so the blind replace produced a non-existent, malformed interface type. Detect the nested-global-field case explicitly in both buildBlockCustomSchema and buildCustomSchema, and point the interface field at the referenced global field's own interface type instead. Also add support for the `taxonomy` field data type, which previously had no schema handling at all — content types with a taxonomy field now correctly expose a `[taxonomyType]` field (taxonomy_uid, term_uid) instead of the field being silently dropped. Fixes DX-10112. --- .talismanrc | 2 + README.md | 22 +++++++ src/normalize.js | 29 ++++++++- ...alize-nested-global-field-taxonomy.test.js | 62 +++++++++++++++++++ ...alize-nested-global-field-taxonomy.test.js | 62 +++++++++++++++++++ 5 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 src/tests/normalize-nested-global-field-taxonomy.test.js create mode 100644 tests/normalize-nested-global-field-taxonomy.test.js diff --git a/.talismanrc b/.talismanrc index 0f337aa..21f869b 100644 --- a/.talismanrc +++ b/.talismanrc @@ -5,4 +5,6 @@ fileignoreconfig: - filename: package-lock.json ignore_detectors: - filecontent +- filename: src/normalize.js + checksum: 471ff9d6a837381c4568abea54abfb5d04821e412b7d046fddacbfd434970d4f version: "1.0" \ No newline at end of file diff --git a/README.md b/README.md index 2553ca7..3dab7bb 100644 --- a/README.md +++ b/README.md @@ -165,6 +165,28 @@ query { } ``` +## Querying taxonomy fields + +Taxonomy fields let you categorize entries using taxonomy terms defined in your stack. Each +taxonomy field resolves to a list of `taxonomyType` objects exposing the taxonomy and term UIDs. + +```graphql +{ + allContentstackBlogs { + edges { + node { + id + title + topics { + taxonomy_uid + term_uid + } + } + } + } +} +``` + ## Querying downloaded images ## Prerequisites diff --git a/src/normalize.js b/src/normalize.js index e477961..abfc0de 100644 --- a/src/normalize.js +++ b/src/normalize.js @@ -206,7 +206,15 @@ const buildBlockCustomSchema = (blocks, types, references, groups, fileFields, j const interfaceFields = {}; for (const key in fields) { typeFields[key] = fields[key].type || fields[key]; - interfaceFields[key] = typeFields[key].replace(newparent, newInterfaceParent); + // A field nested inside this block can itself be a global field: its type name is + // built from its own reference_to, not from newparent, so the blind replace below + // would produce a malformed interface field. Point at its own interface type instead. + const childField = (block.schema || []).find(f => f.uid === key); + if (childField && childField.data_type === 'global_field' && childField.reference_to) { + interfaceFields[key] = typeFields[key].replace(`${newparent}_${key}`, `${prefix}_${childField.reference_to}`); + } else { + interfaceFields[key] = typeFields[key].replace(newparent, newInterfaceParent); + } } if (Object.keys(fields).length > 0) { @@ -429,7 +437,13 @@ const buildCustomSchema = (exports.buildCustomSchema = (schema, types, reference const interfaceFields = {}; for (const key in result.fields) { typeFields[key] = result.fields[key].type || result.fields[key]; - interfaceFields[key] = typeFields[key].replace(newParent, newInterfaceParent); + // Same nested-global-field case as buildBlockCustomSchema above. + const childField = (field.schema || []).find(f => f.uid === key); + if (childField && childField.data_type === 'global_field' && childField.reference_to) { + interfaceFields[key] = typeFields[key].replace(`${newParent}_${key}`, `${prefix}_${childField.reference_to}`); + } else { + interfaceFields[key] = typeFields[key].replace(newParent, newInterfaceParent); + } } if (Object.keys(typeFields).length > 0) { @@ -515,6 +529,17 @@ const buildCustomSchema = (exports.buildCustomSchema = (schema, types, reference } } break; + case 'taxonomy': + types.push('type taxonomyType { taxonomy_uid: String term_uid: String }'); + fields[field.uid] = { + resolve: source => source[field.uid] || null, + }; + if (field.mandatory && !disableMandatoryFields) { + fields[field.uid].type = '[taxonomyType]!'; + } else { + fields[field.uid].type = '[taxonomyType]'; + } + break; } }); return { fields, types, references, groups, fileFields, jsonRteFields }; diff --git a/src/tests/normalize-nested-global-field-taxonomy.test.js b/src/tests/normalize-nested-global-field-taxonomy.test.js new file mode 100644 index 0000000..cd886c0 --- /dev/null +++ b/src/tests/normalize-nested-global-field-taxonomy.test.js @@ -0,0 +1,62 @@ +const { buildCustomSchema } = require('../normalize'); + +describe('buildCustomSchema: nested global field inside a block', () => { + test('interface field points at the nested global field\'s own type, not a blind parent-name substitution', () => { + const schema = [ + { + uid: 'sections', + data_type: 'blocks', + mandatory: false, + multiple: true, + blocks: [ + { + uid: 'hero', + reference_to: 'hero_global', + schema: [ + { + uid: 'author_info', + data_type: 'global_field', + reference_to: 'author_global', + mandatory: false, + multiple: false, + schema: [ + { uid: 'name', data_type: 'text', mandatory: false, multiple: false }, + ], + }, + ], + }, + ], + }, + ]; + + const result = buildCustomSchema(schema, [], [], [], [], [], 'Contentstack_blog', 'Contentstack', false, false, () => {}, undefined); + + const heroInterface = result.types.find((t) => t.startsWith('interface Contentstack_hero_global')); + expect(heroInterface).toBeDefined(); + expect(heroInterface).toContain('author_info:Contentstack_author_global'); + expect(heroInterface).not.toContain('Contentstack_hero_global_author_info'); + }); +}); + +describe('buildCustomSchema: taxonomy field', () => { + test('produces a taxonomyType field instead of being silently dropped', () => { + const schema = [ + { uid: 'topics', data_type: 'taxonomy', mandatory: false, multiple: true }, + ]; + + const result = buildCustomSchema(schema, [], [], [], [], [], 'Contentstack_blog', 'Contentstack', false, false, () => {}, undefined); + + expect(result.fields.topics.type).toBe('[taxonomyType]'); + expect(result.types).toContain('type taxonomyType { taxonomy_uid: String term_uid: String }'); + }); + + test('is wrapped in a non-null list when mandatory', () => { + const schema = [ + { uid: 'topics', data_type: 'taxonomy', mandatory: true, multiple: true }, + ]; + + const result = buildCustomSchema(schema, [], [], [], [], [], 'Contentstack_blog', 'Contentstack', false, false, () => {}, undefined); + + expect(result.fields.topics.type).toBe('[taxonomyType]!'); + }); +}); diff --git a/tests/normalize-nested-global-field-taxonomy.test.js b/tests/normalize-nested-global-field-taxonomy.test.js new file mode 100644 index 0000000..7abd182 --- /dev/null +++ b/tests/normalize-nested-global-field-taxonomy.test.js @@ -0,0 +1,62 @@ +"use strict"; + +var _require = require('../normalize'), + buildCustomSchema = _require.buildCustomSchema; +describe('buildCustomSchema: nested global field inside a block', function () { + test('interface field points at the nested global field\'s own type, not a blind parent-name substitution', function () { + var schema = [{ + uid: 'sections', + data_type: 'blocks', + mandatory: false, + multiple: true, + blocks: [{ + uid: 'hero', + reference_to: 'hero_global', + schema: [{ + uid: 'author_info', + data_type: 'global_field', + reference_to: 'author_global', + mandatory: false, + multiple: false, + schema: [{ + uid: 'name', + data_type: 'text', + mandatory: false, + multiple: false + }] + }] + }] + }]; + var result = buildCustomSchema(schema, [], [], [], [], [], 'Contentstack_blog', 'Contentstack', false, false, function () {}, undefined); + var heroInterface = result.types.find(function (t) { + return t.startsWith('interface Contentstack_hero_global'); + }); + expect(heroInterface).toBeDefined(); + expect(heroInterface).toContain('author_info:Contentstack_author_global'); + expect(heroInterface).not.toContain('Contentstack_hero_global_author_info'); + }); +}); +describe('buildCustomSchema: taxonomy field', function () { + test('produces a taxonomyType field instead of being silently dropped', function () { + var schema = [{ + uid: 'topics', + data_type: 'taxonomy', + mandatory: false, + multiple: true + }]; + var result = buildCustomSchema(schema, [], [], [], [], [], 'Contentstack_blog', 'Contentstack', false, false, function () {}, undefined); + expect(result.fields.topics.type).toBe('[taxonomyType]'); + expect(result.types).toContain('type taxonomyType { taxonomy_uid: String term_uid: String }'); + }); + test('is wrapped in a non-null list when mandatory', function () { + var schema = [{ + uid: 'topics', + data_type: 'taxonomy', + mandatory: true, + multiple: true + }]; + var result = buildCustomSchema(schema, [], [], [], [], [], 'Contentstack_blog', 'Contentstack', false, false, function () {}, undefined); + expect(result.fields.topics.type).toBe('[taxonomyType]!'); + }); +}); +//# sourceMappingURL=normalize-nested-global-field-taxonomy.test.js.map \ No newline at end of file From b7ed12d5d71b92600d34d68ea78860f6ba8d511f Mon Sep 17 00:00:00 2001 From: Abhishek Ezhava Date: Tue, 4 Aug 2026 16:40:35 +0530 Subject: [PATCH 2/3] fix: address Copilot review feedback on nested global fields/taxonomy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Guard the taxonomyType type definition push so a content type with more than one taxonomy field doesn't push duplicate type defs into the same createTypes() call, which Gatsby would reject as a schema build error. - Pre-index each block/group's own schema by uid once before the interface-field loop, instead of calling .find() per field — avoids an O(n^2) scan for large modular blocks. Addresses Copilot review comments on PR #280. --- src/normalize.js | 10 +++++++--- ...alize-nested-global-field-taxonomy.test.js | 14 +++++++++++++ ...alize-nested-global-field-taxonomy.test.js | 20 +++++++++++++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/normalize.js b/src/normalize.js index abfc0de..ca4961d 100644 --- a/src/normalize.js +++ b/src/normalize.js @@ -204,12 +204,13 @@ const buildBlockCustomSchema = (blocks, types, references, groups, fileFields, j const typeFields = {}; const interfaceFields = {}; + const blockSchemaByUid = new Map((block.schema || []).map(f => [f.uid, f])); for (const key in fields) { typeFields[key] = fields[key].type || fields[key]; // A field nested inside this block can itself be a global field: its type name is // built from its own reference_to, not from newparent, so the blind replace below // would produce a malformed interface field. Point at its own interface type instead. - const childField = (block.schema || []).find(f => f.uid === key); + const childField = blockSchemaByUid.get(key); if (childField && childField.data_type === 'global_field' && childField.reference_to) { interfaceFields[key] = typeFields[key].replace(`${newparent}_${key}`, `${prefix}_${childField.reference_to}`); } else { @@ -435,10 +436,11 @@ const buildCustomSchema = (exports.buildCustomSchema = (schema, types, reference const typeFields = {}; const interfaceFields = {}; + const fieldSchemaByUid = new Map((field.schema || []).map(f => [f.uid, f])); for (const key in result.fields) { typeFields[key] = result.fields[key].type || result.fields[key]; // Same nested-global-field case as buildBlockCustomSchema above. - const childField = (field.schema || []).find(f => f.uid === key); + const childField = fieldSchemaByUid.get(key); if (childField && childField.data_type === 'global_field' && childField.reference_to) { interfaceFields[key] = typeFields[key].replace(`${newParent}_${key}`, `${prefix}_${childField.reference_to}`); } else { @@ -530,7 +532,9 @@ const buildCustomSchema = (exports.buildCustomSchema = (schema, types, reference } break; case 'taxonomy': - types.push('type taxonomyType { taxonomy_uid: String term_uid: String }'); + if (!types.includes('type taxonomyType { taxonomy_uid: String term_uid: String }')) { + types.push('type taxonomyType { taxonomy_uid: String term_uid: String }'); + } fields[field.uid] = { resolve: source => source[field.uid] || null, }; diff --git a/src/tests/normalize-nested-global-field-taxonomy.test.js b/src/tests/normalize-nested-global-field-taxonomy.test.js index cd886c0..04c34b8 100644 --- a/src/tests/normalize-nested-global-field-taxonomy.test.js +++ b/src/tests/normalize-nested-global-field-taxonomy.test.js @@ -59,4 +59,18 @@ describe('buildCustomSchema: taxonomy field', () => { expect(result.fields.topics.type).toBe('[taxonomyType]!'); }); + + test('multiple taxonomy fields on one content type do not push duplicate taxonomyType definitions', () => { + const schema = [ + { uid: 'topics', data_type: 'taxonomy', mandatory: false, multiple: true }, + { uid: 'regions', data_type: 'taxonomy', mandatory: false, multiple: true }, + ]; + + const result = buildCustomSchema(schema, [], [], [], [], [], 'Contentstack_blog', 'Contentstack', false, false, () => {}, undefined); + + const taxonomyTypeDefs = result.types.filter((t) => t === 'type taxonomyType { taxonomy_uid: String term_uid: String }'); + expect(taxonomyTypeDefs).toHaveLength(1); + expect(result.fields.topics.type).toBe('[taxonomyType]'); + expect(result.fields.regions.type).toBe('[taxonomyType]'); + }); }); diff --git a/tests/normalize-nested-global-field-taxonomy.test.js b/tests/normalize-nested-global-field-taxonomy.test.js index 7abd182..9e96ff2 100644 --- a/tests/normalize-nested-global-field-taxonomy.test.js +++ b/tests/normalize-nested-global-field-taxonomy.test.js @@ -58,5 +58,25 @@ describe('buildCustomSchema: taxonomy field', function () { var result = buildCustomSchema(schema, [], [], [], [], [], 'Contentstack_blog', 'Contentstack', false, false, function () {}, undefined); expect(result.fields.topics.type).toBe('[taxonomyType]!'); }); + test('multiple taxonomy fields on one content type do not push duplicate taxonomyType definitions', function () { + var schema = [{ + uid: 'topics', + data_type: 'taxonomy', + mandatory: false, + multiple: true + }, { + uid: 'regions', + data_type: 'taxonomy', + mandatory: false, + multiple: true + }]; + var result = buildCustomSchema(schema, [], [], [], [], [], 'Contentstack_blog', 'Contentstack', false, false, function () {}, undefined); + var taxonomyTypeDefs = result.types.filter(function (t) { + return t === 'type taxonomyType { taxonomy_uid: String term_uid: String }'; + }); + expect(taxonomyTypeDefs).toHaveLength(1); + expect(result.fields.topics.type).toBe('[taxonomyType]'); + expect(result.fields.regions.type).toBe('[taxonomyType]'); + }); }); //# sourceMappingURL=normalize-nested-global-field-taxonomy.test.js.map \ No newline at end of file From a69dd3c34f2df85a48e9a00da0bd5deaf71c31f6 Mon Sep 17 00:00:00 2001 From: Abhishek Ezhava Date: Tue, 4 Aug 2026 16:46:51 +0530 Subject: [PATCH 3/3] docs: explain the .talismanrc false-positive entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses a Copilot review comment on PR #280 asking why these whole-file checksum ignores exist. Documents that they're false positives on the literal string "api_key" (a plugin option name, not a credential), and that each checksum is pinned to the file's content at review time so a future edit — including a real secret — changes the checksum and re-triggers scanning. --- .talismanrc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.talismanrc b/.talismanrc index 8057d80..f39b099 100644 --- a/.talismanrc +++ b/.talismanrc @@ -5,6 +5,12 @@ fileignoreconfig: - filename: package-lock.json ignore_detectors: - filecontent +# The four entries below are false positives on the literal string "api_key" — a +# Contentstack plugin option name used throughout this codebase, not a credential. +# Each checksum pins the exact file content at the time it was reviewed: any future +# edit to these files changes the checksum, so Talisman re-scans automatically and a +# real secret added later would still be caught. Narrower per-line ignoring isn't +# supported by this detector, so a whole-file checksum is the tightest scope available. - filename: src/normalize.js checksum: 471ff9d6a837381c4568abea54abfb5d04821e412b7d046fddacbfd434970d4f - filename: src/create-schema-customization.js