@neuralinnovations/dataisland-sdk 0.0.1-dev3 → 0.0.1-dev4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,96 @@
1
1
  # DataIsland Client SDK
2
2
 
3
- ```typescript
3
+ The DataIsland Client SDK is a TypeScript library designed to seamlessly integrate DataIsland web services into websites.
4
4
 
5
+ ## Table of contents
5
6
 
7
+ 1. [Connect](#connect)
8
+ 2. [Create app](#create-app)
9
+ 3. [Use organizations](#use-organizations)
10
+ 4. [Use chat](#use-chat)
11
+ 5. [Use workspaces](#use-workspaces)
12
+ 6. [Use files](#use-files)
13
+ 7. [Use access groups](#use-access-groups)
14
+ 8. [Use invites](#use-invites)
6
15
 
7
- ```
16
+ ### Connect
17
+
18
+ For connecting this library to your website project simply install it using npm package manager.
19
+
20
+ `npm i @neuralinnovations/dataisland-sdk`
21
+
22
+ ### Create app
23
+
24
+ You can initialize default app sdk instance using this code example.
25
+
26
+ ```
27
+ const app = await appSdk("your-app-name", async (builder: AppBuilder) => {
28
+ builder.useHost(HOST)
29
+ builder.useCredential(new BearerCredential(TOKEN))
30
+ })
31
+ ```
32
+
33
+ It is immpossible to create more than one app sdk intance with same name.
34
+
35
+ **HOST** is a DataIsland API url which can be passed using environment file.
36
+
37
+ Second required parameter for builder is Credentials. It is recomended to use Bearer credentials instance and pass your user Auth0 **TOKEN** in order to get access to API.
38
+
39
+ You can also add requests middlewares with builder options.
40
+
41
+ ```
42
+ const app = await appSdk("your-app-name", async (builder: AppBuilder) => {
43
+ builder.useHost(YOUR_HOST)
44
+ builder.useAutomaticDataCollectionEnabled(false)
45
+ builder.useCredential(new BasicCredential("email", "password"))
46
+ builder.registerMiddleware(async (req, next) => {
47
+ req.headers.set("Your-header-name", "value")
48
+ return await next(req)
49
+ })
50
+ })
51
+ ```
52
+
53
+ ### Use organizations
54
+
55
+ Organization is a top data structure object, which represents a company or some group of people using our services.
56
+ It contains of users ( admin or regular ), workspaces with files ( folders with access control features ) and chats.
57
+
58
+ By default all user organizations are fetched with user profile during app sdk start. But if there are no organizations linked to the user, you should run Create organization flow. This flow requires organization name and description data, and also first workspace name and description data provided by user.
59
+
60
+ **NOTE** There are two types of DataIsland web api servers, public and private. On public servers users can create their own organization after first registration, on private servers you must register using invite links.
61
+
62
+ Default organization creation code example:
63
+
64
+ ```
65
+ // create organization
66
+ const org = await app.organizations.create(
67
+ "your-organization-name",
68
+ "your-organization-description"
69
+ )
70
+ ```
71
+
72
+ ### Use workspaces
73
+
74
+ Workspaces are folder-like objects used to store files and controll access to it using acces groups. During creation you must pass organization Id, name and description of new workspace and regulation options. You can pass existing group ID or ask to create new group for this workspace in regulation section.
75
+
76
+ Default workspace creation example:
77
+
78
+ ```
79
+ const wsPromise = org.workspaces.create(
80
+ "your-workspace-name",
81
+ "your-workspace-description",
82
+ regulation: {
83
+ isCreateNewGroup: boolean - "Bool option for new group creation"
84
+ newGroupName: string - "New group name"
85
+ groupIds: string[] - "Array of selected accessed groups IDs"
86
+ }
87
+ )
88
+ ```
89
+
90
+ ### Use files
91
+
92
+ ### Use chat
93
+
94
+ ### Use access groups
95
+
96
+ ### Use Invites
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neuralinnovations/dataisland-sdk",
3
- "version": "0.0.1-dev3",
3
+ "version": "0.0.1-dev4",
4
4
  "description": "SDK for DataIsland project",
5
5
  "licenses": [
6
6
  {
package/src/disposable.ts CHANGED
@@ -9,7 +9,8 @@ export interface Disposable {
9
9
  * Represents a lifetime.
10
10
  */
11
11
  export class Lifetime {
12
- constructor(private readonly container: DisposableContainer) {}
12
+ constructor(private readonly container: DisposableContainer) {
13
+ }
13
14
 
14
15
  /**
15
16
  * Define a new nested disposable to this lifetime.
@@ -147,5 +148,3 @@ export function disposable(action: () => void, target: unknown): Disposable {
147
148
  action.call(target)
148
149
  })
149
150
  }
150
-
151
- export const eternalLifetime = new DisposableContainer().lifetime
@@ -1,5 +1,5 @@
1
1
  import { WorkspaceId } from "../storages/workspaces"
2
- import { FileId } from "../storages/files"
2
+ import { FileId } from "../storages/file"
3
3
 
4
4
  export interface WorkspaceProfileDto {
5
5
  name: string
@@ -26,7 +26,7 @@ export interface FileProgressDto {
26
26
  file_parts_count: number
27
27
  completed_parts_count: number
28
28
  success: boolean
29
- error: string
29
+ error?: string
30
30
  }
31
31
 
32
32
  export interface FileDto {
package/src/events.ts CHANGED
@@ -1,25 +1,25 @@
1
1
  import { type Disposable, DisposableContainer } from "./disposable"
2
2
 
3
- export interface Input<ET, DT> {
4
- type?: ET
5
- data: DT
3
+ export interface Input<EventType, DataType> {
4
+ type?: EventType
5
+ data: DataType
6
6
  }
7
7
 
8
- export interface Event<ET, DT> extends Input<ET, DT> {
8
+ export interface Event<EventType, DataType> extends Input<EventType, DataType> {
9
9
  unsubscribe: () => void
10
10
  }
11
11
 
12
- export interface EventSubscriber<ET, DT> {
13
- subscribe: (callback: (event: Event<ET, DT>) => void, type?: ET) => Disposable
12
+ export interface EventSubscriber<EventType, DataType> {
13
+ subscribe: (callback: (event: Event<EventType, DataType>) => void, type?: EventType) => Disposable
14
14
  }
15
15
 
16
- export class EventDispatcher<ET, DT> implements EventSubscriber<ET, DT> {
16
+ export class EventDispatcher<EventType, DataType> implements EventSubscriber<EventType, DataType> {
17
17
  private _listeners: Array<{
18
- callback: (value: Event<ET, DT>) => void
18
+ callback: (value: Event<EventType, DataType>) => void
19
19
  disposable: Disposable
20
20
  }> = []
21
21
 
22
- dispatch(input: Input<ET, DT>): void {
22
+ dispatch(input: Input<EventType, DataType>): void {
23
23
  this._listeners.slice().forEach(it => {
24
24
  const value = {
25
25
  type: input.type,
@@ -27,16 +27,16 @@ export class EventDispatcher<ET, DT> implements EventSubscriber<ET, DT> {
27
27
  unsubscribe: () => {
28
28
  it.disposable.dispose()
29
29
  }
30
- } satisfies Event<ET, DT>
30
+ } satisfies Event<EventType, DataType>
31
31
  it.callback(value)
32
32
  })
33
33
  }
34
34
 
35
- subscribe(callback: (event: Event<ET, DT>) => void, type?: ET): Disposable {
35
+ subscribe(callback: (event: Event<EventType, DataType>) => void, type?: EventType): Disposable {
36
36
  const container = new DisposableContainer()
37
37
  if (type !== undefined) {
38
38
  const cb = callback
39
- const listener = (evt: Event<ET, DT>): void => {
39
+ const listener = (evt: Event<EventType, DataType>): void => {
40
40
  if (evt.type === type) {
41
41
  cb(evt)
42
42
  }
package/src/index.ts CHANGED
@@ -7,6 +7,17 @@ export * from "./events"
7
7
  export * from "./disposable"
8
8
  export * from "./credentials"
9
9
  export * from "./appSdk"
10
+ export * from "./storages/organizations"
11
+ export * from "./storages/organization"
12
+ export * from "./storages/workspaces"
13
+ export * from "./storages/workspace"
14
+ export * from "./storages/groups"
15
+ export * from "./storages/userProfile"
16
+ export * from "./storages/files"
17
+ export * from "./storages/file"
18
+ export * from "./storages/filesPage"
19
+ export * from "./storages/chats"
20
+ export * from "./storages/chat"
10
21
 
11
22
  const _appsNotReady = new Map<string, Promise<AppSdk>>()
12
23
  const _appsReady = new Map<string, AppSdk>()
@@ -73,3 +84,6 @@ export async function appSdk(
73
84
  }
74
85
  return await appPromise
75
86
  }
87
+
88
+ export { File } from "./storages/file"
89
+ export { FilesPage } from "./storages/filesPage"
@@ -104,13 +104,6 @@ export class AppImplementation extends AppSdk {
104
104
  return new OrganizationService(context)
105
105
  })
106
106
 
107
- // register middlewares
108
- builder.registerMiddleware(async (req, next) => {
109
- req.headers.set("accept", "text/plain")
110
- req.headers.set("content-type", "application/json")
111
- return await next(req)
112
- })
113
-
114
107
  // call customer setup
115
108
  if (setup !== undefined) {
116
109
  await setup(builder)
@@ -154,7 +147,9 @@ export class AppImplementation extends AppSdk {
154
147
  const waitList: Array<Promise<void>> = []
155
148
  // call onRegister service's callback
156
149
  services.forEach(([serviceContext]) => {
157
- waitList.push(serviceContext.onRegister())
150
+ if (typeof serviceContext.onRegister === "function") {
151
+ waitList.push(serviceContext.onRegister())
152
+ }
158
153
  })
159
154
 
160
155
  // wait for all services to register
@@ -167,7 +162,9 @@ export class AppImplementation extends AppSdk {
167
162
  waitList.length = 0
168
163
  // call onStart service's callback
169
164
  services.forEach(([serviceContext]) => {
170
- waitList.push(serviceContext.onStart())
165
+ if (typeof serviceContext.onStart === "function") {
166
+ waitList.push(serviceContext.onStart())
167
+ }
171
168
  })
172
169
 
173
170
  // wait for all services to start
@@ -3,7 +3,7 @@ import { type Middleware } from "../middleware"
3
3
  import { type Disposable } from "../disposable"
4
4
 
5
5
  export class MiddlewareService extends Service {
6
- _middlewares: Middleware[] = []
6
+ private _middlewares: Middleware[] = []
7
7
 
8
8
  public useMiddleware(middleware: Middleware): Disposable {
9
9
  this._middlewares.push(middleware)
@@ -48,18 +48,42 @@ export class RequestBuilder {
48
48
  return this
49
49
  }
50
50
 
51
- public async sendPost(body?: BodyInit | null | object): Promise<Response> {
51
+ public async sendPostFormData(body: FormData): Promise<Response> {
52
52
  const url = this._url
53
+
54
+ // set search params
53
55
  url.search = this._searchParams.toString()
56
+
57
+ // create request
58
+ const req = new Request(url, {
59
+ method: "POST",
60
+ headers: this._headers,
61
+ body
62
+ })
63
+
64
+ // discard content type
65
+ const reqAny = req as any
66
+ reqAny.discardContentType = true
67
+
68
+ return await this._request(
69
+ req
70
+ )
71
+ }
72
+
73
+ public async sendPostJson(body: object | null | undefined): Promise<Response> {
74
+ const url = this._url
75
+ url.search = this._searchParams.toString()
76
+ let json: string | null | undefined = null
54
77
  if (body !== undefined && body !== null && typeof body === "object") {
55
- body = JSON.stringify(body)
78
+ json = JSON.stringify(body)
56
79
  }
80
+ const request = new Request(url, {
81
+ method: "POST",
82
+ headers: this._headers,
83
+ body: json
84
+ })
57
85
  return await this._request(
58
- new Request(url, {
59
- method: "POST",
60
- headers: this._headers,
61
- body
62
- })
86
+ request
63
87
  )
64
88
  }
65
89
 
@@ -85,17 +109,18 @@ export class RequestBuilder {
85
109
  )
86
110
  }
87
111
 
88
- public async sendPut(body?: BodyInit | null | object): Promise<Response> {
112
+ public async sendPutJson(body: object | null | undefined): Promise<Response> {
89
113
  const url = this._url
90
114
  url.search = this._searchParams.toString()
115
+ let json: string | null | undefined = null
91
116
  if (body !== undefined && body !== null && typeof body === "object") {
92
- body = JSON.stringify(body)
117
+ json = JSON.stringify(body)
93
118
  }
94
119
  return await this._request(
95
120
  new Request(url, {
96
121
  method: "PUT",
97
122
  headers: this._headers,
98
- body
123
+ body: json
99
124
  })
100
125
  )
101
126
  }
@@ -14,6 +14,7 @@ export interface RequestOptions {
14
14
  * RPC service.
15
15
  */
16
16
  export class RpcService extends Service {
17
+
17
18
  constructor(
18
19
  serviceContext: ServiceContext,
19
20
  /**
@@ -32,6 +33,22 @@ export class RpcService extends Service {
32
33
  }
33
34
  ) {
34
35
  super(serviceContext)
36
+
37
+ serviceContext.onRegister = async () => {
38
+ serviceContext.resolve(MiddlewareService)?.useMiddleware((req, next) => {
39
+ if (!req.headers.has("accept")) {
40
+ req.headers.set("accept", "text/plain")
41
+ }
42
+
43
+ if ((req as any).discardContentType) {
44
+ delete (req as any).discardContentType
45
+ } else {
46
+ req.headers.set("content-type", "application/json")
47
+ }
48
+
49
+ return next(req)
50
+ })
51
+ }
35
52
  }
36
53
 
37
54
  /**
@@ -87,35 +104,35 @@ export class RpcService extends Service {
87
104
  /**
88
105
  * Send a POST request.
89
106
  * @param path
90
- * @param body
107
+ * @param body JSON object
91
108
  * @param options
92
109
  */
93
110
  async post(
94
111
  path: string,
95
- body?: BodyInit | null,
112
+ body: object | null | undefined,
96
113
  options?: RequestOptions
97
114
  ): Promise<Response> {
98
115
  return this.requestBuilder(path)
99
116
  .searchParams(options?.searchParams)
100
117
  .headers(options?.headers)
101
- .sendPost(body)
118
+ .sendPostJson(body)
102
119
  }
103
120
 
104
121
  /**
105
122
  * Send a PUT request.
106
123
  * @param path
107
- * @param body
124
+ * @param body JSON object
108
125
  * @param options
109
126
  */
110
127
  async put(
111
128
  path: string,
112
- body?: BodyInit | null,
129
+ body: object | null | undefined,
113
130
  options?: RequestOptions
114
131
  ): Promise<Response> {
115
132
  return this.requestBuilder(path)
116
133
  .searchParams(options?.searchParams)
117
134
  .headers(options?.headers)
118
- .sendPut(body)
135
+ .sendPutJson(body)
119
136
  }
120
137
 
121
138
  /**
@@ -6,7 +6,8 @@ export class ServiceContext {
6
6
  constructor(
7
7
  public readonly context: Context,
8
8
  private readonly disposableContainer: DisposableContainer
9
- ) {}
9
+ ) {
10
+ }
10
11
 
11
12
  public get lifetime(): Lifetime {
12
13
  return this.disposableContainer.lifetime
@@ -16,15 +17,15 @@ export class ServiceContext {
16
17
  return this.context.resolve(type)
17
18
  }
18
19
 
19
- public async onRegister(): Promise<void> {
20
+ public onRegister: () => Promise<void> = async (): Promise<void> => {
20
21
  await Promise.resolve()
21
22
  }
22
23
 
23
- public async onStart(): Promise<void> {
24
+ public onStart: () => Promise<void> = async (): Promise<void> => {
24
25
  await Promise.resolve()
25
26
  }
26
27
 
27
- public onUnregister(): void {
28
+ public onUnregister: () => void = (): void => {
28
29
  // do nothing
29
30
  }
30
31
  }
@@ -42,5 +43,6 @@ export abstract class Service {
42
43
  return this.serviceContext.context
43
44
  }
44
45
 
45
- public constructor(private readonly serviceContext: ServiceContext) {}
46
+ public constructor(private readonly serviceContext: ServiceContext) {
47
+ }
46
48
  }
@@ -1,12 +1,5 @@
1
- import { EventDispatcher } from "../events"
2
-
3
1
  export type ChatId = string
4
2
 
5
- export enum ChatsEvent {
6
- ADDED = "added",
7
- REMOVED = "removed"
8
- }
9
-
10
3
  export enum ChatAnswer {
11
4
  SHORT = "short",
12
5
  LONG = "long"
@@ -26,12 +19,3 @@ export abstract class Chat {
26
19
  abstract question(message: string, answer?: ChatAnswer): Promise<void>
27
20
  }
28
21
 
29
- /**
30
- * Chats storage.
31
- */
32
- export abstract class Chats extends EventDispatcher<ChatsEvent, Chat> {
33
- /**
34
- * Create new chat.
35
- */
36
- abstract create(): Promise<Chat>
37
- }
@@ -0,0 +1,17 @@
1
+ import { EventDispatcher } from "../events"
2
+ import { Chat } from "./chat"
3
+
4
+ export enum ChatsEvent {
5
+ ADDED = "added",
6
+ REMOVED = "removed"
7
+ }
8
+
9
+ /**
10
+ * Chats storage.
11
+ */
12
+ export abstract class Chats extends EventDispatcher<ChatsEvent, Chat> {
13
+ /**
14
+ * Create new chat.
15
+ */
16
+ abstract create(): Promise<Chat>
17
+ }
@@ -2,8 +2,8 @@ import { Context } from "../context"
2
2
  import { Disposable } from "../disposable"
3
3
  import { FileDto, FileProgressDto } from "../dto/workspacesResponse"
4
4
  import { RpcService } from "../services/rpcService"
5
- import { File } from "./files"
6
5
  import { ResponseUtils } from "../services/responseUtils"
6
+ import { File } from "./file"
7
7
 
8
8
  export class FileImpl extends File implements Disposable {
9
9
  private _isDisposed: boolean = false
@@ -32,7 +32,7 @@ export class FileImpl extends File implements Disposable {
32
32
  }
33
33
 
34
34
  get name(): string {
35
- return <string>this._content?.id
35
+ return <string>this._content?.name
36
36
  }
37
37
 
38
38
  async url(): Promise<string> {
@@ -55,7 +55,7 @@ export class FileImpl extends File implements Disposable {
55
55
  async status(): Promise<FileProgressDto> {
56
56
  const response = await this.context
57
57
  .resolve(RpcService)
58
- ?.requestBuilder("api/v1/Files/url")
58
+ ?.requestBuilder("api/v1/Files/fetch")
59
59
  .searchParam("id", this.id)
60
60
  .sendGet()
61
61
 
@@ -63,6 +63,7 @@ export class FileImpl extends File implements Disposable {
63
63
  await ResponseUtils.throwError(`Failed to get file ${this.id}`, response)
64
64
  }
65
65
 
66
- return (await response!.json()).progress as FileProgressDto
66
+ const content = await response!.json()
67
+ return content.progress as FileProgressDto
67
68
  }
68
69
  }
@@ -0,0 +1,28 @@
1
+ import { FileProgressDto } from "../dto/workspacesResponse"
2
+
3
+ export type FileId = string
4
+
5
+ /**
6
+ * File.
7
+ */
8
+ export abstract class File {
9
+ /**
10
+ * File id.
11
+ */
12
+ abstract get id(): FileId
13
+
14
+ /**
15
+ * File name.
16
+ */
17
+ abstract get name(): string
18
+
19
+ /**
20
+ * Get temporary url.
21
+ */
22
+ abstract url(): Promise<string>
23
+
24
+ /**
25
+ * Get file status.
26
+ */
27
+ abstract status(): Promise<FileProgressDto>
28
+ }
@@ -1,12 +1,13 @@
1
1
  import { Context } from "../context"
2
2
  import { Disposable } from "../disposable"
3
3
  import { FileDto, FileListResponse } from "../dto/workspacesResponse"
4
- import { OrganizationService } from "../services/organizationService"
5
4
  import { RpcService } from "../services/rpcService"
6
5
  import { FileImpl } from "./file.impl"
7
- import { File, Files, FilesEvent, FilesList as FilesPage } from "./files"
6
+ import { Files, FilesEvent, UploadFile } from "./files"
8
7
  import { WorkspaceImpl } from "./workspace.impl"
9
8
  import { ResponseUtils } from "../services/responseUtils"
9
+ import { File } from "./file"
10
+ import { FilesPage } from "./filesPage"
10
11
 
11
12
  export class FilesPageImpl extends FilesPage implements Disposable {
12
13
  private _isDisposed: boolean = false
@@ -100,9 +101,16 @@ export class FilesImpl extends Files {
100
101
  page: number,
101
102
  limit: number
102
103
  ): Promise<FilesPage> {
104
+
105
+ // check page
103
106
  if (page === undefined || page === null) {
104
107
  throw new Error("File fetch, page is undefined or null")
105
108
  }
109
+ if (page < 0) {
110
+ throw new Error("File fetch, page is negative")
111
+ }
112
+
113
+ // check limit
106
114
  if (limit === undefined || limit === null) {
107
115
  throw new Error("File fetch, limit is undefined or null")
108
116
  }
@@ -110,23 +118,19 @@ export class FilesImpl extends Files {
110
118
  throw new Error("File fetch, limit is 0")
111
119
  }
112
120
 
113
- const orgService = this.context.resolve(OrganizationService)
114
-
115
- if (orgService === undefined) {
116
- throw new Error("File fetch, organization service undefined")
117
- }
118
-
121
+ // send request to the server
119
122
  const response = await this.context
120
123
  .resolve(RpcService)
121
124
  ?.requestBuilder("api/v1/Files/list")
122
125
 
123
126
  .searchParam("workspaceId", this.workspace.id)
124
- .searchParam("organizationId", orgService.organizations.current)
127
+ .searchParam("organizationId", this.workspace.organization.id)
125
128
  .searchParam("query", query)
126
129
  .searchParam("page", page.toString())
127
130
  .searchParam("limit", limit.toString())
128
131
  .sendGet()
129
132
 
133
+ // check response status
130
134
  if (ResponseUtils.isFail(response)) {
131
135
  await ResponseUtils.throwError(
132
136
  `Files fetch query:${query}, page:${page}, limit:${limit}, failed`,
@@ -134,57 +138,74 @@ export class FilesImpl extends Files {
134
138
  )
135
139
  }
136
140
 
141
+ // parse files from the server's response
137
142
  const files = (await response!.json()) as FileListResponse
138
143
 
144
+ // create files list
139
145
  const filesList = new FilesPageImpl()
140
146
  filesList.total = files.totalFilesCount
141
147
  filesList.filesPerPage = files.filesPerPage
142
148
  filesList.page = page
149
+
150
+ // init files from the server's response
143
151
  for (const fl of files.files) {
152
+
153
+ // create file implementation
144
154
  const file = new FileImpl(this.context).initFrom(fl)
145
155
 
156
+ // add file to the collection
146
157
  filesList.files.push(file)
147
158
 
159
+ // dispatch event, file added
148
160
  this.dispatch({
149
161
  type: FilesEvent.ADDED,
150
162
  data: file
151
163
  })
152
164
  }
153
165
 
166
+ // set files list
154
167
  this.filesList = filesList
155
168
 
156
169
  return filesList
157
170
  }
158
171
 
159
- async internalUpload(file: any): Promise<File> {
160
- const orgService = this.context.resolve(OrganizationService)
161
-
162
- if (orgService === undefined) {
163
- throw new Error("File load, organization service undefined")
172
+ async internalUpload(file: UploadFile): Promise<File> {
173
+ // check file
174
+ if (file === undefined || file === null) {
175
+ throw new Error("File upload, file is undefined or null")
164
176
  }
165
177
 
178
+ // form data to send
166
179
  const form = new FormData()
167
- form.append("organizationId", orgService.organizations.current)
180
+ form.append("organizationId", this.workspace.organization.id)
168
181
  form.append("workspaceId", this.workspace.id)
169
182
  form.append("name", file.name)
170
183
  form.append("file", file, file.name)
171
184
 
185
+ // send request to the server
172
186
  const response = await this.context
173
187
  .resolve(RpcService)
174
188
  ?.requestBuilder("api/v1/Files")
175
- .sendPost(form)
189
+ .sendPostFormData(form)
190
+
191
+ // check response status
176
192
  if (ResponseUtils.isFail(response)) {
177
- await ResponseUtils.throwError(`File upload ${file}`, response)
193
+ await ResponseUtils.throwError(`File upload ${file.name}`, response)
178
194
  }
195
+
196
+ // parse file from the server's response
179
197
  const result = (await response!.json()).file as FileDto
180
198
 
199
+ // create file implementation
181
200
  const fileImpl = new FileImpl(this.context).initFrom(result)
182
201
 
183
- this.filesList!.files.push(file)
202
+ // TODO: why is this here?
203
+ this.filesList?.files.push(fileImpl)
184
204
 
205
+ // dispatch event, file added
185
206
  this.dispatch({
186
207
  type: FilesEvent.ADDED,
187
- data: file
208
+ data: fileImpl
188
209
  })
189
210
 
190
211
  return fileImpl
@@ -1,39 +1,19 @@
1
- import { FileProgressDto } from '../dto/workspacesResponse'
2
- import { EventDispatcher } from '../events'
3
-
4
- export type FileId = string
1
+ import { EventDispatcher } from "../events"
2
+ import { File, FileId } from "./file"
3
+ import { FilesPage } from "./filesPage"
5
4
 
5
+ /**
6
+ * Files event.
7
+ */
6
8
  export enum FilesEvent {
7
- ADDED = 'added',
8
- REMOVED = 'removed'
9
+ ADDED = "added",
10
+ REMOVED = "removed"
9
11
  }
10
12
 
11
- export type UploadFile = File | Blob | string
12
-
13
13
  /**
14
- * File.
14
+ * Upload file.
15
15
  */
16
- export abstract class File {
17
- /**
18
- * File id.
19
- */
20
- abstract get id(): FileId
21
-
22
- /**
23
- * File name.
24
- */
25
- abstract get name(): string
26
-
27
- /**
28
- * Get temporary url.
29
- */
30
- abstract url(): Promise<string>
31
-
32
- /**
33
- * Get file status.
34
- */
35
- abstract status(): Promise<FileProgressDto>
36
- }
16
+ export type UploadFile = globalThis.File
37
17
 
38
18
  /**
39
19
  * Files storage.
@@ -42,7 +22,7 @@ export abstract class Files extends EventDispatcher<FilesEvent, File> {
42
22
  /**
43
23
  * Get file by id.
44
24
  */
45
- abstract upload(file: any): Promise<File>
25
+ abstract upload(file: UploadFile): Promise<File>
46
26
 
47
27
  /**
48
28
  * Delete file.
@@ -53,15 +33,6 @@ export abstract class Files extends EventDispatcher<FilesEvent, File> {
53
33
  /**
54
34
  * Query files.
55
35
  */
56
- abstract query(query: string, page: number, limit: number): Promise<FilesList>
36
+ abstract query(query: string, page: number, limit: number): Promise<FilesPage>
57
37
  }
58
38
 
59
- export abstract class FilesList {
60
- abstract get files(): File[]
61
-
62
- abstract get pages(): number
63
-
64
- abstract get total(): number
65
-
66
- abstract get page(): number
67
- }
@@ -0,0 +1,27 @@
1
+ import { File } from "./file"
2
+
3
+ /**
4
+ * Files page.
5
+ */
6
+ export abstract class FilesPage {
7
+
8
+ /**
9
+ * Get files.
10
+ */
11
+ abstract get files(): File[]
12
+
13
+ /**
14
+ * Get pages count.
15
+ */
16
+ abstract get pages(): number
17
+
18
+ /**
19
+ * Get total count.
20
+ */
21
+ abstract get total(): number
22
+
23
+ /**
24
+ * Get current page.
25
+ */
26
+ abstract get page(): number
27
+ }
@@ -73,12 +73,12 @@ export class GroupImpl extends Group implements Disposable {
73
73
  } catch (e) {
74
74
  console.error(e)
75
75
  }
76
-
76
+
77
77
  throw new Error(
78
78
  `Groups get workspaces, response is not ok, status: ${response?.status},${response?.statusText} ${text}`
79
79
  )
80
80
  }
81
-
81
+
82
82
  const workspaces = (await response.json()) as WorkspacesResponse
83
83
 
84
84
  return workspaces.workspaces
@@ -101,7 +101,7 @@ export class GroupImpl extends Group implements Disposable {
101
101
  const response = await this.context
102
102
  .resolve(RpcService)
103
103
  ?.requestBuilder("api/v1/AccessGroups/name")
104
- .sendPut({
104
+ .sendPutJson({
105
105
  groupId: this.id,
106
106
  name: name
107
107
  })
@@ -117,7 +117,7 @@ export class GroupImpl extends Group implements Disposable {
117
117
  const response = await this.context
118
118
  .resolve(RpcService)
119
119
  ?.requestBuilder("api/v1/AccessGroups/permits")
120
- .sendPut({
120
+ .sendPutJson({
121
121
  groupId: this.id,
122
122
  permits: permits
123
123
  })
@@ -138,7 +138,7 @@ export class GroupImpl extends Group implements Disposable {
138
138
  const response = await this.context
139
139
  .resolve(RpcService)
140
140
  ?.requestBuilder("api/v1/AccessGroups/workspaces")
141
- .sendPut({
141
+ .sendPutJson({
142
142
  groupId: this.id,
143
143
  actualWorkspaceIds: workspaces
144
144
  })
@@ -159,7 +159,7 @@ export class GroupImpl extends Group implements Disposable {
159
159
  const response = await this.context
160
160
  .resolve(RpcService)
161
161
  ?.requestBuilder("api/v1/AccessGroups/members")
162
- .sendPut({
162
+ .sendPutJson({
163
163
  groupId: this.id,
164
164
  memberIds: members
165
165
  })
@@ -227,12 +227,12 @@ export class GroupsImpl extends Groups {
227
227
  } catch (e) {
228
228
  console.error(e)
229
229
  }
230
-
230
+
231
231
  throw new Error(
232
232
  `Groups init, response is not ok, status: ${response?.status},${response?.statusText} ${text}`
233
233
  )
234
234
  }
235
-
235
+
236
236
  const groups = (await response.json()) as AccessGroupsResponse
237
237
 
238
238
  for (const gr of groups.groups){
@@ -258,7 +258,7 @@ export class GroupsImpl extends Groups {
258
258
  const response = await this.context
259
259
  .resolve(RpcService)
260
260
  ?.requestBuilder("api/v1/AccessGroups")
261
- .sendPost({
261
+ .sendPostJson({
262
262
  name: name,
263
263
  organizationId: organizationId,
264
264
  permits: permits,
@@ -333,5 +333,5 @@ export class GroupsImpl extends Groups {
333
333
  group.dispose()
334
334
  }
335
335
 
336
-
336
+
337
337
  }
@@ -130,7 +130,7 @@ export class OrganizationsImpl extends Organizations {
130
130
  const response = await this.context
131
131
  .resolve(RpcService)
132
132
  ?.requestBuilder("api/v1/Organizations")
133
- .sendPost({
133
+ .sendPostJson({
134
134
  profile: {
135
135
  name: name,
136
136
  description: description
@@ -72,7 +72,7 @@ export class WorkspaceImpl extends Workspace {
72
72
  const response = await this.context
73
73
  .resolve(RpcService)
74
74
  ?.requestBuilder("api/v1/Workspaces")
75
- .sendPut({
75
+ .sendPutJson({
76
76
  workspaceId: this.id,
77
77
  profile: {
78
78
  name,
@@ -1,6 +1,7 @@
1
1
  import { EventDispatcher } from "../events"
2
2
  import { Files } from "./files"
3
3
  import { WorkspaceId } from "./workspaces"
4
+ import { Organization } from "./organization"
4
5
 
5
6
  /**
6
7
  * Workspace event.
@@ -16,6 +17,11 @@ export abstract class Workspace extends EventDispatcher<
16
17
  WorkspaceEvent,
17
18
  Workspace
18
19
  > {
20
+ /**
21
+ * Organization.
22
+ */
23
+ abstract get organization(): Organization
24
+
19
25
  /**
20
26
  * Workspace id.
21
27
  */
@@ -87,7 +87,7 @@ export class WorkspacesImpl extends Workspaces {
87
87
  const response = await this.context
88
88
  .resolve(RpcService)
89
89
  ?.requestBuilder("api/v1/Workspaces")
90
- .sendPost({
90
+ .sendPostJson({
91
91
  organizationId: this.organization.id,
92
92
  profile: {
93
93
  name: name,
@@ -0,0 +1,52 @@
1
+ import fs from "fs"
2
+ import { testInWorkspace } from "./setup"
3
+
4
+ test("Files", async () => {
5
+ await testInWorkspace(async (app, org, ws) => {
6
+
7
+ expect(app).not.toBeUndefined()
8
+ expect(org).not.toBeUndefined()
9
+
10
+ const buffer = fs.readFileSync("test/data/test_file.pdf")
11
+ const file_obj = new File([new Uint8Array(buffer)], "test_file.pdf", {
12
+ type: "application/pdf"
13
+ })
14
+
15
+ const filePromise = ws.files.upload(file_obj)
16
+ await expect(filePromise).resolves.not.toThrow()
17
+ const file = await filePromise
18
+
19
+ expect(file).not.toBeUndefined()
20
+ expect(file).not.toBeNull()
21
+ expect(file.name).toBe("test_file.pdf")
22
+
23
+ let status = await file.status()
24
+
25
+ expect(status).not.toBeUndefined()
26
+ expect(status).not.toBeNull()
27
+ if (!status.success && status.error) {
28
+ console.error(status.error)
29
+ }
30
+ expect(status.success).toBe(true)
31
+ expect(status.file_id).toBe(file.id)
32
+ expect(status.file_parts_count).toBeGreaterThanOrEqual(status.completed_parts_count)
33
+
34
+ while (
35
+ status.success &&
36
+ status.completed_parts_count !== status.file_parts_count
37
+ ) {
38
+ await new Promise(r => setTimeout(r, 1000))
39
+ status = await file.status()
40
+ }
41
+
42
+ const queryPromise = ws.files.query("", 0, 20)
43
+ await expect(queryPromise).resolves.not.toThrow()
44
+ const filePage = await queryPromise
45
+ expect(filePage).not.toBeUndefined()
46
+ expect(filePage).not.toBeNull()
47
+ expect(filePage.files.length).toBe(1)
48
+ expect(filePage.pages).toBe(1)
49
+
50
+ await expect(ws.files.delete(file.id)).resolves.not.toThrow()
51
+ })
52
+ })
@@ -12,9 +12,7 @@ import { CredentialService } from "../src/services/credentialService"
12
12
  import { RpcService } from "../src/services/rpcService"
13
13
  import { AppBuilder } from "../src/appBuilder"
14
14
  import { UnitTest, AppSdkUnitTest } from "../src/unitTest"
15
- import { HOST, TOKEN } from "./setup"
16
- import { OrganizationImpl } from "../src/storages/organization.impl"
17
- import * as fs from "fs"
15
+ import { HOST, randomHash, TOKEN } from "./setup"
18
16
 
19
17
  test("SDK_VERSION", () => {
20
18
  expect(SDK_VERSION).toBe(version)
@@ -29,110 +27,6 @@ test("Default SDK", async () => {
29
27
  expect(app).not.toBeUndefined()
30
28
  })
31
29
 
32
- test("Create and delete organization, create and delete workspace", async () => {
33
- const randomName = `org-test-${Math.random().toString(16)}`
34
- const app = await appSdk(randomName, async builder => {
35
- builder.useHost(HOST)
36
- builder.useCredential(new DebugCredential(TOKEN))
37
- })
38
-
39
- const initLength = app.organizations.collection.length
40
-
41
- const org = await app.organizations.create(
42
- randomName,
43
- "this is a unitTest description"
44
- )
45
-
46
- // check organization
47
- expect(org).not.toBeUndefined()
48
- expect(org).not.toBeNull()
49
- expect(org).toBeInstanceOf(OrganizationImpl)
50
-
51
- expect(org.id).not.toBeUndefined()
52
- expect(org.id).not.toBeNull()
53
- expect(org.id.trim()).not.toBe("")
54
-
55
- // check name
56
- expect(org.name).not.toBeUndefined()
57
- expect(org.name).not.toBeNull()
58
- expect(org.name.trim()).not.toBe("")
59
-
60
- // check description
61
- expect(org.description).not.toBeUndefined()
62
- expect(org.description).not.toBeNull()
63
- expect(org.description.trim()).not.toBe("")
64
-
65
- // check organizations
66
- expect(app.organizations.get(org.id)).toBe(org)
67
- expect(app.organizations.tryGet(org.id)).toBe(org)
68
- expect(app.organizations.collection.length).toBe(initLength + 1)
69
-
70
- const initWorkspacesLength = org.workspaces.collection.length
71
-
72
- const wsPromise = org.workspaces.create(
73
- "test-workspace",
74
- "test-workspace-description"
75
- )
76
- await expect(wsPromise).resolves.not.toThrow()
77
- const ws = await wsPromise
78
- expect(ws).not.toBeUndefined()
79
- expect(ws).not.toBeNull()
80
- expect(ws.name).toBe("test-workspace")
81
- expect(ws.description).toBe("test-workspace-description")
82
- expect(app.organizations.get(org.id).workspaces.collection.length).toBe(
83
- initWorkspacesLength + 1
84
- )
85
- expect(org.workspaces.collection.length).toBe(initWorkspacesLength + 1)
86
- expect(org.workspaces.get(ws.id)).toBe(ws)
87
- expect(org.workspaces.tryGet(ws.id)).toBe(ws)
88
- expect(org.workspaces.contains(ws.id)).toBe(true)
89
-
90
- const buffer = fs.readFileSync("test_file.pdf")
91
- const file_obj = new File([new Uint8Array(buffer)], "test_file.pdf", {
92
- type: "text/plain"
93
- })
94
-
95
- const filePromise = ws.files.upload(file_obj)
96
- await expect(filePromise).resolves.not.toThrow()
97
- const file = await filePromise
98
-
99
- expect(file).not.toBeUndefined()
100
- expect(file).not.toBeNull()
101
- expect(file.name).toBe("test_file.pdf")
102
-
103
- let status = await file.status()
104
-
105
- expect(status).not.toBeUndefined()
106
- expect(status).not.toBeNull()
107
- expect(status.file_id).toBe(file.id)
108
- expect(status.file_parts_count).toBeGreaterThan(status.completed_parts_count)
109
-
110
- while (
111
- status.success == true &&
112
- status.completed_parts_count !== status.file_parts_count
113
- ) {
114
- await new Promise(r => setTimeout(r, 1000))
115
- status = await file.status()
116
- }
117
-
118
- const queryPromise = ws.files.query("", 0, 20)
119
- await expect(queryPromise).resolves.not.toThrow()
120
- const filePage = await queryPromise
121
- expect(filePage).not.toBeUndefined()
122
- expect(filePage).not.toBeNull()
123
- expect(filePage.files.length).toBe(1)
124
- expect(filePage.pages).toBe(1)
125
-
126
- await expect(ws.files.delete(file.id)).resolves.not.toThrow()
127
-
128
- await expect(org.workspaces.delete(ws.id)).resolves.not.toThrow()
129
-
130
- await expect(app.organizations.delete(org.id)).resolves.not.toThrow()
131
- expect((<OrganizationImpl>org).isDisposed).toBe(true)
132
- expect(app.organizations.collection.length).toBe(initLength)
133
- expect(app.organizations.tryGet(org.id)).toBeUndefined()
134
- })
135
-
136
30
  test("SDK, middleware", async () => {
137
31
  await AppSdkUnitTest.test(UnitTest.DEFAULT, async () => {
138
32
  const app = await appSdk("test-settings", async (builder: AppBuilder) => {
@@ -203,10 +97,12 @@ test("SDK, it is impossible to setup the same application", async () => {
203
97
  // this test is not stable if you run all tests at once
204
98
  // because the app is cached all app instances
205
99
  // we use a random identifier every time
206
- const testId = Math.random().toString(16)
207
- const promise = appSdk(`test-setup-${testId}`).then(() => {})
100
+ const testId = `test-setup-${randomHash()}`
101
+ const promise = appSdk(testId).then(() => {
102
+ })
208
103
  await expect(
209
- appSdk(`test-setup-${testId}`, async () => {})
104
+ appSdk(testId, async () => {
105
+ })
210
106
  ).rejects.toThrow()
211
107
  await promise
212
108
  })
@@ -217,9 +113,10 @@ test("SDK, setup and get this app", async () => {
217
113
  // this test is not stable if you run all tests at once
218
114
  // because the app is cached all app instances
219
115
  // we use a random identifier every time
220
- const testId = Math.random().toString(16)
221
- const promise = appSdk(`test-get-${testId}`).then(() => {})
222
- await expect(appSdk(`test-get-${testId}`)).resolves.toBeInstanceOf(AppSdk)
116
+ const testId = `test-get-${randomHash()}`
117
+ const promise = appSdk(testId).then(() => {
118
+ })
119
+ await expect(appSdk(testId)).resolves.toBeInstanceOf(AppSdk)
223
120
  await promise
224
121
  })
225
122
  })
@@ -0,0 +1,57 @@
1
+ import { appSdk, DebugCredential } from "../src"
2
+ import { HOST, randomHash, TOKEN } from "./setup"
3
+ import { OrganizationImpl } from "../src/storages/organization.impl"
4
+
5
+ test("Organization", async () => {
6
+ // make random name
7
+ const randomName = `org-test-${randomHash()}`
8
+
9
+ // create app
10
+ const app = await appSdk(randomName, async builder => {
11
+ builder.useHost(HOST)
12
+ builder.useCredential(new DebugCredential(TOKEN))
13
+ })
14
+
15
+ // save init length
16
+ const initLength = app.organizations.collection.length
17
+
18
+ // create organization
19
+ const org = await app.organizations.create(
20
+ randomName,
21
+ "this is a unitTest description"
22
+ )
23
+
24
+ // check organization
25
+ expect(org).not.toBeUndefined()
26
+ expect(org).not.toBeNull()
27
+ expect(org).toBeInstanceOf(OrganizationImpl)
28
+
29
+ expect(org.id).not.toBeUndefined()
30
+ expect(org.id).not.toBeNull()
31
+ expect(org.id.trim()).not.toBe("")
32
+
33
+ // check name
34
+ expect(org.name).not.toBeUndefined()
35
+ expect(org.name).not.toBeNull()
36
+ expect(org.name.trim()).not.toBe("")
37
+
38
+ // check description
39
+ expect(org.description).not.toBeUndefined()
40
+ expect(org.description).not.toBeNull()
41
+ expect(org.description.trim()).not.toBe("")
42
+
43
+ // check organizations
44
+ expect(app.organizations.get(org.id)).toBe(org)
45
+ expect(app.organizations.tryGet(org.id)).toBe(org)
46
+ expect(app.organizations.collection.length).toBe(initLength + 1)
47
+
48
+ // delete organization
49
+ await expect(app.organizations.delete(org.id)).resolves.not.toThrow()
50
+ expect((<OrganizationImpl>org).isDisposed).toBe(true)
51
+
52
+ // check init length
53
+ expect(app.organizations.collection.length).toBe(initLength)
54
+
55
+ // check organization must be undefined because it was deleted
56
+ expect(app.organizations.tryGet(org.id)).toBeUndefined()
57
+ })
package/test/setup.ts CHANGED
@@ -1,2 +1,54 @@
1
+ import { appSdk, AppSdk, DebugCredential } from "../src"
2
+ import { Organization } from "../src/storages/organization"
3
+ import { Workspace } from "../src/storages/workspace"
4
+
1
5
  export const HOST = <string>process.env.HOST
2
6
  export const TOKEN = <string>process.env.TOKEN
7
+
8
+ export const randomHash = (length: number = 5) => {
9
+ if (length <= 0) length = 1
10
+ return `name-${((Math.random() * Math.pow(10, length)) | 0).toString(16)}`
11
+ }
12
+
13
+ export const testInOrganization = async (func: (app: AppSdk, org: Organization) => Promise<void>, config ?: {
14
+ host: string,
15
+ token: string
16
+ }
17
+ ): Promise<void> => {
18
+ const randomName = `org-name-${randomHash()}`
19
+ const app = await appSdk(randomName, async builder => {
20
+ builder.useHost(config?.host ?? HOST)
21
+ builder.useCredential(new DebugCredential(config?.token ?? TOKEN))
22
+ })
23
+ const org = await app.organizations.create(
24
+ randomName,
25
+ "this is a unitTest description"
26
+ )
27
+ try {
28
+ await func(app, org)
29
+ } finally {
30
+ if (app
31
+ .organizations.tryGet(org.id)
32
+ ) {
33
+ await app.organizations.delete(org.id)
34
+ }
35
+ }
36
+ }
37
+
38
+ export const testInWorkspace = async (func: (app: AppSdk, org: Organization, workspace: Workspace)
39
+ => Promise<void>, config?: {
40
+ host: string,
41
+ token: string,
42
+ }): Promise<void> => {
43
+ await testInOrganization(async (app, org) => {
44
+ const randomName = `workspace-${randomHash()}`
45
+ const workspace = await org.workspaces.create(randomName, `description of ${randomName}`)
46
+ try {
47
+ await func(app, org, workspace)
48
+ } finally {
49
+ if (org.workspaces.tryGet(workspace.id)) {
50
+ await org.workspaces.delete(workspace.id)
51
+ }
52
+ }
53
+ }, config)
54
+ }
@@ -0,0 +1,71 @@
1
+ import { testInOrganization, testInWorkspace } from "./setup"
2
+
3
+ test("Workspace create / delete", async () => {
4
+ await testInOrganization(async (app, org) => {
5
+ // save init length
6
+ const initWorkspaceCount = org.workspaces.collection.length
7
+
8
+ // create workspace
9
+ const wsPromise = org.workspaces.create(
10
+ "test-workspace",
11
+ "test-workspace-description"
12
+ )
13
+
14
+ // check not throw
15
+ await expect(wsPromise).resolves.not.toThrow()
16
+
17
+ // get workspace
18
+ const ws = await wsPromise
19
+
20
+ // check exists
21
+ expect(ws).not.toBeUndefined()
22
+
23
+ // check exists
24
+ expect(ws).not.toBeNull()
25
+
26
+ // check name
27
+ expect(ws.name).toBe("test-workspace")
28
+
29
+ // check description
30
+ expect(ws.description).toBe("test-workspace-description")
31
+
32
+ // check collection length
33
+ expect(app.organizations.get(org.id).workspaces.collection.length).toBe(
34
+ initWorkspaceCount + 1
35
+ )
36
+
37
+ // check collection length
38
+ expect(org.workspaces.collection.length).toBe(initWorkspaceCount + 1)
39
+
40
+ // check get
41
+ expect(org.workspaces.get(ws.id)).toBe(ws)
42
+
43
+ // check tryGet
44
+ expect(org.workspaces.tryGet(ws.id)).toBe(ws)
45
+
46
+ // check contains
47
+ expect(org.workspaces.contains(ws.id)).toBe(true)
48
+
49
+ // check delete
50
+ await expect(org.workspaces.delete(ws.id)).resolves.not.toThrow()
51
+ })
52
+ })
53
+
54
+ test("Workspace, change", async () => {
55
+ await testInWorkspace(async (app, org, ws) => {
56
+
57
+ expect(ws.name).not.toBe("new-name")
58
+
59
+ // rename
60
+ await ws.change("new-name", "new-description")
61
+
62
+ // check name
63
+ expect(ws.name).toBe("new-name")
64
+
65
+ // check description
66
+ expect(ws.description).toBe("new-description")
67
+
68
+ // check name
69
+ expect(ws.name).toBe("new-name")
70
+ })
71
+ })
File without changes