@unito/integration-sdk 1.0.3 → 1.0.5

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,6 +1,6 @@
1
1
  import { Router } from 'express';
2
2
  import * as API from '@unito/integration-api';
3
- import { GetItemContext, GetCollectionContext, CreateItemContext, UpdateItemContext, DeleteItemContext, GetCredentialAccountContext, ParseWebhooksContext, UpdateWebhookSubscriptionsContext, AcknowledgeWebhooksContext } from './resources/context.js';
3
+ import { GetBlobContext, GetItemContext, GetCollectionContext, CreateItemContext, UpdateItemContext, DeleteItemContext, GetCredentialAccountContext, ParseWebhooksContext, UpdateWebhookSubscriptionsContext, AcknowledgeWebhooksContext } from './resources/context.js';
4
4
  /**
5
5
  * Handler called to get an individual item.
6
6
  *
@@ -35,6 +35,13 @@ export type UpdateItemHandler = (context: UpdateItemContext<any, any, any>) => P
35
35
  * @param context {@link DeleteItemContext}
36
36
  */
37
37
  export type DeleteItemHandler = (context: DeleteItemContext<any, any>) => Promise<void>;
38
+ /**
39
+ * Handler called to get a Binary Large Object.
40
+ *
41
+ * @param context {@link BlobItemContext}
42
+ * @returns A {@link ReadableStream} of the Blob.
43
+ */
44
+ export type GetBlobHandler = (context: GetBlobContext<any, any>) => Promise<ReadableStream<Uint8Array>>;
38
45
  /**
39
46
  * Handler called to retrieve the account details associated with the credentials.
40
47
  *
@@ -83,6 +90,9 @@ export type ItemHandlers = {
83
90
  updateItem?: UpdateItemHandler;
84
91
  deleteItem?: DeleteItemHandler;
85
92
  };
93
+ export type BlobHandlers = {
94
+ getBlob: GetBlobHandler;
95
+ };
86
96
  export type CredentialAccountHandlers = {
87
97
  getCredentialAccount: GetCredentialAccountHandler;
88
98
  };
@@ -95,7 +105,7 @@ export type WebhookSubscriptionHandlers = {
95
105
  export type AcknowledgeWebhookHandlers = {
96
106
  acknowledgeWebhooks: AcknowledgeWebhooksHandler;
97
107
  };
98
- export type HandlersInput = ItemHandlers | CredentialAccountHandlers | ParseWebhookHandlers | WebhookSubscriptionHandlers | AcknowledgeWebhookHandlers;
108
+ export type HandlersInput = ItemHandlers | BlobHandlers | CredentialAccountHandlers | ParseWebhookHandlers | WebhookSubscriptionHandlers | AcknowledgeWebhookHandlers;
99
109
  export declare class Handler {
100
110
  private path;
101
111
  private pathWithIdentifier;
@@ -177,6 +177,41 @@ export class Handler {
177
177
  res.status(204).send(null);
178
178
  });
179
179
  }
180
+ if (this.handlers.getBlob) {
181
+ console.debug(` Enabling getBlob at GET ${this.pathWithIdentifier}`);
182
+ const handler = this.handlers.getBlob;
183
+ router.get(this.pathWithIdentifier, async (req, res) => {
184
+ if (!res.locals.credentials) {
185
+ throw new UnauthorizedError();
186
+ }
187
+ const blob = await handler({
188
+ credentials: res.locals.credentials,
189
+ secrets: res.locals.secrets,
190
+ logger: res.locals.logger,
191
+ signal: res.locals.signal,
192
+ params: req.params,
193
+ query: req.query,
194
+ });
195
+ res.writeHead(200, {
196
+ 'Content-Type': 'application/octet-stream',
197
+ });
198
+ const reader = blob.getReader();
199
+ let isDone = false;
200
+ try {
201
+ while (!isDone) {
202
+ const chunk = await reader.read();
203
+ isDone = chunk.done;
204
+ if (chunk.value) {
205
+ res.write(chunk.value);
206
+ }
207
+ }
208
+ }
209
+ finally {
210
+ reader.releaseLock();
211
+ }
212
+ res.end();
213
+ });
214
+ }
180
215
  if (this.handlers.getCredentialAccount) {
181
216
  const handler = this.handlers.getCredentialAccount;
182
217
  console.debug(` Enabling getCredentialAccount at GET ${this.pathWithIdentifier}`);
@@ -0,0 +1,15 @@
1
+ import { FieldSchema } from '@unito/integration-api';
2
+ import { Filter } from './index.js';
3
+ /**
4
+ * Use this helper function to retrieve the applicable filters from the context object. While using filters
5
+ * directly from context might work, it doesn't offer any guarantees about the shape of the filters nor the
6
+ * validity of the fields against which the filters are applied. On the other hand, this function ensures that
7
+ * all filters are valid and that the fields against which the filters are applied are present in the schema.
8
+ *
9
+ * @param context The object containing the raw filters
10
+ * @param fields The schema of the item against which the filters are applied
11
+ * @returns The validated filters
12
+ */
13
+ export declare const getApplicableFilters: (context: {
14
+ filters: Filter[];
15
+ }, fields: FieldSchema[]) => Filter[];
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Use this helper function to retrieve the applicable filters from the context object. While using filters
3
+ * directly from context might work, it doesn't offer any guarantees about the shape of the filters nor the
4
+ * validity of the fields against which the filters are applied. On the other hand, this function ensures that
5
+ * all filters are valid and that the fields against which the filters are applied are present in the schema.
6
+ *
7
+ * @param context The object containing the raw filters
8
+ * @param fields The schema of the item against which the filters are applied
9
+ * @returns The validated filters
10
+ */
11
+ export const getApplicableFilters = (context, fields) => {
12
+ const applicableFilters = [];
13
+ for (const filter of context.filters) {
14
+ let field = undefined;
15
+ const filterFieldParts = filter.field.split(':', 2);
16
+ switch (filterFieldParts[0]) {
17
+ case 'semantic':
18
+ field = fields.find(f => f.semantic === filterFieldParts[1]);
19
+ break;
20
+ default:
21
+ field = fields.find(f => f.name === filterFieldParts[0]);
22
+ }
23
+ if (field) {
24
+ applicableFilters.push({ ...filter, field: field.name });
25
+ }
26
+ }
27
+ return applicableFilters;
28
+ };
@@ -692,6 +692,41 @@ class Handler {
692
692
  res.status(204).send(null);
693
693
  });
694
694
  }
695
+ if (this.handlers.getBlob) {
696
+ console.debug(` Enabling getBlob at GET ${this.pathWithIdentifier}`);
697
+ const handler = this.handlers.getBlob;
698
+ router.get(this.pathWithIdentifier, async (req, res) => {
699
+ if (!res.locals.credentials) {
700
+ throw new UnauthorizedError();
701
+ }
702
+ const blob = await handler({
703
+ credentials: res.locals.credentials,
704
+ secrets: res.locals.secrets,
705
+ logger: res.locals.logger,
706
+ signal: res.locals.signal,
707
+ params: req.params,
708
+ query: req.query,
709
+ });
710
+ res.writeHead(200, {
711
+ 'Content-Type': 'application/octet-stream',
712
+ });
713
+ const reader = blob.getReader();
714
+ let isDone = false;
715
+ try {
716
+ while (!isDone) {
717
+ const chunk = await reader.read();
718
+ isDone = chunk.done;
719
+ if (chunk.value) {
720
+ res.write(chunk.value);
721
+ }
722
+ }
723
+ }
724
+ finally {
725
+ reader.releaseLock();
726
+ }
727
+ res.end();
728
+ });
729
+ }
695
730
  if (this.handlers.getCredentialAccount) {
696
731
  const handler = this.handlers.getCredentialAccount;
697
732
  console.debug(` Enabling getCredentialAccount at GET ${this.pathWithIdentifier}`);
@@ -1080,9 +1115,39 @@ class Provider {
1080
1115
  }
1081
1116
  }
1082
1117
 
1118
+ /**
1119
+ * Use this helper function to retrieve the applicable filters from the context object. While using filters
1120
+ * directly from context might work, it doesn't offer any guarantees about the shape of the filters nor the
1121
+ * validity of the fields against which the filters are applied. On the other hand, this function ensures that
1122
+ * all filters are valid and that the fields against which the filters are applied are present in the schema.
1123
+ *
1124
+ * @param context The object containing the raw filters
1125
+ * @param fields The schema of the item against which the filters are applied
1126
+ * @returns The validated filters
1127
+ */
1128
+ const getApplicableFilters = (context, fields) => {
1129
+ const applicableFilters = [];
1130
+ for (const filter of context.filters) {
1131
+ let field = undefined;
1132
+ const filterFieldParts = filter.field.split(':', 2);
1133
+ switch (filterFieldParts[0]) {
1134
+ case 'semantic':
1135
+ field = fields.find(f => f.semantic === filterFieldParts[1]);
1136
+ break;
1137
+ default:
1138
+ field = fields.find(f => f.name === filterFieldParts[0]);
1139
+ }
1140
+ if (field) {
1141
+ applicableFilters.push({ ...filter, field: field.name });
1142
+ }
1143
+ }
1144
+ return applicableFilters;
1145
+ };
1146
+
1083
1147
  exports.Api = integrationApi__namespace;
1084
1148
  exports.Cache = Cache;
1085
1149
  exports.Handler = Handler;
1086
1150
  exports.HttpErrors = httpErrors;
1087
1151
  exports.Integration = Integration;
1088
1152
  exports.Provider = Provider;
1153
+ exports.getApplicableFilters = getApplicableFilters;
@@ -5,6 +5,8 @@ export * from './handler.js';
5
5
  export { Provider, type Response as ProviderResponse, type RequestOptions as ProviderRequestOptions, type RateLimiter, } from './resources/provider.js';
6
6
  export type { Secrets } from './middlewares/secrets.js';
7
7
  export type { Credentials } from './middlewares/credentials.js';
8
+ export type { Filter } from './middlewares/filters.js';
8
9
  export * as HttpErrors from './httpErrors.js';
10
+ export { getApplicableFilters } from './helpers.js';
9
11
  export * from './resources/context.js';
10
12
  export { type default as Logger } from './resources/logger.js';
package/dist/src/index.js CHANGED
@@ -5,5 +5,6 @@ export { default as Integration } from './integration.js';
5
5
  export * from './handler.js';
6
6
  export { Provider, } from './resources/provider.js';
7
7
  export * as HttpErrors from './httpErrors.js';
8
+ export { getApplicableFilters } from './helpers.js';
8
9
  export * from './resources/context.js';
9
10
  /* c8 ignore stop */
@@ -61,6 +61,12 @@ export type Context<P extends Maybe<Params> = Params, Q extends Maybe<Query> = Q
61
61
  * @see {@link Context}
62
62
  */
63
63
  export type GetItemContext<P extends Maybe<Params> = Empty, Q extends Query = Empty> = Context<P, Q>;
64
+ /**
65
+ * Context received by the `GetBlobHandler`, same as `GetItemContext`.
66
+ *
67
+ * @see {@link Context}
68
+ */
69
+ export type GetBlobContext<P extends Maybe<Params> = Empty, Q extends Query = Empty> = Context<P, Q>;
64
70
  /**
65
71
  * Context received by the `GetCollectionHandler`.
66
72
  *
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,57 @@
1
+ import { FieldValueType, OperatorType, Semantic } from '@unito/integration-api';
2
+ import assert from 'node:assert/strict';
3
+ import { describe, it } from 'node:test';
4
+ import { getApplicableFilters } from '../src/helpers.js';
5
+ describe('Helpers', () => {
6
+ describe('getApplicableFilters', () => {
7
+ it('returns only filters for defined fields', () => {
8
+ const actual = getApplicableFilters({
9
+ filters: [
10
+ { field: 'status', operator: OperatorType.EQUAL, values: ['active', 'pending'] },
11
+ { field: 'email', operator: OperatorType.IS_NOT_NULL, values: [] },
12
+ ],
13
+ }, [
14
+ {
15
+ name: 'status',
16
+ label: 'Status',
17
+ type: FieldValueType.STRING,
18
+ },
19
+ ]);
20
+ const expected = [{ field: 'status', operator: OperatorType.EQUAL, values: ['active', 'pending'] }];
21
+ assert.deepEqual(actual, expected);
22
+ });
23
+ it('translates semantics into field names', () => {
24
+ const actual = getApplicableFilters({
25
+ filters: [
26
+ { field: 'semantic:displayName', operator: OperatorType.START_WITH, values: ['Bob'] },
27
+ { field: 'semantic:createdAt', operator: OperatorType.EQUAL, values: ['2021-01-01'] },
28
+ ],
29
+ }, [
30
+ {
31
+ name: 'name',
32
+ label: 'Name',
33
+ type: FieldValueType.STRING,
34
+ semantic: Semantic.DISPLAY_NAME,
35
+ },
36
+ ]);
37
+ const expected = [{ field: 'name', operator: OperatorType.START_WITH, values: ['Bob'] }];
38
+ assert.deepEqual(actual, expected);
39
+ });
40
+ it('gracefully handle garbage', () => {
41
+ const actual = getApplicableFilters({
42
+ filters: [
43
+ { field: '...', operator: OperatorType.EQUAL, values: [] },
44
+ { field: ':', operator: OperatorType.EQUAL, values: [] },
45
+ { field: '', operator: OperatorType.EQUAL, values: [] },
46
+ ],
47
+ }, [
48
+ {
49
+ name: 'status',
50
+ label: 'Status',
51
+ type: FieldValueType.STRING,
52
+ },
53
+ ]);
54
+ assert.deepEqual(actual, []);
55
+ });
56
+ });
57
+ });
@@ -3,6 +3,11 @@ import { describe, it } from 'node:test';
3
3
  import { Provider } from '../../src/resources/provider.js';
4
4
  import * as HttpErrors from '../../src/httpErrors.js';
5
5
  import Logger from '../../src/resources/logger.js';
6
+ // There is currently an issue with node 20.12 and fetch mocking. A quick fix is to first call fetch so it's getter
7
+ // get properly instantiated, which allow it to be mocked properly.
8
+ // Issue: https://github.com/nodejs/node/issues/52015
9
+ // PR fix: https://github.com/nodejs/node/pull/52275
10
+ globalThis.fetch = fetch;
6
11
  describe('Provider', () => {
7
12
  const provider = new Provider({
8
13
  prepareRequest: requestOptions => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unito/integration-sdk",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Integration SDK",
5
5
  "type": "module",
6
6
  "types": "dist/src/index.d.ts",
package/src/handler.ts CHANGED
@@ -3,6 +3,7 @@ import * as API from '@unito/integration-api';
3
3
  import { InvalidHandler } from './errors.js';
4
4
  import { UnauthorizedError, BadRequestError } from './httpErrors.js';
5
5
  import {
6
+ GetBlobContext,
6
7
  GetItemContext,
7
8
  GetCollectionContext,
8
9
  CreateItemContext,
@@ -53,6 +54,14 @@ export type UpdateItemHandler = (context: UpdateItemContext<any, any, any>) => P
53
54
  */
54
55
  export type DeleteItemHandler = (context: DeleteItemContext<any, any>) => Promise<void>;
55
56
 
57
+ /**
58
+ * Handler called to get a Binary Large Object.
59
+ *
60
+ * @param context {@link BlobItemContext}
61
+ * @returns A {@link ReadableStream} of the Blob.
62
+ */
63
+ export type GetBlobHandler = (context: GetBlobContext<any, any>) => Promise<ReadableStream<Uint8Array>>;
64
+
56
65
  /**
57
66
  * Handler called to retrieve the account details associated with the credentials.
58
67
  *
@@ -114,6 +123,10 @@ export type ItemHandlers = {
114
123
  deleteItem?: DeleteItemHandler;
115
124
  };
116
125
 
126
+ export type BlobHandlers = {
127
+ getBlob: GetBlobHandler;
128
+ };
129
+
117
130
  export type CredentialAccountHandlers = {
118
131
  getCredentialAccount: GetCredentialAccountHandler;
119
132
  };
@@ -132,6 +145,7 @@ export type AcknowledgeWebhookHandlers = {
132
145
 
133
146
  export type HandlersInput =
134
147
  | ItemHandlers
148
+ | BlobHandlers
135
149
  | CredentialAccountHandlers
136
150
  | ParseWebhookHandlers
137
151
  | WebhookSubscriptionHandlers
@@ -139,6 +153,7 @@ export type HandlersInput =
139
153
 
140
154
  type Handlers = Partial<
141
155
  ItemHandlers &
156
+ BlobHandlers &
142
157
  CredentialAccountHandlers &
143
158
  ParseWebhookHandlers &
144
159
  WebhookSubscriptionHandlers &
@@ -380,6 +395,50 @@ export class Handler {
380
395
  });
381
396
  }
382
397
 
398
+ if (this.handlers.getBlob) {
399
+ console.debug(` Enabling getBlob at GET ${this.pathWithIdentifier}`);
400
+
401
+ const handler = this.handlers.getBlob;
402
+
403
+ router.get(this.pathWithIdentifier, async (req, res) => {
404
+ if (!res.locals.credentials) {
405
+ throw new UnauthorizedError();
406
+ }
407
+
408
+ const blob = await handler({
409
+ credentials: res.locals.credentials,
410
+ secrets: res.locals.secrets,
411
+ logger: res.locals.logger,
412
+ signal: res.locals.signal,
413
+ params: req.params,
414
+ query: req.query,
415
+ });
416
+
417
+ res.writeHead(200, {
418
+ 'Content-Type': 'application/octet-stream',
419
+ });
420
+
421
+ const reader = blob.getReader();
422
+
423
+ let isDone = false;
424
+
425
+ try {
426
+ while (!isDone) {
427
+ const chunk = await reader.read();
428
+ isDone = chunk.done;
429
+
430
+ if (chunk.value) {
431
+ res.write(chunk.value);
432
+ }
433
+ }
434
+ } finally {
435
+ reader.releaseLock();
436
+ }
437
+
438
+ res.end();
439
+ });
440
+ }
441
+
383
442
  if (this.handlers.getCredentialAccount) {
384
443
  const handler = this.handlers.getCredentialAccount;
385
444
 
package/src/helpers.ts ADDED
@@ -0,0 +1,37 @@
1
+ import { FieldSchema } from '@unito/integration-api';
2
+
3
+ import { Filter } from './index.js';
4
+
5
+ /**
6
+ * Use this helper function to retrieve the applicable filters from the context object. While using filters
7
+ * directly from context might work, it doesn't offer any guarantees about the shape of the filters nor the
8
+ * validity of the fields against which the filters are applied. On the other hand, this function ensures that
9
+ * all filters are valid and that the fields against which the filters are applied are present in the schema.
10
+ *
11
+ * @param context The object containing the raw filters
12
+ * @param fields The schema of the item against which the filters are applied
13
+ * @returns The validated filters
14
+ */
15
+ export const getApplicableFilters = (context: { filters: Filter[] }, fields: FieldSchema[]): Filter[] => {
16
+ const applicableFilters: Filter[] = [];
17
+
18
+ for (const filter of context.filters) {
19
+ let field: FieldSchema | undefined = undefined;
20
+
21
+ const filterFieldParts = filter.field.split(':', 2);
22
+
23
+ switch (filterFieldParts[0]) {
24
+ case 'semantic':
25
+ field = fields.find(f => f.semantic === filterFieldParts[1]);
26
+ break;
27
+ default:
28
+ field = fields.find(f => f.name === filterFieldParts[0]);
29
+ }
30
+
31
+ if (field) {
32
+ applicableFilters.push({ ...filter, field: field.name });
33
+ }
34
+ }
35
+
36
+ return applicableFilters;
37
+ };
package/src/index.ts CHANGED
@@ -11,7 +11,9 @@ export {
11
11
  } from './resources/provider.js';
12
12
  export type { Secrets } from './middlewares/secrets.js';
13
13
  export type { Credentials } from './middlewares/credentials.js';
14
+ export type { Filter } from './middlewares/filters.js';
14
15
  export * as HttpErrors from './httpErrors.js';
16
+ export { getApplicableFilters } from './helpers.js';
15
17
  export * from './resources/context.js';
16
18
  export { type default as Logger } from './resources/logger.js';
17
19
  /* c8 ignore stop */
@@ -67,6 +67,13 @@ export type Context<P extends Maybe<Params> = Params, Q extends Maybe<Query> = Q
67
67
  */
68
68
  export type GetItemContext<P extends Maybe<Params> = Empty, Q extends Query = Empty> = Context<P, Q>;
69
69
 
70
+ /**
71
+ * Context received by the `GetBlobHandler`, same as `GetItemContext`.
72
+ *
73
+ * @see {@link Context}
74
+ */
75
+ export type GetBlobContext<P extends Maybe<Params> = Empty, Q extends Query = Empty> = Context<P, Q>;
76
+
70
77
  /**
71
78
  * Context received by the `GetCollectionHandler`.
72
79
  *
@@ -0,0 +1,75 @@
1
+ import { FieldValueType, OperatorType, Semantic } from '@unito/integration-api';
2
+
3
+ import assert from 'node:assert/strict';
4
+ import { describe, it } from 'node:test';
5
+ import { getApplicableFilters } from '../src/helpers.js';
6
+
7
+ describe('Helpers', () => {
8
+ describe('getApplicableFilters', () => {
9
+ it('returns only filters for defined fields', () => {
10
+ const actual = getApplicableFilters(
11
+ {
12
+ filters: [
13
+ { field: 'status', operator: OperatorType.EQUAL, values: ['active', 'pending'] },
14
+ { field: 'email', operator: OperatorType.IS_NOT_NULL, values: [] },
15
+ ],
16
+ },
17
+ [
18
+ {
19
+ name: 'status',
20
+ label: 'Status',
21
+ type: FieldValueType.STRING,
22
+ },
23
+ ],
24
+ );
25
+
26
+ const expected = [{ field: 'status', operator: OperatorType.EQUAL, values: ['active', 'pending'] }];
27
+
28
+ assert.deepEqual(actual, expected);
29
+ });
30
+
31
+ it('translates semantics into field names', () => {
32
+ const actual = getApplicableFilters(
33
+ {
34
+ filters: [
35
+ { field: 'semantic:displayName', operator: OperatorType.START_WITH, values: ['Bob'] },
36
+ { field: 'semantic:createdAt', operator: OperatorType.EQUAL, values: ['2021-01-01'] },
37
+ ],
38
+ },
39
+ [
40
+ {
41
+ name: 'name',
42
+ label: 'Name',
43
+ type: FieldValueType.STRING,
44
+ semantic: Semantic.DISPLAY_NAME,
45
+ },
46
+ ],
47
+ );
48
+
49
+ const expected = [{ field: 'name', operator: OperatorType.START_WITH, values: ['Bob'] }];
50
+
51
+ assert.deepEqual(actual, expected);
52
+ });
53
+
54
+ it('gracefully handle garbage', () => {
55
+ const actual = getApplicableFilters(
56
+ {
57
+ filters: [
58
+ { field: '...', operator: OperatorType.EQUAL, values: [] },
59
+ { field: ':', operator: OperatorType.EQUAL, values: [] },
60
+ { field: '', operator: OperatorType.EQUAL, values: [] },
61
+ ],
62
+ },
63
+ [
64
+ {
65
+ name: 'status',
66
+ label: 'Status',
67
+ type: FieldValueType.STRING,
68
+ },
69
+ ],
70
+ );
71
+
72
+ assert.deepEqual(actual, []);
73
+ });
74
+ });
75
+ });
@@ -5,6 +5,12 @@ import { Provider } from '../../src/resources/provider.js';
5
5
  import * as HttpErrors from '../../src/httpErrors.js';
6
6
  import Logger from '../../src/resources/logger.js';
7
7
 
8
+ // There is currently an issue with node 20.12 and fetch mocking. A quick fix is to first call fetch so it's getter
9
+ // get properly instantiated, which allow it to be mocked properly.
10
+ // Issue: https://github.com/nodejs/node/issues/52015
11
+ // PR fix: https://github.com/nodejs/node/pull/52275
12
+ globalThis.fetch = fetch;
13
+
8
14
  describe('Provider', () => {
9
15
  const provider = new Provider({
10
16
  prepareRequest: requestOptions => {