@netlify/content-engine
Version:
342 lines • 14.7 kB
JavaScript
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.addInferredFields = void 0;
const lodash_groupby_1 = __importDefault(require("lodash.groupby"));
const lodash_sortby_1 = __importDefault(require("lodash.sortby"));
const lodash_get_1 = __importDefault(require("lodash.get"));
const upper_first_1 = require("../../core-utils/upper-first");
const graphql_compose_1 = require("graphql-compose");
const graphql_1 = require("graphql");
const invariant_1 = __importDefault(require("invariant"));
const reporter_1 = __importDefault(require("../../reporter"));
const is_file_1 = require("./is-file");
const date_1 = require("../types/date");
const derived_types_1 = require("../types/derived-types");
const report_once_1 = require("../../utils/report-once");
const is_32_bit_integer_1 = require("../../utils/is-32-bit-integer");
const datastore_1 = require("../../datastore");
const addInferredFields = ({ schemaComposer, typeComposer, exampleValue, typeMapping, }) => {
const config = getInferenceConfig({
typeComposer,
defaults: {
shouldAddFields: true,
},
});
addInferredFieldsImpl({
schemaComposer,
typeComposer,
exampleObject: exampleValue,
prefix: typeComposer.getTypeName(),
unsanitizedFieldPath: [typeComposer.getTypeName()],
typeMapping,
config,
});
if (deprecatedNodeKeys.size > 0) {
const plugin = typeComposer.getExtension(`plugin`);
(0, report_once_1.reportOnce)(`Plugin "${plugin}" is using the ___NODE convention which is deprecated. This plugin should use the @link directive instead.`, `verbose`);
}
};
exports.addInferredFields = addInferredFields;
const addInferredFieldsImpl = ({ schemaComposer, typeComposer, exampleObject, typeMapping, prefix, unsanitizedFieldPath, config, }) => {
const fields = [];
Object.keys(exampleObject).forEach((unsanitizedKey) => {
const key = createFieldName(unsanitizedKey);
fields.push({
key,
unsanitizedKey,
exampleValue: exampleObject[unsanitizedKey],
});
});
const fieldsByKey = (0, lodash_groupby_1.default)(fields, (field) => field.key);
Object.keys(fieldsByKey).forEach((key) => {
const possibleFields = fieldsByKey[key];
let selectedField;
if (possibleFields.length > 1) {
const field = resolveMultipleFields(possibleFields);
const possibleFieldsNames = possibleFields
.map((field) => `\`${field.unsanitizedKey}\``)
.join(`, `);
reporter_1.default.warn(`Multiple node fields resolve to the same GraphQL field \`${prefix}.${field.key}\` - [${possibleFieldsNames}]. Gatsby will use \`${field.unsanitizedKey}\`.`);
selectedField = field;
}
else {
selectedField = possibleFields[0];
}
const fieldConfig = getFieldConfig({
...selectedField,
schemaComposer,
typeComposer,
prefix,
unsanitizedFieldPath,
typeMapping,
config,
});
if (!fieldConfig)
return;
if (!typeComposer.hasField(key)) {
if (config.shouldAddFields) {
typeComposer.addFields({ [key]: fieldConfig });
typeComposer.setFieldExtension(key, `createdFrom`, `inference`);
}
}
});
return typeComposer;
};
const deprecatedNodeKeys = new Set();
const getFieldConfig = ({ schemaComposer, typeComposer, prefix, exampleValue, key, unsanitizedKey, unsanitizedFieldPath, typeMapping, config, }) => {
const selector = `${prefix}.${key}`;
unsanitizedFieldPath.push(unsanitizedKey);
let arrays = 0;
let value = exampleValue;
while (Array.isArray(value)) {
value = value[0];
arrays++;
}
let fieldConfig;
if (hasMapping(typeMapping, selector)) {
// TODO: Use `prefix` instead of `selector` in hasMapping and getFromMapping?
// i.e. does the config contain sanitized field names?
fieldConfig = getFieldConfigFromMapping({ typeMapping, selector });
}
else if (unsanitizedKey.includes(`___NODE`)) {
// TODO(v5): Remove ability to use foreign keys like this (e.g. author___NODE___contact___email)
// and recommend using schema customization instead
fieldConfig = getFieldConfigFromFieldNameConvention({
schemaComposer,
value: exampleValue,
key: unsanitizedKey,
});
arrays = arrays + (value.multiple ? 1 : 0);
deprecatedNodeKeys.add(unsanitizedKey);
}
else {
fieldConfig = getSimpleFieldConfig({
schemaComposer,
typeComposer,
key,
value,
selector,
unsanitizedFieldPath,
typeMapping,
config,
arrays,
});
}
unsanitizedFieldPath.pop();
if (!fieldConfig)
return null;
// Proxy resolver to unsanitized fieldName in case it contained invalid characters
if (key !== unsanitizedKey.split(`___NODE`)[0]) {
fieldConfig = {
...fieldConfig,
extensions: {
...(fieldConfig.extensions || {}),
proxy: { from: unsanitizedKey },
},
};
}
while (arrays > 0) {
fieldConfig = { ...fieldConfig, type: [fieldConfig.type] };
arrays--;
}
return fieldConfig;
};
const resolveMultipleFields = (possibleFields) => {
const nodeField = possibleFields.find((field) => field.unsanitizedKey.includes(`___NODE`));
if (nodeField) {
return nodeField;
}
const canonicalField = possibleFields.find((field) => field.unsanitizedKey === field.key);
if (canonicalField) {
return canonicalField;
}
return (0, lodash_sortby_1.default)(possibleFields, (field) => field.unsanitizedKey)[0];
};
// XXX(freiksenet): removing this as it's a breaking change
// Deeper nested levels should be inferred as JSON.
// const MAX_DEPTH = 5
const hasMapping = (mapping, selector) => mapping && Object.keys(mapping).includes(selector);
const getFieldConfigFromMapping = ({ typeMapping, selector }) => {
const [type, ...path] = typeMapping[selector].split(`.`);
return {
type,
extensions: {
link: { by: path.join(`.`) || `id` },
},
};
};
// probably should be in example value
const getFieldConfigFromFieldNameConvention = ({ schemaComposer, value, key, }) => {
const path = key.split(`___NODE___`)[1];
// Allow linking by nested fields, e.g. `author___NODE___contact___email`
const foreignKey = path && path.replace(/___/g, `.`);
const linkedTypesSet = new Set();
if (foreignKey) {
const linkedValues = new Set(value.linkedNodes);
(0, datastore_1.getDataStore)()
.iterateNodes()
.forEach((node) => {
const value = (0, lodash_get_1.default)(node, foreignKey);
if (linkedValues.has(value)) {
linkedTypesSet.add(node.internal.type);
}
});
}
else {
value.linkedNodes.forEach((id) => {
const node = (0, datastore_1.getDataStore)().getNode(id);
if (node) {
linkedTypesSet.add(node.internal.type);
}
});
}
const linkedTypes = [...linkedTypesSet];
(0, invariant_1.default)(linkedTypes.length, `Encountered an error trying to infer a GraphQL type for: \`${key}\`. ` +
`There is no corresponding node with the \`id\` field matching: "${value.linkedNodes}".`);
let type;
// If the field value is an array that links to more than one type,
// create a GraphQLUnionType. Note that we don't support the case where
// scalar fields link to different types. Similarly, an array of objects
// with foreign-key fields will produce union types if those foreign-key
// fields are arrays, but not if they are scalars. See the tests for an example.
if (linkedTypes.length > 1) {
const typeName = linkedTypes.sort().join(``) + `Union`;
type = schemaComposer.getOrCreateUTC(typeName, (utc) => {
utc.setTypes(linkedTypes.map((typeName) => schemaComposer.getOTC(typeName)));
utc.setResolveType((node) => node.internal.type);
});
}
else {
type = linkedTypes[0];
}
return {
type,
extensions: {
link: { by: foreignKey || `id`, from: key },
},
};
};
const getSimpleFieldConfig = ({ schemaComposer, typeComposer, key, value, selector, unsanitizedFieldPath, typeMapping, config, arrays, }) => {
switch (typeof value) {
case `boolean`:
return { type: `Boolean` };
case `number`:
return { type: (0, is_32_bit_integer_1.is32BitInteger)(value) ? `Int` : `Float` };
case `string`:
if ((0, date_1.isDate)(value)) {
return { type: `Date`, extensions: { dateformat: {} } };
}
if ((0, is_file_1.isFile)(unsanitizedFieldPath, value)) {
// NOTE: For arrays of files, where not every path references
// a File node in the db, it is semi-random if the field is
// inferred as File or String, since the exampleValue only has
// the first entry (which could point to an existing file or not).
return { type: `File`, extensions: { fileByRelativePath: {} } };
}
return { type: `String` };
case `object`:
if (value instanceof Date) {
return { type: `Date`, extensions: { dateformat: {} } };
}
if (value instanceof String) {
return { type: `String` };
}
if (value /* && depth < MAX_DEPTH*/) {
let fieldTypeComposer;
if (typeComposer.hasField(key)) {
fieldTypeComposer = typeComposer.getFieldTC(key);
// If we have an object as a field value, but the field type is
// explicitly defined as something other than an ObjectType
// we can bail early.
if (!(fieldTypeComposer instanceof graphql_compose_1.ObjectTypeComposer))
return null;
// If the array depth of the field value and of the explicitly
// defined field type don't match we can also bail early.
let lists = 0;
let fieldType = typeComposer.getFieldType(key);
while (fieldType.ofType) {
if (fieldType instanceof graphql_1.GraphQLList)
lists++;
fieldType = fieldType.ofType;
}
if (lists !== arrays)
return null;
}
else {
// When the field type has not been explicitly defined, we
// don't need to continue in case of @dontInfer
if (!config.shouldAddFields)
return null;
const typeName = createTypeName(selector);
if (schemaComposer.has(typeName)) {
// Type could have been already created via schema customization
fieldTypeComposer = schemaComposer.getOTC(typeName);
}
else {
fieldTypeComposer = graphql_compose_1.ObjectTypeComposer.create(typeName, schemaComposer);
fieldTypeComposer.setExtension(`createdFrom`, `inference`);
fieldTypeComposer.setExtension(`plugin`, typeComposer.getExtension(`plugin`));
(0, derived_types_1.addDerivedType)({
typeComposer,
derivedTypeName: fieldTypeComposer.getTypeName(),
});
}
}
// Inference config options are either explicitly defined on a type
// with directive/extension, or inherited from the parent type.
const inferenceConfig = getInferenceConfig({
typeComposer: fieldTypeComposer,
defaults: config,
});
return {
type: addInferredFieldsImpl({
schemaComposer,
typeComposer: fieldTypeComposer,
exampleObject: value,
typeMapping,
prefix: selector,
unsanitizedFieldPath,
config: inferenceConfig,
}),
};
}
}
throw new Error(`Can't determine type for "${value}" in \`${selector}\`.`);
};
const createTypeName = (selector) => {
const keys = selector.split(`.`);
const suffix = keys.slice(1).map(upper_first_1.upperFirst).join(``);
return `${keys[0]}${suffix}`;
};
const NON_ALPHA_NUMERIC_EXPR = new RegExp(`[^a-zA-Z0-9_]`, `g`);
/**
* GraphQL field names must be a string and cannot contain anything other than
* alphanumeric characters and `_`. They also can't start with `__` which is
* reserved for internal fields (`___foo` doesn't work either).
*/
const createFieldName = (key) => {
// Check if the key is really a string otherwise GraphQL will throw.
(0, invariant_1.default)(typeof key === `string`, `GraphQL field name (key) is not a string: \`${key}\`.`);
const fieldName = key.split(`___NODE`)[0];
const replaced = fieldName.replace(NON_ALPHA_NUMERIC_EXPR, `_`);
// key is invalid; normalize with leading underscore and rest with x
if (replaced.match(/^__/)) {
return replaced.replace(/_/g, (_char, index) => index === 0 ? `_` : `x`);
}
// key is invalid (starts with numeric); normalize with leading underscore
if (replaced.match(/^[0-9]/)) {
return `_` + replaced;
}
return replaced;
};
const getInferenceConfig = ({ typeComposer, defaults }) => {
return {
shouldAddFields: typeComposer.hasExtension(`infer`)
? typeComposer.getExtension(`infer`)
: defaults.shouldAddFields,
};
};
//# sourceMappingURL=add-inferred-fields.js.map
;