diff --git a/CHANGELOG.md b/CHANGELOG.md index 610c44f..7e05e40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Added `Registrar.restoreDomain` and `Registrar.getDomainRestore` to restore a domain and retrieve a domain restore, and the `DomainRestore` type. (#309) +- Added `Zones.batchChangeZoneRecords` to create, update, and delete zone + records in a single request, and the `BatchChangeZoneRecordsResponse` type. (#313) ### Changed diff --git a/lib/types.ts b/lib/types.ts index 287519e..573d58b 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -328,6 +328,12 @@ export type ZoneRecord = { updated_at: string; }; +export type BatchChangeZoneRecordsResponse = { + creates: Array; + updates: Array; + deletes: Array<{ id: number }>; +}; + export type Contact = { id: number; account_id: number; diff --git a/lib/zones.ts b/lib/zones.ts index 593e5b0..ee4fab6 100644 --- a/lib/zones.ts +++ b/lib/zones.ts @@ -416,6 +416,51 @@ export class Zones { return method; })(); + /** + * Performs multiple record operations (create, update and/or delete) for a zone in a single request for atomicity and efficiency. + * + * POST /{account}/zones/{zone}/batch + * + * @see https://developer.dnsimple.com/v2/zones/records/#batchChangeZoneRecords + * + * @param account The account id + * @param zone The zone name + * @param params Query parameters + */ + batchChangeZoneRecords = (() => { + const method = ( + account: number, + zone: string, + data: Partial<{ + creates: Array<{ + name: string; + type: string; + content: string; + ttl?: number; + priority?: number; + regions?: Array; + }>; + updates: Array<{ + id: number; + name?: string; + content?: string; + ttl?: number; + priority?: number; + regions?: Array; + }>; + deletes: Array<{ id: number }>; + }>, + params: QueryParams & {} = {} + ): Promise<{ data: types.BatchChangeZoneRecordsResponse }> => + this._client.request( + "POST", + `/${account}/zones/${zone}/batch`, + data, + params + ); + return method; + })(); + /** * Checks if a zone record is fully distributed to all our name servers across the globe. * diff --git a/test/zone_records.spec.ts b/test/zone_records.spec.ts index 45d142d..dd8f4e6 100644 --- a/test/zone_records.spec.ts +++ b/test/zone_records.spec.ts @@ -1,5 +1,5 @@ import fetchMock from "fetch-mock"; -import { NotFoundError } from "../lib/main"; +import { ClientError, NotFoundError } from "../lib/main"; import { createTestClient, responseFromFixture } from "./util"; const dnsimple = createTestClient(); @@ -311,4 +311,86 @@ describe("zone records", () => { }); }); }); + + describe("#batchChangeZoneRecords", () => { + const accountId = 1010; + const zoneId = "example.com"; + const attributes = { + creates: [ + { name: "ab", type: "A", content: "3.2.3.4" }, + { name: "ab", type: "A", content: "4.2.3.4" }, + ], + updates: [ + { id: 67622534, content: "3.2.3.40" }, + { id: 67622537, content: "5.2.3.40" }, + ], + deletes: [{ id: 67622509 }, { id: 67622527 }], + }; + + it("builds the correct request", async () => { + fetchMock.post( + "https://api.dnsimple.com/v2/1010/zones/example.com/batch", + responseFromFixture("batchChangeZoneRecords/success.http") + ); + + await dnsimple.zones.batchChangeZoneRecords( + accountId, + zoneId, + attributes + ); + + expect(fetchMock.callHistory.lastCall().options.body).toEqual( + JSON.stringify(attributes) + ); + }); + + it("produces the changed records", async () => { + fetchMock.post( + "https://api.dnsimple.com/v2/1010/zones/example.com/batch", + responseFromFixture("batchChangeZoneRecords/success.http") + ); + + const response = await dnsimple.zones.batchChangeZoneRecords( + accountId, + zoneId, + attributes + ); + + const { creates, updates, deletes } = response.data; + expect(creates.length).toBe(2); + expect(creates[0].id).toBe(67623409); + expect(creates[1].id).toBe(67623410); + expect(updates.length).toBe(2); + expect(updates[0].id).toBe(67622534); + expect(updates[1].id).toBe(67622537); + expect(deletes).toEqual([{ id: 67622509 }, { id: 67622527 }]); + }); + + describe("when the validation fails", () => { + it("produces an error", async () => { + fetchMock.post( + "https://api.dnsimple.com/v2/1010/zones/example.com/batch", + responseFromFixture( + "batchChangeZoneRecords/error_400_create_validation_failed.http" + ) + ); + + const error = await dnsimple.zones + .batchChangeZoneRecords(accountId, zoneId, { + creates: [{ name: "ab", type: "XYZ", content: "3.2.3.4" }], + }) + .catch((e) => e); + + expect(error).toBeInstanceOf(ClientError); + const clientError = error as ClientError; + expect(clientError.status).toBe(400); + expect(clientError.data.message).toBe("Validation failed"); + expect(clientError.attributeErrors().creates[0]).toEqual({ + index: 0, + message: "Validation failed", + errors: { record_type: ["unsupported"] }, + }); + }); + }); + }); });