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

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.0.5
4
+
5
+ ### Patch Changes
6
+
7
+ - df0e350: Implemented testConnection and getBearerToken methods
8
+
3
9
  ## 0.0.4
4
10
 
5
11
  ### Patch Changes
package/README.md CHANGED
@@ -1,2 +1,199 @@
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
+ #### getBearerToken()
143
+
144
+ Implements [OAuth Authentication (Client Credentials)](https://api.qualtrics.com/9398592961ced-o-auth-authentication-client-credentials)
145
+
146
+ `options`
147
+
148
+ | Property | Type | Description | Required |
149
+ | ------------ | ------ | ------------------------- | -------- |
150
+ | clientId | string | Quatrics Client ID | Y |
151
+ | clientSecret | string | Qualtrics Client Password | Y |
152
+
153
+ #### testConnection(options)
154
+
155
+ `options`
156
+
157
+ | Property | Type | Description | Required |
158
+ | ----------- | ------ | ------------------ | -------- |
159
+ | bearerToken | string | Valid Bearer Token | N |
160
+
161
+ ## Responses
162
+
163
+ Responses are provided based on the Result type. Success can be determined by checking the success property.
164
+
165
+ ```ts
166
+ export type Result<TResponse> = Success<TResponse> | Failure
167
+ ```
168
+
169
+ ### Success
170
+
171
+ Response is returned in the data property.
172
+
173
+ ```ts
174
+ type Success<TResponse> = { success: true; data: TResponse }
175
+ ```
176
+
177
+ ### Failure
178
+
179
+ Errors are returned in the error property.
180
+
181
+ ```ts
182
+ type Failure = { success: false; error: string }
183
+ ```
184
+
185
+ ## Contributing
186
+
187
+ This is a pet project to save me time at work. It is still under development and you should use at your own risk.
188
+
189
+ ## Versioning
190
+
191
+ 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).
192
+
193
+ ## Authors
194
+
195
+ - **B Jones RFD** - _Package Noob_ - [B-Jones-RFD](https://github.com/B-Jones-RFD)
196
+
197
+ ## License
198
+
199
+ [MIT License](https://github.com/B-Jones-RFD/sp-rest-connect/blob/main/LICENSE)
package/dist/index.d.mts CHANGED
@@ -1,3 +1,35 @@
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 Action<TConfig, TResponse> = (options: TConfig) => Promise<Result<TResponse>>;
11
+ type ConnectionOptions = {
12
+ datacenterId: string;
13
+ apiToken?: string;
14
+ timeout?: number;
15
+ };
16
+ type ActionFactory<TParams, TResponse> = (options: ConnectionOptions) => Action<TParams, TResponse>;
17
+ type Connection = {
18
+ testConnection: Action<string | undefined, void>;
19
+ getBearerToken: Action<{
20
+ clientId: string;
21
+ clientSecret: string;
22
+ }, string>;
23
+ };
24
+ type ConnectionFactory = (options: ConnectionOptions) => Connection;
2
25
 
3
- export { hello };
26
+ declare const createConnection: ConnectionFactory;
27
+
28
+ declare const getBearerToken: ActionFactory<{
29
+ clientId: string;
30
+ clientSecret: string;
31
+ }, string>;
32
+
33
+ declare const testConnection: ActionFactory<string | undefined, void>;
34
+
35
+ export { type Action, type ActionFactory, type Connection, type ConnectionFactory, type ConnectionOptions, type Failure, type Result, type Success, createConnection, getBearerToken, testConnection };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,35 @@
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 Action<TConfig, TResponse> = (options: TConfig) => Promise<Result<TResponse>>;
11
+ type ConnectionOptions = {
12
+ datacenterId: string;
13
+ apiToken?: string;
14
+ timeout?: number;
15
+ };
16
+ type ActionFactory<TParams, TResponse> = (options: ConnectionOptions) => Action<TParams, TResponse>;
17
+ type Connection = {
18
+ testConnection: Action<string | undefined, void>;
19
+ getBearerToken: Action<{
20
+ clientId: string;
21
+ clientSecret: string;
22
+ }, string>;
23
+ };
24
+ type ConnectionFactory = (options: ConnectionOptions) => Connection;
2
25
 
3
- export { hello };
26
+ declare const createConnection: ConnectionFactory;
27
+
28
+ declare const getBearerToken: ActionFactory<{
29
+ clientId: string;
30
+ clientSecret: string;
31
+ }, string>;
32
+
33
+ declare const testConnection: ActionFactory<string | undefined, void>;
34
+
35
+ export { type Action, type ActionFactory, type Connection, type ConnectionFactory, type ConnectionOptions, type Failure, type Result, type Success, createConnection, getBearerToken, testConnection };
package/dist/index.js CHANGED
@@ -20,13 +20,133 @@ 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
+ getBearerToken: () => getBearerToken,
25
+ testConnection: () => testConnection
24
26
  });
25
27
  module.exports = __toCommonJS(src_exports);
26
- var hello = () => {
27
- console.log("Nothing to see here!");
28
+
29
+ // src/utils.ts
30
+ function success(data) {
31
+ return {
32
+ success: true,
33
+ data
34
+ };
35
+ }
36
+ function failure(error) {
37
+ return {
38
+ success: false,
39
+ error
40
+ };
41
+ }
42
+ function getErrorMessage(error) {
43
+ return error ? typeof error === "object" && "message" in error && typeof error.message === "string" ? error.message : error.toString() : "An error occured";
44
+ }
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
+ function getAuthHeaders(apiToken, bearerToken) {
52
+ if (apiToken)
53
+ return new Headers({
54
+ "X-API-TOKEN": apiToken
55
+ });
56
+ if (bearerToken) {
57
+ return new Headers({
58
+ Authorization: bearerToken
59
+ });
60
+ }
61
+ throw new Error(
62
+ "Unable to authorize request. Bearer Token or Api Token required."
63
+ );
64
+ }
65
+
66
+ // src/qualtrics/index.ts
67
+ async function execute(config) {
68
+ const { datacenterId, route, headers, timeout, body } = config;
69
+ const host = `https://${datacenterId}.qualtrics.com`;
70
+ const resource = new URL(route, host);
71
+ const controller = new AbortController();
72
+ const options = {
73
+ method: "GET",
74
+ headers,
75
+ signal: controller.signal
76
+ };
77
+ if (body) {
78
+ if (typeof body === "string")
79
+ headers.append("Content-Type", "application/json");
80
+ options.method = "POST";
81
+ options.headers = headers;
82
+ options.body = body;
83
+ }
84
+ const timer = setTimeout(() => controller.abort(), timeout ?? 30 * 1e3);
85
+ const response = await fetch(resource, options);
86
+ clearTimeout(timer);
87
+ if (response.ok) {
88
+ const data = await response.json();
89
+ return Promise.resolve(data);
90
+ } else {
91
+ const message = await response.text();
92
+ return Promise.reject(`${response.status}: ${message}`);
93
+ }
94
+ }
95
+
96
+ // src/methods/getBearerToken.ts
97
+ var getBearerToken = (connectionOptions) => async ({ clientId, clientSecret }) => {
98
+ const route = `/oauth2/token`;
99
+ try {
100
+ const { datacenterId, timeout } = connectionOptions;
101
+ const headers = new Headers({
102
+ Authorization: "Basic " + Buffer.from(`${clientId}:${clientSecret}`).toString("base64")
103
+ });
104
+ const body = new FormData();
105
+ body.append("grant_type", "client_credentials");
106
+ body.append("scope", "manage:all");
107
+ const response = await execute({
108
+ datacenterId,
109
+ route,
110
+ headers,
111
+ body,
112
+ timeout
113
+ });
114
+ const result = safeParseBearerToken(response);
115
+ return result;
116
+ } catch (error) {
117
+ const message = getErrorMessage(error);
118
+ return failure(message);
119
+ }
120
+ };
121
+
122
+ // src/methods/testConnection.ts
123
+ var testConnection = (connectionOptions) => async (bearerToken) => {
124
+ const route = "/API/v3/whoami";
125
+ try {
126
+ const headers = getAuthHeaders(connectionOptions.apiToken, bearerToken);
127
+ const config = {
128
+ datacenterId: connectionOptions.datacenterId,
129
+ route,
130
+ headers,
131
+ timeout: connectionOptions.timeout
132
+ };
133
+ const response = await execute(config);
134
+ const result = safeParseTestResponse(response);
135
+ return result;
136
+ } catch (error) {
137
+ const message = getErrorMessage(error);
138
+ return failure(message);
139
+ }
28
140
  };
141
+
142
+ // src/createConnection.ts
143
+ var createConnection = (options) => ({
144
+ testConnection: testConnection(options),
145
+ getBearerToken: getBearerToken(options)
146
+ });
29
147
  // Annotate the CommonJS export names for ESM import in node:
30
148
  0 && (module.exports = {
31
- hello
149
+ createConnection,
150
+ getBearerToken,
151
+ testConnection
32
152
  });
package/dist/index.mjs CHANGED
@@ -1,7 +1,123 @@
1
- // src/index.ts
2
- var hello = () => {
3
- console.log("Nothing to see here!");
1
+ // src/utils.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 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
+ function getAuthHeaders(apiToken, bearerToken) {
24
+ if (apiToken)
25
+ return new Headers({
26
+ "X-API-TOKEN": apiToken
27
+ });
28
+ if (bearerToken) {
29
+ return new Headers({
30
+ Authorization: bearerToken
31
+ });
32
+ }
33
+ throw new Error(
34
+ "Unable to authorize request. Bearer Token or Api Token required."
35
+ );
36
+ }
37
+
38
+ // src/qualtrics/index.ts
39
+ async function execute(config) {
40
+ const { datacenterId, route, headers, timeout, body } = config;
41
+ const host = `https://${datacenterId}.qualtrics.com`;
42
+ const resource = new URL(route, host);
43
+ const controller = new AbortController();
44
+ const options = {
45
+ method: "GET",
46
+ headers,
47
+ signal: controller.signal
48
+ };
49
+ if (body) {
50
+ if (typeof body === "string")
51
+ headers.append("Content-Type", "application/json");
52
+ options.method = "POST";
53
+ options.headers = headers;
54
+ options.body = body;
55
+ }
56
+ const timer = setTimeout(() => controller.abort(), timeout ?? 30 * 1e3);
57
+ const response = await fetch(resource, options);
58
+ clearTimeout(timer);
59
+ if (response.ok) {
60
+ const data = await response.json();
61
+ return Promise.resolve(data);
62
+ } else {
63
+ const message = await response.text();
64
+ return Promise.reject(`${response.status}: ${message}`);
65
+ }
66
+ }
67
+
68
+ // src/methods/getBearerToken.ts
69
+ var getBearerToken = (connectionOptions) => async ({ clientId, clientSecret }) => {
70
+ const route = `/oauth2/token`;
71
+ try {
72
+ const { datacenterId, timeout } = connectionOptions;
73
+ const headers = new Headers({
74
+ Authorization: "Basic " + Buffer.from(`${clientId}:${clientSecret}`).toString("base64")
75
+ });
76
+ const body = new FormData();
77
+ body.append("grant_type", "client_credentials");
78
+ body.append("scope", "manage:all");
79
+ const response = await execute({
80
+ datacenterId,
81
+ route,
82
+ headers,
83
+ body,
84
+ timeout
85
+ });
86
+ const result = safeParseBearerToken(response);
87
+ return result;
88
+ } catch (error) {
89
+ const message = getErrorMessage(error);
90
+ return failure(message);
91
+ }
4
92
  };
93
+
94
+ // src/methods/testConnection.ts
95
+ var testConnection = (connectionOptions) => async (bearerToken) => {
96
+ const route = "/API/v3/whoami";
97
+ try {
98
+ const headers = getAuthHeaders(connectionOptions.apiToken, bearerToken);
99
+ const config = {
100
+ datacenterId: connectionOptions.datacenterId,
101
+ route,
102
+ headers,
103
+ timeout: connectionOptions.timeout
104
+ };
105
+ const response = await execute(config);
106
+ const result = safeParseTestResponse(response);
107
+ return result;
108
+ } catch (error) {
109
+ const message = getErrorMessage(error);
110
+ return failure(message);
111
+ }
112
+ };
113
+
114
+ // src/createConnection.ts
115
+ var createConnection = (options) => ({
116
+ testConnection: testConnection(options),
117
+ getBearerToken: getBearerToken(options)
118
+ });
5
119
  export {
6
- hello
120
+ createConnection,
121
+ getBearerToken,
122
+ testConnection
7
123
  };
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.0.5",
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,54 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import {
3
+ success,
4
+ failure,
5
+ safeParseBearerToken,
6
+ safeParseTestResponse,
7
+ } from '../src/utils'
8
+
9
+ describe('safeParseBearerToken', () => {
10
+ const fixture = {
11
+ access_token: 'sometoken',
12
+ token_type: 'Bearer',
13
+ }
14
+
15
+ it('should pass with correct data', () => {
16
+ const res = fixture
17
+ const expected = success(fixture.access_token)
18
+ const parsed = safeParseBearerToken(res)
19
+ expect(parsed).toStrictEqual(expected)
20
+ })
21
+
22
+ it('should fail when token is incorrect type', () => {
23
+ const res = {
24
+ result: 'bad result',
25
+ }
26
+ const expected = failure('Incorrect token format')
27
+ const parsed = safeParseBearerToken(res)
28
+ expect(parsed).toStrictEqual(expected)
29
+ })
30
+ })
31
+
32
+ describe('safeParseTestResponse', () => {
33
+ const fixture = {
34
+ result: {
35
+ userId: 'testUser',
36
+ },
37
+ }
38
+
39
+ it('should pass with correct data', () => {
40
+ const res = fixture
41
+ const expected = success(undefined)
42
+ const parsed = safeParseTestResponse(res)
43
+ expect(parsed).toStrictEqual(expected)
44
+ })
45
+
46
+ it('should fail when response is incorrect type', () => {
47
+ const res = {
48
+ result: 'bad result',
49
+ }
50
+ const expected = failure('Incorrect test response format')
51
+ const parsed = safeParseTestResponse(res)
52
+ expect(parsed).toStrictEqual(expected)
53
+ })
54
+ })