import moment from 'moment/min/moment-with-locales'; import { TAPi18n } from '/imports/i18n'; import { OPERATOR_ASSIGNEE, OPERATOR_BOARD, OPERATOR_COMMENT, OPERATOR_CREATED_AT, OPERATOR_CREATOR, OPERATOR_DEBUG, OPERATOR_DUE, OPERATOR_HAS, OPERATOR_LABEL, OPERATOR_LIMIT, OPERATOR_LIST, OPERATOR_MEMBER, OPERATOR_MODIFIED_AT, OPERATOR_ORG, OPERATOR_SORT, OPERATOR_STATUS, OPERATOR_SWIMLANE, OPERATOR_TEAM, OPERATOR_UNKNOWN, OPERATOR_USER, ORDER_ASCENDING, ORDER_DESCENDING, PREDICATE_ALL, PREDICATE_ARCHIVED, PREDICATE_ASSIGNEES, PREDICATE_ATTACHMENT, PREDICATE_CHECKLIST, PREDICATE_CREATED_AT, PREDICATE_DESCRIPTION, PREDICATE_DUE_AT, PREDICATE_END_AT, PREDICATE_ENDED, PREDICATE_MEMBERS, PREDICATE_MODIFIED_AT, PREDICATE_MONTH, PREDICATE_OPEN, PREDICATE_OVERDUE, PREDICATE_PRIVATE, PREDICATE_PROJECTION, PREDICATE_PUBLIC, PREDICATE_QUARTER, PREDICATE_SELECTOR, PREDICATE_START_AT, PREDICATE_WEEK, PREDICATE_YEAR, } from './search-const'; import Boards from '../models/boards'; export class QueryDebug { predicate = null; constructor(predicate) { if (predicate) { this.set(predicate) } } get() { return this.predicate; } set(predicate) { if ([PREDICATE_ALL, PREDICATE_SELECTOR, PREDICATE_PROJECTION].includes( predicate )) { this.predicate = predicate; } else { this.predicate = null; } } show() { return (this.predicate !== null); } showAll() { return (this.predicate === PREDICATE_ALL); } showSelector() { return (this.predicate === PREDICATE_ALL || this.predicate === PREDICATE_SELECTOR); } showProjection() { return (this.predicate === PREDICATE_ALL || this.predicate === PREDICATE_PROJECTION); } } export class QueryParams { text = ''; constructor(params = {}, text = '') { this.params = params; this.text = text; } hasOperator(operator) { return ( this.params[operator] !== undefined && (this.params[operator].length === undefined || this.params[operator].length > 0) ); } addPredicate(operator, predicate) { if (!this.hasOperator(operator)) { this.params[operator] = []; } this.params[operator].push(predicate); } setPredicate(operator, predicate) { this.params[operator] = predicate; } getPredicate(operator) { if (this.hasOperator(operator)){ if (typeof this.params[operator] === 'object') { return this.params[operator][0]; } else { return this.params[operator]; } } return null; } getPredicates(operator) { return this.params[operator]; } getParams() { return this.params; } } export class QueryErrors { operatorTagMap = [ [OPERATOR_BOARD, 'board-title-not-found'], [OPERATOR_SWIMLANE, 'swimlane-title-not-found'], [ OPERATOR_LABEL, label => { if (Boards.labelColors().includes(label)) { return { tag: 'label-color-not-found', value: label, color: true, }; } else { return { tag: 'label-not-found', value: label, color: false, }; } }, ], [OPERATOR_LIST, 'list-title-not-found'], [OPERATOR_COMMENT, 'comment-not-found'], [OPERATOR_USER, 'user-username-not-found'], [OPERATOR_ASSIGNEE, 'user-username-not-found'], [OPERATOR_MEMBER, 'user-username-not-found'], [OPERATOR_CREATOR, 'user-username-not-found'], [OPERATOR_ORG, 'org-name-not-found'], [OPERATOR_TEAM, 'team-name-not-found'], ]; constructor() { this._errors = {}; this.operatorTags = {}; this.operatorTagMap.forEach(([operator, tag]) => { this.operatorTags[operator] = tag; }); this.colorMap = Boards.colorMap(); } addError(operator, error) { if (!this._errors[operator]) { this._errors[operator] = []; } this._errors[operator].push(error); } addNotFound(operator, value) { if (typeof this.operatorTags[operator] === 'function') { this.addError(operator, this.operatorTags[operator](value)); } else { this.addError(operator, { tag: this.operatorTags[operator], value }); } } hasErrors() { return Object.entries(this._errors).length > 0; } errors() { const errs = []; // eslint-disable-next-line no-unused-vars Object.entries(this._errors).forEach(([, errors]) => { errors.forEach(err => { errs.push(err); }); }); return errs; } errorMessages() { const messages = []; // eslint-disable-next-line no-unused-vars Object.entries(this._errors).forEach(([, errors]) => { errors.forEach(err => { messages.push(TAPi18n.__(err.tag, err.value)); }); }); return messages; } } export class Query { selector = {}; projection = {}; constructor(selector, projection) { this._errors = new QueryErrors(); this.queryParams = new QueryParams(); this.colorMap = Boards.colorMap(); if (selector) { this.selector = selector; } if (projection) { this.projection = projection; } } hasErrors() { return this._errors.hasErrors(); } errors() { return this._errors.errors(); } addError(operator, error) { this._errors.addError(operator, error) } errorMessages() { return this._errors.errorMessages(); } getQueryParams() { return this.queryParams; } setQueryParams(queryParams) { this.queryParams = queryParams; } addPredicate(operator, predicate) { this.queryParams.addPredicate(operator, predicate); } buildParams(queryText) { this.queryParams = new QueryParams(); queryText = queryText.trim(); // eslint-disable-next-line no-console //console.log('query:', query); if (!queryText) { return; } const reOperator1 = new RegExp( '^((?[\\p{Letter}\\p{Mark}]+):|(?[#@]))(?[\\p{Letter}\\p{Mark}]+)(\\s+|$)', 'iu', ); const reOperator2 = new RegExp( '^((?[\\p{Letter}\\p{Mark}]+):|(?[#@]))(?["\']*)(?.*?)\\k(\\s+|$)', 'iu', ); const reText = new RegExp('^(?\\S+)(\\s+|$)', 'u'); const reQuotedText = new RegExp( '^(?["\'])(?.*?)\\k(\\s+|$)', 'u', ); const reNegatedOperator = new RegExp('^-(?.*)$'); const operators = { 'operator-board': OPERATOR_BOARD, 'operator-board-abbrev': OPERATOR_BOARD, 'operator-swimlane': OPERATOR_SWIMLANE, 'operator-swimlane-abbrev': OPERATOR_SWIMLANE, 'operator-list': OPERATOR_LIST, 'operator-list-abbrev': OPERATOR_LIST, 'operator-label': OPERATOR_LABEL, 'operator-label-abbrev': OPERATOR_LABEL, 'operator-user': OPERATOR_USER, 'operator-user-abbrev': OPERATOR_USER, 'operator-member': OPERATOR_MEMBER, 'operator-member-abbrev': OPERATOR_MEMBER, 'operator-assignee': OPERATOR_ASSIGNEE, 'operator-creator': OPERATOR_CREATOR, 'operator-assignee-abbrev': OPERATOR_ASSIGNEE, 'operator-status': OPERATOR_STATUS, 'operator-due': OPERATOR_DUE, 'operator-created': OPERATOR_CREATED_AT, 'operator-modified': OPERATOR_MODIFIED_AT, 'operator-comment': OPERATOR_COMMENT, 'operator-has': OPERATOR_HAS, 'operator-sort': OPERATOR_SORT, 'operator-limit': OPERATOR_LIMIT, 'operator-debug': OPERATOR_DEBUG, 'operator-org': OPERATOR_ORG, 'operator-team': OPERATOR_TEAM, }; const predicates = { durations: { 'predicate-week': PREDICATE_WEEK, 'predicate-month': PREDICATE_MONTH, 'predicate-quarter': PREDICATE_QUARTER, 'predicate-year': PREDICATE_YEAR, }, }; predicates[OPERATOR_DUE] = { 'predicate-overdue': PREDICATE_OVERDUE, }; predicates[OPERATOR_STATUS] = { 'predicate-archived': PREDICATE_ARCHIVED, 'predicate-all': PREDICATE_ALL, 'predicate-open': PREDICATE_OPEN, 'predicate-ended': PREDICATE_ENDED, 'predicate-public': PREDICATE_PUBLIC, 'predicate-private': PREDICATE_PRIVATE, }; predicates[OPERATOR_SORT] = { 'predicate-due': PREDICATE_DUE_AT, 'predicate-created': PREDICATE_CREATED_AT, 'predicate-modified': PREDICATE_MODIFIED_AT, }; predicates[OPERATOR_HAS] = { 'predicate-description': PREDICATE_DESCRIPTION, 'predicate-checklist': PREDICATE_CHECKLIST, 'predicate-attachment': PREDICATE_ATTACHMENT, 'predicate-start': PREDICATE_START_AT, 'predicate-end': PREDICATE_END_AT, 'predicate-due': PREDICATE_DUE_AT, 'predicate-assignee': PREDICATE_ASSIGNEES, 'predicate-member': PREDICATE_MEMBERS, }; predicates[OPERATOR_DEBUG] = { 'predicate-all': PREDICATE_ALL, 'predicate-selector': PREDICATE_SELECTOR, 'predicate-projection': PREDICATE_PROJECTION, }; const predicateTranslations = {}; Object.entries(predicates).forEach(([category, catPreds]) => { predicateTranslations[category] = {}; Object.entries(catPreds).forEach(([tag, value]) => { predicateTranslations[category][TAPi18n.__(tag)] = value; }); }); // eslint-disable-next-line no-console // console.log('predicateTranslations:', predicateTranslations); const operatorMap = {}; Object.entries(operators).forEach(([key, value]) => { operatorMap[TAPi18n.__(key).toLowerCase()] = value; }); // eslint-disable-next-line no-console // console.log('operatorMap:', operatorMap); let text = ''; while (queryText) { let m = queryText.match(reOperator1); if (!m) { m = queryText.match(reOperator2); if (m) { queryText = queryText.replace(reOperator2, ''); } } else { queryText = queryText.replace(reOperator1, ''); } if (m) { let op; if (m.groups.operator) { op = m.groups.operator.toLowerCase(); } else { op = m.groups.abbrev.toLowerCase(); } // eslint-disable-next-line no-prototype-builtins if (operatorMap.hasOwnProperty(op)) { const operator = operatorMap[op]; let value = m.groups.value; if (operator === OPERATOR_LABEL) { if (value in this.colorMap) { value = this.colorMap[value]; // console.log('found color:', value); } } else if ( [OPERATOR_DUE, OPERATOR_CREATED_AT, OPERATOR_MODIFIED_AT].includes( operator, ) ) { const days = parseInt(value, 10); let duration = null; if (isNaN(days)) { // duration was specified as text if (predicateTranslations.durations[value]) { duration = predicateTranslations.durations[value]; let date = null; switch (duration) { case PREDICATE_WEEK: // eslint-disable-next-line no-case-declarations const week = moment().week(); if (week === 52) { date = moment(1, 'W'); date.set('year', date.year() + 1); } else { date = moment(week + 1, 'W'); } break; case PREDICATE_MONTH: // eslint-disable-next-line no-case-declarations const month = moment().month(); // .month() is zero indexed if (month === 11) { date = moment(1, 'M'); date.set('year', date.year() + 1); } else { date = moment(month + 2, 'M'); } break; case PREDICATE_QUARTER: // eslint-disable-next-line no-case-declarations const quarter = moment().quarter(); if (quarter === 4) { date = moment(1, 'Q'); date.set('year', date.year() + 1); } else { date = moment(quarter + 1, 'Q'); } break; case PREDICATE_YEAR: date = moment(moment().year() + 1, 'YYYY'); break; } if (date) { value = { operator: '$lt', value: date.format('YYYY-MM-DD'), }; } } else if ( operator === OPERATOR_DUE && value === PREDICATE_OVERDUE ) { value = { operator: '$lt', value: moment().format('YYYY-MM-DD'), }; } else { this.addError(OPERATOR_DUE, { tag: 'operator-number-expected', value: { operator: op, value }, }); continue; } } else if (operator === OPERATOR_DUE) { value = { operator: '$lt', value: moment(moment().format('YYYY-MM-DD')) .add(days + 1, duration ? duration : 'days') .format(), }; } else { value = { operator: '$gte', value: moment(moment().format('YYYY-MM-DD')) .subtract(days, duration ? duration : 'days') .format(), }; } } else if (operator === OPERATOR_SORT) { let negated = false; const m = value.match(reNegatedOperator); if (m) { value = m.groups.operator; negated = true; } if (!predicateTranslations[OPERATOR_SORT][value]) { this.addError(OPERATOR_SORT, { tag: 'operator-sort-invalid', value, }); continue; } else { value = { name: predicateTranslations[OPERATOR_SORT][value], order: negated ? ORDER_DESCENDING : ORDER_ASCENDING, }; } } else if (operator === OPERATOR_STATUS) { if (!predicateTranslations[OPERATOR_STATUS][value]) { this.addError(OPERATOR_STATUS, { tag: 'operator-status-invalid', value, }); continue; } else { value = predicateTranslations[OPERATOR_STATUS][value]; } } else if (operator === OPERATOR_HAS) { let negated = false; const m = value.match(reNegatedOperator); if (m) { value = m.groups.operator; negated = true; } if (!predicateTranslations[OPERATOR_HAS][value]) { this.addError(OPERATOR_HAS, { tag: 'operator-has-invalid', value, }); continue; } else { value = { field: predicateTranslations[OPERATOR_HAS][value], exists: !negated, }; } } else if (operator === OPERATOR_LIMIT) { const limit = parseInt(value, 10); if (isNaN(limit) || limit < 0) { this.addError(OPERATOR_LIMIT, { tag: 'operator-limit-invalid', value, }); continue; } else if (limit == 0) { // no limit continue; } else { value = limit; } } else if (operator === OPERATOR_DEBUG) { if (!predicateTranslations[OPERATOR_DEBUG][value]) { this.addError(OPERATOR_DEBUG, { tag: 'operator-debug-invalid', value, }); continue; } else { value = predicateTranslations[OPERATOR_DEBUG][value]; } } this.queryParams.addPredicate(operator, value); } else { this.addError(OPERATOR_UNKNOWN, { tag: 'operator-unknown-error', value: op, }); } continue; } m = queryText.match(reQuotedText); if (!m) { m = queryText.match(reText); if (m) { queryText = queryText.replace(reText, ''); } } else { queryText = queryText.replace(reQuotedText, ''); } if (m) { text += (text ? ' ' : '') + m.groups.text; } } this.queryParams.text = text; // eslint-disable-next-line no-console if (this.queryParams.hasOperator(OPERATOR_DEBUG)) { // eslint-disable-next-line no-console console.log('text:', this.queryParams.text); console.log('queryParams:', this.queryParams); } } }