# Datalynk Client Library

[![pipeline status](https://gitlab.auxiliumgroup.com/auxilium/datalynk/datalynk-client/badges/master/pipeline.svg)](https://gitlab.auxiliumgroup.com/auxilium/datalynk/datalynk-client/-/commits/master)
[![Latest Release](https://gitlab.auxiliumgroup.com/auxilium/datalynk/datalynk-client/-/badges/release.svg)](https://gitlab.auxiliumgroup.com/auxilium/datalynk/datalynk-client/-/releases)

---

Datalynk client library to integrate JavaScript clients with the Datalynk API.

## Table of Contents
- [Datalynk Client Library](#datalynk-client-library)
  - [Table of Contents](#table-of-contents)
  - [Quick Start](#quick-start)
  - [API Documentation](https://datalynk-client.primary.auxilium.world)
  - [Cheatsheet](#cheatsheet)
    - [Integration](#integration)
      - [Angular](#angular)
      - [Node / Vue](#node--vue)
      - [Vanilla JS](#vanilla-js)
    - [Build Models](#build-models)
    - [Api Requests](#api-requests)
    - [Authentication](#authentication)
    - [Offline / PWA](#)
    - [Slice Engine](#slices)
    - [Sockets](#sockets)
    - [Uploads](#uploads)
    - [WebRTC](#webrtc)

## Quick Start
1. Install the client library
```bash
npm install --save @auxilium/datalynk-client
```

2. _Optional: Build Datalynk models_
```bash
$ npx datalynk-models
Output (src/models):
Spoke: spoke
Login: username
Password: ********
```

3. Create an API object & start making requests
```js
import {API} from '@auxilium/datalynk-client';

const api = new API('https://spoke.auxiliumgroup.com');
const resp = await api.request({'$/auth/current':{}});
```

## [API Documentation](https://datalynk-client.primary.auxilium.world)

## Cheatsheet

<details>
  <summary>
    <h3 id="integration" style="display: inline">Integration</h3>
  </summary>

This library is written with vanilia JS & has no framework dependencies allowing easy integration with any front-end website. Here is some boilerplate to get you going:

#### Angular

**File:** `/src/services/datalynk.service.ts`

```ts
import {Injectable} from '@angular/core';
import {Api, Slice} from '@auxilium/datalynk-client';
import {environment} from '../environment/environment';
import {Contact} from '../models/contact';
import {Slices} from '../models/slices';

declare global {
  interface Window {
    api: Api;
  }
}

@Injectable({providedIn: 'root'})
export class DatalynkApi extends Api {
  contacts!: Slice<Contact>;

  constructor() {
    // Create API object & expose to window
    super(environment.api, {/* options */});
    window.api = this;

    // Handle logging in
    this.auth.handleLogin('spoke', {/* login UI options */});

    // Create store to cache slice data
    this.contacts = this.slice<Contact>(Slices.Contact);
  }
}
```

#### Node / Vue

**File:** `/src/services/datalynk.service.ts`

```ts
import {Api} from '@auxilium/datalynk-client';
import {environment} from '../environment/environment';
import {Contact} from '../models/contact';
import {Slices} from '../models/slices';

// Create API object & expose to window
export const api = window.api = new Api(environment.api, {/* options */});

// Handle logging in
api.auth.handleLogin('spoke', {/* login UI options */});

// Create store to cache slice data
export const contacts = api.slice<Contact>(Slices.Contact);
```

#### Vanilla JS

**File:** `/index.html`

```html
<script type="module">
  import {Api} from '@auxilium/datlaynk-client/dist/index.mjs';

  // Create API object & expose to window
  var api = new Api('https://spoke.auxiliumgroup.com', /* options */);

  // Handle logging in
  api.auth.handleLogin('spoke', {/* login UI options */});

  // Create store to cache slice data
  var contacts = api.slice(12345);
</script>
```

</details>

<details>
  <summary>
    <h3 id="build-models" style="display: inline">Build Models</h3>
  </summary>

This library comes with a command line tool for developers to automatically create Typescript models from the Datalynk metadata.

This takes most of the manual labour out of manually mapping the data & provides type safety.

1. Simply run the tool:
```bash
$ npx datalynk-models
Output (src/models):
Spoke: spoke
Login: username
Password: ********
```

2. Import models:
```ts
import {Slices} from 'models/slices'; // Import slices map
import {Contact} from 'models/contact'; // Import model for slice we will be using

const contacts: Contact[] = await api.slice<Contact>(Slices.Contact)
                                     .select()
                                     .rows().exec();
```

</details>

<details>
  <summary>
    <h3 id="api-requests" style="display: inline">Api Requests</h3>
  </summary>

#### Api Request

Raw requests can be made using request

```js
const response = await api.request({...});
```

#### Chain Map

Multiple results can be returned together as dictionary

```js
const {user, report} = await api.chainMap({
    user: {'$/auth/current': {}},
    report: api.slice(52131).select().rows()
});
```

#### Chain Requests

Multiple requests can be chained together to run them in order. The last request's response will be returned

```js
const report = await api.chain({'$/auth/current': {}}, api.slice(12345).select().rows());
```
</details>

<details>
  <summary>
    <h3 id="authentication" style="display: inline">Authentication</h3>
  </summary>

#### Default Flow

99% of the time you will want to let the library handle the login flow:

1. It will check the URL for a token param: `?datalynkToken=...`
2. It will check the localStorage for a saved token
3. It will prompt the user to login via UI
4. Lastly it will, reload page if the token changed

```js
await api.auth.handleLogin('spoke', {/* Login UI Options */});
```

#### Fetch User

```js
api.auth.user
```

#### Guest Login

Login as the guest account:

```js
const guest = await api.auth.loginGuest();
console.log(api.auth.isGuest()) // True
```

#### Login UI

Prompt the user to login with the login page, defaults come from the clients [theme.json](https://sintmaarten.auxiliumgroup.com/static/js/auxilium/dijits/templates/login/sandbox/theme.json) file:

```js
const prompt = api.auth.loginPrompt('spoke', {
  // Overrides theme.json
  title: "Title of App",
  logoPosition: "aboveTitle",
  subtitle: "Subtitle of App",
});
await prompt.wait; // Wait for the user to login/close the prompt
prompt.close(); // Close prompt manually
```

[Full list of options](https://datalynk-client.primary.auxilium.world/types/login-prompt.LoginPromptOptions.html)

#### Manual Login

Login programmatically:

```js
const user = await api.auth.login('spoke', 'username', 'password', '2faCode');
```

</details>

<details>
  <summary>
    <h3 id="pwa" style="display: inline">Offline / PWA</h3>
  </summary>

### Support

#### PWA
Turns the website into an installable native app
- The PWA manifest can be configured with: `manifest`
- The install prompt can be configured with: `pwaSettings`
```ts
const api = new Api(`https://${spoke}.auxiliumgroup.com`, {
    name: "Workplace Occupational Health & Safety Inspection", // REQUIRED: App name
    manifest: { // Set/Override any manifest values
        scope: 'https://lynk-forms.scarborough.auxilium.world/OshawaCL/ohsinspection.html' 
    },
    pwaSettings: { // PWA install prompt settings
        timeout: 45, // seconds before prompt shows (default: 30)
        dismissExpiry: 3, // days before prompt shows again (0 = every refresh, default: 7)
        loginLink: true // Show install link in login screen
    },
});

// Manually trigger
api.pwa.prompt();
```

#### Static assets
Upload the [service worker](https://datalynk-client.primary.auxilium.world/dist/service.worker.mjs) `dist/service.worker.mjs` to the *root* directory of the server
to cache all files for use offline
```ts
const api = new Api(`https://${spoke}.auxiliumgroup.com`, {
  serviceWorker: 'https://.../service.worker.mjs', // Shouldnt be needed but can be overriden
});
```

#### Slice Engine
Slices can be used as normal offline using the slice engine by marking the slice as offline with: `offline`
```ts
const api = new Api(`https://${spoke}.auxiliumgroup.com`, {
  offline: [51306, 51531, 51563], // REQUIRED: Specify slices that must be stored offline
  socket: true // Make sure sockets are enabled
});
await api.slice(51306).insert({...}).exec();
const resp = await api.slice(51306).select().where('id', '<=', 100).exec();
```
#### API Requests
Any API request can be deferred until online, however you dont get responses when offline
  - Only useful for submitting data or when we dont care about the response
```ts
api.request(..., {offline: true}).then((resp: T | void) => {
    if(resp != null) {
        // Online logic (has response payload)
    } else {
        // Offline logic (no response payload)
    }
});
```

#### Connection status
`online$` remains available for a simple boolean check. Use `status$` when the application needs to react differently to connectivity, authentication, and API failures:

```ts
api.status$.subscribe(status => {
    switch(status) {
        case 'online':
            // The API is responding normally
            break;
        case 'offline':
            // The network request could not reach the API
            break;
        case 'unauthorized':
            // The token expired or the API rejected it; prompt for login
            break;
        case 'unavailable':
            // The server returned a failure or a non-JSON API response
            break;
    }
});
```

Expired and rejected tokens are cleared from `token$`. Malformed responses reject with `UnexpectedApiResponseError`. Requests only enter the offline queue when `{offline: true}` is supplied; other requests reject so callers can handle the relevant status. `api.offline` reflects Datalynk availability, not only browser connectivity, so it can be `true` while `navigator.onLine` is still `true` during API/MySQL recovery. Offline-enabled slices use their local IndexedDB cache in either case.
</details>

<details>
  <summary>
    <h3 id="pdf" style="display: inline">PDFs</h3>
  </summary>

PDFs can be generated using the API. PDFs can be optionally related to a record by passing an association

```ts
const resp = await api.pdf.fromUrl('https://google.com', {slice: 12345, row: 1, field: 'resume'});
```

</details>

<details>
  <summary>
    <h3 id="slices" style="display: inline">Slice Engine</h3>
  </summary>

This library comes with LINQ style query language to help make interacting with slices easier by providing types & intellisense.

#### Select

```ts
// Get a single record
const row = await api.slice<T>(12345)
                     .select(12345)
                     .row().exec();

// Get all slice records
const rows = await api.slice<T>(12345)
                      .select()
                      .rows().exec();

// Advanced queries
const rows = await api.slice<T>(12345)
                      .select()
                      .fields({'field1': 'field2'})
                      .where('field1', '<', 0)
                      .or()
                      .where({field1: 0, field2: false})
                      .order('field2', true) // ascending
                      .limit(10)
                      .rows().exec();                      
```

#### Count

```ts
const count = await api.slice(12345)
                       .count()
                       .where({field1: 'value'})
                       .count().exec();
```

#### Insert

```ts
// Insert record
const key = await api.slice<Contact>(12345)
                     .insert({first: 'Bilbo', last: 'Baggins'})
                     .id().exec();

// Insert multiple rows
const keys = await api.slice<Contacts>(12345)
                      .insert([
                        {first: 'Darth', last: 'Vader'},
                        {first: 'John', last: 'Snow'}
                      ])
                      .ids().exec();
```

#### Update

```ts
// Update a record
const success = await api.slice(12345)
                         .update({id: 1, first: 'James', last: 'Kirk'})
                         .id().exec();

// Update multiple rows with where
await api.slice(12345)
         .update({evil: true})
         .where({first: 'Darth'})
         .ids().exec();
```

#### Delete

```ts
// Delete a record
const success = await api.slice(12345)
                         .delete(12345)
                         .id().exec();

// Dlete multiple rows with where
await api.slice(12345)
         .delete()
         .where({first: 'Darth'})
         .ids().exec();
```

</details>

<details>
  <summary>
    <h3 id="sockets" style="display: inline">Sockets</h3>
  </summary>

Sockets (enabled by default) can be turned off or configured to point to a custom URL
```ts
const api = new Api('https://spoke.auxiliumgroup.com', {
  socket: true, // Enable - Use default URL
  // socket: false, // Disable
  // socket: 'http://localhost:3000', // Enable - Custom socket server URL
});
```

#### Slice Engine

The Slice Engine's cache can be synced with the server & subscribed to using [RXJS](https://rxjs.dev):

```ts
// Create cache/store
const contacts = api.slice<Contact>(Slices.Contact);

// Enable syncing
contacts.sync();

// Use RXJS to listen for events
contacts.sync().pipe(...).subscribe((cache: Contact[]) => {...});
// Or using Angular templates
'{{ contacts.sync() | async }}'

// Disable syncing
contacts.sync(false);
```

#### Socket Events

Alternatively socket events can be listened to directly using callbacks:

```ts
api.socket.sliceEvents(123, callbackFn(event)); // Listen to a specific slice
api.socket.addListener(callbackFn(event)); // listen to all socket events
```

</details>

<details>
  <summary>
    <h3 id="uploads" style="display: inline">Uploads</h3>
  </summary>

#### Fetch File

```js
const url = api.files.get(12345);
```

#### Upload File

Uploading files to datalynk is done by first uploading the file as form-data & then creating a reference to the upload ID in a slice.

```js
// Get files from file input
const files = document.querySelector('#upload').files;

// Upload a file & associate it with a record in one call
api.files.upload(files, {slice: 12345, row: 1234, field: 'abc'});
// OR upload a file on its own
api.files.upload(files).then(uploaded => {
  // Associate file with a record manually
  const slice = 12345, row: 123;
  api.files.associate(uploaded.map(r => r.id), slice, row, 'field');
});

```

</details>

<details>
  <summary>
    <h3 id="webrtc" style="display: inline">WebRTC</h3>
  </summary>

Connect to a WebRTC chatroom to send audio/video to peers

```js
// Video elements to display stream on
const local = document.querySelector('#video1'); // <video id="video1" autoplay muted playsinline></video>
const remote = document.querySelector('#video2'); // <video id="video2" autoplay playsinline></video>

// Connect to a WebRTC room
const roomID = 'TEST_ROOM';
const session = await api.webrtc.connect(roomID, true, true); // audio = true, video = true

// Set local audio/video stream
local.srcObject = session.stream;

// Track is null on the initial connection, will fire again when the stream is ready
session.onConnected = (peer, stream) => {
  // Set remote audio/video stream if ready
  if(stream) remote.srcObject = stream;
}

// Disconnect from WebRTC room
session.disconnect();
```

</details>

## Request-owned API recovery (1.5.1)

When a request receives a server-side response failure (HTTP 5xx, malformed/non-JSON API response, or SQLSTATE error), the client now:

1. immediately sets detailed status to `unavailable` (`online === false`, `offline === true`),
2. retries the exact failed HTTP payload twice immediately with no artificial delay,
3. if both immediate retries fail, waits 30 seconds after the previous attempt finishes and retries the same payload again,
4. repeats that sequential 30-second recovery attempt until the exact request returns a valid successful API response,
5. only then returns to `online` and allows queued/normal traffic to resume.

The original failed caller rejects immediately after the client enters `unavailable`, allowing an offline-enabled `Slice` operation to fall back to its local IndexedDB cache while recovery continues in the background. Only one recovery request can be active at a time. Heartbeat/version checks cannot override request-owned recovery, and other normal requests are blocked from the network while recovery is active. HTTP 401 remains `unauthorized`, ordinary 4xx errors still reject without taking the whole client offline, and transport/fetch failures continue to use the network `offline` path.
