graphql-transformer-core
Version:
A framework to transform from GraphQL SDL to AWS cloudFormation.
286 lines (285 loc) • 17.8 kB
JavaScript
;
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 };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.cantRemoveTableAfterCreation = exports.cantHaveMoreThan500ResourcesRule = exports.getCantRemoveLSILater = exports.getCantEditLSIKeySchemaRule = exports.cantMutateMultipleGSIAtUpdateTimeRule = exports.cantBatchMutateGSIAtUpdateTimeRule = exports.cantAddAndRemoveGSIAtSameTimeRule = exports.cantEditGSIKeySchemaRule = exports.getCantAddLSILaterRule = exports.getCantEditKeySchemaRule = exports.sanityCheckDiffs = exports.sanityCheckProject = void 0;
const path = __importStar(require("path"));
const fs = __importStar(require("fs-extra"));
const lodash_1 = __importDefault(require("lodash"));
const amplify_cli_core_1 = require("@aws-amplify/amplify-cli-core");
const deep_diff_1 = require("deep-diff");
const errors_1 = require("../errors");
const __1 = require("..");
const fileUtils_1 = require("./fileUtils");
const sanityCheckProject = async (currentCloudBackendDir, buildDirectory, rootStackName, diffRules, projectRule) => {
const cloudBackendDirectoryExists = fs.existsSync(currentCloudBackendDir);
const buildDirectoryExists = fs.existsSync(buildDirectory);
if (cloudBackendDirectoryExists && buildDirectoryExists) {
const current = await loadDiffableProject(currentCloudBackendDir, rootStackName);
const next = await loadDiffableProject(buildDirectory, rootStackName);
const diffs = (0, deep_diff_1.diff)(current, next);
(0, exports.sanityCheckDiffs)(diffs, current, next, diffRules, projectRule);
}
};
exports.sanityCheckProject = sanityCheckProject;
const sanityCheckDiffs = (diffs, current, next, diffRules, projectRules) => {
if (diffs) {
for (const diff of diffs) {
for (const rule of diffRules) {
rule(diff, current, next);
}
}
for (const projectRule of projectRules) {
projectRule(diffs, current, next);
}
}
};
exports.sanityCheckDiffs = sanityCheckDiffs;
const getCantEditKeySchemaRule = (iterativeUpdatesEnabled = false) => {
const cantEditKeySchemaRule = (diff) => {
const sortKeyAddedOrRemoved = diff.kind === 'A' && diff.path.length === 6 && diff.path[5] === 'KeySchema' && diff.index === 1;
const keySchemaModified = diff.kind === 'E' && diff.path.length === 8 && diff.path[5] === 'KeySchema';
if (sortKeyAddedOrRemoved || keySchemaModified) {
const stackName = path.basename(diff.path[1], '.json');
const tableName = diff.path[3];
if (iterativeUpdatesEnabled) {
throw new errors_1.DestructiveMigrationError('Editing the primary key of a model requires replacement of the underlying DynamoDB table.', [], [tableName]);
}
throw new errors_1.InvalidMigrationError(`Attempting to edit the key schema of the ${tableName} table in the ${stackName} stack. `, 'Adding a primary @key directive to an existing @model. ', 'Remove the @key directive or provide a name e.g @key(name: "ByStatus", fields: ["status"]).');
}
};
return cantEditKeySchemaRule;
};
exports.getCantEditKeySchemaRule = getCantEditKeySchemaRule;
const getCantAddLSILaterRule = (iterativeUpdatesEnabled = false) => {
const cantAddLSILaterRule = (diff) => {
if ((diff.kind === 'N' && diff.path.length === 6 && diff.path[5] === 'LocalSecondaryIndexes') ||
(diff.kind === 'A' && diff.path.length === 6 && diff.path[5] === 'LocalSecondaryIndexes' && diff.item.kind === 'N')) {
const stackName = path.basename(diff.path[1], '.json');
const tableName = diff.path[3];
if (iterativeUpdatesEnabled) {
throw new errors_1.DestructiveMigrationError('Adding an LSI to a model requires replacement of the underlying DynamoDB table.', [], [tableName]);
}
throw new errors_1.InvalidMigrationError(`Attempting to add a local secondary index to the ${tableName} table in the ${stackName} stack. ` +
'Local secondary indexes must be created when the table is created.', "Adding a @key directive where the first field in 'fields' is the same as the first field in the 'fields' of the primary @key.", "Change the first field in 'fields' such that a global secondary index is created or delete and recreate the model.");
}
};
return cantAddLSILaterRule;
};
exports.getCantAddLSILaterRule = getCantAddLSILaterRule;
const cantEditGSIKeySchemaRule = (diff, currentBuild, nextBuild) => {
const throwError = (indexName, stackName, tableName) => {
throw new errors_1.InvalidGSIMigrationError(`Attempting to edit the global secondary index ${indexName} on the ${tableName} table in the ${stackName} stack. `, 'The key schema of a global secondary index cannot be changed after being deployed.', 'If using @key, first add a new @key, run `amplify push`, ' +
'and then remove the old @key. If using @connection, first remove the @connection, run `amplify push`, ' +
'and then add the new @connection with the new configuration.');
};
if ((diff.kind === 'E' && diff.path.length === 10 && diff.path[5] === 'GlobalSecondaryIndexes' && diff.path[7] === 'KeySchema') ||
(diff.kind === 'A' && diff.path.length === 8 && diff.path[5] === 'GlobalSecondaryIndexes' && diff.path[7] === 'KeySchema')) {
const pathToGSIs = diff.path.slice(0, 6);
const oldIndexes = lodash_1.default.get(currentBuild, pathToGSIs);
const newIndexes = lodash_1.default.get(nextBuild, pathToGSIs);
const oldIndexesDiffable = lodash_1.default.keyBy(oldIndexes, 'IndexName');
const newIndexesDiffable = lodash_1.default.keyBy(newIndexes, 'IndexName');
const innerDiffs = (0, deep_diff_1.diff)(oldIndexesDiffable, newIndexesDiffable) || [];
for (const innerDiff of innerDiffs) {
if (innerDiff.kind === 'E' && innerDiff.path.length > 2 && innerDiff.path[1] === 'KeySchema') {
const indexName = innerDiff.path[0];
const stackName = path.basename(diff.path[1], '.json');
const tableName = diff.path[3];
throwError(indexName, stackName, tableName);
}
else if (innerDiff.kind === 'A' && innerDiff.path.length === 2 && innerDiff.path[1] === 'KeySchema') {
const indexName = innerDiff.path[0];
const stackName = path.basename(diff.path[1], '.json');
const tableName = diff.path[3];
throwError(indexName, stackName, tableName);
}
}
}
};
exports.cantEditGSIKeySchemaRule = cantEditGSIKeySchemaRule;
const cantAddAndRemoveGSIAtSameTimeRule = (diff, currentBuild, nextBuild) => {
const throwError = (stackName, tableName) => {
throw new errors_1.InvalidGSIMigrationError(`Attempting to add and remove a global secondary index at the same time on the ${tableName} table in the ${stackName} stack. `, 'You may only change one global secondary index in a single CloudFormation stack update. ', 'If using @key, change one @key at a time. ' +
'If using @connection, add the new @connection, run `amplify push`, ' +
'and then remove the new @connection with the new configuration.');
};
if (diff.kind === 'E' &&
diff.path.length > 6 &&
diff.path[5] === 'GlobalSecondaryIndexes') {
const pathToGSIs = diff.path.slice(0, 6);
const oldIndexes = lodash_1.default.get(currentBuild, pathToGSIs);
const newIndexes = lodash_1.default.get(nextBuild, pathToGSIs);
const oldIndexesDiffable = lodash_1.default.keyBy(oldIndexes, 'IndexName');
const newIndexesDiffable = lodash_1.default.keyBy(newIndexes, 'IndexName');
const innerDiffs = (0, deep_diff_1.diff)(oldIndexesDiffable, newIndexesDiffable) || [];
let sawDelete = false;
let sawNew = false;
for (const diff of innerDiffs) {
if (diff.path.length === 1 && diff.kind === 'D') {
sawDelete = true;
}
if (diff.path.length === 1 && diff.kind === 'N') {
sawNew = true;
}
}
if (sawDelete && sawNew) {
const stackName = path.basename(diff.path[1], '.json');
const tableName = diff.path[3];
throwError(stackName, tableName);
}
}
};
exports.cantAddAndRemoveGSIAtSameTimeRule = cantAddAndRemoveGSIAtSameTimeRule;
const cantBatchMutateGSIAtUpdateTimeRule = (diff, currentBuild, nextBuild) => {
if ((diff.kind === 'D' || diff.kind === 'N') && diff.path.length === 6 && diff.path.slice(-1)[0] === 'GlobalSecondaryIndexes') {
const tableName = diff.path[3];
const stackName = diff.path[1];
throw new errors_1.InvalidGSIMigrationError(`Attempting to add and remove a global secondary index at the same time on the ${tableName} table in the ${stackName} stack. `, 'You may only change one global secondary index in a single CloudFormation stack update. ', 'If using @key, change one @key at a time. ' +
'If using @connection, add the new @connection, run `amplify push`, ' +
'and then remove the new @connection with the new configuration.');
}
};
exports.cantBatchMutateGSIAtUpdateTimeRule = cantBatchMutateGSIAtUpdateTimeRule;
const cantMutateMultipleGSIAtUpdateTimeRule = (diffs, currentBuild, nextBuild) => {
const throwError = (stackName, tableName) => {
throw new errors_1.InvalidGSIMigrationError(`Attempting to mutate more than 1 global secondary index at the same time on the ${tableName} table in the ${stackName} stack. `, 'You may only mutate one global secondary index in a single CloudFormation stack update. ', 'If using @key, include one @key at a time. ' +
'If using @connection, just add one new @connection which is using @key, run `amplify push`, ');
};
if (diffs) {
const seenTables = new Set();
for (const diff of diffs) {
if (diff.kind === 'A' &&
diff.path.length >= 6 &&
diff.path.slice(-1)[0] === 'GlobalSecondaryIndexes') {
const diffTableName = diff.path[3];
if ((diff.item.kind === 'N' || diff.item.kind === 'D') && !seenTables.has(diffTableName)) {
seenTables.add(diffTableName);
}
else if (seenTables.has(diffTableName)) {
const stackName = path.basename(diff.path[1], '.json');
throwError(stackName, diffTableName);
}
}
}
}
};
exports.cantMutateMultipleGSIAtUpdateTimeRule = cantMutateMultipleGSIAtUpdateTimeRule;
const getCantEditLSIKeySchemaRule = (iterativeUpdatesEnabled = false) => {
const cantEditLSIKeySchemaRule = (diff, currentBuild, nextBuild) => {
if (diff.kind === 'E' &&
diff.path.length === 10 &&
diff.path[5] === 'LocalSecondaryIndexes' &&
diff.path[7] === 'KeySchema') {
const pathToGSIs = diff.path.slice(0, 6);
const oldIndexes = lodash_1.default.get(currentBuild, pathToGSIs);
const newIndexes = lodash_1.default.get(nextBuild, pathToGSIs);
const oldIndexesDiffable = lodash_1.default.keyBy(oldIndexes, 'IndexName');
const newIndexesDiffable = lodash_1.default.keyBy(newIndexes, 'IndexName');
const innerDiffs = (0, deep_diff_1.diff)(oldIndexesDiffable, newIndexesDiffable) || [];
for (const innerDiff of innerDiffs) {
if (innerDiff.kind === 'E' && innerDiff.path.length > 2 && innerDiff.path[1] === 'KeySchema') {
const indexName = innerDiff.path[0];
const stackName = path.basename(diff.path[1], '.json');
const tableName = diff.path[3];
if (iterativeUpdatesEnabled) {
throw new errors_1.DestructiveMigrationError('Editing an LSI requires replacement of the underlying DynamoDB table.', [], [tableName]);
}
throw new errors_1.InvalidMigrationError(`Attempting to edit the local secondary index ${indexName} on the ${tableName} table in the ${stackName} stack. `, 'The key schema of a local secondary index cannot be changed after being deployed.', 'When enabling new access patterns you should: 1. Add a new @key 2. run amplify push ' +
'3. Verify the new access pattern and remove the old @key.');
}
}
}
};
return cantEditLSIKeySchemaRule;
};
exports.getCantEditLSIKeySchemaRule = getCantEditLSIKeySchemaRule;
const getCantRemoveLSILater = (iterativeUpdatesEnabled = false) => {
const cantRemoveLSILater = (diff, currentBuild, nextBuild) => {
const throwError = (stackName, tableName) => {
if (iterativeUpdatesEnabled) {
throw new errors_1.DestructiveMigrationError('Removing an LSI requires replacement of the underlying DynamoDB table.', [], [tableName]);
}
throw new errors_1.InvalidMigrationError(`Attempting to remove a local secondary index on the ${tableName} table in the ${stackName} stack.`, 'A local secondary index cannot be removed after deployment.', 'In order to remove the local secondary index you need to delete or rename the table.');
};
if (diff.kind === 'D' && diff.lhs && diff.path.length === 6 && diff.path[5] === 'LocalSecondaryIndexes') {
const tableName = diff.path[3];
const stackName = path.basename(diff.path[1], '.json');
throwError(stackName, tableName);
}
if (diff.kind === 'A' && diff.item.kind === 'D' && diff.path.length === 6 && diff.path[5] === 'LocalSecondaryIndexes') {
const tableName = diff.path[3];
const stackName = path.basename(diff.path[1], '.json');
throwError(stackName, tableName);
}
};
return cantRemoveLSILater;
};
exports.getCantRemoveLSILater = getCantRemoveLSILater;
const cantHaveMoreThan500ResourcesRule = (diffs, currentBuild, nextBuild) => {
const stackKeys = Object.keys(nextBuild.stacks);
for (const stackName of stackKeys) {
const stack = nextBuild.stacks[stackName];
if (stack && stack.Resources && Object.keys(stack.Resources).length > 500) {
throw new errors_1.InvalidMigrationError(`The ${stackName} stack defines more than 500 resources.`, 'CloudFormation templates may contain at most 500 resources.', 'If the stack is a custom stack, break the stack up into multiple files in stacks/. ' +
'If the stack was generated, you have hit a limit and can use the StackMapping argument in ' +
`${__1.TRANSFORM_CONFIG_FILE_NAME} to fine tune how resources are assigned to
stacks.Follow the instruction in this document to divide resources into additional AWS CloudFormation files to overcome the resource count limit:
https://docs.amplify.aws/cli/graphql/override/#place-appsync-resolvers-in-custom-named-stacks`);
}
}
};
exports.cantHaveMoreThan500ResourcesRule = cantHaveMoreThan500ResourcesRule;
const cantRemoveTableAfterCreation = (_, currentBuild, nextBuild) => {
const getTableLogicalIds = (proj) => Object.values(proj.stacks)
.flatMap((stack) => Object.entries(stack.Resources))
.filter(([_, resource]) => resource.Type === 'AWS::DynamoDB::Table')
.map(([name]) => name);
const currentTables = getTableLogicalIds(currentBuild);
const nextTables = getTableLogicalIds(nextBuild);
const removedTables = currentTables.filter((currModel) => !nextTables.includes(currModel));
if (removedTables.length > 0) {
throw new errors_1.DestructiveMigrationError('Removing a model from the GraphQL schema will also remove the underlying DynamoDB table.', removedTables, []);
}
};
exports.cantRemoveTableAfterCreation = cantRemoveTableAfterCreation;
const loadDiffableProject = async (path, rootStackName) => {
const project = await (0, fileUtils_1.readFromPath)(path);
const currentStacks = project.stacks || {};
const diffableProject = {
stacks: {},
root: {},
};
for (const key of Object.keys(currentStacks)) {
diffableProject.stacks[key] = amplify_cli_core_1.JSONUtilities.parse(project.stacks[key]);
}
if (project[rootStackName]) {
diffableProject.root = amplify_cli_core_1.JSONUtilities.parse(project[rootStackName]);
}
return diffableProject;
};
//# sourceMappingURL=sanity-check.js.map