master
Sven Slootweg 3 years ago
parent 05bd98f640
commit 2802cd9964

1
.gitignore vendored

@ -1 +1,2 @@
node_modules
junk

@ -2,31 +2,73 @@
"use strict";
const Promise = require("bluebird");
const yargs = require("yargs");
const matchValue = require("match-value");
const chalk = require("chalk");
const loadConfiguration = require("../src/load-configuration");
const generateSchema = require("../src/schema-generator");
const renderSchema = require("../src/render-schema");
const processSchemaUpdate = require("../src/process-schema-update");
// FIXME: Update to take timestamps instead of revision numbers (and autocomplete them!)
let argv = yargs
.command("change", "Create a new schema revision")
.command("change <description>", "Create a new schema revision", {
description: { describe: "A brief textual description of the purpose of the revision" }
})
.command("upgrade <revision>", "Upgrade the database to a new schema revision", {
revision: { describe: "The number of the revision to upgrade to (or 'latest')" }
})
.command("undo", "Undo the most recent schema revision upgrade")
.command("show <revision>", "Show the full database schema at a given revision")
.command("show <revision>", "Show the full database schema at a given revision", {
revision: { describe: "The number of the revision at which the schema should be shown (or 'latest')" }
})
.argv;
console.log(argv);
matchValue(argv._[0], {
change: () => {
console.log("change");
},
upgrade: () => {
console.log("upgrade");
},
undo: () => {
console.log("undo");
},
show: () => {
console.log("show");
}
// FIXME: Error/help when no subcommand has been specified
// console.log(argv);
return Promise.try(() => {
return loadConfiguration();
}).then(({ configuration, configurationPath }) => {
let state = { configurationPath, configuration };
// FIXME: Make configurable
const schemaProvider = require("../src/schema-providers/fs")(state);
matchValue(argv._[0], {
change: () => {
return Promise.try(() => {
return schemaProvider.create({ description: argv.description });
}).then(({ statusMessage }) => {
console.log(chalk.green(`✔ ${statusMessage}`));
});
},
upgrade: () => {
return Promise.try(() => {
return schemaProvider.getAll();
}).then((schemaUpdates) => {
let processed = schemaUpdates.map((update) => {
return processSchemaUpdate(update);
});
// console.log(require("util").inspect(processed, { colors: true, depth: null }));
});
// TODO: Allow-gap option to permit filling in 'gapped migrations' (eg. after an earlier migration gets merged in via branch merge)
},
undo: () => {
console.log("undo");
},
show: () => {
return Promise.try(() => {
return schemaProvider.getAll();
}).then((schemaUpdates) => {
let schema = generateSchema(schemaUpdates);
console.log(renderSchema(schema));
});
}
});
});

@ -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;

@ -43,19 +43,19 @@ try {
*/
// let niceNumbers = anyOf([ 1, 2, 3 ]);
// query = select("projects", [
// onlyFields([ "foo" ]),
// where({
// number_one: niceNumbers,
// number_two: niceNumbers
// }),
// where({
// number_three: anyOf([ 42, field("number_one") ]),
// number_four: moreThan(1337)
// })
// ]);
let niceNumbers = anyOf([ 1, 2, 3 ]);
query = select("projects", [
onlyFields([ "foo" ]),
where({
number_one: niceNumbers,
number_two: niceNumbers
}),
where({
number_three: anyOf([ 42, field("number_one") ]),
number_four: moreThan(1337)
})
]);
/*
Generation timings:
@ -101,36 +101,36 @@ try {
// ])
// ]);
query = createTable("actors", {
fields: {
imdb_id: string(),
name: [ required(), string() ],
date_of_birth: date({ withTimezone: true }),
place_of_birth: string(),
imdb_metadata_scraped: json()
}
});
query = createTable("movies", {
fields: {
imdb_id: string(),
name: string(),
imdb_metadata_scraped: json()
}
});
query = createTable("appearances", {
fields: {
actor: belongsTo("actors"),
movie: belongsTo("movie")
}
});
updateTable("appearances", {
fields: {
// query = createTable("actors", {
// fields: {
// imdb_id: string(),
// name: [ required(), string() ],
// date_of_birth: date({ withTimezone: true }),
// place_of_birth: string(),
// imdb_metadata_scraped: json()
// }
// });
// query = createTable("movies", {
// fields: {
// imdb_id: string(),
// name: string(),
// imdb_metadata_scraped: json()
// }
// });
// query = createTable("appearances", {
// fields: {
// actor: belongsTo("actors"),
// movie: belongsTo("movie")
// }
// });
// updateTable("appearances", {
// fields: {
}
})
// }
// })
// FIXME: For query generation, ensure we are generating correct queries for TRUE/FALSE/NULL/NOTNULL (in particular, moreThan/lessThan should not allow these!)
// FIXME: Partial indexes

@ -1,5 +1,3 @@
"use strict";
module.exports = {
};
module.exports = require("./src/operations");

@ -14,6 +14,8 @@ Todo:
- Annotate placeholder/parameter nodes with the expected parameter type in their position, for validation of input parameters at query time
- Rename table -> collection everywhere, also column -> field?
- Update wrapError API
- Figure out a way to semantically deal with the SQL boolean behaviour of propagating NULL (eg. as described on https://www.postgresql.org/docs/8.1/functions-subquery.html for IN)
- Some sort of convertAs method for schema building, when changing the data type of a column - disallow a direct type override, and require it to be wrapped in a changeType method that also takes the conversion logic as an argument?
Docs:
- Emphasize that users should feel free to experiment; the library will tell them (safely) if they are trying to do something invalid - need to help the user overcome the "but what if I get it wrong" fear (also emphasize the flexibility of schema updates to help with this, "it can always be fixed later" or so)
@ -42,6 +44,10 @@ Terminology:
- Operation: any kind of item in a query; eg. select(...), but also where(...) and collapseBy(...) -- need to emphasize that this does *not* imply immediate execution
- Clause: any kind of operation that modifies the behaviour of its parent operation in some way; eg. a where(...) inside of a select(...) or inside of a `has(...)`
- Predicate: condition, like moreThan(3)
- Meta-queries?: EXISTS, IN, etc. - queries that operate on other queries
Tricky usecases:
- https://gist.github.com/samsch/83f91a66eaf96a70c909ae80d4adfe3e
----
@ -63,6 +69,7 @@ Recommendations to make:
Ideas:
- Make placeholders typed so that we can warn the user when they try to pass in invalid things?
- Safe client API that returns a disposer instead of a client upon client creation?
- For modular/extensible design, allow plugins to supply operations + optimizers, and expect it all to be compilable to the core operations set? Need to find a way to keep same-named operations in different plugins from clashing. Also need dependency-solving to make it possible to specify "my optimizers should run before those of $otherPlugin".
----

@ -2,6 +2,9 @@
"name": "zapdb",
"version": "0.1.0",
"main": "index.js",
"bin": {
"zap": "bin/zap"
},
"repository": "git@git.cryto.net:joepie91/raqb.git",
"author": "Sven Slootweg <admin@cryto.net>",
"license": "WTFPL OR CC0-1.0",
@ -12,6 +15,7 @@
"@validatem/core": "^0.3.3",
"@validatem/default-to": "^0.1.0",
"@validatem/either": "^0.1.3",
"@validatem/ensure-array": "^0.1.0",
"@validatem/error": "^1.0.0",
"@validatem/forbidden": "^0.1.0",
"@validatem/has-shape": "^0.1.7",
@ -37,19 +41,26 @@
"browser-hrtime": "^1.1.6",
"chalk": "^4.1.0",
"classnames": "^2.2.6",
"date-fns": "^2.16.1",
"debounce": "^1.2.0",
"debug": "^4.1.1",
"default-value": "^1.0.0",
"estree-assign-parent": "^1.0.0",
"find-last": "^1.0.0",
"find-up": "^5.0.0",
"flatten": "^1.0.3",
"is-plain-obj": "^3.0.0",
"map-obj": "^4.1.0",
"match-value": "^1.1.0",
"merge-by-template": "^0.1.3",
"pg": "^8.3.3",
"prismjs": "^1.20.0",
"scope-analyzer": "^2.0.5",
"slug": "^3.3.4",
"split-filter": "^1.1.3",
"split-filter-n": "^1.1.2",
"syncpipe": "^1.0.0",
"table": "^6.0.3",
"yargs": "^15.4.1"
},
"devDependencies": {

@ -229,7 +229,7 @@ let process = {
select: function ({ collection, clauses }) {
let $collection = $handle(collection);
let expectedClauseTypes = [ "where", "addFields", "onlyFields" ];
let expectedClauseTypes = [ "where", "addFields", "onlyFields", "collapseBy" ];
let clausesByType = splitFilterN(clauses, expectedClauseTypes, (clause) => clause.type);
let onlyFields = clausesByType.onlyFields.map((node) => node.fields).flat();
@ -393,6 +393,7 @@ function $handle(node) {
if (processor != null) {
return processor(node);
} else {
// FIXME: unreachable
throw new Error(`Unrecognized node type: ${node.type}`);
}
}

@ -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;
}
};

@ -2,15 +2,18 @@
"use strict";
// Design note: We return stateLogs instead of passing in an object of registered handlers to call, because a node can become obsolete in mid-processing, and in those cases all of its state sets should be ignored. By far the easiest way to implement this, is to just keep a stateLog in the node handling context (since that entire context gets thrown away when processing gets aborted due to a subtree change), and let the parent deal with actually applying any still-relevant setStates to the correct handler functions.
// FIXME: Figure out a way to track 'loss factor' per optimizer, ie. how many (partial or complete) node evaluations have been discarded due to the actions of that optimizer, including subtrees. This can give insight into which optimizers cause unreasonably much wasted work.
// TODO: Figure out a way to track 'loss factor' per optimizer, ie. how many (partial or complete) node evaluations have been discarded due to the actions of that optimizer, including subtrees. This can give insight into which optimizers cause unreasonably much wasted work.
const util = require("util");
const splitFilter = require("split-filter");
const mapObj = require("map-obj");
const defaultValue = require("default-value");
const isPlainObj = require("is-plain-obj");
const findLast = require("find-last");
const NoChange = require("../../optimizers/util/no-change");
const RemoveNode = require("../../optimizers/util/remove-node");
const ConsumeNode = require("../../optimizers/util/consume-node");
const typeOf = require("../../type-of");
const concat = require("../../concat");
const deriveNode = require("../../derive-node");
@ -23,6 +26,8 @@ const combineOptimizers = require("./combine-optimizers");
const createDebuggers = require("./create-debuggers");
// FIXME: Implement a scope tracker of some sort, to decouple the code here a bit more
// TODO: Determine if we can improve performance by avoiding a lot of array allocations for the path tracking; by eg. nesting objects instead and unpacking it into an array on-demand
// FIXME: Verify that the various iterations=0 arguments are actually correct, and don't lose iteration count metadata
let EVALUATION_LIMIT = 10;
@ -30,45 +35,75 @@ function defer(func) {
return { __type: "defer", func: func };
}
function handleNodeChildren(node, handle) {
function handleNodeChildren(node, handleASTNode, path) {
let changedProperties = {};
let stateLogs = [];
for (let [ property, value ] of Object.entries(node)) {
if (value == null) {
continue;
} else if (value.__raqbASTNode === true) {
let result = handle(value);
function tryTransformItem(node, path) {
if (node == null) {
return node;
} else if (node.__raqbASTNode === true) {
let result = handleASTNode(node, 0, path);
if (result.stateLog.length > 0) {
stateLogs.push(result.stateLog);
}
if (result.node !== value) {
changedProperties[property] = result.node;
}
} else if (Array.isArray(value) && value.length > 0 && value[0].__raqbASTNode === true) {
// NOTE: We assume that if an array in an AST node property contains one AST node, *all* of its items are AST nodes. This should be ensured by the input wrapping in the operations API.
// eslint-disable-next-line no-loop-func
let results = value.map((item) => handle(item));
return result.node;
} else if (Array.isArray(node)) {
let valuesHaveChanged = false;
let transformedArray = node.map((value, i) => {
let pathSegment = { type: "$array", key: i };
let transformedValue = tryTransformItem(value, path.concat([ pathSegment ]));
let newStateLogs = results
.filter((result) => result.stateLog.length > 0)
.map((result) => result.stateLog);
if (transformedValue !== value) {
valuesHaveChanged = true;
}
return transformedValue;
});
if (newStateLogs.length > 0) {
stateLogs.push(... newStateLogs);
if (valuesHaveChanged) {
return transformedArray;
} else {
return node;
}
} else if (isPlainObj(node)) {
let newObject = {};
let propertiesHaveChanged = false;
for (let [ key, value ] of Object.entries(node)) {
let pathSegment = { type: "$object", key: key };
let transformedValue = tryTransformItem(value, path.concat([ pathSegment ]));
let newNodes = results.map((result) => result.node);
let hasChangedItems = newNodes.some((newNode, i) => newNode !== value[i]);
if (transformedValue !== value) {
propertiesHaveChanged = true;
}
newObject[key] = transformedValue;
}
if (hasChangedItems) {
changedProperties[property] = newNodes.filter((item) => item !== RemoveNode);
if (propertiesHaveChanged) {
return newObject;
} else {
return node;
}
} else {
// Probably some kind of literal value; we don't touch these.
continue;
return node;
}
}
// FIXME: Delete nulls?
for (let [ property, value ] of Object.entries(node)) {
let childPath = path.concat([{ type: node.type, key: property }]);
let transformedValue = tryTransformItem(value, childPath);
if (transformedValue !== value) {
changedProperties[property] = transformedValue;
}
}
@ -93,7 +128,9 @@ module.exports = function optimizeTree(ast, optimizers) {
];
});
function handleNode(node, iterations = 0) {
function handleASTNode(node, iterations = 0, path = [], initialStateLog) {
// console.log(path.map((item) => String(item.key)).join(" -> "));
// The stateLog contains a record of every setState call that was made during the handling of this node and its children. We keep a log for this rather than calling handlers directly, because setState calls should always apply to *ancestors*, not to the current node. That is, if the current node does a setState for `foo`, and also has a handler registered for `foo`, then that handler should not be called, but the `foo` handler in the *parent* node should be.
// FIXME: Scope stateLog entries by optimizer name? To avoid name clashes for otherwise similar functionality. Like when multiple optimizers track column names.
let stateLog = [];
@ -101,15 +138,20 @@ module.exports = function optimizeTree(ast, optimizers) {
let handlers = createHandlerTracker();
let nodeVisitors = visitorsByType[node.type];
function handleResult({ debuggerName, result, permitDefer }) {
function handleResult({ debuggerName, result, permitDefer, initialStateLog }) {
if (result === NoChange) {
// no-op
} else if (result == null) {
// FIXME: Improve this error so that it actually tells you in what visitor things broke
throw new Error(`A visitor is not allowed to return null or undefined; if you intended to leave the node untouched, return a NoChange marker instead`);
// FIXME: Figure out a better way to indicate the origin of such an issue, than the current error message format?
// FIXME: Include information on which node this failed for
throw new Error(`[${debuggerName}] A visitor is not allowed to return null or undefined; if you intended to leave the node untouched, return a NoChange marker instead`);
} else if (result === RemoveNode) {
debuggers[debuggerName](`Node of type '${typeOf(node)}' removed`);
return { node: RemoveNode, stateLog: [] };
} else if (result === ConsumeNode) {
debuggers[debuggerName](`Node of type '${typeOf(node)}' consumed, but its stateLog was left intact`);
stateLog.forEach((item) => { item.isFromConsumedNode = true; }); // NOTE: Mutates!
return { node: ConsumeNode, stateLog: stateLog };
} else if (result.__type === "defer") {
if (permitDefer) {
debuggers[debuggerName](`Defer was scheduled for node of type '${typeOf(node)}'`);
@ -127,7 +169,7 @@ module.exports = function optimizeTree(ast, optimizers) {
if (iterations >= EVALUATION_LIMIT) {
throw new Error(`Exceeded evaluation limit in optimizer ${debuggerName}; aborting optimization. If you are a user of raqb, please report this as a bug. If you are writing an optimizer, make sure that your optimizer eventually stabilizes on a terminal condition (ie. NoChange)!`);
} else {
return handleNode(result, iterations + 1);
return handleASTNode(result, iterations + 1, path, initialStateLog);
}
}
} else {
@ -135,6 +177,17 @@ module.exports = function optimizeTree(ast, optimizers) {
}
}
function handleStateLog(newStateLog) {
let [ relevantState, otherState ] = splitFilter(newStateLog, (entry) => handlers.has(entry.name));
stateLog = stateLog.concat(otherState);
for (let item of relevantState) {
// FIXME: Log these, and which visitor they originate from
handlers.call(item.name, item.value);
}
}
function applyVisitorFunction({ visitorName, func, node, permitDefer }) {
let { value: result, time } = measureTime(() => {
return func(node, {
@ -145,7 +198,12 @@ module.exports = function optimizeTree(ast, optimizers) {
stateLog.push({ name, value });
},
registerStateHandler: (name, func) => handlers.add(name, func),
defer: (permitDefer === true) ? defer : null
defer: (permitDefer === true) ? defer : null,
findNearestStep: function (type) {
return (type != null)
? findLast(path, (item) => item.type === type)
: path[path.length - 1];
}
});
});
@ -174,28 +232,32 @@ module.exports = function optimizeTree(ast, optimizers) {
}
}
let childResult = handleNodeChildren(node, handleNode);
let childResult = handleNodeChildren(node, handleASTNode, path);
if (Object.keys(childResult.changedProperties).length > 0) {
let newNode = deriveNode(node, childResult.changedProperties);
// We already know that the new node is a different one, but let's just lead it through the same handleResult process, for consistency. Handling of the pre-child-changes node is aborted here, and we re-evaluate with the new node.
return handleResult({
let reevaluatedResult = handleResult({
debuggerName: "(subtree change)",
result: newNode,
permitDefer: false
permitDefer: false,
// NOTE: If we have any leftover state from nodes that were consumed upstream, we should make sure to include this in the reevaluation, even when the subtree was replaced!
initialStateLog: (childResult.stateLog.length > 0)
? childResult.stateLog.filter((item) => item.isFromConsumedNode)
: undefined
});
return reevaluatedResult;
}
if (childResult.stateLog.length > 0) {
let [ relevantState, otherState ] = splitFilter(childResult.stateLog, (entry) => handlers.has(entry.name));
stateLog = stateLog.concat(otherState);
if (initialStateLog != null) {
// NOTE: We intentionally process the initialStateLog here and not earlier; that way it is consistent with how any retained stateLog entries *would* have executed on the node before it got replaced (ie. after evaluation of the children). Conceptually you can think of it as the initialStateLog being prefixed to the stateLog of the childResult.
handleStateLog(initialStateLog);
}
for (let item of relevantState) {
// FIXME: Log these, and which visitor they originate from
handlers.call(item.name, item.value);
}
if (childResult.stateLog.length > 0) {
handleStateLog(childResult.stateLog);
}
for (let defer of defers) {
@ -215,7 +277,7 @@ module.exports = function optimizeTree(ast, optimizers) {
return handled;
}
}
return {
stateLog: stateLog,
node: node
@ -223,12 +285,12 @@ module.exports = function optimizeTree(ast, optimizers) {
}
let { value: rootResult, time } = measureTime(() => {
return handleNode(ast);
return handleASTNode(ast);
});
let timeSpentInOptimizers = Object.values(timings).reduce((sum, n) => sum + n, 0);
if (rootResult.node !== RemoveNode) {
if (rootResult.node !== RemoveNode && rootResult.node !== ConsumeNode) {
return {
ast: rootResult.node,
timings: {

@ -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));
};
};

@ -7,11 +7,11 @@ const arrayOf = require("@validatem/array-of");
const node = require("../ast-node");
module.exports = function (operations) {
const isSelectionField = require("../validators/operations/is-selection-field")(operations); // FIXME: This needs a more descriptive name. Selectable field?
const isSelectableField = require("../validators/operations/is-selectable-field")(operations); // FIXME: This needs a more descriptive name. Selectable field?
return function addFields(_fields) {
let [ fields ] = validateArguments(arguments, {
fields: [ required, arrayOf([ required, isSelectionField ]) ]
fields: [ required, arrayOf([ required, isSelectableField ]) ]
});
return node({ type: "addFields", fields: fields });

@ -8,7 +8,7 @@ const isCollectionName = require("../validators/is-collection-name");
const node = require("../ast-node");
module.exports = function (_operations) {
return function (_name) {
return function collection(_name) {
let [ name ] = validateArguments(arguments, {
name: [ required, isCollectionName ]
});

@ -7,11 +7,11 @@ const node = require("../ast-node");
module.exports = function (operations) {
const isCondition = require("../validators/operations/is-condition")(operations);
const isPossibleRemoteField = require("../validators/operations/is-possibly-remote-field")(operations);
const isPossiblyRemoteField = require("../validators/operations/is-possibly-remote-field")(operations);
return function expression(_options) {
let { left, condition } = validateOptions(arguments, {
left: [ required, isPossibleRemoteField ], // FIXME: allow sqlExpression and such
left: [ required, isPossiblyRemoteField ], // FIXME: allow sqlExpression and such
condition: [ required, isCondition ]
});

@ -35,6 +35,9 @@ let operations = {
}
};
// FIXME: first() and first({ optional: true })
// TODO: Boolean subquery operations (UNION, INTERSECT, EXCEPT) with {keepDuplicates} option: https://www.postgresql.org/docs/9.4/queries-union.html
let operationModules = {
// Base operations
select: require("./select"),
@ -81,7 +84,7 @@ let operationModules = {
through: require("./relations/through"),
withRelations: require("./relations/with-relations"),
define: require("./relations/define"),
resultExists: require("./relations/result-exists"), // FIXME: Better name?
producesResult: require("./relations/produces-result"),
linkTo: require("./relations/link-to"),
// Misc.
@ -98,6 +101,7 @@ let operationModules = {
indexes: require("./schema/indexes"),
deleteField: require("./schema/delete-field"),
restoreAs: require("./schema/restore-as"),
// FIXME: unsafeRestoreAsNull
optional: require("./schema/optional"),
defaultTo: require("./schema/default-to"),
@ -111,9 +115,7 @@ let operationModules = {
// Index types
primaryKey: require("./indexes/primary-key"),
index: require("./indexes/index"),
indexWhere: require("./indexes/index-where"),
unique: require("./indexes/unique"),
uniqueWhere: require("./indexes/unique-where"),
};
Object.assign(module.exports, operations);

@ -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";
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) {
return function index() {
return node({
type: "index"
const makeIndexObject = require("./_make-index-object");
module.exports = function (operations) {
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";
const node = require("../../ast-node");
const { validateArguments } = require("@validatem/core");
module.exports = function (_operations) {
return function primaryKey() {
return node({
type: "primaryKey"
const makeIndexObject = require("./_make-index-object");
module.exports = function (operations) {
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";
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) {
return function unique() {
return node({
type: "uniqueIndex"
const makeIndexObject = require("./_make-index-object");
module.exports = function (operations) {
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"
});
};
};

@ -6,7 +6,7 @@ const defaultTo = require("@validatem/default-to");
const nestedArrayOf = require("@validatem/nested-array-of");
const flatten = require("../../validators/flatten");
const node = require("../ast-node");
const node = require("../../ast-node");
module.exports = function (operations) {
const isRelationClause = require("../../validators/operations/is-relation-clause")(operations);

@ -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
});
};
};

@ -6,7 +6,7 @@ const defaultTo = require("@validatem/default-to");
const nestedArrayOf = require("@validatem/nested-array-of");
const flatten = require("../../validators/flatten");
const node = require("../ast-node");
const node = require("../../ast-node");
module.exports = function (operations) {
const isRelationClause = require("../../validators/operations/is-relation-clause")(operations);

@ -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
});
};
};

@ -5,7 +5,7 @@ const required = require("@validatem/required");
const arrayOf = require("@validatem/array-of");
const either = require("@validatem/either");
const node = require("../ast-node");
const node = require("../../ast-node");
module.exports = function (operations) {
const isObjectType = require("../../validators/operations/is-object-type")(operations);

@ -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
});
};
};

@ -4,13 +4,14 @@ const node = require("../../ast-node");
const { validateArguments } = require("@validatem/core");
const required = require("@validatem/required");
const forbidRemoteFields = require("../../validators/forbid-remote-fields");
module.exports = function (operations) {
const isLocalValueExpression = require("../../validators/operations/is-local-value-expression")(operations);
const isValueExpression = require("../../validators/operations/is-value-expression")(operations);
return function defaultTo(_value) {
let [ value ] = validateArguments(arguments, {
value: [ required, isLocalValueExpression ]
value: [ required, isValueExpression, forbidRemoteFields ] // FIXME: Forbid aggregrate functions?
});
return node({

@ -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
});
};
};

@ -7,11 +7,11 @@ const required = require("@validatem/required");
const arrayOf = require("@validatem/array-of");
module.exports = function (operations) {
const isCompositeIndexType = require("../../validators/operations/schema/is-composite-index-type")(operations);
const isNamedIndexType = require("../../validators/operations/schema/is-named-index-type")(operations);
return function indexes(_indexes) {
let [ indexes ] = validateArguments(arguments, {
indexes: [ required, arrayOf(isCompositeIndexType) ]
indexes: [ required, arrayOf(isNamedIndexType) ]
});
return node({

@ -0,0 +1,11 @@
"use strict";
const node = require("../../ast-node");
module.exports = function (_operations) {
return function optional() {
return node({
type: "optional"
});
};
};

@ -6,15 +6,15 @@ const required = require("@validatem/required");
const node = require("../../ast-node");
module.exports = function (operations) {
return function indexWhere(_expression) {
const isExpression = require("../../validators/operations/is-expression")(operations);
const isValueExpression = require("../../validators/operations/is-value-expression")(operations);
return function restoreAs(_expression) {
let [ expression ] = validateArguments(arguments, {
expression: [ required, isExpression ]
expression: [ required, isValueExpression ]
});
return node({
type: "index",
type: "restoreAs",
expression: expression
});
};

@ -6,7 +6,8 @@ module.exports = function (_operations) {
// FIXME: Length options?
return function autoID() {
return node({
type: "autoIDField"
type: "fieldType",
fieldType: "autoID"
});
};
};

@ -5,7 +5,8 @@ const node = require("../../ast-node");
module.exports = function (_operations) {
return function boolean() {
return node({
type: "booleanField"
type: "fieldType",
fieldType: "boolean"
});
};
};

@ -6,7 +6,8 @@ module.exports = function (_operations) {
// FIXME: Length options?
return function string() {
return node({
type: "stringField"
type: "fieldType",
fieldType: "string"
});
};
};

@ -6,7 +6,8 @@ module.exports = function (_operations) {
// FIXME: withTimezone option
return function timestamp() {
return node({
type: "timestampField"
type: "fieldType",
fieldType: "timestamp"
});
};
};

@ -5,7 +5,8 @@ const node = require("../../ast-node");
module.exports = function (_operations) {
return function uuid() {
return node({
type: "uuidField"
type: "fieldType",
fieldType: "uuid"
});
};
};

@ -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)
}
};

@ -175,6 +175,8 @@ module.exports = {
])
});
}
} else {
return 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`);
}
};

@ -1,9 +1,6 @@
"use strict";
const either = require("@validatem/either");
const wrapError = require("@validatem/wrap-error");
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.
// FIXME: Rename valueExpression -> value?
@ -13,8 +10,8 @@ module.exports = function (operations) {
const isObjectType = require("./is-object-type")(operations);
return either([
isValueExpression,
[ isObjectType("anyOfValues") ],
[ isObjectType("allOfValues") ]
[ isObjectType("allOfValues") ],
isValueExpression
]);
};

@ -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") ]
]);
};

@ -12,14 +12,22 @@ module.exports = function (operations) {
const isObjectType = require("./is-object-type")(operations);
const isAnyFieldObject = require("./is-any-field-object")(operations);
const isAggregrateFunction = require("./is-aggregrate-function")(operations);
const isValueFunction = require("./is-value-function")(operations);
const wrapWithOperation = require("./wrap-with-operation")(operations);
return wrapError("Must be a type of value", either([
// Subqueries
[ isObjectType("producesResult") ],
// Raw SQL
[ isObjectType("sqlExpression") ],
[ isObjectType("literalValue") ],
[ isObjectType("placeholder") ], // TODO: Verify that this also works for the `alias` method
// Functions
[ isValueFunction ], // FIXME: Is it possible for these to be invalid when collapsing, eg. when referencing non-collapsed columns?
[ isAggregrateFunction ], // FIXME: Make sure to check that this is only permitted when collapsing
// Field references
[ isAnyFieldObject ],
// Plain values
[ isObjectType("literalValue") ],
[ isObjectType("placeholder") ], // TODO: Verify that this also works for the `alias` method
[ 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";
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 a field type", either([
isObjectType("autoIDField"),
isObjectType("stringField"),
isObjectType("timestampField"),
isObjectType("booleanField"),
isObjectType("uuidField"),
]));
return wrapError("Must be a field type", isObjectType("fieldType"));
};

@ -4,24 +4,27 @@ const either = require("@validatem/either");
const wrapError = require("@validatem/wrap-error");
const anyProperty = require("@validatem/any-property");
const required = require("@validatem/required");
const ensureArray = require("@validatem/ensure-array");
const arrayOf = require("@validatem/array-of");
const isLocalFieldName = require("../../is-local-field-name");
module.exports = function (operations) {
const isObjectType = require("../is-object-type")(operations);
const isFieldType = require("./is-field-type")(operations);
const isIndexType = require("./is-index-type")(operations);
const isLocalIndexType = require("./is-local-index-type")(operations);
let isItem = either([
isFieldType,
isLocalIndexType,
isObjectType("linkTo"),
isObjectType("defaultTo"),
isObjectType("optional"),
// FIXME: Only allow deleteField/restoreAs for changeCollection, not createCollection
isObjectType("deleteField")
]);
return wrapError("Must be an object of schema fields", anyProperty({
key: [ required, isLocalFieldName ],
value: [ required, either([
isFieldType,
isIndexType,
isObjectType("belongsTo"), // FIXME: Correct type? Need to distinguish between reference and definition usage (local vs. remote field name)
isObjectType("defaultTo"),
isObjectType("optional"),
// FIXME: Only allow deleteField/restoreAs for changeCollection, not createCollection
isObjectType("deleteField"),
isObjectType("restoreAs"),
])]
value: [ required, ensureArray, arrayOf(isItem) ]
}), { preserveOriginalErrors: true });
};

@ -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"));
};

@ -934,7 +934,7 @@
"@validatem/is-array" "^0.1.0"
"@validatem/validation-result" "^0.1.1"
"@validatem/combinator@^0.1.0", "@validatem/combinator@^0.1.1":
"@validatem/combinator@^0.1.0", "@validatem/combinator@^0.1.1", "@validatem/combinator@^0.1.2":
version "0.1.2"
resolved "https://registry.yarnpkg.com/@validatem/combinator/-/combinator-0.1.2.tgz#eab893d55f1643b9c6857eaf6ff7ed2a728e89ff"
integrity sha512-vE8t1tNXknmN62FlN6LxQmA2c6TwVKZ+fl/Wit3H2unFdOhu7SZj2kRPGjAXdK/ARh/3svYfUBeD75pea0j1Sw==
@ -984,6 +984,11 @@
"@validatem/validation-result" "^0.1.2"
flatten "^1.0.3"
"@validatem/ensure-array@^0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@validatem/ensure-array/-/ensure-array-0.1.0.tgz#4903bcc557964377ad6eb060f1fe030786b982ed"
integrity sha512-OPP8BDm2PhmMfxgozVd61W9J57Irksz0IxKWbS3wQVrk/J6MPRX6oxlcCNyFDovsFGmJlIoKgfVrhQuNSo/6nQ==
"@validatem/error@^1.0.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@validatem/error/-/error-1.1.0.tgz#bef46e7066c39761b494ebe3eec2ecdc7348f4ed"
@ -1011,7 +1016,7 @@
default-value "^1.0.0"
flatten "^1.0.3"
"@validatem/is-array@^0.1.0":
"@validatem/is-array@^0.1.0", "@validatem/is-array@^0.1.1":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@validatem/is-array/-/is-array-0.1.1.tgz#fbe15ca8c97c30b622a5bbeb536d341e99cfc2c5"
integrity sha512-XD3C+Nqfpnbb4oO//Ufodzvui7SsCIW/stxZ39dP/fyRsBHrdERinkFATH5HepegtDlWMQswm5m1XFRbQiP2oQ==
@ -1141,6 +1146,11 @@
dependencies:
"@validatem/error" "^1.0.0"
"@validatem/remove-nullish-items@^0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@validatem/remove-nullish-items/-/remove-nullish-items-0.1.0.tgz#fe1a8b64d11276b506fae2bd2c41da4985a5b5ff"
integrity sha512-cs4YSF47TA/gHnV5muSUUqGi5PwybP5ztu5SYnPKxQVTyubvcbrFat51nOvJ2PmUasyrIccoYMmATiviXkTi6g==
"@validatem/require-either@^0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@validatem/require-either/-/require-either-0.1.0.tgz#250e35ab06f124ea90f3925d74b5f53a083923b0"
@ -1190,6 +1200,15 @@
default-value "^1.0.0"
split-filter-n "^1.1.2"
"@validatem/wrap-path@^0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@validatem/wrap-path/-/wrap-path-0.1.0.tgz#777998b62d3e74f2b2897c992dae9b3675161c33"
integrity sha512-6hOqydnr4u8FA0iRv8fyXxsr64T99+w/XL/fixmsgN0uqulEIwGMxCre3y9YkFNcEtysyPHkQl0CrGPcASsZxw==
dependencies:
"@validatem/annotate-errors" "^0.1.2"
"@validatem/combinator" "^0.1.2"
"@validatem/validation-result" "^0.1.2"
JSONStream@^1.0.3:
version "1.3.5"
resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0"
@ -1255,6 +1274,16 @@ ajv@^6.10.0, ajv@^6.10.2:
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
ajv@^6.12.4:
version "6.12.5"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da"
integrity sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag==
dependencies:
fast-deep-equal "^3.1.1"
fast-json-stable-stringify "^2.0.0"
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
ansi-align@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.0.tgz#b536b371cf687caaef236c18d3e21fe3797467cb"
@ -1440,6 +1469,11 @@ astral-regex@^1.0.0:
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9"
integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==
astral-regex@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==
astw@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/astw/-/astw-2.2.0.tgz#7bd41784d32493987aeb239b6b4e1c57a873b917"
@ -2270,6 +2304,11 @@ dash-ast@^1.0.0:
resolved "https://registry.yarnpkg.com/dash-ast/-/dash-ast-1.0.0.tgz#12029ba5fb2f8aa6f0a861795b23c1b4b6c27d37"
integrity sha512-Vy4dx7gquTeMcQR/hDkYLGUnwVil6vk4FOOct+djUnHOUWt+zJPJAaRIXaAFkPXtJjvlY7o3rfRu0/3hpnwoUA==
date-fns@^2.16.1:
version "2.16.1"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.16.1.tgz#05775792c3f3331da812af253e1a935851d3834b"
integrity sha512-sAJVKx/FqrLYHAQeN7VpJrPhagZc9R4ImZIWYRFZaaohR3KzmuK88touwsSwSVT8Qcbd4zoDsnGfX4GFB4imyQ==
debounce@^1.0.0, debounce@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.0.tgz#44a540abc0ea9943018dc0eaa95cce87f65cd131"
@ -2973,6 +3012,18 @@ finalhandler@~1.1.2:
statuses "~1.5.0"
unpipe "~1.0.0"
find-last-index@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/find-last-index/-/find-last-index-1.0.0.tgz#df4d76de9704b3c944f77cb27575822d1e2205c2"
integrity sha512-aPYT8fnE/EegRBn+YBf08NFtjdwCJsL7FiYmy+G3VoN0R4BJ1Q4zAmb7eEbTuASgJmeXh6s8d5+Rlmd980+Q+g==
find-last@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/find-last/-/find-last-1.0.0.tgz#75f7ede230aa19df956da021de3e799da6252207"
integrity sha512-xK6JsdhjNzFRwPiBEG4ObfWJdl8f8BF4Ve5HNmiX7PYGvadoqxONc0F7m9HSIE0pHEx9pDmdTY4CAFG1QyoPTQ==
dependencies:
find-last-index "^1.0.0"
find-up@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
@ -2981,6 +3032,14 @@ find-up@^4.1.0:
locate-path "^5.0.0"
path-exists "^4.0.0"
find-up@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc"
integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==
dependencies:
locate-path "^6.0.0"
path-exists "^4.0.0"
flat-cache@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0"
@ -3037,6 +3096,11 @@ from2@^2.0.3:
inherits "^2.0.1"
readable-stream "^2.0.0"
fromentries@^1.2.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/fromentries/-/fromentries-1.2.1.tgz#64c31665630479bc993cd800d53387920dc61b4d"
integrity sha512-Xu2Qh8yqYuDhQGOhD5iJGninErSfI9A3FrriD3tjUgV5VbJFeH8vfgZ9HnC6jWN80QDVNQK5vmxRAmEAp7Mevw==
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
@ -3655,6 +3719,11 @@ is-plain-obj@^2.0.0, is-plain-obj@^2.1.0:
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287"
integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==
is-plain-obj@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7"
integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==
is-plain-object@^2.0.3, is-plain-object@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
@ -3886,6 +3955,13 @@ locate-path@^5.0.0:
dependencies:
p-locate "^4.1.0"
locate-path@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"
integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==
dependencies:
p-locate "^5.0.0"
lodash.memoize@~3.0.3:
version "3.0.4"
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-3.0.4.tgz#2dcbd2c287cbc0a55cc42328bd0c736150d53e3f"
@ -3896,6 +3972,11 @@ lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.4:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
lodash@^4.17.20:
version "4.17.20"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
@ -3972,6 +4053,22 @@ media-typer@0.3.0:
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
merge-by-template@^0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/merge-by-template/-/merge-by-template-0.1.3.tgz#1df332b60a26bce29134ad6850edb78293ba9e26"
integrity sha512-C4/phAKvNs47OZnA/8WKLJg/G7ywllaRB9W2b6wKGCjFTMvuGO+63HalSB3MECe79W2uQ8waC0UNPPSZDZNGNg==
dependencies:
"@validatem/core" "^0.3.3"
"@validatem/default-to" "^0.1.0"
"@validatem/is-array" "^0.1.1"
"@validatem/is-plain-object" "^0.1.1"
"@validatem/remove-nullish-items" "^0.1.0"
"@validatem/virtual-property" "^0.1.0"
"@validatem/wrap-path" "^0.1.0"
default-value "^1.0.0"
fromentries "^1.2.0"
range "^0.0.3"
merge-descriptors@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
@ -4364,6 +4461,13 @@ p-limit@^2.2.0:
dependencies:
p-try "^2.0.0"
p-limit@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.0.2.tgz#1664e010af3cadc681baafd3e2a437be7b0fb5fe"
integrity sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg==
dependencies:
p-try "^2.0.0"
p-locate@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
@ -4371,6 +4475,13 @@ p-locate@^4.1.0:
dependencies:
p-limit "^2.2.0"
p-locate@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834"
integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==
dependencies:
p-limit "^3.0.2"
p-try@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
@ -4774,6 +4885,11 @@ range-parser@~1.2.1:
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
range@^0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/range/-/range-0.0.3.tgz#b5b8eb2463a516b624a563bd32b18fe89e70151b"
integrity sha1-tbjrJGOlFrYkpWO9MrGP6J5wFRs=
raw-body@2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332"
@ -5270,6 +5386,20 @@ slice-ansi@^2.1.0:
astral-regex "^1.0.0"
is-fullwidth-code-point "^2.0.0"
slice-ansi@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b"
integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==
dependencies:
ansi-styles "^4.0.0"
astral-regex "^2.0.0"
is-fullwidth-code-point "^3.0.0"
slug@^3.3.4:
version "3.3.4"
resolved "https://registry.yarnpkg.com/slug/-/slug-3.3.4.tgz#bde453bee60587505f312c68738b8df21e5d388f"
integrity sha512-VpHbtRCEWmgaZsrZcTsVl/Dhw98lcrOYDO17DNmJCNpppI6s3qJvnNu2Q3D4L84/2bi6vkW40mjNQI9oGQsflg==
snapdragon-node@^2.0.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
@ -5584,6 +5714,16 @@ table@^5.2.3:
slice-ansi "^2.1.0"
string-width "^3.0.0"
table@^6.0.3:
version "6.0.3"
resolved "https://registry.yarnpkg.com/table/-/table-6.0.3.tgz#e5b8a834e37e27ad06de2e0fda42b55cfd8a0123"
integrity sha512-8321ZMcf1B9HvVX/btKv8mMZahCjn2aYrDlpqHaBFCfnox64edeH9kEid0vTLTRR8gWR2A20aDgeuTTea4sVtw==
dependencies:
ajv "^6.12.4"
lodash "^4.17.20"
slice-ansi "^4.0.0"
string-width "^4.2.0"
term-color@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/term-color/-/term-color-1.0.1.tgz#38e192553a473e35e41604ff5199846bf8117a3a"

Loading…
Cancel
Save