From a2c4a6073c03cb7960e3c243ad76a81f3d4092c7 Mon Sep 17 00:00:00 2001 From: priosshrsth Date: Tue, 18 Aug 2026 08:39:41 +0000 Subject: [PATCH 1/2] feat: cover remaining documented Platform API endpoints Adds the 24 operations present in the published OpenAPI spec but missing from the kit, taking coverage from 73 to 97 of 99. The two left out (POST /v1/session, POST /v1/token) mint bearer tokens and need an auth-model decision first. New namespaces: me, refunds, taskComments. Transport gains getRaw() because GET /v1/files/{id}/download streams application/octet-stream rather than JSON. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 63 +++--- README.md | 67 +++--- bun.lock | 4 +- package.json | 2 +- src/client/index.ts | 9 + src/lib/modules/app-installs/resource.ts | 58 ++++- src/lib/modules/app-installs/schema.ts | 99 +++++++++ src/lib/modules/clients/resource.ts | 24 ++- src/lib/modules/clients/schema.ts | 19 +- src/lib/modules/companies/resource.ts | 29 ++- src/lib/modules/companies/schema.ts | 42 ++++ src/lib/modules/custom-fields/resource.ts | 18 +- src/lib/modules/custom-fields/schema.ts | 33 +++ src/lib/modules/files/resource.ts | 58 ++++- src/lib/modules/files/schema.ts | 31 +++ src/lib/modules/forms/resource.ts | 39 +++- src/lib/modules/forms/schema.ts | 96 +++++++++ src/lib/modules/index.ts | 3 + src/lib/modules/internal-users/resource.ts | 33 ++- src/lib/modules/internal-users/schema.ts | 51 +++++ src/lib/modules/me/index.ts | 1 + src/lib/modules/me/resource.ts | 31 +++ src/lib/modules/me/schema.ts | 22 ++ src/lib/modules/message-channels/resource.ts | 19 +- src/lib/modules/message-channels/schema.ts | 17 ++ src/lib/modules/notifications/resource.ts | 10 + src/lib/modules/prices/resource.ts | 8 +- src/lib/modules/prices/schema.ts | 43 ++++ src/lib/modules/products/resource.ts | 8 +- src/lib/modules/products/schema.ts | 14 ++ src/lib/modules/refunds/index.ts | 1 + src/lib/modules/refunds/resource.ts | 49 +++++ src/lib/modules/refunds/schema.ts | 68 ++++++ src/lib/modules/task-comments/index.ts | 1 + src/lib/modules/task-comments/resource.ts | 62 ++++++ src/lib/modules/task-comments/schema.ts | 40 ++++ src/transport/http.ts | 11 + tests/resources.test.ts | 209 +++++++++++++++++++ 38 files changed, 1313 insertions(+), 79 deletions(-) create mode 100644 src/lib/modules/me/index.ts create mode 100644 src/lib/modules/me/resource.ts create mode 100644 src/lib/modules/me/schema.ts create mode 100644 src/lib/modules/refunds/index.ts create mode 100644 src/lib/modules/refunds/resource.ts create mode 100644 src/lib/modules/refunds/schema.ts create mode 100644 src/lib/modules/task-comments/index.ts create mode 100644 src/lib/modules/task-comments/resource.ts create mode 100644 src/lib/modules/task-comments/schema.ts create mode 100644 tests/resources.test.ts diff --git a/CLAUDE.md b/CLAUDE.md index 5f7d572..fcadd08 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -45,36 +45,39 @@ const kit = createAssemblyKit({ Access API resources via `kit..()`: -| Namespace | Methods | -| ----------------------- | ------------------------------------------------------------------ | -| `workspace` | `retrieve()` | -| `clients` | `list()` `retrieve()` `create()` `delete()` `listAll()` | -| `companies` | `list()` `retrieve()` `create()` `update()` `delete()` `listAll()` | -| `internalUsers` | `list()` `retrieve()` `listAll()` | -| `notes` | `list()` `retrieve()` `create()` `update()` `delete()` `listAll()` | -| `tasks` | `list()` `retrieve()` `create()` `update()` `delete()` `listAll()` | -| `taskTemplates` | `list()` `retrieve()` `listAll()` | -| `invoices` | `list()` `retrieve()` `create()` `listAll()` | -| `invoiceTemplates` | `list()` `listAll()` | -| `subscriptions` | `list()` `retrieve()` `create()` `cancel()` `listAll()` | -| `subscriptionTemplates` | `list()` `listAll()` | -| `payments` | `list()` `listAll()` | -| `products` | `list()` `retrieve()` `listAll()` | -| `prices` | `list()` `retrieve()` `listAll()` | -| `contracts` | `retrieve()` `send()` | -| `contractTemplates` | `list()` `retrieve()` | -| `forms` | `list()` `retrieve()` `listAll()` | -| `formResponses` | `list()` `create()` | -| `files` | `list()` `retrieve()` `delete()` `listAll()` | -| `fileChannels` | `list()` `retrieve()` `create()` `listAll()` | -| `messageChannels` | `list()` `retrieve()` `create()` `listAll()` | -| `messages` | `list()` `send()` `listAll()` | -| `events` | `list()` `retrieve()` `create()` `listAll()` | -| `notifications` | `list()` `create()` `delete()` `markRead()` `markUnread()` | -| `customFields` | `list()` | -| `customFieldOptions` | `list()` | -| `appConnections` | `list()` `create()` | -| `appInstalls` | `list()` `retrieve()` | +| Namespace | Methods | +| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `workspace` | `retrieve()` | +| `me` | `retrieve()` | +| `clients` | `list()` `retrieve()` `retrieveWithAppVisibility()` `create()` `update()` `delete()` `listAll()` | +| `companies` | `list()` `retrieve()` `create()` `update()` `delete()` `addClients()` `listAll()` | +| `internalUsers` | `list()` `retrieve()` `update()` `retrieveNotificationSettings()` `listAll()` | +| `notes` | `list()` `retrieve()` `create()` `update()` `delete()` `listAll()` | +| `tasks` | `list()` `retrieve()` `create()` `update()` `delete()` `listAll()` | +| `taskComments` | `list()` `retrieve()` `delete()` `listAll()` | +| `taskTemplates` | `list()` `retrieve()` `listAll()` | +| `invoices` | `list()` `retrieve()` `create()` `listAll()` | +| `invoiceTemplates` | `list()` `listAll()` | +| `subscriptions` | `list()` `retrieve()` `create()` `cancel()` `listAll()` | +| `subscriptionTemplates` | `list()` `listAll()` | +| `refunds` | `list()` `create()` `listAll()` | +| `payments` | `list()` `listAll()` | +| `products` | `list()` `retrieve()` `create()` `listAll()` | +| `prices` | `list()` `retrieve()` `create()` `listAll()` | +| `contracts` | `retrieve()` `send()` | +| `contractTemplates` | `list()` `retrieve()` | +| `forms` | `list()` `retrieve()` `create()` `listSubmissions()` `listAllSubmissions()` `listAll()` | +| `formResponses` | `list()` `create()` | +| `files` | `list()` `retrieve()` `create()` `delete()` `retrieveDownloadUrl()` `download()` `updateFolderPermissions()` `updateRootFolderPermissions()` `listAll()` | +| `fileChannels` | `list()` `retrieve()` `create()` `listAll()` | +| `messageChannels` | `list()` `retrieve()` `create()` `listUnread()` `listAll()` | +| `messages` | `list()` `send()` `listAll()` | +| `events` | `list()` `retrieve()` `create()` `listAll()` | +| `notifications` | `list()` `retrieve()` `create()` `delete()` `markRead()` `markUnread()` | +| `customFields` | `list()` `create()` | +| `customFieldOptions` | `list()` | +| `appConnections` | `list()` `create()` | +| `appInstalls` | `list()` `retrieve()` `retrieveNotificationSettings()` `createNotificationSettings()` `updateNotificationSettings()` | ## Pagination diff --git a/README.md b/README.md index ef96329..c26411c 100644 --- a/README.md +++ b/README.md @@ -141,36 +141,43 @@ const kit = createAssemblyKit({ #### Resource namespaces -| Namespace | Methods | -| ----------------------- | ----------------------------------------------------------------------- | -| `workspace` | `retrieve()` | -| `clients` | `list()`, `retrieve()`, `create()`, `update()`, `delete()`, `listAll()` | -| `companies` | `list()`, `retrieve()`, `create()`, `update()`, `delete()`, `listAll()` | -| `internalUsers` | `list()`, `retrieve()`, `listAll()` | -| `customFields` | `list()` | -| `customFieldOptions` | `list()` | -| `notes` | `list()`, `retrieve()`, `create()`, `update()`, `delete()`, `listAll()` | -| `messageChannels` | `list()`, `retrieve()`, `create()`, `listAll()` | -| `messages` | `list()`, `send()`, `listAll()` | -| `products` | `list()`, `retrieve()`, `listAll()` | -| `prices` | `list()`, `retrieve()`, `listAll()` | -| `invoiceTemplates` | `list()`, `listAll()` | -| `invoices` | `list()`, `retrieve()`, `create()`, `listAll()` | -| `subscriptionTemplates` | `list()`, `listAll()` | -| `subscriptions` | `list()`, `retrieve()`, `create()`, `cancel()`, `listAll()` | -| `payments` | `list()`, `listAll()` | -| `fileChannels` | `list()`, `retrieve()`, `create()`, `listAll()` | -| `files` | `list()`, `retrieve()`, `create()`, `delete()`, `listAll()` | -| `contractTemplates` | `list()`, `retrieve()`, `listAll()` | -| `contracts` | `list()`, `retrieve()`, `send()` | -| `forms` | `list()`, `retrieve()`, `listAll()` | -| `formResponses` | `list()`, `create()` | -| `tasks` | `list()`, `retrieve()`, `create()`, `update()`, `delete()`, `listAll()` | -| `taskTemplates` | `list()`, `retrieve()`, `listAll()` | -| `notifications` | `list()`, `create()`, `delete()`, `markRead()`, `markUnread()` | -| `events` | `list()`, `retrieve()`, `create()`, `listAll()` | -| `appConnections` | `list()`, `create()` | -| `appInstalls` | `list()`, `retrieve()` | +| Namespace | Methods | +| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `workspace` | `retrieve()` | +| `me` | `retrieve()` | +| `clients` | `list()`, `retrieve()`, `retrieveWithAppVisibility()`, `create()`, `update()`, `delete()`, `listAll()` | +| `companies` | `list()`, `retrieve()`, `create()`, `update()`, `delete()`, `addClients()`, `listAll()` | +| `internalUsers` | `list()`, `retrieve()`, `update()`, `retrieveNotificationSettings()`, `listAll()` | +| `customFields` | `list()`, `create()` | +| `customFieldOptions` | `list()` | +| `notes` | `list()`, `retrieve()`, `create()`, `update()`, `delete()`, `listAll()` | +| `messageChannels` | `list()`, `retrieve()`, `create()`, `listUnread()`, `listAll()` | +| `messages` | `list()`, `send()`, `listAll()` | +| `products` | `list()`, `retrieve()`, `create()`, `listAll()` | +| `prices` | `list()`, `retrieve()`, `create()`, `listAll()` | +| `invoiceTemplates` | `list()`, `listAll()` | +| `invoices` | `list()`, `retrieve()`, `create()`, `listAll()` | +| `subscriptionTemplates` | `list()`, `listAll()` | +| `subscriptions` | `list()`, `retrieve()`, `create()`, `cancel()`, `listAll()` | +| `refunds` | `list()`, `create()`, `listAll()` | +| `payments` | `list()`, `listAll()` | +| `fileChannels` | `list()`, `retrieve()`, `create()`, `listAll()` | +| `files` | `list()`, `retrieve()`, `create()`, `delete()`, `retrieveDownloadUrl()`, `download()`, `updateFolderPermissions()`, `updateRootFolderPermissions()`, `listAll()` | +| `contractTemplates` | `list()`, `retrieve()`, `listAll()` | +| `contracts` | `list()`, `retrieve()`, `send()` | +| `forms` | `list()`, `retrieve()`, `create()`, `listSubmissions()`, `listAllSubmissions()`, `listAll()` | +| `formResponses` | `list()`, `create()` | +| `tasks` | `list()`, `retrieve()`, `create()`, `update()`, `delete()`, `listAll()` | +| `taskComments` | `list()`, `retrieve()`, `delete()`, `listAll()` | +| `taskTemplates` | `list()`, `retrieve()`, `listAll()` | +| `notifications` | `list()`, `retrieve()`, `create()`, `delete()`, `markRead()`, `markUnread()` | +| `events` | `list()`, `retrieve()`, `create()`, `listAll()` | +| `appConnections` | `list()`, `create()` | +| `appInstalls` | `list()`, `retrieve()`, `retrieveNotificationSettings()`, `createNotificationSettings()`, `updateNotificationSettings()` | + +`files.download()` returns an `ArrayBuffer` — the endpoint streams `application/octet-stream` +rather than JSON. Use `files.retrieveDownloadUrl()` instead when you want a presigned URL to +hand to a browser. ### Pagination diff --git a/bun.lock b/bun.lock index 734bda3..4ccf785 100644 --- a/bun.lock +++ b/bun.lock @@ -9,7 +9,7 @@ "p-throttle": "^8.1.0", }, "devDependencies": { - "@assembly-js/app-bridge": "^1.1.1", + "@assembly-js/app-bridge": "^1.4.0", "@types/node": "^25.6.0", "@types/react": "^19.2.14", "@typescript/native-preview": "7.0.0-dev.20260505.1", @@ -40,7 +40,7 @@ }, }, "packages": { - "@assembly-js/app-bridge": ["@assembly-js/app-bridge@1.1.1", "", {}, "sha512-NWD8FuRBmLb8+5bMHX8NtepSnIVShmmYoPPD6rHfcFrkN9SKm61/vdCASJnMViWVOjItuKku9QSpn6sOy4L6Og=="], + "@assembly-js/app-bridge": ["@assembly-js/app-bridge@1.4.0", "", {}, "sha512-NAdRvsTfQFIWWi/XqGfDbjusO08fB6MncbTHn8LPrp+l20/4PGawvMJjOI81mdKZz+2ieg7AwTJaRUwWhS2eUg=="], "@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="], diff --git a/package.json b/package.json index dd026ff..639baa4 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "p-throttle": "^8.1.0" }, "devDependencies": { - "@assembly-js/app-bridge": "^1.1.1", + "@assembly-js/app-bridge": "^1.4.0", "@types/node": "^25.6.0", "@types/react": "^19.2.14", "@typescript/native-preview": "7.0.0-dev.20260505.1", diff --git a/src/client/index.ts b/src/client/index.ts index 7bd925b..63492dd 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -15,6 +15,7 @@ import { FormsResource } from "src/lib/modules/forms/resource"; import { InternalUsersResource } from "src/lib/modules/internal-users/resource"; import { InvoiceTemplatesResource } from "src/lib/modules/invoice-templates/resource"; import { InvoicesResource } from "src/lib/modules/invoices/resource"; +import { MeResource } from "src/lib/modules/me/resource"; import { MessageChannelsResource } from "src/lib/modules/message-channels/resource"; import { MessagesResource } from "src/lib/modules/messages/resource"; import { NotesResource } from "src/lib/modules/notes/resource"; @@ -22,8 +23,10 @@ import { NotificationsResource } from "src/lib/modules/notifications/resource"; import { PaymentsResource } from "src/lib/modules/payments/resource"; import { PricesResource } from "src/lib/modules/prices/resource"; import { ProductsResource } from "src/lib/modules/products/resource"; +import { RefundsResource } from "src/lib/modules/refunds/resource"; import { SubscriptionTemplatesResource } from "src/lib/modules/subscription-templates/resource"; import { SubscriptionsResource } from "src/lib/modules/subscriptions/resource"; +import { TaskCommentsResource } from "src/lib/modules/task-comments/resource"; import { TaskTemplatesResource } from "src/lib/modules/task-templates/resource"; import { TasksResource } from "src/lib/modules/tasks/resource"; import { WorkspaceResource } from "src/lib/modules/workspace/resource"; @@ -105,6 +108,7 @@ export class AssemblyKit< readonly internalUsers: InternalUsersResource; readonly invoiceTemplates: InvoiceTemplatesResource; readonly invoices: InvoicesResource; + readonly me: MeResource; readonly messageChannels: MessageChannelsResource; readonly messages: MessagesResource; readonly notes: NotesResource; @@ -112,8 +116,10 @@ export class AssemblyKit< readonly payments: PaymentsResource; readonly prices: PricesResource; readonly products: ProductsResource; + readonly refunds: RefundsResource; readonly subscriptionTemplates: SubscriptionTemplatesResource; readonly subscriptions: SubscriptionsResource; + readonly taskComments: TaskCommentsResource; readonly taskTemplates: TaskTemplatesResource; readonly tasks: TasksResource; readonly workspace: WorkspaceResource; @@ -168,6 +174,7 @@ export class AssemblyKit< this.internalUsers = new InternalUsersResource(ctx); this.invoiceTemplates = new InvoiceTemplatesResource(ctx); this.invoices = new InvoicesResource(ctx); + this.me = new MeResource(ctx); this.messageChannels = new MessageChannelsResource(ctx); this.messages = new MessagesResource(ctx); this.notes = new NotesResource(ctx); @@ -175,8 +182,10 @@ export class AssemblyKit< this.payments = new PaymentsResource(ctx); this.prices = new PricesResource(ctx); this.products = new ProductsResource(ctx); + this.refunds = new RefundsResource(ctx); this.subscriptionTemplates = new SubscriptionTemplatesResource(ctx); this.subscriptions = new SubscriptionsResource(ctx); + this.taskComments = new TaskCommentsResource(ctx); this.taskTemplates = new TaskTemplatesResource(ctx); this.tasks = new TasksResource(ctx); this.workspace = new WorkspaceResource(ctx); diff --git a/src/lib/modules/app-installs/resource.ts b/src/lib/modules/app-installs/resource.ts index 885c51a..17b7a18 100644 --- a/src/lib/modules/app-installs/resource.ts +++ b/src/lib/modules/app-installs/resource.ts @@ -1,8 +1,18 @@ import type { Transport } from "src/transport/http"; import { parseResponse } from "src/transport/parse-response"; -import { AppInstallResponseSchema, AppInstallsResponseSchema } from "./schema"; -import type { AppInstall, AppInstallsResponse } from "./schema"; +import { + AppInstallResponseSchema, + AppInstallsResponseSchema, + AppNotificationSettingsResponseSchema, +} from "./schema"; +import type { + AppInstall, + AppInstallsResponse, + AppNotificationSettingsCreateRequest, + AppNotificationSettingsResponse, + AppNotificationSettingsUpdateRequest, +} from "./schema"; export class AppInstallsResource { readonly #transport: Transport; @@ -34,4 +44,48 @@ export class AppInstallsResource { const raw: unknown = await this.#transport.get(`v1/installs/${installId}`); return parseResponse({ schema: AppInstallResponseSchema, data: raw, validate: this.#validate }); } + + /** Retrieve the notification settings the install's backing app declares. */ + async retrieveNotificationSettings(installId: string): Promise { + const raw: unknown = await this.#transport.get( + `v1/installs/${installId}/notification-settings`, + ); + return parseResponse({ + schema: AppNotificationSettingsResponseSchema, + data: raw, + validate: this.#validate, + }); + } + + /** Declare the initial notification settings of the install's backing app. Create-only — fails with 409 when settings already exist. */ + async createNotificationSettings(args: { + installId: string; + body: AppNotificationSettingsCreateRequest; + }): Promise { + const raw: unknown = await this.#transport.post( + `v1/installs/${args.installId}/notification-settings`, + args.body, + ); + return parseResponse({ + schema: AppNotificationSettingsResponseSchema, + data: raw, + validate: this.#validate, + }); + } + + /** Apply partial changes to the notification settings of the install's backing app. */ + async updateNotificationSettings(args: { + installId: string; + body: AppNotificationSettingsUpdateRequest; + }): Promise { + const raw: unknown = await this.#transport.patch( + `v1/installs/${args.installId}/notification-settings`, + args.body, + ); + return parseResponse({ + schema: AppNotificationSettingsResponseSchema, + data: raw, + validate: this.#validate, + }); + } } diff --git a/src/lib/modules/app-installs/schema.ts b/src/lib/modules/app-installs/schema.ts index 92f779b..95eeef5 100644 --- a/src/lib/modules/app-installs/schema.ts +++ b/src/lib/modules/app-installs/schema.ts @@ -45,3 +45,102 @@ export interface AppInstallsResponse { export const AppInstallsResponseSchema: z.ZodType = z.object({ data: z.array(AppInstallSchema).nullable(), }); + +// ─── Notification settings ──────────────────────────────────────────────────── + +export type NotificationSurface = "email" | "product"; + +export const NotificationSurfaceSchema: z.ZodType = z.enum([ + "product", + "email", +]); + +export interface AppNotificationActionLabel { + pluralNoun?: string; + singularNoun?: string; + verb?: string; +} + +export const AppNotificationActionLabelSchema: z.ZodType = z.object({ + pluralNoun: z.string().optional(), + singularNoun: z.string().optional(), + verb: z.string().optional(), +}); + +export interface AppNotificationSetting { + default?: { + email?: boolean; + product?: boolean; + }; + id?: string; + label?: string; + surfaces?: NotificationSurface[]; +} + +export const AppNotificationSettingSchema: z.ZodType = z.object({ + default: z + .object({ + email: z.boolean().optional(), + product: z.boolean().optional(), + }) + .optional(), + id: z.string().optional(), + label: z.string().optional(), + surfaces: z.array(NotificationSurfaceSchema).optional(), +}); + +export interface AppNotificationSettingsResponse { + actionLabel?: AppNotificationActionLabel; + notifications?: AppNotificationSetting[]; + object?: string; +} + +export const AppNotificationSettingsResponseSchema: z.ZodType = + z.object({ + actionLabel: AppNotificationActionLabelSchema.optional(), + notifications: z.array(AppNotificationSettingSchema).optional(), + object: z.string().optional(), + }); + +export interface AppNotificationSettingsCreateRequest { + actionLabel?: AppNotificationActionLabel; + notifications: { + default?: { email?: boolean; product?: boolean }; + id?: string; + label: string; + surfaces: NotificationSurface[]; + }[]; +} + +export const AppNotificationSettingsCreateRequestSchema: z.ZodType = + z.object({ + actionLabel: AppNotificationActionLabelSchema.optional(), + notifications: z + .array( + z.object({ + default: z + .object({ email: z.boolean().optional(), product: z.boolean().optional() }) + .optional(), + id: z.string().optional(), + label: z.string().max(100), + surfaces: z.array(NotificationSurfaceSchema).min(1), + }), + ) + .min(1) + .max(50), + }); + +/** + * Per-setting deltas: an entry without `id` adds a setting, an entry with only + * an `id` deletes it, and an entry with both patches it. + */ +export interface AppNotificationSettingsUpdateRequest { + actionLabel?: AppNotificationActionLabel; + notifications?: AppNotificationSetting[]; +} + +export const AppNotificationSettingsUpdateRequestSchema: z.ZodType = + z.object({ + actionLabel: AppNotificationActionLabelSchema.optional(), + notifications: z.array(AppNotificationSettingSchema).max(50).optional(), + }); diff --git a/src/lib/modules/clients/resource.ts b/src/lib/modules/clients/resource.ts index 9694555..ce3faf7 100644 --- a/src/lib/modules/clients/resource.ts +++ b/src/lib/modules/clients/resource.ts @@ -4,8 +4,18 @@ import { buildSearchParams } from "src/transport/build-search-params"; import type { Transport } from "src/transport/http"; import { parseResponse } from "src/transport/parse-response"; -import { ClientResponseSchema, ClientsResponseSchema } from "./schema"; -import type { Client, ClientCreateRequest, ClientUpdateRequest, ClientsResponse } from "./schema"; +import { + ClientResponseSchema, + ClientWithAppVisibilitySchema, + ClientsResponseSchema, +} from "./schema"; +import type { + Client, + ClientCreateRequest, + ClientUpdateRequest, + ClientWithAppVisibility, + ClientsResponse, +} from "./schema"; export interface ListClientsArgs extends ListArgs { companyId?: string; @@ -70,6 +80,16 @@ export class ClientsResource< }) as Client; } + /** Retrieve a client by ID together with the apps visible to them, grouped by company. */ + async retrieveWithAppVisibility(id: string): Promise> { + const raw: unknown = await this.#transport.get(`v1/clients/${id}/app-visibility`); + return parseResponse({ + schema: ClientWithAppVisibilitySchema, + data: raw, + validate: this.#validate, + }) as ClientWithAppVisibility; + } + /** Update a client (PATCH — partial update). */ async update(args: { id: string; diff --git a/src/lib/modules/clients/schema.ts b/src/lib/modules/clients/schema.ts index 0807b6b..2e44029 100644 --- a/src/lib/modules/clients/schema.ts +++ b/src/lib/modules/clients/schema.ts @@ -23,7 +23,7 @@ export interface Client = Record = z.object({ +const clientShape = { avatarImageUrl: z.string().nullable(), companyId: z.string().optional(), companyIds: z.array(z.string()).optional(), @@ -41,6 +41,23 @@ export const ClientSchema: z.ZodType = z.object({ object: z.literal("client"), status: z.enum(["active", "invited", "notInvited"]), updatedAt: z.iso.datetime().optional(), +}; + +export const ClientSchema: z.ZodType = z.object(clientShape); + +/** + * `appVisibility` is documented in prose but absent from the OpenAPI response + * schema, so its group shape is kept loose rather than guessed at. + */ +export interface ClientWithAppVisibility< + TCustomFields extends Record = Record, +> extends Client { + appVisibility?: Record[]; +} + +export const ClientWithAppVisibilitySchema: z.ZodType = z.object({ + ...clientShape, + appVisibility: z.array(z.record(z.string(), z.unknown())).optional(), }); // ─── Response ───────────────────────────────────────────────────────────────── diff --git a/src/lib/modules/companies/resource.ts b/src/lib/modules/companies/resource.ts index 3ae7db1..8598ac9 100644 --- a/src/lib/modules/companies/resource.ts +++ b/src/lib/modules/companies/resource.ts @@ -4,10 +4,15 @@ import { buildSearchParams } from "src/transport/build-search-params"; import type { Transport } from "src/transport/http"; import { parseResponse } from "src/transport/parse-response"; -import { CompaniesResponseSchema, CompanyResponseSchema } from "./schema"; +import { + CompaniesResponseSchema, + CompanyAddClientsResponseSchema, + CompanyResponseSchema, +} from "./schema"; import type { CompaniesResponse, Company, + CompanyAddClientsResponse, CompanyCreateRequest, CompanyUpdateRequest, } from "./schema"; @@ -44,6 +49,28 @@ export class CompaniesResource< }) as Company; } + /** Associate existing clients with a company. */ + async addClients(args: { + id: string; + clientIds: string[]; + confirmPromotePlaceholder?: boolean; + }): Promise { + const searchParams = + args.confirmPromotePlaceholder !== undefined + ? buildSearchParams({ confirmPromotePlaceholder: args.confirmPromotePlaceholder }) + : undefined; + const raw: unknown = await this.#transport.post( + `v1/companies/${args.id}/clients`, + { clientIds: args.clientIds }, + { searchParams }, + ); + return parseResponse({ + schema: CompanyAddClientsResponseSchema, + data: raw, + validate: this.#validate, + }); + } + /** List companies with optional filters. */ async list(args: ListCompaniesArgs = {}): Promise> { const raw: unknown = await this.#transport.get("v1/companies", { diff --git a/src/lib/modules/companies/schema.ts b/src/lib/modules/companies/schema.ts index 8cc224f..2d3ade3 100644 --- a/src/lib/modules/companies/schema.ts +++ b/src/lib/modules/companies/schema.ts @@ -74,3 +74,45 @@ export const CompanyUpdateRequestSchema: z.ZodType = z.obj iconImageUrl: z.string().optional(), name: z.string().optional(), }); + +// ─── Add clients ────────────────────────────────────────────────────────────── + +export interface CompanyAddClientsRequest { + clientIds: string[]; +} + +export const CompanyAddClientsRequestSchema: z.ZodType = z.object({ + clientIds: z.array(z.string()).min(1), +}); + +export interface CompanyAddClientError { + clientId?: string; + error?: { + code?: string; + message?: string; + type?: string; + }; +} + +export interface CompanyAddClientsResponse { + addClientErrors?: CompanyAddClientError[]; + addedClientIds?: string[]; +} + +export const CompanyAddClientsResponseSchema: z.ZodType = z.object({ + addClientErrors: z + .array( + z.object({ + clientId: z.string().optional(), + error: z + .object({ + code: z.string().optional(), + message: z.string().optional(), + type: z.string().optional(), + }) + .optional(), + }), + ) + .optional(), + addedClientIds: z.array(z.string()).optional(), +}); diff --git a/src/lib/modules/custom-fields/resource.ts b/src/lib/modules/custom-fields/resource.ts index ea70e3a..eb69412 100644 --- a/src/lib/modules/custom-fields/resource.ts +++ b/src/lib/modules/custom-fields/resource.ts @@ -2,8 +2,12 @@ import { buildSearchParams } from "src/transport/build-search-params"; import type { Transport } from "src/transport/http"; import { parseResponse } from "src/transport/parse-response"; -import { ListCustomFieldResponseSchema } from "./schema"; -import type { ListCustomFieldResponse } from "./schema"; +import { CustomFieldsCreateResponseSchema, ListCustomFieldResponseSchema } from "./schema"; +import type { + CustomFieldsCreateRequest, + CustomFieldsCreateResponse, + ListCustomFieldResponse, +} from "./schema"; export class CustomFieldsResource { readonly #transport: Transport; @@ -20,6 +24,16 @@ export class CustomFieldsResource { this.#validate = validateResponses; } + /** Create one or more custom fields. */ + async create(body: CustomFieldsCreateRequest): Promise { + const raw = await this.#transport.post("v1/custom-fields", body); + return parseResponse({ + schema: CustomFieldsCreateResponseSchema, + data: raw, + validate: this.#validate, + }); + } + /** List custom fields with optional entity type filter. */ async list(args: { entityType?: string } = {}): Promise { const raw = await this.#transport.get("v1/custom-fields", { diff --git a/src/lib/modules/custom-fields/schema.ts b/src/lib/modules/custom-fields/schema.ts index 6c3bdc0..4c2dae7 100644 --- a/src/lib/modules/custom-fields/schema.ts +++ b/src/lib/modules/custom-fields/schema.ts @@ -98,3 +98,36 @@ export interface ListCustomFieldResponse { export const ListCustomFieldResponseSchema: z.ZodType = z.object({ data: z.array(CustomFieldSchema).transform((v) => v || []), }); + +// ─── Requests ───────────────────────────────────────────────────────────────── + +export interface CustomFieldCreateInput { + entityType: CustomFieldEntityType; + name: string; + /** Only valid for `multiSelect` fields. */ + options?: { label: string }[]; + type: CustomFieldType; +} + +export const CustomFieldCreateInputSchema: z.ZodType = z.object({ + entityType: CustomFieldEntityTypeSchema, + name: z.string(), + options: z.array(z.object({ label: z.string() })).optional(), + type: CustomFieldTypeSchema, +}); + +export interface CustomFieldsCreateRequest { + customFields: CustomFieldCreateInput[]; +} + +export const CustomFieldsCreateRequestSchema: z.ZodType = z.object({ + customFields: z.array(CustomFieldCreateInputSchema), +}); + +export interface CustomFieldsCreateResponse { + customFields: CustomField[]; +} + +export const CustomFieldsCreateResponseSchema: z.ZodType = z.object({ + customFields: z.array(CustomFieldSchema).transform((v) => v || []), +}); diff --git a/src/lib/modules/files/resource.ts b/src/lib/modules/files/resource.ts index 1b08e7d..3c6a096 100644 --- a/src/lib/modules/files/resource.ts +++ b/src/lib/modules/files/resource.ts @@ -4,8 +4,19 @@ import { buildSearchParams } from "src/transport/build-search-params"; import type { Transport } from "src/transport/http"; import { parseResponse } from "src/transport/parse-response"; -import { AssemblyFileResponseSchema, AssemblyFilesResponseSchema } from "./schema"; -import type { AssemblyFile, AssemblyFilesResponse } from "./schema"; +import { + AssemblyFileResponseSchema, + AssemblyFilesResponseSchema, + FileDownloadUrlResponseSchema, + FilePermissionsResponseSchema, +} from "./schema"; +import type { + AssemblyFile, + AssemblyFilesResponse, + ClientPermissions, + FileDownloadUrlResponse, + FilePermissionsResponse, +} from "./schema"; export interface ListFilesArgs extends ListArgs { channelId: string; @@ -67,6 +78,49 @@ export class FilesResource { }); } + /** Retrieve a short-lived presigned download URL for a file's contents. */ + async retrieveDownloadUrl(id: string): Promise { + const raw: unknown = await this.#transport.get(`v1/files/${id}/download-url`); + return parseResponse({ + schema: FileDownloadUrlResponseSchema, + data: raw, + validate: this.#validate, + }); + } + + /** Download a file's raw contents. */ + async download(id: string): Promise { + return this.#transport.getRaw(`v1/files/${id}/download`); + } + + /** Update the client access level on a folder. */ + async updateFolderPermissions(args: { + id: string; + clientPermissions: ClientPermissions; + }): Promise { + const raw: unknown = await this.#transport.put(`v1/files/${args.id}/permissions/`, { + clientPermissions: args.clientPermissions, + }); + return parseResponse({ + schema: FilePermissionsResponseSchema, + data: raw, + validate: this.#validate, + }); + } + + /** Update the client access level on a file channel's root folder. */ + async updateRootFolderPermissions(args: { + channelId: string; + clientPermissions: ClientPermissions; + }): Promise { + const raw: unknown = await this.#transport.put("v1/files/root/permissions", args); + return parseResponse({ + schema: FilePermissionsResponseSchema, + data: raw, + validate: this.#validate, + }); + } + /** Delete a file by ID. */ async delete(id: string): Promise { await this.#transport.delete(`v1/files/${id}`); diff --git a/src/lib/modules/files/schema.ts b/src/lib/modules/files/schema.ts index 5d42c87..c0ea548 100644 --- a/src/lib/modules/files/schema.ts +++ b/src/lib/modules/files/schema.ts @@ -48,3 +48,34 @@ export const AssemblyFilesResponseSchema: z.ZodType = z.o data: z.array(AssemblyFileSchema).nullable(), nextToken: z.string().optional(), }); + +// ─── Permissions ────────────────────────────────────────────────────────────── + +export type ClientPermissions = "read_only" | "read_write"; + +/** + * The OpenAPI spec declares this enum as the single string "read_only read_write"; + * the two documented values are modelled separately here. + */ +export const ClientPermissionsSchema: z.ZodType = z.enum([ + "read_only", + "read_write", +]); + +export interface FilePermissionsResponse { + success?: boolean; +} + +export const FilePermissionsResponseSchema: z.ZodType = z.object({ + success: z.boolean().optional(), +}); + +// ─── Download ───────────────────────────────────────────────────────────────── + +export interface FileDownloadUrlResponse { + downloadUrl: string; +} + +export const FileDownloadUrlResponseSchema: z.ZodType = z.object({ + downloadUrl: z.string(), +}); diff --git a/src/lib/modules/forms/resource.ts b/src/lib/modules/forms/resource.ts index 1c1bd19..78ee1ab 100644 --- a/src/lib/modules/forms/resource.ts +++ b/src/lib/modules/forms/resource.ts @@ -4,8 +4,15 @@ import { buildSearchParams } from "src/transport/build-search-params"; import type { Transport } from "src/transport/http"; import { parseResponse } from "src/transport/parse-response"; +import { FormResponsesResponseSchema } from "../form-responses/schema"; +import type { FormResponse, FormResponsesResponse } from "../form-responses/schema"; + import { FormDataResponseSchema, FormsDataResponseSchema } from "./schema"; -import type { Form, FormsDataResponse } from "./schema"; +import type { Form, FormCreateRequest, FormsDataResponse } from "./schema"; + +export interface ListFormSubmissionsArgs extends ListArgs { + status?: "completed" | "pending"; +} export class FormsResource { readonly #transport: Transport; @@ -22,6 +29,12 @@ export class FormsResource { this.#validate = validateResponses; } + /** Create a form with its questions. */ + async create(body: FormCreateRequest): Promise
{ + const raw: unknown = await this.#transport.post("v1/forms", body); + return parseResponse({ schema: FormDataResponseSchema, data: raw, validate: this.#validate }); + } + /** List forms. */ async list(args: ListArgs = {}): Promise { const raw: unknown = await this.#transport.get("v1/forms", { @@ -36,6 +49,30 @@ export class FormsResource { return parseResponse({ schema: FormDataResponseSchema, data: raw, validate: this.#validate }); } + /** List a form's submissions (form responses). */ + async listSubmissions( + args: ListFormSubmissionsArgs & { id: string }, + ): Promise { + const { id, ...filters } = args; + const raw: unknown = await this.#transport.get(`v1/forms/${id}/form-responses`, { + searchParams: buildSearchParams(filters), + }); + return parseResponse({ + schema: FormResponsesResponseSchema, + data: raw, + validate: this.#validate, + }); + } + + /** Iterate over all of a form's submissions, automatically paginating. Default limit per page: 500. */ + async listAllSubmissions( + args: Omit & { id: string }, + ): Promise { + return paginate((listArgs) => this.listSubmissions({ ...args, ...listArgs }), { + limit: args.limit ?? 500, + }); + } + /** Iterate over all forms, automatically paginating. Default limit per page: 500. */ async listAll(args: Omit = {}): Promise { return paginate((listArgs) => this.list({ ...args, ...listArgs }), { diff --git a/src/lib/modules/forms/schema.ts b/src/lib/modules/forms/schema.ts index b2bc3ce..5597a99 100644 --- a/src/lib/modules/forms/schema.ts +++ b/src/lib/modules/forms/schema.ts @@ -4,8 +4,88 @@ import { z } from "zod"; // Base types // --------------------------------------------------------------------------- +export type FormFieldType = + | "date" + | "dropdown" + | "email" + | "fileUpload" + | "longAnswer" + | "multiSelect" + | "phoneNumber" + | "shortAnswer" + | "singleSelect" + | "title"; + +export const FormFieldTypeSchema: z.ZodType = z.enum([ + "shortAnswer", + "longAnswer", + "singleSelect", + "multiSelect", + "title", + "fileUpload", + "phoneNumber", + "email", + "date", + "dropdown", +]); + +export type FormVisibility = "allClients" | "requestedClients"; + +export const FormVisibilitySchema: z.ZodType = z.enum([ + "requestedClients", + "allClients", +]); + +export interface FormField { + formFieldId?: string; + hasOtherOption?: boolean; + isRequired?: boolean; + multipleChoiceOptions?: string[]; + title?: string; + type?: FormFieldType; +} + +export const FormFieldSchema: z.ZodType = z.object({ + formFieldId: z.string().optional(), + hasOtherOption: z.boolean().optional(), + isRequired: z.boolean().optional(), + multipleChoiceOptions: z.array(z.string()).optional(), + title: z.string().optional(), + type: FormFieldTypeSchema.optional(), +}); + +export interface FormFields { + allowMultipleSubmissions?: boolean; + formFieldIds?: string[]; + formFields?: FormField[]; + name?: string; + visibility?: FormVisibility; +} + +export const FormFieldsSchema: z.ZodType = z.object({ + allowMultipleSubmissions: z.boolean().optional(), + formFieldIds: z.array(z.string()).optional(), + formFields: z.array(FormFieldSchema).optional(), + name: z.string().optional(), + visibility: FormVisibilitySchema.optional(), +}); + +export interface FormAdditionalFields { + formResponseRequests?: number; + formResponseSubmissions?: number; + latestSubmissionDate?: string; +} + +export const FormAdditionalFieldsSchema: z.ZodType = z.object({ + formResponseRequests: z.number().optional(), + formResponseSubmissions: z.number().optional(), + latestSubmissionDate: z.string().optional(), +}); + export interface Form { + additionalFields?: FormAdditionalFields; createdAt: string; + fields?: FormFields; id: string; latestSubmissionDate?: string | null; name?: string; @@ -14,7 +94,9 @@ export interface Form { } export const FormSchema: z.ZodType = z.object({ + additionalFields: FormAdditionalFieldsSchema.optional(), createdAt: z.iso.datetime(), + fields: FormFieldsSchema.optional(), id: z.string(), latestSubmissionDate: z.string().nullable().optional(), name: z.string().optional(), @@ -38,3 +120,17 @@ export const FormsDataResponseSchema: z.ZodType = z.object({ data: z.array(FormSchema).nullable(), nextToken: z.string().optional(), }); + +// --------------------------------------------------------------------------- +// Request types +// --------------------------------------------------------------------------- + +export interface FormCreateRequest { + additionalFields: FormAdditionalFields; + fields: FormFields; +} + +export const FormCreateRequestSchema: z.ZodType = z.object({ + additionalFields: FormAdditionalFieldsSchema, + fields: FormFieldsSchema, +}); diff --git a/src/lib/modules/index.ts b/src/lib/modules/index.ts index 3262425..e914ca2 100644 --- a/src/lib/modules/index.ts +++ b/src/lib/modules/index.ts @@ -15,14 +15,17 @@ export * from "./internal-users/schema"; export * from "./invoice-templates/schema"; export * from "./invoices/schema"; export * from "./message-channels/schema"; +export * from "./me/schema"; export * from "./messages/schema"; export * from "./notes/schema"; export * from "./notifications/schema"; export * from "./payments/schema"; export * from "./prices/schema"; export * from "./products/schema"; +export * from "./refunds/schema"; export * from "./subscription-templates/schema"; export * from "./subscriptions/schema"; +export * from "./task-comments/schema"; export * from "./task-templates/schema"; export * from "./tasks/schema"; export * from "./workspace/schema"; diff --git a/src/lib/modules/internal-users/resource.ts b/src/lib/modules/internal-users/resource.ts index 471a592..95ed541 100644 --- a/src/lib/modules/internal-users/resource.ts +++ b/src/lib/modules/internal-users/resource.ts @@ -4,8 +4,17 @@ import { buildSearchParams } from "src/transport/build-search-params"; import type { Transport } from "src/transport/http"; import { parseResponse } from "src/transport/parse-response"; -import { InternalUserResponseSchema, InternalUsersResponseSchema } from "./schema"; -import type { InternalUser, InternalUsersResponse } from "./schema"; +import { + InternalUserNotificationSettingsSchema, + InternalUserResponseSchema, + InternalUsersResponseSchema, +} from "./schema"; +import type { + InternalUser, + InternalUserNotificationSettings, + InternalUserUpdateRequest, + InternalUsersResponse, +} from "./schema"; export class InternalUsersResource { readonly #transport: Transport; @@ -44,6 +53,26 @@ export class InternalUsersResource { }); } + /** Update an internal user's client-access scope. */ + async update(args: { id: string; body: InternalUserUpdateRequest }): Promise { + const raw: unknown = await this.#transport.patch(`v1/internal-users/${args.id}`, args.body); + return parseResponse({ + schema: InternalUserResponseSchema, + data: raw, + validate: this.#validate, + }); + } + + /** Retrieve an internal user's notification preferences. */ + async retrieveNotificationSettings(id: string): Promise { + const raw: unknown = await this.#transport.get(`v1/internal-users/${id}/notification-settings`); + return parseResponse({ + schema: InternalUserNotificationSettingsSchema, + data: raw, + validate: this.#validate, + }); + } + /** Iterate over all internal users, automatically paginating. Default limit per page: 500. */ async listAll(args: Omit = {}): Promise { return paginate((listArgs) => this.list({ ...listArgs }), { diff --git a/src/lib/modules/internal-users/schema.ts b/src/lib/modules/internal-users/schema.ts index a6930d6..e53b0f6 100644 --- a/src/lib/modules/internal-users/schema.ts +++ b/src/lib/modules/internal-users/schema.ts @@ -45,3 +45,54 @@ export const InternalUsersResponseSchema: z.ZodType = z.o data: z.array(InternalUserSchema), nextToken: z.string().optional(), }); + +// ─── Requests ───────────────────────────────────────────────────────────────── + +export interface InternalUserUpdateRequest { + companyAccessList?: string[]; + isClientAccessLimited?: boolean; +} + +export const InternalUserUpdateRequestSchema: z.ZodType = z.object({ + companyAccessList: z.array(z.string()).optional(), + isClientAccessLimited: z.boolean().optional(), +}); + +// ─── Notification settings ──────────────────────────────────────────────────── + +export type InternalUserEmailSetting = "always" | "no_emails" | "not_active"; + +export const InternalUserEmailSettingSchema: z.ZodType = z.enum([ + "always", + "not_active", + "no_emails", +]); + +export interface NotificationCategorySetting { + appId?: string; + disableEmail?: boolean; + disableInProduct?: boolean; + notificationSettingId?: string; +} + +export const NotificationCategorySettingSchema: z.ZodType = z.object({ + appId: z.string().optional(), + disableEmail: z.boolean().optional(), + disableInProduct: z.boolean().optional(), + notificationSettingId: z.string().optional(), +}); + +export interface InternalUserNotificationSettings { + disableInProduct?: boolean; + emailSettings?: InternalUserEmailSetting; + notifyAbout?: Record; + object?: string; +} + +export const InternalUserNotificationSettingsSchema: z.ZodType = + z.object({ + disableInProduct: z.boolean().optional(), + emailSettings: InternalUserEmailSettingSchema.optional(), + notifyAbout: z.record(z.string(), NotificationCategorySettingSchema).optional(), + object: z.string().optional(), + }); diff --git a/src/lib/modules/me/index.ts b/src/lib/modules/me/index.ts new file mode 100644 index 0000000..686fbd9 --- /dev/null +++ b/src/lib/modules/me/index.ts @@ -0,0 +1 @@ +export * from "./schema"; diff --git a/src/lib/modules/me/resource.ts b/src/lib/modules/me/resource.ts new file mode 100644 index 0000000..11d9eea --- /dev/null +++ b/src/lib/modules/me/resource.ts @@ -0,0 +1,31 @@ +import type { Transport } from "src/transport/http"; +import { parseResponse } from "src/transport/parse-response"; + +import { CurrentUserResponseSchema } from "./schema"; +import type { CurrentUser } from "./schema"; + +export class MeResource { + readonly #transport: Transport; + readonly #validate: boolean; + + constructor({ + transport, + validateResponses, + }: { + transport: Transport; + validateResponses: boolean; + }) { + this.#transport = transport; + this.#validate = validateResponses; + } + + /** Retrieve the user that owns the API key making the request. */ + async retrieve(): Promise { + const raw: unknown = await this.#transport.get("v1/me"); + return parseResponse({ + schema: CurrentUserResponseSchema, + data: raw, + validate: this.#validate, + }); + } +} diff --git a/src/lib/modules/me/schema.ts b/src/lib/modules/me/schema.ts new file mode 100644 index 0000000..b9c2252 --- /dev/null +++ b/src/lib/modules/me/schema.ts @@ -0,0 +1,22 @@ +import { z } from "zod"; + +// ─── Base ───────────────────────────────────────────────────────────────────── + +export interface CurrentUser { + email: string; + familyName: string; + givenName: string; + id: string; +} + +export const CurrentUserSchema: z.ZodType = z.object({ + email: z.string(), + familyName: z.string(), + givenName: z.string(), + id: z.string(), +}); + +// ─── Response ───────────────────────────────────────────────────────────────── + +export const CurrentUserResponseSchema: z.ZodType = CurrentUserSchema; +export type CurrentUserResponse = CurrentUser; diff --git a/src/lib/modules/message-channels/resource.ts b/src/lib/modules/message-channels/resource.ts index c7bc336..7c552db 100644 --- a/src/lib/modules/message-channels/resource.ts +++ b/src/lib/modules/message-channels/resource.ts @@ -4,11 +4,16 @@ import { buildSearchParams } from "src/transport/build-search-params"; import type { Transport } from "src/transport/http"; import { parseResponse } from "src/transport/parse-response"; -import { MessageChannelResponseSchema, MessageChannelsResponseSchema } from "./schema"; +import { + MessageChannelResponseSchema, + MessageChannelsResponseSchema, + UnreadMessageChannelsResponseSchema, +} from "./schema"; import type { MessageChannel, MessageChannelCreateRequest, MessageChannelsResponse, + UnreadMessageChannelsResponse, } from "./schema"; export class MessageChannelsResource { @@ -48,6 +53,18 @@ export class MessageChannelsResource { }); } + /** List the channels where a user has unread messages. Defaults to the authenticated user. */ + async listUnread(args: { userId?: string } = {}): Promise { + const raw: unknown = await this.#transport.get("v1/message-channels/unread", { + searchParams: buildSearchParams(args), + }); + return parseResponse({ + schema: UnreadMessageChannelsResponseSchema, + data: raw, + validate: this.#validate, + }); + } + /** Retrieve a single message channel by ID. */ async retrieve(id: string): Promise { const raw: unknown = await this.#transport.get(`v1/message-channels/${id}`); diff --git a/src/lib/modules/message-channels/schema.ts b/src/lib/modules/message-channels/schema.ts index ffa7791..d3862c8 100644 --- a/src/lib/modules/message-channels/schema.ts +++ b/src/lib/modules/message-channels/schema.ts @@ -12,11 +12,15 @@ export interface MessageChannel { createdAt: string; id: string; lastMessageDate?: string | null; + /** Populated by `listUnread()`; omitted otherwise. */ + lastRead?: string; memberIds?: string[]; /** @deprecated Use `clientId`/`companyId` instead. */ membershipEntityId?: string; membershipType: MembershipType; object: "messageChannel"; + /** Populated by `listUnread()`; omitted otherwise. */ + unreadCount?: number; updatedAt?: string; } @@ -26,10 +30,12 @@ export const MessageChannelSchema: z.ZodType = z.object({ createdAt: z.iso.datetime(), id: z.string(), lastMessageDate: z.string().nullable().optional(), + lastRead: z.string().optional(), memberIds: z.array(z.string()).optional(), membershipEntityId: z.string().optional(), membershipType: MembershipTypeSchema, object: z.literal("messageChannel"), + unreadCount: z.number().optional(), updatedAt: z.iso.datetime().optional(), }); @@ -50,6 +56,17 @@ export const MessageChannelsResponseSchema: z.ZodType = nextToken: z.string().optional(), }); +export interface UnreadMessageChannelsResponse { + totalUnreadCount?: number; + unreadChannels?: MessageChannel[]; +} + +export const UnreadMessageChannelsResponseSchema: z.ZodType = + z.object({ + totalUnreadCount: z.number().optional(), + unreadChannels: z.array(MessageChannelSchema).optional(), + }); + // --------------------------------------------------------------------------- // Request types // --------------------------------------------------------------------------- diff --git a/src/lib/modules/notifications/resource.ts b/src/lib/modules/notifications/resource.ts index e57ac24..a601498 100644 --- a/src/lib/modules/notifications/resource.ts +++ b/src/lib/modules/notifications/resource.ts @@ -42,6 +42,16 @@ export class NotificationsResource { }); } + /** Retrieve a single notification by ID. */ + async retrieve(id: string): Promise { + const raw = await this.#transport.get(`v1/notifications/${id}`); + return parseResponse({ + schema: NotificationResponseSchema, + data: raw, + validate: this.#validate, + }); + } + /** Delete a notification by ID. */ async delete(id: string): Promise { await this.#transport.delete(`v1/notifications/${id}`); diff --git a/src/lib/modules/prices/resource.ts b/src/lib/modules/prices/resource.ts index 4c67617..a121738 100644 --- a/src/lib/modules/prices/resource.ts +++ b/src/lib/modules/prices/resource.ts @@ -5,7 +5,7 @@ import type { Transport } from "src/transport/http"; import { parseResponse } from "src/transport/parse-response"; import { PriceResponseSchema, PricesResponseSchema } from "./schema"; -import type { Price, PricesResponse } from "./schema"; +import type { Price, PriceCreateRequest, PricesResponse } from "./schema"; export interface ListPricesArgs extends ListArgs { productId?: string; @@ -26,6 +26,12 @@ export class PricesResource { this.#validate = validateResponses; } + /** Create a price for an existing product. */ + async create(body: PriceCreateRequest): Promise { + const raw: unknown = await this.#transport.post("v1/prices", body); + return parseResponse({ schema: PriceResponseSchema, data: raw, validate: this.#validate }); + } + /** List prices with optional filters. */ async list(args: ListPricesArgs = {}): Promise { const raw: unknown = await this.#transport.get("v1/prices", { diff --git a/src/lib/modules/prices/schema.ts b/src/lib/modules/prices/schema.ts index 9769d09..971ed8d 100644 --- a/src/lib/modules/prices/schema.ts +++ b/src/lib/modules/prices/schema.ts @@ -38,3 +38,46 @@ export const PricesResponseSchema: z.ZodType = z.object({ data: z.array(PriceSchema).nullable(), nextToken: z.string().optional(), }); + +// ─── Requests ───────────────────────────────────────────────────────────────── + +export type BillingPeriod = + | "biannually" + | "day" + | "month" + | "quarterly" + | "semiannually" + | "week" + | "year"; + +export const BillingPeriodSchema: z.ZodType = z.enum([ + "day", + "week", + "month", + "quarterly", + "biannually", + "year", + "semiannually", +]); + +export type PriceType = "oneTime" | "recurring"; + +export const PriceTypeSchema: z.ZodType = z.enum(["oneTime", "recurring"]); + +export interface PriceCreateRequest { + amount: number; + currency?: string; + interval?: BillingPeriod; + intervalCount?: number; + productId: string; + type?: PriceType; +} + +export const PriceCreateRequestSchema: z.ZodType = z.object({ + amount: z.number(), + currency: z.string().optional(), + interval: BillingPeriodSchema.optional(), + intervalCount: z.number().optional(), + productId: z.string(), + type: PriceTypeSchema.optional(), +}); diff --git a/src/lib/modules/products/resource.ts b/src/lib/modules/products/resource.ts index 26e3903..c912781 100644 --- a/src/lib/modules/products/resource.ts +++ b/src/lib/modules/products/resource.ts @@ -5,7 +5,7 @@ import type { Transport } from "src/transport/http"; import { parseResponse } from "src/transport/parse-response"; import { ProductResponseSchema, ProductsResponseSchema } from "./schema"; -import type { Product, ProductsResponse } from "./schema"; +import type { Product, ProductCreateRequest, ProductsResponse } from "./schema"; export class ProductsResource { readonly #transport: Transport; @@ -22,6 +22,12 @@ export class ProductsResource { this.#validate = validateResponses; } + /** Create a product. */ + async create(body: ProductCreateRequest): Promise { + const raw: unknown = await this.#transport.post("v1/products", body); + return parseResponse({ schema: ProductResponseSchema, data: raw, validate: this.#validate }); + } + /** List products. */ async list(args: ListArgs = {}): Promise { const raw: unknown = await this.#transport.get("v1/products", { diff --git a/src/lib/modules/products/schema.ts b/src/lib/modules/products/schema.ts index aa77783..be1aec9 100644 --- a/src/lib/modules/products/schema.ts +++ b/src/lib/modules/products/schema.ts @@ -38,3 +38,17 @@ export const ProductsResponseSchema: z.ZodType = z.object({ data: z.array(ProductSchema).nullable(), nextToken: z.string().optional(), }); + +// ─── Requests ───────────────────────────────────────────────────────────────── + +export interface ProductCreateRequest { + imageUrls?: string[]; + name: string; + status?: "active" | "archived"; +} + +export const ProductCreateRequestSchema: z.ZodType = z.object({ + imageUrls: z.array(z.string()).optional(), + name: z.string(), + status: z.enum(["active", "archived"]).optional(), +}); diff --git a/src/lib/modules/refunds/index.ts b/src/lib/modules/refunds/index.ts new file mode 100644 index 0000000..686fbd9 --- /dev/null +++ b/src/lib/modules/refunds/index.ts @@ -0,0 +1 @@ +export * from "./schema"; diff --git a/src/lib/modules/refunds/resource.ts b/src/lib/modules/refunds/resource.ts new file mode 100644 index 0000000..e488dd4 --- /dev/null +++ b/src/lib/modules/refunds/resource.ts @@ -0,0 +1,49 @@ +import { paginate } from "src/lib/pagination"; +import type { ListArgs } from "src/lib/pagination"; +import { buildSearchParams } from "src/transport/build-search-params"; +import type { Transport } from "src/transport/http"; +import { parseResponse } from "src/transport/parse-response"; + +import { RefundResponseSchema, RefundsResponseSchema } from "./schema"; +import type { Refund, RefundCreateRequest, RefundsResponse } from "./schema"; + +export interface ListRefundsArgs extends ListArgs { + invoiceId: string; +} + +export class RefundsResource { + readonly #transport: Transport; + readonly #validate: boolean; + + constructor({ + transport, + validateResponses, + }: { + transport: Transport; + validateResponses: boolean; + }) { + this.#transport = transport; + this.#validate = validateResponses; + } + + /** Issue a refund against a paid invoice. */ + async create(body: RefundCreateRequest): Promise { + const raw: unknown = await this.#transport.post("v1/refunds", body); + return parseResponse({ schema: RefundResponseSchema, data: raw, validate: this.#validate }); + } + + /** List the refunds issued against an invoice. */ + async list(args: ListRefundsArgs): Promise { + const raw: unknown = await this.#transport.get("v1/refunds", { + searchParams: buildSearchParams(args), + }); + return parseResponse({ schema: RefundsResponseSchema, data: raw, validate: this.#validate }); + } + + /** Iterate over all refunds for an invoice, automatically paginating. Default limit per page: 500. */ + async listAll(args: Omit): Promise { + return paginate((listArgs) => this.list({ ...args, ...listArgs }), { + limit: args.limit ?? 500, + }); + } +} diff --git a/src/lib/modules/refunds/schema.ts b/src/lib/modules/refunds/schema.ts new file mode 100644 index 0000000..b09b10f --- /dev/null +++ b/src/lib/modules/refunds/schema.ts @@ -0,0 +1,68 @@ +import { z } from "zod"; + +// ─── Base ───────────────────────────────────────────────────────────────────── + +export type RefundStatus = "failed" | "pending" | "succeeded"; + +export const RefundStatusSchema: z.ZodType = z.enum([ + "pending", + "succeeded", + "failed", +]); + +export interface Refund { + amount: number; + completedAt?: string; + createdAt: string; + creatorId?: string; + failureReason?: string; + id: string; + identityId?: string; + invoiceId: string; + object: string; + receiptKey?: string; + ref?: string; + status: RefundStatus; + updatedAt?: string; +} + +export const RefundSchema: z.ZodType = z.object({ + amount: z.number(), + completedAt: z.string().optional(), + createdAt: z.string(), + creatorId: z.string().optional(), + failureReason: z.string().optional(), + id: z.string(), + identityId: z.string().optional(), + invoiceId: z.string(), + object: z.string(), + receiptKey: z.string().optional(), + ref: z.string().optional(), + status: RefundStatusSchema, + updatedAt: z.string().optional(), +}); + +// ─── Response ───────────────────────────────────────────────────────────────── + +export const RefundResponseSchema: z.ZodType = RefundSchema; +export type RefundResponse = Refund; + +export interface RefundsResponse { + data: Refund[] | null; + nextToken?: string; +} + +export const RefundsResponseSchema: z.ZodType = z.object({ + data: z.array(RefundSchema).nullable(), + nextToken: z.string().optional(), +}); + +// ─── Requests ───────────────────────────────────────────────────────────────── + +export interface RefundCreateRequest { + invoiceId: string; +} + +export const RefundCreateRequestSchema: z.ZodType = z.object({ + invoiceId: z.string(), +}); diff --git a/src/lib/modules/task-comments/index.ts b/src/lib/modules/task-comments/index.ts new file mode 100644 index 0000000..686fbd9 --- /dev/null +++ b/src/lib/modules/task-comments/index.ts @@ -0,0 +1 @@ +export * from "./schema"; diff --git a/src/lib/modules/task-comments/resource.ts b/src/lib/modules/task-comments/resource.ts new file mode 100644 index 0000000..0a4e39a --- /dev/null +++ b/src/lib/modules/task-comments/resource.ts @@ -0,0 +1,62 @@ +import { paginate } from "src/lib/pagination"; +import type { ListArgs } from "src/lib/pagination"; +import { buildSearchParams } from "src/transport/build-search-params"; +import type { Transport } from "src/transport/http"; +import { parseResponse } from "src/transport/parse-response"; + +import { TaskCommentResponseSchema, TaskCommentsResponseSchema } from "./schema"; +import type { TaskComment, TaskCommentsResponse } from "./schema"; + +export interface ListTaskCommentsArgs extends ListArgs { + taskId?: string; +} + +export class TaskCommentsResource { + readonly #transport: Transport; + readonly #validate: boolean; + + constructor({ + transport, + validateResponses, + }: { + transport: Transport; + validateResponses: boolean; + }) { + this.#transport = transport; + this.#validate = validateResponses; + } + + /** List task comments, optionally scoped to a single task. */ + async list(args: ListTaskCommentsArgs = {}): Promise { + const raw: unknown = await this.#transport.get("v1/comments", { + searchParams: buildSearchParams(args), + }); + return parseResponse({ + schema: TaskCommentsResponseSchema, + data: raw, + validate: this.#validate, + }); + } + + /** Retrieve a single task comment by ID. */ + async retrieve(commentId: string): Promise { + const raw: unknown = await this.#transport.get(`v1/comments/${commentId}`); + return parseResponse({ + schema: TaskCommentResponseSchema, + data: raw, + validate: this.#validate, + }); + } + + /** Delete a task comment by ID. */ + async delete(commentId: string): Promise { + await this.#transport.delete(`v1/comments/${commentId}`); + } + + /** Iterate over all task comments, automatically paginating. Default limit per page: 500. */ + async listAll(args: Omit = {}): Promise { + return paginate((listArgs) => this.list({ ...args, ...listArgs }), { + limit: args.limit ?? 500, + }); + } +} diff --git a/src/lib/modules/task-comments/schema.ts b/src/lib/modules/task-comments/schema.ts new file mode 100644 index 0000000..219bdc8 --- /dev/null +++ b/src/lib/modules/task-comments/schema.ts @@ -0,0 +1,40 @@ +import { z } from "zod"; + +// ─── Base ───────────────────────────────────────────────────────────────────── + +export interface TaskComment { + authorId: string; + content: string; + createdAt: string; + deleted?: boolean; + id: string; + object: string; + taskId: string; + updatedAt?: string; +} + +export const TaskCommentSchema: z.ZodType = z.object({ + authorId: z.string(), + content: z.string(), + createdAt: z.string(), + deleted: z.boolean().optional(), + id: z.string(), + object: z.string(), + taskId: z.string(), + updatedAt: z.string().optional(), +}); + +// ─── Response ───────────────────────────────────────────────────────────────── + +export const TaskCommentResponseSchema: z.ZodType = TaskCommentSchema; +export type TaskCommentResponse = TaskComment; + +export interface TaskCommentsResponse { + data: TaskComment[] | null; + nextToken?: string; +} + +export const TaskCommentsResponseSchema: z.ZodType = z.object({ + data: z.array(TaskCommentSchema).nullable(), + nextToken: z.string().optional(), +}); diff --git a/src/transport/http.ts b/src/transport/http.ts index 97370b4..b03176f 100644 --- a/src/transport/http.ts +++ b/src/transport/http.ts @@ -37,6 +37,8 @@ export interface RequestOpts { export interface Transport { get(path: string, opts?: RequestOpts): Promise; + /** GET returning raw bytes, for endpoints that stream `application/octet-stream`. */ + getRaw(path: string, opts?: RequestOpts): Promise; post(path: string, body?: unknown, opts?: RequestOpts): Promise; put(path: string, body?: unknown, opts?: RequestOpts): Promise; patch(path: string, body?: unknown, opts?: RequestOpts): Promise; @@ -180,6 +182,15 @@ export const createTransport = (options: TransportOptions): Transport => { .json(), ), + getRaw: (path: string, opts?: RequestOpts): Promise => + withErrorMapping( + api + .get(stripLeadingSlash(path), { + searchParams: opts?.searchParams, + }) + .arrayBuffer(), + ), + post: (path: string, body?: unknown, opts?: RequestOpts): Promise => withErrorMapping( api diff --git a/tests/resources.test.ts b/tests/resources.test.ts new file mode 100644 index 0000000..1dc9747 --- /dev/null +++ b/tests/resources.test.ts @@ -0,0 +1,209 @@ +import { beforeEach, describe, expect, it } from "vite-plus/test"; + +import { createAssemblyKit } from "src/client"; +import type { AssemblyKit } from "src/client"; + +interface Call { + body: unknown; + method: string; + url: string; +} + +let calls: Call[]; +let kit: AssemblyKit; + +const jsonBody = (request: Request): Promise => + request + .clone() + .text() + .then((text) => (text ? JSON.parse(text) : undefined)); + +beforeEach(() => { + calls = []; + kit = createAssemblyKit({ + apiKey: "test-key", + workspaceId: "ws-1", + validateResponses: false, + retryCount: 0, + fetch: async (input, init) => { + const request = new Request(input as Request | string, init); + calls.push({ + body: await jsonBody(request), + method: request.method, + url: request.url, + }); + return new Response("{}", { + headers: { "content-type": "application/json" }, + status: 200, + }); + }, + }); +}); + +const lastCall = (): Call => { + const call = calls.at(-1); + if (!call) throw new Error("no request was made"); + return call; +}; + +const lastPath = (): string => new URL(lastCall().url).pathname; + +const lastQuery = (): URLSearchParams => new URL(lastCall().url).searchParams; + +describe("newly added endpoints", () => { + it("clients.retrieveWithAppVisibility", async () => { + await kit.clients.retrieveWithAppVisibility("cl-1"); + expect(lastCall().method).toBe("GET"); + expect(lastPath()).toBe("/v1/clients/cl-1/app-visibility"); + }); + + it("companies.addClients", async () => { + await kit.companies.addClients({ + id: "co-1", + clientIds: ["cl-1", "cl-2"], + confirmPromotePlaceholder: true, + }); + expect(lastCall().method).toBe("POST"); + expect(lastPath()).toBe("/v1/companies/co-1/clients"); + expect(lastQuery().get("confirmPromotePlaceholder")).toBe("true"); + expect(lastCall().body).toEqual({ clientIds: ["cl-1", "cl-2"] }); + }); + + it("internalUsers.update", async () => { + await kit.internalUsers.update({ + id: "iu-1", + body: { isClientAccessLimited: true, companyAccessList: ["co-1"] }, + }); + expect(lastCall().method).toBe("PATCH"); + expect(lastPath()).toBe("/v1/internal-users/iu-1"); + expect(lastCall().body).toEqual({ + isClientAccessLimited: true, + companyAccessList: ["co-1"], + }); + }); + + it("internalUsers.retrieveNotificationSettings", async () => { + await kit.internalUsers.retrieveNotificationSettings("iu-1"); + expect(lastPath()).toBe("/v1/internal-users/iu-1/notification-settings"); + }); + + it("me.retrieve", async () => { + await kit.me.retrieve(); + expect(lastCall().method).toBe("GET"); + expect(lastPath()).toBe("/v1/me"); + }); + + it("customFields.create", async () => { + await kit.customFields.create({ + customFields: [{ entityType: "client", name: "Tier", type: "text" }], + }); + expect(lastCall().method).toBe("POST"); + expect(lastPath()).toBe("/v1/custom-fields"); + }); + + it("forms.create", async () => { + await kit.forms.create({ additionalFields: {}, fields: { name: "Intake" } }); + expect(lastCall().method).toBe("POST"); + expect(lastPath()).toBe("/v1/forms"); + expect(lastCall().body).toEqual({ additionalFields: {}, fields: { name: "Intake" } }); + }); + + it("forms.listSubmissions", async () => { + await kit.forms.listSubmissions({ id: "fm-1", status: "completed" }); + expect(lastPath()).toBe("/v1/forms/fm-1/form-responses"); + expect(lastQuery().get("status")).toBe("completed"); + }); + + it("products.create", async () => { + await kit.products.create({ name: "Retainer" }); + expect(lastCall().method).toBe("POST"); + expect(lastPath()).toBe("/v1/products"); + }); + + it("prices.create", async () => { + await kit.prices.create({ amount: 5000, productId: "pr-1", type: "recurring" }); + expect(lastCall().method).toBe("POST"); + expect(lastPath()).toBe("/v1/prices"); + }); + + it("refunds.create and refunds.list", async () => { + await kit.refunds.create({ invoiceId: "in-1" }); + expect(lastCall().method).toBe("POST"); + expect(lastPath()).toBe("/v1/refunds"); + + await kit.refunds.list({ invoiceId: "in-1" }); + expect(lastCall().method).toBe("GET"); + expect(lastQuery().get("invoiceId")).toBe("in-1"); + }); + + it("messageChannels.listUnread", async () => { + await kit.messageChannels.listUnread({ userId: "u-1" }); + expect(lastPath()).toBe("/v1/message-channels/unread"); + expect(lastQuery().get("userId")).toBe("u-1"); + }); + + it("notifications.retrieve", async () => { + await kit.notifications.retrieve("nt-1"); + expect(lastCall().method).toBe("GET"); + expect(lastPath()).toBe("/v1/notifications/nt-1"); + }); + + it("taskComments.list, retrieve, delete", async () => { + await kit.taskComments.list({ taskId: "tk-1" }); + expect(lastPath()).toBe("/v1/comments"); + expect(lastQuery().get("taskId")).toBe("tk-1"); + + await kit.taskComments.retrieve("cm-1"); + expect(lastPath()).toBe("/v1/comments/cm-1"); + + await kit.taskComments.delete("cm-1"); + expect(lastCall().method).toBe("DELETE"); + expect(lastPath()).toBe("/v1/comments/cm-1"); + }); + + it("files.retrieveDownloadUrl", async () => { + await kit.files.retrieveDownloadUrl("fl-1"); + expect(lastPath()).toBe("/v1/files/fl-1/download-url"); + }); + + it("files.download returns raw bytes", async () => { + const bytes = await kit.files.download("fl-1"); + expect(lastPath()).toBe("/v1/files/fl-1/download"); + expect(bytes).toBeInstanceOf(ArrayBuffer); + }); + + it("files.updateFolderPermissions", async () => { + await kit.files.updateFolderPermissions({ id: "fl-1", clientPermissions: "read_only" }); + expect(lastCall().method).toBe("PUT"); + expect(lastPath()).toBe("/v1/files/fl-1/permissions/"); + expect(lastCall().body).toEqual({ clientPermissions: "read_only" }); + }); + + it("files.updateRootFolderPermissions", async () => { + await kit.files.updateRootFolderPermissions({ + channelId: "ch-1", + clientPermissions: "read_write", + }); + expect(lastCall().method).toBe("PUT"); + expect(lastPath()).toBe("/v1/files/root/permissions"); + expect(lastCall().body).toEqual({ channelId: "ch-1", clientPermissions: "read_write" }); + }); + + it("appInstalls notification settings: get, create, update", async () => { + await kit.appInstalls.retrieveNotificationSettings("ai-1"); + expect(lastCall().method).toBe("GET"); + expect(lastPath()).toBe("/v1/installs/ai-1/notification-settings"); + + await kit.appInstalls.createNotificationSettings({ + installId: "ai-1", + body: { notifications: [{ label: "New thing", surfaces: ["product"] }] }, + }); + expect(lastCall().method).toBe("POST"); + + await kit.appInstalls.updateNotificationSettings({ + installId: "ai-1", + body: { notifications: [{ id: "ns-1" }] }, + }); + expect(lastCall().method).toBe("PATCH"); + }); +}); From 94408264692b35579d5ac53cf7191b07033d037b Mon Sep 17 00:00:00 2001 From: priosshrsth Date: Tue, 18 Aug 2026 08:53:48 +0000 Subject: [PATCH 2/2] fix(schemas): align response enums with the documented API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audited every z.enum in the SDK against assembly.com/docs/api-reference/openapi.json. Four were narrower than the API, so a documented value fails response validation and throws AssemblyResponseParseError for the whole payload — one client with an unlisted status discards every client in the page. - Client.status: add unknown, deleted (this is Sentry C1-APP-3, 170 events) - Client.creationMethod: add import - InvoiceStatus: add processing - TaskStatus: done -> completed. `done` is not in the spec and the live API never returns it; 45 of 51 sampled tasks are `completed`, so every one of them would have failed to parse. Breaking for anyone matching on "done". CustomFieldEntityType looked narrow too but is correct: the spec scopes it to client|company. Co-Authored-By: Claude Opus 5 (1M context) --- src/lib/modules/clients/schema.ts | 8 ++++---- src/lib/modules/invoices/schema.ts | 3 ++- src/lib/modules/tasks/schema.ts | 4 ++-- tests/schemas.test.ts | 18 ++++++++++++++---- 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/lib/modules/clients/schema.ts b/src/lib/modules/clients/schema.ts index 2e44029..5d0ba6d 100644 --- a/src/lib/modules/clients/schema.ts +++ b/src/lib/modules/clients/schema.ts @@ -6,7 +6,7 @@ export interface Client = Record = Record = z.enum([ "draft", "open", "paid", + "processing", "void", ]); diff --git a/src/lib/modules/tasks/schema.ts b/src/lib/modules/tasks/schema.ts index 71793e4..2dd185c 100644 --- a/src/lib/modules/tasks/schema.ts +++ b/src/lib/modules/tasks/schema.ts @@ -4,9 +4,9 @@ import { z } from "zod"; // Base types // --------------------------------------------------------------------------- -export type TaskStatus = "done" | "inProgress" | "todo"; +export type TaskStatus = "completed" | "inProgress" | "todo"; -export const TaskStatusSchema: z.ZodType = z.enum(["todo", "inProgress", "done"]); +export const TaskStatusSchema: z.ZodType = z.enum(["todo", "inProgress", "completed"]); export interface Task { assigneeId?: string; diff --git a/tests/schemas.test.ts b/tests/schemas.test.ts index c362b01..2e73212 100644 --- a/tests/schemas.test.ts +++ b/tests/schemas.test.ts @@ -176,6 +176,16 @@ describe("ClientSchema", () => { const { email: _, ...noEmail } = valid; expect(ClientSchema.safeParse(noEmail).success).toBe(false); }); + + it("accepts every status the API documents", () => { + for (const status of ["unknown", "deleted", "active", "notInvited", "invited"]) { + expect(ClientSchema.safeParse({ ...valid, status }).success).toBe(true); + } + }); + + it("accepts an imported client", () => { + expect(ClientSchema.safeParse({ ...valid, creationMethod: "import" }).success).toBe(true); + }); }); // ─── InternalUserSchema ─────────────────────────────────────────────────────── @@ -298,14 +308,14 @@ describe("CustomFieldSchema", () => { // ─── TaskStatusSchema ───────────────────────────────────────────────────────── describe("TaskStatusSchema", () => { - it("accepts todo, inProgress, done", () => { + it("accepts todo, inProgress, completed", () => { expect(TaskStatusSchema.safeParse("todo").success).toBe(true); expect(TaskStatusSchema.safeParse("inProgress").success).toBe(true); - expect(TaskStatusSchema.safeParse("done").success).toBe(true); + expect(TaskStatusSchema.safeParse("completed").success).toBe(true); }); it("rejects unknown status", () => { - expect(TaskStatusSchema.safeParse("completed").success).toBe(false); + expect(TaskStatusSchema.safeParse("done").success).toBe(false); }); }); @@ -391,7 +401,7 @@ describe("TasksResponseSchema", () => { createdAt: "2024-01-01T00:00:00.000Z", id: "t2", object: "task" as const, - status: "done", + status: "completed", updatedAt: "2024-01-02T00:00:00.000Z", }, ],