@terraforge/core 0.0.18 → 0.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { S3Client } from "@aws-sdk/client-s3";
2
1
  import { DynamoDB } from "@aws-sdk/client-dynamodb";
2
+ import { S3Client } from "@aws-sdk/client-s3";
3
3
  import { UUID } from "node:crypto";
4
4
  import { AwsCredentialIdentity, AwsCredentialIdentityProvider } from "@aws-sdk/types";
5
5
 
@@ -162,6 +162,11 @@ type ResourceStatusInfo = {
162
162
  tag: 'resource' | 'data';
163
163
  status: ResourceStatus;
164
164
  };
165
+ type StackStatusInfo = {
166
+ name: string;
167
+ urn: URN;
168
+ resources: ResourceStatusInfo[];
169
+ };
165
170
  //#endregion
166
171
  //#region src/backend/lock.d.ts
167
172
  type LockBackend = {
@@ -176,6 +181,7 @@ type AppState = {
176
181
  version?: number;
177
182
  idempotentToken?: UUID;
178
183
  stacks: Record<URN, StackState>;
184
+ pendingDeletes?: Record<URN, NodeState>;
179
185
  };
180
186
  type StackState = {
181
187
  name: string;
@@ -202,6 +208,20 @@ type StateBackend = {
202
208
  delete(urn: URN): Promise<void>;
203
209
  };
204
210
  //#endregion
211
+ //#region src/backend/activity-log.d.ts
212
+ type LogProps = {
213
+ action: 'deploy' | 'delete';
214
+ filters?: string[];
215
+ };
216
+ type Log = LogProps & {
217
+ user?: string;
218
+ date?: number;
219
+ };
220
+ type ActivityLogBackend = {
221
+ log(urn: URN, log: LogProps): Promise<void>;
222
+ tail(urn: URN): Promise<Log[]>;
223
+ };
224
+ //#endregion
205
225
  //#region src/provider.d.ts
206
226
  type CreateProps<T = State> = {
207
227
  type: string;
@@ -313,6 +333,7 @@ type WorkSpaceOptions = {
313
333
  backend: {
314
334
  state: StateBackend;
315
335
  lock: LockBackend;
336
+ activityLog?: ActivityLogBackend;
316
337
  };
317
338
  hooks?: Hooks;
318
339
  };
@@ -338,7 +359,7 @@ declare class WorkSpace {
338
359
  /**
339
360
  * Get the status of all resources in the app by comparing current config with state file.
340
361
  */
341
- status(app: App): Promise<ResourceStatusInfo[]>;
362
+ status(app: App): Promise<StackStatusInfo[]>;
342
363
  protected destroyProviders(): Promise<void>;
343
364
  }
344
365
  //#endregion
@@ -361,6 +382,19 @@ declare class AppError extends Error {
361
382
  declare class ResourceNotFound extends Error {}
362
383
  declare class ResourceAlreadyExists extends Error {}
363
384
  //#endregion
385
+ //#region src/backend/memory/activity-log.d.ts
386
+ type Props$4 = {
387
+ user?: string;
388
+ };
389
+ declare class MemoryActivityLogBackend implements ActivityLogBackend {
390
+ private props;
391
+ protected groups: Map<`urn:${string}`, Log[]>;
392
+ constructor(props?: Props$4);
393
+ log(urn: URN, log: LogProps): Promise<void>;
394
+ private getLogGroup;
395
+ tail(urn: URN, limit?: number): Promise<Log[]>;
396
+ }
397
+ //#endregion
364
398
  //#region src/backend/memory/state.d.ts
365
399
  declare class MemoryStateBackend implements StateBackend {
366
400
  protected states: Map<`urn:${string}`, AppState>;
@@ -379,6 +413,20 @@ declare class MemoryLockBackend implements LockBackend {
379
413
  clear(): void;
380
414
  }
381
415
  //#endregion
416
+ //#region src/backend/file/activity-log.d.ts
417
+ type Props$3 = {
418
+ user?: string;
419
+ dir: string;
420
+ };
421
+ declare class FileActivityLogBackend implements ActivityLogBackend {
422
+ private props;
423
+ constructor(props: Props$3);
424
+ private logFile;
425
+ private mkdir;
426
+ log(urn: URN, log: LogProps): Promise<void>;
427
+ tail(urn: URN, limit?: number): Promise<Log[]>;
428
+ }
429
+ //#endregion
382
430
  //#region src/backend/file/state.d.ts
383
431
  declare class FileStateBackend implements StateBackend {
384
432
  private props;
@@ -405,23 +453,23 @@ declare class FileLockBackend implements LockBackend {
405
453
  lock(urn: URN): Promise<() => Promise<void>>;
406
454
  }
407
455
  //#endregion
408
- //#region src/backend/aws/s3-state.d.ts
409
- type Props$1 = {
456
+ //#region src/backend/aws/dynamodb-activity-log.d.ts
457
+ type Props$2 = {
410
458
  credentials: AwsCredentialIdentity | AwsCredentialIdentityProvider;
411
459
  region: string;
412
- bucket: string;
460
+ tableName: string;
461
+ user?: string;
413
462
  };
414
- declare class S3StateBackend implements StateBackend {
463
+ declare class DynamoDBActivityLogBackend implements ActivityLogBackend {
415
464
  private props;
416
- protected client: S3Client;
417
- constructor(props: Props$1);
418
- get(urn: URN): Promise<any>;
419
- update(urn: URN, state: AppState): Promise<void>;
420
- delete(urn: URN): Promise<void>;
465
+ protected client: DynamoDB;
466
+ constructor(props: Props$2);
467
+ log(urn: URN, log: LogProps): Promise<void>;
468
+ tail(urn: URN, limit?: number): Promise<Log[]>;
421
469
  }
422
470
  //#endregion
423
471
  //#region src/backend/aws/dynamodb-lock.d.ts
424
- type Props = {
472
+ type Props$1 = {
425
473
  credentials: AwsCredentialIdentity | AwsCredentialIdentityProvider;
426
474
  region: string;
427
475
  tableName: string;
@@ -429,12 +477,27 @@ type Props = {
429
477
  declare class DynamoLockBackend implements LockBackend {
430
478
  private props;
431
479
  protected client: DynamoDB;
432
- constructor(props: Props);
480
+ constructor(props: Props$1);
433
481
  insecureReleaseLock(urn: URN): Promise<void>;
434
482
  locked(urn: URN): Promise<boolean>;
435
483
  lock(urn: URN): Promise<() => Promise<void>>;
436
484
  }
437
485
  //#endregion
486
+ //#region src/backend/aws/s3-state.d.ts
487
+ type Props = {
488
+ credentials: AwsCredentialIdentity | AwsCredentialIdentityProvider;
489
+ region: string;
490
+ bucket: string;
491
+ };
492
+ declare class S3StateBackend implements StateBackend {
493
+ private props;
494
+ protected client: S3Client;
495
+ constructor(props: Props);
496
+ get(urn: URN): Promise<any>;
497
+ update(urn: URN, state: AppState): Promise<void>;
498
+ delete(urn: URN): Promise<void>;
499
+ }
500
+ //#endregion
438
501
  //#region src/helpers.d.ts
439
502
  declare const file: (path: string, encoding?: BufferEncoding) => Future<string>;
440
503
  declare const hash: (path: string, algo?: string) => Future<string>;
@@ -465,4 +528,4 @@ type CustomResourceProvider = Partial<{
465
528
  }>;
466
529
  declare const createCustomProvider: (providerId: string, resourceProviders: Record<string, CustomResourceProvider>) => Provider;
467
530
  //#endregion
468
- export { App, AppError, type Config, type CreateProps, type CustomResourceProvider, type DataSource, type DataSourceFunction, type DataSourceMeta, type DeleteProps, DynamoLockBackend, FileLockBackend, FileStateBackend, Future, type GetDataProps, type GetProps, Group, type Input, LockBackend, MemoryLockBackend, MemoryStateBackend, type Meta, type Node, type OptionalInput, type OptionalOutput, Output, type PlanProps, type ProcedureOptions, type Provider, type Resource, ResourceAlreadyExists, type ResourceClass, type ResourceConfig, ResourceError, type ResourceMeta, ResourceNotFound, type ResourceStatus, type ResourceStatusInfo, S3StateBackend, Stack, type State, StateBackend, type Tag, type URN, type UpdateProps, WorkSpace, type WorkSpaceOptions, createCustomProvider, createCustomResourceClass, createDebugger, createMeta, deferredOutput, enableDebug, findInputDeps, getMeta, isDataSource, isNode, isResource, nodeMetaSymbol, output, resolveInputs };
531
+ export { ActivityLogBackend, App, AppError, type Config, type CreateProps, type CustomResourceProvider, type DataSource, type DataSourceFunction, type DataSourceMeta, type DeleteProps, DynamoDBActivityLogBackend, DynamoLockBackend, FileActivityLogBackend, FileLockBackend, FileStateBackend, Future, type GetDataProps, type GetProps, Group, type Input, LockBackend, Log, LogProps, MemoryActivityLogBackend, MemoryLockBackend, MemoryStateBackend, type Meta, type Node, type OptionalInput, type OptionalOutput, Output, type PlanProps, type ProcedureOptions, type Provider, type Resource, ResourceAlreadyExists, type ResourceClass, type ResourceConfig, ResourceError, type ResourceMeta, ResourceNotFound, type ResourceStatus, type ResourceStatusInfo, S3StateBackend, Stack, type StackStatusInfo, type State, StateBackend, type Tag, type URN, type UpdateProps, WorkSpace, type WorkSpaceOptions, createCustomProvider, createCustomResourceClass, createDebugger, createMeta, deferredOutput, enableDebug, findInputDeps, getMeta, isDataSource, isNode, isResource, nodeMetaSymbol, output, resolveInputs };
package/dist/index.mjs CHANGED
@@ -4,12 +4,12 @@ import { DirectedGraph } from "graphology";
4
4
  import { topologicalGenerations, willCreateCycle } from "graphology-dag";
5
5
  import { v5 } from "uuid";
6
6
  import { get } from "get-wild";
7
- import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
7
+ import { appendFile, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
8
8
  import { join } from "node:path";
9
9
  import { lock } from "proper-lockfile";
10
- import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
11
10
  import { DynamoDB } from "@aws-sdk/client-dynamodb";
12
11
  import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";
12
+ import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
13
13
  import { createHash } from "node:crypto";
14
14
 
15
15
  //#region src/node.ts
@@ -605,6 +605,10 @@ const deleteResource = async (appToken, urn, state, opt) => {
605
605
  //#endregion
606
606
  //#region src/workspace/procedure/delete-app.ts
607
607
  const deleteApp = async (app, opt) => {
608
+ await opt.backend.activityLog?.log(app.urn, {
609
+ action: "delete",
610
+ filters: opt.filters
611
+ });
608
612
  const latestState = await opt.backend.state.get(app.urn);
609
613
  if (!latestState) throw new AppError(app.name, [], `App already deleted: ${app.name}`);
610
614
  const appState = migrateAppState(latestState);
@@ -623,6 +627,16 @@ const deleteApp = async (app, opt) => {
623
627
  delete stackState.nodes[urn];
624
628
  });
625
629
  const errors = await graph.run();
630
+ if (errors.length === 0 && appState.pendingDeletes) {
631
+ for (const [urn, nodeState] of entries(appState.pendingDeletes)) try {
632
+ await deleteResource(appState.idempotentToken, urn, nodeState, opt);
633
+ delete appState.pendingDeletes[urn];
634
+ } catch (error) {
635
+ if (error instanceof Error) errors.push(error);
636
+ else errors.push(/* @__PURE__ */ new Error(`${error}`));
637
+ }
638
+ if (Object.keys(appState.pendingDeletes).length === 0) delete appState.pendingDeletes;
639
+ }
626
640
  removeEmptyStackStates(appState);
627
641
  delete appState.idempotentToken;
628
642
  await opt.backend.state.update(app.urn, appState);
@@ -813,7 +827,8 @@ const updateResource = async (resource, appToken, priorInputState, priorOutputSt
813
827
  const idempotantToken = createIdempotantToken(appToken, meta.urn, "update");
814
828
  let result;
815
829
  debug$2(meta.type);
816
- debug$2(proposedState);
830
+ debug$2("prior state", priorOutputState);
831
+ debug$2("proposed state", proposedState);
817
832
  try {
818
833
  await opt.hooks?.beforeResourceUpdate?.({
819
834
  urn: resource.urn,
@@ -852,6 +867,10 @@ const updateResource = async (resource, appToken, priorInputState, priorOutputSt
852
867
  const debug$1 = createDebugger("Deploy App");
853
868
  const deployApp = async (app, opt) => {
854
869
  debug$1(app.name, "start");
870
+ await opt.backend.activityLog?.log(app.urn, {
871
+ action: "deploy",
872
+ filters: opt.filters
873
+ });
855
874
  const appState = migrateAppState(await opt.backend.state.get(app.urn) ?? {
856
875
  name: app.name,
857
876
  stacks: {}
@@ -883,7 +902,6 @@ const deployApp = async (app, opt) => {
883
902
  }
884
903
  const queue = createConcurrencyQueue(opt.concurrency ?? 10);
885
904
  const graph = new DependencyGraph();
886
- const replacementDeletes = /* @__PURE__ */ new Map();
887
905
  const allNodes = {};
888
906
  for (const stackState of Object.values(appState.stacks)) for (const [urn, nodeState] of entries(stackState.nodes)) allNodes[urn] = nodeState;
889
907
  for (const stack of filteredOutStacks) {
@@ -966,10 +984,33 @@ const deployApp = async (app, opt) => {
966
984
  let newResourceState;
967
985
  const ignoreReplace = forcedUpdateDependents.has(meta$1.urn);
968
986
  if (!ignoreReplace && requiresReplacement(nodeState.input, input, meta$1.config?.replaceOnChanges ?? [])) if (meta$1.config?.createBeforeReplace) {
987
+ for (const [dependentUrn, dependentNode] of nodeByUrn.entries()) {
988
+ if (!isResource(dependentNode)) continue;
989
+ const dependentMeta = getMeta(dependentNode);
990
+ if (!dependentMeta.dependencies.has(meta$1.urn)) continue;
991
+ const dependentStackState = stackStates.get(dependentMeta.stack.urn);
992
+ const dependentState = dependentStackState?.nodes[dependentUrn];
993
+ if (!dependentStackState || !dependentState) continue;
994
+ const dependencyPaths = findDependencyPaths(dependentMeta.input, meta$1.urn);
995
+ if (dependencyPaths.length === 0) continue;
996
+ const dependentProvider = findProvider(opt.providers, dependentMeta.provider);
997
+ if (dependentProvider.planResourceChange) {
998
+ if ((await dependentProvider.planResourceChange({
999
+ type: dependentMeta.type,
1000
+ priorState: dependentState.output,
1001
+ proposedState: input
1002
+ })).requiresReplacement) {
1003
+ 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.`));
1004
+ }
1005
+ }
1006
+ }
969
1007
  const priorState = { ...nodeState };
970
1008
  newResourceState = await createResource(node, appState.idempotentToken, input, opt);
971
1009
  if (newResourceState.output) meta$1.resolve(newResourceState.output);
972
- if (!meta$1.config?.retainOnDelete) replacementDeletes.set(meta$1.urn, priorState);
1010
+ if (!meta$1.config?.retainOnDelete) {
1011
+ appState.pendingDeletes ??= {};
1012
+ appState.pendingDeletes[meta$1.urn] = priorState;
1013
+ }
973
1014
  } else {
974
1015
  for (const [dependentUrn, dependentNode] of nodeByUrn.entries()) {
975
1016
  if (!isResource(dependentNode)) continue;
@@ -1028,11 +1069,15 @@ const deployApp = async (app, opt) => {
1028
1069
  }
1029
1070
  }
1030
1071
  const errors = await graph.run();
1031
- if (errors.length === 0 && replacementDeletes.size > 0) for (const [urn, nodeState] of replacementDeletes.entries()) try {
1032
- await deleteResource(appState.idempotentToken, urn, nodeState, opt);
1033
- } catch (error) {
1034
- if (error instanceof Error) errors.push(error);
1035
- else errors.push(/* @__PURE__ */ new Error(`${error}`));
1072
+ if (errors.length === 0 && appState.pendingDeletes) {
1073
+ for (const [urn, nodeState] of entries(appState.pendingDeletes)) try {
1074
+ await deleteResource(appState.idempotentToken, urn, nodeState, opt);
1075
+ delete appState.pendingDeletes[urn];
1076
+ } catch (error) {
1077
+ if (error instanceof Error) errors.push(error);
1078
+ else errors.push(/* @__PURE__ */ new Error(`${error}`));
1079
+ }
1080
+ if (Object.keys(appState.pendingDeletes).length === 0) delete appState.pendingDeletes;
1036
1081
  }
1037
1082
  removeEmptyStackStates(appState);
1038
1083
  delete appState.idempotentToken;
@@ -1135,11 +1180,12 @@ const filterStateToMatchConfig = (state, config) => {
1135
1180
  };
1136
1181
  const status = async (app, opt) => {
1137
1182
  const appState = await opt.backend.state.get(app.urn);
1138
- const resources = [];
1183
+ const stacks = [];
1139
1184
  const configuredUrns = /* @__PURE__ */ new Set();
1140
1185
  for (const stack of app.stacks) for (const node of stack.nodes) configuredUrns.add(getMeta(node).urn);
1141
1186
  for (const stack of app.stacks) {
1142
1187
  const stackState = appState?.stacks[stack.urn];
1188
+ const resources = [];
1143
1189
  for (const node of stack.nodes) {
1144
1190
  const meta = getMeta(node);
1145
1191
  const nodeState = stackState?.nodes[meta.urn];
@@ -1172,18 +1218,31 @@ const status = async (app, opt) => {
1172
1218
  status: "stale"
1173
1219
  });
1174
1220
  }
1221
+ stacks.push({
1222
+ name: stack.name,
1223
+ urn: stack.urn,
1224
+ resources
1225
+ });
1175
1226
  }
1176
1227
  if (appState) {
1177
1228
  const configuredStackUrns = new Set(app.stacks.map((s) => s.urn));
1178
- for (const [stackUrn, stackState] of Object.entries(appState.stacks)) if (!configuredStackUrns.has(stackUrn)) for (const [urn, nodeState] of Object.entries(stackState.nodes)) resources.push({
1179
- urn,
1180
- type: nodeState.type,
1181
- provider: nodeState.provider,
1182
- tag: nodeState.tag,
1183
- status: "stale"
1184
- });
1229
+ for (const [stackUrn, stackState] of Object.entries(appState.stacks)) if (!configuredStackUrns.has(stackUrn)) {
1230
+ const resources = [];
1231
+ for (const [urn, nodeState] of Object.entries(stackState.nodes)) resources.push({
1232
+ urn,
1233
+ type: nodeState.type,
1234
+ provider: nodeState.provider,
1235
+ tag: nodeState.tag,
1236
+ status: "stale"
1237
+ });
1238
+ stacks.push({
1239
+ name: stackState.name,
1240
+ urn: stackUrn,
1241
+ resources
1242
+ });
1243
+ }
1185
1244
  }
1186
- return resources;
1245
+ return stacks;
1187
1246
  };
1188
1247
 
1189
1248
  //#endregion
@@ -1253,6 +1312,29 @@ var WorkSpace = class {
1253
1312
  }
1254
1313
  };
1255
1314
 
1315
+ //#endregion
1316
+ //#region src/backend/memory/activity-log.ts
1317
+ var MemoryActivityLogBackend = class {
1318
+ groups = /* @__PURE__ */ new Map();
1319
+ constructor(props = {}) {
1320
+ this.props = props;
1321
+ }
1322
+ async log(urn, log) {
1323
+ this.getLogGroup(urn).push({
1324
+ user: this.props.user,
1325
+ date: Date.now(),
1326
+ ...log
1327
+ });
1328
+ }
1329
+ getLogGroup(urn) {
1330
+ if (!this.groups.has(urn)) this.groups.set(urn, []);
1331
+ return this.groups.get(urn);
1332
+ }
1333
+ async tail(urn, limit = 10) {
1334
+ return this.getLogGroup(urn).slice(-limit);
1335
+ }
1336
+ };
1337
+
1256
1338
  //#endregion
1257
1339
  //#region src/backend/memory/state.ts
1258
1340
  var MemoryStateBackend = class {
@@ -1294,6 +1376,34 @@ var MemoryLockBackend = class {
1294
1376
  }
1295
1377
  };
1296
1378
 
1379
+ //#endregion
1380
+ //#region src/backend/file/activity-log.ts
1381
+ var FileActivityLogBackend = class {
1382
+ constructor(props) {
1383
+ this.props = props;
1384
+ }
1385
+ logFile(urn) {
1386
+ return join(this.props.dir, `${urn}.log.jsonl`);
1387
+ }
1388
+ async mkdir() {
1389
+ await mkdir(this.props.dir, { recursive: true });
1390
+ }
1391
+ async log(urn, log) {
1392
+ const json = JSON.stringify({
1393
+ user: this.props.user,
1394
+ date: Date.now(),
1395
+ ...log
1396
+ });
1397
+ await this.mkdir();
1398
+ await appendFile(this.logFile(urn), `${json}\n`);
1399
+ }
1400
+ async tail(urn, limit = 10) {
1401
+ const file$1 = this.logFile(urn);
1402
+ if (!(await stat(file$1)).isFile()) return [];
1403
+ return (await readFile(file$1, "utf8")).split("\n").filter(Boolean).slice(-limit).map((line) => JSON.parse(line));
1404
+ }
1405
+ };
1406
+
1297
1407
  //#endregion
1298
1408
  //#region src/backend/file/state.ts
1299
1409
  const debug = createDebugger("State");
@@ -1302,7 +1412,7 @@ var FileStateBackend = class {
1302
1412
  this.props = props;
1303
1413
  }
1304
1414
  stateFile(urn) {
1305
- return join(this.props.dir, `${urn}.json`);
1415
+ return join(this.props.dir, `${urn}.state.json`);
1306
1416
  }
1307
1417
  async mkdir() {
1308
1418
  await mkdir(this.props.dir, { recursive: true });
@@ -1354,40 +1464,35 @@ var FileLockBackend = class {
1354
1464
  };
1355
1465
 
1356
1466
  //#endregion
1357
- //#region src/backend/aws/s3-state.ts
1358
- var S3StateBackend = class {
1467
+ //#region src/backend/aws/dynamodb-activity-log.ts
1468
+ var DynamoDBActivityLogBackend = class {
1359
1469
  client;
1360
1470
  constructor(props) {
1361
1471
  this.props = props;
1362
- this.client = new S3Client(props);
1363
- }
1364
- async get(urn) {
1365
- let result;
1366
- try {
1367
- result = await this.client.send(new GetObjectCommand({
1368
- Bucket: this.props.bucket,
1369
- Key: `${urn}.state`
1370
- }));
1371
- } catch (error) {
1372
- if (error instanceof S3ServiceException && error.name === "NoSuchKey") return;
1373
- throw error;
1374
- }
1375
- if (!result.Body) return;
1376
- const body = await result.Body.transformToString("utf8");
1377
- return JSON.parse(body);
1472
+ this.client = new DynamoDB(props);
1378
1473
  }
1379
- async update(urn, state) {
1380
- await this.client.send(new PutObjectCommand({
1381
- Bucket: this.props.bucket,
1382
- Key: `${urn}.state`,
1383
- Body: JSON.stringify(state)
1384
- }));
1474
+ async log(urn, log) {
1475
+ await this.client.putItem({
1476
+ TableName: this.props.tableName,
1477
+ Item: marshall({
1478
+ urn,
1479
+ user: this.props.user,
1480
+ date: Date.now(),
1481
+ ...log
1482
+ })
1483
+ });
1385
1484
  }
1386
- async delete(urn) {
1387
- await this.client.send(new DeleteObjectCommand({
1388
- Bucket: this.props.bucket,
1389
- Key: `${urn}.state`
1390
- }));
1485
+ async tail(urn, limit = 10) {
1486
+ return (await this.client.query({
1487
+ TableName: this.props.tableName,
1488
+ KeyConditionExpression: "#urn = :urn",
1489
+ ExpressionAttributeNames: { "#urn": "urn" },
1490
+ ExpressionAttributeValues: { ":urn": marshall(urn) },
1491
+ ScanIndexForward: false,
1492
+ Limit: limit
1493
+ })).Items?.map((item) => {
1494
+ return unmarshall(item);
1495
+ }) ?? [];
1391
1496
  }
1392
1497
  };
1393
1498
 
@@ -1438,6 +1543,44 @@ var DynamoLockBackend = class {
1438
1543
  }
1439
1544
  };
1440
1545
 
1546
+ //#endregion
1547
+ //#region src/backend/aws/s3-state.ts
1548
+ var S3StateBackend = class {
1549
+ client;
1550
+ constructor(props) {
1551
+ this.props = props;
1552
+ this.client = new S3Client(props);
1553
+ }
1554
+ async get(urn) {
1555
+ let result;
1556
+ try {
1557
+ result = await this.client.send(new GetObjectCommand({
1558
+ Bucket: this.props.bucket,
1559
+ Key: `${urn}.state`
1560
+ }));
1561
+ } catch (error) {
1562
+ if (error instanceof S3ServiceException && error.name === "NoSuchKey") return;
1563
+ throw error;
1564
+ }
1565
+ if (!result.Body) return;
1566
+ const body = await result.Body.transformToString("utf8");
1567
+ return JSON.parse(body);
1568
+ }
1569
+ async update(urn, state) {
1570
+ await this.client.send(new PutObjectCommand({
1571
+ Bucket: this.props.bucket,
1572
+ Key: `${urn}.state`,
1573
+ Body: JSON.stringify(state)
1574
+ }));
1575
+ }
1576
+ async delete(urn) {
1577
+ await this.client.send(new DeleteObjectCommand({
1578
+ Bucket: this.props.bucket,
1579
+ Key: `${urn}.state`
1580
+ }));
1581
+ }
1582
+ };
1583
+
1441
1584
  //#endregion
1442
1585
  //#region src/helpers.ts
1443
1586
  const file = (path, encoding = "utf8") => {
@@ -1548,4 +1691,4 @@ const createCustomProvider = (providerId, resourceProviders) => {
1548
1691
  };
1549
1692
 
1550
1693
  //#endregion
1551
- 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 };
1694
+ export { App, AppError, DynamoDBActivityLogBackend, DynamoLockBackend, FileActivityLogBackend, FileLockBackend, FileStateBackend, Future, Group, MemoryActivityLogBackend, MemoryLockBackend, MemoryStateBackend, Output, ResourceAlreadyExists, ResourceError, ResourceNotFound, S3StateBackend, Stack, WorkSpace, createCustomProvider, createCustomResourceClass, createDebugger, createMeta, deferredOutput, enableDebug, findInputDeps, getMeta, isDataSource, isNode, isResource, nodeMetaSymbol, output, resolveInputs };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@terraforge/core",
3
- "version": "0.0.18",
3
+ "version": "0.0.20",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",