Skip to content

Commit ae9f9a9

Browse files
mxstbrabhiaiyer91wardpeet
authored
chore(gatsby-source-contentful): move to pluginOptionsSchema (#27322)
* Move gatsby-source-contentful to pluginOptionsSchema * Remove old validation options tests * Better error message in case of failed external Contentful validation * Restore old validation, wrap new one in env var * use preinit and overwrite defaults * remove console.log * fix fetch Co-authored-by: Abhi Aiyer <abhiaiyer91@gmail.com> Co-authored-by: Ward Peeters <ward@coding-tech.com>
1 parent d6f033e commit ae9f9a9

File tree

4 files changed

+143
-202
lines changed

4 files changed

+143
-202
lines changed

packages/gatsby-source-contentful/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@
1717
"contentful": "^7.14.7",
1818
"fs-extra": "^9.0.1",
1919
"gatsby-core-utils": "^1.3.23",
20+
"gatsby-plugin-utils": "^0.2.27",
2021
"gatsby-source-filesystem": "^2.3.34",
2122
"is-online": "^8.5.0",
2223
"json-stringify-safe": "^5.0.1",
2324
"lodash": "^4.17.20",
25+
"node-fetch": "^2.6.1",
2426
"progress": "^2.0.3",
2527
"qs": "^6.9.4"
2628
},
Lines changed: 1 addition & 158 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
// disable output coloring for tests
22
process.env.FORCE_COLOR = 0
33

4-
const {
5-
maskText,
6-
validateOptions,
7-
formatPluginOptionsForCLI,
8-
} = require(`../plugin-options`)
4+
const { maskText, formatPluginOptionsForCLI } = require(`../plugin-options`)
95

106
const maskedCharacterCount = input =>
117
input.split(``).filter(char => char === `*`).length
@@ -78,156 +74,3 @@ describe(`Formatting plugin options for CLI`, () => {
7874
)
7975
})
8076
})
81-
82-
describe(`Options validation`, () => {
83-
const reporter = {
84-
panic: jest.fn(),
85-
}
86-
87-
beforeEach(() => {
88-
reporter.panic.mockClear()
89-
})
90-
91-
it(`Passes with valid options`, () => {
92-
validateOptions(
93-
{
94-
reporter,
95-
},
96-
{
97-
spaceId: `spaceId`,
98-
accessToken: `accessToken`,
99-
localeFilter: locale => locale.code === `de`,
100-
downloadLocal: false,
101-
}
102-
)
103-
104-
expect(reporter.panic).not.toBeCalled()
105-
})
106-
107-
it(`Fails with missing required options`, () => {
108-
validateOptions(
109-
{
110-
reporter,
111-
},
112-
{}
113-
)
114-
115-
expect(reporter.panic).toBeCalledWith(
116-
expect.stringContaining(
117-
`Problems with gatsby-source-contentful plugin options`
118-
)
119-
)
120-
expect(reporter.panic).toBeCalledWith(
121-
expect.stringContaining(`"accessToken" is required`)
122-
)
123-
expect(reporter.panic).toBeCalledWith(
124-
expect.stringContaining(`"accessToken" is required`)
125-
)
126-
})
127-
128-
it(`Fails with empty options`, () => {
129-
validateOptions(
130-
{
131-
reporter,
132-
},
133-
{
134-
environment: ``,
135-
host: ``,
136-
accessToken: ``,
137-
spaceId: ``,
138-
}
139-
)
140-
141-
expect(reporter.panic).toBeCalledWith(
142-
expect.stringContaining(
143-
`Problems with gatsby-source-contentful plugin options`
144-
)
145-
)
146-
expect(reporter.panic).toBeCalledWith(
147-
expect.stringContaining(`"environment" is not allowed to be empty`)
148-
)
149-
expect(reporter.panic).toBeCalledWith(
150-
expect.stringContaining(`"host" is not allowed to be empty`)
151-
)
152-
expect(reporter.panic).toBeCalledWith(
153-
expect.stringContaining(`"accessToken" is not allowed to be empty`)
154-
)
155-
expect(reporter.panic).toBeCalledWith(
156-
expect.stringContaining(`"spaceId" is not allowed to be empty`)
157-
)
158-
})
159-
160-
it(`Fails with options of wrong types`, () => {
161-
validateOptions(
162-
{
163-
reporter,
164-
},
165-
{
166-
environment: 1,
167-
host: [],
168-
accessToken: true,
169-
spaceId: {},
170-
localeFilter: `yup`,
171-
downloadLocal: 5,
172-
useNameForId: 5,
173-
pageLimit: `fifty`,
174-
richText: true,
175-
}
176-
)
177-
178-
expect(reporter.panic).toBeCalledWith(
179-
expect.stringContaining(
180-
`Problems with gatsby-source-contentful plugin options`
181-
)
182-
)
183-
expect(reporter.panic).toBeCalledWith(
184-
expect.stringContaining(`"environment" must be a string`)
185-
)
186-
expect(reporter.panic).toBeCalledWith(
187-
expect.stringContaining(`"host" must be a string`)
188-
)
189-
expect(reporter.panic).toBeCalledWith(
190-
expect.stringContaining(`"accessToken" must be a string`)
191-
)
192-
expect(reporter.panic).toBeCalledWith(
193-
expect.stringContaining(`"spaceId" must be a string`)
194-
)
195-
expect(reporter.panic).toBeCalledWith(
196-
expect.stringContaining(`"localeFilter" must be a Function`)
197-
)
198-
expect(reporter.panic).toBeCalledWith(
199-
expect.stringContaining(`"downloadLocal" must be a boolean`)
200-
)
201-
expect(reporter.panic).toBeCalledWith(
202-
expect.stringContaining(`"useNameForId" must be a boolean`)
203-
)
204-
expect(reporter.panic).toBeCalledWith(
205-
expect.stringContaining(`"pageLimit" must be a number`)
206-
)
207-
expect(reporter.panic).toBeCalledWith(
208-
expect.stringContaining(`"richText" must be an object`)
209-
)
210-
})
211-
212-
it(`Fails with undefined option keys`, () => {
213-
validateOptions(
214-
{
215-
reporter,
216-
},
217-
{
218-
spaceId: `spaceId`,
219-
accessToken: `accessToken`,
220-
wat: true,
221-
}
222-
)
223-
224-
expect(reporter.panic).toBeCalledWith(
225-
expect.stringContaining(
226-
`Problems with gatsby-source-contentful plugin options`
227-
)
228-
)
229-
expect(reporter.panic).toBeCalledWith(
230-
expect.stringContaining(`"wat" is not allowed`)
231-
)
232-
})
233-
})

packages/gatsby-source-contentful/src/gatsby-node.js

Lines changed: 140 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,16 @@ const _ = require(`lodash`)
44
const fs = require(`fs-extra`)
55
const { createClient } = require(`contentful`)
66
const v8 = require(`v8`)
7+
const fetch = require(`node-fetch`)
8+
const { Joi } = require(`gatsby-plugin-utils`)
79

810
const normalize = require(`./normalize`)
911
const fetchData = require(`./fetch`)
10-
const { createPluginConfig, validateOptions } = require(`./plugin-options`)
12+
const {
13+
createPluginConfig,
14+
maskText,
15+
formatPluginOptionsForCLI,
16+
} = require(`./plugin-options`)
1117
const { downloadContentfulAssets } = require(`./download-contentful-assets`)
1218

1319
const conflictFieldPrefix = `contentful`
@@ -24,7 +30,138 @@ const restrictedNodeFields = [
2430

2531
exports.setFieldsOnGraphQLNodeType = require(`./extend-node-type`).extendNodeType
2632

27-
exports.onPreBootstrap = validateOptions
33+
// TODO: Remove once pluginOptionsSchema is stable
34+
exports.onPreInit = ({ reporter }, options) => {
35+
const result = pluginOptionsSchema({ Joi }).validate(options, {
36+
abortEarly: false,
37+
externals: false,
38+
})
39+
if (result.error) {
40+
const errors = {}
41+
result.error.details.forEach(detail => {
42+
errors[detail.path[0]] = detail.message
43+
})
44+
reporter.panic(`Problems with gatsby-source-contentful plugin options:
45+
${formatPluginOptionsForCLI(options, errors)}`)
46+
}
47+
48+
options = result.value
49+
}
50+
51+
const validateContentfulAccess = async pluginOptions => {
52+
if (process.env.NODE_ENV === `test`) return undefined
53+
54+
await fetch(`https://${pluginOptions.host}/spaces/${pluginOptions.spaceId}`, {
55+
headers: {
56+
Authorization: `Bearer ${pluginOptions.accessToken}`,
57+
"Content-Type": `application/json`,
58+
},
59+
})
60+
.then(res => res.ok)
61+
.then(ok => {
62+
if (!ok)
63+
throw new Error(
64+
`Cannot access Contentful space "${maskText(
65+
pluginOptions.spaceId
66+
)}" with access token "${maskText(
67+
pluginOptions.accessToken
68+
)}". Make sure to double check them!`
69+
)
70+
})
71+
72+
return undefined
73+
}
74+
75+
const pluginOptionsSchema = ({ Joi }) =>
76+
Joi.object()
77+
.keys({
78+
accessToken: Joi.string()
79+
.description(
80+
`Contentful delivery api key, when using the Preview API use your Preview API key`
81+
)
82+
.required()
83+
.empty(),
84+
spaceId: Joi.string()
85+
.description(`Contentful spaceId`)
86+
.required()
87+
.empty(),
88+
host: Joi.string()
89+
.description(
90+
`The base host for all the API requests, by default it's 'cdn.contentful.com', if you want to use the Preview API set it to 'preview.contentful.com'. You can use your own host for debugging/testing purposes as long as you respect the same Contentful JSON structure.`
91+
)
92+
.default(`cdn.contentful.com`)
93+
.empty(),
94+
environment: Joi.string()
95+
.description(
96+
`The environment to pull the content from, for more info on environments check out this [Guide](https://www.contentful.com/developers/docs/concepts/multiple-environments/).`
97+
)
98+
.default(`master`)
99+
.empty(),
100+
downloadLocal: Joi.boolean()
101+
.description(
102+
`Downloads and caches ContentfulAsset's to the local filesystem. Allows you to query a ContentfulAsset's localFile field, which is not linked to Contentful's CDN. Useful for reducing data usage.
103+
You can pass in any other options available in the [contentful.js SDK](https://github.com/contentful/contentful.js#configuration).`
104+
)
105+
.default(false),
106+
localeFilter: Joi.func()
107+
.description(
108+
`Possibility to limit how many locales/nodes are created in GraphQL. This can limit the memory usage by reducing the amount of nodes created. Useful if you have a large space in contentful and only want to get the data from one selected locale.
109+
For example, to filter locales on only germany \`localeFilter: locale => locale.code === 'de-DE'\`
110+
111+
List of locales and their codes can be found in Contentful app -> Settings -> Locales`
112+
)
113+
.default(() => true),
114+
forceFullSync: Joi.boolean()
115+
.description(
116+
`Prevents the use of sync tokens when accessing the Contentful API.`
117+
)
118+
.default(false),
119+
pageLimit: Joi.number()
120+
.integer()
121+
.description(
122+
`Number of entries to retrieve from Contentful at a time. Due to some technical limitations, the response payload should not be greater than 7MB when pulling content from Contentful. If you encounter this issue you can set this param to a lower number than 100, e.g 50.`
123+
)
124+
.default(100),
125+
proxy: Joi.object()
126+
.keys({
127+
host: Joi.string().required(),
128+
port: Joi.number().required(),
129+
auth: Joi.object().keys({
130+
username: Joi.string(),
131+
password: Joi.string(),
132+
}),
133+
})
134+
.description(
135+
`Axios proxy configuration. See the [axios request config documentation](https://github.com/mzabriskie/axios#request-config) for further information about the supported values.`
136+
),
137+
useNameForId: Joi.boolean()
138+
.description(
139+
`Use the content's \`name\` when generating the GraphQL schema e.g. a Content Type called \`[Component] Navigation bar\` will be named \`contentfulComponentNavigationBar\`.
140+
When set to \`false\`, the content's internal ID will be used instead e.g. a Content Type with the ID \`navigationBar\` will be called \`contentfulNavigationBar\`.
141+
142+
Using the ID is a much more stable property to work with as it will change less often. However, in some scenarios, Content Types' IDs will be auto-generated (e.g. when creating a new Content Type without specifying an ID) which means the name in the GraphQL schema will be something like \`contentfulC6XwpTaSiiI2Ak2Ww0oi6qa\`. This won't change and will still function perfectly as a valid field name but it is obviously pretty ugly to work with.
143+
144+
If you are confident your Content Types will have natural-language IDs (e.g. \`blogPost\`), then you should set this option to \`false\`. If you are unable to ensure this, then you should leave this option set to \`true\` (the default).`
145+
)
146+
.default(true),
147+
// default plugins passed by gatsby
148+
plugins: Joi.array(),
149+
richText: Joi.object()
150+
.keys({
151+
resolveFieldLocales: Joi.boolean()
152+
.description(
153+
`If you want to resolve the locales in fields of assets and entries that are referenced by rich text (e.g., via embedded entries or entry hyperlinks), set this to \`true\`. Otherwise, fields of referenced assets or entries will be objects keyed by locale.`
154+
)
155+
.default(false),
156+
})
157+
.default({}),
158+
})
159+
.external(validateContentfulAccess)
160+
161+
if (process.env.GATSBY_EXPERIMENTAL_PLUGIN_OPTION_VALIDATION) {
162+
exports.pluginOptionsSchema = pluginOptionsSchema
163+
}
164+
28165
/***
29166
* Localization algorithm
30167
*
@@ -151,7 +288,7 @@ exports.sourceNodes = async (
151288
parentSpan,
152289
}
153290
)
154-
console.log(`runs through this part anyways`)
291+
155292
fetchActivity.start()
156293
;({
157294
currentSyncData,

0 commit comments

Comments
 (0)