import { enums , fields } from "./protos" ; import { Constraint , ConstraintKey , ConstraintOperation , ConstraintType1 , ConstraintValue , DateConstant , dateConstants , Order , ReportOptions , RequestOptions , SortOrder , } from "./types" ; enum QueryKeywords { SELECT = "SELECT" , FROM = "FROM" , WHERE = "WHERE" , ORDER_BY = "ORDER BY" , LIMIT = "LIMIT" , AND = "AND" , OR = "OR" } type ParsedConstraintValue = string | number | boolean ; type ConstraintString = `${ string } ${ ConstraintOperation } ${ ParsedConstraintValue }` ; type SelectClause = `${ QueryKeywords . SELECT } ${ string }` ; type FromClause = ` ${ QueryKeywords . FROM } ${ fields . Resource }` ; type WhereClause = ` ${ QueryKeywords . WHERE } ${ string }` | `` ; export type OrderClause = ` ${ QueryKeywords . ORDER_BY } ${ string } ${ SortOrder }` | ` ${ QueryKeywords . ORDER_BY } ${ string }` | `` ; type LimitClause = ` ${ QueryKeywords . LIMIT } ${ number }` | `` ; type Query = `${ SelectClause }${ FromClause }${ WhereClause }${ OrderClause }${ LimitClause }` ; export const QueryError = { INVALID_CONSTRAINTS_FORMAT : "Constraints must be an array of objects or a singular object." , INVALID_CONSTRAINT_KEY : "A constraint key must have a string value." , INVALID_CONSTRAINT_VALUE : ( key , val ) : string => `The value of the constraint ${ key } must be a string, number, boolean, or an array of these types. Here, typeof ${ key } is ${ typeof val }.` , INVALID_CONSTRAINT_OBJECT_FORMAT : "Must specify { key, op, val } or { key: value } when using object-style constraints." , INVALID_DATE_CONSTANT_TYPE : ( dateConstant ) : string => `Date constant must be a string. Here, typeof date constant is ${ typeof dateConstant }` , INVALID_FROM_DATE_TYPE : ( fromDate ) : string => `From date must be a string. Here, typeof from date is ${ typeof fromDate }` , INVALID_TO_DATE_TYPE : ( toDate ) : string => `To date must be a string. Here, typeof to date is ${ typeof toDate }` , INVALID_LIMIT : "Limit must be a positive integer." , INVALID_ORDER : "Order must be an array." , INVALID_ORDERLY : "OrderBy arrays must only contain strings." , INVALID_ORDERBY : "OrderBy must be a string or an array of strings." , INVALID_SORT_ORDER : `Sort order must be "ASC" or "DESC".` , MISSING_FIELDS : 'Must specify at least one field in ("attributes","metrics","segments").' , MISSING_FROM_DATE : 'Expected start date range is missing - "from_date".' , UNDEFINED_ENTITY : "The entity of the query must be defined." , } ; export function buildSelectClause ( attributes , metrics , segments ) { if ( ! attributes ?. length && ! metrics ?. length && ! segments ?. length ) { throw new Error ( QueryError . MISSING_FIELDS ) ; } const selections = [ ... ( attributes || [ ] ) , ... ( metrics || [ ] ) , ... ( segments || [ ] ) , ] . join ( ", " ) ; return `${ QueryKeywords . SELECT } ${ selections }` as const ; } export function buildFromClause ( entity ) { if ( typeof entity === "undefined" ) { throw new Error ( QueryError . UNDEFINED_ENTITY ) ; } return ` ${ QueryKeywords . FROM } ${ entity }` as const ; } export function validateConstraintKeyAndValue ( key , op , val ) { if ( typeof val === "number" || typeof val === "boolean" ) { return { op : "=" , val : convertNumericEnumToString ( key , val ) } ; } if ( typeof val === "string" ) { if ( dateConstants . includes ( val as DateConstant ) ) { return { op , val } ; } return { op : "=" , val : new RegExp ( /^'.*'$|^".*"$/g ) . test ( val ) ? val : `"${ val }"` , } ; } if ( Array . isArray ( val ) ) { const stringifiedValue = val . map ( ( v ) => { if ( typeof v === "string" ) { return `"${ v }"` ; } else { return convertNumericEnumToString ( key , v ) ; } } ) . join ( `, ` ) ; return { op : "IN" , val : `(${ stringifiedValue })` } ; } throw new Error ( QueryError . INVALID_CONSTRAINT_VALUE ( key , val ) ) ; } export function convertNumericEnumToString ( key , val ) { if ( fields . enumFields [ key ] && typeof val === "number" ) { const enumStringValue = enums [ fields . enumFields [ key ] ] [ val ] ; if ( enumStringValue ) { return `"${ enumStringValue }"` ; } } return val ; } export function extractConstraintConditions ( constraints ) { if ( typeof constraints === "undefined" ) { return [ ] ; } else if ( Array . isArray ( constraints ) ) { return constraints . map ( ( con ) => { if ( typeof con === "object" && ! Array . isArray ( con ) && con !== null ) { if ( con . key && con . op && typeof con . val !== "undefined" ) { const { key , op , val } = con as ConstraintType1 ; if ( typeof key !== "string" ) { throw new Error ( QueryError . INVALID_CONSTRAINT_KEY ) ; } const validatedValue = validateConstraintKeyAndValue ( key , op , val ) ; return `${ key } ${ op } ${ validatedValue . val }` as const ; } else if ( Object . keys ( con ) . length === 1 ) { const [ [ key , val ] ] = Object . entries ( con ) ; const validatedValue = validateConstraintKeyAndValue ( key as ConstraintKey , "=" , val as ConstraintValue ) ; return `${ key } ${ validatedValue . op } ${ validatedValue . val }` as const ; } else { throw new Error ( QueryError . INVALID_CONSTRAINT_OBJECT_FORMAT ) ; } } else if ( typeof con === "string" ) { return con ; } else { throw new Error ( QueryError . INVALID_CONSTRAINT_OBJECT_FORMAT ) ; } } ) ; } else if ( typeof constraints === "object" && constraints !== null ) { return Object . entries ( constraints ) . map ( ( [ key , val ] ) => { const validatedValue = validateConstraintKeyAndValue ( key as ConstraintKey , "=" , val as ConstraintValue ) ; return `${ key } ${ validatedValue . op } ${ validatedValue . val }` as const ; } ) ; } else { throw new Error ( QueryError . INVALID_CONSTRAINTS_FORMAT ) ; } } export function extractDateConstantConditions ( dateConstant ) { if ( typeof dateConstant === "undefined" ) { return [ ] ; } else if ( typeof dateConstant !== "string" ) { throw new Error ( QueryError . INVALID_DATE_CONSTANT_TYPE ( dateConstant ) ) ; } return [ `segments.date DURING ${ dateConstant }` as const ] ; } export function extractDateConditions ( fromDate , toDate ) { if ( typeof fromDate === "undefined" && typeof toDate === "undefined" ) { return [ ] ; } if ( typeof fromDate === "undefined" && typeof toDate !== "undefined" ) { throw new Error ( QueryError . MISSING_FROM_DATE ) ; } if ( typeof fromDate !== "undefined" && typeof toDate === "undefined" ) { const d = new Date ( ) ; toDate = `${ d . getFullYear ( ) }-${ ( "0" + ( d . getMonth ( ) + 1 ) ) . slice ( - 2 ) }-${ ( "0" + d . getDate ( ) ) . slice ( - 2 ) }` ; } if ( typeof fromDate !== "string" ) { throw new Error ( QueryError . INVALID_FROM_DATE_TYPE ( fromDate ) ) ; } if ( typeof toDate !== "string" ) { throw new Error ( QueryError . INVALID_TO_DATE_TYPE ( toDate ) ) ; } return [ `segments.date >= "${ fromDate }"` , `segments.date <= "${ toDate }"` , ] as ConstraintString [ ] ; } export function buildWhereClause ( constraints , dateConstant , fromDate , toDate ) { const constraintClauses = extractConstraintConditions ( constraints ) ; const dateConstantClauses = extractDateConstantConditions ( dateConstant ) ; const dateClauses = extractDateConditions ( fromDate , toDate ) ; const whereClauses = [ ... constraintClauses , ... dateConstantClauses , ... dateClauses , ] . join ( ` ${ QueryKeywords . AND } ` ) ; return whereClauses . length ? ( ` ${ QueryKeywords . WHERE } ${ whereClauses }` as const ) : `` ; } export function buildLimitClause ( limit ) { if ( typeof limit === "undefined" ) { return `` ; } if ( typeof limit !== "number" || limit < 1 || ! Number . isInteger ( limit ) ) { throw new Error ( QueryError . INVALID_LIMIT ) ; } return ` ${ QueryKeywords . LIMIT } ${ limit }` as const ; } export function completeOrderly ( orderly , entity ) { if ( ! orderly . length ) { throw new Error ( QueryError . INVALID_ORDERLY ) ; } else if ( new RegExp ( /^[^.\s]+(\.[^.\s]+)+$/g ) . test ( orderly ) ) { return orderly ; } else if ( new RegExp ( /^[^.\s]+$/g ) . test ( orderly ) ) { return `${ entity }.${ orderly }` ; } else { throw new Error ( QueryError . INVALID_ORDERLY ) ; } } export function buildOrderClauseOld ( orderBy , sortOrder , entity ) { if ( typeof orderBy === "undefined" ) { return `` ; } else if ( typeof sortOrder === "undefined" ) { sortOrder = "DESC" ; } else if ( sortOrder !== "ASC" && sortOrder !== "DESC" ) { throw new Error ( QueryError . INVALID_SORT_ORDER ) ; } if ( Array . isArray ( orderBy ) ) { const orders = orderBy . map ( ( orderly ) => { if ( typeof orderly !== "string" ) { throw new Error ( QueryError . INVALID_ORDERLY ) ; } else { return completeOrderly ( orderly , entity ) ; } } ) . join ( ", " ) ; return ` ${ QueryKeywords . ORDER_BY } ${ orders } ${ sortOrder }` as const ; } else if ( typeof orderBy === "string" ) { return ` ${ QueryKeywords . ORDER_BY } ${ completeOrderly ( orderBy , entity ) } ${ sortOrder }` as const ; } else { throw new Error ( QueryError . INVALID_ORDERBY ) ; } } export function buildOrderClauseNew ( order , entity ) { if ( ! order || ! Array . isArray ( order ) ) { throw new Error ( QueryError . INVALID_ORDER ) ; } if ( ! order . length ) { return "" ; } const orders = order . map ( ( o ) => { const orderly = completeOrderly ( o . field , entity ) ; const sortOrder = o . sort_order ? o . sort_order : "DESC" ; return `${ orderly } ${ sortOrder }` ; } ) . join ( ", " ) ; return ` ${ QueryKeywords . ORDER_BY } ${ orders }` as const ; } export function buildOrderClause ( order , orderBy , sortOrder , entity ) { if ( order ) { return buildOrderClauseNew ( order , entity ) ; } else { return buildOrderClauseOld ( orderBy , sortOrder , entity ) ; } } export function buildRequestOptions ( reportOptions ) { const { page_size , page_token , validate_only , summary_row_setting } = reportOptions ; return { page_size , page_token , validate_only , summary_row_setting } ; } export function buildQuery ( reportOptions ) { const SELECT = buildSelectClause ( reportOptions . attributes , reportOptions . metrics , reportOptions . segments ) ; const FROM = buildFromClause ( reportOptions . entity ) ; const WHERE = buildWhereClause ( reportOptions . constraints , reportOptions . date_constant , reportOptions . from_date , reportOptions . to_date ) ; const LIMIT = buildLimitClause ( reportOptions . limit ) ; const ORDER = buildOrderClause ( reportOptions . order , reportOptions . order_by , reportOptions . sort_order , reportOptions . entity ) ; const requestOptions = buildRequestOptions ( reportOptions ) ; return { gaqlQuery : `${ SELECT }${ FROM }${ WHERE }${ ORDER }${ LIMIT }` , requestOptions , } as const ; }