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

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.
Files changed (58) hide show
  1. package/.editorconfig +4 -1
  2. package/.eslintrc.json +1 -1
  3. package/jest.config.ts +4 -4
  4. package/jest.setup.ts +2 -0
  5. package/package.json +4 -2
  6. package/src/appBuilder.ts +24 -5
  7. package/src/appSdk.ts +31 -12
  8. package/src/commands/startCommandHandler.ts +14 -0
  9. package/src/context.ts +31 -0
  10. package/src/credentials.ts +31 -9
  11. package/src/disposable.ts +2 -2
  12. package/src/dto/accessGroupResponse.ts +35 -0
  13. package/src/dto/chatResponse.ts +104 -0
  14. package/src/dto/userInfoResponse.ts +47 -0
  15. package/src/dto/workspacesResponse.ts +49 -0
  16. package/src/events.ts +1 -5
  17. package/src/index.ts +17 -11
  18. package/src/internal/app.impl.ts +98 -30
  19. package/src/internal/appBuilder.impl.ts +39 -12
  20. package/src/internal/createApp.impl.ts +3 -3
  21. package/src/middleware.ts +1 -1
  22. package/src/services/commandService.ts +44 -0
  23. package/src/services/credentialService.ts +3 -3
  24. package/src/services/middlewareService.ts +7 -5
  25. package/src/services/organizationService.ts +28 -0
  26. package/src/services/requestBuilder.ts +102 -0
  27. package/src/services/responseUtils.ts +32 -0
  28. package/src/services/rpcService.ts +113 -53
  29. package/src/services/service.ts +3 -3
  30. package/src/services/userProfileService.ts +38 -0
  31. package/src/storages/chat.ts +37 -0
  32. package/src/storages/file.impl.ts +68 -0
  33. package/src/storages/files.impl.ts +192 -0
  34. package/src/storages/files.ts +67 -0
  35. package/src/storages/groups.impl.ts +337 -0
  36. package/src/storages/groups.ts +43 -0
  37. package/src/storages/organization.impl.ts +68 -0
  38. package/src/storages/organization.ts +33 -0
  39. package/src/storages/organizations.impl.ts +191 -0
  40. package/src/storages/organizations.ts +56 -0
  41. package/src/storages/userProfile.impl.ts +56 -0
  42. package/src/storages/userProfile.ts +42 -0
  43. package/src/storages/workspace.impl.ts +109 -0
  44. package/src/storages/workspace.ts +43 -0
  45. package/src/storages/workspaces.impl.ts +212 -0
  46. package/src/storages/workspaces.ts +53 -0
  47. package/src/unitTest.ts +42 -0
  48. package/test/commands.test.ts +24 -0
  49. package/test/disposable.test.ts +3 -3
  50. package/test/events.test.ts +4 -4
  51. package/test/index.test.ts +204 -62
  52. package/test/registry.test.ts +8 -8
  53. package/test/services.test.ts +56 -0
  54. package/test/setup.ts +2 -0
  55. package/test/unitTest.test.ts +21 -0
  56. package/test_file.pdf +0 -0
  57. package/src/internal/context.ts +0 -13
  58. package/src/types.ts +0 -110
@@ -1,72 +1,132 @@
1
- import { Service, type ServiceContext } from './service'
2
- import { MiddlewareService } from './middlewareService'
1
+ import { Service, type ServiceContext } from "./service"
2
+ import { MiddlewareService } from "./middlewareService"
3
+ import { RequestBuilder } from "./requestBuilder"
3
4
 
4
- export class RpcService extends Service {
5
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
6
- async request(req: Request): Promise<Response> {
7
- throw new Error('Not implemented')
8
- }
5
+ /**
6
+ * Options for the RpcService.
7
+ */
8
+ export interface RequestOptions {
9
+ searchParams?: Map<string, string>
10
+ headers?: [string, string][] | Record<string, string> | Headers
11
+ }
9
12
 
10
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
11
- buildUrl(path: string): string {
12
- throw new Error('Not implemented')
13
+ /**
14
+ * RPC service.
15
+ */
16
+ export class RpcService extends Service {
17
+ constructor(
18
+ serviceContext: ServiceContext,
19
+ /**
20
+ * Host of the RPC service.
21
+ * It is not used if you use the `urlBuilder` option.
22
+ */
23
+ public readonly host: string,
24
+ /**
25
+ * Options for the RpcService.
26
+ */
27
+ private readonly options?: {
28
+ // make it possible to override the url builder
29
+ urlBuilder?: (path: string) => URL
30
+ // make it possible to override the fetch method
31
+ fetchMethod?: (uri: Request) => Promise<Response>
32
+ }
33
+ ) {
34
+ super(serviceContext)
13
35
  }
14
36
 
15
- async get(path: string): Promise<Response> {
16
- return await this.request(
17
- new Request(this.buildUrl(path), {
18
- method: 'GET'
37
+ /**
38
+ * Request method.
39
+ */
40
+ async request(req: Request): Promise<Response> {
41
+ const middlewareService = this.resolve(MiddlewareService)
42
+ if (middlewareService !== undefined) {
43
+ return await middlewareService.process(req, async req => {
44
+ return (await this.options?.fetchMethod?.(req)) ?? (await fetch(req))
19
45
  })
20
- )
46
+ }
47
+ return (await this.options?.fetchMethod?.(req)) ?? (await fetch(req))
21
48
  }
22
49
 
23
- async post(path: string, body?: BodyInit | null): Promise<Response> {
24
- return await this.request(
25
- new Request(this.buildUrl(path), {
26
- method: 'POST',
27
- body
28
- })
29
- )
50
+ /**
51
+ * Build URL.
52
+ * @param path
53
+ */
54
+ buildUrl(path: string): URL {
55
+ if (this.options !== undefined && this.options.urlBuilder !== undefined) {
56
+ return this.options.urlBuilder(path)
57
+ }
58
+ if (this.host.endsWith("/") && path.startsWith("/")) {
59
+ return new URL(`${this.host}${path.slice(1)}`)
60
+ }
61
+ if (!this.host.endsWith("/") && !path.startsWith("/")) {
62
+ return new URL(`${this.host}/${path}`)
63
+ }
64
+ return new URL(`${this.host}${path}`)
30
65
  }
31
66
 
32
- async put(path: string, body?: BodyInit | null): Promise<Response> {
33
- return await this.request(
34
- new Request(this.buildUrl(path), {
35
- method: 'PUT',
36
- body
37
- })
38
- )
67
+ /**
68
+ * Create a request builder.
69
+ * @param path
70
+ */
71
+ requestBuilder(path: string): RequestBuilder {
72
+ return new RequestBuilder(this.buildUrl(path), this.request.bind(this))
39
73
  }
40
74
 
41
- async delete(path: string): Promise<Response> {
42
- return await this.request(
43
- new Request(this.buildUrl(path), {
44
- method: 'DELETE'
45
- })
46
- )
75
+ /**
76
+ * Send a GET request.
77
+ * @param path
78
+ * @param options
79
+ */
80
+ async get(path: string, options?: RequestOptions): Promise<Response> {
81
+ return this.requestBuilder(path)
82
+ .searchParams(options?.searchParams)
83
+ .headers(options?.headers)
84
+ .sendGet()
47
85
  }
48
- }
49
86
 
50
- export class RpcServiceImpl extends RpcService {
51
- constructor(serviceContext: ServiceContext, public readonly host: string) {
52
- super(serviceContext)
87
+ /**
88
+ * Send a POST request.
89
+ * @param path
90
+ * @param body
91
+ * @param options
92
+ */
93
+ async post(
94
+ path: string,
95
+ body?: BodyInit | null,
96
+ options?: RequestOptions
97
+ ): Promise<Response> {
98
+ return this.requestBuilder(path)
99
+ .searchParams(options?.searchParams)
100
+ .headers(options?.headers)
101
+ .sendPost(body)
53
102
  }
54
103
 
55
- override async request(req: Request): Promise<Response> {
56
- const middlewareService = this.resolve(MiddlewareService)
57
- if (middlewareService !== undefined) {
58
- return await middlewareService.process(req, async req => {
59
- return await fetch(req)
60
- })
61
- } else {
62
- return await fetch(req)
63
- }
104
+ /**
105
+ * Send a PUT request.
106
+ * @param path
107
+ * @param body
108
+ * @param options
109
+ */
110
+ async put(
111
+ path: string,
112
+ body?: BodyInit | null,
113
+ options?: RequestOptions
114
+ ): Promise<Response> {
115
+ return this.requestBuilder(path)
116
+ .searchParams(options?.searchParams)
117
+ .headers(options?.headers)
118
+ .sendPut(body)
64
119
  }
65
120
 
66
- override buildUrl(path: string): string {
67
- if (this.host.endsWith('/') && path.startsWith('/')) {
68
- return `${this.host}${path.slice(1)}`
69
- }
70
- return `${this.host}${path}`
121
+ /**
122
+ * Send a DELETE request.
123
+ * @param path
124
+ * @param options
125
+ */
126
+ async delete(path: string, options?: RequestOptions): Promise<Response> {
127
+ return this.requestBuilder(path)
128
+ .searchParams(options?.searchParams)
129
+ .headers(options?.headers)
130
+ .sendDelete()
71
131
  }
72
132
  }
@@ -1,6 +1,6 @@
1
- import { type Context } from '../internal/context'
2
- import { type Constructor } from '../internal/registry'
3
- import { type DisposableContainer, type Lifetime } from '../disposable'
1
+ import { type Context } from "../context"
2
+ import { type Constructor } from "../internal/registry"
3
+ import { type DisposableContainer, type Lifetime } from "../disposable"
4
4
 
5
5
  export class ServiceContext {
6
6
  constructor(
@@ -0,0 +1,38 @@
1
+ import { Service } from "./service"
2
+ import { RpcService } from "./rpcService"
3
+ import { UserProfile } from "../storages/userProfile"
4
+ import { UserInfoResponse } from "../dto/userInfoResponse"
5
+ import { OrganizationService } from "./organizationService"
6
+ import { UserProfileImpl } from "../storages/userProfile.impl"
7
+ import { ResponseUtils } from "./responseUtils"
8
+
9
+ export class UserProfileService extends Service {
10
+ private readonly impl: UserProfileImpl = new UserProfileImpl()
11
+
12
+ get userProfile(): UserProfile {
13
+ return this.impl
14
+ }
15
+
16
+ async fetch() {
17
+ const rpc = this.resolve(RpcService) as RpcService
18
+ const response = await rpc.requestBuilder("api/v1/Users/self2").sendGet()
19
+ if (ResponseUtils.isFail(response)) {
20
+ await ResponseUtils.throwError("Failed to fetch user profile", response)
21
+ }
22
+ const content = (await response.json()) as UserInfoResponse
23
+
24
+ // init user profile from the server's response
25
+ this.impl.initFrom(content)
26
+
27
+ const organizationService = this.resolve(
28
+ OrganizationService
29
+ ) as OrganizationService
30
+
31
+ // init organization service from user profile
32
+ await organizationService.initFrom(
33
+ content.adminInOrganization,
34
+ content.organizations,
35
+ content.user.settings
36
+ )
37
+ }
38
+ }
@@ -0,0 +1,37 @@
1
+ import { EventDispatcher } from "../events"
2
+
3
+ export type ChatId = string
4
+
5
+ export enum ChatsEvent {
6
+ ADDED = "added",
7
+ REMOVED = "removed"
8
+ }
9
+
10
+ export enum ChatAnswer {
11
+ SHORT = "short",
12
+ LONG = "long"
13
+ }
14
+
15
+ export abstract class Chat {
16
+ /**
17
+ * Chat id.
18
+ */
19
+ abstract get id(): ChatId
20
+
21
+ /**
22
+ * Chat name.
23
+ */
24
+ abstract get name(): string
25
+
26
+ abstract question(message: string, answer?: ChatAnswer): Promise<void>
27
+ }
28
+
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,68 @@
1
+ import { Context } from "../context"
2
+ import { Disposable } from "../disposable"
3
+ import { FileDto, FileProgressDto } from "../dto/workspacesResponse"
4
+ import { RpcService } from "../services/rpcService"
5
+ import { File } from "./files"
6
+ import { ResponseUtils } from "../services/responseUtils"
7
+
8
+ export class FileImpl extends File implements Disposable {
9
+ private _isDisposed: boolean = false
10
+ private _content?: FileDto
11
+
12
+ constructor(private readonly context: Context) {
13
+ super()
14
+ }
15
+
16
+ public initFrom(file: FileDto): File {
17
+ this._content = file
18
+
19
+ return this
20
+ }
21
+
22
+ get isDisposed(): boolean {
23
+ return this._isDisposed
24
+ }
25
+
26
+ dispose(): void {
27
+ this._isDisposed = true
28
+ }
29
+
30
+ get id(): string {
31
+ return <string>this._content?.id
32
+ }
33
+
34
+ get name(): string {
35
+ return <string>this._content?.id
36
+ }
37
+
38
+ async url(): Promise<string> {
39
+ const response = await this.context
40
+ .resolve(RpcService)
41
+ ?.requestBuilder("api/v1/Files/url")
42
+ .searchParam("id", this.id)
43
+ .sendGet()
44
+
45
+ if (ResponseUtils.isFail(response)) {
46
+ await ResponseUtils.throwError(
47
+ `Failed to get file ${this.id} url`,
48
+ response
49
+ )
50
+ }
51
+
52
+ return (await response!.json()).url
53
+ }
54
+
55
+ async status(): Promise<FileProgressDto> {
56
+ const response = await this.context
57
+ .resolve(RpcService)
58
+ ?.requestBuilder("api/v1/Files/url")
59
+ .searchParam("id", this.id)
60
+ .sendGet()
61
+
62
+ if (ResponseUtils.isFail(response)) {
63
+ await ResponseUtils.throwError(`Failed to get file ${this.id}`, response)
64
+ }
65
+
66
+ return (await response!.json()).progress as FileProgressDto
67
+ }
68
+ }
@@ -0,0 +1,192 @@
1
+ import { Context } from "../context"
2
+ import { Disposable } from "../disposable"
3
+ import { FileDto, FileListResponse } from "../dto/workspacesResponse"
4
+ import { OrganizationService } from "../services/organizationService"
5
+ import { RpcService } from "../services/rpcService"
6
+ import { FileImpl } from "./file.impl"
7
+ import { File, Files, FilesEvent, FilesList as FilesPage } from "./files"
8
+ import { WorkspaceImpl } from "./workspace.impl"
9
+ import { ResponseUtils } from "../services/responseUtils"
10
+
11
+ export class FilesPageImpl extends FilesPage implements Disposable {
12
+ private _isDisposed: boolean = false
13
+
14
+ public files: File[] = []
15
+ public total: number = 0
16
+ public filesPerPage: number = 0
17
+ public page: number = 0
18
+
19
+ get pages(): number {
20
+ return Math.ceil(Math.max(this.total / this.filesPerPage, 1.0))
21
+ }
22
+
23
+ get isDisposed(): boolean {
24
+ return this._isDisposed
25
+ }
26
+
27
+ dispose(): void {
28
+ this._isDisposed = true
29
+ }
30
+ }
31
+
32
+ export class FilesImpl extends Files {
33
+ constructor(
34
+ private readonly workspace: WorkspaceImpl,
35
+ private readonly context: Context
36
+ ) {
37
+ super()
38
+ }
39
+
40
+ // Object used as files page data, returned by "query"
41
+ public filesList?: FilesPage
42
+
43
+ async upload(file: any): Promise<File> {
44
+ return await this.internalUpload(file)
45
+ }
46
+
47
+ async delete(id: string): Promise<void> {
48
+ return await this.internalDeleteFile(id)
49
+ }
50
+
51
+ async query(query: string, page: number, limit: number): Promise<FilesPage> {
52
+ return await this.internalQuery(query, page, limit)
53
+ }
54
+
55
+ //----------------------------------------------------------------------------
56
+ // INTERNALS
57
+ //----------------------------------------------------------------------------
58
+
59
+ /**
60
+ * Delete organization.
61
+ * @param id
62
+ */
63
+ async internalDeleteFile(id: string): Promise<void> {
64
+ if (id === undefined || id === null) {
65
+ throw new Error("File delete, id is undefined or null")
66
+ }
67
+ if (id.length === 0 || id.trim().length === 0) {
68
+ throw new Error("File delete, id is empty")
69
+ }
70
+
71
+ const response = await this.context
72
+ .resolve(RpcService)
73
+ ?.requestBuilder("/api/v1/Files")
74
+ .searchParam("id", id)
75
+ .sendDelete()
76
+ if (ResponseUtils.isFail(response)) {
77
+ await ResponseUtils.throwError(`File ${id} delete, failed`, response)
78
+ }
79
+ const file = <FileImpl>this.filesList!.files.find(f => f.id === id)
80
+ const index = this.filesList!.files.indexOf(file)
81
+ if (index < 0) {
82
+ throw new Error("Organization delete, index is not found")
83
+ }
84
+
85
+ // remove file from collection
86
+ this.filesList!.files.splice(index, 1)
87
+
88
+ // dispatch event, file removed
89
+ this.dispatch({
90
+ type: FilesEvent.REMOVED,
91
+ data: file
92
+ })
93
+
94
+ // dispose file
95
+ file.dispose()
96
+ }
97
+
98
+ async internalQuery(
99
+ query: string,
100
+ page: number,
101
+ limit: number
102
+ ): Promise<FilesPage> {
103
+ if (page === undefined || page === null) {
104
+ throw new Error("File fetch, page is undefined or null")
105
+ }
106
+ if (limit === undefined || limit === null) {
107
+ throw new Error("File fetch, limit is undefined or null")
108
+ }
109
+ if (limit === 0) {
110
+ throw new Error("File fetch, limit is 0")
111
+ }
112
+
113
+ const orgService = this.context.resolve(OrganizationService)
114
+
115
+ if (orgService === undefined) {
116
+ throw new Error("File fetch, organization service undefined")
117
+ }
118
+
119
+ const response = await this.context
120
+ .resolve(RpcService)
121
+ ?.requestBuilder("api/v1/Files/list")
122
+
123
+ .searchParam("workspaceId", this.workspace.id)
124
+ .searchParam("organizationId", orgService.organizations.current)
125
+ .searchParam("query", query)
126
+ .searchParam("page", page.toString())
127
+ .searchParam("limit", limit.toString())
128
+ .sendGet()
129
+
130
+ if (ResponseUtils.isFail(response)) {
131
+ await ResponseUtils.throwError(
132
+ `Files fetch query:${query}, page:${page}, limit:${limit}, failed`,
133
+ response
134
+ )
135
+ }
136
+
137
+ const files = (await response!.json()) as FileListResponse
138
+
139
+ const filesList = new FilesPageImpl()
140
+ filesList.total = files.totalFilesCount
141
+ filesList.filesPerPage = files.filesPerPage
142
+ filesList.page = page
143
+ for (const fl of files.files) {
144
+ const file = new FileImpl(this.context).initFrom(fl)
145
+
146
+ filesList.files.push(file)
147
+
148
+ this.dispatch({
149
+ type: FilesEvent.ADDED,
150
+ data: file
151
+ })
152
+ }
153
+
154
+ this.filesList = filesList
155
+
156
+ return filesList
157
+ }
158
+
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")
164
+ }
165
+
166
+ const form = new FormData()
167
+ form.append("organizationId", orgService.organizations.current)
168
+ form.append("workspaceId", this.workspace.id)
169
+ form.append("name", file.name)
170
+ form.append("file", file, file.name)
171
+
172
+ const response = await this.context
173
+ .resolve(RpcService)
174
+ ?.requestBuilder("api/v1/Files")
175
+ .sendPost(form)
176
+ if (ResponseUtils.isFail(response)) {
177
+ await ResponseUtils.throwError(`File upload ${file}`, response)
178
+ }
179
+ const result = (await response!.json()).file as FileDto
180
+
181
+ const fileImpl = new FileImpl(this.context).initFrom(result)
182
+
183
+ this.filesList!.files.push(file)
184
+
185
+ this.dispatch({
186
+ type: FilesEvent.ADDED,
187
+ data: file
188
+ })
189
+
190
+ return fileImpl
191
+ }
192
+ }
@@ -0,0 +1,67 @@
1
+ import { FileProgressDto } from '../dto/workspacesResponse'
2
+ import { EventDispatcher } from '../events'
3
+
4
+ export type FileId = string
5
+
6
+ export enum FilesEvent {
7
+ ADDED = 'added',
8
+ REMOVED = 'removed'
9
+ }
10
+
11
+ export type UploadFile = File | Blob | string
12
+
13
+ /**
14
+ * File.
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
+ }
37
+
38
+ /**
39
+ * Files storage.
40
+ */
41
+ export abstract class Files extends EventDispatcher<FilesEvent, File> {
42
+ /**
43
+ * Get file by id.
44
+ */
45
+ abstract upload(file: any): Promise<File>
46
+
47
+ /**
48
+ * Delete file.
49
+ * @param id
50
+ */
51
+ abstract delete(id: FileId): Promise<void>
52
+
53
+ /**
54
+ * Query files.
55
+ */
56
+ abstract query(query: string, page: number, limit: number): Promise<FilesList>
57
+ }
58
+
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
+ }