amplify-cli-core
Version:
Amplify CLI Core
663 lines • 29.7 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
var _a;
Object.defineProperty(exports, "__esModule", { value: true });
exports.FeatureFlags = void 0;
const ajv_1 = __importDefault(require("ajv"));
const ci_info_1 = require("ci-info");
const fs = __importStar(require("fs-extra"));
const lodash_1 = __importDefault(require("lodash"));
const path = __importStar(require("path"));
const amplify_error_1 = require("../errors/amplify-error");
const amplify_fault_1 = require("../errors/amplify-fault");
const jsonUtilities_1 = require("../jsonUtilities");
const state_manager_1 = require("../state-manager");
const featureFlagEnvironmentProvider_1 = require("./featureFlagEnvironmentProvider");
const featureFlagFileProvider_1 = require("./featureFlagFileProvider");
class FeatureFlags {
constructor(environmentProvider, projectPath, useNewDefaults) {
this.environmentProvider = environmentProvider;
this.projectPath = projectPath;
this.useNewDefaults = useNewDefaults;
this.registrations = new Map();
this.effectiveFlags = {};
this.newProjectDefaults = {};
this.existingProjectDefaults = {};
this.getValue = (flagName, type) => {
var _b;
if (!flagName) {
throw new Error("'flagName' argument is required");
}
let value;
const parts = flagName.toLowerCase().split('.');
if (parts.length !== 2) {
throw new Error(`Invalid flagName value: '${flagName}'`);
}
const sectionRegistration = this.registrations.get(parts[0]);
if (!sectionRegistration) {
throw new Error(`Section '${parts[0]}' is not registered in feature provider`);
}
const flagRegistrationEntry = sectionRegistration === null || sectionRegistration === void 0 ? void 0 : sectionRegistration.find((flag) => flag.name === parts[1]);
if (!flagRegistrationEntry) {
throw new Error(`Flag '${parts[1]}' within '${parts[0]}' is not registered in feature provider`);
}
if ((flagRegistrationEntry === null || flagRegistrationEntry === void 0 ? void 0 : flagRegistrationEntry.type) !== type) {
throw new Error(`'${flagName}' is a ${flagRegistrationEntry.type} type, not ${type}`);
}
value = (_b = this.effectiveFlags[parts[0]]) === null || _b === void 0 ? void 0 : _b[parts[1]];
if (value === undefined && flagRegistrationEntry) {
if (this.useNewDefaults) {
value = flagRegistrationEntry.defaultValueForNewProjects;
}
else {
value = flagRegistrationEntry.defaultValueForExistingProjects;
}
}
return value !== null && value !== void 0 ? value : false;
};
this.buildJSONSchemaFromRegistrations = () => [...this.registrations.entries()].reduce((schema, r) => {
var _b;
const currentSection = ((_b = schema.properties[r[0].toLowerCase()]) !== null && _b !== void 0 ? _b : {
type: 'object',
additionalProperties: false,
});
currentSection.properties = r[1].reduce((p, fr) => {
p[fr.name.toLowerCase()] = {
type: fr.type,
default: fr.defaultValueForNewProjects,
};
return p;
}, {});
schema.properties[r[0].toLowerCase()] = currentSection;
return schema;
}, {
$schema: 'http://json-schema.org/draft-07/schema#',
type: 'object',
additionalProperties: false,
properties: {},
});
this.buildDefaultValues = () => {
this.newProjectDefaults = [...this.registrations.entries()].reduce((result, r) => {
const nest = r[1].reduce((p, fr) => {
p[fr.name] = fr.defaultValueForNewProjects;
return p;
}, {});
result[r[0]] = {
...result[r[0]],
...nest,
};
return result;
}, {});
this.existingProjectDefaults = [...this.registrations.entries()].reduce((result, r) => {
const nest = r[1].reduce((p, fr) => {
p[fr.name] = fr.defaultValueForExistingProjects;
return p;
}, {});
result[r[0]] = {
...result[r[0]],
...nest,
};
return result;
}, {});
};
this.validateFlags = (allFlags) => {
const schema = this.buildJSONSchemaFromRegistrations();
const ajv = new ajv_1.default({
allErrors: true,
});
const schemaValidate = ajv.compile(schema);
const validator = (flags) => {
const valid = schemaValidate(flags);
if (!valid && schemaValidate.errors) {
const unknownFlags = [];
const otherErrors = [];
schemaValidate.errors.forEach((error) => {
var _b;
if (error.keyword === 'additionalProperties') {
const additionalProperty = (_b = error.params) === null || _b === void 0 ? void 0 : _b.additionalProperty;
let flagName = error.dataPath.length > 0 && error.dataPath[0] === '.' ? `${error.dataPath.slice(1)}.` : '';
if (additionalProperty) {
if (flags[flagName.replace('.', '')][additionalProperty] === false) {
return;
}
flagName += additionalProperty;
}
if (flagName.length > 0) {
unknownFlags.push(flagName);
}
}
else {
const errorMessage = error.dataPath.length > 0 && error.dataPath[0] === '.'
? `${error.dataPath.slice(1)}: ${error.message}`
: `${error.dataPath}: ${error.message}`;
otherErrors.push(errorMessage);
}
});
if (unknownFlags.length > 0 || otherErrors.length > 0) {
throw new amplify_error_1.AmplifyError('FeatureFlagsValidationError', {
message: 'Invalid feature flag configuration',
details: (unknownFlags.length > 0
? `These feature flags are defined in the "amplify/cli.json" configuration file and are unknown to the currently running Amplify CLI:\n${unknownFlags
.map((el) => `- ${el}`)
.join(',\n')}\n`
: '') +
(otherErrors.length > 0
? `The following feature flags have validation errors:\n${otherErrors.map((el) => `- ${el}`).join(',\n')}`
: ''),
resolution: `This issue likely happens when the project has been pushed with a newer version of Amplify CLI, try updating to a newer version.${ci_info_1.isCI ? '\nEnsure that the CI/CD pipeline is not using an older or pinned down version of Amplify CLI.' : ''}`,
link: 'https://docs.amplify.aws/cli/reference/feature-flags',
});
}
}
};
const featureFlagsValidator = (features) => {
validator(features.project);
Object.keys(features.environments).forEach((env) => {
validator(features.environments[env]);
});
};
allFlags.forEach((flagItem) => {
featureFlagsValidator(flagItem.flags);
});
};
this.transformEnvFlags = (features) => {
const convertValue = (section, flagName, value) => {
const sectionRegistration = this.registrations.get(section);
if (!sectionRegistration) {
throw new Error(`Section '${section}' is not registered in feature provider`);
}
const flagRegistrationEntry = sectionRegistration.find((flag) => flag.name === flagName);
if (!flagRegistrationEntry) {
throw new Error(`Flag '${flagName}' within '${section}' is not registered in feature provider`);
}
switch (flagRegistrationEntry.type) {
case 'boolean':
if (value === 'true') {
return true;
}
if (value === 'false') {
return false;
}
throw new Error(`Invalid boolean value: '${value}' for '${flagName}' in section '${section}'`);
case 'number': {
const n = Number.parseInt(value, 10);
if (!Number.isNaN(n)) {
return n;
}
throw new Error(`Invalid number value: '${value}' for '${flagName}' in section '${section}'`);
}
default:
throw new Error(`Invalid number value: ${value} for ${flagName}`);
}
};
const mapFeatureFlagEntry = (input) => Object.keys(input).reduce((result, section) => {
const sourceObject = input[section];
result[section] = Object.keys(sourceObject).reduce((resultFlag, flagName) => {
const sourceValue = sourceObject[flagName];
resultFlag[flagName] = convertValue(section, flagName, sourceValue);
return resultFlag;
}, {});
return result;
}, {});
features.project = mapFeatureFlagEntry(features.project);
Object.keys(features.environments).forEach((env) => {
features.environments[env] = mapFeatureFlagEntry(features.environments[env]);
});
return features;
};
this.loadValues = async () => {
var _b, _c;
const fileFlags = await this.fileValueProvider.load();
const envFlags = this.transformEnvFlags(await this.envValueProvider.load());
this.validateFlags([
{
name: 'File',
flags: fileFlags,
},
{
name: 'Environment',
flags: envFlags,
},
]);
this.buildDefaultValues();
this.effectiveFlags = lodash_1.default.merge(this.useNewDefaults ? this.newProjectDefaults : this.existingProjectDefaults, fileFlags.project, (_b = fileFlags.environments[this.environmentProvider.getCurrentEnvName()]) !== null && _b !== void 0 ? _b : {}, envFlags.project, (_c = envFlags.environments[this.environmentProvider.getCurrentEnvName()]) !== null && _c !== void 0 ? _c : {});
};
this.registerFlag = (section, flags) => {
var _b;
if (!section) {
throw new Error("'section' argument is required");
}
if (!flags) {
throw new Error("'flags' argument is required");
}
const newFlags = (_b = this.registrations.get(section.toLowerCase())) !== null && _b !== void 0 ? _b : new Array();
flags.forEach((flag) => {
if (!flag.name || flag.name.trim().length === 0) {
throw new Error('Flag does not have a name specified');
}
if (newFlags.find((f) => f.name === flag.name.toLowerCase())) {
throw new Error(`Flag with name: '${flag.name}' is already registered in section: '${section}'`);
}
flag.name = flag.name.toLowerCase();
newFlags.push(flag);
});
this.registrations.set(section.toLowerCase(), newFlags);
};
this.registerFlags = () => {
this.registerFlag('graphQLTransformer', [
{
name: 'addMissingOwnerFields',
type: 'boolean',
defaultValueForExistingProjects: false,
defaultValueForNewProjects: true,
},
{
name: 'improvePluralization',
type: 'boolean',
defaultValueForExistingProjects: false,
defaultValueForNewProjects: false,
},
{
name: 'validateTypeNameReservedWords',
type: 'boolean',
defaultValueForExistingProjects: true,
defaultValueForNewProjects: true,
},
{
name: 'useExperimentalPipelinedTransformer',
type: 'boolean',
defaultValueForExistingProjects: false,
defaultValueForNewProjects: true,
},
{
name: 'enableIterativeGSIUpdates',
type: 'boolean',
defaultValueForExistingProjects: false,
defaultValueForNewProjects: true,
},
{
name: 'secondaryKeyAsGSI',
type: 'boolean',
defaultValueForExistingProjects: false,
defaultValueForNewProjects: true,
},
{
name: 'skipOverrideMutationInputTypes',
type: 'boolean',
defaultValueForExistingProjects: false,
defaultValueForNewProjects: true,
},
{
name: 'transformerVersion',
type: 'number',
defaultValueForExistingProjects: 1,
defaultValueForNewProjects: 2,
},
{
name: 'suppressSchemaMigrationPrompt',
type: 'boolean',
defaultValueForExistingProjects: true,
defaultValueForNewProjects: true,
},
{
name: 'securityEnhancementNotification',
type: 'boolean',
defaultValueForExistingProjects: true,
defaultValueForNewProjects: false,
},
{
name: 'showFieldAuthNotification',
type: 'boolean',
defaultValueForExistingProjects: true,
defaultValueForNewProjects: false,
},
{
name: 'useSubUsernameForDefaultIdentityClaim',
type: 'boolean',
defaultValueForExistingProjects: true,
defaultValueForNewProjects: true,
},
{
name: 'useFieldNameForPrimaryKeyConnectionField',
type: 'boolean',
defaultValueForExistingProjects: false,
defaultValueForNewProjects: false,
},
{
name: 'enableAutoIndexQueryNames',
type: 'boolean',
defaultValueForExistingProjects: false,
defaultValueForNewProjects: true,
},
{
name: 'respectPrimaryKeyAttributesOnConnectionField',
type: 'boolean',
defaultValueForExistingProjects: false,
defaultValueForNewProjects: true,
},
{
name: 'shouldDeepMergeDirectiveConfigDefaults',
type: 'boolean',
defaultValueForExistingProjects: false,
defaultValueForNewProjects: false,
},
{
name: 'populateOwnerFieldForStaticGroupAuth',
type: 'boolean',
defaultValueForExistingProjects: false,
defaultValueForNewProjects: true,
},
]);
this.registerFlag('frontend-ios', [
{
name: 'enableXcodeIntegration',
type: 'boolean',
defaultValueForExistingProjects: false,
defaultValueForNewProjects: true,
},
]);
this.registerFlag('auth', [
{
name: 'enableCaseInsensitivity',
type: 'boolean',
defaultValueForExistingProjects: false,
defaultValueForNewProjects: true,
},
{
name: 'useInclusiveTerminology',
type: 'boolean',
defaultValueForExistingProjects: false,
defaultValueForNewProjects: true,
},
{
name: 'breakCircularDependency',
type: 'boolean',
defaultValueForExistingProjects: false,
defaultValueForNewProjects: true,
},
{
name: 'forceAliasAttributes',
type: 'boolean',
defaultValueForExistingProjects: false,
defaultValueForNewProjects: false,
},
{
name: 'useEnabledMfas',
type: 'boolean',
defaultValueForExistingProjects: false,
defaultValueForNewProjects: true,
},
]);
this.registerFlag('codegen', [
{
name: 'useAppSyncModelgenPlugin',
type: 'boolean',
defaultValueForExistingProjects: false,
defaultValueForNewProjects: true,
},
{
name: 'useDocsGeneratorPlugin',
type: 'boolean',
defaultValueForExistingProjects: false,
defaultValueForNewProjects: true,
},
{
name: 'useTypesGeneratorPlugin',
type: 'boolean',
defaultValueForExistingProjects: false,
defaultValueForNewProjects: true,
},
{
name: 'cleanGeneratedModelsDirectory',
type: 'boolean',
defaultValueForExistingProjects: false,
defaultValueForNewProjects: true,
},
{
name: 'retainCaseStyle',
type: 'boolean',
defaultValueForExistingProjects: false,
defaultValueForNewProjects: true,
},
{
name: 'addTimestampFields',
type: 'boolean',
defaultValueForExistingProjects: false,
defaultValueForNewProjects: true,
},
{
name: 'handleListNullabilityTransparently',
type: 'boolean',
defaultValueForExistingProjects: false,
defaultValueForNewProjects: true,
},
{
name: 'emitAuthProvider',
type: 'boolean',
defaultValueForExistingProjects: false,
defaultValueForNewProjects: true,
},
{
name: 'generateIndexRules',
type: 'boolean',
defaultValueForExistingProjects: false,
defaultValueForNewProjects: true,
},
{
name: 'enableDartNullSafety',
type: 'boolean',
defaultValueForExistingProjects: false,
defaultValueForNewProjects: true,
},
{
name: 'generateModelsForLazyLoadAndCustomSelectionSet',
type: 'boolean',
defaultValueForExistingProjects: false,
defaultValueForNewProjects: false,
},
]);
this.registerFlag('appSync', [
{
name: 'generateGraphQLPermissions',
type: 'boolean',
defaultValueForExistingProjects: false,
defaultValueForNewProjects: true,
},
]);
this.registerFlag('latestRegionSupport', [
{
name: 'pinpoint',
type: 'number',
defaultValueForExistingProjects: 0,
defaultValueForNewProjects: 1,
},
{
name: 'translate',
type: 'number',
defaultValueForExistingProjects: 0,
defaultValueForNewProjects: 1,
},
{
name: 'transcribe',
type: 'number',
defaultValueForExistingProjects: 0,
defaultValueForNewProjects: 1,
},
{
name: 'rekognition',
type: 'number',
defaultValueForExistingProjects: 0,
defaultValueForNewProjects: 1,
},
{
name: 'textract',
type: 'number',
defaultValueForExistingProjects: 0,
defaultValueForNewProjects: 1,
},
{
name: 'comprehend',
type: 'number',
defaultValueForExistingProjects: 0,
defaultValueForNewProjects: 1,
},
]);
this.registerFlag('project', [
{
name: 'overrides',
type: 'boolean',
defaultValueForExistingProjects: true,
defaultValueForNewProjects: true,
},
]);
};
}
}
exports.FeatureFlags = FeatureFlags;
_a = FeatureFlags;
FeatureFlags.initialize = async (environmentProvider, useNewDefaults = false, additionalFlags) => {
var _b;
if (typeof jest === 'undefined' && FeatureFlags.instance) {
throw new Error('FeatureFlags can only be initialized once');
}
if (!environmentProvider) {
throw new Error("'environmentProvider' argument is required");
}
const projectPath = (_b = state_manager_1.pathManager.findProjectRoot()) !== null && _b !== void 0 ? _b : process.cwd();
await FeatureFlags.removeOriginalConfigFile(projectPath);
const instance = new FeatureFlags(environmentProvider, projectPath, useNewDefaults);
instance.registerFlags();
if (additionalFlags) {
Object.keys(additionalFlags).forEach((sectionName) => {
const flags = additionalFlags[sectionName];
instance.registerFlag(sectionName, flags);
});
}
instance.fileValueProvider = new featureFlagFileProvider_1.FeatureFlagFileProvider(environmentProvider, {
projectPath,
});
instance.envValueProvider = new featureFlagEnvironmentProvider_1.FeatureFlagEnvironmentProvider({
projectPath,
});
await instance.loadValues();
FeatureFlags.instance = instance;
};
FeatureFlags.ensureDefaultFeatureFlags = async (newProject) => {
FeatureFlags.ensureInitialized();
let config = state_manager_1.stateManager.getCLIJSON(FeatureFlags.instance.projectPath, undefined, {
throwIfNotExist: false,
preserveComments: true,
});
if (!config || !config.features) {
config = {
...(config !== null && config !== void 0 ? config : {}),
features: newProject ? FeatureFlags.getNewProjectDefaults() : FeatureFlags.getExistingProjectDefaults(),
};
state_manager_1.stateManager.setCLIJSON(FeatureFlags.instance.projectPath, config);
}
};
FeatureFlags.ensureFeatureFlag = async (featureFlagSection, featureFlagName) => {
var _b, _c;
FeatureFlags.ensureInitialized();
const config = state_manager_1.stateManager.getCLIJSON(FeatureFlags.instance.projectPath, undefined, {
throwIfNotExist: false,
preserveComments: true,
});
if (!(config === null || config === void 0 ? void 0 : config.features)) {
await FeatureFlags.ensureDefaultFeatureFlags(false);
}
else if (((_c = (_b = config.features) === null || _b === void 0 ? void 0 : _b[featureFlagSection]) === null || _c === void 0 ? void 0 : _c[featureFlagName]) === undefined) {
const features = FeatureFlags.getExistingProjectDefaults();
lodash_1.default.setWith(config, ['features', featureFlagSection, featureFlagName], features[featureFlagSection][featureFlagName]);
state_manager_1.stateManager.setCLIJSON(FeatureFlags.instance.projectPath, config);
}
};
FeatureFlags.getBoolean = (flagName) => {
FeatureFlags.ensureInitialized();
return FeatureFlags.instance.getValue(flagName, 'boolean');
};
FeatureFlags.getNumber = (flagName) => {
FeatureFlags.ensureInitialized();
return FeatureFlags.instance.getValue(flagName, 'number');
};
FeatureFlags.getEffectiveFlags = () => {
FeatureFlags.ensureInitialized();
return FeatureFlags.instance.effectiveFlags;
};
FeatureFlags.getNewProjectDefaults = () => {
FeatureFlags.ensureInitialized();
return FeatureFlags.instance.newProjectDefaults;
};
FeatureFlags.getExistingProjectDefaults = () => {
FeatureFlags.ensureInitialized();
return FeatureFlags.instance.existingProjectDefaults;
};
FeatureFlags.removeFeatureFlagConfiguration = async (removeProjectConfiguration, envNames) => {
FeatureFlags.ensureInitialized();
if (!envNames) {
throw new amplify_fault_1.AmplifyFault('ConfigurationFault', {
message: 'Environment names could not be loaded or were not provided.',
});
}
if (removeProjectConfiguration) {
const configFileName = state_manager_1.pathManager.getCLIJSONFilePath(FeatureFlags.instance.projectPath);
await fs.remove(configFileName);
}
for (const envName of envNames) {
const configFileName = state_manager_1.pathManager.getCLIJSONFilePath(FeatureFlags.instance.projectPath, envName);
await fs.remove(configFileName);
}
};
FeatureFlags.isInitialized = () => FeatureFlags.instance !== undefined;
FeatureFlags.reloadValues = async () => {
FeatureFlags.ensureInitialized();
await FeatureFlags.instance.loadValues();
};
FeatureFlags.removeOriginalConfigFile = async (projectPath) => {
const originalConfigFileName = 'amplify.json';
try {
if (!projectPath) {
return;
}
const originalConfigFilePath = path.join(projectPath, originalConfigFileName);
const configFileData = jsonUtilities_1.JSONUtilities.readJson(originalConfigFilePath, {
throwIfNotExist: false,
});
if ((configFileData === null || configFileData === void 0 ? void 0 : configFileData.features) !== undefined) {
fs.removeSync(originalConfigFilePath);
}
}
catch (_b) {
}
};
FeatureFlags.ensureInitialized = () => {
if (!FeatureFlags.instance) {
throw new Error('FeatureFlags is not initialized');
}
};
//# sourceMappingURL=featureFlags.js.map