@b-jones-rfd/qualtrics-api-tasks 0.0.4 → 0.1.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/CHANGELOG.md +12 -0
- package/README.md +287 -1
- package/dist/index.d.mts +102 -2
- package/dist/index.d.ts +102 -2
- package/dist/index.js +279 -4
- package/dist/index.mjs +271 -4
- package/package.json +3 -1
- package/tests/utils.test.ts +160 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @b-jones-rfd/qualtrics-api-tasks
|
|
2
2
|
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- efa27c7: Implemented survey file response methods
|
|
8
|
+
|
|
9
|
+
## 0.0.5
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- df0e350: Implemented testConnection and getBearerToken methods
|
|
14
|
+
|
|
3
15
|
## 0.0.4
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -1,2 +1,288 @@
|
|
|
1
|
+
[](https://github.com/prettier/prettier)
|
|
2
|
+
|
|
1
3
|
# qualtrics-api-tasks
|
|
2
|
-
|
|
4
|
+
|
|
5
|
+
Helpers to perform common tasks using the [Qualtrics API](https://api.qualtrics.com/). This is an exercise to avoid code reuse in my own projects. Use at your own risk.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
This project requires NodeJS (version >= 18) and NPM.
|
|
10
|
+
[Node](http://nodejs.org/) and [NPM](https://npmjs.org/) are really easy to install.
|
|
11
|
+
To make sure you have them available on your machine,
|
|
12
|
+
try running the following command.
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
$ npm -v && node -v
|
|
16
|
+
10.2.4
|
|
17
|
+
v20.11.1
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
[PNPM] (https://pnpm.io/) is a awesome alternative to NPM and is recommended.
|
|
21
|
+
|
|
22
|
+
## Table of contents
|
|
23
|
+
|
|
24
|
+
- [Qualtrics API Tasks](#qualtics-api-tasks)
|
|
25
|
+
- [Prerequisites](#prerequisites)
|
|
26
|
+
- [Table of contents](#table-of-contents)
|
|
27
|
+
- [Getting Started](#getting-started)
|
|
28
|
+
- [Installation](#installation)
|
|
29
|
+
- [Usage](#usage)
|
|
30
|
+
- [API](#api)
|
|
31
|
+
- [createConnection](#createSiteConnection)
|
|
32
|
+
- [Actions](#actions)
|
|
33
|
+
- [Responses](#responses)
|
|
34
|
+
- [Contributing](#contributing)
|
|
35
|
+
- [Versioning](#versioning)
|
|
36
|
+
- [Authors](#authors)
|
|
37
|
+
- [License](#license)
|
|
38
|
+
|
|
39
|
+
## Getting Started
|
|
40
|
+
|
|
41
|
+
These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. See deployment for notes on how to deploy the project on a live system.
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
**BEFORE YOU INSTALL:** please read the [prerequisites](#prerequisites)
|
|
46
|
+
|
|
47
|
+
To install and set up the library, run:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
$ npm i @b-jones-rfd/qualtrics-api-tasks
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Or if you prefer using Yarn:
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
$ yarn add @b-jones-rfd/qualtrics-api-tasks
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Or for PNPM:
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
$ pnpm add @b-jones-rfd/qualtrics-api-tasks
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Usage
|
|
66
|
+
|
|
67
|
+
### Instance Methods
|
|
68
|
+
|
|
69
|
+
Actions can be performed against the Qualtrics API using instance methods on a Qualtrics Connection instance.
|
|
70
|
+
|
|
71
|
+
If authenticating using [OAuth 2.0](https://api.qualtrics.com/9398592961ced-o-auth-authentication-client-credentials) use the getBearerToken action with a Client ID and secret to obtain a bearer token. If authenticating with an [API Token](https://api.qualtrics.com/2b4ffbd8af74e-api-key-authentication) pass the token as a connection option using the apiToken property.
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { createConnection } from '@b-jones-rfd/qualtrics-api-tasks'
|
|
75
|
+
|
|
76
|
+
const connectionOptions = {
|
|
77
|
+
datacenterId: 'az1',
|
|
78
|
+
apiToken: 'my API Token', // optional
|
|
79
|
+
timeout: 20000, // optional timeout in milliseconds, default is 30 seconds
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const connection: SiteConnection = createConnection(connectionOptions)
|
|
83
|
+
|
|
84
|
+
async function getBearerToken(clientId: string, clientSecret: string) {
|
|
85
|
+
const contents = await connection.getBearerToken({ clientId, clientSecret })
|
|
86
|
+
if (contents.success) return contents.data
|
|
87
|
+
else throw new Error(contents.error)
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Factory Action Methods
|
|
92
|
+
|
|
93
|
+
Additionally, for single use or reduced import size, action factory methods can be imported directly. Call the factory method with a SiteConnectionOptions object to return an asynchronous action function that can be called directly.
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import { testConnection } from '@b-jones-rfd/qualtrics-api-tasks'
|
|
97
|
+
|
|
98
|
+
async function testMyQualtricsConnection(listName: string) {
|
|
99
|
+
const action = testConnection(connectionOpts)
|
|
100
|
+
const contents = await action()
|
|
101
|
+
if (contents.success) return 'Successful connection!'
|
|
102
|
+
else throw new Error(contents.error)
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## API
|
|
107
|
+
|
|
108
|
+
### createSiteConnection
|
|
109
|
+
|
|
110
|
+
#### ConnectionOptions
|
|
111
|
+
|
|
112
|
+
`datacenterId`
|
|
113
|
+
|
|
114
|
+
| Type | Description | Example | Required |
|
|
115
|
+
| ------ | ------------------------ | ------- | -------- |
|
|
116
|
+
| string | Qualtrics Data Center Id | 'ca1' | Y |
|
|
117
|
+
|
|
118
|
+
`apiToken`
|
|
119
|
+
|
|
120
|
+
| Type | Description | Required |
|
|
121
|
+
| ------ | ------------------- | -------- |
|
|
122
|
+
| string | Qualtrics API Token | N |
|
|
123
|
+
|
|
124
|
+
`timeout`
|
|
125
|
+
|
|
126
|
+
| Type | Default value | Description | Example | Required |
|
|
127
|
+
| ------ | ------------- | ------------------------- | --------------------- | -------- |
|
|
128
|
+
| number | 30000 | Qualtrics request timeout | sharepoint.domain.com | N |
|
|
129
|
+
|
|
130
|
+
### Actions
|
|
131
|
+
|
|
132
|
+
Connection instance action methods.
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
export type Action<TConfig, TResponse> = (
|
|
136
|
+
options: TConfig
|
|
137
|
+
) => Promise<Result<TResponse>>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
If using the actions directly call the factory method with a ConnectionOptions object to return an action that can be used to execute a Qualtrics action.
|
|
141
|
+
|
|
142
|
+
#### exportResponses(options)
|
|
143
|
+
|
|
144
|
+
Implements [Survey Response File Export](https://api.qualtrics.com/u9e5lh4172v0v-survey-response-export-guide) multistep process.
|
|
145
|
+
|
|
146
|
+
`options`
|
|
147
|
+
|
|
148
|
+
| Property | Type | Description | Required | Default |
|
|
149
|
+
| ------------------------------- | -------- | ------------------------------------------- | -------- | ------- |
|
|
150
|
+
| surveyId | string | Quatrics Survey ID | Y | |
|
|
151
|
+
| startDate | Date | Export start date and time | Y | |
|
|
152
|
+
| endDate | Date | Export end date and time | Y | |
|
|
153
|
+
| format | Enum | File format | N | 'csv' |
|
|
154
|
+
| breakoutSets | boolean | Split multi-value fields into columns | N | true |
|
|
155
|
+
| compress | boolean | Compress final export | N | true |
|
|
156
|
+
| exportResponsesInProgress | boolean | Only export not complete | N | false |
|
|
157
|
+
| filterId | string | Return responses matching id | N | |
|
|
158
|
+
| formatDecimalAsComma | boolean | Use comma as decimal separator | N | false |
|
|
159
|
+
| includeDisplayOrder | boolean | Include display order in export | N | false |
|
|
160
|
+
| limit | number | Max responses to export | N | |
|
|
161
|
+
| multiselectSeenUnansweredRecode | number | Recode seen, but unanswered for multiselect | N | |
|
|
162
|
+
| newlineReplacement | string | Replace newline character with this | N | |
|
|
163
|
+
| seenUnansweredRecode | number | Recode seen, but unanswered with this | N | |
|
|
164
|
+
| timeZone | string | Timezone used to determine response date | N | 'UTC" |
|
|
165
|
+
| useLabels | boolean | Export text of answer choice | N | false |
|
|
166
|
+
| embeddedDataIds | string[] | Only export these embedded data fields | N | |
|
|
167
|
+
| questionIds | string[] | Only export these question IDs | N | |
|
|
168
|
+
| surveyMetadataIds | string[] | Only export these metadata fields | N | |
|
|
169
|
+
| continuationToken | string | Previous export continuation token | N | |
|
|
170
|
+
| allowContinuation | boolean | Request continuation token | N | false |
|
|
171
|
+
| includeLabelColumns | boolean | Export two columns, recode and labels | N | false |
|
|
172
|
+
| sortByLastModifiedDate | boolean | Sort responses by modified date | N | false |
|
|
173
|
+
| bearerToken | string | Valid Bearer Token | N | |
|
|
174
|
+
|
|
175
|
+
#### getBearerToken(options)
|
|
176
|
+
|
|
177
|
+
Implements [OAuth Authentication (Client Credentials)](https://api.qualtrics.com/9398592961ced-o-auth-authentication-client-credentials)
|
|
178
|
+
|
|
179
|
+
`options`
|
|
180
|
+
|
|
181
|
+
| Property | Type | Description | Required |
|
|
182
|
+
| ------------ | ------ | --------------------------------- | -------- |
|
|
183
|
+
| clientId | string | Quatrics Client ID | Y |
|
|
184
|
+
| clientSecret | string | Qualtrics Client Password | Y |
|
|
185
|
+
| scope | string | Qualtrics Client requested scopes | Y |
|
|
186
|
+
|
|
187
|
+
#### getResponseExportFile(options)
|
|
188
|
+
|
|
189
|
+
Implements [Get Response Export File](https://api.qualtrics.com/41296b6f2e828-get-response-export-file)
|
|
190
|
+
|
|
191
|
+
`options`
|
|
192
|
+
|
|
193
|
+
| Property | Type | Description | Required |
|
|
194
|
+
| ----------- | ------ | ------------------ | -------- |
|
|
195
|
+
| surveyId | string | Quatrics Survey ID | Y |
|
|
196
|
+
| fileId | string | File ID | Y |
|
|
197
|
+
| bearerToken | string | Valid Bearer Token | N |
|
|
198
|
+
|
|
199
|
+
#### getResponseExportProgress(options)
|
|
200
|
+
|
|
201
|
+
Implements [Get Response Export Progress](https://api.qualtrics.com/37e6a66f74ab4-get-response-export-progress)
|
|
202
|
+
|
|
203
|
+
`options`
|
|
204
|
+
|
|
205
|
+
| Property | Type | Description | Required |
|
|
206
|
+
| ---------------- | ------ | ------------------ | -------- |
|
|
207
|
+
| surveyId | string | Quatrics Survey ID | Y |
|
|
208
|
+
| exportProgressId | string | Progress ID | Y |
|
|
209
|
+
| bearerToken | string | Valid Bearer Token | N |
|
|
210
|
+
|
|
211
|
+
#### startResponseExports(options)
|
|
212
|
+
|
|
213
|
+
Implements [Start Response Exports](https://api.qualtrics.com/6b00592b9c013-start-response-export)
|
|
214
|
+
|
|
215
|
+
`options`
|
|
216
|
+
|
|
217
|
+
| Property | Type | Description | Required | Default |
|
|
218
|
+
| ------------------------------- | -------- | ------------------------------------------- | -------- | ------- |
|
|
219
|
+
| surveyId | string | Quatrics Survey ID | Y | |
|
|
220
|
+
| startDate | Date | Export start date and time | Y | |
|
|
221
|
+
| endDate | Date | Export end date and time | Y | |
|
|
222
|
+
| format | Enum | File format | N | 'csv' |
|
|
223
|
+
| breakoutSets | boolean | Split multi-value fields into columns | N | true |
|
|
224
|
+
| compress | boolean | Compress final export | N | true |
|
|
225
|
+
| exportResponsesInProgress | boolean | Only export not complete | N | false |
|
|
226
|
+
| filterId | string | Return responses matching id | N | |
|
|
227
|
+
| formatDecimalAsComma | boolean | Use comma as decimal separator | N | false |
|
|
228
|
+
| includeDisplayOrder | boolean | Include display order in export | N | false |
|
|
229
|
+
| limit | number | Max responses to export | N | |
|
|
230
|
+
| multiselectSeenUnansweredRecode | number | Recode seen, but unanswered for multiselect | N | |
|
|
231
|
+
| newlineReplacement | string | Replace newline character with this | N | |
|
|
232
|
+
| seenUnansweredRecode | number | Recode seen, but unanswered with this | N | |
|
|
233
|
+
| timeZone | string | Timezone used to determine response date | N | 'UTC" |
|
|
234
|
+
| useLabels | boolean | Export text of answer choice | N | false |
|
|
235
|
+
| embeddedDataIds | string[] | Only export these embedded data fields | N | |
|
|
236
|
+
| questionIds | string[] | Only export these question IDs | N | |
|
|
237
|
+
| surveyMetadataIds | string[] | Only export these metadata fields | N | |
|
|
238
|
+
| continuationToken | string | Previous export continuation token | N | |
|
|
239
|
+
| allowContinuation | boolean | Request continuation token | N | false |
|
|
240
|
+
| includeLabelColumns | boolean | Export two columns, recode and labels | N | false |
|
|
241
|
+
| sortByLastModifiedDate | boolean | Sort responses by modified date | N | false |
|
|
242
|
+
| bearerToken | string | Valid Bearer Token | N | |
|
|
243
|
+
|
|
244
|
+
#### testConnection(bearerToken)
|
|
245
|
+
|
|
246
|
+
| Parameter | Type | Description | Required |
|
|
247
|
+
| ----------- | ------ | ------------------ | -------- |
|
|
248
|
+
| bearerToken | string | Valid Bearer Token | N |
|
|
249
|
+
|
|
250
|
+
## Responses
|
|
251
|
+
|
|
252
|
+
Responses are provided based on the Result type. Success can be determined by checking the success property.
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
export type Result<TResponse> = Success<TResponse> | Failure
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Success
|
|
259
|
+
|
|
260
|
+
Response is returned in the data property.
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
type Success<TResponse> = { success: true; data: TResponse }
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Failure
|
|
267
|
+
|
|
268
|
+
Errors are returned in the error property.
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
type Failure = { success: false; error: string }
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Contributing
|
|
275
|
+
|
|
276
|
+
This is a pet project to save me time at work. It is still under development and you should use at your own risk.
|
|
277
|
+
|
|
278
|
+
## Versioning
|
|
279
|
+
|
|
280
|
+
We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/B-Jones-RFD/qualtrics-api-tasks/tags).
|
|
281
|
+
|
|
282
|
+
## Authors
|
|
283
|
+
|
|
284
|
+
- **B Jones RFD** - _Package Noob_ - [B-Jones-RFD](https://github.com/B-Jones-RFD)
|
|
285
|
+
|
|
286
|
+
## License
|
|
287
|
+
|
|
288
|
+
[MIT License](https://github.com/B-Jones-RFD/qualtrics-api-tasks/blob/main/LICENSE)
|
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,103 @@
|
|
|
1
|
-
|
|
1
|
+
type Success<T> = {
|
|
2
|
+
success: true;
|
|
3
|
+
data: T;
|
|
4
|
+
};
|
|
5
|
+
type Failure = {
|
|
6
|
+
success: false;
|
|
7
|
+
error: string;
|
|
8
|
+
};
|
|
9
|
+
type Result<T> = Success<T> | Failure;
|
|
10
|
+
type ResponseFormat = 'csv' | 'json' | 'ndjson' | 'spss' | 'tsv' | 'xml';
|
|
11
|
+
type Action<TConfig, TResponse> = (options: TConfig) => Promise<Result<TResponse>>;
|
|
12
|
+
type ConnectionOptions = {
|
|
13
|
+
datacenterId: string;
|
|
14
|
+
apiToken?: string;
|
|
15
|
+
timeout?: number;
|
|
16
|
+
};
|
|
17
|
+
type ActionFactory<TParams, TResponse> = (options: ConnectionOptions) => Action<TParams, TResponse>;
|
|
18
|
+
type Connection = {
|
|
19
|
+
exportResponses: Action<ExportResponsesOptions, Buffer>;
|
|
20
|
+
getBearerToken: Action<{
|
|
21
|
+
clientId: string;
|
|
22
|
+
clientSecret: string;
|
|
23
|
+
scope: string;
|
|
24
|
+
}, string>;
|
|
25
|
+
getResponseExportProgress: Action<{
|
|
26
|
+
exportProgressId: string;
|
|
27
|
+
surveyId: string;
|
|
28
|
+
bearerToken?: string;
|
|
29
|
+
}, FileProgressResponse>;
|
|
30
|
+
startResponseExport: Action<ExportResponsesOptions, {
|
|
31
|
+
progressId: string;
|
|
32
|
+
percentComplete: number;
|
|
33
|
+
status: string;
|
|
34
|
+
continuationToken?: string;
|
|
35
|
+
}>;
|
|
36
|
+
testConnection: (bearerToken?: string) => Promise<Result<void>>;
|
|
37
|
+
};
|
|
38
|
+
type ConnectionFactory = (options: ConnectionOptions) => Connection;
|
|
39
|
+
type ExportResponsesOptions = {
|
|
40
|
+
surveyId: string;
|
|
41
|
+
startDate: Date;
|
|
42
|
+
endDate: Date;
|
|
43
|
+
format?: ResponseFormat;
|
|
44
|
+
breakoutSets?: boolean;
|
|
45
|
+
compress?: boolean;
|
|
46
|
+
exportResponsesInProgress?: boolean;
|
|
47
|
+
filterId?: string;
|
|
48
|
+
formatDecimalAsComma?: boolean;
|
|
49
|
+
includeDisplayOrder?: boolean;
|
|
50
|
+
limit?: number;
|
|
51
|
+
multiselectSeenUnansweredRecode?: number;
|
|
52
|
+
newlineReplacement?: string;
|
|
53
|
+
seenUnansweredRecode?: number;
|
|
54
|
+
timeZone?: string;
|
|
55
|
+
useLabels?: boolean;
|
|
56
|
+
embeddedDataIds?: string[];
|
|
57
|
+
questionIds?: string[];
|
|
58
|
+
surveyMetadataIds?: string[];
|
|
59
|
+
continuationToken?: string;
|
|
60
|
+
allowContinuation?: boolean;
|
|
61
|
+
includeLabelColumns?: boolean;
|
|
62
|
+
sortByLastModifiedDate?: boolean;
|
|
63
|
+
bearerToken?: string;
|
|
64
|
+
};
|
|
65
|
+
type FileProgressResponse = {
|
|
66
|
+
fileId: string;
|
|
67
|
+
percentComplete: number;
|
|
68
|
+
status: string;
|
|
69
|
+
continuationToken?: string;
|
|
70
|
+
};
|
|
2
71
|
|
|
3
|
-
|
|
72
|
+
declare const createConnection: ConnectionFactory;
|
|
73
|
+
|
|
74
|
+
declare const exportResponses: ActionFactory<ExportResponsesOptions, Buffer>;
|
|
75
|
+
|
|
76
|
+
declare const getBearerToken: ActionFactory<{
|
|
77
|
+
clientId: string;
|
|
78
|
+
clientSecret: string;
|
|
79
|
+
scope: string;
|
|
80
|
+
}, string>;
|
|
81
|
+
|
|
82
|
+
declare const getResponseExportProgress: ActionFactory<{
|
|
83
|
+
exportProgressId: string;
|
|
84
|
+
surveyId: string;
|
|
85
|
+
bearerToken?: string;
|
|
86
|
+
}, FileProgressResponse>;
|
|
87
|
+
|
|
88
|
+
declare const getResponseExportFile: ActionFactory<{
|
|
89
|
+
surveyId: string;
|
|
90
|
+
fileId: string;
|
|
91
|
+
bearerToken?: string;
|
|
92
|
+
}, Buffer>;
|
|
93
|
+
|
|
94
|
+
declare const startResponseExport: ActionFactory<ExportResponsesOptions, {
|
|
95
|
+
progressId: string;
|
|
96
|
+
percentComplete: number;
|
|
97
|
+
status: string;
|
|
98
|
+
continuationToken?: string;
|
|
99
|
+
}>;
|
|
100
|
+
|
|
101
|
+
declare const testConnection: (connectionOptions: ConnectionOptions) => (bearerToken?: string) => Promise<Result<void>>;
|
|
102
|
+
|
|
103
|
+
export { type Action, type ActionFactory, type Connection, type ConnectionFactory, type ConnectionOptions, type ExportResponsesOptions, type Failure, type FileProgressResponse, type ResponseFormat, type Result, type Success, createConnection, exportResponses, getBearerToken, getResponseExportFile, getResponseExportProgress, startResponseExport, testConnection };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,103 @@
|
|
|
1
|
-
|
|
1
|
+
type Success<T> = {
|
|
2
|
+
success: true;
|
|
3
|
+
data: T;
|
|
4
|
+
};
|
|
5
|
+
type Failure = {
|
|
6
|
+
success: false;
|
|
7
|
+
error: string;
|
|
8
|
+
};
|
|
9
|
+
type Result<T> = Success<T> | Failure;
|
|
10
|
+
type ResponseFormat = 'csv' | 'json' | 'ndjson' | 'spss' | 'tsv' | 'xml';
|
|
11
|
+
type Action<TConfig, TResponse> = (options: TConfig) => Promise<Result<TResponse>>;
|
|
12
|
+
type ConnectionOptions = {
|
|
13
|
+
datacenterId: string;
|
|
14
|
+
apiToken?: string;
|
|
15
|
+
timeout?: number;
|
|
16
|
+
};
|
|
17
|
+
type ActionFactory<TParams, TResponse> = (options: ConnectionOptions) => Action<TParams, TResponse>;
|
|
18
|
+
type Connection = {
|
|
19
|
+
exportResponses: Action<ExportResponsesOptions, Buffer>;
|
|
20
|
+
getBearerToken: Action<{
|
|
21
|
+
clientId: string;
|
|
22
|
+
clientSecret: string;
|
|
23
|
+
scope: string;
|
|
24
|
+
}, string>;
|
|
25
|
+
getResponseExportProgress: Action<{
|
|
26
|
+
exportProgressId: string;
|
|
27
|
+
surveyId: string;
|
|
28
|
+
bearerToken?: string;
|
|
29
|
+
}, FileProgressResponse>;
|
|
30
|
+
startResponseExport: Action<ExportResponsesOptions, {
|
|
31
|
+
progressId: string;
|
|
32
|
+
percentComplete: number;
|
|
33
|
+
status: string;
|
|
34
|
+
continuationToken?: string;
|
|
35
|
+
}>;
|
|
36
|
+
testConnection: (bearerToken?: string) => Promise<Result<void>>;
|
|
37
|
+
};
|
|
38
|
+
type ConnectionFactory = (options: ConnectionOptions) => Connection;
|
|
39
|
+
type ExportResponsesOptions = {
|
|
40
|
+
surveyId: string;
|
|
41
|
+
startDate: Date;
|
|
42
|
+
endDate: Date;
|
|
43
|
+
format?: ResponseFormat;
|
|
44
|
+
breakoutSets?: boolean;
|
|
45
|
+
compress?: boolean;
|
|
46
|
+
exportResponsesInProgress?: boolean;
|
|
47
|
+
filterId?: string;
|
|
48
|
+
formatDecimalAsComma?: boolean;
|
|
49
|
+
includeDisplayOrder?: boolean;
|
|
50
|
+
limit?: number;
|
|
51
|
+
multiselectSeenUnansweredRecode?: number;
|
|
52
|
+
newlineReplacement?: string;
|
|
53
|
+
seenUnansweredRecode?: number;
|
|
54
|
+
timeZone?: string;
|
|
55
|
+
useLabels?: boolean;
|
|
56
|
+
embeddedDataIds?: string[];
|
|
57
|
+
questionIds?: string[];
|
|
58
|
+
surveyMetadataIds?: string[];
|
|
59
|
+
continuationToken?: string;
|
|
60
|
+
allowContinuation?: boolean;
|
|
61
|
+
includeLabelColumns?: boolean;
|
|
62
|
+
sortByLastModifiedDate?: boolean;
|
|
63
|
+
bearerToken?: string;
|
|
64
|
+
};
|
|
65
|
+
type FileProgressResponse = {
|
|
66
|
+
fileId: string;
|
|
67
|
+
percentComplete: number;
|
|
68
|
+
status: string;
|
|
69
|
+
continuationToken?: string;
|
|
70
|
+
};
|
|
2
71
|
|
|
3
|
-
|
|
72
|
+
declare const createConnection: ConnectionFactory;
|
|
73
|
+
|
|
74
|
+
declare const exportResponses: ActionFactory<ExportResponsesOptions, Buffer>;
|
|
75
|
+
|
|
76
|
+
declare const getBearerToken: ActionFactory<{
|
|
77
|
+
clientId: string;
|
|
78
|
+
clientSecret: string;
|
|
79
|
+
scope: string;
|
|
80
|
+
}, string>;
|
|
81
|
+
|
|
82
|
+
declare const getResponseExportProgress: ActionFactory<{
|
|
83
|
+
exportProgressId: string;
|
|
84
|
+
surveyId: string;
|
|
85
|
+
bearerToken?: string;
|
|
86
|
+
}, FileProgressResponse>;
|
|
87
|
+
|
|
88
|
+
declare const getResponseExportFile: ActionFactory<{
|
|
89
|
+
surveyId: string;
|
|
90
|
+
fileId: string;
|
|
91
|
+
bearerToken?: string;
|
|
92
|
+
}, Buffer>;
|
|
93
|
+
|
|
94
|
+
declare const startResponseExport: ActionFactory<ExportResponsesOptions, {
|
|
95
|
+
progressId: string;
|
|
96
|
+
percentComplete: number;
|
|
97
|
+
status: string;
|
|
98
|
+
continuationToken?: string;
|
|
99
|
+
}>;
|
|
100
|
+
|
|
101
|
+
declare const testConnection: (connectionOptions: ConnectionOptions) => (bearerToken?: string) => Promise<Result<void>>;
|
|
102
|
+
|
|
103
|
+
export { type Action, type ActionFactory, type Connection, type ConnectionFactory, type ConnectionOptions, type ExportResponsesOptions, type Failure, type FileProgressResponse, type ResponseFormat, type Result, type Success, createConnection, exportResponses, getBearerToken, getResponseExportFile, getResponseExportProgress, startResponseExport, testConnection };
|
package/dist/index.js
CHANGED
|
@@ -20,13 +20,288 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var src_exports = {};
|
|
22
22
|
__export(src_exports, {
|
|
23
|
-
|
|
23
|
+
createConnection: () => createConnection,
|
|
24
|
+
exportResponses: () => exportResponses,
|
|
25
|
+
getBearerToken: () => getBearerToken,
|
|
26
|
+
getResponseExportFile: () => getResponseExportFile,
|
|
27
|
+
getResponseExportProgress: () => getResponseExportProgress,
|
|
28
|
+
startResponseExport: () => startResponseExport,
|
|
29
|
+
testConnection: () => testConnection
|
|
24
30
|
});
|
|
25
31
|
module.exports = __toCommonJS(src_exports);
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
|
|
33
|
+
// src/utils/index.ts
|
|
34
|
+
function success(data) {
|
|
35
|
+
return {
|
|
36
|
+
success: true,
|
|
37
|
+
data
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function failure(error) {
|
|
41
|
+
return {
|
|
42
|
+
success: false,
|
|
43
|
+
error
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function getErrorMessage(error) {
|
|
47
|
+
return error ? typeof error === "object" && "message" in error && typeof error.message === "string" ? error.message : error.toString() : "An error occured";
|
|
48
|
+
}
|
|
49
|
+
function getAuthHeaders(apiToken, bearerToken) {
|
|
50
|
+
if (apiToken)
|
|
51
|
+
return new Headers({
|
|
52
|
+
"X-API-TOKEN": apiToken
|
|
53
|
+
});
|
|
54
|
+
if (bearerToken) {
|
|
55
|
+
return new Headers({
|
|
56
|
+
Authorization: `Bearer ${bearerToken}`
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
throw new Error(
|
|
60
|
+
"Unable to authorize request. Bearer Token or Api Token required."
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/utils/poll.ts
|
|
65
|
+
async function poll({
|
|
66
|
+
fn,
|
|
67
|
+
validate,
|
|
68
|
+
interval,
|
|
69
|
+
maxAttempts
|
|
70
|
+
}) {
|
|
71
|
+
let attempts = 0;
|
|
72
|
+
const executePoll = async (resolve, reject) => {
|
|
73
|
+
try {
|
|
74
|
+
const result = await fn();
|
|
75
|
+
attempts++;
|
|
76
|
+
if (validate(result)) {
|
|
77
|
+
return resolve(result);
|
|
78
|
+
} else if (maxAttempts && attempts === maxAttempts) {
|
|
79
|
+
return reject(new Error("Exceeded max attempts"));
|
|
80
|
+
} else {
|
|
81
|
+
setTimeout(executePoll, interval, resolve, reject);
|
|
82
|
+
}
|
|
83
|
+
} catch (error) {
|
|
84
|
+
reject(new Error(`Poll function execution failed: ${error}`));
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
return new Promise(executePoll);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/methods/exportResponses.ts
|
|
91
|
+
var exportResponses = (connectionOptions) => async (responseOptions) => {
|
|
92
|
+
try {
|
|
93
|
+
const startResponseAction = startResponseExport(connectionOptions);
|
|
94
|
+
const startResponse = await startResponseAction(responseOptions);
|
|
95
|
+
if (!startResponse.success)
|
|
96
|
+
return failure(startResponse.error);
|
|
97
|
+
const exportProgressAction = getResponseExportProgress(connectionOptions);
|
|
98
|
+
const progressResponse = await poll({
|
|
99
|
+
fn: async () => await exportProgressAction({
|
|
100
|
+
exportProgressId: startResponse.data.progressId,
|
|
101
|
+
surveyId: responseOptions.surveyId,
|
|
102
|
+
bearerToken: responseOptions.bearerToken
|
|
103
|
+
}),
|
|
104
|
+
validate: (res) => res.success && res.data.percentComplete === 100,
|
|
105
|
+
interval: 1,
|
|
106
|
+
maxAttempts: 60
|
|
107
|
+
});
|
|
108
|
+
if (!progressResponse.success)
|
|
109
|
+
return failure(progressResponse.error);
|
|
110
|
+
const getFileResponseAction = getResponseExportFile(connectionOptions);
|
|
111
|
+
const fileResponse = await getFileResponseAction({
|
|
112
|
+
surveyId: responseOptions.surveyId,
|
|
113
|
+
fileId: progressResponse.data.fileId,
|
|
114
|
+
bearerToken: responseOptions.bearerToken
|
|
115
|
+
});
|
|
116
|
+
return fileResponse;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
const message = getErrorMessage(error);
|
|
119
|
+
return failure(message);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// src/utils/parsers.ts
|
|
124
|
+
function safeParseBearerToken(response) {
|
|
125
|
+
return response?.access_token && typeof response.access_token === "string" ? success(response.access_token) : failure("Incorrect token format");
|
|
126
|
+
}
|
|
127
|
+
function safeParseTestResponse(response) {
|
|
128
|
+
return response?.result?.userId ? success(void 0) : failure("Incorrect test response format");
|
|
129
|
+
}
|
|
130
|
+
function safeParseStartFileExportResponse(response) {
|
|
131
|
+
return "result" in response ? "progressId" in response.result && "percentComplete" in response.result && "status" in response.result ? success(response.result) : failure("Start Export Response invalid format") : "error" in response ? failure(`${response.error.errorCode}: ${response.error.errorMessage}`) : failure(`Unable to parse Start Export Response`);
|
|
132
|
+
}
|
|
133
|
+
function safeParseFileProgressResponse(response) {
|
|
134
|
+
return "result" in response ? "fileId" in response.result && "percentComplete" in response.result && "status" in response.result ? success(response.result) : failure("File Progress Response invalid format") : "error" in response ? failure(`${response.error.errorCode}: ${response.error.errorMessage}`) : failure(`Unable to parse File Progress Response`);
|
|
135
|
+
}
|
|
136
|
+
function safeParseFileResponse(response) {
|
|
137
|
+
return Buffer.isBuffer(response) ? success(response) : failure("Incorrect file response format");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/qualtrics/index.ts
|
|
141
|
+
async function execute(config) {
|
|
142
|
+
const { datacenterId, route, headers, timeout, body } = config;
|
|
143
|
+
const host = `https://${datacenterId}.qualtrics.com`;
|
|
144
|
+
const resource = new URL(route, host);
|
|
145
|
+
const controller = new AbortController();
|
|
146
|
+
const options = {
|
|
147
|
+
method: "GET",
|
|
148
|
+
headers,
|
|
149
|
+
signal: controller.signal
|
|
150
|
+
};
|
|
151
|
+
if (body) {
|
|
152
|
+
if (typeof body === "string")
|
|
153
|
+
headers.append("Content-Type", "application/json");
|
|
154
|
+
options.method = "POST";
|
|
155
|
+
options.headers = headers;
|
|
156
|
+
options.body = body;
|
|
157
|
+
}
|
|
158
|
+
const timer = setTimeout(() => controller.abort(), timeout ?? 30 * 1e3);
|
|
159
|
+
const response = await fetch(resource, options);
|
|
160
|
+
clearTimeout(timer);
|
|
161
|
+
if (response.ok) {
|
|
162
|
+
const data = await response.json();
|
|
163
|
+
return Promise.resolve(data);
|
|
164
|
+
} else {
|
|
165
|
+
const message = await response.text();
|
|
166
|
+
return Promise.reject(`${response.status}: ${message}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/methods/getBearerToken.ts
|
|
171
|
+
var getBearerToken = (connectionOptions) => async ({ clientId, clientSecret, scope }) => {
|
|
172
|
+
const route = `/oauth2/token`;
|
|
173
|
+
try {
|
|
174
|
+
const { datacenterId, timeout } = connectionOptions;
|
|
175
|
+
const headers = new Headers({
|
|
176
|
+
Authorization: "Basic " + Buffer.from(`${clientId}:${clientSecret}`).toString("base64")
|
|
177
|
+
});
|
|
178
|
+
const body = new FormData();
|
|
179
|
+
body.append("grant_type", "client_credentials");
|
|
180
|
+
body.append("scope", scope);
|
|
181
|
+
const response = await execute({
|
|
182
|
+
datacenterId,
|
|
183
|
+
route,
|
|
184
|
+
headers,
|
|
185
|
+
body,
|
|
186
|
+
timeout
|
|
187
|
+
});
|
|
188
|
+
const result = safeParseBearerToken(response);
|
|
189
|
+
return result;
|
|
190
|
+
} catch (error) {
|
|
191
|
+
const message = getErrorMessage(error);
|
|
192
|
+
return failure(message);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// src/methods/getResponseExportProgress.ts
|
|
197
|
+
var getResponseExportProgress = (connectionOptions) => async ({ exportProgressId, surveyId, bearerToken }) => {
|
|
198
|
+
const route = `/surveys/${surveyId}/export-responses/${exportProgressId}`;
|
|
199
|
+
try {
|
|
200
|
+
const headers = getAuthHeaders(connectionOptions.apiToken, bearerToken);
|
|
201
|
+
const config = {
|
|
202
|
+
datacenterId: connectionOptions.datacenterId,
|
|
203
|
+
route,
|
|
204
|
+
headers,
|
|
205
|
+
timeout: connectionOptions.timeout
|
|
206
|
+
};
|
|
207
|
+
const response = await execute(config);
|
|
208
|
+
const result = safeParseFileProgressResponse(response);
|
|
209
|
+
return result;
|
|
210
|
+
} catch (error) {
|
|
211
|
+
const message = getErrorMessage(error);
|
|
212
|
+
return failure(message);
|
|
213
|
+
}
|
|
28
214
|
};
|
|
215
|
+
|
|
216
|
+
// src/methods/getResponseExportFile.ts
|
|
217
|
+
var getResponseExportFile = (connectionOptions) => async ({ surveyId, fileId, bearerToken }) => {
|
|
218
|
+
const route = `/surveys/${surveyId}/export-responses/${fileId}/file`;
|
|
219
|
+
try {
|
|
220
|
+
const headers = getAuthHeaders(connectionOptions.apiToken, bearerToken);
|
|
221
|
+
const config = {
|
|
222
|
+
datacenterId: connectionOptions.datacenterId,
|
|
223
|
+
route,
|
|
224
|
+
headers,
|
|
225
|
+
timeout: connectionOptions.timeout
|
|
226
|
+
};
|
|
227
|
+
const response = await execute(config);
|
|
228
|
+
const result = safeParseFileResponse(response);
|
|
229
|
+
return result;
|
|
230
|
+
} catch (error) {
|
|
231
|
+
const message = getErrorMessage(error);
|
|
232
|
+
return failure(message);
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// src/methods/startResponseExport.ts
|
|
237
|
+
var startResponseExport = (connectionOptions) => async ({
|
|
238
|
+
surveyId,
|
|
239
|
+
startDate,
|
|
240
|
+
endDate,
|
|
241
|
+
format = "csv",
|
|
242
|
+
bearerToken = void 0,
|
|
243
|
+
...rest
|
|
244
|
+
}) => {
|
|
245
|
+
const route = `/surveys/${surveyId}/export-responses`;
|
|
246
|
+
try {
|
|
247
|
+
const headers = getAuthHeaders(connectionOptions.apiToken, bearerToken);
|
|
248
|
+
const body = JSON.stringify({
|
|
249
|
+
startDate: startDate.toISOString(),
|
|
250
|
+
endDate: endDate.toDateString(),
|
|
251
|
+
format,
|
|
252
|
+
...rest
|
|
253
|
+
});
|
|
254
|
+
const config = {
|
|
255
|
+
datacenterId: connectionOptions.datacenterId,
|
|
256
|
+
route,
|
|
257
|
+
headers,
|
|
258
|
+
body,
|
|
259
|
+
timeout: connectionOptions.timeout
|
|
260
|
+
};
|
|
261
|
+
const response = await execute(config);
|
|
262
|
+
const result = safeParseStartFileExportResponse(response);
|
|
263
|
+
return result;
|
|
264
|
+
} catch (error) {
|
|
265
|
+
const message = getErrorMessage(error);
|
|
266
|
+
return failure(message);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
// src/methods/testConnection.ts
|
|
271
|
+
var testConnection = (connectionOptions) => async (bearerToken) => {
|
|
272
|
+
const route = "/API/v3/whoami";
|
|
273
|
+
try {
|
|
274
|
+
const headers = getAuthHeaders(connectionOptions.apiToken, bearerToken);
|
|
275
|
+
const config = {
|
|
276
|
+
datacenterId: connectionOptions.datacenterId,
|
|
277
|
+
route,
|
|
278
|
+
headers,
|
|
279
|
+
timeout: connectionOptions.timeout
|
|
280
|
+
};
|
|
281
|
+
const response = await execute(config);
|
|
282
|
+
const result = safeParseTestResponse(response);
|
|
283
|
+
return result;
|
|
284
|
+
} catch (error) {
|
|
285
|
+
const message = getErrorMessage(error);
|
|
286
|
+
return failure(message);
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// src/createConnection.ts
|
|
291
|
+
var createConnection = (options) => ({
|
|
292
|
+
exportResponses: exportResponses(options),
|
|
293
|
+
getBearerToken: getBearerToken(options),
|
|
294
|
+
getResponseExportProgress: getResponseExportProgress(options),
|
|
295
|
+
startResponseExport: startResponseExport(options),
|
|
296
|
+
testConnection: testConnection(options)
|
|
297
|
+
});
|
|
29
298
|
// Annotate the CommonJS export names for ESM import in node:
|
|
30
299
|
0 && (module.exports = {
|
|
31
|
-
|
|
300
|
+
createConnection,
|
|
301
|
+
exportResponses,
|
|
302
|
+
getBearerToken,
|
|
303
|
+
getResponseExportFile,
|
|
304
|
+
getResponseExportProgress,
|
|
305
|
+
startResponseExport,
|
|
306
|
+
testConnection
|
|
32
307
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,274 @@
|
|
|
1
|
-
// src/index.ts
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// src/utils/index.ts
|
|
2
|
+
function success(data) {
|
|
3
|
+
return {
|
|
4
|
+
success: true,
|
|
5
|
+
data
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
function failure(error) {
|
|
9
|
+
return {
|
|
10
|
+
success: false,
|
|
11
|
+
error
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function getErrorMessage(error) {
|
|
15
|
+
return error ? typeof error === "object" && "message" in error && typeof error.message === "string" ? error.message : error.toString() : "An error occured";
|
|
16
|
+
}
|
|
17
|
+
function getAuthHeaders(apiToken, bearerToken) {
|
|
18
|
+
if (apiToken)
|
|
19
|
+
return new Headers({
|
|
20
|
+
"X-API-TOKEN": apiToken
|
|
21
|
+
});
|
|
22
|
+
if (bearerToken) {
|
|
23
|
+
return new Headers({
|
|
24
|
+
Authorization: `Bearer ${bearerToken}`
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
throw new Error(
|
|
28
|
+
"Unable to authorize request. Bearer Token or Api Token required."
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/utils/poll.ts
|
|
33
|
+
async function poll({
|
|
34
|
+
fn,
|
|
35
|
+
validate,
|
|
36
|
+
interval,
|
|
37
|
+
maxAttempts
|
|
38
|
+
}) {
|
|
39
|
+
let attempts = 0;
|
|
40
|
+
const executePoll = async (resolve, reject) => {
|
|
41
|
+
try {
|
|
42
|
+
const result = await fn();
|
|
43
|
+
attempts++;
|
|
44
|
+
if (validate(result)) {
|
|
45
|
+
return resolve(result);
|
|
46
|
+
} else if (maxAttempts && attempts === maxAttempts) {
|
|
47
|
+
return reject(new Error("Exceeded max attempts"));
|
|
48
|
+
} else {
|
|
49
|
+
setTimeout(executePoll, interval, resolve, reject);
|
|
50
|
+
}
|
|
51
|
+
} catch (error) {
|
|
52
|
+
reject(new Error(`Poll function execution failed: ${error}`));
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
return new Promise(executePoll);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/methods/exportResponses.ts
|
|
59
|
+
var exportResponses = (connectionOptions) => async (responseOptions) => {
|
|
60
|
+
try {
|
|
61
|
+
const startResponseAction = startResponseExport(connectionOptions);
|
|
62
|
+
const startResponse = await startResponseAction(responseOptions);
|
|
63
|
+
if (!startResponse.success)
|
|
64
|
+
return failure(startResponse.error);
|
|
65
|
+
const exportProgressAction = getResponseExportProgress(connectionOptions);
|
|
66
|
+
const progressResponse = await poll({
|
|
67
|
+
fn: async () => await exportProgressAction({
|
|
68
|
+
exportProgressId: startResponse.data.progressId,
|
|
69
|
+
surveyId: responseOptions.surveyId,
|
|
70
|
+
bearerToken: responseOptions.bearerToken
|
|
71
|
+
}),
|
|
72
|
+
validate: (res) => res.success && res.data.percentComplete === 100,
|
|
73
|
+
interval: 1,
|
|
74
|
+
maxAttempts: 60
|
|
75
|
+
});
|
|
76
|
+
if (!progressResponse.success)
|
|
77
|
+
return failure(progressResponse.error);
|
|
78
|
+
const getFileResponseAction = getResponseExportFile(connectionOptions);
|
|
79
|
+
const fileResponse = await getFileResponseAction({
|
|
80
|
+
surveyId: responseOptions.surveyId,
|
|
81
|
+
fileId: progressResponse.data.fileId,
|
|
82
|
+
bearerToken: responseOptions.bearerToken
|
|
83
|
+
});
|
|
84
|
+
return fileResponse;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
const message = getErrorMessage(error);
|
|
87
|
+
return failure(message);
|
|
88
|
+
}
|
|
4
89
|
};
|
|
90
|
+
|
|
91
|
+
// src/utils/parsers.ts
|
|
92
|
+
function safeParseBearerToken(response) {
|
|
93
|
+
return response?.access_token && typeof response.access_token === "string" ? success(response.access_token) : failure("Incorrect token format");
|
|
94
|
+
}
|
|
95
|
+
function safeParseTestResponse(response) {
|
|
96
|
+
return response?.result?.userId ? success(void 0) : failure("Incorrect test response format");
|
|
97
|
+
}
|
|
98
|
+
function safeParseStartFileExportResponse(response) {
|
|
99
|
+
return "result" in response ? "progressId" in response.result && "percentComplete" in response.result && "status" in response.result ? success(response.result) : failure("Start Export Response invalid format") : "error" in response ? failure(`${response.error.errorCode}: ${response.error.errorMessage}`) : failure(`Unable to parse Start Export Response`);
|
|
100
|
+
}
|
|
101
|
+
function safeParseFileProgressResponse(response) {
|
|
102
|
+
return "result" in response ? "fileId" in response.result && "percentComplete" in response.result && "status" in response.result ? success(response.result) : failure("File Progress Response invalid format") : "error" in response ? failure(`${response.error.errorCode}: ${response.error.errorMessage}`) : failure(`Unable to parse File Progress Response`);
|
|
103
|
+
}
|
|
104
|
+
function safeParseFileResponse(response) {
|
|
105
|
+
return Buffer.isBuffer(response) ? success(response) : failure("Incorrect file response format");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// src/qualtrics/index.ts
|
|
109
|
+
async function execute(config) {
|
|
110
|
+
const { datacenterId, route, headers, timeout, body } = config;
|
|
111
|
+
const host = `https://${datacenterId}.qualtrics.com`;
|
|
112
|
+
const resource = new URL(route, host);
|
|
113
|
+
const controller = new AbortController();
|
|
114
|
+
const options = {
|
|
115
|
+
method: "GET",
|
|
116
|
+
headers,
|
|
117
|
+
signal: controller.signal
|
|
118
|
+
};
|
|
119
|
+
if (body) {
|
|
120
|
+
if (typeof body === "string")
|
|
121
|
+
headers.append("Content-Type", "application/json");
|
|
122
|
+
options.method = "POST";
|
|
123
|
+
options.headers = headers;
|
|
124
|
+
options.body = body;
|
|
125
|
+
}
|
|
126
|
+
const timer = setTimeout(() => controller.abort(), timeout ?? 30 * 1e3);
|
|
127
|
+
const response = await fetch(resource, options);
|
|
128
|
+
clearTimeout(timer);
|
|
129
|
+
if (response.ok) {
|
|
130
|
+
const data = await response.json();
|
|
131
|
+
return Promise.resolve(data);
|
|
132
|
+
} else {
|
|
133
|
+
const message = await response.text();
|
|
134
|
+
return Promise.reject(`${response.status}: ${message}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/methods/getBearerToken.ts
|
|
139
|
+
var getBearerToken = (connectionOptions) => async ({ clientId, clientSecret, scope }) => {
|
|
140
|
+
const route = `/oauth2/token`;
|
|
141
|
+
try {
|
|
142
|
+
const { datacenterId, timeout } = connectionOptions;
|
|
143
|
+
const headers = new Headers({
|
|
144
|
+
Authorization: "Basic " + Buffer.from(`${clientId}:${clientSecret}`).toString("base64")
|
|
145
|
+
});
|
|
146
|
+
const body = new FormData();
|
|
147
|
+
body.append("grant_type", "client_credentials");
|
|
148
|
+
body.append("scope", scope);
|
|
149
|
+
const response = await execute({
|
|
150
|
+
datacenterId,
|
|
151
|
+
route,
|
|
152
|
+
headers,
|
|
153
|
+
body,
|
|
154
|
+
timeout
|
|
155
|
+
});
|
|
156
|
+
const result = safeParseBearerToken(response);
|
|
157
|
+
return result;
|
|
158
|
+
} catch (error) {
|
|
159
|
+
const message = getErrorMessage(error);
|
|
160
|
+
return failure(message);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// src/methods/getResponseExportProgress.ts
|
|
165
|
+
var getResponseExportProgress = (connectionOptions) => async ({ exportProgressId, surveyId, bearerToken }) => {
|
|
166
|
+
const route = `/surveys/${surveyId}/export-responses/${exportProgressId}`;
|
|
167
|
+
try {
|
|
168
|
+
const headers = getAuthHeaders(connectionOptions.apiToken, bearerToken);
|
|
169
|
+
const config = {
|
|
170
|
+
datacenterId: connectionOptions.datacenterId,
|
|
171
|
+
route,
|
|
172
|
+
headers,
|
|
173
|
+
timeout: connectionOptions.timeout
|
|
174
|
+
};
|
|
175
|
+
const response = await execute(config);
|
|
176
|
+
const result = safeParseFileProgressResponse(response);
|
|
177
|
+
return result;
|
|
178
|
+
} catch (error) {
|
|
179
|
+
const message = getErrorMessage(error);
|
|
180
|
+
return failure(message);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// src/methods/getResponseExportFile.ts
|
|
185
|
+
var getResponseExportFile = (connectionOptions) => async ({ surveyId, fileId, bearerToken }) => {
|
|
186
|
+
const route = `/surveys/${surveyId}/export-responses/${fileId}/file`;
|
|
187
|
+
try {
|
|
188
|
+
const headers = getAuthHeaders(connectionOptions.apiToken, bearerToken);
|
|
189
|
+
const config = {
|
|
190
|
+
datacenterId: connectionOptions.datacenterId,
|
|
191
|
+
route,
|
|
192
|
+
headers,
|
|
193
|
+
timeout: connectionOptions.timeout
|
|
194
|
+
};
|
|
195
|
+
const response = await execute(config);
|
|
196
|
+
const result = safeParseFileResponse(response);
|
|
197
|
+
return result;
|
|
198
|
+
} catch (error) {
|
|
199
|
+
const message = getErrorMessage(error);
|
|
200
|
+
return failure(message);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// src/methods/startResponseExport.ts
|
|
205
|
+
var startResponseExport = (connectionOptions) => async ({
|
|
206
|
+
surveyId,
|
|
207
|
+
startDate,
|
|
208
|
+
endDate,
|
|
209
|
+
format = "csv",
|
|
210
|
+
bearerToken = void 0,
|
|
211
|
+
...rest
|
|
212
|
+
}) => {
|
|
213
|
+
const route = `/surveys/${surveyId}/export-responses`;
|
|
214
|
+
try {
|
|
215
|
+
const headers = getAuthHeaders(connectionOptions.apiToken, bearerToken);
|
|
216
|
+
const body = JSON.stringify({
|
|
217
|
+
startDate: startDate.toISOString(),
|
|
218
|
+
endDate: endDate.toDateString(),
|
|
219
|
+
format,
|
|
220
|
+
...rest
|
|
221
|
+
});
|
|
222
|
+
const config = {
|
|
223
|
+
datacenterId: connectionOptions.datacenterId,
|
|
224
|
+
route,
|
|
225
|
+
headers,
|
|
226
|
+
body,
|
|
227
|
+
timeout: connectionOptions.timeout
|
|
228
|
+
};
|
|
229
|
+
const response = await execute(config);
|
|
230
|
+
const result = safeParseStartFileExportResponse(response);
|
|
231
|
+
return result;
|
|
232
|
+
} catch (error) {
|
|
233
|
+
const message = getErrorMessage(error);
|
|
234
|
+
return failure(message);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// src/methods/testConnection.ts
|
|
239
|
+
var testConnection = (connectionOptions) => async (bearerToken) => {
|
|
240
|
+
const route = "/API/v3/whoami";
|
|
241
|
+
try {
|
|
242
|
+
const headers = getAuthHeaders(connectionOptions.apiToken, bearerToken);
|
|
243
|
+
const config = {
|
|
244
|
+
datacenterId: connectionOptions.datacenterId,
|
|
245
|
+
route,
|
|
246
|
+
headers,
|
|
247
|
+
timeout: connectionOptions.timeout
|
|
248
|
+
};
|
|
249
|
+
const response = await execute(config);
|
|
250
|
+
const result = safeParseTestResponse(response);
|
|
251
|
+
return result;
|
|
252
|
+
} catch (error) {
|
|
253
|
+
const message = getErrorMessage(error);
|
|
254
|
+
return failure(message);
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// src/createConnection.ts
|
|
259
|
+
var createConnection = (options) => ({
|
|
260
|
+
exportResponses: exportResponses(options),
|
|
261
|
+
getBearerToken: getBearerToken(options),
|
|
262
|
+
getResponseExportProgress: getResponseExportProgress(options),
|
|
263
|
+
startResponseExport: startResponseExport(options),
|
|
264
|
+
testConnection: testConnection(options)
|
|
265
|
+
});
|
|
5
266
|
export {
|
|
6
|
-
|
|
267
|
+
createConnection,
|
|
268
|
+
exportResponses,
|
|
269
|
+
getBearerToken,
|
|
270
|
+
getResponseExportFile,
|
|
271
|
+
getResponseExportProgress,
|
|
272
|
+
startResponseExport,
|
|
273
|
+
testConnection
|
|
7
274
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@b-jones-rfd/qualtrics-api-tasks",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Perform common tasks using the Qualtrics API",
|
|
5
5
|
"private": false,
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"devDependencies": {
|
|
13
13
|
"@changesets/cli": "^2.27.1",
|
|
14
|
+
"@types/node": "^20.11.20",
|
|
15
|
+
"prettier": "^3.2.5",
|
|
14
16
|
"tsup": "^8.0.2",
|
|
15
17
|
"typescript": "^5.3.3",
|
|
16
18
|
"vitest": "^1.3.1"
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { success, failure } from '../src/utils'
|
|
3
|
+
import {
|
|
4
|
+
safeParseBearerToken,
|
|
5
|
+
safeParseTestResponse,
|
|
6
|
+
safeParseStartFileExportResponse,
|
|
7
|
+
safeParseFileProgressResponse,
|
|
8
|
+
safeParseFileResponse,
|
|
9
|
+
} from '../src/utils/parsers'
|
|
10
|
+
|
|
11
|
+
describe('safeParseBearerToken', () => {
|
|
12
|
+
const fixture = {
|
|
13
|
+
access_token: 'sometoken',
|
|
14
|
+
token_type: 'Bearer',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
it('should pass with correct data', () => {
|
|
18
|
+
const res = fixture
|
|
19
|
+
const expected = success(fixture.access_token)
|
|
20
|
+
const parsed = safeParseBearerToken(res)
|
|
21
|
+
expect(parsed).toStrictEqual(expected)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('should fail when token is incorrect type', () => {
|
|
25
|
+
const res = {
|
|
26
|
+
result: 'bad result',
|
|
27
|
+
}
|
|
28
|
+
const expected = failure('Incorrect token format')
|
|
29
|
+
const parsed = safeParseBearerToken(res)
|
|
30
|
+
expect(parsed).toStrictEqual(expected)
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
describe('safeParseFileProgressResponse', () => {
|
|
35
|
+
const fixture = {
|
|
36
|
+
result: {
|
|
37
|
+
fileId: 'sometoken',
|
|
38
|
+
percentComplete: 0,
|
|
39
|
+
status: 'inProgress',
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
it('should pass with correct data', () => {
|
|
44
|
+
const res = fixture
|
|
45
|
+
const expected = success(fixture.result)
|
|
46
|
+
const parsed = safeParseFileProgressResponse(res)
|
|
47
|
+
expect(parsed).toStrictEqual(expected)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('should fail when error response', () => {
|
|
51
|
+
const res = {
|
|
52
|
+
error: {
|
|
53
|
+
errorCode: '401',
|
|
54
|
+
errorMessage: 'Some error',
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
const expected = failure(
|
|
58
|
+
`${res.error.errorCode}: ${res.error.errorMessage}`
|
|
59
|
+
)
|
|
60
|
+
const parsed = safeParseFileProgressResponse(res)
|
|
61
|
+
expect(parsed).toStrictEqual(expected)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('should fail with missing props', () => {
|
|
65
|
+
const res = {
|
|
66
|
+
result: {
|
|
67
|
+
percentComplete: 0,
|
|
68
|
+
status: 'inProgress',
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
const expected = failure('File Progress Response invalid format')
|
|
72
|
+
const parsed = safeParseFileProgressResponse(res)
|
|
73
|
+
expect(parsed).toStrictEqual(expected)
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe('safeParseStartFileExportResponse', () => {
|
|
78
|
+
const fixture = {
|
|
79
|
+
result: {
|
|
80
|
+
progressId: 'sometoken',
|
|
81
|
+
percentComplete: 0,
|
|
82
|
+
status: 'inProgress',
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
it('should pass with correct data', () => {
|
|
87
|
+
const res = fixture
|
|
88
|
+
const expected = success(fixture.result)
|
|
89
|
+
const parsed = safeParseStartFileExportResponse(res)
|
|
90
|
+
expect(parsed).toStrictEqual(expected)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('should fail when error response', () => {
|
|
94
|
+
const res = {
|
|
95
|
+
error: {
|
|
96
|
+
errorCode: '401',
|
|
97
|
+
errorMessage: 'Some error',
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
const expected = failure(
|
|
101
|
+
`${res.error.errorCode}: ${res.error.errorMessage}`
|
|
102
|
+
)
|
|
103
|
+
const parsed = safeParseStartFileExportResponse(res)
|
|
104
|
+
expect(parsed).toStrictEqual(expected)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('should fail with missing props', () => {
|
|
108
|
+
const res = {
|
|
109
|
+
result: {
|
|
110
|
+
percentComplete: 0,
|
|
111
|
+
status: 'inProgress',
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
const expected = failure('Start Export Response invalid format')
|
|
115
|
+
const parsed = safeParseStartFileExportResponse(res)
|
|
116
|
+
expect(parsed).toStrictEqual(expected)
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
describe('safeParseTestResponse', () => {
|
|
121
|
+
const fixture = {
|
|
122
|
+
result: {
|
|
123
|
+
userId: 'testUser',
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
it('should pass with correct data', () => {
|
|
128
|
+
const res = fixture
|
|
129
|
+
const expected = success(undefined)
|
|
130
|
+
const parsed = safeParseTestResponse(res)
|
|
131
|
+
expect(parsed).toStrictEqual(expected)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('should fail when response is incorrect type', () => {
|
|
135
|
+
const res = {
|
|
136
|
+
result: 'bad result',
|
|
137
|
+
}
|
|
138
|
+
const expected = failure('Incorrect test response format')
|
|
139
|
+
const parsed = safeParseTestResponse(res)
|
|
140
|
+
expect(parsed).toStrictEqual(expected)
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
describe('safeParseFileResponse', () => {
|
|
145
|
+
const fixture = Buffer.from('file content')
|
|
146
|
+
|
|
147
|
+
it('should pass with correct data', () => {
|
|
148
|
+
const res = fixture
|
|
149
|
+
const expected = success(fixture)
|
|
150
|
+
const parsed = safeParseFileResponse(res)
|
|
151
|
+
expect(parsed).toStrictEqual(expected)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('should fail with invalid response type', () => {
|
|
155
|
+
const res = { fixture }
|
|
156
|
+
const expected = failure('Incorrect file response format')
|
|
157
|
+
const parsed = safeParseFileResponse(res)
|
|
158
|
+
expect(parsed).toStrictEqual(expected)
|
|
159
|
+
})
|
|
160
|
+
})
|