@b-jones-rfd/qualtrics-api-tasks 0.0.5 → 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,11 @@
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
+
3
9
  ## 0.0.5
4
10
 
5
11
  ### Patch Changes
package/README.md CHANGED
@@ -139,23 +139,112 @@ export type Action<TConfig, TResponse> = (
139
139
 
140
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
141
 
142
- #### getBearerToken()
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)
143
176
 
144
177
  Implements [OAuth Authentication (Client Credentials)](https://api.qualtrics.com/9398592961ced-o-auth-authentication-client-credentials)
145
178
 
146
179
  `options`
147
180
 
148
- | Property | Type | Description | Required |
149
- | ------------ | ------ | ------------------------- | -------- |
150
- | clientId | string | Quatrics Client ID | Y |
151
- | clientSecret | string | Qualtrics Client Password | Y |
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)
152
188
 
153
- #### testConnection(options)
189
+ Implements [Get Response Export File](https://api.qualtrics.com/41296b6f2e828-get-response-export-file)
154
190
 
155
191
  `options`
156
192
 
157
193
  | Property | Type | Description | Required |
158
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
+ | ----------- | ------ | ------------------ | -------- |
159
248
  | bearerToken | string | Valid Bearer Token | N |
160
249
 
161
250
  ## Responses
@@ -196,4 +285,4 @@ We use [SemVer](http://semver.org/) for versioning. For the versions available,
196
285
 
197
286
  ## License
198
287
 
199
- [MIT License](https://github.com/B-Jones-RFD/sp-rest-connect/blob/main/LICENSE)
288
+ [MIT License](https://github.com/B-Jones-RFD/qualtrics-api-tasks/blob/main/LICENSE)
package/dist/index.d.mts CHANGED
@@ -7,6 +7,7 @@ type Failure = {
7
7
  error: string;
8
8
  };
9
9
  type Result<T> = Success<T> | Failure;
10
+ type ResponseFormat = 'csv' | 'json' | 'ndjson' | 'spss' | 'tsv' | 'xml';
10
11
  type Action<TConfig, TResponse> = (options: TConfig) => Promise<Result<TResponse>>;
11
12
  type ConnectionOptions = {
12
13
  datacenterId: string;
@@ -15,21 +16,88 @@ type ConnectionOptions = {
15
16
  };
16
17
  type ActionFactory<TParams, TResponse> = (options: ConnectionOptions) => Action<TParams, TResponse>;
17
18
  type Connection = {
18
- testConnection: Action<string | undefined, void>;
19
+ exportResponses: Action<ExportResponsesOptions, Buffer>;
19
20
  getBearerToken: Action<{
20
21
  clientId: string;
21
22
  clientSecret: string;
23
+ scope: string;
22
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>>;
23
37
  };
24
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
+ };
25
71
 
26
72
  declare const createConnection: ConnectionFactory;
27
73
 
74
+ declare const exportResponses: ActionFactory<ExportResponsesOptions, Buffer>;
75
+
28
76
  declare const getBearerToken: ActionFactory<{
29
77
  clientId: string;
30
78
  clientSecret: string;
79
+ scope: string;
31
80
  }, string>;
32
81
 
33
- declare const testConnection: ActionFactory<string | undefined, void>;
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>>;
34
102
 
35
- export { type Action, type ActionFactory, type Connection, type ConnectionFactory, type ConnectionOptions, type Failure, type Result, type Success, createConnection, getBearerToken, testConnection };
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
@@ -7,6 +7,7 @@ type Failure = {
7
7
  error: string;
8
8
  };
9
9
  type Result<T> = Success<T> | Failure;
10
+ type ResponseFormat = 'csv' | 'json' | 'ndjson' | 'spss' | 'tsv' | 'xml';
10
11
  type Action<TConfig, TResponse> = (options: TConfig) => Promise<Result<TResponse>>;
11
12
  type ConnectionOptions = {
12
13
  datacenterId: string;
@@ -15,21 +16,88 @@ type ConnectionOptions = {
15
16
  };
16
17
  type ActionFactory<TParams, TResponse> = (options: ConnectionOptions) => Action<TParams, TResponse>;
17
18
  type Connection = {
18
- testConnection: Action<string | undefined, void>;
19
+ exportResponses: Action<ExportResponsesOptions, Buffer>;
19
20
  getBearerToken: Action<{
20
21
  clientId: string;
21
22
  clientSecret: string;
23
+ scope: string;
22
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>>;
23
37
  };
24
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
+ };
25
71
 
26
72
  declare const createConnection: ConnectionFactory;
27
73
 
74
+ declare const exportResponses: ActionFactory<ExportResponsesOptions, Buffer>;
75
+
28
76
  declare const getBearerToken: ActionFactory<{
29
77
  clientId: string;
30
78
  clientSecret: string;
79
+ scope: string;
31
80
  }, string>;
32
81
 
33
- declare const testConnection: ActionFactory<string | undefined, void>;
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>>;
34
102
 
35
- export { type Action, type ActionFactory, type Connection, type ConnectionFactory, type ConnectionOptions, type Failure, type Result, type Success, createConnection, getBearerToken, testConnection };
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
@@ -21,12 +21,16 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var src_exports = {};
22
22
  __export(src_exports, {
23
23
  createConnection: () => createConnection,
24
+ exportResponses: () => exportResponses,
24
25
  getBearerToken: () => getBearerToken,
26
+ getResponseExportFile: () => getResponseExportFile,
27
+ getResponseExportProgress: () => getResponseExportProgress,
28
+ startResponseExport: () => startResponseExport,
25
29
  testConnection: () => testConnection
26
30
  });
27
31
  module.exports = __toCommonJS(src_exports);
28
32
 
29
- // src/utils.ts
33
+ // src/utils/index.ts
30
34
  function success(data) {
31
35
  return {
32
36
  success: true,
@@ -42,12 +46,6 @@ function failure(error) {
42
46
  function getErrorMessage(error) {
43
47
  return error ? typeof error === "object" && "message" in error && typeof error.message === "string" ? error.message : error.toString() : "An error occured";
44
48
  }
45
- function safeParseBearerToken(response) {
46
- return response?.access_token && typeof response.access_token === "string" ? success(response.access_token) : failure("Incorrect token format");
47
- }
48
- function safeParseTestResponse(response) {
49
- return response?.result?.userId ? success(void 0) : failure("Incorrect test response format");
50
- }
51
49
  function getAuthHeaders(apiToken, bearerToken) {
52
50
  if (apiToken)
53
51
  return new Headers({
@@ -55,7 +53,7 @@ function getAuthHeaders(apiToken, bearerToken) {
55
53
  });
56
54
  if (bearerToken) {
57
55
  return new Headers({
58
- Authorization: bearerToken
56
+ Authorization: `Bearer ${bearerToken}`
59
57
  });
60
58
  }
61
59
  throw new Error(
@@ -63,6 +61,82 @@ function getAuthHeaders(apiToken, bearerToken) {
63
61
  );
64
62
  }
65
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
+
66
140
  // src/qualtrics/index.ts
67
141
  async function execute(config) {
68
142
  const { datacenterId, route, headers, timeout, body } = config;
@@ -94,7 +168,7 @@ async function execute(config) {
94
168
  }
95
169
 
96
170
  // src/methods/getBearerToken.ts
97
- var getBearerToken = (connectionOptions) => async ({ clientId, clientSecret }) => {
171
+ var getBearerToken = (connectionOptions) => async ({ clientId, clientSecret, scope }) => {
98
172
  const route = `/oauth2/token`;
99
173
  try {
100
174
  const { datacenterId, timeout } = connectionOptions;
@@ -103,7 +177,7 @@ var getBearerToken = (connectionOptions) => async ({ clientId, clientSecret }) =
103
177
  });
104
178
  const body = new FormData();
105
179
  body.append("grant_type", "client_credentials");
106
- body.append("scope", "manage:all");
180
+ body.append("scope", scope);
107
181
  const response = await execute({
108
182
  datacenterId,
109
183
  route,
@@ -119,6 +193,80 @@ var getBearerToken = (connectionOptions) => async ({ clientId, clientSecret }) =
119
193
  }
120
194
  };
121
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
+ }
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
+
122
270
  // src/methods/testConnection.ts
123
271
  var testConnection = (connectionOptions) => async (bearerToken) => {
124
272
  const route = "/API/v3/whoami";
@@ -141,12 +289,19 @@ var testConnection = (connectionOptions) => async (bearerToken) => {
141
289
 
142
290
  // src/createConnection.ts
143
291
  var createConnection = (options) => ({
144
- testConnection: testConnection(options),
145
- getBearerToken: getBearerToken(options)
292
+ exportResponses: exportResponses(options),
293
+ getBearerToken: getBearerToken(options),
294
+ getResponseExportProgress: getResponseExportProgress(options),
295
+ startResponseExport: startResponseExport(options),
296
+ testConnection: testConnection(options)
146
297
  });
147
298
  // Annotate the CommonJS export names for ESM import in node:
148
299
  0 && (module.exports = {
149
300
  createConnection,
301
+ exportResponses,
150
302
  getBearerToken,
303
+ getResponseExportFile,
304
+ getResponseExportProgress,
305
+ startResponseExport,
151
306
  testConnection
152
307
  });
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- // src/utils.ts
1
+ // src/utils/index.ts
2
2
  function success(data) {
3
3
  return {
4
4
  success: true,
@@ -14,12 +14,6 @@ function failure(error) {
14
14
  function getErrorMessage(error) {
15
15
  return error ? typeof error === "object" && "message" in error && typeof error.message === "string" ? error.message : error.toString() : "An error occured";
16
16
  }
17
- function safeParseBearerToken(response) {
18
- return response?.access_token && typeof response.access_token === "string" ? success(response.access_token) : failure("Incorrect token format");
19
- }
20
- function safeParseTestResponse(response) {
21
- return response?.result?.userId ? success(void 0) : failure("Incorrect test response format");
22
- }
23
17
  function getAuthHeaders(apiToken, bearerToken) {
24
18
  if (apiToken)
25
19
  return new Headers({
@@ -27,7 +21,7 @@ function getAuthHeaders(apiToken, bearerToken) {
27
21
  });
28
22
  if (bearerToken) {
29
23
  return new Headers({
30
- Authorization: bearerToken
24
+ Authorization: `Bearer ${bearerToken}`
31
25
  });
32
26
  }
33
27
  throw new Error(
@@ -35,6 +29,82 @@ function getAuthHeaders(apiToken, bearerToken) {
35
29
  );
36
30
  }
37
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
+ }
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
+
38
108
  // src/qualtrics/index.ts
39
109
  async function execute(config) {
40
110
  const { datacenterId, route, headers, timeout, body } = config;
@@ -66,7 +136,7 @@ async function execute(config) {
66
136
  }
67
137
 
68
138
  // src/methods/getBearerToken.ts
69
- var getBearerToken = (connectionOptions) => async ({ clientId, clientSecret }) => {
139
+ var getBearerToken = (connectionOptions) => async ({ clientId, clientSecret, scope }) => {
70
140
  const route = `/oauth2/token`;
71
141
  try {
72
142
  const { datacenterId, timeout } = connectionOptions;
@@ -75,7 +145,7 @@ var getBearerToken = (connectionOptions) => async ({ clientId, clientSecret }) =
75
145
  });
76
146
  const body = new FormData();
77
147
  body.append("grant_type", "client_credentials");
78
- body.append("scope", "manage:all");
148
+ body.append("scope", scope);
79
149
  const response = await execute({
80
150
  datacenterId,
81
151
  route,
@@ -91,6 +161,80 @@ var getBearerToken = (connectionOptions) => async ({ clientId, clientSecret }) =
91
161
  }
92
162
  };
93
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
+
94
238
  // src/methods/testConnection.ts
95
239
  var testConnection = (connectionOptions) => async (bearerToken) => {
96
240
  const route = "/API/v3/whoami";
@@ -113,11 +257,18 @@ var testConnection = (connectionOptions) => async (bearerToken) => {
113
257
 
114
258
  // src/createConnection.ts
115
259
  var createConnection = (options) => ({
116
- testConnection: testConnection(options),
117
- getBearerToken: getBearerToken(options)
260
+ exportResponses: exportResponses(options),
261
+ getBearerToken: getBearerToken(options),
262
+ getResponseExportProgress: getResponseExportProgress(options),
263
+ startResponseExport: startResponseExport(options),
264
+ testConnection: testConnection(options)
118
265
  });
119
266
  export {
120
267
  createConnection,
268
+ exportResponses,
121
269
  getBearerToken,
270
+ getResponseExportFile,
271
+ getResponseExportProgress,
272
+ startResponseExport,
122
273
  testConnection
123
274
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@b-jones-rfd/qualtrics-api-tasks",
3
- "version": "0.0.5",
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",
@@ -1,10 +1,12 @@
1
1
  import { describe, it, expect } from 'vitest'
2
+ import { success, failure } from '../src/utils'
2
3
  import {
3
- success,
4
- failure,
5
4
  safeParseBearerToken,
6
5
  safeParseTestResponse,
7
- } from '../src/utils'
6
+ safeParseStartFileExportResponse,
7
+ safeParseFileProgressResponse,
8
+ safeParseFileResponse,
9
+ } from '../src/utils/parsers'
8
10
 
9
11
  describe('safeParseBearerToken', () => {
10
12
  const fixture = {
@@ -29,6 +31,92 @@ describe('safeParseBearerToken', () => {
29
31
  })
30
32
  })
31
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
+
32
120
  describe('safeParseTestResponse', () => {
33
121
  const fixture = {
34
122
  result: {
@@ -52,3 +140,21 @@ describe('safeParseTestResponse', () => {
52
140
  expect(parsed).toStrictEqual(expected)
53
141
  })
54
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
+ })