You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

239 lines
7.2 KiB
JavaScript

"use strict";
// FIXME: Validation for all methods
// FIXME: For validation of things where there are defaults/implicits, make sure to strictly validate either the allowed operations OR the allowed implicit values (eg. `columnName` implicits should only accept strings) - these validations can be abstracted out per type of implicit
// FIXME: Upon query compilation, keep a 'stack' of operation types, to track what context we are in (eg. because values can only be parameterized in some contexts)
// FIXME: Verify that all wrapWith calls have a corresponding isObjectType arm
require("array.prototype.flat").shim();
const splitFilter = require("split-filter");
const asExpression = require("as-expression");
const { validateArguments } = require("@validatem/core");
const required = require("@validatem/required");
const nestedArrayOf = require("@validatem/nested-array-of");
const either = require("@validatem/either");
const isString = require("@validatem/is-string");
const isFunction = require("@validatem/is-function");
const defaultTo = require("@validatem/default-to");
const anyProperty = require("@validatem/any-property");
const node = require("../ast-node");
const flatten = require("../validators/flatten");
const evaluateCyclicalModulesOnto = require("../evaluate-cyclical-modules-onto");
// FIXME: Hack until all operations have been moved over to modules
const isPossiblyForeignColumn = require("../validators/operations/is-possibly-foreign-column")(module.exports);
const isSelectClause = require("../validators/operations/is-select-clause")(module.exports);
const isObjectType = require("../validators/operations/is-object-type")(module.exports);
function normalizeClauses(clauses) {
if (clauses != null) {
return clauses.flat(Infinity);
} else {
return clauses;
}
}
// FIXME: All of the below need to be refactored, and moved into operation modules
let operations = {
withRelations: function (relations) {
// FIXME: Default relation types for literal column names and table.column syntax
// FIXME: Flesh this out further
// Main types: hasMany, belongsTo; hasOne is just hasMany with a 'single' constraint; simplify hasMany to 'has'?
// through relation is actually hasMany(through(...)) -- how to make `through` itself composable?
// test case for composable through: user -> membership -> usergroup -> community
// should fold composed throughs in compiler step
return node({
type: "withRelations",
relations: Object.entries(relations).map(([ key, relation ]) => {
return {
key: key,
relation: normalizeRelation(relation)
};
})
});
},
withDerived: function (_derivations) {
let [ derivations ] = validateArguments(arguments, {
derivations: [ required, anyProperty({
key: [ required, isString ], // FIXME: Verify that this is not a foreign column name?
value: [ required, either([
[ isFunction ],
[ isObjectType("sqlExpression") ]
])]
})]
});
let derivationEntries = Object.entries(derivations);
let [ functionTransforms, sqlTransforms ] = splitFilter(derivationEntries, ([ _key, value ]) => {
return (typeof value === "function");
});
let postProcessClauses = asExpression(() => {
return functionTransforms.map(([ key, handler ]) => {
return module.exports.postProcess((results) => {
return results.map((result) => {
return {
... result,
[key]: handler(result)
};
});
});
});
});
let columnClause = asExpression(() => {
if (sqlTransforms.length > 0) {
return module.exports.addColumns(sqlTransforms.map(([ key, expression ]) => {
return module.exports.alias(key, expression);
}));
}
});
return [
postProcessClauses,
columnClause
].filter((clause) => clause != null);
},
has: function (_column, _options) {
let [ column, { query }] = validateArguments(arguments, {
column: [ required, isPossiblyForeignColumn ],
options: [ defaultTo({}), {
query: [ defaultTo([]), nestedArrayOf(isSelectClause), flatten ]
}]
});
// column: string or columnName or foreignColumnName
// query: array of clauses
return node({
type: "has",
column: normalizePossiblyRemoteColumnName(column),
clauses: normalizeClauses(query)
});
},
belongsTo: function (column, { query } = {}) {
// column: string or columnName or foreignColumnName
// query: array of clauses
return node({
type: "belongsTo",
column: normalizePossiblyRemoteColumnName(column),
clauses: normalizeClauses(query)
});
},
// FIXME: Refactor below
through: function (relations) {
// relations: array of has/belongsTo or string or columnName or foreignColumnName
return node({
type: "through",
relations: relations.map(normalizeRelation)
});
}
};
let operationModules = {
// Base operations
select: require("./select"),
// Column selection
addColumns: require("./add-columns"),
onlyColumns: require("./only-columns"),
alias: require("./alias"),
// Reference/scalar types
column: require("./column"),
foreignColumn: require("./column"),
table: require("./table"),
value: require("./value"),
// Filtering
where: require("./where"),
expression: require("./expression"),
// Predicate lists/combinators
allOf: require("./all-of"),
anyOf: require("./any-of"),
// Conditions
equals: require("./equals"),
lessThan: require("./less-than"),
moreThan: require("./more-than"),
not: require("./not"),
// Collapsing/grouping
collapseBy: require("./collapse-by"),
hierarchical: require("./hierarchical"),
// Computation
compute: require("./compute"),
// Aggregrate functions
count: require("./count"),
sum: require("./sum"),
// Misc.
parameter: require("./parameter"),
postProcess: require("./post-process"),
unsafeSQL: require("./unsafe-sql"),
};
Object.assign(module.exports, operations);
evaluateCyclicalModulesOnto(module.exports, operationModules);
// module.exports = {
// ... operations,
// ... evaluateCyclicalModules(operationModules)
// };
// function normalizeNotExpression(input) {
// if (input == null || typeOf(input) === "condition") {
// return input;
// } else {
// return module.exports.equals(input);
// }
// }
// function normalizePossiblyRemoteColumnName(input) {
// // FIXME: Validation
// if (typeof input === "object") {
// // FIXME: Better check
// return input;
// } else if (input != null) {
// if (input.includes(".")) {
// return module.exports.foreignColumn(input);
// } else {
// return module.exports.column(input);
// }
// } else {
// return input;
// }
// }
// function normalizeRelation(input) {
// // FIXME: Validation
// // accept columnName, foreignColumnName, string with or without dot
// if (typeof input === "object" && [ "has", "belongsTo", "through" ].includes(typeOf(input))) {
// return input;
// } else {
// let columnName = (typeof input === "string")
// ? normalizePossiblyRemoteColumnName(input)
// : input;
// if (typeOf(columnName) === "columnName") {
// return module.exports.belongsTo(input);
// } else if (typeOf(columnName) === "foreignColumnName") {
// return module.exports.has(input);
// } else {
// unreachable(`Invalid type: ${typeOf(columnName)}`);
// }
// }
// }
// NOTE: normalizeExpression should sometimes only accept sql/literal, but sometimes also sql/literal/condition?