# # JavaScript SDK

> The JS SDK provides an intuitive interface for the Gitaflow API from within a JavaScript-powered project (browsers and Node.js). The default implementation uses Axios
> (opens new window) for transport and `localStorage` for storing state.

- Installation
- Usage
- TypeScript
- Reference
- Auth
- Transport
- Storage
- Items
- Activity
- Comments
- Collections
- Fields
- Files
- Folders
- Permissions
- Presets
- Relations
- Revisions
- Roles
- Settings
- Server
- Users
- Utils

## # Installation

    npm install @gitaflow/sdk

## # Usage

    import { Gitaflow } from '@gitaflow/sdk';

    const gitaflow = new Gitaflow('http://gitaflow.example.com');

    async function start() {
    	// We don't need to authenticate if data is public
    	const publicData = await gitaflow.items('public').readByQuery({ meta: 'total_count' });

    	console.log({
    		items: publicData.data,
    		total: publicData.meta.total_count,
    	});

    	// But, we need to authenticate if data is private
    	let authenticated = false;

    	// Try to authenticate with token if exists
    	await gitaflow.auth
    		.refresh()
    		.then(() => {
    			authenticated = true;
    		})
    		.catch(() => {});

    	// Let's login in case we don't have token or it is invalid / expired
    	while (!authenticated) {
    		const email = window.prompt('Email:');
    		const password = window.prompt('Password:');

    		await gitaflow.auth
    			.login({ email, password })
    			.then(() => {
    				authenticated = true;
    			})
    			.catch(() => {
    				window.alert('Invalid credentials');
    			});
    	}

    	// After authentication, we can fetch the private data in case the user has access to it
    	const privateData = await gitaflow.items('privateData').readByQuery({ meta: 'total_count' });

    	console.log({
    		items: privateData.data,
    		total: privateData.meta.total_count,
    	});
    }

    start();

## # TypeScript

Version >= 4.1

Although it's not required, it is recommended to use Typescript to have an easy development experience. This allows more detailed IDE suggestions for return types, sorting, filtering, etc.

To feed the SDK with your current schema, you need to pass it on the constructor.

    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 Gitaflow.
    const gitaflow = new Gitaflow<MyCollections>('http://url');

    // ...

    const post = await gitaflow.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 Gitaflow system type information by providing type information for system collections as well.

    import { Gitaflow } from '@gitaflow/sdk';

    // Custom fields added to Gitaflow user collection.
    type UserType = {
    	level: number;
    	experience: number;
    };

    type CustomTypes = {
    	/*
    	This type will be merged with Gitaflow user type.
    	It's important that the naming matches a gitaflow
    	collection name exactly. Typos won't get caught here
    	since SDK will assume it's a custom user collection.
    	*/
    	gitaflow_users: UserType;
    };

    const gitaflow = new Gitaflow<CustomTypes>('https://api.example.com');

    await gitaflow.auth.login({
    	email: 'admin@example.com',
    	password: 'password',
    });

    const me = await gitaflow.users.me.read();
    // typeof me = partial GitaflowUser & UserType;

    // OK
    me.level = 42;

    // Error TS2322: Type "string" is not assignable to type "number".
    me.experience = 'high';

## # Reference

### # Constructor

This is the starting point to use the SDK. You need to create an instance and invoke methods from it. In most cases a single instance is sufficient, but in case you need more, you need to define `options.storage.prefix`.

    import { Gitaflow } from '@gitaflow/sdk';

    const gitaflow = new Gitaflow(url, init);

- `url` [required] String - A string pointing to your Gitaflow instance. E.g. `https://admin.gitaflow.io`

- `init` [optional] Object - Define settings that you want to customize .The possible options are:

  - `auth` [optional] Object - Defines settings you want to customize regarding authentication. The possible options are:

    - `mode` [optional] String - Defines the mode you want to use for authentication. It can be `'cookie'` for cookies or `'json'` for JWT. Defaults to `'cookie'` on browsers and `'json'` otherwise. We recommend using cookies when possible to prevent any kind of attacks, mostly XSS.

    - `autoRefresh` [optional] Boolean - Tells SDK if it should handle refresh tokens automatically. Defaults to `true`.

    - `msRefreshBeforeExpires` [optional] Number - When `autoRefresh` is enabled, this tells how many milliseconds before the refresh token expires and needs to be refreshed. Defaults to `30000`.

    - `staticToken` [optional] String - Defines the static token to use. It is not compatible with the options above since it does not refresh. Defaults to `''` (no static token).

  - `storage` [optional] Object - Defines settings you want to customize regarding storage.

    - `prefix` [optional] String - Defines the tokens prefix tokens that are saved. This should be fulfilled with different values when using multiple instances of SDK. Defaults to `''` (no prefix).
    - `mode` [optional] String - Defines the storage location to be used to save tokens. Allowed values are `LocalStorage` and `MemoryStorage`. Defaults to `LocalStorage` on browsers and `MemoryStorage` on Node.js. The mode `LocalStorage` is not compatible with Node.js.

  - `transport` [optional] Object - Defines settings you want to customize regarding transport.
    - `params` [optional] Object - Defines an object with keys and values to be passed as additional query string.
    - `headers` [optional] Object - Defines an object with keys and values to be passed as additional headers.
    - `onUploadProgress` [optional] (event: ProgressEvent
      (opens new window) => void) - Defines a callback function to indicate the upload progress.

## # Auth

Defines how authentication is handled by the SDK.

### # Custom Implementation

It is possible to provide a custom implementation by extending `IAuth`. While this could be useful for advanced usage, most use-cases do not need it.

    import { IAuth, Gitaflow } from '@gitaflow/sdk';

    class MyAuth extends IAuth {
    	async login() {
    		return { access_token: '', expires: 0 };
    	}
    	async logout() {}
    	async refresh() {
    		return { access_token: '', expires: 0 };
    	}
    	async static() {
    		return true;
    	}
    }

    const gitaflow = new Gitaflow('http://gitaflow.example.com', { auth: new MyAuth() });

### # Gitaflow Implementation

By default, Gitaflow creates an instance of `Auth` which handles refresh tokens automatically. Check `options.auth` to see the available settings.

### # Get current token

    const token = gitaflow.auth.token;

### # Login

#### # With credentials

    await gitaflow.auth.login({
    	email: 'admin@example.com',
    	password: 'd1r3ctu5',
    });

#### # With static tokens

    await gitaflow.auth.static('static_token');

### # Refresh Auth Token

By default, Gitaflow will handle token refreshes. Although, you can handle this behaviour manually by setting `autoRefresh` to `false`.

    await gitaflow.auth.refresh();

Developing Locally

If you're developing locally, you might not be able to refresh your auth token automatically in all browsers. This is because the default auth configuration requires secure cookies to be set, and not all browsers allow this for localhost. You can use a browser which does support this such as Firefox, or disable secure cookies.

### # Logout

    await gitaflow.auth.logout();

### # Request a Password Reset

By default, the address defined in `PUBLIC_URL` on `.env` file is used for the link to the reset password page sent in the email:

    await gitaflow.auth.password.request('admin@example.com');

But a custom address can be passed as second argument:

    await gitaflow.auth.password.request(
    	'admin@example.com',
    	'https://myapp.com' // In this case, the link will be https://myapp.com?token=FEE0A...
    );

### # Reset a Password

    await gitaflow.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 Gitaflow. Transports can be customized to use different HTTP libraries for example.

### # Custom Implementation

It is possible to provide a custom implementation by extending `ITransport`. While, this could be useful for advanced usage, it is not needed for most use-cases.

    import { ITransport, Gitaflow } from '@gitaflow/sdk';

    class MyTransport extends ITransport {
    	buildResponse() {
    		return {
    			raw: '',
    			data: {},
    			status: 0,
    			headers: {},
    		};
    	}

    	async get(path, options) {
    		return this.buildResponse();
    	}
    	async head(path, options) {
    		return this.buildResponse();
    	}
    	async options(path, options) {
    		return this.buildResponse();
    	}
    	async delete(path, data, options) {
    		return this.buildResponse();
    	}
    	async post(path, data, options) {
    		return this.buildResponse();
    	}
    	async put(path, data, options) {
    		return this.buildResponse();
    	}
    	async patch(path, data, options) {
    		return this.buildResponse();
    	}
    }

    const gitaflow = new Gitaflow('http://gitaflow.example.com', { transport: new MyTransport() });

### # Gitaflow Implementation

By default, Gitaflow creates an instance of `Transport` which handles requests automatically. Check `options.transport` to see the available settings.

To make HTTP requests SDK uses `axios` so it is compatible in both browsers and Node.js. Also, it is possible to handle upload progress (a downside of `fetch`).

## # Storage

The storage is used to load and save token information.

### # Custom Implementation

It is possible to provide a custom implementation by extending `BaseStorage`. While, this could be useful for advanced usage, it is not needed for most use-cases.

    import { BaseStorage, Gitaflow } from '@gitaflow/sdk';

    class SessionStorage extends BaseStorage {
    	get(key) {
    		return sessionStorage.getItem(key);
    	}
    	set(key, value) {
    		return sessionStorage.setItem(key, value);
    	}
    	delete(key) {
    		return sessionStorage.removeItem(key);
    	}
    }

    const gitaflow = new Gitaflow('http://gitaflow.example.com', { storage: new SessionStorage() });

### # Gitaflow Implementation

By default, Gitaflow creates an instance of `Storage` which handles store information automatically. Check `options.storage` to see the available settings.

SDK uses `localStorage` on browsers and the memory itself on Node.js to save tokens. This behavior can be configured in `options.storage.mode`. The `LocalStorage` is only available on browsers and the `MemoryStorage` is not persistent, i.e., once you leave the tab or quit the process, you will need to authenticate again.

If you want to use multiple instances of the SDK you should set a different `prefix` for each one.

## # 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 { Gitaflow, ID } from '@gitaflow/sdk';
    const { Gitaflow } = require('@gitaflow/sdk');

    const gitaflow = new Gitaflow('http://gitaflow.example.com');

    const articles = gitaflow.items('articles');

> TypeScript

    import { Gitaflow, ID } from '@gitaflow/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 Gitaflow collection. The naming has
    	// to match a Gitaflow system collection and it will be merged
    	// into the system spec.
    	gitaflow_users: {
    		bio: string;
    	};
    };

    // Let the SDK know about your collection types.
    const gitaflow = new Gitaflow<MyBlog>('https://gitaflow.myblog.com');

    // typeof(article) is a partial "Article"
    const article = await gitaflow.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 By Query

    await articles.readByQuery({
    	search: 'Gitaflow',
    	filter: {
    		date_published: {
    			_gte: '$NOW',
    		},
    	},
    });

### # Read All

    await articles.readByQuery({
    	// By default API limits results to 100.
    	// With -1, it will return all results, but it may lead to performance degradation
    	// for large result sets.
    	limit: -1,
    });

### # Read Single Item

    await articles.readOne(15);

Supports optional query:

    await articles.readOne(15, {
    	fields: ['title'],
    });

### # Read Multiple Items

    await articles.readMany([15, 16, 17]);

Supports optional query:

    await articles.readMany([15, 16, 17], {
    	fields: ['title'],
    });

### # Update Single Item

    await articles.updateOne(15, {
    	title: 'This articles now has a different title',
    });

Supports optional query:

    await articles.updateOne(
    	42,
    	{
    		title: 'This articles now has a similar title',
    	},
    	{
    		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]);

### # Request Parameter Overrides

To override any of the axios request parameters, provide an additional parameter with a `requestOptions` object:

    await articles.createOne(
    	{ title: 'example' },
    	{ fields: ['id'] },
    	{
    		requestOptions: {
    			headers: {
    				'X-My-Custom-Header': 'example',
    			},
    		},
    	}
    );

## # Activity

    gitaflow.activity;

Same methods as `gitaflow.items("gitaflow_activity")`.

## # Comments

    gitaflow.comments;

Same methods as `gitaflow.items("gitaflow_comments")`.

## # Collections

    gitaflow.collections;

Same methods as `gitaflow.items("gitaflow_collections")`.

## # Fields

    gitaflow.fields;

Same methods as `gitaflow.items("gitaflow_fields")`.

## # Files

    gitaflow.files;

Same methods as `gitaflow.items("gitaflow_files")`.

### # Uploading a file

To upload a file you will need to send a `multipart/form-data` as body. On browser side you do so:

    /* index.js */
    import { Gitaflow } from 'https://unpkg.com/@gitaflow/sdk@latest/dist/sdk.esm.min.js';

    const gitaflow = new Gitaflow('http://localhost:8055', {
    	auth: {
    		staticToken: 'STATIC_TOKEN', // If you want to use a static token, otherwise check below how you can use email and password.
    	},
    });

    // await gitaflow.auth.login({ email, password }) // If you want to use email and password. You should remove the staticToken above

    const form = document.querySelector('#upload-file');

    if (form && form instanceof HTMLFormElement) {
    	form.addEventListener('submit', async (event) => {
    		event.preventDefault();

    		const form = new FormData(event.target);
    		await gitaflow.files.createOne(form);
    	});
    }

    <!-- index.html -->
    <head></head>
    <body>
    	<form id="upload-file">
    		<input type="text" name="title" />
    		<input type="file" name="file" />
        	<button>Send</button>
    	</form>
    	<script src="/index.js" type="module"></script>
    </body>
    </html>

#### # NodeJS usage

When uploading a file from a NodeJS environment, you'll have to override the headers to ensure the correct boundary is set:

    import { Gitaflow } from 'https://unpkg.com/@gitaflow/sdk@latest/dist/sdk.esm.min.js';

    const gitaflow = new Gitaflow('http://localhost:8055', {
    	auth: {
    		staticToken: 'STATIC_TOKEN', // If you want to use a static token, otherwise check below how you can use email and password.
    	},
    });

    const form = new FormData();
    form.append("file", fs.createReadStream("./to_upload.jpeg"));

    const fileId = await gitaflow.files.createOne(form, {}, {
      requestOptions: {
        headers: {
          ...form.getHeaders()
        }
      }
    );

### # Importing a file

Example of importing a file from a URL:

    await gitaflow.files.import({
    	url: 'http://www.example.com/example-image.jpg',
    });

Example of importing file with custom data:

    await gitaflow.files.import({
    	url: 'http://www.example.com/example-image.jpg',
    	data: {
    		title: 'My Custom File',
    	},
    });

## # Folders

    gitaflow.folders;

Same methods as `gitaflow.items("gitaflow_folders")`.

## # Permissions

    gitaflow.permissions;

Same methods as `gitaflow.items("gitaflow_permissions")`.

## # Presets

    gitaflow.presets;

Same methods as `gitaflow.items("gitaflow_presets")`.

## # Relations

    gitaflow.relations;

Same methods as `gitaflow.items("gitaflow_relations")`.

## # Revisions

    gitaflow.revisions;

Same methods as `gitaflow.items("gitaflow_revisions")`.

## # Roles

    gitaflow.roles;

Same methods as `gitaflow.items("gitaflow_roles")`.

## # Settings

    gitaflow.settings;

Same methods as `gitaflow.items("gitaflow_settings")`.

## # Server

### # Ping the Server

    await gitaflow.server.ping();

### # Get Server/Project Info

    await gitaflow.server.info();

## # Users

    gitaflow.users;

Same methods as `gitaflow.items("gitaflow_users")`, and:

### # Invite a New User

    await gitaflow.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 gitaflow.users.invites.accept('<accept-token>', 'n3w-p455w0rd');

The provided token is sent to the user's email

### # Enable Two-Factor Authentication

    await gitaflow.users.tfa.enable('my-password');

### # Disable Two-Factor Authentication

    await gitaflow.users.tfa.disable('691402');

### # Get the Current User

    await gitaflow.users.me.read();

Supports optional query:

    await gitaflow.users.me.read({
    	fields: ['last_access'],
    });

### # Update the Current Users

    await gitaflow.users.me.update({ first_name: 'Admin' });

Supports optional query:

    await gitaflow.users.me.update({ first_name: 'Admin' }, { fields: ['last_access'] });

## # Utils

### # Get a Random String

    await gitaflow.utils.random.string();

Supports an optional `length` (defaults to 32):

    await gitaflow.utils.random.string(50);

### # Generate a Hash for a Given Value

    await gitaflow.utils.hash.generate('My String');

### # Verify if a Hash is Valid

    await gitaflow.utils.hash.verify('My String', '$argon2i$v=19$m=4096,t=3,p=1$A5uogJh');

### # Sort Items in a Collection

    await gitaflow.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 gitaflow.utils.revert(451);

Note: The key passed is the primary key of the revision you'd like to apply.