UNPKG

node-red-contrib-chatbot

Version:

REDBot a Chat bot for a full featured chat bot for Telegram, Facebook Messenger and Slack. Almost no coding skills required

1,955 lines (1,870 loc) 72.3 kB
const { ApolloServer } = require('apollo-server-express'); const { resolver } = require('graphql-sequelize'); const { Kind } = require('graphql/language'); const { PubSub } = require('graphql-subscriptions'); const _ = require('lodash'); const geolib = require('geolib'); const Sequelize = require('sequelize'); const fetch = require('node-fetch'); const fs = require('fs'); const geohash = require('ngeohash'); const { Directus } = require('@directus/sdk', { auth: { autoRefresh: false } }); const { when, hash } = require('../lib/utils'); const translateWhere = require('../src/helpers/translate-where'); const directus = new Directus('https://dashboard.red-bot.io'); const Op = Sequelize.Op; const pubsub = new PubSub(); const deleteFile = filename => new Promise((resolve, reject) => { fs.unlink(filename, err => { if (err) { reject(err) } else { resolve(); } }); }); const compactObject = obj => { return Object.entries(obj) .reduce((accumulator, current) => { return current[1] != null ? { ...accumulator, [current[0]]: current[1] } : accumulator; }, {}); } const splitOrder = order => { if (!_.isEmpty(order)) { return [[order.replace('reverse:', ''), order.startsWith('reverse:') ? 'DESC' : 'ASC']]; } return null; } const { GraphQLSchema, GraphQLObjectType, GraphQLNonNull, GraphQLFloat, GraphQLInt, GraphQLString, GraphQLList, GraphQLBoolean, GraphQLInputObjectType, GraphQLScalarType } = require('graphql'); const DateType = new GraphQLScalarType({ name: 'Date', description: 'Date type', parseValue(value) { return new Date(value); // value from the client }, serialize(value) { return value; //return value.getTime(); // value sent to the client }, parseLiteral(ast) { if (ast.kind === Kind.INT) { return new Date(ast.value) // ast value is always in string format } return null; }, }); const JSONType = new GraphQLScalarType({ name: 'JSON', description: 'JSON data type', parseValue(value) { return JSON.stringify(value); }, serialize(value) { let result; try { result = JSON.parse(value); } catch(e) { // do nothing } return result; }, parseLiteral() { return null; }, }); const PayloadType = new GraphQLScalarType({ name: 'Payload', description: 'Payload (join custom fields in an hash)', parseValue() { // return JSON.stringify(value); }, serialize(fields) { let result = {}; fields.forEach(field => { try { result[field.name] = JSON.parse(field.value); } catch(e) { // do nothing } }); return result; }, parseLiteral() { return null; }, }); module.exports = ({ Configuration, Message, User, ChatId, Event, Content, Category, Field, Context, Admin, Record, Device, ChatBot, Plugin, sequelize, sequelizeTasks, mcSettings }) => { let _cachedChatbotIds; const createChatbotIdIfNotExist = async (chatbotId, transaction) => { // load cache if (_cachedChatbotIds == null) { _cachedChatbotIds = await ChatBot.findAll({ transaction }).map(({ chatbotId }) => chatbotId); } if (!_.isEmpty(chatbotId) && !_cachedChatbotIds.includes(chatbotId)) { await ChatBot.create({ chatbotId, name: chatbotId }, { transaction }); _cachedChatbotIds = [..._cachedChatbotIds, chatbotId]; } }; const InputUserType = new GraphQLInputObjectType({ name: 'InputUser', description: 'tbd', fields: { userId: { type: GraphQLString, description: '', }, email: { type: GraphQLString, description: '', }, first_name: { type: GraphQLString, description: '', }, last_name: { type: GraphQLString, description: '', }, language: { type: GraphQLString, description: '', }, username: { type: GraphQLString, description: '', }, payload: { type: JSONType, description: '', }, context: { type: JSONType, description: 'The context to update', }, chatbotId: { type: GraphQLString, description: '', } } }); const InputTaskType = new GraphQLInputObjectType({ name: 'InputTask', description: 'Input task for a queue', fields: () => ({ taskId: { type: GraphQLString, description: 'The task id', }, priority: { type: GraphQLInt, description: 'The task priority for the current queue' }, task: { type: JSONType, description: 'The JSON payload of the task' }, createdAt: { type: DateType } }) }); const taskType = new GraphQLObjectType({ name: 'Task', description: 'Task for a queue', fields: () => ({ id: { type: GraphQLInt, description: 'The SQLIte id of the task', }, taskId: { type: GraphQLString, description: 'The task id', }, priority: { type: GraphQLInt, description: 'The task priority for the current queue' }, task: { type: JSONType, description: 'The JSON payload of the task' }, createdAt: { type: DateType } }) }); const queueType = new GraphQLObjectType({ name: 'Queue', description: 'Queue of tasks', fields: () => ({ name: { type: GraphQLString, description: '' }, label: { type: GraphQLString, description: '' }, tasks: { type: new GraphQLList(taskType), args: { offset: { type: GraphQLInt }, limit: { type: GraphQLInt }, }, resolve: async function(root, { offset = 0, limit = 10 }) { try { const [tasks] = await sequelizeTasks.query( `SELECT * FROM :queue ORDER BY priority DESC, id ASC LIMIT :offset, :limit `, { replacements: { queue: root.name, offset, limit } } ); return tasks; } catch(e) { throw queueErrorMessage(root.name); } } } }) }); const chatIdType = new GraphQLObjectType({ name: 'ChatId', description: 'ChatId record, relation between a platform specific chatId and the userId', fields: () => ({ id: { type: new GraphQLNonNull(GraphQLInt), description: 'The internal id of the user', }, userId: { type: GraphQLString, description: '' }, chatId: { type: GraphQLString, description: '' }, transport: { type: GraphQLString, description: '' }, user: { type: userType, description: 'User related to this chatId', resolve: (chatId) => User.findOne({ where: { userId: chatId.userId }}) }, chatbotId: { type: GraphQLString } }) }); const contextType = new GraphQLObjectType({ name: 'Context', description: 'tbd', fields: () => ({ id: { type: new GraphQLNonNull(GraphQLInt), description: 'The internal id of the chat context', }, chatId: { type: GraphQLString, description: '' }, userId: { type: GraphQLString, description: '' }, payload: { type: JSONType, description: '' } }) }); const deviceType = new GraphQLObjectType({ name: 'Device', description: 'tbd', fields: () => ({ id: { type: new GraphQLNonNull(GraphQLInt), description: 'The unique id of the device', }, status: { type: GraphQLString, description: '' }, name: { type: GraphQLString, description: '' }, version: { type: GraphQLString, description: '' }, payload: { type: JSONType, description: '' }, jsonSchema: { type: JSONType, description: '' }, snapshot: { type: JSONType, description: '' }, lat: { type: GraphQLFloat, description: '' }, lon: { type: GraphQLFloat, description: '' }, createdAt: { type: DateType }, updatedAt: { type: DateType }, lastUpdate: { type: DateType } }) }); const recordType = new GraphQLObjectType({ name: 'Record', description: 'tbd', fields: () => ({ id: { type: new GraphQLNonNull(GraphQLInt), description: '', }, type: { type: GraphQLString, description: '' }, title: { type: GraphQLString, description: '' }, status: { type: GraphQLString, description: '' }, transport: { type: GraphQLString, description: '' }, userId: { type: GraphQLString, description: '' }, payload: { type: JSONType, description: '' }, createdAt: { type: DateType }, user: { type: userType, resolve: user => User.findOne({ where: { userId: user.userId }}) }, latitude: { type: GraphQLFloat }, longitude: { type: GraphQLFloat }, geohash: { type: GraphQLString }, chatbotId: { type: GraphQLString, description: '', } }) }); const InputRecordType = new GraphQLInputObjectType({ name: 'InputRecord', description: 'tbd', fields: () => ({ type: { type: GraphQLString, description: '' }, title: { type: GraphQLString, description: '' }, status: { type: GraphQLString, description: '' }, transport: { type: GraphQLString, description: '' }, userId: { type: GraphQLString, description: '' }, payload: { type: JSONType, description: '' }, createdAt: { type: DateType }, latitude: { type: GraphQLFloat }, longitude: { type: GraphQLFloat }, geohash: { type: GraphQLString }, chatbotId: { type: GraphQLString, description: '', } }) }); const userType = new GraphQLObjectType({ name: 'User', description: 'tbd', fields: () => ({ id: { type: new GraphQLNonNull(GraphQLInt), description: 'The internal id of the user', }, userId: { type: GraphQLString, description: '', }, email: { type: GraphQLString, description: '', }, first_name: { type: GraphQLString, description: '', }, last_name: { type: GraphQLString, description: '', }, language: { type: GraphQLString, description: '', }, username: { type: GraphQLString, description: '', }, createdAt: { type: DateType }, payload: { type: JSONType, description: '', }, context: { type: JSONType, description: 'The chat context associated with the user', resolve: async user => { const context = await Context.findOne({ where: { userId: user.userId }}); return context != null ? context.payload : null; } }, chatIds: { type: new GraphQLList(chatIdType), args: { transport: { type: GraphQLString } }, resolve: (user, args) => { const where = { userId: user.userId }; if (args.transport != null) { where.transport = args.transport; } if (user.chatbotId != null) { where.chatbotId = user.chatbotId; } return ChatId.findAll({ where }); } }, messages: { type: GraphQLList(messageType), args: { offset: { type: GraphQLInt }, limit: { type: GraphQLInt }, order: { type: GraphQLString } }, resolve: (user, args = {}) => { let order; if (args.order != null) { order = [ [args.order.replace('reverse:', ''), args.order.startsWith('reverse:') ? 'ASC' : 'DESC'] ]; } return Message.findAll({ where: { userId: user.userId}, limit: args.limit, offset: args.offset, order }); } }, records: { type: new GraphQLList(recordType), args: { order: { type: GraphQLString }, type: { type: GraphQLString }, status: { type: GraphQLString }, offset: { type: GraphQLInt }, limit: { type: GraphQLInt } }, resolve: (user, { order = 'createdAt', offset, limit, type, status }) => { return Record.findAll({ limit, offset, order: splitOrder(order), where: compactObject({ type, status, userId: user.userId }) }); } }, chatbotId: { type: GraphQLString, description: '', } }) }); const adminType = new GraphQLObjectType({ name: 'Admin', description: 'tbd', fields: () => ({ id: { type: new GraphQLNonNull(GraphQLInt), description: 'The internal id of the user', }, email: { type: GraphQLString, description: '', }, first_name: { type: GraphQLString, description: '', }, last_name: { type: GraphQLString, description: '', }, avatar: { type: GraphQLString, description: '', }, username: { type: GraphQLString, description: '', }, password: { type: GraphQLString, description: '', }, permissions: { type: GraphQLString, description: '', }, createdAt: { type: DateType }, payload: { type: JSONType, description: '', }, chatbotIds: { type: GraphQLString, description: '', } }) }); const InputAdminType = new GraphQLInputObjectType({ name: 'InputAdmin', description: 'tbd', fields: () => ({ email: { type: GraphQLString, description: '', }, first_name: { type: GraphQLString, description: '', }, last_name: { type: GraphQLString, description: '', }, avatar: { type: GraphQLString, description: '', }, username: { type: GraphQLString, description: '', }, password: { type: GraphQLString, description: '', }, permissions: { type: GraphQLString, description: '', }, createdAt: { type: DateType }, payload: { type: JSONType, description: '', }, chatbotIds: { type: GraphQLString, description: '', } }) }); const InputMessageType = new GraphQLInputObjectType({ name: 'InputMessage', description: 'tbd', fields: () => ({ user: { type: InputUserType, description: 'User of the chat message' }, chatId: { type: GraphQLString, description: '', }, userId: { type: GraphQLString, description: '', }, messageId: { type: GraphQLString, description: '', }, from: { type: GraphQLString, description: '', }, type: { type: GraphQLString, description: '', }, content: { type: GraphQLString, description: '', }, transport: { type: GraphQLString, description: '', }, flag: { type: GraphQLString, description: '', }, inbound: { type: GraphQLBoolean, description: '' }, ts: { type: GraphQLString, description: '', }, chatbotId: { type: GraphQLString, description: '', } }) }); const categoryType = new GraphQLObjectType({ name: 'Category', description: 'tbd', fields: { id: { type: new GraphQLNonNull(GraphQLInt), description: 'The id of the category', }, name: { type: GraphQLString, description: '', }, language: { type: GraphQLString, description: '', }, createdAt: { type: DateType }, chatbotId: { type: GraphQLString, description: '', } } }); const InputCategoryType = new GraphQLInputObjectType({ name: 'InputCategory', description: 'tbd', fields: { name: { type: GraphQLString, description: '', }, language: { type: GraphQLString, description: '', }, namespace: { type: GraphQLString, description: '', }, chatbotId: { type: GraphQLString, description: '', } } }); const pluginType = new GraphQLObjectType({ name: 'Plugin', description: 'tbd', fields: { id: { type: new GraphQLNonNull(GraphQLInt), description: 'The id of the plugin', }, plugin: { type: GraphQLString, description: '', }, version: { type: GraphQLString, description: '', }, filename: { type: GraphQLString, description: '', } } }); const chatbotType = new GraphQLObjectType({ name: 'Chatbot', description: 'tbd', fields: { id: { type: new GraphQLNonNull(GraphQLInt), description: 'The id of the chatbot', }, name: { type: GraphQLString, description: '', }, description: { type: GraphQLString, description: '', }, guid: { type: GraphQLString, description: '', }, plugins: { type: new GraphQLList(pluginType), description: 'The list of installed plugins', resolve: (root) => Plugin.findAll({ where: { chatbotId: root.chatbotId }, limit: 9999 }) }, chatbotId: { type: GraphQLString, description: '', } } }); const inputChatbotType = new GraphQLInputObjectType({ name: 'InputChatbot', description: 'tbd', fields: { name: { type: GraphQLString, description: '', }, description: { type: GraphQLString, description: '', }, guid: { type: GraphQLString, description: '', }, chatbotId: { type: GraphQLString, description: '', } } }); const messageType = new GraphQLObjectType({ name: 'Message', description: 'tbd', fields: { id: { type: new GraphQLNonNull(GraphQLInt), description: 'The id of the message', }, chatId: { type: GraphQLString, description: '', }, userId: { type: GraphQLString, description: '', }, messageId: { type: GraphQLString, description: '', }, from: { type: GraphQLString, description: '', }, type: { type: GraphQLString, description: '', }, transport: { type: GraphQLString, description: '', }, flag: { type: GraphQLString, description: '', }, content: { type: GraphQLString, description: '', }, inbound: { type: GraphQLBoolean, description: '' }, createdAt: { type: DateType }, ts: { type: GraphQLString, description: '', }, user: { type: userType, resolve: (message) => { return User.findOne({ where: { userId: message.userId }}); } }, chatbotId: { type: GraphQLString, description: '', } } }); const configurationType = new GraphQLObjectType({ name: 'Configuration', description: 'tbd', fields: { id: { type: new GraphQLNonNull(GraphQLInt), description: 'The id of the configuration', }, namespace: { type: GraphQLString, description: '', }, payload: { type: GraphQLString, description: '', }, chatbotId: { type: GraphQLString, description: '', } } }); const InputFieldType = new GraphQLInputObjectType({ name: 'InputField', description: 'tbd', fields: { id: { type: GraphQLInt, description: 'The id of the field', }, name: { type: GraphQLString, description: '', }, type: { type: GraphQLString, description: '', }, value: { type: JSONType, description: '', } } }); const fieldType = new GraphQLObjectType({ name: 'Field', description: 'tbd', fields: { id: { type: new GraphQLNonNull(GraphQLInt), description: 'The id of the field', }, name: { type: GraphQLString, description: '', }, type: { type: GraphQLString, description: '', }, value: { type: JSONType, description: '', } } }); const InputContentType = new GraphQLInputObjectType({ name: 'InputContent', description: 'tbd', fields: { title: { type: GraphQLString, description: '', }, slug: { type: GraphQLString, description: '', }, language: { type: GraphQLString, description: '', }, body: { type: GraphQLString, description: '', }, namespace: { type: GraphQLString, description: '', }, payload: { type: JSONType, description: '', }, fields: { type: new GraphQLList(InputFieldType), description: '' }, categoryId: { type: GraphQLInt }, latitude: { type: GraphQLFloat }, longitude: { type: GraphQLFloat }, geohash: { type: GraphQLString }, chatbotId: { type: GraphQLString, description: '', } } }); const contentType = new GraphQLObjectType({ name: 'Content', description: 'tbd', fields: { id: { type: new GraphQLNonNull(GraphQLInt), description: 'The id of the content', }, title: { type: GraphQLString, description: '', }, slug: { type: GraphQLString, description: '', }, language: { type: GraphQLString, description: '', }, namespace: { type: GraphQLString, description: '', }, body: { type: GraphQLString, description: '', }, fields: { type: new GraphQLList(fieldType), resolve(root) { return root.getFields({ limit: 9999 }); } }, categoryId: { type: GraphQLInt }, category: { type: categoryType, resolve(root) { return root.getCategory(); } }, payload: { type: JSONType, description: '', }, json: { type: PayloadType, resolve(root) { return root.getFields({ limit: 9999 }); } }, createdAt: { type: DateType }, latitude: { type: GraphQLFloat }, longitude: { type: GraphQLFloat }, geohash: { type: GraphQLString }, chatbotId: { type: GraphQLString, description: '', }, order: { type: GraphQLInt, description: 'If specified, the order of the content' } } }); const InputConfigurationType = new GraphQLInputObjectType({ name: 'InputConfiguration', description: 'tbd', fields: () => ({ namespace: { type: GraphQLString, description: '', }, payload: { type: GraphQLString, description: '', }, chatbotId: { type: GraphQLString, description: '', } }) }); const messageCounterType = new GraphQLObjectType({ name: 'MessageCounters', description: 'Message Counters', fields: { count: { type: GraphQLInt, args: { type: { type: GraphQLString }, transport: { type: GraphQLString }, messageId: { type: GraphQLString }, chatId: { type: GraphQLString }, userId: { type: GraphQLString }, flag: { type: GraphQLString }, inbound: { type: GraphQLBoolean }, chatbotId: { type: GraphQLString } }, description: 'Total messages', resolve: (root, { type, transport, messageId, chatId, userId, flag, inbound, chatbotId }) => Message.count({ where: compactObject({ type, transport, messageId, chatId, userId, flag, inbound, chatbotId }) }) } } }); const aggregatedEvent = new GraphQLObjectType({ name: 'aggregatedEvent', description: 'Aggregation of event', fields: { flow: { type: GraphQLString }, count: { type: GraphQLInt } } }); const eventCounterType = new GraphQLObjectType({ name: 'EventCounters', description: 'Event Counters', fields: { count: { type: GraphQLInt, description: 'Total events', resolve: () => Event.count() }, events: { type: new GraphQLList(aggregatedEvent), resolve() { return Event .findAll({ group: ['flow'], attributes: ['flow', [sequelize.fn('COUNT', 'flow'), 'count']], }) .then(res => res.map(item => item.dataValues)); } } } }); const userCounterType = new GraphQLObjectType({ name: 'UserCounters', description: 'User Counters', fields: { count: { type: GraphQLInt, description: 'Total users', args: { userId: { type: GraphQLString }, username: { type: GraphQLString }, chatbotId: { type: GraphQLString } }, resolve: (root, { userId, username, chatbotId }) => User.count({ where: compactObject({ userId, chatbotId, username: username != null ? { [Op.like]: `%${username}%` } : null }) }) } } }); const deviceCounterType = new GraphQLObjectType({ name: 'DeviceCounters', description: 'Device Counters', fields: { count: { type: GraphQLInt, description: 'Total devices', args: { }, resolve: () => Device.count() } } }); const queueErrorMessage = queue => { if (queue === 'tasks') { return 'The default queue still doesn\'t exist. Add a \'MC queue\' node with an empty queue name and add some elements'; } else { return `Queue '${queue}' doesn't exist yet. Add a 'MC queue' node with the queue name '${queue}' and add some elements`; } } const taskCounterType = new GraphQLObjectType({ name: 'TaskCounters', description: 'Task Counters', fields: { count: { type: GraphQLInt, description: 'Total tasks', args: { queue: { type: GraphQLString } }, resolve: async(root, { queue }) => { try { const [count] = await sequelizeTasks.query( 'SELECT count(*) as \'total\' FROM :queue;', { replacements: { queue } } ); return !_.isEmpty(count) ? count[0].total : 0; } catch(e) { throw queueErrorMessage(queue); } } } } }); const adminCounterType = new GraphQLObjectType({ name: 'AdminCounters', description: 'User Counters', fields: { count: { type: GraphQLInt, description: 'Total admins', args: { username: { type: GraphQLString }, chatbotId: { type: GraphQLString } }, resolve: (root, { username, chatbotId }) => Admin.count({ where: compactObject({ username: username != null ? { [Op.like]: `%${username}%` } : null, chatbotId }) }) } } }); const categoryCounterType = new GraphQLObjectType({ name: 'CategoryCounters', description: 'Category Counters', fields: { count: { type: GraphQLInt, args: { namespace: { type: GraphQLString }, chatbotId: { type: GraphQLString } }, description: 'Total categories', resolve: (root, { namespace, chatbotId }) => Category.count({ where: compactObject({ namespace, chatbotId }) }) } } }); const recordCounterType = new GraphQLObjectType({ name: 'RecordCounters', description: 'Record Counters', fields: { count: { type: GraphQLInt, args: { type: { type: GraphQLString }, userId: { type: GraphQLString }, status: { type: GraphQLString }, chatbotId: { type: GraphQLString } }, description: 'Total records', resolve: (root, { type, userId, status, chatbotId }) => Record.count({ where: compactObject({ type, userId, status, chatbotId }) }) } } }); const buildContentQuery = ({ slug, categoryId, language, title, id, ids, namespace, search, slugs, chatbotId, latitude, longitude, precision = 100 }) => { const whereParams = compactObject({ id: _.isArray(ids) && !_.isEmpty(ids) ? { [Op.in]: ids } : id, categoryId, slug: _.isArray(slugs) && !_.isEmpty(slugs) ? { [Op.in]: slugs } : slug, language, namespace }); if (title != null) { whereParams.title = { [Op.like]: `%${title}%` }; } if (search != null) { whereParams[Op.or] = [ { title: { [Op.like]: `%${search}%` } }, { slug: { [Op.like]: `%${search}%` } }, ] } if (!_.isEmpty(chatbotId)) { whereParams.chatbotId = chatbotId; } if (longitude != null && latitude != null && precision > 9) { const [sw, ne] = geolib.getBoundsOfDistance({ latitude, longitude }, precision); whereParams[Op.and] = [ { 'latitude': { [Op.gte]: sw.latitude }}, { 'latitude': { [Op.lte]: ne.latitude }}, { 'longitude': { [Op.gte]: sw.longitude }}, { 'longitude': { [Op.lte]: ne.longitude }}, ] } /*if (longitude != null && latitude != null && precision > 9) { const [sw, ne] = geolib.getBoundsOfDistance({ latitude, longitude }, precision); where[Op.and] = [ { 'latitude': { [Op.gte]: sw.latitude }}, { 'latitude': { [Op.lte]: ne.latitude }}, { 'longitude': { [Op.gte]: sw.longitude }}, { 'longitude': { [Op.lte]: ne.longitude }}, ] }*/ return whereParams; } const contentCounterType = new GraphQLObjectType({ name: 'ContentCounters', description: 'Content Counters', fields: { count: { type: GraphQLInt, description: 'Total contents', args: { slug: { type: GraphQLString }, order: { type: GraphQLString }, offset: { type: GraphQLInt }, limit: { type: GraphQLInt }, categoryId: { type: GraphQLInt }, id: { type: GraphQLInt }, ids: { type: new GraphQLList(GraphQLInt)}, slugs: { type: new GraphQLList(GraphQLString)}, language: { type: GraphQLString }, namespace: { type: GraphQLString }, title: { type: GraphQLString }, search: { type: GraphQLString }, chatbotId: { type: GraphQLString } }, resolve(root, { slug, categoryId, language, title, id, ids, namespace, search, slugs, chatbotId }) { return Content.count({ where: buildContentQuery({ slug, categoryId, language, title, id, ids, namespace, search, slugs, chatbotId }) }); } } } }); const countersType = new GraphQLObjectType({ name: 'Counters', description: 'Counters', fields: { messages: { type: messageCounterType, description: 'Counters for messages', resolve: () => { return {}; } }, users: { type: userCounterType, description: 'Counters for users', resolve: () => { return {}; } }, admins: { type: adminCounterType, description: 'Counters for users', resolve: () => { return {}; } }, events: { type: eventCounterType, description: 'Counters for events', resolve: () => { return {}; } }, contents: { type: contentCounterType, description: 'Counters for contents', resolve: () => { return {}; } }, categories: { type: categoryCounterType, description: 'Counters for categories', resolve: () => { return {}; } }, records: { type: recordCounterType, description: 'Counters for user records', resolve: () => { return {}; } }, devices: { type: deviceCounterType, description: 'Counters for devices', resolve: () => { return {}; } }, tasks: { type: taskCounterType, description: 'Counters for devices', resolve: () => { return {}; } } } }); const schema = new GraphQLSchema({ mutation: new GraphQLObjectType({ name: 'Mutations', description: 'These are the things we can change', fields: { deleteEvent: { type: GraphQLString, args: { flow: { type: new GraphQLNonNull(GraphQLString) } }, resolve: async function(root, { flow }) { await Event.destroy({ where: { flow }}); return true; } }, createConfiguration: { type: configurationType, args: { configuration: { type: new GraphQLNonNull(InputConfigurationType) } }, resolve: async (root, { configuration }) => { const found = await Configuration.findOne({ where: compactObject({ namespace: configuration.namespace, chatbotId: configuration.chatbotId }) }); if (found != null) { await Configuration.update(configuration, { where: { id: found.id }}) return await Configuration.findByPk(found.id); } else { return await Configuration.create(configuration); } } }, createContent: { type: contentType, args: { content: { type: new GraphQLNonNull(InputContentType) } }, resolve: function(root, { content }) { // calculate geohash if not provided if (content.longitude != null && content.latitude != null) { content.geohash = geohash.encode(content.latitude, content.longitude); } return Content.create(content, { include: [Content.Fields] }); } }, createRecord: { type: recordType, args: { record: { type: new GraphQLNonNull(InputRecordType) } }, resolve: function(root, { record }) { // calculate geohash if not provided if (record.longitude != null && record.latitude != null) { record.geohash = geohash.encode(record.latitude, record.longitude); record.geohash_int = geohash.encode_int(record.latitude, record.longitude); } return Record.create(record); } }, createCategory: { type: categoryType, args: { category: { type: new GraphQLNonNull(InputCategoryType)} }, resolve: function(root, { category }) { return Category.create(category); } }, editChatbot: { type: chatbotType, args: { id: { type: GraphQLNonNull(GraphQLInt) }, chatbot: { type: new GraphQLNonNull(inputChatbotType)} }, async resolve(root, { chatbot, id }) { await ChatBot.update(chatbot, { where: { id } }); return await ChatBot.findOne({ where: { id }}); } }, editCategory: { type: categoryType, args: { id: { type: new GraphQLNonNull(GraphQLInt)}, category: { type: new GraphQLNonNull(InputCategoryType)} }, resolve(root, { id, category }) { return Category.update(category, { where: { id } }) .then(() => Category.findByPk(id)); } }, editRecord: { type: recordType, args: { id: { type: new GraphQLNonNull(GraphQLInt)}, record: { type: new GraphQLNonNull(InputRecordType)} }, resolve(root, { id, record }) { // calculate geohash if not provided if (record.longitude != null && record.latitude != null) { record.geohash = geohash.encode(record.latitude, record.longitude); record.geohash_int = geohash.encode_int(record.latitude, record.longitude); } return Record.update(record, { where: { id } }) .then(() => Record.findByPk(id)); } }, deleteCategory: { type: contentType, args: { id: { type: new GraphQLNonNull(GraphQLInt)} }, resolve: async function(root, { id }) { const category = await Category.findByPk(id); // destroy user and related chatIds if (category != null) { await category.destroy(); } return category; } }, swapContent: { type: contentType, args: { id: { type: new GraphQLNonNull(GraphQLInt) }, withId: { type: new GraphQLNonNull(GraphQLInt) } }, resolve: async (root, { id, withId }) => { const content = await Content.findByPk(id); const withContent = await Content.findByPk(withId); if (content == null || withContent == null) { throw `Unable to swap order of contents: ${id} and ${withId}`; } const contentOrder = content.order || content.id; const withContentOrder = withContent.order || withContent.id; // now swap content.order = withContentOrder; withContent.order = contentOrder; await content.save(); await withContent.save(); return content; } }, editContent: { type: contentType, args: { id: { type: new GraphQLNonNull(GraphQLInt)}, content: { type: new GraphQLNonNull(InputContentType) } }, resolve: async (root, { id, content }) => { // calculate geohash if not provided if (content.longitude != null && content.latitude != null) { content.geohash = geohash.encode(content.latitude, content.longitude); } await Content.update(content, { where: { id } }) const updatedContent = await Content.findByPk(id, { include: [Content.Fields]} ); const currentFieldIds = updatedContent.fields.map(field => field.id); if (_.isArray(content.fields) && content.fields.length !== 0) { let task = when(true); const newFieldIds = _.compact(content.fields.map(field => field.id)); // now add or update each field present in the payload content.fields.forEach(field => { if (field.id != null) { task = task.then(() => Field.update(field, { where: { id: field.id } })); } else { task = task.then(() => updatedContent.createField(field)); } }); // remove all current id field that are not included in the list of new ids currentFieldIds .filter(id => !newFieldIds.includes(id)) .forEach(id => { task = task.then(() => Field.destroy({ where: { id }})); }); await task; return Content.findByPk(id, { include: [Content.Fields]} ); } else { return updatedContent; } } }, deleteContent: { type: contentType, args: { id: { type: new GraphQLNonNull(GraphQLInt)} }, resolve: async function(root, { id }) { const content = await Content.findByPk(id); // destroy user and related chatIds if (content != null) { await content.destroy(); } return content; } }, deleteRecord: { type: recordType, args: { id: { type: new GraphQLNonNull(GraphQLInt)} }, resolve: async function(root, { id }) { const record = await Record.findByPk(id); // destroy user and related chatIds if (record != null) { await record.destroy(); } return record; } }, deleteAdmin: { type: adminType, args: { id: { type: new GraphQLNonNull(GraphQLInt)} }, resolve: async function(root, { id }) { const admin = await Admin.findByPk(id); await Admin.destroy({ where: { id }}); return admin; } }, updateTask: { type: taskType, args: { id: { type: new GraphQLNonNull(GraphQLInt)}, queue: { type: GraphQLString }, task: { type: InputTaskType} }, resolve: async function(root, { id, queue, task }) { // update the json await sequelizeTasks.query( 'UPDATE :queue SET task = :json, priority = :priority WHERE id = :id;', { replacements: { id, queue, json: task.task, priority: task.priority } } ); // get again const [updatedTask] = await sequelizeTasks.query( 'SELECT * FROM :queue WHERE id = :id;', { replacements: { id, queue } } ); return updatedTask[0]; } }, deleteTask: { type: taskType, args: { id: { type: new GraphQLNonNull(GraphQLInt)}, queue: { type: GraphQLString } }, resolve: async function(root, { id, queue }) { const task = await sequelizeTasks.query( 'SELECT * FROM :queue WHERE id = :id;', { replacements: { id, queue } } ); if (task.length !== 0) { await sequelizeTasks.query( 'DELETE FROM :queue WHERE id = :id;', { replacements: { id, queue } } ); return task[0]; } return null; } }, deleteTasks: { type: new GraphQLList(GraphQLInt), args: { ids: { type: new GraphQLList(GraphQLInt)}, queue: { type: GraphQLString }, all: { type: GraphQLBoolean } }, resolve: async function(root, { all, ids, queue }) { if (all) { await sequelizeTasks.query( 'DELETE FROM :queue;', { replacements: { queue } } ); return []; } else { await sequelizeTasks.query( 'DELETE FROM :queue WHERE id IN (:ids);', { replacements: { ids, queue } } ); return ids; } } }, editAdmin: { type: adminType, args: { id: { type: GraphQLInt}, admin: { type: new GraphQLNonNull(InputAdminType) } }, async resolve(root, { id, admin }) { if (!_.isEmpty(admin.password)) { admin.password = hash(admin.password, { salt: mcSettings.salt }); } await Admin.update(admin, { where: { id } }) return await Admin.findOne({ where: { id } }); } }, createAdmin: { type: adminType, args: { admin: { type: new GraphQLNonNull(InputAdminType)} }, resolve: function(root, { admin }) { if (!_.isEmpty(admin.password)) { admin.password = hash(admin.password, { salt: mcSettings.salt }); } return Admin.create(admin); } }, editUser: { type: userType, args: { id: { type: GraphQLInt}, userId: { type: GraphQLString }, user: { type: new GraphQLNonNull(InputUserType) } }, async resolve(root, { id, userId, user: value }) { let where; if (id != null) { where = { id }; } else if (userId != null) { where = { userId }; } else { throw 'Missing both id and userId'; } // if context is present, update using userId if (value.context) { const user = await User.findOne({ where }); await Context.update({ payload: value.context }, { where: { userId: user.userId }}); delete value.context; } await User.update(value, { where }) return await User.findOne({ where }); } }, mergeUser: { type: userType, args: { fromId: { type: new GraphQLNonNull(GraphQLInt)}, toId: { type: new GraphQLNonNull(GraphQLInt)}, chatbotId: { type: GraphQLString } }, resolve: async function(root, { fromId, toId, chatbotId }) { const fromUser = await User.findByPk(fromId); const toUser = await User.findByPk(toId); const fromChatIds = await ChatId.findAll({ where: { userId: fromUser.userId, chatbotId }}); const toChatIds = await ChatId.findAll({ where: { userId: toUser.userId, chatbotId }}); // find all fields from the source user that are empty in the destination user and can be used const fieldsToUpdate = ['email', 'first_name', 'last_name', 'username', 'language']; const updateToUser = {}; for (const field of fieldsToUpdate) { if (!_.isEmpty(fromUser[field]) && _.isEmpty(toUser[field])) { updateToUser[field] = fromUser[field]; toUser[field] = fromUser[field]; } } // update user if not empty if (_.isEmpty(updateToUser)) { await User.update(updateToUser, { where: { id: toUser.id }}); } // turn only chatIds that don't already exists for (const item of fromChatIds) { const hasTransport = toChatIds.filter(({ transport }) => transport === item.transport).length !== 0; if (!hasTransport) { await ChatId.update({ userId: toUser.userId }, { where: { id: item.id }}); } } // turn all old messages into new userId await Message.update({ userId: toUser.userId }, { where: { userId: fromUser.userId, chatbotId }}); // finally destroy source user await User.destroy({ where: { id: fromUser.id }}); return toUser; } }, deleteUser: { type: userType, args: { id: { type: new GraphQLNonNull(GraphQLInt)} }, resolve: async function(root, { id }) { const user = await User.findByPk(id); const userId = user.userId; // destroy user and related chatIds if (user != null) { await user.destroy(); } await ChatId.destroy({ where: { userId }}); await Context.destroy({ where: { userId }}); return user; } }, deleteChat