@terraforge/core 0.0.9 → 0.0.11
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 +447 -0
- package/dist/index.mjs +1454 -0
- package/package.json +6 -6
- package/dist/index.d.ts +0 -421
- package/dist/index.js +0 -1849
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1454 @@
|
|
|
1
|
+
import asyncOnExit from "async-on-exit";
|
|
2
|
+
import promiseLimit from "p-limit";
|
|
3
|
+
import { DirectedGraph } from "graphology";
|
|
4
|
+
import { topologicalGenerations, willCreateCycle } from "graphology-dag";
|
|
5
|
+
import { v5 } from "uuid";
|
|
6
|
+
import { get } from "get-wild";
|
|
7
|
+
import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { lock } from "proper-lockfile";
|
|
10
|
+
import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
|
|
11
|
+
import { DynamoDB } from "@aws-sdk/client-dynamodb";
|
|
12
|
+
import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
|
|
15
|
+
//#region src/node.ts
|
|
16
|
+
const nodeMetaSymbol = Symbol("metadata");
|
|
17
|
+
const isNode = (obj) => {
|
|
18
|
+
const meta = obj[nodeMetaSymbol];
|
|
19
|
+
return meta && typeof meta === "object" && meta !== null && "tag" in meta && typeof meta.tag === "string";
|
|
20
|
+
};
|
|
21
|
+
function getMeta(node) {
|
|
22
|
+
return node[nodeMetaSymbol];
|
|
23
|
+
}
|
|
24
|
+
const isResource = (obj) => {
|
|
25
|
+
return isNode(obj) && obj[nodeMetaSymbol].tag === "resource";
|
|
26
|
+
};
|
|
27
|
+
const isDataSource = (obj) => {
|
|
28
|
+
return isNode(obj) && obj[nodeMetaSymbol].tag === "data";
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/group.ts
|
|
33
|
+
var Group = class Group {
|
|
34
|
+
children = [];
|
|
35
|
+
constructor(parent, type, name) {
|
|
36
|
+
this.parent = parent;
|
|
37
|
+
this.type = type;
|
|
38
|
+
this.name = name;
|
|
39
|
+
parent?.children.push(this);
|
|
40
|
+
}
|
|
41
|
+
get urn() {
|
|
42
|
+
return `${this.parent ? this.parent.urn : "urn"}:${this.type}:{${this.name}}`;
|
|
43
|
+
}
|
|
44
|
+
addChild(child) {
|
|
45
|
+
if (isNode(child)) {
|
|
46
|
+
const meta = getMeta(child);
|
|
47
|
+
if (this.children.filter((c) => isResource(c)).map((c) => getMeta(c)).find((c) => c.type === meta.type && c.logicalId === meta.logicalId)) throw new Error(`Duplicate node found: ${meta.type}:${meta.logicalId}`);
|
|
48
|
+
}
|
|
49
|
+
if (child instanceof Group) {
|
|
50
|
+
if (this.children.filter((c) => c instanceof Group).find((c) => c.type === child.type && c.name === child.name)) throw new Error(`Duplicate group found: ${child.type}:${child.name}`);
|
|
51
|
+
}
|
|
52
|
+
this.children.push(child);
|
|
53
|
+
}
|
|
54
|
+
add(...children) {
|
|
55
|
+
for (const child of children) this.addChild(child);
|
|
56
|
+
}
|
|
57
|
+
get nodes() {
|
|
58
|
+
return this.children.map((child) => {
|
|
59
|
+
if (child instanceof Group) return child.nodes;
|
|
60
|
+
if (isNode(child)) return child;
|
|
61
|
+
}).flat().filter((child) => !!child);
|
|
62
|
+
}
|
|
63
|
+
get resources() {
|
|
64
|
+
return this.nodes.filter((node) => isResource(node));
|
|
65
|
+
}
|
|
66
|
+
get dataSources() {
|
|
67
|
+
return this.nodes.filter((node) => isDataSource(node));
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/stack.ts
|
|
73
|
+
var Stack = class extends Group {
|
|
74
|
+
dependencies = /* @__PURE__ */ new Set();
|
|
75
|
+
constructor(app, name) {
|
|
76
|
+
super(app, "stack", name);
|
|
77
|
+
this.app = app;
|
|
78
|
+
}
|
|
79
|
+
dependsOn(...stacks) {
|
|
80
|
+
for (const stack of stacks) {
|
|
81
|
+
if (stack.app !== this.app) throw new Error(`Stacks that belong to different apps can't be dependent on each other`);
|
|
82
|
+
this.dependencies.add(stack);
|
|
83
|
+
}
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const findParentStack = (group) => {
|
|
88
|
+
if (group instanceof Stack) return group;
|
|
89
|
+
if (!group.parent) throw new Error("No stack found");
|
|
90
|
+
return findParentStack(group.parent);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/app.ts
|
|
95
|
+
var App = class extends Group {
|
|
96
|
+
constructor(name) {
|
|
97
|
+
super(void 0, "app", name);
|
|
98
|
+
this.name = name;
|
|
99
|
+
}
|
|
100
|
+
get stacks() {
|
|
101
|
+
return this.children.filter((child) => child instanceof Stack);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
//#endregion
|
|
106
|
+
//#region src/future.ts
|
|
107
|
+
const IDLE = 0;
|
|
108
|
+
const PENDING = 1;
|
|
109
|
+
const RESOLVED = 2;
|
|
110
|
+
const REJECTED = 3;
|
|
111
|
+
var Future = class Future {
|
|
112
|
+
listeners = /* @__PURE__ */ new Set();
|
|
113
|
+
status = IDLE;
|
|
114
|
+
data;
|
|
115
|
+
error;
|
|
116
|
+
constructor(callback) {
|
|
117
|
+
this.callback = callback;
|
|
118
|
+
}
|
|
119
|
+
get [Symbol.toStringTag]() {
|
|
120
|
+
switch (this.status) {
|
|
121
|
+
case IDLE: return `<idle>`;
|
|
122
|
+
case PENDING: return `<pending>`;
|
|
123
|
+
case RESOLVED: return `${this.data}`;
|
|
124
|
+
case REJECTED: return `<rejected> ${this.error}`;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
pipe(cb) {
|
|
128
|
+
return new Future((resolve$1, reject) => {
|
|
129
|
+
this.then((value) => {
|
|
130
|
+
Promise.resolve(cb(value)).then((value$1) => {
|
|
131
|
+
resolve$1(value$1);
|
|
132
|
+
}).catch(reject);
|
|
133
|
+
}, reject);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
then(resolve$1, reject) {
|
|
137
|
+
if (this.status === RESOLVED) resolve$1(this.data);
|
|
138
|
+
else if (this.status === REJECTED) reject?.(this.error);
|
|
139
|
+
else {
|
|
140
|
+
this.listeners.add({
|
|
141
|
+
resolve: resolve$1,
|
|
142
|
+
reject
|
|
143
|
+
});
|
|
144
|
+
if (this.status === IDLE) {
|
|
145
|
+
this.status = PENDING;
|
|
146
|
+
this.callback((data) => {
|
|
147
|
+
if (this.status === PENDING) {
|
|
148
|
+
this.status = RESOLVED;
|
|
149
|
+
this.data = data;
|
|
150
|
+
this.listeners.forEach(({ resolve: resolve$2 }) => resolve$2(data));
|
|
151
|
+
this.listeners.clear();
|
|
152
|
+
}
|
|
153
|
+
}, (error) => {
|
|
154
|
+
if (this.status === PENDING) {
|
|
155
|
+
this.status = REJECTED;
|
|
156
|
+
this.error = error;
|
|
157
|
+
this.listeners.forEach(({ reject: reject$1 }) => reject$1?.(error));
|
|
158
|
+
this.listeners.clear();
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
//#endregion
|
|
167
|
+
//#region src/input.ts
|
|
168
|
+
const findInputDeps = (props) => {
|
|
169
|
+
const deps = [];
|
|
170
|
+
const find = (props$1) => {
|
|
171
|
+
if (props$1 instanceof Output) deps.push(...props$1.dependencies);
|
|
172
|
+
else if (Array.isArray(props$1)) props$1.map(find);
|
|
173
|
+
else if (props$1?.constructor === Object) Object.values(props$1).map(find);
|
|
174
|
+
};
|
|
175
|
+
find(props);
|
|
176
|
+
return deps;
|
|
177
|
+
};
|
|
178
|
+
const resolveInputs = async (inputs) => {
|
|
179
|
+
const unresolved = [];
|
|
180
|
+
const find = (props, parent, key) => {
|
|
181
|
+
if (props instanceof Output || props instanceof Future || props instanceof Promise) unresolved.push([parent, key]);
|
|
182
|
+
else if (Array.isArray(props)) props.map((value, index) => find(value, props, index));
|
|
183
|
+
else if (props?.constructor === Object) Object.entries(props).map(([key$1, value]) => find(value, props, key$1));
|
|
184
|
+
};
|
|
185
|
+
find(inputs, {}, "root");
|
|
186
|
+
const responses = await Promise.all(unresolved.map(async ([obj, key]) => {
|
|
187
|
+
const promise = obj[key];
|
|
188
|
+
let timeout;
|
|
189
|
+
const response = await Promise.race([promise, new Promise((_, reject) => {
|
|
190
|
+
timeout = setTimeout(() => {
|
|
191
|
+
if (promise instanceof Output) reject(/* @__PURE__ */ new Error(`Resolving Output<${[...promise.dependencies].map((d) => d.urn).join(", ")}> took too long.`));
|
|
192
|
+
else if (promise instanceof Future) reject(/* @__PURE__ */ new Error("Resolving Future took too long."));
|
|
193
|
+
else reject(/* @__PURE__ */ new Error("Resolving Promise took too long."));
|
|
194
|
+
}, 3e3);
|
|
195
|
+
})]);
|
|
196
|
+
clearTimeout(timeout);
|
|
197
|
+
return response;
|
|
198
|
+
}));
|
|
199
|
+
unresolved.forEach(([props, key], i) => {
|
|
200
|
+
props[key] = responses[i];
|
|
201
|
+
});
|
|
202
|
+
return inputs;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
//#endregion
|
|
206
|
+
//#region src/output.ts
|
|
207
|
+
var Output = class Output extends Future {
|
|
208
|
+
constructor(dependencies, callback) {
|
|
209
|
+
super(callback);
|
|
210
|
+
this.dependencies = dependencies;
|
|
211
|
+
}
|
|
212
|
+
pipe(cb) {
|
|
213
|
+
return new Output(this.dependencies, (resolve$1, reject) => {
|
|
214
|
+
this.then((value) => {
|
|
215
|
+
Promise.resolve(cb(value)).then((value$1) => {
|
|
216
|
+
resolve$1(value$1);
|
|
217
|
+
}).catch(reject);
|
|
218
|
+
}, reject);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
const deferredOutput = (cb) => {
|
|
223
|
+
return new Output(/* @__PURE__ */ new Set(), cb);
|
|
224
|
+
};
|
|
225
|
+
const output = (value) => {
|
|
226
|
+
return deferredOutput((resolve$1) => resolve$1(value));
|
|
227
|
+
};
|
|
228
|
+
const combine = (...inputs) => {
|
|
229
|
+
return new Output(new Set(findInputDeps(inputs)), (resolve$1, reject) => {
|
|
230
|
+
Promise.all(inputs).then((result) => {
|
|
231
|
+
resolve$1(result);
|
|
232
|
+
}, reject);
|
|
233
|
+
});
|
|
234
|
+
};
|
|
235
|
+
const resolve = (inputs, transformer) => {
|
|
236
|
+
return combine(...inputs).pipe((data) => {
|
|
237
|
+
return transformer(...data);
|
|
238
|
+
});
|
|
239
|
+
};
|
|
240
|
+
const interpolate = (literals, ...placeholders) => {
|
|
241
|
+
return combine(...placeholders).pipe((unwrapped) => {
|
|
242
|
+
const result = [];
|
|
243
|
+
for (let i = 0; i < unwrapped.length; i++) result.push(literals[i], unwrapped[i]);
|
|
244
|
+
result.push(literals.at(-1));
|
|
245
|
+
return result.join("");
|
|
246
|
+
});
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
//#endregion
|
|
250
|
+
//#region src/urn.ts
|
|
251
|
+
const createUrn = (tag, type, name, parentUrn) => {
|
|
252
|
+
return `${parentUrn ? parentUrn : "urn"}:${tag}:${type}:{${name}}`;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
//#endregion
|
|
256
|
+
//#region src/meta.ts
|
|
257
|
+
const createMeta = (tag, provider, parent, type, logicalId, input, config) => {
|
|
258
|
+
const urn = createUrn(tag, type, logicalId, parent.urn);
|
|
259
|
+
const stack = findParentStack(parent);
|
|
260
|
+
let output$1;
|
|
261
|
+
return {
|
|
262
|
+
tag,
|
|
263
|
+
urn,
|
|
264
|
+
logicalId,
|
|
265
|
+
type,
|
|
266
|
+
stack,
|
|
267
|
+
provider,
|
|
268
|
+
input,
|
|
269
|
+
config,
|
|
270
|
+
get dependencies() {
|
|
271
|
+
const dependencies = /* @__PURE__ */ new Set();
|
|
272
|
+
const linkMetaDep = (dep) => {
|
|
273
|
+
if (dep.urn === urn) throw new Error("You can't depend on yourself");
|
|
274
|
+
dependencies.add(dep.urn);
|
|
275
|
+
};
|
|
276
|
+
for (const dep of findInputDeps(input)) linkMetaDep(dep);
|
|
277
|
+
for (const dep of config?.dependsOn ?? []) linkMetaDep(getMeta(dep));
|
|
278
|
+
return dependencies;
|
|
279
|
+
},
|
|
280
|
+
resolve(data) {
|
|
281
|
+
output$1 = data;
|
|
282
|
+
},
|
|
283
|
+
output(cb) {
|
|
284
|
+
return new Output(new Set([this]), (resolve$1) => {
|
|
285
|
+
if (!output$1) throw new Error(`Unresolved output for ${tag}: ${urn}`);
|
|
286
|
+
resolve$1(cb(output$1));
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
//#endregion
|
|
293
|
+
//#region src/debug.ts
|
|
294
|
+
let enabled = false;
|
|
295
|
+
const enableDebug = () => {
|
|
296
|
+
enabled = true;
|
|
297
|
+
};
|
|
298
|
+
const createDebugger = (group) => {
|
|
299
|
+
return (...args) => {
|
|
300
|
+
if (!enabled) return;
|
|
301
|
+
console.log();
|
|
302
|
+
console.log(`${group}:`, ...args);
|
|
303
|
+
console.log();
|
|
304
|
+
};
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
//#endregion
|
|
308
|
+
//#region src/workspace/exit.ts
|
|
309
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
310
|
+
let listening = false;
|
|
311
|
+
const onExit = (cb) => {
|
|
312
|
+
listeners.add(cb);
|
|
313
|
+
if (!listening) {
|
|
314
|
+
listening = true;
|
|
315
|
+
asyncOnExit(async () => {
|
|
316
|
+
await Promise.allSettled([...listeners].map((cb$1) => cb$1()));
|
|
317
|
+
}, true);
|
|
318
|
+
}
|
|
319
|
+
return () => {
|
|
320
|
+
listeners.delete(cb);
|
|
321
|
+
if (listeners.size === 0) {
|
|
322
|
+
listening = false;
|
|
323
|
+
asyncOnExit.dispose();
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
//#endregion
|
|
329
|
+
//#region src/workspace/lock.ts
|
|
330
|
+
const lockApp = async (lockBackend, app, fn) => {
|
|
331
|
+
let releaseLock;
|
|
332
|
+
try {
|
|
333
|
+
releaseLock = await lockBackend.lock(app.urn);
|
|
334
|
+
} catch (error) {
|
|
335
|
+
throw new Error(`Already in progress: ${app.urn}`);
|
|
336
|
+
}
|
|
337
|
+
const releaseExit = onExit(async () => {
|
|
338
|
+
await releaseLock();
|
|
339
|
+
});
|
|
340
|
+
let result;
|
|
341
|
+
try {
|
|
342
|
+
result = await fn();
|
|
343
|
+
} catch (error) {
|
|
344
|
+
throw error;
|
|
345
|
+
} finally {
|
|
346
|
+
await releaseLock();
|
|
347
|
+
releaseExit();
|
|
348
|
+
}
|
|
349
|
+
return result;
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
//#endregion
|
|
353
|
+
//#region src/workspace/concurrency.ts
|
|
354
|
+
const createConcurrencyQueue = (concurrency) => {
|
|
355
|
+
const queue = promiseLimit(concurrency);
|
|
356
|
+
return (cb) => {
|
|
357
|
+
return queue(cb);
|
|
358
|
+
};
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
//#endregion
|
|
362
|
+
//#region src/workspace/entries.ts
|
|
363
|
+
const entries = (object) => {
|
|
364
|
+
return Object.entries(object);
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
//#endregion
|
|
368
|
+
//#region src/workspace/dependency.ts
|
|
369
|
+
var DependencyGraph = class {
|
|
370
|
+
graph = new DirectedGraph();
|
|
371
|
+
callbacks = /* @__PURE__ */ new Map();
|
|
372
|
+
add(urn, deps, callback) {
|
|
373
|
+
this.callbacks.set(urn, callback);
|
|
374
|
+
this.graph.mergeNode(urn);
|
|
375
|
+
for (const dep of deps) {
|
|
376
|
+
if (!dep) throw new Error(`Resource ${urn} has an undefined dependency.`);
|
|
377
|
+
if (willCreateCycle(this.graph, dep, urn)) throw new Error(`There is a circular dependency between ${urn} -> ${dep}`);
|
|
378
|
+
this.graph.mergeEdge(dep, urn);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
validate() {
|
|
382
|
+
const nodes = this.graph.nodes();
|
|
383
|
+
for (const urn of nodes) if (!this.callbacks.has(urn)) {
|
|
384
|
+
const deps = this.graph.filterNodes((node) => {
|
|
385
|
+
return this.graph.areNeighbors(node, urn);
|
|
386
|
+
});
|
|
387
|
+
throw new Error(`The following resources ${deps.join(", ")} have a missing dependency: ${urn}`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
async run() {
|
|
391
|
+
this.validate();
|
|
392
|
+
const graph = topologicalGenerations(this.graph);
|
|
393
|
+
const errors = [];
|
|
394
|
+
for (const list of graph) {
|
|
395
|
+
const result = await Promise.allSettled(list.map((urn) => {
|
|
396
|
+
const callback = this.callbacks.get(urn);
|
|
397
|
+
if (!callback) return;
|
|
398
|
+
return callback();
|
|
399
|
+
}));
|
|
400
|
+
for (const entry of result) if (entry.status === "rejected") if (entry.reason instanceof Error) errors.push(entry.reason);
|
|
401
|
+
else errors.push(/* @__PURE__ */ new Error(`Unknown error: ${entry.reason}`));
|
|
402
|
+
if (errors.length > 0) break;
|
|
403
|
+
}
|
|
404
|
+
return errors;
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
const dependentsOn = (resources, dependency) => {
|
|
408
|
+
const dependents = [];
|
|
409
|
+
for (const [urn, resource] of entries(resources)) if (resource.dependencies.includes(dependency)) dependents.push(urn);
|
|
410
|
+
return dependents;
|
|
411
|
+
};
|
|
412
|
+
const findDependencyPaths = (value, dependencyUrn, path = []) => {
|
|
413
|
+
const paths = [];
|
|
414
|
+
const visit = (current, currentPath) => {
|
|
415
|
+
if (current instanceof Output) {
|
|
416
|
+
for (const dep of current.dependencies) if (dep.urn === dependencyUrn) {
|
|
417
|
+
paths.push(currentPath);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
if (Array.isArray(current)) {
|
|
423
|
+
current.forEach((item, index) => {
|
|
424
|
+
visit(item, [...currentPath, index]);
|
|
425
|
+
});
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (current && typeof current === "object") for (const [key, item] of Object.entries(current)) visit(item, [...currentPath, key]);
|
|
429
|
+
};
|
|
430
|
+
visit(value, path);
|
|
431
|
+
return paths;
|
|
432
|
+
};
|
|
433
|
+
const cloneState = (value) => JSON.parse(JSON.stringify(value));
|
|
434
|
+
const removeAtPath = (target, path) => {
|
|
435
|
+
if (path.length === 0) return;
|
|
436
|
+
let parent = target;
|
|
437
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
438
|
+
if (parent == null) return;
|
|
439
|
+
parent = parent[path[i]];
|
|
440
|
+
}
|
|
441
|
+
const last = path[path.length - 1];
|
|
442
|
+
if (Array.isArray(parent) && typeof last === "number") {
|
|
443
|
+
if (last >= 0 && last < parent.length) parent.splice(last, 1);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (parent && typeof parent === "object") delete parent[last];
|
|
447
|
+
};
|
|
448
|
+
const stripDependencyInputs = (input, metaInput, dependencyUrn) => {
|
|
449
|
+
const paths = findDependencyPaths(metaInput, dependencyUrn);
|
|
450
|
+
if (paths.length === 0) return input;
|
|
451
|
+
const detached = cloneState(input);
|
|
452
|
+
const sortedPaths = [...paths].sort((a, b) => {
|
|
453
|
+
if (a.length !== b.length) return b.length - a.length;
|
|
454
|
+
const aLast = a[a.length - 1];
|
|
455
|
+
const bLast = b[b.length - 1];
|
|
456
|
+
if (typeof aLast === "number" && typeof bLast === "number") return bLast - aLast;
|
|
457
|
+
return 0;
|
|
458
|
+
});
|
|
459
|
+
for (const path of sortedPaths) removeAtPath(detached, path);
|
|
460
|
+
return detached;
|
|
461
|
+
};
|
|
462
|
+
const allowsDependentReplace = (replaceOnChanges, dependencyPaths) => {
|
|
463
|
+
if (!replaceOnChanges || replaceOnChanges.length === 0) return false;
|
|
464
|
+
for (const path of dependencyPaths) {
|
|
465
|
+
const base = typeof path[0] === "string" ? path[0] : void 0;
|
|
466
|
+
if (!base) continue;
|
|
467
|
+
for (const replacePath of replaceOnChanges) if (replacePath === base || replacePath.startsWith(`${base}.`) || replacePath.startsWith(`${base}[`) || replacePath.startsWith(`${base}.*`)) return true;
|
|
468
|
+
}
|
|
469
|
+
return false;
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
//#endregion
|
|
473
|
+
//#region src/workspace/error.ts
|
|
474
|
+
var ResourceError = class ResourceError extends Error {
|
|
475
|
+
static wrap(urn, type, operation, error) {
|
|
476
|
+
if (error instanceof Error) return new ResourceError(urn, type, operation, error.message);
|
|
477
|
+
return new ResourceError(urn, type, operation, "Unknown Error");
|
|
478
|
+
}
|
|
479
|
+
constructor(urn, type, operation, message) {
|
|
480
|
+
super(message);
|
|
481
|
+
this.urn = urn;
|
|
482
|
+
this.type = type;
|
|
483
|
+
this.operation = operation;
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
var AppError = class extends Error {
|
|
487
|
+
constructor(app, issues, message) {
|
|
488
|
+
super(message);
|
|
489
|
+
this.app = app;
|
|
490
|
+
this.issues = issues;
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
var ResourceNotFound = class extends Error {};
|
|
494
|
+
var ResourceAlreadyExists = class extends Error {};
|
|
495
|
+
|
|
496
|
+
//#endregion
|
|
497
|
+
//#region src/workspace/state.ts
|
|
498
|
+
const compareState = (left, right) => {
|
|
499
|
+
const replacer = (_, value) => {
|
|
500
|
+
if (value !== null && value instanceof Object && !Array.isArray(value)) return Object.keys(value).sort().reduce((sorted, key) => {
|
|
501
|
+
sorted[key] = value[key];
|
|
502
|
+
return sorted;
|
|
503
|
+
}, {});
|
|
504
|
+
return value;
|
|
505
|
+
};
|
|
506
|
+
return JSON.stringify(left, replacer) === JSON.stringify(right, replacer);
|
|
507
|
+
};
|
|
508
|
+
const removeEmptyStackStates = (appState) => {
|
|
509
|
+
for (const [stackUrn, stackState] of entries(appState.stacks)) if (Object.keys(stackState.nodes).length === 0) delete appState.stacks[stackUrn];
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
//#endregion
|
|
513
|
+
//#region src/workspace/state/v1.ts
|
|
514
|
+
const v1 = (oldAppState) => {
|
|
515
|
+
const stacks = {};
|
|
516
|
+
for (const [urn, stack] of entries(oldAppState.stacks)) {
|
|
517
|
+
const nodes = {};
|
|
518
|
+
for (const [urn$1, resource] of entries(stack.resources)) nodes[urn$1] = {
|
|
519
|
+
...resource,
|
|
520
|
+
tag: "resource"
|
|
521
|
+
};
|
|
522
|
+
stacks[urn] = {
|
|
523
|
+
name: stack.name,
|
|
524
|
+
dependencies: stack.dependencies,
|
|
525
|
+
nodes
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
return {
|
|
529
|
+
...oldAppState,
|
|
530
|
+
stacks,
|
|
531
|
+
version: 1
|
|
532
|
+
};
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
//#endregion
|
|
536
|
+
//#region src/workspace/state/v2.ts
|
|
537
|
+
const v2 = (oldAppState) => {
|
|
538
|
+
const stacks = {};
|
|
539
|
+
for (const [urn, stack] of entries(oldAppState.stacks)) stacks[urn] = {
|
|
540
|
+
name: stack.name,
|
|
541
|
+
nodes: stack.nodes
|
|
542
|
+
};
|
|
543
|
+
return {
|
|
544
|
+
...oldAppState,
|
|
545
|
+
stacks,
|
|
546
|
+
version: 2
|
|
547
|
+
};
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
//#endregion
|
|
551
|
+
//#region src/workspace/state/migrate.ts
|
|
552
|
+
const versions = [[1, v1], [2, v2]];
|
|
553
|
+
const migrateAppState = (oldState) => {
|
|
554
|
+
const version = "version" in oldState && oldState.version || 0;
|
|
555
|
+
for (const [v, migrate] of versions) if (v > version) oldState = migrate(oldState);
|
|
556
|
+
return oldState;
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
//#endregion
|
|
560
|
+
//#region src/provider.ts
|
|
561
|
+
const findProvider = (providers, id) => {
|
|
562
|
+
for (const provider of providers) if (provider.ownResource(id)) return provider;
|
|
563
|
+
throw new TypeError(`Can't find the "${id}" provider.`);
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
//#endregion
|
|
567
|
+
//#region src/workspace/token.ts
|
|
568
|
+
const createIdempotantToken = (appToken, urn, operation) => {
|
|
569
|
+
return v5(`${urn}-${operation}`, appToken);
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
//#endregion
|
|
573
|
+
//#region src/workspace/procedure/delete-resource.ts
|
|
574
|
+
const debug$7 = createDebugger("Delete");
|
|
575
|
+
const deleteResource = async (appToken, urn, state, opt) => {
|
|
576
|
+
debug$7(state.type);
|
|
577
|
+
debug$7(state);
|
|
578
|
+
if (state.lifecycle?.retainOnDelete) {
|
|
579
|
+
debug$7("retain", state.type);
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
const idempotantToken = createIdempotantToken(appToken, urn, "delete");
|
|
583
|
+
const provider = findProvider(opt.providers, state.provider);
|
|
584
|
+
try {
|
|
585
|
+
await opt.hooks?.beforeResourceDelete?.({
|
|
586
|
+
urn,
|
|
587
|
+
type: state.type,
|
|
588
|
+
oldInput: state.input,
|
|
589
|
+
oldOutput: state.output
|
|
590
|
+
});
|
|
591
|
+
await provider.deleteResource({
|
|
592
|
+
type: state.type,
|
|
593
|
+
state: state.output,
|
|
594
|
+
idempotantToken
|
|
595
|
+
});
|
|
596
|
+
await opt.hooks?.afterResourceDelete?.({
|
|
597
|
+
urn,
|
|
598
|
+
type: state.type,
|
|
599
|
+
oldInput: state.input,
|
|
600
|
+
oldOutput: state.output
|
|
601
|
+
});
|
|
602
|
+
} catch (error) {
|
|
603
|
+
if (error instanceof ResourceNotFound) debug$7(state.type, "already deleted");
|
|
604
|
+
else throw ResourceError.wrap(urn, state.type, "delete", error);
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
//#endregion
|
|
609
|
+
//#region src/workspace/procedure/delete-app.ts
|
|
610
|
+
const deleteApp = async (app, opt) => {
|
|
611
|
+
const latestState = await opt.backend.state.get(app.urn);
|
|
612
|
+
if (!latestState) throw new AppError(app.name, [], `App already deleted: ${app.name}`);
|
|
613
|
+
const appState = migrateAppState(latestState);
|
|
614
|
+
if (opt.idempotentToken || !appState.idempotentToken) {
|
|
615
|
+
appState.idempotentToken = opt.idempotentToken ?? crypto.randomUUID();
|
|
616
|
+
await opt.backend.state.update(app.urn, appState);
|
|
617
|
+
}
|
|
618
|
+
let stackStates = Object.values(appState.stacks);
|
|
619
|
+
if (opt.filters && opt.filters.length > 0) stackStates = stackStates.filter((stackState) => opt.filters.includes(stackState.name));
|
|
620
|
+
const queue = createConcurrencyQueue(opt.concurrency ?? 10);
|
|
621
|
+
const graph = new DependencyGraph();
|
|
622
|
+
const allNodes = {};
|
|
623
|
+
for (const stackState of Object.values(appState.stacks)) for (const [urn, nodeState] of entries(stackState.nodes)) allNodes[urn] = nodeState;
|
|
624
|
+
for (const stackState of stackStates) for (const [urn, state] of entries(stackState.nodes)) graph.add(urn, dependentsOn(allNodes, urn), async () => {
|
|
625
|
+
if (state.tag === "resource") await queue(() => deleteResource(appState.idempotentToken, urn, state, opt));
|
|
626
|
+
delete stackState.nodes[urn];
|
|
627
|
+
});
|
|
628
|
+
const errors = await graph.run();
|
|
629
|
+
removeEmptyStackStates(appState);
|
|
630
|
+
delete appState.idempotentToken;
|
|
631
|
+
await opt.backend.state.update(app.urn, appState);
|
|
632
|
+
if (errors.length > 0) throw new AppError(app.name, [...new Set(errors)], "Deleting app failed.");
|
|
633
|
+
if (Object.keys(appState.stacks).length === 0) await opt.backend.state.delete(app.urn);
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
//#endregion
|
|
637
|
+
//#region src/workspace/replacement.ts
|
|
638
|
+
const requiresReplacement = (priorState, proposedState, replaceOnChanges) => {
|
|
639
|
+
for (const path of replaceOnChanges) {
|
|
640
|
+
const priorValue = get(priorState, path);
|
|
641
|
+
const proposedValue = get(proposedState, path);
|
|
642
|
+
if (path.includes("*") && Array.isArray(priorValue)) {
|
|
643
|
+
for (let i = 0; i < priorValue.length; i++) if (!compareState(priorValue[i], proposedValue[i])) return true;
|
|
644
|
+
}
|
|
645
|
+
if (!compareState(priorValue, proposedValue)) return true;
|
|
646
|
+
}
|
|
647
|
+
return false;
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
//#endregion
|
|
651
|
+
//#region src/workspace/procedure/create-resource.ts
|
|
652
|
+
const debug$6 = createDebugger("Create");
|
|
653
|
+
const createResource = async (resource, appToken, input, opt) => {
|
|
654
|
+
const meta = getMeta(resource);
|
|
655
|
+
const provider = findProvider(opt.providers, meta.provider);
|
|
656
|
+
const idempotantToken = createIdempotantToken(appToken, meta.urn, "create");
|
|
657
|
+
debug$6(meta.type);
|
|
658
|
+
debug$6(input);
|
|
659
|
+
let result;
|
|
660
|
+
try {
|
|
661
|
+
await opt.hooks?.beforeResourceCreate?.({
|
|
662
|
+
urn: resource.urn,
|
|
663
|
+
type: meta.type,
|
|
664
|
+
resource,
|
|
665
|
+
newInput: input
|
|
666
|
+
});
|
|
667
|
+
result = await provider.createResource({
|
|
668
|
+
type: meta.type,
|
|
669
|
+
state: input,
|
|
670
|
+
idempotantToken
|
|
671
|
+
});
|
|
672
|
+
await opt.hooks?.afterResourceCreate?.({
|
|
673
|
+
urn: resource.urn,
|
|
674
|
+
type: meta.type,
|
|
675
|
+
resource,
|
|
676
|
+
newInput: input,
|
|
677
|
+
newOutput: result.state
|
|
678
|
+
});
|
|
679
|
+
} catch (error) {
|
|
680
|
+
throw ResourceError.wrap(meta.urn, meta.type, "create", error);
|
|
681
|
+
}
|
|
682
|
+
return {
|
|
683
|
+
tag: "resource",
|
|
684
|
+
version: result.version,
|
|
685
|
+
type: meta.type,
|
|
686
|
+
provider: meta.provider,
|
|
687
|
+
input: meta.input,
|
|
688
|
+
output: result.state
|
|
689
|
+
};
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
//#endregion
|
|
693
|
+
//#region src/workspace/procedure/get-data-source.ts
|
|
694
|
+
const debug$5 = createDebugger("Data Source");
|
|
695
|
+
const getDataSource = async (dataSource, input, opt) => {
|
|
696
|
+
const provider = findProvider(opt.providers, dataSource.provider);
|
|
697
|
+
debug$5(dataSource.type);
|
|
698
|
+
if (!provider.getData) throw new Error(`Provider doesn't support data sources`);
|
|
699
|
+
let result;
|
|
700
|
+
try {
|
|
701
|
+
result = await provider.getData({
|
|
702
|
+
type: dataSource.type,
|
|
703
|
+
state: input
|
|
704
|
+
});
|
|
705
|
+
} catch (error) {
|
|
706
|
+
throw ResourceError.wrap(dataSource.urn, dataSource.type, "get", error);
|
|
707
|
+
}
|
|
708
|
+
return {
|
|
709
|
+
tag: "data",
|
|
710
|
+
type: dataSource.type,
|
|
711
|
+
provider: dataSource.provider,
|
|
712
|
+
input,
|
|
713
|
+
output: result.state
|
|
714
|
+
};
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
//#endregion
|
|
718
|
+
//#region src/workspace/procedure/import-resource.ts
|
|
719
|
+
const debug$4 = createDebugger("Import");
|
|
720
|
+
const importResource = async (resource, input, opt) => {
|
|
721
|
+
const meta = getMeta(resource);
|
|
722
|
+
const provider = findProvider(opt.providers, meta.provider);
|
|
723
|
+
debug$4(meta.type);
|
|
724
|
+
debug$4(input);
|
|
725
|
+
let result;
|
|
726
|
+
try {
|
|
727
|
+
result = await provider.getResource({
|
|
728
|
+
type: meta.type,
|
|
729
|
+
state: {
|
|
730
|
+
...input,
|
|
731
|
+
id: meta.config?.import
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
} catch (error) {
|
|
735
|
+
throw ResourceError.wrap(meta.urn, meta.type, "import", error);
|
|
736
|
+
}
|
|
737
|
+
return {
|
|
738
|
+
tag: "resource",
|
|
739
|
+
version: result.version,
|
|
740
|
+
type: meta.type,
|
|
741
|
+
provider: meta.provider,
|
|
742
|
+
input: meta.input,
|
|
743
|
+
output: result.state
|
|
744
|
+
};
|
|
745
|
+
};
|
|
746
|
+
|
|
747
|
+
//#endregion
|
|
748
|
+
//#region src/workspace/procedure/replace-resource.ts
|
|
749
|
+
const debug$3 = createDebugger("Replace");
|
|
750
|
+
const replaceResource = async (resource, appToken, priorInputState, priorOutputState, proposedState, opt) => {
|
|
751
|
+
const meta = getMeta(resource);
|
|
752
|
+
const urn = meta.urn;
|
|
753
|
+
const type = meta.type;
|
|
754
|
+
const provider = findProvider(opt.providers, meta.provider);
|
|
755
|
+
const idempotantToken = createIdempotantToken(appToken, meta.urn, "replace");
|
|
756
|
+
debug$3(meta.type);
|
|
757
|
+
debug$3(proposedState);
|
|
758
|
+
if (meta.config?.retainOnDelete) debug$3("retain", type);
|
|
759
|
+
else try {
|
|
760
|
+
await opt.hooks?.beforeResourceDelete?.({
|
|
761
|
+
urn,
|
|
762
|
+
type,
|
|
763
|
+
oldInput: priorInputState,
|
|
764
|
+
oldOutput: priorOutputState
|
|
765
|
+
});
|
|
766
|
+
await provider.deleteResource({
|
|
767
|
+
type,
|
|
768
|
+
state: priorOutputState,
|
|
769
|
+
idempotantToken
|
|
770
|
+
});
|
|
771
|
+
await opt.hooks?.afterResourceDelete?.({
|
|
772
|
+
urn,
|
|
773
|
+
type,
|
|
774
|
+
oldInput: priorInputState,
|
|
775
|
+
oldOutput: priorOutputState
|
|
776
|
+
});
|
|
777
|
+
} catch (error) {
|
|
778
|
+
if (error instanceof ResourceNotFound) debug$3(type, "already deleted");
|
|
779
|
+
else throw ResourceError.wrap(urn, type, "replace", error);
|
|
780
|
+
}
|
|
781
|
+
let result;
|
|
782
|
+
try {
|
|
783
|
+
await opt.hooks?.beforeResourceCreate?.({
|
|
784
|
+
urn,
|
|
785
|
+
type,
|
|
786
|
+
resource,
|
|
787
|
+
newInput: proposedState
|
|
788
|
+
});
|
|
789
|
+
result = await provider.createResource({
|
|
790
|
+
type,
|
|
791
|
+
state: proposedState,
|
|
792
|
+
idempotantToken
|
|
793
|
+
});
|
|
794
|
+
await opt.hooks?.afterResourceCreate?.({
|
|
795
|
+
urn,
|
|
796
|
+
type,
|
|
797
|
+
resource,
|
|
798
|
+
newInput: proposedState,
|
|
799
|
+
newOutput: result.state
|
|
800
|
+
});
|
|
801
|
+
} catch (error) {
|
|
802
|
+
throw ResourceError.wrap(urn, type, "replace", error);
|
|
803
|
+
}
|
|
804
|
+
return {
|
|
805
|
+
version: result.version,
|
|
806
|
+
output: result.state
|
|
807
|
+
};
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
//#endregion
|
|
811
|
+
//#region src/workspace/procedure/update-resource.ts
|
|
812
|
+
const debug$2 = createDebugger("Update");
|
|
813
|
+
const updateResource = async (resource, appToken, priorInputState, priorOutputState, proposedState, opt) => {
|
|
814
|
+
const meta = getMeta(resource);
|
|
815
|
+
const provider = findProvider(opt.providers, meta.provider);
|
|
816
|
+
const idempotantToken = createIdempotantToken(appToken, meta.urn, "update");
|
|
817
|
+
let result;
|
|
818
|
+
debug$2(meta.type);
|
|
819
|
+
debug$2(proposedState);
|
|
820
|
+
try {
|
|
821
|
+
await opt.hooks?.beforeResourceUpdate?.({
|
|
822
|
+
urn: resource.urn,
|
|
823
|
+
type: meta.type,
|
|
824
|
+
resource,
|
|
825
|
+
newInput: proposedState,
|
|
826
|
+
oldInput: priorInputState,
|
|
827
|
+
oldOutput: priorOutputState
|
|
828
|
+
});
|
|
829
|
+
result = await provider.updateResource({
|
|
830
|
+
type: meta.type,
|
|
831
|
+
priorState: priorOutputState,
|
|
832
|
+
proposedState,
|
|
833
|
+
idempotantToken
|
|
834
|
+
});
|
|
835
|
+
await opt.hooks?.afterResourceUpdate?.({
|
|
836
|
+
urn: resource.urn,
|
|
837
|
+
type: meta.type,
|
|
838
|
+
resource,
|
|
839
|
+
newInput: proposedState,
|
|
840
|
+
oldInput: priorInputState,
|
|
841
|
+
newOutput: result.state,
|
|
842
|
+
oldOutput: priorOutputState
|
|
843
|
+
});
|
|
844
|
+
} catch (error) {
|
|
845
|
+
throw ResourceError.wrap(meta.urn, meta.type, "update", error);
|
|
846
|
+
}
|
|
847
|
+
return {
|
|
848
|
+
version: result.version,
|
|
849
|
+
output: result.state
|
|
850
|
+
};
|
|
851
|
+
};
|
|
852
|
+
|
|
853
|
+
//#endregion
|
|
854
|
+
//#region src/workspace/procedure/deploy-app.ts
|
|
855
|
+
const debug$1 = createDebugger("Deploy App");
|
|
856
|
+
const deployApp = async (app, opt) => {
|
|
857
|
+
debug$1(app.name, "start");
|
|
858
|
+
const appState = migrateAppState(await opt.backend.state.get(app.urn) ?? {
|
|
859
|
+
name: app.name,
|
|
860
|
+
stacks: {}
|
|
861
|
+
});
|
|
862
|
+
const releaseOnExit = onExit(async () => {
|
|
863
|
+
await opt.backend.state.update(app.urn, appState);
|
|
864
|
+
});
|
|
865
|
+
if (opt.idempotentToken || !appState.idempotentToken) {
|
|
866
|
+
appState.idempotentToken = opt.idempotentToken ?? crypto.randomUUID();
|
|
867
|
+
await opt.backend.state.update(app.urn, appState);
|
|
868
|
+
}
|
|
869
|
+
let stacks = app.stacks;
|
|
870
|
+
let filteredOutStacks = [];
|
|
871
|
+
if (opt.filters && opt.filters.length > 0) {
|
|
872
|
+
stacks = app.stacks.filter((stack) => opt.filters.includes(stack.name));
|
|
873
|
+
filteredOutStacks = app.stacks.filter((stack) => !opt.filters.includes(stack.name));
|
|
874
|
+
}
|
|
875
|
+
const nodeByUrn = /* @__PURE__ */ new Map();
|
|
876
|
+
const stackStates = /* @__PURE__ */ new Map();
|
|
877
|
+
const plannedDependents = /* @__PURE__ */ new Set();
|
|
878
|
+
const forcedUpdateDependents = /* @__PURE__ */ new Set();
|
|
879
|
+
for (const stack of stacks) {
|
|
880
|
+
const stackState = appState.stacks[stack.urn] = appState.stacks[stack.urn] ?? {
|
|
881
|
+
name: stack.name,
|
|
882
|
+
nodes: {}
|
|
883
|
+
};
|
|
884
|
+
stackStates.set(stack.urn, stackState);
|
|
885
|
+
for (const node of stack.nodes) nodeByUrn.set(getMeta(node).urn, node);
|
|
886
|
+
}
|
|
887
|
+
const queue = createConcurrencyQueue(opt.concurrency ?? 10);
|
|
888
|
+
const graph = new DependencyGraph();
|
|
889
|
+
const replacementDeletes = /* @__PURE__ */ new Map();
|
|
890
|
+
const allNodes = {};
|
|
891
|
+
for (const stackState of Object.values(appState.stacks)) for (const [urn, nodeState] of entries(stackState.nodes)) allNodes[urn] = nodeState;
|
|
892
|
+
for (const stack of filteredOutStacks) {
|
|
893
|
+
const stackState = appState.stacks[stack.urn];
|
|
894
|
+
if (stackState) for (const node of stack.nodes) {
|
|
895
|
+
const meta = getMeta(node);
|
|
896
|
+
const nodeState = stackState.nodes[meta.urn];
|
|
897
|
+
if (nodeState && nodeState.output) graph.add(meta.urn, [], async () => {
|
|
898
|
+
debug$1("hydrate", meta.urn);
|
|
899
|
+
meta.resolve(nodeState.output);
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
for (const [urn, stackState] of entries(appState.stacks)) {
|
|
904
|
+
const found = app.stacks.find((stack) => {
|
|
905
|
+
return stack.urn === urn;
|
|
906
|
+
});
|
|
907
|
+
const filtered = opt.filters ? opt.filters.find((filter) => filter === stackState.name) : true;
|
|
908
|
+
if (!found && filtered) for (const [urn$1, nodeState] of entries(stackState.nodes)) graph.add(urn$1, dependentsOn(allNodes, urn$1), async () => {
|
|
909
|
+
if (nodeState.tag === "resource") await queue(() => deleteResource(appState.idempotentToken, urn$1, nodeState, opt));
|
|
910
|
+
delete stackState.nodes[urn$1];
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
for (const stack of stacks) {
|
|
914
|
+
const stackState = stackStates.get(stack.urn);
|
|
915
|
+
for (const [urn, nodeState] of entries(stackState.nodes)) if (!stack.nodes.find((r) => getMeta(r).urn === urn)) graph.add(urn, dependentsOn(allNodes, urn), async () => {
|
|
916
|
+
if (nodeState.tag === "resource") await queue(() => deleteResource(appState.idempotentToken, urn, nodeState, opt));
|
|
917
|
+
delete stackState.nodes[urn];
|
|
918
|
+
});
|
|
919
|
+
for (const node of stack.nodes) {
|
|
920
|
+
const meta = getMeta(node);
|
|
921
|
+
const dependencies = [...meta.dependencies];
|
|
922
|
+
const partialNewResourceState = {
|
|
923
|
+
dependencies,
|
|
924
|
+
lifecycle: isResource(node) ? { retainOnDelete: getMeta(node).config?.retainOnDelete } : void 0
|
|
925
|
+
};
|
|
926
|
+
graph.add(meta.urn, dependencies, () => {
|
|
927
|
+
return queue(async () => {
|
|
928
|
+
let nodeState = stackState.nodes[meta.urn];
|
|
929
|
+
let input;
|
|
930
|
+
try {
|
|
931
|
+
input = await resolveInputs(meta.input);
|
|
932
|
+
} catch (error) {
|
|
933
|
+
throw ResourceError.wrap(meta.urn, meta.type, "resolve", error);
|
|
934
|
+
}
|
|
935
|
+
if (isDataSource(node)) {
|
|
936
|
+
const meta$1 = getMeta(node);
|
|
937
|
+
if (!nodeState) {
|
|
938
|
+
const dataSourceState = await getDataSource(meta$1, input, opt);
|
|
939
|
+
nodeState = stackState.nodes[meta$1.urn] = {
|
|
940
|
+
...dataSourceState,
|
|
941
|
+
...partialNewResourceState
|
|
942
|
+
};
|
|
943
|
+
} else if (!compareState(nodeState.input, input)) {
|
|
944
|
+
const dataSourceState = await getDataSource(meta$1, input, opt);
|
|
945
|
+
Object.assign(nodeState, {
|
|
946
|
+
...dataSourceState,
|
|
947
|
+
...partialNewResourceState
|
|
948
|
+
});
|
|
949
|
+
} else Object.assign(nodeState, partialNewResourceState);
|
|
950
|
+
}
|
|
951
|
+
if (isResource(node)) {
|
|
952
|
+
const meta$1 = getMeta(node);
|
|
953
|
+
if (!nodeState) if (meta$1.config?.import) {
|
|
954
|
+
const importedState = await importResource(node, input, opt);
|
|
955
|
+
const newResourceState = await updateResource(node, appState.idempotentToken, importedState.input, importedState.output, input, opt);
|
|
956
|
+
nodeState = stackState.nodes[meta$1.urn] = {
|
|
957
|
+
...importedState,
|
|
958
|
+
...newResourceState,
|
|
959
|
+
...partialNewResourceState
|
|
960
|
+
};
|
|
961
|
+
} else {
|
|
962
|
+
const newResourceState = await createResource(node, appState.idempotentToken, input, opt);
|
|
963
|
+
nodeState = stackState.nodes[meta$1.urn] = {
|
|
964
|
+
...newResourceState,
|
|
965
|
+
...partialNewResourceState
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
else if (!compareState(nodeState.input, input)) {
|
|
969
|
+
let newResourceState;
|
|
970
|
+
const ignoreReplace = forcedUpdateDependents.has(meta$1.urn);
|
|
971
|
+
if (!ignoreReplace && requiresReplacement(nodeState.input, input, meta$1.config?.replaceOnChanges ?? [])) if (meta$1.config?.createBeforeReplace) {
|
|
972
|
+
const priorState = { ...nodeState };
|
|
973
|
+
newResourceState = await createResource(node, appState.idempotentToken, input, opt);
|
|
974
|
+
if (!meta$1.config?.retainOnDelete) replacementDeletes.set(meta$1.urn, priorState);
|
|
975
|
+
} else {
|
|
976
|
+
for (const [dependentUrn, dependentNode] of nodeByUrn.entries()) {
|
|
977
|
+
if (!isResource(dependentNode)) continue;
|
|
978
|
+
const dependentMeta = getMeta(dependentNode);
|
|
979
|
+
if (!dependentMeta.dependencies.has(meta$1.urn)) continue;
|
|
980
|
+
if (plannedDependents.has(dependentUrn)) continue;
|
|
981
|
+
const dependentStackState = stackStates.get(dependentMeta.stack.urn);
|
|
982
|
+
const dependentState = dependentStackState?.nodes[dependentUrn];
|
|
983
|
+
if (!dependentStackState || !dependentState) continue;
|
|
984
|
+
const dependencyPaths = findDependencyPaths(dependentMeta.input, meta$1.urn);
|
|
985
|
+
if (dependencyPaths.length === 0) continue;
|
|
986
|
+
const detachedInput = stripDependencyInputs(dependentState.input, dependentMeta.input, meta$1.urn);
|
|
987
|
+
if (compareState(dependentState.input, detachedInput)) continue;
|
|
988
|
+
plannedDependents.add(dependentUrn);
|
|
989
|
+
let dependentRequiresReplacement = false;
|
|
990
|
+
const dependentProvider = findProvider(opt.providers, dependentMeta.provider);
|
|
991
|
+
if (dependentProvider.planResourceChange) try {
|
|
992
|
+
dependentRequiresReplacement = (await dependentProvider.planResourceChange({
|
|
993
|
+
type: dependentMeta.type,
|
|
994
|
+
priorState: dependentState.output,
|
|
995
|
+
proposedState: detachedInput
|
|
996
|
+
})).requiresReplacement;
|
|
997
|
+
} catch (error) {
|
|
998
|
+
throw ResourceError.wrap(dependentMeta.urn, dependentMeta.type, "update", error);
|
|
999
|
+
}
|
|
1000
|
+
if (dependentRequiresReplacement) {
|
|
1001
|
+
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.`));
|
|
1002
|
+
await deleteResource(appState.idempotentToken, dependentUrn, dependentState, opt);
|
|
1003
|
+
delete dependentStackState.nodes[dependentUrn];
|
|
1004
|
+
} else {
|
|
1005
|
+
const updated = await updateResource(dependentNode, appState.idempotentToken, dependentState.input, dependentState.output, detachedInput, opt);
|
|
1006
|
+
Object.assign(dependentState, {
|
|
1007
|
+
input: detachedInput,
|
|
1008
|
+
...updated
|
|
1009
|
+
});
|
|
1010
|
+
forcedUpdateDependents.add(dependentUrn);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
newResourceState = await replaceResource(node, appState.idempotentToken, nodeState.input, nodeState.output, input, opt);
|
|
1014
|
+
}
|
|
1015
|
+
else {
|
|
1016
|
+
newResourceState = await updateResource(node, appState.idempotentToken, nodeState.input, nodeState.output, input, opt);
|
|
1017
|
+
if (ignoreReplace) forcedUpdateDependents.delete(meta$1.urn);
|
|
1018
|
+
}
|
|
1019
|
+
Object.assign(nodeState, {
|
|
1020
|
+
input,
|
|
1021
|
+
...newResourceState,
|
|
1022
|
+
...partialNewResourceState
|
|
1023
|
+
});
|
|
1024
|
+
} else Object.assign(nodeState, partialNewResourceState);
|
|
1025
|
+
}
|
|
1026
|
+
if (nodeState?.output) meta.resolve(nodeState.output);
|
|
1027
|
+
});
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
const errors = await graph.run();
|
|
1032
|
+
if (errors.length === 0 && replacementDeletes.size > 0) for (const [urn, nodeState] of replacementDeletes.entries()) try {
|
|
1033
|
+
await deleteResource(appState.idempotentToken, urn, nodeState, opt);
|
|
1034
|
+
} catch (error) {
|
|
1035
|
+
if (error instanceof Error) errors.push(error);
|
|
1036
|
+
else errors.push(/* @__PURE__ */ new Error(`${error}`));
|
|
1037
|
+
}
|
|
1038
|
+
removeEmptyStackStates(appState);
|
|
1039
|
+
delete appState.idempotentToken;
|
|
1040
|
+
await opt.backend.state.update(app.urn, appState);
|
|
1041
|
+
releaseOnExit();
|
|
1042
|
+
debug$1(app.name, "done");
|
|
1043
|
+
if (errors.length > 0) throw new AppError(app.name, [...new Set(errors)], "Deploying app failed.");
|
|
1044
|
+
if (Object.keys(appState.stacks).length === 0) await opt.backend.state.delete(app.urn);
|
|
1045
|
+
return appState;
|
|
1046
|
+
};
|
|
1047
|
+
|
|
1048
|
+
//#endregion
|
|
1049
|
+
//#region src/workspace/procedure/hydrate.ts
|
|
1050
|
+
const hydrate = async (app, opt) => {
|
|
1051
|
+
const appState = await opt.backend.state.get(app.urn);
|
|
1052
|
+
if (appState) for (const stack of app.stacks) {
|
|
1053
|
+
const stackState = appState.stacks[stack.urn];
|
|
1054
|
+
if (stackState) for (const node of stack.nodes) {
|
|
1055
|
+
const meta = getMeta(node);
|
|
1056
|
+
const nodeState = stackState.nodes[meta.urn];
|
|
1057
|
+
if (nodeState && nodeState.output) meta.resolve(nodeState.output);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
};
|
|
1061
|
+
|
|
1062
|
+
//#endregion
|
|
1063
|
+
//#region src/workspace/procedure/refresh.ts
|
|
1064
|
+
const refresh = async (app, opt) => {
|
|
1065
|
+
const appState = await opt.backend.state.get(app.urn);
|
|
1066
|
+
const queue = createConcurrencyQueue(opt.concurrency ?? 10);
|
|
1067
|
+
if (appState) {
|
|
1068
|
+
await Promise.all(Object.values(appState.stacks).map((stackState) => {
|
|
1069
|
+
return Promise.all(Object.values(stackState.nodes).map((nodeState) => {
|
|
1070
|
+
return queue(async () => {
|
|
1071
|
+
const provider = findProvider(opt.providers, nodeState.provider);
|
|
1072
|
+
if (nodeState.tag === "data") {
|
|
1073
|
+
const result = await provider.getData?.({
|
|
1074
|
+
type: nodeState.type,
|
|
1075
|
+
state: nodeState.output
|
|
1076
|
+
});
|
|
1077
|
+
if (result && !compareState(result.state, nodeState.output)) {
|
|
1078
|
+
nodeState.output = result.state;
|
|
1079
|
+
nodeState.input = result.state;
|
|
1080
|
+
}
|
|
1081
|
+
} else if (nodeState.tag === "resource") {
|
|
1082
|
+
const result = await provider.getResource({
|
|
1083
|
+
type: nodeState.type,
|
|
1084
|
+
state: nodeState.output
|
|
1085
|
+
});
|
|
1086
|
+
if (result && !compareState(result.state, nodeState.output)) {
|
|
1087
|
+
nodeState.output = result.state;
|
|
1088
|
+
nodeState.input = result.state;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
});
|
|
1092
|
+
}));
|
|
1093
|
+
}));
|
|
1094
|
+
await opt.backend.state.update(app.urn, appState);
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
|
|
1098
|
+
//#endregion
|
|
1099
|
+
//#region src/workspace/workspace.ts
|
|
1100
|
+
var WorkSpace = class {
|
|
1101
|
+
constructor(props) {
|
|
1102
|
+
this.props = props;
|
|
1103
|
+
}
|
|
1104
|
+
/**
|
|
1105
|
+
* Deploy the entire app or use the filter option to deploy specific stacks inside your app.
|
|
1106
|
+
*/
|
|
1107
|
+
deploy(app, options = {}) {
|
|
1108
|
+
return lockApp(this.props.backend.lock, app, async () => {
|
|
1109
|
+
try {
|
|
1110
|
+
await deployApp(app, {
|
|
1111
|
+
...this.props,
|
|
1112
|
+
...options
|
|
1113
|
+
});
|
|
1114
|
+
} finally {
|
|
1115
|
+
await this.destroyProviders();
|
|
1116
|
+
}
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Delete the entire app or use the filter option to delete specific stacks inside your app.
|
|
1121
|
+
*/
|
|
1122
|
+
delete(app, options = {}) {
|
|
1123
|
+
return lockApp(this.props.backend.lock, app, async () => {
|
|
1124
|
+
try {
|
|
1125
|
+
await deleteApp(app, {
|
|
1126
|
+
...this.props,
|
|
1127
|
+
...options
|
|
1128
|
+
});
|
|
1129
|
+
} finally {
|
|
1130
|
+
await this.destroyProviders();
|
|
1131
|
+
}
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* Hydrate the outputs of the resources & data-sources inside your app.
|
|
1136
|
+
*/
|
|
1137
|
+
hydrate(app) {
|
|
1138
|
+
return hydrate(app, this.props);
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Refresh the state of the resources & data-sources inside your app.
|
|
1142
|
+
*/
|
|
1143
|
+
refresh(app) {
|
|
1144
|
+
return lockApp(this.props.backend.lock, app, async () => {
|
|
1145
|
+
try {
|
|
1146
|
+
await refresh(app, this.props);
|
|
1147
|
+
} finally {
|
|
1148
|
+
await this.destroyProviders();
|
|
1149
|
+
}
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
async destroyProviders() {
|
|
1153
|
+
await Promise.all(this.props.providers.map((p) => {
|
|
1154
|
+
return p.destroy?.();
|
|
1155
|
+
}));
|
|
1156
|
+
}
|
|
1157
|
+
};
|
|
1158
|
+
|
|
1159
|
+
//#endregion
|
|
1160
|
+
//#region src/backend/memory/state.ts
|
|
1161
|
+
var MemoryStateBackend = class {
|
|
1162
|
+
states = /* @__PURE__ */ new Map();
|
|
1163
|
+
async get(urn) {
|
|
1164
|
+
return this.states.get(urn);
|
|
1165
|
+
}
|
|
1166
|
+
async update(urn, state) {
|
|
1167
|
+
this.states.set(urn, state);
|
|
1168
|
+
}
|
|
1169
|
+
async delete(urn) {
|
|
1170
|
+
this.states.delete(urn);
|
|
1171
|
+
}
|
|
1172
|
+
clear() {
|
|
1173
|
+
this.states.clear();
|
|
1174
|
+
}
|
|
1175
|
+
};
|
|
1176
|
+
|
|
1177
|
+
//#endregion
|
|
1178
|
+
//#region src/backend/memory/lock.ts
|
|
1179
|
+
var MemoryLockBackend = class {
|
|
1180
|
+
locks = /* @__PURE__ */ new Map();
|
|
1181
|
+
async insecureReleaseLock(urn) {
|
|
1182
|
+
this.locks.delete(urn);
|
|
1183
|
+
}
|
|
1184
|
+
async locked(urn) {
|
|
1185
|
+
return this.locks.has(urn);
|
|
1186
|
+
}
|
|
1187
|
+
async lock(urn) {
|
|
1188
|
+
if (this.locks.has(urn)) throw new Error("Already locked");
|
|
1189
|
+
const id = Math.random();
|
|
1190
|
+
this.locks.set(urn, id);
|
|
1191
|
+
return async () => {
|
|
1192
|
+
if (this.locks.get(urn) === id) this.locks.delete(urn);
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
clear() {
|
|
1196
|
+
this.locks.clear();
|
|
1197
|
+
}
|
|
1198
|
+
};
|
|
1199
|
+
|
|
1200
|
+
//#endregion
|
|
1201
|
+
//#region src/backend/file/state.ts
|
|
1202
|
+
const debug = createDebugger("State");
|
|
1203
|
+
var FileStateBackend = class {
|
|
1204
|
+
constructor(props) {
|
|
1205
|
+
this.props = props;
|
|
1206
|
+
}
|
|
1207
|
+
stateFile(urn) {
|
|
1208
|
+
return join(this.props.dir, `${urn}.json`);
|
|
1209
|
+
}
|
|
1210
|
+
async mkdir() {
|
|
1211
|
+
await mkdir(this.props.dir, { recursive: true });
|
|
1212
|
+
}
|
|
1213
|
+
async get(urn) {
|
|
1214
|
+
debug("get");
|
|
1215
|
+
let json;
|
|
1216
|
+
try {
|
|
1217
|
+
json = await readFile(join(this.stateFile(urn)), "utf8");
|
|
1218
|
+
} catch (error) {
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
return JSON.parse(json);
|
|
1222
|
+
}
|
|
1223
|
+
async update(urn, state) {
|
|
1224
|
+
debug("update");
|
|
1225
|
+
await this.mkdir();
|
|
1226
|
+
await writeFile(this.stateFile(urn), JSON.stringify(state, void 0, 2));
|
|
1227
|
+
}
|
|
1228
|
+
async delete(urn) {
|
|
1229
|
+
debug("delete");
|
|
1230
|
+
await this.mkdir();
|
|
1231
|
+
await rm(this.stateFile(urn));
|
|
1232
|
+
}
|
|
1233
|
+
};
|
|
1234
|
+
|
|
1235
|
+
//#endregion
|
|
1236
|
+
//#region src/backend/file/lock.ts
|
|
1237
|
+
var FileLockBackend = class {
|
|
1238
|
+
constructor(props) {
|
|
1239
|
+
this.props = props;
|
|
1240
|
+
}
|
|
1241
|
+
lockFile(urn) {
|
|
1242
|
+
return join(this.props.dir, `${urn}.lock`);
|
|
1243
|
+
}
|
|
1244
|
+
async mkdir() {
|
|
1245
|
+
await mkdir(this.props.dir, { recursive: true });
|
|
1246
|
+
}
|
|
1247
|
+
async insecureReleaseLock(urn) {
|
|
1248
|
+
if (await this.locked(urn)) await rm(this.lockFile(urn));
|
|
1249
|
+
}
|
|
1250
|
+
async locked(urn) {
|
|
1251
|
+
return (await stat(this.lockFile(urn))).isFile();
|
|
1252
|
+
}
|
|
1253
|
+
async lock(urn) {
|
|
1254
|
+
await this.mkdir();
|
|
1255
|
+
return lock(this.lockFile(urn), { realpath: false });
|
|
1256
|
+
}
|
|
1257
|
+
};
|
|
1258
|
+
|
|
1259
|
+
//#endregion
|
|
1260
|
+
//#region src/backend/aws/s3-state.ts
|
|
1261
|
+
var S3StateBackend = class {
|
|
1262
|
+
client;
|
|
1263
|
+
constructor(props) {
|
|
1264
|
+
this.props = props;
|
|
1265
|
+
this.client = new S3Client(props);
|
|
1266
|
+
}
|
|
1267
|
+
async get(urn) {
|
|
1268
|
+
let result;
|
|
1269
|
+
try {
|
|
1270
|
+
result = await this.client.send(new GetObjectCommand({
|
|
1271
|
+
Bucket: this.props.bucket,
|
|
1272
|
+
Key: `${urn}.state`
|
|
1273
|
+
}));
|
|
1274
|
+
} catch (error) {
|
|
1275
|
+
if (error instanceof S3ServiceException && error.name === "NoSuchKey") return;
|
|
1276
|
+
throw error;
|
|
1277
|
+
}
|
|
1278
|
+
if (!result.Body) return;
|
|
1279
|
+
const body = await result.Body.transformToString("utf8");
|
|
1280
|
+
return JSON.parse(body);
|
|
1281
|
+
}
|
|
1282
|
+
async update(urn, state) {
|
|
1283
|
+
await this.client.send(new PutObjectCommand({
|
|
1284
|
+
Bucket: this.props.bucket,
|
|
1285
|
+
Key: `${urn}.state`,
|
|
1286
|
+
Body: JSON.stringify(state)
|
|
1287
|
+
}));
|
|
1288
|
+
}
|
|
1289
|
+
async delete(urn) {
|
|
1290
|
+
await this.client.send(new DeleteObjectCommand({
|
|
1291
|
+
Bucket: this.props.bucket,
|
|
1292
|
+
Key: `${urn}.state`
|
|
1293
|
+
}));
|
|
1294
|
+
}
|
|
1295
|
+
};
|
|
1296
|
+
|
|
1297
|
+
//#endregion
|
|
1298
|
+
//#region src/backend/aws/dynamodb-lock.ts
|
|
1299
|
+
var DynamoLockBackend = class {
|
|
1300
|
+
client;
|
|
1301
|
+
constructor(props) {
|
|
1302
|
+
this.props = props;
|
|
1303
|
+
this.client = new DynamoDB(props);
|
|
1304
|
+
}
|
|
1305
|
+
async insecureReleaseLock(urn) {
|
|
1306
|
+
await this.client.updateItem({
|
|
1307
|
+
TableName: this.props.tableName,
|
|
1308
|
+
Key: marshall({ urn }),
|
|
1309
|
+
ExpressionAttributeNames: { "#lock": "lock" },
|
|
1310
|
+
UpdateExpression: "REMOVE #lock"
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
async locked(urn) {
|
|
1314
|
+
const result = await this.client.getItem({
|
|
1315
|
+
TableName: this.props.tableName,
|
|
1316
|
+
Key: marshall({ urn })
|
|
1317
|
+
});
|
|
1318
|
+
if (!result.Item) return false;
|
|
1319
|
+
return typeof unmarshall(result.Item).lock === "number";
|
|
1320
|
+
}
|
|
1321
|
+
async lock(urn) {
|
|
1322
|
+
const id = Math.floor(Math.random() * 1e5);
|
|
1323
|
+
const props = {
|
|
1324
|
+
TableName: this.props.tableName,
|
|
1325
|
+
Key: marshall({ urn }),
|
|
1326
|
+
ExpressionAttributeNames: { "#lock": "lock" },
|
|
1327
|
+
ExpressionAttributeValues: { ":id": marshall(id) }
|
|
1328
|
+
};
|
|
1329
|
+
await this.client.updateItem({
|
|
1330
|
+
...props,
|
|
1331
|
+
UpdateExpression: "SET #lock = :id",
|
|
1332
|
+
ConditionExpression: "attribute_not_exists(#lock)"
|
|
1333
|
+
});
|
|
1334
|
+
return async () => {
|
|
1335
|
+
await this.client.updateItem({
|
|
1336
|
+
...props,
|
|
1337
|
+
UpdateExpression: "REMOVE #lock",
|
|
1338
|
+
ConditionExpression: "#lock = :id"
|
|
1339
|
+
});
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
};
|
|
1343
|
+
|
|
1344
|
+
//#endregion
|
|
1345
|
+
//#region src/helpers.ts
|
|
1346
|
+
const file = (path, encoding = "utf8") => {
|
|
1347
|
+
return new Future(async (resolve$1, reject) => {
|
|
1348
|
+
try {
|
|
1349
|
+
resolve$1(await readFile(path, { encoding }));
|
|
1350
|
+
} catch (error) {
|
|
1351
|
+
reject(error);
|
|
1352
|
+
}
|
|
1353
|
+
});
|
|
1354
|
+
};
|
|
1355
|
+
const hash = (path, algo = "sha256") => {
|
|
1356
|
+
return file(path).pipe((file$1) => createHash(algo).update(file$1).digest("hex"));
|
|
1357
|
+
};
|
|
1358
|
+
|
|
1359
|
+
//#endregion
|
|
1360
|
+
//#region src/globals.ts
|
|
1361
|
+
globalThis.$resolve = resolve;
|
|
1362
|
+
globalThis.$combine = combine;
|
|
1363
|
+
globalThis.$interpolate = interpolate;
|
|
1364
|
+
globalThis.$hash = hash;
|
|
1365
|
+
globalThis.$file = file;
|
|
1366
|
+
|
|
1367
|
+
//#endregion
|
|
1368
|
+
//#region src/custom/resource.ts
|
|
1369
|
+
const createCustomResourceClass = (providerId, resourceType) => {
|
|
1370
|
+
return new Proxy(class {}, { construct(_, [parent, id, input, config]) {
|
|
1371
|
+
const meta = createMeta("resource", `custom:${providerId}`, parent, resourceType, id, input, config);
|
|
1372
|
+
const node = new Proxy({}, { get(_$1, key) {
|
|
1373
|
+
if (key === nodeMetaSymbol) return meta;
|
|
1374
|
+
if (key === "urn") return meta.urn;
|
|
1375
|
+
if (typeof key === "symbol") return;
|
|
1376
|
+
return meta.output((data) => data[key]);
|
|
1377
|
+
} });
|
|
1378
|
+
parent.add(node);
|
|
1379
|
+
return node;
|
|
1380
|
+
} });
|
|
1381
|
+
};
|
|
1382
|
+
|
|
1383
|
+
//#endregion
|
|
1384
|
+
//#region src/custom/provider.ts
|
|
1385
|
+
const createCustomProvider = (providerId, resourceProviders) => {
|
|
1386
|
+
const version = 1;
|
|
1387
|
+
const getProvider = (type) => {
|
|
1388
|
+
const provider = resourceProviders[type];
|
|
1389
|
+
if (!provider) throw new Error(`The "${providerId}" provider doesn't support the "${type}" resource type.`);
|
|
1390
|
+
return provider;
|
|
1391
|
+
};
|
|
1392
|
+
return {
|
|
1393
|
+
ownResource(id) {
|
|
1394
|
+
return id === `custom:${providerId}`;
|
|
1395
|
+
},
|
|
1396
|
+
async getResource({ type, ...props }) {
|
|
1397
|
+
const provider = getProvider(type);
|
|
1398
|
+
if (!provider.getResource) return {
|
|
1399
|
+
version,
|
|
1400
|
+
state: props.state
|
|
1401
|
+
};
|
|
1402
|
+
return {
|
|
1403
|
+
version,
|
|
1404
|
+
state: await provider.getResource(props)
|
|
1405
|
+
};
|
|
1406
|
+
},
|
|
1407
|
+
async createResource({ type, ...props }) {
|
|
1408
|
+
const provider = getProvider(type);
|
|
1409
|
+
if (!provider.createResource) return {
|
|
1410
|
+
version,
|
|
1411
|
+
state: props.state
|
|
1412
|
+
};
|
|
1413
|
+
return {
|
|
1414
|
+
version,
|
|
1415
|
+
state: await provider.createResource(props)
|
|
1416
|
+
};
|
|
1417
|
+
},
|
|
1418
|
+
async updateResource({ type, ...props }) {
|
|
1419
|
+
const provider = getProvider(type);
|
|
1420
|
+
if (!provider.updateResource) return {
|
|
1421
|
+
version,
|
|
1422
|
+
state: props.proposedState
|
|
1423
|
+
};
|
|
1424
|
+
return {
|
|
1425
|
+
version,
|
|
1426
|
+
state: await provider.updateResource(props)
|
|
1427
|
+
};
|
|
1428
|
+
},
|
|
1429
|
+
async deleteResource({ type, ...props }) {
|
|
1430
|
+
await getProvider(type).deleteResource?.(props);
|
|
1431
|
+
},
|
|
1432
|
+
async planResourceChange({ type, ...props }) {
|
|
1433
|
+
const provider = getProvider(type);
|
|
1434
|
+
if (!provider.planResourceChange) return {
|
|
1435
|
+
version,
|
|
1436
|
+
state: props.proposedState,
|
|
1437
|
+
requiresReplacement: false
|
|
1438
|
+
};
|
|
1439
|
+
return {
|
|
1440
|
+
version,
|
|
1441
|
+
...await provider.planResourceChange(props)
|
|
1442
|
+
};
|
|
1443
|
+
},
|
|
1444
|
+
async getData({ type, ...props }) {
|
|
1445
|
+
return {
|
|
1446
|
+
version,
|
|
1447
|
+
state: await getProvider(type).getData?.(props) ?? {}
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
};
|
|
1451
|
+
};
|
|
1452
|
+
|
|
1453
|
+
//#endregion
|
|
1454
|
+
export { App, AppError, DynamoLockBackend, FileLockBackend, FileStateBackend, Future, Group, MemoryLockBackend, MemoryStateBackend, Output, ResourceAlreadyExists, ResourceError, ResourceNotFound, S3StateBackend, Stack, WorkSpace, createCustomProvider, createCustomResourceClass, createDebugger, createMeta, deferredOutput, enableDebug, findInputDeps, getMeta, isDataSource, isNode, isResource, nodeMetaSymbol, output, resolveInputs };
|