From d5fc08f3b889eb3938600737ec17927008f7bfb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?TATSUNO=20=E2=80=9CTaz=E2=80=9D=20Yasuhiro?= Date: Thu, 12 Oct 2023 03:46:04 +0900 Subject: [PATCH] feat(sdk): implement cloud.Bucket inflight method metadata for AWS targets and Simulator (#4338) Closes https://github.com/winglang/wing/issues/4331 Closes https://github.com/winglang/wing/issues/4330 This PR implements inflgiht method `Bucket.metadata` for AWS. Other implementations are stubbed. ## Checklist - [x] Title matches [Winglang's style guide](https://www.winglang.io/contributing/start-here/pull_requests#how-are-pull-request-titles-formatted) - [x] Description explains motivation and solution - [x] Tests added (always) - [x] Docs updated (only required for features) - [ ] Added `pr/e2e-full` label if this feature requires end-to-end testing *By submitting this pull request, I confirm that my contribution is made under the terms of the [Wing Cloud Contribution License](https://github.com/winglang/wing/blob/main/CONTRIBUTION_LICENSE.md)*. --- .../04-standard-library/01-cloud/bucket.md | 75 ++++++++++ .../tests/sdk_tests/bucket/metadata.test.w | 17 +++ .../completions/capture_in_test.snap | 12 ++ .../incomplete_inflight_namespace.snap | 8 +- .../completions/namespace_middle_dot.snap | 8 +- .../variable_type_annotation_namespace.snap | 8 +- libs/wingsdk/src/cloud/bucket.ts | 26 +++- .../wingsdk/src/shared-aws/bucket.inflight.ts | 36 ++++- libs/wingsdk/src/shared-aws/permissions.ts | 1 + .../wingsdk/src/target-sim/bucket.inflight.ts | 31 ++++- .../src/target-tf-azure/bucket.inflight.ts | 16 ++- .../test/shared-aws/bucket.inflight.test.ts | 69 ++++++++++ .../bucket/metadata.test.w_compile_tf-aws.md | 129 ++++++++++++++++++ .../bucket/metadata.test.w_test_sim.md | 12 ++ 14 files changed, 440 insertions(+), 8 deletions(-) create mode 100644 examples/tests/sdk_tests/bucket/metadata.test.w create mode 100644 tools/hangar/__snapshots__/test_corpus/sdk_tests/bucket/metadata.test.w_compile_tf-aws.md create mode 100644 tools/hangar/__snapshots__/test_corpus/sdk_tests/bucket/metadata.test.w_test_sim.md diff --git a/docs/docs/04-standard-library/01-cloud/bucket.md b/docs/docs/04-standard-library/01-cloud/bucket.md index 98067babf3b..f051c28e501 100644 --- a/docs/docs/04-standard-library/01-cloud/bucket.md +++ b/docs/docs/04-standard-library/01-cloud/bucket.md @@ -166,6 +166,7 @@ new cloud.Bucket(props?: BucketProps); | get | Retrieve an object from the bucket. | | getJson | Retrieve a Json object from the bucket. | | list | Retrieve existing objects keys from the bucket. | +| metadata | Get the metadata of an object in the bucket. | | publicUrl | Returns a url to the given file. | | put | Put an object in the bucket. | | putJson | Put a Json object in the bucket. | @@ -401,6 +402,22 @@ Limits the response to keys that begin with the specified prefix. --- +##### `metadata` + +```wing +inflight metadata(key: str): ObjectMetadata +``` + +Get the metadata of an object in the bucket. + +###### `key`Required + +- *Type:* str + +Key of the object. + +--- + ##### `publicUrl` ```wing @@ -723,6 +740,64 @@ Whether the bucket's objects should be publicly accessible. --- +### ObjectMetadata + +Metadata of a bucket object. + +#### Initializer + +```wing +bring cloud; + +let ObjectMetadata = cloud.ObjectMetadata{ ... }; +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| lastModified | datetime | The time the object was last modified. | +| size | num | The size of the object in bytes. | +| contentType | str | The content type of the object, if it is known. | + +--- + +##### `lastModified`Required + +```wing +lastModified: datetime; +``` + +- *Type:* datetime + +The time the object was last modified. + +--- + +##### `size`Required + +```wing +size: num; +``` + +- *Type:* num + +The size of the object in bytes. + +--- + +##### `contentType`Optional + +```wing +contentType: str; +``` + +- *Type:* str + +The content type of the object, if it is known. + +--- + ### SignedUrlOptions Interface for signed url options. diff --git a/examples/tests/sdk_tests/bucket/metadata.test.w b/examples/tests/sdk_tests/bucket/metadata.test.w new file mode 100644 index 00000000000..3584614b0ec --- /dev/null +++ b/examples/tests/sdk_tests/bucket/metadata.test.w @@ -0,0 +1,17 @@ +bring cloud; + +let b = new cloud.Bucket(); + +test "metadata" { + b.put("test1.txt", "Foo"); + + assert(b.metadata("test1.txt").size == 3); + assert(b.metadata("test1.txt").contentType == "application/octet-stream"); + assert(b.metadata("test1.txt").lastModified.year >= 2023); + + try { + b.metadata("no-such-file.txt").lastModified; + } catch e { + assert(e == "Object does not exist (key=no-such-file.txt)."); + } +} diff --git a/libs/wingc/src/lsp/snapshots/completions/capture_in_test.snap b/libs/wingc/src/lsp/snapshots/completions/capture_in_test.snap index 8cafefce8c9..641fe4e6f1b 100644 --- a/libs/wingc/src/lsp/snapshots/completions/capture_in_test.snap +++ b/libs/wingc/src/lsp/snapshots/completions/capture_in_test.snap @@ -68,6 +68,18 @@ source: libs/wingc/src/lsp/completions.rs command: title: triggerParameterHints command: editor.action.triggerParameterHints +- label: metadata + kind: 2 + detail: "inflight (key: str): ObjectMetadata" + documentation: + kind: markdown + value: "```wing\ninflight metadata: inflight (key: str): ObjectMetadata\n```\n---\nGet the metadata of an object in the bucket.\n\n\n*@Throws* *if there is no object with the given key.*" + sortText: ff|metadata + insertText: metadata($0) + insertTextFormat: 2 + command: + title: triggerParameterHints + command: editor.action.triggerParameterHints - label: publicUrl kind: 2 detail: "inflight (key: str): str" diff --git a/libs/wingc/src/lsp/snapshots/completions/incomplete_inflight_namespace.snap b/libs/wingc/src/lsp/snapshots/completions/incomplete_inflight_namespace.snap index d55a4eb8453..1e7bfa13719 100644 --- a/libs/wingc/src/lsp/snapshots/completions/incomplete_inflight_namespace.snap +++ b/libs/wingc/src/lsp/snapshots/completions/incomplete_inflight_namespace.snap @@ -217,6 +217,12 @@ source: libs/wingc/src/lsp/completions.rs kind: markdown value: "```wing\nstruct GetSecretValueOptions\n```\n---\nOptions when getting a secret value.\n### Fields\n- `cache?` — Whether to cache the value." sortText: hh|GetSecretValueOptions +- label: ObjectMetadata + kind: 22 + documentation: + kind: markdown + value: "```wing\nstruct ObjectMetadata\n```\n---\nMetadata of a bucket object.\n### Fields\n- `contentType?` — The content type of the object, if it is known.\n- `lastModified` — The time the object was last modified.\n- `size` — The size of the object in bytes." + sortText: hh|ObjectMetadata - label: OnDeployProps kind: 22 documentation: @@ -317,7 +323,7 @@ source: libs/wingc/src/lsp/completions.rs kind: 8 documentation: kind: markdown - value: "```wing\ninterface IBucketClient\n```\n---\nInflight interface for `Bucket`.\n### Methods\n- `delete` — Delete an existing object using a key from the bucket.\n- `exists` — Check if an object exists in the bucket.\n- `get` — Retrieve an object from the bucket.\n- `getJson` — Retrieve a Json object from the bucket.\n- `list` — Retrieve existing objects keys from the bucket.\n- `publicUrl` — Returns a url to the given file.\n- `put` — Put an object in the bucket.\n- `putJson` — Put a Json object in the bucket.\n- `signedUrl` — Returns a signed url to the given file.\n- `tryDelete` — Delete an object from the bucket if it exists.\n- `tryGet` — Get an object from the bucket if it exists.\n- `tryGetJson` — Gets an object from the bucket if it exists, parsing it as Json." + value: "```wing\ninterface IBucketClient\n```\n---\nInflight interface for `Bucket`.\n### Methods\n- `delete` — Delete an existing object using a key from the bucket.\n- `exists` — Check if an object exists in the bucket.\n- `get` — Retrieve an object from the bucket.\n- `getJson` — Retrieve a Json object from the bucket.\n- `list` — Retrieve existing objects keys from the bucket.\n- `metadata` — Get the metadata of an object in the bucket.\n- `publicUrl` — Returns a url to the given file.\n- `put` — Put an object in the bucket.\n- `putJson` — Put a Json object in the bucket.\n- `signedUrl` — Returns a signed url to the given file.\n- `tryDelete` — Delete an object from the bucket if it exists.\n- `tryGet` — Get an object from the bucket if it exists.\n- `tryGetJson` — Gets an object from the bucket if it exists, parsing it as Json." sortText: ii|IBucketClient - label: IBucketEventHandler kind: 8 diff --git a/libs/wingc/src/lsp/snapshots/completions/namespace_middle_dot.snap b/libs/wingc/src/lsp/snapshots/completions/namespace_middle_dot.snap index d55a4eb8453..1e7bfa13719 100644 --- a/libs/wingc/src/lsp/snapshots/completions/namespace_middle_dot.snap +++ b/libs/wingc/src/lsp/snapshots/completions/namespace_middle_dot.snap @@ -217,6 +217,12 @@ source: libs/wingc/src/lsp/completions.rs kind: markdown value: "```wing\nstruct GetSecretValueOptions\n```\n---\nOptions when getting a secret value.\n### Fields\n- `cache?` — Whether to cache the value." sortText: hh|GetSecretValueOptions +- label: ObjectMetadata + kind: 22 + documentation: + kind: markdown + value: "```wing\nstruct ObjectMetadata\n```\n---\nMetadata of a bucket object.\n### Fields\n- `contentType?` — The content type of the object, if it is known.\n- `lastModified` — The time the object was last modified.\n- `size` — The size of the object in bytes." + sortText: hh|ObjectMetadata - label: OnDeployProps kind: 22 documentation: @@ -317,7 +323,7 @@ source: libs/wingc/src/lsp/completions.rs kind: 8 documentation: kind: markdown - value: "```wing\ninterface IBucketClient\n```\n---\nInflight interface for `Bucket`.\n### Methods\n- `delete` — Delete an existing object using a key from the bucket.\n- `exists` — Check if an object exists in the bucket.\n- `get` — Retrieve an object from the bucket.\n- `getJson` — Retrieve a Json object from the bucket.\n- `list` — Retrieve existing objects keys from the bucket.\n- `publicUrl` — Returns a url to the given file.\n- `put` — Put an object in the bucket.\n- `putJson` — Put a Json object in the bucket.\n- `signedUrl` — Returns a signed url to the given file.\n- `tryDelete` — Delete an object from the bucket if it exists.\n- `tryGet` — Get an object from the bucket if it exists.\n- `tryGetJson` — Gets an object from the bucket if it exists, parsing it as Json." + value: "```wing\ninterface IBucketClient\n```\n---\nInflight interface for `Bucket`.\n### Methods\n- `delete` — Delete an existing object using a key from the bucket.\n- `exists` — Check if an object exists in the bucket.\n- `get` — Retrieve an object from the bucket.\n- `getJson` — Retrieve a Json object from the bucket.\n- `list` — Retrieve existing objects keys from the bucket.\n- `metadata` — Get the metadata of an object in the bucket.\n- `publicUrl` — Returns a url to the given file.\n- `put` — Put an object in the bucket.\n- `putJson` — Put a Json object in the bucket.\n- `signedUrl` — Returns a signed url to the given file.\n- `tryDelete` — Delete an object from the bucket if it exists.\n- `tryGet` — Get an object from the bucket if it exists.\n- `tryGetJson` — Gets an object from the bucket if it exists, parsing it as Json." sortText: ii|IBucketClient - label: IBucketEventHandler kind: 8 diff --git a/libs/wingc/src/lsp/snapshots/completions/variable_type_annotation_namespace.snap b/libs/wingc/src/lsp/snapshots/completions/variable_type_annotation_namespace.snap index d55a4eb8453..1e7bfa13719 100644 --- a/libs/wingc/src/lsp/snapshots/completions/variable_type_annotation_namespace.snap +++ b/libs/wingc/src/lsp/snapshots/completions/variable_type_annotation_namespace.snap @@ -217,6 +217,12 @@ source: libs/wingc/src/lsp/completions.rs kind: markdown value: "```wing\nstruct GetSecretValueOptions\n```\n---\nOptions when getting a secret value.\n### Fields\n- `cache?` — Whether to cache the value." sortText: hh|GetSecretValueOptions +- label: ObjectMetadata + kind: 22 + documentation: + kind: markdown + value: "```wing\nstruct ObjectMetadata\n```\n---\nMetadata of a bucket object.\n### Fields\n- `contentType?` — The content type of the object, if it is known.\n- `lastModified` — The time the object was last modified.\n- `size` — The size of the object in bytes." + sortText: hh|ObjectMetadata - label: OnDeployProps kind: 22 documentation: @@ -317,7 +323,7 @@ source: libs/wingc/src/lsp/completions.rs kind: 8 documentation: kind: markdown - value: "```wing\ninterface IBucketClient\n```\n---\nInflight interface for `Bucket`.\n### Methods\n- `delete` — Delete an existing object using a key from the bucket.\n- `exists` — Check if an object exists in the bucket.\n- `get` — Retrieve an object from the bucket.\n- `getJson` — Retrieve a Json object from the bucket.\n- `list` — Retrieve existing objects keys from the bucket.\n- `publicUrl` — Returns a url to the given file.\n- `put` — Put an object in the bucket.\n- `putJson` — Put a Json object in the bucket.\n- `signedUrl` — Returns a signed url to the given file.\n- `tryDelete` — Delete an object from the bucket if it exists.\n- `tryGet` — Get an object from the bucket if it exists.\n- `tryGetJson` — Gets an object from the bucket if it exists, parsing it as Json." + value: "```wing\ninterface IBucketClient\n```\n---\nInflight interface for `Bucket`.\n### Methods\n- `delete` — Delete an existing object using a key from the bucket.\n- `exists` — Check if an object exists in the bucket.\n- `get` — Retrieve an object from the bucket.\n- `getJson` — Retrieve a Json object from the bucket.\n- `list` — Retrieve existing objects keys from the bucket.\n- `metadata` — Get the metadata of an object in the bucket.\n- `publicUrl` — Returns a url to the given file.\n- `put` — Put an object in the bucket.\n- `putJson` — Put a Json object in the bucket.\n- `signedUrl` — Returns a signed url to the given file.\n- `tryDelete` — Delete an object from the bucket if it exists.\n- `tryGet` — Get an object from the bucket if it exists.\n- `tryGetJson` — Gets an object from the bucket if it exists, parsing it as Json." sortText: ii|IBucketClient - label: IBucketEventHandler kind: 8 diff --git a/libs/wingsdk/src/cloud/bucket.ts b/libs/wingsdk/src/cloud/bucket.ts index da916ba857f..c26ca8122ef 100644 --- a/libs/wingsdk/src/cloud/bucket.ts +++ b/libs/wingsdk/src/cloud/bucket.ts @@ -5,7 +5,7 @@ import { Topic } from "./topic"; import { fqnForType } from "../constants"; import { App } from "../core"; import { convertBetweenHandlers } from "../shared/convert"; -import { Json, IResource, Node, Resource, Duration } from "../std"; +import { Json, IResource, Node, Resource, Datetime, Duration } from "../std"; /** * Global identifier for `Bucket`. @@ -68,6 +68,7 @@ export abstract class Bucket extends Resource { BucketInflightMethods.TRY_GET_JSON, BucketInflightMethods.TRY_DELETE, BucketInflightMethods.SIGNED_URL, + BucketInflightMethods.METADATA, ]; } @@ -363,6 +364,28 @@ export interface IBucketClient { * @inflight */ signedUrl(key: string, options?: SignedUrlOptions): Promise; + + /** + * Get the metadata of an object in the bucket. + * @param key Key of the object. + * @Throws if there is no object with the given key. + * @inflight + */ + metadata(key: string): Promise; +} + +/** + * Metadata of a bucket object. + */ +export interface ObjectMetadata { + /** The size of the object in bytes. */ + readonly size: number; + + /** The time the object was last modified. */ + readonly lastModified: Datetime; + + /** The content type of the object, if it is known. */ + readonly contentType?: string; } /** @@ -467,4 +490,5 @@ export enum BucketInflightMethods { TRY_DELETE = "tryDelete", SIGNED_URL = "signedUrl", + METADATA = "metadata", } diff --git a/libs/wingsdk/src/shared-aws/bucket.inflight.ts b/libs/wingsdk/src/shared-aws/bucket.inflight.ts index 7d61b09ae0c..953e2ed2b99 100644 --- a/libs/wingsdk/src/shared-aws/bucket.inflight.ts +++ b/libs/wingsdk/src/shared-aws/bucket.inflight.ts @@ -12,12 +12,19 @@ import { GetPublicAccessBlockCommandOutput, S3Client, GetObjectOutput, + NotFound, NoSuchKey, __Client, } from "@aws-sdk/client-s3"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; -import { BucketDeleteOptions, IBucketClient, SignedUrlOptions } from "../cloud"; -import { Json } from "../std"; +import { + BucketDeleteOptions, + IBucketClient, + SignedUrlOptions, + ObjectMetadata, +} from "../cloud"; +import { Datetime, Json } from "../std"; + export class BucketClient implements IBucketClient { constructor( private readonly bucketName: string, @@ -298,6 +305,31 @@ export class BucketClient implements IBucketClient { } } + /** + * Get the metadata of an object in the bucket. + * @param key Key of the object. + */ + public async metadata(key: string): Promise { + const command = new HeadObjectCommand({ + Bucket: this.bucketName, + Key: key, + }); + try { + const resp = await this.s3Client.send(command); + return { + contentType: resp.ContentType, + lastModified: Datetime.fromIso(resp.LastModified!.toISOString()), + size: resp.ContentLength!, + }; + } catch (error) { + // 403 is thrown if s3:ListObject is not granted. + if (error instanceof NotFound || (error as Error).name === "403") { + throw new Error(`Object does not exist (key=${key}).`); + } + throw error; + } + } + private async getLocation(): Promise { const command = new GetBucketLocationCommand({ Bucket: this.bucketName, diff --git a/libs/wingsdk/src/shared-aws/permissions.ts b/libs/wingsdk/src/shared-aws/permissions.ts index 6a84f3df486..d5d149e9431 100644 --- a/libs/wingsdk/src/shared-aws/permissions.ts +++ b/libs/wingsdk/src/shared-aws/permissions.ts @@ -116,6 +116,7 @@ export function calculateBucketPermissions( if ( ops.includes(cloud.BucketInflightMethods.GET) || ops.includes(cloud.BucketInflightMethods.GET_JSON) || + ops.includes(cloud.BucketInflightMethods.METADATA) || ops.includes(cloud.BucketInflightMethods.LIST) || ops.includes(cloud.BucketInflightMethods.TRY_GET) || ops.includes(cloud.BucketInflightMethods.TRY_GET_JSON) || diff --git a/libs/wingsdk/src/target-sim/bucket.inflight.ts b/libs/wingsdk/src/target-sim/bucket.inflight.ts index 554a8733e69..c7162645dcd 100644 --- a/libs/wingsdk/src/target-sim/bucket.inflight.ts +++ b/libs/wingsdk/src/target-sim/bucket.inflight.ts @@ -10,12 +10,13 @@ import { IBucketClient, ITopicClient, SignedUrlOptions, + ObjectMetadata, } from "../cloud"; import { ISimulatorContext, ISimulatorResourceInstance, } from "../simulator/simulator"; -import { Json } from "../std"; +import { Datetime, Json } from "../std"; export class Bucket implements IBucketClient, ISimulatorResourceInstance { private readonly objectKeys: Set; @@ -221,6 +222,34 @@ export class Bucket implements IBucketClient, ISimulatorResourceInstance { }); } + /** + * Get the metadata of an object in the bucket. + * @param key Key of the object. + * @throws if the object does not exist. + */ + public async metadata(key: string): Promise { + return this.context.withTrace({ + message: `Metadata (key=${key}).`, + activity: async () => { + const hash = this.hashKey(key); + const filename = join(this._fileDir, hash); + try { + const filestat = await fs.promises.stat(filename, { + bigint: false, + }); + return { + size: filestat.size, + lastModified: Datetime.fromIso(filestat.mtime.toISOString()), + // fs does not provide a way to get the content-type + contentType: "application/octet-stream", + }; + } catch (e) { + throw new Error(`Object does not exist (key=${key}).`); + } + }, + }); + } + private async addFile(key: string, value: string): Promise { const actionType: BucketEventType = this.objectKeys.has(key) ? BucketEventType.UPDATE diff --git a/libs/wingsdk/src/target-tf-azure/bucket.inflight.ts b/libs/wingsdk/src/target-tf-azure/bucket.inflight.ts index afc725063a7..bed51b0784d 100644 --- a/libs/wingsdk/src/target-tf-azure/bucket.inflight.ts +++ b/libs/wingsdk/src/target-tf-azure/bucket.inflight.ts @@ -4,7 +4,12 @@ import { BlobServiceClient, ContainerClient, } from "@azure/storage-blob"; -import { BucketDeleteOptions, IBucketClient, SignedUrlOptions } from "../cloud"; +import { + BucketDeleteOptions, + IBucketClient, + SignedUrlOptions, + ObjectMetadata, +} from "../cloud"; import { Json } from "../std"; export class BucketClient implements IBucketClient { @@ -225,6 +230,15 @@ export class BucketClient implements IBucketClient { ); } + /** + * Get the metadata of an object in the bucket. + * @throws if the object does not exist. + * @param key Key of the object. + */ + public async metadata(key: string): Promise { + return Promise.reject(`metadata is not implemented: (key=${key})`); + } + /** * Required helper function for node js only. * diff --git a/libs/wingsdk/test/shared-aws/bucket.inflight.test.ts b/libs/wingsdk/test/shared-aws/bucket.inflight.test.ts index 2add2861feb..b8301769528 100644 --- a/libs/wingsdk/test/shared-aws/bucket.inflight.test.ts +++ b/libs/wingsdk/test/shared-aws/bucket.inflight.test.ts @@ -8,6 +8,7 @@ import { ListObjectsV2Command, PutObjectCommand, S3Client, + NotFound, NoSuchKey, } from "@aws-sdk/client-s3"; import * as s3RequestPresigner from "@aws-sdk/s3-request-presigner/dist-cjs/getSignedUrl"; @@ -16,6 +17,7 @@ import { sdkStreamMixin } from "@aws-sdk/util-stream-node"; import { mockClient } from "aws-sdk-client-mock"; import { test, expect, beforeEach, vi, Mock } from "vitest"; import { BucketClient } from "../../src/shared-aws/bucket.inflight"; +import { Datetime } from "../../src/std"; const s3Mock = mockClient(S3Client); @@ -590,3 +592,70 @@ test("Given a bucket, when giving one of its keys, we should get its signed url" expect(signedUrlFn).toBeCalledTimes(1); expect(signedUrl).toBe(VALUE); }); + +test("get metadata of an object", async () => { + // GIVEN + const BUCKET_NAME = "BUCKET_NAME"; + const KEY = "KEY"; + s3Mock.on(HeadObjectCommand, { Bucket: BUCKET_NAME, Key: KEY }).resolves({ + AcceptRanges: "bytes", + ContentLength: 3191, + ContentType: "image/jpeg", + ETag: "6805f2cfc46c0f04559748bb039d69ae", + LastModified: new Date("Thu, 15 Dec 2016 01:19:41 GMT"), + Metadata: {}, + VersionId: "null", + }); + + // WHEN + const client = new BucketClient(BUCKET_NAME); + const response = await client.metadata(KEY); + + // THEN + expect(response).toEqual({ + size: 3191, + lastModified: Datetime.fromIso("2016-12-15T01:19:41Z"), + contentType: "image/jpeg", + }); +}); + +test("metadata may not contains content-type if it is unknown", async () => { + // GIVEN + const BUCKET_NAME = "BUCKET_NAME"; + const KEY = "KEY"; + s3Mock.on(HeadObjectCommand, { Bucket: BUCKET_NAME, Key: KEY }).resolves({ + AcceptRanges: "bytes", + ContentLength: 1234, + ETag: "6805f2cfc46c0f04559748bb039d69ae", + LastModified: new Date("Thu, 15 Dec 2016 01:19:41 GMT"), + Metadata: {}, + VersionId: "null", + }); + + // WHEN + const client = new BucketClient(BUCKET_NAME); + const response = await client.metadata(KEY); + + // THEN + expect(response).toEqual({ + size: 1234, + lastModified: Datetime.fromIso("2016-12-15T01:19:41Z"), + }); +}); + +test("metadata fail on non-existent object", async () => { + // GIVEN + const BUCKET_NAME = "BUCKET_NAME"; + const KEY = "KEY"; + s3Mock + .on(HeadObjectCommand, { Bucket: BUCKET_NAME, Key: KEY }) + .rejects(new NotFound({ message: "NotFound error", $metadata: {} })); + + // WHEN + const client = new BucketClient(BUCKET_NAME); + + // THEN + await expect(() => client.metadata(KEY)).rejects.toThrowError( + "Object does not exist (key=KEY)." + ); +}); diff --git a/tools/hangar/__snapshots__/test_corpus/sdk_tests/bucket/metadata.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/sdk_tests/bucket/metadata.test.w_compile_tf-aws.md new file mode 100644 index 00000000000..397c3433cea --- /dev/null +++ b/tools/hangar/__snapshots__/test_corpus/sdk_tests/bucket/metadata.test.w_compile_tf-aws.md @@ -0,0 +1,129 @@ +# [metadata.test.w](../../../../../../examples/tests/sdk_tests/bucket/metadata.test.w) | compile | tf-aws + +## inflight.$Closure1-1.js +```js +module.exports = function({ $b }) { + class $Closure1 { + constructor({ }) { + const $obj = (...args) => this.handle(...args); + Object.setPrototypeOf($obj, this); + return $obj; + } + async handle() { + (await $b.put("test1.txt","Foo")); + {((cond) => {if (!cond) throw new Error("assertion failed: b.metadata(\"test1.txt\").size == 3")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })((await $b.metadata("test1.txt")).size,3)))}; + {((cond) => {if (!cond) throw new Error("assertion failed: b.metadata(\"test1.txt\").contentType == \"application/octet-stream\"")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })((await $b.metadata("test1.txt")).contentType,"application/octet-stream")))}; + {((cond) => {if (!cond) throw new Error("assertion failed: b.metadata(\"test1.txt\").lastModified.year >= 2023")})(((await $b.metadata("test1.txt")).lastModified.year >= 2023))}; + try { + (await $b.metadata("no-such-file.txt")).lastModified; + } + catch ($error_e) { + const e = $error_e.message; + {((cond) => {if (!cond) throw new Error("assertion failed: e == \"Object does not exist (key=no-such-file.txt).\"")})((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })(e,"Object does not exist (key=no-such-file.txt).")))}; + } + } + } + return $Closure1; +} + +``` + +## main.tf.json +```json +{ + "//": { + "metadata": { + "backend": "local", + "stackName": "root", + "version": "0.17.0" + }, + "outputs": { + "root": { + "Default": { + "cloud.TestRunner": { + "TestFunctionArns": "WING_TEST_RUNNER_FUNCTION_ARNS" + } + } + } + } + }, + "output": { + "WING_TEST_RUNNER_FUNCTION_ARNS": { + "value": "[]" + } + }, + "provider": { + "aws": [ + {} + ] + }, + "resource": { + "aws_s3_bucket": { + "cloudBucket": { + "//": { + "metadata": { + "path": "root/Default/Default/cloud.Bucket/Default", + "uniqueId": "cloudBucket" + } + }, + "bucket_prefix": "cloud-bucket-c87175e7-", + "force_destroy": false + } + } + } +} +``` + +## preflight.js +```js +const $stdlib = require('@winglang/sdk'); +const $plugins = ((s) => !s ? [] : s.split(';'))(process.env.WING_PLUGIN_PATHS); +const $outdir = process.env.WING_SYNTH_DIR ?? "."; +const $wing_is_test = process.env.WING_IS_TEST === "true"; +const std = $stdlib.std; +const cloud = $stdlib.cloud; +class $Root extends $stdlib.std.Resource { + constructor(scope, id) { + super(scope, id); + class $Closure1 extends $stdlib.std.Resource { + constructor(scope, id, ) { + super(scope, id); + (std.Node.of(this)).hidden = true; + } + static _toInflightType(context) { + return ` + require("./inflight.$Closure1-1.js")({ + $b: ${context._lift(b)}, + }) + `; + } + _toInflight() { + return ` + (await (async () => { + const $Closure1Client = ${$Closure1._toInflightType(this)}; + const client = new $Closure1Client({ + }); + if (client.$inflight_init) { await client.$inflight_init(); } + return client; + })()) + `; + } + _getInflightOps() { + return ["handle", "$inflight_init"]; + } + _registerBind(host, ops) { + if (ops.includes("handle")) { + $Closure1._registerBindObject(b, host, ["metadata", "put"]); + } + super._registerBind(host, ops); + } + } + const b = this.node.root.newAbstract("@winglang/sdk.cloud.Bucket",this,"cloud.Bucket"); + this.node.root.new("@winglang/sdk.std.Test",std.Test,this,"test:metadata",new $Closure1(this,"$Closure1")); + } +} +const $App = $stdlib.core.App.for(process.env.WING_TARGET); +new $App({ outdir: $outdir, name: "metadata.test", rootConstruct: $Root, plugins: $plugins, isTestEnvironment: $wing_is_test, entrypointDir: process.env['WING_SOURCE_DIR'], rootId: process.env['WING_ROOT_ID'] }).synth(); + +``` + diff --git a/tools/hangar/__snapshots__/test_corpus/sdk_tests/bucket/metadata.test.w_test_sim.md b/tools/hangar/__snapshots__/test_corpus/sdk_tests/bucket/metadata.test.w_test_sim.md new file mode 100644 index 00000000000..545db18c9b9 --- /dev/null +++ b/tools/hangar/__snapshots__/test_corpus/sdk_tests/bucket/metadata.test.w_test_sim.md @@ -0,0 +1,12 @@ +# [metadata.test.w](../../../../../../examples/tests/sdk_tests/bucket/metadata.test.w) | test | sim + +## stdout.log +```log +pass ─ metadata.test.wsim » root/env0/test:metadata + + +Tests 1 passed (1) +Test Files 1 passed (1) +Duration +``` +