UNPKG

gatsby

Version:
920 lines (760 loc) • 26.9 kB
"use strict"; var _redux = require("../redux"); var _datastore = require("../datastore"); var _iterable = require("../datastore/common/iterable"); var _reportOnce = require("../utils/report-once"); var _detectNodeMutations = require("../utils/detect-node-mutations"); var _utils = require("./utils"); const _ = require(`lodash`); const { isAbstractType, GraphQLOutputType, GraphQLUnionType, GraphQLList, getNamedType, getNullableType, isCompositeType } = require(`graphql`); const invariant = require(`invariant`); const reporter = require(`gatsby-cli/lib/reporter`); class LocalNodeModel { constructor({ schema, schemaComposer, createPageDependency, _rootNodeMap, _trackedRootNodes }) { this.schema = schema; this.schemaComposer = schemaComposer; this.createPageDependencyActionCreator = createPageDependency; this._rootNodeMap = _rootNodeMap || new WeakMap(); this._trackedRootNodes = _trackedRootNodes || new WeakSet(); this._prepareNodesQueues = {}; this._prepareNodesPromises = {}; this._preparedNodesCache = new Map(); this.replaceFiltersCache(); } createPageDependency(createPageDependencyArgs) { if (createPageDependencyArgs.connection) { const nodeTypeNames = (0, _utils.toNodeTypeNames)(this.schema, createPageDependencyArgs.connection); if (nodeTypeNames) { nodeTypeNames.forEach(typeName => { this.createPageDependencyActionCreator({ ...createPageDependencyArgs, connection: typeName }); }); return; } } this.createPageDependencyActionCreator(createPageDependencyArgs); } /** * Replace the cache either with the value passed on (mainly for tests) or * an empty new Map. * * @param {undefined | null | FiltersCache} map * (This cached is used in redux/nodes.js and caches a set of buckets (Sets) * of Nodes based on filter and tracks this for each set of types which are * actually queried. If the filter targets `id` directly, only one Node is * cached instead of a Set of Nodes. If null, don't create or use a cache. */ replaceFiltersCache(map = new Map()) { this._filtersCache = map; // See redux/nodes.js for usage } withContext(context) { return new ContextualNodeModel(this, context); } /** * Get a node from the store by ID and optional type. * * @param {Object} args * @param {string} args.id ID of the requested node * @param {(string|GraphQLOutputType)} [args.type] Optional type of the node * @param {PageDependencies} [pageDependencies] * @returns {(Node|null)} */ getNodeById(args, pageDependencies) { const { id, type } = args || {}; const node = getNodeById(id); let result; if (!node) { result = null; } else if (!type) { result = node; } else { const nodeTypeNames = (0, _utils.toNodeTypeNames)(this.schema, type); result = nodeTypeNames.includes(node.internal.type) ? node : null; } if (result) { this.trackInlineObjectsInRootNode(node); } return (0, _detectNodeMutations.wrapNode)(this.trackPageDependencies(result, pageDependencies)); } /** * Get nodes from the store by IDs and optional type. * * @param {Object} args * @param {string[]} args.ids IDs of the requested nodes * @param {(string|GraphQLOutputType)} [args.type] Optional type of the nodes * @param {PageDependencies} [pageDependencies] * @returns {Node[]} */ getNodesByIds(args, pageDependencies) { const { ids, type } = args || {}; const nodes = Array.isArray(ids) ? ids.map(id => getNodeById(id)).filter(Boolean) : []; let result; if (!nodes.length || !type) { result = nodes; } else { const nodeTypeNames = (0, _utils.toNodeTypeNames)(this.schema, type); result = nodes.filter(node => nodeTypeNames.includes(node.internal.type)); } if (result) { result.forEach(node => this.trackInlineObjectsInRootNode(node)); } return (0, _detectNodeMutations.wrapNodes)(this.trackPageDependencies(result, pageDependencies)); } /** * Get all nodes in the store, or all nodes of a specified type. Note that * this adds connectionType tracking by default if type is passed. * * @deprecated Since version 4.0 - Use nodeModel.findAll() instead * @param {Object} args * @param {(string|GraphQLOutputType)} [args.type] Optional type of the nodes * @param {PageDependencies} [pageDependencies] * @returns {Node[]} */ getAllNodes(args, pageDependencies = {}) { // TODO(v5): Remove API (0, _reportOnce.reportOnce)(`nodeModel.getAllNodes() is deprecated. Use nodeModel.findAll() with an empty query instead.`); const { type = `Node` } = args || {}; let result; if (type === `Node`) { result = (0, _datastore.getNodes)(); } else { const nodeTypeNames = (0, _utils.toNodeTypeNames)(this.schema, type); const nodesByType = nodeTypeNames.map(typeName => (0, _datastore.getNodesByType)(typeName)); const nodes = [].concat(...nodesByType); result = nodes.filter(Boolean); } if (result) { result.forEach(node => this.trackInlineObjectsInRootNode(node)); } if (typeof pageDependencies.connectionType === `undefined`) { pageDependencies.connectionType = typeof type === `string` ? type : type.name; } return (0, _detectNodeMutations.wrapNodes)(this.trackPageDependencies(result, pageDependencies)); } /** * Get nodes of a type matching the specified query. * * @deprecated Since version 4.0 - Use nodeModel.findAll() or nodeModel.findOne() instead * @param {Object} args * @param {Object} args.query Query arguments (`filter` and `sort`) * @param {(string|GraphQLOutputType)} args.type Type * @param {boolean} [args.firstOnly] If true, return only first match * @param {PageDependencies} [pageDependencies] * @returns {Promise<Node[]>} */ async runQuery(args, pageDependencies = {}) { // TODO(v5): Remove API (0, _reportOnce.reportOnce)(`nodeModel.runQuery() is deprecated. Use nodeModel.findAll() or nodeModel.findOne() instead.`); if (args.firstOnly) { return this.findOne(args, pageDependencies); } const { skip, limit, ...query } = args.query; const result = await this.findAll({ ...args, query }, pageDependencies); return Array.from(result.entries); } async _query(args) { var _query$filter, _query$filter$id; const { query = {}, type, stats, tracer } = args || {}; // We don't support querying union types (yet?), because the combined types // need not have any fields in common. const gqlType = typeof type === `string` ? this.schema.getType(type) : type; invariant(!(gqlType instanceof GraphQLUnionType), `Querying GraphQLUnion types is not supported.`); const nodeTypeNames = (0, _utils.toNodeTypeNames)(this.schema, gqlType); let runQueryActivity; // check if we can get node by id and skip // more expensive query pipeline if (typeof (query === null || query === void 0 ? void 0 : (_query$filter = query.filter) === null || _query$filter === void 0 ? void 0 : (_query$filter$id = _query$filter.id) === null || _query$filter$id === void 0 ? void 0 : _query$filter$id.eq) !== `undefined` && Object.keys(query.filter).length === 1 && Object.keys(query.filter.id).length === 1) { if (tracer) { runQueryActivity = reporter.phantomActivity(`runQuerySimpleIdEq`, { parentSpan: tracer.getParentActivity().span }); runQueryActivity.start(); } const nodeFoundById = this.getNodeById({ id: query.filter.id.eq, type: gqlType }); if (runQueryActivity) { runQueryActivity.end(); } return { gqlType, entries: new _iterable.GatsbyIterable(nodeFoundById ? [nodeFoundById] : []), totalCount: async () => nodeFoundById ? 1 : 0 }; } let materializationActivity; if (tracer) { materializationActivity = reporter.phantomActivity(`Materialization`, { parentSpan: tracer.getParentActivity().span }); materializationActivity.start(); } const fields = getQueryFields({ filter: query.filter, sort: query.sort, group: query.group, distinct: query.distinct, max: query.max, min: query.min, sum: query.sum }); const fieldsToResolve = determineResolvableFields(this.schemaComposer, this.schema, gqlType, fields); for (const nodeTypeName of nodeTypeNames) { const gqlNodeType = this.schema.getType(nodeTypeName); await this.prepareNodes(gqlNodeType, fields, fieldsToResolve); } if (materializationActivity) { materializationActivity.end(); } if (tracer) { runQueryActivity = reporter.phantomActivity(`runQuery`, { parentSpan: tracer.getParentActivity().span }); runQueryActivity.start(); } const { entries, totalCount } = await (0, _datastore.getDataStore)().runQuery({ queryArgs: query, gqlSchema: this.schema, gqlComposer: this.schemaComposer, gqlType, resolvedFields: fieldsToResolve, nodeTypeNames, filtersCache: this._filtersCache, stats }); if (runQueryActivity) { runQueryActivity.end(); } return { gqlType, entries: entries.map(node => { // With GatsbyIterable it happens lazily as we iterate this.trackInlineObjectsInRootNode(node); return node; }), totalCount }; } /** * Get all nodes in the store, or all nodes of a specified type (optionally with limit/skip). * Returns slice of result as iterable and total count of nodes. * * @param {*} args * @param {Object} args.query Query arguments (e.g. `limit` and `skip`) * @param {(string|GraphQLOutputType)} args.type Type * @param {PageDependencies} [pageDependencies] * @return {Promise<Object>} Object containing `{ entries: GatsbyIterable, totalCount: () => Promise<number> }` */ async findAll(args, pageDependencies = {}) { const { gqlType, ...result } = await this._query(args, pageDependencies); // Tracking connections by default: if (typeof pageDependencies.connectionType === `undefined`) { pageDependencies.connectionType = gqlType.name; } this.trackPageDependencies(result.entries, pageDependencies); return { entries: (0, _detectNodeMutations.wrapNodes)(result.entries), totalCount: result.totalCount }; } /** * Get one node in the store. Only returns the first result. * * @param {*} args * @param {Object} args.query Query arguments (e.g. `filter`). Doesn't support `sort`, `limit`, `skip`. * @param {(string|GraphQLOutputType)} args.type Type * @param {PageDependencies} [pageDependencies] * @returns {Promise<Node>} */ async findOne(args, pageDependencies = {}) { var _query$sort, _query$sort$fields, _result$; const { query = {} } = args; if (((_query$sort = query.sort) === null || _query$sort === void 0 ? void 0 : (_query$sort$fields = _query$sort.fields) === null || _query$sort$fields === void 0 ? void 0 : _query$sort$fields.length) > 0) { // If we support sorting and return the first node based on sorting // we'll have to always track connection not an individual node throw new Error(`nodeModel.findOne() does not support sorting. Use nodeModel.findAll({ query: { limit: 1 } }) instead.`); } const { gqlType, entries } = await this._query({ ...args, query: { ...query, skip: 0, limit: 1, sort: undefined } }); const result = Array.from(entries); const first = (_result$ = result[0]) !== null && _result$ !== void 0 ? _result$ : null; if (!first) { // Couldn't find matching node. // This leads to a state where data tracking for this query gets empty. // It means we will NEVER re-run this query on any data updates // (even if a new node matching this query is added at some point). // To workaround this, we have to add a connection tracking to re-run // the query whenever any node of this type changes. pageDependencies.connectionType = gqlType.name; } return (0, _detectNodeMutations.wrapNode)(this.trackPageDependencies(first, pageDependencies)); } prepareNodes(type, queryFields, fieldsToResolve) { const typeName = type.name; if (!this._prepareNodesQueues[typeName]) { this._prepareNodesQueues[typeName] = []; } this._prepareNodesQueues[typeName].push({ queryFields, fieldsToResolve }); if (!this._prepareNodesPromises[typeName]) { this._prepareNodesPromises[typeName] = new Promise(resolve => { process.nextTick(async () => { await this._doResolvePrepareNodesQueue(type); resolve(); }); }); } return this._prepareNodesPromises[typeName]; } async _doResolvePrepareNodesQueue(type) { const typeName = type.name; const queue = this._prepareNodesQueues[typeName]; this._prepareNodesQueues[typeName] = []; this._prepareNodesPromises[typeName] = null; const { queryFields, fieldsToResolve } = queue.reduce(({ queryFields, fieldsToResolve }, { queryFields: nextQueryFields, fieldsToResolve: nextFieldsToResolve }) => { return { queryFields: _.merge(queryFields, nextQueryFields), fieldsToResolve: _.merge(fieldsToResolve, nextFieldsToResolve) }; }, { queryFields: {}, fieldsToResolve: {} }); const actualFieldsToResolve = deepObjectDifference(fieldsToResolve, this._preparedNodesCache.get(typeName) || {}); if (!_.isEmpty(actualFieldsToResolve)) { const { schemaCustomization: { context: customContext } } = _redux.store.getState(); const resolvedNodes = new Map(); for (const node of (0, _datastore.getDataStore)().iterateNodesByType(typeName)) { this.trackInlineObjectsInRootNode(node); const resolvedFields = await resolveRecursive(this, this.schemaComposer, this.schema, node, type, queryFields, actualFieldsToResolve, customContext); resolvedNodes.set(node.id, resolvedFields); } if (resolvedNodes.size) { await saveResolvedNodes(typeName, resolvedNodes); } this._preparedNodesCache.set(typeName, _.merge({}, this._preparedNodesCache.get(typeName) || {}, actualFieldsToResolve)); } } /** * Get the names of all node types in the store. * * @returns {string[]} */ getTypes() { return (0, _datastore.getTypes)(); } /** * Adds link between inline objects/arrays contained in Node object * and that Node object. * @param {Node} node Root Node */ trackInlineObjectsInRootNode(node) { if (!this._trackedRootNodes.has(node)) { addRootNodeToInlineObject(this._rootNodeMap, node, node.id, true, new Set()); this._trackedRootNodes.add(node); } } /** * Finds top most ancestor of node that contains passed Object or Array * @param {(Object|Array)} obj Object/Array belonging to Node object or Node object * @param {nodePredicate} [predicate] Optional callback to check if ancestor meets defined conditions * @returns {Node} Top most ancestor if predicate is not specified * or first node that meet predicate conditions if predicate is specified */ findRootNodeAncestor(obj, predicate = null) { let iterations = 0; let ids = this._rootNodeMap.get(obj); if (!ids) { ids = []; } if (obj !== null && obj !== void 0 && obj.parent) { ids.push(obj.parent); } let matchingRoot = null; if (ids) { for (const id of ids) { let tracked = getNodeById(id); if (tracked) { const visited = new Set(); while (iterations++ < 100) { if (predicate && predicate(tracked)) { return tracked; } if (visited.has(tracked)) { reporter.error(`It looks like you have a node that's set its parent as itself:\n\n` + tracked); break; } visited.add(tracked); const parent = getNodeById(tracked.parent); if (!parent) { break; } tracked = parent; } if (tracked && !predicate) { matchingRoot = tracked; } } } } return matchingRoot; } /** * Given a result, that's either a single node or an array of them, track them * using pageDependencies. Defaults to tracking according to current resolver * path. Returns the result back. * * @param {Node | Node[]} result * @param {PageDependencies} [pageDependencies] * @returns {Node | Node[]} */ trackPageDependencies(result, pageDependencies = {}) { const { path, connectionType, track = true } = pageDependencies; if (path && track) { if (connectionType) { this.createPageDependency({ path, connection: connectionType }); } else { const nodes = (0, _iterable.isIterable)(result) ? result : [result]; for (const node of nodes) { if (node) { this.createPageDependency({ path, nodeId: node.id }); } } } } return result; } } class ContextualNodeModel { constructor(rootNodeModel, context) { this.nodeModel = rootNodeModel; this.context = context; } withContext(context) { return new ContextualNodeModel(this.nodeModel, { ...this.context, ...context }); } _getFullDependencies(pageDependencies) { return { path: this.context.path, ...(pageDependencies || {}) }; } getNodeById(args, pageDependencies) { return this.nodeModel.getNodeById(args, this._getFullDependencies(pageDependencies)); } getNodesByIds(args, pageDependencies) { return this.nodeModel.getNodesByIds(args, this._getFullDependencies(pageDependencies)); } getAllNodes(args, pageDependencies) { return this.nodeModel.getAllNodes(args, this._getFullDependencies(pageDependencies)); } runQuery(args, pageDependencies) { return this.nodeModel.runQuery(args, this._getFullDependencies(pageDependencies)); } findOne(args, pageDependencies) { return this.nodeModel.findOne(args, this._getFullDependencies(pageDependencies)); } findAll(args, pageDependencies) { return this.nodeModel.findAll(args, this._getFullDependencies(pageDependencies)); } prepareNodes(...args) { return this.nodeModel.prepareNodes(...args); } getTypes(...args) { return this.nodeModel.getTypes(...args); } trackInlineObjectsInRootNode(...args) { return this.nodeModel.trackInlineObjectsInRootNode(...args); } findRootNodeAncestor(...args) { return this.nodeModel.findRootNodeAncestor(...args); } createPageDependency(...args) { return this.nodeModel.createPageDependency(...args); } trackPageDependencies(result, pageDependencies) { return this.nodeModel.trackPageDependencies(result, this._getFullDependencies(pageDependencies)); } } const getNodeById = id => id != null ? (0, _datastore.getNode)(id) : null; const getQueryFields = ({ filter, sort, group, distinct, max, min, sum }) => { const filterFields = filter ? dropQueryOperators(filter) : {}; const sortFields = sort && sort.fields || []; if (group && !Array.isArray(group)) { group = [group]; } else if (group == null) { group = []; } if (distinct && !Array.isArray(distinct)) { distinct = [distinct]; } else if (distinct == null) { distinct = []; } if (max && !Array.isArray(max)) { max = [max]; } else if (max == null) { max = []; } if (min && !Array.isArray(min)) { min = [min]; } else if (min == null) { min = []; } if (sum && !Array.isArray(sum)) { sum = [sum]; } else if (sum == null) { sum = []; } return _.merge(filterFields, ...sortFields.map(pathToObject), ...group.map(pathToObject), ...distinct.map(pathToObject), ...max.map(pathToObject), ...min.map(pathToObject), ...sum.map(pathToObject)); }; const pathToObject = path => { if (path && typeof path === `string`) { return path.split(`.`).reduceRight((acc, key) => { return { [key]: acc }; }, true); } return {}; }; const dropQueryOperators = filter => Object.keys(filter).reduce((acc, key) => { const value = filter[key]; const k = Object.keys(value)[0]; const v = value[k]; if (_.isPlainObject(value) && _.isPlainObject(v)) { acc[key] = k === `elemMatch` ? dropQueryOperators(v) : dropQueryOperators(value); } else { acc[key] = true; } return acc; }, {}); const getFields = (schema, type, node) => { if (!isAbstractType(type)) { return type.getFields(); } const concreteType = type.resolveType(node); return schema.getType(concreteType).getFields(); }; async function resolveRecursive(nodeModel, schemaComposer, schema, node, type, queryFields, fieldsToResolve, customContext) { const gqlFields = getFields(schema, type, node); const resolvedFields = {}; for (const fieldName of Object.keys(fieldsToResolve)) { const fieldToResolve = fieldsToResolve[fieldName]; const queryField = queryFields[fieldName]; const gqlField = gqlFields[fieldName]; const gqlNonNullType = getNullableType(gqlField.type); const gqlFieldType = getNamedType(gqlField.type); let innerValue = await resolveField(nodeModel, schemaComposer, schema, node, gqlField, fieldName, customContext); if (gqlField && innerValue != null) { if (isCompositeType(gqlFieldType) && !(gqlNonNullType instanceof GraphQLList)) { innerValue = await resolveRecursive(nodeModel, schemaComposer, schema, innerValue, gqlFieldType, queryField, _.isObject(fieldToResolve) ? fieldToResolve : queryField, customContext); } else if (isCompositeType(gqlFieldType) && (_.isArray(innerValue) || innerValue instanceof _iterable.GatsbyIterable) && gqlNonNullType instanceof GraphQLList) { innerValue = await Promise.all(innerValue.map(item => item == null ? item : resolveRecursive(nodeModel, schemaComposer, schema, item, gqlFieldType, queryField, _.isObject(fieldToResolve) ? fieldToResolve : queryField, customContext))); } } if (innerValue != null) { resolvedFields[fieldName] = innerValue; } } for (const fieldName of Object.keys(queryFields)) { if (!fieldsToResolve[fieldName] && node[fieldName]) { // It is possible that this field still has a custom resolver // See https://github.com/gatsbyjs/gatsby/issues/27368 resolvedFields[fieldName] = await resolveField(nodeModel, schemaComposer, schema, node, gqlFields[fieldName], fieldName, customContext); } } return _.pickBy(resolvedFields, (value, key) => queryFields[key]); } let withResolverContext; function resolveField(nodeModel, schemaComposer, schema, node, gqlField, fieldName, customContext) { if (!(gqlField !== null && gqlField !== void 0 && gqlField.resolve)) { return node[fieldName]; } // We require this inline as there's a circular dependency from context back to this file. // https://github.com/gatsbyjs/gatsby/blob/9d33b107d167e3e9e2aa282924a0c409f6afd5a0/packages/gatsby/src/schema/context.ts#L5 if (!withResolverContext) { withResolverContext = require(`./context`); } return gqlField.resolve(node, gqlField.args.reduce((acc, arg) => { acc[arg.name] = arg.defaultValue; return acc; }, {}), withResolverContext({ schema, schemaComposer, nodeModel, customContext }), { fieldName, schema, returnType: gqlField.type }); } const determineResolvableFields = (schemaComposer, schema, type, fields, isNestedAndParentNeedsResolve = false) => { const fieldsToResolve = {}; const gqlFields = type.getFields(); Object.keys(fields).forEach(fieldName => { const field = fields[fieldName]; const gqlField = gqlFields[fieldName]; const gqlFieldType = getNamedType(gqlField.type); const typeComposer = schemaComposer.getAnyTC(type.name); const needsResolve = (0, _utils.fieldNeedToResolve)({ schema, gqlType: type, typeComposer, schemaComposer, fieldName }); if (_.isObject(field) && gqlField) { const innerResolved = determineResolvableFields(schemaComposer, schema, gqlFieldType, field, isNestedAndParentNeedsResolve || needsResolve); if (!_.isEmpty(innerResolved)) { fieldsToResolve[fieldName] = innerResolved; } } if (!fieldsToResolve[fieldName] && needsResolve) { fieldsToResolve[fieldName] = true; } if (!fieldsToResolve[fieldName] && isNestedAndParentNeedsResolve) { // If parent field needs to be resolved - all nested fields should be added as well // See https://github.com/gatsbyjs/gatsby/issues/26056 fieldsToResolve[fieldName] = true; } }); return fieldsToResolve; }; const addRootNodeToInlineObject = (rootNodeMap, data, nodeId, isNode /* : boolean */ , path /* : Set<mixed> */ ) => { const isPlainObject = _.isPlainObject(data); if (isPlainObject || _.isArray(data)) { if (path.has(data)) return; path.add(data); _.each(data, (o, key) => { if (!isNode || key !== `internal`) { addRootNodeToInlineObject(rootNodeMap, o, nodeId, false, path); } }); // don't need to track node itself if (!isNode) { let nodeIds = rootNodeMap.get(data); if (!nodeIds) { nodeIds = new Set([nodeId]); } else { nodeIds.add(nodeId); } rootNodeMap.set(data, nodeIds); } } }; const saveResolvedNodes = (typeName, resolvedNodes) => { _redux.store.dispatch({ type: `SET_RESOLVED_NODES`, payload: { key: typeName, nodes: resolvedNodes } }); }; const deepObjectDifference = (from, to) => { const result = {}; Object.keys(from).forEach(key => { const toValue = to[key]; if (toValue) { if (_.isPlainObject(toValue)) { const deepResult = deepObjectDifference(from[key], toValue); if (!_.isEmpty(deepResult)) { result[key] = deepResult; } } } else { result[key] = from[key]; } }); return result; }; module.exports = { LocalNodeModel }; //# sourceMappingURL=node-model.js.map