@terraforge/core 0.0.17 → 0.0.19

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.mts CHANGED
@@ -162,6 +162,11 @@ type ResourceStatusInfo = {
162
162
  tag: 'resource' | 'data';
163
163
  status: ResourceStatus;
164
164
  };
165
+ type StackStatusInfo = {
166
+ name: string;
167
+ urn: URN;
168
+ resources: ResourceStatusInfo[];
169
+ };
165
170
  //#endregion
166
171
  //#region src/backend/lock.d.ts
167
172
  type LockBackend = {
@@ -176,6 +181,7 @@ type AppState = {
176
181
  version?: number;
177
182
  idempotentToken?: UUID;
178
183
  stacks: Record<URN, StackState>;
184
+ pendingDeletes?: Record<URN, NodeState>;
179
185
  };
180
186
  type StackState = {
181
187
  name: string;
@@ -338,7 +344,7 @@ declare class WorkSpace {
338
344
  /**
339
345
  * Get the status of all resources in the app by comparing current config with state file.
340
346
  */
341
- status(app: App): Promise<ResourceStatusInfo[]>;
347
+ status(app: App): Promise<StackStatusInfo[]>;
342
348
  protected destroyProviders(): Promise<void>;
343
349
  }
344
350
  //#endregion
@@ -465,4 +471,4 @@ type CustomResourceProvider = Partial<{
465
471
  }>;
466
472
  declare const createCustomProvider: (providerId: string, resourceProviders: Record<string, CustomResourceProvider>) => Provider;
467
473
  //#endregion
468
- export { App, AppError, type Config, type CreateProps, type CustomResourceProvider, type DataSource, type DataSourceFunction, type DataSourceMeta, type DeleteProps, DynamoLockBackend, FileLockBackend, FileStateBackend, Future, type GetDataProps, type GetProps, Group, type Input, LockBackend, MemoryLockBackend, MemoryStateBackend, type Meta, type Node, type OptionalInput, type OptionalOutput, Output, type PlanProps, type ProcedureOptions, type Provider, type Resource, ResourceAlreadyExists, type ResourceClass, type ResourceConfig, ResourceError, type ResourceMeta, ResourceNotFound, type ResourceStatus, type ResourceStatusInfo, S3StateBackend, Stack, type State, StateBackend, type Tag, type URN, type UpdateProps, WorkSpace, type WorkSpaceOptions, createCustomProvider, createCustomResourceClass, createDebugger, createMeta, deferredOutput, enableDebug, findInputDeps, getMeta, isDataSource, isNode, isResource, nodeMetaSymbol, output, resolveInputs };
474
+ export { App, AppError, type Config, type CreateProps, type CustomResourceProvider, type DataSource, type DataSourceFunction, type DataSourceMeta, type DeleteProps, DynamoLockBackend, FileLockBackend, FileStateBackend, Future, type GetDataProps, type GetProps, Group, type Input, LockBackend, MemoryLockBackend, MemoryStateBackend, type Meta, type Node, type OptionalInput, type OptionalOutput, Output, type PlanProps, type ProcedureOptions, type Provider, type Resource, ResourceAlreadyExists, type ResourceClass, type ResourceConfig, ResourceError, type ResourceMeta, ResourceNotFound, type ResourceStatus, type ResourceStatusInfo, S3StateBackend, Stack, type StackStatusInfo, type State, StateBackend, type Tag, type URN, type UpdateProps, WorkSpace, type WorkSpaceOptions, createCustomProvider, createCustomResourceClass, createDebugger, createMeta, deferredOutput, enableDebug, findInputDeps, getMeta, isDataSource, isNode, isResource, nodeMetaSymbol, output, resolveInputs };
package/dist/index.mjs CHANGED
@@ -623,6 +623,16 @@ const deleteApp = async (app, opt) => {
623
623
  delete stackState.nodes[urn];
624
624
  });
625
625
  const errors = await graph.run();
626
+ if (errors.length === 0 && appState.pendingDeletes) {
627
+ for (const [urn, nodeState] of entries(appState.pendingDeletes)) try {
628
+ await deleteResource(appState.idempotentToken, urn, nodeState, opt);
629
+ delete appState.pendingDeletes[urn];
630
+ } catch (error) {
631
+ if (error instanceof Error) errors.push(error);
632
+ else errors.push(/* @__PURE__ */ new Error(`${error}`));
633
+ }
634
+ if (Object.keys(appState.pendingDeletes).length === 0) delete appState.pendingDeletes;
635
+ }
626
636
  removeEmptyStackStates(appState);
627
637
  delete appState.idempotentToken;
628
638
  await opt.backend.state.update(app.urn, appState);
@@ -813,7 +823,8 @@ const updateResource = async (resource, appToken, priorInputState, priorOutputSt
813
823
  const idempotantToken = createIdempotantToken(appToken, meta.urn, "update");
814
824
  let result;
815
825
  debug$2(meta.type);
816
- debug$2(proposedState);
826
+ debug$2("prior state", priorOutputState);
827
+ debug$2("proposed state", proposedState);
817
828
  try {
818
829
  await opt.hooks?.beforeResourceUpdate?.({
819
830
  urn: resource.urn,
@@ -883,7 +894,6 @@ const deployApp = async (app, opt) => {
883
894
  }
884
895
  const queue = createConcurrencyQueue(opt.concurrency ?? 10);
885
896
  const graph = new DependencyGraph();
886
- const replacementDeletes = /* @__PURE__ */ new Map();
887
897
  const allNodes = {};
888
898
  for (const stackState of Object.values(appState.stacks)) for (const [urn, nodeState] of entries(stackState.nodes)) allNodes[urn] = nodeState;
889
899
  for (const stack of filteredOutStacks) {
@@ -966,10 +976,33 @@ const deployApp = async (app, opt) => {
966
976
  let newResourceState;
967
977
  const ignoreReplace = forcedUpdateDependents.has(meta$1.urn);
968
978
  if (!ignoreReplace && requiresReplacement(nodeState.input, input, meta$1.config?.replaceOnChanges ?? [])) if (meta$1.config?.createBeforeReplace) {
979
+ for (const [dependentUrn, dependentNode] of nodeByUrn.entries()) {
980
+ if (!isResource(dependentNode)) continue;
981
+ const dependentMeta = getMeta(dependentNode);
982
+ if (!dependentMeta.dependencies.has(meta$1.urn)) continue;
983
+ const dependentStackState = stackStates.get(dependentMeta.stack.urn);
984
+ const dependentState = dependentStackState?.nodes[dependentUrn];
985
+ if (!dependentStackState || !dependentState) continue;
986
+ const dependencyPaths = findDependencyPaths(dependentMeta.input, meta$1.urn);
987
+ if (dependencyPaths.length === 0) continue;
988
+ const dependentProvider = findProvider(opt.providers, dependentMeta.provider);
989
+ if (dependentProvider.planResourceChange) {
990
+ if ((await dependentProvider.planResourceChange({
991
+ type: dependentMeta.type,
992
+ priorState: dependentState.output,
993
+ proposedState: input
994
+ })).requiresReplacement) {
995
+ if (!allowsDependentReplace(dependentMeta.config?.replaceOnChanges, dependencyPaths)) throw ResourceError.wrap(dependentMeta.urn, dependentMeta.type, "update", /* @__PURE__ */ new Error(`Replacing ${meta$1.urn} requires ${dependentMeta.urn} to set replaceOnChanges for its dependency fields.`));
996
+ }
997
+ }
998
+ }
969
999
  const priorState = { ...nodeState };
970
1000
  newResourceState = await createResource(node, appState.idempotentToken, input, opt);
971
1001
  if (newResourceState.output) meta$1.resolve(newResourceState.output);
972
- if (!meta$1.config?.retainOnDelete) replacementDeletes.set(meta$1.urn, priorState);
1002
+ if (!meta$1.config?.retainOnDelete) {
1003
+ appState.pendingDeletes ??= {};
1004
+ appState.pendingDeletes[meta$1.urn] = priorState;
1005
+ }
973
1006
  } else {
974
1007
  for (const [dependentUrn, dependentNode] of nodeByUrn.entries()) {
975
1008
  if (!isResource(dependentNode)) continue;
@@ -1028,11 +1061,15 @@ const deployApp = async (app, opt) => {
1028
1061
  }
1029
1062
  }
1030
1063
  const errors = await graph.run();
1031
- if (errors.length === 0 && replacementDeletes.size > 0) for (const [urn, nodeState] of replacementDeletes.entries()) try {
1032
- await deleteResource(appState.idempotentToken, urn, nodeState, opt);
1033
- } catch (error) {
1034
- if (error instanceof Error) errors.push(error);
1035
- else errors.push(/* @__PURE__ */ new Error(`${error}`));
1064
+ if (errors.length === 0 && appState.pendingDeletes) {
1065
+ for (const [urn, nodeState] of entries(appState.pendingDeletes)) try {
1066
+ await deleteResource(appState.idempotentToken, urn, nodeState, opt);
1067
+ delete appState.pendingDeletes[urn];
1068
+ } catch (error) {
1069
+ if (error instanceof Error) errors.push(error);
1070
+ else errors.push(/* @__PURE__ */ new Error(`${error}`));
1071
+ }
1072
+ if (Object.keys(appState.pendingDeletes).length === 0) delete appState.pendingDeletes;
1036
1073
  }
1037
1074
  removeEmptyStackStates(appState);
1038
1075
  delete appState.idempotentToken;
@@ -1097,26 +1134,50 @@ const refresh = async (app, opt) => {
1097
1134
  //#endregion
1098
1135
  //#region src/workspace/procedure/status.ts
1099
1136
  /**
1100
- * Extract static values from inputs, replacing Output/Future/Promise with a placeholder.
1101
- * This allows comparing config without needing to resolve dependencies.
1137
+ * Extract static values from inputs, omitting Output/Future/Promise values.
1138
+ * This allows comparing only the static parts of config without needing to resolve dependencies.
1139
+ * We omit dynamic values entirely rather than using placeholders, since the state file
1140
+ * contains resolved values that we can't meaningfully compare against.
1102
1141
  */
1103
1142
  const extractStaticInputs = (inputs) => {
1104
- if (inputs instanceof Output || inputs instanceof Future || inputs instanceof Promise) return "__unresolved__";
1143
+ if (inputs instanceof Output || inputs instanceof Future || inputs instanceof Promise) return;
1105
1144
  if (Array.isArray(inputs)) return inputs.map(extractStaticInputs);
1106
1145
  if (inputs !== null && typeof inputs === "object" && inputs.constructor === Object) {
1107
1146
  const result = {};
1108
- for (const [key, value] of Object.entries(inputs)) result[key] = extractStaticInputs(value);
1147
+ for (const [key, value] of Object.entries(inputs)) {
1148
+ const extracted = extractStaticInputs(value);
1149
+ if (extracted !== void 0) result[key] = extracted;
1150
+ }
1109
1151
  return result;
1110
1152
  }
1111
1153
  return inputs;
1112
1154
  };
1155
+ /**
1156
+ * Remove keys from state that correspond to dynamic (Output/Future/Promise) values in config.
1157
+ * This ensures we only compare static values that exist in both.
1158
+ */
1159
+ const filterStateToMatchConfig = (state, config) => {
1160
+ if (config instanceof Output || config instanceof Future || config instanceof Promise) return;
1161
+ if (Array.isArray(config) && Array.isArray(state)) return config.map((configItem, index) => filterStateToMatchConfig(state[index], configItem));
1162
+ if (config !== null && typeof config === "object" && config.constructor === Object && state !== null && typeof state === "object") {
1163
+ const result = {};
1164
+ for (const [key, configValue] of Object.entries(config)) {
1165
+ const stateValue = state[key];
1166
+ const filtered = filterStateToMatchConfig(stateValue, configValue);
1167
+ if (filtered !== void 0) result[key] = filtered;
1168
+ }
1169
+ return result;
1170
+ }
1171
+ return state;
1172
+ };
1113
1173
  const status = async (app, opt) => {
1114
1174
  const appState = await opt.backend.state.get(app.urn);
1115
- const resources = [];
1175
+ const stacks = [];
1116
1176
  const configuredUrns = /* @__PURE__ */ new Set();
1117
1177
  for (const stack of app.stacks) for (const node of stack.nodes) configuredUrns.add(getMeta(node).urn);
1118
1178
  for (const stack of app.stacks) {
1119
1179
  const stackState = appState?.stacks[stack.urn];
1180
+ const resources = [];
1120
1181
  for (const node of stack.nodes) {
1121
1182
  const meta = getMeta(node);
1122
1183
  const nodeState = stackState?.nodes[meta.urn];
@@ -1134,7 +1195,7 @@ const status = async (app, opt) => {
1134
1195
  continue;
1135
1196
  }
1136
1197
  const currentInput = extractStaticInputs(meta.input);
1137
- const hasChanged = !compareState(extractStaticInputs(nodeState.input), currentInput);
1198
+ const hasChanged = !compareState(filterStateToMatchConfig(nodeState.input, meta.input), currentInput);
1138
1199
  resources.push({
1139
1200
  ...baseInfo,
1140
1201
  status: hasChanged ? "changed" : "created"
@@ -1149,18 +1210,31 @@ const status = async (app, opt) => {
1149
1210
  status: "stale"
1150
1211
  });
1151
1212
  }
1213
+ stacks.push({
1214
+ name: stack.name,
1215
+ urn: stack.urn,
1216
+ resources
1217
+ });
1152
1218
  }
1153
1219
  if (appState) {
1154
1220
  const configuredStackUrns = new Set(app.stacks.map((s) => s.urn));
1155
- for (const [stackUrn, stackState] of Object.entries(appState.stacks)) if (!configuredStackUrns.has(stackUrn)) for (const [urn, nodeState] of Object.entries(stackState.nodes)) resources.push({
1156
- urn,
1157
- type: nodeState.type,
1158
- provider: nodeState.provider,
1159
- tag: nodeState.tag,
1160
- status: "stale"
1161
- });
1221
+ for (const [stackUrn, stackState] of Object.entries(appState.stacks)) if (!configuredStackUrns.has(stackUrn)) {
1222
+ const resources = [];
1223
+ for (const [urn, nodeState] of Object.entries(stackState.nodes)) resources.push({
1224
+ urn,
1225
+ type: nodeState.type,
1226
+ provider: nodeState.provider,
1227
+ tag: nodeState.tag,
1228
+ status: "stale"
1229
+ });
1230
+ stacks.push({
1231
+ name: stackState.name,
1232
+ urn: stackUrn,
1233
+ resources
1234
+ });
1235
+ }
1162
1236
  }
1163
- return resources;
1237
+ return stacks;
1164
1238
  };
1165
1239
 
1166
1240
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@terraforge/core",
3
- "version": "0.0.17",
3
+ "version": "0.0.19",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",