@kensio/yulin 1.21.30 → 1.21.32

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.
Files changed (67) hide show
  1. package/dist/serve/message/sim-message-log-console.js +3 -3
  2. package/dist/service/apigateway/api/deployment/sim-rest-api-deployment.js +3 -3
  3. package/dist/service/apigateway/api/method/sim-rest-api-method.js +12 -12
  4. package/dist/service/apigateway/api/resource/sim-rest-api-resource.js +11 -9
  5. package/dist/service/apigateway/api/sim-rest-api-view.js +3 -3
  6. package/dist/service/apigateway/api/stage/sim-rest-api-stage.js +8 -10
  7. package/dist/service/apigatewayv2/api/integration/sim-http-api-integration.js +3 -3
  8. package/dist/service/apigatewayv2/api/route/sim-http-api-route-view.js +6 -6
  9. package/dist/service/apigatewayv2/api/sim-http-api-view.js +3 -3
  10. package/dist/service/apigatewayv2/api/stage/settings/sim-http-api-stage-route-settings.js +8 -7
  11. package/dist/service/apigatewayv2/api/stage/sim-http-api-stage.js +9 -9
  12. package/dist/service/athena/engine/sim-athena-partitioned-objects.js +7 -7
  13. package/dist/service/athena/sim-athena-engine.fixture.js +3 -0
  14. package/dist/service/athena/table/sim-athena-partition-filters.js +6 -5
  15. package/dist/service/backup/command/sim-backup-authorizer.js +5 -4
  16. package/dist/service/bedrock/response/sim-bedrock-declared-content.js +5 -10
  17. package/dist/service/cloudformation/stack/update/sim-cfn-stack-resource-changes.js +19 -10
  18. package/dist/service/cloudfront/cfn/response-headers-policy/sim-cfn-cf-rh-policy-security-values.js +2 -6
  19. package/dist/service/cognito/user-pool/trigger/sim-cognito-pre-sign-up-response.js +4 -7
  20. package/dist/service/dynamodb/command/transact/sim-dynamodb-transact-write.js +34 -25
  21. package/dist/service/dynamodb/secondary-index/sim-dynamodb-index-limits.js +5 -4
  22. package/dist/service/dynamodb/stream/sim-dynamodb-stream-shard-iterator.js +1 -3
  23. package/dist/service/dynamodb/time-to-live/sim-dynamodb-time-to-live.js +4 -3
  24. package/dist/service/iam/authorize/context/sim-iam-auth-z-allow-requirement.js +6 -6
  25. package/dist/service/kinesis/stream/sim-kinesis-shard-iterator.js +1 -3
  26. package/dist/service/lambda/destination/sim-lambda-destination-record.js +7 -7
  27. package/dist/service/lambda/function/outbound/sim-lambda-aws-api-outbound.d.ts +7 -1
  28. package/dist/service/lambda/function/outbound/sim-lambda-aws-api-outbound.js +15 -7
  29. package/dist/service/lambda/function/outbound/sim-lambda-http2-session.d.ts +3 -0
  30. package/dist/service/lambda/function/outbound/sim-lambda-http2-session.js +4 -0
  31. package/dist/service/lambda/function/policy/sim-lambda-permission.js +15 -14
  32. package/dist/service/lambda/metric/sim-lambda-function-metrics.js +11 -9
  33. package/dist/service/logs/metric/sim-logs-metric-filter.js +1 -8
  34. package/dist/service/route53/dns/answer/sim-route53-dns-zone-finder.js +5 -4
  35. package/dist/service/route53/resolve/sim-r53-zone-record-finder.js +8 -6
  36. package/dist/service/s3/bucket/notification/sim-s3-notification-filter.js +4 -7
  37. package/dist/service/s3/bucket/sim-s3-bucket.d.ts +8 -0
  38. package/dist/service/s3/bucket/sim-s3-bucket.js +11 -0
  39. package/dist/service/s3/bucket/sim-s3-key-writes.d.ts +16 -0
  40. package/dist/service/s3/bucket/sim-s3-key-writes.js +43 -0
  41. package/dist/service/s3/bucket/versioning/sim-s3-bucket-versioning.js +1 -3
  42. package/dist/service/s3/command/complete-multipart-upload/complete-multipart-upload.command.d.ts +2 -1
  43. package/dist/service/s3/command/complete-multipart-upload/complete-multipart-upload.handler.js +5 -1
  44. package/dist/service/s3/command/copy-object/copy-object-precondition.d.ts +23 -0
  45. package/dist/service/s3/command/copy-object/copy-object-precondition.js +34 -0
  46. package/dist/service/s3/command/copy-object/copy-object.command.d.ts +3 -1
  47. package/dist/service/s3/command/copy-object/copy-object.handler.d.ts +4 -0
  48. package/dist/service/s3/command/copy-object/copy-object.handler.js +11 -1
  49. package/dist/service/s3/command/object/sim-s3-write-precondition.d.ts +34 -0
  50. package/dist/service/s3/command/object/sim-s3-write-precondition.js +47 -0
  51. package/dist/service/s3/command/put-object/put-object.command.d.ts +2 -1
  52. package/dist/service/s3/command/put-object/put-object.handler.js +5 -1
  53. package/dist/service/s3/error/sim-s3.error.d.ts +10 -0
  54. package/dist/service/s3/error/sim-s3.error.js +12 -0
  55. package/dist/service/s3/object/s3-object-listing.js +4 -3
  56. package/dist/service/s3/serve/api/sim-s3-api-input.d.ts +4 -0
  57. package/dist/service/s3/serve/api/sim-s3-api-input.js +10 -0
  58. package/dist/service/s3/serve/api/sim-s3-api-multipart-input.js +2 -0
  59. package/dist/service/s3/serve/api/sim-s3-api-object-input.d.ts +6 -0
  60. package/dist/service/s3/serve/api/sim-s3-api-object-input.js +34 -2
  61. package/dist/service/s3/storage/filesystem/s3-filesystem-object-keys.js +9 -8
  62. package/dist/service/secretsmanager/cfn/secret/sim-cfn-secrets-manager-secret-template.test-support.js +15 -23
  63. package/dist/service/sqs/message/sim-sqs-message.js +3 -3
  64. package/dist/service/stepfunctions/machine/sim-state-machine.js +4 -3
  65. package/dist/util/schedule/cron/sim-cron-field-bounds.js +1 -1
  66. package/docs/services/s3/README.md +68 -3
  67. package/package.json +2 -2
@@ -61,6 +61,16 @@ export function listObjectsInput(request) {
61
61
  export function optional(name, value) {
62
62
  return value === null ? {} : { [name]: value };
63
63
  }
64
+ /**
65
+ * The date form of the same, dropping a value that is not a date.
66
+ */
67
+ export function optionalDate(name, value) {
68
+ if (value === null) {
69
+ return {};
70
+ }
71
+ const parsed = new Date(value);
72
+ return Number.isNaN(parsed.getTime()) ? {} : { [name]: parsed };
73
+ }
64
74
  /**
65
75
  * The numeric form of the same, dropping a value that is not a number.
66
76
  */
@@ -1,4 +1,5 @@
1
1
  import { optional, optionalNumber } from "./sim-s3-api-input.js";
2
+ import { writePreconditionInput } from "./sim-s3-api-object-input.js";
2
3
  import { readSimS3CompletedUpload } from "./sim-s3-api-xml-read.js";
3
4
  /**
4
5
  * Reading the multipart upload operations' inputs out of a request.
@@ -33,6 +34,7 @@ export function uploadPartInput(request) {
33
34
  export function completeMultipartUploadInput(request) {
34
35
  return {
35
36
  ...uploadInput(request),
37
+ ...writePreconditionInput(request),
36
38
  MultipartUpload: readSimS3CompletedUpload(request.body),
37
39
  };
38
40
  }
@@ -35,6 +35,12 @@ export declare function getObjectInput(request: SimS3ApiRequest): object;
35
35
  * them.
36
36
  */
37
37
  export declare function putObjectInput(request: SimS3ApiRequest): object;
38
+ /**
39
+ * The conditions a write places on the Object already under its key, which
40
+ * PutObject, CopyObject and CompleteMultipartUpload all read from the same two
41
+ * headers.
42
+ */
43
+ export declare function writePreconditionInput(request: SimS3ApiRequest): object;
38
44
  /**
39
45
  * The input of a copy, which names its source in a header and carries no bytes
40
46
  * of its own.
@@ -1,5 +1,5 @@
1
1
  import { simS3WriteMetadataHeaders } from "../../object/s3-write-metadata.js";
2
- import { optional, utf8 } from "./sim-s3-api-input.js";
2
+ import { optional, optionalDate, utf8 } from "./sim-s3-api-input.js";
3
3
  import { readSimS3Tagging } from "./sim-s3-api-tagging-read.js";
4
4
  /**
5
5
  * Reading the Object operations' inputs out of a request.
@@ -54,7 +54,22 @@ export function getObjectInput(request) {
54
54
  * them.
55
55
  */
56
56
  export function putObjectInput(request) {
57
- return { ...createMultipartUploadInput(request), Body: request.body };
57
+ return {
58
+ ...createMultipartUploadInput(request),
59
+ ...writePreconditionInput(request),
60
+ Body: request.body,
61
+ };
62
+ }
63
+ /**
64
+ * The conditions a write places on the Object already under its key, which
65
+ * PutObject, CopyObject and CompleteMultipartUpload all read from the same two
66
+ * headers.
67
+ */
68
+ export function writePreconditionInput(request) {
69
+ return {
70
+ ...optional("IfMatch", request.headers.get("if-match")),
71
+ ...optional("IfNoneMatch", request.headers.get("if-none-match")),
72
+ };
58
73
  }
59
74
  /**
60
75
  * The input of a copy, which names its source in a header and carries no bytes
@@ -67,11 +82,28 @@ export function putObjectInput(request) {
67
82
  export function copyObjectInput(request) {
68
83
  return {
69
84
  ...createMultipartUploadInput(request),
85
+ ...writePreconditionInput(request),
86
+ ...copySourcePreconditionInput(request),
70
87
  ...optional("CopySource", request.headers.get(simS3CopySourceHeader)),
71
88
  ...optional("MetadataDirective", request.headers.get("x-amz-metadata-directive")),
72
89
  ...optional("TaggingDirective", request.headers.get("x-amz-tagging-directive")),
73
90
  };
74
91
  }
92
+ /**
93
+ * The conditions a copy places on its source Object.
94
+ *
95
+ * The two date headers are HTTP dates, which the SDK hands the operation as
96
+ * `Date` values.
97
+ */
98
+ function copySourcePreconditionInput(request) {
99
+ const header = (name) => request.headers.get(`x-amz-copy-source-if-${name}`);
100
+ return {
101
+ ...optional("CopySourceIfMatch", header("match")),
102
+ ...optional("CopySourceIfNoneMatch", header("none-match")),
103
+ ...optionalDate("CopySourceIfModifiedSince", header("modified-since")),
104
+ ...optionalDate("CopySourceIfUnmodifiedSince", header("unmodified-since")),
105
+ };
106
+ }
75
107
  /**
76
108
  * The input of a request that describes an Object without carrying its bytes,
77
109
  * which is what starting a multipart upload is.
@@ -60,14 +60,15 @@ export class FilesystemS3ObjectKeys {
60
60
  nestedKeyPromises.push(this.listInDirectory(entryPath));
61
61
  continue;
62
62
  }
63
- if (entry.isFile()) {
64
- const key = path
65
- .relative(this.directoryPath, entryPath)
66
- .split(path.sep)
67
- .join("/");
68
- if (this.safety.isAllowedObjectKeyExtension(key)) {
69
- keys.push(key);
70
- }
63
+ if (!entry.isFile()) {
64
+ continue;
65
+ }
66
+ const key = path
67
+ .relative(this.directoryPath, entryPath)
68
+ .split(path.sep)
69
+ .join("/");
70
+ if (this.safety.isAllowedObjectKeyExtension(key)) {
71
+ keys.push(key);
71
72
  }
72
73
  }
73
74
  const nestedKeys = await Promise.all(nestedKeyPromises);
@@ -18,29 +18,21 @@ export function secretTemplate(properties = {}) {
18
18
  }
19
19
  function secretProperties(properties) {
20
20
  const { name, description, kmsKeyId, tags, secretString } = properties;
21
- const declared = {};
22
- if (name !== undefined) {
23
- declared["Name"] = name;
24
- }
25
- if (description !== undefined) {
26
- declared["Description"] = description;
27
- }
28
- if (kmsKeyId !== undefined) {
29
- declared["KmsKeyId"] = kmsKeyId;
30
- }
31
- if (tags !== undefined) {
32
- declared["Tags"] = [...tags];
33
- }
34
- if (secretString === undefined) {
35
- declared["GenerateSecretString"] = {
36
- SecretStringTemplate: "{}",
37
- GenerateStringKey: "current",
38
- PasswordLength: properties.passwordLength ?? 64,
39
- };
40
- }
41
- else {
42
- declared["SecretString"] = secretString;
43
- }
21
+ const declared = {
22
+ ...(name !== undefined && { Name: name }),
23
+ ...(description !== undefined && { Description: description }),
24
+ ...(kmsKeyId !== undefined && { KmsKeyId: kmsKeyId }),
25
+ ...(tags !== undefined && { Tags: [...tags] }),
26
+ ...(secretString === undefined
27
+ ? {
28
+ GenerateSecretString: {
29
+ SecretStringTemplate: "{}",
30
+ GenerateStringKey: "current",
31
+ PasswordLength: properties.passwordLength ?? 64,
32
+ },
33
+ }
34
+ : { SecretString: secretString }),
35
+ };
44
36
  return declared;
45
37
  }
46
38
  function readerResource() {
@@ -126,10 +126,10 @@ export class SimSqsMessage {
126
126
  "ApproximateFirstReceiveTimestamp",
127
127
  String(this.visibility.firstReceived.getTime()),
128
128
  ],
129
+ ...(this.deadLetterSourceArn === undefined
130
+ ? []
131
+ : [["DeadLetterQueueSourceArn", this.deadLetterSourceArn]]),
129
132
  ]);
130
- if (this.deadLetterSourceArn !== undefined) {
131
- values.set("DeadLetterQueueSourceArn", this.deadLetterSourceArn);
132
- }
133
133
  return values;
134
134
  }
135
135
  }
@@ -56,9 +56,10 @@ export class SimStateMachine {
56
56
  */
57
57
  update(changes) {
58
58
  this.#roleArn = changes.roleArn ?? this.#roleArn;
59
- if (changes.definition !== undefined && changes.parsed !== undefined) {
60
- this.#definition = changes.definition;
61
- this.#parsed = changes.parsed;
59
+ if (changes.definition === undefined || changes.parsed === undefined) {
60
+ return;
62
61
  }
62
+ this.#definition = changes.definition;
63
+ this.#parsed = changes.parsed;
63
64
  }
64
65
  }
@@ -58,5 +58,5 @@ export function boundsOf(spec, written, stepped) {
58
58
  ];
59
59
  }
60
60
  const only = valueOf(spec, written);
61
- return stepped ? [only, spec.maximum] : [only, only];
61
+ return [only, stepped ? spec.maximum : only];
62
62
  }
@@ -531,6 +531,74 @@ reports it there.
531
531
  An Object uploaded in parts gets a different form. See
532
532
  [Uploading an Object in parts](#uploading-an-object-in-parts).
533
533
 
534
+ ## Conditional writes
535
+
536
+ `PutObject`, `CopyObject` and `CompleteMultipartUpload` take `IfNoneMatch` and `IfMatch`, which
537
+ state what the key has to hold before the write goes ahead. `IfNoneMatch: "*"` writes only to a key
538
+ that holds nothing. `IfMatch` writes only over an Object with that ETag. A write whose condition
539
+ fails is refused with `PreconditionFailed` (status `412`), and the Object already under the key is
540
+ left as it was.
541
+
542
+ ```typescript sim-s3-conditional-write
543
+ /**
544
+ * Writing a simulated S3 Object only where the key is free.
545
+ */
546
+
547
+ import {
548
+ CopyObjectCommand,
549
+ CreateBucketCommand,
550
+ PutObjectCommand,
551
+ } from "@aws-sdk/client-s3";
552
+ import { SimAws } from "@kensio/yulin";
553
+
554
+ const simAws = new SimAws();
555
+ const simS3 = simAws.s3();
556
+
557
+ await simS3.createBucket(new CreateBucketCommand({ Bucket: "audio-bucket" }));
558
+
559
+ await simS3.putObject(
560
+ new PutObjectCommand({
561
+ Bucket: "audio-bucket",
562
+ Key: "uploads/take-1.mp3",
563
+ Body: "first take",
564
+ }),
565
+ );
566
+
567
+ for (let attempt = 0; attempt < 2; attempt++) {
568
+ try {
569
+ await simS3.copyObject(
570
+ new CopyObjectCommand({
571
+ Bucket: "audio-bucket",
572
+ Key: "shared/take.mp3",
573
+ CopySource: "audio-bucket/uploads/take-1.mp3",
574
+ IfNoneMatch: "*",
575
+ }),
576
+ );
577
+ console.log("copied");
578
+ } catch (error) {
579
+ // The second attempt finds the shared key taken.
580
+ if (error instanceof Error && error.name === "PreconditionFailed") {
581
+ console.log("already there");
582
+ } else {
583
+ throw error;
584
+ }
585
+ }
586
+ }
587
+ ```
588
+
589
+ Writes to one key run one at a time. Of several conditional writes racing for an empty key, the first
590
+ to finish succeeds and the rest are refused. `IfMatch` on a key holding nothing answers `NoSuchKey`. On a versioned Bucket both conditions look at
591
+ the current version, and a delete marker counts as nothing. `IfNoneMatch` on a write accepts only
592
+ `*`, and real S3 refuses anything else with `NotImplemented`.
593
+
594
+ A copy also takes conditions on its source. `CopySourceIfMatch`, `CopySourceIfNoneMatch`,
595
+ `CopySourceIfModifiedSince` and `CopySourceIfUnmodifiedSince` refuse the copy with
596
+ `PreconditionFailed` when the source does not meet them. An ETag condition outranks the date
597
+ condition paired with it, as RFC 7232 has it. The served endpoint reads all of these from the same
598
+ headers real S3 does.
599
+
600
+ A failed completion leaves the upload in place, and it can be completed again.
601
+
534
602
  ## Uploading an Object in parts
535
603
 
536
604
  Yulin supports multipart upload through the SDK and served S3 endpoint. This covers the operations
@@ -834,9 +902,6 @@ See [Serve simulated S3 on localhost](#serve-simulated-s3-on-localhost) for sett
834
902
  - Both Buckets have to belong to the same simulated S3. A copy across Accounts or Regions is left
835
903
  out.
836
904
  - A presigned copy is left out, and so is a copy reaching a Bucket through simulated CloudFront.
837
- - `CopySourceIfMatch`, `CopySourceIfNoneMatch`, `CopySourceIfModifiedSince` and
838
- `CopySourceIfUnmodifiedSince` are ignored. A conditional copy happens whatever the condition
839
- says.
840
905
  - `ACL` is ignored. Sim S3 models no Object ACL.
841
906
  - A `versionId` in `CopySource` is refused with `NotImplemented`.
842
907
  - A copy of an Object that was uploaded in parts gets a plain ETag rather than the multipart form.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kensio/yulin",
3
- "version": "1.21.30",
3
+ "version": "1.21.32",
4
4
  "description": "AWS system behaviour simulation for isolated unit testing",
5
5
  "repository": "https://github.com/KensioSoftware/yulin",
6
6
  "homepage": "https://yulinsim.dev/",
@@ -293,7 +293,7 @@
293
293
  "eslint-plugin-jsdoc": "^64.0.1",
294
294
  "eslint-plugin-no-secrets": "^2.3.3",
295
295
  "eslint-plugin-security": "^4.0.1",
296
- "eslint-plugin-unicorn": "^74.0.0",
296
+ "eslint-plugin-unicorn": "^75.0.0",
297
297
  "execa": "^10.0.0",
298
298
  "fta-cli": "^3.0.0",
299
299
  "hyparquet": "^1.29.2",