/* eslint-disable no-loop-func */ "use strict"; const matchValue = require("match-value"); const mergeByTemplate = require("merge-by-template"); const mapObj = require("map-obj"); const createMergeMapper = require("../merge-map"); // FIXME: Move all the console.logs out of here, return some sort of action log instead? For display by whatever is applying/displaying the schema. Or is this not necessary as we will be separately handling the mutations anyway? // FIXME: Ensure that the actual migration handling code always looks at a generated schema *at the revision being processed* as a reference, not whatever the target revision is! Otherwise the wrong values may end up being combined. // FIXME: Ensure that the schema has been optimized first! // FIXME: Track schemaAfter for every individual schema update + keep a log of changes for each update, so that we have all the information needed to generate SQL queries + look up schema state in the at-that-revision schema where needed for schema operations that depend on previous state // MARKER: Generate operations list function setOnce(a, b) { if (a === undefined) { return b; } else { throw new Error(`Value cannot be overridden`); } } let mergeFieldSchema = mergeByTemplate.createMerger({ type: setOnce, // FIXME: Also disallow combination with linkTo }); let mergeCollectionSchema = mergeByTemplate.createMerger({ fields: mergeByTemplate.anyProperty(mergeFieldSchema) }); let mergeSchema = mergeByTemplate.createMerger( mergeByTemplate.anyProperty(mergeCollectionSchema) ); // FIXME: Figure out a way to do this with merge-by-template let fieldDefaults = { optional: false }; let mapFieldOperations = createMergeMapper({ merge: mergeFieldSchema, map: (operation) => matchValue(operation.type, { fieldType: () => ({ type: operation.fieldType }), optional: () => ({ optional: true }), required: () => ({ optional: false }), defaultTo: () => ({ defaultTo: operation.value }), // FIXME: Check that this does not need any post-processing linkTo: () => ({ linkTo: operation.field }), // FIXME: Actually extract the related information (eg. column type) from the schema afterwards, but *before* sanity checks deleteField: () => mergeByTemplate.DeleteValue, index: () => undefined, // FIXME: Move these out into `indexes` as an optimizer step }) }); let mapCollectionOperations = createMergeMapper({ merge: mergeCollectionSchema, map: (operation) => matchValue(operation.type, { schemaFields: () => ({ fields: mapObj(operation.fields, (key, value) => { return [ key, mapFieldOperations(value) ]; }) }), }) }); // NOTE: We set consumeDeleteNodes to false in various places below; this is because we first convert each series of operations in a schema update to a cumulative update, and then merge that cumulative update to the final schema. This means that there are *two* merge operations, and by only allowing DeleteValue nodes to be consumed in the second merge operation, we avoid the situation where the first merge operation would simply return 'undefined' for something and this would be (wrongly) interpreted by the second merge operation to mean "no changes made". module.exports = function generateSchema(updates) { let builtSchema = {}; for (let update of updates) { for (let operation of update.operations) { matchValue(operation.type, { createCollectionCommand: () => { if (builtSchema[operation.name] == null) { let collectionOperations = mapCollectionOperations(operation.operations); builtSchema = mergeSchema([ builtSchema, { [operation.name]: collectionOperations }]); } else { throw new Error(`Cannot create collection '${operation.name}' because it already exists; maybe you meant to use changeCollection instead?`); } }, changeCollectionCommand: () => { if (builtSchema[operation.name] != null) { let collectionOperations = mapCollectionOperations(operation.operations, builtSchema[operation.name]); builtSchema = mergeSchema([ builtSchema, { [operation.name]: collectionOperations }]); } else { throw new Error(`Cannot change collection '${operation.name}' because it does not exist; maybe you meant to use createCollection instead?`); } }, deleteCollectionCommand: () => { builtSchema = mergeSchema([ builtSchema, { [operation.name]: undefined }]); }, }); } } return builtSchema; };