kaimly-sdk v10.0.4
Installation
npm install kaimly-sdk
Usage
import { Kaimly } from 'kaimly-sdk';
const kaimly = new Kaimly('https://api.example.com/');
NOTE All methods return promises. Make sure to await methods, for example:
import { Kaimly } from 'kaimly-sdk';
const kaimly = new Kaimly('https://api.example.com/');
// Wait for login to be done...
await kaimly.auth.login({
email: 'admin@example.com',
password: 'password',
});
// ... before fetching items
const articles = await kaimly.items('articles').readMany();
console.log({
items: articles.data,
total: articles.meta.total_count,
});
Global
Initialize
import { Kaimly } from 'kaimly-sdk';
const kaimly = new Kaimly('https://api.example.com/');
url
The constructor accepts the URL as the first parameter.
options.auth
The authentication implementation. See Auth for more information.
Defaults to an instance Auth
.
options.storage
The storage implementation. See Storage for more information.
Defaults to an instance of MemoryStorage
when in node.js, and LocalStorage
when in browsers.
NOTE:
If you plan to use multiple SDK instances at once, keep in mind that they will share the Storage across them, leading to unpredictable behaviors. This scenario might be a case while writing tests.
For example, the SDK instance that executed last the login()
method writes the resulting access_token
into the
Storage and overwrites any prior fetched access_token
from any other SDK instance. That might mix up your test
scenario by granting false access rights to your previous logged-in users.
Adding prefixes to your Storage instances would solve this error:
import { Kaimly, MemoryStorage } from 'kaimly-sdk';
import { randomBytes } from 'crypto';
// ...
const prefix = randomBytes(8).toString('hex');
const storage = new MemoryStorage(prefix);
const url = `http://${host}:${port}`;
const kaimly = new Kaimly(url, { storage });
options.transport
The transport implementation. See Transport for more information.
Defaults to an instance of AxiosTransport
.
Example
const kaimly = new Kaimly('http://api.example.com/');
Advanced Example
//
// WARNING: OK, not exactly a warning, but this isn't needed at all.
// It's just to illustrate how the SDK is instantiated by default when
// passing only the URL as the first parameter.
//
const url = 'http://api.example.com/';
const isBrowser = typeof window !== 'undefined';
// Storage adapter where authentication state (token & expiration) is stored.
const storage = isBrowser ? new LocalStorage() : new MemoryStorage();
// Transport used to communicate with the server.
const transport = new AxiosTransport(url, storage, async () => {
await auth.refresh(); // This is how axios checks for refresh
});
// Auth is how authentication is handled, stored, and refreshed.
const auth = new Auth(transport, storage, {
mode: isBrowser ? 'cookie' : 'json',
});
const kaimly = new Kaimly(url, {
auth,
storage,
transport,
});
Get / Set API URL
// Get the API base URL
console.log(kaimly.transport.url); // => https://api.example.com/
// Set the API base URL
kaimly.transport.url = 'https://api2.example.com';
Access to transport/Axios
You can tap into the transport through kaimly.transport
. If you are using the (default) AxiosTransport
, you can
access axios through kaimly.transport.axios
.
Intercepting requests and responses
Axios transport offers a wrapper around Axios interceptors to make it easy for you to inject/eject interceptors.
const requestInterceptor = kaimly.transport.requests.intercept((config) => {
config.headers['My-Custom-Header'] = 'Header value';
return config;
});
// If you don't want the interceptor anymore, remove it
requestInterceptor.eject();
const responseInterceptor = kaimly.transport.responses.intercept((response) => {
console.log('Response received', { response });
return response;
});
// If you don't want the interceptor anymore, remove it
responseInterceptor.eject();
Items
You can get an instance of the item handler by providing the collection (and type, in the case of TypeScript) to the
items
function. The following examples will use the Article
type.
JavaScript
// import { Kaimly, ID } from 'kaimly-sdk';
const { Kaimly } = require('kaimly-sdk');
const kaimly = new Kaimly('https://api.example.com');
const articles = kaimly.items('articles');
TypeScript
import { Kaimly, ID } from 'kaimly-sdk';
// Map your collection structure based on its fields.
type Article = {
id: ID;
title: string;
body: string;
published: boolean;
};
// Map your collections to its respective types. The SDK will
// infer its types based on usage later.
type MyBlog = {
// [collection_name]: typescript_type
articles: Article;
// You can also extend Kaimly collection. The naming has
// to match a Kaimly system collection and it will be merged
// into the system spec.
kaimly_users: {
bio: string;
};
};
// Let the SDK know about your collection types.
const kaimly = new Kaimly<MyBlog>('https://kaimly.myblog.com');
// typeof(article) is a partial "Article"
const article = await kaimly.items('articles').readOne(10);
// Error TS2322: "hello" is not assignable to type "boolean".
// post.published = 'hello';
Create Single Item
await articles.createOne({
title: 'My New Article',
});
Create Multiple Items
await articles.createMany([
{
title: 'My First Article',
},
{
title: 'My Second Article',
},
]);
Read All
await articles.readMany();
Read By Query
await articles.readMany({
search: 'Kaimly',
filter: {
date_published: {
_gte: '$NOW',
},
},
});
Read By Primary Key(s)
await articles.readOne(15);
Supports optional query:
await articles.readOne(15, { fields: ['title'] });
Update Multiple Items
await articles.updateMany([15, 42], {
title: 'Both articles now have the same title',
});
Supports optional query:
await articles.updateMany(
[15, 42],
{
title: 'Both articles now have the same title',
},
{
fields: ['title'],
}
);
Delete
// One
await articles.deleteOne(15);
// Multiple
await articles.deleteMany([15, 42]);
Activity
Read All Activity
await kaimly.activity.readMany();
Read Activity By Query
await kaimly.activity.readMany({
filter: {
action: {
_eq: 'create',
},
},
});
Read Activity By Primary Key(s)
await kaimly.activity.readOne(15);
Supports optional query:
await kaimly.activity.readOne(15, { fields: ['action'] });
Create a Comment
await kaimly.activity.comments.create({
collection: 'articles',
item: 15,
comment: 'Hello, world!',
});
Update a comment
await kaimly.activity.comments.update(31, {
comment: 'Howdy, world!',
});
Note: The passed key is the primary key of the comment
Delete a comment
await kaimly.activity.comments.delete(31);
Note: The passed key is the primary key of the comment
Auth
Configuration
Kaimly will accept custom implementations of the IAuth
interface. The default implementation Auth
can be imported
from kaimly-sdk
. The default implementation will require you to pass the transport and storage implementations. All
options are optional.
import { Auth } from 'kaimly-sdk';
// ...
const sdk = new Kaimly('url', {
auth: new Auth(transport, storage, options);
// ...
});
transport
The transport responsible for communicating with Kaimly backend.
Defaults to an instance of AxiosTransport
when not creating Auth
youself.
storage
The storage responsible for storing authentication and sdk state.
When not creating Auth
youself, defaults to MemoryStorage
in node.js, and LocalStorage
in browsers.
options
options.mode
Accepts cookie
or json
.
When in cookie
mode, the API will set the refresh token in an httpOnly
secure cookie that can't be accessed from
client side JavaScript. This is the most secure way to connect to the API from a public front-end website.
When you can't rely on cookies, or need more control over handling the storage of the cookie (like in node.js), use
json
mode. This will return the refresh token in the "normal" payload. The storage of these tokens are handled by the
storage
implementation.
Defaults to cookie
in browsers, json
in node.js.
options.refresh
See Refresh auth token.
Get current token
const token = kaimly.auth.token;
Login
With credentials
await kaimly.auth.login({
email: 'admin@example.com',
password: 'd1r3ctu5',
});
With static tokens
await kaimly.auth.static('static_token');
Refresh auth token
You can set authentication to auto-refresh the token once it's close to expire.
await kaimly.auth.login(
{
email: 'admin@example.com',
password: 'd1r3ctu5',
},
{
refresh: {
auto: true,
},
}
);
You can also set how much time before the expiration you want to auto-refresh the token. When not specified, 30 sec is the default time.
await kaimly.auth.login(
{
email: 'admin@example.com',
password: 'd1r3ctu5',
},
{
refresh: {
auto: true,
time: 30000, // refesh the token 30 secs before the expiration
},
}
);
Refresh Auth Token
You can manually refresh the authentication token. This won't try to refresh the token if it's still valid in the eyes of the SDK.
Also worth mentioning that any concurrent refreshes (trying to refresh while a there's an existing refresh running), will result in only a single refresh hitting the server, and promises resolving/rejecting with the result from the first call. This depends on the implementation of IAuth you're using.
await kaimly.auth.refresh();
You can force the refresh by passing true in the first parameter.
An optional parameter will accept auto-refresh information.
await kaimly.auth.refresh(true);
This function can either return the AuthResult
in case a refresh was made, false
in case SDK thinks it's not needed,
or throw an error in case refresh fails.
Logout
await kaimly.auth.logout();
Request a Password Reset
await kaimly.auth.password.request('admin@example.com');
Reset a Password
await kaimly.auth.password.reset('abc.def.ghi', 'n3w-p455w0rd');
Note: The token passed in the first parameter is sent in an email to the user when using request()
Transport
The transport object abstracts how you communicate with Kaimly. Transports can be customized to use different HTTP libraries for example.
Interface
// Simplified version, `import { ITransport } from 'kaimly-sdk';`
interface ITransport {
url;
get(path);
head(path);
options(path);
delete(path, data = undefined);
post(path, data);
put(path, data);
patch(path, data);
}
AxiosTransport
The default transport used in both browser and node deployments. It supports auto refresh on request.
Options
AxiosTransport requires a base URL and a storage implementation to work.
const transport = new AxiosTransport('http://example.com', new MemoryStorage(), async () => {
await sdk.auth.refresh();
});
await transport.get('/server/info');
Storage
The storage is used to load and save SDK data.
LocalStorage
The storage used in environments where Local Storage is supported.
Options
The LocalStorage
implementation accepts a transparent prefix. Use this when you need multiple SDK instances with
independent authentication for example.
MemoryStorage
The storage used when SDK data is ephemeral. For example: only during the lifecycle of the process.
Options
The MemoryStorage
implementation accepts a transparent prefix so you can have multiple instances of the SDK without
having clashing keys.
Collections
kaimly.collections;
Same methods as kaimly.items("kaimly_collections")
.
Fields
kaimly.fields;
Same methods as kaimly.items("kaimly_fields")
.
Files
kaimly.files;
Same methods as kaimly.items("kaimly_files")
.
Folders
kaimly.folders;
Same methods as kaimly.items("kaimly_folders")
.
Permissions
kaimly.permissions;
Same methods as kaimly.items("kaimly_permissions")
.
Presets
kaimly.presets;
Same methods as kaimly.items("kaimly_presets")
.
Relations
kaimly.relations;
Same methods as kaimly.items("kaimly_relations")
.
Revisions
kaimly.revisions;
Same methods as kaimly.items("kaimly_revisions")
.
Roles
kaimly.roles;
Same methods as kaimly.items("kaimly_roles")
.
Settings
kaimly.settings;
Same methods as kaimly.items("kaimly_settings")
.
Server
Ping the Server
await kaimly.server.ping();
Get Server/Project Info
await kaimly.server.info();
Users
kaimly.users;
Same methods as kaimly.items("kaimly_users")
, and:
Invite a New User
await kaimly.users.invites.send('admin@example.com', 'fe38136e-52f7-4622-8498-112b8a32a1e2');
The second parameter is the role of the user
Accept a User Invite
await kaimly.users.invites.accept('<accept-token>', 'n3w-p455w0rd');
The provided token is sent to the user's email
Enable Two-Factor Authentication
await kaimly.users.tfa.enable('my-password');
Disable Two-Factor Authentication
await kaimly.users.tfa.disable('691402');
Get the Current User
await kaimly.users.me.read();
Supports optional query:
await kaimly.users.me.read({
fields: ['last_access'],
});
Update the Current Users
await kaimly.users.me.update({ first_name: 'Admin' });
Supports optional query:
await kaimly.users.me.update({ first_name: 'Admin' }, { fields: ['last_access'] });
Utils
Get a Random String
await kaimly.utils.random.string();
Supports an optional length
(defaults to 32):
await kaimly.utils.random.string(50);
Generate a Hash for a Given Value
await kaimly.utils.hash.generate('My String');
Verify if a Hash is Valid
await kaimly.utils.hash.verify('My String', '$argon2i$v=19$m=4096,t=3,p=1$A5uogJh');
Sort Items in a Collection
await kaimly.utils.sort('articles', 15, 42);
This will move item 15
to the position of item 42
, and move everything in between one "slot" up.
Revert to a Previous Revision
await kaimly.utils.revert(451);
Note: The key passed is the primary key of the revision you'd like to apply.
TypeScript
If you are using TypeScript, the JS-SDK requires TypeScript 3.8 or newer. TypeScript will also improve the development
experience by providing relevant information when manipulating your data. For example, kaimly.items
knows about your
collection types if you feed the SDK with enough information in the construction of the SDK instance. This allows for a
more detailed IDE suggestions for return types, sorting, and filtering.
type BlogPost = {
id: ID;
title: string;
};
type BlogSettings = {
display_promotions: boolean;
};
type MyCollections = {
posts: BlogPost;
settings: BlogSettings;
};
// This is how you feed custom type information to Kaimly.
const kaimly = new Kaimly<MyCollections>('http://url');
// ...
const post = await kaimly.items('posts').readOne(1);
// typeof(post) is a partial BlogPost object
const settings = await posts.singleton('settings').read();
// typeof(settings) is a partial BlogSettings object
You can also extend the Kaimly system type information by providing type information for system collections as well.
import { Kaimly } from 'kaimly-sdk';
// Custom fields added to Kaimly user collection.
type UserType = {
level: number;
experience: number;
};
type CustomTypes = {
/*
This type will be merged with Kaimly user type.
It's important that the naming matches a kaimly
collection name exactly. Typos won't get caught here
since SDK will assume it's a custom user collection.
*/
kaimly_users: UserType;
};
const kaimly = new Kaimly<CustomTypes>('https://api.example.com');
await kaimly.auth.login({
email: 'admin@example.com',
password: 'password',
});
const me = await kaimly.users.me.read();
// typeof me = partial KaimlyUser & UserType;
// OK
me.level = 42;
// Error TS2322: Type "string" is not assignable to type "number".
me.experience = 'high';