Skip to content

Commit

Permalink
feat(gatsby): Schema rebuilding (#19092)
Browse files Browse the repository at this point in the history
* Refactor example-value to support fast incremental rebuilding

* Detect node structure changes incrementally (to avoid expensive checks later)

* Track metadata for inference in the redux

* New `rebuildWithTypes` API for incremental schema rebuilding

* Schema hot reloading for `develop`

* Move things around for better cohesion

* Replace old `getExampleValue` with new APIs based on inference metadata

* Cleanup / rename things for consistency

* Proper handling of ADD_FIELD_TO_NODE action

* Make sure stale inferred fields are removed from the schema

* More tests to and related fixes to conflict reporting

* Clear derived TypeComposers and InputTypeComposers on type rebuild

* More tests for schema rebuilding

* Delete empty inferred type

* Refactor: use functions for field names generated out of a type name

* More tests + switched to inline snapshots for tests readability

* Support adding / deleting child convenience fields on parent type

* Added nested extension test

* Apply extensions and other special logic to all nested types

* Make sure all derived types are processed on rebuild (including recursive)

* Re-using common method in schema-hot-reloader

* Test conflicts during rebuild-schema

* Test incremental example value building

* Tests for compatibility with schema customization

* Parent type processing should not mess with child structure

* Fix typo in comments

Co-Authored-By: Michael <184316+muescha@users.noreply.github.com>

* Moved `createSchemaCustomization` API call before node sourcing

* Do not collect inference metadata for types with @dontInfer directive (or infer:false extension)

* Use constants vs literal strings for extension names when analyzing type defs

* Develop: reload eslint config on schema rebuild

* Re-run queries on schema rebuild

* Fix loki tests

* Fix eslint error

* Example value: do not return empty object

* Updating tests structure

* Split tests for sitepage and schema rebuild to separate files

* Tests for rebuilding types with existing relations

* Step back and use full schema rebuild (which is relatively fast with inference metadata)

* Fix eslint errors

* Fix invalid test

* Take a list of types for inference from metadata vs node store

* Handle DELETE_NODES action

* Cleaning up a little bit

* Fix loki reducer for DELETE_NODES action

* More descriptive naming for eslint graphql schema reload

* Fix invalid webpack compiler hook argument

* Better detection of changed types to avoid unnecessary rebuilds

* Add missing snapshot

* Add new tests to clarify updating of ___NODE fields

* Be a bit more defensive with haveEqualFields args

* Re-run schema customization on __refresh

* Rebuild schema on schema customization in develop (i.e. called via __refresh)

* Add support for node update

* Fix rebuildWithSitePage extensions
  • Loading branch information
vladar authored and GatsbyJS Bot committed Nov 19, 2019
1 parent 393c1f0 commit e4dae4d
Show file tree
Hide file tree
Showing 46 changed files with 3,699 additions and 728 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,19 @@ async function queryResult(
addInferredFields,
} = require(`../../../gatsby/src/schema/infer/add-inferred-fields`)
const {
getExampleValue,
} = require(`../../../gatsby/src/schema/infer/example-value`)
addNodes,
getExampleObject,
} = require(`../../../gatsby/src/schema/infer/inference-metadata`)

const typeName = `MarkdownRemark`
const sc = createSchemaComposer()
const tc = sc.createObjectTC(typeName)
sc.addTypeDefs(typeDefs)
const inferenceMetadata = addNodes({ typeName }, nodes)
addInferredFields({
schemaComposer: sc,
typeComposer: tc,
exampleValue: getExampleValue({ nodes, typeName }),
exampleValue: getExampleObject(inferenceMetadata),
})
tc.addFields(extendNodeTypeFields)
sc.Query.addFields({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,16 +127,18 @@ yadda yadda
addInferredFields,
} = require(`../../../gatsby/src/schema/infer/add-inferred-fields`)
const {
getExampleValue,
} = require(`../../../gatsby/src/schema/infer/example-value`)
addNodes,
getExampleObject,
} = require(`../../../gatsby/src/schema/infer/inference-metadata`)

const sc = createSchemaComposer()
const typeName = `MarkdownRemark`
const tc = sc.createObjectTC(typeName)
const inferenceMetadata = addNodes({ typeName }, nodes)
addInferredFields({
schemaComposer: sc,
typeComposer: tc,
exampleValue: getExampleValue({ nodes, typeName }),
exampleValue: getExampleObject(inferenceMetadata),
})
sc.Query.addFields({
listNode: { type: [tc], resolve: () => nodes },
Expand Down
10 changes: 10 additions & 0 deletions packages/gatsby/src/bootstrap/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,16 @@ module.exports = async (args: BootstrapArgs) => {
})
activity.end()

// Prepare static schema types
activity = report.activityTimer(`createSchemaCustomization`, {
parentSpan: bootstrapSpan,
})
activity.start()
await require(`../utils/create-schema-customization`)({
parentSpan: bootstrapSpan,
})
activity.end()

// Source nodes
activity = report.activityTimer(`source and transform nodes`, {
parentSpan: bootstrapSpan,
Expand Down
49 changes: 49 additions & 0 deletions packages/gatsby/src/bootstrap/schema-hot-reloader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const { debounce, cloneDeep } = require(`lodash`)
const { emitter, store } = require(`../redux`)
const { rebuild } = require(`../schema`)
const { haveEqualFields } = require(`../schema/infer/inference-metadata`)
const { updateStateAndRunQueries } = require(`../query/query-watcher`)
const report = require(`gatsby-cli/lib/reporter`)

const inferredTypesChanged = (inferenceMetadata, prevInferenceMetadata) =>
Object.keys(inferenceMetadata).filter(
type =>
inferenceMetadata[type].dirty &&
!haveEqualFields(inferenceMetadata[type], prevInferenceMetadata[type])
).length > 0

const schemaChanged = (schemaCustomization, lastSchemaCustomization) =>
[`fieldExtensions`, `printConfig`, `thirdPartySchemas`, `types`].some(
key => schemaCustomization[key] !== lastSchemaCustomization[key]
)

let lastMetadata
let lastSchemaCustomization

// API_RUNNING_QUEUE_EMPTY could be emitted multiple types
// in a short period of time, so debounce seems reasonable
const maybeRebuildSchema = debounce(async () => {
const { inferenceMetadata, schemaCustomization } = store.getState()

if (
!inferredTypesChanged(inferenceMetadata, lastMetadata) &&
!schemaChanged(schemaCustomization, lastSchemaCustomization)
) {
return
}

const activity = report.activityTimer(`rebuild schema`)
activity.start()
lastMetadata = cloneDeep(inferenceMetadata)
lastSchemaCustomization = schemaCustomization
await rebuild({ parentSpan: activity })
await updateStateAndRunQueries(false, { parentSpan: activity })
activity.end()
}, 1000)

module.exports = () => {
const { inferenceMetadata, schemaCustomization } = store.getState()
lastMetadata = cloneDeep(inferenceMetadata)
lastSchemaCustomization = schemaCustomization
emitter.on(`API_RUNNING_QUEUE_EMPTY`, maybeRebuildSchema)
}
26 changes: 19 additions & 7 deletions packages/gatsby/src/commands/develop.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const WorkerPool = require(`../utils/worker/pool`)

const withResolverContext = require(`../schema/context`)
const sourceNodes = require(`../utils/source-nodes`)
const createSchemaCustomization = require(`../utils/create-schema-customization`)
const websocketManager = require(`../utils/websocket-manager`)
const getSslCert = require(`../utils/get-ssl-cert`)
const { slash } = require(`gatsby-core-utils`)
Expand Down Expand Up @@ -187,6 +188,20 @@ async function startServer(program) {
* If no GATSBY_REFRESH_TOKEN env var is available, then no Authorization header is required
**/
const REFRESH_ENDPOINT = `/__refresh`
const refresh = async req => {
let activity = report.activityTimer(`createSchemaCustomization`, {})
activity.start()
await createSchemaCustomization({
refresh: true,
})
activity.end()
activity = report.activityTimer(`Refreshing source data`, {})
activity.start()
await sourceNodes({
webhookBody: req.body,
})
activity.end()
}
app.use(REFRESH_ENDPOINT, express.json())
app.post(REFRESH_ENDPOINT, (req, res) => {
const enableRefresh = process.env.ENABLE_GATSBY_REFRESH_ENDPOINT
Expand All @@ -195,13 +210,7 @@ async function startServer(program) {
!refreshToken || req.headers.authorization === refreshToken

if (enableRefresh && authorizedRefresh) {
const activity = report.activityTimer(`Refreshing source data`, {})
activity.start()
sourceNodes({
webhookBody: req.body,
}).then(() => {
activity.end()
})
refresh(req)
}
res.end()
})
Expand Down Expand Up @@ -374,6 +383,9 @@ module.exports = async (program: any) => {
// Start the createPages hot reloader.
require(`../bootstrap/page-hot-reloader`)(graphqlRunner)

// Start the schema hot reloader.
require(`../bootstrap/schema-hot-reloader`)()

await queryUtil.initialProcessQueries()

require(`../redux/actions`).boundActionCreators.setProgramStatus(
Expand Down
42 changes: 20 additions & 22 deletions packages/gatsby/src/db/loki/nodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ const _ = require(`lodash`)
const invariant = require(`invariant`)
const { getDb, colls } = require(`./index`)

/////////////////////////////////////////////////////////////////////
// ///////////////////////////////////////////////////////////////////
// Node collection metadata
/////////////////////////////////////////////////////////////////////
// ///////////////////////////////////////////////////////////////////

function makeTypeCollName(type) {
return `gatsby:nodeType:${type}`
Expand Down Expand Up @@ -39,7 +39,7 @@ function createNodeTypeCollection(type) {
function getTypeCollName(type) {
const nodeTypesColl = getDb().getCollection(colls.nodeTypes.name)
invariant(nodeTypesColl, `Collection ${colls.nodeTypes.name} should exist`)
let nodeTypeInfo = nodeTypesColl.by(`type`, type)
const nodeTypeInfo = nodeTypesColl.by(`type`, type)
return nodeTypeInfo ? nodeTypeInfo.collName : undefined
}

Expand Down Expand Up @@ -72,7 +72,7 @@ function deleteNodeTypeCollections(force = false) {
// find() returns all objects in collection
const nodeTypes = nodeTypesColl.find()
for (const nodeType of nodeTypes) {
let coll = getDb().getCollection(nodeType.collName)
const coll = getDb().getCollection(nodeType.collName)
if (coll.count() === 0 || force) {
getDb().removeCollection(coll.name)
nodeTypesColl.remove(nodeType)
Expand All @@ -93,9 +93,9 @@ function deleteAll() {
}
}

/////////////////////////////////////////////////////////////////////
// ///////////////////////////////////////////////////////////////////
// Queries
/////////////////////////////////////////////////////////////////////
// ///////////////////////////////////////////////////////////////////

/**
* Returns the node with `id` == id, or null if not found
Expand Down Expand Up @@ -191,9 +191,9 @@ function hasNodeChanged(id, digest) {
}
}

/////////////////////////////////////////////////////////////////////
// /////////////////////////////////////////////////////////////////
// Create/Update/Delete
/////////////////////////////////////////////////////////////////////
// ///////////////////////////////////////////////////////////////////

/**
* Creates a node in the DB. Will create a collection for the node
Expand Down Expand Up @@ -242,11 +242,8 @@ function updateNode(node) {
invariant(node.internal.type, `node has no "internal.type" field`)
invariant(node.id, `node has no "id" field`)

const type = node.internal.type

let coll = getNodeTypeCollection(type)
invariant(coll, `${type} collection doesn't exist. When trying to update`)
coll.update(node)
const oldNode = getNode(node.id)
return createNode(node, oldNode)
}

/**
Expand All @@ -264,22 +261,23 @@ function deleteNode(node) {

const type = node.internal.type

let nodeTypeColl = getNodeTypeCollection(type)
const nodeTypeColl = getNodeTypeCollection(type)
if (!nodeTypeColl) {
invariant(
nodeTypeColl,
`${type} collection doesn't exist. When trying to delete`
)
}

if (nodeTypeColl.by(`id`, node.id)) {
const obj = nodeTypeColl.by(`id`, node.id)
if (obj) {
const nodeMetaColl = getDb().getCollection(colls.nodeMeta.name)
invariant(nodeMetaColl, `Collection ${colls.nodeMeta.name} should exist`)
nodeMetaColl.findAndRemove({ id: node.id })
// TODO What if this `remove()` fails? We will have removed the id
// -> collName mapping, but not the actual node in the
// collection. Need to make this into a transaction
nodeTypeColl.remove(node)
nodeTypeColl.remove(obj)
}
// idempotent. Do nothing if node wasn't already in DB
}
Expand Down Expand Up @@ -326,7 +324,7 @@ function ensureFieldIndexes(typeName, lokiArgs, sortArgs) {
const { emitter } = require(`../../redux`)

emitter.on(`DELETE_CACHE`, () => {
for (var field in fieldUsages) {
for (const field in fieldUsages) {
delete fieldUsages[field]
}
})
Expand All @@ -350,9 +348,9 @@ function ensureFieldIndexes(typeName, lokiArgs, sortArgs) {
})
}

/////////////////////////////////////////////////////////////////////
// ///////////////////////////////////////////////////////////////////
// Reducer
/////////////////////////////////////////////////////////////////////
// ///////////////////////////////////////////////////////////////////

function reducer(state = new Map(), action) {
switch (action.type) {
Expand All @@ -378,7 +376,7 @@ function reducer(state = new Map(), action) {
}

case `DELETE_NODES`: {
deleteNodes(action.payload)
deleteNodes(action.fullNodes)
return null
}

Expand All @@ -387,9 +385,9 @@ function reducer(state = new Map(), action) {
}
}

/////////////////////////////////////////////////////////////////////
// ///////////////////////////////////////////////////////////////////
// Exports
/////////////////////////////////////////////////////////////////////
// ///////////////////////////////////////////////////////////////////

module.exports = {
getNodeTypeCollection,
Expand Down
2 changes: 2 additions & 0 deletions packages/gatsby/src/query/query-watcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -265,3 +265,5 @@ exports.startWatchDeletePage = () => {
}
})
}

exports.updateStateAndRunQueries = updateStateAndRunQueries
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Object {
"query": "",
},
},
"inferenceMetadata": Object {},
"staticQueryComponents": Map {},
"status": Object {
"plugins": Object {},
Expand Down
8 changes: 7 additions & 1 deletion packages/gatsby/src/redux/actions/public.js
Original file line number Diff line number Diff line change
Expand Up @@ -531,10 +531,15 @@ actions.deleteNodes = (nodes: any[], plugin: Plugin) => {
nodes.map(n => findChildren(getNode(n).children))
)

const nodeIds = [...nodes, ...descendantNodes]

const deleteNodesAction = {
type: `DELETE_NODES`,
plugin,
payload: [...nodes, ...descendantNodes],
// Payload contains node IDs but inference-metadata and loki reducers require
// full node instances
payload: nodeIds,
fullNodes: nodeIds.map(getNode),
}
return deleteNodesAction
}
Expand Down Expand Up @@ -961,6 +966,7 @@ actions.createNodeField = (
type: `ADD_FIELD_TO_NODE`,
plugin,
payload: node,
addedField: name,
}
}

Expand Down
5 changes: 4 additions & 1 deletion packages/gatsby/src/redux/actions/restricted.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// @flow
const { camelCase } = require(`lodash`)
const report = require(`gatsby-cli/lib/reporter`)
const { parseTypeDef } = require(`../../schema/types/type-defs`)

import type { Plugin } from "./types"

Expand Down Expand Up @@ -187,7 +188,9 @@ actions.createTypes = (
type: `CREATE_TYPES`,
plugin,
traceId,
payload: types,
payload: Array.isArray(types)
? types.map(parseTypeDef)
: parseTypeDef(types),
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/gatsby/src/redux/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const saveState = () => {
`components`,
`staticQueryComponents`,
`webpackCompilationHash`,
`inferenceMetadata`,
])

return writeToCache(pickedState)
Expand Down
1 change: 1 addition & 0 deletions packages/gatsby/src/redux/reducers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,5 @@ module.exports = {
schemaCustomization: require(`./schema-customization`),
themes: require(`./themes`),
logs: require(`gatsby-cli/lib/reporter/redux/reducer`),
inferenceMetadata: require(`./inference-metadata`),
}
Loading

0 comments on commit e4dae4d

Please sign in to comment.