2020-08-21 03:31:43 +12:00
|
|
|
const TOKEN_MAP = {
|
|
|
|
EQUALS: "===",
|
2020-10-15 09:43:36 +13:00
|
|
|
NOT_EQUALS: "!==",
|
2020-08-21 03:31:43 +12:00
|
|
|
LT: "<",
|
|
|
|
LTE: "<=",
|
|
|
|
MT: ">",
|
|
|
|
MTE: ">=",
|
2020-08-22 04:05:26 +12:00
|
|
|
CONTAINS: "includes",
|
2020-08-21 03:31:43 +12:00
|
|
|
AND: "&&",
|
2020-08-22 04:05:26 +12:00
|
|
|
OR: "||",
|
2020-08-21 03:31:43 +12:00
|
|
|
}
|
|
|
|
|
2022-05-14 05:09:39 +12:00
|
|
|
const CONDITIONS = {
|
|
|
|
EMPTY: "EMPTY",
|
|
|
|
NOT_EMPTY: "NOT_EMPTY",
|
|
|
|
CONTAINS: "CONTAINS",
|
|
|
|
}
|
|
|
|
|
2021-10-25 21:58:05 +13:00
|
|
|
const isEmptyExpression = key => {
|
2021-10-22 02:15:55 +13:00
|
|
|
return `(
|
|
|
|
doc["${key}"] === undefined ||
|
|
|
|
doc["${key}"] === null ||
|
|
|
|
doc["${key}"] === "" ||
|
|
|
|
(Array.isArray(doc["${key}"]) && doc["${key}"].length === 0)
|
|
|
|
)`
|
|
|
|
}
|
|
|
|
|
2020-09-02 22:52:32 +12:00
|
|
|
const GROUP_PROPERTY = {
|
|
|
|
group: {
|
|
|
|
type: "string",
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
const FIELD_PROPERTY = {
|
|
|
|
field: {
|
|
|
|
type: "string",
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2020-08-24 22:46:28 +12:00
|
|
|
const SCHEMA_MAP = {
|
2020-10-15 22:48:57 +13:00
|
|
|
sum: {
|
|
|
|
field: "string",
|
|
|
|
value: "number",
|
|
|
|
},
|
|
|
|
count: {
|
|
|
|
field: "string",
|
|
|
|
value: "number",
|
|
|
|
},
|
2020-08-24 22:46:28 +12:00
|
|
|
stats: {
|
2020-08-25 02:48:34 +12:00
|
|
|
sum: {
|
|
|
|
type: "number",
|
2020-08-24 22:46:28 +12:00
|
|
|
},
|
2020-08-25 02:48:34 +12:00
|
|
|
min: {
|
|
|
|
type: "number",
|
2020-08-24 22:46:28 +12:00
|
|
|
},
|
2020-08-25 02:48:34 +12:00
|
|
|
max: {
|
|
|
|
type: "number",
|
2020-08-24 22:46:28 +12:00
|
|
|
},
|
2020-08-25 02:48:34 +12:00
|
|
|
count: {
|
|
|
|
type: "number",
|
2020-08-24 22:46:28 +12:00
|
|
|
},
|
2020-08-25 02:48:34 +12:00
|
|
|
sumsqr: {
|
|
|
|
type: "number",
|
2020-08-24 22:46:28 +12:00
|
|
|
},
|
2020-08-25 02:48:34 +12:00
|
|
|
avg: {
|
|
|
|
type: "number",
|
|
|
|
},
|
|
|
|
},
|
2020-08-24 22:46:28 +12:00
|
|
|
}
|
|
|
|
|
2020-08-22 04:05:26 +12:00
|
|
|
/**
|
|
|
|
* Iterates through the array of filters to create a JS
|
|
|
|
* expression that gets used in a CouchDB view.
|
|
|
|
* @param {Array} filters - an array of filter objects
|
|
|
|
* @returns {String} JS Expression
|
|
|
|
*/
|
|
|
|
function parseFilterExpression(filters) {
|
|
|
|
const expression = []
|
|
|
|
|
2021-04-29 01:57:52 +12:00
|
|
|
let first = true
|
2020-08-22 04:05:26 +12:00
|
|
|
for (let filter of filters) {
|
2021-04-29 01:57:52 +12:00
|
|
|
if (!first && filter.conjunction) {
|
|
|
|
expression.push(TOKEN_MAP[filter.conjunction])
|
|
|
|
}
|
2020-08-22 04:05:26 +12:00
|
|
|
|
2022-05-14 05:09:39 +12:00
|
|
|
if (filter.condition === CONDITIONS.CONTAINS) {
|
2020-08-22 04:05:26 +12:00
|
|
|
expression.push(
|
2020-08-25 02:48:34 +12:00
|
|
|
`doc["${filter.key}"].${TOKEN_MAP[filter.condition]}("${filter.value}")`
|
|
|
|
)
|
2022-05-14 05:09:39 +12:00
|
|
|
} else if (filter.condition === CONDITIONS.EMPTY) {
|
2021-10-25 21:58:05 +13:00
|
|
|
expression.push(isEmptyExpression(filter.key))
|
2022-05-14 05:09:39 +12:00
|
|
|
} else if (filter.condition === CONDITIONS.NOT_EMPTY) {
|
2021-10-25 21:58:05 +13:00
|
|
|
expression.push(`!${isEmptyExpression(filter.key)}`)
|
2020-08-25 02:48:34 +12:00
|
|
|
} else {
|
2020-10-02 04:55:17 +13:00
|
|
|
const value =
|
|
|
|
typeof filter.value == "string" ? `"${filter.value}"` : filter.value
|
|
|
|
|
2020-08-25 02:48:34 +12:00
|
|
|
expression.push(
|
2020-10-02 04:55:17 +13:00
|
|
|
`doc["${filter.key}"] ${TOKEN_MAP[filter.condition]} ${value}`
|
2020-08-25 02:48:34 +12:00
|
|
|
)
|
2020-08-22 04:05:26 +12:00
|
|
|
}
|
2021-04-29 01:57:52 +12:00
|
|
|
first = false
|
2020-08-22 04:05:26 +12:00
|
|
|
}
|
|
|
|
|
2020-08-21 03:31:43 +12:00
|
|
|
return expression.join(" ")
|
|
|
|
}
|
|
|
|
|
2020-08-25 02:48:34 +12:00
|
|
|
/**
|
|
|
|
* Returns a CouchDB compliant emit() expression that is used to emit the
|
|
|
|
* correct key/value pairs for custom views.
|
|
|
|
* @param {String?} field - field to use for calculations, if any
|
|
|
|
* @param {String?} groupBy - field to group calculation results on, if any
|
|
|
|
*/
|
2020-08-22 04:05:26 +12:00
|
|
|
function parseEmitExpression(field, groupBy) {
|
2020-10-16 00:09:41 +13:00
|
|
|
return `emit(doc["${groupBy || "_id"}"], doc["${field}"]);`
|
2020-08-22 04:05:26 +12:00
|
|
|
}
|
|
|
|
|
2020-08-25 02:48:34 +12:00
|
|
|
/**
|
|
|
|
* Return a fully parsed CouchDB compliant view definition
|
|
|
|
* that will be stored in the design document in the database.
|
|
|
|
*
|
|
|
|
* @param {Object} viewDefinition - the JSON definition for a custom view.
|
|
|
|
* field: field that calculations will be performed on
|
2020-10-10 06:49:23 +13:00
|
|
|
* tableId: tableId of the table this view was created from
|
2020-08-25 02:48:34 +12:00
|
|
|
* groupBy: field that calculations will be grouped by. Field must be present for this to be useful
|
|
|
|
* filters: Array of filter objects containing predicates that are parsed into a JS expression
|
|
|
|
* calculation: an optional calculation to be performed over the view data.
|
|
|
|
*/
|
2020-10-10 06:49:23 +13:00
|
|
|
function viewTemplate({ field, tableId, groupBy, filters = [], calculation }) {
|
2021-07-10 04:50:01 +12:00
|
|
|
// first filter can't have a conjunction
|
2021-04-29 01:57:52 +12:00
|
|
|
if (filters && filters.length > 0 && filters[0].conjunction) {
|
|
|
|
delete filters[0].conjunction
|
|
|
|
}
|
2020-08-24 22:46:28 +12:00
|
|
|
|
2020-09-02 22:52:32 +12:00
|
|
|
let schema = null
|
|
|
|
|
|
|
|
if (calculation) {
|
2020-09-15 02:40:45 +12:00
|
|
|
schema = {
|
|
|
|
...(groupBy ? GROUP_PROPERTY : FIELD_PROPERTY),
|
2020-09-15 02:41:20 +12:00
|
|
|
...SCHEMA_MAP[calculation],
|
2020-09-15 02:40:45 +12:00
|
|
|
}
|
2022-05-14 05:09:39 +12:00
|
|
|
if (
|
|
|
|
!filters.find(
|
|
|
|
filter =>
|
|
|
|
filter.key === field && filter.condition === CONDITIONS.NOT_EMPTY
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
filters.push({ key: field, condition: CONDITIONS.NOT_EMPTY })
|
|
|
|
}
|
2020-09-02 22:52:32 +12:00
|
|
|
}
|
|
|
|
|
2022-05-14 05:09:39 +12:00
|
|
|
const parsedFilters = parseFilterExpression(filters)
|
|
|
|
const filterExpression = parsedFilters ? `&& (${parsedFilters})` : ""
|
|
|
|
|
|
|
|
const emitExpression = parseEmitExpression(field, groupBy)
|
|
|
|
|
|
|
|
const reduction = field && calculation ? { reduce: `_${calculation}` } : {}
|
|
|
|
|
2020-08-15 03:31:53 +12:00
|
|
|
return {
|
|
|
|
meta: {
|
|
|
|
field,
|
2020-10-10 06:49:23 +13:00
|
|
|
tableId,
|
2020-08-18 08:01:43 +12:00
|
|
|
groupBy,
|
2020-08-22 04:05:26 +12:00
|
|
|
filters,
|
2020-09-02 22:52:32 +12:00
|
|
|
schema,
|
2020-08-25 02:48:34 +12:00
|
|
|
calculation,
|
2020-08-15 03:31:53 +12:00
|
|
|
},
|
|
|
|
map: `function (doc) {
|
2020-10-10 06:49:23 +13:00
|
|
|
if (doc.tableId === "${tableId}" ${filterExpression}) {
|
2020-08-22 04:05:26 +12:00
|
|
|
${emitExpression}
|
2020-08-15 03:31:53 +12:00
|
|
|
}
|
|
|
|
}`,
|
2020-08-25 02:48:34 +12:00
|
|
|
...reduction,
|
2020-08-15 03:31:53 +12:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-24 22:46:28 +12:00
|
|
|
module.exports = viewTemplate
|