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