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..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.object({ +const clientShape = { avatarImageUrl: z.string().nullable(), companyId: z.string().optional(), companyIds: z.array(z.string()).optional(), createdAt: z.iso.datetime(), - creationMethod: z.enum(["client", "directSignUp", "internalUser"]), + creationMethod: z.enum(["client", "directSignUp", "import", "internalUser"]), customFields: z.record(z.string(), z.unknown()).nullable().optional(), email: z.string(), fallbackColor: z.string().nullable(), @@ -39,8 +39,25 @@ export const ClientSchema: z.ZodType = z.object({ inviteUrl: z.string().optional(), lastLoginDate: z.string().nullable(), object: z.literal("client"), - status: z.enum(["active", "invited", "notInvited"]), + status: z.enum(["unknown", "deleted", "active", "notInvited", "invited"]), 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/invoices/schema.ts b/src/lib/modules/invoices/schema.ts index 8ead42a..5e4f6ca 100644 --- a/src/lib/modules/invoices/schema.ts +++ b/src/lib/modules/invoices/schema.ts @@ -4,12 +4,13 @@ import { z } from "zod"; // Base types // --------------------------------------------------------------------------- -export type InvoiceStatus = "draft" | "open" | "paid" | "void"; +export type InvoiceStatus = "draft" | "open" | "paid" | "processing" | "void"; export const InvoiceStatusSchema: z.ZodType = z.enum([ "draft", "open", "paid", + "processing", "void", ]); 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/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/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"); + }); +}); 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", }, ],