1import * as dm from "@distilled.cloud/gcp/datamigration_v1";
2import * as Effect from "effect/Effect";
3import * as Schedule from "effect/Schedule";
4import * as Stream from "effect/Stream";
5import { Unowned } from "../../AdoptPolicy.ts";
6import { isResolved } from "../../Diff.ts";
7import * as Provider from "../../Provider.ts";
8import { Resource } from "../../Resource.ts";
9import { tagRecord } from "../../Tags.ts";
10import { GcpEnvironment } from "../Environment.ts";
11import {
12 createInternalLabels,
13 diffLabels,
14 hasAlchemyLabels,
15 toLabels,
16} from "../Labels.ts";
17import type { Providers } from "../Providers.ts";
18import {
19 connectionProfileOf,
20 fieldMask,
21 fingerprint,
22 hasAlchemyLabelMap,
23 lastSegment,
24 locationParent,
25 normalizeLocation,
26 parseName,
27 replaceOnIdentity,
28 ResourceNotResolved,
29 toPhysicalId,
30 userLabels,
31 waitForOperation,
32 waitUntilExists,
33 waitUntilGone,
34} from "./internal.ts";
35
36export type MigrationJobType = dm.MigrationJobTypeEnum | (string & {});
37export type MigrationJobState = dm.MigrationJobStateEnum | (string & {});
38export type MigrationJobPhase = dm.MigrationJobPhaseEnum | (string & {});
39export type MigrationJobPurpose = dm.MigrationJobPurposeEnum | (string & {});
40export type MigrationJobDumpType = dm.MigrationJobDumpTypeEnum | (string & {});
41export type DatabaseType = dm.DatabaseType;
42export type DumpFlags = dm.DumpFlags;
43export type ConversionWorkspaceInfo = dm.ConversionWorkspaceInfo;
44export type VpcPeeringConnectivity = dm.VpcPeeringConnectivity;
45export type ReverseSshConnectivity = dm.ReverseSshConnectivity;
46export type PerformanceConfig = dm.PerformanceConfig;
47export type MigrationJobObjectsConfig = dm.MigrationJobObjectsConfig;
48export type SqlServerToPostgresConfig = dm.SqlServerToPostgresConfig;
49export type OracleToPostgresConfig = dm.OracleToPostgresConfig;
50export type PostgresToSqlServerConfig = dm.PostgresToSqlServerConfig;
51export type MySqlHomogeneousConfig = dm.MySqlHomogeneousConfig;
52export type PostgresHomogeneousConfig = dm.PostgresHomogeneousConfig;
53export type SqlServerHomogeneousMigrationJobConfig =
54 dm.SqlServerHomogeneousMigrationJobConfig;
55type Status = dm.Status;
56
57export type MigrationJobProps = {
58 /**
59 * Migration job id (the `{migrationJob}` segment of
60 * `projects/{project}/locations/{location}/migrationJobs/{migrationJob}`).
61 * If omitted, a unique RFC1035 name is generated. Immutable — changing
62 * it replaces the job.
63 */
64 migrationJobId?: string;
65 /**
66 * Region (`us-central1`, …). Immutable — changing it replaces the job.
67 * `US-CENTRAL1` is accepted and normalized to `us-central1`.
68 * @default the stack's GCP region (`GCP.Region`, else the profile region, else `us-central1`)
69 */
70 location?: string;
71 /**
72 * User-friendly display name.
73 */
74 displayName?: string;
75 /**
76 * User labels. Alchemy ownership labels are merged in automatically.
77 */
78 labels?: Record<string, string>;
79 /**
80 * Migration type (`ONE_TIME` or `CONTINUOUS`). Immutable — changing it
81 * replaces the job.
82 */
83 type: MigrationJobType;
84 /**
85 * Source connection profile. Full name or id (combined with
86 * `location`). Immutable — changing it replaces the job.
87 */
88 source: string;
89 /**
90 * Destination connection profile. Full name or id (combined with
91 * `location`). Immutable — changing it replaces the job.
92 */
93 destination: string;
94 /**
95 * Source database engine/provider metadata.
96 */
97 sourceDatabase?: DatabaseType;
98 /**
99 * Destination database engine/provider metadata.
100 */
101 destinationDatabase?: DatabaseType;
102 /**
103 * Conversion workspace used by heterogeneous migrations.
104 */
105 conversionWorkspace?: ConversionWorkspaceInfo;
106 /**
107 * Static IP connectivity (default). Mutually exclusive with VPC peering
108 * and reverse SSH.
109 */
110 staticIpConnectivity?: Record<string, never>;
111 /**
112 * VPC peering connectivity to the source.
113 */
114 vpcPeeringConnectivity?: VpcPeeringConnectivity;
115 /**
116 * Reverse SSH tunnel connectivity to the source.
117 */
118 reverseSshConnectivity?: ReverseSshConnectivity;
119 /**
120 * Initial dump flags. Mutually exclusive with `dumpPath`.
121 */
122 dumpFlags?: DumpFlags;
123 /**
124 * GCS dump path (`gs://bucket/object`). Mutually exclusive with
125 * `dumpFlags`.
126 */
127 dumpPath?: string;
128 /**
129 * Dump type for MySQL to Cloud SQL for MySQL.
130 */
131 dumpType?: MigrationJobDumpType;
132 /**
133 * Dump parallelism settings.
134 */
135 performanceConfig?: PerformanceConfig;
136 /**
137 * AIP-160 entity filter when a conversion workspace is attached.
138 */
139 filter?: string;
140 /**
141 * Objects that need to be migrated.
142 */
143 objectsConfig?: MigrationJobObjectsConfig;
144 /**
145 * CMEK used for the job. Immutable — changing it replaces the job.
146 */
147 cmekKeyName?: string;
148 /**
149 * Failback pointer to the original migration job.
150 */
151 originalMigrationName?: string;
152 /**
153 * Heterogeneous SQL Server to PostgreSQL config.
154 */
155 sqlserverToPostgresConfig?: SqlServerToPostgresConfig;
156 /**
157 * Heterogeneous Oracle to PostgreSQL config.
158 */
159 oracleToPostgresConfig?: OracleToPostgresConfig;
160 /**
161 * Heterogeneous PostgreSQL to SQL Server config.
162 */
163 postgresToSqlserverConfig?: PostgresToSqlServerConfig;
164 /**
165 * Homogeneous MySQL config.
166 */
167 mysqlHomogeneousConfig?: MySqlHomogeneousConfig;
168 /**
169 * Homogeneous PostgreSQL config.
170 */
171 postgresHomogeneousConfig?: PostgresHomogeneousConfig;
172 /**
173 * Homogeneous SQL Server config.
174 */
175 sqlserverHomogeneousMigrationJobConfig?: SqlServerHomogeneousMigrationJobConfig;
176};
177
178export type MigrationJob = Resource<
179 "GCP.DataMigration.MigrationJob",
180 MigrationJobProps,
181 {
182 /** Full resource name. */
183 name: string;
184 /** Migration job id (last path segment). */
185 migrationJobId: string;
186 /** Project id. */
187 project: string;
188 /** Location id. */
189 location: string;
190 /** User-friendly display name. */
191 displayName: string | undefined;
192 /** User labels (Alchemy ownership labels stripped). */
193 labels: Record<string, string>;
194 /** Migration type. */
195 type: string | undefined;
196 /** Source connection profile name. */
197 source: string | undefined;
198 /** Destination connection profile name. */
199 destination: string | undefined;
200 /** Source database metadata. */
201 sourceDatabase: DatabaseType | undefined;
202 /** Destination database metadata. */
203 destinationDatabase: DatabaseType | undefined;
204 /** Conversion workspace info. */
205 conversionWorkspace: ConversionWorkspaceInfo | undefined;
206 /** Static IP connectivity. */
207 staticIpConnectivity: Record<string, never> | undefined;
208 /** VPC peering connectivity. */
209 vpcPeeringConnectivity: VpcPeeringConnectivity | undefined;
210 /** Reverse SSH connectivity. */
211 reverseSshConnectivity: ReverseSshConnectivity | undefined;
212 /** Dump flags. */
213 dumpFlags: DumpFlags | undefined;
214 /** GCS dump path. */
215 dumpPath: string | undefined;
216 /** Dump type. */
217 dumpType: string | undefined;
218 /** Dump parallelism. */
219 performanceConfig: PerformanceConfig | undefined;
220 /** Entity filter. */
221 filter: string | undefined;
222 /** Objects config. */
223 objectsConfig: MigrationJobObjectsConfig | undefined;
224 /** CMEK name. */
225 cmekKeyName: string | undefined;
226 /** Original migration job for failback. */
227 originalMigrationName: string | undefined;
228 /** SQL Server to PostgreSQL config. */
229 sqlserverToPostgresConfig: SqlServerToPostgresConfig | undefined;
230 /** Oracle to PostgreSQL config. */
231 oracleToPostgresConfig: OracleToPostgresConfig | undefined;
232 /** PostgreSQL to SQL Server config. */
233 postgresToSqlserverConfig: PostgresToSqlServerConfig | undefined;
234 /** Homogeneous MySQL config. */
235 mysqlHomogeneousConfig: MySqlHomogeneousConfig | undefined;
236 /** Homogeneous PostgreSQL config. */
237 postgresHomogeneousConfig: PostgresHomogeneousConfig | undefined;
238 /** Homogeneous SQL Server config. */
239 sqlserverHomogeneousMigrationJobConfig:
240 | SqlServerHomogeneousMigrationJobConfig
241 | undefined;
242 /** Server-reported state. */
243 state: string | undefined;
244 /** Current phase. */
245 phase: string | undefined;
246 /** Forward or failback purpose. */
247 purpose: string | undefined;
248 /** Duration of the job. */
249 duration: string | undefined;
250 /** Failure details when `state` is `FAILED`. */
251 error: Status | undefined;
252 /** RFC3339 creation timestamp. */
253 createTime: string | undefined;
254 /** RFC3339 last-update timestamp. */
255 updateTime: string | undefined;
256 /** RFC3339 completion timestamp. */
257 endTime: string | undefined;
258 },
259 never,
260 Providers
261>;
262
263/**
264 * A Database Migration Service migration job: copies data from a source
265 * connection profile to a destination (Cloud SQL or AlloyDB).
266 *
267 * `migrationJobId`, `location`, `type`, `source`, `destination`, and
268 * `cmekKeyName` are replacement triggers. Display name, labels, dump
269 * flags, connectivity, and performance config update in place. Delete
270 * force-deletes the destination Cloud SQL replica.
271 *
272 * ### Creating a Migration Job
273 * **Example:** Continuous MySQL to Cloud SQL
274 * ```typescript
275 * const source = yield* GCP.DataMigration.ConnectionProfile("MysqlSrc", {
276 * mysql: {
277 * host: "10.0.0.8",
278 * port: 3306,
279 * username: "dms",
280 * password: process.env.MYSQL_PASSWORD,
281 * },
282 * });
283 * const dest = yield* GCP.DataMigration.ConnectionProfile("MysqlDest", {
284 * cloudsql: {
285 * settings: {
286 * sourceId: source.name,
287 * databaseVersion: "MYSQL_8_0",
288 * tier: "db-n1-standard-1",
289 * rootPassword: process.env.MYSQL_ROOT_PASSWORD,
290 * },
291 * },
292 * });
293 * const job = yield* GCP.DataMigration.MigrationJob("Replica", {
294 * type: "CONTINUOUS",
295 * source: source.name,
296 * destination: dest.name,
297 * staticIpConnectivity: {},
298 * });
299 * ```
300 *
301 * @resource
302 * @category DataMigration
303 */
304export const MigrationJob = Resource<MigrationJob>(
305 "GCP.DataMigration.MigrationJob",
306);
307
308const resourceName = (
309 project: string,
310 location: string,
311 migrationJobId: string,
312) => `${locationParent(project, location)}/migrationJobs/${migrationJobId}`;
313
314const emptyConnectivity = (value: unknown) =>
315 value === undefined ? undefined : {};
316
317const publicReverseSsh = (value: ReverseSshConnectivity | undefined) =>
318 value === undefined
319 ? undefined
320 : {
321 vmIp: value.vmIp,
322 vmPort: value.vmPort,
323 vpc: value.vpc,
324 };
325
326const toAttrs = (job: dm.MigrationJob, project: string) => {
327 const name = job.name ?? "";
328 const parsed = parseName(name, "migrationJobs");
329 return {
330 name,
331 migrationJobId: parsed.id,
332 project: parsed.project || project,
333 location: parsed.location,
334 displayName: job.displayName,
335 labels: userLabels(job.labels),
336 type: job.type,
337 source: job.source,
338 destination: job.destination,
339 sourceDatabase: job.sourceDatabase,
340 destinationDatabase: job.destinationDatabase,
341 conversionWorkspace: job.conversionWorkspace,
342 staticIpConnectivity: emptyConnectivity(job.staticIpConnectivity),
343 vpcPeeringConnectivity: job.vpcPeeringConnectivity,
344 reverseSshConnectivity: publicReverseSsh(job.reverseSshConnectivity),
345 dumpFlags: job.dumpFlags,
346 dumpPath: job.dumpPath,
347 dumpType: job.dumpType,
348 performanceConfig: job.performanceConfig,
349 filter: job.filter,
350 objectsConfig: job.objectsConfig,
351 cmekKeyName: job.cmekKeyName,
352 originalMigrationName: job.originalMigrationName,
353 sqlserverToPostgresConfig: job.sqlserverToPostgresConfig,
354 oracleToPostgresConfig: job.oracleToPostgresConfig,
355 postgresToSqlserverConfig: job.postgresToSqlserverConfig,
356 mysqlHomogeneousConfig: job.mysqlHomogeneousConfig,
357 postgresHomogeneousConfig: job.postgresHomogeneousConfig,
358 sqlserverHomogeneousMigrationJobConfig:
359 job.sqlserverHomogeneousMigrationJobConfig,
360 state: job.state,
361 phase: job.phase,
362 purpose: job.purpose,
363 duration: job.duration,
364 error: job.error,
365 createTime: job.createTime,
366 updateTime: job.updateTime,
367 endTime: job.endTime,
368 };
369};
370
371const getByName = (name: string) =>
372 name.length === 0
373 ? Effect.succeed(undefined)
374 : dm
375 .getProjectsLocationsMigrationJobs({ name })
376 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
377
378const listOwned = (project: string, region: string) =>
379 dm.listProjectsLocationsMigrationJobs
380 .pages({
381 parent: `projects/${project}/locations/-`,
382 pageSize: 1000,
383 })
384 .pipe(
385 Stream.flatMap((page) => Stream.fromIterable(page.migrationJobs ?? [])),
386 Stream.filter((item) => hasAlchemyLabelMap(item.labels)),
387 Stream.runCollect,
388 Effect.map((chunk) => Array.from(chunk)),
389 Effect.catchTag("NotFound", () =>
390 dm.listProjectsLocationsMigrationJobs
391 .pages({
392 parent: locationParent(project, region),
393 pageSize: 1000,
394 })
395 .pipe(
396 Stream.flatMap((page) =>
397 Stream.fromIterable(page.migrationJobs ?? []),
398 ),
399 Stream.filter((item) => hasAlchemyLabelMap(item.labels)),
400 Stream.runCollect,
401 Effect.map((chunk) => Array.from(chunk)),
402 Effect.catchTag("NotFound", () =>
403 Effect.succeed([] as dm.MigrationJob[]),
404 ),
405 ),
406 ),
407 );
408
409const connectivityKind = (value: {
410 staticIpConnectivity?: unknown;
411 vpcPeeringConnectivity?: unknown;
412 reverseSshConnectivity?: unknown;
413}) =>
414 value.vpcPeeringConnectivity
415 ? "vpc"
416 : value.reverseSshConnectivity
417 ? "ssh"
418 : value.staticIpConnectivity !== undefined
419 ? "static"
420 : "";
421
422export const MigrationJobProvider = () =>
423 Provider.succeed(MigrationJob, {
424 stables: ["name", "migrationJobId", "project", "location", "createTime"],
425
426 diff: Effect.fn(function* ({ news, olds, output }) {
427 if (!isResolved(news)) return undefined;
428 const env = yield* GcpEnvironment.current;
429 const previousType = olds?.type ?? output?.type;
430 const nextType = news.type ?? previousType;
431 const previousCmek = olds?.cmekKeyName ?? output?.cmekKeyName;
432 const nextCmek = news.cmekKeyName ?? previousCmek;
433 const previousSource = lastSegment(olds?.source ?? output?.source ?? "");
434 const nextSource = lastSegment(news.source);
435 const previousDestination = lastSegment(
436 olds?.destination ?? output?.destination ?? "",
437 );
438 const nextDestination = lastSegment(news.destination);
439 return replaceOnIdentity({
440 previousId: olds?.migrationJobId ?? output?.migrationJobId,
441 nextId:
442 news.migrationJobId ?? olds?.migrationJobId ?? output?.migrationJobId,
443 previousLocation: normalizeLocation(
444 olds?.location ?? output?.location,
445 env.region,
446 ),
447 nextLocation: normalizeLocation(
448 news.location ?? olds?.location ?? output?.location,
449 env.region,
450 ),
451 extra:
452 previousType !== nextType ||
453 previousCmek !== nextCmek ||
454 (previousSource.length > 0 &&
455 nextSource.length > 0 &&
456 previousSource !== nextSource) ||
457 (previousDestination.length > 0 &&
458 nextDestination.length > 0 &&
459 previousDestination !== nextDestination),
460 });
461 }),
462
463 read: Effect.fn(function* ({ id, olds, output }) {
464 const env = yield* GcpEnvironment.current;
465 const migrationJobId = yield* toPhysicalId(
466 id,
467 olds?.migrationJobId,
468 output?.migrationJobId,
469 "job",
470 );
471 const location = normalizeLocation(
472 olds?.location ?? output?.location,
473 env.region,
474 );
475 const name =
476 output?.name ?? resourceName(env.project, location, migrationJobId);
477 const existing = yield* getByName(name);
478 if (existing === undefined) return undefined;
479 const attrs = toAttrs(existing, env.project);
480 return (yield* hasAlchemyLabels(id, tagRecord(existing.labels)))
481 ? attrs
482 : Unowned(attrs);
483 }),
484
485 list: () =>
486 Effect.gen(function* () {
487 const env = yield* GcpEnvironment.current;
488 const items = yield* listOwned(env.project, env.region);
489 return items.map((item) => toAttrs(item, env.project));
490 }),
491
492 reconcile: Effect.fn(function* ({ id, news, output }) {
493 const env = yield* GcpEnvironment.current;
494 const migrationJobId = yield* toPhysicalId(
495 id,
496 news.migrationJobId,
497 output?.migrationJobId,
498 "job",
499 );
500 const location = normalizeLocation(
501 news.location ?? output?.location,
502 env.region,
503 );
504 const name = resourceName(env.project, location, migrationJobId);
505 const desiredLabels = {
506 ...toLabels(news.labels),
507 ...(yield* createInternalLabels(id)),
508 };
509 const displayName = news.displayName ?? migrationJobId;
510 const source = connectionProfileOf(news.source, env.project, location);
511 const destination = connectionProfileOf(
512 news.destination,
513 env.project,
514 location,
515 );
516 const staticIpConnectivity =
517 news.staticIpConnectivity !== undefined ||
518 (news.vpcPeeringConnectivity === undefined &&
519 news.reverseSshConnectivity === undefined)
520 ? {}
521 : undefined;
522 const body: dm.MigrationJob = {
523 displayName,
524 labels: desiredLabels,
525 type: news.type,
526 source,
527 destination,
528 sourceDatabase: news.sourceDatabase,
529 destinationDatabase: news.destinationDatabase,
530 conversionWorkspace: news.conversionWorkspace,
531 staticIpConnectivity,
532 vpcPeeringConnectivity: news.vpcPeeringConnectivity,
533 reverseSshConnectivity: news.reverseSshConnectivity,
534 dumpFlags: news.dumpFlags,
535 dumpPath: news.dumpPath,
536 dumpType: news.dumpType,
537 performanceConfig: news.performanceConfig,
538 filter: news.filter,
539 objectsConfig: news.objectsConfig,
540 cmekKeyName: news.cmekKeyName,
541 originalMigrationName: news.originalMigrationName,
542 sqlserverToPostgresConfig: news.sqlserverToPostgresConfig,
543 oracleToPostgresConfig: news.oracleToPostgresConfig,
544 postgresToSqlserverConfig: news.postgresToSqlserverConfig,
545 mysqlHomogeneousConfig: news.mysqlHomogeneousConfig,
546 postgresHomogeneousConfig: news.postgresHomogeneousConfig,
547 sqlserverHomogeneousMigrationJobConfig:
548 news.sqlserverHomogeneousMigrationJobConfig,
549 };
550
551 let current = yield* getByName(output?.name ?? name);
552
553 if (current === undefined) {
554 const created = yield* dm
555 .createProjectsLocationsMigrationJobs({
556 parent: locationParent(env.project, location),
557 migrationJobId,
558 body,
559 })
560 .pipe(Effect.catchTag("Conflict", () => Effect.succeed(undefined)));
561 if (created !== undefined) {
562 yield* waitForOperation(created);
563 }
564 current = yield* waitUntilExists(getByName(name), name);
565 }
566
567 if (current === undefined) {
568 return yield* new ResourceNotResolved({ name });
569 }
570
571 const observedLabels = tagRecord(current.labels);
572 const { upsert, removed } = diffLabels(observedLabels, desiredLabels);
573 const labelsChanged = upsert.length > 0 || removed.length > 0;
574 const displayNameChanged = (current.displayName ?? "") !== displayName;
575 const dumpFlagsChanged =
576 fingerprint(current.dumpFlags) !== fingerprint(news.dumpFlags);
577 const dumpPathChanged =
578 (current.dumpPath ?? "") !== (news.dumpPath ?? "");
579 const dumpTypeChanged =
580 news.dumpType !== undefined &&
581 (current.dumpType ?? "") !== news.dumpType;
582 const performanceChanged =
583 fingerprint(current.performanceConfig) !==
584 fingerprint(news.performanceConfig);
585 const filterChanged = (current.filter ?? "") !== (news.filter ?? "");
586 const objectsChanged =
587 fingerprint(current.objectsConfig) !== fingerprint(news.objectsConfig);
588 const conversionChanged =
589 fingerprint(current.conversionWorkspace) !==
590 fingerprint(news.conversionWorkspace);
591 const vpcChanged =
592 fingerprint(current.vpcPeeringConnectivity) !==
593 fingerprint(news.vpcPeeringConnectivity);
594 const sshChanged =
595 fingerprint(publicReverseSsh(current.reverseSshConnectivity)) !==
596 fingerprint(publicReverseSsh(news.reverseSshConnectivity));
597 const nextKind = connectivityKind({
598 staticIpConnectivity,
599 vpcPeeringConnectivity: news.vpcPeeringConnectivity,
600 reverseSshConnectivity: news.reverseSshConnectivity,
601 });
602 const previousKind = connectivityKind(current);
603 const staticChanged = nextKind === "static" && previousKind !== "static";
604 const homogeneousChanged =
605 fingerprint(current.mysqlHomogeneousConfig) !==
606 fingerprint(news.mysqlHomogeneousConfig) ||
607 fingerprint(current.postgresHomogeneousConfig) !==
608 fingerprint(news.postgresHomogeneousConfig) ||
609 fingerprint(current.sqlserverHomogeneousMigrationJobConfig) !==
610 fingerprint(news.sqlserverHomogeneousMigrationJobConfig) ||
611 fingerprint(current.sqlserverToPostgresConfig) !==
612 fingerprint(news.sqlserverToPostgresConfig) ||
613 fingerprint(current.oracleToPostgresConfig) !==
614 fingerprint(news.oracleToPostgresConfig) ||
615 fingerprint(current.postgresToSqlserverConfig) !==
616 fingerprint(news.postgresToSqlserverConfig);
617 const mask = fieldMask([
618 labelsChanged && "labels",
619 displayNameChanged && "displayName",
620 dumpFlagsChanged && "dumpFlags",
621 dumpPathChanged && "dumpPath",
622 dumpTypeChanged && "dumpType",
623 performanceChanged && "performanceConfig",
624 filterChanged && "filter",
625 objectsChanged && "objectsConfig",
626 conversionChanged && "conversionWorkspace",
627 vpcChanged && "vpcPeeringConnectivity",
628 sshChanged && "reverseSshConnectivity",
629 staticChanged && "staticIpConnectivity",
630 homogeneousChanged && "mysqlHomogeneousConfig",
631 homogeneousChanged && "postgresHomogeneousConfig",
632 homogeneousChanged && "sqlserverHomogeneousMigrationJobConfig",
633 homogeneousChanged && "sqlserverToPostgresConfig",
634 homogeneousChanged && "oracleToPostgresConfig",
635 homogeneousChanged && "postgresToSqlserverConfig",
636 ]);
637
638 if (mask.length > 0) {
639 const operation = yield* dm.patchProjectsLocationsMigrationJobs({
640 name: current.name ?? name,
641 updateMask: mask,
642 body: {
643 ...body,
644 name: current.name ?? name,
645 },
646 });
647 yield* waitForOperation(operation);
648 current = yield* waitUntilExists(
649 getByName(current.name ?? name),
650 current.name ?? name,
651 );
652 }
653
654 return toAttrs(current, env.project);
655 }),
656
657 delete: Effect.fn(function* ({ output }) {
658 const operation = yield* dm
659 .deleteProjectsLocationsMigrationJobs({
660 name: output.name,
661 force: true,
662 })
663 .pipe(
664 Effect.retry({
665 while: (error) => error._tag === "Conflict",
666 times: 8,
667 schedule: Schedule.spaced("2 seconds"),
668 }),
669 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
670 );
671 if (operation !== undefined) {
672 yield* waitForOperation(operation, { notFoundOk: true });
673 }
674 yield* waitUntilGone(getByName(output.name), output.name);
675 }),
676 });