@prisme.ai/sdk 0.0.2 → 1.0.0

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.
@@ -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
+ 'x-prismeai-token': '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
+ 'x-prismeai-token': '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,87 @@
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
+ apiKey,
28
+ apiHost = 'https://api.eda.prisme.ai',
29
+ filters,
30
+ api,
31
+ }: {
32
+ workspaceId: string;
33
+ token: string;
34
+ apiKey?: string;
35
+ apiHost?: string;
36
+ filters?: Record<string, any>;
37
+ api: Api;
38
+ }) {
12
39
  this.workspaceId = workspaceId;
13
- this.client = io(`${apiHost}/workspaces/${workspaceId}/events`, {
14
- extraHeaders: {
15
- 'x-prismeai-session-token': token,
16
- },
40
+ const queryString = new URLSearchParams(filters || {}).toString();
41
+ const fullQueryString = queryString ? `?${queryString}` : '';
42
+ const extraHeaders: any = {
43
+ 'x-prismeai-token': token,
44
+ };
45
+ this.lastReceivedEventDate = new Date();
46
+ if (apiKey) {
47
+ extraHeaders['x-prismeai-api-key'] = apiKey;
48
+ }
49
+
50
+ this.client = io(
51
+ `${apiHost}/workspaces/${workspaceId}/events${fullQueryString}`,
52
+ {
53
+ extraHeaders,
54
+ withCredentials: true,
55
+ }
56
+ );
57
+
58
+ const onConnect = () => {
59
+ // Reset last filters
60
+ this.updateFilters({
61
+ payloadQuery: this.filters,
62
+ });
63
+ setTimeout(async () => {
64
+ const events = await api.getEvents(workspaceId, {
65
+ ...this.filters[this.filters.length - 1],
66
+ afterDate: this.lastReceivedEventDate.toISOString(),
67
+ });
68
+ events.reverse().forEach((event) => {
69
+ (this.listeners.get(event.type) || []).forEach((listener) =>
70
+ listener(event)
71
+ );
72
+ });
73
+ }, 2000);
74
+ };
75
+ this.client.on('disconnect', () => {
76
+ if (!this.lastReceivedEventDate) {
77
+ this.lastReceivedEventDate = new Date();
78
+ }
79
+ this.client.off('connect', onConnect);
80
+ this.client.on('connect', onConnect);
17
81
  });
82
+
83
+ this.filters = [filters || {}];
84
+ this.listenedUserTopics = new Map();
18
85
  }
19
86
 
20
87
  get socket() {
@@ -34,31 +101,71 @@ export class Events {
34
101
  }
35
102
 
36
103
  all(listener: (eventName: string, eventData: Prismeai.PrismeEvent) => void) {
37
- this.client.onAny(listener);
104
+ this.client.onAny((eventName: string, eventData: Prismeai.PrismeEvent) => {
105
+ this.lastReceivedEventDate = new Date(eventData?.createdAt);
106
+ return listener(eventName, eventData);
107
+ });
38
108
 
39
109
  return () => this.client.offAny(listener);
40
110
  }
41
111
 
42
- on(
43
- ev: string,
44
- listener: (eventName: string, eventData: Prismeai.PrismeEvent) => void
45
- ) {
112
+ on(ev: string, listener: (eventData: Prismeai.PrismeEvent) => void) {
113
+ this.listeners.set(ev, [...(this.listeners.get(ev) || []), listener]);
114
+
46
115
  this.client.on(ev, listener);
47
- return () => this.client.off(ev, listener);
116
+ return () => {
117
+ this.listeners.set(
118
+ ev,
119
+ (this.listeners.get(ev) || []).filter((l) => l !== listener)
120
+ );
121
+ this.client.off(ev, listener);
122
+ };
48
123
  }
49
124
 
50
- emit(event: string, payload?: any) {
125
+ emit(event: string, payload?: any, options?: any) {
51
126
  this.client.emit('event', {
52
127
  type: event,
53
128
  payload,
129
+ options,
54
130
  });
55
131
  }
56
132
 
133
+ listenTopics({
134
+ event,
135
+ topics,
136
+ }: {
137
+ event: string;
138
+ topics: string | string[];
139
+ }) {
140
+ topics = Array.isArray(topics) ? topics : [topics];
141
+
142
+ this.listenedUserTopics.set(event, topics);
143
+
144
+ this.filters = [
145
+ { ...this.filters[0] },
146
+ {
147
+ 'target.userTopic': Array.from(this.listenedUserTopics).flatMap(
148
+ ([_event, topics]) => topics
149
+ ),
150
+ },
151
+ ];
152
+ this.updateFilters({
153
+ payloadQuery: this.filters,
154
+ });
155
+ }
156
+
157
+ updateFilters(filters: SearchOptions) {
158
+ this.client.emit('filters', filters);
159
+ }
160
+
57
161
  once(
58
162
  ev: string,
59
163
  listener: (eventName: string, eventData: Prismeai.PrismeEvent) => void
60
164
  ) {
61
165
  this.client.once(ev, listener);
166
+ return () => {
167
+ this.client.off(ev, listener);
168
+ };
62
169
  }
63
170
 
64
171
  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
@@ -13,19 +13,31 @@ const headersAsObject = (headers: Headers) =>
13
13
  export class Fetcher {
14
14
  public host: string;
15
15
  public token: string | null = null;
16
+ protected _apiKey: string | null = null;
17
+ public language: string | undefined;
16
18
 
17
19
  constructor(host: string) {
18
20
  this.host = host;
19
21
  }
20
22
 
21
- protected async _fetch<T>(
22
- url: string,
23
- options: RequestInit = {}
24
- ): Promise<T> {
23
+ set apiKey(apiKey: string) {
24
+ this._apiKey = apiKey;
25
+ }
26
+
27
+ prepareRequest(url: string,
28
+ options: RequestInit = {}) {
25
29
  const headers = new Headers(options.headers || {});
26
30
 
27
- if (this.token && !headers.has('x-prismeai-session-token')) {
28
- headers.append('x-prismeai-session-token', this.token);
31
+ if (this.token && !headers.has('x-prismeai-token')) {
32
+ headers.append('x-prismeai-token', this.token);
33
+ }
34
+
35
+ if (this._apiKey && !headers.has('x-prismeai-apikey')) {
36
+ headers.append('x-prismeai-api-key', this._apiKey);
37
+ }
38
+
39
+ if (this.language) {
40
+ headers.append('accept-language', this.language);
29
41
  }
30
42
 
31
43
  if (
@@ -37,10 +49,17 @@ export class Fetcher {
37
49
 
38
50
  headers.append('Access-Control-Allow-Origin', '*');
39
51
 
40
- const res = await global.fetch(`${this.host}${url}`, {
52
+ return global.fetch(`${this.host}${url}`, {
41
53
  ...options,
42
54
  headers,
43
55
  });
56
+ }
57
+
58
+ protected async _fetch<T>(
59
+ url: string,
60
+ options: RequestInit = {}
61
+ ): Promise<T> {
62
+ const res = await this.prepareRequest(url, options);
44
63
 
45
64
  if (!res.ok) {
46
65
  let error;
@@ -52,18 +71,28 @@ export class Fetcher {
52
71
  throw error;
53
72
  }
54
73
 
55
- const clone = res.clone();
74
+ const contentType = res.headers.get('content-type');
75
+ if (contentType && contentType.includes('application/json')) {
76
+ try {
77
+ const response = (await res.json()) || {};
78
+ Object.defineProperty(response, 'headers', {
79
+ value: headersAsObject(res.headers),
80
+ configurable: false,
81
+ enumerable: false,
82
+ writable: false,
83
+ });
84
+ return response as T;
85
+ } catch (e) {
86
+ return {} as T;
87
+ }
88
+ }
89
+
90
+ const text = await res.text();
91
+
56
92
  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;
93
+ return JSON.parse(text) as T;
94
+ } catch {
95
+ return text as T;
67
96
  }
68
97
  }
69
98
 
@@ -80,6 +109,13 @@ export class Fetcher {
80
109
  });
81
110
  }
82
111
 
112
+ async put<T>(url: string, body: Record<string, any>) {
113
+ return this._fetch<T>(url, {
114
+ method: 'PUT',
115
+ body: JSON.stringify(body),
116
+ });
117
+ }
118
+
83
119
  async patch<T>(url: string, body: Record<string, any>) {
84
120
  return this._fetch<T>(url, {
85
121
  method: 'PATCH',
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.0",
4
4
  "description": "Communicate with Prisme.ai API",
5
5
  "main": "dist/sdk/index.js",
6
6
  "scripts": {