516 lines
18 KiB
Plaintext
516 lines
18 KiB
Plaintext
<cfcomponent
|
|
displayname="ReST API helper"
|
|
output="true"
|
|
hint="Static helper methods for ReST API">
|
|
<!--- v0.02 2024-10-15 --->
|
|
<!--- v0.03 2024-11-27 --->
|
|
<!--- v0.04 2025-11-20 columns--->
|
|
<!--- v0.05 2025-11-20 multiple field entries in filter, filter became array instead of structure--->
|
|
<!--- v0.06 2025-11-22 custom url parser used to prevent comma collision in lists--->
|
|
|
|
<cffunction name="empty2null"
|
|
access="public"
|
|
returntype="any"
|
|
output="false"
|
|
hint="replace empty field with null">
|
|
|
|
<cfargument name="value" type="any" required="true" />
|
|
|
|
<cfif isEmpty(ARGUMENTS.value)><!---*** некорректно, но просто, пустые строки превращает в нулл--->
|
|
<cfreturn javacast('null','')/>
|
|
<cfelse>
|
|
<cfreturn ARGUMENTS.value />
|
|
</cfif>
|
|
</cffunction>
|
|
|
|
|
|
<cffunction name="passThrough"
|
|
returntype="any"
|
|
output="false"
|
|
hint="just return argument">
|
|
<cfargument name="x" type="ANY" required="true"/>
|
|
<cfreturn #ARGUMENTS.x#/>
|
|
</cffunction>
|
|
|
|
|
|
<cffunction name="appendRecord"
|
|
access="public"
|
|
returntype="any"
|
|
output="false"
|
|
hint="put data into struct, replace empty fields with null">
|
|
|
|
<cfargument name="struct" type="struct" required="true" />
|
|
<cfargument name="key" type="string" required="true" />
|
|
<cfargument name="fieldMap" type="struct" required="true" /><!--- используется только список и опциональный атрибут formatter --->
|
|
<cfargument name="query" type="query" required="true" />
|
|
<cfargument name="fieldNameDecorator" type="function" required="false" default=#passThrough#/>
|
|
|
|
<cfif len(ARGUMENTS.key)>
|
|
<cfset ARGUMENTS.struct[ARGUMENTS.key]=structNew()/>
|
|
<cfset var container=ARGUMENTS.struct[ARGUMENTS.key]/>
|
|
<cfelse>
|
|
<cfset var container=ARGUMENTS.struct/>
|
|
</cfif>
|
|
|
|
<cfloop list=#structKeyList(ARGUMENTS.fieldMap)# index="local.col">
|
|
<cfset var obj=container/>
|
|
<cfset var formatter = structFind(ARGUMENTS.fieldMap[local.col], "formatter")/>
|
|
<cfif structKeyExists(ARGUMENTS.fieldMap[#local.col#],"container")>
|
|
<cfset var subContainerName = structFind(ARGUMENTS.fieldMap[local.col], "container")/>
|
|
|
|
<cfif len(subContainerName)>
|
|
<cfif !structKeyExists(container, subContainerName)>
|
|
<cfset structInsert(container, subContainerName, structNew())/>
|
|
</cfif>
|
|
<cfset obj=container[subContainerName]/>
|
|
</cfif>
|
|
</cfif>
|
|
|
|
<cfif queryColumnExists(ARGUMENTS.query, local.col)>
|
|
<cfset obj[ARGUMENTS.fieldNameDecorator(local.col)]=empty2null(formatter(ARGUMENTS.query[local.col][ARGUMENTS.query.currentRow]))/>
|
|
</cfif>
|
|
<!---*** не уверен в надежности конструкции во всех реализациях CF, контекст query передастся ли--->
|
|
</cfloop>
|
|
|
|
<cfreturn ARGUMENTS.struct/>
|
|
|
|
</cffunction>
|
|
|
|
|
|
<cffunction name="parseFilterParams"
|
|
returntype="array"
|
|
output="true"
|
|
hint="Parse and collect filter params from URL. For operators see filter_build">
|
|
<cfargument name="params" type="struct" required="true" />
|
|
<!--- используется альтернативный парсер query_string, чтобы не путать запятые в значении с разделителями списка list --->
|
|
<!--- имена полей в URL в snake_case или camelCase, имена колонок в snake_case --->
|
|
<!--- token example minSize=NEQ:2--->
|
|
|
|
<cfset var local = {}/><!--- *** --->
|
|
<cfset var out = []/>
|
|
<cfset var urlParams = parseQs()/>
|
|
|
|
<cfloop collection=#params# index="local.item">
|
|
|
|
<cfset var urlParamName=snake2camel(lCase(local.item))/>
|
|
<cfif structKeyExists(urlParams,urlParamName)>
|
|
|
|
<cfloop array=#urlParams[urlParamName]# item="local.rawValue"><!--- when parameter occures in URL more than once, its values compose a comma delimited list --->
|
|
<cfset var operator="EQ"/>
|
|
<cfset var value=#local.rawValue#/>
|
|
<cfif listLen(local.rawValue,":") GT 1>
|
|
<cfset var operator=listGetAt(local.rawValue,1,":")/>
|
|
<cfset var value=listGetAt(local.rawValue,2,":")/>
|
|
</cfif>
|
|
<cfif !isValid(ARGUMENTS.params[local.item].type,value)>
|
|
<cfthrow type="invalidParamValue" message="Filter parameter #urlParamName# value is not a valid #ARGUMENTS.params[local.item].type#"/><!---не выводим значение, чтобы исключить XSS--->
|
|
</cfif>
|
|
|
|
<cfif structKeyExists(ARGUMENTS.params[local.item], "expression") AND len(ARGUMENTS.params[local.item].expression)>
|
|
<cfset var expr="#ARGUMENTS.params[local.item].expression#"/>
|
|
<cfelse>
|
|
<cfset var expr=local.item/>
|
|
</cfif>
|
|
|
|
<cfif structKeyExists(ARGUMENTS.params[local.item], "prefix") AND len(ARGUMENTS.params[local.item].prefix)>
|
|
<cfset var fieldName="#ARGUMENTS.params[local.item].prefix#.#expr#"/>
|
|
<cfelse>
|
|
<cfset var fieldName=#expr#/>
|
|
</cfif>
|
|
|
|
<cfif structKeyExists(ARGUMENTS.params[local.item], "list") AND ARGUMENTS.params[local.item].list GT 0>
|
|
<cfset var list=ARGUMENTS.params[local.item].list />
|
|
<cfelse>
|
|
<cfset var list=false />
|
|
</cfif>
|
|
|
|
<cfset var rec={field=#fieldName#, val=#value#, ftype=#ARGUMENTS.params[local.item].type#, compare=#operator#, list=#list#}/>
|
|
<cfset arrayAppend(out, rec)/>
|
|
</cfloop>
|
|
</cfif>
|
|
|
|
</cfloop>
|
|
<cfreturn out/>
|
|
</cffunction>
|
|
|
|
<cffunction name="parseFilterParamsV1"
|
|
returntype="array"
|
|
output="true"
|
|
hint="parse and collect filter params, for operators see filter_build">
|
|
<!---e.g. duration=NEQ:2--->
|
|
<!---*** add test for multiple values--->
|
|
<cfargument name="params" type="struct" required="true" /><!--- имена полей и колонок в snake_case --->
|
|
|
|
<cfset var local = {}/><!--- *** --->
|
|
<cfset var out = []/>
|
|
|
|
<cfloop collection=#params# index="local.item">
|
|
|
|
<cfset var urlParamName=snake2camel(lCase(local.item))/>
|
|
<cfif structKeyExists(URL,urlParamName)>
|
|
|
|
<cfloop list=#URL[urlParamName]# item="local.rawValue"><!--- when parameter occures in URL more than once, its values compose a comma delimited list --->
|
|
<cfset var operator="EQ"/>
|
|
<cfset var value=#local.rawValue#/>
|
|
<cfif listLen(local.rawValue,":") GT 1>
|
|
<cfset var operator=listGetAt(local.rawValue,1,":")/>
|
|
<cfset var value=listGetAt(local.rawValue,2,":")/>
|
|
</cfif>
|
|
<cfif !isValid(ARGUMENTS.params[local.item].type,value)>
|
|
<cfthrow type="invalidParamValue" message="Filter parameter #urlParamName# value is not a valid #ARGUMENTS.params[local.item].type#"/><!---не выводим значение, чтобы исключить XSS--->
|
|
</cfif>
|
|
|
|
<cfif structKeyExists(ARGUMENTS.params[local.item], "expression") AND len(ARGUMENTS.params[local.item].expression)>
|
|
<cfset var expr="#ARGUMENTS.params[local.item].expression#"/>
|
|
<cfelse>
|
|
<cfset var expr=local.item/>
|
|
</cfif>
|
|
|
|
<cfif structKeyExists(ARGUMENTS.params[local.item], "prefix") AND len(ARGUMENTS.params[local.item].prefix)>
|
|
<cfset var fieldName="#ARGUMENTS.params[local.item].prefix#.#expr#"/>
|
|
<cfelse>
|
|
<cfset var fieldName=#expr#/>
|
|
</cfif>
|
|
|
|
<cfif structKeyExists(ARGUMENTS.params[local.item], "list") AND ARGUMENTS.params[local.item].list GT 0>
|
|
<cfset var list=ARGUMENTS.params[local.item].list />
|
|
<cfelse>
|
|
<cfset var list=false />
|
|
</cfif>
|
|
|
|
<cfset var rec={field=#fieldName#, val=#value#, ftype=#ARGUMENTS.params[local.item].type#, compare=#operator#, list=#list#}/>
|
|
<cfset arrayAppend(out, rec)/>
|
|
</cfloop>
|
|
</cfif>
|
|
|
|
</cfloop>
|
|
<cfreturn out/>
|
|
</cffunction>
|
|
|
|
|
|
<cffunction name="parseOrderBy"
|
|
returntype="struct"
|
|
output="false"
|
|
hint="parse and collect sort order param"><!--- deprecated --->
|
|
|
|
<cfargument name="params" type="struct" required="true" />
|
|
<cfargument name="orderBy" type="string" required="true" />
|
|
|
|
<cfset var out={}/>
|
|
|
|
<cfloop list=#ARGUMENTS.orderBy# index="local.item">
|
|
<cfif listLen(local.item,".") GT 1>
|
|
<cfset var fld=camel2snake(listGetAt(local.item,1,"."))/>
|
|
<cfswitch expression=#uCase(listGetAt(local.item,2,"."))#>
|
|
<cfcase value="ASC"><cfset var asc=true/></cfcase>
|
|
<cfcase value="DESC"><cfset var asc=false/></cfcase>
|
|
<cfdefaultcase>
|
|
<cfthrow type="InvalidParamValue" message="Invalid orderBy format" detail="orderBy suffix should be '.asc' or '.desc'" /><!---*** XSS vulnerability, so we would not show the suffix--->
|
|
</cfdefaultcase>
|
|
</cfswitch>
|
|
<cfelse>
|
|
<cfset var fld=camel2snake(local.item)/>
|
|
<cfset var asc=true/>
|
|
</cfif>
|
|
|
|
<cfif structKeyExists(ARGUMENTS.params, fld)>
|
|
<cfset structInsert(out,"#ARGUMENTS.params[fld].prefix#.#fld#", {fld="#ARGUMENTS.params[fld].prefix#.#fld#",asc=#asc#}, true)/>
|
|
<cfelse>
|
|
<cfthrow type="InvalidParamValue" message="Invalid orderBy field"/>
|
|
</cfif>
|
|
</cfloop>
|
|
<cfreturn out/>
|
|
</cffunction>
|
|
|
|
<cffunction name="parseNumericOrder"
|
|
returntype="array"
|
|
output="false"
|
|
hint="parse and collect sort order param with numeric notation">
|
|
|
|
<cfargument name="fieldSet" type="struct" required="true" />
|
|
<cfargument name="orderBy" type="string" required="true" />
|
|
|
|
<cfset var out=[]/>
|
|
|
|
<cfloop list=#ARGUMENTS.orderBy# index="local.item">
|
|
<cfif listLen(local.item,".") GT 1>
|
|
<cfset var fld=camel2snake(listGetAt(local.item,1,"."))/>
|
|
<cfswitch expression=#uCase(listGetAt(local.item,2,"."))#>
|
|
<cfcase value="ASC"><cfset var asc=true/></cfcase>
|
|
<cfcase value="DESC"><cfset var asc=false/></cfcase>
|
|
<cfdefaultcase>
|
|
<cfthrow type="InvalidParamValue" message="Invalid orderBy format" detail="orderBy suffix should be '.asc' or '.desc'" /><!---*** XSS vulnerability, so we would not show the suffix--->
|
|
</cfdefaultcase>
|
|
</cfswitch>
|
|
<cfelse>
|
|
<cfset var fld=camel2snake(local.item)/>
|
|
<cfset var asc=true/>
|
|
</cfif>
|
|
<cfif structKeyExists(ARGUMENTS.fieldSet, fld)>
|
|
<cfset arrayAppend(out,{fld="#ARGUMENTS.fieldSet[fld].ordinal#",asc=#asc#})/>
|
|
<cfelse>
|
|
<cfthrow type="InvalidParamValue" message="Invalid orderBy field"/>
|
|
</cfif>
|
|
</cfloop>
|
|
<cfreturn out/>
|
|
</cffunction>
|
|
|
|
|
|
<!---<cffunction name="snake2camel"
|
|
access="public"
|
|
returntype="any"
|
|
output="false"
|
|
hint="convert snake style name to camel style name">
|
|
|
|
<cfargument name="snake" type="string" required="true" />
|
|
|
|
<cfset var snake=ARGUMENTS.snake/>
|
|
<cfset var camel=""/>
|
|
<cfset var pos=1/>
|
|
<cfloop condition="true">
|
|
<cfset next=reFind("(_[a-z])", snake, pos, false)/>
|
|
<cfif NOT next GT 0>
|
|
<cfbreak/>
|
|
</cfif>
|
|
<cfset camel="#camel##mid(snake,pos,next-pos)##uCase(mid(snake,next+1,1))#"/>
|
|
<cfset pos=next+2/>
|
|
</cfloop>
|
|
<cfset camel="#camel##mid(snake,pos,len(snake))#"/>
|
|
|
|
<cfreturn #camel#/>
|
|
</cffunction>--->
|
|
|
|
<!---not used any more--->
|
|
<cffunction name="query4json" access="public" returntype="any" output="false"
|
|
hint="Converts query to an array of structs. Key names are lowercase. Empty fields are treated as nulls">
|
|
|
|
<cfargument name="Query" type="query" required="true" />
|
|
|
|
<cfset var out=arrayNew(1)/>
|
|
<cfset var i=0/>
|
|
|
|
<cfloop query=ARGUMENTS.Query>
|
|
<cfset i=i+1/>
|
|
<cfset out[i]=structNew()/>
|
|
<cfloop list=#ARGUMENTS.Query.ColumnList# index="local.col">
|
|
<cfset var value=ARGUMENTS.Query[local.col][currentRow]/>
|
|
<cfif isEmpty(value)><!---*** некорректно, но просто, пустые строки превращает в нулл--->
|
|
<cfset out[i][local.col]=javacast('null','')/>
|
|
<cfelse>
|
|
<cfset out[i][local.col]=ARGUMENTS.Query[local.col][currentRow]/>
|
|
</cfif>
|
|
</cfloop>
|
|
</cfloop>
|
|
|
|
<cfreturn(out)/>
|
|
</cffunction>
|
|
|
|
|
|
<cffunction name="snake2camel"
|
|
access="public"
|
|
returntype="any"
|
|
output="false"
|
|
hint="convert snake style name to camel style name">
|
|
<cfargument name="snake" type="string" required="true" />
|
|
<cfreturn #reReplace(ARGUMENTS.snake,"_([a-z])","\u\1","ALL")#/>
|
|
</cffunction>
|
|
|
|
|
|
<cffunction name="camel2snake"
|
|
access="public"
|
|
returntype="any"
|
|
output="false"
|
|
hint="convert camel style name to snake style name">
|
|
<cfargument name="snake" type="string" required="true" />
|
|
<cfreturn #reReplace(ARGUMENTS.snake,"([A-Z])","_\l\1","ALL")#/>
|
|
</cffunction>
|
|
|
|
<cffunction name="formatMessage"
|
|
access="public"
|
|
returntype="any"
|
|
output="true"
|
|
hint="formats Exception for display">
|
|
|
|
<cfargument name="message" type="string" required="true" />
|
|
<cfargument name="title" type="string" required="false" />
|
|
|
|
<cfset var title=structKeyExists(ARGUMENTS,"title") ? #ARGUMENTS.title# : #ARGUMENTS.message# />
|
|
|
|
<cfreturn {type="about:blank", title="#title#", detail="#ARGUMENTS.message#"}/>
|
|
</cffunction>
|
|
|
|
|
|
<cffunction name="formatException"
|
|
access="public"
|
|
returntype="any"
|
|
output="true"
|
|
hint="formats Exception for display">
|
|
|
|
<cfargument name="ex" type="struct" required="true" />
|
|
<cfargument name="title" type="string" required="false" />
|
|
|
|
<cfset var detail = listAppend(#ARGUMENTS.ex.message#,#ARGUMENTS.ex.detail#,": " )/>
|
|
<cfset var title = structKeyExists(ARGUMENTS,"title") ? #ARGUMENTS.title# : #ARGUMENTS.ex.message# />
|
|
|
|
<cfreturn formatMessage(detail, title) />
|
|
</cffunction>
|
|
|
|
<cffunction name="formatBadRequestError"
|
|
access="public"
|
|
returntype="any"
|
|
output="true"
|
|
hint="formats Bad Request Exception">
|
|
|
|
<cfargument name="ex" type="struct" required="true" />
|
|
|
|
<cfset var detail=listAppend(#ARGUMENTS.ex.message#,#ARGUMENTS.ex.detail#,":" )/>
|
|
|
|
<cfreturn {type="about:blank", title="Bad Request", detail="#detail#"}/>
|
|
</cffunction>
|
|
|
|
<!---<cffunction name="wrapResultSet"
|
|
access="public"
|
|
returntype="any"
|
|
output="true"
|
|
hint="format resultset as json">
|
|
|
|
<cfargument name="qRead" type="query" required="true" />
|
|
<cfargument name="titleMap" type="struct" required="true" />
|
|
<cfargument name="startrow" type="integer" required="false" default="1" />
|
|
|
|
<cfset var resultSet=[]/>
|
|
<cfloop query=#ARGUMENTS.qRead# startRow=#ARGUMENTS.startrow#>
|
|
<cfset var rec={}/>
|
|
|
|
<cfset appendRecord(rec, "", ARGUMENTS.titleMap, ARGUMENTS.qRead, snake2camel)/>
|
|
<cfset arrayAppend(resultSet, rec)/>
|
|
</cfloop>
|
|
|
|
<cfset var out={
|
|
pageSize=
|
|
}/>
|
|
|
|
|
|
<cfcontent type="application/json"/>
|
|
<cfoutput>{
|
|
"pageSize":#pageSize#,
|
|
"page":#page#,
|
|
"orderBy":"#orderBy#",
|
|
"size":"#arrayLen(resultSet)#",
|
|
"total":"#qTotal.cnt#",
|
|
"results":#serializeJson(resultSet)#,
|
|
"queryDurationMs":#queryDurationMs#, <cfset runDurationMs=getTickCount()-request.startTickCount/>
|
|
"runDurationMs":#runDurationMs#
|
|
}</cfoutput>--->
|
|
<cffunction name="validateField"
|
|
access="public"
|
|
returntype="any"
|
|
output="false"
|
|
hint="validate field of a structure, if exisits">
|
|
|
|
<cfargument name="struct" type="struct" required=true/>
|
|
<cfargument name="name" required=true/>
|
|
<cfargument name="type" default="string"/>
|
|
<cfargument name="required" type="boolean" default="false"/>
|
|
|
|
<cfif structKeyExists(ARGUMENTS.struct, ARGUMENTS.name)>
|
|
<cfif NOT isValid(ARGUMENTS.type, ARGUMENTS.struct[ARGUMENTS.name])>
|
|
<cfthrow type="invalidParamValue" message="Parameter validation failed" detail="Supplied value of '#ARGUMENTS.name#' is not a valid #ARGUMENTS.type#"/>
|
|
</cfif>
|
|
<cfelse>
|
|
<cfif ARGUMENTS.required>
|
|
<cfthrow type="invalidParamValue" message="Required field not supplied" detail="Required field #ARGUMENTS.name# not found"/>
|
|
</cfif>
|
|
</cfif>
|
|
</cffunction>
|
|
|
|
<!--- returns false if the key does not exist and throws exception on invalid format--->
|
|
<cffunction name="keyExistsAndValid" access="public" returntype="boolean">
|
|
<cfargument name="struct" type="struct" required=true/>
|
|
<cfargument name="key" type="string" required=true/>
|
|
<cfargument name="type" type="string" required=false/>
|
|
<cfargument name="maxLength" type="numeric" required=false hint="Checked for strings only, negative value means no limit"/>
|
|
|
|
<cfif structKeyExists(arguments.struct, arguments.key)>
|
|
<cfif structKeyExists(arguments, "type")>
|
|
<cfset var x=arguments.struct[arguments.key]/>
|
|
<cfif isValid(arguments.type, x)>
|
|
<cfif lCase(arguments.type) EQ "string">
|
|
<cfif structKeyExists(arguments, "maxLength")>
|
|
<cfif ARGUMENTS.maxLength GE 0>
|
|
<cfif len(x) GT #arguments.maxLength#>
|
|
<cfthrow type="invalidParamValue" message="Invalid parameter value (string too long)" detail="Parameter #arguments.key# length #len(x)# is greater than #arguments.maxLength#" errorcode=400/>
|
|
</cfif>
|
|
</cfif>
|
|
</cfif>
|
|
</cfif>
|
|
<cfelse>
|
|
<cfthrow type="invalidParamValue" message="Invalid parameter value" detail="Parameter #arguments.key# is not a valid #arguments.type#" errorcode=400/>
|
|
</cfif>
|
|
</cfif>
|
|
<cfreturn true/>
|
|
<cfelse>
|
|
<cfreturn false/>
|
|
</cfif>
|
|
</cffunction>
|
|
|
|
<!--- returns false if the key does not exist or has invalid format--->
|
|
<cffunction name="keyExistsAndValidGraceful" access="public" returntype="boolean">
|
|
<cfargument name="struct" type="struct" required=true/>
|
|
<cfargument name="key" type="string" required=true/>
|
|
<cfargument name="type" type="string" required=false/>
|
|
<cfargument name="maxLength" type="numeric" required=false default="-1" hint="Checked for strings only"/>
|
|
|
|
<cftry>
|
|
<cfreturn keyExistsAndValid(ARGUMENTS.struct, ARGUMENTS.key, ARGUMENTS.type, ARGUMENTS.maxLength)/>
|
|
<cfcatch type="ANY"><!---<cfrethrow/>---></cfcatch>
|
|
</cftry>
|
|
<cfreturn false/>
|
|
</cffunction>
|
|
|
|
|
|
<cffunction name="parseQsToNVArray"
|
|
returntype="array"
|
|
output="false"
|
|
hint="Parse query string. Returns an array of name-value pairs">
|
|
<cfargument name="qs" type="string" default=#CGI.QUERY_STRING#/>
|
|
<cfreturn
|
|
arrayMap(
|
|
CreateObject("java", "org.apache.http.client.utils.URLEncodedUtils")
|
|
.parse(
|
|
CreateObject("java", "java.net.URI").init("http://localhost?#arguments.qs#"),
|
|
'UTF-8'
|
|
),
|
|
function(v) {
|
|
return {"name"=v.getName(),"value"=v.getValue()}
|
|
}
|
|
)
|
|
/>
|
|
</cffunction>
|
|
|
|
<cffunction name="parseQs"
|
|
returntype="struct"
|
|
output="false"
|
|
hint="Parse query string. Returns a map of arrays instead of lists to prevent delimiter problem">
|
|
<cfargument name="qs" type="string" default=#CGI.QUERY_STRING#/>
|
|
<cfreturn
|
|
arrayReduce(
|
|
CreateObject("java", "org.apache.http.client.utils.URLEncodedUtils")
|
|
.parse(
|
|
CreateObject("java", "java.net.URI").init("http://localhost?#arguments.qs#"),
|
|
'UTF-8'
|
|
),
|
|
function(struct, nvPair) {
|
|
if (structKeyExists(struct, nvPair.getName())) {
|
|
arrayAppend(struct[nvPair.getName()],nvPair.getValue())
|
|
} else {
|
|
structInsert(struct, nvPair.getName(), [nvPair.getValue()])
|
|
}
|
|
return struct;
|
|
},
|
|
{}
|
|
)
|
|
/>
|
|
</cffunction>
|
|
|
|
</cfcomponent> |