@outseta/api-client 0.2.0 → 0.2.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/dist/index.js +17 -19
- package/package.json +3 -6
- package/src/__tests__/accounts-crud.integration.test.ts +41 -39
- package/src/__tests__/client.test.ts +3 -3
- package/src/__tests__/people.integration.test.ts +28 -32
- package/src/__tests__/setup.ts +5 -6
- package/src/__tests__/support.integration.test.ts +28 -35
- package/src/client.ts +38 -23
- package/src/generated/activity/activity.ts +335 -0
- package/src/generated/billing/billing.ts +1006 -0
- package/src/generated/crm/crm.ts +1143 -1
- package/src/generated/email/email.ts +798 -0
- package/src/generated/models/accountAddPersonToAccountBody.ts +227 -0
- package/src/generated/models/accountCancelAccountBody.ts +227 -0
- package/src/generated/models/accountCancelationAllOf.ts +2 -0
- package/src/generated/models/accountExtendTrialBody.ts +227 -0
- package/src/generated/models/accountStage.ts +2 -1
- package/src/generated/models/accountUpdateAccountBody.ts +227 -0
- package/src/generated/models/accountUpdateMembershipBody.ts +227 -0
- package/src/generated/models/activityAddCustomActivityBody.ts +227 -0
- package/src/generated/models/activityGetAllParams.ts +241 -0
- package/src/generated/models/articleGetAllArticlesParams.ts +228 -0
- package/src/generated/models/backGroundTaskType.ts +3 -1
- package/src/generated/models/broadcastCampaignAllOf.ts +4 -0
- package/src/generated/models/broadcastCampaignStatus.ts +2 -1
- package/src/generated/models/campaignAddBroadcastEmailBody.ts +227 -0
- package/src/generated/models/campaignSendTestCampaignEmailBody.ts +227 -0
- package/src/generated/models/campaignUpdateBroadcastEmailBody.ts +227 -0
- package/src/generated/models/cancelationStatus.ts +236 -0
- package/src/generated/models/caseAddCaseBody.ts +227 -0
- package/src/generated/models/caseAddCaseParams.ts +229 -0
- package/src/generated/models/crmSettingsAllOf.ts +2 -0
- package/src/generated/models/dealAddDealBody.ts +227 -0
- package/src/generated/models/dealGetAllDealsParams.ts +234 -0
- package/src/generated/models/dealUpdateDealBody.ts +227 -0
- package/src/generated/models/discountCouponAddDiscountCouponBody.ts +227 -0
- package/src/generated/models/dnsEntry.ts +2 -0
- package/src/generated/models/emailListAddSubscriptionBody.ts +227 -0
- package/src/generated/models/emailListGetAllSubscriptionsParams.ts +229 -0
- package/src/generated/models/extendSubscriptionOptions.ts +225 -0
- package/src/generated/models/index.ts +40 -0
- package/src/generated/models/invoiceAddInvoiceBody.ts +227 -0
- package/src/generated/models/invoiceAllOf.ts +8 -0
- package/src/generated/models/messageAllOf.ts +1 -0
- package/src/generated/models/paymentInformationSavePaymentInformationBody.ts +227 -0
- package/src/generated/models/personAddPersonBody.ts +227 -0
- package/src/generated/models/personAllOf.ts +3 -3
- package/src/generated/models/personForgotPasswordBody.ts +227 -0
- package/src/generated/models/personGetAllPeopleParams.ts +229 -0
- package/src/generated/models/personSetTemporaryPasswordBody.ts +227 -0
- package/src/generated/models/personUpdatePersonBody.ts +227 -0
- package/src/generated/models/qcountConfigSettingType.ts +3 -1
- package/src/generated/models/resendEmailsRequest.ts +226 -0
- package/src/generated/models/sendGridDomainAuthenticationAllOf.ts +1 -0
- package/src/generated/models/setupIntent.ts +2 -0
- package/src/generated/models/stripeCreditNoteAllOf.ts +9 -0
- package/src/generated/models/stripeInvoiceAllOf.ts +9 -0
- package/src/generated/models/stripePriceAllOf.ts +2 -0
- package/src/generated/models/stripeProductAllOf.ts +5 -0
- package/src/generated/models/stripeSubscriptionAllOf.ts +4 -0
- package/src/generated/models/subscriptionAddOnAddSubscriptionAddOnBody.ts +227 -0
- package/src/generated/models/subscriptionChangeSubscriptionBody.ts +227 -0
- package/src/generated/models/subscriptionChangeSubscriptionParams.ts +229 -0
- package/src/generated/models/subscriptionChangeSubscriptionPreviewBody.ts +227 -0
- package/src/generated/models/subscriptionChangeSubscriptionPreviewParams.ts +229 -0
- package/src/generated/models/subscriptionFirstTimeSubscriptionBody.ts +227 -0
- package/src/generated/models/subscriptionFirstTimeSubscriptionPreviewBody.ts +227 -0
- package/src/generated/models/subscriptionFirstTimeSubscriptionPreviewParams.ts +229 -0
- package/src/generated/models/subscriptionSetSubscriptionUpgradeRequiredBody.ts +227 -0
- package/src/generated/models/transactionsAddPaymentTransactionBody.ts +227 -0
- package/src/generated/models/usageAddUsageBody.ts +227 -0
- package/src/generated/support/support.ts +341 -1
- package/src/index.ts +2 -0
- package/src/generated/zod.ts +0 -191087
package/dist/index.js
CHANGED
|
@@ -1,35 +1,33 @@
|
|
|
1
1
|
// src/client.ts
|
|
2
|
-
import ky, { HTTPError } from "ky";
|
|
3
2
|
function getAuthorization(creds) {
|
|
4
3
|
if ("accessToken" in creds) {
|
|
5
4
|
return `Bearer ${creds.accessToken}`;
|
|
6
5
|
}
|
|
7
6
|
return `Outseta ${creds.apiKey}:${creds.apiSecret}`;
|
|
8
7
|
}
|
|
8
|
+
function getRequestUrl(subdomain, path) {
|
|
9
|
+
if (/^https?:\/\//.test(path)) {
|
|
10
|
+
return path;
|
|
11
|
+
}
|
|
12
|
+
const normalizedPath = path.startsWith("/api/v1/") ? path : `/api/v1/${path.replace(/^\/+/, "")}`;
|
|
13
|
+
return new URL(normalizedPath, `https://${subdomain}.outseta.com`).toString();
|
|
14
|
+
}
|
|
9
15
|
function createClient(creds) {
|
|
10
|
-
return
|
|
11
|
-
|
|
12
|
-
headers
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
return (path, init = {}) => {
|
|
17
|
+
const headers = new Headers(init.headers);
|
|
18
|
+
headers.set("Authorization", getAuthorization(creds));
|
|
19
|
+
return fetch(getRequestUrl(creds.subdomain, path), {
|
|
20
|
+
...init,
|
|
21
|
+
headers
|
|
22
|
+
});
|
|
23
|
+
};
|
|
17
24
|
}
|
|
18
25
|
var customFetch = async (url, options) => {
|
|
19
26
|
const { client, ...init } = options;
|
|
20
27
|
if (!client) {
|
|
21
|
-
throw new Error("@outseta/api-client: No client provided. Pass a
|
|
22
|
-
}
|
|
23
|
-
let response;
|
|
24
|
-
try {
|
|
25
|
-
response = await client(url.replace(/^\/api\/v1\//, ""), init);
|
|
26
|
-
} catch (error) {
|
|
27
|
-
if (error instanceof HTTPError) {
|
|
28
|
-
response = error.response;
|
|
29
|
-
} else {
|
|
30
|
-
throw error;
|
|
31
|
-
}
|
|
28
|
+
throw new Error("@outseta/api-client: No client provided. Pass a client from createClient() via the `client` option.");
|
|
32
29
|
}
|
|
30
|
+
const response = await client(url, init);
|
|
33
31
|
const data = response.headers.get("content-type")?.includes("application/json") ? await response.json() : undefined;
|
|
34
32
|
return { data, status: response.status, headers: response.headers };
|
|
35
33
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@outseta/api-client",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Generated API client for the Outseta REST API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -18,15 +18,12 @@
|
|
|
18
18
|
}
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
|
-
"build": "bun build src/index.ts --outdir dist --target node --format esm --packages external",
|
|
21
|
+
"build": "rimraf dist && bun build src/index.ts --outdir dist --target node --format esm --packages external",
|
|
22
22
|
"typecheck": "tsc --noEmit",
|
|
23
23
|
"lint": "eslint src --ignore-pattern generated",
|
|
24
24
|
"test": "bun test --env-file ../../.env.test"
|
|
25
25
|
},
|
|
26
|
-
"dependencies": {
|
|
27
|
-
"ky": "^1.7.0",
|
|
28
|
-
"zod": "^3.24.0"
|
|
29
|
-
},
|
|
26
|
+
"dependencies": {},
|
|
30
27
|
"devDependencies": {
|
|
31
28
|
"@types/bun": "^1.3.10",
|
|
32
29
|
"typescript": "^5.7.0"
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { describe, expect, test, afterAll } from "bun:test";
|
|
2
|
-
import { hasCredentials, getTestClient } from "./setup.js";
|
|
2
|
+
import { hasCredentials, getTestClient, opts, type PaginatedResponse } from "./setup.js";
|
|
3
|
+
import type { Account, AccountAddAccountBody, AccountUpdateAccountBody, Person } from "../generated/models/index.js";
|
|
4
|
+
import {
|
|
5
|
+
accountAddAccount,
|
|
6
|
+
accountGetAccount,
|
|
7
|
+
accountUpdateAccount,
|
|
8
|
+
accountDeleteAccount,
|
|
9
|
+
personGetAllPeople,
|
|
10
|
+
personDeletePerson,
|
|
11
|
+
} from "../generated/crm/crm.js";
|
|
3
12
|
|
|
4
13
|
const emailsToCleanup: string[] = [];
|
|
5
14
|
const accountUidsToCleanup: string[] = [];
|
|
@@ -10,17 +19,17 @@ describe.skipIf(!hasCredentials)("Accounts CRUD (live)", () => {
|
|
|
10
19
|
|
|
11
20
|
// Delete any accounts that weren't cleaned up by the test
|
|
12
21
|
for (const uid of accountUidsToCleanup) {
|
|
13
|
-
await
|
|
22
|
+
await accountDeleteAccount(uid, opts(client));
|
|
14
23
|
}
|
|
15
24
|
|
|
16
|
-
// Delete orphaned people by email
|
|
25
|
+
// Delete orphaned people by email
|
|
17
26
|
if (emailsToCleanup.length > 0) {
|
|
18
|
-
const res = await
|
|
19
|
-
if (res.
|
|
20
|
-
const body =
|
|
27
|
+
const res = await personGetAllPeople(undefined, opts(client));
|
|
28
|
+
if (res.status === 200) {
|
|
29
|
+
const body = res.data as unknown as PaginatedResponse<Person>;
|
|
21
30
|
for (const person of body.items) {
|
|
22
|
-
if (emailsToCleanup.includes(person.Email)) {
|
|
23
|
-
await
|
|
31
|
+
if (person.Email && emailsToCleanup.includes(person.Email)) {
|
|
32
|
+
await personDeletePerson(person.Uid!, opts(client));
|
|
24
33
|
}
|
|
25
34
|
}
|
|
26
35
|
}
|
|
@@ -33,9 +42,8 @@ describe.skipIf(!hasCredentials)("Accounts CRUD (live)", () => {
|
|
|
33
42
|
emailsToCleanup.push(email);
|
|
34
43
|
|
|
35
44
|
// CREATE
|
|
36
|
-
const createRes = await
|
|
37
|
-
|
|
38
|
-
json: {
|
|
45
|
+
const createRes = await accountAddAccount(
|
|
46
|
+
{
|
|
39
47
|
Name: "Lifecycle Test Corp",
|
|
40
48
|
PersonAccount: [
|
|
41
49
|
{
|
|
@@ -43,55 +51,49 @@ describe.skipIf(!hasCredentials)("Accounts CRUD (live)", () => {
|
|
|
43
51
|
IsPrimary: true,
|
|
44
52
|
},
|
|
45
53
|
],
|
|
46
|
-
},
|
|
47
|
-
|
|
48
|
-
|
|
54
|
+
} as AccountAddAccountBody,
|
|
55
|
+
undefined,
|
|
56
|
+
opts(client),
|
|
57
|
+
);
|
|
49
58
|
expect(createRes.status).toBe(200);
|
|
50
|
-
const created =
|
|
59
|
+
const created = createRes.data as unknown as Account;
|
|
51
60
|
expect(created.Uid).toBeDefined();
|
|
52
61
|
expect(created.Name).toBe("Lifecycle Test Corp");
|
|
53
|
-
accountUidsToCleanup.push(created.Uid);
|
|
62
|
+
accountUidsToCleanup.push(created.Uid!);
|
|
54
63
|
|
|
55
64
|
// READ
|
|
56
|
-
const getRes = await
|
|
57
|
-
throwHttpErrors: false,
|
|
58
|
-
});
|
|
65
|
+
const getRes = await accountGetAccount(created.Uid!, opts(client));
|
|
59
66
|
expect(getRes.status).toBe(200);
|
|
60
|
-
const fetched =
|
|
67
|
+
const fetched = getRes.data as unknown as Account;
|
|
61
68
|
expect(fetched.Uid).toBe(created.Uid);
|
|
62
69
|
|
|
63
70
|
// UPDATE
|
|
64
|
-
const updateRes = await
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
71
|
+
const updateRes = await accountUpdateAccount(
|
|
72
|
+
created.Uid!,
|
|
73
|
+
{ Name: "Lifecycle Test Corp (Updated)" } as AccountUpdateAccountBody,
|
|
74
|
+
opts(client),
|
|
75
|
+
);
|
|
69
76
|
expect(updateRes.status).toBe(200);
|
|
70
|
-
const updated =
|
|
77
|
+
const updated = updateRes.data as unknown as Account;
|
|
71
78
|
expect(updated.Name).toBe("Lifecycle Test Corp (Updated)");
|
|
72
79
|
|
|
73
80
|
// DELETE account
|
|
74
|
-
const deleteRes = await
|
|
75
|
-
method: "DELETE",
|
|
76
|
-
throwHttpErrors: false,
|
|
77
|
-
});
|
|
81
|
+
const deleteRes = await accountDeleteAccount(created.Uid!, opts(client));
|
|
78
82
|
expect(deleteRes.status).toBe(200);
|
|
79
83
|
|
|
80
84
|
// Verify deleted — should 404
|
|
81
|
-
const verifyRes = await
|
|
82
|
-
throwHttpErrors: false,
|
|
83
|
-
});
|
|
85
|
+
const verifyRes = await accountGetAccount(created.Uid!, opts(client));
|
|
84
86
|
expect(verifyRes.status).toBe(404);
|
|
85
87
|
});
|
|
86
88
|
|
|
87
89
|
test("create account without required person returns error", async () => {
|
|
88
90
|
const client = getTestClient();
|
|
89
91
|
|
|
90
|
-
const res = await
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
expect(res.
|
|
92
|
+
const res = await accountAddAccount(
|
|
93
|
+
{ Name: "No Person Corp" } as AccountAddAccountBody,
|
|
94
|
+
undefined,
|
|
95
|
+
opts(client),
|
|
96
|
+
);
|
|
97
|
+
expect(res.status).not.toBe(200);
|
|
96
98
|
});
|
|
97
99
|
});
|
|
@@ -3,7 +3,7 @@ import { createClient, customFetch } from "../client.js";
|
|
|
3
3
|
import { hasCredentials, getTestClient } from "./setup.js";
|
|
4
4
|
|
|
5
5
|
describe("createClient", () => {
|
|
6
|
-
test("returns a
|
|
6
|
+
test("returns a callable fetch client", () => {
|
|
7
7
|
const client = createClient({
|
|
8
8
|
subdomain: "test",
|
|
9
9
|
apiKey: "key",
|
|
@@ -43,7 +43,7 @@ describe("customFetch", () => {
|
|
|
43
43
|
describe.skipIf(!hasCredentials)("Auth (live)", () => {
|
|
44
44
|
test("valid credentials return 200", async () => {
|
|
45
45
|
const client = getTestClient();
|
|
46
|
-
const res = await client("crm/accounts"
|
|
46
|
+
const res = await client("crm/accounts");
|
|
47
47
|
expect(res.status).toBe(200);
|
|
48
48
|
});
|
|
49
49
|
|
|
@@ -53,7 +53,7 @@ describe.skipIf(!hasCredentials)("Auth (live)", () => {
|
|
|
53
53
|
apiKey: "bad-key",
|
|
54
54
|
apiSecret: "bad-secret",
|
|
55
55
|
});
|
|
56
|
-
const res = await badClient("crm/accounts"
|
|
56
|
+
const res = await badClient("crm/accounts");
|
|
57
57
|
expect(res.status).toBe(401);
|
|
58
58
|
});
|
|
59
59
|
});
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { describe, expect, test, afterAll } from "bun:test";
|
|
2
|
-
import { hasCredentials, getTestClient } from "./setup.js";
|
|
2
|
+
import { hasCredentials, getTestClient, opts, type PaginatedResponse } from "./setup.js";
|
|
3
|
+
import type { Person, PersonAddPersonBody, PersonUpdatePersonBody } from "../generated/models/index.js";
|
|
4
|
+
import {
|
|
5
|
+
personGetAllPeople,
|
|
6
|
+
personAddPerson,
|
|
7
|
+
personGetPerson,
|
|
8
|
+
personUpdatePerson,
|
|
9
|
+
personDeletePerson,
|
|
10
|
+
} from "../generated/crm/crm.js";
|
|
3
11
|
|
|
4
12
|
const peopleToCleanup: string[] = [];
|
|
5
13
|
|
|
@@ -7,16 +15,16 @@ describe.skipIf(!hasCredentials)("People (live)", () => {
|
|
|
7
15
|
afterAll(async () => {
|
|
8
16
|
const client = getTestClient();
|
|
9
17
|
for (const uid of peopleToCleanup) {
|
|
10
|
-
await
|
|
18
|
+
await personDeletePerson(uid, opts(client));
|
|
11
19
|
}
|
|
12
20
|
});
|
|
13
21
|
|
|
14
22
|
test("list all people", async () => {
|
|
15
23
|
const client = getTestClient();
|
|
16
|
-
const res = await
|
|
24
|
+
const res = await personGetAllPeople(undefined, opts(client));
|
|
17
25
|
|
|
18
26
|
expect(res.status).toBe(200);
|
|
19
|
-
const body =
|
|
27
|
+
const body = res.data as unknown as PaginatedResponse<Person>;
|
|
20
28
|
expect(body.metadata).toBeDefined();
|
|
21
29
|
expect(body.items).toBeInstanceOf(Array);
|
|
22
30
|
expect(body.metadata.total).toBeGreaterThan(0);
|
|
@@ -27,52 +35,40 @@ describe.skipIf(!hasCredentials)("People (live)", () => {
|
|
|
27
35
|
const email = `person-crud-${Date.now()}@test.com`;
|
|
28
36
|
|
|
29
37
|
// CREATE
|
|
30
|
-
const createRes = await
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
FirstName: "CrudTest",
|
|
35
|
-
LastName: "Person",
|
|
36
|
-
},
|
|
37
|
-
throwHttpErrors: false,
|
|
38
|
-
});
|
|
38
|
+
const createRes = await personAddPerson(
|
|
39
|
+
{ Email: email, FirstName: "CrudTest", LastName: "Person" } as PersonAddPersonBody,
|
|
40
|
+
opts(client),
|
|
41
|
+
);
|
|
39
42
|
expect(createRes.status).toBe(200);
|
|
40
|
-
const created =
|
|
43
|
+
const created = createRes.data as unknown as Person;
|
|
41
44
|
expect(created.Uid).toBeDefined();
|
|
42
45
|
expect(created.Email).toBe(email);
|
|
43
|
-
peopleToCleanup.push(created.Uid);
|
|
46
|
+
peopleToCleanup.push(created.Uid!);
|
|
44
47
|
|
|
45
48
|
// READ
|
|
46
|
-
const getRes = await
|
|
47
|
-
throwHttpErrors: false,
|
|
48
|
-
});
|
|
49
|
+
const getRes = await personGetPerson(created.Uid!, opts(client));
|
|
49
50
|
expect(getRes.status).toBe(200);
|
|
50
|
-
const fetched =
|
|
51
|
+
const fetched = getRes.data as unknown as Person;
|
|
51
52
|
expect(fetched.Uid).toBe(created.Uid);
|
|
52
53
|
|
|
53
54
|
// UPDATE
|
|
54
|
-
const updateRes = await
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
const updateRes = await personUpdatePerson(
|
|
56
|
+
created.Uid!,
|
|
57
|
+
{ FirstName: "UpdatedFirst" } as PersonUpdatePersonBody,
|
|
58
|
+
opts(client),
|
|
59
|
+
);
|
|
59
60
|
expect(updateRes.status).toBe(200);
|
|
60
|
-
const updated =
|
|
61
|
+
const updated = updateRes.data as unknown as Person;
|
|
61
62
|
expect(updated.FirstName).toBe("UpdatedFirst");
|
|
62
63
|
|
|
63
64
|
// DELETE
|
|
64
|
-
const deleteRes = await
|
|
65
|
-
method: "DELETE",
|
|
66
|
-
throwHttpErrors: false,
|
|
67
|
-
});
|
|
65
|
+
const deleteRes = await personDeletePerson(created.Uid!, opts(client));
|
|
68
66
|
expect(deleteRes.status).toBe(200);
|
|
69
67
|
});
|
|
70
68
|
|
|
71
69
|
test("get non-existent person returns 404", async () => {
|
|
72
70
|
const client = getTestClient();
|
|
73
|
-
const res = await
|
|
74
|
-
throwHttpErrors: false,
|
|
75
|
-
});
|
|
71
|
+
const res = await personGetPerson("nonexistent-uid-000", opts(client));
|
|
76
72
|
expect(res.status).toBe(404);
|
|
77
73
|
});
|
|
78
74
|
});
|
package/src/__tests__/setup.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { createClient } from "../client.js";
|
|
2
|
-
import type { KyInstance } from "ky";
|
|
1
|
+
import { createClient, type OutsetaClient } from "../client.js";
|
|
3
2
|
|
|
4
3
|
const subdomain = process.env.OUTSETA_SUBDOMAIN;
|
|
5
4
|
const apiKey = process.env.OUTSETA_API_KEY;
|
|
@@ -7,13 +6,13 @@ const apiSecret = process.env.OUTSETA_API_SECRET;
|
|
|
7
6
|
|
|
8
7
|
export const hasCredentials = !!(subdomain && apiKey && apiSecret);
|
|
9
8
|
|
|
10
|
-
let _client:
|
|
9
|
+
let _client: OutsetaClient | undefined;
|
|
11
10
|
|
|
12
11
|
/**
|
|
13
12
|
* Generated functions type options as RequestInit, but the customFetch mutator
|
|
14
|
-
* expects { client:
|
|
13
|
+
* expects { client: OutsetaClient } at runtime. This bridges the gap.
|
|
15
14
|
*/
|
|
16
|
-
export const opts = (client:
|
|
15
|
+
export const opts = (client: OutsetaClient) =>
|
|
17
16
|
({ client }) as unknown as RequestInit;
|
|
18
17
|
|
|
19
18
|
/** Outseta list endpoints return this paginated wrapper, not a flat array. */
|
|
@@ -22,7 +21,7 @@ export interface PaginatedResponse<T> {
|
|
|
22
21
|
items: T[];
|
|
23
22
|
}
|
|
24
23
|
|
|
25
|
-
export function getTestClient():
|
|
24
|
+
export function getTestClient(): OutsetaClient {
|
|
26
25
|
if (!hasCredentials) {
|
|
27
26
|
throw new Error("Missing OUTSETA_SUBDOMAIN, OUTSETA_API_KEY, or OUTSETA_API_SECRET env vars");
|
|
28
27
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { describe, expect, test, afterAll } from "bun:test";
|
|
2
2
|
import { hasCredentials, getTestClient, opts, type PaginatedResponse } from "./setup.js";
|
|
3
|
-
import type { Case } from "../generated/models/index.js";
|
|
4
|
-
import { caseGetAllCases } from "../generated/support/support.js";
|
|
3
|
+
import type { Case, CaseAddCaseBody, Person } from "../generated/models/index.js";
|
|
4
|
+
import { caseGetAllCases, caseAddCase, caseGetCase } from "../generated/support/support.js";
|
|
5
|
+
import { personGetAllPeople } from "../generated/crm/crm.js";
|
|
5
6
|
|
|
6
7
|
describe.skipIf(!hasCredentials)("Support Cases — generated endpoints (live)", () => {
|
|
7
8
|
test("caseGetAllCases returns a paginated response", async () => {
|
|
@@ -18,67 +19,59 @@ describe.skipIf(!hasCredentials)("Support Cases — generated endpoints (live)",
|
|
|
18
19
|
});
|
|
19
20
|
|
|
20
21
|
// Cases cannot be deleted via API (405), so we close them after the test
|
|
21
|
-
// to keep the test account tidy.
|
|
22
|
+
// to keep the test account tidy. No generated caseUpdateCase exists,
|
|
23
|
+
// so cleanup uses the low-level client.
|
|
22
24
|
const caseUidsToClose: string[] = [];
|
|
23
25
|
|
|
24
26
|
describe.skipIf(!hasCredentials)("Support Cases — CRUD (live)", () => {
|
|
25
27
|
afterAll(async () => {
|
|
26
28
|
const client = getTestClient();
|
|
27
29
|
for (const uid of caseUidsToClose) {
|
|
28
|
-
const getRes = await
|
|
29
|
-
if (getRes.
|
|
30
|
-
const full =
|
|
30
|
+
const getRes = await caseGetCase(uid, opts(client));
|
|
31
|
+
if (getRes.status === 200) {
|
|
32
|
+
const full = getRes.data as unknown as Case;
|
|
33
|
+
// No generated update function — fall back to the low-level client
|
|
31
34
|
await client(`support/cases/${uid}`, {
|
|
32
35
|
method: "PUT",
|
|
33
|
-
|
|
34
|
-
|
|
36
|
+
headers: { "Content-Type": "application/json" },
|
|
37
|
+
body: JSON.stringify({ ...full, Status: 2 }),
|
|
35
38
|
});
|
|
36
39
|
}
|
|
37
40
|
}
|
|
38
41
|
});
|
|
39
42
|
|
|
40
|
-
test("create → read
|
|
43
|
+
test("create → read case", async () => {
|
|
41
44
|
const client = getTestClient();
|
|
42
45
|
|
|
43
46
|
// Get a person to be the case author
|
|
44
|
-
const peopleRes = await
|
|
45
|
-
|
|
47
|
+
const peopleRes = await personGetAllPeople(undefined, opts(client));
|
|
48
|
+
expect(peopleRes.status).toBe(200);
|
|
49
|
+
const people = peopleRes.data as unknown as PaginatedResponse<Person>;
|
|
46
50
|
expect(people.items.length).toBeGreaterThan(0);
|
|
47
|
-
const
|
|
51
|
+
const person = people.items[0];
|
|
48
52
|
|
|
49
53
|
// CREATE
|
|
50
|
-
const createRes = await
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
FromPerson: { Uid: personUid },
|
|
54
|
+
const createRes = await caseAddCase(
|
|
55
|
+
{
|
|
56
|
+
FromPerson: { Uid: person.Uid! },
|
|
54
57
|
Subject: `CRUD test case ${Date.now()}`,
|
|
55
58
|
Body: "Created by integration test",
|
|
56
59
|
Source: 2,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
Status: 1,
|
|
61
|
+
} as CaseAddCaseBody,
|
|
62
|
+
{ sendautoresponder: "false" },
|
|
63
|
+
opts(client),
|
|
64
|
+
);
|
|
60
65
|
expect(createRes.status).toBe(200);
|
|
61
|
-
const created =
|
|
66
|
+
const created = createRes.data as unknown as Case;
|
|
62
67
|
expect(created.Uid).toBeDefined();
|
|
63
68
|
expect(created.Subject).toContain("CRUD test case");
|
|
64
|
-
caseUidsToClose.push(created.Uid
|
|
69
|
+
caseUidsToClose.push(created.Uid!);
|
|
65
70
|
|
|
66
71
|
// READ
|
|
67
|
-
const getRes = await
|
|
68
|
-
throwHttpErrors: false,
|
|
69
|
-
});
|
|
72
|
+
const getRes = await caseGetCase(created.Uid!, opts(client));
|
|
70
73
|
expect(getRes.status).toBe(200);
|
|
71
|
-
const fetched =
|
|
74
|
+
const fetched = getRes.data as unknown as Case;
|
|
72
75
|
expect(fetched.Uid).toBe(created.Uid);
|
|
73
|
-
|
|
74
|
-
// UPDATE (close case: Status 2 = Closed)
|
|
75
|
-
const closeRes = await client(`support/cases/${created.Uid}`, {
|
|
76
|
-
method: "PUT",
|
|
77
|
-
json: { ...fetched, Status: 2 },
|
|
78
|
-
throwHttpErrors: false,
|
|
79
|
-
});
|
|
80
|
-
expect(closeRes.status).toBe(200);
|
|
81
|
-
const closed = await closeRes.json<Record<string, unknown>>();
|
|
82
|
-
expect(closed.Status).toBe(2);
|
|
83
76
|
});
|
|
84
77
|
});
|
package/src/client.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import ky, { HTTPError, type KyInstance } from "ky";
|
|
2
|
-
|
|
3
1
|
export type OutsetaApiKeyCredentials = {
|
|
4
2
|
subdomain: string;
|
|
5
3
|
apiKey: string;
|
|
@@ -15,6 +13,15 @@ export type OutsetaCredentials =
|
|
|
15
13
|
| OutsetaApiKeyCredentials
|
|
16
14
|
| OutsetaBearerCredentials;
|
|
17
15
|
|
|
16
|
+
export type OutsetaClient = (
|
|
17
|
+
path: string,
|
|
18
|
+
init?: RequestInit,
|
|
19
|
+
) => Promise<Response>;
|
|
20
|
+
|
|
21
|
+
export type OutsetaRequestInit = RequestInit & {
|
|
22
|
+
client?: OutsetaClient;
|
|
23
|
+
};
|
|
24
|
+
|
|
18
25
|
function getAuthorization(creds: OutsetaCredentials): string {
|
|
19
26
|
if ("accessToken" in creds) {
|
|
20
27
|
return `Bearer ${creds.accessToken}`;
|
|
@@ -22,44 +29,52 @@ function getAuthorization(creds: OutsetaCredentials): string {
|
|
|
22
29
|
return `Outseta ${creds.apiKey}:${creds.apiSecret}`;
|
|
23
30
|
}
|
|
24
31
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
function getRequestUrl(subdomain: string, path: string): string {
|
|
33
|
+
if (/^https?:\/\//.test(path)) {
|
|
34
|
+
return path;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const normalizedPath = path.startsWith("/api/v1/")
|
|
38
|
+
? path
|
|
39
|
+
: `/api/v1/${path.replace(/^\/+/, "")}`;
|
|
40
|
+
|
|
41
|
+
return new URL(normalizedPath, `https://${subdomain}.outseta.com`).toString();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createClient(creds: OutsetaCredentials): OutsetaClient {
|
|
45
|
+
return (path, init = {}) => {
|
|
46
|
+
const headers = new Headers(init.headers);
|
|
47
|
+
headers.set("Authorization", getAuthorization(creds));
|
|
48
|
+
|
|
49
|
+
return fetch(getRequestUrl(creds.subdomain, path), {
|
|
50
|
+
...init,
|
|
51
|
+
headers,
|
|
52
|
+
});
|
|
53
|
+
};
|
|
33
54
|
}
|
|
34
55
|
|
|
35
56
|
/**
|
|
36
57
|
* Custom Orval mutator for the fetch client.
|
|
37
58
|
*
|
|
38
59
|
* Generated functions call `customFetch(url, { ...options, method })`.
|
|
39
|
-
* Consumers pass `{ client }` in the options to inject
|
|
60
|
+
* Consumers pass `{ client }` in the options to inject a client from
|
|
61
|
+
* `createClient()`.
|
|
40
62
|
*/
|
|
41
63
|
export const customFetch = async <T>(
|
|
42
64
|
url: string,
|
|
43
|
-
options:
|
|
65
|
+
options: OutsetaRequestInit,
|
|
44
66
|
): Promise<T> => {
|
|
45
67
|
const { client, ...init } = options;
|
|
46
68
|
if (!client) {
|
|
47
69
|
throw new Error(
|
|
48
|
-
"@outseta/api-client: No client provided. Pass a
|
|
70
|
+
"@outseta/api-client: No client provided. Pass a client from createClient() via the `client` option.",
|
|
49
71
|
);
|
|
50
72
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
response = await client(url.replace(/^\/api\/v1\//, ""), init);
|
|
54
|
-
} catch (error) {
|
|
55
|
-
if (error instanceof HTTPError) {
|
|
56
|
-
response = error.response;
|
|
57
|
-
} else {
|
|
58
|
-
throw error;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
73
|
+
|
|
74
|
+
const response = await client(url, init);
|
|
61
75
|
const data = response.headers.get("content-type")?.includes("application/json")
|
|
62
76
|
? await response.json()
|
|
63
77
|
: undefined;
|
|
78
|
+
|
|
64
79
|
return { data, status: response.status, headers: response.headers } as T;
|
|
65
80
|
};
|