src/OperationFactory.ts
// tslint:disable-next-line:no-reference
/// <reference path="./@types/graphql-sequelize/index.d.ts" />
import {
GraphQLObjectType,
GraphQLInt,
GraphQLInputObjectType,
GraphQLID,
GraphQLFieldConfigMap,
GraphQLFieldConfig,
GraphQLInputFieldConfigMap,
GraphQLFieldResolver,
} from 'graphql';
import * as _ from 'lodash';
import * as camelcase from 'camelcase';
import {
mutationWithClientMutationId
} from "graphql-relay";
import {
defaultArgs,
defaultListArgs,
attributeFields,
resolver,
SequelizeConnection,
Cache,
} from "graphql-sequelize-teselagen";
import {
convertFieldsFromGlobalId,
mutationName,
getTableName,
convertFieldsToGlobalId,
queryName,
globalIdInputField,
createNonNullList,
createNonNullListResolver,
} from "./utils";
import {
Model,
ModelsHashInterface as Models,
ModelTypes,
} from "./types";
export class OperationFactory {
private models: Models;
private modelTypes: ModelTypes;
private associationsToModel: AssociationToModels;
private associationsFromModel: AssociationFromModels;
private cache: Cache;
constructor(config: OperationFactoryConfig) {
this.models = config.models;
this.modelTypes = config.modelTypes;
this.associationsToModel = config.associationsToModel;
this.associationsFromModel = config.associationsFromModel;
this.cache = config.cache;
}
public createRecord({
mutations,
model,
modelType,
}: {
mutations: Mutations,
model: Model,
modelType: GraphQLObjectType,
}) {
const {
models,
modelTypes,
associationsToModel,
associationsFromModel,
cache,
} = this;
const createMutationName = mutationName(model, 'create');
mutations[createMutationName] = mutationWithClientMutationId({
name: createMutationName,
description: `Create ${getTableName(model)} record.`,
inputFields: () => {
const exclude = model.excludeFields ? model.excludeFields : [];
const fields = attributeFields(model, {
exclude,
commentToDescription: true,
cache
}) as GraphQLInputFieldConfigMap;
convertFieldsToGlobalId(model, fields);
// FIXME: Handle timestamps
// console.log('_timestampAttributes', Model._timestampAttributes);
delete fields.createdAt;
delete fields.updatedAt;
return fields;
},
outputFields: () => {
const output: GraphQLFieldConfigMap<any, any> = {};
// New Record
output[camelcase(`new_${getTableName(model)}`)] = {
type: modelType,
description: `The new ${getTableName(model)}, if successfully created.`,
// tslint:disable-next-line:max-func-args
resolve: (args: any, arg2: any, context: any, info: any) => {
return resolver(model, {
})({}, {
[model.primaryKeyAttribute]: args[model.primaryKeyAttribute]
}, context, info);
}
};
// New Edges
_.each(associationsToModel[getTableName(model)], (association) => {
const {
from,
type: atype,
key: field
} = association;
// console.log("Edge To", getTableName(Model), "From", from, field, atype);
if (atype !== "BelongsTo") {
// HasMany Association
const { connection } = associationsFromModel[from][`${getTableName(model)}_${field}`];
const fromType = modelTypes[from] as GraphQLObjectType;
// let nodeType = conn.nodeType;
// let association = Model.associations[field];
// let targetType = association
// console.log("Connection", getTableName(Model), field, nodeType, conn, association);
output[camelcase(`new_${fromType.name}_${field}_Edge`)] = {
type: connection.edgeType,
resolve: (payload: any) => connection.resolveEdge(payload)
};
}
});
_.each(associationsFromModel[getTableName(model)], (association) => {
const {
to,
type: atype,
foreignKey,
key: field
} = association;
// console.log("Edge From", getTableName(Model), "To", to, field, as, atype, foreignKey);
if (atype === "BelongsTo") {
// BelongsTo association
const toType = modelTypes[to] as GraphQLObjectType;
output[field] = {
type: toType,
// tslint:disable-next-line:max-func-args
resolve: (args: any, arg2: any, context: any, info: any) => {
// console.log('Models', Models, Models[toType.name]);
return resolver(models[toType.name], {})({}, { id: args[foreignKey] }, context, info);
}
};
}
});
// console.log(`${getTableName(Model)} mutation output`, output);
return output;
},
mutateAndGetPayload: (data) => {
convertFieldsFromGlobalId(model, data);
return model.create(data);
}
});
}
public findRecord({
queries,
model,
modelType
}: {
queries: Queries;
model: Model;
modelType: GraphQLObjectType;
}) {
const findByIdQueryName = queryName(model, 'findById');
const queryArgs = defaultArgs(model);
convertFieldsToGlobalId(model, queryArgs);
const baseResolve = resolver(model, {});
// tslint:disable-next-line:max-func-args
const resolve: GraphQLFieldResolver<any, any> = (source, args, context, info) => {
convertFieldsFromGlobalId(model, args);
if (args.where) {
convertFieldsFromGlobalId(model, args.where);
}
return baseResolve(source, args, context, info);
};
queries[findByIdQueryName] = {
type: modelType,
args: queryArgs,
resolve
};
}
public findAll({
queries,
model,
modelType
}: {
model: Model;
modelType: GraphQLObjectType;
queries: Queries;
}) {
const findAllQueryName = queryName(model, 'findAll');
const queryArgs = defaultListArgs(model);
const baseResolve = createNonNullListResolver(resolver(model, { list: true }));
// tslint:disable-next-line:max-func-args
const resolve: GraphQLFieldResolver<any, any> = (source, args, context, info) => {
if (args.where) {
convertFieldsFromGlobalId(model, args.where);
}
if (args.include) {
convertFieldsFromGlobalId(model, args.include);
}
return baseResolve(source, args, context, info);
};
queries[findAllQueryName] = {
type: createNonNullList(modelType),
args: queryArgs,
resolve,
};
}
public updateRecords({
mutations,
model,
modelType,
}: {
mutations: Mutations,
model: Model,
modelType: GraphQLObjectType,
}) {
const {
models,
modelTypes,
associationsToModel,
associationsFromModel,
cache,
} = this;
const updateMutationName = mutationName(model, 'update');
mutations[updateMutationName] = mutationWithClientMutationId({
name: updateMutationName,
description: `Update multiple ${getTableName(model)} records.`,
inputFields: () => {
const fields = attributeFields(model, {
exclude: model.excludeFields ? model.excludeFields : [],
commentToDescription: true,
allowNull: true,
cache
}) as GraphQLInputFieldConfigMap;
convertFieldsToGlobalId(model, fields);
const updateModelTypeName = `Update${getTableName(model)}ValuesInput`;
const updateModelValuesType: GraphQLInputObjectType = (
(cache[updateModelTypeName] as GraphQLInputObjectType)
|| new GraphQLInputObjectType({
name: updateModelTypeName,
description: "Values to update",
fields
}));
cache[updateModelTypeName] = updateModelValuesType;
const updateModelWhereType: GraphQLInputObjectType = new GraphQLInputObjectType({
name: `Update${getTableName(model)}WhereInput`,
description: "Options to describe the scope of the search.",
fields
});
return {
values: {
type: updateModelValuesType
},
where: {
type: updateModelWhereType,
}
};
},
outputFields: () => {
const output: GraphQLFieldConfigMap<any, any> = {};
// New Record
output[camelcase(`new_${getTableName(model)}`)] = {
type: modelType,
description: `The new ${getTableName(model)}, if successfully created.`,
// tslint:disable-next-line max-func-args
resolve: (args: any, arg2: any, context: any, info: any) => {
return resolver(model, {
})({}, {
[model.primaryKeyAttribute]: args[model.primaryKeyAttribute]
}, context, info);
}
};
// New Edges
_.each(associationsToModel[getTableName(model)], (association) => {
const {
from,
type: atype,
key: field
} = association;
// console.log("Edge To", getTableName(Model), "From", from, field, atype);
if (atype !== "BelongsTo") {
// HasMany Association
const { connection } = associationsFromModel[from][`${getTableName(model)}_${field}`];
const fromType = modelTypes[from] as GraphQLObjectType;
// console.log("Connection", getTableName(Model), field, nodeType, conn, association);
output[camelcase(`new_${fromType.name}_${field}_Edge`)] = {
type: connection.edgeType,
resolve: (payload) => connection.resolveEdge(payload)
};
}
});
_.each(associationsFromModel[getTableName(model)], (association) => {
const {
to,
type: atype,
foreignKey,
key: field
} = association;
// console.log("Edge From", getTableName(Model), "To", to, field, as, atype, foreignKey);
if (atype === "BelongsTo") {
// BelongsTo association
const toType = modelTypes[to] as GraphQLObjectType;
output[field] = {
type: toType,
// tslint:disable-next-line max-func-args
resolve: (args: any, arg2: any, context: any, info: any) => {
// console.log('Models', models, models[toType.name]);
return resolver(models[toType.name], {})({}, { id: args[foreignKey] }, context, info);
}
};
}
});
// console.log(`${getTableName(Model)} mutation output`, output);
const updateModelOutputTypeName = `Update${getTableName(model)}Output`;
const outputType: GraphQLObjectType = (
cache[updateModelOutputTypeName] as GraphQLObjectType
|| new GraphQLObjectType({
name: updateModelOutputTypeName,
fields: output
}));
cache[updateModelOutputTypeName] = outputType;
return {
nodes: {
type: createNonNullList(outputType),
// tslint:disable-next-line max-func-args
resolve: createNonNullListResolver((source: any, args: any, context: any, info: any) => {
// console.log('update', source, args);
return model.findAll({
where: source.where
});
})
},
affectedCount: {
type: GraphQLInt
}
};
},
mutateAndGetPayload: (data) => {
// console.log('mutate', data);
const { values, where } = data;
convertFieldsFromGlobalId(model, values);
convertFieldsFromGlobalId(model, where);
return model.update(values, {
where
})
.then((result) => {
return {
where,
affectedCount: result[0]
};
});
}
});
}
public updateRecord({
mutations,
model,
modelType,
}: {
mutations: Mutations,
model: Model,
modelType: GraphQLObjectType,
}) {
const {
models,
modelTypes,
associationsToModel,
associationsFromModel,
cache,
} = this;
const updateMutationName = mutationName(model, 'updateOne');
mutations[updateMutationName] = mutationWithClientMutationId({
name: updateMutationName,
description: `Update a single ${getTableName(model)} record.`,
inputFields: () => {
const fields = attributeFields(model, {
exclude: model.excludeFields ? model.excludeFields : [],
commentToDescription: true,
allowNull: true,
cache
}) as GraphQLInputFieldConfigMap;
convertFieldsToGlobalId(model, fields);
const updateModelInputTypeName = `Update${getTableName(model)}ValuesInput`;
const updateModelValuesType = cache[updateModelInputTypeName] || new GraphQLInputObjectType({
name: updateModelInputTypeName,
description: "Values to update",
fields
});
cache[updateModelInputTypeName] = updateModelValuesType;
return {
[model.primaryKeyAttribute]: globalIdInputField(getTableName(model)),
values: {
type: updateModelValuesType
}
} as any;
},
outputFields: () => {
const output: GraphQLFieldConfigMap<any, any> = {};
// New Record
output[camelcase(`new_${getTableName(model)}`)] = {
type: modelType,
description: `The new ${getTableName(model)}, if successfully created.`,
// tslint:disable-next-line max-func-args
resolve: (args: any, arg2: any, context: any, info: any) => {
return resolver(model, {
})({}, {
[model.primaryKeyAttribute]: args[model.primaryKeyAttribute]
}, context, info);
}
};
// New Edges
_.each(associationsToModel[getTableName(model)], (association) => {
const {
from,
type: atype,
key: field
} = association;
// console.log("Edge To", getTableName(Model), "From", from, field, atype);
if (atype !== "BelongsTo") {
// HasMany Association
const { connection } = associationsFromModel[from][`${getTableName(model)}_${field}`];
const fromType = modelTypes[from] as GraphQLObjectType;
// console.log("Connection", getTableName(Model), field, nodeType, conn, association);
output[camelcase(`new_${fromType.name}_${field}_Edge`)] = {
type: connection.edgeType,
resolve: (payload) => connection.resolveEdge(payload)
};
}
});
_.each(associationsFromModel[getTableName(model)], (association) => {
const {
to,
type: atype,
foreignKey,
key: field
} = association;
// console.log("Edge From", getTableName(Model), "To", to, field, as, atype, foreignKey);
if (atype === "BelongsTo") {
// BelongsTo association
const toType = modelTypes[to] as GraphQLObjectType;
output[field] = {
type: toType,
// tslint:disable-next-line:max-func-args
resolve: (args: any, arg2: any, context: any, info: any) => {
// console.log('Models', Models, Models[toType.name]);
return resolver(models[toType.name], {})({}, { id: args[foreignKey] }, context, info);
}
};
}
});
// console.log(`${getTableName(Model)} mutation output`, output);
const updateModelOutputTypeName = `Update${getTableName(model)}Output`;
const outputType = cache[updateModelOutputTypeName] || new GraphQLObjectType({
name: updateModelOutputTypeName,
fields: output
});
cache[updateModelOutputTypeName] = outputType;
return output;
},
mutateAndGetPayload: (data) => {
// console.log('mutate', data);
const { values } = data;
const where = {
[model.primaryKeyAttribute]: data[model.primaryKeyAttribute]
};
convertFieldsFromGlobalId(model, values);
convertFieldsFromGlobalId(model, where);
return model.update(values, {
where
})
.then((result) => {
return where;
});
}
});
}
public deleteRecords({
mutations,
model,
modelType,
}: {
mutations: Mutations,
model: Model,
modelType: GraphQLObjectType,
}) {
const {
cache,
} = this;
const deleteMutationName = mutationName(model, 'delete');
mutations[deleteMutationName] = mutationWithClientMutationId({
name: deleteMutationName,
description: `Delete ${getTableName(model)} records.`,
inputFields: () => {
const fields = attributeFields(model, {
exclude: model.excludeFields ? model.excludeFields : [],
commentToDescription: true,
allowNull: true,
cache
}) as GraphQLInputFieldConfigMap;
convertFieldsToGlobalId(model, fields);
const deleteModelWhereType = new GraphQLInputObjectType({
name: `Delete${getTableName(model)}WhereInput`,
description: "Options to describe the scope of the search.",
fields
});
return {
where: {
type: deleteModelWhereType,
}
};
},
outputFields: () => {
return {
affectedCount: {
type: GraphQLInt
}
};
},
mutateAndGetPayload: (data) => {
const { where } = data;
convertFieldsFromGlobalId(model, where);
return model.destroy({
where
})
.then((affectedCount) => {
return {
where,
affectedCount
};
});
}
});
}
public deleteRecord({
mutations,
model,
modelType,
}: {
mutations: Mutations,
model: Model,
modelType: GraphQLObjectType,
}) {
const deleteMutationName = mutationName(model, 'deleteOne');
mutations[deleteMutationName] = mutationWithClientMutationId({
name: deleteMutationName,
description: `Delete single ${getTableName(model)} record.`,
inputFields: () => {
return {
[model.primaryKeyAttribute]: globalIdInputField(getTableName(model)),
} as any;
},
outputFields: () => {
const idField = camelcase(`deleted_${getTableName(model)}_id`);
return {
[idField]: {
type: GraphQLID,
resolve: (source) => {
return source[model.primaryKeyAttribute];
}
}
};
},
mutateAndGetPayload: (data) => {
const where = {
[model.primaryKeyAttribute]: data[model.primaryKeyAttribute]
};
convertFieldsFromGlobalId(model, where);
return model.destroy({
where
})
.then((affectedCount) => {
return data;
});
}
});
}
}
export interface AssociationToModel {
from: string;
type: string;
key: string;
connection: SequelizeConnection;
as: any;
}
export interface AssociationToModels {
[tableName: string]: {
[fieldName: string]: AssociationToModel;
};
}
export interface AssociationFromModel {
to: string;
type: string;
foreignKey: string;
key: string;
connection: SequelizeConnection;
as: any;
}
export interface AssociationFromModels {
[tableName: string]: {
[fieldName: string]: AssociationFromModel;
};
}
export interface Queries extends GraphQLFieldConfigMap<any, any> {
[queryName: string]: GraphQLFieldConfig<any, any>;
}
export interface Mutations extends GraphQLFieldConfigMap<any, any> {
[mutationName: string]: GraphQLFieldConfig<any, any>;
}
export interface OperationFactoryConfig {
models: Models;
modelTypes: ModelTypes;
associationsToModel: AssociationToModels;
associationsFromModel: AssociationFromModels;
cache: Cache;
}