"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?