@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.
Files changed (74) hide show
  1. package/dist/index.js +17 -19
  2. package/package.json +3 -6
  3. package/src/__tests__/accounts-crud.integration.test.ts +41 -39
  4. package/src/__tests__/client.test.ts +3 -3
  5. package/src/__tests__/people.integration.test.ts +28 -32
  6. package/src/__tests__/setup.ts +5 -6
  7. package/src/__tests__/support.integration.test.ts +28 -35
  8. package/src/client.ts +38 -23
  9. package/src/generated/activity/activity.ts +335 -0
  10. package/src/generated/billing/billing.ts +1006 -0
  11. package/src/generated/crm/crm.ts +1143 -1
  12. package/src/generated/email/email.ts +798 -0
  13. package/src/generated/models/accountAddPersonToAccountBody.ts +227 -0
  14. package/src/generated/models/accountCancelAccountBody.ts +227 -0
  15. package/src/generated/models/accountCancelationAllOf.ts +2 -0
  16. package/src/generated/models/accountExtendTrialBody.ts +227 -0
  17. package/src/generated/models/accountStage.ts +2 -1
  18. package/src/generated/models/accountUpdateAccountBody.ts +227 -0
  19. package/src/generated/models/accountUpdateMembershipBody.ts +227 -0
  20. package/src/generated/models/activityAddCustomActivityBody.ts +227 -0
  21. package/src/generated/models/activityGetAllParams.ts +241 -0
  22. package/src/generated/models/articleGetAllArticlesParams.ts +228 -0
  23. package/src/generated/models/backGroundTaskType.ts +3 -1
  24. package/src/generated/models/broadcastCampaignAllOf.ts +4 -0
  25. package/src/generated/models/broadcastCampaignStatus.ts +2 -1
  26. package/src/generated/models/campaignAddBroadcastEmailBody.ts +227 -0
  27. package/src/generated/models/campaignSendTestCampaignEmailBody.ts +227 -0
  28. package/src/generated/models/campaignUpdateBroadcastEmailBody.ts +227 -0
  29. package/src/generated/models/cancelationStatus.ts +236 -0
  30. package/src/generated/models/caseAddCaseBody.ts +227 -0
  31. package/src/generated/models/caseAddCaseParams.ts +229 -0
  32. package/src/generated/models/crmSettingsAllOf.ts +2 -0
  33. package/src/generated/models/dealAddDealBody.ts +227 -0
  34. package/src/generated/models/dealGetAllDealsParams.ts +234 -0
  35. package/src/generated/models/dealUpdateDealBody.ts +227 -0
  36. package/src/generated/models/discountCouponAddDiscountCouponBody.ts +227 -0
  37. package/src/generated/models/dnsEntry.ts +2 -0
  38. package/src/generated/models/emailListAddSubscriptionBody.ts +227 -0
  39. package/src/generated/models/emailListGetAllSubscriptionsParams.ts +229 -0
  40. package/src/generated/models/extendSubscriptionOptions.ts +225 -0
  41. package/src/generated/models/index.ts +40 -0
  42. package/src/generated/models/invoiceAddInvoiceBody.ts +227 -0
  43. package/src/generated/models/invoiceAllOf.ts +8 -0
  44. package/src/generated/models/messageAllOf.ts +1 -0
  45. package/src/generated/models/paymentInformationSavePaymentInformationBody.ts +227 -0
  46. package/src/generated/models/personAddPersonBody.ts +227 -0
  47. package/src/generated/models/personAllOf.ts +3 -3
  48. package/src/generated/models/personForgotPasswordBody.ts +227 -0
  49. package/src/generated/models/personGetAllPeopleParams.ts +229 -0
  50. package/src/generated/models/personSetTemporaryPasswordBody.ts +227 -0
  51. package/src/generated/models/personUpdatePersonBody.ts +227 -0
  52. package/src/generated/models/qcountConfigSettingType.ts +3 -1
  53. package/src/generated/models/resendEmailsRequest.ts +226 -0
  54. package/src/generated/models/sendGridDomainAuthenticationAllOf.ts +1 -0
  55. package/src/generated/models/setupIntent.ts +2 -0
  56. package/src/generated/models/stripeCreditNoteAllOf.ts +9 -0
  57. package/src/generated/models/stripeInvoiceAllOf.ts +9 -0
  58. package/src/generated/models/stripePriceAllOf.ts +2 -0
  59. package/src/generated/models/stripeProductAllOf.ts +5 -0
  60. package/src/generated/models/stripeSubscriptionAllOf.ts +4 -0
  61. package/src/generated/models/subscriptionAddOnAddSubscriptionAddOnBody.ts +227 -0
  62. package/src/generated/models/subscriptionChangeSubscriptionBody.ts +227 -0
  63. package/src/generated/models/subscriptionChangeSubscriptionParams.ts +229 -0
  64. package/src/generated/models/subscriptionChangeSubscriptionPreviewBody.ts +227 -0
  65. package/src/generated/models/subscriptionChangeSubscriptionPreviewParams.ts +229 -0
  66. package/src/generated/models/subscriptionFirstTimeSubscriptionBody.ts +227 -0
  67. package/src/generated/models/subscriptionFirstTimeSubscriptionPreviewBody.ts +227 -0
  68. package/src/generated/models/subscriptionFirstTimeSubscriptionPreviewParams.ts +229 -0
  69. package/src/generated/models/subscriptionSetSubscriptionUpgradeRequiredBody.ts +227 -0
  70. package/src/generated/models/transactionsAddPaymentTransactionBody.ts +227 -0
  71. package/src/generated/models/usageAddUsageBody.ts +227 -0
  72. package/src/generated/support/support.ts +341 -1
  73. package/src/index.ts +2 -0
  74. 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 ky.create({
11
- prefixUrl: `https://${creds.subdomain}.outseta.com/api/v1/`,
12
- headers: {
13
- Authorization: getAuthorization(creds)
14
- },
15
- retry: { limit: 3, statusCodes: [408, 429, 500, 502, 503, 504] }
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 ky instance from createClient() via the `client` option.");
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.0",
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 client(`crm/accounts/${uid}`, { method: "DELETE", throwHttpErrors: false });
22
+ await accountDeleteAccount(uid, opts(client));
14
23
  }
15
24
 
16
- // Delete orphaned people by email (API doesn't embed Person in account responses)
25
+ // Delete orphaned people by email
17
26
  if (emailsToCleanup.length > 0) {
18
- const res = await client("crm/people", { throwHttpErrors: false });
19
- if (res.ok) {
20
- const body = await res.json<{ items: Array<{ Uid: string; Email: string }> }>();
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 client(`crm/people/${person.Uid}`, { method: "DELETE", throwHttpErrors: false });
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 client("crm/accounts", {
37
- method: "POST",
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
- throwHttpErrors: false,
48
- });
54
+ } as AccountAddAccountBody,
55
+ undefined,
56
+ opts(client),
57
+ );
49
58
  expect(createRes.status).toBe(200);
50
- const created = await createRes.json<{ Uid: string; Name: string }>();
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 client(`crm/accounts/${created.Uid}`, {
57
- throwHttpErrors: false,
58
- });
65
+ const getRes = await accountGetAccount(created.Uid!, opts(client));
59
66
  expect(getRes.status).toBe(200);
60
- const fetched = await getRes.json<{ Uid: string; Name: string }>();
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 client(`crm/accounts/${created.Uid}`, {
65
- method: "PUT",
66
- json: { Name: "Lifecycle Test Corp (Updated)" },
67
- throwHttpErrors: false,
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 = await updateRes.json<{ Uid: string; Name: string }>();
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 client(`crm/accounts/${created.Uid}`, {
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 client(`crm/accounts/${created.Uid}`, {
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 client("crm/accounts", {
91
- method: "POST",
92
- json: { Name: "No Person Corp" },
93
- throwHttpErrors: false,
94
- });
95
- expect(res.ok).toBe(false);
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 ky instance (callable function)", () => {
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", { throwHttpErrors: false });
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", { throwHttpErrors: false });
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 client(`crm/people/${uid}`, { method: "DELETE", throwHttpErrors: false });
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 client("crm/people", { throwHttpErrors: false });
24
+ const res = await personGetAllPeople(undefined, opts(client));
17
25
 
18
26
  expect(res.status).toBe(200);
19
- const body = await res.json<{ metadata: { total: number }; items: unknown[] }>();
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 client("crm/people", {
31
- method: "POST",
32
- json: {
33
- Email: email,
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 = await createRes.json<{ Uid: string; Email: string; FirstName: string }>();
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 client(`crm/people/${created.Uid}`, {
47
- throwHttpErrors: false,
48
- });
49
+ const getRes = await personGetPerson(created.Uid!, opts(client));
49
50
  expect(getRes.status).toBe(200);
50
- const fetched = await getRes.json<{ Uid: string; Email: string }>();
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 client(`crm/people/${created.Uid}`, {
55
- method: "PUT",
56
- json: { FirstName: "UpdatedFirst" },
57
- throwHttpErrors: false,
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 = await updateRes.json<{ FirstName: string }>();
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 client(`crm/people/${created.Uid}`, {
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 client("crm/people/nonexistent-uid-000", {
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
  });
@@ -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: KyInstance | undefined;
9
+ let _client: OutsetaClient | undefined;
11
10
 
12
11
  /**
13
12
  * Generated functions type options as RequestInit, but the customFetch mutator
14
- * expects { client: KyInstance } at runtime. This bridges the gap.
13
+ * expects { client: OutsetaClient } at runtime. This bridges the gap.
15
14
  */
16
- export const opts = (client: KyInstance) =>
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(): KyInstance {
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 client(`support/cases/${uid}`, { throwHttpErrors: false });
29
- if (getRes.ok) {
30
- const full = await getRes.json<Record<string, unknown>>();
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
- json: { ...full, Status: 2 },
34
- throwHttpErrors: false,
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 → update case", async () => {
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 client("crm/people", { throwHttpErrors: false });
45
- const people = await peopleRes.json<{ items: Array<{ Uid: string }> }>();
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 personUid = people.items[0].Uid;
51
+ const person = people.items[0];
48
52
 
49
53
  // CREATE
50
- const createRes = await client("support/cases?sendAutoResponder=false", {
51
- method: "POST",
52
- json: {
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
- throwHttpErrors: false,
59
- });
60
+ Status: 1,
61
+ } as CaseAddCaseBody,
62
+ { sendautoresponder: "false" },
63
+ opts(client),
64
+ );
60
65
  expect(createRes.status).toBe(200);
61
- const created = await createRes.json<Record<string, unknown>>();
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 as string);
69
+ caseUidsToClose.push(created.Uid!);
65
70
 
66
71
  // READ
67
- const getRes = await client(`support/cases/${created.Uid}`, {
68
- throwHttpErrors: false,
69
- });
72
+ const getRes = await caseGetCase(created.Uid!, opts(client));
70
73
  expect(getRes.status).toBe(200);
71
- const fetched = await getRes.json<Record<string, unknown>>();
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
- export function createClient(creds: OutsetaCredentials): KyInstance {
26
- return ky.create({
27
- prefixUrl: `https://${creds.subdomain}.outseta.com/api/v1/`,
28
- headers: {
29
- Authorization: getAuthorization(creds),
30
- },
31
- retry: { limit: 3, statusCodes: [408, 429, 500, 502, 503, 504] },
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 their ky instance.
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: RequestInit & { client?: KyInstance },
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 ky instance from createClient() via the `client` option.",
70
+ "@outseta/api-client: No client provided. Pass a client from createClient() via the `client` option.",
49
71
  );
50
72
  }
51
- let response: Response;
52
- try {
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
  };