WIP
parent
05bd98f640
commit
2802cd9964
@ -1 +1,2 @@
|
|||||||
node_modules
|
node_modules
|
||||||
|
junk
|
||||||
|
@ -0,0 +1,49 @@
|
|||||||
|
// Testcase from ThePendulum
|
||||||
|
|
||||||
|
// Building must contain any of the specified people
|
||||||
|
select("buildings", [
|
||||||
|
define("tenants", has("tenants.building_id")),
|
||||||
|
where({ tenants: { name: anyOf([ "james", "luke", "stanley" ]) } })
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
// Building must contain *all* of the specified people, and optionally others
|
||||||
|
select("buildings", [
|
||||||
|
define("tenants", has("tenants.building_id")),
|
||||||
|
where({ tenants: { name: allOf([ "james", "luke", "stanley" ]) } })
|
||||||
|
])
|
||||||
|
|
||||||
|
// Additional testcase: Building must contain *only* the specified people
|
||||||
|
|
||||||
|
|
||||||
|
// Related code:
|
||||||
|
/* GraphQL/Postgraphile 'every' applies to the data, will only include scenes for which every assigned tag is selected,
|
||||||
|
instead of what we want; scenes with every selected tag, but possibly also some others */
|
||||||
|
CREATE FUNCTION actors_scenes(actor actors, selected_tags text[], mode text DEFAULT 'all') RETURNS SETOF releases AS $$
|
||||||
|
SELECT releases.*
|
||||||
|
FROM releases
|
||||||
|
LEFT JOIN
|
||||||
|
releases_actors ON releases_actors.release_id = releases.id
|
||||||
|
LEFT JOIN
|
||||||
|
releases_tags ON releases_tags.release_id = releases.id
|
||||||
|
LEFT JOIN
|
||||||
|
tags ON tags.id = releases_tags.tag_id
|
||||||
|
WHERE releases_actors.actor_id = actor.id
|
||||||
|
AND CASE
|
||||||
|
/* match at least one of the selected tags */
|
||||||
|
WHEN mode = 'any' AND array_length(selected_tags, 1) > 0
|
||||||
|
THEN tags.slug = ANY(selected_tags)
|
||||||
|
ELSE true
|
||||||
|
END
|
||||||
|
GROUP BY releases.id
|
||||||
|
HAVING CASE
|
||||||
|
/* match all of the selected tags */
|
||||||
|
WHEN mode = 'all' AND array_length(selected_tags, 1) > 0
|
||||||
|
THEN COUNT(
|
||||||
|
CASE WHEN tags.slug = ANY(selected_tags)
|
||||||
|
THEN true
|
||||||
|
END
|
||||||
|
) = array_length(selected_tags, 1)
|
||||||
|
ELSE true
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE SQL STABLE;
|
@ -1,5 +1,3 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
module.exports = {
|
module.exports = require("./src/operations");
|
||||||
|
|
||||||
};
|
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
module.exports = function containsRemoteFields(node) {
|
||||||
|
if (node == null) {
|
||||||
|
return false;
|
||||||
|
} else if (Array.isArray(node)) {
|
||||||
|
return node.some((item) => containsRemoteFields(item));
|
||||||
|
} else if (node.__raqbASTNode === true) {
|
||||||
|
if (node.type === "remoteField") {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return Object.values(node).some((item) => containsRemoteFields(item));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,14 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
module.exports = function compareStrings(a, b) {
|
||||||
|
let aUppercase = a.toUpperCase();
|
||||||
|
let bUppercase = b.toUpperCase();
|
||||||
|
|
||||||
|
if (aUppercase < bUppercase) {
|
||||||
|
return -1;
|
||||||
|
} else if (aUppercase > bUppercase) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,20 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const Promise = require("bluebird");
|
||||||
|
const findUp = require("find-up");
|
||||||
|
|
||||||
|
module.exports = function loadConfiguration(basePath) {
|
||||||
|
return Promise.try(() => {
|
||||||
|
return findUp("zapfile.js", { cwd: basePath });
|
||||||
|
}).then((configurationPath) => {
|
||||||
|
if (configurationPath != null) {
|
||||||
|
return {
|
||||||
|
configurationPath: configurationPath,
|
||||||
|
configuration: require(configurationPath)
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// FIXME: Link to configuration documentation
|
||||||
|
throw new Error(`Unable to find a zapfile; make sure that you've created one in the root of your project`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,7 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
module.exports = function createMergeMapper({ merge, map }) {
|
||||||
|
return function mergeMap(items) {
|
||||||
|
return merge(items.map(map));
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,26 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const matchValue = require("match-value");
|
||||||
|
|
||||||
|
const node = require("../../ast-node");
|
||||||
|
|
||||||
|
module.exports = function makeIndexObject(fieldsResult, properties) {
|
||||||
|
if (fieldsResult.type === "local") {
|
||||||
|
return node({
|
||||||
|
type: "localIndex"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let isComposite = matchValue(fieldsResult.type, {
|
||||||
|
single: false,
|
||||||
|
composite: true
|
||||||
|
});
|
||||||
|
|
||||||
|
return node({
|
||||||
|
type: "index",
|
||||||
|
isComposite: isComposite,
|
||||||
|
field: (isComposite === false) ? fieldsResult.value : undefined,
|
||||||
|
fields: (isComposite === true) ? fieldsResult.value : undefined,
|
||||||
|
... properties
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
@ -1,11 +1,24 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const node = require("../../ast-node");
|
const { validateArguments } = require("@validatem/core");
|
||||||
|
const defaultTo = require("@validatem/default-to");
|
||||||
|
const arrayOf = require("@validatem/array-of");
|
||||||
|
|
||||||
module.exports = function (_operations) {
|
const makeIndexObject = require("./_make-index-object");
|
||||||
return function index() {
|
|
||||||
return node({
|
module.exports = function (operations) {
|
||||||
type: "index"
|
const isIndexFields = require("../../validators/operations/is-index-fields")(operations);
|
||||||
|
const isObjectType = require("../../validators/operations/is-object-type")(operations);
|
||||||
|
|
||||||
|
return function index(_fields, _clauses) {
|
||||||
|
let [ fields, clauses ] = validateArguments(arguments, {
|
||||||
|
fields: isIndexFields,
|
||||||
|
clauses: [ defaultTo([]), arrayOf(isObjectType("where")) ]
|
||||||
|
});
|
||||||
|
|
||||||
|
return makeIndexObject(fields, {
|
||||||
|
clauses: clauses,
|
||||||
|
indexType: "index"
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,11 +1,19 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const node = require("../../ast-node");
|
const { validateArguments } = require("@validatem/core");
|
||||||
|
|
||||||
module.exports = function (_operations) {
|
const makeIndexObject = require("./_make-index-object");
|
||||||
return function primaryKey() {
|
|
||||||
return node({
|
module.exports = function (operations) {
|
||||||
type: "primaryKey"
|
const isIndexFields = require("../../validators/operations/is-index-fields")(operations);
|
||||||
|
|
||||||
|
return function primaryKey(_fields) {
|
||||||
|
let [ fields ] = validateArguments(arguments, {
|
||||||
|
fields: isIndexFields
|
||||||
|
});
|
||||||
|
|
||||||
|
return makeIndexObject(fields, {
|
||||||
|
indexType: "primaryKey"
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
const { validateArguments } = require("@validatem/core");
|
|
||||||
const required = require("@validatem/required");
|
|
||||||
|
|
||||||
const node = require("../../ast-node");
|
|
||||||
|
|
||||||
module.exports = function (operations) {
|
|
||||||
return function uniqueWhere(_expression) {
|
|
||||||
const isExpression = require("../../validators/operations/is-expression")(operations);
|
|
||||||
|
|
||||||
let [ expression ] = validateArguments(arguments, {
|
|
||||||
expression: [ required, isExpression ]
|
|
||||||
});
|
|
||||||
|
|
||||||
return node({
|
|
||||||
type: "uniqueIndex",
|
|
||||||
expression: expression
|
|
||||||
});
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,11 +1,24 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const node = require("../../ast-node");
|
const { validateArguments } = require("@validatem/core");
|
||||||
|
const defaultTo = require("@validatem/default-to");
|
||||||
|
const arrayOf = require("@validatem/array-of");
|
||||||
|
|
||||||
module.exports = function (_operations) {
|
const makeIndexObject = require("./_make-index-object");
|
||||||
return function unique() {
|
|
||||||
return node({
|
module.exports = function (operations) {
|
||||||
type: "uniqueIndex"
|
const isIndexFields = require("../../validators/operations/is-index-fields")(operations);
|
||||||
|
const isObjectType = require("../../validators/operations/is-object-type")(operations);
|
||||||
|
|
||||||
|
return function unique(_fields, _clauses) {
|
||||||
|
let [ fields, clauses ] = validateArguments(arguments, {
|
||||||
|
fields: isIndexFields,
|
||||||
|
clauses: [ defaultTo([]), arrayOf(isObjectType("where")) ]
|
||||||
|
});
|
||||||
|
|
||||||
|
return makeIndexObject(fields, {
|
||||||
|
clauses: clauses,
|
||||||
|
indexType: "unique"
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const { validateArguments } = require("@validatem/core");
|
||||||
|
const required = require("@validatem/required");
|
||||||
|
|
||||||
|
const node = require("../../ast-node");
|
||||||
|
const isCollectionName = require("../../validators/is-collection-name");
|
||||||
|
|
||||||
|
module.exports = function (operations) {
|
||||||
|
const isObjectType = require("../../validators/operations/is-object-type")(operations);
|
||||||
|
|
||||||
|
return function define(_name, _value) {
|
||||||
|
// NOTE: `define` essentially creates a virtual collection, which means that the name should comply with the usual collection naming rules
|
||||||
|
let [ name, value ] = validateArguments(arguments, {
|
||||||
|
name: [ required, isCollectionName ],
|
||||||
|
value: [ required, isObjectType("relation") ] // FIXME: Support subqueries
|
||||||
|
});
|
||||||
|
|
||||||
|
return node({
|
||||||
|
type: "define",
|
||||||
|
name: name,
|
||||||
|
value: value
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,21 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const { validateArguments } = require("@validatem/core");
|
||||||
|
const required = require("@validatem/required");
|
||||||
|
|
||||||
|
const node = require("../../ast-node");
|
||||||
|
|
||||||
|
module.exports = function (operations) {
|
||||||
|
const isRemoteField = require("../../validators/operations/is-remote-field")(operations);
|
||||||
|
|
||||||
|
return function linkTo(_field) {
|
||||||
|
let [ field ] = validateArguments(arguments, {
|
||||||
|
field: [ required, isRemoteField ]
|
||||||
|
});
|
||||||
|
|
||||||
|
return node({
|
||||||
|
type: "linkTo",
|
||||||
|
field: field
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,21 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const { validateArguments } = require("@validatem/core");
|
||||||
|
const required = require("@validatem/required");
|
||||||
|
|
||||||
|
const node = require("../../ast-node");
|
||||||
|
|
||||||
|
module.exports = function (operations) {
|
||||||
|
const isObjectType = require("../../validators/operations/is-object-type")(operations);
|
||||||
|
|
||||||
|
return function producesResult(_subquery) {
|
||||||
|
let [ subquery ] = validateArguments(arguments, {
|
||||||
|
subquery: [ required, isObjectType("relation") ] // FIXME: Support subqueries
|
||||||
|
});
|
||||||
|
|
||||||
|
return node({
|
||||||
|
type: "producesResult",
|
||||||
|
subquery: subquery
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,29 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const { validateArguments } = require("@validatem/core");
|
||||||
|
const required = require("@validatem/required");
|
||||||
|
const anyProperty = require("@validatem/any-property");
|
||||||
|
|
||||||
|
const node = require("../../ast-node");
|
||||||
|
const isLocalFieldName = require("../../validators/is-local-field-name");
|
||||||
|
|
||||||
|
// NOTE: `withRelations` structurally functions like a `compute`, except the computation value is a has/belongsTo relation specifier, which gets resolved at query time
|
||||||
|
// FIXME: Actually implement relation fetching logic. Start by generating relational queries after retrieving the initial data, eventually change this to pre-compute most of the relational queries and leave placeholders for table/column/etc. names. Implement the query generation as a stand-alone "generate relational query" function that takes in the current DB schema + relational specifier (+ clauses).
|
||||||
|
|
||||||
|
module.exports = function (operations) {
|
||||||
|
const isObjectType = require("../../validators/operations/is-object-type")(operations);
|
||||||
|
|
||||||
|
return function withRelations(_items) {
|
||||||
|
let [ items ] = validateArguments(arguments, {
|
||||||
|
items: [ required, anyProperty({
|
||||||
|
key: [ required, isLocalFieldName ], // FIXME: Support dot-path notation for nested relation specification? Or let this be handled by a relation's clauses?
|
||||||
|
value: [ required, isObjectType("relation") ]
|
||||||
|
})]
|
||||||
|
});
|
||||||
|
|
||||||
|
return node({
|
||||||
|
type: "withRelations",
|
||||||
|
items: items
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,21 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const { validateArguments } = require("@validatem/core");
|
||||||
|
const required = require("@validatem/required");
|
||||||
|
|
||||||
|
const node = require("../../ast-node");
|
||||||
|
|
||||||
|
module.exports = function (operations) {
|
||||||
|
const isObjectType = require("../../validators/operations/is-object-type")(operations);
|
||||||
|
|
||||||
|
return function deleteField(_restoreOperation) {
|
||||||
|
let [ restoreOperation ] = validateArguments(arguments, {
|
||||||
|
restoreOperation: [ required, isObjectType("restoreAs") ]
|
||||||
|
});
|
||||||
|
|
||||||
|
return node({
|
||||||
|
type: "deleteField",
|
||||||
|
restoreOperation: restoreOperation
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,11 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const node = require("../../ast-node");
|
||||||
|
|
||||||
|
module.exports = function (_operations) {
|
||||||
|
return function optional() {
|
||||||
|
return node({
|
||||||
|
type: "optional"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,7 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
module.exports = [
|
||||||
|
// The optimizers applied to a schema AST are all those for regular queries + the schema-specific ones
|
||||||
|
... require("../"),
|
||||||
|
require("./move-out-indexes")
|
||||||
|
];
|
@ -0,0 +1,69 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const splitFilter = require("split-filter");
|
||||||
|
const matchValue = require("match-value");
|
||||||
|
|
||||||
|
const operations = require("../../operations");
|
||||||
|
const NoChange = require("../util/no-change");
|
||||||
|
const ConsumeNode = require("../util/consume-node");
|
||||||
|
|
||||||
|
/*
|
||||||
|
Translate index modifiers on a single field into a top-level (non-composite) index element
|
||||||
|
{ type: "index"|"removeIndex", indexType, isComposite: true|false, field|fields}
|
||||||
|
*/
|
||||||
|
|
||||||
|
function handleCollection(node, { registerStateHandler, defer }) {
|
||||||
|
let createNode = matchValue.literal(node.type, {
|
||||||
|
createCollectionCommand: operations.createCollection,
|
||||||
|
changeCollectionCommand: operations.changeCollection
|
||||||
|
});
|
||||||
|
|
||||||
|
let indexNodes = [];
|
||||||
|
|
||||||
|
registerStateHandler("encounteredLocalIndex", (item) => {
|
||||||
|
// FIXME: Make named index
|
||||||
|
// FIXME: Default name generation for indexes
|
||||||
|
indexNodes.push(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
return defer(() => {
|
||||||
|
if (indexNodes.length > 0) {
|
||||||
|
let indexesObject = operations.indexes(indexNodes.map((item) => {
|
||||||
|
console.log(item); // node, property
|
||||||
|
}));
|
||||||
|
|
||||||
|
return NoChange;
|
||||||
|
return createNode(name, operations.concat([ indexesObject ]));
|
||||||
|
} else {
|
||||||
|
return NoChange;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
[ createCollection, operations ]
|
||||||
|
[ _array, 0 ]
|
||||||
|
[ schemaFields, fields ]
|
||||||
|
[ _object, last_activity ]
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: "move-out-indexes",
|
||||||
|
category: [ "normalization" ],
|
||||||
|
visitors: {
|
||||||
|
localIndex: (node, { setState, findNearestStep }) => {
|
||||||
|
let { key } = findNearestStep("$object");
|
||||||
|
setState("encounteredLocalIndex", { node, key });
|
||||||
|
|
||||||
|
return ConsumeNode;
|
||||||
|
},
|
||||||
|
createCollectionCommand: handleCollection,
|
||||||
|
changeCollectionCommand: handleCollection
|
||||||
|
|
||||||
|
|
||||||
|
// MARKER: Move indexes from column definitions to table index definition, with auto-generated name; may need some way to select "only within node of type X" (where X = fields)
|
||||||
|
// IDEA: propertyPath and typePath arguments, for the visitor to determine whether it is appearing in the correct place (otherwise NoChange)
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,6 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
// NOTE: This marker differs from RemoveNode in that it *doesn't* wipe out the state collected by the removed node; that is, it is assumed that the node is "consumed" and the stateLog is the result of that consumption. This is useful for various "meta-operations" which just serve to annotate some other operation with a modifier, and where the meta-operations themselves do not have any representation in the resulting query. In those cases, the meta-operation would be consumed and the parent node updated to reflect the modifier.
|
||||||
|
// FIXME: Check for existing places in optimizers where nodes are currently left lingering around, that should be consumed instead
|
||||||
|
|
||||||
|
module.exports = Symbol("ConsumeNode");
|
@ -0,0 +1,23 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const syncpipe = require("syncpipe");
|
||||||
|
|
||||||
|
const astToQuery = require("../ast-to-query");
|
||||||
|
|
||||||
|
const optimizeAST = require("../ast/optimize");
|
||||||
|
const optimizers = require("../optimizers/schema");
|
||||||
|
|
||||||
|
module.exports = function processSchemaUpdate(update) {
|
||||||
|
return {
|
||||||
|
... update,
|
||||||
|
operations: update.operations.map((operation) => {
|
||||||
|
// FIXME: This is too simplified. A single operation may result in multiple queries.
|
||||||
|
return syncpipe(operation, [
|
||||||
|
(_) => optimizeAST(_, optimizers),
|
||||||
|
// (_) => astToQuery(_.ast)
|
||||||
|
(_) => _.ast
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,56 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const syncpipe = require("syncpipe");
|
||||||
|
const table = require("table").table;
|
||||||
|
const chalk = require("chalk");
|
||||||
|
|
||||||
|
const unreachable = require("./unreachable");
|
||||||
|
|
||||||
|
function renderTable(data) {
|
||||||
|
return table(data, {
|
||||||
|
border: {
|
||||||
|
topBody: "",
|
||||||
|
topJoin: "",
|
||||||
|
topLeft: "",
|
||||||
|
topRight: "",
|
||||||
|
bottomBody: "",
|
||||||
|
bottomJoin: "",
|
||||||
|
bottomLeft: "",
|
||||||
|
bottomRight: "",
|
||||||
|
bodyLeft: "",
|
||||||
|
bodyRight: "",
|
||||||
|
joinLeft: "",
|
||||||
|
joinRight: ""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = function renderSchema(dbSchema) {
|
||||||
|
return Object.entries(dbSchema)
|
||||||
|
.map(([ collectionName, schema ]) => {
|
||||||
|
let tableData = [
|
||||||
|
[ chalk.bold("Column"), chalk.bold("Type"), " " ]
|
||||||
|
].concat(syncpipe(schema.fields, [
|
||||||
|
(_) => Object.entries(_),
|
||||||
|
(_) => _.filter(([ _columnName, definition ]) => definition != null),
|
||||||
|
(_) => _.map(([ fieldName, definition ]) => {
|
||||||
|
let link = definition.linkTo;
|
||||||
|
|
||||||
|
if (link != null) {
|
||||||
|
if (link.type === "remoteField") {
|
||||||
|
let linkedField = dbSchema[link.collectionName].fields[link.fieldName];
|
||||||
|
|
||||||
|
return [ fieldName, chalk.gray(linkedField.type), `-> ${link.collectionName}.${link.fieldName}` ];
|
||||||
|
} else {
|
||||||
|
unreachable("Non-remoteField link encountered");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return [ fieldName, definition.type, " " ]; // FIXME: Linked type
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]));
|
||||||
|
|
||||||
|
return chalk.bold.green(collectionName) + "\n\n" + renderTable(tableData);
|
||||||
|
})
|
||||||
|
.join("\n\n");
|
||||||
|
};
|
@ -0,0 +1,105 @@
|
|||||||
|
/* 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;
|
||||||
|
};
|
@ -0,0 +1,87 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const Promise = require("bluebird");
|
||||||
|
const fs = require("fs").promises;
|
||||||
|
const path = require("path");
|
||||||
|
const dateFns = require("date-fns");
|
||||||
|
const slug = require("slug");
|
||||||
|
|
||||||
|
const compareStrings = require("../compare-strings");
|
||||||
|
|
||||||
|
let suffixRegex = /^(.+)\.js$/;
|
||||||
|
let filenameRegex = /^(\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2})_(.+)$/;
|
||||||
|
let templateFolder = path.resolve(__dirname, "../templates");
|
||||||
|
|
||||||
|
module.exports = function ({ configurationPath, configuration }) {
|
||||||
|
let schemaUpdateFolder = path.resolve(path.dirname(configurationPath), configuration.schema.fs.root);
|
||||||
|
|
||||||
|
return {
|
||||||
|
create: function ({ description }) {
|
||||||
|
return Promise.try(() => {
|
||||||
|
let timestamp = dateFns.format(new Date(), "yyyy-MM-dd_hh-mm-ss");
|
||||||
|
let destinationFilename = `${timestamp}_${slug(description)}.js`;
|
||||||
|
|
||||||
|
let sourceTemplate = path.join(templateFolder, "schema-update.js");
|
||||||
|
let destinationPath = path.join(schemaUpdateFolder, destinationFilename);
|
||||||
|
|
||||||
|
return Promise.try(() => {
|
||||||
|
return fs.copyFile(sourceTemplate, destinationPath);
|
||||||
|
}).then(() => {
|
||||||
|
return {
|
||||||
|
statusMessage: `New schema update created at ${destinationPath}`
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getAll: function () {
|
||||||
|
return Promise.try(() => {
|
||||||
|
return fs.readdir(schemaUpdateFolder);
|
||||||
|
}).then((schemaFiles) => {
|
||||||
|
let seenDates = new Set();
|
||||||
|
|
||||||
|
return schemaFiles
|
||||||
|
.map((filename) => {
|
||||||
|
let match = suffixRegex.exec(filename);
|
||||||
|
|
||||||
|
if (match != null) {
|
||||||
|
let basename = match[1];
|
||||||
|
let parsed = filenameRegex.exec(basename);
|
||||||
|
|
||||||
|
if (parsed != null) {
|
||||||
|
let [ _, date, description ] = parsed;
|
||||||
|
|
||||||
|
if (!seenDates.has(date)) {
|
||||||
|
seenDates.add(date);
|
||||||
|
|
||||||
|
return {
|
||||||
|
filename: filename,
|
||||||
|
timestamp: date,
|
||||||
|
description: description
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// FIXME: Link to docs explaining this
|
||||||
|
throw new Error(`Encountered timestamp prefix twice: ${date} -- this is not allowed, change one of the timestamps to indicate the desired order.`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// FIXME: Link to docs explaining this
|
||||||
|
throw new Error(`Filename does not match the expected format: ${filename}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(`The schema folder must only contain .js files; encountered ${filename}`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
return compareStrings(a.timestamp, b.timestamp);
|
||||||
|
})
|
||||||
|
.map((item) => {
|
||||||
|
return {
|
||||||
|
timestamp: item.timestamp, // NOTE: This is a unique sortable ID, that is used to identify the schema update in the internal schema state
|
||||||
|
description: item.description,
|
||||||
|
operations: require(path.join(schemaUpdateFolder, item.filename))
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// FIXME: Error case handling
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,7 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const { createCollection, changeCollection, deleteCollection } = require("zapdb");
|
||||||
|
|
||||||
|
module.exports = [
|
||||||
|
/* your schema update operations go here */
|
||||||
|
];
|
@ -0,0 +1,10 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const containsRemoteFields = require("../ast/contains-remote-fields");
|
||||||
|
const ValidationError = require("@validatem/error");
|
||||||
|
|
||||||
|
module.exports = function forbidRemoteFields(value) {
|
||||||
|
if (containsRemoteFields(value)) {
|
||||||
|
throw new ValidationError(`Must not reference any remote fields`);
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,19 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const defaultTo = require("@validatem/default-to");
|
||||||
|
const either = require("@validatem/either");
|
||||||
|
const arrayOf = require("@validatem/array-of");
|
||||||
|
|
||||||
|
const tagAsType = require("../../validators/tag-as-type");
|
||||||
|
|
||||||
|
module.exports = function (operations) {
|
||||||
|
const isField = require("./is-field")(operations);
|
||||||
|
|
||||||
|
return [
|
||||||
|
either([
|
||||||
|
[ isField, tagAsType("single") ],
|
||||||
|
[ arrayOf(isField), tagAsType("composite") ],
|
||||||
|
]),
|
||||||
|
defaultTo({ type: "local" }),
|
||||||
|
];
|
||||||
|
};
|
@ -1,19 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
const either = require("@validatem/either");
|
|
||||||
|
|
||||||
const isLiteralValue = require("../is-literal-value");
|
|
||||||
|
|
||||||
// NOTE: This validator should typically come last in an `either`, since it will catch various types of inputs (sqlExpression, literal values, etc.) that might need to be interpreted differently in specific contexts.
|
|
||||||
|
|
||||||
module.exports = function (operations) {
|
|
||||||
const isObjectType = require("./is-object-type")(operations);
|
|
||||||
const wrapWithOperation = require("./wrap-with-operation")(operations);
|
|
||||||
|
|
||||||
return either([
|
|
||||||
[ isObjectType("sqlExpression") ],
|
|
||||||
[ isObjectType("literalValue") ],
|
|
||||||
[ isObjectType("field") ],
|
|
||||||
[ isLiteralValue, wrapWithOperation("value") ]
|
|
||||||
]);
|
|
||||||
};
|
|
@ -0,0 +1,7 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
module.exports = function (operations) {
|
||||||
|
const isObjectType = require("./is-object-type")(operations);
|
||||||
|
|
||||||
|
return isObjectType("sqlFunction");
|
||||||
|
};
|
@ -1,17 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
const wrapError = require("@validatem/wrap-error");
|
|
||||||
const ValidationError = require("@validatem/error");
|
|
||||||
|
|
||||||
module.exports = function (operations) {
|
|
||||||
const isIndexType = require("./is-index-type")(operations);
|
|
||||||
|
|
||||||
return wrapError("Must be a composite index type", [
|
|
||||||
isIndexType,
|
|
||||||
(node) => {
|
|
||||||
if (node.composite !== true) {
|
|
||||||
return new ValidationError(`Must be a composite index`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
};
|
|
@ -1,16 +1,9 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const either = require("@validatem/either");
|
|
||||||
const wrapError = require("@validatem/wrap-error");
|
const wrapError = require("@validatem/wrap-error");
|
||||||
|
|
||||||
module.exports = function (operations) {
|
module.exports = function (operations) {
|
||||||
const isObjectType = require("../is-object-type")(operations);
|
const isObjectType = require("../is-object-type")(operations);
|
||||||
|
|
||||||
return wrapError("Must be a field type", either([
|
return wrapError("Must be a field type", isObjectType("fieldType"));
|
||||||
isObjectType("autoIDField"),
|
|
||||||
isObjectType("stringField"),
|
|
||||||
isObjectType("timestampField"),
|
|
||||||
isObjectType("booleanField"),
|
|
||||||
isObjectType("uuidField"),
|
|
||||||
]));
|
|
||||||
};
|
};
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
const either = require("@validatem/either");
|
|
||||||
const wrapError = require("@validatem/wrap-error");
|
|
||||||
|
|
||||||
module.exports = function (operations) {
|
|
||||||
const isObjectType = require("../is-object-type")(operations);
|
|
||||||
|
|
||||||
return wrapError("Must be an index type", either([
|
|
||||||
isObjectType("index"),
|
|
||||||
isObjectType("indexWhere"),
|
|
||||||
isObjectType("unique"),
|
|
||||||
isObjectType("uniqueWhere"),
|
|
||||||
]));
|
|
||||||
};
|
|
@ -0,0 +1,9 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const wrapError = require("@validatem/wrap-error");
|
||||||
|
|
||||||
|
module.exports = function (operations) {
|
||||||
|
const isObjectType = require("../is-object-type")(operations);
|
||||||
|
|
||||||
|
return wrapError("Must be an index type (without specifying a field name)", isObjectType("localIndex"));
|
||||||
|
};
|
@ -0,0 +1,9 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const wrapError = require("@validatem/wrap-error");
|
||||||
|
|
||||||
|
module.exports = function (operations) {
|
||||||
|
const isObjectType = require("../is-object-type")(operations);
|
||||||
|
|
||||||
|
return wrapError("Must be an index type (specifying one or more field names)", isObjectType("index"));
|
||||||
|
};
|
Loading…
Reference in New Issue