master
Sven Slootweg 4 years ago
commit 2eb636924c

@ -0,0 +1,3 @@
{
"extends": "@joepie91/eslint-config"
}

1
.gitignore vendored

@ -0,0 +1 @@
node_modules

@ -0,0 +1,30 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "attach",
"name": "Attach to Process",
"address": "localhost",
"port": 9229,
"localRoot": "${workspaceFolder}",
"remoteRoot": "${workspaceFolder}",
"restart": true,
"skipFiles": [
"<node_internals>/**"
],
},
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"program": "${workspaceFolder}/.bin/nodemon ${workspaceFolder}/experiments/raqb-concepts.js",
"skipFiles": [
"<node_internals>/**"
],
}
]
}

@ -0,0 +1 @@
./node_modules/.bin/nodemon --inspect-brk experiments/raqb-concepts.js

@ -0,0 +1,5 @@
# The experiments
This directory contains various experiments in API design (and sometimes code to test them out) that I wrote up over a couple of years, in the process of trying to determine the right API design for raqb. They are kept here for historical purposes; it might give some insight into the design process.
Most of this code won't actually be runnable, and __none of it is production-ready code, nor any kind of recommendation__. Definitely don't copy-paste code from here!

@ -0,0 +1,144 @@
"use strict";
let expressions = [
function foo({id, other_prop}) {
return (other_prop < 34 && other_prop >= 2 && id !== "foo");
},
(data) => {
let id = data.id;
let someProp = data.prop;
let otherProp = someProp;
if (id !== "foo") {
if (someProp < 34) {
return (otherProp >= 2);
} else {
return false;
}
}
},
({foo: bar}) => bar > 4,
function (data) {
let {foo} = data;
return (foo < 4);
},
42,
"bar"
];
const util = require("util");
const acorn = require("acorn");
const estreeAssignParent = require("estree-assign-parent");
const scopeAnalyzer = require("scope-analyzer");
const astw = require("astw");
function logAst(ast, depth = 7) {
return util.inspect(ast, {colors: true, depth: depth});
}
function analyzeFunction(func) {
let source = func.toString();
console.log(source);
console.log("");
/* The below is a hack to make anonymous functions parse correctly. */
let ast = acorn.parse(`[${source}]`);
let functionAst = ast.body[0].expression.elements[0];
estreeAssignParent(functionAst);
scopeAnalyzer.crawl(functionAst);
let functionArg = functionAst.params[0];
if (functionArg.type === "ObjectPattern") {
for (let property of functionArg.properties) {
// key = from prop, value = to var
let sourcePropertyName = property.key.name;
property.value._sourceProp = sourcePropertyName;
}
} else if (functionArg.type === "Identifier") {
functionArg._dataSource = true;
} else {
throw new Error(`Encountered unrecognized function argument type: ${functionArg.type}`);
}
let walk = astw(functionAst);
walk((node) => {
if (node.type === "VariableDeclarator") {
if (node.id.type === "ObjectPattern") {
if (node.init.type === "Identifier" && scopeAnalyzer.getBinding(node.init).definition._dataSource === true) {
for (let property of node.id.properties) {
property.value._sourceProp = property.key.name;
}
}
} else if (node.id.type === "Identifier") {
if (node.init.type === "MemberExpression" && scopeAnalyzer.getBinding(node.init.object).definition._dataSource === true) {
node.id._sourceProp = node.init.property.name;
} else if (node.init.type === "Identifier") {
let definition = scopeAnalyzer.getBinding(node.init).definition;
if (definition._sourceProp != null) {
node.id._sourceProp = definition._sourceProp;
}
}
} else {
throw new Error(`Encountered unrecognized assignment id type: ${node.id.type}`);
}
}
});
let walkBody = astw(functionAst.body);
function getDataProperty(node) {
if (node.type === "Identifier" && node.parent.type !== "MemberExpression") {
let binding = scopeAnalyzer.getBinding(node);
if (binding != null) {
let definition = binding.definition;
if (definition != null) {
if (definition._dataSource !== true) {
if (definition._sourceProp != null) {
return definition._sourceProp;
} else {
throw new Error(`Could not connect variable '${node.name}' to a data property`);
}
}
} else {
throw new Error(`Encountered undefined variable: ${node.name}`);
}
} else {
throw new Error(`Encountered undefined variable: ${node.name}`);
}
} else {
throw new Error("Node is not a free-standing (variable) identifier");
}
}
walkBody((node) => {
if (node.type === "Identifier" && node.parent.type !== "MemberExpression") {
let sourceProperty = getDataProperty(node);
if (sourceProperty != null) {
console.log(`Variable '${node.name}' is derived from data property '${sourceProperty}'`);
}
}
});
// console.log(util.inspect(functionAst, {depth: 7, colors: true}));
}
for (let expression of expressions) {
if (typeof expression === "function") {
let result = analyzeFunction(expression);
// console.log(expression.toString());
} else {
console.log(`!! ${expression} is not a function`);
}
console.log("\n=\n");
}

@ -0,0 +1,30 @@
"use strict";
let pluginLimit = {
name: "npm:raqb:limit",
methods: {
limit: {
applyBuilder: function (query, [ limit ]) {
return query.internalSingleState({ limit: limit });
},
prepareQueryOnce: (query) => {
}
}
}
};
let pluginFirst = {
name: "npm:raqb:first",
methods: {
first: {
applyBuilder: function (query) {
return query.limit(1);
},
processResult: (result) => {
return result[0];
}
}
}
};

@ -0,0 +1,79 @@
db("networks").where({
user_id: userId
}).with({
tabs: db("tabs")
.onField("network_id")
.with({
$merge: db("channel_events")
.onOwnField("channel_id")
})
db.combine([
db("channels")
.onField("network_id")
.with({
events: db("channel_events")
.onField("channel_id")
.with({
notifications: db("notifications").onField("channel_events_id")
})
}),
db("private")
])
})
/* FIXME: tagging for (potentially nested) query modifications for eg. batching */
db("networks")
.where({ user_id: userId })
.withMany("tabs", ({$db, id}) => {
return $db("tabs")
.where({ network_id: id })
.withOne("channel_data", ({$db, type, channel_id}) => {
if (type === "channel") {
return $db("channels")
.where({ id: channel_id })
}
})
.withMany("events", ({$db, type, channel_id, nickname}) => {
if (type === "channel") {
return $db("presences")
.where({
channel_id: channel_id,
user_id: userId
})
.flatten(({$db, first_event, last_event}) => {
return $db("channel_events")
.where({ channel_id: channel_id })
.where(({id}) => id >= first_event && id <= last_event);
});
} else if (type === "privateSession") {
return $db("private_messages")
.where({
user_id: userId,
nickname: nickname
});
}
})
})
/*
# Relation operations
.withOne - subquery for each item, one result, store as property
.withMany - subquery for each item, many results, store as property
.merge - subquery for each item, one result, merge into item
.flatten - subquery for each item, many results, replace item set with combined subquery result sets (can be done recursively)
# Query phases
Compilation - turn query into structured data, parse callbacks, etc.
Validation - verify that all of the referenced tables/columns/etc. exist in the DB
Scheduling - turn into underlying SQL queries
Execution - run the query
Any modification to a query (which is immutable) produces a new query, and existing compilation/validation/scheduling results do not carry over!
Calling .execute(db/tx) directly on a built query will go through all four phases automatically; the developer may also choose to pre-compile their query with .compile(), and pre-validate it by calling .validate(db/tx), and then finally pre-schedule it by calling .schedule(). Alternatively, they can call .prepare(db/tx) which does all of those three steps?
How to capture user-specified variables from outer scope? Also should wrap parsed callbacks within arrow function wrapper instead of array, to pass in variable definitions from outer callbacks.
*/

@ -0,0 +1,113 @@
"use strict";
const util = require("util");
let { select, onlyColumns, where, withRelations, withDerived, column, through, inValues, sql, postProcess } = require("../operations");
function withOwner() {
return withRelations({ owner: "owner_id" });
}
// TODO: Allow specifying placeholders that can be filled in later, for truly reusable query builders (so that eg. query planning only needs to be done once, this could even use prepared statements under the hood)
let query = select("projects", [
onlyColumns([ "id", "name" ]),
where({
active: true,
visible: true,
primary_category_id: inValues([ 2, 3, 5, 7, 8 ])
}),
// FIXME: where pivot table entry exists for category in that list
withRelations({
primaryCategory: belongsTo({
column: "primary_category_id",
query: [ withOwner() ]
}),
categories: through({
path: [ "projects_categories.project_id", "category_id" ],
query: [ // Optional extra clauses for the query on the pivot table, eg. for filtering entries
where({ adminApproved: true })
]
}),
// all user groups for a given project ID -> all memberships for the given user group IDs -> for each membership, the record referenced by the given user_id
users: through([ "user_groups.project_id", "membership.user_group_id", "user_id" ]),
// ... expands to ...
users: through([
has(foreignColumn({ table: "user_groups", column: "project_id" })),
has(foreignColumn({ table: "memberships", column: "user_group_id" })),
belongsTo(column("project_id")),
]),
owner: "owner_id",
// ... expands to
owner: belongsTo({ column: "owner_id" }),
releases: "releases.project_id",
// ... expands to ...
releases: has({ column: "releases.project_id" })
// primaryCategory: [
// column("primary_category_id"),
// withOwner()
// ],
// categories: has(through({
// table: "projects_categories",
// localSide: "project_id",
// remoteSide: "category_id",
// })),
// users: has(through({
// table: "user_groups",
// localSide: "project_id",
// remoteSide: through({
// table: "memberships",
// localSide: "user_group_id",
// remoteSide: "user_id"
// })
// })),
// categories: [
// through({
// table: "projects_categories",
// localSide: "project_id",
// remoteSide: "category_id",
// query: [ // Optional extra clauses for the query on the pivot table, eg. for filtering entries
// where({ adminApproved: true })
// ]
// }),
// withOwner()
// ],
}),
withDerived({
capitalizedName: sql("UPPER(name)"),
teamCount: sql("moderator_count + admin_count"),
nameDistance: (project) => wordDistanceAlgorithm(project.name, "someReferenceName") // NOTE: This could have returned a Promise!
}),
mapCase({ from: "snake", to: "camel" })
]);
console.log(util.inspect(query, { depth: null, colors: true }));
// FIXME: Pre-processing (eg. inverse case-mapping for inserted objects or WHERE clauses) - maybe pre-processing that subscribes to particular operations? Something along the lines of axios interceptors perhaps
// FIXME: `either`/`all` for OR/AND representation respectively?
// FIXME: I guess `withDerived` could be implemented externally, as something that (depending on value type) either a) adds a column selector or b) adds a post-processing hook
// FIXME: Aggregrates (GROUP BY)
// return db.execute(query);
/* Hypothetical `mapCase` implementation */
function mapObjectCase(object, from, to) {
return mapObject(object, (key, value) => {
return [
caseMapper(key, from, to),
value
];
});
}
function mapCase({ from, to }) {
return postProcess((results) => {
// NOTE: This could have returned a Promise!
return results.map((result) => mapObjectCase(result, from, to));
});
}

@ -0,0 +1,200 @@
"use strict";
const util = require("util");
let { select, onlyColumns, where, withRelations, withDerived, column, through, inValues, sql, postProcess, belongsTo, has, value, parameter, not, anyOf, allOf, lessThan, moreThan, alias, foreignColumn, table, expression, equals } = require("../src/operations");
const astToQuery = require("../src/ast-to-query");
Error.stackTraceLimit = Infinity;
function withOwner() {
return withRelations({ owner: "owner_id" });
}
// FIXME: Mark AST nodes with a special marker -- and disallow these from being interpreted as a WHERE object!
// FIXME: Figure out composability for something like connect-session-knex
console.time("queryGen");
////// VALIDATION TESTING START ///////
let query;
// Assume that the below numbers were generated by some external code somehow, and we don't know upfront which one is going to be lowest
let a = 523;
let b = 62;
let c = 452;
// let flooz = { a: anyOf };
try {
// query = expression({
// left: "foo",
// condition: equals("bar")
// });
query = select("projects", [
where({
// foo: anyOf([ "bar", "baz", anyOf([ "bar2", "baz2" ]), sql("TRUE") ]),
// qux: anyOf([ 13, moreThan(42) ]),
complex: anyOf([
30,
40,
allOf([
moreThan(100),
lessThan(200),
lessThan(parameter("max"))
])
])
}),
where({ second: 2 })
]);
// query = select("projects", [
// onlyColumns([
// "foo",
// alias("bar", 42),
// alias("baz", sql("foo"))
// ]),
// where(anyOf([
// { foo: "bar", qux: anyOf([ "quz", "quy" ]) },
// { baz: lessThan(42) }
// ]))
// ]);
/* {
query: 'SELECT foo, ? AS bar, foo AS baz FROM projects WHERE foo = ? OR baz < ?;',
params: [ 42, 'bar', 42 ],
placeholders: []
} */
// query = select("projects", [
// onlyColumns([ "id", "name" ]),
// where({
// active: true,
// visible: true,
// // primary_category_id: anyOf([ 2, 3, 5, 7, 8 ])
// // primary_category_id: anyOf(parameter("categoryIDs"))
// primary_category_id: not(anyOf(parameter("categoryIDs"))) // FIXME/MARKER: This gets stringified wrong!
// }),
// // FIXME: where pivot table entry exists for category in that list
// withRelations({
// primaryCategory: belongsTo("primary_category_id", { query: [ withOwner() ] }),
// categories: through([
// has("projects_categories.project_id", { query: [
// // Optional extra clauses for the query on the pivot table, eg. for filtering entries
// where({ adminApproved: true })
// ]}),
// "category_id"
// ]),
// // all user groups for a given project ID -> all memberships for the given user group IDs -> for each membership, the record referenced by the given user_id
// users: through([ "user_groups.project_id", "membership.user_group_id", "user_id" ]),
// // ... expands to ...
// // users: through([
// // has({ column: foreignColumn({ table: "user_groups", column: "project_id" }) }),
// // has({ column: foreignColumn({ table: "memberships", column: "user_group_id" }) }),
// // belongsTo({ column: column("user_id") }),
// // ]),
// owner: "owner_id",
// // ... expands to
// // owner: belongsTo({ column: "owner_id" }),
// releases: "releases.project_id",
// // ... expands to ...
// // releases: has({ column: "releases.project_id" })
// }),
// withDerived({
// capitalized_name: sql("UPPER(name)"),
// team_count: sql("moderator_count + admin_count"),
// // fourty_two: value(42), // This makes no sense in withDerived!
// name_distance: (project) => wordDistanceAlgorithm(project.name, "someReferenceName") // NOTE: This could have returned a Promise!
// }),
// mapCase({ from: "snake", to: "camel" })
// ]);
console.timeEnd("queryGen");
console.log(util.inspect(query, { depth: null, colors: true }));
console.log("");
console.log(util.inspect(astToQuery(query), { depth: null, colors: true }));
} catch (error) {
// console.error(error.message);
// console.error("");
if (error.name === "AggregrateValidationError") {
console.error(error.stack);
console.error("# Inputs:", util.inspect(error.errors.map((error) => {
let stringifiedPath = error.path.join(" -> ");
return [ stringifiedPath + ":", error.value ];
}), { colors: true, depth: null }));
process.exit(1);
} else {
throw error;
}
}
////// VALIDATION TESTING END ///////
// TODO: Allow specifying placeholders that can be filled in later, for truly reusable query builders (so that eg. query planning only needs to be done once, this could even use prepared statements under the hood)
// let query = select("projects", [
// where(allOf([
// { score: not(lessThan(anyOf([ a, b, parameter("score") ]))) },
// anyOf([
// { active: true },
// { always_show: true }
// ])
// ]))
// ]);
// let query = select("projects", [
// where({
// active: true,
// // score: not(lessThan(anyOf([ a, b, c ])))
// // MARKER: Implement parameter tracking, so that the below can be made to work (needs a `placeholder` query gen handler that just returns parameter metadata or something, and this should be used elsewhere as well, and the anyOf/allOf code should be updated to also check for placeholders, not just SQL expressions)
// score: not(lessThan(anyOf([
// a,
// b,
// sql("44 + 33"),
// parameter("foo")
// ])))
// })
// ]);
// FIXME: Pre-processing (eg. inverse case-mapping for inserted objects or WHERE clauses) - maybe pre-processing that subscribes to particular operations? Something along the lines of axios interceptors perhaps
// FIXME: `either`/`all` for OR/AND representation respectively?
// FIXME: I guess `withDerived` could be implemented externally, as something that (depending on value type) either a) adds a column selector or b) adds a post-processing hook
// FIXME: Aggregrates (GROUP BY)
// return db.execute(query);
/* Hypothetical `mapCase` implementation */
function mapObjectCase(object, from, to) {
return mapObject(object, (key, value) => {
return [
caseMapper(key, from, to),
value
];
});
}
function mapCase({ from, to }) {
return postProcess((results) => {
// NOTE: This could have returned a Promise!
return results.map((result) => mapObjectCase(result, from, to));
});
}

@ -0,0 +1,7 @@
https://github.com/rich-harris/magic-string
https://github.com/goto-bus-stop/transform-ast
https://github.com/goto-bus-stop/scope-analyzer
usage: https://github.com/browserify/static-module/blob/master/index.js#L185
bundling/optimization
https://github.com/choojs/bankai

@ -0,0 +1,5 @@
"use strict";
module.exports = {
};

@ -0,0 +1,142 @@
Todo:
- Operators
- BETWEEN
- LIKE/ILIKE?
- ISNULL / NOTNULL
- IS ...
- Condition modifiers
x allOf
x anyOf
x not
- Boolean logic
x anyOf(...)
x allOf(...)
Docs:
- Instruct users to report it if raqb generates invalid SQL, as this is considered a bug in raqb, regardless of the input (except when it's caused by `unsafeSQL`)
Considerations:
- Disallow non-lowercase column/table names entirely due to PostgreSQL weirdness? Or just cast them to lowercase?
----
foo = ANY (subquery)
foo = ANY (array)
foo = IN (subquery)
foo = IN (a, b, c)
alias: SOME -> ANY
----
Recommendations to make:
- Express moreThanOrEqualTo as not(lessThan(...))
- Express lessThanOrEqualTo as not(moreThan(...))
----
Ideas:
- Make placeholders typed so that we can warn the user when they try to pass in invalid things?
----
Things to check:
- How are NULLs handled in all cases/permutations? Is that handling correct?
- Maybe rename `sqlExpression` to `sqlFragment` or so, to avoid confusion with `expression`? Or is it `expression` that needs to be renamed to something else, like `predicate` or `assertion`?
----
Boolean operations:
_,
not {
equals,
moreThan,
lessThan {
_,
anyOf,
allOf {
ARRAY,
PLACEHOLDER<ARRAY>
}
LITERAL,
PLACEHOLDER<LITERAL>
}
}
Considerations:
- Allow nested not() modifiers? For composability with things that use not() themselves
{ foo: anyOf([ 30, moreThan(42) ]) } -> WHERE foo = 30 OR foo > 42
{ foo: anyOf([ 30, 42 ]) } -> WHERE foo = ANY(ARRAY[30, 42])
{ foo: anyOf([ moreThan(30), moreThan(42) ]) } -> WHERE foo > ANY(ARRAY[30, 42])
Group into condition types, and based on the count of each, determine how to represent that particular type, and then glue the types together
eg. WHERE foo > ANY(ARRAY[30, 42]) OR foo < 42 OR foo = ANY(ARRAY[30, 42])
Make $or/$and abstraction to make this gluing easier in multiple places
----
MARKER: Relation resolution
- Generate relation queries after executing their base query, because by then we have access to the necessary schema information (as we are in the execution phase)
- Eventually replace this with a mechanism where relation queries can be pre-generated, with special placeholder markers to be filled in later, to:
a) aid static analysis
b) allow alternative relation resolving implementations to queue follow-up queries with (arbitrary?) to-be-filled-in details
- Maybe use ["foo", placeholder, "bar"] format instead of a single string, when generating the query? That would be a bit hacky...
- Implement various kinds of JOINs with table-prefixed resultset column names, those column names being determined at query execution time
- Figure out a way to represent this in the pre-generated query, maybe the same mechanism as the relation query placeholder mechanism above?
- Different API designs for different JOIN types? eg. a `with(otherTableQuery)` for left joins, something similar for right joins, and a `combine(tableAQuery, tableBQuery)` for full joins and inner joins
- Implement through relation queries with the JOIN mechanism
- AND/OR as `all()` and `any()`
- Can also fix the placeholder thing by having a separarate kind of nodePlaceholder that gets filled in with a query node; and then only doing the astToQuery operation at the very last moment, after state has been collected from the database and all of those nodes can be filled in
- In this case, there should probably be a way to provide 'global' values for nodePlaceholders (applicable to the query + any derived ones like relations) and local ones (just the query itself)? Maybe this is already solved by using strings vs. symbols for the placeholder names? That would work the same as with normal placeholders, and therefore be consistent implementation-wise
-----------
Generic AST optimization infrastructure instead of inline modifications in the stringification phase
Pipeline would then look like:
Build AST -> Optimize AST -> Compile to SQL
Three AST optimization purposes:
- Normalization (required)
- Readability (optional)
- Performance (optional)
--------
Renames:
- remote -> foreign
- remoteSide? -> foreignSide
- sql -> unsafeSQL
Transforms should ideally be cached for a query, maybe auto-do this for top-level constructs like SELECT, so that a full query is immediately performantly reusable?
How to deal with such queries being specified as sub-expressions then? Maybe cross-query optimization needs to happen?
FIXME: Document the design for dealing with the potentially-cyclical operations dependencies within the module
FIXME: Linter rule for verifying that all requires inside of the affected folders (operations/ and validators/operations/) correctly pass in operations, to avoid polluting the AST with Validatem validators
Walker design:
- Allow a visitor to place a 'marker' on a given node, that can be referenced back to from another, more deeply-nested visitor (referencing the nearest, a la React context) - this is useful for hoisting things up in the AST. Need to figure out how to combine this with immutable AST walking, though, without pulling the carpet from under the currently-walked subtree.
- Possibly Babel's walker implementation might have a solution to this category of problem?
- Possibly cursors are a useful concept to use here? As mutable pointers to otherwise immutable data. This might cause unexpected behaviour though, with different items being walked than currently (newly) exist in the tree, as the walker would still be operating on the old tree -- unless a change higher-up would trigger a re-walk of that entire subtree, which would require all optimizations to be idempotent.
- Make sure to iterate again from whatever new AST subtree was returned from a visitor! So that other visitors get a chance.
- How to deal with a case where in notExpression(foo), foo returns a notExpression(bar), and the notExpression deduplicator does not happen anymore because the top-most notExpression was already previously visited? Maybe re-run the whole sequence of visitors repeatedly until the entire tree has stabilized? This also requires idempotence of visitors! And makes persisting/caching processed query ASTs even more important.
-------
Planned AST optimization phases (loosely ordered):
- Combine multiple `where(...)` into a single `where(allOf(...))`
- Flatten nested notCondition
- Flatten nested same-typed allOfCondition/anyOfCondition
- Group >1 same-typed conditionTypes into a single condition(_internalArray(...))
- Invert condition lists into expression lists
- Flatten nested notExpression
- Flatten nested same-typed allOfExpression/anyOfExpression
-----
MARKER:
- Refactor relation operations to new design
- Implement AST optimization infrastructure (incl. solving the immutable reference problem?)

@ -0,0 +1,49 @@
{
"name": "raqb",
"version": "1.0.0",
"main": "index.js",
"repository": "git@git.cryto.net:joepie91/raqb.git",
"author": "Sven Slootweg <admin@cryto.net>",
"license": "MIT",
"dependencies": {
"@validatem/allow-extra-properties": "^0.1.0",
"@validatem/any-property": "^0.1.3",
"@validatem/array-of": "^0.1.2",
"@validatem/core": "^0.3.3",
"@validatem/default-to": "^0.1.0",
"@validatem/either": "^0.1.3",
"@validatem/error": "^1.0.0",
"@validatem/forbidden": "^0.1.0",
"@validatem/has-shape": "^0.1.7",
"@validatem/is-boolean": "^0.1.1",
"@validatem/is-date": "^0.1.0",
"@validatem/is-function": "^0.1.0",
"@validatem/is-number": "^0.1.2",
"@validatem/is-plain-object": "^0.1.1",
"@validatem/is-string": "^0.1.1",
"@validatem/is-value": "^0.1.0",
"@validatem/matches-format": "^0.1.0",
"@validatem/nested-array-of": "^0.1.0",
"@validatem/required": "^0.1.1",
"@validatem/wrap-error": "^0.1.2",
"acorn": "^6.0.5",
"array.prototype.flat": "^1.2.3",
"as-expression": "^1.0.0",
"assure-array": "^1.0.0",
"astw": "^2.2.0",
"default-value": "^1.0.0",
"estree-assign-parent": "^1.0.0",
"flatten": "^1.0.3",
"map-obj": "^4.1.0",
"match-value": "^1.1.0",
"scope-analyzer": "^2.0.5",
"split-filter": "^1.1.3",
"split-filter-n": "^1.1.2",
"syncpipe": "^1.0.0"
},
"devDependencies": {
"@joepie91/eslint-config": "^1.1.0",
"eslint": "^7.3.1",
"nodemon": "^2.0.4"
}
}

@ -0,0 +1,11 @@
"use strict";
module.exports = function createASTNode(object) {
// NOTE: Mutates!
Object.defineProperty(object, "__raqbASTNode", {
enumerable: false,
value: true
});
return object;
};

@ -0,0 +1,483 @@
"use strict";
// FIXME: Make sure to account for placeholder nodes *within* arrays passed as single parameters, in query optimization/compilation/execution!
require("array.prototype.flat").shim(); // FIXME: Use `flatten` method instead
const splitFilterN = require("split-filter-n");
const asExpression = require("as-expression");
const defaultValue = require("default-value");
const matchValue = require("match-value");
const syncpipe = require("syncpipe");
const flatten = require("flatten");
const operations = require("./operations");
const concat = require("./concat");
const typeOf = require("./type-of");
const merge = require("./merge");
const unreachable = require("./unreachable");
const isLiteralValue = require("./validators/is-literal-value");
let NoQuery = Symbol("NoQuery");
// NOTE: A function or variable/property starting with a $, signifies that it returns/contains an (array of) query objects, like { query, params }
const { validateArguments, validateOptions, RemainingArguments } = require("@validatem/core");
const required = require("@validatem/required");
const either = require("@validatem/either");
const arrayOf = require("@validatem/array-of");
const isString = require("@validatem/is-string");
const isValue = require("@validatem/is-value");
const allowExtraProperties = require("@validatem/allow-extra-properties");
let isPlaceholder = {
__raqbASTNode: isValue(true),
type: isValue("placeholder"),
name: isString
};
// FIXME: Do we want a nested array here? Probably not, since PostgreSQL might not support arbitrarily nested arrays
let isValidParameter = either([
isLiteralValue,
isPlaceholder,
arrayOf(either([
isLiteralValue,
isPlaceholder
])),
]);
let is$ObjectParameters = {
query: [ isString ],
params: arrayOf(isValidParameter),
placeholders: [ arrayOf([ required, isString ]) ]
};
let is$Object = [
is$ObjectParameters, {
query: [ required ],
params: [ required ],
placeholders: [ required ]
}
];
function $object({ query, params, placeholders }) {
validateOptions(arguments, is$ObjectParameters);
return {
query: defaultValue(query, ""),
params: defaultValue(params, []),
placeholders: defaultValue(placeholders, [])
};
}
function $join(joiner, items) {
validateArguments(arguments, {
joiner: [ required, isString ],
items: [ required, arrayOf(is$Object) ]
});
return $object({
query: items
.map((item) => item.query)
.join(joiner),
params: items
.map((item) => item.params)
.flat(),
placeholders: items
.map((item) => item.placeholders)
.flat()
});
}
function $joinWhereClauses(joiner, clauses) {
let $handledClauses = clauses.map((clause) => {
console.log("CONSIDERING CLAUSE", require("util").inspect(clause, { colors: true, depth: null }));
let isSimpleClause = (typeOf(clause) === "expression");
let $handled = $handle(clause);
if (isSimpleClause) {
return $handled;
} else {
return $parenthesize($handled);
}
});
return $join(joiner, $handledClauses);
}
function $or(clauses) {
return $joinWhereClauses(" OR ", clauses);
}
function $and(clauses) {
return $joinWhereClauses(" AND ", clauses);
}
function $handleAll(nodes) {
return nodes.map((node) => $handle(node));
}
function $combine(_strings, ... _nodes) {
let [ strings, nodes ] = validateArguments(arguments, {
strings: [ required, arrayOf(isString) ],
[RemainingArguments]: [ required, arrayOf([
required,
either([ is$Object, isValue(NoQuery) ])
])]
});
// FIXME: Make or find template literal abstraction, to make this more readable and declarative
// Maybe something that passes in a single array of interspersed strings/nodes with a property to indicate which each value is?
let query = "";
let params = [];
let placeholders = [];
nodes.forEach((node, i) => {
query += strings[i];
// NOTE: Uses a NoQuery symbol to indicate "don't generate anything here", to avoid interpreting accidental nulls as intentional omissions
if (node !== NoQuery ) {
query += node.query;
// FIXME: Investigate whether the below is faster with concat
params.push(... node.params);
placeholders.push(... node.placeholders);
}
});
query += strings[strings.length - 1];
return $object({ query, params, placeholders });
}
function columnList({ onlyColumns, addColumns }) {
let primaryColumns = (onlyColumns.length > 0)
? onlyColumns
: [ { type: "allColumns" } ];
return $join(", ", $handleAll(concat([ primaryColumns, addColumns ])));
}
function $parenthesize(query) {
return $combine`(${query})`;
}
function $maybeParenthesize($query) {
if ($query.query === "?") {
// We don't want to bloat the generated query with unnecessary parentheses, so we leave them out for things that only evaluated to placeholders anyway. Other simple cases may be added here in the future, though we're limited in what we can *safely and reliably* analyze from already-generated SQL output.
return $query;
} else {
return $parenthesize($query);
}
}
function $parenthesizeAll(nodes) {
return nodes.map((node) => $parenthesize(node));
}
function $maybeParenthesizeAll(nodes) {
return nodes.map((node) => $maybeParenthesize(node));
}
function $arrayFrom(items) {
let containsSQLExpressions = Array.isArray(items) && items.some((item) => typeOf(item) === "sqlExpression");
if (typeOf(items) === "placeholder") {
return $handle(items);
} else if (containsSQLExpressions) {
// If the list contains SQL expressions, we cannot pass it in as a param at query time; we need to explicitly compile the expressions into the query
let $items = items.map((item) => $maybeParenthesize($handle(item)));
return $combine`ARRAY[${$join(", ", $items)}]`;
} else {
let encounteredPlaceHolders = [];
// FIXME: This currently assumes everything is a literal or placeholder, we should maybe add a guard to ensure that? Or is this already validated elsewhere?
let values = items.map((item) => {
if (typeOf(item) === "placeholder") {
encounteredPlaceHolders.push(item.name);
return item;
} else {
return item.value;
}
});
return $object({ query: "?", params: [ values ], placeholders: encounteredPlaceHolders });
}
}
// TODO: Eventually have some sort of generic Babel-esque AST transformation setup for all of these unpacking functions?
function unpackNotConditions(expression) {
// This translates `expression(notCondition(condition))` to `notExpression(expression(condition))`, including for multiple nested levels of `notCondition`
let notLevels = 0;
let currentItem = expression.condition;
while(typeOf(currentItem) === "notCondition") {
notLevels += 1;
currentItem = currentItem.condition;
}
// This handles double negatives, which may occur if someone is unknowingly nesting `not` conditions; eg. when generating query chunks with a third-party library and inverting the result
// FIXME: Generalize not-deduplication to work for user-provided `notExpression` as well? -> This should be moved to generic AST optimization infrastructure anyway!
let hasNot = (notLevels % 2) === 1;
let unpackedExpression = merge(expression, { condition: currentItem });
if (hasNot) {
return operations.not(unpackedExpression);
} else {
return unpackedExpression;
}
}
function flattenConditionList(conditionList) {
let ownType = typeOf(conditionList);
// TODO: Replace with Array#flat when Node 10 goes EOL (April 2021)
return flatten(conditionList.items.map((item) => {
let itemType = typeOf(item);
if (itemType === "condition") {
return item;
} else if (itemType === ownType) {
return flattenConditionList(item);
} else {
// Inverse type (eg. any instead of all); just return it as-is, and let the caller deal with that and the necessary recursion
return item;
}
}));
}
let allConditionTypes = [ "moreThan", "equals", "lessThan", "list" ];
function unpackListConditions(expression) {
// This (recursively) translates `expression(anyOfConditions([ condition1, condition2 ]))` into anyOfExpressions([ expression(condition1), expression(condition2) ]), with some batching of like-typed conditions for more compact queries
// FIXME: Merge multiple `where` clauses, prior to having it processed here, eg. in the `select` handler or an abstraction that can be used in other places (eg. for CHECK constraints)
function conditionsToExpressions(conditionList) {
let listType = typeOf(conditionList);
if (listType === "condition") {
// Nothing to unpack here, this is not actually a list.
return expression;
} else if (listType === "anyOfConditions" || listType === "allOfConditions") {
// FIXME: Detect unexpected conditionType
let conditionsByType = syncpipe(conditionList, [
(_) => flattenConditionList(_),
(_) => splitFilterN(_, allConditionTypes, (condition) => defaultValue(condition.conditionType, "list"))
]);
let newExpressions = syncpipe(allConditionTypes, [
(_) => _.map((conditionType) => {
if (conditionType === "list") {
return conditionsByType.list.map((subList) => {
return conditionsToExpressions(subList);
});
} else {
let relevantConditions = conditionsByType[conditionType];
if (relevantConditions.length === 0) {
return null;
} else if (relevantConditions.length === 1) {
return operations.expression({
left: expression.left,
condition: relevantConditions[0]
});
} else {
// [lessThan(a), lessThan(b)] -> lessThan(_internalArray([a, b]))
let internalArrayType = matchValue(listType, {
anyOfConditions: "_internalAnyOfArray",
allOfConditions: "_internalAllOfArray",
});
// FIXME: Can this be represented with standard operations alone? Or would that result in an infinite cycle of list condition unpacking? The dynamic access on a public API is also pretty nasty...
return operations.expression({
left: expression.left,
condition: operations[conditionType]({
type: internalArrayType,
items: relevantConditions.map((condition) => condition.expression)
})
});
}
}
}),
(_) => _.filter((expression) => expression != null),
(_) => flatten(_)
]);
if (newExpressions.length === 0) {
unreachable("Left with 0 new expressions after unpacking");
} else if (newExpressions.length === 1) {
return newExpressions[0];
} else {
let wrapperType = matchValue.literal(listType, {
anyOfConditions: operations.anyOf,
allOfConditions: operations.allOf
});
return wrapperType(newExpressions);
}
} else {
unreachable(`Unexpected condition type '${listType}'`);
}
}
return conditionsToExpressions(expression.condition);
}
// FIXME: createQueryObject/$ wrapper, including properties like relation mappings and dependent query queue
let process = {
select: function ({ table, clauses }) {
let $table = $handle(table);
let expectedClauseTypes = [ "where", "addColumns", "onlyColumns" ];
let clausesByType = splitFilterN(clauses, expectedClauseTypes, (clause) => clause.type);
let onlyColumns = clausesByType.onlyColumns.map((node) => node.columns).flat();
let addColumns = clausesByType.addColumns.map((node) => node.columns).flat();
let whereExpressions = clausesByType.where.map((node) => node.expression);
// FIXME: Fold relations
// FIXME: This logic should move to an AST optimization phase
let $whereClause = asExpression(() => {
if (whereExpressions.length > 0) {
return $combine`WHERE ${$handle(operations.allOf(whereExpressions))}`;
} else {
return NoQuery;
}
});
let $columnSelection = columnList({ onlyColumns, addColumns });
return $combine`SELECT ${$columnSelection} FROM ${$table} ${$whereClause}`;
},
tableName: function ({ name }) {
// FIXME: escape
return $object({
query: name,
params: []
});
},
allColumns: function () {
// Special value used in column selection, to signify any/all columns
return $object({
query: "*",
params: []
});
},
columnName: function ({ name }) {
// FIXME: escape
return $object({
query: name,
params: []
});
},
alias: function ({ column, expression }) {
// FIXME: escape
let $column = $handle(column);
let $expression = $handle(expression);
return $combine`${$expression} AS ${$column}`;
},
sqlExpression: function ({ sql, parameters }) {
return $object({
query: sql,
params: parameters
});
},
literalValue: function ({ value }) {
// FIXME: Check if this is valid in column selections / aliases
return $object({
query: "?",
params: [ value ]
});
},
expression: function (expression) {
// FIXME: wrap nested expression in parens
let unpackedExpression = syncpipe(expression, [
(_) => unpackNotConditions(expression),
(_) => unpackListConditions(expression)
]);
if (typeOf(unpackedExpression) !== "expression") {
// We got back an `expression` that was wrapped in a `notExpression`/`anyOfExpression`/`allOfExpression`, so let the respective handler deal with that wrapper first, and we'll come back here later when that handler invokes $handle again with a (now unpackable-condition-less) expression.
return $handle(unpackedExpression);
} else {
let { left, condition } = unpackedExpression;
return $combine`${$handle(left)} ${$handle(condition)}`;
}
},
notExpression: function ({ expression }) {
return $combine`NOT (${$handle(expression)})`;
},
_internalAnyOfArray: function ({ items }) {
// anyOfConditions: function ({ items }) {
return $combine`ANY(${$maybeParenthesize($arrayFrom(items))})`;
},
_internalAllOfArray: function ({ items }) {
// allOfConditions: function ({ items }) {
return $combine`ALL(${$maybeParenthesize($arrayFrom(items))})`;
},
anyOfExpressions: function ({ items }) {
if (items.length === 1) {
return $handle(items[0]);
} else {
return $or(items);
}
},
allOfExpressions: function ({ items }) {
if (items.length === 1) {
return $handle(items[0]);
} else {
return $and(items);
}
},
condition: function ({ conditionType, expression }) {
if (conditionType === "equals") {
return $combine`= ${$handle(expression)}`;
} else if (conditionType === "lessThan") {
return $combine`< ${$handle(expression)}`;
} else if (conditionType === "moreThan") {
return $combine`> ${$handle(expression)}`;
} else {
// FIXME: unreachable marker
throw new Error(`Unrecognized condition type: ${conditionType}`);
}
},
placeholder: function (placeholder) {
return $object({
query: "?",
params: [ placeholder ],
placeholders: [ placeholder.name ]
});
}
};
function $handle(node) {
validateArguments(arguments, {
node: allowExtraProperties({
__raqbASTNode: isValue(true)
})
});
let processor = process[node.type];
if (processor != null) {
return processor(node);
} else {
throw new Error(`Unrecognized node type: ${node.type}`);
}
}
// FIXME: Disallow stringifying things that are not top-level queries! Eg. `columnName`
module.exports = function astToQuery(ast) {
let result = $handle(ast);
return $object({
query: result.query + ";",
params: result.params,
placeholders: result.placeholders
});
};

@ -0,0 +1,5 @@
"use strict";
module.exports = function concat(arrays) {
return arrays[0].concat(... arrays.slice(1));
};

@ -0,0 +1,5 @@
"use strict";
module.exports = function merge(... items) {
return Object.assign({}, ... items);
};

@ -0,0 +1,19 @@
"use strict";
const { validateArguments } = require("@validatem/core");
const required = require("@validatem/required");
const arrayOf = require("@validatem/array-of");
const node = require("../ast-node");
module.exports = function (operations) {
const isSelectionColumn = require("../validators/operations/is-selection-column")(operations);
return function addColumns(_columns) {
let [ columns ] = validateArguments(arguments, {
columns: [ required, arrayOf([ required, isSelectionColumn ]) ]
});
return node({ type: "addColumns", columns: columns });
};
};

@ -0,0 +1,26 @@
"use strict";
const { validateArguments } = require("@validatem/core");
const required = require("@validatem/required");
const either = require("@validatem/either");
const isString = require("@validatem/is-string");
const node = require("../ast-node");
module.exports = function (operations) {
const isValueExpression = require("../validators/operations/is-value-expression")(operations);
const isObjectType = require("../validators/operations/is-object-type")(operations);
const wrapWithOperation = require("../validators/operations/wrap-with-operation")(operations);
return function alias(_name, _expression) {
let [ name, expression ] = validateArguments(arguments, {
name: [ required, either([
[ isObjectType("column") ],
[ isString, wrapWithOperation("column") ]
])],
expression: [ required, isValueExpression ]
});
return node({ type: "alias", column: name, expression: expression });
};
};

@ -0,0 +1,31 @@
"use strict";
const { validateArguments } = require("@validatem/core");
const required = require("@validatem/required");
const unreachable = require("../unreachable");
const node = require("../ast-node");
module.exports = function (operations) {
const isPredicateList = require("../validators/operations/is-predicate-list")(operations);
return function allOf(_items) {
let [ items ] = validateArguments(arguments, {
items: [ required, isPredicateList ]
});
if (items.type === "conditions") {
return node({
type: "allOfConditions",
items: items.value
});
} else if (items.type === "expressions") {
return node({
type: "allOfExpressions",
items: items.value
});
} else {
unreachable(`Invalid tagged type '${items.type}'`);
}
};
};

@ -0,0 +1,31 @@
"use strict";
const { validateArguments } = require("@validatem/core");
const required = require("@validatem/required");
const unreachable = require("../unreachable");
const node = require("../ast-node");
module.exports = function (operations) {
const isPredicateList = require("../validators/operations/is-predicate-list")(operations);
return function anyOf(_items) {
let [ items ] = validateArguments(arguments, {
items: [ required, isPredicateList ]
});
if (items.type === "conditions") {
return node({
type: "anyOfConditions",
items: items.value
});
} else if (items.type === "expressions") {
return node({
type: "anyOfExpressions",
items: items.value
});
} else {
unreachable(`Invalid tagged type '${items.type}'`);
}
};
};

@ -0,0 +1,18 @@
"use strict";
const { validateArguments } = require("@validatem/core");
const required = require("@validatem/required");
const isLocalColumnName = require("../validators/is-local-column-string");
const node = require("../ast-node");
module.exports = function column(_operations) {
return function (_name) {
let [ name ] = validateArguments(arguments, {
name: [ required, isLocalColumnName ]
});
return node({ type: "columnName", name });
};
};

@ -0,0 +1,20 @@
"use strict";
const { validateArguments } = require("@validatem/core");
const required = require("@validatem/required");
const either = require("@validatem/either");
const node = require("../ast-node");
module.exports = function (operations) {
const isValueExpression = require("../validators/operations/is-value-expression")(operations);
const isInternalArrayType = require("../validators/operations/is-internal-array-type")(operations);
return function equals(_name, _expression) {
let [ expression ] = validateArguments(arguments, {
expression: [ required, either([ isValueExpression, isInternalArrayType ]) ]
});
return node({ type: "condition", conditionType: "equals", expression: expression });
};
};

@ -0,0 +1,26 @@
"use strict";
const { validateOptions } = require("@validatem/core");
const required = require("@validatem/required");
const node = require("../ast-node");
module.exports = function (operations) {
const isCondition = require("../validators/operations/is-condition")(operations);
const isPossiblyForeignColumn = require("../validators/operations/is-possibly-foreign-column")(operations);
return function expression(_options) {
let { left, condition } = validateOptions(arguments, {
left: [ required, isPossiblyForeignColumn ], // FIXME: allow sqlExpression and such
condition: [ required, isCondition ]
});
// FIXME/MARKER: Rename to 'assert'?
return node({
type: "expression",
left: left,
condition: condition
});
};
};

@ -0,0 +1,30 @@
"use strict";
const { validateArguments } = require("@validatem/core");
const required = require("@validatem/required");
const either = require("@validatem/either");
const isString = require("@validatem/is-string");
const isForeignColumnString = require("../validators/is-foreign-column-string");
const node = require("../ast-node");
module.exports = function (_operations) {
return function foreignColumn(_descriptor) {
let [ descriptor ] = validateArguments(arguments, {
descriptor: [ required, either([
[ isForeignColumnString ],
// FIXME: Instruct users to use the below format specifically when dynamically specifying a foreign column, rather than string-concatenating
[{
table: [ required, isString ],
column: [ required, isString ]
}]
])]
});
return node({
type: "foreignColumnName",
tableName: descriptor.table,
columnName: descriptor.column
});
};
};

@ -0,0 +1,224 @@
"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?

@ -0,0 +1,20 @@
"use strict";
const { validateArguments } = require("@validatem/core");
const required = require("@validatem/required");
const either = require("@validatem/either");
const node = require("../ast-node");
module.exports = function (operations) {
const isValueExpression = require("../validators/operations/is-value-expression")(operations);
const isInternalArrayType = require("../validators/operations/is-internal-array-type")(operations);
return function lessThan(_name, _expression) {
let [ expression ] = validateArguments(arguments, {
expression: [ required, either([ isValueExpression, isInternalArrayType ]) ]
});
return node({ type: "condition", conditionType: "lessThan", expression: expression });
};
};

@ -0,0 +1,20 @@
"use strict";
const { validateArguments } = require("@validatem/core");
const required = require("@validatem/required");
const either = require("@validatem/either");
const node = require("../ast-node");
module.exports = function (operations) {
const isValueExpression = require("../validators/operations/is-value-expression")(operations);
const isInternalArrayType = require("../validators/operations/is-internal-array-type")(operations);
return function moreThan(_name, _expression) {
let [ expression ] = validateArguments(arguments, {
expression: [ required, either([ isValueExpression, isInternalArrayType ]) ]
});
return node({ type: "condition", conditionType: "moreThan", expression: expression });
};
};

@ -0,0 +1,33 @@
"use strict";
const { validateArguments } = require("@validatem/core");
const required = require("@validatem/required");
const either = require("@validatem/either");
const node = require("../ast-node");
const unreachable = require("../unreachable");
const tagAsType = require("../validators/tag-as-type");
module.exports = function (operations) {
const isExpression = require("../validators/operations/is-expression")(operations);
const isCondition = require("../validators/operations/is-condition")(operations);
return function not(_expression) {
let [ expression ] = validateArguments(arguments, {
expression: [ required, either([
[ isExpression, tagAsType("expression") ],
[ isCondition, tagAsType("condition") ],
])]
});
// FIXME: rename expression to something clearer, that indicates its relationship to conditions?
if (expression.type === "expression") {
return node({ type: "notExpression", expression: expression.value });
} else if (expression.type === "condition") {
return node({ type: "notCondition", condition: expression.value });
} else {
unreachable(`Invalid tagged type '${expression.type}'`);
}
};
};

@ -0,0 +1,19 @@
"use strict";
const { validateArguments } = require("@validatem/core");
const required = require("@validatem/required");
const arrayOf = require("@validatem/array-of");
const node = require("../ast-node");
module.exports = function (operations) {
const isSelectionColumn = require("../validators/operations/is-selection-column")(operations);
return function onlyColumns(_columns) {
let [ columns ] = validateArguments(arguments, {
columns: [ required, arrayOf([ required, isSelectionColumn ]) ]
});
return node({ type: "onlyColumns", columns: columns });
};
};

@ -0,0 +1,20 @@
"use strict";
const { validateArguments } = require("@validatem/core");
const required = require("@validatem/required");
const isString = require("@validatem/is-string");
const node = require("../ast-node");
module.exports = function (_operations) {
return function placeholder(name) {
validateArguments(arguments, {
name: [ required, isString ]
});
return node({
type: "placeholder",
name: name
});
};
};

@ -0,0 +1,18 @@
"use strict";
const { validateArguments } = require("@validatem/core");
const required = require("@validatem/required");
const isFunction = require("@validatem/is-function");
const node = require("../ast-node");
module.exports = function (_operations) {
return function postProcess(_handler) {
let [ handler ] = validateArguments(arguments, {
handler: [ required, isFunction ]
});
// FIXME: Allow specifying before/after options? That tell it to place itself before or after a particular other postprocessing operation. With a special $begin and $end for the edges
return node({ type: "postProcess", handler });
};
};

@ -0,0 +1,28 @@
"use strict";
const { validateArguments } = require("@validatem/core");
const required = require("@validatem/required");
const either = require("@validatem/either");
const isString = require("@validatem/is-string");
const nestedArrayOf = require("@validatem/nested-array-of");
const node = require("../ast-node");
const flatten = require("../validators/flatten");
module.exports = function (operations) {
const isObjectType = require("../validators/operations/is-object-type")(operations);
const isSelectClause = require("../validators/operations/is-select-clause")(operations);
const wrapWithOperation = require("../validators/operations/wrap-with-operation")(operations);
return function select(_table, _clauses) {
let [ table, clauses ] = validateArguments(arguments, {
table: [ required, either([
[ isObjectType("tableName") ],
[ isString, wrapWithOperation("table") ]
])],
clauses: [ required, nestedArrayOf(isSelectClause), flatten ]
});
return node({ type: "select", table: table, clauses: clauses });
};
};

@ -0,0 +1,18 @@
"use strict";
const { validateArguments } = require("@validatem/core");
const required = require("@validatem/required");
const isTableName = require("../validators/is-table-name");
const node = require("../ast-node");
module.exports = function (_operations) {
return function (_name) {
let [ name ] = validateArguments(arguments, {
name: [ required, isTableName ]
});
return node({ type: "tableName", name });
};
};

@ -0,0 +1,22 @@
"use strict";
const { validateArguments } = require("@validatem/core");
const arrayOf = require("@validatem/array-of");
const defaultTo = require("@validatem/default-to");
const required = require("@validatem/required");
const isString = require("@validatem/is-string");
const isLiteralValue = require("../validators/is-literal-value");
const node = require("../ast-node");
module.exports = function (_operations) {
return function unsafeSQL(_sql, _parameters) {
// FIXME: Wrap in parens within an alias, when stringifying?
let [ sql, parameters ] = validateArguments(arguments, {
sql: [ required, isString ],
parameters: [ defaultTo([]), arrayOf(isLiteralValue) ]
});
return node({ type: "sqlExpression", sql, parameters });
};
};

@ -0,0 +1,17 @@
"use strict";
const { validateArguments } = require("@validatem/core");
const required = require("@validatem/required");
const node = require("../ast-node");
const isLiteralValue = require("../validators/is-literal-value");
module.exports = function (_operations) {
return function literalValue(_value) {
let [ value ] = validateArguments(arguments, {
value: [ required, isLiteralValue ]
});
return node({ type: "literalValue", value });
};
};

@ -0,0 +1,28 @@
"use strict";
const { validateArguments } = require("@validatem/core");
const required = require("@validatem/required");
const either = require("@validatem/either");
const node = require("../ast-node");
module.exports = function (operations) {
const isObjectType = require("../validators/operations/is-object-type")(operations);
const isExpression = require("../validators/operations/is-expression")(operations);
const isWhereObject = require("../validators/operations/is-where-object")(operations);
return function (_predicates) {
let [ predicates ] = validateArguments(arguments, {
predicates: [ required, either([
[ isObjectType("sqlExpression") ],
[ isExpression ], // FIXME: Make sure to document that this is what is to be used instead of two-argument WHERE syntax
[ isWhereObject ]
])]
});
return node({
type: "where",
expression: predicates
});
};
};

@ -0,0 +1,10 @@
"use strict";
module.exports = function typeOf(value) {
// FIXME: Better check
if (typeof value === "object") {
return value.type;
} else {
return null;
}
};

@ -0,0 +1,5 @@
"use strict";
module.exports = function unreachable(reason) {
throw new Error(`This code should never be run: ${reason}; this is a bug in raqb, please report it!`);
};

@ -0,0 +1,5 @@
"use strict";
module.exports = function flatten(array) {
return array.flat(Infinity);
};

@ -0,0 +1,16 @@
"use strict";
const matchesFormat = require("@validatem/matches-format");
module.exports = [
// FIXME: Validate format properly
matchesFormat(/^[^.]+\.[^.]+$/),
(string) => {
let [ tableName, columnName ] = string.split(".");
return {
table: tableName,
column: columnName
};
}
];

@ -0,0 +1,15 @@
"use strict";
const either = require("@validatem/either");
const isString = require("@validatem/is-string");
const isDate = require("@validatem/is-date");
const isBoolean = require("@validatem/is-boolean");
const isNumber = require("@validatem/is-number");
// FIXME: Add null, but not undefined
module.exports = either([
isString,
isNumber, // FIXME: Disallow Infinity?
isBoolean,
isDate
]);

@ -0,0 +1,8 @@
"use strict";
const matchesFormat = require("@validatem/matches-format");
const wrapError = require("@validatem/wrap-error");
module.exports = wrapError("Must not include a table name", [
matchesFormat(/^[^.]+$/)
]);

@ -0,0 +1,9 @@
"use strict";
const allowExtraProperties = require("@validatem/allow-extra-properties");
const forbidden = require("@validatem/forbidden");
// FIXME: Check that this works correctly without explicit hasShape
module.exports = allowExtraProperties({
__raqbASTNode: forbidden
});

@ -0,0 +1,6 @@
"use strict";
const isString = require("@validatem/is-string");
// FIXME: Validate properly
module.exports = [ isString ];

@ -0,0 +1,14 @@
"use strict";
const either = require("@validatem/either");
// FIXME: Maybe change this to include auto-wrapping strings, and change/reorder is-value-expression to prioritize string literal wrapping instead?
module.exports = function (operations) {
const isObjectType = require("./is-object-type")(operations);
return either([
[ isObjectType("columnName") ],
[ isObjectType("foreignColumnName") ],
]);
};

@ -0,0 +1,14 @@
"use strict";
const wrapError = require("@validatem/wrap-error");
const required = require("@validatem/required");
const arrayOf = require("@validatem/array-of");
module.exports = function (operations) {
const isCondition = require("./is-condition")(operations);
return wrapError("Must be a list of conditions and/or values", [
// FIXME: Do we actually want `required` here? We might want to eventually accept `null` as an automatically-wrapped literal value, or do we want a marker object for that instead?
arrayOf([ required, isCondition ])
], { preserveOriginalErrors: true });
};

@ -0,0 +1,18 @@
"use strict";
const either = require("@validatem/either");
const wrapError = require("@validatem/wrap-error");
module.exports = function (operations) {
const isObjectType = require("./is-object-type")(operations);
const isValueExpression = require("./is-value-expression")(operations);
const wrapWithOperation = require("./wrap-with-operation")(operations);
return wrapError("Must be a type of condition or value", either([
[ isObjectType("condition") ], // equals, lessThan, moreThan
[ isObjectType("notCondition") ], // not(condition)
[ isObjectType("anyOfConditions") ], // anyOf(...)
[ isObjectType("allOfConditions") ], // allOf(...)
[ isValueExpression, wrapWithOperation("equals") ]
]));
};

@ -0,0 +1,13 @@
"use strict";
const wrapError = require("@validatem/wrap-error");
const required = require("@validatem/required");
const arrayOf = require("@validatem/array-of");
module.exports = function (operations) {
const isExpression = require("./is-expression")(operations);
return wrapError("Must be a list of expression objects", [
arrayOf([ required, isExpression ])
], { preserveOriginalErrors: true });
};

@ -0,0 +1,15 @@
"use strict";
const either = require("@validatem/either");
const wrapError = require("@validatem/wrap-error");
module.exports = function (operations) {
const isObjectType = require("./is-object-type")(operations);
return wrapError("Must be a type of expression object", either([
[ isObjectType("expression") ],
[ isObjectType("notExpression") ],
[ isObjectType("anyOfExpressions") ],
[ isObjectType("allOfExpressions") ],
]));
};

@ -0,0 +1,12 @@
"use strict";
const either = require("@validatem/either");
module.exports = function (operations) {
const isObjectType = require("./is-object-type")(operations);
return either([
[ isObjectType("_internalAnyOfArray") ],
[ isObjectType("_internalAllOfArray") ],
]);
};

@ -0,0 +1,21 @@
"use strict";
const wrapError = require("@validatem/wrap-error");
const isPlainObject = require("@validatem/is-plain-object");
const ValidationError = require("@validatem/error");
module.exports = function (operations) {
return function isObjectType(expectedType) {
// FIXME: Why not using allowExtraProperties + object syntax here?
// FIXME: Add AST node check
return wrapError(`Must be a query object of type '${expectedType}'`, [
isPlainObject,
function (value) {
if (value.type !== expectedType) {
// FIXME: Maybe figure out a better user-facing name for the concept of a 'query object' (query segment)?
throw new ValidationError(`Must be of type '${expectedType}'`);
}
}
]);
};
};

@ -0,0 +1,15 @@
"use strict";
const wrapError = require("@validatem/wrap-error");
const either = require("@validatem/either");
const isString = require("@validatem/is-string");
module.exports = function (operations) {
const isColumnObject = require("./is-column-object")(operations);
const wrapPossiblyForeignColumnName = require("./wrap-possibly-foreign-column-name")(operations);
return wrapError("Must be a column name or object", either([
[ isColumnObject ],
[ isString, wrapPossiblyForeignColumnName ]
]));
};

@ -0,0 +1,27 @@
"use strict";
const either = require("@validatem/either");
const tagAsType = require("../tag-as-type");
module.exports = function (operations) {
const isObjectType = require("./is-object-type")(operations);
const isExpressionList = require("./is-expression-list")(operations);
const isConditionList = require("./is-condition-list")(operations);
const isWhereObjectList = require("./is-where-object-list")(operations);
return either([
// Boolean AND/OR
[ isExpressionList, tagAsType("expressions") ],
// Combine (JOIN)
// FIXME
// ...
// Multiple-choice conditions
[ isWhereObjectList, tagAsType("expressions") ],
[ either([
[ isObjectType("sqlExpression") ],
[ isObjectType("placeholder") ], // for dynamically specified array of values
[ isConditionList ],
]), tagAsType("conditions") ],
]);
};

@ -0,0 +1,15 @@
"use strict";
const either = require("@validatem/either");
module.exports = function (operations) {
const isObjectType = require("./is-object-type")(operations);
return either([
[ isObjectType("where") ],
[ isObjectType("addColumns") ],
[ isObjectType("onlyColumns") ],
[ isObjectType("withRelations") ],
[ isObjectType("postProcess") ],
]);
};

@ -0,0 +1,13 @@
"use strict";
const either = require("@validatem/either");
module.exports = function (operations) {
const isObjectType = require("./is-object-type")(operations);
const isPossiblyForeignColumn = require("./is-possibly-foreign-column")(operations);
return either([
isObjectType("alias"),
isPossiblyForeignColumn,
]);
};

@ -0,0 +1,23 @@
"use strict";
const either = require("@validatem/either");
const wrapError = require("@validatem/wrap-error");
const isLiteralValue = require("../is-literal-value");
// NOTE: This validator should typically come last in an `either`, since it will catch various types of inputs (sqlExpression, literal values, etc.) that might need to be interpreted differently in specific contexts.
// FIXME: Rename valueExpression -> value?
module.exports = function (operations) {
const isObjectType = require("./is-object-type")(operations);
const isColumnObject = require("./is-column-object")(operations);
const wrapWithOperation = require("./wrap-with-operation")(operations);
return wrapError("Must be a type of value", either([
[ isObjectType("sqlExpression") ],
[ isObjectType("literalValue") ],
[ isObjectType("placeholder") ], // TODO: Verify that this also works for the `alias` method
[ isColumnObject ],
[ isLiteralValue, wrapWithOperation("value") ]
]));
};

@ -0,0 +1,13 @@
"use strict";
const wrapError = require("@validatem/wrap-error");
const required = require("@validatem/required");
const arrayOf = require("@validatem/array-of");
module.exports = function (operations) {
const isWhereObject = require("./is-where-object")(operations);
return wrapError("Must be a list of WHERE condition objects", [
arrayOf([ required, isWhereObject ])
], { preserveOriginalErrors: true });
};

@ -0,0 +1,35 @@
"use strict";
const wrapError = require("@validatem/wrap-error");
const required = require("@validatem/required");
const anyProperty = require("@validatem/any-property");
const isString = require("@validatem/is-string");
const isPlainObject = require("@validatem/is-plain-object");
const isNotAnASTNode = require("../is-not-an-ast-node");
module.exports = function (operations) {
const isCondition = require("./is-condition")(operations);
const wrapPossiblyForeignColumnName = require("./wrap-possibly-foreign-column-name")(operations);
function wrapWhereObject(fields) {
let expressions = Object.entries(fields).map(([ key, value ]) => {
return operations.expression({
left: wrapPossiblyForeignColumnName(key),
condition: value
});
});
return operations.allOf(expressions);
}
let isConditionsMapping = anyProperty({
// NOTE: We cannot wrap the key as a column name object here, since object keys can only be strings; we do so within wrapWhereObject instead
key: [ required, isString ],
value: [ required, isCondition ]
});
let rules = [ isPlainObject, isNotAnASTNode, isConditionsMapping, wrapWhereObject ];
return wrapError("Must be an object of WHERE conditions", rules, { preserveOriginalErrors: true });
};

@ -0,0 +1,11 @@
"use strict";
module.exports = function (operations) {
return function wrapPossiblyForeignColumnName(value) {
if (value.includes(".")) {
return operations.foreignColumn(value);
} else {
return operations.column(value);
}
};
};

@ -0,0 +1,11 @@
"use strict";
module.exports = function (operations) {
return function wrapWithOperation(name) {
return function wrap(value) {
// NOTE: It is important that we only do this lookup at wrapping time! Otherwise we don't have a guarantee that all the operation modules have been fully initialized yet, due to potential cyclical references or out-of-order loading.
return operations[name](value);
};
};
}

@ -0,0 +1,8 @@
"use strict";
module.exports = function tagAsType(type) {
// In operation methods that can be used in multiple places with different semantic meanings (eg. `not`, `anyOf`) and where a different object type needs to be returned based on the observed input type, we use this method to 'annotate' the input based on what type it was determined to be during validation. That way, we don't need to duplicate the type-checking logic in the implementation code, which reduces the chance of bugs due to out-of-sync code.
return function (value) {
return { type: type, value: value };
};
};

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save