Hi,
Hereby a sample of an endpoint as you can use it for BOA.
Below are some functions that can be used to start a customer multi tab form.
BOA will call the customers function with the following parameters:
To build an empty grid:
/customers/grid
To fill the grid after entering a search key
/customers?key=abc
Open a grid and fill with data and headers
/customers?key=abc&headers=1
To get all the tabpages with the titles and endpoints
/customers/form
To get all the info for an id, number of the tabpage, and all the labels to display
/customers/id?tab=1&labels=1
For this first test I added the DBF files you will need:
klant with 5000 customers
klantnum.ntx with index on nummer
klantnaa.ntx with index on naam
postnrs with the zipcodes, and with the indexes
postnrs.ntx index on newpost
poststad index on upper(plaats+postnr)
landcode with the countrycodes and indexes.
landcode.ntx with index on code
landcod1.ntx with index on landcode
In the customers function, there is a call to:
APIHEADERS: this will create the headers for the grid
fCustpages(cTabpage,lLabels,@aLabels) which will create the content for the tabpage.
APIblock('klant',cFields) which return the data for the row. cFields is created as a codeblock, and is evaluated.
I also added the following:
webfileopen() to open the files. I don't know how to specify the path.
allfileopen() to check if all files are correctly openend.
allfileclose(cFileopen) closes all files
I also added the source code for sendjson(), so you can see what this does. This also uses convertJSONtoCHAR to get the json object in a string.
function rest_Customers(cCommand,aPara,cDossier,cTaal,cUser,cCodeVert,nCode) ************************************************************************* Local aVarList, aVars:={}, cZoek:="" , nNummer := 0 local aJSON:={} , x:=0, rec, nLenZoek:=0 , index:=0 Local cFileopen:=webfileopen("klant") + webfileopen("postnrs")+ webfileopen("landcode") Local oClient:=ThreadObject(), bFilter:={|| .T. } Local oRec , cVar , nPos , oSubRec Local cContent := "", cContentType := "" , oJson Local nFields := 0 , lCorrectie := .F. Local cField := "" , uValue := nil , nFieldpos , cType Local aUnknown := {} , cIndexNr , over_arr := {} , uHeaders Local lHeaders := .F. , aHeaders := {} , aOptions := {} Local uVar :="" , lOnlyHeaders := .F. , lLabels := .F. Local amaandArray , nShowmaand := 1 , oOnclick , ctabPage := "" Static cFields := "" , alabels := {} , cTab := "x" select klant aVarListgetVar(nil,VAR_QUERY) IF Valtype(aVarList) == 'A' FOR x := 1 TO Len(aVarList) AAdd(aVars,aVarList[x]) NEXT ENDIF if aPara[1] == "v1.0" x := 0 if !empty(aVars) cFields := DC_HtmlGetVar(aVars, 'fields', .T.) cVar := DC_HtmlGetVar(aVars, 'key', .T.) lHeaders := if(var2char(DC_HtmlGetVar(aVars, 'headers', .T.))="1",.T.,.F.) lLabels := if(var2char(DC_HtmlGetVar(aVars, 'labels', .T.))="1",.T.,.F.) cIndexNr := DC_HtmlGetVar(aVars, 'index', .T.) // you can create endpoints which defines the ctive index. cTabPage := DC_HtmlGetVar(aVars, 'tab', .T.) // which tab-page is needed. endif if cTab <> cTabpage .or. valtype(cFields) = "U" cFields := "" asize(aLabels,0) cTab := cTabpage endif if len(aPara)>2 .And. !empty(aPara[3]) uVar := aPara[3] if upper(uVar) == "GRID" // start for the grid. BOA will add this to the request. So you know you don't need to return data. lOnlyHeaders := .t. elseif upper(uVar) == "FORM" // start a multi tab form. BOA will the form structure, so you can send the captions and number of pages. // each tab is a form, and each form has his own endpoint. cTabpage := "0" else nNummer := val(uVar) endif else if !empty(cVar) cZoek:=ConvUtfToCp(upper(cVar)) endif endif if cTabpage == "0" // send the info for all the tab-pages of the multi tab form. recnew() rec:title := "Address" rec:tab := '1' rec:endpoint := '/customers/${id}?tab=1' rec:pagetype := "form" aadd(aheaders,rec) recnew() rec:title := "Administration" rec:tab := '2' rec:endpoint := '/customers/${id}?tab=2' rec:pagetype := "form" aadd(aheaders,rec) recnew() rec:title := "Extra" rec:tab := '3' rec:endpoint := '/customers/${id}?tab=3' rec:pagetype := "form" aadd(aheaders,rec) recnew() rec:title := "History" rec:tab := '4' rec:endpoint := '/customers/${id}?tab=4' rec:pagetype := "form" aadd(aheaders,rec) recnew() rec:title := "Bill" rec:tab := '5' rec:endpoint := '/customers/${id}?tab=5' rec:pagetype := "form" aadd(aheaders,rec) recnew() rec:title := "Documents" rec:tab := '6' rec:endpoint := '/customers/${id}?tab=6' rec:pagetype := "form" aadd(aheaders,rec) recnew() rec:title := "Turnover" rec:tab := '7' rec:endpoint := '/customers/${id}?tab=7' rec:pagetype := "form" aadd(aheaders,rec) recnew() rec:title := "Data" rec:tab := '8' rec:endpoint := '/customers/${id}?tab=8' rec:pagetype := "form" aadd(aheaders,rec) recnew() rec:file := "klant" rec:titlefield := 'naam' rec:tabpages := aHeaders allfileclose(cFileopen) return sendjson(rec) endif IF lOnlyHeaders // Send only headers, so this is done. APIheaders('klant',@aHeaders,cTaal) recnew() rec:file := "klant" rec:headers := aHeaders allfileclose(cFileopen) return sendjson(rec) endif do while .T. Do CASE case cCommand ="DELETE" if !empty(nNummer) klant->(dbsetorder(1)) klant->(dbseek(nNummer)) if !klant->(eof()) .and. klant->(locked()) // Here you can delete a record. klant->(dbrunlock()) endif endif case cCommand == "GET" if empty(nNummer) .and. empty(cFields) .and. lHeaders APIheaders('klant',@aHeaders,cTaal) // with API headers you can create the headers for different requests. endif if !empty(nNummer) if empty(cFields) .and. cTabPage <> "99" // if tabpage == 99 , only return the fields for the grid. if lLabels recnew() rec:display := "" rec:fieldname := "id" rec:length := 6 rec:inputtype := 'hidden' aadd(aLabels,rec) endif cFields := fCustpages(cTabpage,lLabels,@aLabels) endif if nNummer > 0 klant->(dbsetorder(1)) klant->(dbseek(nNummer)) if !klant->(eof()) oRec := APIblock('klant',cFields) aadd(aJson,oRec) else recnew() rec:type := "ID does not exist" rec:id := nNummer orecnew() oRec:file := "klant" oRec:error := rec allfileclose(cFileopen) return sendjson(oRec) endif endif elseif !empty(cZoek) x:=0 nNummer := 0 cZoek:=upper(alltrim(strip(cZoek))) if left(cZoek,1)=="=" nNummer := val(substr(cZoek,2)) klant->(dbsetorder(1)) klant->(dbseek(nNummer)) oRec := APIblock('klant',cFields) aadd(aJson,oRec) else nLenZoek:=len(cZoek) klant->(dbsetorder(2)) klant->(dbseek(cZoek,.T.)) Do while upper(left(strip(klant->naam),nLenZoek)) = cZoek x++ oRec := APIblock('klant',cFields) aadd(aJson,oRec) klant->(dbskip(1)) enddo endif if lHeaders .and. empty(aHeaders) .and. x > 0 APIheaders('klant',@aHeaders,cTaal) endif endif case cCommand == "POST" // create the record, then change command to PUT for update klant->(dbsetorder(1)) set deleted off klant->(dbgobottom()) nNummer := klant->nummer+1 // get next number for the customer if klant->(append()) klant->nummer := nNummer klant->(dbrunlock()) endif cCommand := "PUT" loop case cCommand == "PUT" // update after creation cContent := oClientcontent cContentType := oClientcontenttype if lower(cContentType) = "application/json" oJson := json():new(cContent) if empty(nNummer) .and. oJson:hasvar("id") nNummer := var2num(oJson:id) endif nFields := len(oJson:_aJson) // oJson:_aJson returns an array with the fieldnames and the data klant->(dbsetorder(1)) klant->(dbseek(nNummer)) if klant->(eof()) recnew() rec:type := "ID does not exist" rec:id := nNummer orecnew() oRec:file := "klant" oRec:error := rec allfileclose(cFileopen) return sendjson(oRec) endif if klant->(locked()) for x = 1 to nFields cField := oJson:_aJson[x][1] uValue := oJson:_aJson[x][2] if (nFieldpos := klant->(fieldpos(cField))) > 0 cType := upper( valtype(klant->(fieldget(nFieldpos)))) if cType $ "CM" // Character or memo if valtype(uValue) = "L" uValue:=if(uValue,"J","N") elseif empty(uValue) uValue := "" endif klant->(fieldput(nFieldpos, uValue)) elseif cType == "N" // Numeric if empty(uValue) uValue := 0 endif klant->(fieldput(nFieldpos, var2num(uValue))) elseif cType == "D" .and. !empty(uValue) // Date if empty(uValue) uvalue := " / / " endif klant->(fieldput(nFieldpos, ctod(uValue))) elseif cType == "L" // Logical if empty(uValue) uValue := .f. endif if valtype(uValue) == "L" klant->(fieldput(nFieldpos, uValue)) else klant->(fieldput(nFieldpos, if(upper(uValue)$"0NFALSE",.F.,.T.))) endif endif elseif upper(left(cField,2)) <> "ID" aadd(aUnknown,cField) // cUnknown += if(empty(cUnknown),""," | ") + cField endif next klant->(dbrunlock()) endif oRec := APIblock('klant',cFields) aadd(aJson,oRec) endif endcase exit enddo endif // end Version 1.0 recnew() rec:file := "klant" if lHeaders .and. !empty(aheaders) rec:headers := aHeaders endif if lLabels .and. !empty(aLabels) rec:labels := aLabels endif if nNummer >= 0 rec:data := aJson endif allfileclose(cFileopen) return sendjson(rec) function fCustpages(cTabPage,lLabels,aLabels) *************************************** Local rec , cFields, oSubRec , aOptions := {} do case case cTabpage = "1" if lLabels recnew() rec:display := "Name / Company:" rec:tooltip := "Name of the customer." rec:fieldname := 'naam' rec:length := 40 rec:inputtype := 'edit' rec:newline := .T. rec:labelwidth := 2 rec:fieldwidth := 4 aadd(aLabels,rec) recnew() rec:display := "Contact:" rec:tooltip := "Name of contact person." rec:fieldname := 'naam2' rec:length := 40 rec:inputtype := 'edit' rec:newline := .F. rec:labelwidth := 2 rec:fieldwidth := 4 aadd(aLabels,rec) recnew() rec:display := "Address:" rec:tooltip := "Address:" rec:fieldname := 'adres' rec:length := 40 rec:inputtype := 'edit' rec:newline := .T. rec:labelwidth := 2 rec:fieldwidth := 4 aadd(aLabels,rec) endif cFields := "naam,naam2,adres" if lLabels recnew() rec:display := "Address 2:" rec:tooltip := "Address 2:" rec:fieldname := 'adres2' rec:length := 40 rec:inputtype := 'edit' rec:newline := .F. rec:labelwidth := 2 rec:fieldwidth := 4 aadd(aLabels,rec) recnew() rec:display := '' rec:fieldname := 'idpostnr' rec:length := 10 rec:inputtype := 'hidden' oSubRec := json():new() oSubrec:endpoint := "/files/postnrs" oSubrec:file := "postnrs" oSubrec:fieldname := "id" rec:data := oSubRec aadd(aLabels,rec) recnew() rec:display := '' rec:fieldname := 'newpost' rec:length := 10 rec:inputtype := 'hidden' oSubRec := json():new() oSubrec:endpoint := "/postnrs" oSubrec:file := "postnrs" oSubrec:fieldname := "newpost" rec:data := oSubRec aadd(aLabels,rec) recnew() rec:display := "Zipcode:" rec:tooltip := "Postal code." rec:fieldname := 'postnr' rec:length := 8 rec:inputtype := 'search' rec:newline := .T. rec:labelwidth := 2 rec:fieldwidth := 2 rec:grid := "postnrs" oSubRec := json():new() oSubrec:endpoint := "/postnrs" oSubrec:file := "postnrs" oSubrec:fieldname := "postnr" oSubrec:buttons := "select,add,edit,exit" rec:data := oSubRec aadd(aLabels,rec) recnew() rec:display := '' rec:fieldname := 'idlandcode' rec:length := 10 rec:inputtype := 'hidden' oSubRec := json():new() oSubrec:endpoint := "/landcodes" oSubrec:file := "/landcodes" oSubrec:fieldname := "id" rec:data := oSubRec aadd(aLabels,rec) recnew() rec:display := "" //fWebMessage(cTaal,60) rec:tooltip := "Country codes" rec:fieldname := 'land' rec:length := 3 rec:inputtype := 'dropdown' rec:newline := .F. rec:labelwidth := 0 rec:fieldwidth := 2 rec:grid := "landcode" oSubRec := json():new() oSubrec:endpoint := "/landcodes" oSubrec:file := "landcodes" oSubrec:fieldname := "code" rec:data := oSubRec aadd(aLabels,rec) recnew() rec:display := "Hometown:" rec:tooltip := "" rec:fieldname := 'postnrs_plaats' rec:length := 30 rec:inputtype := 'noedit' rec:newline := .F. rec:labelwidth := 2 rec:fieldwidth := 4 oSubRec := json():new() oSubrec:endpoint := "/postnrs" oSubrec:file := "postnrs" oSubrec:fieldname := "plaats" rec:data := oSubRec aadd(aLabels,rec) recnew() rec:display := "Phone:" rec:tooltip := "Telephone" rec:fieldname := 'telefoon' rec:length := 16 rec:inputtype := 'phone' rec:newline := .T. rec:labelwidth := 2 rec:fieldwidth := 4 aadd(aLabels,rec) recnew() rec:display := "Fax." rec:tooltip := "Fax." rec:fieldname := 'telefoon2' rec:length := 16 rec:inputtype := 'phone' rec:newline := .F. rec:labelwidth := 2 rec:fieldwidth := 4 aadd(aLabels,rec) recnew() rec:display := "Mobile:" rec:tooltip := "Mobile number" rec:fieldname := 'gsm' rec:length := 16 rec:inputtype := 'phone' rec:newline := .T. rec:labelwidth := 2 rec:fieldwidth := 4 aadd(aLabels,rec) recnew() rec:display := "E-Mail:" rec:tooltip := "E-Mail address." rec:fieldname := 'email' rec:length := 40 rec:inputtype := 'email' rec:newline := .T. rec:labelwidth := 2 rec:fieldwidth := 10 aadd(aLabels,rec) recnew() rec:display := "VAT:" rec:tooltip := "" rec:fieldname := 'btw_kode' rec:length := 3 rec:inputtype := 'combobox' rec:newline := .T. rec:labelwidth := 2 rec:fieldwidth := 2 oSubRec := json():new() oSubrec:option := "Individual" oSubrec:value := "P" aadd(aOptions,oSubrec) oSubRec := json():new() oSubrec:option := "Company" oSubrec:value := "B" aadd(aOptions,oSubrec) oSubRec := json():new() oSubrec:option := "School" oSubrec:value := "V" aadd(aOptions,oSubrec) oSubRec := json():new() oSubrec:option := "No VAT" oSubrec:value := "N" aadd(aOptions,oSubrec) rec:options := aOptions aadd(aLabels,rec) recnew() rec:display := "" rec:fieldname := 'landcode' rec:length := 3 rec:inputtype := 'noedit' rec:newline := .F. rec:labelwidth := 0 rec:fieldwidth := 2 oSubRec := json():new() oSubrec:endpoint := "/landcodes" oSubrec:file := "landcodes" oSubrec:fieldname := "landcode" rec:data := oSubRec aadd(aLabels,rec) recnew() rec:display := "VAT Nr." rec:tooltip := "VAT number of the customer." rec:fieldname := 'btw_nr' rec:length := 16 rec:inputtype := 'edit' rec:newline := .F. rec:labelwidth := 2 rec:fieldwidth := 4 aadd(aLabels,rec) endif cFields += ",adres2,idpostnrs,postnr,newpost,idlandcode,land,postnrs->plaats,telefoon,telefoon2,gsm" cFields += ",email,btw_kode,landcode,btw_nr" /* tabpage 2 to 9 not converted yet case cTabPage = "2" .... endcase return cFields function APIblock(cAlias,cFields,lLevel) // creates the json for the data. ************************************* Local oRecord := json():new() , aFields := {} , aSubFields := {} Local cBlock := '' , x , nField := 2 , cFieldname , y , cField // , bBlock := "" Local nRecord := 0 , cZoek := "" , nParent := 0 , cType := "" , nFldPos := 0 Local bBlock := "" default lLevel := .F. cAlias := upper(cAlias) // according to the opened file, actions can be done. Some of them can be avoided by using set relation while opening the files do case case "newpost" $ cFields .and. !cAlias== "POSTNRS" postnrs->(dbseek( (cAlias)->newpost)) case "land" $ cFields .and. !cAlias== "LANDCODE" landcode->(dbseek( (cAlias)->land)) endcase // a codeblock with all the data has to be created. This is evaluated and returns the result. cBlock := '{|rec| rec:id := alltrim(str(recno())),' if empty(cFields) cFields := (cAlias)->(fieldname(1)) do while !empty( cFieldname := (cAlias)->(fieldname(nField)) ) cFields += ','+cFieldname nField ++ enddo endif aFields := dc_tokenarray(cFields,",") for x = 1 to len(aFields) cField := aFields[x] do case // In some cases the has to be an json object in the data. This is the system to have relations. // Sample in customers there is a link to the zipcodes. // Sample in a stock product there is a relation to productcategories // Below some samples to check and to create the data case "newpost" == cField .and. !cAlias == "POSTNRS" postnrs->(dbseek( (cAlias)->newpost)) cBlock += 'rec= apiblock("postnrs","postnr,plaats,newpost",.t.),' case "land" == cField .and. !cAlias == "LANDCODE" landcode->(dbseek( (cAlias)->land)) cBlock += 'rec= apiblock("landcode","code,land,landcode",.t.),' endcase endif if (nFldPos:=(cAlias)->(fieldpos( cField ))) > 0 .or. "->"$cField .or. "+"$cField cField := strtran(cField,"->","_") cField := strtran(cField,"+","_") cField := strtran(cField,"-","_") cField := strtran(cField,"(recno())","id") if nFldPos > 0 cType := (cAlias)->(fieldinfo(nFldPos , FLD_TYPE)) if cType == "M" cBlock += 'rec=memoeon('+aFields[x]+'),' elseif cType == "D" cBlock += 'rec=dtoc('+aFields[x]+'),' else cBlock += 'rec='+aFields[x]+',' endif else cBlock += 'rec='+aFields[x]+',' endif endif next cBlock += 'rec}' bBlock := &(cBlock) endif return (cAlias)->(eval(bBlock,oRecord )) function APIHeaders(cAlias,aHeaders,cTaal) // create headers for a grid. ************************************ Local oRecord := json():new() Local cBlock := '' , x , bBlock , nField := 2 , cFieldname cAlias := upper(cAlias) if cAlias == "KLANT" // you can create headers as below, or you could read some klant.headers.language.txt with the json in it. oRecord:title := 'Customer name:' oRecord:data := 'naam' oRecord:type := 'string' oRecord:width := '25%' aadd(aHeaders , oRecord) oRecord := json():new() oRecord:title := 'Address:' oRecord:data := 'adres' oRecord:type := 'string' oRecord:width := '20%' aadd(aHeaders , oRecord) oRecord := json():new() oRecord:title := 'City' oRecord:data := 'postnrs->plaats' oRecord:type := 'string' oRecord:width := '20%' aadd(aHeaders , oRecord) oRecord := json():new() oRecord:title := 'Cell phone:' oRecord:data := 'gsm' oRecord:type := 'string' oRecord:width := '15%' aadd(aHeaders , oRecord) oRecord := json():new() oRecord:title := 'Email' oRecord:data := 'email' oRecord:type := 'string' oRecord:width := '20%' aadd(aHeaders , oRecord) elseif cAlias == "LEVER" oRecord:title := 'Supplier:' oRecord:data := 'naam' oRecord:type := 'string' oRecord:width := '25%' aadd(aHeaders , oRecord) oRecord := json():new() oRecord:title := 'Address:' oRecord:data := 'adres' oRecord:type := 'string' oRecord:width := '20%' aadd(aHeaders , oRecord) oRecord := json():new() oRecord:title := 'City:' oRecord:data := 'postnrs->plaats' oRecord:type := 'string' oRecord:width := '15%' aadd(aHeaders , oRecord) oRecord := json():new() oRecord:title := 'Cell phone:' oRecord:data := 'gsm' oRecord:type := 'string' oRecord:width := '15%' aadd(aHeaders , oRecord) oRecord := json():new() oRecord:title := 'Email:' oRecord:data := 'email' oRecord:type := 'string' oRecord:width := '25%' aadd(aHeaders , oRecord) elseif cAlias == "STOCK" .... else endif return .t. Function webfileopen(cFile) ************************************ local nDelta := 0 cFile := upper(cFile) // sample for the opening of the files and corresponding indexes. if cFile == "KLANT" do while nDelta < 10 use klant alias klant new if .not. neterr() set index to klantnum,klantnaa return cFile+"/" else nDelta ++ endif sleep(nDelta*3) enddo return 'error/' endif if cFile == "POSTNRS" do while nDelta < 10 use postnrs alias postnrs new if .not. neterr() set index to postnrs, poststad return cFile+"/" else nDelta ++ endif sleep(nDelta*3) enddo return 'error/' endif if cFile == "LANDCODE" do while nDelta < 10 use landcode alias landcode new if .not. neterr() set index to landcode, landcod1 return cFile+"/" else nDelta ++ endif sleep(nDelta*3) enddo return 'error/' endif return "" function allfileopen(cFiles) ************************* if !"error" $ lower(cFiles) return .T. else allfileclose(cFiles) endif return .F. function allfileclose(cFiles) ********************** Local nPos do while (nPos:=at("/",cFiles)) > 0 close(substr(cFiles,1,nPos-1)) cFiles := substr(cFiles,nPos+1) enddo return .F. function SENDJSON(ocJSON,lFlat) // ************************** local nErr := 0 local o := threadobject() local cContent := if(valtype(ocJson)=="C",ocJson,convertJSONtoCHAR(ocJSON)) default lFlat := .F. ocontenttype := 'application/json' if len(cContent) < 10000 .or. lFlat oContentEncoding("identity") oCompressLevel = 0 ocontent := cContent else oContentEncoding( "deflate" ) ocontent := xbZCompress( cContent, 4, @nErr, .t. ) endif return .t.
The above is not a copy/paste solution, since it is dependant on the json parser you uses. It should give a enough information to understand how BOA works, to get an idea about the work you have to do, and if further interested to start with a test which you can do for free. You can test BOA without buying anything.
In attachment the files the above sample is refering to.
If any question, you can ask them.
Best regards,
Chris