WIP
parent
8bbd02eec8
commit
05bd98f640
@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
"use strict";
|
||||
|
||||
const yargs = require("yargs");
|
||||
const matchValue = require("match-value");
|
||||
|
||||
let argv = yargs
|
||||
.command("change", "Create a new schema revision")
|
||||
.command("upgrade <revision>", "Upgrade the database to a new schema revision", {
|
||||
revision: { describe: "The number of the revision to upgrade to (or 'latest')" }
|
||||
})
|
||||
.command("undo", "Undo the most recent schema revision upgrade")
|
||||
.command("show <revision>", "Show the full database schema at a given revision")
|
||||
.argv;
|
||||
|
||||
console.log(argv);
|
||||
|
||||
matchValue(argv._[0], {
|
||||
change: () => {
|
||||
console.log("change");
|
||||
},
|
||||
upgrade: () => {
|
||||
console.log("upgrade");
|
||||
},
|
||||
undo: () => {
|
||||
console.log("undo");
|
||||
},
|
||||
show: () => {
|
||||
console.log("show");
|
||||
}
|
||||
});
|
@ -0,0 +1,43 @@
|
||||
"use strict";
|
||||
|
||||
const Promise = require("bluebird");
|
||||
const asTable = require("as-table");
|
||||
|
||||
const createClient = require("../src/client");
|
||||
const { select, collapseBy, hierarchical, compute, count, sum, where, anyOf, allOf, parameter, unsafeSQL, moreThan } = require("../src/operations");
|
||||
|
||||
// let query = select("sales", [
|
||||
// // where({ size: moreThan(anyOf(parameter("sizes"))) }), // should work
|
||||
// // where({ size: moreThan(anyOf([1, 2, 3])) }), // should work
|
||||
// where({ size: moreThan(anyOf([1, allOf([2, 3]) ])) }), // should work
|
||||
// // where({ size: anyOf(unsafeSQL("")) }),
|
||||
// collapseBy([ "color", "size", hierarchical([ "country_id", "store_id" ]) ], [
|
||||
// compute({
|
||||
// total_sold: count(),
|
||||
// total_revenue: sum("price")
|
||||
// })
|
||||
// ])
|
||||
// ]);
|
||||
|
||||
let query = select("sales", [
|
||||
where({ size: anyOf(parameter("sizes")) }),
|
||||
collapseBy(["size"], [
|
||||
compute({ total_revenue: sum("price") })
|
||||
])
|
||||
]);
|
||||
|
||||
let client = createClient({
|
||||
socket: "/var/run/postgresql",
|
||||
database: "movietest"
|
||||
});
|
||||
|
||||
return Promise.try(() => {
|
||||
let sizes = [ "S", "M", "L", "XL" ];
|
||||
|
||||
return client.query(query, { sizes });
|
||||
}).then((results) => {
|
||||
console.log(asTable(results));
|
||||
|
||||
return client.destroy();
|
||||
});
|
||||
|
@ -0,0 +1,173 @@
|
||||
/* eslint-disable no-undef */
|
||||
"use strict";
|
||||
|
||||
// FIXME: Think about whether to do type(clauses) or [ type(), ... clauses ]
|
||||
// FIXME: Defaults via defaultTo! And make sure to document that these can *only* consist of query objects (literals or expressions), not arbitrary JS
|
||||
// FIXME: Think about field validation API; both schema-time CHECK constraints and runtime validation
|
||||
// FIXME: Think about some way to integrate partial cacheing of queries (eg. for determining permissions for roles)
|
||||
// FIXME: Document that due to how schema creation works, only belongsTo(...) is possible and has(...) isn't
|
||||
// FIXME: last() in addition to first()? Would need to find a way to combine that with sorting, and think about how to handle that when no sorting criterium is given.
|
||||
// FIXME: duration() and time parsing
|
||||
// FIXME: ensure that relations work on collapsed queries, but *only* if the FK column appears in the permitted output columns
|
||||
// FIXME: Separate batch-insert API so that data(...) calls are composable without implying multiple items
|
||||
// FIXME: Have a short-hand (non-composable) API for create("foo", {obj}) and select("foo", {whereObj}) type usage? Especially for eg. `select("posts", { id: postID })`
|
||||
// FIXME: Also for createTable? So that you can do eg. createTable("posts", { title: string() }) instead of the `columns` indirection, when you don't have stuff like composite indexes
|
||||
// FIXME: Allow passing a custom type to autoID?
|
||||
|
||||
let timestamps = {
|
||||
created_at: [ timestamp(), defaultTo(now()) ],
|
||||
updated_at: [ timestamp(), optional() ]
|
||||
};
|
||||
|
||||
createTable("permissions", {
|
||||
role: belongsTo("roles.id"),
|
||||
permission: string() // defined in the application code
|
||||
});
|
||||
|
||||
createTable("roles", {
|
||||
name: string(),
|
||||
color: string()
|
||||
});
|
||||
|
||||
createTable("users", {
|
||||
username: string(),
|
||||
password_hash: string(),
|
||||
email_address: string(),
|
||||
is_banned: [ boolean(), defaultTo(false) ],
|
||||
ban_reason: [ string(), optional() ],
|
||||
last_activity: [ timestamp(), defaultTo(now()), index() ],
|
||||
activation_key: [ uuid(), optional(), index() ],
|
||||
activation_expiry: [ timestamp(), optional() ],
|
||||
password_reset_key: [ uuid(), optional(), index() ],
|
||||
password_reset_expiry: [ timestamp(), optional() ]
|
||||
});
|
||||
|
||||
createTable("categories", {
|
||||
name: string(),
|
||||
created_by: belongsTo("users.id"),
|
||||
visible_on_frontpage: [ boolean(), defaultTo(true) ]
|
||||
});
|
||||
|
||||
createTable("threads", {
|
||||
title: string(),
|
||||
category: belongsTo("categories.id"),
|
||||
user: belongsTo("users.id"), // FIXME: Figure out how to auto-detect the column type for relations
|
||||
visible: [ boolean(), defaultTo(true) ],
|
||||
... timestamps
|
||||
});
|
||||
|
||||
createTable("posts", {
|
||||
thread: belongsTo("threads.id"),
|
||||
user: belongsTo("users.id"),
|
||||
body: string(),
|
||||
visible: [ boolean(), defaultTo(true) ],
|
||||
... timestamps
|
||||
});
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
// List active users and include their role information for highlighting moderators etc.
|
||||
|
||||
function timeAgo(time) {
|
||||
return subtract(now(), duration(time));
|
||||
}
|
||||
|
||||
select("users", [
|
||||
where({ last_activity: lessThan(timeAgo("1h")) }),
|
||||
withRelations({ role: belongsTo("role") })
|
||||
]);
|
||||
|
||||
// Count the active users by role
|
||||
// NOTE: This returns an object { role, count } where `role` is the actual data from the `roles` table
|
||||
|
||||
select("users", [
|
||||
where({ last_activity: moreThan(timeAgo("1h")) }),
|
||||
withRelations({ role: belongsTo("role") }),
|
||||
collapseBy("role", [
|
||||
compute({ count: count() })
|
||||
])
|
||||
]);
|
||||
|
||||
// Update a user's last activity
|
||||
|
||||
update("users", [
|
||||
where({ id: userID }),
|
||||
set({ last_activity: now() })
|
||||
]);
|
||||
|
||||
// Show latest threads in all categories except hidden threads and frontpage-hidden categories
|
||||
|
||||
function mostRecent(field) {
|
||||
return [
|
||||
first(),
|
||||
sortedBy(descending(field))
|
||||
];
|
||||
}
|
||||
|
||||
select("threads", [
|
||||
define("category", belongsTo("category")),
|
||||
define("last_post", has("posts.thread", [
|
||||
mostRecent("created_at")
|
||||
])),
|
||||
where({
|
||||
visible: true,
|
||||
category: { visible_on_frontpage: true }
|
||||
}),
|
||||
sortedBy(descending("last_post.created_at"))
|
||||
]);
|
||||
|
||||
// Get a thread with all its posts
|
||||
|
||||
select("threads", [
|
||||
first(),
|
||||
where({ id: threadID }),
|
||||
withRelations({
|
||||
posts: has("posts.thread", [
|
||||
where({ visible: true }),
|
||||
startAt(offset),
|
||||
first(10)
|
||||
])
|
||||
})
|
||||
]);
|
||||
|
||||
// Create a new thread
|
||||
|
||||
create("threads", [
|
||||
withRelations({ posts: has("posts.thread") }),
|
||||
set({
|
||||
title: title,
|
||||
user: userID,
|
||||
posts: [{
|
||||
user: userID,
|
||||
body: body
|
||||
}]
|
||||
})
|
||||
]);
|
||||
|
||||
// Update the thread title
|
||||
|
||||
update("threads", [
|
||||
where({ id: threadID }),
|
||||
set({
|
||||
title: newTitle,
|
||||
updated_at: now()
|
||||
})
|
||||
]);
|
||||
|
||||
// Create a new post
|
||||
|
||||
create("posts", {
|
||||
thread: threadID,
|
||||
user: userID,
|
||||
body: body
|
||||
});
|
||||
|
||||
// Edit a post body
|
||||
|
||||
update("posts", [
|
||||
where({ id: postID }),
|
||||
set({
|
||||
body: newBody,
|
||||
updated_at: now()
|
||||
})
|
||||
]);
|
@ -0,0 +1,171 @@
|
||||
"use strict";
|
||||
|
||||
const Promise = require("bluebird");
|
||||
const defaultValue = require("default-value");
|
||||
const syncpipe = require("syncpipe");
|
||||
const pg = require("pg");
|
||||
|
||||
const { validateOptions } = require("@validatem/core");
|
||||
const isString = require("@validatem/is-string");
|
||||
const isInteger = require("@validatem/is-integer");
|
||||
const required = require("@validatem/required");
|
||||
const requireEither = require("@validatem/require-either");
|
||||
|
||||
const astToQuery = require("../ast-to-query");
|
||||
const optimizeAST = require("../ast/optimize");
|
||||
const optimizers = require("../optimizers");
|
||||
const typeOf = require("../type-of");
|
||||
|
||||
function numberPlaceholders(query) {
|
||||
// FIXME: This is a hack. Need to find a better way to assemble queries that doesn't rely on fragile regex afterwards to produce correctly-numbered placeholders.
|
||||
let i = 0;
|
||||
|
||||
return query.replace(/\?/g, () => {
|
||||
i += 1;
|
||||
return `$${i}`;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = function createClient(_options) {
|
||||
let options = validateOptions(arguments, {
|
||||
hostname: [ isString ],
|
||||
port: [ isInteger ], // FIXME: isPortNumber
|
||||
socket: [ isString ],
|
||||
username: [ isString ],
|
||||
password: [ isString ],
|
||||
database: [ required, isString ]
|
||||
}, requireEither([ "hostname", "socket" ]));
|
||||
|
||||
let pool = new pg.Pool({
|
||||
// This is a `pg` weird-ism, it expects you to provide a socket path like it is a hostname
|
||||
host: defaultValue(options.host, options.socket),
|
||||
port: options.port,
|
||||
user: options.username,
|
||||
password: options.password,
|
||||
database: options.database
|
||||
});
|
||||
|
||||
function claimConnection() {
|
||||
return Promise.try(() => {
|
||||
return pool.connect();
|
||||
}).disposer((connection) => {
|
||||
connection.release();
|
||||
});
|
||||
}
|
||||
|
||||
function createClientInstance(options) {
|
||||
return {
|
||||
// FIXME: Rename to something that is clearly a verb, eg. `execute` or `do`
|
||||
query: function (ast, parameters = {}) {
|
||||
// FIXME/NOTE: `parameters` is an object, not an array! it fills in the placeholders in the built query
|
||||
let pgClient = defaultValue(options.pgClient, pool);
|
||||
|
||||
// FIXME: Allow passing in a precomputed query
|
||||
let queryObject = syncpipe(ast, [
|
||||
(_) => optimizeAST(_, optimizers),
|
||||
(_) => astToQuery(_.ast)
|
||||
]);
|
||||
|
||||
// FIXME: Switch this over to proper typed Validatem validation
|
||||
for (let key of queryObject.placeholders) {
|
||||
if (!(key in parameters)) {
|
||||
throw new Error(`Query requires a parameter '${key}', but it was not specified`);
|
||||
}
|
||||
}
|
||||
|
||||
let numberedQuery = numberPlaceholders(queryObject.query);
|
||||
|
||||
let processedParams = queryObject.params.map((param) => {
|
||||
if (typeof param === "object" && typeOf(param) === "placeholder") {
|
||||
return parameters[param.name];
|
||||
} else {
|
||||
return param;
|
||||
}
|
||||
});
|
||||
|
||||
console.log({queryObject, numberedQuery, processedParams});
|
||||
|
||||
|
||||
// FIXME: Process placeholders!
|
||||
return Promise.try(() => {
|
||||
return pgClient.query(numberedQuery, processedParams);
|
||||
}).then((result) => {
|
||||
return result.rows;
|
||||
});
|
||||
},
|
||||
tryQuery: function (query, parameters) {
|
||||
// This automagically wraps a query in a conceptual transaction, so that if it fails, we can roll back to the point before the query. This is useful for eg. cases where you want to run some custom logic (that attempts a fallback query) on a UNIQUE violation *within a transaction*. Normally, in this situation the transaction would be 'locked' until a rollback is carried out, but if we don't have a savepoint right before the expected-to-fail query, we can't automatically roll back to the correct point.
|
||||
// FIXME: Check whether an outer transaction is necessary for this to work correctly!
|
||||
// TODO: Document this as "opportunistically try out a query, so that if it fails, it will be like the query never happened (but it will still throw the original error)". Make sure to explain why the regular query behaviour is different from this; automatically creating a savepoint for every single query could have undesirable performance implications.
|
||||
return this.transaction((tx) => {
|
||||
return tx.query(query, parameters);
|
||||
});
|
||||
},
|
||||
transaction: function (callback) {
|
||||
if (options.pgClient == null) {
|
||||
// Not inside a transaction yet
|
||||
return Promise.try(() => {
|
||||
return claimConnection();
|
||||
}).then((disposableClient) => {
|
||||
return Promise.using(disposableClient, (pgClient) => {
|
||||
let instance = createClientInstance({ pgClient: pgClient });
|
||||
|
||||
return Promise.try(() => {
|
||||
return callback(instance);
|
||||
}).then(() => {
|
||||
return pgClient.query("COMMIT");
|
||||
}).catch((error) => {
|
||||
return Promise.try(() => {
|
||||
return pgClient.query("ROLLBACK");
|
||||
}).then(() => {
|
||||
throw error;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Inside a transaction already
|
||||
let currentSavepointID = defaultValue(options.savepointID, 0);
|
||||
let newSavepointID = currentSavepointID + 1;
|
||||
let newSavepointName = `ZAP_${newSavepointID}`;
|
||||
|
||||
let instance = createClientInstance({
|
||||
pgClient: options.pgClient,
|
||||
savepointID: newSavepointID
|
||||
});
|
||||
|
||||
return Promise.try(() => {
|
||||
// FIXME: Can we parameterize this?
|
||||
return options.pgClient.query(`SAVEPOINT ${newSavepointName}`);
|
||||
}).then(() => {
|
||||
// Nested to ensure that we only try to rollback if it's actually the *user query itself* that fails, not the SAVEPOINT command
|
||||
return Promise.try(() => {
|
||||
return callback(instance);
|
||||
}).catch((error) => {
|
||||
return Promise.try(() => {
|
||||
// FIXME: Can we parameterize this?
|
||||
return options.pgClient.query(`ROLLBACK TO SAVEPOINT ${newSavepointName}`);
|
||||
}).then(() => {
|
||||
throw error;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
destroy: function () {
|
||||
if (options.pgClient == null) {
|
||||
return pool.end();
|
||||
} else {
|
||||
// FIXME: Add an explicit rollback function to the API, for cases where there's no thrown error but the transaction needs to be rolled back anyway due to some external condition? Or just expect the user to throw an error? Maybe a special error type that gets absorbed after applying the rollback? Maybe that should be named 'unsafe rollback' as it could wrongly represent a failure as a success from a Promises perspective?
|
||||
throw new Error(`You can only destroy the ZapDB client outside of a transaction`);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return createClientInstance({});
|
||||
};
|
||||
|
||||
|
||||
// SELECT color, size, country_id, store_id, COUNT(*) AS total_sold, SUM(price) AS total_revenue FROM sales GROUP BY color, size, ROLLUP (country_id, store_id);
|
||||
// SELECT * FROM sales GROUP BY color, size, ROLLUP (country_id, store_id);
|
@ -1,19 +0,0 @@
|
||||
"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,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 isSelectionField = require("../validators/operations/is-selection-field")(operations); // FIXME: This needs a more descriptive name. Selectable field?
|
||||
|
||||
return function addFields(_fields) {
|
||||
let [ fields ] = validateArguments(arguments, {
|
||||
fields: [ required, arrayOf([ required, isSelectionField ]) ]
|
||||
});
|
||||
|
||||
return node({ type: "addFields", fields: fields });
|
||||
};
|
||||
};
|
@ -1,27 +1,26 @@
|
||||
"use strict";
|
||||
|
||||
const { validateArguments } = require("@validatem/core");
|
||||
const required = require("@validatem/required");
|
||||
|
||||
const node = require("../ast-node");
|
||||
|
||||
module.exports = function count(operations) {
|
||||
const isTable = require("../validators/operations/is-table")(operations);
|
||||
const isCollection = require("../validators/operations/is-collection")(operations);
|
||||
|
||||
return function count(_table) {
|
||||
let [ table ] = validateArguments(arguments, {
|
||||
table: [ isTable ]
|
||||
return function count(_collection) {
|
||||
let [ collection ] = validateArguments(arguments, {
|
||||
collection: [ isCollection ]
|
||||
});
|
||||
|
||||
// TODO: Investigate whether this can be made more performant by counting a specific column rather than *. That would probably require stateful knowledge of the database schema, though.
|
||||
let columnReference = (table != null)
|
||||
? operations.foreignColumn({ table: table, column: "*" }) // FIXME: Make sure not to break this (internally) when making column name checks more strict
|
||||
: operations.column("*");
|
||||
// TODO: Investigate whether this can be made more performant by counting a specific field rather than *. That would probably require stateful knowledge of the database schema, though.
|
||||
let fieldReference = (collection != null)
|
||||
? operations.remoteField({ collection: collection, field: "*" }) // FIXME: Make sure not to break this (internally) when making field name checks more strict
|
||||
: operations.field("*");
|
||||
|
||||
return node({
|
||||
type: "aggregrateFunction",
|
||||
functionName: "count",
|
||||
args: [ columnReference ]
|
||||
args: [ fieldReference ]
|
||||
});
|
||||
};
|
||||
};
|
||||
|
@ -1,30 +0,0 @@
|
||||
"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,14 @@
|
||||
"use strict";
|
||||
|
||||
const node = require("../../ast-node");
|
||||
|
||||
module.exports = function now() {
|
||||
// FIXME: Allow selecting statement_timestamp/clock_timestamp instead (as either a separate operation, or an option to this one)
|
||||
return function now() {
|
||||
return node({
|
||||
type: "sqlFunction",
|
||||
functionName: "transaction_timestamp", // Equivalent to now(), but more clearly named
|
||||
args: []
|
||||
});
|
||||
};
|
||||
};
|
@ -0,0 +1,21 @@
|
||||
"use strict";
|
||||
|
||||
const { validateArguments } = require("@validatem/core");
|
||||
const required = require("@validatem/required");
|
||||
|
||||
const node = require("../../ast-node");
|
||||
|
||||
module.exports = function (operations) {
|
||||
return function indexWhere(_expression) {
|
||||
const isExpression = require("../../validators/operations/is-expression")(operations);
|
||||
|
||||
let [ expression ] = validateArguments(arguments, {
|
||||
expression: [ required, isExpression ]
|
||||
});
|
||||
|
||||
return node({
|
||||
type: "index",
|
||||
expression: expression
|
||||
});
|
||||
};
|
||||
};
|
@ -0,0 +1,11 @@
|
||||
"use strict";
|
||||
|
||||
const node = require("../../ast-node");
|
||||
|
||||
module.exports = function (_operations) {
|
||||
return function index() {
|
||||
return node({
|
||||
type: "index"
|
||||
});
|
||||
};
|
||||
};
|
@ -0,0 +1,11 @@
|
||||
"use strict";
|
||||
|
||||
const node = require("../../ast-node");
|
||||
|
||||
module.exports = function (_operations) {
|
||||
return function primaryKey() {
|
||||
return node({
|
||||
type: "primaryKey"
|
||||
});
|
||||
};
|
||||
};
|
@ -0,0 +1,21 @@
|
||||
"use strict";
|
||||
|
||||
const { validateArguments } = require("@validatem/core");
|
||||
const required = require("@validatem/required");
|
||||
|
||||
const node = require("../../ast-node");
|
||||
|
||||
module.exports = function (operations) {
|
||||
return function uniqueWhere(_expression) {
|
||||
const isExpression = require("../../validators/operations/is-expression")(operations);
|
||||
|
||||
let [ expression ] = validateArguments(arguments, {
|
||||
expression: [ required, isExpression ]
|
||||
});
|
||||
|
||||
return node({
|
||||
type: "uniqueIndex",
|
||||
expression: expression
|
||||
});
|
||||
};
|
||||
};
|
@ -0,0 +1,11 @@
|
||||
"use strict";
|
||||
|
||||
const node = require("../../ast-node");
|
||||
|
||||
module.exports = function (_operations) {
|
||||
return function unique() {
|
||||
return node({
|
||||
type: "uniqueIndex"
|
||||
});
|
||||
};
|
||||
};
|
@ -1,19 +0,0 @@
|
||||
"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,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 isSelectableField = require("../validators/operations/is-selectable-field")(operations);
|
||||
|
||||
return function onlyFields(_fields) {
|
||||
let [ fields ] = validateArguments(arguments, {
|
||||
fields: [ required, arrayOf([ required, isSelectableField ]) ]
|
||||
});
|
||||
|
||||
return node({ type: "onlyFields", fields: fields });
|
||||
};
|
||||
};
|
@ -0,0 +1,28 @@
|
||||
"use strict";
|
||||
|
||||
const { validateArguments } = require("@validatem/core");
|
||||
const required = require("@validatem/required");
|
||||
const defaultTo = require("@validatem/default-to");
|
||||
const nestedArrayOf = require("@validatem/nested-array-of");
|
||||
const flatten = require("../../validators/flatten");
|
||||
|
||||
const node = require("../ast-node");
|
||||
|
||||
module.exports = function (operations) {
|
||||
const isRelationClause = require("../../validators/operations/is-relation-clause")(operations);
|
||||
const isField = require("../../validators/operations/is-field")(operations);
|
||||
|
||||
return function belongsTo(_field, _clauses) {
|
||||
let [ field, clauses ] = validateArguments(arguments, {
|
||||
field: [ required, isField ],
|
||||
clauses: [ defaultTo([]), nestedArrayOf(isRelationClause), flatten ]
|
||||
});
|
||||
|
||||
return node({
|
||||
type: "relation",
|
||||
relationType: "belongsTo",
|
||||
field: field,
|
||||
clauses: clauses
|
||||
});
|
||||
};
|
||||
};
|
@ -0,0 +1,28 @@
|
||||
"use strict";
|
||||
|
||||
const { validateArguments } = require("@validatem/core");
|
||||
const required = require("@validatem/required");
|
||||
const defaultTo = require("@validatem/default-to");
|
||||
const nestedArrayOf = require("@validatem/nested-array-of");
|
||||
const flatten = require("../../validators/flatten");
|
||||
|
||||
const node = require("../ast-node");
|
||||
|
||||
module.exports = function (operations) {
|
||||
const isRelationClause = require("../../validators/operations/is-relation-clause")(operations);
|
||||
const isRemoteField = require("../../validators/operations/is-remote-field")(operations);
|
||||
|
||||
return function has(_field, _clauses) {
|
||||
let [ field, clauses ] = validateArguments(arguments, {
|
||||
field: [ required, isRemoteField ],
|
||||
clauses: [ defaultTo([]), nestedArrayOf(isRelationClause), flatten ]
|
||||
});
|
||||
|
||||
return node({
|
||||
type: "relation",
|
||||
relationType: "has",
|
||||
field: field,
|
||||
clauses: clauses
|
||||
});
|
||||
};
|
||||
};
|
@ -0,0 +1,31 @@
|
||||
"use strict";
|
||||
|
||||
const { validateArguments } = require("@validatem/core");
|
||||
const required = require("@validatem/required");
|
||||
const arrayOf = require("@validatem/array-of");
|
||||
const either = require("@validatem/either");
|
||||
|
||||
const node = require("../ast-node");
|
||||
|
||||
module.exports = function (operations) {
|
||||
const isObjectType = require("../../validators/operations/is-object-type")(operations);
|
||||
const isPossiblyRemoteField = require("../../validators/operations/is-possibly-remote-field")(operations);
|
||||
const wrapWithOperation = require("../../validators/operations/wrap-with-operation")(operations);
|
||||
|
||||
return function through(_relations) {
|
||||
let [ relations ] = validateArguments(arguments, {
|
||||
relations: [ required, arrayOf(either([
|
||||
[ isObjectType("relation") ],
|
||||
[ isPossiblyRemoteField, either([
|
||||
[ isObjectType("field"), wrapWithOperation("belongsTo") ],
|
||||
[ isObjectType("remoteField"), wrapWithOperation("has") ],
|
||||
])]
|
||||
]))]
|
||||
});
|
||||
|
||||
return node({
|
||||
type: "throughRelations",
|
||||
relations: relations
|
||||
});
|
||||
};
|
||||
};
|
@ -0,0 +1,31 @@
|
||||
"use strict";
|
||||
|
||||
const { validateArguments } = require("@validatem/core");
|
||||
const required = require("@validatem/required");
|
||||
const either = require("@validatem/either");
|
||||
|
||||
const isRemoteFieldName = require("../validators/is-remote-field-name");
|
||||
const isLocalFieldName = require("../validators/is-local-field-name");
|
||||
const isCollectionName = require("../validators/is-collection-name");
|
||||
const node = require("../ast-node");
|
||||
|
||||
module.exports = function (_operations) {
|
||||
return function remoteField(_descriptor) {
|
||||
let [ descriptor ] = validateArguments(arguments, {
|
||||
descriptor: [ required, either([
|
||||
[ isRemoteFieldName ], // NOTE: This returns a parsed version of the name
|
||||
// FIXME: Instruct users to use the below format specifically when dynamically specifying a remote field, rather than string-concatenating
|
||||
[{
|
||||
collection: [ required, isCollectionName ],
|
||||
field: [ required, isLocalFieldName ]
|
||||
}]
|
||||
])]
|
||||
});
|
||||
|
||||
return node({
|
||||
type: "remoteField",
|
||||
collectionName: descriptor.collection,
|
||||
fieldName: descriptor.field
|
||||
});
|
||||
};
|
||||
};
|
@ -0,0 +1,24 @@
|
||||
"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) {
|
||||
return function resultExists(_expression) {
|
||||
const isExpression = require("../../validators/operations/is-expression")(operations);
|
||||
const isRelation = require("../../validators/operations/is-relation")(operations); // FIXME: More specific type?
|
||||
// MARKER: Finish this
|
||||
|
||||
let [ expression ] = validateArguments(arguments, {
|
||||
expression: [ required, either([ isExpression, isRelation ]) ]
|
||||
});
|
||||
|
||||
return node({
|
||||
type: "index",
|
||||
expression: expression
|
||||
});
|
||||
};
|
||||
};
|
@ -0,0 +1,36 @@
|
||||
"use strict";
|
||||
|
||||
const node = require("../../ast-node");
|
||||
|
||||
const { validateArguments } = require("@validatem/core");
|
||||
const either = require("@validatem/either");
|
||||
const required = require("@validatem/required");
|
||||
const arrayOf = require("@validatem/array-of");
|
||||
|
||||
const isCollectionName = require("../../validators/is-collection-name");
|
||||
|
||||
module.exports = function (operations) {
|
||||
const isObjectType = require("../../validators/operations/is-object-type")(operations);
|
||||
const isFieldsObject = require("../../validators/operations/schema/is-fields-object")(operations);
|
||||
|
||||
return function changeCollection(_name, _operations) {
|
||||
let [ name, collectionOperations ] = validateArguments(arguments, {
|
||||
name: [ required, isCollectionName ],
|
||||
operations: [ required, either([
|
||||
[ isFieldsObject, (fieldsObject) => {
|
||||
return [ operations.fields(fieldsObject) ];
|
||||
}],
|
||||
arrayOf(either([
|
||||
isObjectType("schemaFields"),
|
||||
isObjectType("schemaIndexes")
|
||||
]))
|
||||
]) ]
|
||||
});
|
||||
|
||||
return node({
|
||||
type: "changeCollectionCommand",
|
||||
name: name,
|
||||
operations: collectionOperations
|
||||
});
|
||||
};
|
||||
};
|
@ -0,0 +1,36 @@
|
||||
"use strict";
|
||||
|
||||
const node = require("../../ast-node");
|
||||
|
||||
const { validateArguments } = require("@validatem/core");
|
||||
const either = require("@validatem/either");
|
||||
const required = require("@validatem/required");
|
||||
const arrayOf = require("@validatem/array-of");
|
||||
|
||||
const isCollectionName = require("../../validators/is-collection-name");
|
||||
|
||||
module.exports = function (operations) {
|
||||
const isObjectType = require("../../validators/operations/is-object-type")(operations);
|
||||
const isFieldsObject = require("../../validators/operations/schema/is-fields-object")(operations);
|
||||
|
||||
return function createCollection(_name, _operations) {
|
||||
let [ name, collectionOperations ] = validateArguments(arguments, {
|
||||
name: [ required, isCollectionName ],
|
||||
operations: [ required, either([
|
||||
[ isFieldsObject, (fieldsObject) => {
|
||||
return [ operations.fields(fieldsObject) ];
|
||||
}],
|
||||
arrayOf(either([
|
||||
isObjectType("schemaFields"),
|
||||
isObjectType("schemaIndexes")
|
||||
])) // FIXME: At least one of schemaFields
|
||||
]) ]
|
||||
});
|
||||
|
||||
return node({
|
||||
type: "createCollectionCommand",
|
||||
name: name,
|
||||
operations: collectionOperations
|
||||
});
|
||||
};
|
||||
};
|
@ -0,0 +1,21 @@
|
||||
"use strict";
|
||||
|
||||
const node = require("../../ast-node");
|
||||
|
||||
const { validateArguments } = require("@validatem/core");
|
||||
const required = require("@validatem/required");
|
||||
|
||||
module.exports = function (operations) {
|
||||
const isLocalValueExpression = require("../../validators/operations/is-local-value-expression")(operations);
|
||||
|
||||
return function defaultTo(_value) {
|
||||
let [ value ] = validateArguments(arguments, {
|
||||
value: [ required, isLocalValueExpression ]
|
||||
});
|
||||
|
||||
return node({
|
||||
type: "defaultTo",
|
||||
value: value
|
||||
});
|
||||
};
|
||||
};
|
@ -0,0 +1,21 @@
|
||||
"use strict";
|
||||
|
||||
const node = require("../../ast-node");
|
||||
|
||||
const { validateArguments } = require("@validatem/core");
|
||||
const required = require("@validatem/required");
|
||||
|
||||
const isCollectionName = require("../../validators/is-collection-name");
|
||||
|
||||
module.exports = function (_operations) {
|
||||
return function deleteCollection(_name) {
|
||||
let [ name ] = validateArguments(arguments, {
|
||||
name: [ required, isCollectionName ]
|
||||
});
|
||||
|
||||
return node({
|
||||
type: "deleteCollectionCommand",
|
||||
name: name
|
||||
});
|
||||
};
|
||||
};
|
@ -0,0 +1,21 @@
|
||||
"use strict";
|
||||
|
||||
const node = require("../../ast-node");
|
||||
|
||||
const { validateArguments } = require("@validatem/core");
|
||||
const required = require("@validatem/required");
|
||||
|
||||
module.exports = function (operations) {
|
||||
const isFieldsObject = require("../../validators/operations/schema/is-fields-object")(operations);
|
||||
|
||||
return function fields(_fields) {
|
||||
let [ fields ] = validateArguments(arguments, {
|
||||
fields: [ required, isFieldsObject ]
|
||||
});
|
||||
|
||||
return node({
|
||||
type: "schemaFields",
|
||||
fields: fields
|
||||
});
|
||||
};
|
||||
};
|
@ -0,0 +1,22 @@
|
||||
"use strict";
|
||||
|
||||
const node = require("../../ast-node");
|
||||
|
||||
const { validateArguments } = require("@validatem/core");
|
||||
const required = require("@validatem/required");
|
||||
const arrayOf = require("@validatem/array-of");
|
||||
|
||||
module.exports = function (operations) {
|
||||
const isCompositeIndexType = require("../../validators/operations/schema/is-composite-index-type")(operations);
|
||||
|
||||
return function indexes(_indexes) {
|
||||
let [ indexes ] = validateArguments(arguments, {
|
||||
indexes: [ required, arrayOf(isCompositeIndexType) ]
|
||||
});
|
||||
|
||||
return node({
|
||||
type: "schemaIndexes",
|
||||
indexes: indexes
|
||||
});
|
||||
};
|
||||
};
|
@ -0,0 +1,12 @@
|
||||
"use strict";
|
||||
|
||||
const node = require("../../ast-node");
|
||||
|
||||
module.exports = function (_operations) {
|
||||
// FIXME: Length options?
|
||||
return function autoID() {
|
||||
return node({
|
||||
type: "autoIDField"
|
||||
});
|
||||
};
|
||||
};
|
@ -0,0 +1,11 @@
|
||||
"use strict";
|
||||
|
||||
const node = require("../../ast-node");
|
||||
|
||||
module.exports = function (_operations) {
|
||||
return function boolean() {
|
||||
return node({
|
||||
type: "booleanField"
|
||||
});
|
||||
};
|
||||
};
|
@ -0,0 +1,12 @@
|
||||
"use strict";
|
||||
|
||||
const node = require("../../ast-node");
|
||||
|
||||
module.exports = function (_operations) {
|
||||
// FIXME: Length options?
|
||||
return function string() {
|
||||
return node({
|
||||
type: "stringField"
|
||||
});
|
||||
};
|
||||
};
|
@ -0,0 +1,12 @@
|
||||
"use strict";
|
||||
|
||||
const node = require("../../ast-node");
|
||||
|
||||
module.exports = function (_operations) {
|
||||
// FIXME: withTimezone option
|
||||
return function timestamp() {
|
||||
return node({
|
||||
type: "timestampField"
|
||||
});
|
||||
};
|
||||
};
|
@ -0,0 +1,11 @@
|
||||
"use strict";
|
||||
|
||||
const node = require("../../ast-node");
|
||||
|
||||
module.exports = function (_operations) {
|
||||
return function uuid() {
|
||||
return node({
|
||||
type: "uuidField"
|
||||
});
|
||||
};
|
||||
};
|
@ -1,157 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
const NoChange = require("./util/no-change");
|
||||
const deriveNode = require("../derive-node");
|
||||
const operations = require("../operations");
|
||||
const typeOf = require("../type-of");
|
||||
const unreachable = require("../unreachable");
|
||||
const concat = require("../concat");
|
||||
|
||||
const uniqueByPredicate = require("../unique-by-predicate");
|
||||
|
||||
// FIXME: Support for foreign column names
|
||||
|
||||
function uniqueColumns(columns) {
|
||||
return uniqueByPredicate(columns, (column) => column.name);
|
||||
}
|
||||
|
||||
/*
|
||||
valid columns when collapsing:
|
||||
- columns that appear in the collapseBy column list, within or without a hierarchical wrapper
|
||||
- any column that is wrapped in an aggregrate function of some sort
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
name: "set-collapse-by-columns",
|
||||
category: [ "normalization" ],
|
||||
visitors: {
|
||||
collapseBy: (node, { setState }) => {
|
||||
setState("isCollapsing", true);
|
||||
return NoChange;
|
||||
},
|
||||
columnName: (node, { setState }) => {
|
||||
setState("columnSeen", node);
|
||||
return NoChange;
|
||||
},
|
||||
// FIXME: Think of a generic way to express "only match columns under this specific child property"
|
||||
collapseByColumns: (node, { registerStateHandler, setState, defer }) => {
|
||||
let columns = [];
|
||||
|
||||
registerStateHandler("columnSeen", (node) => {
|
||||
columns.push(node);
|
||||
});
|
||||
|
||||
return defer(() => {
|
||||
setState("setCollapsedColumns", columns);
|
||||
return NoChange;
|
||||
});
|
||||
},
|
||||
addColumns: (node, { setState }) => {
|
||||
setState("setAddColumns", node.columns);
|
||||
return NoChange;
|
||||
},
|
||||
onlyColumns: (node, { setState }) => {
|
||||
setState("setOnlyColumns", node.columns);
|
||||
return NoChange;
|
||||
},
|
||||
aggregrateFunction: (node, { registerStateHandler }) => {
|
||||
// FIXME: Also report isCollapsing here, due to aggregrate function use, but make sure that the error describes this as the (possible) cause
|
||||
return NoChange;
|
||||
},
|
||||
compute: (node, { setState }) => {
|
||||
setState("computeSeen", node);
|
||||
return NoChange;
|
||||
},
|
||||
select: (node, { registerStateHandler, defer }) => {
|
||||
let isCollapsing;
|
||||
let onlyColumns = [];
|
||||
let addColumns = [];
|
||||
let computes = [];
|
||||
let collapsedColumns;
|
||||
|
||||
registerStateHandler("isCollapsing", (value) => {
|
||||
isCollapsing = isCollapsing || value;
|
||||
});
|
||||
|
||||
registerStateHandler("setCollapsedColumns", (columns) => {
|
||||
if (collapsedColumns == null) {
|
||||
collapsedColumns = columns;
|
||||
} else {
|
||||
throw new Error(`You can currently only specify a single 'collapseBy' clause. Please file an issue if you have a reason to need more than one!`);
|
||||
}
|
||||
});
|
||||
|
||||
registerStateHandler("setOnlyColumns", (columns) => {
|
||||
onlyColumns = onlyColumns.concat(columns);
|
||||
});
|
||||
|
||||
registerStateHandler("setAddColumns", (columns) => {
|
||||
addColumns = addColumns.concat(columns);
|
||||
});
|
||||
|
||||
registerStateHandler("computeSeen", (node) => {
|
||||
computes.push(node);
|
||||
});
|
||||
|
||||
return defer(() => {
|
||||
if (isCollapsing) {
|
||||
if (addColumns.length > 0) {
|
||||
let extraColumnNames = addColumns.map((column) => column.name);
|
||||
|
||||
throw new Error(`You tried to add extra columns (${extraColumnNames.join(", ")}) in your query, but this is not possible when using collapseBy. See [FIXME: link] for more information, and how to solve this.`);
|
||||
} else if (onlyColumns.length > 0) {
|
||||
// NOTE: This can happen either because the user specified an onlyColumns clause, *or* because a previous run of this optimizer did so!
|
||||
let uniqueSelectedColumns = uniqueColumns(onlyColumns);
|
||||
let collapsedColumnNames = collapsedColumns.map((column) => column.name);
|
||||
|
||||
let invalidColumnSelection = uniqueSelectedColumns.filter((node) => {
|
||||
let isAggregrateComputation = typeOf(node) === "alias" && typeOf(node.expression) === "aggregrateFunction";
|
||||
let isCollapsedColumn = typeOf(node) === "columnName" && collapsedColumnNames.includes(node.name);
|
||||
|
||||
let isValid = isAggregrateComputation || isCollapsedColumn;
|
||||
|
||||
return !isValid;
|
||||
});
|
||||
|
||||
// FIXME: We can probably optimize this by marking the optimizer-created onlyColumns as inherently-valid, via some sort of node metadata mechanism
|
||||
|
||||
if (invalidColumnSelection.length > 0) {
|
||||
let invalidColumnNames = invalidColumnSelection.map((column) => {
|
||||
let columnType = typeOf(column);
|
||||
|
||||
if (columnType === "columnName") {
|
||||
return column.name;
|
||||
} else if (columnType === "alias") {
|
||||
// FIXME: Show alias target instead of column name here?
|
||||
return column.column.name;
|
||||
} else {
|
||||
return unreachable(`Encountered '${columnType}' node in invalid columns`);
|
||||
}
|
||||
});
|
||||
|
||||
throw new Error(`You tried to include one or more columns in your query (${invalidColumnNames.join(", ")}), that are not used in a collapseBy clause or aggregrate function. See [FIXME: link] for more information.`);
|
||||
} else {
|
||||
return NoChange;
|
||||
}
|
||||
} else {
|
||||
let computeAliases = computes.map((node) => {
|
||||
return operations.alias(node.column, node.expression);
|
||||
});
|
||||
|
||||
return deriveNode(node, {
|
||||
clauses: node.clauses.concat([
|
||||
operations.onlyColumns(concat([
|
||||
collapsedColumns,
|
||||
computeAliases
|
||||
]))
|
||||
])
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
// FIXME: a ConsumeNode marker, like RemoveNode but it does not invalidate that node's state... may need to actually make it a reference, so that a parent node can decide whether to consume that node. Basically passing a "consume this node" function as a state value, that correctly internally triggers the optimizer infrastructure to change the tree as a result.
|
||||
// FIXME: Consume the compute nodes, and have an optimizer that removes empty computeMultiple nodes
|
@ -0,0 +1,185 @@
|
||||
"use strict";
|
||||
|
||||
const syncpipe = require("syncpipe");
|
||||
const splitFilter = require("split-filter");
|
||||
const asExpression = require("as-expression");
|
||||
|
||||
const NoChange = require("./util/no-change");
|
||||
const deriveNode = require("../derive-node");
|
||||
const operations = require("../operations");
|
||||
const typeOf = require("../type-of");
|
||||
const unreachable = require("../unreachable");
|
||||
const concat = require("../concat");
|
||||
const merge = require("../merge");
|
||||
|
||||
const uniqueByPredicate = require("../unique-by-predicate");
|
||||
|
||||
// FIXME: Support for remote field names
|
||||
|
||||
function uniqueFields(fields) {
|
||||
return uniqueByPredicate(fields, (field) => field.name);
|
||||
}
|
||||
|
||||
/*
|
||||
valid fields when collapsing:
|
||||
- fields that appear in the collapseBy field list, within or without a hierarchical wrapper
|
||||
- any field that is wrapped in an aggregrate function of some sort
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
name: "set-collapse-by-fields",
|
||||
category: [ "normalization" ],
|
||||
visitors: {
|
||||
collapseBy: (_node, { setState }) => {
|
||||
setState("isCollapsing", true);
|
||||
return NoChange;
|
||||
},
|
||||
field: (node, { setState }) => {
|
||||
setState("fieldSeen", node);
|
||||
return NoChange;
|
||||
},
|
||||
// FIXME: Think of a generic way to express "only match fields under this specific child property" (+ direct descendants also?) -- maybe (node, property, parentNode) signature?
|
||||
collapseByFields: (node, { registerStateHandler, setState, defer }) => {
|
||||
let fields = [];
|
||||
|
||||
registerStateHandler("fieldSeen", (node) => {
|
||||
fields.push(node);
|
||||
});
|
||||
|
||||
return defer(() => {
|
||||
setState("setCollapsedFields", fields);
|
||||
return NoChange;
|
||||
});
|
||||
},
|
||||
addFields: (node, { setState }) => {
|
||||
setState("setAddFields", node.fields);
|
||||
return NoChange;
|
||||
},
|
||||
onlyFields: (node, { setState }) => {
|
||||
setState("setOnlyFields", node.fields);
|
||||
return NoChange;
|
||||
},
|
||||
aggregrateFunction: (node, { registerStateHandler }) => {
|
||||
// FIXME: Also report isCollapsing here, due to aggregrate function use, but make sure that the error describes this as the (possible) cause
|
||||
return NoChange;
|
||||
},
|
||||
compute: (node, { setState }) => {
|
||||
setState("computeSeen", node);
|
||||
return NoChange;
|
||||
},
|
||||
select: (node, { registerStateHandler, defer }) => {
|
||||
let isCollapsing;
|
||||
let onlyFields = [];
|
||||
let addFields = [];
|
||||
let computes = [];
|
||||
let collapsedFields;
|
||||
|
||||
registerStateHandler("isCollapsing", (value) => {
|
||||
isCollapsing = isCollapsing || value;
|
||||
});
|
||||
|
||||
registerStateHandler("setCollapsedFields", (fields) => {
|
||||
if (collapsedFields == null) {
|
||||
collapsedFields = fields;
|
||||
} else {
|
||||
throw new Error(`You can currently only specify a single 'collapseBy' clause. Please file an issue if you have a reason to need more than one!`);
|
||||
}
|
||||
});
|
||||
|
||||
registerStateHandler("setOnlyFields", (fields) => {
|
||||
onlyFields = onlyFields.concat(fields);
|
||||
});
|
||||
|
||||
registerStateHandler("setAddFields", (fields) => {
|
||||
addFields = addFields.concat(fields);
|
||||
});
|
||||
|
||||
registerStateHandler("computeSeen", (node) => {
|
||||
computes.push(node);
|
||||
});
|
||||
|
||||
return defer(() => {
|
||||
if (isCollapsing) {
|
||||
if (addFields.length > 0) {
|
||||
let extraFieldNames = addFields.map((field) => field.name);
|
||||
|
||||
throw new Error(`You tried to add extra fields (${extraFieldNames.join(", ")}) in your query, but this is not possible when using collapseBy. See [FIXME: link] for more information, and how to solve this.`);
|
||||
} else if (onlyFields.length > 0) {
|
||||
// NOTE: This can happen either because the user specified an onlyFields clause, *or* because a previous run of this optimizer did so!
|
||||
let uniqueSelectedFields = uniqueFields(onlyFields);
|
||||
let collapsedFieldNames = collapsedFields.map((field) => field.name);
|
||||
|
||||
let invalidFieldSelection = uniqueSelectedFields.filter((node) => {
|
||||
let isAggregrateComputation = typeOf(node) === "alias" && typeOf(node.expression) === "aggregrateFunction";
|
||||
let isCollapsedField = typeOf(node) === "field" && collapsedFieldNames.includes(node.name);
|
||||
|
||||
let isValid = isAggregrateComputation || isCollapsedField;
|
||||
|
||||
return !isValid;
|
||||
});
|
||||
|
||||
// FIXME: We can probably optimize this by marking the optimizer-created onlyFields as inherently-valid, via some sort of node metadata mechanism
|
||||
|
||||
if (invalidFieldSelection.length > 0) {
|
||||
let invalidFieldNames = invalidFieldSelection.map((item) => {
|
||||
let fieldType = typeOf(item);
|
||||
|
||||
if (fieldType === "field") {
|
||||
return item.name;
|
||||
} else if (fieldType === "alias") {
|
||||
// FIXME: Show alias target instead of field name here?
|
||||
return item.field.name;
|
||||
} else {
|
||||
return unreachable(`Encountered '${fieldType}' node in invalid fields`);
|
||||
}
|
||||
});
|
||||
|
||||
throw new Error(`You tried to include one or more field in your query (${invalidFieldNames.join(", ")}), that are not used in a collapseBy clause or aggregrate function. See [FIXME: link] for more information.`);
|
||||
} else {
|
||||
return NoChange;
|
||||
}
|
||||
} else {
|
||||
// TODO: Move compute -> alias/withResult conversion out into its own optimizer eventually, as it's a separate responsibility
|
||||
let [ withResultComputes, aliasComputes ] = splitFilter(computes, (node) => typeof node.expression === "function");
|
||||
|
||||
let computeAliases = aliasComputes.map((node) => {
|
||||
return operations.alias(node.field, node.expression);
|
||||
});
|
||||
|
||||
let withResultsOperations = asExpression(() => {
|
||||
if (withResultComputes.length > 0) {
|
||||
return operations.withResult((item) => {
|
||||
// FIXME: Remote fields support
|
||||
let computedFields = syncpipe(withResultComputes, [
|
||||
(_) => _.map((compute) => {
|
||||
let fieldName = compute.field.name;
|
||||
return [ fieldName, compute.expression(item[fieldName]) ];
|
||||
}),
|
||||
(_) => Object.fromEntries(_)
|
||||
]);
|
||||
|
||||
return merge(item, computedFields);
|
||||
});
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
return deriveNode(node, {
|
||||
clauses: node.clauses.concat([
|
||||
operations.onlyFields(concat([
|
||||
collapsedFields,
|
||||
computeAliases
|
||||
])),
|
||||
withResultsOperations
|
||||
])
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
// FIXME: a ConsumeNode marker, like RemoveNode but it does not invalidate that node's state... may need to actually make it a reference, so that a parent node can decide whether to consume that node. Basically passing a "consume this node" function as a state value, that correctly internally triggers the optimizer infrastructure to change the tree as a result.
|
||||
// FIXME: Consume the compute nodes, and have an optimizer that removes empty computeMultiple nodes
|
@ -0,0 +1,47 @@
|
||||
"use strict";
|
||||
|
||||
const matchValue = require("match-value");
|
||||
|
||||
const operations = require("../operations");
|
||||
const internalOperations = require("../internal-operations");
|
||||
const NoChange = require("./util/no-change");
|
||||
const typeOf = require("../type-of");
|
||||
|
||||
let valueListTypes = new Set([ "anyOfValues", "allOfValues" ]);
|
||||
|
||||
module.exports = {
|
||||
name: "values-to-conditions",
|
||||
category: [ "normalization" ],
|
||||
visitors: {
|
||||
condition: (rootNode) => {
|
||||
let isListType = valueListTypes.has(typeOf(rootNode.expression));
|
||||
let listContainsArray = isListType && Array.isArray(rootNode.expression.items); // FIXME: Make this `unpackable` metadata on the AST node?
|
||||
|
||||
if (!isListType || !listContainsArray) {
|
||||
return NoChange;
|
||||
} else {
|
||||
function convertListNode(node) {
|
||||
let listOperation = matchValue.literal(typeOf(node), {
|
||||
anyOfValues: operations.anyOf,
|
||||
allOfValues: operations.allOf,
|
||||
});
|
||||
|
||||
let conditions = node.items.map((item) => {
|
||||
if (valueListTypes.has(typeOf(item))) {
|
||||
return convertListNode(item);
|
||||
} else {
|
||||
return internalOperations._condition({
|
||||
type: rootNode.conditionType,
|
||||
expression: item
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return listOperation(conditions);
|
||||
}
|
||||
|
||||
return convertListNode(rootNode.expression);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
@ -1,16 +0,0 @@
|
||||
"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,23 @@
|
||||
"use strict";
|
||||
|
||||
const matchesFormat = require("@validatem/matches-format");
|
||||
|
||||
const isCollectionName = require("./is-collection-name");
|
||||
const isLocalFieldName = require("./is-local-field-name");
|
||||
|
||||
module.exports = [
|
||||
// FIXME: Validate format properly
|
||||
matchesFormat(/^[^.]+\.[^.]+$/),
|
||||
(string) => {
|
||||
let [ collectionName, fieldName ] = string.split(".");
|
||||
|
||||
return {
|
||||
collection: collectionName,
|
||||
field: fieldName
|
||||
};
|
||||
},
|
||||
{ // FIXME: Make any validation errors at this point be represented as virtual properties, as it's a parse result
|
||||
collection: [ isCollectionName ],
|
||||
field: [ isLocalFieldName ]
|
||||
}
|
||||
];
|
@ -0,0 +1,20 @@
|
||||
"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 isValueExpression = require("./is-value-expression")(operations);
|
||||
const isObjectType = require("./is-object-type")(operations);
|
||||
|
||||
return either([
|
||||
isValueExpression,
|
||||
[ isObjectType("anyOfValues") ],
|
||||
[ isObjectType("allOfValues") ]
|
||||
]);
|
||||
};
|
@ -0,0 +1,15 @@
|
||||
"use strict";
|
||||
|
||||
const wrapError = require("@validatem/wrap-error");
|
||||
const either = require("@validatem/either");
|
||||
const isLocalFieldName = require("../is-local-field-name");
|
||||
|
||||
module.exports = function (operations) {
|
||||
const wrapWithOperation = require("./wrap-with-operation")(operations);
|
||||
const isObjectType = require("./is-object-type")(operations);
|
||||
|
||||
return wrapError("Must be a local field name or object", either([
|
||||
[ isObjectType("field") ],
|
||||
[ isLocalFieldName, wrapWithOperation("field") ]
|
||||
]));
|
||||
};
|
@ -0,0 +1,19 @@
|
||||
"use strict";
|
||||
|
||||
const either = require("@validatem/either");
|
||||
|
||||
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.
|
||||
|
||||
module.exports = function (operations) {
|
||||
const isObjectType = require("./is-object-type")(operations);
|
||||
const wrapWithOperation = require("./wrap-with-operation")(operations);
|
||||
|
||||
return either([
|
||||
[ isObjectType("sqlExpression") ],
|
||||
[ isObjectType("literalValue") ],
|
||||
[ isObjectType("field") ],
|
||||
[ isLiteralValue, wrapWithOperation("value") ]
|
||||
]);
|
||||
};
|
@ -1,15 +0,0 @@
|
||||
"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,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 isAnyFieldObject = require("./is-any-field-object")(operations);
|
||||
const wrapPossiblyRemoteFieldName = require("./wrap-possibly-remote-field-name")(operations);
|
||||
|
||||
return wrapError("Must be a field name or object", either([
|
||||
[ isAnyFieldObject ],
|
||||
[ isString, wrapPossiblyRemoteFieldName ]
|
||||
]));
|
||||
};
|
@ -0,0 +1,8 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = function (operations) {
|
||||
const isSelectClause = require("./is-select-clause")(operations);
|
||||
|
||||
// FIXME: Check whether certain select clauses are invalid in relations, and/or whether additional clauses need to be made valid
|
||||
return isSelectClause;
|
||||
};
|
@ -0,0 +1,15 @@
|
||||
"use strict";
|
||||
|
||||
const wrapError = require("@validatem/wrap-error");
|
||||
const either = require("@validatem/either");
|
||||
const isRemoteFieldName = require("../is-remote-field-name");
|
||||
|
||||
module.exports = function (operations) {
|
||||
const wrapWithOperation = require("./wrap-with-operation")(operations);
|
||||
const isObjectType = require("./is-object-type")(operations);
|
||||
|
||||
return wrapError("Must be a remote field name or object", either([
|
||||
[ isObjectType("remoteField") ],
|
||||
[ isRemoteFieldName, wrapWithOperation("remoteField") ]
|
||||
]));
|
||||
};
|
@ -0,0 +1,17 @@
|
||||
"use strict";
|
||||
|
||||
const wrapError = require("@validatem/wrap-error");
|
||||
const ValidationError = require("@validatem/error");
|
||||
|
||||
module.exports = function (operations) {
|
||||
const isIndexType = require("./is-index-type")(operations);
|
||||
|
||||
return wrapError("Must be a composite index type", [
|
||||
isIndexType,
|
||||
(node) => {
|
||||
if (node.composite !== true) {
|
||||
return new ValidationError(`Must be a composite index`);
|
||||
}
|
||||
}
|
||||
]);
|
||||
};
|
@ -0,0 +1,16 @@
|
||||
"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 field type", either([
|
||||
isObjectType("autoIDField"),
|
||||
isObjectType("stringField"),
|
||||
isObjectType("timestampField"),
|
||||
isObjectType("booleanField"),
|
||||
isObjectType("uuidField"),
|
||||
]));
|
||||
};
|
@ -0,0 +1,27 @@
|
||||
"use strict";
|
||||
|
||||
const either = require("@validatem/either");
|
||||
const wrapError = require("@validatem/wrap-error");
|
||||
const anyProperty = require("@validatem/any-property");
|
||||
const required = require("@validatem/required");
|
||||
const isLocalFieldName = require("../../is-local-field-name");
|
||||
|
||||
module.exports = function (operations) {
|
||||
const isObjectType = require("../is-object-type")(operations);
|
||||
const isFieldType = require("./is-field-type")(operations);
|
||||
const isIndexType = require("./is-index-type")(operations);
|
||||
|
||||
return wrapError("Must be an object of schema fields", anyProperty({
|
||||
key: [ required, isLocalFieldName ],
|
||||
value: [ required, either([
|
||||
isFieldType,
|
||||
isIndexType,
|
||||
isObjectType("belongsTo"), // FIXME: Correct type? Need to distinguish between reference and definition usage (local vs. remote field name)
|
||||
isObjectType("defaultTo"),
|
||||
isObjectType("optional"),
|
||||
// FIXME: Only allow deleteField/restoreAs for changeCollection, not createCollection
|
||||
isObjectType("deleteField"),
|
||||
isObjectType("restoreAs"),
|
||||
])]
|
||||
}), { 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 an index type", either([
|
||||
isObjectType("index"),
|
||||
isObjectType("indexWhere"),
|
||||
isObjectType("unique"),
|
||||
isObjectType("uniqueWhere"),
|
||||
]));
|
||||
};
|
@ -1,11 +0,0 @@
|
||||
"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 wrapPossiblyRemoteFieldName(value) {
|
||||
if (value.includes(".")) {
|
||||
return operations.remoteField(value);
|
||||
} else {
|
||||
return operations.field(value);
|
||||
}
|
||||
};
|
||||
};
|
Loading…
Reference in New Issue