Finding from the review at be79f3a.
createFilterQueryTable's IN / NOT IN branch read element.val directly, so it
produced a temporal literal only because whereClauseCreator had already mutated
query.value[i].val from the formatted label to the unformatted count in place
before submit. Any path reaching the branch without the preview having run
first would have put the formatted string into RAW_VALUE.
The entry now goes through the same unformattedValue() lookup as the BETWEEN
ends. An already-unformatted value passes through unchanged, since it no longer
matches a formatted label.
Two unit cases added: an IN entry carrying the formatted label, and one already
carrying the internal count. 578/578 unit tests pass.
965 lines
30 KiB
TypeScript
965 lines
30 KiB
TypeScript
import { Injectable } from '@angular/core'
|
|
import { Subject } from 'rxjs'
|
|
|
|
import { SpecObj } from '../interfaces'
|
|
import { SasService } from './sas.service'
|
|
import { HelperService } from './helper.service'
|
|
|
|
import { QueryClause } from '../models/TableData'
|
|
import { globals } from '../_globals'
|
|
import { FilterClause, FilterGroup, FilterQuery } from '../models/FilterQuery'
|
|
import {
|
|
$DataFormats,
|
|
EditorsGetDataSASResponse,
|
|
EditorsGetDataServiceResponse
|
|
} from '../models/sas/editors-getdata.model'
|
|
import { LoggerService } from './logger.service'
|
|
import { isSpecialMissing } from '@sasjs/utils/input/validators'
|
|
import { Col } from '../shared/dc-validator/models/col.model'
|
|
import { get } from 'lodash-es'
|
|
import { EditorsStageDataSASResponse } from '../models/sas/editors-stagedata.model'
|
|
import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapperResponse'
|
|
|
|
@Injectable()
|
|
export class SasStoreService {
|
|
public response: Subject<any> = new Subject<any>()
|
|
public changedTable: Subject<any> = new Subject<any>()
|
|
public details: Subject<any> = new Subject<any>()
|
|
public diffs: Subject<any> = new Subject<any>()
|
|
public columns: Subject<any> = new Subject<any>()
|
|
public values: Subject<any> = new Subject<any>()
|
|
public filter: Subject<any> = new Subject<any>()
|
|
public query: Subject<any> = new Subject<any>()
|
|
public submittDetail: Subject<any> = new Subject<any>()
|
|
public removeQuery: Subject<any> = new Subject<any>()
|
|
public setSubmit: Subject<any> = new Subject<any>()
|
|
public setSubmitList: Subject<any> = new Subject<any>()
|
|
|
|
/**
|
|
* Submit queue payload carried across the navigation from the submit
|
|
* list to a details URL - the re-created list component renders it
|
|
* without fetching the queue again.
|
|
*/
|
|
private submitsHandoff: Array<any> | null = null
|
|
|
|
constructor(
|
|
private sasService: SasService,
|
|
private helperService: HelperService,
|
|
private loggerService: LoggerService
|
|
) {}
|
|
|
|
/**
|
|
* Wrapper for making request to service
|
|
* Should be removed, as it's redundant now
|
|
* TODO: Refactor to call editors/getdata directly
|
|
* @param tableData
|
|
* @param tableName
|
|
* @param program
|
|
* @param libds
|
|
* @returns
|
|
*/
|
|
public async callService(
|
|
tableData: Array<any>,
|
|
tableName: string,
|
|
program: string,
|
|
libds: string
|
|
) {
|
|
const tables: any = {}
|
|
tables[tableName] = [tableData]
|
|
const res: RequestWrapperResponse<EditorsGetDataSASResponse> =
|
|
await this.sasService.request(program, tables)
|
|
const response: EditorsGetDataServiceResponse = {
|
|
data: res.adapterResponse,
|
|
libds: libds
|
|
}
|
|
return response
|
|
}
|
|
|
|
/**
|
|
* Calling editors/stagedata - saving table data, sending request to backend
|
|
* @param tableParams params to send to backend
|
|
* @param tableData data to be updated
|
|
* @param tableName name of the table to be updated
|
|
* @param program service against which we send request
|
|
* @param $dataFormats column data formats recieved from backend, sending it back
|
|
* @returns adapter.request() response
|
|
*/
|
|
public async updateTable(
|
|
tableParams: any,
|
|
tableData: any,
|
|
tableName: string,
|
|
program: string,
|
|
$dataFormats: $DataFormats | null,
|
|
suppressErrorSuccessMessages?: boolean,
|
|
adapterConfig?: any
|
|
): Promise<RequestWrapperResponse<EditorsStageDataSASResponse>> {
|
|
// add sp as third argument of createData call
|
|
|
|
let tables: any = {
|
|
jsdata: tableData
|
|
}
|
|
|
|
if ($dataFormats) {
|
|
let formats = this.parseFormats($dataFormats)
|
|
|
|
tables.$jsdata = { formats: formats }
|
|
}
|
|
|
|
tables[tableName] = [tableParams]
|
|
|
|
let res = await this.sasService.request<EditorsStageDataSASResponse>(
|
|
program,
|
|
tables,
|
|
adapterConfig,
|
|
{
|
|
suppressErrorAbortModal: suppressErrorSuccessMessages,
|
|
suppressSuccessAbortModal: suppressErrorSuccessMessages
|
|
}
|
|
)
|
|
|
|
return res
|
|
}
|
|
|
|
/**
|
|
* Sending request to 'approvers/getapprovals' to fetch approvals list
|
|
* @param tableData sending to backend table data
|
|
* @param tableName sending to backend table name
|
|
* @param program service to run request on
|
|
* @returns HTTP Response
|
|
*/
|
|
public async getApprovals(
|
|
tableData: any,
|
|
tableName: string,
|
|
program: string
|
|
) {
|
|
let tables: any = {}
|
|
tables[tableName] = [tableData]
|
|
return (await this.sasService.request(program, tables)).adapterResponse
|
|
}
|
|
|
|
/**
|
|
* Interceptor for loading of the submitted details
|
|
* @param detail submitter
|
|
* @param index submitter index
|
|
* @param data submit data
|
|
*/
|
|
public async sendDetails(detail: any, index: any, data: any) {
|
|
let details = Object.assign({ sub: true }, detail)
|
|
let subData = data[index]
|
|
let allData = {
|
|
data: subData,
|
|
viewData: details
|
|
}
|
|
this.submittDetail.next(allData)
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @returns All submits
|
|
*/
|
|
public async getSubmitts() {
|
|
return (await this.sasService.request('editors/getsubmits', null))
|
|
.adapterResponse
|
|
}
|
|
|
|
/**
|
|
* Carries the fetched submit queue over to the details URL navigation,
|
|
* so the re-created submit list component does not fetch it again.
|
|
*/
|
|
public handoffSubmits(fromsas: Array<any> | undefined) {
|
|
this.submitsHandoff = fromsas || null
|
|
}
|
|
|
|
/**
|
|
* Returns the submit queue carried by the current navigation, if any,
|
|
* and clears it - it is only valid for the navigation that set it.
|
|
*/
|
|
public takeSubmitsHandoff(): Array<any> | null {
|
|
const fromsas = this.submitsHandoff
|
|
this.submitsHandoff = null
|
|
return fromsas
|
|
}
|
|
|
|
private libsPromise: Promise<any> | null = null
|
|
|
|
/**
|
|
*
|
|
* @returns All libraries
|
|
*/
|
|
public viewLibs() {
|
|
if (!this.libsPromise) {
|
|
this.libsPromise = this.sasService
|
|
.request('public/viewlibs', null)
|
|
.then((res: any) => res.adapterResponse)
|
|
.catch((err: any) => {
|
|
this.libsPromise = null
|
|
throw err
|
|
})
|
|
}
|
|
return this.libsPromise
|
|
}
|
|
|
|
public async refreshLibInfo(libref: string) {
|
|
const data = {
|
|
lib2refresh: [{ libref }]
|
|
}
|
|
|
|
return (await this.sasService.request('public/refreshlibinfo', data))
|
|
.adapterResponse
|
|
}
|
|
|
|
public async viewTables(lib: any) {
|
|
let tables = { SASControlTable: [{ MPLIB: lib }] }
|
|
return (await this.sasService.request('public/viewtables', tables))
|
|
.adapterResponse
|
|
}
|
|
|
|
public async viewData(libDataset: any, filter_pk: any) {
|
|
let tables = {
|
|
SASControlTable: [{ LIBDS: libDataset, FILTER_RK: filter_pk }]
|
|
}
|
|
return (await this.sasService.request('public/viewdata', tables))
|
|
.adapterResponse
|
|
}
|
|
|
|
public async viewDataSearch(
|
|
searchVal: string,
|
|
numeric: boolean = false,
|
|
libDataset: any,
|
|
filter_pk: any
|
|
) {
|
|
let SEARCHTYPE = numeric ? 'NUM' : 'CHAR'
|
|
|
|
let tables = {
|
|
SASControlTable: [
|
|
{
|
|
SEARCHTYPE: searchVal.length > 0 ? SEARCHTYPE : 'NONE',
|
|
SEARCHVAL: searchVal,
|
|
LIBDS: libDataset,
|
|
FILTER_RK: filter_pk
|
|
}
|
|
]
|
|
}
|
|
|
|
return (await this.sasService.request('public/viewdata', tables))
|
|
.adapterResponse
|
|
}
|
|
|
|
public async getXLMapRules(id: string) {
|
|
const tables = {
|
|
getxlmaps_in: [{ XLMAP_ID: id }]
|
|
}
|
|
return (await this.sasService.request('editors/getxlmaps', tables))
|
|
.adapterResponse
|
|
}
|
|
|
|
public async showDiffs(tableData: any, tableName: string, program: string) {
|
|
let tables: any = {}
|
|
tables[tableName] = [tableData]
|
|
return (
|
|
await this.sasService.request(program, tables, {
|
|
useComputeApi: null // Using WEB APPROACH as a temporary workaround until VIYA JES API is fixed. For other server types then VIYA this is not applicable
|
|
})
|
|
).adapterResponse
|
|
}
|
|
public async rejecting(tableData: any, tableName: string, program: string) {
|
|
let tables: any = {}
|
|
tables[tableName] = [tableData]
|
|
return (
|
|
await this.sasService.request(program, tables, {
|
|
useComputeApi: null // Using WEB APPROACH as a temporary workaround until VIYA JES API is fixed. For other server types then VIYA this is not applicable
|
|
})
|
|
).adapterResponse
|
|
}
|
|
|
|
public async approveTable(
|
|
tableData: any,
|
|
tableName: string,
|
|
program: string
|
|
) {
|
|
let tables: any = {}
|
|
tables[tableName] = [tableData]
|
|
|
|
return (await this.sasService.request(program, tables)).adapterResponse
|
|
}
|
|
|
|
public async getHistory(tableData: any, tableName: string, program: string) {
|
|
let tables: any = {}
|
|
tables[tableName] = [tableData]
|
|
return (await this.sasService.request(program, tables)).adapterResponse
|
|
}
|
|
|
|
setQueryVariables(dataset: string, cols: any) {
|
|
let columnsData: any = { data: { cols: cols }, libds: dataset }
|
|
|
|
this.columns.next(columnsData)
|
|
}
|
|
|
|
/**
|
|
* Column metadata for the filter picker, read from the service.
|
|
*
|
|
* The picker's variable list normally comes from the columns that arrived
|
|
* with the table (see setQueryVariablesFromTable). This is the fallback for
|
|
* when there is no table - public/getcols reads the metadata only.
|
|
*
|
|
* public/getcols speaks the SAS vocabulary: TYPE is C/N and DDTYPE separates
|
|
* DATE, DATETIME and TIME. The picker keys its operator set (query.component),
|
|
* its variable highlight and its value quoting (sas-store) on the client
|
|
* vocabulary instead - TYPE is exactly 'num' or 'char' - which
|
|
* mergeColsRules derives from $dataFormats on the normal path. That pass
|
|
* never runs for this fallback, so map TYPE here, with the same rule: the
|
|
* date and time types are numeric.
|
|
*/
|
|
public async getCols(libds: string) {
|
|
const tables = { iwant: [{ libds: libds }] }
|
|
|
|
const res = await this.sasService.request('public/getcols', tables)
|
|
|
|
const cols = res.adapterResponse.cols || []
|
|
|
|
return cols.map((col: any) => ({
|
|
...col,
|
|
TYPE:
|
|
['DATE', 'DATETIME', 'TIME'].includes(col.DDTYPE) || col.TYPE === 'N'
|
|
? 'num'
|
|
: 'char'
|
|
}))
|
|
}
|
|
|
|
/**
|
|
* Feeds the filter picker its variable list.
|
|
*
|
|
* `cols` is the column metadata that came back with the table, which costs
|
|
* nothing extra and is the normal path. When the table was not returned - the
|
|
* EDIT/VIEW row and cell limits abort the request before the data arrives -
|
|
* that list is empty, which is what makes "please filter and try again"
|
|
* unactionable: the picker has nothing to offer. Fall back to
|
|
* public/getcols, which reads the column metadata without the rows.
|
|
*/
|
|
public async setQueryVariablesFromTable(libds: string, cols: any[]) {
|
|
if (cols && cols.length > 0) {
|
|
this.setQueryVariables(libds, cols)
|
|
return
|
|
}
|
|
|
|
const fallback = await this.getCols(libds)
|
|
|
|
/*
|
|
public/getcols emits an empty cols table when the dataset has no
|
|
metadata. Publishing that would push an empty array into the filter
|
|
picker, whose subscriber reads cols[0].NAME - the throw is caught, but
|
|
the picker is left half-updated (this.cols assigned, the filter.cols
|
|
assignment below the throwing line never run). There is nothing to
|
|
offer, so leave the picker as it was.
|
|
*/
|
|
if (!fallback || fallback.length === 0) return
|
|
|
|
this.setQueryVariables(libds, fallback)
|
|
}
|
|
|
|
public async getChangeInfo(tableId: any) {
|
|
let obj = { TABLE: tableId }
|
|
let table = { SASControlTable: [obj] }
|
|
return (await this.sasService.request('public/getchangeinfo', table))
|
|
.adapterResponse
|
|
}
|
|
|
|
public async getQueryValues(
|
|
variable: string,
|
|
dataset: string,
|
|
filterQuery: any[],
|
|
STARTROW?: number,
|
|
ROWS?: number
|
|
) {
|
|
let tables: any = {
|
|
iwant: [
|
|
{
|
|
libds: dataset,
|
|
col: variable,
|
|
...(STARTROW && { STARTROW: STARTROW }), //add only if present
|
|
...(ROWS && { ROWS: ROWS }) //add only if present
|
|
}
|
|
]
|
|
}
|
|
|
|
if (filterQuery.length > 0) {
|
|
tables.FILTERQUERY = filterQuery
|
|
}
|
|
|
|
return (
|
|
await this.sasService
|
|
.request('public/getcolvals', tables)
|
|
.catch((er: any) => {
|
|
throw er
|
|
})
|
|
).adapterResponse
|
|
}
|
|
|
|
public async saveQuery(libds: string, filterQuery: QueryClause[]) {
|
|
let tables = {
|
|
iwant: [{ filter_table: libds }],
|
|
filterquery: filterQuery
|
|
}
|
|
|
|
const res = await this.sasService.request('public/validatefilter', tables)
|
|
|
|
this.filter.next(res)
|
|
|
|
return res.adapterResponse
|
|
}
|
|
|
|
public async openTable(tableId: string) {
|
|
let tables = { iwant: [{ table_id: tableId }] }
|
|
return (await this.sasService.request('auditors/getstagetable', tables))
|
|
.adapterResponse
|
|
}
|
|
|
|
public checkOperator(operator: any, value: any, type: string) {
|
|
let val
|
|
let operators = ['=', '>', '<', '<=', '>=', 'ne']
|
|
switch (operator) {
|
|
case 'BETWEEN':
|
|
if (value instanceof Array) {
|
|
if (value[0] !== '' && value[1] !== '') {
|
|
val = value[0] + ' AND ' + value[1]
|
|
} else if (value[0] !== '' && value[1] === '') {
|
|
value[1] = '.'
|
|
val = value[0] + ' AND ' + value[1]
|
|
} else if (value[0] === '' && value[1] !== '') {
|
|
value[0] = '.'
|
|
val = value[0] + ' AND ' + value[1]
|
|
} else {
|
|
value[0] = '.'
|
|
value[1] = '.'
|
|
val = value[0] + ' AND ' + value[1]
|
|
}
|
|
}
|
|
break
|
|
case 'IN':
|
|
case 'NOT IN':
|
|
let arr = []
|
|
if (typeof value !== 'undefined') {
|
|
for (let ind = 0; ind < value.length; ind++) {
|
|
if (value[ind].checked === true) {
|
|
if (type === 'char') {
|
|
if (typeof value[ind].val === 'string') {
|
|
value[ind].val = value[ind].val.replace("'", "''")
|
|
}
|
|
|
|
arr.push("'" + value[ind].val + "'")
|
|
} else {
|
|
if (value[ind].val === null) {
|
|
value[ind].val = '.'
|
|
}
|
|
arr.push(value[ind].val)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
value = '(' + arr + ')'
|
|
break
|
|
case 'CONTAINS':
|
|
operator = '?'
|
|
break
|
|
case 'NOT EQUAL':
|
|
operator = 'ne'
|
|
break
|
|
default:
|
|
break
|
|
}
|
|
|
|
if (type === 'num' && value === null) {
|
|
value = '.'
|
|
}
|
|
|
|
if (value === 'Please select value') {
|
|
value = ''
|
|
}
|
|
|
|
if (operators.indexOf(operator) !== -1 && type === 'num' && value === '') {
|
|
value = '.'
|
|
}
|
|
|
|
if (operator === 'BETWEEN') {
|
|
return { value: val, operator: operator }
|
|
} else {
|
|
return { value: value, operator: operator }
|
|
}
|
|
}
|
|
|
|
public whereClauseCreator(
|
|
clauses: any,
|
|
groupOperator: string,
|
|
libds: string
|
|
) {
|
|
let whereString: string = ''
|
|
let clausesArr = clauses.queryObj
|
|
let clausesLogic = clauses.groupLogic
|
|
let opr
|
|
let where = ''
|
|
let clauseArr = []
|
|
|
|
for (let index = 0; index < clauses.queryObj.length; index++) {
|
|
let string = ''
|
|
let clause = clauses.queryObj[index]
|
|
|
|
for (let ind = 0; ind < clause.elements.length; ind++) {
|
|
let query = clause.elements[ind]
|
|
|
|
if (ind < clause.elements.length - 1) {
|
|
opr = clause.clauseLogic
|
|
} else {
|
|
opr = ''
|
|
}
|
|
|
|
let val: any
|
|
|
|
for (let k = 0; k < query.values.length; k++) {
|
|
if (
|
|
typeof query.value === 'string' &&
|
|
typeof query.values[k].formatted === 'number'
|
|
) {
|
|
if (query.value === JSON.stringify(query.values[k].formatted)) {
|
|
val = query.values[k].unformatted
|
|
}
|
|
} else {
|
|
let dropValue =
|
|
typeof query.values[k].formatted !== 'number' &&
|
|
query.values[k].formatted !== null
|
|
? query.values[k].formatted.trim()
|
|
: query.values[k].formatted
|
|
|
|
if (query.value === dropValue) {
|
|
val = query.values[k].unformatted
|
|
}
|
|
}
|
|
|
|
let operators = ['=', '>', '<', '<=', '>=', 'ne']
|
|
|
|
if (
|
|
query.value === 'Please select value' &&
|
|
operators.indexOf(query.operator) !== -1
|
|
) {
|
|
val = ''
|
|
}
|
|
|
|
if (
|
|
query.operator === 'CONTAINS' ||
|
|
query.operator === 'LIKE' ||
|
|
query.operator === 'BEGINS_WITH'
|
|
) {
|
|
val = query.value
|
|
}
|
|
}
|
|
|
|
if (!val) {
|
|
val = query.value
|
|
}
|
|
|
|
if (query.value instanceof Array && query.operator === 'BETWEEN') {
|
|
val = []
|
|
val.push(query.value[0])
|
|
val.push(query.value[1])
|
|
for (let i = 0; i < query.values.length; i++) {
|
|
if (val[0] === query.values[i].formatted) {
|
|
val[0] = query.values[i].unformatted
|
|
}
|
|
if (val[1] === query.values[i].formatted) {
|
|
val[1] = query.values[i].unformatted
|
|
}
|
|
}
|
|
}
|
|
|
|
if (
|
|
query.value instanceof Array &&
|
|
(query.operator === 'IN' || query.operator === 'NOT IN')
|
|
) {
|
|
val = []
|
|
val = query.value
|
|
for (let i = 0; i < query.values.length; i++) {
|
|
if (val[i] && val[i].val === query.values[i].formatted) {
|
|
val[i].val = query.values[i].unformatted
|
|
}
|
|
}
|
|
}
|
|
|
|
let type = query.type
|
|
//if the value is variable, omit quotes in the 'where' string
|
|
const isValueVariable = query.valueVariable
|
|
let variable = query.variable === null ? '' : query.variable
|
|
let oper = query.operator === null ? '' : query.operator
|
|
// let value = val === null ? "''" : val;
|
|
let value
|
|
value = this.checkOperator(oper, val, type).value
|
|
if (typeof value === 'string' && value[0] !== '(') {
|
|
value = value.replace("'", "''")
|
|
}
|
|
oper = this.checkOperator(oper, value, type).operator
|
|
|
|
/* A temporal value is shown as a SAS literal (eg '20DEC2053'd) rather
|
|
than the internal day / second count. Only rewrite when the composed
|
|
value is nothing but the numbers the clause is built from - a single
|
|
value, a BETWEEN pair or an IN list. The BETWEEN separator is
|
|
removed before the test because ' AND ' is not part of that character
|
|
set; a typed value that is not a number, such as a date entered by
|
|
hand, would otherwise have every digit run inside it rewritten into
|
|
an unrelated literal. */
|
|
if (
|
|
!isValueVariable &&
|
|
['DATE', 'DATETIME', 'TIME'].includes(query.ddtype) &&
|
|
/^[-0-9.,()\s]+$/.test(String(value).replace(/\s+AND\s+/gi, ' '))
|
|
) {
|
|
value = String(value).replace(
|
|
/-?\d+(?:\.\d+)?/g,
|
|
(match) =>
|
|
this.helperService.sasTemporalLiteral(match, query.ddtype) ??
|
|
match
|
|
)
|
|
}
|
|
|
|
if (type === 'char' && oper !== 'IN' && oper !== 'NOT IN') {
|
|
if (typeof value === 'undefined') {
|
|
value = ''
|
|
}
|
|
|
|
if (isValueVariable) {
|
|
value = ' ' + value + ' ' //without quotes, with spaces
|
|
} else {
|
|
value = " '" + value + "' " //with quotes and spaces
|
|
}
|
|
|
|
string = string + ' ' + variable + ' ' + oper + value + opr
|
|
} else {
|
|
if (type === 'num' && typeof value === 'undefined') {
|
|
value = '.'
|
|
}
|
|
|
|
if (typeof value === 'undefined') {
|
|
value = ''
|
|
} else {
|
|
value = ' ' + value + ' '
|
|
}
|
|
string = string + ' ' + variable + ' ' + oper + value + opr
|
|
}
|
|
}
|
|
clauseArr.push(string)
|
|
}
|
|
where = ''
|
|
let groupOper
|
|
for (let i = 0; i < clauseArr.length; i++) {
|
|
if (i < clauseArr.length - 1) {
|
|
groupOper = groupOperator
|
|
} else {
|
|
groupOper = ''
|
|
}
|
|
if (clauseArr.length === 1) {
|
|
where = clauseArr[0]
|
|
} else {
|
|
where = where + ' (' + clauseArr[i] + ') ' + groupOper
|
|
}
|
|
}
|
|
|
|
whereString = where.substr(1).slice(0, -1)
|
|
where = 'WHERE' + where + ';'
|
|
|
|
let queryClause = {
|
|
whereClause: where,
|
|
string: whereString,
|
|
obj: clauses,
|
|
libds: libds
|
|
}
|
|
this.query.next(queryClause)
|
|
return { whereClause: where, string: whereString }
|
|
}
|
|
|
|
/**
|
|
* This function creates Filter Query Table
|
|
* @param filterQuery an object of type FilterQuery
|
|
* @returns {QueryClause[]} array of QueryClause
|
|
*/
|
|
public createFilterQueryTable(filterQuery: FilterQuery): QueryClause[] {
|
|
const filterQueryClauseTable: QueryClause[] = []
|
|
|
|
filterQuery.filterGroups.forEach(
|
|
(filterGroup: FilterGroup, groupIndex: number) => {
|
|
filterGroup.filterClauses.forEach((filterClause: FilterClause) => {
|
|
/* A temporal value is sent as a SAS literal (eg '20APR2042'd) rather
|
|
than the internal day / second count, so the resolved WHERE clause
|
|
- and the FILTER_TEXT shown above the grid - carries a date. */
|
|
const temporalValue = (value: any) =>
|
|
filterClause.valueVariable
|
|
? null
|
|
: this.helperService.sasTemporalLiteral(
|
|
value,
|
|
filterClause.ddtype
|
|
)
|
|
|
|
/* The range dropdown binds the formatted label, as the value list
|
|
and the IN modal do, so each end of a BETWEEN is mapped back to
|
|
its unformatted value before the literal is built. This is the
|
|
same lookup whereClauseCreator applies for the preview - without
|
|
it the formatted string reaches sasTemporalLiteral, comes back
|
|
null, and RAW_VALUE carries an unquoted date that the backend
|
|
rejects on a numeric column. */
|
|
const unformattedValue = (value: any) => {
|
|
const match = (filterClause.values || []).find(
|
|
(entry: any) => entry.formatted === value
|
|
)
|
|
return match ? match.unformatted : value
|
|
}
|
|
|
|
let rawValue = ''
|
|
if (filterClause.operator === 'BETWEEN') {
|
|
const from = unformattedValue(filterClause.value[0])
|
|
const to = unformattedValue(filterClause.value[1])
|
|
rawValue = `${temporalValue(from) ?? from} AND ${
|
|
temporalValue(to) ?? to
|
|
}`
|
|
} else if (
|
|
filterClause.operator === 'IN' ||
|
|
filterClause.operator === 'NOT IN'
|
|
) {
|
|
filterClause.value.forEach((element: any) => {
|
|
if (element.checked) {
|
|
/* Mapped like the BETWEEN ends - whereClauseCreator normally
|
|
rewrites element.val to the unformatted count before submit,
|
|
so this is a pass-through for the usual path and a guard for
|
|
any path that reaches here without the preview having run. */
|
|
const value = unformattedValue(element.val)
|
|
const literal = temporalValue(value)
|
|
if (literal) {
|
|
rawValue += (rawValue.length > 0 ? ',' : '(') + literal
|
|
} else if (
|
|
typeof value === 'string' &&
|
|
(!isSpecialMissing(value) || filterClause.type === 'char')
|
|
) {
|
|
if (rawValue.length > 0) {
|
|
rawValue += `,'${value.replace(/'/g, "''")}'`
|
|
} else {
|
|
rawValue = `('${value.replace(/'/g, "''")}'`
|
|
}
|
|
} else {
|
|
if (rawValue.length > 0) {
|
|
rawValue += `,${value}`
|
|
} else {
|
|
rawValue = `(${value}`
|
|
}
|
|
}
|
|
}
|
|
})
|
|
if (rawValue.length > 0) {
|
|
rawValue += ')'
|
|
}
|
|
} else if (filterClause.value === '') {
|
|
if (filterClause.type === 'char') {
|
|
rawValue = `' '`
|
|
} else {
|
|
rawValue = '.'
|
|
}
|
|
} else {
|
|
const literal = temporalValue(filterClause.value)
|
|
if (literal) {
|
|
rawValue = literal
|
|
} else if (
|
|
filterClause.type === 'char' &&
|
|
!filterClause.valueVariable
|
|
) {
|
|
rawValue = `'${filterClause.value.replace(/'/g, "''")}'`
|
|
}
|
|
}
|
|
filterQueryClauseTable.push({
|
|
GROUP_LOGIC: filterQuery.groupLogic,
|
|
SUBGROUP_LOGIC: filterGroup.clauseLogic || 'AND',
|
|
SUBGROUP_ID: groupIndex,
|
|
VARIABLE_NM: filterClause.variable,
|
|
OPERATOR_NM: filterClause.operator,
|
|
RAW_VALUE: rawValue || filterClause.value
|
|
})
|
|
})
|
|
}
|
|
)
|
|
|
|
return filterQueryClauseTable
|
|
}
|
|
|
|
/**
|
|
* This helper function initializes global filter when user load the filtered url
|
|
* @param path it tells whether function is being called from viewer component or editor
|
|
* @param cols array of columns metadata
|
|
*/
|
|
public initializeGlobalFilterClause(page: string, cols: Col[]): void {
|
|
const clauses: any = {
|
|
queryObj: [],
|
|
clauseLogic: [],
|
|
groupLogic: get(globals, page).filter.query[0].GROUP_LOGIC
|
|
}
|
|
|
|
get(globals, page).filter.clauses = this.helperService.deepClone(clauses)
|
|
get(globals, page).filter.groupLogic = get(
|
|
globals,
|
|
page
|
|
).filter.query[0].GROUP_LOGIC
|
|
|
|
let subGroupLogic = ''
|
|
let group: any = { elements: [] }
|
|
let groupID = 0
|
|
|
|
get(globals, page).filter.query.forEach((obj: any) => {
|
|
const temp: any = {}
|
|
for (let i = 0; i < cols.length; i++) {
|
|
if (cols[i].NAME === obj.VARIABLE_NM) {
|
|
subGroupLogic = obj.SUBGROUP_LOGIC
|
|
temp['ddtype'] = cols[i].DDTYPE
|
|
temp['type'] = cols[i].TYPE
|
|
|
|
if (cols[i].TYPE === 'num') {
|
|
temp['operators'] = globals.operators.numOperators
|
|
} else {
|
|
temp['operators'] = globals.operators.charOperators
|
|
}
|
|
|
|
temp['logic'] = null
|
|
temp['operator'] = obj.OPERATOR_NM
|
|
temp['variable'] = obj.VARIABLE_NM
|
|
|
|
if (cols[i].TYPE === 'char') {
|
|
if (obj.OPERATOR_NM === 'IN' || obj.OPERATOR_NM === 'NOT IN') {
|
|
const newValueArray: { checked: boolean; val: any }[] = []
|
|
const valueString = obj.RAW_VALUE.slice(1, -1)
|
|
const valueArray = valueString.split("','")
|
|
|
|
if (valueArray.length === 1) {
|
|
newValueArray.push({
|
|
checked: true,
|
|
val: valueArray[0].slice(1, -1).replace(/''/g, "'")
|
|
})
|
|
} else {
|
|
valueArray.forEach((element: string, index: number) => {
|
|
if (index === 0) {
|
|
newValueArray.push({
|
|
checked: true,
|
|
val: element.slice(1).replace(/''/g, "'")
|
|
})
|
|
} else if (index === valueArray.length - 1) {
|
|
newValueArray.push({
|
|
checked: true,
|
|
val: element.slice(0, -1).replace(/''/g, "'")
|
|
})
|
|
} else {
|
|
newValueArray.push({
|
|
checked: true,
|
|
val: element.replace(/''/g, "'")
|
|
})
|
|
}
|
|
})
|
|
}
|
|
temp['value'] = this.helperService.deepClone(newValueArray)
|
|
} else {
|
|
temp['value'] = obj.RAW_VALUE.slice(1, -1).replace(/''/g, "'")
|
|
}
|
|
} else {
|
|
if (obj.OPERATOR_NM === 'IN' || obj.OPERATOR_NM === 'NOT IN') {
|
|
const newValueArray: { checked: boolean; val: any }[] = []
|
|
const valueString = obj.RAW_VALUE.slice(1, -1)
|
|
const valueArray = valueString.split(',')
|
|
valueArray.forEach((element: string, index: number) => {
|
|
newValueArray.push({
|
|
checked: true,
|
|
val: element
|
|
})
|
|
})
|
|
temp['value'] = this.helperService.deepClone(newValueArray)
|
|
} else if (obj.OPERATOR_NM === 'BETWEEN') {
|
|
const valueArray = obj.RAW_VALUE.split(' AND ')
|
|
temp['value'] = valueArray
|
|
} else {
|
|
temp['value'] = obj.RAW_VALUE
|
|
}
|
|
}
|
|
|
|
temp['values'] = []
|
|
if (groupID === obj.SUBGROUP_ID) {
|
|
group.elements.push(this.helperService.deepClone(temp))
|
|
} else {
|
|
get(globals, page).filter.clauses.queryObj.push(
|
|
this.helperService.deepClone(group)
|
|
)
|
|
get(globals, page).filter.clauses.queryObj[groupID].clauseLogic =
|
|
subGroupLogic
|
|
get(globals, page).filter.clauses.queryObj[groupID].invalidClause =
|
|
false
|
|
groupID++
|
|
group.elements = []
|
|
group.elements.push(this.helperService.deepClone(temp))
|
|
}
|
|
break
|
|
}
|
|
}
|
|
})
|
|
if (group.elements.length > 0) {
|
|
get(globals, page).filter.clauses.queryObj.push(
|
|
this.helperService.deepClone(group)
|
|
)
|
|
get(globals, page).filter.clauses.queryObj[groupID].clauseLogic =
|
|
subGroupLogic
|
|
get(globals, page).filter.clauses.queryObj[groupID].invalidClause = false
|
|
}
|
|
get(globals, page).filter.query = []
|
|
}
|
|
|
|
public async removeClause() {
|
|
let removeFlag = true
|
|
this.removeQuery.next(removeFlag)
|
|
return removeFlag
|
|
}
|
|
|
|
public specConversion(spec: any) {
|
|
let specObj: SpecObj = {}
|
|
let namesArr = []
|
|
namesArr = spec.map(function (item: any) {
|
|
return item.NAME.toUpperCase()
|
|
})
|
|
if (spec instanceof Array) {
|
|
for (let index = 0; index < namesArr.length; index++) {
|
|
if (spec[index].TYPE === 1) {
|
|
specObj[namesArr[index]] = {
|
|
colType: 'num',
|
|
colLength: spec[index].LENGTH
|
|
}
|
|
} else {
|
|
specObj[namesArr[index]] = {
|
|
colType: 'string',
|
|
colLength: spec[index].LENGTH
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return specObj
|
|
}
|
|
|
|
setSubmitReady() {
|
|
let ready = true
|
|
this.setSubmit.next(ready)
|
|
}
|
|
|
|
parseFormats($dataFormats: $DataFormats) {
|
|
const formats: {
|
|
[key: string]: any
|
|
} = {}
|
|
const vars = $dataFormats.vars
|
|
|
|
for (let col of Object.keys(vars)) {
|
|
const type = vars[col].type
|
|
const length = vars[col].length
|
|
|
|
if (type === 'num') {
|
|
formats[col] = 'best.'
|
|
} else if (type === 'char') {
|
|
formats[col] = `$char${length}.`
|
|
}
|
|
}
|
|
|
|
return formats
|
|
}
|
|
}
|