@terraforge/core 0.0.6 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -58,12 +58,13 @@ declare const isDataSource: (obj: object) => obj is DataSource;
58
58
  type ResourceConfig = Config & {
59
59
  /** Import an existing resource instead of creating a new resource. */
60
60
  import?: string;
61
- /** If true the resource will be retained in the backing cloud provider during a Pulumi delete operation. */
61
+ /** If true the resource will be retained in the backing cloud provider during a delete operation. */
62
62
  retainOnDelete?: boolean;
63
- /** Override the default create-after-delete behavior when replacing a resource. */
64
63
  /** If set, the provider’s Delete method will not be called for this resource if the specified resource is being deleted as well. */
65
64
  /** Declare that changes to certain properties should be treated as forcing a replacement. */
66
65
  replaceOnChanges?: string[];
66
+ /** If true, create the replacement before deleting the existing resource. */
67
+ createBeforeReplace?: boolean;
67
68
  };
68
69
  type ResourceMeta<I extends State = State, O extends State = State> = Meta<'resource', I, O, ResourceConfig>;
69
70
  type Resource<I extends State = State, O extends State = State> = O & {
package/dist/index.js CHANGED
@@ -186,6 +186,40 @@ var findInputDeps = (props) => {
186
186
  find(props);
187
187
  return deps;
188
188
  };
189
+ var formatPath = (path) => {
190
+ if (path.length === 0) {
191
+ return "<root>";
192
+ }
193
+ return path.map((part) => {
194
+ if (typeof part === "number") {
195
+ return `[${part}]`;
196
+ }
197
+ return `.${part}`;
198
+ }).join("").replace(/^\./, "");
199
+ };
200
+ var findInputDepsWithPaths = (props) => {
201
+ const deps = [];
202
+ const find = (value, path) => {
203
+ if (value instanceof Output) {
204
+ for (const meta of value.dependencies) {
205
+ deps.push({
206
+ meta,
207
+ path: formatPath(path)
208
+ });
209
+ }
210
+ return;
211
+ }
212
+ if (Array.isArray(value)) {
213
+ value.map((item, index) => find(item, [...path, index]));
214
+ return;
215
+ }
216
+ if (value?.constructor === Object) {
217
+ Object.entries(value).map(([key, item]) => find(item, [...path, key]));
218
+ }
219
+ };
220
+ find(props, []);
221
+ return deps;
222
+ };
189
223
  var resolveInputs = async (inputs) => {
190
224
  const unresolved = [];
191
225
  const find = (props, parent, key) => {
@@ -420,6 +454,9 @@ var DependencyGraph = class {
420
454
  this.callbacks.set(urn, callback);
421
455
  this.graph.mergeNode(urn);
422
456
  for (const dep of deps) {
457
+ if (!dep) {
458
+ throw new Error(`Resource ${urn} has an undefined dependency.`);
459
+ }
423
460
  if (willCreateCycle(this.graph, dep, urn)) {
424
461
  throw new Error(`There is a circular dependency between ${urn} -> ${dep}`);
425
462
  }
@@ -1030,6 +1067,7 @@ var deployApp = async (app, opt) => {
1030
1067
  }
1031
1068
  const queue = createConcurrencyQueue(opt.concurrency ?? 10);
1032
1069
  const graph = new DependencyGraph();
1070
+ const replacementDeletes = /* @__PURE__ */ new Map();
1033
1071
  const allNodes = {};
1034
1072
  for (const stackState of Object.values(appState.stacks)) {
1035
1073
  for (const [urn, nodeState] of entries(stackState.nodes)) {
@@ -1099,6 +1137,14 @@ var deployApp = async (app, opt) => {
1099
1137
  for (const node of stack.nodes) {
1100
1138
  const meta = getMeta(node);
1101
1139
  const dependencies = [...meta.dependencies];
1140
+ for (let i = 0; i < dependencies.length; i++) {
1141
+ if (!dependencies[i]) {
1142
+ const depPaths = findInputDepsWithPaths(meta.input).map((dep) => `${dep.path} -> ${dep.meta?.urn ?? "undefined"}`).join(", ");
1143
+ throw new Error(
1144
+ `Resource ${meta.urn} has an undefined dependency at index ${i}. Check inputs for missing/undefined Output references. ` + (depPaths ? `Dependency sources: ${depPaths}` : "")
1145
+ );
1146
+ }
1147
+ }
1102
1148
  const partialNewResourceState = {
1103
1149
  dependencies,
1104
1150
  lifecycle: isResource(node) ? {
@@ -1177,99 +1223,107 @@ var deployApp = async (app, opt) => {
1177
1223
  let newResourceState;
1178
1224
  const ignoreReplace = forcedUpdateDependents.has(meta2.urn);
1179
1225
  if (!ignoreReplace && requiresReplacement(nodeState.input, input, meta2.config?.replaceOnChanges ?? [])) {
1180
- for (const [dependentUrn, dependentNode] of nodeByUrn.entries()) {
1181
- if (!isResource(dependentNode)) {
1182
- continue;
1183
- }
1184
- const dependentMeta = getMeta(dependentNode);
1185
- if (!dependentMeta.dependencies.has(meta2.urn)) {
1186
- continue;
1187
- }
1188
- if (plannedDependents.has(dependentUrn)) {
1189
- continue;
1226
+ if (meta2.config?.createBeforeReplace) {
1227
+ const priorState = { ...nodeState };
1228
+ newResourceState = await createResource(node, appState.idempotentToken, input, opt);
1229
+ if (!meta2.config?.retainOnDelete) {
1230
+ replacementDeletes.set(meta2.urn, priorState);
1190
1231
  }
1191
- const dependentStackState = stackStates.get(dependentMeta.stack.urn);
1192
- const dependentState = dependentStackState?.nodes[dependentUrn];
1193
- if (!dependentStackState || !dependentState) {
1194
- continue;
1195
- }
1196
- const dependencyPaths = findDependencyPaths(dependentMeta.input, meta2.urn);
1197
- if (dependencyPaths.length === 0) {
1198
- continue;
1199
- }
1200
- const detachedInput = stripDependencyInputs(
1201
- dependentState.input,
1202
- dependentMeta.input,
1203
- meta2.urn
1204
- );
1205
- if (compareState(dependentState.input, detachedInput)) {
1206
- continue;
1207
- }
1208
- plannedDependents.add(dependentUrn);
1209
- let dependentRequiresReplacement = false;
1210
- const dependentProvider = findProvider(opt.providers, dependentMeta.provider);
1211
- if (dependentProvider.planResourceChange) {
1212
- try {
1213
- const dependentPlan = await dependentProvider.planResourceChange({
1214
- type: dependentMeta.type,
1215
- priorState: dependentState.output,
1216
- proposedState: detachedInput
1217
- });
1218
- dependentRequiresReplacement = dependentPlan.requiresReplacement;
1219
- } catch (error) {
1220
- throw ResourceError.wrap(
1221
- dependentMeta.urn,
1222
- dependentMeta.type,
1223
- "update",
1224
- error
1225
- );
1232
+ } else {
1233
+ for (const [dependentUrn, dependentNode] of nodeByUrn.entries()) {
1234
+ if (!isResource(dependentNode)) {
1235
+ continue;
1226
1236
  }
1227
- }
1228
- if (dependentRequiresReplacement) {
1229
- if (!allowsDependentReplace(
1230
- dependentMeta.config?.replaceOnChanges,
1231
- dependencyPaths
1232
- )) {
1233
- throw ResourceError.wrap(
1234
- dependentMeta.urn,
1235
- dependentMeta.type,
1236
- "update",
1237
- new Error(
1238
- `Replacing ${meta2.urn} requires ${dependentMeta.urn} to set replaceOnChanges for its dependency fields.`
1239
- )
1240
- );
1237
+ const dependentMeta = getMeta(dependentNode);
1238
+ if (!dependentMeta.dependencies.has(meta2.urn)) {
1239
+ continue;
1241
1240
  }
1242
- await deleteResource(
1243
- appState.idempotentToken,
1244
- dependentUrn,
1245
- dependentState,
1246
- opt
1247
- );
1248
- delete dependentStackState.nodes[dependentUrn];
1249
- } else {
1250
- const updated = await updateResource(
1251
- dependentNode,
1252
- appState.idempotentToken,
1241
+ if (plannedDependents.has(dependentUrn)) {
1242
+ continue;
1243
+ }
1244
+ const dependentStackState = stackStates.get(dependentMeta.stack.urn);
1245
+ const dependentState = dependentStackState?.nodes[dependentUrn];
1246
+ if (!dependentStackState || !dependentState) {
1247
+ continue;
1248
+ }
1249
+ const dependencyPaths = findDependencyPaths(dependentMeta.input, meta2.urn);
1250
+ if (dependencyPaths.length === 0) {
1251
+ continue;
1252
+ }
1253
+ const detachedInput = stripDependencyInputs(
1253
1254
  dependentState.input,
1254
- dependentState.output,
1255
- detachedInput,
1256
- opt
1255
+ dependentMeta.input,
1256
+ meta2.urn
1257
1257
  );
1258
- Object.assign(dependentState, {
1259
- input: detachedInput,
1260
- ...updated
1261
- });
1262
- forcedUpdateDependents.add(dependentUrn);
1258
+ if (compareState(dependentState.input, detachedInput)) {
1259
+ continue;
1260
+ }
1261
+ plannedDependents.add(dependentUrn);
1262
+ let dependentRequiresReplacement = false;
1263
+ const dependentProvider = findProvider(opt.providers, dependentMeta.provider);
1264
+ if (dependentProvider.planResourceChange) {
1265
+ try {
1266
+ const dependentPlan = await dependentProvider.planResourceChange({
1267
+ type: dependentMeta.type,
1268
+ priorState: dependentState.output,
1269
+ proposedState: detachedInput
1270
+ });
1271
+ dependentRequiresReplacement = dependentPlan.requiresReplacement;
1272
+ } catch (error) {
1273
+ throw ResourceError.wrap(
1274
+ dependentMeta.urn,
1275
+ dependentMeta.type,
1276
+ "update",
1277
+ error
1278
+ );
1279
+ }
1280
+ }
1281
+ if (dependentRequiresReplacement) {
1282
+ if (!allowsDependentReplace(
1283
+ dependentMeta.config?.replaceOnChanges,
1284
+ dependencyPaths
1285
+ )) {
1286
+ throw ResourceError.wrap(
1287
+ dependentMeta.urn,
1288
+ dependentMeta.type,
1289
+ "update",
1290
+ new Error(
1291
+ `Replacing ${meta2.urn} requires ${dependentMeta.urn} to set replaceOnChanges for its dependency fields.`
1292
+ )
1293
+ );
1294
+ }
1295
+ await deleteResource(
1296
+ appState.idempotentToken,
1297
+ dependentUrn,
1298
+ dependentState,
1299
+ opt
1300
+ );
1301
+ delete dependentStackState.nodes[dependentUrn];
1302
+ } else {
1303
+ const updated = await updateResource(
1304
+ dependentNode,
1305
+ appState.idempotentToken,
1306
+ dependentState.input,
1307
+ dependentState.output,
1308
+ detachedInput,
1309
+ opt
1310
+ );
1311
+ Object.assign(dependentState, {
1312
+ input: detachedInput,
1313
+ ...updated
1314
+ });
1315
+ forcedUpdateDependents.add(dependentUrn);
1316
+ }
1263
1317
  }
1318
+ newResourceState = await replaceResource(
1319
+ node,
1320
+ appState.idempotentToken,
1321
+ nodeState.input,
1322
+ nodeState.output,
1323
+ input,
1324
+ opt
1325
+ );
1264
1326
  }
1265
- newResourceState = await replaceResource(
1266
- node,
1267
- appState.idempotentToken,
1268
- nodeState.input,
1269
- nodeState.output,
1270
- input,
1271
- opt
1272
- );
1273
1327
  } else {
1274
1328
  newResourceState = await updateResource(
1275
1329
  node,
@@ -1300,6 +1354,19 @@ var deployApp = async (app, opt) => {
1300
1354
  }
1301
1355
  }
1302
1356
  const errors = await graph.run();
1357
+ if (errors.length === 0 && replacementDeletes.size > 0) {
1358
+ for (const [urn, nodeState] of replacementDeletes.entries()) {
1359
+ try {
1360
+ await deleteResource(appState.idempotentToken, urn, nodeState, opt);
1361
+ } catch (error) {
1362
+ if (error instanceof Error) {
1363
+ errors.push(error);
1364
+ } else {
1365
+ errors.push(new Error(`${error}`));
1366
+ }
1367
+ }
1368
+ }
1369
+ }
1303
1370
  removeEmptyStackStates(appState);
1304
1371
  delete appState.idempotentToken;
1305
1372
  await opt.backend.state.update(app.urn, appState);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@terraforge/core",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",