feat: init

This commit is contained in:
montaghanmy
2023-03-23 11:03:16 +01:00
commit 10fe6f78d1
11518 changed files with 509786 additions and 0 deletions
@@ -0,0 +1,36 @@
---
description: >-
This page will document all the services implemented in the new NodeJS
backend. For all the PHP services not yet migrated, please ask us directly on
https://community.twake.app/
---
# 🧱 Backend and APIs
## Twake Services
As a frontend developer / connector developer to read our APIs, or to understand our data models or just for fun, here is all the details over Twake backend services.
If document are empty, check out our Notion documentation: [https://www.notion.so/twake/Backend-documentation-e219323593d2401c9887d0e11b2a597b](https://www.notion.so/twake/Backend-documentation-e219323593d2401c9887d0e11b2a597b)
### Services
[users/](users/README.md)
[applications/](applications/README.md))
[channels/](channels/README.md))
[messages/](messages/README.md))
[notifications/](notifications/README.md))
## Get started to code in Twake
Want to edit Twake code ? Congratulation ! You participate in the development of a great product 😃
[start-working-into-a-service](intro/start-working-into-a-service.md)
[create-a-new-twake-service](intro/create-a-new-twake-service.md)
[intro/platform/](intro/platform/README.md))
@@ -0,0 +1,22 @@
---
description: How applications are managed in Twake backend
---
# 🍎 Applications
#### Description
**Applications** is everything related to connectors in Twake.
An application is described by an identity sheet containing this information:
- **Identity** \(name, description, logo\)
- **API** **preferences** \(Twake→Connector events endpoint, API id and secret, and Connector→Twake allowed IPs\)
- **Privileges and capabilities** \(List of things the connector can read and can write\)
- **Display information** \(Where the connector is visible, button in chat, configuration in application list etc\)
This is all we need to define a connector.
[database-models](database-models.md)
[rest-apis](rest-apis.md)
@@ -0,0 +1,130 @@
---
description: Application models for backend
---
# Database models
**applications**\
****Represent an application in the database
```javascript
{
//PK
"company_id": uuid;
"id": uuid;
"identity": ApplicationIdentity;
"api": ApplicationApi;
"access": ApplicationAccess;
"display": ApplicationDisplay;
"publication": ApplicationPublication;
"stats": ApplicationStatistics;
}
type ApplicationIdentity = {
name: string;
iconUrl: string;
description: string;
website: string;
categories: string[];
compatibility: "twake"[];
};
type ApplicationPublication = {
published: boolean;
};
type ApplicationStatistics = {
createdAt: number;
updatedAt: number;
version: number;
};
type ApplicationApi = {
hooksUrl: string;
allowedIps: string;
privateKey: string;
};
type ApplicationAccess = {
privileges: string[];
capabilities: string[];
hooks: string[];
};
type ApplicationDisplay = {
twake: {
"version": 1,
"files" : {
"preview": {
"url": "", //Url to preview file (full screen or inline)
"inline": true,
"main_ext": ["docx", "xlsx"], //Main extensions app can read
"other_ext": ["txt", "html"] //Secondary extensions app can read
},
"actions": [ //List of action that can apply on a file
{
"name": "string",
"id": "string"
}
]
},
//Chat plugin
"chat": {
"input": {
"icon": "", //If defined replace original icon url of your app
"type": "file" | "call" //To add in existing apps folder / default icon
},
"commands": [
{
"command": "mycommand", // my_app mycommand
"description": "fdsqfds"
}
],
"actions": [ //List of action that can apply on a message
{
"name": "string",
"id": "string"
}
]
},
//Allow app to appear as a bot user in direct chat
"direct": {
"name": "My app Bot",
"icon": "", //If defined replace original icon url of your app
},
//Display app as a standalone application in a tab
"tab": {
"url": ""
},
//Display app as a standalone application on the left bar
"standalone": {
"url": ""
},
//Define where the app can be configured from
"configuration": ["global", "channel"]
};
};
```
**company\_application**\
****Represent an application in a company
```javascript
{
"company_id": uuid;
"application_id": uuid;
"id": uuid;
"created_at": number;
"created_by": string; //Will be the default delegated user when doing actions on Twake
}
```
@@ -0,0 +1,6 @@
---
description: Rest API for application
---
# REST APIs
@@ -0,0 +1,40 @@
---
description: >-
Channels are topics in Twake, users can subscribe to them, can make them
private or can create them.
---
# 🎩 Channels and tabs
### Wording
**Member:** member in company without the tag "guest"
**Guest:** company member with the tag "guest"
**Channel member:** member in the channel
**Channels types:
→ Private:** Relative to a workspace but restricted to a defined group of members
**→ Public:** Relative to a workspace anyone can find the channel and join
**→ Direct:** Relative to a company, a discussion between a set of members but without defined topic
**To understand the routing** please read **"**Collections based rest api:" in "Wording and formating"
TODO [Wording and formatting](https://www.notion.so/Wording-and-formatting-24ef27e094a042aea4899ac6a8039dee)
### What it does
Channel component is **not** responsible of the red badge counter \(notification service is\).
Channel component **is** responsible of keeping track of the message from where the user need to start reading but can be override by notification service if there is a mention to start reading from.
TODO [Some process details and constant s](https://www.notion.so/Some-process-details-and-constant-s-fb5b2d4974da490aa87bb87082af8454)
### Models an APIs
[Database models](database-models.md)
TODO [REST Api / Websockets Api](https://www.notion.so/REST-Api-Websockets-Api-458b153a6a6e46c2925dfc1db3859d3b)
TODO [In/Out microservice](https://www.notion.so/In-Out-microservice-e721e72e542244a69ca3a913e0b405ad)
TODO [Activity models](https://www.notion.so/Activity-models-0fa3acb0a13f41fd98bc98709908eedf)
@@ -0,0 +1,152 @@
---
description: Database models of channel
---
# Database models
## Channels
#### **channels**
The main channel table, this table should only be used when changing things on the channel \(not frequently\) so we don't add counters or last\_activity or anything like that.
```javascript
{
//Primary key
"company_id": "uuid-v4",
"workspace_id": "string" // ("uuid-v4" | "direct")
"id": "uuid-v4",
//Content
"owner": "uuid-v4", //User-id of the channel owner (invisible but used on some access restriction-
"icon": "String",
"name": "String",
"description": "String",
"channel_group": "String",
"visibility": "private" | "public" | "direct"
"default": true | false, //The new members join by default this channel
"archived": false | true,
"archivation_date": 0, //Timestamp
}
```
#### **channel\_defaults**
Contain the default channels
```javascript
{
//Primary key
"company_id": "uuid-v4",
"workspace_id": "uuid-v4"
"id": "uuid-v4" //Channel id
}
```
#### **channel\_counters**
We use a separated table to manage counters for this channel. Currently this is not used to do statistics but can be used to this goal in the future.
```javascript
{
//Primary key
"company_id": "uuid-v4",
"workspace_id": "string", // "uuid-v4" | "direct"
"channel_id": "uuid-v4",
"type": "members" | "guests" | "messages",
"value": 0,
}
```
#### **channel\_last\_activity**
Store last channel activity for bold/not bold management
```javascript
{
//Primary key
"company_id": "uuid-v4",
"channel_id": "uuid-v4",
"last_activity": 0, //Timestamp in seconds
}
```
#### **direct\_channel\_identifiers**
This table is used to find an existing discussion with a group of members. The "identifier" is generated from the group of members.
```javascript
{
//Primary key
"company_id": "uuid-v4",
"identifier": "string", // ordered CSV list of user ids
"channel_id": "uuid-v4" //A way to find it in the channel table
}
```
## **Channels tabs**
#### **channel\_tabs**
Channels can have tabs that are connexion to other apps or different views.
```javascript
{
//Primary key
"company_id": "uuid-v4",
"workspace_id": "string", // "uuid-v4" | "direct"
"channel_id": "uuid-v4",
"tab_id": "uuid-v4",
"name": "String",
"configuration": JSON,
"application_id": "uuid-v4",
"owner": "uuid-v4",
"order": "string"
}
```
## **Channels members**
#### **channel\_members**
List of channels for an user
```javascript
{
//Primary key
"company_id": "uuid-v4", //Partition Key
"workspace_id": "string", // "uuid-v4" | "direct", //Clustering key
"user_id": "uuid-v4",
"channel_id": "uuid-v4",
"type": "member" | "guest" | "bot",
"last_access": 0, //Timestamp in seconds
"last_increment": 0, //Number
"favorite": false | true, //Did the user add this channel to its favorites
"notification_level": "all" | "none" | "group_mentions" | "user_mentions",
"expiration": false | Timestamp, //Member expiration in channel (only for guests)
}
```
#### **channel\_members\_reversed**
List of users in channel
**Not implemented:** We need to ensure this replication regularly \(on each user open channel\) ?
```javascript
{
//Primary key
"company_id": "uuid-v4",
"workspace_id": "string", // "uuid-v4" | "direct",
"channel_id": "uuid-v4",
"type": "guest" | "bot" | "member",
"user_id": "uuid-v4",
}
```
@@ -0,0 +1,26 @@
---
description: Drive on Twake
---
# 📁 Drive
## description
**Drive** or **Documents** is the Nodejs drive implementation for twake, it contains drive items.
A **Drive item** can be:
- a file
- a directory
- the root folder
- the trash
## wording
**version** each file in the drive has a version associated to each update.
## Models and APIs
[database-models](database-models.md)
[rest-api](rest-apis.md)
@@ -0,0 +1,86 @@
---
description: Documents database models
---
# Database models
**DriveFile**
```javascript
{
// Primary Key
"company_id": uuid;
"id": uuid;
"parent_id": string;
"is_in_trash": boolean;
"is_directory": boolean;
"name": string;
"extension": string;
"description": string;
"tags": string[];
"added": string;
"last_modified": string;
"access_info": AccessInformation;
"content_keywords": string;
"hidden_data": unknown;
"last_version_cache": Partial<FileVersion>;
}
type AccessInformation = {
public: {
token: string;
level: publicAccessLevel;
};
entities: AuthEntity[];
};
type AuthEntity = {
type: "user" | "channel" | "company" | "folder";
id: string | "parent";
level: publicAccessLevel | DriveFileAccessLevel;
};
```
**FileVersion**
```javascript
{
"id": string;
"provider": "internal" | "drive" | string;
"file_id": string;
"file_metadata": DriveFileMetadata;
"date_added": number;
"creator_id": string;
"application_id": string;
"realname": string;
"key": string;
"mode": string | "OpenSSL-2";
"file_size": number;
"filename": string;
"data": unknown;
}
type DriveFileMetadata = {
source: "internal" | "drive" | string;
external_id: string;
name?: string;
mime?: string;
size?: number;
thumbnails?: DriveFileThumbnail;
};
type DriveFileThumbnail = {
index: number;
id: string;
type: string;
size: number;
width: number;
height: number;
url: string;
full_url?: string;
};
```
@@ -0,0 +1,360 @@
---
description: Documents API
---
# Authentication
All the following routes require the usual authentication header. But you can also use other ways of authentication:
- For the download routes, you can use a token generated by the `/internal/services/documents/v1/companies/:company_id/download/token` route (see bellow).
- All the routes can use a query string `?public_token=token` to authenticate the user.
- All the routes can use a query string `?twake_tab_token=token` to authenticate the user in the context of a channel tab for instance.
# Navigation and drive capabilities
## Fetch a drive item
Used to fetch a drive item
**URL** : `/internal/services/documents/v1/companies/:company_id/item/:id`
**Method** : `GET`
**Auth required** : Yes
### Success Response
**Code** : `200 OK`
**Content example**
```javascript
{
item: {
id: string;
company_id: string;
parent_id: string;
in_trash: boolean;
is_directory: string;
name: string;
extension: string;
description: string;
tags: [];
added: string;
last_modified: string;
last_version_cache: DriveItemVersion;
access_info: DriveItemAccessInfo;
}
versions: {
id: string;
provider: string | "drive" | "internal";
file_id: string;
file_metadata: FileMetadata;
date_added: number;
creator_id: string;
application_id: string;
}
[];
children: {
id: string;
company_id: string;
parent_id: string;
in_trash: boolean;
is_directory: string;
name: string;
extension: string;
description: string;
tags: [];
added: string;
last_modified: string;
last_version_cache: DriveItemVersion;
access_info: DriveItemAccessInfo;
}
[];
}
```
### Error Responses
If the item cannot be fetched the server will return an error with one of the following status codes:
- 401 Unauthorized - The user is not authorized.
- 500 Internal Server Error - An error occurred while performing the operation.
## Create a drive item
Used to create a drive item
**URL** : `/internal/services/documents/v1/companies/:company_id/item`
**Method** : `POST`
**Headers**: `Content-Type: application/json` OR `Content-Type: multipart/form-data`
**Auth required** : Yes
**Data constraints**
```javascript
{
item: {
id: string;
company_id: string;
parent_id: string;
in_trash: boolean;
is_directory: string;
name: string;
extension: string;
description: string;
tags: [];
added: string;
last_modified: string;
last_version_cache: DriveItemVersion;
access_info: DriveItemAccessInfo;
},
version: {
id: string;
provider: string | 'drive' | 'internal';
file_id: string;
file_metadata: FileMetadata;
date_added: number;
creator_id: string;
application_id: string;
},
file?: File // The multipart/form-data file to be uploaded ( optional )
}
```
### Success Response
**Code** : `200 OK`
### Error Responses
If the request is missing required fields or the item cannot be created, the server will return an error with one of the following status codes:
- 400 Bad Request - The request is missing required fields.
- 401 Unauthorized - The user is not authorized.
- 500 Internal Server Error - An error occurred while performing the operation.
## Update a drive item
Used to update a drive item
**URL** : `/internal/services/documents/v1/companies/:company_id/item/:id`
**Method** : `POST`
**Auth required** : Yes
**Data constraints**
```javascript
{
id: string;
company_id: string;
parent_id: string;
in_trash: boolean;
is_directory: string;
name: string;
extension: string;
description: string;
tags: [];
added: string;
last_modified: string;
last_version_cache: DriveItemVersion;
access_info: DriveItemAccessInfo;
}
```
### Success Response
**Code** : `200 OK`
### Error Responses
If the request is missing required fields or the item cannot be updated, the server will return an error with one of the following status codes:
- 400 Bad Request - The request is missing required fields.
- 401 Unauthorized - The user is not authorized.
- 500 Internal Server Error - An error occurred while performing the operation.
## Delete a drive item
Used to delete a drive item
**URL** : `/internal/services/documents/v1/companies/:company_id/item/:id`
**Method** : `DELETE`
**Auth required** : Yes
### Success Response
**Code** : `200 OK`
## Create a drive item version
Used to create a drive item version
**URL** : `/internal/services/documents/v1/companies/:company_id/item/:id/version`
**Method** : `POST`
**Auth required** : Yes
**Data constraints**
```javascript
{
id: string;
provider: string | 'drive' | 'internal';
file_id: string;
file_metadata: {
source: 'internal' | 'drive' | string;
external_id: string | any;
name?: string;
mime?: string;
size?: number;
thumbnails?: {
index: number;
id: string;
type: string;
size: number;
width: number;
height: number;
url: string;
full_url?: string;
}[];
};
date_added: number;
creator_id: string;
application_id: string;
}
```
### Success Response
**Code** : `200 OK`
# Download
## Get a download token
Before to download, if you can't pass an authorisation token (for example in the browser context) you can generate a token that you will pass in the query string to download the file using the next routes.
**URL** : `/internal/services/documents/v1/companies/:company_id/item/download/token?items=id1,id2,id3&version_id:optional_id`
**Method** : `GET`
**Auth required** : Yes
### Success Response
```
{
"token": string
}
```
## Download
Shortcut to download a file (you can also use the file-service directly).
If the item is a folder, a zip will be automatically generated.
**URL** : `/internal/services/documents/v1/companies/:company_id/item/:id/download?version_id=:optional_id`
**Method** : `GET`
**Auth required** : Yes
## Zip download
Used to create a zip archive containing the requested drive items ( files and folders ).
**URL** : `/internal/services/documents/v1/companies/:company_id/item/download/zip?items=id1,id2,id3`
**Method** : `GET`
**Auth required** : Yes
# Tabs (for Twake)
If you want to use the Twake tabs, you must store the configuration of the tabs in the database.
## Get tab configuration
Get a tab configuration to get the attached folder/document id.
**URL** : `/internal/services/documents/v1/companies/:company_id/tabs/:id`
**Method** : `GET`
**Auth required** : Yes
### Success Response
```
{
"company_id": string;
"tab_id": string;
"channel_id": string;
"item_id": string;
"level": "read" | "write";
}
```
## Set tab configuration
Get a tab configuration to get the attached folder/document id.
**URL** : `/internal/services/documents/v1/companies/:company_id/tabs/:id`
**Method** : `POST`
**Auth required** : Yes
**Data constraints** :
```
{
"company_id": string;
"tab_id": string;
"channel_id": string;
"item_id": string;
"level": "read" | "write";
}
```
### Success Response
```
{
"company_id": string;
"tab_id": string;
"channel_id": string;
"item_id": string;
"level": "read" | "write";
}
```
@@ -0,0 +1,34 @@
---
description: File on Twake
---
# 📄 Files
## description
**Files** is everything related to file upload in Twake after the migration to Node.js. Note that the Twake Drive isn't part of this migration because it will be replaced by Linshare.
Twake Files upload support chunk upload and file encryption.
## Wording
**File:** document to upload, no constraint on the type of document \(image, text, pdf ..\)
**Chunk:** Large file are split in multiple chunk for the upload process
## Encryption
Files and Storage services in Twake feature encryption at rest in **aes-256-cbc**.
Each file is encrypted with two layers:
- A file encryption key and iv stored in database and different for each file.
- A global encryption key and iv used in addition to the previous one and equal for each file.
## Models and APIs
[database-models](database-models.md)
## Miscellaneous
[resumablejs](resumablejs.md)
@@ -0,0 +1,40 @@
---
description: File database models
---
# Database models
* **files** The main file object in database
```javascript
javascript
{
//Primary key: [["company_id"], "id"]
"company_id": "uuid-v4",
"id": "uuid-v4",
"user_id": "string",
"application_id": "string",
"updated_at": "number",
"created_at":"number
"upload_data": (json){
"size": number, //Total file size
"chunks": number, //Number of chunks
}
"metadata": (json){
"name": "string", //File name
"mime": "type/subtype",
}
"thumbnails": (json) { //Url to thumbnail (or set it to undefined if no relevant)
"index": "string",
"id": "uuid-v4",
"type": "string",
"size": "number,
"width": number, //Thumbnail width (for images only)
"height": number, //Thumbnail height (for images only)
}
}
```
@@ -0,0 +1,25 @@
---
description: How we use resumable on Twake
---
# Resumable.js
**Link to the official documentation:** [http://resumablejs.com](http://resumablejs.com/)
## What is Resumable.js
::: info
Using Resumable is not mandatory to upload a file to Twake. You can do it manually, see REST APIs page.
:::
Its a JavaScript library providing multiple simultaneous, stable and resumable uploads via the HTML5 File API.
The library is designed to introduce fault-tolerance into the upload of large files through HTTP. This is done by splitting each files into small chunks; whenever the upload of a chunk fails, uploading is retried until the procedure completes. This allows uploads to automatically resume uploading after a network connection is lost either locally or to the server. Additionally, it allows for users to pause, resume and even recover uploads without losing state.
Resumable.js does not have any external dependencies other the `HTML5 File API`. This is relied on for the ability to chunk files into smaller pieces. Currently, this means that support is limited to Firefox 4+ and Chrome 11+.
## What Resumable.js does
In Frontend side resumable split the file in multiple small chunks. For the upload process resumable ask the server with an HTTP GET request to know if the chunk requested is saved on the server side. If yes, resumable ask for the next chunk. If not, resumable send an HTTP POST request with the chunk to the server.
With POST request from resumable, there is multiple information fields contains for exemple the number of chunk, the chunk size, the total size of the file, an unique identifier for the file, and the name of the file.
@@ -0,0 +1,11 @@
---
description: Get started with Twake service development
---
# 🛠 Twake service development
[start-working-into-a-service](start-working-into-a-service.md)
[create-a-new-twake-service](create-a-new-twake-service.md)
[platform/](platform/README.md))
@@ -0,0 +1,173 @@
---
description: >-
If you are here, you probably have a very great idea for Twake, like adding a
brand new feature into Twake, maybe a coffee maker service ? ☕️
---
# Create a new service
::: info
Please ensure you read the [Start working into a service](start-working-into-a-service.md) before
:::
To create a new component, a new folder must be created under the `src/services` one and an `index.ts` file must export the a class. This class will be instantiated by the platform and will be linked to the required services automatically.
In order to illustrate how to create a component, let's create a fake Notification service.
1. Create the folder `src/services/notification`
2. Create an `index.ts` file which exports a `NotificationService` class
```javascript
// File src/services/notification/index.ts
import { TwakeService } from "../../core/platform/framework";
import NotificationServiceAPI from "./api.ts";
export default class NotificationService extends TwakeService<NotificationServiceAPI> {
version = "1";
name = "notification";
service: NotificationServiceAPI;
api(): NotificationServiceAPI {
return this.service;
}
}
```
1. Our `NotificationService` class extends the generic `TwakeService` class and we defined the `NotificationServiceAPI` as its generic type parameter. It means that in the platform, the other components will be able to retrieve the component from its name and then consume the API defined in the `NotificationServiceAPI` interface and exposed by the `api` method.
We need to create this `NotificationServiceAPI` interface which must extend the `TwakeServiceProvider` from the platform like:
```javascript
// File src/services/notification/api.ts
import { TwakeServiceProvider } from "../../core/platform/framework/api";
export default interface NotificationServiceAPI extends TwakeServiceProvider {
/**
* Send a message to a list of recipients
*/
send(message: string, recipients: string[]): Promise<string>;
}
```
1. Now that the interfaces are defined, we need to create the `NotificationServiceAPI` implementation \(this is a dummy implementation which does nothing but illustrates the process\):
```javascript
// File src/services/notification/services/api.ts
import NotificationServiceAPI from "../api";
export class NotificationServiceImpl implements NotificationServiceAPI {
version = "1";
async send(message: string, recipients: string[]): Promise<string> {
return Promise.resolve(`${message} sent`);
}
}
```
1. `NotificationServiceImpl` now needs to be instanciated from the `NotificationService` class since this is where we choose to keep its reference and expose it. There are several places which can be used to instanciate it, in the constructor itself, or in one of the `TwakeService` lifecycle hooks. The `TwakeService` abstract class has several lifecycle hooks which can be extended by the service implementation for customization pusposes:
2. `public async doInit(): Promise<this>;` Customize the `init` step of the component. This is generally the place where services are instanciated. From this step, you can retrieve services consumed by the current component which have been already initialized by the platform.
3. `public async doStart(): Promise<this>;` Customize the `start` step of the component. You have access to all other services which are already started.
```javascript
// File src/services/notification/index.ts
import { TwakeService } from "../../core/platform/framework";
import NotificationServiceAPI from "./api.ts";
import NotificationServiceImpl from "./services/api.ts";
export default class NotificationService extends TwakeService<NotificationServiceAPI> {
version = "1";
name = "notification";
service: NotificationServiceAPI;
api(): NotificationServiceAPI {
return this.service;
}
public async doInit(): Promise<this> {
this.service = new NotificationServiceImpl();
return this;
}
}
```
1. Now that the service is fully created, we can consume it from any other service in the platform. To do this, we rely on Typescript decorators to define the links between components. For example, let's say that the a `MessageService` needs to call the `NotificationServiceAPI`, we can create the link with the help of the `@Consumes` decorator and get a reference to the `NotificationServiceAPI` by calling the `getProvider` on the component context like:
```javascript
import { TwakeService, Consumes } from "../../core/platform/framework";
import MessageServiceAPI from "./providapier";
import NotificationServiceAPI from "../notification/api";
@Consumes(["notification"])
export default class MessageService extends TwakeService<MessageServiceAPI> {
public async doInit(): Promise<this> {
const notificationService = this.context.getProvider<NotificationServiceAPI>("notification");
// You can not call anything defined in the NotificationServiceAPI interface from here or from inner services by passing down the reference to notificationService.
}
}
```
**Configuration**
The platform and services configuration is defined in the `config/default.json` file. It uses [node-config](https://github.com/lorenwest/node-config) under the hood and to configuration file inheritence is supported in the platform.
The list of services to start is defined in the `services` array like:
```javascript
{
"services": ["auth", "user", "channels", "webserver", "websocket", "database", "realtime"]
}
```
Then each service can have its own configuration block which is accessible from its service name i.e. `websocket` service configuration is defined in the `websocket` element like:
```javascript
{
"services": ["auth", "user", "channels", "webserver", "websocket", "orm"],
"websocket": {
"path": "/socket",
"adapters": {
"types": [],
"redis": {
"host": "redis",
"port": 6379
}
}
}
}
```
On the component class side, the configuration object is directly accessible from the `configuration` property like:
```javascript
export default class WebSocket extends TwakeService<WebSocketAPI> {
async doInit(): Promise<this> {
// get the "path" value, defaults to "/socket" if not defined
const path = this.configuration.get < string > ("path", "/socket");
// The "get" method is generic and can accept custom types like
const adapters =
this.configuration.get < AdaptersConfiguration > "adapters";
}
}
interface AdaptersConfiguration {
types: Array<string>;
redis: SocketIORedis.SocketIORedisOptions;
}
```
::: info
After creating a new service, you can add controllers, business services and entities, go back to the [What is a service section](start-working-into-a-service.md)
:::
## Create a new technical service
Now you are bringing things a step further, you are going to add new core services in Twake, like for instance a new database connector or encryption system.
Creating a new core service is as easy as creating a functional service. But it must be in `src/core/platform/services` .
You can read the [complete list of existing technical services here](platform/README.md)) .
@@ -0,0 +1,247 @@
---
description: >-
List of core shared components in Twake backend, available in
src/core/platform/services
---
# Platform/Technical services
## **Database Technical Service**
Twake uses a custom ORM to work with both MongoDB and CassandraDB/ScyllaDB.
::: info
This paragraph is not ready yet, you can contribute to this documentation on our Github!
:::
## **Realtime Technical Service**
The framework provides simple way to create CRUD Services which will notify clients using Websockets by following some conventions:
1. The service must implement the `CRUDService` generic interface
2. In order to push notification to clients, Typescript decorators must be added on methods \(Create, Update, Delete methods\)
For example, let's say that we want to implement a CRUD Service for `messages`:
```javascript
import {
CRUDService,
CreateResult,
DeleteResult,
UpdateResult,
EntityId,
} from "@/core/platform/framework/api/crud-service";
class Message {
text: string;
createdAt: Date;
author: string;
}
class MessageService implements CRUDService<Message> {
async create(item: Message): Promise<CreateResult<Message>> {
// save the message then return a CreateResult instance
}
async get(id: EntityId): Promise<Message> {
// get the message from its ID and return it
}
async update(id: EntityId, item: Message): Promise<UpdateResult<Message>> {
// update the message with the given id, patch it with `item` values
// then return an instance of UpdateResult
}
async delete(id: EntityId): Promise<DeleteResult<Message>> {
// delete the message from its id then return a DeleteResult instance
}
async list(): Promise<Message[]> {
// get a list of messages
}
}
```
By implementing the CRUD service following the API, we can now add realtime message to clients connected to Websocket on a selected collection of CRUD operations. For example, if we want to just notify when a `message` is created, we just have to add the right decorator on the `create` method like:
```javascript
import { RealtimeCreated } from "@/core/platform/framework/decorators";
// top and bottom code removed for clarity
class MessageService implements CRUDService<Message> {
@RealtimeCreated<Message>({
room: "/messages",
path: message => `/messages/${message.id}`
})
async create(item: Message): Promise<CreateResult<Message>> {
// save the message then return a CreateResult instance
const created: Message = new Message(/* */);
return new CreateResult<Message>("message", created)
}
// ...
}
```
The `RealtimeCreated` decorator will intercept the `create` call and will publish an event in an internal event bus with the creation result, the input data and the `"/messages"` path. On the other side of the event bus, an event listener will be in charge of delivering the event to the right Websocket clients as described next.
The Realtime\* decorators to add on methods to intercept calls and publish data are all following the same API. A decorator takes two parameters as input:
- First one is the room name to publish the notification to \(`/messages` in the example above\)
- Second one is the full path of the resource linked to the action \(`message => /messages/${message.id}` in the example above\)
Both parameters can take a string or an arrow function as parameter. If arrow function is used, the input parameter will be the result element. By doing this, the paths can be generated dynamically at runtime.
## **Websocket Technical Service**
Services annotated as described above automatically publish events to WebSockets. Under the hood, it uses Socket.IO rooms to send events to the right clients.
**Authentication**
- Client have to provide a valid JWT token to be able to connect and be authenticated by providing it as string in the `authenticate` event like `{ token: "the jwt token value" }`
- If the JWT token is valid, the client will receive a `authenticated` event
- If the JWT token is not valid or empty, the client will receive a `unauthorized` event
**Example**
```javascript
const io = require("socket.io-client");
// Get a JWT token from the Twake API first
const token =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOjEsImlhdCI6MTYwMzE5ODkzMn0.NvQoV9KeWuTNzRvzqbJ5uZCQ8Nmi2rCYQzcKk-WsJJ8";
const socket = io.connect("http://localhost:3000", { path: "/socket" });
socket.on("connect", () => {
socket
.emit("authenticate", { token })
.on("authenticated", () => {
console.log("User Authenticated");
})
.on("unauthorized", (err) => {
console.log("User is not authorized", err);
});
});
socket.on("disconnected", () => console.log("Disconnected"));
```
**Joining rooms**
CRUD operations on resources are pushing events in Socket.io rooms. In order to receive events, clients must subscribe to rooms by sending an `realtime:join` on an authenticated socket with the name of the room to join and with a valid JWT token like `{ name: "room name", token: "the jwt token for this room" }`: Users can not subscribe to arbitratry rooms, they have to be authorized to.
As a result, the client will receive events:
- `realtime:join:success` when join is succesful with data containing the name of the linked room like `{ name: "room" }`.
- `realtime:join:error` when join failed with data containing the name of the linked room and the error details like `{ name: "room", error: "some error message" }`.
**Example**
```javascript
const io = require("socket.io-client");
const token =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOjEsImlhdCI6MTYwMzE5ODkzMn0.NvQoV9KeWuTNzRvzqbJ5uZCQ8Nmi2rCYQzcKk-WsJJ8";
const socket = io.connect("http://localhost:3000", { path: "/socket" });
socket.on("connect", () => {
socket
.emit("authenticate", { token })
.on("authenticated", () => {
// join the /channels room
socket.emit("realtime:join", { name: "/channels", token: "twake" });
socket.on("realtime:join:error", (message) => {
// will fire when join does not provide a valid token
console.log("Error on join", message);
});
// will be fired on each successful join.
// As event based, this event is not linked only to the join above
// but to all joins. So you have to dig into the message to know which one
// is successful.
socket.on("realtime:join:success", (message) => {
console.log("Successfully joined room", message.name);
});
})
.on("unauthorized", (err) => {
console.log("Unauthorized", err);
});
});
socket.on("disconnected", () => console.log("Disconnected"));
```
**Leaving rooms**
The client can leave the room by emitting a `realtime:leave` event with the name of the room given as `{ name: "the room to leave" }`.
As a result, the client will receive events:
- `realtime:leave:success` when leave is succesful with data containing the name of the linked room like `{ name: "room" }`.
- `realtime:leave:error` when leave failed with data containing the name of the linked room and the error details like `{ name: "room", error: "some error message" }`.
Note: Asking to leave a room which has not been joined will not fire any error.
**Example**
```javascript
socket.on("connect", () => {
socket.emit("authenticate", { token }).on("authenticated", () => {
// leave the "/channels" room
socket.emit("realtime:leave", { name: "/channels" });
socket.on("realtime:leave:error", (message) => {
// will fire when join does not provide a valid token
console.log("Error on leave", message);
});
// will be fired on each successful leave.
// As event based, this event is not linked only to the leave above
// but to all leaves. So you have to dig into the message to know which one
// is successful.
socket.on("realtime:leave:success", (message) => {
console.log("Successfully left room", message.name);
});
});
});
```
**Subscribe to resource events**
Once the given room has been joined, the client will receive `realtime:resource` events with the resource linked to the event as data:
```javascript
{
"action": "created",
"room": "/channels",
"type": "channel",
"path": "/channels/5f905327e3e1626399aaad79",
"resource": {
"name": "My channel",
"id": "5f905327e3e1626399aaad79"
}
```
**Example:**
```javascript
socket.on("connect", () => {
socket
.emit("authenticate", { token })
.on("authenticated", () => {
// join the "/channels" room
socket.emit("realtime:join", { name: "/channels", token: "twake" });
// will only occur when an action occured on a resource
// and if and only if the client joined the room
// in which the resource is linked
socket.on("realtime:resource", (event) => {
console.log("Resource has been ${event.action}", event.resource);
});
})
.on("unauthorized", (err) => {
console.log("Unauthorized", err);
});
});
```
@@ -0,0 +1,245 @@
---
description: >-
List of core shared components in Twake backend, available in
src/core/platform/services
---
# Platform/Technical services
## **Database Technical Service**
Twake uses a custom ORM to work with both MongoDB and CassandraDB/ScyllaDB.
[database-orm-platform-service](database-orm-platform-service.md)
## **Realtime Technical Service**
The framework provides simple way to create CRUD Services which will notify clients using Websockets by following some conventions:
1. The service must implement the `CRUDService` generic interface
2. In order to push notification to clients, Typescript decorators must be added on methods \(Create, Update, Delete methods\)
For example, let's say that we want to implement a CRUD Service for `messages`:
```javascript
import {
CRUDService,
CreateResult,
DeleteResult,
UpdateResult,
EntityId,
} from "@/core/platform/framework/api/crud-service";
class Message {
text: string;
createdAt: Date;
author: string;
}
class MessageService implements CRUDService<Message> {
async create(item: Message): Promise<CreateResult<Message>> {
// save the message then return a CreateResult instance
}
async get(id: EntityId): Promise<Message> {
// get the message from its ID and return it
}
async update(id: EntityId, item: Message): Promise<UpdateResult<Message>> {
// update the message with the given id, patch it with `item` values
// then return an instance of UpdateResult
}
async delete(id: EntityId): Promise<DeleteResult<Message>> {
// delete the message from its id then return a DeleteResult instance
}
async list(): Promise<Message[]> {
// get a list of messages
}
}
```
By implementing the CRUD service following the API, we can now add realtime message to clients connected to Websocket on a selected collection of CRUD operations. For example, if we want to just notify when a `message` is created, we just have to add the right decorator on the `create` method like:
```javascript
import { RealtimeCreated } from "@/core/platform/framework/decorators";
// top and bottom code removed for clarity
class MessageService implements CRUDService<Message> {
@RealtimeCreated<Message>({
room: "/messages",
path: message => `/messages/${message.id}`
})
async create(item: Message): Promise<CreateResult<Message>> {
// save the message then return a CreateResult instance
const created: Message = new Message(/* */);
return new CreateResult<Message>("message", created)
}
// ...
}
```
The `RealtimeCreated` decorator will intercept the `create` call and will publish an event in an internal event bus with the creation result, the input data and the `"/messages"` path. On the other side of the event bus, an event listener will be in charge of delivering the event to the right Websocket clients as described next.
The Realtime\* decorators to add on methods to intercept calls and publish data are all following the same API. A decorator takes two parameters as input:
- First one is the room name to publish the notification to \(`/messages` in the example above\)
- Second one is the full path of the resource linked to the action \(`message => /messages/${message.id}` in the example above\)
Both parameters can take a string or an arrow function as parameter. If arrow function is used, the input parameter will be the result element. By doing this, the paths can be generated dynamically at runtime.
## **Websocket Technical Service**
Services annotated as described above automatically publish events to WebSockets. Under the hood, it uses Socket.IO rooms to send events to the right clients.
**Authentication**
- Client have to provide a valid JWT token to be able to connect and be authenticated by providing it as string in the `authenticate` event like `{ token: "the jwt token value" }`
- If the JWT token is valid, the client will receive a `authenticated` event
- If the JWT token is not valid or empty, the client will receive a `unauthorized` event
**Example**
```javascript
const io = require("socket.io-client");
// Get a JWT token from the Twake API first
const token =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOjEsImlhdCI6MTYwMzE5ODkzMn0.NvQoV9KeWuTNzRvzqbJ5uZCQ8Nmi2rCYQzcKk-WsJJ8";
const socket = io.connect("http://localhost:3000", { path: "/socket" });
socket.on("connect", () => {
socket
.emit("authenticate", { token })
.on("authenticated", () => {
console.log("User Authenticated");
})
.on("unauthorized", (err) => {
console.log("User is not authorized", err);
});
});
socket.on("disconnected", () => console.log("Disconnected"));
```
**Joining rooms**
CRUD operations on resources are pushing events in Socket.io rooms. In order to receive events, clients must subscribe to rooms by sending an `realtime:join` on an authenticated socket with the name of the room to join and with a valid JWT token like `{ name: "room name", token: "the jwt token for this room" }`: Users can not subscribe to arbitratry rooms, they have to be authorized to.
As a result, the client will receive events:
- `realtime:join:success` when join is succesful with data containing the name of the linked room like `{ name: "room" }`.
- `realtime:join:error` when join failed with data containing the name of the linked room and the error details like `{ name: "room", error: "some error message" }`.
**Example**
```javascript
const io = require("socket.io-client");
const token =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOjEsImlhdCI6MTYwMzE5ODkzMn0.NvQoV9KeWuTNzRvzqbJ5uZCQ8Nmi2rCYQzcKk-WsJJ8";
const socket = io.connect("http://localhost:3000", { path: "/socket" });
socket.on("connect", () => {
socket
.emit("authenticate", { token })
.on("authenticated", () => {
// join the /channels room
socket.emit("realtime:join", { name: "/channels", token: "twake" });
socket.on("realtime:join:error", (message) => {
// will fire when join does not provide a valid token
console.log("Error on join", message);
});
// will be fired on each successful join.
// As event based, this event is not linked only to the join above
// but to all joins. So you have to dig into the message to know which one
// is successful.
socket.on("realtime:join:success", (message) => {
console.log("Successfully joined room", message.name);
});
})
.on("unauthorized", (err) => {
console.log("Unauthorized", err);
});
});
socket.on("disconnected", () => console.log("Disconnected"));
```
**Leaving rooms**
The client can leave the room by emitting a `realtime:leave` event with the name of the room given as `{ name: "the room to leave" }`.
As a result, the client will receive events:
- `realtime:leave:success` when leave is succesful with data containing the name of the linked room like `{ name: "room" }`.
- `realtime:leave:error` when leave failed with data containing the name of the linked room and the error details like `{ name: "room", error: "some error message" }`.
Note: Asking to leave a room which has not been joined will not fire any error.
**Example**
```javascript
socket.on("connect", () => {
socket.emit("authenticate", { token }).on("authenticated", () => {
// leave the "/channels" room
socket.emit("realtime:leave", { name: "/channels" });
socket.on("realtime:leave:error", (message) => {
// will fire when join does not provide a valid token
console.log("Error on leave", message);
});
// will be fired on each successful leave.
// As event based, this event is not linked only to the leave above
// but to all leaves. So you have to dig into the message to know which one
// is successful.
socket.on("realtime:leave:success", (message) => {
console.log("Successfully left room", message.name);
});
});
});
```
**Subscribe to resource events**
Once the given room has been joined, the client will receive `realtime:resource` events with the resource linked to the event as data:
```javascript
{
"action": "created",
"room": "/channels",
"type": "channel",
"path": "/channels/5f905327e3e1626399aaad79",
"resource": {
"name": "My channel",
"id": "5f905327e3e1626399aaad79"
}
```
**Example:**
```javascript
socket.on("connect", () => {
socket
.emit("authenticate", { token })
.on("authenticated", () => {
// join the "/channels" room
socket.emit("realtime:join", { name: "/channels", token: "twake" });
// will only occur when an action occured on a resource
// and if and only if the client joined the room
// in which the resource is linked
socket.on("realtime:resource", (event) => {
console.log("Resource has been ${event.action}", event.resource);
});
})
.on("unauthorized", (err) => {
console.log("Unauthorized", err);
});
});
```
@@ -0,0 +1,69 @@
# Database ORM platform service
## How to use it ?
A. Create an entity and put it anywhere in the code
```typescript
import { Entity, Column } from "../../../core/platform/services/database/services/orm/decorators";
@Entity("my_entity", {
primaryKey: [["company_id"], "id"], //Primary key, see Cassandra documentation for more details
type: "my_entity",
})
export class MyEntity {
@Type(() => String)
@Column("company_id", "uuid", { generator: "uuid" })
company_id: string;
@Column("workspace_id", { generator: "uuid" })
id: string;
@Column("text", "encoded")
text: string;
}
```
B. Create a repository to manage entities
```typescript
const database: DatabaseServiceAPI = ...;
const repository = await database.getRepository("my_entity", MyEntity);
const newEntity = new MyEntity();
newEntity.company_id = "";
newEntity.text = "";
await repository.save(newEntity);
const entities = await repository.find({company_id: "", id: ""});
const entity = await repository.findOne({company_id: "", id: ""});
await repository.remove({company_id: "", id: ""});
```
## FAQ
#### I set a column to a type but I get an other type on code. Why for two identical definitions it created fields of different types?
It depends on what database you use \(mongo or scylladb\) for development. Here is the process for each:
Scylla:
* on startup it creates the tables with the requested types, in this case twake\_boolean =&gt; tinyint on scylla side
* on save entity it will convert the node type \(boolean\) to the good cql request: "{bool: false}" =&gt; "SET bool = 0", it happens in the transformValueToDbString method
* on find entity it will convert the database raw value \(a tinyint\) to the nodejs type \(boolean\): 1 =&gt; true, 0 =&gt; false.
Mongo:
* on startup it does nothing \(mongo don't need to initialise columns
* on save entity it will create a document, it means in mongo we just store json for each entity, there is no really a column concept.
* on find entity we just get back the saved json and map it to the entity in node.
Even if mongo just store json directly from mongo, we sometime do some changes to the data before to save in mongo, it will also be in the typeTransforms.ts file.
So what could have happened in you case ?
* \(1\) if you use mongodb and we did not enforce the type before to save to mongo, then maybe you used a string instead of a boolean at some point in time while working and mongo just saved it as it was \(without checking the requested type on entity\)
* \(2\) other possibility is that we incorrectly get the information from the database on the typeTransforms.ts file, from cassandra for instance I think we don't convert tinyint back to clean boolean, so you could get 0 and 1 instead of false and true. And maybe instead of 0 and 1 sometime undefined values can convert to ''.
* To fix all this just enforce the types in typeTransforms.ts for the twake\_boolean type.
@@ -0,0 +1,48 @@
---
description: >-
You want to add new routes in an existing service, for instance add a feature
to our channel service ? You are in the right place !
---
# What is a service in Twake ?
The backend is developed using a **software component approach** in order to compose and adapt the platform based on needs and constraints. The current section describes this approach, and how to extend it by creating new components.
The platform has the following properties:
- A platform is composed of multiple components
- A component has an unique name in the platform
- A component can provide a `service`
- A component can consume `services` from other components
- A component has a lifecycle composed of several states: `ready`, `initialized`, `started`, `stopped`
- A component lifecycle changes when a lifecycle event is triggered by the platform: `init`, `start`, `stop`
- By creating links between components \(service producers and consumers\), components lifecycles **are also linked together**: A component going from `ready` to `initialized` will wait for all its dependencies to be in `initialized` state. This is automatically handled by the platform.
The platform currently have some limitations:
- Components can not have cyclic dependencies: if `component X` requires a component which requires `component X` directly or in one of its dependencies, the platform will not start
- Components can only have local dependencies.
## Discover what is in a service
To unfold the internal ways of services in Twake, we will follow a simple request journey into our framework.
1. The requests starts from Twake Frontend or Postman for instance,
2. it then goes to a controller which validate the request parameters and extract them for the services,
3. the services uses the given parameters to get/set entities in database and returns a proper reply.
### /web/controllers : where everything starts
This is where you declare the routing you want to use.
### /services : where the magic happen
This is where you work for real, calling databases, sending websockets events, using tasks pushers etc.
### /entities : where we keep the data
If you store data, you must define its data model and how it is stored in our database middleware.
::: danger
This document is not finished, you can contribute to it on our Github.
:::
@@ -0,0 +1,49 @@
# Knowledge-graph
## Twake to Knowledge-graph events
Twake can send events to the knowledge graph following this documentation:
[Events from Twake](./pushed-from-twake.md)
## Knowledge-graph to Twake REST
The knowledge graph can send us event at the following endpoint:
`POST https://api.twake.app/internal/services/knowledge-graph/v1/push`
Authorized by a Token authorization header:
`Authorization: Token {some token defined together}`
And with the following data in JSON:
```
{
events: [KnowledgeGraphCallbackEvent, KnowledgeGraphCallbackEvent, KnowledgeGraphCallbackEvent, ...]
}
type KnowledgeGraphCallbackEvent = {
recipients: {
type: "user";
id: string; // KG user id which is a md5 of the email
}[];
event: {
type: "user_tags"; //More events will be added later
data: {
//For user_tags event only
tags?: {
value: string;
weight: number;
}[];
};
};
};
```
The reply will be if everything was alright:
```
{
"status": "success"
}
```
@@ -0,0 +1,172 @@
# Knowledge-graph
## Knowledge-graph to Twake events
[Send events to Twake](./README.md)
## Events sent by twake to the knowledge graph
### User created / updated
```typescript
{
records: [
{
key: "null",
value: {
id: "User",
properties: {
_kg_user_id: string; //Console wide user id (based on the console id), prefer to use this one
_kg_email_id: string; //Console wide user id (based on the email md5)
_kg_company_all_id: string[]; //All console wide companies_ids for this user
user_id: string; //Twake internal user id
company_id: string; //Company internal id
user_created_at: number;
user_last_activity: string;
email: string;
first_name: string;
last_name: string;
},
},
},
]
}
```
### Channel created / updated
```typescript
{
records: [
{
key: "null",
value: {
id: "Channel",
properties: {
_kg_user_id: string; //Console wide user id (based on the console id), prefer to use this one
_kg_email_id: string; //Console wide user id (based on the email md5)
_kg_company_id: string; //Console wide company id
channel_id: string;
channel_name: string;
channel_owner: string;
workspace_id: string;
company_id: string;
},
},
},
],
}
```
### Message created / updated
```typescript
{
records: [
{
key: "null",
value: {
id: "Message",
properties: {
_kg_user_id: string; //Console wide user id (based on the console id), prefer to use this one
_kg_email_id: string; //Console wide user id (based on the email md5)
_kg_company_id: string; //Console wide company id
message_thread_id: message.thread_id,
message_content: string; //Empty if user requested not to share data to KG
message_created_at: number;
message_updated_at: number;
type_message: string;
company_id: string; //Internal to Twake company id
workspace_id: string;
channel_id: string;
user_id: string; //Internal to Twake user id
},
},
},
],
}
```
### Company created / updated
```typescript
{
records: [
{
key: "null",
value: {
id: "Company",
properties: {
_kg_company_id: string; //Console wide company id
company_id: string; //Internal to Twake company id
company_name: string;
},
},
},
],
}
```
### Workspace created / updated
```typescript
{
records: [
{
key: "null",
value: {
id: "Workspace",
properties: {
_kg_company_id: string; //Console wide company id
company_id: string; //Internal to Twake company id
workspace_id: string; //Internal to Twake workspace id
workspace_name: string;
},
},
},
],
}
```
## Sending events to Twake from the KG
Twake listen at at:
`POST https://domain/internal/services/knowledge-graph/v1/push`
Authorised by a Token authorization header:
`Authorization: Token {some token defined together}`
And with the following data in JSON:
```typescript
{
events: [KnowledgeGraphCallbackEvent, KnowledgeGraphCallbackEvent, KnowledgeGraphCallbackEvent, ...]
}
type KnowledgeGraphCallbackEvent = {
recipients: {
type: "user";
id: string; // KG user id which is a md5 of the email
}[];
event: {
type: "user_tags"; //More events will be added later
data: {
//For user_tags event only
tags?: {
value: string;
weight: number;
}[];
};
};
};
```
The reply will be if everything was alright:
```typescript
{
"status": "success"
}
```
@@ -0,0 +1,8 @@
---
description: Message api
---
# 💬 Messages
Document still on Notion: [https://www.notion.so/twake/Backend-documentation-e219323593d2401c9887d0e11b2a597b](https://www.notion.so/twake/Backend-documentation-e219323593d2401c9887d0e11b2a597b)
@@ -0,0 +1,6 @@
---
description: Message database models
---
# Database models
@@ -0,0 +1,6 @@
---
description: Notifications on Twake
---
# 📲 Notifications
@@ -0,0 +1,59 @@
---
description: Notification database model
---
# Database models
#### **channel\_members\_notification\_preferences**
```javascript
{
//Primary key
"company_id": "uuid-v4",
"channel_id": "uuid-v4",
"user_id": "uuid-v4",
"preferences": "all" | "mentions" | "me" | "none",
"last_read": 16000000, //Timestamp in seconds
}
```
#### **channel\_thread\_users**
```javascript
{
//Primary key
"company_id": "uuid-v4",
"channel_id": "uuid-v4",
"thread_id": "uuid-v4",
"user_id": "uuid-v4"
}
```
#### **user\_notifications\_badges \(this is not a counter like scylladb counters here, just classic table\)**
```javascript
{
//Primary key
"company_id": "uuid-v4", (partition key)
"user_id": "uuid-v4", (clustering key)
"workspace_id": "uuid-v4", (clustering key)
"channel_id": "uuid-v4", (clustering key)
"thread_id": "uuid-v4", (clustering key)
}
```
#### **user\_notifications\_general\_preferences**
```javascript
{
//Primary key
"company_id": "uuid-v4",
"user_id": "uuid-v4",
"workspace_id": "uuid-v4",
"preferences": PreferencesObject
}
```
@@ -0,0 +1,14 @@
---
description: Tags on Twake
---
# 📁 Tags
## description
**Tags** is everything related to tafs in Twake after the migration to Node.js.
## Models and APIs
[database-models](database-models.md)
[rest-api](rest-apis.md)
@@ -0,0 +1,18 @@
---
description: Documents database models
---
# Database models
**DriveFile**
```javascript
{
// Primary Key
"company_id": uuid;
"tag_id": string;
"name": string;
"colour": string;
}
```
@@ -0,0 +1,123 @@
---
description: Tags API
---
## GET a tag
Used to get a tag
**URL** : `/internal/services/tags/v1/companies/:company_id/tags/:tag_id`
**Method** : `GET`
**Auth required** : Yes
### Success Response
**Code** : `200 OK`
**Content example**
```javascript
{
{
company_id: string,
tag_id: string,
name: string,
colour: string,
};
}
```
## LIST a tags
Used to get a tag
**URL** : `/internal/services/tags/v1/companies/:company_id/tags`
**Method** : `LIST`
**Auth required** : Yes
### Success Response
**Code** : `200 OK`
**Content example**
```javascript
{
{
company_id: string,
tag_id: string,
name: string,
colour: string,
}[];
}
```
## Create a tag
Used to create a tag
**URL** : `/internal/services/tags/v1/companies/:company_id/tags`
**Method** : `POST`
**Auth required** : Yes
**Owner/admin right required** : Yes
**Data constraints**
```javascript
{
name: string,
colour: string,
}
```
### Success Response
**Code** : `201 CREATED`
## Update a tag
Used to update a tag
**URL** : `/internal/services/tags/v1/companies/:company_id/tag/:tag_id`
**Method** : `POST`
**Auth required** : Yes
**Owner/admin right required** : Yes
**Data constraints**
```javascript
{
name: string,
colour: string,
}
```
### Success Response
**Code** : `201 OK`
## Delete a tag
Used to delete a tag
**URL** : `/internal/services/tags/v1/companies/:company_id/tags/:tag_id`
**Method** : `DELETE`
**Auth required** : Yes
**Owner/admin right required** : Yes
### Success Response
**Code** : `200 OK`
@@ -0,0 +1,8 @@
---
description: How users and workspaces are managed in backend
---
# 👥 Users and workspaces
Document still on notion: [https://www.notion.so/twake/Backend-documentation-e219323593d2401c9887d0e11b2a597b](https://www.notion.so/twake/Backend-documentation-e219323593d2401c9887d0e11b2a597b)
@@ -0,0 +1,9 @@
---
description: Get started with Twake frontend.
---
# Storybook
Twake components are available through our Storybook. You can find it here:
[https://web.twake.app/storybook/index.html](https://web.twake.app/storybook/index.html)
@@ -0,0 +1,6 @@
---
description: Not documented yet.
---
# MediumPopupManager
@@ -0,0 +1,6 @@
---
description: Not documented yet.
---
# MenuManager
@@ -0,0 +1,44 @@
---
description: >-
A beautiful, centered, medium modal with all you need to structure its
content.
---
# ObjectModal
#### Usage
```jsx
import ObjectModal from "components/ObjectModal/ObjectModal.js";
```
```jsx
<ObjectModal
title={<ObjectModalTitle>My awesome title !</ObjectModalTitle>}
onClose={() => myComponent.closeSomething()}
disabled
footer={<Button>Definitly not usefull</Button>}
>
<FirstChild />
<span>Some text</span>
</ObjectModal>
```
####
#### Props
| **name** | **Description** | **Type** | **Default** |
| ------------ | -------------------------------------------------------------- | --------- | ----------- |
| **disabled** | Disable scrollbar X axis (need to rename for a better clarity) | Boolean | false |
| **footer** | Define a footer component | ReactNode | null |
| **onClose** | Add close icon in the component, waiting for a function | Function | null |
| **title** | _Define a title component_ | ReactNode | null |
####
#### Preview
![Modal example with task editor](../../../assets/screenshot-2020-07-15-at-17.00.48.png)
---
@@ -0,0 +1,32 @@
---
description: Icon with sub heading for ObjectModal component. Perfect before a form field.
---
# ObjectModalFormTitle
#### Usage
```jsx
import ObjectModalFormTitle from 'components/ObjectModal/ObjectModal.js';
```
```jsx
<ObjectModalFormTitle
name={'Awesome Title!'}
icon="star"
/>
```
####
#### Props
| **name** | **Description** | **Type** | **Default** |
| :--- | :--- | :--- | :--- |
| **className** | Use predefined classes for the title | string | null |
| **icon** | Define icon for the title | string | null |
| **name** | Define the title name | string | null |
| **style** | Define a custom style for the title | object | null |
@@ -0,0 +1,34 @@
---
description: Section title, perfect after a separator.
---
# ObjectModalSectionTitle
#### Usage
```jsx
import ObjectModalFormTitle from "components/ObjectModal/ObjectModal.js";
```
```jsx
<ObjectModalFormTitle name={"Awesome Title!"} />
```
####
#### Props
| **name** | **Description** | **Type** | **Default** |
| ------------- | ------------------------------------ | -------- | ----------- |
| **className** | Use predefined classes for the title | string | null |
| **icon** | Define icon for the title | string | null |
| **name** | Define the title name | string | null |
| **style** | Define a custom style for the title | object | null |
####
#### Preview
![](../../../assets/capture-decran-de-2020-07-17-17-58-03.png)
---
@@ -0,0 +1,29 @@
---
description: Separate your component sections with a simple line.
---
# ObjectModalSeparator
#### Usage
```jsx
import ObjectModalSeparator from "components/ObjectModal/ObjectModal.js";
```
```jsx
<ObjectModalSeparator />
```
####
#### Props
| Name | **Description** | **Type** | **Default** |
| :--: | :----------------------------: | :------: | :---------: |
| | no property for this component | | |
####
#### Preview
![ObjectModalSeparator example in the ChannelWorkspaceEditor](../../../assets/capture-decran-de-2020-07-17-17-05-36.png)
@@ -0,0 +1,34 @@
---
description: Main title for the ObjectModal component.
---
# ObjectModalTitle
#### Usage
```jsx
import ObjectModalTitle from "components/ObjectModal/ObjectModal.js";
```
```jsx
<ObjectModalTitle className="my-awesome-class" style={{ marginBottom: 0 }}>
My awesome title !
</ObjectModalTitle>
```
####
#### Props
| **name** | **Description** | **Type** | **Default** |
| ------------- | ------------------------------------ | -------- | ----------- |
| **className** | Use predefined classes for the title | string | null |
| **style** | Define a custom style for the title | object | null |
####
#### Preview
![](../../../assets/capture-decran-de-2020-07-17-17-48-56.png)
---
@@ -0,0 +1,43 @@
---
description: Create a table
---
# Table
#### Usage
```jsx
import Table from 'components/Table/Table.js';
```
```jsx
<Table />
```
####
#### Props
| **name** | **Description** | **Type** | **Default** |
| :--- | :--- | :--- | :--- |
| **columns** | | ReactNode | null |
| **onAdd** | If defined a add button appear and clicking on it call this callback. | Function | null |
| **addText** | Button content | ReactNode | null |
| **onSearch** | Called when search bar is changed. Return a Promise that resolve with a list of users. Argument is \(query: string, maxResult: number\)=&gt;{return new Promise\(...\)} | Function | null |
| **onRequestNextPage, onRequestPreviousPage** | \(token: string, maxResult: number\)=&gt;{return new Promise\(...\)} | Function | null |
| **rowKey** | \(row\)=&gt;return row.id | Function | null |
**Functions \(callable on the component from parent\)**
| **name** | **Description** | **Type** |
| :--- | :--- | :--- |
| **newElements\(elements\)** | Add new rows at the beginning of the table. | Function |
**Internal implementation**
* If no data in table, set loading to true, call onRequestNextPage without offset token.
* If next page button is clicked, get last row token, set Table to loading, and requestNextPage.
* When recieve result in requestNextPage, set loading to false and update data.
* Table must memorize already loaded pages to be faster
* Table must detect when request a page and no result is returned in this case disable corresponding button.
@@ -0,0 +1,6 @@
---
description: Not documented yet?
---
# UserListManager
@@ -0,0 +1,34 @@
---
description: Here is the list of our middlewares and their usages.
---
# 📚 Our stack
Write an article describing our stack at Twake composed of:
- In full mode a docker containing: node, react behind nginx, elasticsearch, scylladb, redis, rabbitmq
- In simple mode a docker containing: node, react and mongodb for db and search
### Simple mode
The simple mode of Twake messaging app is designed for quick setup and development purposes. It is a more straightforward mode that uses a single container to deploy the components. The technical stack of this mode includes the following components:
- Node: A JavaScript runtime environment used to develop server-side applications.
- React: A JavaScript library used to build user interfaces.
- MongoDB: A cross-platform document-oriented database used to store and retrieve data.
The simple mode is great for companies that want a quick and easy setup for their messaging app. It requires less technical expertise and is ideal for small to medium-sized businesses.
### Full mode
The full mode of Twake messaging app is designed to handle large-scale production loads with more than 1000 active users. It uses a container-based approach to deploy the components and ensure scalability. The technical stack of this mode includes the following components:
- Node: A JavaScript runtime environment used to develop server-side applications.
- React: A JavaScript library used to build user interfaces.
- Nginx: A web server used as a reverse proxy to distribute incoming requests to the appropriate backend service.
- Elasticsearch: A distributed search and analytics engine used to perform advanced search operations.
- Scylladb: A NoSQL database used to store large amounts of structured and unstructured data.
- Redis: An in-memory data structure store used to implement caching, messaging, and pub/sub functionalities.
- Rabbitmq: An open-source message broker used to transmit messages between applications.
The full mode is ideal for companies with large-scale deployment requirements but requires more technical expertise to install and maintain the infrastructure.
@@ -0,0 +1,25 @@
---
description: Want to translate Twake ?
---
# 🎭 Translation
## Translation process
Twake is built for everyone. That means we support languages that user want. If we do not already support your language, or you find a mistake in the translation, you have a simply way to do that.
Send us a mail to this address saying that you want to help us on the translation : [sales@twake.app](mailto:sales@twake.app). After, you'll have to follow the bellow steps.
## Join weblate server
At Twake, we use [weblate](https://hosted.weblate.org/), a tool that allow you to translate Twake without working on the code. After you sent an email, we'll invite you to the project. Inside the Twake project you can find [twake-web-frontend](https://hosted.weblate.org/projects/twake/twake-web-frontend/) that contains all translation for the web version.
## Start to translate
After choosing the language you want to contribute, you can click on categories that need some work \(like `Not translated strings` . Now you can start to translate : you have to fulfill the input You can see `Other languages` tab to give you more context about the string. After, just click on save and it is done !
![](<../assets/image-(6).png>)
## Thank you!
Twake is build by and for the community. Your commitment is highly appreciate! In reward, we can offer you a year on our SaaS version. You just have to contact us through this email : [sales@twake.app](mailto:sales@twake.app)
@@ -0,0 +1,85 @@
---
description: >-
Global guidelines for any new projects around Twake, Frontend and Backend
guidelines are discussed here.
---
# 🎨 Twake Ecosystem Guidelines
## Frontend guidelines
### Logo, UI and UX guidelines
#### Logos
{% file src="../assets/logo\_shape.svg" caption="Logo shape in SVG format" %}
#### Colors and fonts
The fonts and colors to use are defined in the document bellow, scroll down for the hexadecimal codes of each color.
![](../assets/screenshot-2021-03-31-at-14.51.23.png)
Colors code extracted from the Twake theme [https://github.com/linagora/Twake/blob/main/twake/frontend/src/app/theme.less](https://github.com/linagora/Twake/blob/main/twake/frontend/src/app/theme.less)
```css
// Circular Std = France
// Helvetica Neue = Vietnam
// TT Norms = Russia
// --- Twake fonts --- //
@main-font: "Circular Std", -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue",
"Apple Color Emoji", Arial, sans-serif, "Segoe UI Emoji", "Segoe UI Symbol";
// --- Twake colors --- //
@primary: #3840f7;
@primary-background: #3842f723;
@primary-hover: #3850f7;
@secondary: #0d0f38;
@black: #000000;
@black-alpha-50: #18295288;
@black-alpha-70: #18295255;
@grey-background: #f5f5f7;
@grey-light: #eeeeee;
@grey-dark: #6a6a6a;
@error: #ff5154;
@success: #04aa11;
@warning: #ff8607;
@white: #fff;
```
#### Icons and emojis \(⚠️ not validated by UX designer yet\)
Twake is currently using feather icons [https://feathericons.com/](https://feathericons.com/) and can fallback to Material Icons or FontAwesome.
Emojis on web must use the Apple emojis set preferably. On device, prefer to use the device emoji set.
### Frameworks and component system
#### Languages and frameworks
We recommend **TypeScript** and **VueJS** for any new projects around Twake. \(But Twake itself currently uses ReactJS with typescript.\)
#### **Components system**
We strongly recommend using Antd design system: [https://ant.design/](https://ant.design/) for 3 reasons:
1. We want to **differ from Material** UI that is too recognisable
2. Antd is very customisable, and **we provide a Twake theme here:** [**https://github.com/linagora/Twake/blob/main/twake/frontend/src/app/theme.less**](https://github.com/linagora/Twake/blob/main/twake/frontend/src/app/theme.less)\*\*\*\*
3. Antd contain more components than Material UI
### Libraries for common use cases
Feel free to add any library in this table.
| Use case | used by | prefered library |
| :-------------- | :----------------- | :------------------------------------------------------- |
| Infinite scroll | Twake message feed | [https://virtuoso.dev/](https://virtuoso.dev/) \(React\) |
## Backend guidelines
### Programmation languages
### Databases and middlewares