UNPKG

graphql-transformer-core

Version:

A framework to transform from GraphQL SDL to AWS cloudFormation.

286 lines (285 loc) • 17.8 kB
"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 }; }; 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