@kensio/yulin 1.21.24 → 1.21.26

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 (49) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.js +1 -0
  3. package/dist/sdk/error/sim-sdk.error.d.ts +18 -0
  4. package/dist/sdk/error/sim-sdk.error.js +41 -0
  5. package/dist/sdk/index.d.ts +1 -1
  6. package/dist/sdk/index.js +1 -1
  7. package/dist/sdk/send-abort.d.ts +28 -0
  8. package/dist/sdk/send-abort.js +112 -0
  9. package/dist/sdk/send-patch.js +3 -1
  10. package/dist/service/aws/factory/sim-aws-account-region-service-builder.js +1 -0
  11. package/dist/service/aws/sim-aws-properties.d.ts +8 -0
  12. package/dist/service/aws/sim-aws.d.ts +5 -0
  13. package/dist/service/aws/sim-aws.js +7 -1
  14. package/dist/service/dynamodb/command/authorize/sim-dynamodb-authorizer.d.ts +19 -1
  15. package/dist/service/dynamodb/command/authorize/sim-dynamodb-authorizer.js +21 -3
  16. package/dist/service/dynamodb/command/authorize/sim-dynamodb-reached.d.ts +6 -3
  17. package/dist/service/dynamodb/command/query/sim-dynamodb-query.js +3 -1
  18. package/dist/service/dynamodb/command/scan/sim-dynamodb-scan.js +3 -1
  19. package/dist/service/dynamodb/command/table/sim-dynamodb-table-reach.d.ts +4 -0
  20. package/dist/service/dynamodb/command/table/sim-dynamodb-table-reach.js +1 -0
  21. package/dist/service/dynamodb/table/sim-dynamodb-table-arn.d.ts +13 -0
  22. package/dist/service/dynamodb/table/sim-dynamodb-table-arn.js +20 -0
  23. package/dist/service/scheduler/cfn/sim-cfn-schedule-properties.d.ts +0 -4
  24. package/dist/service/scheduler/cfn/sim-cfn-schedule-properties.js +2 -19
  25. package/dist/service/scheduler/cfn/sim-cfn-schedule-time-window.d.ts +10 -0
  26. package/dist/service/scheduler/cfn/sim-cfn-schedule-time-window.js +37 -0
  27. package/dist/service/scheduler/command/schedule/sim-scheduler-described-schedule.d.ts +1 -2
  28. package/dist/service/scheduler/command/schedule/sim-scheduler-described-schedule.js +2 -3
  29. package/dist/service/scheduler/command/schedule/sim-scheduler-schedule-writer.js +3 -0
  30. package/dist/service/scheduler/command/schedule/sim-scheduler-unsimulated-input.js +0 -25
  31. package/dist/service/scheduler/command/sim-scheduler-commands.d.ts +2 -0
  32. package/dist/service/scheduler/command/sim-scheduler-commands.js +1 -0
  33. package/dist/service/scheduler/schedule/sim-scheduler-pending-occurrences.d.ts +23 -0
  34. package/dist/service/scheduler/schedule/sim-scheduler-pending-occurrences.js +36 -0
  35. package/dist/service/scheduler/schedule/sim-scheduler-schedule.d.ts +7 -0
  36. package/dist/service/scheduler/schedule/sim-scheduler-schedule.js +5 -0
  37. package/dist/service/scheduler/schedule/sim-scheduler-schedules.d.ts +27 -1
  38. package/dist/service/scheduler/schedule/sim-scheduler-schedules.js +52 -8
  39. package/dist/service/scheduler/schedule/sim-scheduler-time-window.d.ts +33 -0
  40. package/dist/service/scheduler/schedule/sim-scheduler-time-window.js +93 -0
  41. package/dist/service/scheduler/sim-scheduler.d.ts +6 -0
  42. package/dist/service/scheduler/sim-scheduler.js +3 -1
  43. package/dist/util/random/sim-random.d.ts +35 -0
  44. package/dist/util/random/sim-random.js +33 -0
  45. package/docs/sdk/README.md +23 -0
  46. package/docs/services/dynamodb/README.md +26 -0
  47. package/docs/services/scheduler/README.md +68 -5
  48. package/docs/time/README.md +18 -0
  49. package/package.json +3 -3
@@ -172,6 +172,24 @@ specific commands:
172
172
  The list accepts command classes or command names. Sending another command throws
173
173
  `SimSdkCommandNotInterceptedError`.
174
174
 
175
+ ## Abort a send
176
+
177
+ An intercepted client honours the `abortSignal` option of `send`, as the SDK's own HTTP handler does:
178
+
179
+ `await client.send(command, { abortSignal: AbortSignal.timeout(5000) })`
180
+
181
+ A signal that has already fired rejects the send before it reaches the simulation. A signal that fires
182
+ while the simulated operation is pending rejects the send then. A Lambda handler sleeping on the
183
+ simulation's clock is one operation that stays pending.
184
+
185
+ Either way the send rejects with an `AbortError` shaped like the one the SDK throws. Its message is
186
+ `Request aborted` and its `cause` is the signal's reason (a `TimeoutError` for
187
+ `AbortSignal.timeout`). Match it by `error.name`.
188
+
189
+ A send carrying a signal waits one turn of the host's timers before it reaches the simulation. That
190
+ turn stands in for the request's time on the network. A timeout of 0 or 1 milliseconds set before the
191
+ send aborts it, as it would a request to AWS.
192
+
175
193
  ## Intercept the DynamoDB document client
176
194
 
177
195
  The DynamoDB document client accepts plain JavaScript values. Intercept the document client object,
@@ -282,6 +300,11 @@ service throws `SimSdkUnknownServiceError`.
282
300
  - Simulated errors have SDK-shaped `name` and `$metadata` fields. They are separate classes from the
283
301
  SDK exceptions, so match them by `error.name` instead of `instanceof`.
284
302
  - The callback form of `send(command, callback)` is not supported. Use the promise form.
303
+ - An aborted send stops waiting for its answer. The simulated operation it started still runs to the
304
+ end, as a request AWS has already received does.
305
+ - Most simulated operations answer within one turn of the host's timers. A timeout longer than a few
306
+ milliseconds only fires during an operation that waits, such as a sleeping Lambda handler. A
307
+ simulated Bedrock answer cannot be delayed.
285
308
  - Yulin reads the `marshallOptions` in
286
309
  `DynamoDBDocumentClient.from(client, { marshallOptions, unmarshallOptions })` and ignores the
287
310
  `unmarshallOptions`. A stored value comes back the way a document client built with no options of
@@ -1434,6 +1434,8 @@ from a different partition key is refused, since it names a collection this quer
1434
1434
  ## Reading a global secondary index
1435
1435
 
1436
1436
  Set `IndexName` on `Query` or `Scan` to read an index. Key conditions then use the index key schema.
1437
+ IAM authorizes the read against the index's own ARN, covered under
1438
+ [authorizing a read of an index](#authorizing-a-read-of-an-index).
1437
1439
 
1438
1440
  ```typescript sim-dynamodb-query-index
1439
1441
  /**
@@ -3716,6 +3718,26 @@ unauthorized caller cannot find out which names are taken.
3716
3718
  in the same way, each against the `dynamodb:` action of its own name. `ListTables` names no table.
3717
3719
  It authorizes against `*`.
3718
3720
 
3721
+ ### Authorizing a read of an index
3722
+
3723
+ A `Query` or `Scan` of the table authorizes against the table ARN. One naming an `IndexName`
3724
+ authorizes against the index's own ARN, the table ARN followed by `/index/` and the index name:
3725
+
3726
+ ```text
3727
+ arn:aws:dynamodb:eu-west-2:111111111111:table/OrdersTable/index/byStatus
3728
+ ```
3729
+
3730
+ Global and local secondary indexes work the same way. A statement allowing `dynamodb:Query` on the
3731
+ table ARN alone refuses a query of any index, with an `AccessDenied` naming the index ARN. A
3732
+ statement allowing only an index ARN refuses a query of the table. A caller that reads both needs
3733
+ the table ARN and `<table ARN>/index/*` (or each index ARN) in its policy.
3734
+
3735
+ CDK's `grantReadData` and `grantReadWriteData` add `<table ARN>/index/*` only for a table construct
3736
+ that declares its indexes. A table imported with `Table.fromTableName` declares none, so its grant
3737
+ covers the table ARN alone, and a query of an index fails here as it fails on AWS. Import the table
3738
+ with `Table.fromTableAttributes` and its `globalIndexes` or `localIndexes` to have the grant cover
3739
+ them.
3740
+
3719
3741
  ### Fine-grained access control with dynamodb:LeadingKeys
3720
3742
 
3721
3743
  `GetItem`, `BatchGetItem`, `Query`, `PutItem`, `UpdateItem`, `DeleteItem` and `BatchWriteItem`
@@ -3723,6 +3745,8 @@ supply the partition key values they reach as `dynamodb:LeadingKeys`. A policy c
3723
3745
  key restricts a caller to the items under particular partition keys. AWS scopes one user of a
3724
3746
  shared table to their own rows this way.
3725
3747
 
3748
+ A `Query` of an index supplies the index's partition key value, the value its key condition names.
3749
+
3726
3750
  The condition takes the `ForAllValues:` qualifier, as AWS requires for this key. Every partition key
3727
3751
  value the request reaches has to match. A batch or a write naming one item outside the allowed set
3728
3752
  is refused whole, and nothing it asked for is applied.
@@ -3847,6 +3871,8 @@ is written.
3847
3871
  answering with the attributes it projects, and paging with a `LastEvaluatedKey` carrying the index
3848
3872
  key and the table key together. A local secondary index also answers a strongly consistent read,
3849
3873
  and fetches an unprojected attribute from the base table.
3874
+ - `Query` and `Scan` of an index authorized against the index ARN, for a global and a local
3875
+ secondary index alike.
3850
3876
  - `dynamodb:LeadingKeys` on `GetItem`, `BatchGetItem`, `Query`, `PutItem`, `UpdateItem`,
3851
3877
  `DeleteItem` and `BatchWriteItem`, carrying the partition key values the request reaches so a
3852
3878
  `ForAllValues:` condition can scope a caller to particular items.
@@ -303,6 +303,64 @@ regardless of `ActionAfterCompletion`.
303
303
  enabling it picks up from the next due instant. What it missed is never replayed. An update that
304
304
  changes the expression reschedules from the new one.
305
305
 
306
+ ### Flexible time windows
307
+
308
+ `FlexibleTimeWindow: { Mode: "FLEXIBLE", MaximumWindowInMinutes: n }` delays each invocation by up
309
+ to `n` minutes. The schedule still falls due at the times its expression gives. Each occurrence then
310
+ invokes its target at a moment drawn inside `[due, due + n minutes)`. The moment is never before the
311
+ due time and always before the window closes.
312
+
313
+ The draw is made separately for every occurrence. A test that advances the clock past the end of an
314
+ occurrence's window sees exactly one invocation for it, whatever was drawn. A test that stops inside
315
+ the window may or may not see it. Assert after the window has closed.
316
+
317
+ The draws come from the `random` source the `SimAws` was built with. It defaults to the host's
318
+ `Math.random`, and the moments differ from run to run. Pass a `SimSeededRandom` to draw the same
319
+ moments every time (to reproduce a failure, for example).
320
+
321
+ ```typescript sim-scheduler-flexible-window
322
+ /**
323
+ * A schedule invoking its target up to fifteen minutes after each hour.
324
+ */
325
+
326
+ import {
327
+ CreateScheduleCommand,
328
+ GetScheduleCommand,
329
+ } from "@aws-sdk/client-scheduler";
330
+
331
+ import { SimAws, SimSeededRandom } from "@kensio/yulin";
332
+
333
+ // The same seed draws the same invocation moments on every run.
334
+ const simAws = new SimAws({ random: new SimSeededRandom(2026) });
335
+
336
+ await simAws.scheduler().createSchedule(
337
+ new CreateScheduleCommand({
338
+ Name: "hourly-digest",
339
+ ScheduleExpression: "cron(0 * * * ? *)",
340
+ FlexibleTimeWindow: { Mode: "FLEXIBLE", MaximumWindowInMinutes: 15 },
341
+ Target: {
342
+ Arn: "arn:aws:lambda:us-east-1:888888888888:function:digest",
343
+ RoleArn: "arn:aws:iam::888888888888:role/SchedulerRole",
344
+ },
345
+ }),
346
+ );
347
+
348
+ const described = await simAws
349
+ .scheduler()
350
+ .getSchedule(new GetScheduleCommand({ Name: "hourly-digest" }));
351
+
352
+ console.log(described.FlexibleTimeWindow);
353
+ // { Mode: "FLEXIBLE", MaximumWindowInMinutes: 15 }
354
+ ```
355
+
356
+ A `FLEXIBLE` window needs `MaximumWindowInMinutes`, a whole number from 1 to 1440. An `OFF` window
357
+ must leave it out. Either mistake is a `ValidationException`. `GetSchedule` reports the window the
358
+ schedule was created with.
359
+
360
+ A schedule deleted or replaced while an occurrence is waiting inside its window does not invoke for
361
+ that occurrence. A one-time schedule with `ActionAfterCompletion: "DELETE"` is removed once its
362
+ delayed invocation has been made.
363
+
306
364
  ## Running an ECS task on a schedule
307
365
 
308
366
  A target whose ARN names an ECS cluster runs a [simulated ECS](https://yulinsim.dev/services/ecs/)
@@ -812,6 +870,8 @@ Resources of the same stack.
812
870
  authorized against that role's own policies. Lambda targets use asynchronous Event invocation.
813
871
  - ECS targets, running a simulated task as the execution role, with the task definition and
814
872
  `TaskCount` from `EcsParameters` and container overrides from the target's `Input`.
873
+ - `FlexibleTimeWindow` in both modes, with each `FLEXIBLE` invocation drawn inside its window from
874
+ the simulation's `random` source.
815
875
  - `ActionAfterCompletion`, and `deliveryFailures` for invocations that did not happen.
816
876
  - Target retry policies driven by simulated time, and standard SQS dead-letter queues carrying the
817
877
  original input and Scheduler diagnostic attributes.
@@ -829,8 +889,8 @@ Resources of the same stack.
829
889
 
830
890
  - A schedule only fires while a test advances the simulation's clock. The host's clock drives none
831
891
  of it, and a simulation left alone in real time never fires however long it is left.
832
- - Firing is exact and exactly once. Real Scheduler invokes within a minute of the due time, and its
833
- promise is at-least-once.
892
+ - Firing is exact and exactly once with the window `OFF`. Real Scheduler invokes within a minute of
893
+ the due time, and its promise is at-least-once.
834
894
  - Retry delays are deterministic powers of two seconds. Real Scheduler uses exponential backoff
835
895
  without publishing an exact sequence. The fixed sequence lets a test advance to a known retry
836
896
  instant.
@@ -844,9 +904,12 @@ Resources of the same stack.
844
904
  real Scheduler holds the group in `DELETING` until they have gone.
845
905
  - The `default` schedule group cannot be deleted. AWS leaves the answer to that request
846
906
  undocumented.
847
- - `FlexibleTimeWindow` with `Mode: "FLEXIBLE"` is refused. Real Scheduler invokes the target at an
848
- unpredictable moment inside the window, and firing at the exact due time instead would let a test
849
- rely on timing AWS leaves unpromised.
907
+ - A flexible window draws each invocation moment uniformly, to the millisecond. AWS documents only
908
+ that the target is invoked inside the window after the scheduled time, and publishes no
909
+ distribution.
910
+ - The wording of the refusals for a `FLEXIBLE` window with no `MaximumWindowInMinutes`, and an `OFF`
911
+ window with one, is Yulin's own. AWS refuses both and does not document the message. The range
912
+ refusals use the API's constraint wording.
850
913
  - `ScheduleExpressionTimezone` is read from the host's own timezone data through `Intl`, so a zone
851
914
  the host has never heard of is refused even where AWS would take it. A schedule that names none
852
915
  runs in UTC.
@@ -247,6 +247,24 @@ Yulin uses the clock for:
247
247
  - `Date`, global timers, and invocation deadlines inside a simulated Lambda invocation
248
248
  - HTTP response `Date` headers
249
249
 
250
+ ## Values AWS leaves to chance
251
+
252
+ Some AWS behaviour is unpredictable by design. An EventBridge Scheduler
253
+ [flexible time window](https://yulinsim.dev/services/scheduler/#flexible-time-windows) invokes its
254
+ target at a moment AWS chooses inside the window. Yulin draws those values from the `random` option
255
+ of `SimAws`, which defaults to the host's `Math.random`.
256
+
257
+ Pass a `SimSeededRandom` to draw the same values on every run:
258
+
259
+ ```typescript
260
+ import { SimAws, SimSeededRandom } from "@kensio/yulin";
261
+
262
+ const simAws = new SimAws({ random: new SimSeededRandom(2026) });
263
+ ```
264
+
265
+ A test that advances the clock past the end of a window passes whatever was drawn. A seed is for
266
+ reproducing a run (one that failed, for example).
267
+
250
268
  ## Limitations
251
269
 
252
270
  - Scheduled [EventBridge rules](https://yulinsim.dev/services/eventbridge/#rules-that-fire-on-a-schedule)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kensio/yulin",
3
- "version": "1.21.24",
3
+ "version": "1.21.26",
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,13 +293,13 @@
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": "^73.0.0",
296
+ "eslint-plugin-unicorn": "^74.0.0",
297
297
  "execa": "^10.0.0",
298
298
  "fta-cli": "^3.0.0",
299
299
  "hyparquet": "^1.29.2",
300
300
  "hyparquet-writer": "^0.16.9",
301
301
  "node-sql-parser": "^5.4.0",
302
- "oxfmt": "^0.70.0",
302
+ "oxfmt": "^0.71.0",
303
303
  "oxlint": "^1.77.0",
304
304
  "oxlint-tsgolint": "^7.0.2001",
305
305
  "semantic-release": "25.0.9",