@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.
- package/dist/src/handler.d.ts +12 -2
- package/dist/src/handler.js +35 -0
- package/dist/src/helpers.d.ts +15 -0
- package/dist/src/helpers.js +28 -0
- package/dist/src/index.cjs +65 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +1 -0
- package/dist/src/resources/context.d.ts +6 -0
- package/dist/test/helpers.test.d.ts +1 -0
- package/dist/test/helpers.test.js +57 -0
- package/dist/test/resources/provider.test.js +5 -0
- package/package.json +1 -1
- package/src/handler.ts +59 -0
- package/src/helpers.ts +37 -0
- package/src/index.ts +2 -0
- package/src/resources/context.ts +7 -0
- package/test/helpers.test.ts +75 -0
- package/test/resources/provider.test.ts +6 -0
package/dist/src/handler.d.ts
CHANGED
|
@@ -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;
|
package/dist/src/handler.js
CHANGED
|
@@ -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
|
+
};
|
package/dist/src/index.cjs
CHANGED
|
@@ -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;
|
package/dist/src/index.d.ts
CHANGED
|
@@ -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
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 */
|
package/src/resources/context.ts
CHANGED
|
@@ -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 => {
|