From 3fdfdff1bf6423df531d68e12a6605e0e375b154 Mon Sep 17 00:00:00 2001 From: Al Harris Date: Tue, 12 Sep 2023 15:09:03 -0700 Subject: [PATCH] feat: support modelgen without an amplify backend --- .../amplify-codegen/src/commands/models.js | 129 +++++++++++------- .../src/utils/getModelSchemaPathParam.js | 35 +++++ .../src/utils/getOutputDirParam.js | 9 +- .../src/utils/getProjectRoot.js | 14 ++ 4 files changed, 137 insertions(+), 50 deletions(-) create mode 100644 packages/amplify-codegen/src/utils/getModelSchemaPathParam.js create mode 100644 packages/amplify-codegen/src/utils/getProjectRoot.js diff --git a/packages/amplify-codegen/src/commands/models.js b/packages/amplify-codegen/src/commands/models.js index 3a29d6443..151980646 100644 --- a/packages/amplify-codegen/src/commands/models.js +++ b/packages/amplify-codegen/src/commands/models.js @@ -5,6 +5,8 @@ const { FeatureFlags, pathManager } = require('@aws-amplify/amplify-cli-core'); const { generateModels: generateModelsHelper } = require('@aws-amplify/graphql-generator'); const { validateAmplifyFlutterMinSupportedVersion } = require('../utils/validateAmplifyFlutterMinSupportedVersion'); const defaultDirectiveDefinitions = require('../utils/defaultDirectiveDefinitions'); +const getProjectRoot = require('../utils/getProjectRoot'); +const { getModelSchemaPathParam, hasModelSchemaPathParam } = require('../utils/getModelSchemaPathParam'); /** * Amplify Context type. @@ -34,45 +36,55 @@ const modelgenFrontendToTargetMap = { introspection: 'introspection', }; +/** + * Return feature flag override values from the cli in the format --feature-flag: + * @param {!AmplifyContext} context the amplify runtime context + * @param {!string} flagName the feature flag name + * @returns {any | null} the raw value if found, else null + */ +const cliFeatureFlagOverride = (context, flagName) => context.parameters?.options?.[`feature-flag:${flagName}`]; + /** * Returns feature flag value, default to `false` - * @param {!string} key feature flag id + * @param {!AmplifyContext} context the amplify runtime context + * @param {!string} flagName feature flag id * @returns {!boolean} the feature flag value */ -const readFeatureFlag = key => { - let flagValue = false; +const readFeatureFlag = (context, flagName) => { + const cliValue = cliFeatureFlagOverride(context, flagName); + if (cliValue) { + if (cliValue === 'true' || cliValue === 'True' || cliValue === true) { + return true; + } + if (cliValue === 'false' || cliValue === 'False' || cliValue === false) { + return false; + } + throw new Error(`Feature flag value for parameter ${flagName} could not be marshalled to boolean type, found ${cliValue}`); + } + try { - flagValue = FeatureFlags.getBoolean(key); - } catch (err) { - flagValue = false; + return FeatureFlags.getBoolean(flagName); + } catch (_) { + return false; } - return flagValue; }; /** * Returns feature flag value, default to `1` - * @param {!string} key feature flag id + * @param {!AmplifyContext} context the amplify runtime context + * @param {!string} flagName feature flag id * @returns {!number} the feature flag value */ -const readNumericFeatureFlag = key => { - try { - return FeatureFlags.getNumber(key); - } catch (err) { - return 1; +const readNumericFeatureFlag = (context, flagName) => { + const cliValue = cliFeatureFlagOverride(context, flagName); + if (cliValue) { + return Number.parseInt(cliValue, 10); } -}; -/** - * Retrieve the project root for use in validation and tk - * @param {!AmplifyContext} context the amplify runtime context - * @returns {!string} path to the project root, or cwd if not found - */ -const getProjectRoot = (context) => { try { - context.amplify.getProjectMeta(); - return context.amplify.getEnvInfo().projectPath; + return FeatureFlags.getNumber(flagName); } catch (_) { - return process.cwd(); + return 1; } }; @@ -82,6 +94,11 @@ const getProjectRoot = (context) => { * @returns {!Promise} the api path, if one can be found, else null */ const getApiResourcePath = async (context) => { + const modelSchemaPathParam = getModelSchemaPathParam(context); + if (modelSchemaPathParam) { + return modelSchemaPathParam; + } + const allApiResources = await context.amplify.getResourceStatus('api'); const apiResource = allApiResources.allResources.find( resource => resource.service === 'AppSync' && resource.providerPlugin === 'awscloudformation', @@ -123,7 +140,11 @@ const getOutputDir = (context, projectRoot, overrideOutputDir) => { if (overrideOutputDir) { return overrideOutputDir; } - return path.join(projectRoot, getModelOutputPath(context)); + try { + return path.join(projectRoot, getModelOutputPath(context.amplify.getProjectConfig())); + } catch (_) { + throw new Error('Output directory could not be determined, to specify, set the --output-dir CLI property.') + } }; /** @@ -135,6 +156,12 @@ const getFrontend = (context, isIntrospection) => { if (isIntrospection === true) { return 'introspection'; } + + const targetParam = context.parameters?.options?.['target']; + if (targetParam) { + return targetParam; + } + return context.amplify.getProjectConfig().frontend; }; @@ -149,14 +176,16 @@ const validateProject = async (context, frontend, projectRoot) => { const validationFailures = []; const validationWarnings = []; - // Attempt to validate schema compilation, and print any errors + // Attempt to validate schema compilation, and print any errors if an override schema path was not presented (in which case this will fail) try { - await context.amplify.executeProviderUtils(context, 'awscloudformation', 'compileSchema', { - noConfig: true, - forceCompile: true, - dryRun: true, - disableResolverOverrides: true, - }); + if (!hasModelSchemaPathParam(context)) { + await context.amplify.executeProviderUtils(context, 'awscloudformation', 'compileSchema', { + noConfig: true, + forceCompile: true, + dryRun: true, + disableResolverOverrides: true, + }); + } } catch (err) { validationWarnings.push(err.toString()); } @@ -221,15 +250,15 @@ async function generateModels(context, generateOptions = null) { schema: loadSchema(apiResourcePath), directives: await getDirectives(context, apiResourcePath), target: modelgenFrontendToTargetMap[frontend], - generateIndexRules: readFeatureFlag('codegen.generateIndexRules'), - emitAuthProvider: readFeatureFlag('codegen.emitAuthProvider'), - useExperimentalPipelinedTransformer: readFeatureFlag('graphQLTransformer.useExperimentalPipelinedTransformer'), - transformerVersion: readNumericFeatureFlag('graphQLTransformer.transformerVersion'), - respectPrimaryKeyAttributesOnConnectionField: readFeatureFlag('graphQLTransformer.respectPrimaryKeyAttributesOnConnectionField'), - improvePluralization: readFeatureFlag('graphQLTransformer.improvePluralization'), - generateModelsForLazyLoadAndCustomSelectionSet: readFeatureFlag('codegen.generateModelsForLazyLoadAndCustomSelectionSet'), - addTimestampFields: readFeatureFlag('codegen.addTimestampFields'), - handleListNullabilityTransparently: readFeatureFlag('codegen.handleListNullabilityTransparently'), + generateIndexRules: readFeatureFlag(context, 'codegen.generateIndexRules'), + emitAuthProvider: readFeatureFlag(context, 'codegen.emitAuthProvider'), + useExperimentalPipelinedTransformer: readFeatureFlag(context, 'graphQLTransformer.useExperimentalPipelinedTransformer'), + transformerVersion: readNumericFeatureFlag(context, 'graphQLTransformer.transformerVersion'), + respectPrimaryKeyAttributesOnConnectionField: readFeatureFlag(context, 'graphQLTransformer.respectPrimaryKeyAttributesOnConnectionField'), + improvePluralization: readFeatureFlag(context, 'graphQLTransformer.improvePluralization'), + generateModelsForLazyLoadAndCustomSelectionSet: readFeatureFlag(context, 'codegen.generateModelsForLazyLoadAndCustomSelectionSet'), + addTimestampFields: readFeatureFlag(context, 'codegen.addTimestampFields'), + handleListNullabilityTransparently: readFeatureFlag(context, 'codegen.handleListNullabilityTransparently'), }); if (writeToDisk) { @@ -253,6 +282,9 @@ async function generateModels(context, generateOptions = null) { * @returns {!string} the graphql string for all schema files found */ function loadSchema(apiResourcePath) { + if (fs.lstatSync(apiResourcePath).isFile()) { + return fs.readFileSync(apiResourcePath, 'utf8'); + } const schemaFilePath = path.join(apiResourcePath, 'schema.graphql'); const schemaDirectory = path.join(apiResourcePath, 'schema'); if (fs.pathExistsSync(schemaFilePath)) { @@ -269,11 +301,10 @@ function loadSchema(apiResourcePath) { /** * Retrieve the model output path for the given project configuration - * @param {!AmplifyContext} context the amplify runtime context + * @param {any} projectConfig the amplify runtime context * @returns the model output path, relative to the project root */ -function getModelOutputPath(context) { - const projectConfig = context.amplify.getProjectConfig(); +function getModelOutputPath(projectConfig) { switch (projectConfig.frontend) { case 'javascript': return path.join( @@ -301,20 +332,26 @@ function getModelOutputPath(context) { * @returns once eslint side effecting is complete */ function generateEslintIgnore(context) { - const projectConfig = context.amplify.getProjectConfig(); + let projectConfig; + let projectPath; + try { + projectConfig = context.amplify.getProjectConfig(); + projectPath = pathManager.findProjectRoot(); + } catch (_) { + return; + } if (projectConfig.frontend !== 'javascript') { return; } - const projectPath = pathManager.findProjectRoot(); if (!projectPath) { return; } const eslintIgnorePath = path.join(projectPath, '.eslintignore'); - const modelFolder = path.join(getModelOutputPath(context), 'models'); + const modelFolder = path.join(getModelOutputPath(projectConfig), 'models'); if (!fs.existsSync(eslintIgnorePath)) { fs.writeFileSync(eslintIgnorePath, modelFolder); diff --git a/packages/amplify-codegen/src/utils/getModelSchemaPathParam.js b/packages/amplify-codegen/src/utils/getModelSchemaPathParam.js new file mode 100644 index 000000000..861a65e43 --- /dev/null +++ b/packages/amplify-codegen/src/utils/getModelSchemaPathParam.js @@ -0,0 +1,35 @@ +const path = require('path'); + +const modelSchemaParamKey = 'model-schema'; +/** + * Retrieve the specified model schema path parameter, returning as an absolute path. + * @param {!import('@aws-amplify/amplify-cli-core').$TSContext} context the CLI invocation context + * @returns {string | null} the absolute path to the model schema path + */ +const getModelSchemaPathParam = (context) => { + const modelSchemaPathParam = context.parameters?.options?.[modelSchemaParamKey]; + if ( !modelSchemaPathParam ) { + return null; + } + let projectRoot; + try { + context.amplify.getProjectMeta(); + projectRoot = context.amplify.getEnvInfo().projectPath; + } catch (e) { + projectRoot = process.cwd(); + } + return path.isAbsolute(modelSchemaPathParam) ? modelSchemaPathParam : path.join(projectRoot, modelSchemaPathParam); +}; + +/** + * Retrieve whether or not a model schema path param was specified during invocation. + * @param {!import('@aws-amplify/amplify-cli-core').$TSContext} context the CLI invocation context + * @returns {!boolean} whether or not a model schema path param is specified via the CLI + */ +const hasModelSchemaPathParam = (context) => context?.parameters?.options + && Object.keys(context.parameters.options).find((optionKey) => optionKey === modelSchemaParamKey) !== undefined; + +module.exports = { + getModelSchemaPathParam, + hasModelSchemaPathParam, +}; diff --git a/packages/amplify-codegen/src/utils/getOutputDirParam.js b/packages/amplify-codegen/src/utils/getOutputDirParam.js index 3f9f23e2f..8a7577bdf 100644 --- a/packages/amplify-codegen/src/utils/getOutputDirParam.js +++ b/packages/amplify-codegen/src/utils/getOutputDirParam.js @@ -1,11 +1,12 @@ const path = require('path'); +const getProjectRoot = require('./getProjectRoot'); /** * Retrieve the output directory parameter from the command line. Throws on invalid value, * or if isRequired is set and the flag isn't in the options. Returns null on optional and not defined. - * @param context the CLI invocation context - * @param isRequired whether or not the flag is required - * @returns the absolute path to the output directory + * @param {!import('@aws-amplify/amplify-cli-core').$TSContext} context the CLI invocation context + * @param {!boolean} isRequired whether or not the flag is required + * @returns {!string} the absolute path to the output directory */ function getOutputDirParam(context, isRequired) { const outputDirParam = context.parameters?.options?.['output-dir']; @@ -15,7 +16,7 @@ function getOutputDirParam(context, isRequired) { if ( !outputDirParam ) { return null; } - return path.isAbsolute(outputDirParam) ? outputDirParam : path.join(context.amplify.getEnvInfo().projectPath, outputDirParam); + return path.isAbsolute(outputDirParam) ? outputDirParam : path.join(getProjectRoot(context), outputDirParam); } module.exports = getOutputDirParam; diff --git a/packages/amplify-codegen/src/utils/getProjectRoot.js b/packages/amplify-codegen/src/utils/getProjectRoot.js new file mode 100644 index 000000000..c142b5717 --- /dev/null +++ b/packages/amplify-codegen/src/utils/getProjectRoot.js @@ -0,0 +1,14 @@ +/** + * Find the project root. + * @param {!import('@aws-amplify/amplify-cli-core').$TSContext} context the amplify runtime context + * @returns {!string} the project root, or cwd + */ +const getProjectRoot = (context) => { + try { + return context.amplify.getEnvInfo().projectPath; + } catch (_) { + return process.cwd(); + } + }; + +module.exports = getProjectRoot;