#+TITLE: SAPI SDK
#+CATEGORY: sapi
#+AUTHOR: SAPI Squad
#+SETUPFILE: https://fniessen.github.io/org-html-themes/org/theme-readtheorg.setup
#+HTML_HEAD: #content{max-width:1800px;}
#+HTML_HEAD: p{max-width:800px;}
#+HTML_HEAD: li{max-width:800px;}
SDK provides a high level TypeScript/JavaScript API for searching
hotels, hotels' offers and rooms.
- Table of contents :noexport:
:PROPERTIES:
:TOC: :include siblings :depth 2 :ignore this
:END:
:CONTENTS:
- [[#internal][Internal]]
- [[#how-to-work-with-this-document][How to work with this document]]
- [[#automatic-publishing][Automatic publishing]]
- [[#flow-diagrams-a--vs-b-side-on-sapi-sdk-on-srp][Flow diagrams A- vs B-side on SAPI SDK on SRP]]
- [[#long-polling-flow][Long-polling flow]]
- [[#sapi-application-frontend-saf][SAPI Application Frontend (SAF)]]
- [[#glossary][Glossary]]
- [[#tutorials][Tutorials]]
- [[#getting-started][Getting Started]]
- [[#usage][Usage]]
- [[#api-reference][API Reference]]
- [[#sapi-client][SAPI client]]
- [[#search-method][search() method]]
- [[#rooms-method][rooms() method]]
:END:
- Internal :internal:
This section contains various sections on SAPI design and architecture.
** How to work with this document
See https://orgmode.org to learn about Org Mode markup language. It's
greatly [[https://karl-voit.at/2017/09/23/orgmode-as-markup-only/][superior to any other markup languages]] (Markdown included).
To edit it's better to use [[https://www.gnu.org/software/emacs/][Emacs]] (for macOS use either [[https://github.com/railwaycat/homebrew-emacsmacport/releases][emacs-mac-port]],
[[https://github.com/d12frosted/homebrew-emacs-plus#install][emacs-plus]] or from [[https://emacsformacosx.com][emacsformacosx.com]]) but you can also use a plugin
for VSCode, search for "Org Mode" on Marketplace, [[https://marketplace.visualstudio.com/items?itemName=tootone.org-mode][this one]] looks sane.
If you're new to Emacs and use macOS, it's recommended to start with
[[https://github.com/freetonik/castlemacs][castlemacs]], so install it using the below snippet:
#+begin_src sh
brew install ripgrep aspell gnutls
mv ~/.emacs.d ~/.emacs.d.bak
git clone https://github.com/freetonik/castlemacs ~/.emacs.d
#+end_src
It will enable regular shortcuts as in macOS, like Command-C for
copy, Command-O for opening a file and so on.
If you're a vim user, there's [[https://github.com/jceb/vim-orgmode][vim-orgmode]] plugin, but the better
option is to install [[https://github.com/emacs-evil/evil][Evil]] (Emacs VI Layer), which turns Emacs to be a
better vim but keeping all Emacs' goodies.
This table shows how section tags affect subtrees been
present in published documentation:
| | GitHub | Internal | Public |
|--------------+--------+----------+--------|
| =:noexport:= | ✅ | ❌ | ❌ |
| =:internal:= | ✅ | ✅ | ❌ |
| other tags | ✅ | ✅ | ✅ |
Then the file should be copied on S3:
To generate documentation, run
#+begin_src sh
make generate-docs # HTML
make generate-docs FORMAT=md # Markdown
#+end_src
To publish documentation online, run
#+begin_src sh
make upload-docs PREFIX=
#+end_src
so it's available online on [[http://fh-sapi-docs.s3-website-eu-west-1.amazonaws.com/][fh-sapi-docs.s3-website-eu-west-1.amazonaws.com]].
** Automatic publishing
This file will be automatically published on S3 making it available
live.
*** DONE Setup publishing via Github Actions
CLOSED: [2021-06-30 Wed 23:36]
It should publish README.org into html file in S3 bucket.
From =main= branch to =s3://fh-sapi-docs/index.html=
Fron PR branch to =s3://fh-sapi-docs/pr-NNN/index.html=
[[https://github.com/marketplace/actions/set-up-emacs][Set up Emacs]] action step looks useful in the process.
GitHub Actions publish documentation to =s3://fh-sapi-docs= the following prefixes:
- =/index.html= :: from =main= branch
- =/NN/index.html= :: from an open PR =#NN= to =main= branch
- =/vX.Y.Z/index.html= :: from a release =vX.Y.Z=
The bucket is publicly available on
[[http://fh-sapi-docs.s3-website-eu-west-1.amazonaws.com/][fh-sapi-docs.s3-website-eu-west-1.amazonaws.com]].
To publish manually run:
#+begin_src sh :results verbatim
make generate-docs FORMAT=html
make generate-docs FORMAT=md
make upload-docs PREFIX=foo/ # will publish to /foo/index.html
#+end_src
#+RESULTS:
: emacs --batch --load docs/publish.el --funcall publish-html
: emacs --batch --load docs/publish.el --funcall publish-md
: aws s3 cp target/docs/README.html s3://fh-sapi-docs/foo/index.html
: Completed 27.7 KiB/27.7 KiB (77.1 KiB/s) with 1 file(s) remaining upload: target/docs/README.html to s3://fh-sapi-docs/foo/index.html
*** TODO Bring in all CSS/JS dependencies locally
** Flow diagrams A- vs B-side on SAPI SDK on SRP
*** Example wrapper for results :noexport:
#+NAME: img_wrap :exports none
#+BEGIN_SRC sh :var data="" :var width="\textwidth" :results output
echo "#+ATTR_LATEX: :width $width"
echo "$data"
#+END_SRC
*** Flow A-side
#+BEGIN_SRC plantuml :file docs/assets/sapi-srp-flow-a.png :exports results
title Flow A side
"Page request"->App: bootstrapping application
activate App
App->"Search route":navigate to search route
activate "Search route"
"Search route"->Analytics:EVENT: page view
"Search route"->"Search View":render search view
activate "Search View"
deactivate "Search route"
group fetch configs
"Search View"->Algolia:fetch HSO/LOV/Currency
activate Algolia
Algolia->"Search View":response
deactivate Algolia
end
group fetch anchor
"Search View"->Algolia:fetch anchor
activate Algolia
Algolia->"Search View":anchor
deactivate Algolia
end
group fetch hotels
"Search View"->Algolia:fetch hotels
activate Algolia
Algolia->"Search View":hotels
deactivate Algolia
"Search View"->Analytics:EVENT: hotels received
end
group fetch offers
"Search View"->RAA:fetch offers
activate RAA
RAA->"Search View":offers
"Search View"->Analytics:EVENT: first offers
RAA->"Search View":offers
RAA->"Search View":all Offers
deactivate RAA
"Search View"->Analytics:EVENT: all offers
end
deactivate "Search View"
deactivate App
#+end_src
#+RESULTS:
:results:
#+ATTR_LATEX: :width 5cm
[[file:docs/assets/sapi-srp-flow-a.png]]
:end:
*** Flow B-side
#+begin_src plantuml :file docs/assets/sapi-srp-flow-b.png :exports results
title Flow B side
"Page request"->App:bootstrapping application
activate App
group sapi
App->SAPI:initialize SAPI
activate SAPI
group fetch configs
SAPI->Algolia:fetch HSO/LOV/Currency
activate Algolia
Algolia->SAPI:response
deactivate Algolia
end
SAPI->App:Sapi Client
deactivate SAPI
end
App->"Search route":navigate to search route
activate "Search route"
"Search route"->Analytics:EVENT: page view
"Search route"->"Search View":render search view
deactivate "Search route"
activate "Search View"
group fetch anchor
"Search View"->Algolia:fetch anchor
activate Algolia
Algolia->"Search View":anchor
deactivate Algolia
end
group fetch hotels
"Search View"->Algolia:fetch hotels
activate Algolia
Algolia->"Search View":hotels
deactivate Algolia
"Search View"->Analytics:EVENT: hotels received
end
group fetch offers
"Search View"->RAA:fetch offers
activate RAA
RAA->"Search View":offers
"Search View"->Analytics:EVENT: first offers
RAA->"Search View":offers
RAA->"Search View":all Offers
deactivate RAA
"Search View"->Analytics:EVENT: all offers
end
deactivate "Search View"
deactivate App
#+end_src
#+RESULTS:
[[file:docs/assets/sapi-srp-flow-b.png]]
*** Flow B-side old
#+begin_src plantuml :file docs/assets/sapi-srp-flow-b-old.png :exports results
title Flow B side old
"Page request"->App:bootstrapping application
activate App
group sapi
App->SAPI:initialize SAPI
activate SAPI
group fetch SAPI config
SAPI->Algolia:fetch SAPI config
activate Algolia
Algolia->SAPI:response
deactivate Algolia
end
group fetch configs
SAPI->Algolia:fetch HSO/LOV/Currency
activate Algolia
Algolia->SAPI:response
deactivate Algolia
end
SAPI->App:Sapi Client
deactivate SAPI
end
App->"Search route":navigate to search route
activate "Search route"
"Search route"->Analytics:EVENT: page view
"Search route"->"Search View":render search view
deactivate "Search route"
activate "Search View"
group fetch anchor
"Search View"->Algolia:fetch anchor
activate Algolia
Algolia->"Search View":anchor
deactivate Algolia
end
group fetch hotels
"Search View"->Algolia:fetch hotels
activate Algolia
Algolia->"Search View":hotels
deactivate Algolia
"Search View"->Analytics:EVENT: hotels received
end
group fetch offers
"Search View"->RAA:fetch offers
activate RAA
RAA->"Search View":offers
"Search View"->Analytics:EVENT: first offers
RAA->"Search View":offers
RAA->"Search View":all Offers
deactivate RAA
"Search View"->Analytics:EVENT: all offers
end
deactivate "Search View"
deactivate App
#+end_src
#+RESULTS:
[[file:docs/assets/sapi-srp-flow-b-old.png]]
** Long-polling flow
:PROPERTIES:
:CUSTOM_ID: long-polling
:END:
On <2021-07-06 Tue> Artem and Pavel had discussed the long-polling
flow that can happen during client-server communication which is asked
to implement in [[https://github.com/findhotel/sapi-be-challenge][sapi-be-challenge]] code challenge for candidates.
The below diagram represents the flow we expect from a candidate.
#+BEGIN_SRC plantuml :file docs/assets/sapi-long-polling.png :exports results
autonumber
title Long-polling flow
client -> server: new TCP connection
server -> client: connected
loop until complete
client -> server: query (HTTP request)
activate server
server -> sessions : Upsert session
sessions -> server : offers delivered so far
loop until newOffer[] not empty or complete
server -> cache : GetOffers
cache -> server : offer[]
RAA -> cache : offer[]
server -> server : build newOffer[] and "complete"
server -> sessions : newOffer[] delivered
group If not complete and a new session and the first iteration
server -> RAA : RetrieveOffers
end
group if newOffer[] is empty and not complete
server -> server : timeout
end
end
server -> client : newOffer[], complete
deactivate server
end
#+end_src
#+RESULTS:
[[file:docs/assets/sapi-long-polling.png]]
** SAPI Application Frontend (SAF)
:PROPERTIES:
:CUSTOM_ID: saf
:END:
SAPI Application Frontend (SAF) is a component that bundles the communication to
backend services and exposes results to be consumed from SAPI Client.
The diagram is actual as of <2021-07-19 Mon>.
#+begin_src dot :file docs/assets/saf.svg :exports results
digraph
SAPI_SAF {
newrank=true
colorscheme=oranges9
bgcolor=1
node [shape=box; colorscheme=oranges9; color=9]
edge [colorscheme=oranges9; color=8]
subgraph cluster_Algolia {
label="Algolia"
node [shape=cylinder]
AlgoliaCurrIndex [label="curr_v1"]
AlgoliaHSOIndex [label="hso_v1"]
AlgoliaLovIndex [label="lov_v2"]
AlgoliaProfileIndex [label="profile_v1"]
}
subgraph cluster_RAA {
label="RAA"
RAARoomsEndpoint [label="RAA/rooms"]
}
subgraph cluster_Content {
label="Content"
ContentRoomsS3 [label="Rooms data\nS3"; shape=folder]
}
subgraph cluster_SAF {
label="SAPI App Frontend (SAF)"
subgraph cluster_WorkersKV {
label="Cloudflare Workers KV"
node [shape=cylinder]
KVHotelCollection [label="HOTEL"]
}
subgraph cluster_Workers {
label="Cloudflare Workers"
subgraph cluster_Handlers {
style=invis
getRoom [label="getRooms(id, provider?)"]
handleHotel [label="handleHotel(id[])"]
KVHotelCollection->handleHotel
}
subgraph cluster_Endpoints {
style=invis
SapiInitEndpoint [label="SAF/sapi-init"]
OffersRoomsEndpoint [label="SAF/offers-rooms"]
RoomEndpoint [label="SAF/room"]
HotelEndpoint [label="SAF/hotel"]
HotelsEndpoint [label="SAF/hotels"]
handleHotel->HotelEndpoint
handleHotel->HotelsEndpoint
}
}
}
subgraph cluster_SAPI_Client {
label="SAPI Client (SDK)"
SAPIRooms [label="SAPI.rooms()"]
SAPIConstructor [label="SAPI.constructor()"]
}
HotelsEndpoint->HotelExplorer
ContentRoomsS3 -> getRoom
getRoom -> RoomEndpoint
getRoom -> OffersRoomsEndpoint
RAARoomsEndpoint -> OffersRoomsEndpoint
AlgoliaLovIndex -> SapiInitEndpoint
AlgoliaCurrIndex -> SapiInitEndpoint
AlgoliaProfileIndex -> SapiInitEndpoint
AlgoliaHSOIndex -> SapiInitEndpoint
SapiInitEndpoint -> SAPIConstructor
OffersRoomsEndpoint -> SAPIRooms
{rank=same; getRoom; handleHotel;}
{rank=same; SapiInitEndpoint; OffersRoomsEndpoint; RoomEndpoint; HotelEndpoint; HotelsEndpoint;}
{rank=min; RAARoomsEndpoint; ContentRoomsS3;
AlgoliaCurrIndex; AlgoliaHSOIndex; AlgoliaLovIndex; AlgoliaProfileIndex
}
#{rank=max; HotelExplorer; SAPIConstructor; SAPIRooms; }
edge [style=invis]
AlgoliaCurrIndex -> KVHotelCollection
ContentRoomsS3 -> KVHotelCollection
RAARoomsEndpoint -> KVHotelCollection
#handleHotel -> getRoom
#KVHotelCollection -> getRoom
#HotelExplorer->SAPIRooms
}
#+end_src
#+RESULTS:
[[file:docs/assets/saf.svg]]
- Glossary
:PROPERTIES:
:CUSTOM_ID: glossary
:END:
Glossary contains terms that are used throuout the documentation.
- HotelId :: FindHotel hotel id
- Itinerary :: tuple of =hotelId=, =checkIn=, =checkOut=
- Tutorials
:PROPERTIES:
:CUSTOM_ID: tutorials
:END:
** Getting Started
:PROPERTIES:
:CUSTOM_ID: getting-started
:END:
First, install SAPI SDK via the [[https://www.npmjs.com/get-npm][npm]] package manager:
#+begin_src sh
npm install @findhotel/sapi
#+end_src
Then, import SAPI into your project:
#+begin_src js
import sapi from '@findhotel/sapi'
#+end_src
Create SAPI client:
#+begin_src js
const clientId = 'client-id'
const clientKey = 'client-key'
const options = {
anonymousId: 'fd9dbb5f-b337-4dd7-b640-1f177d1d3caa',
language: 'en',
currency: 'USD',
userCountry: 'US'
}
const sapiClient = await sapi(clientId, clientKey, options)
#+end_src
Now SAPI client is ready to be used in your application.
For full documentation and supported options check [[#sapi-client][client api]].
** Usage
:PROPERTIES:
:CUSTOM_ID: usage
:END:
*** Hotels search
:PROPERTIES:
:CUSTOM_ID: tutorial-hotels-search
:END:
Search for the hotels and hotels' offers:
#+begin_src js
const searchParameters = {
placeId: '47319',
checkIn: '2021-10-10',
checkOut: '2021-10-11',
rooms: '2'
}
const callbacks = {
onStart: (response) => {
console.log('Search started', response)
},
onAnchorReceived: (response) => {
console.log('Anchor received', response)
},
onHotelsReceived: (response) => {
console.log('Hotels received', response)
},
onOffersReceived: (response) => {
console.log('Offers received', response)
},
onComplete: (response) => {
console.log('Search completed', response)
}
}
const search = await sapiClient.search(searchParameters, callbacks)
#+end_src
For full documentation, check [[#search-method][search method api]].
*** Get rooms
:PROPERTIES:
:CUSTOM_ID: tutorial-get-rooms
:END:
Get rooms and rooms' offers:
#+begin_src js
const rooms = await sapiClient.rooms({
hotelId: '47319',
checkIn: '2021-10-10',
checkOut: '2021-10-11',
rooms: '2'
})
#+end_src
For full documentation, check [[#rooms-method][rooms method api]].
- API Reference
:PROPERTIES:
:CUSTOM_ID: api-reference
:END:
** SAPI client :Constructor:
:PROPERTIES:
:CUSTOM_ID: sapi-client
:END:
Create SAPI client:
#+begin_src js
const clientId = 'client-id'
const clientKey = 'client-key'
const options = {
anonymousId: 'fd9dbb5f-b337-4dd7-b640-1f177d1d3caa',
language: 'en',
currency: 'USD',
userCountry: 'US'
}
const sapiClient = await sapi(clientId, clientKey, options)
#+end_src
*** Supported options
:PROPERTIES:
:CUSTOM_ID: client-options
:END:
| name | required | type | default | description | example |
|------------------------+----------+--------------------------+---------+---------------------------------------------------------------------------------------------+----------------------------------------|
| =anonymousId= | yes | =string= | | Unique ID identifying users | =2d360284-577b-4a53-8b91-68f72b9227fa= |
| =language= | yes | =string= | =en= | 2-char language code | =en= |
| =currency= | yes | =string= | =USD= | 3-char uppercased ISO currency code | =USD= |
| =userCountry= | yes | =string= | =US= | 2-char uppercased ISO country code | =US= |
| =deviceCategory= | yes | =string= | | =desktop= or =mobile= | =desktop= |
| =includeLocalTaxes= | no | =boolean= | | Include or not local taxes based in the displayed price | =false= |
| =includeTaxes= | no | =boolean= | | Include or not taxes based in the displayed price | =false= |
| =pageSize= | no | =number= | =20= | Displayed page size | =20= |
| =initWithProfile= | no | =Record<string, any>= | | External profile to override internal client profile | |
| =algoliaClientOptions= | no | =AlgoliaSearchOptions= | | Algolia client options used for debugging and setting additional options like timeouts etc. | |
** =search()= method :Method:
:PROPERTIES:
:CUSTOM_ID: search-method
:END:
Search is a method of sapiClient for searching hotels and offers for
provided =searchParameters=:
#+begin_src js
const search = await sapiClient.search(searchParameters, callbacks)
#+end_src
*** Search parameters :Parameters:
:PROPERTIES:
:CUSTOM_ID: search-parameters
:END:
| name | required | type | description | example |
|---------------+----------+------------------------------+-------------------------------+--------------------------------------|
| =hotelId= | no | =string= | Hotel Id for hotel search | =1371626= |
| =placeId= | no | =string= | Place Id for place search | =47319= |
| =query= | no | =string= | Text query | =Amsterdam city= |
| =geolocation= | no | ={lat: number, lon: number}= | Geolocation query | ={lat: 36.114303, lon: -115.178312}= |
| =checkIn= | no | =string= | Check in date (=YYYY-MM-DD=) | =2021-10-10= |
| =checkOut= | no | =string= | Check out date (=YYYY-MM-DD=) | =2021-10-11= |
| =rooms= | no | =string= | Rooms configuration | =2= |
*** Callbacks :Parameters:
:PROPERTIES:
:CUSTOM_ID: search-callbacks
:END:
Search method receives callbacks object as the second argument:
#+begin_src js
const callbacks = {
onStart: (response) => {
console.log('Search started', response)
},
onAnchorReceived: (response) => {
console.log('Anchor received', response)
},
onHotelsReceived: (response) => {
console.log('Hotels received', response)
},
onOffersReceived: (response) => {
console.log('Offers received', response)
},
onComplete: (response) => {
console.log('Search completed', response)
}
}
#+end_src
**** onStart(response)
Runs at the beginning of the each new search\
=response= - /in progress.../
**** onAnchorReceived(response)
Runs when SAPI receives anchor (and?) anchor hotel\
=response= - /in progress.../
**** onHotelsReceived(response)
Runs when SAPI receives static search results\
=response= - /in progress.../
**** onOffersReceived(response)
Runs when SAPI receives a bunch of offers\
=response= - /in progress.../
**** onComplete(response)
Runs when current search is complete and all offers are retrieved\
=response= - /in progress.../
*** Response :Response:
:PROPERTIES:
:CUSTOM_ID: search-response
:END:
/in progress.../
** =rooms()= method :Method:
:PROPERTIES:
:CUSTOM_ID: rooms-method
:END:
Rooms is a method of sapiClient for retrieving rooms information and
offers for a particular itinerary:
#+begin_src js
const rooms = sapiClient.rooms({
hotelId: '47319',
checkIn: '2021-10-10',
checkOut: '2021-10-11',
rooms: '2'
})
#+end_src
*** Rooms query parameters :Parameters:
:PROPERTIES:
:CUSTOM_ID: rooms-parameters
:END:
| name | required | type | default | description | example |
|------------+----------+----------+---------+-------------------------------+--------------|
| =hotelId= | yes | =string= | | Hotel Id to retrieve rooms | =1371626= |
| =checkIn= | yes | =string= | | Check in date (=YYYY-MM-DD=) | =2021-10-10= |
| =checkOut= | yes | =string= | | Check out date (=YYYY-MM-DD=) | =2021-10-11= |
| =rooms= | yes | =string= | | Rooms configuration | =2= |
*** Response :Response:
:PROPERTIES:
:CUSTOM_ID: rooms-response
:END:
/in progress.../
SAPI =rooms()= method will have the similar response body as BoFH API
=/rooms= endpoint. For now please refer to [[https://github.com/FindHotel/bofh-api/blob/master/docs/consumers/default/endpoints/rooms.md#example-1][BoFH documentation]].