| 1 | import * as dns from "@distilled.cloud/cloudflare/dns"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as Predicate from "effect/Predicate"; |
| 4 | |
| 5 | import * as Provider from "../../Provider.ts"; |
| 6 | import { Resource } from "../../Resource.ts"; |
| 7 | import { CloudflareEnvironment } from "../CloudflareEnvironment.ts"; |
| 8 | import type { Providers } from "../Providers.ts"; |
| 9 | |
| 10 | const TypeId = "Cloudflare.DNS.AccountSettings" as const; |
| 11 | type TypeId = typeof TypeId; |
| 12 | |
| 13 | /** |
| 14 | * Default nameserver assignment for new zones in the account. |
| 15 | */ |
| 16 | export interface AccountDnsNameservers { |
| 17 | /** |
| 18 | * Nameserver kind: Cloudflare's standard pair, a random standard |
| 19 | * assignment, or custom nameservers defined at the account or tenant |
| 20 | * level. |
| 21 | */ |
| 22 | type: |
| 23 | | "cloudflare.standard" |
| 24 | | "cloudflare.standard.random" |
| 25 | | "custom.account" |
| 26 | | "custom.tenant" |
| 27 | | (string & {}); |
| 28 | } |
| 29 | |
| 30 | /** |
| 31 | * Components of the default SOA record for new zones. Every field is |
| 32 | * optional — omitted fields keep their current value. |
| 33 | */ |
| 34 | export interface AccountDnsSoa { |
| 35 | /** Time in seconds after which secondaries stop answering (`expire`). */ |
| 36 | expire?: number; |
| 37 | /** Negative-caching TTL in seconds (`minimum`). */ |
| 38 | minTtl?: number; |
| 39 | /** Primary nameserver (`MNAME`). */ |
| 40 | mname?: string; |
| 41 | /** Secondary refresh interval in seconds. */ |
| 42 | refresh?: number; |
| 43 | /** Secondary retry interval in seconds. */ |
| 44 | retry?: number; |
| 45 | /** Zone administrator mailbox (`RNAME`). */ |
| 46 | rname?: string; |
| 47 | /** TTL of the SOA record itself. */ |
| 48 | ttl?: number; |
| 49 | } |
| 50 | |
| 51 | /** |
| 52 | * Default DNS settings applied to new zones in the account. Only the |
| 53 | * fields you provide are compared and patched. |
| 54 | */ |
| 55 | export interface AccountDnsZoneDefaults { |
| 56 | /** |
| 57 | * Flatten all CNAME records by default. |
| 58 | * @default false |
| 59 | */ |
| 60 | flattenAllCnames?: boolean; |
| 61 | /** |
| 62 | * Enable Foundation DNS Advanced Nameservers by default (paid |
| 63 | * add-on). |
| 64 | * @default false |
| 65 | */ |
| 66 | foundationDns?: boolean; |
| 67 | /** |
| 68 | * Default settings for internal zones (Enterprise Internal DNS only). |
| 69 | */ |
| 70 | internalDns?: { |
| 71 | /** Zone to resolve from when an internal zone has no match. */ |
| 72 | referenceZoneId?: string; |
| 73 | }; |
| 74 | /** |
| 75 | * Enable multi-provider DNS by default. |
| 76 | * @default false |
| 77 | */ |
| 78 | multiProvider?: boolean; |
| 79 | /** |
| 80 | * Default nameserver assignment for new zones. |
| 81 | * @default { type: "cloudflare.standard" } |
| 82 | */ |
| 83 | nameservers?: AccountDnsNameservers; |
| 84 | /** |
| 85 | * Default TTL (seconds) of zones' NS records. |
| 86 | * @default 86400 |
| 87 | */ |
| 88 | nsTtl?: number; |
| 89 | /** |
| 90 | * Allow secondary zones to use proxied override records and CNAME |
| 91 | * flattening at the apex by default. |
| 92 | * @default false |
| 93 | */ |
| 94 | secondaryOverrides?: boolean; |
| 95 | /** |
| 96 | * Components of the default SOA record for new zones. |
| 97 | */ |
| 98 | soa?: AccountDnsSoa; |
| 99 | /** |
| 100 | * Default zone mode for new zones. |
| 101 | * @default "standard" |
| 102 | */ |
| 103 | zoneMode?: "standard" | "cdn_only" | "dns_only"; |
| 104 | } |
| 105 | |
| 106 | export interface AccountDnsSettingsProps { |
| 107 | /** |
| 108 | * Force all proxied DNS records in the account to behave as DNS-only |
| 109 | * at the edge, regardless of each record's individual proxy setting. |
| 110 | * |
| 111 | * Mutable — patched in place. |
| 112 | * @default false |
| 113 | */ |
| 114 | enforceDnsOnly?: boolean; |
| 115 | /** |
| 116 | * Default DNS settings applied to new zones created in the account. |
| 117 | * Only the fields you declare are compared and patched; the rest keep |
| 118 | * their live values. |
| 119 | * |
| 120 | * Mutable — patched in place. |
| 121 | */ |
| 122 | zoneDefaults?: AccountDnsZoneDefaults; |
| 123 | } |
| 124 | |
| 125 | /** |
| 126 | * Fully-resolved snapshot of the account's DNS settings as Cloudflare |
| 127 | * reports them (`GET /accounts/{account_id}/dns_settings`). |
| 128 | */ |
| 129 | export interface AccountDnsSettingsSnapshot { |
| 130 | /** Whether proxied records are forced to DNS-only. */ |
| 131 | enforceDnsOnly: boolean; |
| 132 | /** Resolved zone defaults. */ |
| 133 | zoneDefaults: { |
| 134 | /** Whether all CNAMEs are flattened by default. */ |
| 135 | flattenAllCnames: boolean; |
| 136 | /** Whether Foundation DNS is enabled by default. */ |
| 137 | foundationDns: boolean; |
| 138 | /** Default internal DNS reference zone, if configured. */ |
| 139 | internalDns: { referenceZoneId: string | undefined }; |
| 140 | /** Whether multi-provider DNS is enabled by default. */ |
| 141 | multiProvider: boolean; |
| 142 | /** Default nameserver assignment. */ |
| 143 | nameservers: { type: string }; |
| 144 | /** Default TTL of zones' NS records. */ |
| 145 | nsTtl: number; |
| 146 | /** Whether secondary overrides are enabled by default. */ |
| 147 | secondaryOverrides: boolean; |
| 148 | /** Resolved default SOA components. */ |
| 149 | soa: { |
| 150 | expire: number | undefined; |
| 151 | minTtl: number | undefined; |
| 152 | mname: string | undefined; |
| 153 | refresh: number | undefined; |
| 154 | retry: number | undefined; |
| 155 | rname: string | undefined; |
| 156 | ttl: number | undefined; |
| 157 | }; |
| 158 | /** Default zone mode. */ |
| 159 | zoneMode: string; |
| 160 | }; |
| 161 | } |
| 162 | |
| 163 | export interface AccountDnsSettingsAttributes extends AccountDnsSettingsSnapshot { |
| 164 | /** The Cloudflare account whose DNS settings are managed. */ |
| 165 | accountId: string; |
| 166 | /** |
| 167 | * Snapshot of every DNS setting taken before Alchemy first patched |
| 168 | * the account. The managed fields are restored from it on destroy, so |
| 169 | * deleting the resource puts the account back the way it was found. |
| 170 | */ |
| 171 | initialSettings: AccountDnsSettingsSnapshot; |
| 172 | /** |
| 173 | * Which settings this resource has managed (union across all |
| 174 | * reconciles). Only these are restored on destroy — settings the user |
| 175 | * never touched are left alone. |
| 176 | */ |
| 177 | managedKeys: ReadonlyArray<string>; |
| 178 | } |
| 179 | |
| 180 | export type AccountDnsSettings = Resource< |
| 181 | TypeId, |
| 182 | AccountDnsSettingsProps, |
| 183 | AccountDnsSettingsAttributes, |
| 184 | never, |
| 185 | Providers |
| 186 | >; |
| 187 | |
| 188 | /** |
| 189 | * The DNS settings of a Cloudflare account |
| 190 | * (`/accounts/{account_id}/dns_settings`) — the account-wide |
| 191 | * `enforceDnsOnly` override and the default DNS settings applied to |
| 192 | * every new zone (`zoneDefaults`). |
| 193 | * |
| 194 | * The settings object is a per-account singleton — it always exists |
| 195 | * with Cloudflare defaults, so this resource never creates or deletes |
| 196 | * anything physical. Reconcile patches only the fields you declare |
| 197 | * (and only when the observed value differs); destroy restores the |
| 198 | * managed fields to the values they had before Alchemy first touched |
| 199 | * the account (captured as `initialSettings`). |
| 200 | * |
| 201 | * Some fields are plan-gated: `zoneDefaults.nsTtl` and custom SOA |
| 202 | * values require the custom nameserver TTL / custom SOA entitlements, |
| 203 | * `foundationDns` is a paid add-on, and `internalDns` is Enterprise |
| 204 | * Internal DNS only. |
| 205 | * ### Account-wide overrides |
| 206 | * **Example:** Force every proxied record to DNS-only |
| 207 | * ```typescript |
| 208 | * yield* Cloudflare.DNS.AccountDnsSettings("DnsSettings", { |
| 209 | * enforceDnsOnly: true, |
| 210 | * }); |
| 211 | * ``` |
| 212 | * |
| 213 | * ### Zone defaults |
| 214 | * **Example:** Flatten CNAMEs in every new zone |
| 215 | * ```typescript |
| 216 | * yield* Cloudflare.DNS.AccountDnsSettings("DnsSettings", { |
| 217 | * zoneDefaults: { flattenAllCnames: true }, |
| 218 | * }); |
| 219 | * ``` |
| 220 | * |
| 221 | * **Example:** Default new zones to multi-provider DNS |
| 222 | * ```typescript |
| 223 | * yield* Cloudflare.DNS.AccountDnsSettings("DnsSettings", { |
| 224 | * zoneDefaults: { multiProvider: true }, |
| 225 | * }); |
| 226 | * ``` |
| 227 | * |
| 228 | * @resource |
| 229 | * @product DNS |
| 230 | * @category Domains & DNS |
| 231 | */ |
| 232 | export const AccountDnsSettings = Resource<AccountDnsSettings>(TypeId, { |
| 233 | aliases: ["Cloudflare.Dns.AccountSettings"], |
| 234 | }); |
| 235 | |
| 236 | /** |
| 237 | * Returns true if the given value is an AccountDnsSettings resource. |
| 238 | */ |
| 239 | export const isAccountDnsSettings = ( |
| 240 | value: unknown, |
| 241 | ): value is AccountDnsSettings => |
| 242 | Predicate.hasProperty(value, "Type") && value.Type === TypeId; |
| 243 | |
| 244 | export const AccountDnsSettingsProvider = () => |
| 245 | Provider.succeed(AccountDnsSettings, { |
| 246 | stables: ["accountId", "initialSettings", "managedKeys"], |
| 247 | |
| 248 | // Account singleton — the DNS settings object always exists for the |
| 249 | // ambient account. There is no enumeration API, so read the single |
| 250 | // object and return it as a one-element array (mirrors `read` with no |
| 251 | // prior output: the observed snapshot is its own `initialSettings`, |
| 252 | // nothing is being managed yet). |
| 253 | list: Effect.fn(function* () { |
| 254 | const { accountId } = yield* yield* CloudflareEnvironment; |
| 255 | const observed = yield* dns.getSettingAccount({ accountId }); |
| 256 | const snapshot = toSnapshot(observed); |
| 257 | return [ |
| 258 | { |
| 259 | accountId, |
| 260 | ...snapshot, |
| 261 | initialSettings: snapshot, |
| 262 | managedKeys: [], |
| 263 | } satisfies AccountDnsSettingsAttributes, |
| 264 | ]; |
| 265 | }), |
| 266 | |
| 267 | diff: Effect.fn(function* ({ output }) { |
| 268 | const { accountId } = yield* yield* CloudflareEnvironment; |
| 269 | // The settings object is an account singleton — a different |
| 270 | // account is a different resource. |
| 271 | if (output !== undefined && output.accountId !== accountId) { |
| 272 | return { action: "replace" } as const; |
| 273 | } |
| 274 | return undefined; |
| 275 | }), |
| 276 | |
| 277 | read: Effect.fn(function* ({ output }) { |
| 278 | const { accountId } = yield* yield* CloudflareEnvironment; |
| 279 | const acct = output?.accountId ?? accountId; |
| 280 | const observed = yield* dns.getSettingAccount({ accountId: acct }); |
| 281 | const snapshot = toSnapshot(observed); |
| 282 | // Settings are a singleton that always exists with Cloudflare |
| 283 | // defaults — there is nothing to "own", so a cold read adopts |
| 284 | // freely (never `Unowned`). The observed snapshot at adoption |
| 285 | // time becomes the baseline restored on destroy. |
| 286 | return { |
| 287 | accountId: acct, |
| 288 | ...snapshot, |
| 289 | initialSettings: output?.initialSettings ?? snapshot, |
| 290 | managedKeys: output?.managedKeys ?? [], |
| 291 | } satisfies AccountDnsSettingsAttributes; |
| 292 | }), |
| 293 | |
| 294 | reconcile: Effect.fn(function* ({ news, output }) { |
| 295 | const { accountId } = yield* yield* CloudflareEnvironment; |
| 296 | |
| 297 | // 1. Observe — the settings object always exists for an account. |
| 298 | const observed = yield* dns.getSettingAccount({ accountId }); |
| 299 | let snapshot = toSnapshot(observed); |
| 300 | |
| 301 | // 2. Capture — the pre-management snapshot, restored on destroy. |
| 302 | const initialSettings = output?.initialSettings ?? snapshot; |
| 303 | // Track every key we have ever managed so destroy restores fields |
| 304 | // even after the user drops them from props. |
| 305 | const managedKeys = [ |
| 306 | ...new Set([...(output?.managedKeys ?? []), ...declaredKeys(news)]), |
| 307 | ].sort(); |
| 308 | |
| 309 | // 3. Sync — patch only the declared fields whose observed value |
| 310 | // differs from the desired one; skip the API on no delta. |
| 311 | const delta = computeDelta(news, snapshot); |
| 312 | if (delta !== undefined) { |
| 313 | const patched = yield* dns.patchSettingAccount({ |
| 314 | accountId, |
| 315 | ...delta, |
| 316 | }); |
| 317 | snapshot = toSnapshot(patched); |
| 318 | } |
| 319 | |
| 320 | return { |
| 321 | accountId, |
| 322 | ...snapshot, |
| 323 | initialSettings, |
| 324 | managedKeys, |
| 325 | } satisfies AccountDnsSettingsAttributes; |
| 326 | }), |
| 327 | |
| 328 | delete: Effect.fn(function* ({ output }) { |
| 329 | const { accountId, initialSettings, managedKeys } = output; |
| 330 | // Observe, then restore only the fields this resource managed, |
| 331 | // and only the ones that still differ (idempotent re-delete after |
| 332 | // a crash). |
| 333 | const observed = yield* dns.getSettingAccount({ accountId }); |
| 334 | const snapshot = toSnapshot(observed); |
| 335 | const restore = computeRestore(managedKeys, initialSettings, snapshot); |
| 336 | if (restore === undefined) return; |
| 337 | yield* dns.patchSettingAccount({ accountId, ...restore }); |
| 338 | }), |
| 339 | }); |
| 340 | |
| 341 | // --------------------------------------------------------------------------- |
| 342 | // Snapshot + diff helpers |
| 343 | // --------------------------------------------------------------------------- |
| 344 | |
| 345 | type SettingsResponse = |
| 346 | | dns.GetSettingAccountResponse |
| 347 | | dns.PatchSettingAccountResponse; |
| 348 | |
| 349 | type PatchBody = Omit<dns.PatchSettingAccountRequest, "accountId">; |
| 350 | type ZoneDefaultsPatch = NonNullable<PatchBody["zoneDefaults"]>; |
| 351 | |
| 352 | const undef = <T>(v: T | null | undefined): T | undefined => |
| 353 | v == null ? undefined : v; |
| 354 | |
| 355 | const toSnapshot = (r: SettingsResponse): AccountDnsSettingsSnapshot => ({ |
| 356 | enforceDnsOnly: undef(r.enforceDnsOnly) ?? false, |
| 357 | zoneDefaults: { |
| 358 | flattenAllCnames: r.zoneDefaults.flattenAllCnames, |
| 359 | foundationDns: r.zoneDefaults.foundationDns, |
| 360 | internalDns: { |
| 361 | referenceZoneId: undef(r.zoneDefaults.internalDns.referenceZoneId), |
| 362 | }, |
| 363 | multiProvider: r.zoneDefaults.multiProvider, |
| 364 | nameservers: { type: r.zoneDefaults.nameservers.type }, |
| 365 | nsTtl: r.zoneDefaults.nsTtl, |
| 366 | secondaryOverrides: r.zoneDefaults.secondaryOverrides, |
| 367 | soa: { |
| 368 | expire: undef(r.zoneDefaults.soa.expire), |
| 369 | minTtl: undef(r.zoneDefaults.soa.minTtl), |
| 370 | mname: undef(r.zoneDefaults.soa.mname), |
| 371 | refresh: undef(r.zoneDefaults.soa.refresh), |
| 372 | retry: undef(r.zoneDefaults.soa.retry), |
| 373 | rname: undef(r.zoneDefaults.soa.rname), |
| 374 | ttl: undef(r.zoneDefaults.soa.ttl), |
| 375 | }, |
| 376 | zoneMode: r.zoneDefaults.zoneMode, |
| 377 | }, |
| 378 | }); |
| 379 | |
| 380 | const ZONE_DEFAULT_KEYS = [ |
| 381 | "flattenAllCnames", |
| 382 | "foundationDns", |
| 383 | "internalDns", |
| 384 | "multiProvider", |
| 385 | "nameservers", |
| 386 | "nsTtl", |
| 387 | "secondaryOverrides", |
| 388 | "soa", |
| 389 | "zoneMode", |
| 390 | ] as const; |
| 391 | |
| 392 | /** Keys the user declared, e.g. `enforceDnsOnly`, `zoneDefaults.nsTtl`. */ |
| 393 | const declaredKeys = (news: AccountDnsSettingsProps): string[] => [ |
| 394 | ...(news.enforceDnsOnly !== undefined ? ["enforceDnsOnly"] : []), |
| 395 | ...ZONE_DEFAULT_KEYS.filter((k) => news.zoneDefaults?.[k] !== undefined).map( |
| 396 | (k) => `zoneDefaults.${k}`, |
| 397 | ), |
| 398 | ]; |
| 399 | |
| 400 | /** |
| 401 | * Merge desired SOA subfields over the observed SOA so the PATCH always |
| 402 | * carries a complete, consistent record (Cloudflare validates SOA as a |
| 403 | * whole). |
| 404 | */ |
| 405 | const mergedSoa = ( |
| 406 | desired: AccountDnsSoa, |
| 407 | observed: AccountDnsSettingsSnapshot["zoneDefaults"]["soa"], |
| 408 | ): NonNullable<ZoneDefaultsPatch["soa"]> => ({ |
| 409 | expire: desired.expire ?? observed.expire, |
| 410 | minTtl: desired.minTtl ?? observed.minTtl, |
| 411 | mname: desired.mname ?? observed.mname, |
| 412 | refresh: desired.refresh ?? observed.refresh, |
| 413 | retry: desired.retry ?? observed.retry, |
| 414 | rname: desired.rname ?? observed.rname, |
| 415 | ttl: desired.ttl ?? observed.ttl, |
| 416 | }); |
| 417 | |
| 418 | const soaDiffers = ( |
| 419 | desired: AccountDnsSoa, |
| 420 | observed: AccountDnsSettingsSnapshot["zoneDefaults"]["soa"], |
| 421 | ): boolean => |
| 422 | (desired.expire !== undefined && desired.expire !== observed.expire) || |
| 423 | (desired.minTtl !== undefined && desired.minTtl !== observed.minTtl) || |
| 424 | (desired.mname !== undefined && desired.mname !== observed.mname) || |
| 425 | (desired.refresh !== undefined && desired.refresh !== observed.refresh) || |
| 426 | (desired.retry !== undefined && desired.retry !== observed.retry) || |
| 427 | (desired.rname !== undefined && desired.rname !== observed.rname) || |
| 428 | (desired.ttl !== undefined && desired.ttl !== observed.ttl); |
| 429 | |
| 430 | /** |
| 431 | * Build the PATCH body converging observed → desired for the fields the |
| 432 | * user declared. Returns `undefined` when nothing differs. |
| 433 | */ |
| 434 | const computeDelta = ( |
| 435 | news: AccountDnsSettingsProps, |
| 436 | observed: AccountDnsSettingsSnapshot, |
| 437 | ): PatchBody | undefined => { |
| 438 | const body: PatchBody = {}; |
| 439 | let dirty = false; |
| 440 | if ( |
| 441 | news.enforceDnsOnly !== undefined && |
| 442 | news.enforceDnsOnly !== observed.enforceDnsOnly |
| 443 | ) { |
| 444 | body.enforceDnsOnly = news.enforceDnsOnly; |
| 445 | dirty = true; |
| 446 | } |
| 447 | const zd = news.zoneDefaults; |
| 448 | const ozd = observed.zoneDefaults; |
| 449 | if (zd !== undefined) { |
| 450 | const zdBody: ZoneDefaultsPatch = {}; |
| 451 | let zdDirty = false; |
| 452 | if ( |
| 453 | zd.flattenAllCnames !== undefined && |
| 454 | zd.flattenAllCnames !== ozd.flattenAllCnames |
| 455 | ) { |
| 456 | zdBody.flattenAllCnames = zd.flattenAllCnames; |
| 457 | zdDirty = true; |
| 458 | } |
| 459 | if ( |
| 460 | zd.foundationDns !== undefined && |
| 461 | zd.foundationDns !== ozd.foundationDns |
| 462 | ) { |
| 463 | zdBody.foundationDns = zd.foundationDns; |
| 464 | zdDirty = true; |
| 465 | } |
| 466 | if ( |
| 467 | zd.internalDns?.referenceZoneId !== undefined && |
| 468 | zd.internalDns.referenceZoneId !== ozd.internalDns.referenceZoneId |
| 469 | ) { |
| 470 | zdBody.internalDns = { referenceZoneId: zd.internalDns.referenceZoneId }; |
| 471 | zdDirty = true; |
| 472 | } |
| 473 | if ( |
| 474 | zd.multiProvider !== undefined && |
| 475 | zd.multiProvider !== ozd.multiProvider |
| 476 | ) { |
| 477 | zdBody.multiProvider = zd.multiProvider; |
| 478 | zdDirty = true; |
| 479 | } |
| 480 | if ( |
| 481 | zd.nameservers !== undefined && |
| 482 | zd.nameservers.type !== ozd.nameservers.type |
| 483 | ) { |
| 484 | zdBody.nameservers = { type: zd.nameservers.type }; |
| 485 | zdDirty = true; |
| 486 | } |
| 487 | if (zd.nsTtl !== undefined && zd.nsTtl !== ozd.nsTtl) { |
| 488 | zdBody.nsTtl = zd.nsTtl; |
| 489 | zdDirty = true; |
| 490 | } |
| 491 | if ( |
| 492 | zd.secondaryOverrides !== undefined && |
| 493 | zd.secondaryOverrides !== ozd.secondaryOverrides |
| 494 | ) { |
| 495 | zdBody.secondaryOverrides = zd.secondaryOverrides; |
| 496 | zdDirty = true; |
| 497 | } |
| 498 | if (zd.soa !== undefined && soaDiffers(zd.soa, ozd.soa)) { |
| 499 | zdBody.soa = mergedSoa(zd.soa, ozd.soa); |
| 500 | zdDirty = true; |
| 501 | } |
| 502 | if (zd.zoneMode !== undefined && zd.zoneMode !== ozd.zoneMode) { |
| 503 | zdBody.zoneMode = zd.zoneMode; |
| 504 | zdDirty = true; |
| 505 | } |
| 506 | if (zdDirty) { |
| 507 | body.zoneDefaults = zdBody; |
| 508 | dirty = true; |
| 509 | } |
| 510 | } |
| 511 | return dirty ? body : undefined; |
| 512 | }; |
| 513 | |
| 514 | /** |
| 515 | * Build the PATCH body that restores the managed fields to their |
| 516 | * pre-management values. Returns `undefined` when nothing drifted. |
| 517 | */ |
| 518 | const computeRestore = ( |
| 519 | managedKeys: ReadonlyArray<string>, |
| 520 | initial: AccountDnsSettingsSnapshot, |
| 521 | observed: AccountDnsSettingsSnapshot, |
| 522 | ): PatchBody | undefined => { |
| 523 | const body: PatchBody = {}; |
| 524 | const zdBody: ZoneDefaultsPatch = {}; |
| 525 | let dirty = false; |
| 526 | let zdDirty = false; |
| 527 | const izd = initial.zoneDefaults; |
| 528 | const ozd = observed.zoneDefaults; |
| 529 | for (const key of managedKeys) { |
| 530 | switch (key) { |
| 531 | case "enforceDnsOnly": |
| 532 | if (initial.enforceDnsOnly !== observed.enforceDnsOnly) { |
| 533 | body.enforceDnsOnly = initial.enforceDnsOnly; |
| 534 | dirty = true; |
| 535 | } |
| 536 | break; |
| 537 | case "zoneDefaults.flattenAllCnames": |
| 538 | if (izd.flattenAllCnames !== ozd.flattenAllCnames) { |
| 539 | zdBody.flattenAllCnames = izd.flattenAllCnames; |
| 540 | zdDirty = true; |
| 541 | } |
| 542 | break; |
| 543 | case "zoneDefaults.foundationDns": |
| 544 | if (izd.foundationDns !== ozd.foundationDns) { |
| 545 | zdBody.foundationDns = izd.foundationDns; |
| 546 | zdDirty = true; |
| 547 | } |
| 548 | break; |
| 549 | case "zoneDefaults.internalDns": |
| 550 | if ( |
| 551 | izd.internalDns.referenceZoneId !== ozd.internalDns.referenceZoneId && |
| 552 | izd.internalDns.referenceZoneId !== undefined |
| 553 | ) { |
| 554 | zdBody.internalDns = { |
| 555 | referenceZoneId: izd.internalDns.referenceZoneId, |
| 556 | }; |
| 557 | zdDirty = true; |
| 558 | } |
| 559 | break; |
| 560 | case "zoneDefaults.multiProvider": |
| 561 | if (izd.multiProvider !== ozd.multiProvider) { |
| 562 | zdBody.multiProvider = izd.multiProvider; |
| 563 | zdDirty = true; |
| 564 | } |
| 565 | break; |
| 566 | case "zoneDefaults.nameservers": |
| 567 | if (izd.nameservers.type !== ozd.nameservers.type) { |
| 568 | zdBody.nameservers = { type: izd.nameservers.type }; |
| 569 | zdDirty = true; |
| 570 | } |
| 571 | break; |
| 572 | case "zoneDefaults.nsTtl": |
| 573 | if (izd.nsTtl !== ozd.nsTtl) { |
| 574 | zdBody.nsTtl = izd.nsTtl; |
| 575 | zdDirty = true; |
| 576 | } |
| 577 | break; |
| 578 | case "zoneDefaults.secondaryOverrides": |
| 579 | if (izd.secondaryOverrides !== ozd.secondaryOverrides) { |
| 580 | zdBody.secondaryOverrides = izd.secondaryOverrides; |
| 581 | zdDirty = true; |
| 582 | } |
| 583 | break; |
| 584 | case "zoneDefaults.soa": |
| 585 | if ( |
| 586 | izd.soa.expire !== ozd.soa.expire || |
| 587 | izd.soa.minTtl !== ozd.soa.minTtl || |
| 588 | izd.soa.mname !== ozd.soa.mname || |
| 589 | izd.soa.refresh !== ozd.soa.refresh || |
| 590 | izd.soa.retry !== ozd.soa.retry || |
| 591 | izd.soa.rname !== ozd.soa.rname || |
| 592 | izd.soa.ttl !== ozd.soa.ttl |
| 593 | ) { |
| 594 | zdBody.soa = { |
| 595 | expire: izd.soa.expire, |
| 596 | minTtl: izd.soa.minTtl, |
| 597 | mname: izd.soa.mname, |
| 598 | refresh: izd.soa.refresh, |
| 599 | retry: izd.soa.retry, |
| 600 | rname: izd.soa.rname, |
| 601 | ttl: izd.soa.ttl, |
| 602 | }; |
| 603 | zdDirty = true; |
| 604 | } |
| 605 | break; |
| 606 | case "zoneDefaults.zoneMode": |
| 607 | if (izd.zoneMode !== ozd.zoneMode) { |
| 608 | zdBody.zoneMode = izd.zoneMode; |
| 609 | zdDirty = true; |
| 610 | } |
| 611 | break; |
| 612 | default: |
| 613 | break; |
| 614 | } |
| 615 | } |
| 616 | if (zdDirty) { |
| 617 | body.zoneDefaults = zdBody; |
| 618 | dirty = true; |
| 619 | } |
| 620 | return dirty ? body : undefined; |
| 621 | }; |