master
Sven Slootweg 4 years ago
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()
})
]);

@ -5,7 +5,7 @@ Error.stackTraceLimit = 100;
const util = require("util");
const chalk = require("chalk");
let { select, onlyColumns, where, withRelations, withDerived, column, through, inValues, unsafeSQL, postProcess, belongsTo, has, value, parameter, not, anyOf, allOf, lessThan, moreThan, alias, foreignColumn, table, expression, equals, collapseBy, compute, hierarchical, sum, count, addColumns } = require("../src/operations");
let { select, onlyFields, where, withRelations, withDerived, field, through, inValues, unsafeSQL, postProcess, belongsTo, has, value, parameter, not, anyOf, allOf, lessThan, moreThan, alias, foreignColumn, collection, expression, equals, collapseBy, compute, hierarchical, sum, count, addFields } = require("../src/operations");
const astToQuery = require("../src/ast-to-query");
const optimizeAST = require("../src/ast/optimize");
const measureTime = require("../src/measure-time");
@ -34,8 +34,8 @@ try {
// Edit me!
/* Available functions:
select, onlyColumns, addColumns, alias,
where, column, foreignColumn, table,
select, onlyFields, addFields, alias,
where, field, foreignColumn, collection,
value, parameter,
not, anyOf, allOf,
lessThan, moreThan, equals,
@ -46,13 +46,13 @@ try {
// let niceNumbers = anyOf([ 1, 2, 3 ]);
// query = select("projects", [
// onlyColumns([ "foo" ]),
// onlyFields([ "foo" ]),
// where({
// number_one: niceNumbers,
// number_two: niceNumbers
// }),
// where({
// number_three: anyOf([ 42, column("number_one") ]),
// number_three: anyOf([ 42, field("number_one") ]),
// number_four: moreThan(1337)
// })
// ]);
@ -78,14 +78,65 @@ try {
*/
// SELECT country_id, store_id, color, size, COUNT(*) AS total_sold, SUM(price) AS total_revenue FROM sales GROUP BY color, size, ROLLUP (country_id, store_id);
query = select("sales", [
collapseBy([ "color", "size", hierarchical([ "country_id", "store_id" ]) ], [
compute({
total_sold: count(),
total_revenue: sum("price")
})
])
]);
// query = select("sales", [
// collapseBy([ "color", "size", hierarchical([ "country_id", "store_id" ]) ], [
// compute({
// total_sold: count(),
// total_revenue: sum("price")
// })
// ])
// ]);
// FIXME: Test with nested any/all
// query = select("sales", [
// where({ size: moreThan(anyOf(parameter("sizes"))) }), // should work
// // where({ size: moreThan(anyOf([1, unsafeSQL("foo"), 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")
// })
// ])
// ]);
query = createTable("actors", {
fields: {
imdb_id: string(),
name: [ required(), string() ],
date_of_birth: date({ withTimezone: true }),
place_of_birth: string(),
imdb_metadata_scraped: json()
}
});
query = createTable("movies", {
fields: {
imdb_id: string(),
name: string(),
imdb_metadata_scraped: json()
}
});
query = createTable("appearances", {
fields: {
actor: belongsTo("actors"),
movie: belongsTo("movie")
}
});
updateTable("appearances", {
fields: {
}
})
// FIXME: For query generation, ensure we are generating correct queries for TRUE/FALSE/NULL/NOTNULL (in particular, moreThan/lessThan should not allow these!)
// FIXME: Partial indexes
// FIXME: not(anyOf(...))
// FIXME: anyOfValues rendering without arrayify optimizer
// FIXME: index, indexWhere(...), unique
});
console.log(util.inspect(query, { depth: null, colors: true }));
@ -164,7 +215,7 @@ try {
// 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: I guess `withDerived` could be implemented externally, as something that (depending on value type) either a) adds a field selector or b) adds a post-processing hook
// FIXME: Aggregrates (GROUP BY)
// return db.execute(query);

@ -1,3 +1,4 @@
/* eslint-disable no-undef */
"use strict";
// JOIN
@ -64,8 +65,10 @@ query = select("reviews", [
movie: { title: includes("Movie") }
}),
collapseBy("movie_id", [
compute({ positive_review_count: count() }),
renameColumn("movie.title", "title")
compute({
positive_review_count: count(),
title: "movie.title"
}),
])
]);
@ -432,3 +435,63 @@ combine(/* [ clause ], allOf([ clause ]), anyOf([ clause]) */);
// }),
// mapCase({ from: "snake", to: "camel" })
// ]);
query = select("bandwidth_measurements", [
collapseBy("customer_id", [
compute({ usage: nthPercentile(95, "usage") })
])
]);
SELECT expirations.ban_id, max(expire_id) FROM expirations
WHERE expire < ? AND removed_at IS NULL
GROUP BY expirations.ban_id
query = select("expirations", [
where({
expire: lessThan(currentTime()),
removedAt: isNull()
}),
collapseBy("ban_id", [
compute({ latest_expiry_id: max("expire_id") })
])
]);
let waypointsForTrip = select("waypoints", [
onlyColumns([ "position" ]),
compute({
distance: postgis.distanceBetween(
column("position"),
fromPreviousRow("position", sortedBy("generated_at"))
)
}),
where({
user_id: foreignColumn("trips.user_id"),
generated_at: between(column("trips.started_at"), column("trips.ended_at"))
}),
sortedBy("generated_at")
]);
select("trips", [
define({
user: belongsTo("user_id"),
waypoints: waypointsForTrip,
computed_trip: select("waypoints", [
compute({
distance: sum("distance"),
route: postgis.makeLine("waypoints")
})
]),
}),
where({ user: { team_id: 42 } }),
addColumns([ "user.*" ]),
compute({
average_distance_per_ms: divide("computed_trip.distance", subtract("ended_at", "started_at")),
route_as_geojson: postgis.asGeoJSON("computed_trip.route"),
route_as_polyline: postgis.asEncodedPolyline("computed_trip.route"),
distance: "computed_trip.distance",
route: "computed_trip.route"
}),
sortedBy(descending("ended_at"))
])

@ -11,12 +11,37 @@ Todo:
- Boolean logic
x anyOf(...)
x allOf(...)
- Annotate placeholder/parameter nodes with the expected parameter type in their position, for validation of input parameters at query time
- Rename table -> collection everywhere, also column -> field?
- Update wrapError API
Docs:
- Emphasize that users should feel free to experiment; the library will tell them (safely) if they are trying to do something invalid - need to help the user overcome the "but what if I get it wrong" fear (also emphasize the flexibility of schema updates to help with this, "it can always be fixed later" or so)
- 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`)
- Explain the base mental model for relations; the relationship between things is always that one Thing 'owns' many other things. Therefore, has(...) means all the items in a collection that reference the current item in their (remote) column, whereas belongsTo(...) means an item in another collection that's referenced in one of the current item's local columns
- Explain that aggregrates (collapseBy) are always applied in reference to the being-queried table; eg. if you want to collapse user counts by their role_id, you should base-query the users table, not the roles table
- Demonstrate the usefulness of normalization through examples:
- Deduplication of data (eg. user.friends containing duplicate user records)
- Recursion is impossible (eg. user -> posts -> replyUsers or something)
Considerations:
- Disallow non-lowercase column/table names entirely due to PostgreSQL weirdness? Or just cast them to lowercase?
- For rollback of NOT NULL column deletions in migrations: require each column deletion change to include an initializer function for recreating it afterwards, otherwise it cannot be rolled back
- Before applying or rolling back a migration, all the migrations should be evaluated in-memory, so that the state at any given migration can be determined
- Instead of applying the delta in the migration directly, always generate the "actual delta" from the evaluated state at the previous point vs. that at the new point? This can skip intermediate steps when eg. one migration adds a column that the next migration removes, and should generally result in a much more consistent outcome (similar to deterministic package management)
FAQ:
- Why have you changed all the names of things, like table and column and GROUP BY?
- One of the primary goals of zapdb is to get more people using relational databases who were previously intimidated by them. Unfortunately, typical RDMBSes are full of jargon that isn't intuitively understandable to many new users. Because of this, zapdb uses more intuitive terms that users can understand without having to memorize them - it's much easier for experienced RDBMS users to adapt to intuitive terms, than it is for inexpected users to learn the jargon.
Terminology:
- Collection: table
- Field: column
- Local
- Remote: foreign column
- Operation: any kind of item in a query; eg. select(...), but also where(...) and collapseBy(...) -- need to emphasize that this does *not* imply immediate execution
- Clause: any kind of operation that modifies the behaviour of its parent operation in some way; eg. a where(...) inside of a select(...) or inside of a `has(...)`
- Predicate: condition, like moreThan(3)
----
@ -37,6 +62,7 @@ Recommendations to make:
Ideas:
- Make placeholders typed so that we can warn the user when they try to pass in invalid things?
- Safe client API that returns a disposer instead of a client upon client creation?
----
@ -174,7 +200,7 @@ MARKER:
NOTE: May need https://www.npmjs.com/package/browser-hrtime for process.hrtime.bigint support in browsers! Need to investigate whether this (or something similar) is already being used by bundlers by default, or whether they use a shim without bigint support.
FIXME: Remove all the .type stuff and replace with typeOf()
FIXME: Document that `DEBUG=raqb:ast:optimize:*` can be used for tracking down unstable optimizers (symptom = stack overflow)
FIXME: Document that `DEBUG=raqb:ast:optimize:*` can be used for tracking down unstable optimizers (symptom = error telling you so, should not be a stack overflow anymore)
Pitch: "Easier than MongoDB", no more if(foo.bar) checks, no more data format mismatches, the database ensures for you that all data looks like expected, and it's easy to change the format if needed.
@ -320,3 +346,34 @@ for each country_id
for (each store_id + null)
for each color
for each size
NTILE(3) OVER ( ORDER BY amount )
order by `amount`, then divide into 3 equally-sized buckets, and for each record return its bucket ID
NTILE(3) OVER ( PARTITION BY YEAR ORDER BY amount )
same as above, but produce a record for each distinct year (like a GROUP BY)
SELECT foo, bar, <foo> OVER ( <bar> )
Windowing function
like GROUP BY, but the rows are not actually collapsed in the output result; they are just collapsed behind the scenes, so that aggregrate functions can be used over their virtual groups
SELECT employee_id, avg(salary) OVER (PARTITION BY department_id) AS average_department_salary FROM salary;
SELECT department_id, avg(salary) FROM salary GROUP BY department_id;
---------
# Relation operations
through([ item, item, ... ])
item: columName [belongsTo] | remoteColumnName [has] | belongsTo | has
has(remoteColumnName, [ operation, operation, ... ])
operation: any operation that is valid in a SELECT?
belongsTo(columnName)
linkTo(remoteColumnName)

@ -1,10 +1,10 @@
{
"name": "raqb",
"version": "1.0.0",
"name": "zapdb",
"version": "0.1.0",
"main": "index.js",
"repository": "git@git.cryto.net:joepie91/raqb.git",
"author": "Sven Slootweg <admin@cryto.net>",
"license": "MIT",
"license": "WTFPL OR CC0-1.0",
"dependencies": {
"@validatem/allow-extra-properties": "^0.1.0",
"@validatem/any-property": "^0.1.3",
@ -18,6 +18,7 @@
"@validatem/is-boolean": "^0.1.1",
"@validatem/is-date": "^0.1.0",
"@validatem/is-function": "^0.1.0",
"@validatem/is-integer": "^0.1.0",
"@validatem/is-number": "^0.1.2",
"@validatem/is-plain-object": "^0.1.1",
"@validatem/is-string": "^0.1.1",
@ -25,6 +26,7 @@
"@validatem/matches-format": "^0.1.0",
"@validatem/nested-array-of": "^0.1.0",
"@validatem/one-of": "^0.1.1",
"@validatem/require-either": "^0.1.0",
"@validatem/required": "^0.1.1",
"@validatem/wrap-error": "^0.1.2",
"acorn": "^6.0.5",
@ -42,17 +44,20 @@
"flatten": "^1.0.3",
"map-obj": "^4.1.0",
"match-value": "^1.1.0",
"pg": "^8.3.3",
"prismjs": "^1.20.0",
"scope-analyzer": "^2.0.5",
"split-filter": "^1.1.3",
"split-filter-n": "^1.1.2",
"syncpipe": "^1.0.0"
"syncpipe": "^1.0.0",
"yargs": "^15.4.1"
},
"devDependencies": {
"@babel/core": "^7.10.4",
"@babel/preset-env": "^7.10.4",
"@babel/preset-react": "^7.10.4",
"@joepie91/eslint-config": "^1.1.0",
"as-table": "^1.0.55",
"babelify": "^10.0.0",
"benchmark": "^2.1.4",
"budo-express": "^1.0.2",

@ -30,13 +30,15 @@ const required = require("@validatem/required");
const either = require("@validatem/either");
const arrayOf = require("@validatem/array-of");
const isString = require("@validatem/is-string");
const isBoolean = require("@validatem/is-boolean");
const isValue = require("@validatem/is-value");
const allowExtraProperties = require("@validatem/allow-extra-properties");
let isPlaceholder = {
__raqbASTNode: isValue(true),
type: isValue("placeholder"),
name: isString
name: isString,
parameterizable: isBoolean
};
// FIXME: Do we want a nested array here? Probably not, since PostgreSQL might not support arbitrarily nested arrays
@ -164,10 +166,10 @@ function $combine(_strings, ... _nodes) {
return $object({ query, params, placeholders });
}
function columnList({ onlyColumns, addColumns }) {
function $fieldList({ onlyColumns, addColumns }) {
let primaryColumns = (onlyColumns.length > 0)
? onlyColumns
: [ { type: "allColumns" } ];
: [ { type: "allFields" } ];
return $join(", ", $handleAll(concat([ primaryColumns, addColumns ])));
}
@ -181,7 +183,7 @@ let simpleColumnNameRegex = /^[a-z_]+(?:\.[a-z_]+)?$/;
function $maybeParenthesize($query) {
if ($query.query === "?" || simpleColumnNameRegex.test($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 or column names 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.
// We don't want to bloat the generated query with unnecessary parentheses, so we leave them out for things that only evaluated to placeholders or field names 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);
@ -200,7 +202,7 @@ function $arrayFrom(items, canBeParameterized) {
if (typeOf(items) === "placeholder") {
return $handle(items);
} else if (!canBeParameterized) {
// 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
// If the list contains eg. 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)}]`;
@ -224,14 +226,14 @@ function $arrayFrom(items, canBeParameterized) {
// FIXME: createQueryObject/$ wrapper, including properties like relation mappings and dependent query queue
let process = {
select: function ({ table, clauses }) {
let $table = $handle(table);
select: function ({ collection, clauses }) {
let $collection = $handle(collection);
let expectedClauseTypes = [ "where", "addColumns", "onlyColumns" ];
let expectedClauseTypes = [ "where", "addFields", "onlyFields" ];
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 onlyFields = clausesByType.onlyFields.map((node) => node.fields).flat();
let addFields = clausesByType.addFields.map((node) => node.fields).flat();
let whereExpressions = clausesByType.where.map((node) => node.expression);
// FIXME: Fold relations
@ -245,56 +247,60 @@ let process = {
});
let $groupByClause = asExpression(() => {
if (clausesByType.collapseBy.length > 0) {
if (clausesByType.collapseBy.length === 1) {
// NOTE: We currently only support a single collapseBy clause
let collapseColumns = clausesByType.collapseBy[0].columns.columns;
let collapseFields = clausesByType.collapseBy[0].fields.fields;
return $combine`GROUP BY ${$join(", ", $handleAll(collapseColumns))}`;
return $combine`GROUP BY ${$join(", ", $handleAll(collapseFields))}`;
} else if (clausesByType.collapseBy.length > 1) {
// FIXME: Is this the correct place to check this?
throw new Error(`Encountered multiple collapseBy clauses in the same query; this is not currently supported`);
} else {
return NoQuery;
}
});
let $columnSelection = columnList({ onlyColumns, addColumns });
let $fieldSelection = $fieldList({ onlyColumns: onlyFields, addColumns: addFields });
let $orderedClauses = [
$table,
$collection,
$whereClause,
$groupByClause
];
return $combine`SELECT ${$columnSelection} FROM ${$join(" ", $orderedClauses)}`;
return $combine`SELECT ${$fieldSelection} FROM ${$join(" ", $orderedClauses)}`;
},
tableName: function ({ name }) {
collection: function ({ name }) {
// FIXME: escape
return $object({
query: name,
params: []
});
},
allColumns: function () {
// Special value used in column selection, to signify any/all columns
allFields: function () {
// Special value used in field selection, to signify any/all field
return $object({
query: "*",
params: []
});
},
columnName: function ({ name }) {
field: function ({ name }) {
// FIXME: escape
return $object({
query: name,
params: []
});
},
hierarchical: function ({ columns }) {
return $combine`ROLLUP (${$join(", ", $handleAll(columns))})`;
// FIXME: Shouldn't there by some remoteField/foreignColumn handling here?
hierarchical: function ({ fields }) {
return $combine`ROLLUP (${$join(", ", $handleAll(fields))})`;
},
alias: function ({ column, expression }) {
alias: function ({ field, expression }) {
// FIXME: escape
let $column = $handle(column);
let $field = $handle(field);
let $expression = $handle(expression);
return $combine`${$expression} AS ${$column}`;
return $combine`${$expression} AS ${$field}`;
},
sqlExpression: function ({ sql, parameters }) {
return $object({
@ -303,7 +309,7 @@ let process = {
});
},
literalValue: function ({ value }) {
// FIXME: Check if this is valid in column selections / aliases
// FIXME: Check if this is valid in field selections / aliases
return $object({
query: "?",
params: [ value ]
@ -391,8 +397,10 @@ function $handle(node) {
}
}
// FIXME: Disallow stringifying things that are not top-level queries! Eg. `columnName`
// FIXME: Disallow stringifying things that are not top-level queries! Eg. `field`
module.exports = function astToQuery(ast) {
// console.log(require("util").inspect({ast}, {colors: true,depth:null}));
let result = $handle(ast);
return $object({

@ -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,6 +1,7 @@
"use strict";
const { validateOptions } = require("@validatem/core");
const either = require("@validatem/either");
const required = require("@validatem/required");
const oneOf = require("@validatem/one-of");
const arrayOf = require("@validatem/array-of");
@ -9,13 +10,20 @@ const isBoolean = require("@validatem/is-boolean");
const node = require("../ast-node");
module.exports = function (operations) {
const isValueExpression = require("../validators/operations/is-value-expression")(operations);
const isConditionValue = require("../validators/operations/is-condition-value")(operations);
const isObjectType = require("../validators/operations/is-object-type")(operations);
return function _arrayOf(_options) {
let { type, items, canBeParameterized } = validateOptions(arguments, {
type: [ required, oneOf([ "anyOf", "allOf" ]) ],
canBeParameterized: [ required, isBoolean ],
items: [ required, arrayOf(isValueExpression) ]
items: [ required, either([
arrayOf(either([
isConditionValue,
isObjectType("_arrayOf")
])),
isObjectType("placeholder")
])]
});
return node({

@ -9,12 +9,12 @@ const node = require("../ast-node");
module.exports = function (operations) {
const isInternalArrayType = require("../validators/operations/is-internal-array-type")(operations);
const isValueExpression = require("../validators/operations/is-value-expression")(operations);
const isConditionValue = require("../validators/operations/is-condition-value")(operations);
return function _condition(_options) {
let { type, expression } = validateOptions(arguments, {
type: [ required, oneOf([ "lessThan", "moreThan", "equals" ]) ],
expression: [ required, either([ isValueExpression, isInternalArrayType ]) ]
expression: [ required, either([ isConditionValue, isInternalArrayType ]) ]
});
return node({

@ -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 });
};
};

@ -2,25 +2,20 @@
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);
const isField = require("../validators/operations/is-field");
return function alias(_name, _expression) {
let [ name, expression ] = validateArguments(arguments, {
name: [ required, either([
[ isObjectType("columnName") ],
[ isString, wrapWithOperation("column") ]
])],
name: [ required, isField ],
// FIXME: Make more strict to only accept column references, and expect the user to use compute for the rest (which should internally use an _expression type or so to pass into alias, during set-collapse-by-field)
expression: [ required, isValueExpression ]
});
return node({ type: "alias", column: name, expression: expression });
return node({ type: "alias", field: name, expression: expression });
};
};

@ -14,7 +14,12 @@ module.exports = function (operations) {
items: [ required, isPredicateList ]
});
if (items.type === "conditions") {
if (items.type === "values") {
return node({
type: "allOfValues",
items: items.value
});
} else if (items.type === "conditions") {
return node({
type: "allOfConditions",
items: items.value

@ -14,7 +14,12 @@ module.exports = function (operations) {
items: [ required, isPredicateList ]
});
if (items.type === "conditions") {
if (items.type === "values") {
return node({
type: "anyOfValues",
items: items.value
});
} else if (items.type === "conditions") {
return node({
type: "anyOfConditions",
items: items.value

@ -6,21 +6,23 @@ const arrayOf = require("@validatem/array-of");
const node = require("../ast-node");
// FIXME: Allow single field that gets auto-array-wrapped?
module.exports = function (operations) {
const isCollapsibleColumn = require("../validators/operations/is-collapsible-column")(operations);
const isCollapsibleField = require("../validators/operations/is-collapsible-field")(operations);
const isCollapseByClause = require("../validators/operations/is-collapse-by-clause")(operations);
return function collapseBy(_columns) {
let [ columns, clauses ] = validateArguments(arguments, {
columns: [ required, arrayOf([ required, isCollapsibleColumn ]) ],
return function collapseBy(_fields) {
let [ fields, clauses ] = validateArguments(arguments, {
fields: [ required, arrayOf([ required, isCollapsibleField ]) ],
clauses: [ arrayOf([ isCollapseByClause ]) ]
});
return node({
type: "collapseBy",
columns: node({
type: "collapseByColumns",
columns: columns
fields: node({
type: "collapseByFields",
fields: fields
}),
clauses: clauses
});

@ -1,7 +1,6 @@
"use strict";
const { validateArguments } = require("@validatem/core");
const either = require("@validatem/either");
const required = require("@validatem/required");
const anyProperty = require("@validatem/any-property");
const isString = require("@validatem/is-string");
@ -35,7 +34,7 @@ module.exports = function (operations) {
items: Object.entries(items.value).map(([ key, value ]) => {
return node({
type: "compute",
column: operations.column(key),
field: operations.field(key),
expression: value
});
})

@ -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 ]
});
};
};

@ -6,11 +6,11 @@ const required = require("@validatem/required");
const node = require("../ast-node");
module.exports = function (operations) {
const isValueExpression = require("../validators/operations/is-value-expression")(operations);
const isConditionValue = require("../validators/operations/is-condition-value")(operations);
return function equals(_name, _expression) {
let [ expression ] = validateArguments(arguments, {
expression: [ required, isValueExpression ]
expression: [ required, isConditionValue ]
});
return node({ type: "condition", conditionType: "equals", expression: expression });

@ -7,15 +7,15 @@ 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);
const isPossibleRemoteField = require("../validators/operations/is-possibly-remote-field")(operations);
return function expression(_options) {
let { left, condition } = validateOptions(arguments, {
left: [ required, isPossiblyForeignColumn ], // FIXME: allow sqlExpression and such
left: [ required, isPossibleRemoteField ], // FIXME: allow sqlExpression and such
condition: [ required, isCondition ]
});
// FIXME/MARKER: Rename to 'assert'?
// FIXME/MARKER: Rename to 'assert' or something like that?
return node({
type: "expression",

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

@ -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: []
});
};
};

@ -7,16 +7,16 @@ const arrayOf = require("@validatem/array-of");
const node = require("../ast-node");
module.exports = function (operations) {
const isPossiblyForeignColumn = require("../validators/operations/is-possibly-foreign-column")(operations);
const isPossiblyRemoteField = require("../validators/operations/is-possibly-remote-field")(operations);
return function hierarchical(_columns) {
let [ columns ] = validateArguments(arguments, {
columns: [ required, arrayOf([ required, isPossiblyForeignColumn ]) ] // FIXME: Require minimum 2, probably
return function hierarchical(_fields) {
let [ fields ] = validateArguments(arguments, {
fields: [ required, arrayOf([ required, isPossiblyRemoteField ]) ] // FIXME: Require minimum 2, probably
});
return node({
type: "hierarchical",
columns: columns
fields: fields
});
};
};

@ -7,35 +7,11 @@
// FIXME: Verify that all wrapWith calls have a corresponding isObjectType arm
require("array.prototype.flat").shim();
const splitFilter = require("split-filter");
const asExpression = require("as-expression");
const { validateArguments } = require("@validatem/core");
const required = require("@validatem/required");
const nestedArrayOf = require("@validatem/nested-array-of");
const either = require("@validatem/either");
const isString = require("@validatem/is-string");
const isFunction = require("@validatem/is-function");
const defaultTo = require("@validatem/default-to");
const anyProperty = require("@validatem/any-property");
const node = require("../ast-node");
const flatten = require("../validators/flatten");
const evaluateCyclicalModulesOnto = require("../evaluate-cyclical-modules-onto");
// FIXME: Hack until all operations have been moved over to modules
const isPossiblyForeignColumn = require("../validators/operations/is-possibly-foreign-column")(module.exports);
const isSelectClause = require("../validators/operations/is-select-clause")(module.exports);
const isObjectType = require("../validators/operations/is-object-type")(module.exports);
function normalizeClauses(clauses) {
if (clauses != null) {
return clauses.flat(Infinity);
} else {
return clauses;
}
}
// NOTE: withDerived has been replaced with compute
// FIXME: All of the below need to be refactored, and moved into operation modules
let operations = {
@ -56,82 +32,6 @@ let operations = {
};
})
});
},
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)
});
}
};
@ -139,15 +39,15 @@ let operationModules = {
// Base operations
select: require("./select"),
// Column selection
addColumns: require("./add-columns"),
onlyColumns: require("./only-columns"),
// Field selection
addFields: require("./add-fields"),
onlyFields: require("./only-fields"),
alias: require("./alias"),
// Reference/scalar types
column: require("./column"),
foreignColumn: require("./column"),
table: require("./table"),
field: require("./field"),
remoteField: require("./remote-field"),
collection: require("./collection"),
value: require("./value"),
// Filtering
@ -175,64 +75,48 @@ let operationModules = {
count: require("./count"),
sum: require("./sum"),
// Relations and subqueries
belongsTo: require("./relations/belongs-to"),
has: require("./relations/has"),
through: require("./relations/through"),
withRelations: require("./relations/with-relations"),
define: require("./relations/define"),
resultExists: require("./relations/result-exists"), // FIXME: Better name?
linkTo: require("./relations/link-to"),
// Misc.
parameter: require("./parameter"),
postProcess: require("./post-process"),
unsafeSQL: require("./unsafe-sql"),
now: require("./functions/now"),
// Schema definitions
createCollection: require("./schema/create-collection"),
changeCollection: require("./schema/change-collection"),
deleteCollection: require("./schema/delete-collection"),
fields: require("./schema/fields"),
indexes: require("./schema/indexes"),
deleteField: require("./schema/delete-field"),
restoreAs: require("./schema/restore-as"),
optional: require("./schema/optional"),
defaultTo: require("./schema/default-to"),
// Field types
autoID: require("./types/auto-id"),
boolean: require("./types/boolean"),
string: require("./types/string"),
uuid: require("./types/uuid"),
timestamp: require("./types/timestamp"),
// Index types
primaryKey: require("./indexes/primary-key"),
index: require("./indexes/index"),
indexWhere: require("./indexes/index-where"),
unique: require("./indexes/unique"),
uniqueWhere: require("./indexes/unique-where"),
};
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,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"
});
};
};

@ -6,11 +6,11 @@ const required = require("@validatem/required");
const node = require("../ast-node");
module.exports = function (operations) {
const isValueExpression = require("../validators/operations/is-value-expression")(operations);
const isConditionValue = require("../validators/operations/is-condition-value")(operations);
return function lessThan(_name, _expression) {
let [ expression ] = validateArguments(arguments, {
expression: [ required, isValueExpression ]
expression: [ required, isConditionValue ]
});
// FIXME: Calling it `expression` is super confusing. That needs a better name.

@ -6,11 +6,11 @@ const required = require("@validatem/required");
const node = require("../ast-node");
module.exports = function (operations) {
const isValueExpression = require("../validators/operations/is-value-expression")(operations);
const isConditionValue = require("../validators/operations/is-condition-value")(operations);
return function moreThan(_name, _expression) {
let [ expression ] = validateArguments(arguments, {
expression: [ required, isValueExpression ]
expression: [ required, isConditionValue ]
});
return node({ type: "condition", conditionType: "moreThan", expression: expression });

@ -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 });
};
};

@ -14,7 +14,8 @@ module.exports = function (_operations) {
return node({
type: "placeholder",
name: name
name: name,
parameterizable: true
});
};
};

@ -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
});
};
};

@ -14,15 +14,15 @@ module.exports = function (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") ]
return function select(_collection, _clauses) {
let [ collection, clauses ] = validateArguments(arguments, {
collection: [ required, either([
[ isObjectType("collection") ],
[ isString, wrapWithOperation("collection") ]
])],
clauses: [ required, nestedArrayOf(isSelectClause), flatten ]
});
return node({ type: "select", table: table, clauses: clauses });
return node({ type: "select", collection: collection, clauses: clauses });
};
};

@ -6,17 +6,17 @@ const required = require("@validatem/required");
const node = require("../ast-node");
module.exports = function count(operations) {
const isPossiblyForeignColumn = require("../validators/operations/is-possibly-foreign-column")(operations);
const isPossiblyRemoteField = require("../validators/operations/is-possibly-remote-field")(operations);
return function sum(_column) {
let [ column ] = validateArguments(arguments, {
column: [ required, isPossiblyForeignColumn ]
return function sum(_field) {
let [ field ] = validateArguments(arguments, {
field: [ required, isPossiblyRemoteField ]
});
return node({
type: "aggregrateFunction",
functionName: "sum",
args: [ column ]
args: [ field ]
});
};
};

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

@ -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"
});
};
};

@ -10,16 +10,17 @@ const concat = require("../concat");
const typeOf = require("../type-of");
const NoChange = require("./util/no-change");
let parameterizableTypes = [ "literalValue", "placeholder" ];
let parameterizableTypes = new Set([ "literalValue", "placeholder" ]);
let disallowedNestedTypes = new Set([ "_arrayOf" ]);
// FIXME: Have some sort of internally-cacheable way to find nodes of a certain type? So that different optimizer visitors don't need to filter the list of clauses over and over again...
function leftIdentity(left) {
// NOTE: This uses JSON.stringify, since that gives us fast escaping for free; which is important to prevent bugs and/or injection-related security issues in the serialized names
if (left.type === "columnName") {
return `column:${JSON.stringify(left.name)}`;
} else if (left.type === "foreignColumnName") {
return `foreignColumn:${JSON.stringify([ left.table.name, left.column.name ])}`;
if (left.type === "field") {
return `field:${JSON.stringify(left.name)}`;
} else if (left.type === "remoteField") {
return `remoteField:${JSON.stringify([ left.collection.name, left.field.name ])}`;
} else if (left.type === "sqlExpression") {
return `sqlExpression:${JSON.stringify(left.expression)}`;
} else {
@ -61,7 +62,7 @@ function createExpressionTracker() {
};
}
function createHandler(type) {
function createExpressionHandler(type) {
// FIXME: Improve matchValue to distinguish between "arm not specified at all" and "arm holds undefined as a specified value", to deal with things like accidental operations.anyOfExpressions
let expressionOperation = matchValue.literal(type, {
"anyOfExpressions": operations.anyOf,
@ -79,12 +80,16 @@ function createHandler(type) {
for (let item of node.items) {
// Only regular expressions can be arrayified, not {all,any}OfExpressions, which will get visited by this optimizer later on anyway
// FIXME: Also ignore already-processed arrays
if (item.type === "expression") {
tracker.addExpression(item);
if (item.type === "expression") { // FIXME: typeOf
if (disallowedNestedTypes.has(typeOf(item.condition.expression))) { // FIXME: Check that this full path is always valid
// Abort immediately, we have encountered a previously-processed array which means we cannot nest any further.
return NoChange;
} else {
tracker.addExpression(item);
}
}
}
if (tracker.arrayIsPossible()) {
let newExpressions = syncpipe(tracker, [
(_) => _.getMapping(),
@ -96,8 +101,8 @@ function createHandler(type) {
return expressions[0];
} else {
let allValues = expressions.map((expression) => expression.condition.expression);
let canBeParameterized = allValues.every((value) => parameterizableTypes.includes(typeOf(value)));
let canBeParameterized = allValues.every((value) => parameterizableTypes.has(typeOf(value)));
return operations.expression({
left: expressions[0].left,
condition: internalOperations._condition({
@ -127,11 +132,33 @@ function createHandler(type) {
};
}
function createValueHandler(type) {
let internalArrayType = matchValue(type, {
"anyOfValues": "anyOf",
"allOfValues": "allOf"
});
return function arrayifyValueList(node) {
if (!Array.isArray(node.items)) {
// We only handle the placeholder / single SQL fragment / etc. special cases here, not the usual list-of-items cases
return internalOperations._arrayOf({
type: internalArrayType,
canBeParameterized: parameterizableTypes.has(typeOf(node.items)),
items: node.items
});
} else {
return NoChange;
}
};
}
module.exports = {
name: "arrayify-predicate-lists",
category: [ "readability" ],
visitors: {
allOfExpressions: createHandler("allOfExpressions"),
anyOfExpressions: createHandler("anyOfExpressions"),
allOfExpressions: createExpressionHandler("allOfExpressions"),
anyOfExpressions: createExpressionHandler("anyOfExpressions"),
allOfValues: createValueHandler("allOfValues"),
anyOfValues: createValueHandler("anyOfValues"),
}
};

@ -11,14 +11,14 @@ module.exports = {
name: "collapse-where",
category: [ "normalization" ],
visitors: {
select: ({ table, clauses }) => {
select: ({ collection, clauses }) => {
let [ whereClauses, otherClauses ] = splitFilter(clauses, (clause) => clause.type === "where");
if (whereClauses.length > 1) {
let whereExpressions = whereClauses.map((clause) => clause.expression);
let newWhere = operations.where(operations.allOf(whereExpressions));
return operations.select(table, [ newWhere ].concat(otherClauses));
return operations.select(collection, [ newWhere ].concat(otherClauses));
} else {
return NoChange;
}

@ -3,16 +3,22 @@
const matchValue = require("match-value");
const operations = require("../operations");
const internalOperations = require("../internal-operations");
const typeOf = require("../type-of");
const unreachable = require("../unreachable");
const NoChange = require("./util/no-change");
// FIXME: anyOf(not(placeholder)) should be disallowed, but probably currently is allowed
// not(anyOf(placeholder)) is fine though
let unprocessableTypes = new Set([ "sqlExpression", "placeholder" ]);
module.exports = {
name: "conditions-to-expressions",
category: [ "normalization" ],
visitors: {
expression: (rootNode) => {
if (rootNode.condition.type === "condition") {
if (rootNode.condition.type === "condition") { // FIXME: typeOf
return NoChange;
} else {
// anyOfConditions, allOfConditions, notCondition
@ -26,7 +32,29 @@ module.exports = {
});
if (listOperation != null) {
return listOperation(node.items.map((item) => convertNode(item)));
if (Array.isArray(node.items)) {
return listOperation(node.items.map((item) => convertNode(item)));
} else if (unprocessableTypes.has(typeOf(node.items))) {
// Placeholders / unsafe SQL expressions are not representable as a series of expressions, as we don't know the value of them upfront. Therefore we immediately convert it to an internal ANY/ALL expression here, so that it doesn't accidentally get touched by anything else afterwards.
// FIXME: Check the relevance here of node.items.parameterizable for placeholders
return operations.expression({
left: rootNode.left,
condition: internalOperations._condition({
type: "equals",
expression: internalOperations._arrayOf({
type: matchValue(typeOf(node), {
anyOfConditions: "anyOf",
allOfConditions: "allOf"
}),
canBeParameterized: true,
items: node.items
})
})
});
} else {
unreachable("Condition item was not an array");
}
} else if (typeOf(node) === "notCondition") {
return operations.not(convertNode(node.condition));
} else if (typeOf(node) === "condition") {

@ -15,40 +15,47 @@ function createHandler(type) {
allOfExpressions: operations.allOf,
anyOfConditions: operations.anyOf,
allOfConditions: operations.allOf,
anyOfValues: operations.anyOf,
allOfValues: operations.allOf,
});
return function flattenPredicateList(list) {
let hasNestedPredicates = list.items.some((item) => typeOf(item) === type);
let hasSingleItem = (list.items.length === 1); // For unnecessary anyOf/allOf wrapping, which is also handled by this optimizer
let mustFlatten = hasNestedPredicates || hasSingleItem;
if (Array.isArray(list.items)) {
let hasNestedPredicates = list.items.some((item) => typeOf(item) === type);
let hasSingleItem = (list.items.length === 1); // For unnecessary anyOf/allOf wrapping, which is also handled by this optimizer
let mustFlatten = hasNestedPredicates || hasSingleItem;
if (mustFlatten) {
let actualItems = [];
if (mustFlatten) {
let actualItems = [];
function collectItemsRecursively(node) {
for (let subItem of node.items) {
if (typeOf(subItem) === type) {
collectItemsRecursively(subItem);
} else {
actualItems.push(subItem);
function collectItemsRecursively(node) {
for (let subItem of node.items) {
if (typeOf(subItem) === type) {
collectItemsRecursively(subItem);
} else {
actualItems.push(subItem);
}
}
}
}
collectItemsRecursively(list);
collectItemsRecursively(list);
if (actualItems.length === 0) {
// FIXME: Do we want to log this as a warning? It *could* occur when items get eliminated by another optimizer, but it could also be the result of a bug...
console.warn("Encountered 0 actual items in predicate list");
return RemoveNode;
} else if (actualItems.length === 1) {
// Wrapping is pointless here.
return actualItems[0];
if (actualItems.length === 0) {
// FIXME: Do we want to log this as a warning? It *could* occur when items get eliminated by another optimizer, but it could also be the result of a bug...
console.warn("Encountered 0 actual items in predicate list");
return RemoveNode;
} else if (actualItems.length === 1) {
// Wrapping is pointless here.
return actualItems[0];
} else {
return listOperation(actualItems);
}
} else {
return listOperation(actualItems);
return NoChange;
}
} else {
// Ignore placeholders, SQL fragments, etc.
return NoChange;
}
};
@ -62,5 +69,7 @@ module.exports = {
anyOfExpressions: createHandler("anyOfExpressions"),
allOfConditions: createHandler("allOfConditions"),
anyOfConditions: createHandler("anyOfConditions"),
allOfValues: createHandler("allOfValues"),
anyOfValues: createHandler("anyOfValues"),
}
};

@ -2,10 +2,11 @@
module.exports = [
require("./collapse-where"),
require("./values-to-conditions"),
require("./conditions-to-expressions"),
require("./flatten-not-predicates"),
require("./flatten-predicate-lists"),
require("./arrayify-predicate-lists"),
require("./set-collapse-by-columns"),
require("./set-collapse-by-fields"),
// require("./test-context"),
];

@ -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

@ -8,45 +8,45 @@ module.exports = {
name: "test-context",
category: [ "testing" ],
visitors: {
columnName: (node, { setState }) => {
setState("seenColumn", node.name);
field: (node, { setState }) => {
setState("seenField", node.name);
return NoChange;
},
select: (node, { registerStateHandler, defer }) => {
let seenColumns = new Set();
let seenFields = new Set();
registerStateHandler("seenColumnsInWhere", (names) => {
registerStateHandler("seenFieldsInWhere", (names) => {
for (let name of names) {
seenColumns = seenColumns.add(name);
seenFields = seenFields.add(name);
}
});
// FIXME: Definitely need better AST modification/derivation tools... probably some sort of deep-modifying utility, for starters. Maybe merge-by-template can be of use here? With a custom AST node merger? It probably doesn't support non-enumerable properties correctly right now, though...
return defer(() => {
console.log("Seen columns in WHERE in SELECT:", seenColumns);
// console.log("Seen fields in WHERE in SELECT:", seenFields);
let onlyColumnsClause = node.clauses.find((clause) => clause.type === "onlyColumns");
let onlyFieldsClause = node.clauses.find((clause) => clause.type === "onlyFields");
let columnsAlreadyAdded = onlyColumnsClause != null && Array.from(seenColumns).every((column) => {
return onlyColumnsClause.columns.some((existingColumn) => existingColumn.name === column);
let fieldsAlreadyAdded = onlyFieldsClause != null && Array.from(seenFields).every((field) => {
return onlyFieldsClause.fields.some((existingField) => existingField.name === field);
});
if (!columnsAlreadyAdded) {
if (!fieldsAlreadyAdded) {
// NOTE: This is a good test case for optimizer stability! Just returning a derived node in every case.
let newOnlyColumnsClause = (onlyColumnsClause == null)
? operations.onlyColumns(Array.from(seenColumns))
: deriveNode(onlyColumnsClause, {
columns: onlyColumnsClause.columns.concat(Array.from(seenColumns).map((columnName) => {
return operations.column(columnName);
let newOnlyFieldsClause = (onlyFieldsClause == null)
? operations.onlyFields(Array.from(seenFields))
: deriveNode(onlyFieldsClause, {
fields: onlyFieldsClause.fields.concat(Array.from(seenFields).map((fieldName) => {
return operations.field(fieldName);
}))
});
return deriveNode(node, {
clauses: node.clauses
.filter((clause) => clause.type !== "onlyColumns")
.concat([ newOnlyColumnsClause ])
.filter((clause) => clause.type !== "onlyFields")
.concat([ newOnlyFieldsClause ])
});
} else {
return NoChange;
@ -54,20 +54,20 @@ module.exports = {
});
},
where: (node, { registerStateHandler, defer, setState }) => {
let seenColumns = [];
let seenFields = [];
registerStateHandler("seenColumn", (name) => seenColumns.push(name));
registerStateHandler("seenField", (name) => seenFields.push(name));
return defer(() => {
setState("seenColumnsInWhere", seenColumns);
setState("seenFieldsInWhere", seenFields);
return NoChange;
});
// let seenColumns = [];
// let seenFields = [];
// let id = Math.random();
// registerStateHandler("seenColumn", (name) => {
// seenColumns.push(name);
// registerStateHandler("seenField", (name) => {
// seenFields.push(name);
// });
// console.log("Scheduling defer", id);
@ -76,8 +76,8 @@ module.exports = {
// console.log("Defer called", id);
// // MARKER: This gets called twice, but should only be called once!
// // console.log("Seen columns in WHERE:", seenColumns, require("util").inspect(node, {colors:true,depth:null}));
// console.log("Seen columns in WHERE:", seenColumns);
// // console.log("Seen fields in WHERE:", seenFields, require("util").inspect(node, {colors:true,depth:null}));
// console.log("Seen fields in WHERE:", seenFields);
// return NoChange;
// });
}

@ -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
};
}
];

@ -5,11 +5,12 @@ const isString = require("@validatem/is-string");
const isDate = require("@validatem/is-date");
const isBoolean = require("@validatem/is-boolean");
const isNumber = require("@validatem/is-number");
const wrapError = require("@validatem/wrap-error");
// FIXME: Add null, but not undefined
module.exports = either([
module.exports = wrapError("Must be a literal value", either([
isString,
isNumber, // FIXME: Disallow Infinity?
isBoolean,
isDate
]);
]));

@ -3,6 +3,7 @@
const matchesFormat = require("@validatem/matches-format");
const wrapError = require("@validatem/wrap-error");
module.exports = wrapError("Must not include a table name", [
module.exports = wrapError("Must not include a collection name", [
// FIXME: Validate format properly (including disallowing uppercase!)
matchesFormat(/^[^.]+$/)
]);

@ -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 ]
}
];

@ -7,8 +7,8 @@ module.exports = function (operations) {
const isObjectType = require("./is-object-type")(operations);
return either([
[ isObjectType("columnName") ],
[ isObjectType("foreignColumnName") ],
[ isObjectType("field") ],
[ isObjectType("remoteField") ],
]);
};

@ -2,12 +2,13 @@
const either = require("@validatem/either");
// FIXME: Maybe this should actually be called isCollapsibleFieldSpecification?
module.exports = function (operations) {
const isObjectType = require("./is-object-type")(operations);
const isPossiblyForeignColumn = require("./is-possibly-foreign-column")(operations);
const isPossiblyRemoteField = require("./is-possibly-remote-field")(operations);
return either([
isPossiblyForeignColumn,
isPossiblyRemoteField,
isObjectType("hierarchical")
]);
};

@ -8,8 +8,8 @@ module.exports = function (operations) {
const isObjectType = require("./is-object-type")(operations);
const wrapWithOperation = require("./wrap-with-operation")(operations);
return wrapError("Must be a table name or object", either([
[ isObjectType("tableName") ],
[ isString, wrapWithOperation("table") ]
return wrapError("Must be a collection name or object", either([
[ isObjectType("collection") ],
[ isString, wrapWithOperation("collection") ]
]));
};

@ -4,13 +4,13 @@ const either = require("@validatem/either");
module.exports = function (operations) {
const isObjectType = require("./is-object-type")(operations);
const isPossiblyForeignColumn = require("./is-possibly-foreign-column")(operations);
const isPossiblyRemoteField = require("./is-possibly-remote-field")(operations);
return either([
isObjectType("sqlExpression"),
isObjectType("aggregrateFunction"),
isObjectType("valueFrom"),
isObjectType("literalValue"),
isPossiblyForeignColumn
isPossiblyRemoteField
]);
};

@ -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") ]
]);
};

@ -5,7 +5,7 @@ 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 isConditionValue = require("./is-condition-value")(operations);
const wrapWithOperation = require("./wrap-with-operation")(operations);
return wrapError("Must be a type of condition or value", either([
@ -13,6 +13,6 @@ module.exports = function (operations) {
[ isObjectType("notCondition") ], // not(condition)
[ isObjectType("anyOfConditions") ], // anyOf(...)
[ isObjectType("allOfConditions") ], // allOf(...)
[ isValueExpression, wrapWithOperation("equals") ]
[ isConditionValue, wrapWithOperation("equals") ]
]));
};

@ -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 ]
]));
};

@ -1,6 +1,7 @@
"use strict";
const either = require("@validatem/either");
const arrayOf = require("@validatem/array-of");
const tagAsType = require("../tag-as-type");
@ -9,19 +10,22 @@ module.exports = function (operations) {
const isExpressionList = require("./is-expression-list")(operations);
const isConditionList = require("./is-condition-list")(operations);
const isWhereObjectList = require("./is-where-object-list")(operations);
const isConditionValue = require("./is-condition-value")(operations);
return either([
// Boolean AND/OR
[ isExpressionList, tagAsType("expressions") ],
// Combine (JOIN)
// FIXME
// FIXME?
// ...
// Value-expression-only conditions (eg. `moreThan(anyOf(...))`)
[ either([
arrayOf(isConditionValue),
isObjectType("placeholder"), // for dynamically specified array of values
isObjectType("sqlExpression"), // FIXME: Verify that this should be treated as a value, not an expression
]), tagAsType("values") ],
// Multiple-choice conditions
[ isWhereObjectList, tagAsType("expressions") ],
[ either([
[ isObjectType("sqlExpression") ],
[ isObjectType("placeholder") ], // for dynamically specified array of values
[ isConditionList ],
]), tagAsType("conditions") ],
[ isConditionList, tagAsType("conditions") ],
]);
};

@ -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") ]
]));
};

@ -9,8 +9,8 @@ module.exports = function (operations) {
return either([
[ isObjectType("where") ],
[ isComputeClause ],
[ isObjectType("addColumns") ],
[ isObjectType("onlyColumns") ],
[ isObjectType("addFields") ],
[ isObjectType("onlyFields") ],
[ isObjectType("withRelations") ],
[ isObjectType("postProcess") ],
[ isObjectType("collapseBy") ],

@ -4,10 +4,10 @@ const either = require("@validatem/either");
module.exports = function (operations) {
const isObjectType = require("./is-object-type")(operations);
const isPossiblyForeignColumn = require("./is-possibly-foreign-column")(operations);
const isPossiblyRemoteField = require("./is-possibly-remote-field")(operations);
return either([
isObjectType("alias"),
isPossiblyForeignColumn, // FIXME: Think about whether we actually want to permit foreign columns here, without an alias.
isPossiblyRemoteField, // FIXME: Think about whether we actually want to permit remote fields here, without an alias.
]);
};

@ -10,7 +10,7 @@ const isLiteralValue = require("../is-literal-value");
module.exports = function (operations) {
const isObjectType = require("./is-object-type")(operations);
const isColumnObject = require("./is-column-object")(operations);
const isAnyFieldObject = require("./is-any-field-object")(operations);
const isAggregrateFunction = require("./is-aggregrate-function")(operations);
const wrapWithOperation = require("./wrap-with-operation")(operations);
@ -18,8 +18,8 @@ module.exports = function (operations) {
[ isObjectType("sqlExpression") ],
[ isObjectType("literalValue") ],
[ isObjectType("placeholder") ], // TODO: Verify that this also works for the `alias` method
[ isAggregrateFunction ],
[ isColumnObject ],
[ isAggregrateFunction ], // FIXME: Make sure to check that this is only permitted when collapsing
[ isAnyFieldObject ],
[ isLiteralValue, wrapWithOperation("value") ]
]));
};

@ -10,12 +10,12 @@ 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);
const wrapPossiblyRemoteFieldName = require("./wrap-possibly-remote-field-name")(operations);
function wrapWhereObject(fields) {
let expressions = Object.entries(fields).map(([ key, value ]) => {
return operations.expression({
left: wrapPossiblyForeignColumnName(key),
left: wrapPossiblyRemoteFieldName(key),
condition: value
});
});
@ -24,7 +24,7 @@ module.exports = function (operations) {
}
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
// NOTE: We cannot wrap the key as a field name object here, since object keys can only be strings; we do so within wrapWhereObject instead
key: [ required, isString ],
value: [ required, isCondition ]
});

@ -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);
}
};
};

@ -7,5 +7,5 @@ module.exports = function (operations) {
return operations[name](value);
};
};
}
};

@ -973,7 +973,7 @@
dependencies:
is-callable "^1.1.5"
"@validatem/either@^0.1.3":
"@validatem/either@^0.1.3", "@validatem/either@^0.1.9":
version "0.1.9"
resolved "https://registry.yarnpkg.com/@validatem/either/-/either-0.1.9.tgz#0d753ef8fe04486d2b7122de3dd3ac51b3acaacf"
integrity sha512-cUqlRjy02qDcZ166/D6duk8lrtqrHynHuSakU0TvMGMBiLzjWpMJ+3beAWHe+kILB5/dlXVyc68ZIjSNhBi8Kw==
@ -1042,6 +1042,14 @@
"@validatem/error" "^1.0.0"
is-callable "^1.1.5"
"@validatem/is-integer@^0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@validatem/is-integer/-/is-integer-0.1.0.tgz#52c544063aaeabb630854e1822298f5c043196a0"
integrity sha512-sSp66uxfirIFMqro64DAdfM+UKo+IICmHdy/x3ZJXUM9F4byz/GyFmhR4wfcQswywwF1fqKw9458GE38fozjOQ==
dependencies:
"@validatem/error" "^1.0.0"
"@validatem/is-number" "^0.1.2"
"@validatem/is-number@^0.1.2":
version "0.1.3"
resolved "https://registry.yarnpkg.com/@validatem/is-number/-/is-number-0.1.3.tgz#0f8ce8c72970dbedbbd04d12942e5ab48a44cda6"
@ -1133,6 +1141,18 @@
dependencies:
"@validatem/error" "^1.0.0"
"@validatem/require-either@^0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@validatem/require-either/-/require-either-0.1.0.tgz#250e35ab06f124ea90f3925d74b5f53a083923b0"
integrity sha512-UyZtJieT3aJhO9tj1OJp47V9jpHCE7RSohue9jg3FyDGwmIBVYXCfASeM19mWg9W0lp6IevsqTmaGQhqQOQYJg==
dependencies:
"@validatem/allow-extra-properties" "^0.1.0"
"@validatem/either" "^0.1.9"
"@validatem/forbidden" "^0.1.0"
"@validatem/required" "^0.1.1"
assure-array "^1.0.0"
flatten "^1.0.3"
"@validatem/required@^0.1.0", "@validatem/required@^0.1.1":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@validatem/required/-/required-0.1.1.tgz#64f4a87333fc5955511634036b7f8948ed269170"
@ -1284,7 +1304,7 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1:
dependencies:
color-convert "^1.9.0"
ansi-styles@^4.1.0:
ansi-styles@^4.0.0, ansi-styles@^4.1.0:
version "4.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359"
integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==
@ -1381,6 +1401,13 @@ as-expression@^1.0.0:
resolved "https://registry.yarnpkg.com/as-expression/-/as-expression-1.0.0.tgz#7bc620ca4cb2fe0ee90d86729bd6add33b8fd831"
integrity sha512-Iqh4GxNUfxbJdGn6b7/XMzc8m1Dz2ZHouBQ9DDTzyMRO3VPPIAXeoY/sucRxxxXKbUtzwzWZSN6jPR3zfpYHHA==
as-table@^1.0.55:
version "1.0.55"
resolved "https://registry.yarnpkg.com/as-table/-/as-table-1.0.55.tgz#dc984da3937745de902cea1d45843c01bdbbec4f"
integrity sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==
dependencies:
printable-characters "^1.0.42"
asn1.js@^4.0.0:
version "4.10.1"
resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0"
@ -1790,6 +1817,11 @@ buffer-from@^1.0.0:
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
buffer-writer@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-2.0.0.tgz#ce7eb81a38f7829db09c873f2fbb792c0c98ec04"
integrity sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==
buffer-xor@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9"
@ -1851,7 +1883,7 @@ callsites@^3.0.0:
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
camelcase@^5.3.1:
camelcase@^5.0.0, camelcase@^5.3.1:
version "5.3.1"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
@ -1978,6 +2010,15 @@ clipboard@^2.0.0:
select "^1.1.2"
tiny-emitter "^2.0.0"
cliui@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1"
integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==
dependencies:
string-width "^4.2.0"
strip-ansi "^6.0.0"
wrap-ansi "^6.2.0"
clone-regexp@^2.1.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/clone-regexp/-/clone-regexp-2.2.0.tgz#7d65e00885cd8796405c35a737e7a86b7429e36f"
@ -2255,6 +2296,11 @@ debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
dependencies:
ms "^2.1.1"
decamelize@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
decode-uri-component@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
@ -2927,6 +2973,14 @@ finalhandler@~1.1.2:
statuses "~1.5.0"
unpipe "~1.0.0"
find-up@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
dependencies:
locate-path "^5.0.0"
path-exists "^4.0.0"
flat-cache@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0"
@ -3037,6 +3091,11 @@ get-assigned-identifiers@^1.1.0, get-assigned-identifiers@^1.2.0:
resolved "https://registry.yarnpkg.com/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz#6dbf411de648cbaf8d9169ebb0d2d576191e2ff1"
integrity sha512-mBBwmeGTrxEMO4pMaaf/uUEFHnYtwr8FTe8Y/mer4rcV/bye0qGm6pw1bGZFGStxC5O76c5ZAVBGnqHmOaJpdQ==
get-caller-file@^2.0.1:
version "2.0.5"
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
get-ports@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/get-ports/-/get-ports-1.0.3.tgz#f40bd580aca7ec0efb7b96cbfcbeb03ef894b5e8"
@ -3820,6 +3879,13 @@ levn@^0.4.1:
prelude-ls "^1.2.1"
type-check "~0.4.0"
locate-path@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
dependencies:
p-locate "^4.1.0"
lodash.memoize@~3.0.3:
version "3.0.4"
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-3.0.4.tgz#2dcbd2c287cbc0a55cc42328bd0c736150d53e3f"
@ -4291,6 +4357,25 @@ p-finally@^1.0.0:
resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=
p-limit@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
dependencies:
p-try "^2.0.0"
p-locate@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
dependencies:
p-limit "^2.2.0"
p-try@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
package-json@^6.3.0:
version "6.5.0"
resolved "https://registry.yarnpkg.com/package-json/-/package-json-6.5.0.tgz#6feedaca35e75725876d0b0e64974697fed145b0"
@ -4301,6 +4386,11 @@ package-json@^6.3.0:
registry-url "^5.0.0"
semver "^6.2.0"
packet-reader@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/packet-reader/-/packet-reader-1.0.0.tgz#9238e5480dedabacfe1fe3f2771063f164157d74"
integrity sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==
pad-left@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/pad-left/-/pad-left-2.1.0.tgz#16e6a3b2d44a8e138cb0838cc7cb403a4fc9e994"
@ -4371,6 +4461,11 @@ path-dirname@^1.0.0:
resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0"
integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=
path-exists@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
path-is-absolute@^1.0.0, path-is-absolute@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
@ -4422,6 +4517,58 @@ pem@^1.13.2:
os-tmpdir "^1.0.1"
which "^2.0.2"
pg-connection-string@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.3.0.tgz#c13fcb84c298d0bfa9ba12b40dd6c23d946f55d6"
integrity sha512-ukMTJXLI7/hZIwTW7hGMZJ0Lj0S2XQBCJ4Shv4y1zgQ/vqVea+FLhzywvPj0ujSuofu+yA4MYHGZPTsgjBgJ+w==
pg-int8@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c"
integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==
pg-pool@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.2.1.tgz#5f4afc0f58063659aeefa952d36af49fa28b30e0"
integrity sha512-BQDPWUeKenVrMMDN9opfns/kZo4lxmSWhIqo+cSAF7+lfi9ZclQbr9vfnlNaPr8wYF3UYjm5X0yPAhbcgqNOdA==
pg-protocol@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.2.5.tgz#28a1492cde11646ff2d2d06bdee42a3ba05f126c"
integrity sha512-1uYCckkuTfzz/FCefvavRywkowa6M5FohNMF5OjKrqo9PSR8gYc8poVmwwYQaBxhmQdBjhtP514eXy9/Us2xKg==
pg-types@^2.1.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3"
integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==
dependencies:
pg-int8 "1.0.1"
postgres-array "~2.0.0"
postgres-bytea "~1.0.0"
postgres-date "~1.0.4"
postgres-interval "^1.1.0"
pg@^8.3.3:
version "8.3.3"
resolved "https://registry.yarnpkg.com/pg/-/pg-8.3.3.tgz#0338631ca3c39b4fb425b699d494cab17f5bb7eb"
integrity sha512-wmUyoQM/Xzmo62wgOdQAn5tl7u+IA1ZYK7qbuppi+3E+Gj4hlUxVHjInulieWrd0SfHi/ADriTb5ILJ/lsJrSg==
dependencies:
buffer-writer "2.0.0"
packet-reader "1.0.0"
pg-connection-string "^2.3.0"
pg-pool "^3.2.1"
pg-protocol "^1.2.5"
pg-types "^2.1.0"
pgpass "1.x"
semver "4.3.2"
pgpass@1.x:
version "1.0.2"
resolved "https://registry.yarnpkg.com/pgpass/-/pgpass-1.0.2.tgz#2a7bb41b6065b67907e91da1b07c1847c877b306"
integrity sha1-Knu0G2BltnkH6R2hsHwYR8h3swY=
dependencies:
split "^1.0.0"
pick-random-weighted@^1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/pick-random-weighted/-/pick-random-weighted-1.2.3.tgz#3d337543ff59b53c7aad17aa97560981a4ac0311"
@ -4447,6 +4594,28 @@ posix-character-classes@^0.1.0:
resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=
postgres-array@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e"
integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==
postgres-bytea@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-1.0.0.tgz#027b533c0aa890e26d172d47cf9ccecc521acd35"
integrity sha1-AntTPAqokOJtFy1Hz5zOzFIazTU=
postgres-date@~1.0.4:
version "1.0.7"
resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.7.tgz#51bc086006005e5061c591cee727f2531bf641a8"
integrity sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==
postgres-interval@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-1.2.0.tgz#b460c82cb1587507788819a06aa0fffdb3544695"
integrity sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==
dependencies:
xtend "^4.0.0"
prelude-ls@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
@ -4471,6 +4640,11 @@ pretty-ms@^2.1.0:
parse-ms "^1.0.0"
plur "^1.0.0"
printable-characters@^1.0.42:
version "1.0.42"
resolved "https://registry.yarnpkg.com/printable-characters/-/printable-characters-1.0.42.tgz#3f18e977a9bd8eb37fcc4ff5659d7be90868b3d8"
integrity sha1-Pxjpd6m9jrN/zE/1ZZ176Qhos9g=
prismjs@^1.20.0:
version "1.20.0"
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.20.0.tgz#9b685fc480a3514ee7198eac6a3bf5024319ff03"
@ -4809,6 +4983,16 @@ repeat-string@^1.5.2, repeat-string@^1.5.4, repeat-string@^1.6.1:
resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc=
require-directory@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
require-main-filename@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
resolve-from@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
@ -4926,6 +5110,11 @@ semver-diff@^3.1.1:
dependencies:
semver "^6.3.0"
semver@4.3.2:
version "4.3.2"
resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.2.tgz#c7a07158a80bedd052355b770d82d6640f803be7"
integrity sha1-x6BxWKgL7dBSNVt3DYLWZA+AO+c=
semver@7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e"
@ -4975,6 +5164,11 @@ serve-static@1.14.1, serve-static@^1.10.0:
parseurl "~1.3.3"
send "0.17.1"
set-blocking@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
set-value@^2.0.0, set-value@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b"
@ -5151,6 +5345,13 @@ split2@^0.2.1:
dependencies:
through2 "~0.6.1"
split@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9"
integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==
dependencies:
through "2"
sprintf-js@~1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
@ -5229,7 +5430,7 @@ string-width@^3.0.0:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^5.1.0"
string-width@^4.0.0, string-width@^4.1.0:
string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5"
integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==
@ -5417,7 +5618,7 @@ through2@~0.6.1:
readable-stream ">=1.0.33-1 <1.1.0-0"
xtend ">=4.0.0 <4.1.0-0"
"through@>=2.2.7 <3":
through@2, "through@>=2.2.7 <3":
version "2.3.8"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
@ -5746,6 +5947,11 @@ watchify@^3.11.1:
through2 "^2.0.0"
xtend "^4.0.0"
which-module@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
which@^1.2.9:
version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
@ -5772,6 +5978,15 @@ word-wrap@^1.2.3:
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
wrap-ansi@^6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
@ -5810,3 +6025,33 @@ xdg-basedir@^4.0.0:
version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
y18n@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==
yargs-parser@^18.1.2:
version "18.1.3"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
dependencies:
camelcase "^5.0.0"
decamelize "^1.2.0"
yargs@^15.4.1:
version "15.4.1"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
dependencies:
cliui "^6.0.0"
decamelize "^1.2.0"
find-up "^4.1.0"
get-caller-file "^2.0.1"
require-directory "^2.1.1"
require-main-filename "^2.0.0"
set-blocking "^2.0.0"
string-width "^4.2.0"
which-module "^2.0.0"
y18n "^4.0.0"
yargs-parser "^18.1.2"

Loading…
Cancel
Save