@unito/integration-sdk 3.0.0 → 4.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.
- package/CLAUDE.md +80 -0
- package/dist/src/helpers.d.ts +14 -0
- package/dist/src/helpers.js +20 -0
- package/dist/src/index.cjs +21 -0
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/test/helpers.test.js +95 -1
- package/package.json +2 -2
- package/src/helpers.ts +27 -0
- package/src/index.ts +1 -1
- package/test/helpers.test.ts +110 -1
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## What This Is
|
|
6
|
+
|
|
7
|
+
`@unito/integration-sdk` — a TypeScript framework for building REST API integrations compatible with the Unito Integration API Specification. It wraps Express 5 and provides handler routing, middleware chains for request parsing, structured logging, a Provider HTTP client for downstream APIs, and standardized error handling.
|
|
8
|
+
|
|
9
|
+
## Commands
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install # Install deps (also runs compile via prepare hook)
|
|
13
|
+
npm run compile # Build ESM (tsc) + CJS (rollup)
|
|
14
|
+
npm run compile:watch # Watch mode recompile
|
|
15
|
+
npm test # Run all tests
|
|
16
|
+
ONLY=Handler npm test # Run tests matching pattern "Handler"
|
|
17
|
+
npm run test:debug # Run tests with --inspect-brk
|
|
18
|
+
npm run lint # ESLint --fix + Prettier --write on src/ and test/
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Tests use **Node.js built-in test runner** (`node:test` + `node:assert/strict`) with `ts-node/esm` loader — no compilation needed to run tests. HTTP mocking uses `nock`.
|
|
22
|
+
|
|
23
|
+
## Architecture
|
|
24
|
+
|
|
25
|
+
### Request Flow
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
HTTP Request
|
|
29
|
+
→ finish (error/log hooks on response end)
|
|
30
|
+
→ express.json (1MB limit)
|
|
31
|
+
→ start (records hrtime)
|
|
32
|
+
→ correlationId (generates/extracts X-Correlation-Id)
|
|
33
|
+
→ logger (structured logger decorated with correlation ID)
|
|
34
|
+
→ credentials (decodes base64 JSON from X-Unito-Credentials header)
|
|
35
|
+
→ secrets (decodes base64 JSON from X-Unito-Secrets header)
|
|
36
|
+
→ filters / search / selects / relations (parse query params)
|
|
37
|
+
→ signal (AbortSignal from X-Unito-Operation-Deadline timestamp)
|
|
38
|
+
→ Handler routes (user-registered)
|
|
39
|
+
→ errors middleware (catches, formats HttpError responses)
|
|
40
|
+
→ notFound middleware (404 fallback)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Core Classes
|
|
44
|
+
|
|
45
|
+
**Integration** (`src/integration.ts`) — entry point. Call `addHandler(path, handlers)` to register routes, then `start()`. All handlers must be added before `start()` — the server exits on violation.
|
|
46
|
+
|
|
47
|
+
**Handler** (`src/handler.ts`) — takes a path and a handlers object, generates an Express Router. Path determines routing:
|
|
48
|
+
- `/things/:thingId` — last segment is `:variable` → item route. `getItem` at `GET /things/:thingId`, `getCollection` at `GET /things`, `createItem` at `POST /things`, etc.
|
|
49
|
+
- `/things` — no trailing variable → either item-level or collection-level ops, but not both on the same path.
|
|
50
|
+
|
|
51
|
+
Handler types (pick one per `addHandler` call):
|
|
52
|
+
- `ItemHandlers` — getItem, getCollection, createItem, updateItem, deleteItem
|
|
53
|
+
- `BlobHandlers` — getBlob, createBlob
|
|
54
|
+
- `CredentialAccountHandlers` — getCredentialAccount
|
|
55
|
+
- `ParseWebhookHandlers`, `WebhookSubscriptionHandlers`, `AcknowledgeWebhookHandlers`
|
|
56
|
+
|
|
57
|
+
**Provider** (`src/resources/provider.ts`) — HTTP client for calling downstream provider APIs. Initialized with `prepareRequest` (returns base URL + auth headers), optional `rateLimiter`, and optional `customErrorHandler`. Methods: `get`, `post`, `put`, `patch`, `delete`, `streamingGet`, `postForm`, `postStream`, `putBuffer`.
|
|
58
|
+
|
|
59
|
+
**Context** (`src/resources/context.ts`) — typed context object passed to every handler. Contains `credentials`, `secrets`, `logger`, `signal`, `params`, `query`. GetCollectionContext additionally has `filters`, `search`, `selects`, `relations`. Create/Update contexts have `body`.
|
|
60
|
+
|
|
61
|
+
**HttpErrors** (`src/httpErrors.ts`) — throw these from handlers for proper error responses. BadRequestError(400), UnauthorizedError(401), ForbiddenError(403), NotFoundError(404), TimeoutError(408), UnprocessableEntityError(422), ProviderInstanceLockedError(423), RateLimitExceededError(429). The latter two support `retryAfter`.
|
|
62
|
+
|
|
63
|
+
### Logging
|
|
64
|
+
|
|
65
|
+
Logger (`src/resources/logger.ts`) produces Datadog-compatible structured JSON in production, colored output in dev. Keys are auto-converted to snake_case. Sensitive fields (access_token, password, email, etc.) are auto-redacted. Log size truncated at 20KB (configurable via `MAX_LOG_MESSAGE_SIZE` env var).
|
|
66
|
+
|
|
67
|
+
### Cache
|
|
68
|
+
|
|
69
|
+
`Cache.create(redisUrl?)` returns a Redis-backed or local in-memory cache via the `cachette` library.
|
|
70
|
+
|
|
71
|
+
## Code Conventions
|
|
72
|
+
|
|
73
|
+
- **ES Modules** — `"type": "module"` in package.json. All internal imports use `.js` extensions (`import X from './foo.js'`).
|
|
74
|
+
- **Dual output** — ESM via `tsc`, CJS via Rollup. Published with conditional exports.
|
|
75
|
+
- **Strict TypeScript** — strict mode, noUncheckedIndexedAccess, exactOptionalPropertyTypes, no unused locals/params.
|
|
76
|
+
- **Branded types** — e.g. `type Path = string & { __brand: 'Path' }` with assertion functions (`asserts path is Path`).
|
|
77
|
+
- **No barrel files** — import directly from source modules, not from index re-exports.
|
|
78
|
+
- **Prettier** — single quotes, trailing commas, 120 char width, no parens on single-param arrows.
|
|
79
|
+
- **Test files** — `test/**/*.test.ts` mirroring src structure. Use `describe`/`it`/`beforeEach`/`afterEach` from `node:test`, assertions from `node:assert/strict`, mocking with `mock.method()`.
|
|
80
|
+
- **Express typing** — `res.locals` is typed via global Express namespace augmentation.
|
package/dist/src/helpers.d.ts
CHANGED
|
@@ -13,3 +13,17 @@ import { Filter } from './index.js';
|
|
|
13
13
|
export declare function getApplicableFilters(context: {
|
|
14
14
|
filters: Filter[];
|
|
15
15
|
}, fields: FieldSchema[]): Filter[];
|
|
16
|
+
/**
|
|
17
|
+
* Use this helper function to build the query params for a collection next page URL.
|
|
18
|
+
* It re-encodes the parsed filters and selects back into query-param form,
|
|
19
|
+
* so integrations can easily construct `nextPage` URLs.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* const params = buildCollectionQueryParams({ filters: context.filters, selects: context.selects });
|
|
23
|
+
* params.append('cursor', nextCursor);
|
|
24
|
+
* return { info: { nextPage: `/items?${params.toString()}` }, data: [...] };
|
|
25
|
+
*/
|
|
26
|
+
export declare function buildCollectionQueryParams(params?: {
|
|
27
|
+
filters?: Filter[];
|
|
28
|
+
selects?: string[];
|
|
29
|
+
}): URLSearchParams;
|
package/dist/src/helpers.js
CHANGED
|
@@ -26,3 +26,23 @@ export function getApplicableFilters(context, fields) {
|
|
|
26
26
|
}
|
|
27
27
|
return applicableFilters;
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Use this helper function to build the query params for a collection next page URL.
|
|
31
|
+
* It re-encodes the parsed filters and selects back into query-param form,
|
|
32
|
+
* so integrations can easily construct `nextPage` URLs.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* const params = buildCollectionQueryParams({ filters: context.filters, selects: context.selects });
|
|
36
|
+
* params.append('cursor', nextCursor);
|
|
37
|
+
* return { info: { nextPage: `/items?${params.toString()}` }, data: [...] };
|
|
38
|
+
*/
|
|
39
|
+
export function buildCollectionQueryParams(params) {
|
|
40
|
+
const result = new URLSearchParams();
|
|
41
|
+
if (params?.filters?.length) {
|
|
42
|
+
result.set('filter', params.filters.map(f => `${f.field}${f.operator}${(f.values ?? []).map(encodeURIComponent).join('|')}`).join(','));
|
|
43
|
+
}
|
|
44
|
+
if (params?.selects?.length) {
|
|
45
|
+
result.set('select', params.selects.map(encodeURIComponent).join(','));
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
package/dist/src/index.cjs
CHANGED
|
@@ -1705,6 +1705,26 @@ function getApplicableFilters(context, fields) {
|
|
|
1705
1705
|
}
|
|
1706
1706
|
return applicableFilters;
|
|
1707
1707
|
}
|
|
1708
|
+
/**
|
|
1709
|
+
* Use this helper function to build the query params for a collection next page URL.
|
|
1710
|
+
* It re-encodes the parsed filters and selects back into query-param form,
|
|
1711
|
+
* so integrations can easily construct `nextPage` URLs.
|
|
1712
|
+
*
|
|
1713
|
+
* @example
|
|
1714
|
+
* const params = buildCollectionQueryParams({ filters: context.filters, selects: context.selects });
|
|
1715
|
+
* params.append('cursor', nextCursor);
|
|
1716
|
+
* return { info: { nextPage: `/items?${params.toString()}` }, data: [...] };
|
|
1717
|
+
*/
|
|
1718
|
+
function buildCollectionQueryParams(params) {
|
|
1719
|
+
const result = new URLSearchParams();
|
|
1720
|
+
if (params?.filters?.length) {
|
|
1721
|
+
result.set('filter', params.filters.map(f => `${f.field}${f.operator}${(f.values ?? []).map(encodeURIComponent).join('|')}`).join(','));
|
|
1722
|
+
}
|
|
1723
|
+
if (params?.selects?.length) {
|
|
1724
|
+
result.set('select', params.selects.map(encodeURIComponent).join(','));
|
|
1725
|
+
}
|
|
1726
|
+
return result;
|
|
1727
|
+
}
|
|
1708
1728
|
|
|
1709
1729
|
exports.Api = integrationApi__namespace;
|
|
1710
1730
|
exports.Cache = Cache;
|
|
@@ -1713,4 +1733,5 @@ exports.HttpErrors = httpErrors;
|
|
|
1713
1733
|
exports.Integration = Integration;
|
|
1714
1734
|
exports.NULL_LOGGER = NULL_LOGGER;
|
|
1715
1735
|
exports.Provider = Provider;
|
|
1736
|
+
exports.buildCollectionQueryParams = buildCollectionQueryParams;
|
|
1716
1737
|
exports.getApplicableFilters = getApplicableFilters;
|
package/dist/src/index.d.ts
CHANGED
|
@@ -7,6 +7,6 @@ export type { Secrets } from './middlewares/secrets.js';
|
|
|
7
7
|
export type { Credentials } from './middlewares/credentials.js';
|
|
8
8
|
export type { Filter } from './middlewares/filters.js';
|
|
9
9
|
export * as HttpErrors from './httpErrors.js';
|
|
10
|
-
export { getApplicableFilters } from './helpers.js';
|
|
10
|
+
export { buildCollectionQueryParams, getApplicableFilters } from './helpers.js';
|
|
11
11
|
export * from './resources/context.js';
|
|
12
12
|
export { type default as Logger, NULL_LOGGER } from './resources/logger.js';
|
package/dist/src/index.js
CHANGED
|
@@ -5,7 +5,7 @@ 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
|
+
export { buildCollectionQueryParams, getApplicableFilters } from './helpers.js';
|
|
9
9
|
export * from './resources/context.js';
|
|
10
10
|
export { NULL_LOGGER } from './resources/logger.js';
|
|
11
11
|
/* c8 ignore stop */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { FieldValueTypes, OperatorTypes, Semantics } from '@unito/integration-api';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { describe, it } from 'node:test';
|
|
4
|
-
import { getApplicableFilters } from '../src/helpers.js';
|
|
4
|
+
import { buildCollectionQueryParams, getApplicableFilters } from '../src/helpers.js';
|
|
5
5
|
describe('Helpers', () => {
|
|
6
6
|
describe('getApplicableFilters', () => {
|
|
7
7
|
it('returns only filters for defined fields', () => {
|
|
@@ -54,4 +54,98 @@ describe('Helpers', () => {
|
|
|
54
54
|
assert.deepEqual(actual, []);
|
|
55
55
|
});
|
|
56
56
|
});
|
|
57
|
+
describe('buildCollectionQueryParams', () => {
|
|
58
|
+
it('encodes a single filter with one value', () => {
|
|
59
|
+
const result = buildCollectionQueryParams({
|
|
60
|
+
filters: [{ field: 'status', operator: OperatorTypes.EQUAL, values: ['active'] }],
|
|
61
|
+
});
|
|
62
|
+
assert.equal(result.get('filter'), 'status=active');
|
|
63
|
+
assert.equal(result.get('select'), null);
|
|
64
|
+
});
|
|
65
|
+
it('encodes filter values with special characters', () => {
|
|
66
|
+
const result = buildCollectionQueryParams({
|
|
67
|
+
filters: [{ field: 'status', operator: OperatorTypes.EQUAL, values: ['foo,bar'] }],
|
|
68
|
+
});
|
|
69
|
+
assert.equal(result.get('filter'), 'status=foo%2Cbar');
|
|
70
|
+
});
|
|
71
|
+
it('encodes filter values containing pipe characters', () => {
|
|
72
|
+
const result = buildCollectionQueryParams({
|
|
73
|
+
filters: [{ field: 'tags', operator: OperatorTypes.EQUAL, values: ['a|b'] }],
|
|
74
|
+
});
|
|
75
|
+
assert.equal(result.get('filter'), 'tags=a%7Cb');
|
|
76
|
+
});
|
|
77
|
+
it('joins multiple values for one filter with |', () => {
|
|
78
|
+
const result = buildCollectionQueryParams({
|
|
79
|
+
filters: [{ field: 'status', operator: OperatorTypes.EQUAL, values: ['active', 'pending'] }],
|
|
80
|
+
});
|
|
81
|
+
assert.equal(result.get('filter'), 'status=active|pending');
|
|
82
|
+
});
|
|
83
|
+
it('joins multiple filters with ,', () => {
|
|
84
|
+
const result = buildCollectionQueryParams({
|
|
85
|
+
filters: [
|
|
86
|
+
{ field: 'status', operator: OperatorTypes.EQUAL, values: ['active'] },
|
|
87
|
+
{ field: 'name', operator: OperatorTypes.INCLUDE, values: ['john'] },
|
|
88
|
+
],
|
|
89
|
+
});
|
|
90
|
+
assert.equal(result.get('filter'), 'status=active,name*=john');
|
|
91
|
+
});
|
|
92
|
+
it('handles IS_NULL operator (no values)', () => {
|
|
93
|
+
const result = buildCollectionQueryParams({
|
|
94
|
+
filters: [{ field: 'status', operator: OperatorTypes.IS_NULL, values: [] }],
|
|
95
|
+
});
|
|
96
|
+
assert.equal(result.get('filter'), 'status!!');
|
|
97
|
+
});
|
|
98
|
+
it('handles IS_NULL operator with undefined values', () => {
|
|
99
|
+
const result = buildCollectionQueryParams({
|
|
100
|
+
filters: [{ field: 'status', operator: OperatorTypes.IS_NULL, values: undefined }],
|
|
101
|
+
});
|
|
102
|
+
assert.equal(result.get('filter'), 'status!!');
|
|
103
|
+
});
|
|
104
|
+
it('handles IS_NOT_NULL operator (no values)', () => {
|
|
105
|
+
const result = buildCollectionQueryParams({
|
|
106
|
+
filters: [{ field: 'assignee', operator: OperatorTypes.IS_NOT_NULL, values: [] }],
|
|
107
|
+
});
|
|
108
|
+
assert.equal(result.get('filter'), 'assignee!');
|
|
109
|
+
});
|
|
110
|
+
it('encodes selects', () => {
|
|
111
|
+
const result = buildCollectionQueryParams({
|
|
112
|
+
selects: ['name', 'dept.id'],
|
|
113
|
+
});
|
|
114
|
+
assert.equal(result.get('select'), 'name,dept.id');
|
|
115
|
+
assert.equal(result.get('filter'), null);
|
|
116
|
+
});
|
|
117
|
+
it('encodes selects with special characters', () => {
|
|
118
|
+
const result = buildCollectionQueryParams({
|
|
119
|
+
selects: ['field with spaces'],
|
|
120
|
+
});
|
|
121
|
+
assert.equal(result.get('select'), 'field%20with%20spaces');
|
|
122
|
+
});
|
|
123
|
+
it('encodes both filters and selects', () => {
|
|
124
|
+
const result = buildCollectionQueryParams({
|
|
125
|
+
filters: [{ field: 'status', operator: OperatorTypes.EQUAL, values: ['active'] }],
|
|
126
|
+
selects: ['name', 'status'],
|
|
127
|
+
});
|
|
128
|
+
assert.equal(result.get('filter'), 'status=active');
|
|
129
|
+
assert.equal(result.get('select'), 'name,status');
|
|
130
|
+
});
|
|
131
|
+
it('returns empty URLSearchParams when called with no arguments', () => {
|
|
132
|
+
const result = buildCollectionQueryParams();
|
|
133
|
+
assert.equal(result.toString(), '');
|
|
134
|
+
});
|
|
135
|
+
it('omits filter key for empty filters array', () => {
|
|
136
|
+
const result = buildCollectionQueryParams({ filters: [] });
|
|
137
|
+
assert.equal(result.get('filter'), null);
|
|
138
|
+
});
|
|
139
|
+
it('omits select key for empty selects array', () => {
|
|
140
|
+
const result = buildCollectionQueryParams({ selects: [] });
|
|
141
|
+
assert.equal(result.get('select'), null);
|
|
142
|
+
});
|
|
143
|
+
it('can be extended with additional params (e.g. cursor)', () => {
|
|
144
|
+
const result = buildCollectionQueryParams({
|
|
145
|
+
selects: ['name'],
|
|
146
|
+
});
|
|
147
|
+
result.append('cursor', 'abc123');
|
|
148
|
+
assert.equal(result.toString(), 'select=name&cursor=abc123');
|
|
149
|
+
});
|
|
150
|
+
});
|
|
57
151
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unito/integration-sdk",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "Integration SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "dist/src/index.d.ts",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"@types/express": "5.x",
|
|
56
|
-
"@unito/integration-api": "
|
|
56
|
+
"@unito/integration-api": "5.x",
|
|
57
57
|
"busboy": "^1.6.0",
|
|
58
58
|
"cachette": "4.x",
|
|
59
59
|
"express": "^5.1",
|
package/src/helpers.ts
CHANGED
|
@@ -35,3 +35,30 @@ export function getApplicableFilters(context: { filters: Filter[] }, fields: Fie
|
|
|
35
35
|
|
|
36
36
|
return applicableFilters;
|
|
37
37
|
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Use this helper function to build the query params for a collection next page URL.
|
|
41
|
+
* It re-encodes the parsed filters and selects back into query-param form,
|
|
42
|
+
* so integrations can easily construct `nextPage` URLs.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* const params = buildCollectionQueryParams({ filters: context.filters, selects: context.selects });
|
|
46
|
+
* params.append('cursor', nextCursor);
|
|
47
|
+
* return { info: { nextPage: `/items?${params.toString()}` }, data: [...] };
|
|
48
|
+
*/
|
|
49
|
+
export function buildCollectionQueryParams(params?: { filters?: Filter[]; selects?: string[] }): URLSearchParams {
|
|
50
|
+
const result = new URLSearchParams();
|
|
51
|
+
|
|
52
|
+
if (params?.filters?.length) {
|
|
53
|
+
result.set(
|
|
54
|
+
'filter',
|
|
55
|
+
params.filters.map(f => `${f.field}${f.operator}${(f.values ?? []).map(encodeURIComponent).join('|')}`).join(','),
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (params?.selects?.length) {
|
|
60
|
+
result.set('select', params.selects.map(encodeURIComponent).join(','));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return result;
|
|
64
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -14,7 +14,7 @@ export type { Secrets } from './middlewares/secrets.js';
|
|
|
14
14
|
export type { Credentials } from './middlewares/credentials.js';
|
|
15
15
|
export type { Filter } from './middlewares/filters.js';
|
|
16
16
|
export * as HttpErrors from './httpErrors.js';
|
|
17
|
-
export { getApplicableFilters } from './helpers.js';
|
|
17
|
+
export { buildCollectionQueryParams, getApplicableFilters } from './helpers.js';
|
|
18
18
|
export * from './resources/context.js';
|
|
19
19
|
export { type default as Logger, NULL_LOGGER } from './resources/logger.js';
|
|
20
20
|
/* c8 ignore stop */
|
package/test/helpers.test.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { FieldValueTypes, OperatorTypes, Semantics } from '@unito/integration-ap
|
|
|
2
2
|
|
|
3
3
|
import assert from 'node:assert/strict';
|
|
4
4
|
import { describe, it } from 'node:test';
|
|
5
|
-
import { getApplicableFilters } from '../src/helpers.js';
|
|
5
|
+
import { buildCollectionQueryParams, getApplicableFilters } from '../src/helpers.js';
|
|
6
6
|
|
|
7
7
|
describe('Helpers', () => {
|
|
8
8
|
describe('getApplicableFilters', () => {
|
|
@@ -72,4 +72,113 @@ describe('Helpers', () => {
|
|
|
72
72
|
assert.deepEqual(actual, []);
|
|
73
73
|
});
|
|
74
74
|
});
|
|
75
|
+
|
|
76
|
+
describe('buildCollectionQueryParams', () => {
|
|
77
|
+
it('encodes a single filter with one value', () => {
|
|
78
|
+
const result = buildCollectionQueryParams({
|
|
79
|
+
filters: [{ field: 'status', operator: OperatorTypes.EQUAL, values: ['active'] }],
|
|
80
|
+
});
|
|
81
|
+
assert.equal(result.get('filter'), 'status=active');
|
|
82
|
+
assert.equal(result.get('select'), null);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('encodes filter values with special characters', () => {
|
|
86
|
+
const result = buildCollectionQueryParams({
|
|
87
|
+
filters: [{ field: 'status', operator: OperatorTypes.EQUAL, values: ['foo,bar'] }],
|
|
88
|
+
});
|
|
89
|
+
assert.equal(result.get('filter'), 'status=foo%2Cbar');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('encodes filter values containing pipe characters', () => {
|
|
93
|
+
const result = buildCollectionQueryParams({
|
|
94
|
+
filters: [{ field: 'tags', operator: OperatorTypes.EQUAL, values: ['a|b'] }],
|
|
95
|
+
});
|
|
96
|
+
assert.equal(result.get('filter'), 'tags=a%7Cb');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('joins multiple values for one filter with |', () => {
|
|
100
|
+
const result = buildCollectionQueryParams({
|
|
101
|
+
filters: [{ field: 'status', operator: OperatorTypes.EQUAL, values: ['active', 'pending'] }],
|
|
102
|
+
});
|
|
103
|
+
assert.equal(result.get('filter'), 'status=active|pending');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('joins multiple filters with ,', () => {
|
|
107
|
+
const result = buildCollectionQueryParams({
|
|
108
|
+
filters: [
|
|
109
|
+
{ field: 'status', operator: OperatorTypes.EQUAL, values: ['active'] },
|
|
110
|
+
{ field: 'name', operator: OperatorTypes.INCLUDE, values: ['john'] },
|
|
111
|
+
],
|
|
112
|
+
});
|
|
113
|
+
assert.equal(result.get('filter'), 'status=active,name*=john');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('handles IS_NULL operator (no values)', () => {
|
|
117
|
+
const result = buildCollectionQueryParams({
|
|
118
|
+
filters: [{ field: 'status', operator: OperatorTypes.IS_NULL, values: [] }],
|
|
119
|
+
});
|
|
120
|
+
assert.equal(result.get('filter'), 'status!!');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('handles IS_NULL operator with undefined values', () => {
|
|
124
|
+
const result = buildCollectionQueryParams({
|
|
125
|
+
filters: [{ field: 'status', operator: OperatorTypes.IS_NULL, values: undefined }],
|
|
126
|
+
});
|
|
127
|
+
assert.equal(result.get('filter'), 'status!!');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('handles IS_NOT_NULL operator (no values)', () => {
|
|
131
|
+
const result = buildCollectionQueryParams({
|
|
132
|
+
filters: [{ field: 'assignee', operator: OperatorTypes.IS_NOT_NULL, values: [] }],
|
|
133
|
+
});
|
|
134
|
+
assert.equal(result.get('filter'), 'assignee!');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('encodes selects', () => {
|
|
138
|
+
const result = buildCollectionQueryParams({
|
|
139
|
+
selects: ['name', 'dept.id'],
|
|
140
|
+
});
|
|
141
|
+
assert.equal(result.get('select'), 'name,dept.id');
|
|
142
|
+
assert.equal(result.get('filter'), null);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('encodes selects with special characters', () => {
|
|
146
|
+
const result = buildCollectionQueryParams({
|
|
147
|
+
selects: ['field with spaces'],
|
|
148
|
+
});
|
|
149
|
+
assert.equal(result.get('select'), 'field%20with%20spaces');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('encodes both filters and selects', () => {
|
|
153
|
+
const result = buildCollectionQueryParams({
|
|
154
|
+
filters: [{ field: 'status', operator: OperatorTypes.EQUAL, values: ['active'] }],
|
|
155
|
+
selects: ['name', 'status'],
|
|
156
|
+
});
|
|
157
|
+
assert.equal(result.get('filter'), 'status=active');
|
|
158
|
+
assert.equal(result.get('select'), 'name,status');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('returns empty URLSearchParams when called with no arguments', () => {
|
|
162
|
+
const result = buildCollectionQueryParams();
|
|
163
|
+
assert.equal(result.toString(), '');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('omits filter key for empty filters array', () => {
|
|
167
|
+
const result = buildCollectionQueryParams({ filters: [] });
|
|
168
|
+
assert.equal(result.get('filter'), null);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('omits select key for empty selects array', () => {
|
|
172
|
+
const result = buildCollectionQueryParams({ selects: [] });
|
|
173
|
+
assert.equal(result.get('select'), null);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('can be extended with additional params (e.g. cursor)', () => {
|
|
177
|
+
const result = buildCollectionQueryParams({
|
|
178
|
+
selects: ['name'],
|
|
179
|
+
});
|
|
180
|
+
result.append('cursor', 'abc123');
|
|
181
|
+
assert.equal(result.toString(), 'select=name&cursor=abc123');
|
|
182
|
+
});
|
|
183
|
+
});
|
|
75
184
|
});
|