diff --git a/.talismanrc b/.talismanrc index 6c8ea20..f39b099 100644 --- a/.talismanrc +++ b/.talismanrc @@ -5,6 +5,14 @@ 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 checksum: f4745db1b4f868844d5668b56c1401c8c9d87fb82a8742d2a9ac97f37dc5c587 - filename: src/tests/exclude-content-types.test.js 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..ca4961d 100644 --- a/src/normalize.js +++ b/src/normalize.js @@ -204,9 +204,18 @@ 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]; - 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 = 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 { + interfaceFields[key] = typeFields[key].replace(newparent, newInterfaceParent); + } } if (Object.keys(fields).length > 0) { @@ -427,9 +436,16 @@ 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]; - interfaceFields[key] = typeFields[key].replace(newParent, newInterfaceParent); + // Same nested-global-field case as buildBlockCustomSchema above. + 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 { + interfaceFields[key] = typeFields[key].replace(newParent, newInterfaceParent); + } } if (Object.keys(typeFields).length > 0) { @@ -515,6 +531,19 @@ const buildCustomSchema = (exports.buildCustomSchema = (schema, types, reference } } break; + case 'taxonomy': + 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, + }; + 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..04c34b8 --- /dev/null +++ b/src/tests/normalize-nested-global-field-taxonomy.test.js @@ -0,0 +1,76 @@ +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]!'); + }); + + 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 new file mode 100644 index 0000000..9e96ff2 --- /dev/null +++ b/tests/normalize-nested-global-field-taxonomy.test.js @@ -0,0 +1,82 @@ +"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]!'); + }); + 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