Skip to content

Commit aa21755

Browse files
superhawk610wardpeet
authored andcommitted
feat(gatsby-source-filesystem): add createFileNodeFromBuffer (#14576)
1 parent b8fe293 commit aa21755

File tree

6 files changed

+468
-14
lines changed

6 files changed

+468
-14
lines changed

packages/gatsby-source-filesystem/README.md

+89-1
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,11 @@ To filter by the `name` you specified in the config, use `sourceInstanceName`:
100100

101101
## Helper functions
102102

103-
`gatsby-source-filesystem` exports two helper functions:
103+
`gatsby-source-filesystem` exports three helper functions:
104104

105105
- `createFilePath`
106106
- `createRemoteFileNode`
107+
- `createFileNodeFromBuffer`
107108

108109
### createFilePath
109110

@@ -258,3 +259,90 @@ createRemoteFileNode({
258259
name: "image",
259260
})
260261
```
262+
263+
### createFileNodeFromBuffer
264+
265+
When working with data that isn't already stored in a file, such as when querying binary/blob fields from a database, it's helpful to cache that data to the filesystem in order to use it with other transformers that accept files as input.
266+
267+
The `createFileNodeFromBuffer` helper accepts a `Buffer`, caches its contents to disk, and creates a file node that points to it.
268+
269+
## Example usage
270+
271+
The following example is adapted from the source of [`gatsby-source-mysql`](https://github.com/malcolm-kee/gatsby-source-mysql):
272+
273+
```js
274+
// gatsby-node.js
275+
const createMySqlNodes = require(`./create-nodes`)
276+
277+
exports.sourceNodes = async (
278+
{ actions, createNodeId, store, cache },
279+
config
280+
) => {
281+
const { createNode } = actions
282+
const { conn, queries } = config
283+
const { db, results } = await query(conn, queries)
284+
285+
try {
286+
queries
287+
.map((query, i) => ({ ...query, ___sql: results[i] }))
288+
.forEach(result =>
289+
createMySqlNodes(result, results, createNode, {
290+
createNode,
291+
createNodeId,
292+
store,
293+
cache,
294+
})
295+
)
296+
db.end()
297+
} catch (e) {
298+
console.error(e)
299+
db.end()
300+
}
301+
}
302+
303+
// create-nodes.js
304+
const { createFileNodeFromBuffer } = require(`gatsby-source-filesystem`)
305+
const createNodeHelpers = require(`gatsby-node-helpers`).default
306+
307+
const { createNodeFactory } = createNodeHelpers({ typePrefix: `mysql` })
308+
309+
function attach(node, key, value, ctx) {
310+
if (Buffer.isBuffer(value)) {
311+
ctx.linkChildren.push(parentNodeId =>
312+
createFileNodeFromBuffer({
313+
buffer: value,
314+
store: ctx.store,
315+
cache: ctx.cache,
316+
createNode: ctx.createNode,
317+
createNodeId: ctx.createNodeId,
318+
})
319+
)
320+
value = `Buffer`
321+
}
322+
323+
node[key] = value
324+
}
325+
326+
function createMySqlNodes({ name, __sql, idField, keys }, results, ctx) {
327+
const MySqlNode = createNodeFactory(name)
328+
ctx.linkChildren = []
329+
330+
return __sql.forEach(row => {
331+
if (!keys) keys = Object.keys(row)
332+
333+
const node = { id: row[idField] }
334+
335+
for (const key of keys) {
336+
attach(node, key, row[key], ctx)
337+
}
338+
339+
node = ctx.createNode(node)
340+
341+
for (const link of ctx.linkChildren) {
342+
link(node.id)
343+
}
344+
})
345+
}
346+
347+
module.exports = createMySqlNodes
348+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
jest.mock(`fs-extra`, () => {
2+
return {
3+
ensureDir: jest.fn(() => true),
4+
writeFile: jest.fn((_f, _b, cb) => cb()),
5+
stat: jest.fn(() => {
6+
return {
7+
isDirectory: jest.fn(),
8+
}
9+
}),
10+
}
11+
})
12+
jest.mock(`../create-file-node`, () => {
13+
return {
14+
createFileNode: jest.fn(() => {
15+
return { internal: {} }
16+
}),
17+
}
18+
})
19+
20+
const { ensureDir, writeFile } = require(`fs-extra`)
21+
const { createFileNode } = require(`../create-file-node`)
22+
const createFileNodeFromBuffer = require(`../create-file-node-from-buffer`)
23+
24+
const createMockBuffer = content => {
25+
const buffer = Buffer.alloc(content.length)
26+
buffer.write(content)
27+
return buffer
28+
}
29+
30+
const createMockCache = () => {
31+
return {
32+
get: jest.fn(),
33+
set: jest.fn(),
34+
}
35+
}
36+
37+
const bufferEq = (b1, b2) => Buffer.compare(b1, b2) === 0
38+
39+
describe(`create-file-node-from-buffer`, () => {
40+
const defaultArgs = {
41+
store: {
42+
getState: jest.fn(() => {
43+
return {
44+
program: {
45+
directory: `__whatever__`,
46+
},
47+
}
48+
}),
49+
},
50+
createNode: jest.fn(),
51+
createNodeId: jest.fn(),
52+
}
53+
54+
describe(`functionality`, () => {
55+
afterEach(() => jest.clearAllMocks())
56+
57+
const setup = ({
58+
hash,
59+
buffer = createMockBuffer(`some binary content`),
60+
cache = createMockCache(),
61+
} = {}) =>
62+
createFileNodeFromBuffer({
63+
...defaultArgs,
64+
buffer,
65+
hash,
66+
cache,
67+
})
68+
69+
it(`rejects when the buffer can't be read`, () => {
70+
expect(setup({ buffer: null })).rejects.toEqual(
71+
expect.stringContaining(`bad buffer`)
72+
)
73+
})
74+
75+
it(`caches the buffer's content locally`, async () => {
76+
expect.assertions(2)
77+
78+
let output
79+
writeFile.mockImplementationOnce((_f, buf, cb) => {
80+
output = buf
81+
cb()
82+
})
83+
84+
const buffer = createMockBuffer(`buffer-content`)
85+
await setup({ buffer })
86+
87+
expect(ensureDir).toBeCalledTimes(2)
88+
expect(bufferEq(buffer, output)).toBe(true)
89+
})
90+
91+
it(`uses cached file Promise for buffer with a matching hash`, async () => {
92+
expect.assertions(3)
93+
94+
const cache = createMockCache()
95+
96+
await setup({ cache, hash: `same-hash` })
97+
await setup({ cache, hash: `same-hash` })
98+
99+
expect(cache.get).toBeCalledTimes(1)
100+
expect(cache.set).toBeCalledTimes(1)
101+
expect(writeFile).toBeCalledTimes(1)
102+
})
103+
104+
it(`uses cached file from previous run with a matching hash`, async () => {
105+
expect.assertions(3)
106+
107+
const cache = createMockCache()
108+
cache.get.mockImplementationOnce(() => `cached-file-path`)
109+
110+
await setup({ cache, hash: `cached-hash` })
111+
112+
expect(cache.get).toBeCalledWith(expect.stringContaining(`cached-hash`))
113+
expect(cache.set).not.toBeCalled()
114+
expect(createFileNode).toBeCalledWith(
115+
expect.stringContaining(`cached-file-path`),
116+
expect.any(Function),
117+
expect.any(Object)
118+
)
119+
})
120+
})
121+
122+
describe(`validation`, () => {
123+
it(`throws on invalid inputs: createNode`, () => {
124+
expect(() => {
125+
createFileNodeFromBuffer({
126+
...defaultArgs,
127+
createNode: undefined,
128+
})
129+
}).toThrowErrorMatchingInlineSnapshot(
130+
`"createNode must be a function, was undefined"`
131+
)
132+
})
133+
134+
it(`throws on invalid inputs: createNodeId`, () => {
135+
expect(() => {
136+
createFileNodeFromBuffer({
137+
...defaultArgs,
138+
createNodeId: undefined,
139+
})
140+
}).toThrowErrorMatchingInlineSnapshot(
141+
`"createNodeId must be a function, was undefined"`
142+
)
143+
})
144+
145+
it(`throws on invalid inputs: cache`, () => {
146+
expect(() => {
147+
createFileNodeFromBuffer({
148+
...defaultArgs,
149+
cache: undefined,
150+
})
151+
}).toThrowErrorMatchingInlineSnapshot(
152+
`"cache must be the Gatsby cache, was undefined"`
153+
)
154+
})
155+
156+
it(`throws on invalid inputs: store`, () => {
157+
expect(() => {
158+
createFileNodeFromBuffer({
159+
...defaultArgs,
160+
store: undefined,
161+
})
162+
}).toThrowErrorMatchingInlineSnapshot(
163+
`"store must be the redux store, was undefined"`
164+
)
165+
})
166+
})
167+
})

0 commit comments

Comments
 (0)