@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 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
+ [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier)
2
+
1
3
  # qualtrics-api-tasks
2
- Helpers to perform common tasks using the Qualtrics API
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
- declare const hello: () => void;
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
- export { hello };
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
- declare const hello: () => void;
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
- export { hello };
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
- hello: () => hello
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
- var hello = () => {
27
- console.log("Nothing to see here!");
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
- hello
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
- var hello = () => {
3
- console.log("Nothing to see here!");
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
- hello
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.4",
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
+ })