@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 +8 -2
- package/dist/index.mjs +96 -22
- package/package.json +1 -1
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<
|
|
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(
|
|
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)
|
|
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 &&
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
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,
|
|
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
|
|
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))
|
|
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
|
|
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(
|
|
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))
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
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
|
|
1237
|
+
return stacks;
|
|
1164
1238
|
};
|
|
1165
1239
|
|
|
1166
1240
|
//#endregion
|