From ca84bde5f29644ea24ed23350195dc69ca6b962e Mon Sep 17 00:00:00 2001 From: Oleksandr Samofan Date: Mon, 23 Jul 2018 16:16:15 +0300 Subject: [PATCH 01/60] Add cli-crud core functionality --- .prettierignore | 2 + .../src/modules/common/generatedSchemas.js | 0 tools/cli.js | 18 +- tools/cli/commands/addModule.js | 94 ++++++ tools/cli/commands/deleteModule.js | 134 ++++++++ tools/cli/commands/updateSchema.js | 291 ++++++++++++++++++ tools/cli/helpers/util.js | 124 ++++++++ tools/cli/module.js | 145 ++------- .../crud/client/components/Module.jsx | 37 +++ .../crud/client/components/Module.web.jsx | 38 +++ .../crud/client/containers/Module.spec.js | 17 + .../crud/client/containers/ModuleEdit.jsx | 87 ++++++ .../crud/client/containers/ModuleFilter.jsx | 34 ++ .../crud/client/containers/ModuleList.jsx | 215 +++++++++++++ .../crud/client/containers/ModuleQuery.jsx | 27 ++ .../crud/client/graphql/CreateModule.graphql | 13 + .../client/graphql/DeleteManyModules.graphql | 9 + .../crud/client/graphql/DeleteModule.graphql | 11 + .../crud/client/graphql/Module.graphql | 4 + .../crud/client/graphql/ModuleQuery.graphql | 9 + .../client/graphql/ModuleState.client.graphql | 12 + .../graphql/ModuleStateQuery.client.graphql | 7 + .../crud/client/graphql/ModulesQuery.graphql | 13 + .../graphql/ModulesSubscription.graphql | 10 + .../crud/client/graphql/SortModules.graphql | 9 + .../graphql/UpdateFilter.client.graphql | 5 + .../client/graphql/UpdateManyModules.graphql | 9 + .../crud/client/graphql/UpdateModule.graphql | 13 + .../graphql/UpdateOrderBy.client.graphql | 5 + tools/templates/crud/client/index.native.jsx | 61 ++++ tools/templates/crud/client/index.web.jsx | 40 +++ .../templates/crud/client/resolvers/index.js | 79 +++++ .../crud/database/migrations/_Module.js | 7 + .../templates/crud/database/seeds/_Module.js | 7 + .../{module => crud}/server/Module.spec.js | 0 tools/templates/crud/server/index.js | 10 + tools/templates/crud/server/resolvers.js | 56 ++++ tools/templates/crud/server/schema.graphql | 64 ++++ tools/templates/crud/server/schema.js | 12 + tools/templates/crud/server/sql.js | 9 + .../client/components/ModuleView.web.jsx | 2 +- .../module/client/containers/Module.jsx | 6 +- .../module/client/graphql/ModuleQuery.graphql | 1 - .../client/{index.jsx => index.native.jsx} | 0 tools/templates/module/client/index.web.jsx | 2 +- tools/templates/module/server/index.js | 9 +- tools/templates/module/server/resolvers.js | 6 - tools/templates/module/server/schema.graphql | 5 - 48 files changed, 1626 insertions(+), 142 deletions(-) create mode 100644 .prettierignore create mode 100644 packages/server/src/modules/common/generatedSchemas.js create mode 100644 tools/cli/commands/addModule.js create mode 100644 tools/cli/commands/deleteModule.js create mode 100644 tools/cli/commands/updateSchema.js create mode 100644 tools/cli/helpers/util.js create mode 100644 tools/templates/crud/client/components/Module.jsx create mode 100644 tools/templates/crud/client/components/Module.web.jsx create mode 100644 tools/templates/crud/client/containers/Module.spec.js create mode 100644 tools/templates/crud/client/containers/ModuleEdit.jsx create mode 100644 tools/templates/crud/client/containers/ModuleFilter.jsx create mode 100644 tools/templates/crud/client/containers/ModuleList.jsx create mode 100644 tools/templates/crud/client/containers/ModuleQuery.jsx create mode 100644 tools/templates/crud/client/graphql/CreateModule.graphql create mode 100644 tools/templates/crud/client/graphql/DeleteManyModules.graphql create mode 100644 tools/templates/crud/client/graphql/DeleteModule.graphql create mode 100644 tools/templates/crud/client/graphql/Module.graphql create mode 100644 tools/templates/crud/client/graphql/ModuleQuery.graphql create mode 100644 tools/templates/crud/client/graphql/ModuleState.client.graphql create mode 100644 tools/templates/crud/client/graphql/ModuleStateQuery.client.graphql create mode 100644 tools/templates/crud/client/graphql/ModulesQuery.graphql create mode 100644 tools/templates/crud/client/graphql/ModulesSubscription.graphql create mode 100644 tools/templates/crud/client/graphql/SortModules.graphql create mode 100644 tools/templates/crud/client/graphql/UpdateFilter.client.graphql create mode 100644 tools/templates/crud/client/graphql/UpdateManyModules.graphql create mode 100644 tools/templates/crud/client/graphql/UpdateModule.graphql create mode 100644 tools/templates/crud/client/graphql/UpdateOrderBy.client.graphql create mode 100644 tools/templates/crud/client/index.native.jsx create mode 100644 tools/templates/crud/client/index.web.jsx create mode 100644 tools/templates/crud/client/resolvers/index.js create mode 100644 tools/templates/crud/database/migrations/_Module.js create mode 100644 tools/templates/crud/database/seeds/_Module.js rename tools/templates/{module => crud}/server/Module.spec.js (100%) create mode 100644 tools/templates/crud/server/index.js create mode 100644 tools/templates/crud/server/resolvers.js create mode 100644 tools/templates/crud/server/schema.graphql create mode 100644 tools/templates/crud/server/schema.js create mode 100644 tools/templates/crud/server/sql.js delete mode 100644 tools/templates/module/client/graphql/ModuleQuery.graphql rename tools/templates/module/client/{index.jsx => index.native.jsx} (100%) delete mode 100644 tools/templates/module/server/resolvers.js delete mode 100644 tools/templates/module/server/schema.graphql diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000000..6eae774147 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +**/tools/templates/crud/client/graphql/* +**/tools/templates/crud/server/*.graphql \ No newline at end of file diff --git a/packages/server/src/modules/common/generatedSchemas.js b/packages/server/src/modules/common/generatedSchemas.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tools/cli.js b/tools/cli.js index 1a535a90d3..f54b027841 100644 --- a/tools/cli.js +++ b/tools/cli.js @@ -1,3 +1,6 @@ +require('babel-register')({ presets: ['env'], plugins: ['transform-class-properties'] }); +require('babel-polyfill'); + const prog = require('caporal'); const moduleCmd = require('./cli/module'); @@ -12,6 +15,16 @@ prog 'both' ) .action((args, options, logger) => moduleCmd('addmodule', args, options, logger)) + .command('addcrud', 'Create a new Module with CRUD') + .argument('', 'Module name') + .argument( + '[location]', + 'Where should new module be created. [both, server, client]', + ['both', 'server', 'client'], + 'both' + ) + .argument('[tablePrefix]', 'DB table prefix.') + .action((args, options, logger) => moduleCmd('addcrud', args, options, logger)) .command('deletemodule', 'Delete a Module') .argument('', 'Module name') .argument( @@ -20,6 +33,9 @@ prog ['both', 'server', 'client'], 'both' ) - .action((args, options, logger) => moduleCmd('deletemodule', args, options, logger)); + .action((args, options, logger) => moduleCmd('deletemodule', args, options, logger)) + .command('updateschema', 'Update Module Schema') + .argument('', 'Module name') + .action((args, options, logger) => moduleCmd('updateschema', args, options, logger)); prog.parse(process.argv); diff --git a/tools/cli/commands/addModule.js b/tools/cli/commands/addModule.js new file mode 100644 index 0000000000..de25d0a718 --- /dev/null +++ b/tools/cli/commands/addModule.js @@ -0,0 +1,94 @@ +const shell = require('shelljs'); +const fs = require('fs'); +const chalk = require('chalk'); +const { pascalize } = require('humps'); +const { renameFiles, updateFileWithExports } = require('../helpers/util'); + +/** + * + * @param logger + * @param templatePath + * @param module + * @param action + * @param tablePrefix + * @param location + */ +function addModule(logger, templatePath, module, action, tablePrefix, location) { + logger.info(`Copying ${location} files…`); + + // pascalize + const Module = pascalize(module); + + // create new module directory + const startPath = `${__dirname}/../../..`; + const mkdir = shell.mkdir(`${startPath}/packages/${location}/src/modules/${module}`); + + // continue only if directory does not jet exist + if (mkdir.code === 0) { + const destinationPath = `${startPath}/packages/${location}/src/modules/${module}`; + renameFiles(destinationPath, templatePath, module, location); + + logger.info(chalk.green(`✔ The ${location} files have been copied!`)); + + shell.cd('..'); + // get module input data + const path = `${startPath}/packages/${location}/src/modules/index.js`; + let data = fs.readFileSync(path); + + // extract Feature modules + const re = /Feature\(([^()]+)\)/g; + const match = re.exec(data); + + // prepend import module + const prepend = `import ${module} from './${module}';\n`; + fs.writeFileSync(path, prepend + data); + + // add module to Feature function + shell.ShellString(shell.cat('index.js').replace(RegExp(re, 'g'), `Feature(${module}, ${match[1]})`)).to('index.js'); + + if (action === 'addcrud' && location === 'client') { + const generatedContainerFile = 'generatedContainers.js'; + const graphqlQuery = `${Module}Query`; + const options = { + pathToFileWithExports: `${startPath}/packages/${location}/src/modules/common/${generatedContainerFile}`, + exportName: graphqlQuery, + importString: `import ${graphqlQuery} from '../${module}/containers/${graphqlQuery}';\n` + }; + updateFileWithExports(options); + } + + if (action === 'addcrud' && location === 'server') { + console.log('copy database files'); + const destinationPath = `${startPath}/packages/${location}/src/database`; + renameFiles(destinationPath, templatePath, module, 'database'); + + const timestamp = new Date().getTime(); + shell.cd(`${startPath}/packages/${location}/src/database/migrations`); + shell.mv(`_${Module}.js`, `${timestamp}_${Module}.js`); + shell.cd(`${startPath}/packages/${location}/src/database/seeds`); + shell.mv(`_${Module}.js`, `${timestamp}_${Module}.js`); + + logger.info(chalk.green(`✔ The database files have been copied!`)); + + if (tablePrefix !== '') { + shell.cd(`${startPath}/packages/${location}/src/modules/${module}`); + shell.sed('-i', /this.prefix = '';/g, `this.prefix = '${tablePrefix}';`, 'sql.js'); + + logger.info(chalk.green(`✔ Inserted db table prefix!`)); + } + + const generatedSchemasFile = 'generatedSchemas.js'; + const schema = `${Module}Schema`; + const options = { + pathToFileWithExports: `${startPath}/packages/${location}/src/modules/common/${generatedSchemasFile}`, + exportName: schema, + importString: `import { ${schema} } from '../${module}/schema';\n` + }; + updateFileWithExports(options); + } + + logger.info(chalk.green(`✔ Module for ${location} successfully created!`)); + } +} + +module.exports = addModule; diff --git a/tools/cli/commands/deleteModule.js b/tools/cli/commands/deleteModule.js new file mode 100644 index 0000000000..957ca4cb92 --- /dev/null +++ b/tools/cli/commands/deleteModule.js @@ -0,0 +1,134 @@ +const shell = require('shelljs'); +const fs = require('fs'); +const chalk = require('chalk'); +const { pascalize } = require('humps'); +const readline = require('readline'); + +const { deleteFromFileWithExports } = require('../helpers/util'); +/** + * + * @param logger + * @param templatePath + * @param module + * @param location + */ +async function deleteModule(logger, templatePath, module, location) { + logger.info(`Deleting ${location} files…`); + + // pascalize + const Module = pascalize(module); + const startPath = `${__dirname}/../../..`; + const modulePath = `${startPath}/packages/${location}/src/modules/${module}`; + const generatedContainerFile = 'generatedContainers.js'; + const generatedContainerPath = `${startPath}/packages/${location}/src/modules/common/${generatedContainerFile}`; + const generatedSchemasFile = 'generatedSchemas.js'; + const generatedSchemaPath = `${startPath}/packages/${location}/src/modules/common/${generatedSchemasFile}`; + let userAnswer; + + if (fs.existsSync(modulePath)) { + if (location === 'server') { + // get list of migrations + const migrationNames = shell.find(`${startPath}/packages/${location}/src/database/migrations/`); + + // open server directory + shell.cd(`${startPath}/packages/server`); + + const isLastCreated = !!(migrationNames && migrationNames[migrationNames.length - 1].match(`_${Module}.js`)); + const isExecuted = shell.exec('yarn knex-migrate list', { silent: true }).includes(`_${Module}.js`); + + if (isLastCreated && isExecuted) { + // open server directory + shell.cd(`${startPath}/packages/server`); + // rollback migration + shell.exec('yarn knex-migrate down'); + logger.info(chalk.green(`✔ Rollback migration was successfully!`)); + } else if (isExecuted) { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + const askQuestion = () => { + return new Promise(resolve => { + rl.question(`The table ${module} won't be deleted. Do you want continue y/n?\n`, answer => { + rl.close(answer); + resolve(answer); + }); + }); + }; + + do { + userAnswer = await askQuestion(); + } while (userAnswer !== 'y' && userAnswer !== 'n'); + } + + if (userAnswer === 'n') { + return; + } + + // change to database migrations directory + shell.cd(`${startPath}/packages/${location}/src/database/migrations`); + // check if any migrations files for this module existed + if (shell.find('.').filter(file => file.search(`_${Module}.js`) > -1).length > 0) { + let okMigrations = shell.rm(`*_${Module}.js`); + if (okMigrations) { + logger.info(chalk.green(`✔ Database migrations files successfully deleted!`)); + } + } + + // change to database seeds directory + shell.cd(`${startPath}/packages/${location}/src/database/seeds`); + // check if any seed files for this module exist + if (shell.find('.').filter(file => file.search(`_${Module}.js`) > -1).length > 0) { + let okSeeds = shell.rm(`*_${Module}.js`); + if (okSeeds) { + logger.info(chalk.green(`✔ Database seed files successfully deleted!`)); + } + } + } + + // remove module directory + shell.rm('-rf', modulePath); + + // change to destination directory + shell.cd(`${startPath}/packages/${location}/src/modules/`); + + // get module input data + const path = `${startPath}/packages/${location}/src/modules/index.js`; + let data = fs.readFileSync(path); + + // extract Feature modules + const re = /Feature\(([^()]+)\)/g; + const match = re.exec(data); + const modules = match[1].split(',').filter(featureModule => featureModule.trim() !== module); + + // remove import module line + const lines = data + .toString() + .split('\n') + .filter(line => line.match(`import ${module} from './${module}';`) === null); + fs.writeFileSync(path, lines.join('\n')); + + // remove module from Feature function + //shell.sed('-i', re, `Feature(${modules.toString().trim()})`, 'index.js'); + shell + .ShellString(shell.cat('index.js').replace(RegExp(re, 'g'), `Feature(${modules.toString().trim()})`)) + .to('index.js'); + + // continue only if directory does not jet exist + logger.info(chalk.green(`✔ Module for ${location} successfully deleted!`)); + } else { + logger.info(chalk.red(`✘ Module ${location} location for ${modulePath} not found!`)); + } + + if (fs.existsSync(generatedContainerPath)) { + const graphqlQuery = `${Module}Query`; + deleteFromFileWithExports(generatedContainerPath, graphqlQuery); + } + if (fs.existsSync(generatedSchemaPath)) { + const schema = `${Module}Schema`; + deleteFromFileWithExports(generatedSchemaPath, schema); + } +} + +module.exports = deleteModule; diff --git a/tools/cli/commands/updateSchema.js b/tools/cli/commands/updateSchema.js new file mode 100644 index 0000000000..199453fc52 --- /dev/null +++ b/tools/cli/commands/updateSchema.js @@ -0,0 +1,291 @@ +const shell = require('shelljs'); +const fs = require('fs'); +const chalk = require('chalk'); +const GraphQLGenerator = require('@domain-schema/graphql').default; +const { pascalize, camelize } = require('humps'); + +const { generateField } = require('../helpers/util'); +const schemas = require('../../../packages/server/src/modules/common/generatedSchemas'); + +/** + * + * @param logger + * @param module + * @returns {*|void} + */ +function updateSchema(logger, module) { + logger.info(`Updating ${module} Schema…`); + + // pascalize + const Module = pascalize(module); + + const startPath = `${__dirname}/../../..`; + const modulePath = `${startPath}/packages/server/src/modules/${module}`; + + if (fs.existsSync(modulePath)) { + // get module schema + const schema = schemas.default[`${Module}Schema`]; + + // get schema file + const pathSchema = `${startPath}/packages/server/src/modules/${module}/`; + if (fs.existsSync(pathSchema)) { + const file = `schema.graphql`; + + // regenerate input fields + let inputCreate = ''; + let inputUpdate = ''; + let inputFilter = ` searchText: String\n`; + let manyInput = ''; + for (const key of schema.keys()) { + const value = schema.values[key]; + const hasTypeOf = targetType => value.type === targetType || value.type.prototype instanceof targetType; + if (value.type.isSchema) { + let required = value.optional ? '' : '!'; + inputCreate += ` ${key}Id: Int${required}\n`; + inputUpdate += ` ${key}Id: Int\n`; + inputFilter += ` ${key}Id: Int\n`; + } else if (value.type.constructor !== Array) { + if (key !== 'id') { + inputCreate += ` ${key}: ${generateField(value)}\n`; + inputUpdate += ` ${key}: ${generateField(value, true)}\n`; + } + + if (hasTypeOf(Date)) { + inputFilter += ` ${key}_lte: ${generateField(value, true)}\n`; + inputFilter += ` ${key}_gte: ${generateField(value, true)}\n`; + } else { + inputFilter += ` ${key}: ${generateField(value, true)}\n`; + } + } else if (value.type.constructor === Array && value.type[0].isSchema) { + inputCreate += ` ${key}: ${pascalize(key)}CreateManyInput\n`; + inputUpdate += ` ${key}: ${pascalize(key)}UpdateManyInput\n`; + + manyInput += ` + +input ${pascalize(key)}CreateManyInput { + create: [${pascalize(value.type[0].name)}CreateInput!] +} + +input ${pascalize(key)}UpdateManyInput { + create: [${pascalize(value.type[0].name)}CreateInput!] + delete: [${pascalize(value.type[0].name)}WhereUniqueInput!] + update: [${pascalize(value.type[0].name)}UpdateWhereInput!] +} + +input ${pascalize(value.type[0].name)}UpdateWhereInput { + where: ${pascalize(value.type[0].name)}WhereUniqueInput! + data: ${pascalize(value.type[0].name)}UpdateInput! +}`; + } + } + + shell.cd(pathSchema); + // override Module type in schema.graphql file + const replaceType = `### schema type definitions([^()]+)### end schema type definitions`; + shell + .ShellString( + shell + .cat(file) + .replace( + RegExp(replaceType, 'g'), + `### schema type definitions\n${new GraphQLGenerator().generateTypes( + schema + )}${manyInput}\n\n### end schema type definitions` + ) + ) + .to(file); + + // override ModuleCreateInput in schema.graphql file + const replaceCreate = `input ${Module}CreateInput {([^}])*\\n}`; + shell + .ShellString( + shell.cat(file).replace(RegExp(replaceCreate, 'g'), `input ${Module}CreateInput {\n${inputCreate}}`) + ) + .to(file); + + // override ModuleUpdateInput in schema.graphql file + const replaceUpdate = `input ${Module}UpdateInput {([^}])*\\n}`; + shell + .ShellString( + shell.cat(file).replace(RegExp(replaceUpdate, 'g'), `input ${Module}UpdateInput {\n${inputUpdate}}`) + ) + .to(file); + + // override ModuleFilterInput in schema.graphql file + const replaceFilter = `input ${Module}FilterInput {([^}])*\\n}`; + shell + .ShellString( + shell.cat(file).replace(RegExp(replaceFilter, 'g'), `input ${Module}FilterInput {\n${inputFilter}}`) + ) + .to(file); + + logger.info(chalk.green(`✔ Schema in ${pathSchema}${file} successfully updated!`)); + + const resolverFile = `resolvers.js`; + let replace = ` ${schema.name}: { +`; + for (const key of schema.keys()) { + const value = schema.values[key]; + if (value.type.constructor === Array) { + replace += ` ${key}: createBatchResolver((sources, args, ctx, info) => { + return ctx.${schema.name}.getByIds(sources.map(({ id }) => id), '${camelize(schema.name)}', ctx.${ + value.type[0].name + }, info); + }), +`; + } + } + replace += ` }, +`; + + // override batch resolvers in resolvers.js file + const replaceBatchResolvers = `// schema batch resolvers([^*]+)// end schema batch resolvers`; + shell + .ShellString( + shell + .cat(resolverFile) + .replace( + RegExp(replaceBatchResolvers, 'g'), + `// schema batch resolvers\n${replace} // end schema batch resolvers` + ) + ) + .to(resolverFile); + + logger.info(chalk.green(`✔ Resolver in ${pathSchema}${resolverFile} successfully updated!`)); + } else { + logger.error(chalk.red(`✘ Schema path ${pathSchema} not found!`)); + } + + // get fragment file + const pathFragment = `${startPath}/packages/client/src/modules/${module}/graphql/`; + if (fs.existsSync(pathFragment)) { + const fragmentGraphqlFile = `${Module}.graphql`; + + // regenerate graphql fragment + let fragmentGraphql = ''; + for (const key of schema.keys()) { + fragmentGraphql += regenerateGraphqlFragment(schema.values[key], key); + } + + shell.cd(pathFragment); + // override graphql fragment file + const replaceFragment = `${Module} {(.|\n)*\n}`; + fragmentGraphql = shell + .cat(fragmentGraphqlFile) + .replace(RegExp(replaceFragment, 'g'), `${Module} {\n${fragmentGraphql}}`); + try { + fs.writeFileSync(pathFragment + fragmentGraphqlFile, fragmentGraphql); + } catch (err) { + return logger.error(chalk.red(`✘ Failed to write a ${pathFragment}${fragmentGraphqlFile} file!`)); + } + logger.info(chalk.green(`✔ Fragment in ${pathFragment}${fragmentGraphqlFile} successfully updated!`)); + } else { + logger.error(chalk.red(`✘ Fragment path ${pathFragment} not found!`)); + } + + // get state client file + const pathStateClient = `${startPath}/packages/client/src/modules/${module}/graphql/`; + if (fs.existsSync(pathStateClient)) { + const file = `${Module}State.client.graphql`; + + // regenerate graphql fragment + let graphql = ''; + for (const key of schema.keys()) { + const value = schema.values[key]; + const hasTypeOf = targetType => value.type === targetType || value.type.prototype instanceof targetType; + if (value.type.isSchema) { + graphql += ` ${key}Id\n`; + } else { + if (hasTypeOf(Date)) { + graphql += ` ${key}_lte\n`; + graphql += ` ${key}_gte\n`; + } else { + graphql += ` ${key}\n`; + } + } + } + graphql += ` searchText + }\n`; + + shell.cd(pathStateClient); + // override graphql fragment file + const replaceStateClient = `filter {(.|\n)*\n}\n`; + shell.ShellString(shell.cat(file).replace(RegExp(replaceStateClient, 'g'), `filter {\n${graphql}}\n`)).to(file); + + logger.info(chalk.green(`✔ State Client in ${pathStateClient}${file} successfully updated!`)); + } else { + logger.error(chalk.red(`✘ State Client path ${pathStateClient} not found!`)); + } + + // get state resolver file + const pathStateResolver = `${startPath}/packages/client/src/modules/${module}/resolvers/`; + if (fs.existsSync(pathStateResolver)) { + const file = `index.js`; + + // regenerate resolver defaults + let defaults = `const defaultFilters = {\n`; + for (const key of schema.keys()) { + const value = schema.values[key]; + const hasTypeOf = targetType => value.type === targetType || value.type.prototype instanceof targetType; + if (value.type.isSchema) { + defaults += ` ${key}Id: '',\n`; + } else { + if (hasTypeOf(Date)) { + defaults += ` ${key}_lte: '',\n`; + defaults += ` ${key}_gte: '',\n`; + } else { + defaults += ` ${key}: '',\n`; + } + } + } + + defaults += ` searchText: '' +};\n`; + + shell.cd(pathStateResolver); + // override state resolver file + const replaceStateResolverDefaults = `// filter data([^*]+)// end filter data`; + shell + .ShellString( + shell + .cat(file) + .replace(RegExp(replaceStateResolverDefaults, 'g'), `// filter data\n${defaults}// end filter data`) + ) + .to(file); + + logger.info(chalk.green(`✔ State Resolver in ${pathStateResolver}${file} successfully updated!`)); + } else { + logger.error(chalk.red(`✘ State Resolver path ${pathStateResolver} not found!`)); + } + } else { + logger.info(chalk.red(`✘ Module ${module} in path ${modulePath} not found!`)); + } +} + +const regenerateGraphqlFragment = (value, key, spaces = '') => { + if (value.type.isSchema) { + let column = 'name'; + for (const remoteKey of value.type.keys()) { + const remoteValue = value.type.values[remoteKey]; + if (remoteValue.sortBy) { + column = remoteKey; + } + } + return ` ${key} {\n ${spaces}id\n ${spaces}${column}\n ${spaces}}\n`; + } else if (value.type.constructor === Array && value.type[0].isSchema) { + let str = ` ${key} {\n`; + for (const remoteKey of value.type[0].keys()) { + const remoteValue = value.type[0].values[remoteKey]; + if (remoteValue.type.isSchema) { + str += ` ${regenerateGraphqlFragment(remoteValue, remoteKey, ' ')}`; + } else if (remoteValue.type.constructor !== Array) { + str += ` ${remoteKey}\n`; + } + } + return str + ` }\n`; + } else { + return ` ${key}\n`; + } +}; + +module.exports = updateSchema; diff --git a/tools/cli/helpers/util.js b/tools/cli/helpers/util.js new file mode 100644 index 0000000000..4d3d78372c --- /dev/null +++ b/tools/cli/helpers/util.js @@ -0,0 +1,124 @@ +const shell = require('shelljs'); +const fs = require('fs'); +const DomainSchema = require('@domain-schema/core').default; +const { pascalize, decamelize } = require('humps'); +const { startCase } = require('lodash'); + +/** + * + * @param destinationPath + * @param templatePath + * @param module + * @param location + */ +function renameFiles(destinationPath, templatePath, module, location) { + // pascalize + const Module = pascalize(module); + + shell.cp('-R', `${templatePath}/${location}/*`, destinationPath); + + // change to destination directory + shell.cd(destinationPath); + + // rename files + shell.ls('-Rl', '.').forEach(entry => { + if (entry.isFile()) { + const moduleFile = entry.name.replace('Module', Module); + shell.mv(entry.name, moduleFile); + } + }); + + // replace module names + shell.ls('-Rl', '.').forEach(entry => { + if (entry.isFile()) { + shell.sed('-i', /\$module\$/g, module, entry.name); + shell.sed('-i', /\$_module\$/g, decamelize(module), entry.name); + shell.sed('-i', /\$Module\$/g, Module, entry.name); + shell.sed('-i', /\$MoDuLe\$/g, startCase(Module), entry.name); + shell.sed('-i', /\$MODULE\$/g, module.toUpperCase(), entry.name); + } + }); +} + +/** + * + * @param value + * @param update + * @returns {string} + */ +function generateField(value, update = false) { + let result = ''; + const hasTypeOf = targetType => value.type === targetType || value.type.prototype instanceof targetType; + if (hasTypeOf(Boolean)) { + result += 'Boolean'; + } else if (hasTypeOf(DomainSchema.ID)) { + result += 'ID'; + } else if (hasTypeOf(DomainSchema.Int)) { + result += 'Int'; + } else if (hasTypeOf(DomainSchema.Float)) { + result += 'Float'; + } else if (hasTypeOf(String)) { + result += 'String'; + } else if (hasTypeOf(Date)) { + result += 'Date'; + } else if (hasTypeOf(DomainSchema.DateTime)) { + result += 'DateTime'; + } else if (hasTypeOf(DomainSchema.Time)) { + result += 'Time'; + } + + if (!update && !value.optional) { + result += '!'; + } + + return result; +} + +/** + * + * @param pathToFileWithExports + * @param exportName + * @param importString + */ +function updateFileWithExports({ pathToFileWithExports, exportName, importString }) { + const exportGraphqlContainer = `\nexport default {\n ${exportName}\n};\n`; + + if (fs.existsSync(pathToFileWithExports)) { + const generatedContainerData = fs.readFileSync(pathToFileWithExports); + const generatedContainer = generatedContainerData.toString().trim(); + if (generatedContainer.length > 1) { + const index = generatedContainer.lastIndexOf("';"); + const computedIndex = index >= 0 ? index + 3 : false; + if (computedIndex) { + let computedGeneratedContainer = + generatedContainer.slice(0, computedIndex) + + importString + + generatedContainer.slice(computedIndex, generatedContainer.length); + computedGeneratedContainer = computedGeneratedContainer.replace(/(,|)\s};/g, `,\n ${exportName}\n};`); + return fs.writeFileSync(pathToFileWithExports, computedGeneratedContainer); + } + } + } + return fs.writeFileSync(pathToFileWithExports, importString + exportGraphqlContainer); +} + +/** + * + * @param pathToFileWithExports + * @param exportName + */ +function deleteFromFileWithExports(pathToFileWithExports, exportName) { + if (fs.existsSync(pathToFileWithExports)) { + const generatedElementData = fs.readFileSync(pathToFileWithExports); + const reg = `(\\n\\s\\s${exportName}(.|)|import (${exportName}|{ ${exportName} }).+;\\n+(?!ex))`; + const generatedElement = generatedElementData.toString().replace(new RegExp(reg, 'g'), ''); + fs.writeFileSync(pathToFileWithExports, generatedElement); + } +} + +module.exports = { + renameFiles, + generateField, + updateFileWithExports, + deleteFromFileWithExports +}; diff --git a/tools/cli/module.js b/tools/cli/module.js index e062ae5a5a..fd0164b9cf 100644 --- a/tools/cli/module.js +++ b/tools/cli/module.js @@ -1,133 +1,50 @@ -const shell = require('shelljs'); const fs = require('fs'); +const chalk = require('chalk'); +const addModule = require('./commands/addModule'); +const deleteModule = require('./commands/deleteModule'); +const updateSchema = require('./commands/updateSchema'); -String.prototype.toCamelCase = function() { - return this.replace(/^([A-Z])|\s(\w)/g, function(match, p1, p2) { - if (p2) return p2.toUpperCase(); - return p1.toLowerCase(); - }); -}; - -String.prototype.capitalize = function() { - return this.charAt(0).toUpperCase() + this.slice(1); -}; - -function copyFiles(logger, templatePath, module, location) { - logger.info(`Copying ${location} files…`); - - // create new module directory - const mkdir = shell.mkdir(`${__dirname}/../../packages/${location}/src/modules/${module}`); - - // continue only if directory does not jet exist - if (mkdir.code === 0) { - const destinationPath = `${__dirname}/../../packages/${location}/src/modules/${module}`; - shell.cp('-R', `${templatePath}/${location}/*`, destinationPath); - - logger.info(`✔ The ${location} files have been copied!`); - - // change to destination directory - shell.cd(destinationPath); - - // rename files - shell.ls('-Rl', '.').forEach(entry => { - if (entry.isFile()) { - const moduleFile = entry.name.replace('Module', module.capitalize()); - shell.mv(entry.name, moduleFile); - } - }); - - // replace module names - shell.ls('-Rl', '.').forEach(entry => { - if (entry.isFile()) { - shell.sed('-i', /\$module\$/g, module, entry.name); - shell.sed('-i', /\$Module\$/g, module.toCamelCase().capitalize(), entry.name); - } - }); - - shell.cd('..'); - // get module input data - const path = `${__dirname}/../../packages/${location}/src/modules/index.js`; - let data = fs.readFileSync(path); - - // extract Feature modules - const re = /Feature\(([^()]+)\)/g; - const match = re.exec(data); - - // prepend import module - const prepend = `import ${module} from './${module}';\n`; - fs.writeFileSync(path, prepend + data); - - // add module to Feature function - shell.sed('-i', re, `Feature(${module}, ${match[1]})`, 'index.js'); - - logger.info(`✔ Module for ${location} successfully created!`); +module.exports = (action, args, options, logger) => { + const module = args.module; + let location = 'both'; + if (args.location) { + location = args.location; } -} - -function deleteFiles(logger, templatePath, module, location) { - logger.info(`Deleting ${location} files…`); - - const modulePath = `${__dirname}/../../packages/${location}/src/modules/${module}`; - - if (fs.existsSync(modulePath)) { - // create new module directory - shell.rm('-rf', modulePath); - - // change to destination directory - shell.cd(`${__dirname}/../../packages/${location}/src/modules/`); - - // add module to Feature function - //let ok = shell.sed('-i', `import ${module} from '.\/${module}';`, '', 'index.js'); - - // get module input data - const path = `${__dirname}/../../packages/${location}/src/modules/index.js`; - let data = fs.readFileSync(path); - - // extract Feature modules - const re = /Feature\(([^()]+)\)/g; - const match = re.exec(data); - const modules = match[1].split(',').filter(featureModule => featureModule.trim() !== module); - - // remove import module line - const lines = data - .toString() - .split('\n') - .filter(line => line.match(`import ${module} from './${module}';`) === null); - fs.writeFileSync(path, lines.join('\n')); - - // remove module from Feature function - shell.sed('-i', re, `Feature(${modules.toString().trim()})`, 'index.js'); - - // continue only if directory does not jet exist - logger.info(`✔ Module for ${location} successfully deleted!`); - } else { - logger.info(`✔ Module ${location} location for ${modulePath} wasn't found!`); + let tablePrefix = ''; + if (args.tablePrefix) { + tablePrefix = args.tablePrefix; + } + console.log(tablePrefix); + let templatePath = `${__dirname}/../templates/module`; + if (action === 'addcrud') { + templatePath = `${__dirname}/../templates/crud`; } -} - -module.exports = (action, args, options, logger) => { - const templatePath = `${__dirname}/../templates/module`; if (!fs.existsSync(templatePath)) { - logger.error(`The requested location for ${args.location} wasn't found.`); + logger.error(chalk.red(`The requested location for ${location} not found.`)); process.exit(1); } // client - if (args.location === 'client' || args.location === 'both') { - if (action === 'addmodule') { - copyFiles(logger, templatePath, args.module, 'client'); + if (location === 'client' || location === 'both') { + if (action === 'addmodule' || action === 'addcrud') { + addModule(logger, templatePath, module, action, tablePrefix, 'client'); } else if (action === 'deletemodule') { - deleteFiles(logger, templatePath, args.module, 'client'); + deleteModule(logger, templatePath, module, 'client'); } } // server - if (args.location === 'server' || args.location === 'both') { - if (action === 'addmodule') { - copyFiles(logger, templatePath, args.module, 'server'); + if (location === 'server' || location === 'both') { + if (action === 'addmodule' || action === 'addcrud') { + addModule(logger, templatePath, module, action, tablePrefix, 'server'); } else if (action === 'deletemodule') { - deleteFiles(logger, templatePath, args.module, 'server'); + deleteModule(logger, templatePath, module, 'server'); } } + + // update schema + if (action === 'updateschema') { + updateSchema(logger, module); + } }; diff --git a/tools/templates/crud/client/components/Module.jsx b/tools/templates/crud/client/components/Module.jsx new file mode 100644 index 0000000000..59b72c186e --- /dev/null +++ b/tools/templates/crud/client/components/Module.jsx @@ -0,0 +1,37 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { StyleSheet, Text, View } from 'react-native'; + +export default class $Module$ extends React.Component { + static propTypes = { + title: PropTypes.string.isRequired + }; + + render() { + const { title } = this.props; + return ( + + + Hello {title}! + + + ); + } +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#fff', + alignItems: 'center', + justifyContent: 'center' + }, + element: { + paddingTop: 30 + }, + box: { + textAlign: 'center', + marginLeft: 15, + marginRight: 15 + } +}); diff --git a/tools/templates/crud/client/components/Module.web.jsx b/tools/templates/crud/client/components/Module.web.jsx new file mode 100644 index 0000000000..ef88b4b4b0 --- /dev/null +++ b/tools/templates/crud/client/components/Module.web.jsx @@ -0,0 +1,38 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Helmet from 'react-helmet'; +import { PageLayout } from '../../common/components/web'; + +import $Module$Filter from '../containers/$Module$Filter'; +import $Module$List from '../containers/$Module$List'; +import settings from '../../../../../../settings'; + +export default class $Module$ extends React.PureComponent { + static propTypes = { + title: PropTypes.string.isRequired + }; + + renderMetaData = title => ( + + ); + + render() { + const { title } = this.props; + return ( + + {this.renderMetaData(title)} +

{title}

+ <$Module$Filter {...this.props} /> + <$Module$List {...this.props} /> +
+ ); + } +} diff --git a/tools/templates/crud/client/containers/Module.spec.js b/tools/templates/crud/client/containers/Module.spec.js new file mode 100644 index 0000000000..ac34fe5ce1 --- /dev/null +++ b/tools/templates/crud/client/containers/Module.spec.js @@ -0,0 +1,17 @@ +import { expect } from 'chai'; +import { step } from 'mocha-steps'; +import Renderer from '../../../testHelpers/Renderer'; +import Routes from '../../../app/Routes'; + +describe('$Module$ UI works', () => { + const renderer = new Renderer({}); + let app; + let content; + + step('$Module$ page renders on mount', () => { + app = renderer.mount(Routes); + renderer.history.push('/$module$'); + content = app.find('#content'); + expect(content).to.not.be.empty; + }); +}); diff --git a/tools/templates/crud/client/containers/ModuleEdit.jsx b/tools/templates/crud/client/containers/ModuleEdit.jsx new file mode 100644 index 0000000000..c45b7b3ecd --- /dev/null +++ b/tools/templates/crud/client/containers/ModuleEdit.jsx @@ -0,0 +1,87 @@ +import React from 'react'; +import { graphql, compose } from 'react-apollo'; + +import { EditView } from '../../common/components/crud'; +import { $Module$Schema } from '../../../../../server/src/modules/$module$/schema'; +import $MODULE$_QUERY from '../graphql/$Module$Query.graphql'; +import CREATE_$MODULE$ from '../graphql/Create$Module$.graphql'; +import UPDATE_$MODULE$ from '../graphql/Update$Module$.graphql'; + +class $Module$Edit extends React.Component { + render() { + return ; + } +} + +export default compose( + graphql($MODULE$_QUERY, { + options: props => { + let id = 0; + if (props.match) { + id = props.match.params.id; + } else if (props.navigation) { + id = props.navigation.state.params.id; + } + + return { + fetchPolicy: 'cache-and-network', + variables: { where: { id } } + }; + }, + props({ data: { loading, $module$ } }) { + return { loading, data: $module$ }; + } + }), + graphql(CREATE_$MODULE$, { + props: ({ ownProps: { history, navigation }, mutate }) => ({ + createEntry: async data => { + try { + const { + data: { create$Module$ } + } = await mutate({ + variables: { data } + }); + + if (create$Module$.errors) { + return { errors: create$Module$.errors }; + } + + if (history) { + return history.push('/$module$'); + } + if (navigation) { + return navigation.goBack(); + } + } catch (e) { + console.log(e.graphQLErrors); + } + } + }) + }), + graphql(UPDATE_$MODULE$, { + props: ({ ownProps: { history, navigation }, mutate }) => ({ + updateEntry: async (data, where) => { + try { + const { + data: { update$Module$ } + } = await mutate({ + variables: { data, where } + }); + + if (update$Module$.errors) { + return { errors: update$Module$.errors }; + } + + if (history) { + return history.push('/$module$'); + } + if (navigation) { + return navigation.goBack(); + } + } catch (e) { + console.log(e.graphQLErrors); + } + } + }) + }) +)($Module$Edit); diff --git a/tools/templates/crud/client/containers/ModuleFilter.jsx b/tools/templates/crud/client/containers/ModuleFilter.jsx new file mode 100644 index 0000000000..457b546f59 --- /dev/null +++ b/tools/templates/crud/client/containers/ModuleFilter.jsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { graphql, compose } from 'react-apollo'; + +import { removeTypename } from '../../../../../common/utils'; +import { FilterView } from '../../common/components/crud'; +import { $Module$Schema } from '../../../../../server/src/modules/$module$/schema'; + +import $MODULE$_STATE_QUERY from '../graphql/$Module$StateQuery.client.graphql'; +import UPDATE_FILTER from '../graphql/UpdateFilter.client.graphql'; + +class $Module$Filter extends React.Component { + render() { + return ; + } +} + +export default compose( + graphql($MODULE$_STATE_QUERY, { + props({ + data: { + $module$State: { filter } + } + }) { + return removeTypename(filter); + } + }), + graphql(UPDATE_FILTER, { + props: ({ mutate }) => ({ + onFilterChange(filter) { + mutate({ variables: { filter } }); + } + }) + }) +)($Module$Filter); diff --git a/tools/templates/crud/client/containers/ModuleList.jsx b/tools/templates/crud/client/containers/ModuleList.jsx new file mode 100644 index 0000000000..0a71141909 --- /dev/null +++ b/tools/templates/crud/client/containers/ModuleList.jsx @@ -0,0 +1,215 @@ +import React from 'react'; +import { graphql, compose } from 'react-apollo'; +import update from 'immutability-helper'; +import PropTypes from 'prop-types'; + +import { removeTypename, removeEmpty } from '../../../../../common/utils'; +import { ListView } from '../../common/components/crud'; +import { updateEntry, deleteEntry } from '../../common/crud'; +import { $Module$Schema } from '../../../../../server/src/modules/$module$/schema'; + +import $MODULE$_STATE_QUERY from '../graphql/$Module$StateQuery.client.graphql'; +import UPDATE_ORDER_BY from '../graphql/UpdateOrderBy.client.graphql'; +import $MODULE$S_QUERY from '../graphql/$Module$sQuery.graphql'; +import UPDATE_$MODULE$ from '../graphql/Update$Module$.graphql'; +import DELETE_$MODULE$ from '../graphql/Delete$Module$.graphql'; +import SORT_$MODULE$S from '../graphql/Sort$Module$s.graphql'; +import DELETEMANY_$MODULE$S from '../graphql/DeleteMany$Module$s.graphql'; +import UPDATEMANY_$MODULE$S from '../graphql/UpdateMany$Module$s.graphql'; +import $MODULE$S_SUBSCRIPTION from '../graphql/$Module$sSubscription.graphql'; + +function Add$Module$(prev, node) { + return update(prev, { + $module$sConnection: { + totalCount: { + $set: prev.$module$sConnection.totalCount + 1 + }, + edges: { + $set: [...prev.$module$sConnection.edges, node] + } + } + }); +} + +class $Module$ extends React.Component { + static propTypes = { + subscribeToMore: PropTypes.func.isRequired + }; + + constructor(props) { + super(props); + this.subscription = null; + } + + componentDidMount() { + this.init$Module$ListSubscription(); + } + + componentWillUnmount() { + if (this.subscription) { + // unsubscribe + this.subscription(); + this.subscription = null; + } + } + + init$Module$ListSubscription() { + if (!this.subscription) { + this.subscribeTo$Module$sList(); + } + } + + subscribeTo$Module$sList = () => { + const { subscribeToMore } = this.props; + + this.subscription = subscribeToMore({ + document: $MODULE$S_SUBSCRIPTION, + updateQuery: ( + prev, + { + subscriptionData: { + data: { + $module$sUpdated: { mutation, node } + } + } + } + ) => { + let newResult = prev; + + if (mutation === 'CREATED') { + newResult = Add$Module$(prev, node); + } + return newResult; + } + }); + }; + render() { + return ; + } +} + +export default compose( + graphql($MODULE$_STATE_QUERY, { + props({ data: { $module$State } }) { + return removeTypename($module$State); + } + }), + graphql($MODULE$S_QUERY, { + options: ({ limit, orderBy, filter }) => { + return { + fetchPolicy: 'cache-and-network', + variables: { limit, orderBy, filter: removeEmpty(filter) } + }; + }, + props: ({ data: { loading, $module$sConnection, refetch, error, fetchMore, subscribeToMore } }) => { + const loadMoreRows = () => { + return fetchMore({ + variables: { + offset: $module$sConnection.edges.length + }, + updateQuery: (previousResult, { fetchMoreResult }) => { + const newEdges = fetchMoreResult.$module$sConnection.edges; + const pageInfo = fetchMoreResult.$module$sConnection.pageInfo; + + return { + $module$sConnection: { + edges: [...previousResult.$module$sConnection.edges, ...newEdges], + pageInfo, + __typename: '$Module$sConnection' + } + }; + } + }); + }; + if (error) throw new Error(error); + return { + loading, + data: $module$sConnection, + loadMoreRows, + refetch, + subscribeToMore, + errors: error ? error.graphQLErrors : null + }; + } + }), + graphql(UPDATE_$MODULE$, { + props: props => ({ + updateEntry: args => updateEntry(props, args, 'update$Module$') + }) + }), + graphql(DELETE_$MODULE$, { + props: props => ({ + deleteEntry: args => deleteEntry(props, args, 'delete$Module$') + }) + }), + graphql(SORT_$MODULE$S, { + props: ({ ownProps: { refetch }, mutate }) => ({ + sortEntries: async data => { + try { + const { + data: { sort$Module$s } + } = await mutate({ + variables: { data } + }); + + if (sort$Module$s.errors) { + return { errors: sort$Module$s.errors }; + } + + refetch(); + } catch (e) { + console.log(e.graphQLErrors); + } + } + }) + }), + graphql(DELETEMANY_$MODULE$S, { + props: ({ ownProps: { refetch }, mutate }) => ({ + deleteManyEntries: async where => { + try { + const { + data: { deleteMany$Module$s } + } = await mutate({ + variables: { where } + }); + + if (deleteMany$Module$s.errors) { + return { errors: deleteMany$Module$s.errors }; + } + + refetch(); + } catch (e) { + console.log(e.graphQLErrors); + } + } + }) + }), + graphql(UPDATEMANY_$MODULE$S, { + props: ({ ownProps: { refetch }, mutate }) => ({ + updateManyEntries: async (data, where) => { + try { + const { + data: { updateMany$Module$s } + } = await mutate({ + variables: { data: removeEmpty(data), where } + }); + + if (updateMany$Module$s.errors) { + return { errors: updateMany$Module$s.errors }; + } + + refetch(); + } catch (e) { + console.log(e.graphQLErrors); + } + } + }) + }), + graphql(UPDATE_ORDER_BY, { + props: ({ mutate }) => ({ + onOrderBy: orderBy => { + mutate({ variables: { orderBy } }); + } + }) + }) +)($Module$); diff --git a/tools/templates/crud/client/containers/ModuleQuery.jsx b/tools/templates/crud/client/containers/ModuleQuery.jsx new file mode 100644 index 0000000000..9d0a5f7f94 --- /dev/null +++ b/tools/templates/crud/client/containers/ModuleQuery.jsx @@ -0,0 +1,27 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { graphql } from 'react-apollo'; + +import $MODULE$S_QUERY from '../graphql/$Module$sQuery.graphql'; + +class $Module$sQuery extends React.Component { + static propTypes = { + children: PropTypes.func.isRequired, + loading: PropTypes.bool.isRequired, + data: PropTypes.object, + errors: PropTypes.object + }; + + render() { + const { loading, data, errors } = this.props; + return this.props.children({ loading, data, errors }); + } +} + +export default graphql($MODULE$S_QUERY, { + options: ({ limit, orderBy, filter }) => ({ variables: { limit, orderBy, filter } }), + props: ({ data: { loading, $module$sConnection, error } }) => { + if (error) throw new Error(error); + return { loading, data: $module$sConnection, errors: error ? error.graphQLErrors : null }; + } +})($Module$sQuery); diff --git a/tools/templates/crud/client/graphql/CreateModule.graphql b/tools/templates/crud/client/graphql/CreateModule.graphql new file mode 100644 index 0000000000..27f604e763 --- /dev/null +++ b/tools/templates/crud/client/graphql/CreateModule.graphql @@ -0,0 +1,13 @@ +#import "./$Module$.graphql" + +mutation create$Module$($data: $Module$CreateInput!) { + create$Module$(data: $data) { + node { + ...$Module$Info + } + errors { + field + message + } + } +} diff --git a/tools/templates/crud/client/graphql/DeleteManyModules.graphql b/tools/templates/crud/client/graphql/DeleteManyModules.graphql new file mode 100644 index 0000000000..70a9836a66 --- /dev/null +++ b/tools/templates/crud/client/graphql/DeleteManyModules.graphql @@ -0,0 +1,9 @@ +mutation deleteMany$Module$s($where: $Module$WhereInput!) { + deleteMany$Module$s(where: $where) { + count + errors { + field + message + } + } +} diff --git a/tools/templates/crud/client/graphql/DeleteModule.graphql b/tools/templates/crud/client/graphql/DeleteModule.graphql new file mode 100644 index 0000000000..316dd1ff23 --- /dev/null +++ b/tools/templates/crud/client/graphql/DeleteModule.graphql @@ -0,0 +1,11 @@ +mutation delete$Module$($where: $Module$WhereUniqueInput!) { + delete$Module$(where: $where) { + node { + id + } + errors { + field + message + } + } +} diff --git a/tools/templates/crud/client/graphql/Module.graphql b/tools/templates/crud/client/graphql/Module.graphql new file mode 100644 index 0000000000..a6d63fad05 --- /dev/null +++ b/tools/templates/crud/client/graphql/Module.graphql @@ -0,0 +1,4 @@ +fragment $Module$Info on $Module$ { + id + name +} \ No newline at end of file diff --git a/tools/templates/crud/client/graphql/ModuleQuery.graphql b/tools/templates/crud/client/graphql/ModuleQuery.graphql new file mode 100644 index 0000000000..76df5c5591 --- /dev/null +++ b/tools/templates/crud/client/graphql/ModuleQuery.graphql @@ -0,0 +1,9 @@ +#import "./$Module$.graphql" + +query $module$($where: $Module$WhereUniqueInput!) { + $module$(where: $where) { + node { + ...$Module$Info + } + } +} diff --git a/tools/templates/crud/client/graphql/ModuleState.client.graphql b/tools/templates/crud/client/graphql/ModuleState.client.graphql new file mode 100644 index 0000000000..6f653cfdab --- /dev/null +++ b/tools/templates/crud/client/graphql/ModuleState.client.graphql @@ -0,0 +1,12 @@ +fragment $Module$StateInfo on $Module$State { + limit + orderBy { + column + order + } + filter { + id + name + searchText + } +} diff --git a/tools/templates/crud/client/graphql/ModuleStateQuery.client.graphql b/tools/templates/crud/client/graphql/ModuleStateQuery.client.graphql new file mode 100644 index 0000000000..9bebe879a5 --- /dev/null +++ b/tools/templates/crud/client/graphql/ModuleStateQuery.client.graphql @@ -0,0 +1,7 @@ +#import "./$Module$State.client.graphql" + +query $Module$State { + $module$State @client { + ...$Module$StateInfo + } +} diff --git a/tools/templates/crud/client/graphql/ModulesQuery.graphql b/tools/templates/crud/client/graphql/ModulesQuery.graphql new file mode 100644 index 0000000000..327959f1ee --- /dev/null +++ b/tools/templates/crud/client/graphql/ModulesQuery.graphql @@ -0,0 +1,13 @@ +#import "./$Module$.graphql" + +query $module$sConnection($limit: Int, $offset: Int, $orderBy: OrderByInput, $filter: $Module$FilterInput) { + $module$sConnection(limit: $limit, offset: $offset, orderBy: $orderBy, filter: $filter) { + pageInfo { + totalCount + hasNextPage + } + edges { + ...$Module$Info + } + } +} diff --git a/tools/templates/crud/client/graphql/ModulesSubscription.graphql b/tools/templates/crud/client/graphql/ModulesSubscription.graphql new file mode 100644 index 0000000000..5aad2587ff --- /dev/null +++ b/tools/templates/crud/client/graphql/ModulesSubscription.graphql @@ -0,0 +1,10 @@ +#import "./$Module$.graphql" + +subscription on$Module$sUpdated { + $module$sUpdated { + mutation + node { + ...$Module$Info + } + } +} \ No newline at end of file diff --git a/tools/templates/crud/client/graphql/SortModules.graphql b/tools/templates/crud/client/graphql/SortModules.graphql new file mode 100644 index 0000000000..c9b29dc597 --- /dev/null +++ b/tools/templates/crud/client/graphql/SortModules.graphql @@ -0,0 +1,9 @@ +mutation sort$Module$s($data: [Int!]) { + sort$Module$s(data: $data) { + count + errors { + field + message + } + } +} diff --git a/tools/templates/crud/client/graphql/UpdateFilter.client.graphql b/tools/templates/crud/client/graphql/UpdateFilter.client.graphql new file mode 100644 index 0000000000..b63db8bcc5 --- /dev/null +++ b/tools/templates/crud/client/graphql/UpdateFilter.client.graphql @@ -0,0 +1,5 @@ +#import "./$Module$State.client.graphql" + +mutation update$Module$Filter($filter: Filter$Module$Input!) { + update$Module$Filter(filter: $filter) @client +} diff --git a/tools/templates/crud/client/graphql/UpdateManyModules.graphql b/tools/templates/crud/client/graphql/UpdateManyModules.graphql new file mode 100644 index 0000000000..a388ee6ab4 --- /dev/null +++ b/tools/templates/crud/client/graphql/UpdateManyModules.graphql @@ -0,0 +1,9 @@ +mutation updateMany$Module$s($data: $Module$UpdateInput!, $where: $Module$WhereInput!) { + updateMany$Module$s(data: $data, where: $where) { + count + errors { + field + message + } + } +} diff --git a/tools/templates/crud/client/graphql/UpdateModule.graphql b/tools/templates/crud/client/graphql/UpdateModule.graphql new file mode 100644 index 0000000000..57da8097fb --- /dev/null +++ b/tools/templates/crud/client/graphql/UpdateModule.graphql @@ -0,0 +1,13 @@ +#import "./$Module$.graphql" + +mutation update$Module$($data: $Module$UpdateInput!, $where: $Module$WhereUniqueInput!) { + update$Module$(data: $data, where: $where) { + node { + ...$Module$Info + } + errors { + field + message + } + } +} diff --git a/tools/templates/crud/client/graphql/UpdateOrderBy.client.graphql b/tools/templates/crud/client/graphql/UpdateOrderBy.client.graphql new file mode 100644 index 0000000000..b55d1dd827 --- /dev/null +++ b/tools/templates/crud/client/graphql/UpdateOrderBy.client.graphql @@ -0,0 +1,5 @@ +#import "./$Module$State.client.graphql" + +mutation update$Module$OrderBy($orderBy: OrderBy$Module$Input!) { + update$Module$OrderBy(orderBy: $orderBy) @client +} diff --git a/tools/templates/crud/client/index.native.jsx b/tools/templates/crud/client/index.native.jsx new file mode 100644 index 0000000000..974fa8310a --- /dev/null +++ b/tools/templates/crud/client/index.native.jsx @@ -0,0 +1,61 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button } from 'react-native'; +import { createStackNavigator } from 'react-navigation'; +import { Ionicons } from '@expo/vector-icons'; +import { createTabBarIconWrapper } from '../common/components/native'; +import $Module$List from './containers/$Module$List'; +import $Module$Edit from './containers/$Module$Edit'; +import resolvers from './resolvers'; + +import Feature from '../connector'; + +class $Module$ListScreen extends React.Component { + static navigationOptions = ({ navigation }) => ({ + title: '$Module$ list', + headerRight: