diff --git a/README.md b/README.md index 3e2621d..8839376 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ const make = new Make('your-api-key', 'eu2.make.com', { - **Keys** - API keys and secrets - **On-Prem Agents** - On-prem bridge agents running on customer infrastructure - **Organizations** - Top-level account and billing management +- **Private Spaces** - Per-user private workspaces within an organization (list, get, update) - **Scenarios** - Scenario management - **Teams** - Team management and collaboration - **Public Templates** - Public template discovery and blueprint export (read-only) @@ -209,6 +210,7 @@ All tools are organized into the following categories: - `keys` - `on-prem-agent` - `organizations` +- `private-spaces` - `scenarios` - `teams` - `public-templates` diff --git a/src/endpoints/organizations.ts b/src/endpoints/organizations.ts index ee19eae..826bc7e 100644 --- a/src/endpoints/organizations.ts +++ b/src/endpoints/organizations.ts @@ -99,6 +99,20 @@ export type Organization = { /** Webhook log retention period in days */ webhookLogRetentionDays?: number; }; + /** + * Private spaces the requesting user is a member of within this organization. + * Only returned when requested via `cols`; available on public cloud only. + */ + privateSpaces?: { + /** Unique identifier of the private space */ + id: number; + /** Name of the private space */ + name: string; + /** Whether the requesting user owns the space */ + isOwner: boolean; + /** Whether org admins can see into the space (mirrors the organization's "add admins as observers" setting) */ + hasAdminVisibility: boolean; + }[]; /** The name of the subscription for the organization */ serviceName: string; /** Teams in the organization */ diff --git a/src/endpoints/private-spaces.tools.ts b/src/endpoints/private-spaces.tools.ts new file mode 100644 index 0000000..7ea7e26 --- /dev/null +++ b/src/endpoints/private-spaces.tools.ts @@ -0,0 +1,106 @@ +import type { Make } from '../make.js'; +import type { MakeTool } from '../tools.js'; + +export const tools: MakeTool[] = [ + { + name: 'private-spaces_list', + title: 'List private spaces', + description: + "List the private spaces of an organization. Requires the organization's private-spaces feature to be enabled (error IM903 otherwise) and the 'personal team manage' permission. Private spaces cannot be created or deleted through the API — they are provisioned automatically by organization settings.", + category: 'private-spaces', + scope: 'private-spaces:read', + scopeId: 'organizationId', + identifier: 'organizationId', + annotations: { + readOnlyHint: true, + destructiveHint: false, + openWorldHint: false, + }, + inputSchema: { + type: 'object', + properties: { + organizationId: { type: 'number', description: 'The organization ID to list private spaces for' }, + externalId: { type: 'string', description: 'Filter private spaces by their external ID' }, + }, + required: ['organizationId'], + }, + examples: [{ organizationId: 5 }, { organizationId: 5, externalId: 'ext-1' }], + execute: async (make: Make, args: { organizationId: number; externalId?: string }) => { + const { organizationId, ...options } = args; + return await make.privateSpaces.list(organizationId, { ...options, cols: ['*'] }); + }, + }, + { + name: 'private-spaces_get', + title: 'Get private space', + description: + 'Get details of a specific private space, including usage totals (operations, transfer, centicredits). Callers who are not members of the space receive a 404 even when the space exists.', + category: 'private-spaces', + scope: 'private-spaces:read', + scopeId: 'privateSpaceId', + identifier: 'privateSpaceId', + resourceId: 'privateSpaceId', + annotations: { + readOnlyHint: true, + destructiveHint: false, + openWorldHint: false, + }, + inputSchema: { + type: 'object', + properties: { + privateSpaceId: { type: 'number', description: 'The private space ID to retrieve' }, + }, + required: ['privateSpaceId'], + }, + examples: [{ privateSpaceId: 101 }], + execute: async (make: Make, args: { privateSpaceId: number }) => { + return await make.privateSpaces.get(args.privateSpaceId, { cols: ['*'] }); + }, + }, + { + name: 'private-spaces_update', + title: 'Update private space', + description: + "Update a private space's operations limit. Set operationsLimit to null to remove the limit (unlimited); the transfer limit is derived automatically. When the new limit is below the space's current consumption the call fails with IM004 unless 'confirmed' is true — confirming pauses the space.", + category: 'private-spaces', + scope: 'private-spaces:write', + scopeId: 'privateSpaceId', + identifier: 'privateSpaceId', + resourceId: 'privateSpaceId', + annotations: { + readOnlyHint: false, + destructiveHint: true, + idempotentHint: true, + openWorldHint: false, + }, + inputSchema: { + type: 'object', + properties: { + privateSpaceId: { type: 'number', description: 'The private space ID to update' }, + operationsLimit: { + oneOf: [{ type: 'number' }, { type: 'null' }], + description: + 'Maximum operations limit (minimum 0). Pass null to remove the limit; omit to leave unchanged.', + }, + confirmed: { + type: 'boolean', + description: + "Confirmation of the update. Required when the new limit is below the space's current consumption; confirming pauses the space.", + }, + }, + required: ['privateSpaceId'], + }, + examples: [ + { privateSpaceId: 101, operationsLimit: 10000 }, + { privateSpaceId: 101, operationsLimit: 50, confirmed: true }, + { privateSpaceId: 101, operationsLimit: null }, + ], + execute: async ( + make: Make, + args: { privateSpaceId: number; operationsLimit?: number | null; confirmed?: boolean }, + ) => { + const { privateSpaceId, confirmed, ...body } = args; + return await make.privateSpaces.update(privateSpaceId, body, { confirmed }); + }, + }, +]; diff --git a/src/endpoints/private-spaces.ts b/src/endpoints/private-spaces.ts new file mode 100644 index 0000000..33e2b0b --- /dev/null +++ b/src/endpoints/private-spaces.ts @@ -0,0 +1,230 @@ +import type { FetchFunction, Pagination, PickColumns } from '../types.js'; + +/** + * Represents a private space in Make. + * A private space is a per-user personal workspace inside an organization where + * scenarios and related entities (including connections) stay visible only to the + * owner. Private spaces cannot be created or deleted through the API — they are + * provisioned automatically based on the organization's private-spaces settings. + */ +export type PrivateSpace = { + /** Unique identifier of the private space */ + id: number; + /** Name of the private space */ + name: string; + /** ID of the organization this private space belongs to */ + organizationId: number; + /** Whether Make global AI agents are enabled for the space */ + globalAgentsEnabled?: boolean; + /** Type of the underlying team; always `personal` for private spaces */ + type?: 'personal'; + /** Name of the space owner */ + privateSpaceOwnerName?: string; + /** Email of the space owner */ + privateSpaceOwnerEmail?: string; + /** User ID of the space owner */ + privateSpaceOwnerId?: number; + /** Maximum operations limit; null means unlimited */ + operationsLimit?: number | null; + /** + * Maximum data transfer limit in bytes; derived from the operations limit. + * Serialized as a string by `list()`/`get()` but as a number by `update()`. + */ + transferLimit?: string | number | null; + /** Number of operations consumed in the current period */ + consumedOperations?: number | null; + /** Amount of data transfer consumed in the current period, in bytes */ + consumedTransfer?: string | null; + /** Whether the space is paused due to exceeded limits */ + isPaused?: boolean | null; + /** Number of centicredits consumed in the current period, serialized as a string */ + consumedCenticredits?: string | null; + /** Total operations since the last reset; only selectable via `cols` on `get()` */ + operations?: string; + /** Total data transfer since the last reset, in bytes; only selectable via `cols` on `get()` */ + transfer?: string; + /** Total centicredits since the last reset; only selectable via `cols` on `get()` */ + centicredits?: string; + /** Whether the space is deleted; returned by `update()` */ + deleted?: boolean; + /** External identifier of the space; returned by `update()` */ + externalId?: string | null; +}; + +/** + * Options for listing private spaces. + * @template C Keys of the PrivateSpace type to include in the response + */ +export type ListPrivateSpacesOptions = { + /** Specific columns/fields to include in the response */ + cols?: C[] | ['*']; + /** Pagination options (the API supports sorting by `name` only) */ + pg?: Partial>; + /** Filter spaces by their external ID */ + externalId?: string; +}; + +/** + * Options for retrieving a private space. + * @template C Keys of the PrivateSpace type to include in the response + */ +export type GetPrivateSpaceOptions = { + /** + * Specific columns/fields to include in the response. In addition to the list + * columns, `get()` supports the usage totals `operations`, `transfer` and + * `centicredits` (computed from analytics storage; the API responds with 503 + * when that storage is unavailable). + */ + cols?: C[] | ['*']; +}; + +/** + * Body for updating a private space. + */ +export type UpdatePrivateSpaceBody = { + /** + * Maximum operations limit (minimum 0). Set to `null` to remove the limit + * (unlimited); omit to leave unchanged. The transfer limit is derived from + * this value by the API. + */ + operationsLimit?: number | null; +}; + +/** + * Options for updating a private space. + */ +export type UpdatePrivateSpaceOptions = { + /** + * Confirmation of the update. Required (the API fails with IM004 otherwise) + * when the new operations limit is below the space's current consumption; + * confirming pauses the space. + */ + confirmed?: boolean; +}; + +/** + * Response format for listing private spaces. + */ +type ListPrivateSpacesResponse = { + /** List of private spaces matching the query */ + privateSpaces: PickColumns[]; + /** Pagination information */ + pg: Pagination; +}; + +/** + * Response format for getting a private space. + */ +type GetPrivateSpaceResponse = { + /** The requested private space */ + privateSpace: PickColumns; +}; + +/** + * Response format for updating a private space. + */ +type UpdatePrivateSpaceResponse = { + /** The updated private space */ + privateSpace: PrivateSpace; +}; + +/** + * Class providing methods for working with Make private spaces. + * Requires the organization's private-spaces feature to be enabled; the API + * responds with error IM903 when it is not. + */ +export class PrivateSpaces { + readonly #fetch: FetchFunction; + + /** + * Create a new PrivateSpaces instance. + * @param fetch Function for making API requests + */ + constructor(fetch: FetchFunction) { + this.#fetch = fetch; + } + + /** + * List private spaces of an organization. + * Requires the `personal team manage` organization permission. + * @param organizationId The organization ID to list private spaces for + * @param options Optional parameters for filtering and pagination + * @returns Promise with the list of private spaces + * + * @example + * ```typescript + * const spaces = await make.privateSpaces.list(123); + * ``` + */ + async list( + organizationId: number, + options?: ListPrivateSpacesOptions, + ): Promise[]> { + return ( + await this.#fetch>('/private-spaces', { + query: { + organizationId, + externalId: options?.externalId, + cols: options?.cols, + pg: options?.pg, + }, + }) + ).privateSpaces; + } + + /** + * Get details of a specific private space. + * Requires the `personal team own view` organization permission; callers who are + * not members of the space receive a 404 even when the space exists. + * @param privateSpaceId The private space ID to get + * @param options Optional parameters for filtering returned fields + * @returns Promise with the private space information + * + * @example + * ```typescript + * const space = await make.privateSpaces.get(101); + * ``` + */ + async get( + privateSpaceId: number, + options?: GetPrivateSpaceOptions, + ): Promise> { + return ( + await this.#fetch>(`/private-spaces/${privateSpaceId}`, { + query: { + cols: options?.cols, + }, + }) + ).privateSpace; + } + + /** + * Update a private space. + * Requires the `personal team manage` organization permission. + * @param privateSpaceId The private space ID to update + * @param body The fields to update + * @param options Optional update options + * @returns Promise with the updated private space + * + * @example + * ```typescript + * // Remove the operations limit + * const space = await make.privateSpaces.update(101, { operationsLimit: null }); + * ``` + */ + async update( + privateSpaceId: number, + body: UpdatePrivateSpaceBody, + options?: UpdatePrivateSpaceOptions, + ): Promise { + return ( + await this.#fetch(`/private-spaces/${privateSpaceId}`, { + method: 'PATCH', + query: { + confirmed: options?.confirmed, + }, + body, + }) + ).privateSpace; + } +} diff --git a/src/index.ts b/src/index.ts index b414cec..1759e96 100644 --- a/src/index.ts +++ b/src/index.ts @@ -199,4 +199,12 @@ export type { ListPublicTemplatesOptions, GetPublicTemplateOptions, } from './endpoints/public-templates.js'; +export type { + PrivateSpace, + PrivateSpaces, + ListPrivateSpacesOptions, + GetPrivateSpaceOptions, + UpdatePrivateSpaceBody, + UpdatePrivateSpaceOptions, +} from './endpoints/private-spaces.js'; export type { User, Users } from './endpoints/users.js'; diff --git a/src/make.ts b/src/make.ts index 1920184..a8ad417 100644 --- a/src/make.ts +++ b/src/make.ts @@ -18,6 +18,7 @@ import { Enums } from './endpoints/enums.js'; import { OnPremAgents } from './endpoints/on-prem-agents.js'; import { ConnectedSystems } from './endpoints/connected-systems.js'; import { PublicTemplates } from './endpoints/public-templates.js'; +import { PrivateSpaces } from './endpoints/private-spaces.js'; import { SDKApps } from './endpoints/sdk/apps.js'; import { SDKModules } from './endpoints/sdk/modules.js'; import { SDKConnections } from './endpoints/sdk/connections.js'; @@ -185,6 +186,13 @@ export class Make { */ public readonly publicTemplates: PublicTemplates; + /** + * Access to private space endpoints. + * Private spaces are per-user personal workspaces within an organization; they are + * provisioned automatically and cannot be created or deleted through the API. + */ + public readonly privateSpaces: PrivateSpaces; + /** * Access to SDK-related endpoints */ @@ -278,6 +286,7 @@ export class Make { this.connectedSystems = new ConnectedSystems(this.fetch.bind(this)); this.credentialRequests = new CredentialRequests(this.fetch.bind(this)); this.publicTemplates = new PublicTemplates(this.fetch.bind(this)); + this.privateSpaces = new PrivateSpaces(this.fetch.bind(this)); this.sdk = { apps: new SDKApps(this.fetch.bind(this)), modules: new SDKModules(this.fetch.bind(this)), diff --git a/src/tools.ts b/src/tools.ts index cd0ae82..5fc31ab 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -29,6 +29,7 @@ import { tools as EnumsTools } from './endpoints/enums.tools.js'; import { tools as OnPremAgentsTools } from './endpoints/on-prem-agents.tools.js'; import { tools as ConnectedSystemsTools } from './endpoints/connected-systems.tools.js'; import { tools as PublicTemplatesTools } from './endpoints/public-templates.tools.js'; +import { tools as PrivateSpacesTools } from './endpoints/private-spaces.tools.js'; /** * JSON Schema definition for input parameters. @@ -231,4 +232,5 @@ export const MakeTools = [ ...ConnectedSystemsTools, ...EnumsTools, ...PublicTemplatesTools, + ...PrivateSpacesTools, ] as MakeTool[]; diff --git a/test/mocks/organizations/get.json b/test/mocks/organizations/get.json index d446caa..93a0b71 100644 --- a/test/mocks/organizations/get.json +++ b/test/mocks/organizations/get.json @@ -33,6 +33,14 @@ "id": 16, "name": "My Team" } + ], + "privateSpaces": [ + { + "id": 101, + "name": "Becca's space", + "isOwner": true, + "hasAdminVisibility": false + } ] } } diff --git a/test/mocks/organizations/list.json b/test/mocks/organizations/list.json index 8995084..a1a18bc 100644 --- a/test/mocks/organizations/list.json +++ b/test/mocks/organizations/list.json @@ -3,7 +3,15 @@ { "id": 5, "name": "New organization", - "timezoneId": 113 + "timezoneId": 113, + "privateSpaces": [ + { + "id": 101, + "name": "Becca's space", + "isOwner": true, + "hasAdminVisibility": false + } + ] }, { "id": 6, diff --git a/test/mocks/private-spaces/get.json b/test/mocks/private-spaces/get.json new file mode 100644 index 0000000..6249ebc --- /dev/null +++ b/test/mocks/private-spaces/get.json @@ -0,0 +1,21 @@ +{ + "privateSpace": { + "id": 101, + "name": "Becca's space", + "organizationId": 5, + "globalAgentsEnabled": false, + "type": "personal", + "privateSpaceOwnerName": "Becca Smith", + "privateSpaceOwnerEmail": "becca.smith@example.com", + "privateSpaceOwnerId": 42, + "operationsLimit": 1000, + "transferLimit": "1073741824", + "consumedOperations": 250, + "consumedTransfer": "52428800", + "isPaused": false, + "consumedCenticredits": "12345", + "operations": "250", + "transfer": "52428800", + "centicredits": "12345" + } +} diff --git a/test/mocks/private-spaces/list.json b/test/mocks/private-spaces/list.json new file mode 100644 index 0000000..66f9fc8 --- /dev/null +++ b/test/mocks/private-spaces/list.json @@ -0,0 +1,42 @@ +{ + "privateSpaces": [ + { + "id": 101, + "name": "Becca's space", + "organizationId": 5, + "globalAgentsEnabled": false, + "type": "personal", + "privateSpaceOwnerName": "Becca Smith", + "privateSpaceOwnerEmail": "becca.smith@example.com", + "privateSpaceOwnerId": 42, + "operationsLimit": 1000, + "transferLimit": "536870912", + "consumedOperations": 250, + "consumedTransfer": "52428800", + "isPaused": false, + "consumedCenticredits": "12345" + }, + { + "id": 102, + "name": "Jan's space", + "organizationId": 5, + "globalAgentsEnabled": true, + "type": "personal", + "privateSpaceOwnerName": "Jan Novak", + "privateSpaceOwnerEmail": "jan.novak@example.com", + "privateSpaceOwnerId": 43, + "operationsLimit": null, + "transferLimit": null, + "consumedOperations": 25, + "consumedTransfer": "1048576", + "isPaused": false, + "consumedCenticredits": "1200" + } + ], + "pg": { + "sortBy": "name", + "sortDir": "asc", + "offset": 0, + "limit": 100 + } +} diff --git a/test/mocks/private-spaces/update.json b/test/mocks/private-spaces/update.json new file mode 100644 index 0000000..144b15d --- /dev/null +++ b/test/mocks/private-spaces/update.json @@ -0,0 +1,20 @@ +{ + "privateSpace": { + "id": 101, + "name": "Becca's space", + "organizationId": 5, + "globalAgentsEnabled": false, + "type": "personal", + "privateSpaceOwnerName": "Becca Smith", + "privateSpaceOwnerEmail": "becca.smith@example.com", + "privateSpaceOwnerId": 42, + "operationsLimit": 100, + "transferLimit": 107374182, + "consumedOperations": 250, + "consumedTransfer": "52428800", + "isPaused": true, + "consumedCenticredits": "12345", + "deleted": false, + "externalId": null + } +} diff --git a/test/organizations.spec.ts b/test/organizations.spec.ts index eed27d6..8001503 100644 --- a/test/organizations.spec.ts +++ b/test/organizations.spec.ts @@ -96,4 +96,18 @@ describe('Endpoints: Organizations', () => { await make.organizations.delete(organizationId); }); + + it('Should list organizations with the privateSpaces column', async () => { + const cols: (keyof Organization)[] = ['id', 'name', 'privateSpaces']; + mockFetch( + `GET https://make.local/api/v2/organizations?cols%5B%5D=${cols.join('&cols%5B%5D=')}`, + organizationsListMock, + ); + + const result = await make.organizations.list({ + cols, + }); + + expect(result).toStrictEqual(organizationsListMock.organizations); + }); }); diff --git a/test/private-spaces-tools.spec.ts b/test/private-spaces-tools.spec.ts new file mode 100644 index 0000000..fc31802 --- /dev/null +++ b/test/private-spaces-tools.spec.ts @@ -0,0 +1,88 @@ +import { describe, expect, it } from '@jest/globals'; +import { Make } from '../src/make.js'; +import { MakeTools } from '../src/tools.js'; +import { mockFetch } from './test.utils.js'; + +import * as privateSpacesListMock from './mocks/private-spaces/list.json'; +import * as privateSpaceGetMock from './mocks/private-spaces/get.json'; +import * as privateSpaceUpdateMock from './mocks/private-spaces/update.json'; + +const MAKE_API_KEY = 'api-key'; +const MAKE_ZONE = 'make.local'; +const ORGANIZATION_ID = 5; +const PRIVATE_SPACE_ID = 101; + +function getTool(name: string) { + const tool = MakeTools.find(entry => entry.name === name); + if (!tool) { + throw new Error(`Missing MCP tool: ${name}`); + } + return tool; +} + +describe('MCP tools: private-spaces', () => { + const make = new Make(MAKE_API_KEY, MAKE_ZONE); + + it('Should execute private-spaces_list', async () => { + mockFetch( + `GET https://make.local/api/v2/private-spaces?organizationId=${ORGANIZATION_ID}&cols%5B%5D=*`, + privateSpacesListMock, + ); + + const tool = getTool('private-spaces_list'); + const result = await tool.execute(make, { organizationId: ORGANIZATION_ID }); + + expect(result).toStrictEqual(privateSpacesListMock.privateSpaces); + }); + + it('Should execute private-spaces_get', async () => { + mockFetch(`GET https://make.local/api/v2/private-spaces/${PRIVATE_SPACE_ID}?cols%5B%5D=*`, privateSpaceGetMock); + + const tool = getTool('private-spaces_get'); + const result = await tool.execute(make, { privateSpaceId: PRIVATE_SPACE_ID }); + + expect(result).toStrictEqual(privateSpaceGetMock.privateSpace); + }); + + it('Should execute private-spaces_update', async () => { + mockFetch( + `PATCH https://make.local/api/v2/private-spaces/${PRIVATE_SPACE_ID}?confirmed=true`, + privateSpaceUpdateMock, + req => { + expect(req.body).toStrictEqual({ operationsLimit: 100 }); + }, + ); + + const tool = getTool('private-spaces_update'); + const result = await tool.execute(make, { + privateSpaceId: PRIVATE_SPACE_ID, + operationsLimit: 100, + confirmed: true, + }); + + expect(result).toStrictEqual(privateSpaceUpdateMock.privateSpace); + }); + + it('Should execute private-spaces_list with externalId filter', async () => { + mockFetch( + `GET https://make.local/api/v2/private-spaces?organizationId=${ORGANIZATION_ID}&externalId=ext-1&cols%5B%5D=*`, + privateSpacesListMock, + ); + + const tool = getTool('private-spaces_list'); + const result = await tool.execute(make, { organizationId: ORGANIZATION_ID, externalId: 'ext-1' }); + + expect(result).toStrictEqual(privateSpacesListMock.privateSpaces); + }); + + it('Should execute private-spaces_update without confirmation and null limit', async () => { + mockFetch(`PATCH https://make.local/api/v2/private-spaces/${PRIVATE_SPACE_ID}`, privateSpaceUpdateMock, req => { + expect(req.body).toStrictEqual({ operationsLimit: null }); + }); + + const tool = getTool('private-spaces_update'); + const result = await tool.execute(make, { privateSpaceId: PRIVATE_SPACE_ID, operationsLimit: null }); + + expect(result).toStrictEqual(privateSpaceUpdateMock.privateSpace); + }); +}); diff --git a/test/private-spaces.integration.test.ts b/test/private-spaces.integration.test.ts new file mode 100644 index 0000000..d34b40a --- /dev/null +++ b/test/private-spaces.integration.test.ts @@ -0,0 +1,62 @@ +import 'dotenv/config'; +import { describe, expect, it } from '@jest/globals'; +import { Make } from '../src/make.js'; + +const MAKE_API_KEY = String(process.env.MAKE_API_KEY || ''); +const MAKE_ZONE = String(process.env.MAKE_ZONE || ''); +const MAKE_ORGANIZATION = Number(process.env.MAKE_ORGANIZATION || 0); + +describe('Integration: PrivateSpaces', () => { + const make = new Make(MAKE_API_KEY, MAKE_ZONE); + + let privateSpaceId: number | undefined; + let originalOperationsLimit: number | null | undefined; + let consumedOperations: number | null | undefined; + + it('Should list private spaces', async () => { + const spaces = await make.privateSpaces.list(MAKE_ORGANIZATION); + + expect(Array.isArray(spaces)).toBe(true); + + // Prefer the caller's own space: get() returns 404 for non-admin non-members, and update() mutates live state. + const me = await make.users.me(); + privateSpaceId = (spaces.find(space => space.privateSpaceOwnerEmail === me.email) ?? spaces[0])?.id; + }); + + it('Should get a private space', async () => { + if (privateSpaceId === undefined) return; + + const space = await make.privateSpaces.get(privateSpaceId, { cols: ['*'] }); + + expect(space.id).toBe(privateSpaceId); + expect(space.organizationId).toBe(MAKE_ORGANIZATION); + expect(space.type).toBe('personal'); + + originalOperationsLimit = space.operationsLimit; + consumedOperations = space.consumedOperations; + }); + + it('Should update a private space and restore the original limit', async () => { + if (privateSpaceId === undefined) return; + + // Either value missing means get() never ran or failed — we can't safely reconstruct the original state. + if (originalOperationsLimit === undefined || consumedOperations === undefined) return; + + // Stay above current consumption so the update needs no confirmation and cannot pause the space. + const safeLimit = Math.max(consumedOperations ?? 0, originalOperationsLimit ?? 0) + 10000; + + try { + const updated = await make.privateSpaces.update(privateSpaceId, { operationsLimit: safeLimit }); + expect(updated.operationsLimit).toBe(safeLimit); + } finally { + // Restore even when the assertion above fails — the limit is live, billing-relevant state. + const needsConfirm = (originalOperationsLimit ?? Infinity) < (consumedOperations ?? 0); + const restored = await make.privateSpaces.update( + privateSpaceId, + { operationsLimit: originalOperationsLimit ?? null }, + needsConfirm ? { confirmed: true } : {}, + ); + expect(restored.operationsLimit).toBe(originalOperationsLimit ?? null); + } + }); +}); diff --git a/test/private-spaces.spec.ts b/test/private-spaces.spec.ts new file mode 100644 index 0000000..8cfbb5b --- /dev/null +++ b/test/private-spaces.spec.ts @@ -0,0 +1,105 @@ +import { describe, expect, it } from '@jest/globals'; +import { Make } from '../src/make.js'; +import { mockFetch } from './test.utils.js'; +import type { PrivateSpace } from '../src/endpoints/private-spaces.js'; + +import * as privateSpacesListMock from './mocks/private-spaces/list.json'; +import * as privateSpaceGetMock from './mocks/private-spaces/get.json'; +import * as privateSpaceUpdateMock from './mocks/private-spaces/update.json'; + +const MAKE_API_KEY = 'api-key'; +const MAKE_ZONE = 'make.local'; + +describe('Endpoints: PrivateSpaces', () => { + const make = new Make(MAKE_API_KEY, MAKE_ZONE); + + it('Should list private spaces', async () => { + mockFetch('GET https://make.local/api/v2/private-spaces?organizationId=5', privateSpacesListMock); + + const result = await make.privateSpaces.list(5); + + expect(result).toStrictEqual(privateSpacesListMock.privateSpaces); + }); + + it('Should list private spaces filtered by externalId with pagination', async () => { + mockFetch( + 'GET https://make.local/api/v2/private-spaces?organizationId=5&externalId=ext-1&pg%5BsortBy%5D=name&pg%5BsortDir%5D=asc', + privateSpacesListMock, + ); + + const result = await make.privateSpaces.list(5, { + externalId: 'ext-1', + pg: { + sortBy: 'name', + sortDir: 'asc', + }, + }); + + expect(result).toStrictEqual(privateSpacesListMock.privateSpaces); + }); + + it('Should list private spaces with selected columns', async () => { + const cols: (keyof PrivateSpace)[] = ['id', 'name', 'isPaused']; + mockFetch( + `GET https://make.local/api/v2/private-spaces?organizationId=5&cols%5B%5D=${cols.join('&cols%5B%5D=')}`, + privateSpacesListMock, + ); + + const result = await make.privateSpaces.list(5, { + cols, + }); + + expect(result).toStrictEqual(privateSpacesListMock.privateSpaces); + }); + + it('Should get a private space', async () => { + mockFetch('GET https://make.local/api/v2/private-spaces/101', privateSpaceGetMock); + + const result = await make.privateSpaces.get(101); + + expect(result).toStrictEqual(privateSpaceGetMock.privateSpace); + }); + + it('Should get a private space with usage columns', async () => { + const cols: (keyof PrivateSpace)[] = ['id', 'operations', 'transfer', 'centicredits']; + mockFetch( + `GET https://make.local/api/v2/private-spaces/101?cols%5B%5D=${cols.join('&cols%5B%5D=')}`, + privateSpaceGetMock, + ); + + const result = await make.privateSpaces.get(101, { + cols, + }); + + expect(result).toStrictEqual(privateSpaceGetMock.privateSpace); + }); + + it('Should update a private space', async () => { + const body = { + operationsLimit: 100, + }; + + mockFetch('PATCH https://make.local/api/v2/private-spaces/101', privateSpaceUpdateMock, req => { + expect(req.body).toStrictEqual(body); + expect(req.headers.get('content-type')).toBe('application/json'); + }); + + const result = await make.privateSpaces.update(101, body); + + expect(result).toStrictEqual(privateSpaceUpdateMock.privateSpace); + }); + + it('Should update a private space with confirmation and null limit', async () => { + const body = { + operationsLimit: null, + }; + + mockFetch('PATCH https://make.local/api/v2/private-spaces/101?confirmed=true', privateSpaceUpdateMock, req => { + expect(req.body).toStrictEqual(body); + }); + + const result = await make.privateSpaces.update(101, body, { confirmed: true }); + + expect(result).toStrictEqual(privateSpaceUpdateMock.privateSpace); + }); +});