LokAPI is a javascript library intended to be used in mobile applications or web application to abstract all logics with lokavaluto’s server.
From the root of your project:
npm install --save @lokavaluto/lokapi
Or better, as @lokavaluto/lokapi
is still in early release,
npm install --save Lokavaluto/lokapi#master
To be sure to get the latest version, relaunch this last command whenever you want to update.
Please note that an important part of the following is already
packaged in package @lokavaluto/lokapi-browser
(for node and browser
support), and thus allow you to quick-start your projects with less
code. Here’s a link to the project page of
lokapi-browser and how to use it, with an example.
Lokapi will require a way to make HTTP request, access to a permanent store, or various other tasks. Depending on your platform or environment, you might have different way to implement these.
To inject these exchangeable dependency, you are invited to subclass LokAPIAbstract and define these objects:
- BackendFactories
- A mapping of currency backends loaded
- httpRequest
- an implementation of
HTTPRequest
- base64Encode
- a function to encode a string to base64
- persistentStore
- an object implementing this interface:
export interface IPersistentStore { get(key: string, defaultValue?: string): string set(key: string, value: string): void del(key: string): void }
- (optional) requestLogin
- a function to trigger a login screen when automatic authorization fails or when no automatic autorization data exists. This will be triggered only if a protected request is made on the administration backend side.
- (optional) requestLocalPassword
- a function for backends to
trigger a request to the user for a password that is meant
to be kept locally on the device. This is typically used before
payment with some backends (to see an example see package
lokapi-backend-comchain
), or administrative tasks. And takes usually the form of a popup.
On instantiation time, LokAPI
class will require you to provide a
host
as first argument (a string, describing the URL to reach the
administrative backend), and can receive a database name as second
argument. If you don’t specify the database name, it’ll be the default
one selected by the server.
Using node’s core https
as HTTP requestion implementation:
import http from "http"
import https from "https"
import { VueCookieNext } from 'vue-cookie-next'
import { LokAPIAbstract, e as LokAPIExc, t as LokAPIType } from "@lokavaluto/lokapi"
import cyclos from '@lokavaluto/lokapi-backend-cyclos'
class cookieStore implements LokAPIType.IPersistentStore {
constructor() {
VueCookieNext.config({ expire: '7d' })
}
get(key: string, defaultValue?: string): string {
let value = VueCookieNext.getCookie("lokapi_" + key)
// XXXvlab: CookieViewNext is JSON.parsing string sometimes and this
// breaks interface !
if (value !== null && typeof value === "object") {
return JSON.stringify(value)
}
return value
}
set(key: string, value: string): void {
VueCookieNext.setCookie("lokapi_" + key, value)
}
del(key: string): void {
VueCookieNext.removeCookie("lokapi_" + key)
}
}
const requesters: any = { http, https }
class NodeLokAPI extends LokApiAbstract {
BackendFactories = {
cyclos,
}
httpRequest = (opts: LokAPIType.coreHttpOpts) => {
const httpsOpts = {
host: opts.host,
path: opts.path,
method: opts.method,
...opts.headers && { headers: opts.headers },
...opts.port && { port: opts.port }
}
const requester = requesters[opts.protocol]
if (!requester) {
throw new Error(`Protocol ${opts.protocol} unsupported by this implementation`)
}
return new Promise((resolve, reject) => {
let req = requester.request(httpsOpts, (res: any) => {
const { statusCode } = res
let rawData = ''
res.on('data', (chunk: any) => { rawData += chunk })
res.on('end', () => {
if (!statusCode || statusCode.toString().slice(0, 1) !== '2') {
res.resume();
reject(new LokAPIExc.HttpError(statusCode, res.statusMessage, rawData, res))
return
} else {
if (opts.responseHeaders) {
for (const header in res.headers) {
opts.responseHeaders[header] = res.headers[header]
}
}
resolve(rawData)
}
})
})
if (opts.data) {
if (typeof opts.data !== "string")
opts.data = JSON.stringify(opts.data)
req.write(opts.data)
}
req.end()
req.on('error', (err: any) => {
console.error(`Encountered an error trying to make a request: ${err.message}`);
reject(new LokAPIExc.RequestFailed(err.message))
})
})
}
base64Encode = (s: string) => Buffer.from(s).toString('base64')
persistentStore = new cookieStore()
}
if (!process.env.VUE_APP_LOKAPI_HOST) {
throw new Error("Please specify VUE_APP_LOKAPI_HOST in '.env'")
}
var lokAPI = new LokAPI(
process.env.VUE_APP_LOKAPI_HOST,
process.env.VUE_APP_LOKAPI_DB,
)
Using @nativescript-community/https
as HTTP request implementation:
Note that this example couldn’t be thoroughly tested as much as it should. Use with caution.
import * as https from '@nativescript-community/https';
import { LokAPIAbstract, e as LokAPIExc, t as LokAPIType } from "@lokavaluto/lokapi"
import { getString, remove as removeSetting, setString } from '@nativescript/core/application-settings';
import cyclos from '@lokavaluto/lokapi-backend-cyclos'
class applicationSetting implements LokAPIType.IPersistentStore {
get(key: string, defaultValue?: string): string {
return getString("lokapi_" + key, defaultValue)
}
set(key: string, value: string): void {
setString("lokapi_" + key, value)
}
del(key: string): void {
removeSetting("lokapi_" + key)
}
}
class NativeLokAPI extends LokAPIAbstract {
BackendFactories = {
cyclos,
}
httpRequest = async (opts: LokAPIType.coreHttpOpts) => {
const nativeRequestOpts = {
url: opts.protocol + "://" + opts.host + opts.path,
method: opts.method,
headers: opts.headers,
body: opts.data,
useLegacy: true,
}
let response
try {
response = await https.request(nativeRequestOpts)
} catch (err) {
console.error(
`Encountered an error trying to make a request: ${err.message}`)
throw new LokAPIExc.RequestFailed(err.message)
}
const statusCode = response.statusCode;
let rawData = await response.content.toStringAsync();
if (!statusCode || statusCode.toString().slice(0, 1) !== '2') {
throw new LokAPIExc.HttpError(statusCode, response.reason, "", response)
}
if (opts.responseHeaders) {
for (const header in response.headers) {
opts.responseHeaders[header] = response.headers[header]
}
}
return rawData
}
base64Encode = base64Encode
persistentStore = new applicationSetting()
}
var lokAPI = new NativeLokAPI(APP_HOST, APP_DB)
Before being able to log in and use the local currency, you must have a user account on the administrative backend. If you don’t already have one yet, this is how to request the creation of one from the `lokApi` instance:
if (await lokApi.canSignup()) {
await lokApi.signup(
"[email protected]", // login
"Doe", // firstname
"John", // lastname
"myp4ss0rd", // password
)
}
Note that the administrative backend might implement sign-up
mechanism or choose not to. Thus, you can check if this is possible
through lokApi.canSignup()
first before trying to effectively
use lokApi.signup(..)
.
Under the hood, the later will trigger the administrative backend to
take actions to process your sign-up request. In odoo
administrative
backend, traditionally, that could means sending you an email with
instructions you’ll need to follow to effectively complete the
registration process.
Note that lokApi.signup(..)
/ lokApi.canSignup()
, along with
lokApi.resetPassword(..)
/ lokApi.canResetPassword()
do NOT require
to be logged in before.
If you forgot your password, you can trigger a request to reset your password by providing your login.
if (await lokApi.canResetPassword()) {
await lokApi.resetPassword("myuser")
}
Note that the administrative backend might implement password reset
mechanism or choose not to. Thus, you can check if this is possible
through lokApi.canResetPassword()
first before trying to effectively
use lokApi.resetPassword(..)
.
Under the hood, the later will trigger the administrative backend to
take actions to reset your password. In odoo
administrative backend,
traditionally, that could means sending you an email with instructions
you’ll need to follow to reset your password.
Note that lokApi.resetPassword(..)
/ lokApi.canResetPassword()
do
NOT require to be logged in before.
You must log in to the server with an existing account on the administrative backend:
await lokApi.login("myuser", "mypassword")
Note that you can check if you are logged in with lokApi.isLogged()
We assume that you’ve instanciated LokAPI
as stated in the previous
section, and you have logged in.
let accounts = await lokAPI.getAccounts()
let balance = await accounts[0].getBalance()
let symbol= await accounts[0].getSymbol()
console.log(`balance in first account: ${balance} ${symbol}`)
backend.getAccounts()
is the list of accounts in that connection (warning, this is a promise).account.getBalance()
is the balance of the accountaccount.getSymbol()
is the currency symbol for the account
You can credit your account thanks to account.getCreditUrl(amount)
.
This will return an url to finish the purchase of new credits.
let accounts = await lokAPI.getAccounts()
url = await accounts[0].getCreditUrl(100)
console.log(`I need to follow instructions on $url ` +
'to purchase credit to store in my account.')
Note that depending on the backend, an admin might have to manually validate the final step of crediting the money on your account.
Recipients are receiving ends of a transfer of money. These are
connected to contacts in lokapi
.
let recipients = await lokAPI.searchRecipients("Alain")
for await (const recipient of recipients) {
console.log(`name: ${recipient.name}`)
}
Note that if you look for an empty string,
lokAPI.searchRecipients("")
will return all favorite recipients.
Recipients are always ordered with favorites first and by name.
There is also a IBackend.searchRecipients(..)
that works similarly
and limits search to recipients able to receive money in the selected
backend. Using the general lokAPI.searchRecipients(..)
will look for
any recipients in all the loaded backends.
You can also grab recipients by url. This url is the identity url created by odoo. It’ll return a list of recipients, one for each backend you can use to send money.
let url = "https://myhost.com/fr_FR/partners/foo-bar-13"
let recipients = await lokAPI.getRecipientsFromUrl(url)
recipients.forEach(recipient => {
console.log(`name: ${recipient.name}`)
})
Transfering money is done from an account of the logged-in user to a recipient:
// Fetch recipients named 'Alain'
let recipients = await lokAPI.searchRecipients("Alain")
await recipients[0].transfer("12", "Dinner Party participation")
Note that .transfer(..)
can lead to these exceptions to be thrown:
- sub-classes of
InvalidAmount
NegativeAmount
: Upon negative amountsNullAmount
: Upon 0 amountRefusedAmount
: When the backend refuses the transaction (often linked to insufficient funds).
InactiveAccount
: Source or destination account is inactive.
If the backend supports it (test Backend.splitMemoSupport
to check
for this support), you can then also provide a third string argument
as a recipient memo, this allows you to set different memo
(descriptions) for the transaction for the recipient and the sender.
Without support for split memo, or without specifying a third argument, the second argument is used as the description for both sides.
The method lokAPI.getMyContact()
allows you to get back
your own information.:
// My own information
let me = await lokAPI.getMyContact()
console.log(`My user name: ${me.name}`)
You can set or unset the “favorite” state of a given contact with the
lokAPI.setFavorite(..)
, lokAPI.unsetFavorite(..)
, or
lokAPI.toggleFavorite(..)
method. This can be used on a recipient
(from .searchRecipients()
) or a contact (but for now, only
.getMyContact()
is outputting a contact, and it doesn’t make
sense to be your own favorite, does it ?).
It’ll not return any value, but the contact will be updated accordingly.
let recipients = await lokAPI.searchRecipients("Alain")
await recipients[2].setFavorite()
await recipients[3].unsetFavorite()
console.log(recipients[3].is_favorite) // is expected to be unset
List past transactions for the current logged in user.
let transactions = await lokAPI.getTransactions()
for await (const tr of transactions) {
console.log(` ${tr.date} ${tr.amount} ${tr.currency}`)
}
You can also retrieve transaction in a specific date span:
let transactions = await lokAPI.getTransactions({
dateBegin: new Date(2020, 1, 1),
dateEnd: new Date(2021, 1, 1)
})
for (const tr of transactions) {
console.log(` ${tr.date} ${tr.amount} ${tr.currency}`)
}
Lokapi provides advanced backend actions directly on it’s backend’s
instances. You can list them with cached, debounced, async call
lokAPI.getBackends()
.
Backend
objects offers minCreditAmount
and maxCreditAmount
that
allows you to know what would be the minimum and maximum accepted
amount for credit requests if set in the administrative backend.
let backends = await lokAPI.getBackends()
for (const backend of backends) {
console.log(
`${backend.internalId}:\n` +
` min: ${backend.minCreditAmount}\n` +
` max: ${backend.maxCreditAmount}\n`)
if (backend.safeWalletRecipient) {
console.log(` safe wallet: {backend.safeWalletRecipient.name}`)
}
}
If you don’t already have a money account, some backends will allow you to submit a money account creation request. Once accepted by an admin, you’ll be able to use it.
This is done directly on the backend object thanks to
IBackend.createUserAccount(data)
, provided that the backend
implements this.
Argument of IBackend.createUserAccount(data)
is a simple object
whose content depends on the backend. Please refer to the target
backend documentation to provide the correct information.
You might want to have a look at the section talking about caching
and debouncing, because the backend list won’t be udpated after
the user account created unless you flush the backend caches
with lokAPI.flushBackendCaches()
.
Backends holds several user accounts. These are often not so much advertised intermediary object as they can be confusingly close from bank accounts, as in most backend, either there are no real user accounts but only an account, and in others, there are only one bank account per user account. But they share one aspect : they are the object requiring authentication of the holder.
You can list them from a Backend
object thanks to property
backend.userAccounts
, which output an Object associating the
internalId
with the corresponding userAccount
:
let backends = await lokAPI.getBackends()
for (const backend of backends) {
console.log(` ${backend.internalId}`)
for (const userAccount of Object.values(backend.userAccounts)) {
console.log(` - account: ${userAccount.internalId}`)
}
}
You can also get the full list of all user accounts from the main
LokAPI
object:
const userAccounts = await lokAPI.getUserAccounts()
for (const userAccount of userAccounts) {
console.log(` ${userAccount.internalId}`)
}
lokAPI
instances provide partial caching and debouncing. Caching
means that most calls that generates an http request will have their
results stored, and the next call won’t be actually performed but the
lokAPI
instance will provide the caller with the stored result.
Debouncing occurs when you have several callers waiting for the same async results, they will all be coalesced into waiting for the same query, avoiding to make N calls for the same query.
This is especially useful if you deal with reactive GUIs with lots of components in a typically event-driven environment.
An issue with this, is that you need to know how to forget and ask for a new query when you know data might have changed on the server side.
We provide the lokAPI.flushBackendCaches()
to deal with that.
This is especially important after creating a new account, as backends creation are cached and debounced, because these are information that typically won’t change often.
These are usage for users having special administrative rights on the administrative backends or the currency backends.
Users can request the creation of a new bank account via
IBackend.createUserAccount(..)
, provided that the backend
implements this.
The call to .hasUserAccountValidationRights()
will return a boolean
to tell you if any backends has at least one of their user accounts
with Account Validation
rights.
Please note that this function is available on top-level lokApi
instance, and also on Backend
’s instances (it’ll only check for
rights in its own user accounts), and finally directly on each
UserAccount
instances.
let hasRights = await lokAPI.hasUserAccountValidationRights()
if (hasRights) {
console.log("I've got the power !")
}
.getStagingUserAccounts()
on main instance will query
administrative backend for the list of user account created that needs
validation from an administrative account.
Please note that it returns IRecipient
objects, that allows you
to print names.
let recipients = await lokAPI.getStagingUserAccounts()
recipients.forEach(recipient => {
console.log(`${recipient.name} has created his account and awaits validation.`)
})
.validateCreation()
upon a recipient instance will request the
validation of that account. The current user logged in need to have
user account validation rights.
Depending on the backend, it could have to check your identity or your credentials.
let recipients = await lokAPI.getStagingUserAccounts()
recipients.forEach(recipient => {
await recipient.validateCreation()
})
Credit requests (the action of requesting to get some additional credit on your account) can be issued indirectly by the administrative backend. There are no direct method on LokAPI to create a credit request.
The call to .hasCreditRequestValidationRights()
will return a
boolean to tell you if any backends has at least one of their user
accounts with Credit Request Validation
rights.
Please note that this method is available on top-level lokApi
instance, and also on Backend
’s instances (it’ll only check for
rights in its own user accounts), and finally directly on each
UserAccount
instances.
let hasRights = await lokAPI.hasCreditRequestValidationRights()
if (hasRights) {
console.log("I've got the power !")
}
.getCreditRequests()
on main instance will query administrative
backend for the list of credit requests that needs validation
from an administrative account.
Please note that it returns ICreditRequest
objects, allowing you
to query the recipient requesting the credit, and the amount.
Note that ICreditRequest
are ITransactions
, so you can expect
the same properties (amount
, related
, currency
, …)
let creditRequests = await lokAPI.getCreditRequests()
creditRequests.forEach(creditRequest => {
console.log(`${creditRequest.related} has requested a credit of ${creditRequest.amount}.`)
})
.validate()
upon a credit request instance will send the validation
of that credit request (and thus credit the account with the given
amount). The current user logged in need to have credit request
validation rights.
Depending on the backend, you might have to confirm your identity or your credentials.
let creditRequests = await lokAPI.getCreditRequests()
creditRequests.forEach(creditRequest => {
await creditRequest.validate()
})
You can query the lokapi
to get the backend list available on the
administration backend side. A Backend
instance is the main object
whose instance is responsible for a currency domain. You have a
Backend
per currency. So in short, .getBackends()
will give you
the available currencies available.
This function is cached, so it doesn’t get updated if you happen to add a new currency backend in the administration backend.
.getBackends()
returns an Object mapping a string identifier for the
backend (ie: cyclos:cyclos.example.com
, comchain:Lemanopolis
,
…), and the Backend object instance.
let backends = await lokAPI.getBackends()
for (const b in backends) {
console.log(` Backend ${b}:`, backends[b])
}
You can use lokapi
instance to query directly the odoo api trough
the get
, post
, put
, delete
methods and their authenticated
counterparts, $get
, $post
, $put
, $delete
.
// All 8 methods have this signature:
// type restMethod = (path: string, data?: JsonData, headers?: { [label: string]: string }): Promise<JsonData>
// Notice that the next call is an example, but you don't need to
// use this endpoint as it is used by the lokAPI.login() and
// manages token for you.
lokAPI.post('/auth/authenticate', {
api_version: 2,
db: 'mydb',
params: ['lcc_app']
}, {
'Authorization': 'XYZ',
})
lokAPI.$post(`/partner/${userId}`)
lokAPI.$put(`/partner/${userId}/favorite/set`)
lokAPI.$get(`/partner/partner_search`, {
value: "foo"
})
Please note that .get(..)
and .$get(..)
have same prototype
and usage than other function and do not require you to build a query
string as it’ll encode in the URL as a querystring the data you’ve
provided.