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.
225 lines
7.1 KiB
JavaScript
225 lines
7.1 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");
|
|
|
|
// MARKER: Modularizing operations (and eventually also AST stringification?)
|
|
const flatten = require("../validators/flatten");
|
|
|
|
// 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 = {
|
|
addColumns: require("./add-columns"),
|
|
alias: require("./alias"),
|
|
allOf: require("./all-of"),
|
|
anyOf: require("./any-of"),
|
|
column: require("./column"),
|
|
equals: require("./equals"),
|
|
expression: require("./expression"),
|
|
foreignColumn: require("./column"),
|
|
lessThan: require("./less-than"),
|
|
moreThan: require("./more-than"),
|
|
not: require("./not"),
|
|
onlyColumns: require("./only-columns"),
|
|
parameter: require("./parameter"),
|
|
postProcess: require("./post-process"),
|
|
select: require("./select"),
|
|
table: require("./table"),
|
|
unsafeSQL: require("./unsafe-sql"),
|
|
value: require("./value"),
|
|
where: require("./where"),
|
|
};
|
|
|
|
function evaluateCyclicalModulesOnto(resultObject, moduleMapping) {
|
|
// let resultObject = {}; // FIXME: Uncomment after refactoring is complete
|
|
|
|
for (let [ key, moduleInitializer ] of Object.entries(moduleMapping)) {
|
|
resultObject[key] = moduleInitializer(resultObject);
|
|
}
|
|
|
|
return resultObject;
|
|
}
|
|
|
|
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?
|