Skip to content

Commit

Permalink
feat: support modelgen without an amplify backend
Browse files Browse the repository at this point in the history
  • Loading branch information
alharris-at committed Sep 12, 2023
1 parent 9337382 commit 3fdfdff
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 50 deletions.
129 changes: 83 additions & 46 deletions packages/amplify-codegen/src/commands/models.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -34,45 +36,55 @@ const modelgenFrontendToTargetMap = {
introspection: 'introspection',
};

/**
* Return feature flag override values from the cli in the format --feature-flag:<feature flag name> <feature flag value>
* @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;
}
};

Expand All @@ -82,6 +94,11 @@ const getProjectRoot = (context) => {
* @returns {!Promise<string | null>} 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',
Expand Down Expand Up @@ -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.')
}
};

/**
Expand All @@ -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;
};

Expand All @@ -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());
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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)) {
Expand All @@ -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(
Expand Down Expand Up @@ -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);
Expand Down
35 changes: 35 additions & 0 deletions packages/amplify-codegen/src/utils/getModelSchemaPathParam.js
Original file line number Diff line number Diff line change
@@ -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,
};
9 changes: 5 additions & 4 deletions packages/amplify-codegen/src/utils/getOutputDirParam.js
Original file line number Diff line number Diff line change
@@ -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'];
Expand All @@ -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;
14 changes: 14 additions & 0 deletions packages/amplify-codegen/src/utils/getProjectRoot.js
Original file line number Diff line number Diff line change
@@ -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;

0 comments on commit 3fdfdff

Please sign in to comment.