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