@graphql-tools/mock
Version:
A set of utils for faster development of GraphQL tools
512 lines (511 loc) • 20.4 kB
JavaScript
import stringify from 'fast-json-stable-stringify';
import { getNullableType, GraphQLString, isAbstractType, isCompositeType, isEnumType, isInterfaceType, isListType, isNullableType, isObjectType, isScalarType, } from 'graphql';
import { deepResolveMockList, isMockList } from './MockList.js';
import { assertIsRef, isRecord, isRef, } from './types.js';
import { makeRef, randomListLength, takeRandom, uuidv4 } from './utils.js';
export const defaultMocks = {
Int: () => Math.round(Math.random() * 200) - 100,
Float: () => Math.random() * 200 - 100,
String: () => 'Hello World',
Boolean: () => Math.random() > 0.5,
ID: () => uuidv4(),
};
const defaultKeyFieldNames = ['id', '_id'];
export class MockStore {
schema;
mocks;
typePolicies;
store = {};
constructor({ schema, mocks, typePolicies, }) {
this.schema = schema;
this.mocks = { ...defaultMocks, ...mocks };
this.typePolicies = typePolicies || {};
}
has(typeName, key) {
return !!this.store[typeName] && !!this.store[typeName][key];
}
get(_typeName, _key, _fieldName, _fieldArgs) {
if (typeof _typeName !== 'string') {
if (_key === undefined) {
if (isRef(_typeName)) {
throw new Error("Can't provide a ref as first argument and no other argument");
}
// get({...})
return this.getImpl(_typeName);
}
else {
assertIsRef(_typeName);
const { $ref } = _typeName;
// arguments shift
_fieldArgs = _fieldName;
_fieldName = _key;
_key = $ref.key;
_typeName = $ref.typeName;
}
}
const args = {
typeName: _typeName,
};
if (isRecord(_key) || _key === undefined) {
// get('User', { name: 'Alex'})
args.defaultValue = _key;
return this.getImpl(args);
}
args.key = _key;
if (Array.isArray(_fieldName) && _fieldName.length === 1) {
_fieldName = _fieldName[0];
}
if (typeof _fieldName !== 'string' && !Array.isArray(_fieldName)) {
// get('User', 'me', { name: 'Alex'})
args.defaultValue = _fieldName;
return this.getImpl(args);
}
if (Array.isArray(_fieldName)) {
// get('User', 'me', ['father', 'name'])
const ref = this.get(_typeName, _key, _fieldName[0], _fieldArgs);
assertIsRef(ref);
return this.get(ref.$ref.typeName, ref.$ref.key, _fieldName.slice(1, _fieldName.length));
}
// get('User', 'me', 'name'...);
args.fieldName = _fieldName;
args.fieldArgs = _fieldArgs;
return this.getImpl(args);
}
set(_typeName, _key, _fieldName, _value) {
if (typeof _typeName !== 'string') {
if (_key === undefined) {
if (isRef(_typeName)) {
throw new Error("Can't provide a ref as first argument and no other argument");
}
// set({...})
return this.setImpl(_typeName);
}
else {
assertIsRef(_typeName);
const { $ref } = _typeName;
// arguments shift
_value = _fieldName;
_fieldName = _key;
_key = $ref.key;
_typeName = $ref.typeName;
}
}
assertIsDefined(_key, 'key was not provided');
const args = {
typeName: _typeName,
key: _key,
};
if (typeof _fieldName !== 'string') {
// set('User', 1, { name: 'Foo' })
if (!isRecord(_fieldName))
throw new Error('Expected value to be a record');
args.value = _fieldName;
return this.setImpl(args);
}
args.fieldName = _fieldName;
args.value = _value;
return this.setImpl(args);
}
reset() {
this.store = {};
}
filter(key, predicate) {
const entity = this.store[key];
return entity ? Object.values(entity).filter(predicate) : [];
}
find(key, predicate) {
const entity = this.store[key];
return entity ? Object.values(entity).find(predicate) : undefined;
}
getImpl(args) {
const { typeName, key, fieldName, fieldArgs, defaultValue } = args;
if (!fieldName) {
if (defaultValue !== undefined && !isRecord(defaultValue)) {
throw new Error('`defaultValue` should be an object');
}
let valuesToInsert = defaultValue || {};
if (key) {
valuesToInsert = { ...valuesToInsert, ...makeRef(typeName, key) };
}
return this.insert(typeName, valuesToInsert, true);
}
assertIsDefined(key, 'key argument should be given when fieldName is given');
const fieldNameInStore = getFieldNameInStore(fieldName, fieldArgs);
if (this.store[typeName] === undefined ||
this.store[typeName][key] === undefined ||
this.store[typeName][key][fieldNameInStore] === undefined) {
let value;
if (defaultValue !== undefined) {
value = defaultValue;
}
else if (this.isKeyField(typeName, fieldName)) {
value = key;
}
else {
value = this.generateFieldValue(typeName, fieldName, fieldArgs, (otherFieldName, otherValue) => {
// if we get a key field in the mix we don't care
if (this.isKeyField(typeName, otherFieldName))
return;
this.set({
typeName,
key,
fieldName: otherFieldName,
value: otherValue,
noOverride: true,
});
});
}
this.set({ typeName, key, fieldName, fieldArgs, value, noOverride: true });
}
return this.store[typeName][key][fieldNameInStore];
}
setImpl(args) {
const { typeName, key, fieldName, fieldArgs, noOverride } = args;
let { value } = args;
if (isMockList(value)) {
value = deepResolveMockList(value);
}
if (typeName === '__proto__' || typeName === 'constructor' || typeName === 'prototype') {
throw new Error(`Invalid typeName: ${typeName}`);
}
if (this.store[typeName] === undefined) {
this.store[typeName] = {};
}
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
throw new Error(`Invalid key: ${key}`);
}
if (this.store[typeName][key] === undefined) {
this.store[typeName][key] = {};
}
if (!fieldName) {
if (!isRecord(value)) {
throw new Error('When no `fieldName` is provided, `value` should be a record.');
}
for (const fieldName in value) {
this.setImpl({
typeName,
key,
fieldName,
value: value[fieldName],
noOverride,
});
}
return;
}
const fieldNameInStore = getFieldNameInStore(fieldName, fieldArgs);
if (this.isKeyField(typeName, fieldName) && value !== key) {
throw new Error(`Field ${fieldName} is a key field of ${typeName} and you are trying to set it to ${value} while the key is ${key}`);
}
// if already set and we don't override
if (this.store[typeName][key][fieldNameInStore] !== undefined && noOverride) {
return;
}
const fieldType = this.getFieldType(typeName, fieldName);
const currentValue = this.store[typeName][key][fieldNameInStore];
let valueToStore;
try {
valueToStore = this.normalizeValueToStore(fieldType, value, currentValue, (typeName, values) => this.insert(typeName, values, noOverride));
}
catch (e) {
throw new Error(`Value to set in ${typeName}.${fieldName} in not normalizable: ${e.message}`);
}
this.store[typeName][key] = {
...this.store[typeName][key],
[fieldNameInStore]: valueToStore,
};
}
normalizeValueToStore(fieldType, value, currentValue, onInsertType) {
const fieldTypeName = fieldType.toString();
if (value === null) {
if (!isNullableType(fieldType)) {
throw new Error(`should not be null because ${fieldTypeName} is not nullable. Received null.`);
}
return null;
}
const nullableFieldType = getNullableType(fieldType);
if (value === undefined)
return this.generateValueFromType(nullableFieldType);
// deal with nesting insert
if (isCompositeType(nullableFieldType)) {
if (!isRecord(value))
throw new Error(`should be an object or null or undefined. Received ${value}`);
let joinedTypeName;
if (isAbstractType(nullableFieldType)) {
if (isRef(value)) {
joinedTypeName = value.$ref.typeName;
}
else {
if (typeof value['__typename'] !== 'string') {
throw new Error(`should contain a '__typename' because ${nullableFieldType.name} an abstract type`);
}
joinedTypeName = value['__typename'];
}
}
else {
joinedTypeName = nullableFieldType.name;
}
return onInsertType(joinedTypeName, isRef(currentValue) ? { ...currentValue, ...value } : value);
}
if (isListType(nullableFieldType)) {
if (!Array.isArray(value))
throw new Error(`should be an array or null or undefined. Received ${value}`);
return value.map((v, index) => {
return this.normalizeValueToStore(nullableFieldType.ofType, v, typeof currentValue === 'object' && currentValue != null && currentValue[index]
? currentValue
: undefined, onInsertType);
});
}
return value;
}
insert(typeName, values, noOverride) {
const keyFieldName = this.getKeyFieldName(typeName);
let key;
// when we generate a key for the type, we might produce
// other associated values with it
// We keep track of them and we'll insert them, with propririty
// for the ones that we areasked to insert
const otherValues = {};
if (isRef(values)) {
key = values.$ref.key;
}
else if (keyFieldName && keyFieldName in values) {
key = values[keyFieldName];
}
else {
key = this.generateKeyForType(typeName, (otherFieldName, otherFieldValue) => {
otherValues[otherFieldName] = otherFieldValue;
});
}
const toInsert = { ...otherValues, ...values };
for (const fieldName in toInsert) {
if (fieldName === '$ref')
continue;
if (fieldName === '__typename')
continue;
this.set({
typeName,
key,
fieldName,
value: toInsert[fieldName],
noOverride,
});
}
if (typeName === '__proto__' || typeName === 'constructor' || typeName === 'prototype') {
throw new Error(`Invalid typeName: ${typeName}`);
}
if (this.store[typeName] === undefined) {
this.store[typeName] = {};
}
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
throw new Error(`Invalid key: ${key}`);
}
if (this.store[typeName][key] === undefined) {
this.store[typeName][key] = {};
}
return makeRef(typeName, key);
}
generateFieldValue(typeName, fieldName, fieldArgs, onOtherFieldsGenerated) {
const mockedValue = this.generateFieldValueFromMocks(typeName, fieldName, fieldArgs, onOtherFieldsGenerated);
if (mockedValue !== undefined)
return mockedValue;
const fieldType = this.getFieldType(typeName, fieldName);
return this.generateValueFromType(fieldType);
}
generateFieldValueFromMocks(typeName, fieldName, fieldArgs, onOtherFieldsGenerated) {
let value;
const mock = this.mocks ? this.mocks[typeName] : undefined;
if (mock) {
if (typeof mock === 'function') {
const values = mock();
if (typeof values !== 'object' || values == null) {
throw new Error(`Value returned by the mock for ${typeName} is not an object`);
}
for (const otherFieldName in values) {
if (otherFieldName === fieldName)
continue;
if (typeof values[otherFieldName] === 'function')
continue;
onOtherFieldsGenerated?.(otherFieldName, values[otherFieldName]);
}
value = values[fieldName];
if (typeof value === 'function')
value = value(fieldArgs);
}
else if (typeof mock === 'object' &&
mock != null &&
typeof mock[fieldName] === 'function') {
value = mock[fieldName](fieldArgs);
}
}
if (value !== undefined)
return value;
const type = this.getType(typeName);
// GraphQL 14 Compatibility
const interfaces = 'getInterfaces' in type ? type.getInterfaces() : [];
if (interfaces.length > 0) {
for (const interface_ of interfaces) {
if (value)
break;
value = this.generateFieldValueFromMocks(interface_.name, fieldName, fieldArgs, onOtherFieldsGenerated);
}
}
return value;
}
generateKeyForType(typeName, onOtherFieldsGenerated) {
const keyFieldName = this.getKeyFieldName(typeName);
if (!keyFieldName)
return uuidv4();
return this.generateFieldValue(typeName, keyFieldName, undefined, onOtherFieldsGenerated);
}
generateValueFromType(fieldType) {
const nullableType = getNullableType(fieldType);
if (isScalarType(nullableType)) {
const mockFn = this.mocks[nullableType.name];
if (typeof mockFn !== 'function')
throw new Error(`No mock defined for type "${nullableType.name}"`);
return mockFn();
}
else if (isEnumType(nullableType)) {
const mockFn = this.mocks[nullableType.name];
if (typeof mockFn === 'function')
return mockFn();
const values = nullableType.getValues().map(v => v.value);
return takeRandom(values);
}
else if (isObjectType(nullableType)) {
// this will create a new random ref
return this.insert(nullableType.name, {});
}
else if (isListType(nullableType)) {
return [...new Array(randomListLength())].map(() => this.generateValueFromType(nullableType.ofType));
}
else if (isAbstractType(nullableType)) {
const mock = this.mocks[nullableType.name];
let typeName;
let values = {};
if (!mock) {
typeName = takeRandom(this.schema.getPossibleTypes(nullableType).map(t => t.name));
}
else if (typeof mock === 'function') {
const mockRes = mock();
if (mockRes === null)
return null;
if (!isRecord(mockRes)) {
throw new Error(`Value returned by the mock for ${nullableType.name} is not an object or null`);
}
values = mockRes;
if (typeof values['__typename'] !== 'string') {
throw new Error(`Please return a __typename in "${nullableType.name}"`);
}
typeName = values['__typename'];
}
else if (typeof mock === 'object' &&
mock != null &&
typeof mock['__typename'] === 'function') {
const mockRes = mock['__typename']();
if (typeof mockRes !== 'string')
throw new Error(`'__typename' returned by the mock for abstract type ${nullableType.name} is not a string`);
typeName = mockRes;
}
else {
throw new Error(`Please return a __typename in "${nullableType.name}"`);
}
const toInsert = {};
for (const fieldName in values) {
if (fieldName === '__typename')
continue;
const fieldValue = values[fieldName];
toInsert[fieldName] = typeof fieldValue === 'function' ? fieldValue() : fieldValue;
}
return this.insert(typeName, toInsert);
}
else {
throw new Error(`${nullableType} not implemented`);
}
}
getFieldType(typeName, fieldName) {
if (fieldName === '__typename') {
return GraphQLString;
}
const type = this.getType(typeName);
const field = type.getFields()[fieldName];
if (!field) {
throw new Error(`${fieldName} does not exist on type ${typeName}`);
}
return field.type;
}
getType(typeName) {
const type = this.schema.getType(typeName);
if (!type || !(isObjectType(type) || isInterfaceType(type))) {
throw new Error(`${typeName} does not exist on schema or is not an object or interface`);
}
return type;
}
isKeyField(typeName, fieldName) {
return this.getKeyFieldName(typeName) === fieldName;
}
getKeyFieldName(typeName) {
const typePolicyKeyField = this.typePolicies[typeName]?.keyFieldName;
if (typePolicyKeyField !== undefined) {
if (typePolicyKeyField === false)
return null;
return typePolicyKeyField;
}
// How about common key field names?
const gqlType = this.getType(typeName);
for (const fieldName in gqlType.getFields()) {
if (defaultKeyFieldNames.includes(fieldName)) {
return fieldName;
}
}
return null;
}
}
const getFieldNameInStore = (fieldName, fieldArgs) => {
if (!fieldArgs)
return fieldName;
if (typeof fieldArgs === 'string') {
return `${fieldName}:${fieldArgs}`;
}
// empty args
if (Object.keys(fieldArgs).length === 0) {
return fieldName;
}
return `${fieldName}:${stringify(fieldArgs)}`;
};
function assertIsDefined(value, message) {
if (value !== undefined && value !== null) {
return;
}
throw new Error(process.env['NODE_ENV'] === 'production'
? 'Invariant failed:'
: `Invariant failed: ${message || ''}`);
}
/**
* Will create `MockStore` for the given `schema`.
*
* A `MockStore` will generate mock values for the given schema when queried.
*
* It will store generated mocks, so that, provided with same arguments
* the returned values will be the same.
*
* Its API also allows to modify the stored values.
*
* Basic example:
* ```ts
* store.get('User', 1, 'name');
* // > "Hello World"
* store.set('User', 1, 'name', 'Alexandre');
* store.get('User', 1, 'name');
* // > "Alexandre"
* ```
*
* The storage key will correspond to the "key field"
* of the type. Field with name `id` or `_id` will be
* by default considered as the key field for the type.
* However, use `typePolicies` to precise the field to use
* as key.
*/
export function createMockStore(options) {
return new MockStore(options);
}