@b-jones-rfd/qualtrics-api-tasks 0.0.5 → 0.1.1

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.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 2f8dbdf: Added JSDoc action method descriptions
8
+
9
+ ## 0.1.0
10
+
11
+ ### Minor Changes
12
+
13
+ - efa27c7: Implemented survey file response methods
14
+
3
15
  ## 0.0.5
4
16
 
5
17
  ### Patch Changes
package/README.md CHANGED
@@ -76,7 +76,7 @@ import { createConnection } from '@b-jones-rfd/qualtrics-api-tasks'
76
76
  const connectionOptions = {
77
77
  datacenterId: 'az1',
78
78
  apiToken: 'my API Token', // optional
79
- timeout: 20000, // optional timeout in milliseconds, default is 30 seconds
79
+ timeout: 20 * 1000, // optional timeout in milliseconds, default is 30 seconds
80
80
  }
81
81
 
82
82
  const connection: SiteConnection = createConnection(connectionOptions)
@@ -123,9 +123,9 @@ async function testMyQualtricsConnection(listName: string) {
123
123
 
124
124
  `timeout`
125
125
 
126
- | Type | Default value | Description | Example | Required |
127
- | ------ | ------------- | ------------------------- | --------------------- | -------- |
128
- | number | 30000 | Qualtrics request timeout | sharepoint.domain.com | N |
126
+ | Type | Default value | Description | Example | Required |
127
+ | ------ | ------------- | ------------------------- | ------- | -------- |
128
+ | number | 30000 | Qualtrics request timeout | 1000 | N |
129
129
 
130
130
  ### Actions
131
131
 
@@ -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,126 @@ 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
+ getResponseExportFile: Action<{
26
+ surveyId: string;
27
+ fileId: string;
28
+ bearerToken?: string;
29
+ }, Buffer>;
30
+ getResponseExportProgress: Action<{
31
+ exportProgressId: string;
32
+ surveyId: string;
33
+ bearerToken?: string;
34
+ }, FileProgressResponse>;
35
+ startResponseExport: Action<ExportResponsesOptions, {
36
+ progressId: string;
37
+ percentComplete: number;
38
+ status: string;
39
+ continuationToken?: string;
40
+ }>;
41
+ testConnection: (bearerToken?: string) => Promise<Result<string>>;
23
42
  };
24
43
  type ConnectionFactory = (options: ConnectionOptions) => Connection;
44
+ type ExportResponsesOptions = {
45
+ surveyId: string;
46
+ startDate: Date;
47
+ endDate: Date;
48
+ format?: ResponseFormat;
49
+ breakoutSets?: boolean;
50
+ compress?: boolean;
51
+ exportResponsesInProgress?: boolean;
52
+ filterId?: string;
53
+ formatDecimalAsComma?: boolean;
54
+ includeDisplayOrder?: boolean;
55
+ limit?: number;
56
+ multiselectSeenUnansweredRecode?: number;
57
+ newlineReplacement?: string;
58
+ seenUnansweredRecode?: number;
59
+ timeZone?: string;
60
+ useLabels?: boolean;
61
+ embeddedDataIds?: string[];
62
+ questionIds?: string[];
63
+ surveyMetadataIds?: string[];
64
+ continuationToken?: string;
65
+ allowContinuation?: boolean;
66
+ includeLabelColumns?: boolean;
67
+ sortByLastModifiedDate?: boolean;
68
+ bearerToken?: string;
69
+ };
70
+ type FileProgressResponse = {
71
+ fileId: string;
72
+ percentComplete: number;
73
+ status: string;
74
+ continuationToken?: string;
75
+ };
25
76
 
26
77
  declare const createConnection: ConnectionFactory;
27
78
 
79
+ /**
80
+ * Export Survey Response File
81
+ *
82
+ * Implements Survey Response File Export end to end
83
+ * @see https://api.qualtrics.com/u9e5lh4172v0v-survey-response-export-guide
84
+ */
85
+ declare const exportResponses: ActionFactory<ExportResponsesOptions, Buffer>;
86
+
87
+ /**
88
+ * Get Bearer Token
89
+ *
90
+ * Implements OAuth Authentication (Client Credentials
91
+ * @see https://api.qualtrics.com/9398592961ced-o-auth-authentication-client-credentials
92
+ */
28
93
  declare const getBearerToken: ActionFactory<{
29
94
  clientId: string;
30
95
  clientSecret: string;
96
+ scope: string;
31
97
  }, string>;
32
98
 
33
- declare const testConnection: ActionFactory<string | undefined, void>;
99
+ /**
100
+ * Get Response Export File
101
+ *
102
+ * Implements Get Response Export Progress. This is step 2 in the survey response file export process.
103
+ * @see https://api.qualtrics.com/37e6a66f74ab4-get-response-export-progress
104
+ */
105
+ declare const getResponseExportProgress: ActionFactory<{
106
+ exportProgressId: string;
107
+ surveyId: string;
108
+ bearerToken?: string;
109
+ }, FileProgressResponse>;
110
+
111
+ /**
112
+ * Get Response Export File
113
+ *
114
+ * Implements Get Response Export File. This is step 3 in the survey response file export process.
115
+ * @see https://api.qualtrics.com/41296b6f2e828-get-response-export-file
116
+ */
117
+ declare const getResponseExportFile: ActionFactory<{
118
+ surveyId: string;
119
+ fileId: string;
120
+ bearerToken?: string;
121
+ }, Buffer>;
122
+
123
+ /**
124
+ * Start Response Export
125
+ *
126
+ * Implements Start Response File Export
127
+ * @see https://api.qualtrics.com/6b00592b9c013-start-response-export
128
+ */
129
+ declare const startResponseExport: ActionFactory<ExportResponsesOptions, {
130
+ progressId: string;
131
+ percentComplete: number;
132
+ status: string;
133
+ continuationToken?: string;
134
+ }>;
135
+
136
+ /**
137
+ * Test connection to Qualtrics API
138
+ */
139
+ declare const testConnection: (connectionOptions: ConnectionOptions) => (bearerToken?: string) => Promise<Result<string>>;
34
140
 
35
- export { type Action, type ActionFactory, type Connection, type ConnectionFactory, type ConnectionOptions, type Failure, type Result, type Success, createConnection, getBearerToken, testConnection };
141
+ 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,126 @@ 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
+ getResponseExportFile: Action<{
26
+ surveyId: string;
27
+ fileId: string;
28
+ bearerToken?: string;
29
+ }, Buffer>;
30
+ getResponseExportProgress: Action<{
31
+ exportProgressId: string;
32
+ surveyId: string;
33
+ bearerToken?: string;
34
+ }, FileProgressResponse>;
35
+ startResponseExport: Action<ExportResponsesOptions, {
36
+ progressId: string;
37
+ percentComplete: number;
38
+ status: string;
39
+ continuationToken?: string;
40
+ }>;
41
+ testConnection: (bearerToken?: string) => Promise<Result<string>>;
23
42
  };
24
43
  type ConnectionFactory = (options: ConnectionOptions) => Connection;
44
+ type ExportResponsesOptions = {
45
+ surveyId: string;
46
+ startDate: Date;
47
+ endDate: Date;
48
+ format?: ResponseFormat;
49
+ breakoutSets?: boolean;
50
+ compress?: boolean;
51
+ exportResponsesInProgress?: boolean;
52
+ filterId?: string;
53
+ formatDecimalAsComma?: boolean;
54
+ includeDisplayOrder?: boolean;
55
+ limit?: number;
56
+ multiselectSeenUnansweredRecode?: number;
57
+ newlineReplacement?: string;
58
+ seenUnansweredRecode?: number;
59
+ timeZone?: string;
60
+ useLabels?: boolean;
61
+ embeddedDataIds?: string[];
62
+ questionIds?: string[];
63
+ surveyMetadataIds?: string[];
64
+ continuationToken?: string;
65
+ allowContinuation?: boolean;
66
+ includeLabelColumns?: boolean;
67
+ sortByLastModifiedDate?: boolean;
68
+ bearerToken?: string;
69
+ };
70
+ type FileProgressResponse = {
71
+ fileId: string;
72
+ percentComplete: number;
73
+ status: string;
74
+ continuationToken?: string;
75
+ };
25
76
 
26
77
  declare const createConnection: ConnectionFactory;
27
78
 
79
+ /**
80
+ * Export Survey Response File
81
+ *
82
+ * Implements Survey Response File Export end to end
83
+ * @see https://api.qualtrics.com/u9e5lh4172v0v-survey-response-export-guide
84
+ */
85
+ declare const exportResponses: ActionFactory<ExportResponsesOptions, Buffer>;
86
+
87
+ /**
88
+ * Get Bearer Token
89
+ *
90
+ * Implements OAuth Authentication (Client Credentials
91
+ * @see https://api.qualtrics.com/9398592961ced-o-auth-authentication-client-credentials
92
+ */
28
93
  declare const getBearerToken: ActionFactory<{
29
94
  clientId: string;
30
95
  clientSecret: string;
96
+ scope: string;
31
97
  }, string>;
32
98
 
33
- declare const testConnection: ActionFactory<string | undefined, void>;
99
+ /**
100
+ * Get Response Export File
101
+ *
102
+ * Implements Get Response Export Progress. This is step 2 in the survey response file export process.
103
+ * @see https://api.qualtrics.com/37e6a66f74ab4-get-response-export-progress
104
+ */
105
+ declare const getResponseExportProgress: ActionFactory<{
106
+ exportProgressId: string;
107
+ surveyId: string;
108
+ bearerToken?: string;
109
+ }, FileProgressResponse>;
110
+
111
+ /**
112
+ * Get Response Export File
113
+ *
114
+ * Implements Get Response Export File. This is step 3 in the survey response file export process.
115
+ * @see https://api.qualtrics.com/41296b6f2e828-get-response-export-file
116
+ */
117
+ declare const getResponseExportFile: ActionFactory<{
118
+ surveyId: string;
119
+ fileId: string;
120
+ bearerToken?: string;
121
+ }, Buffer>;
122
+
123
+ /**
124
+ * Start Response Export
125
+ *
126
+ * Implements Start Response File Export
127
+ * @see https://api.qualtrics.com/6b00592b9c013-start-response-export
128
+ */
129
+ declare const startResponseExport: ActionFactory<ExportResponsesOptions, {
130
+ progressId: string;
131
+ percentComplete: number;
132
+ status: string;
133
+ continuationToken?: string;
134
+ }>;
135
+
136
+ /**
137
+ * Test connection to Qualtrics API
138
+ */
139
+ declare const testConnection: (connectionOptions: ConnectionOptions) => (bearerToken?: string) => Promise<Result<string>>;
34
140
 
35
- export { type Action, type ActionFactory, type Connection, type ConnectionFactory, type ConnectionOptions, type Failure, type Result, type Success, createConnection, getBearerToken, testConnection };
141
+ 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("Connection successful") : 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;
@@ -75,8 +149,10 @@ async function execute(config) {
75
149
  signal: controller.signal
76
150
  };
77
151
  if (body) {
78
- if (typeof body === "string")
152
+ if (typeof body === "string") {
79
153
  headers.append("Content-Type", "application/json");
154
+ headers.append("Accept", "application/json");
155
+ }
80
156
  options.method = "POST";
81
157
  options.headers = headers;
82
158
  options.body = body;
@@ -88,13 +164,13 @@ async function execute(config) {
88
164
  const data = await response.json();
89
165
  return Promise.resolve(data);
90
166
  } else {
91
- const message = await response.text();
167
+ const message = await response.json();
92
168
  return Promise.reject(`${response.status}: ${message}`);
93
169
  }
94
170
  }
95
171
 
96
172
  // src/methods/getBearerToken.ts
97
- var getBearerToken = (connectionOptions) => async ({ clientId, clientSecret }) => {
173
+ var getBearerToken = (connectionOptions) => async ({ clientId, clientSecret, scope }) => {
98
174
  const route = `/oauth2/token`;
99
175
  try {
100
176
  const { datacenterId, timeout } = connectionOptions;
@@ -103,7 +179,7 @@ var getBearerToken = (connectionOptions) => async ({ clientId, clientSecret }) =
103
179
  });
104
180
  const body = new FormData();
105
181
  body.append("grant_type", "client_credentials");
106
- body.append("scope", "manage:all");
182
+ body.append("scope", scope);
107
183
  const response = await execute({
108
184
  datacenterId,
109
185
  route,
@@ -119,6 +195,82 @@ var getBearerToken = (connectionOptions) => async ({ clientId, clientSecret }) =
119
195
  }
120
196
  };
121
197
 
198
+ // src/methods/getResponseExportProgress.ts
199
+ var getResponseExportProgress = (connectionOptions) => async ({ exportProgressId, surveyId, bearerToken }) => {
200
+ const route = `/surveys/${surveyId}/export-responses/${exportProgressId}`;
201
+ try {
202
+ const headers = getAuthHeaders(connectionOptions.apiToken, bearerToken);
203
+ const config = {
204
+ datacenterId: connectionOptions.datacenterId,
205
+ route,
206
+ headers,
207
+ timeout: connectionOptions.timeout
208
+ };
209
+ const response = await execute(config);
210
+ const result = safeParseFileProgressResponse(response);
211
+ return result;
212
+ } catch (error) {
213
+ const message = getErrorMessage(error);
214
+ return failure(message);
215
+ }
216
+ };
217
+
218
+ // src/methods/getResponseExportFile.ts
219
+ var getResponseExportFile = (connectionOptions) => async ({ surveyId, fileId, bearerToken }) => {
220
+ const route = `/surveys/${surveyId}/export-responses/${fileId}/file`;
221
+ try {
222
+ const headers = getAuthHeaders(connectionOptions.apiToken, bearerToken);
223
+ const config = {
224
+ datacenterId: connectionOptions.datacenterId,
225
+ route,
226
+ headers,
227
+ timeout: connectionOptions.timeout
228
+ };
229
+ const response = await execute(config);
230
+ const result = safeParseFileResponse(response);
231
+ return result;
232
+ } catch (error) {
233
+ const message = getErrorMessage(error);
234
+ return failure(message);
235
+ }
236
+ };
237
+
238
+ // src/methods/startResponseExport.ts
239
+ var startResponseExport = (connectionOptions) => async ({
240
+ surveyId,
241
+ startDate,
242
+ endDate,
243
+ format = "csv",
244
+ bearerToken = void 0,
245
+ ...rest
246
+ }) => {
247
+ const route = `/surveys/${surveyId}/export-responses`;
248
+ try {
249
+ const headers = getAuthHeaders(connectionOptions.apiToken, bearerToken);
250
+ const body = JSON.stringify({
251
+ startDate: "2024-03-01T06:00:00Z",
252
+ //startDate.toISOString(),
253
+ endDate: "2024-03-05T06:00:00Z",
254
+ //endDate.toISOString(),
255
+ format,
256
+ ...rest
257
+ });
258
+ const config = {
259
+ datacenterId: connectionOptions.datacenterId,
260
+ route,
261
+ headers,
262
+ body,
263
+ timeout: connectionOptions.timeout
264
+ };
265
+ const response = await execute(config);
266
+ const result = safeParseStartFileExportResponse(response);
267
+ return result;
268
+ } catch (error) {
269
+ const message = getErrorMessage(error);
270
+ return failure(message);
271
+ }
272
+ };
273
+
122
274
  // src/methods/testConnection.ts
123
275
  var testConnection = (connectionOptions) => async (bearerToken) => {
124
276
  const route = "/API/v3/whoami";
@@ -141,12 +293,20 @@ var testConnection = (connectionOptions) => async (bearerToken) => {
141
293
 
142
294
  // src/createConnection.ts
143
295
  var createConnection = (options) => ({
144
- testConnection: testConnection(options),
145
- getBearerToken: getBearerToken(options)
296
+ exportResponses: exportResponses(options),
297
+ getBearerToken: getBearerToken(options),
298
+ getResponseExportFile: getResponseExportFile(options),
299
+ getResponseExportProgress: getResponseExportProgress(options),
300
+ startResponseExport: startResponseExport(options),
301
+ testConnection: testConnection(options)
146
302
  });
147
303
  // Annotate the CommonJS export names for ESM import in node:
148
304
  0 && (module.exports = {
149
305
  createConnection,
306
+ exportResponses,
150
307
  getBearerToken,
308
+ getResponseExportFile,
309
+ getResponseExportProgress,
310
+ startResponseExport,
151
311
  testConnection
152
312
  });
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("Connection successful") : 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;
@@ -47,8 +117,10 @@ async function execute(config) {
47
117
  signal: controller.signal
48
118
  };
49
119
  if (body) {
50
- if (typeof body === "string")
120
+ if (typeof body === "string") {
51
121
  headers.append("Content-Type", "application/json");
122
+ headers.append("Accept", "application/json");
123
+ }
52
124
  options.method = "POST";
53
125
  options.headers = headers;
54
126
  options.body = body;
@@ -60,13 +132,13 @@ async function execute(config) {
60
132
  const data = await response.json();
61
133
  return Promise.resolve(data);
62
134
  } else {
63
- const message = await response.text();
135
+ const message = await response.json();
64
136
  return Promise.reject(`${response.status}: ${message}`);
65
137
  }
66
138
  }
67
139
 
68
140
  // src/methods/getBearerToken.ts
69
- var getBearerToken = (connectionOptions) => async ({ clientId, clientSecret }) => {
141
+ var getBearerToken = (connectionOptions) => async ({ clientId, clientSecret, scope }) => {
70
142
  const route = `/oauth2/token`;
71
143
  try {
72
144
  const { datacenterId, timeout } = connectionOptions;
@@ -75,7 +147,7 @@ var getBearerToken = (connectionOptions) => async ({ clientId, clientSecret }) =
75
147
  });
76
148
  const body = new FormData();
77
149
  body.append("grant_type", "client_credentials");
78
- body.append("scope", "manage:all");
150
+ body.append("scope", scope);
79
151
  const response = await execute({
80
152
  datacenterId,
81
153
  route,
@@ -91,6 +163,82 @@ var getBearerToken = (connectionOptions) => async ({ clientId, clientSecret }) =
91
163
  }
92
164
  };
93
165
 
166
+ // src/methods/getResponseExportProgress.ts
167
+ var getResponseExportProgress = (connectionOptions) => async ({ exportProgressId, surveyId, bearerToken }) => {
168
+ const route = `/surveys/${surveyId}/export-responses/${exportProgressId}`;
169
+ try {
170
+ const headers = getAuthHeaders(connectionOptions.apiToken, bearerToken);
171
+ const config = {
172
+ datacenterId: connectionOptions.datacenterId,
173
+ route,
174
+ headers,
175
+ timeout: connectionOptions.timeout
176
+ };
177
+ const response = await execute(config);
178
+ const result = safeParseFileProgressResponse(response);
179
+ return result;
180
+ } catch (error) {
181
+ const message = getErrorMessage(error);
182
+ return failure(message);
183
+ }
184
+ };
185
+
186
+ // src/methods/getResponseExportFile.ts
187
+ var getResponseExportFile = (connectionOptions) => async ({ surveyId, fileId, bearerToken }) => {
188
+ const route = `/surveys/${surveyId}/export-responses/${fileId}/file`;
189
+ try {
190
+ const headers = getAuthHeaders(connectionOptions.apiToken, bearerToken);
191
+ const config = {
192
+ datacenterId: connectionOptions.datacenterId,
193
+ route,
194
+ headers,
195
+ timeout: connectionOptions.timeout
196
+ };
197
+ const response = await execute(config);
198
+ const result = safeParseFileResponse(response);
199
+ return result;
200
+ } catch (error) {
201
+ const message = getErrorMessage(error);
202
+ return failure(message);
203
+ }
204
+ };
205
+
206
+ // src/methods/startResponseExport.ts
207
+ var startResponseExport = (connectionOptions) => async ({
208
+ surveyId,
209
+ startDate,
210
+ endDate,
211
+ format = "csv",
212
+ bearerToken = void 0,
213
+ ...rest
214
+ }) => {
215
+ const route = `/surveys/${surveyId}/export-responses`;
216
+ try {
217
+ const headers = getAuthHeaders(connectionOptions.apiToken, bearerToken);
218
+ const body = JSON.stringify({
219
+ startDate: "2024-03-01T06:00:00Z",
220
+ //startDate.toISOString(),
221
+ endDate: "2024-03-05T06:00:00Z",
222
+ //endDate.toISOString(),
223
+ format,
224
+ ...rest
225
+ });
226
+ const config = {
227
+ datacenterId: connectionOptions.datacenterId,
228
+ route,
229
+ headers,
230
+ body,
231
+ timeout: connectionOptions.timeout
232
+ };
233
+ const response = await execute(config);
234
+ const result = safeParseStartFileExportResponse(response);
235
+ return result;
236
+ } catch (error) {
237
+ const message = getErrorMessage(error);
238
+ return failure(message);
239
+ }
240
+ };
241
+
94
242
  // src/methods/testConnection.ts
95
243
  var testConnection = (connectionOptions) => async (bearerToken) => {
96
244
  const route = "/API/v3/whoami";
@@ -113,11 +261,19 @@ var testConnection = (connectionOptions) => async (bearerToken) => {
113
261
 
114
262
  // src/createConnection.ts
115
263
  var createConnection = (options) => ({
116
- testConnection: testConnection(options),
117
- getBearerToken: getBearerToken(options)
264
+ exportResponses: exportResponses(options),
265
+ getBearerToken: getBearerToken(options),
266
+ getResponseExportFile: getResponseExportFile(options),
267
+ getResponseExportProgress: getResponseExportProgress(options),
268
+ startResponseExport: startResponseExport(options),
269
+ testConnection: testConnection(options)
118
270
  });
119
271
  export {
120
272
  createConnection,
273
+ exportResponses,
121
274
  getBearerToken,
275
+ getResponseExportFile,
276
+ getResponseExportProgress,
277
+ startResponseExport,
122
278
  testConnection
123
279
  };
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.1",
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: {
@@ -38,7 +126,7 @@ describe('safeParseTestResponse', () => {
38
126
 
39
127
  it('should pass with correct data', () => {
40
128
  const res = fixture
41
- const expected = success(undefined)
129
+ const expected = success('Connection successful')
42
130
  const parsed = safeParseTestResponse(res)
43
131
  expect(parsed).toStrictEqual(expected)
44
132
  })
@@ -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
+ })