@prisme.ai/sdk 0.0.2 → 1.0.1

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.
@@ -0,0 +1,22 @@
1
+ import { Api } from '../api';
2
+
3
+ export class UsersEndpoint {
4
+ private id: string;
5
+ private api: Api;
6
+
7
+ constructor(id: string, api: Api) {
8
+ this.id = id;
9
+ this.api = api;
10
+ }
11
+
12
+ async setMeta(k: string, v: any) {
13
+ await this.api.post(`/user/meta`, {
14
+ [k]: v,
15
+ });
16
+ }
17
+ async deleteMeta(k: string) {
18
+ await this.api.delete(`/user/meta/${k}`);
19
+ }
20
+ }
21
+
22
+ export default UsersEndpoint;
@@ -0,0 +1,18 @@
1
+ import { Api } from '../api';
2
+ import WorkspacesVersionsEndpoint from './workspacesVersions';
3
+
4
+ export class WorkspacesEndpoint {
5
+ private id: string;
6
+ private api: Api;
7
+
8
+ constructor(id: string, api: Api) {
9
+ this.id = id;
10
+ this.api = api;
11
+ }
12
+
13
+ get versions() {
14
+ return new WorkspacesVersionsEndpoint(this.id, this.api);
15
+ }
16
+ }
17
+
18
+ export default WorkspacesEndpoint;
@@ -0,0 +1,34 @@
1
+ import { Api } from '../api';
2
+
3
+ export class WorkspacesVersionsEndpoint {
4
+ private workspaceId: string;
5
+ private api: Api;
6
+
7
+ constructor(workspaceId: string, api: Api) {
8
+ this.workspaceId = workspaceId;
9
+ this.api = api;
10
+ }
11
+
12
+ create(version?: PrismeaiAPI.PublishWorkspaceVersion.RequestBody) {
13
+ this.api.post(`/workspaces/${this.workspaceId}/versions`, version);
14
+ }
15
+ rollback(
16
+ versionId: PrismeaiAPI.RollbackWorkspaceVersion.PathParameters['versionId']
17
+ ) {
18
+ this.api.post(
19
+ `/workspaces/${this.workspaceId}/versions/${versionId}/rollback`
20
+ );
21
+ }
22
+
23
+ async export(version: PrismeaiAPI.ExportWorkspaceVersion.Parameters.VersionId = 'current') {
24
+ const res = await this.api.prepareRequest(
25
+ `/workspaces/${this.workspaceId}/versions/${version}/export`, {
26
+ method: 'post'
27
+ }
28
+ )
29
+ return new Blob([await res.arrayBuffer()], { type: 'application/zip' });
30
+
31
+ }
32
+ }
33
+
34
+ export default WorkspacesVersionsEndpoint;
@@ -1,14 +1,14 @@
1
1
  import getConfig from 'next/config';
2
2
  import Events from './events';
3
3
  import io from 'socket.io-client';
4
-
5
- const { publicRuntimeConfig } = getConfig();
4
+ import { Api } from './api';
6
5
 
7
6
  jest.mock('socket.io-client', () => {
8
7
  const mock = {
9
8
  disconnect: jest.fn(),
10
9
  onAny: jest.fn(),
11
10
  offAny: jest.fn(),
11
+ on: jest.fn(),
12
12
  once: jest.fn(),
13
13
  };
14
14
  const io = jest.fn(() => mock);
@@ -16,26 +16,54 @@ jest.mock('socket.io-client', () => {
16
16
  });
17
17
 
18
18
  it('should connect to Websocket', () => {
19
- const client = new Events('1', 'abcde');
19
+ new Events({ workspaceId: '1', token: 'abcde', api: {} as Api });
20
+ expect(io).toHaveBeenCalledWith(
21
+ `https://api.eda.prisme.ai/workspaces/1/events`,
22
+ {
23
+ extraHeaders: {
24
+ Authorization: 'Bearer abcde',
25
+ },
26
+ withCredentials: true,
27
+ }
28
+ );
29
+ });
30
+
31
+ it('should connect to Websocket with apiKey', () => {
32
+ new Events({
33
+ workspaceId: '1',
34
+ token: 'abcde',
35
+ apiKey: 'fghij',
36
+ api: {} as Api,
37
+ });
20
38
  expect(io).toHaveBeenCalledWith(
21
39
  `https://api.eda.prisme.ai/workspaces/1/events`,
22
40
  {
23
41
  extraHeaders: {
24
- 'x-prismeai-session-token': 'abcde',
42
+ Authorization: 'Bearer abcde',
43
+ 'x-prismeai-api-key': 'fghij',
25
44
  },
45
+ withCredentials: true,
26
46
  }
27
47
  );
28
48
  });
29
49
 
30
50
  it('should disconnect to Websocket', () => {
31
- const client = new Events('1', 'abcde');
51
+ const client = new Events({
52
+ workspaceId: '1',
53
+ token: 'abcde',
54
+ api: {} as Api,
55
+ });
32
56
  (client as any).client.connected = true;
33
57
  client.destroy();
34
58
  expect(io().disconnect).toHaveBeenCalled();
35
59
  });
36
60
 
37
61
  it('should wait before disconnecting Websocket', () => {
38
- const client = new Events('1', 'abcde');
62
+ const client = new Events({
63
+ workspaceId: '1',
64
+ token: 'abcde',
65
+ api: {} as Api,
66
+ });
39
67
  const ioInstance = io();
40
68
  (client as any).client.connected = false;
41
69
  ((client as any).client.once as jest.Mock).mockClear();
@@ -48,7 +76,11 @@ it('should wait before disconnecting Websocket', () => {
48
76
  });
49
77
 
50
78
  it('should listen to all events', () => {
51
- const client = new Events('1', 'abcde');
79
+ const client = new Events({
80
+ workspaceId: '1',
81
+ token: 'abcde',
82
+ api: {} as Api,
83
+ });
52
84
  const listener = () => null;
53
85
  const off = client.all(listener);
54
86
  expect(io().onAny).toHaveBeenCalledWith(listener);
package/lib/events.ts CHANGED
@@ -1,20 +1,91 @@
1
1
  import io, { Socket } from 'socket.io-client';
2
2
 
3
+ import { Api } from './api';
4
+
5
+ export type PayloadQuery = Record<string, string | string[]>;
6
+ export type OrQuery = PayloadQuery[];
7
+
8
+ export type SearchOptions = Omit<
9
+ PrismeaiAPI.EventsLongpolling.QueryParameters,
10
+ 'query' | 'types'
11
+ > & {
12
+ payloadQuery?: PayloadQuery | OrQuery;
13
+ types?: string[];
14
+ };
15
+
3
16
  export class Events {
4
17
  protected client: Socket;
5
18
  public workspaceId: string;
19
+ private filters: Record<string, any>[];
20
+ private listenedUserTopics: Map<string, string[]>;
21
+ private listeners: Map<string, Function[]> = new Map();
22
+ private lastReceivedEventDate: Date;
6
23
 
7
- constructor(
8
- workspaceId: string,
9
- token: string,
10
- apiHost: string = 'https://api.eda.prisme.ai'
11
- ) {
24
+ constructor({
25
+ workspaceId,
26
+ token,
27
+ legacyToken,
28
+ apiKey,
29
+ apiHost = 'https://api.eda.prisme.ai',
30
+ filters,
31
+ api,
32
+ }: {
33
+ workspaceId: string;
34
+ token: string;
35
+ legacyToken?: string;
36
+ apiKey?: string;
37
+ apiHost?: string;
38
+ filters?: Record<string, any>;
39
+ api: Api;
40
+ }) {
12
41
  this.workspaceId = workspaceId;
13
- this.client = io(`${apiHost}/workspaces/${workspaceId}/events`, {
14
- extraHeaders: {
15
- 'x-prismeai-session-token': token,
16
- },
42
+ const queryString = new URLSearchParams(filters || {}).toString();
43
+ const fullQueryString = queryString ? `?${queryString}` : '';
44
+ const extraHeaders: any = token
45
+ ? {
46
+ Authorization: `Bearer ${token}`,
47
+ }
48
+ : { 'x-prismeai-token': legacyToken };
49
+ this.lastReceivedEventDate = new Date();
50
+ if (apiKey) {
51
+ extraHeaders['x-prismeai-api-key'] = apiKey;
52
+ }
53
+
54
+ this.client = io(
55
+ `${apiHost}/workspaces/${workspaceId}/events${fullQueryString}`,
56
+ {
57
+ extraHeaders,
58
+ withCredentials: true,
59
+ }
60
+ );
61
+
62
+ const onConnect = () => {
63
+ // Reset last filters
64
+ this.updateFilters({
65
+ payloadQuery: this.filters,
66
+ });
67
+ setTimeout(async () => {
68
+ const events = await api.getEvents(workspaceId, {
69
+ ...this.filters[this.filters.length - 1],
70
+ afterDate: this.lastReceivedEventDate.toISOString(),
71
+ });
72
+ events.reverse().forEach((event) => {
73
+ (this.listeners.get(event.type) || []).forEach((listener) =>
74
+ listener(event)
75
+ );
76
+ });
77
+ }, 2000);
78
+ };
79
+ this.client.on('disconnect', () => {
80
+ if (!this.lastReceivedEventDate) {
81
+ this.lastReceivedEventDate = new Date();
82
+ }
83
+ this.client.off('connect', onConnect);
84
+ this.client.on('connect', onConnect);
17
85
  });
86
+
87
+ this.filters = [filters || {}];
88
+ this.listenedUserTopics = new Map();
18
89
  }
19
90
 
20
91
  get socket() {
@@ -34,31 +105,71 @@ export class Events {
34
105
  }
35
106
 
36
107
  all(listener: (eventName: string, eventData: Prismeai.PrismeEvent) => void) {
37
- this.client.onAny(listener);
108
+ this.client.onAny((eventName: string, eventData: Prismeai.PrismeEvent) => {
109
+ this.lastReceivedEventDate = new Date(eventData?.createdAt);
110
+ return listener(eventName, eventData);
111
+ });
38
112
 
39
113
  return () => this.client.offAny(listener);
40
114
  }
41
115
 
42
- on(
43
- ev: string,
44
- listener: (eventName: string, eventData: Prismeai.PrismeEvent) => void
45
- ) {
116
+ on(ev: string, listener: (eventData: Prismeai.PrismeEvent) => void) {
117
+ this.listeners.set(ev, [...(this.listeners.get(ev) || []), listener]);
118
+
46
119
  this.client.on(ev, listener);
47
- return () => this.client.off(ev, listener);
120
+ return () => {
121
+ this.listeners.set(
122
+ ev,
123
+ (this.listeners.get(ev) || []).filter((l) => l !== listener)
124
+ );
125
+ this.client.off(ev, listener);
126
+ };
48
127
  }
49
128
 
50
- emit(event: string, payload?: any) {
129
+ emit(event: string, payload?: any, options?: any) {
51
130
  this.client.emit('event', {
52
131
  type: event,
53
132
  payload,
133
+ options,
54
134
  });
55
135
  }
56
136
 
137
+ listenTopics({
138
+ event,
139
+ topics,
140
+ }: {
141
+ event: string;
142
+ topics: string | string[];
143
+ }) {
144
+ topics = Array.isArray(topics) ? topics : [topics];
145
+
146
+ this.listenedUserTopics.set(event, topics);
147
+
148
+ this.filters = [
149
+ { ...this.filters[0] },
150
+ {
151
+ 'target.userTopic': Array.from(this.listenedUserTopics).flatMap(
152
+ ([_event, topics]) => topics
153
+ ),
154
+ },
155
+ ];
156
+ this.updateFilters({
157
+ payloadQuery: this.filters,
158
+ });
159
+ }
160
+
161
+ updateFilters(filters: SearchOptions) {
162
+ this.client.emit('filters', filters);
163
+ }
164
+
57
165
  once(
58
166
  ev: string,
59
167
  listener: (eventName: string, eventData: Prismeai.PrismeEvent) => void
60
168
  ) {
61
169
  this.client.once(ev, listener);
170
+ return () => {
171
+ this.client.off(ev, listener);
172
+ };
62
173
  }
63
174
 
64
175
  close() {
@@ -7,16 +7,15 @@ it('should fetch', async () => {
7
7
  // @ts-ignore
8
8
  global.fetch = jest.fn(() => ({
9
9
  ok: true,
10
- headers: [['foo', 'bar']],
11
- json() {
12
- return undefined;
10
+ headers: new Headers([['foo', 'bar']]),
11
+ text() {
12
+ return '';
13
13
  },
14
14
  clone() {
15
15
  return { ...this };
16
16
  },
17
17
  }));
18
18
  const o = await fetcher.get('url');
19
- expect(o.headers).toEqual({ foo: 'bar' });
20
19
  expect(global.fetch).toHaveBeenCalledWith('http/url', {
21
20
  headers: expect.any(Headers),
22
21
  method: 'GET',
@@ -31,9 +30,9 @@ it('should fetch with auth', async () => {
31
30
  // @ts-ignore
32
31
  global.fetch = jest.fn(() => ({
33
32
  ok: true,
34
- headers: {},
35
- json() {
36
- return {};
33
+ headers: new Headers(),
34
+ text() {
35
+ return '';
37
36
  },
38
37
  clone() {
39
38
  return { ...this };
@@ -47,7 +46,33 @@ it('should fetch with auth', async () => {
47
46
  });
48
47
  const headers = (global.fetch as jest.Mock).mock.calls[0][1].headers;
49
48
  expect(headers.get('Access-Control-Allow-Origin')).toBe('*');
50
- expect(headers.get('x-prismeai-session-token')).toBe('token');
49
+ expect(headers.get('x-prismeai-token')).toBe('token');
50
+ });
51
+
52
+ it('should fetch with api key', async () => {
53
+ const fetcher = new Fetcher('http/');
54
+ // @ts-ignore
55
+ global.fetch = jest.fn(() => ({
56
+ ok: true,
57
+ headers: new Headers(),
58
+ text() {
59
+ return '';
60
+ },
61
+ clone() {
62
+ return { ...this };
63
+ },
64
+ }));
65
+ fetcher.token = 'token';
66
+ fetcher.apiKey = 'api-key';
67
+ await fetcher.get('url');
68
+ expect(global.fetch).toHaveBeenCalledWith('http/url', {
69
+ headers: expect.any(Headers),
70
+ method: 'GET',
71
+ });
72
+ const headers = (global.fetch as jest.Mock).mock.calls[0][1].headers;
73
+ expect(headers.get('Access-Control-Allow-Origin')).toBe('*');
74
+ expect(headers.get('x-prismeai-token')).toBe('token');
75
+ expect(headers.get('x-prismeai-api-key')).toBe('api-key');
51
76
  });
52
77
 
53
78
  it('should fail to fetch', async () => {
@@ -103,7 +128,7 @@ it('should post', async () => {
103
128
  // @ts-ignore
104
129
  global.fetch = jest.fn(() => ({
105
130
  ok: true,
106
- headers: {},
131
+ headers: new Headers([['content-type', 'application/json']]),
107
132
  json() {
108
133
  return {};
109
134
  },
@@ -123,7 +148,7 @@ it('should post with body', async () => {
123
148
  // @ts-ignore
124
149
  global.fetch = jest.fn(() => ({
125
150
  ok: true,
126
- headers: {},
151
+ headers: new Headers([['content-type', 'application/json']]),
127
152
  json() {
128
153
  return {};
129
154
  },
@@ -139,12 +164,33 @@ it('should post with body', async () => {
139
164
  });
140
165
  });
141
166
 
167
+ it('should put', async () => {
168
+ const fetcher = new Fetcher('http/');
169
+ // @ts-ignore
170
+ global.fetch = jest.fn(() => ({
171
+ ok: true,
172
+ headers: new Headers([['content-type', 'application/json']]),
173
+ json() {
174
+ return {};
175
+ },
176
+ clone() {
177
+ return { ...this };
178
+ },
179
+ }));
180
+ await fetcher.put('url', {});
181
+ expect(global.fetch).toHaveBeenCalledWith('http/url', {
182
+ headers: expect.any(Headers),
183
+ method: 'PUT',
184
+ body: '{}',
185
+ });
186
+ });
187
+
142
188
  it('should patch', async () => {
143
189
  const fetcher = new Fetcher('http/');
144
190
  // @ts-ignore
145
191
  global.fetch = jest.fn(() => ({
146
192
  ok: true,
147
- headers: {},
193
+ headers: new Headers([['content-type', 'application/json']]),
148
194
  json() {
149
195
  return {};
150
196
  },
@@ -165,7 +211,7 @@ it('should delete', async () => {
165
211
  // @ts-ignore
166
212
  global.fetch = jest.fn(() => ({
167
213
  ok: true,
168
- headers: {},
214
+ headers: new Headers([['content-type', 'application/json']]),
169
215
  json() {
170
216
  return {};
171
217
  },
@@ -185,7 +231,7 @@ it('should use formData', async () => {
185
231
  // @ts-ignore
186
232
  global.fetch = jest.fn(() => ({
187
233
  ok: true,
188
- headers: {},
234
+ headers: new Headers([['content-type', 'application/json']]),
189
235
  json() {
190
236
  return {};
191
237
  },
package/lib/fetcher.ts CHANGED
@@ -10,22 +10,48 @@ const headersAsObject = (headers: Headers) =>
10
10
  {}
11
11
  );
12
12
 
13
+ export type Fetched<T> = T & {
14
+ headers?: Record<string, any>;
15
+ };
16
+
13
17
  export class Fetcher {
14
18
  public host: string;
15
19
  public token: string | null = null;
20
+ public legacyToken: string | null = null;
21
+ public overwriteClientId?: string;
22
+ private clientIdHeader?: string;
23
+ protected _apiKey: string | null = null;
24
+ public language: string | undefined;
25
+ public lastReceivedHeaders?: Record<string, any>;
16
26
 
17
- constructor(host: string) {
27
+ constructor(host: string, clientIdHeader?: string) {
18
28
  this.host = host;
29
+ this.clientIdHeader = clientIdHeader;
19
30
  }
20
31
 
21
- protected async _fetch<T>(
22
- url: string,
23
- options: RequestInit = {}
24
- ): Promise<T> {
32
+ set apiKey(apiKey: string) {
33
+ this._apiKey = apiKey;
34
+ }
35
+
36
+ prepareRequest(url: string, options: RequestInit = {}) {
25
37
  const headers = new Headers(options.headers || {});
26
38
 
27
- if (this.token && !headers.has('x-prismeai-session-token')) {
28
- headers.append('x-prismeai-session-token', this.token);
39
+ if (this.token && !headers.has('Authorization')) {
40
+ headers.append('Authorization', `Bearer ${this.token}`);
41
+ } else if (this.legacyToken && !headers.has('Authorization')) {
42
+ headers.append('x-prismeai-token', this.legacyToken);
43
+ }
44
+
45
+ if (this._apiKey && !headers.has('x-prismeai-apikey')) {
46
+ headers.append('x-prismeai-api-key', this._apiKey);
47
+ }
48
+
49
+ if (this.language) {
50
+ headers.append('accept-language', this.language);
51
+ }
52
+
53
+ if (options.body instanceof URLSearchParams) {
54
+ headers.set('Content-Type', 'application/x-www-form-urlencoded');
29
55
  }
30
56
 
31
57
  if (
@@ -35,13 +61,30 @@ export class Fetcher {
35
61
  headers.append('Content-Type', 'application/json');
36
62
  }
37
63
 
38
- headers.append('Access-Control-Allow-Origin', '*');
39
-
40
- const res = await global.fetch(`${this.host}${url}`, {
64
+ const fullUrl =
65
+ url.startsWith('http://') || url.startsWith('https://')
66
+ ? url
67
+ : `${this.host}${url}`;
68
+ return global.fetch(fullUrl, {
69
+ credentials: 'include',
41
70
  ...options,
42
71
  headers,
43
72
  });
73
+ }
44
74
 
75
+ protected async _fetch<T>(
76
+ url: string,
77
+ options: RequestInit = {}
78
+ ): Promise<T> {
79
+ const res = await this.prepareRequest(url, options);
80
+ if (options.redirect === 'manual' && res.status === 0) {
81
+ return { redirected: true } as T;
82
+ }
83
+
84
+ this.lastReceivedHeaders = headersAsObject(res.headers);
85
+ if (this.clientIdHeader && this.lastReceivedHeaders[this.clientIdHeader]) {
86
+ this.overwriteClientId = this.lastReceivedHeaders[this.clientIdHeader];
87
+ }
45
88
  if (!res.ok) {
46
89
  let error;
47
90
  try {
@@ -52,18 +95,28 @@ export class Fetcher {
52
95
  throw error;
53
96
  }
54
97
 
55
- const clone = res.clone();
98
+ const contentType = res.headers.get('content-type');
99
+ if (contentType && contentType.includes('application/json')) {
100
+ try {
101
+ const response = (await res.json()) || {};
102
+ Object.defineProperty(response, 'headers', {
103
+ value: headersAsObject(res.headers),
104
+ configurable: false,
105
+ enumerable: false,
106
+ writable: false,
107
+ });
108
+ return response as T;
109
+ } catch (e) {
110
+ return {} as T;
111
+ }
112
+ }
113
+
114
+ const text = await res.text();
115
+
56
116
  try {
57
- const response = (await res.json()) || {};
58
- Object.defineProperty(response, 'headers', {
59
- value: headersAsObject(res.headers),
60
- configurable: false,
61
- enumerable: false,
62
- writable: false,
63
- });
64
- return response;
65
- } catch (e) {
66
- return ((await clone.text()) as unknown) as T;
117
+ return JSON.parse(text) as T;
118
+ } catch {
119
+ return text as T;
67
120
  }
68
121
  }
69
122
 
@@ -73,10 +126,23 @@ export class Fetcher {
73
126
  });
74
127
  }
75
128
 
76
- async post<T>(url: string, body?: Record<string, any>) {
129
+ async post<T>(url: string, body?: Record<string, any>, opts?: RequestInit) {
77
130
  return this._fetch<T>(url, {
78
131
  method: 'POST',
79
- body: body && !(body instanceof FormData) ? JSON.stringify(body) : body,
132
+ body:
133
+ body &&
134
+ !(body instanceof FormData) &&
135
+ !(body instanceof URLSearchParams)
136
+ ? JSON.stringify(body)
137
+ : body,
138
+ ...opts,
139
+ });
140
+ }
141
+
142
+ async put<T>(url: string, body: Record<string, any>) {
143
+ return this._fetch<T>(url, {
144
+ method: 'PUT',
145
+ body: JSON.stringify(body),
80
146
  });
81
147
  }
82
148
 
package/lib/types.ts CHANGED
@@ -2,6 +2,10 @@ import '@prisme.ai/types';
2
2
 
3
3
  export interface Workspace extends Prismeai.Workspace {
4
4
  id: string;
5
+ updatedAt: Date;
6
+ updatedBy: string;
7
+ createdAt: Date;
8
+ createdBy: string;
5
9
  }
6
10
 
7
11
  export interface Event<DateType extends Date | string>
@@ -9,7 +13,7 @@ export interface Event<DateType extends Date | string>
9
13
  createdAt: DateType;
10
14
  }
11
15
 
12
- export type EventsFilters = {
16
+ export type EventsFilters = Record<string, string> & {
13
17
  afterDate?: PrismeaiAPI.EventsLongpolling.Parameters.AfterDate;
14
18
  beforeDate?: PrismeaiAPI.EventsLongpolling.Parameters.BeforeDate;
15
19
  text?: PrismeaiAPI.EventsLongpolling.Parameters.Text;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prisme.ai/sdk",
3
- "version": "0.0.2",
3
+ "version": "1.0.1",
4
4
  "description": "Communicate with Prisme.ai API",
5
5
  "main": "dist/sdk/index.js",
6
6
  "scripts": {
@@ -10,6 +10,7 @@
10
10
  "license": "ISC",
11
11
  "dependencies": {
12
12
  "@prisme.ai/types": "^1.0.9",
13
+ "pkce-challenge": "^3.1.0",
13
14
  "qs": "^6.10.3",
14
15
  "socket.io-client": "^4.4.1"
15
16
  }