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