@kensio/yulin 1.21.21 → 1.21.23

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 (85) hide show
  1. package/dist/service/athena/engine/sim-athena-ast-nodes.d.ts +11 -0
  2. package/dist/service/athena/engine/sim-athena-ast-nodes.js +22 -0
  3. package/dist/service/athena/engine/sim-athena-engine-result.d.ts +9 -1
  4. package/dist/service/athena/engine/sim-athena-engine-result.js +15 -4
  5. package/dist/service/athena/engine/sim-athena-engine-run.d.ts +2 -0
  6. package/dist/service/athena/engine/sim-athena-engine-run.js +6 -1
  7. package/dist/service/athena/engine/sim-athena-query-engine.js +1 -0
  8. package/dist/service/athena/engine/sim-athena-result-columns.d.ts +1 -1
  9. package/dist/service/athena/engine/sim-athena-result-columns.js +11 -4
  10. package/dist/service/athena/engine/sim-athena-round-scales.d.ts +19 -0
  11. package/dist/service/athena/engine/sim-athena-round-scales.js +80 -0
  12. package/dist/service/athena/engine/sim-athena-sql-translation.d.ts +9 -0
  13. package/dist/service/athena/engine/sim-athena-sql-translation.js +17 -2
  14. package/dist/service/athena/engine/sim-athena-string-shims.js +29 -1
  15. package/dist/service/athena/engine/sim-athena-unnest-item.js +6 -15
  16. package/dist/service/athena/engine/sim-athena-unnest-rewrite.d.ts +4 -4
  17. package/dist/service/athena/engine/sim-athena-unnest-rewrite.js +4 -4
  18. package/dist/service/athena/engine/sim-athena-unnest-source.d.ts +10 -9
  19. package/dist/service/athena/engine/sim-athena-unnest-source.js +25 -11
  20. package/dist/service/athena/sim-athena-flattened-events.fixture.d.ts +3 -0
  21. package/dist/service/athena/sim-athena-flattened-events.fixture.js +46 -0
  22. package/dist/service/lambda/cfn/event-source-mapping/sim-cfn-lambda-event-source-mapping-properties.js +2 -0
  23. package/dist/service/lambda/cfn/event-source-mapping/sim-cfn-lambda-event-source-mapping-property-names.js +1 -5
  24. package/dist/service/lambda/command/event-source-mapping/create-event-source-mapping-input.d.ts +5 -0
  25. package/dist/service/lambda/command/event-source-mapping/create-event-source-mapping-input.js +7 -0
  26. package/dist/service/lambda/command/event-source-mapping/create-event-source-mapping-refusals.js +0 -1
  27. package/dist/service/lambda/command/event-source-mapping/create-event-source-mapping.handler.js +1 -0
  28. package/dist/service/lambda/command/event-source-mapping/event-source-mapping.command.d.ts +2 -1
  29. package/dist/service/lambda/command/event-source-mapping/event-source-mapping.command.js +0 -6
  30. package/dist/service/lambda/event-source/filter/sim-lambda-filter-criteria.d.ts +48 -0
  31. package/dist/service/lambda/event-source/filter/sim-lambda-filter-criteria.js +129 -0
  32. package/dist/service/lambda/event-source/filter/sim-lambda-filtered-delivery.d.ts +27 -0
  33. package/dist/service/lambda/event-source/filter/sim-lambda-filtered-delivery.js +15 -0
  34. package/dist/service/lambda/event-source/filter/sim-lambda-filtered-records.d.ts +24 -0
  35. package/dist/service/lambda/event-source/filter/sim-lambda-filtered-records.js +35 -0
  36. package/dist/service/lambda/event-source/poll/kinesis/sim-lambda-kinesis-shard-poller.d.ts +7 -4
  37. package/dist/service/lambda/event-source/poll/kinesis/sim-lambda-kinesis-shard-poller.js +9 -6
  38. package/dist/service/lambda/event-source/poll/kinesis/sim-lambda-kinesis-stream-delivery.d.ts +16 -4
  39. package/dist/service/lambda/event-source/poll/kinesis/sim-lambda-kinesis-stream-delivery.js +36 -5
  40. package/dist/service/lambda/event-source/poll/kinesis/sim-lambda-kinesis-stream-event-source-poller.d.ts +4 -3
  41. package/dist/service/lambda/event-source/poll/kinesis/sim-lambda-kinesis-stream-event-source-poller.js +6 -7
  42. package/dist/service/lambda/event-source/poll/kinesis/sim-lambda-kinesis-stream-event.d.ts +14 -2
  43. package/dist/service/lambda/event-source/poll/kinesis/sim-lambda-kinesis-stream-event.js +21 -2
  44. package/dist/service/lambda/event-source/poll/sim-lambda-dynamodb-stream-delivery.d.ts +19 -6
  45. package/dist/service/lambda/event-source/poll/sim-lambda-dynamodb-stream-delivery.js +41 -7
  46. package/dist/service/lambda/event-source/poll/sim-lambda-dynamodb-stream-event-source-poller.d.ts +7 -3
  47. package/dist/service/lambda/event-source/poll/sim-lambda-dynamodb-stream-event-source-poller.js +10 -9
  48. package/dist/service/lambda/event-source/poll/sim-lambda-dynamodb-stream-event.d.ts +6 -2
  49. package/dist/service/lambda/event-source/poll/sim-lambda-dynamodb-stream-event.js +6 -2
  50. package/dist/service/lambda/event-source/poll/sim-lambda-event-source-delivery.d.ts +21 -1
  51. package/dist/service/lambda/event-source/poll/sim-lambda-event-source-delivery.js +32 -2
  52. package/dist/service/lambda/event-source/poll/sim-lambda-sqs-delivery.js +1 -0
  53. package/dist/service/lambda/event-source/poll/sim-lambda-sqs-event.d.ts +8 -0
  54. package/dist/service/lambda/event-source/poll/sim-lambda-sqs-event.js +11 -0
  55. package/dist/service/lambda/event-source/sim-lambda-event-source-mapping.d.ts +10 -0
  56. package/dist/service/lambda/event-source/sim-lambda-event-source-mapping.js +11 -0
  57. package/dist/service/lambda/event-source/stream/sim-lambda-stream-cascade-guard.d.ts +35 -8
  58. package/dist/service/lambda/event-source/stream/sim-lambda-stream-cascade-guard.js +35 -12
  59. package/dist/service/lambda/event-source/stream/sim-lambda-stream-cascade.error.d.ts +13 -4
  60. package/dist/service/lambda/event-source/stream/sim-lambda-stream-cascade.error.js +12 -6
  61. package/dist/service/lambda/event-source/stream/sim-lambda-stream-halt.d.ts +18 -0
  62. package/dist/service/lambda/event-source/stream/sim-lambda-stream-halt.js +22 -0
  63. package/dist/service/scheduler/command/schedule/schedule.command.d.ts +1 -0
  64. package/dist/service/scheduler/command/schedule/sim-scheduler-described-schedule.js +1 -0
  65. package/dist/service/scheduler/command/schedule/sim-scheduler-schedule-writer.js +2 -1
  66. package/dist/service/scheduler/command/schedule/sim-scheduler-unsimulated-input.d.ts +3 -3
  67. package/dist/service/scheduler/command/schedule/sim-scheduler-unsimulated-input.js +19 -11
  68. package/dist/service/scheduler/schedule/sim-scheduler-schedule-expression.d.ts +5 -3
  69. package/dist/service/scheduler/schedule/sim-scheduler-schedule-expression.js +6 -4
  70. package/dist/service/scheduler/schedule/sim-scheduler-schedule.d.ts +9 -0
  71. package/dist/service/scheduler/schedule/sim-scheduler-schedule.js +8 -0
  72. package/dist/util/schedule/at/sim-at-expression.d.ts +2 -1
  73. package/dist/util/schedule/at/sim-at-expression.js +7 -3
  74. package/dist/util/schedule/cron/sim-cron-expression.d.ts +6 -3
  75. package/dist/util/schedule/cron/sim-cron-expression.js +24 -21
  76. package/dist/util/schedule/sim-schedule-zone-clock.d.ts +26 -0
  77. package/dist/util/schedule/sim-schedule-zone-clock.js +56 -0
  78. package/dist/util/schedule/sim-schedule-zone.d.ts +58 -0
  79. package/dist/util/schedule/sim-schedule-zone.js +83 -0
  80. package/dist/util/schedule/sim-schedule.d.ts +1 -1
  81. package/dist/util/schedule/sim-schedule.js +6 -5
  82. package/docs/services/athena/README.md +26 -2
  83. package/docs/services/lambda/README.md +159 -15
  84. package/docs/services/scheduler/README.md +35 -6
  85. package/package.json +1 -1
@@ -0,0 +1,58 @@
1
+ import { type SimScheduleZoneParts } from "./sim-schedule-zone-clock.js";
2
+ /**
3
+ * The zone a schedule expression is read in.
4
+ *
5
+ * A cron expression names wall-clock fields, and which instants those are
6
+ * depends on the zone the schedule was created with. Everything a schedule does
7
+ * with a calendar therefore goes through one of these, and a schedule created
8
+ * without a timezone gets the UTC one, which is what every schedule used to
9
+ * get.
10
+ */
11
+ export declare class SimScheduleZone {
12
+ /**
13
+ * The zone name as the request wrote it, which a describe reports back.
14
+ */
15
+ readonly name: string;
16
+ private readonly clock;
17
+ private constructor();
18
+ /**
19
+ * The zone a name calls for, refusing a name no zone answers to.
20
+ */
21
+ static of(name?: string): SimScheduleZone;
22
+ /**
23
+ * What a clock in this zone reads at an instant.
24
+ */
25
+ partsAt(instant: number): SimScheduleZoneParts;
26
+ /**
27
+ * The instant a clock in this zone reads a wall-clock time at.
28
+ *
29
+ * Two offsets are in play on the day a zone changes one, so both are tried
30
+ * and the answer is whichever of them the zone actually reads back. A time
31
+ * the clocks read twice has two, and the earlier is the answer, which is the
32
+ * one real Scheduler fires at. A time the clocks skip over has neither, and
33
+ * the answer is the later of the two, which is the first instant after the
34
+ * hour that never happened.
35
+ */
36
+ instantOf(year: number, month: number, day: number, hour?: number, minute?: number): number;
37
+ /**
38
+ * Whether an instant is the one this zone puts its own wall-clock reading at.
39
+ *
40
+ * The second of a repeated hour reads the same as the first and is not the
41
+ * instant that reading resolves to, which is what tells the two apart.
42
+ */
43
+ isCanonical(instant: number): boolean;
44
+ /**
45
+ * The instants a wall-clock time could be, under the offsets either side of
46
+ * a change.
47
+ */
48
+ private candidatesFor;
49
+ /**
50
+ * How far ahead of UTC this zone is at an instant, in milliseconds.
51
+ */
52
+ private offsetAt;
53
+ /**
54
+ * What a clock in this zone reads at an instant, as a UTC instant of the same
55
+ * wall-clock fields, which is what makes two readings comparable.
56
+ */
57
+ private wallClockOf;
58
+ }
@@ -0,0 +1,83 @@
1
+ import { simScheduleZoneClock, } from "./sim-schedule-zone-clock.js";
2
+ const minuteMs = 60_000;
3
+ /**
4
+ * The zone a schedule expression is read in.
5
+ *
6
+ * A cron expression names wall-clock fields, and which instants those are
7
+ * depends on the zone the schedule was created with. Everything a schedule does
8
+ * with a calendar therefore goes through one of these, and a schedule created
9
+ * without a timezone gets the UTC one, which is what every schedule used to
10
+ * get.
11
+ */
12
+ export class SimScheduleZone {
13
+ /**
14
+ * The zone name as the request wrote it, which a describe reports back.
15
+ */
16
+ name;
17
+ clock;
18
+ constructor(name, clock) {
19
+ this.name = name;
20
+ this.clock = clock;
21
+ }
22
+ /**
23
+ * The zone a name calls for, refusing a name no zone answers to.
24
+ */
25
+ static of(name = "UTC") {
26
+ return new this(name, simScheduleZoneClock(name));
27
+ }
28
+ /**
29
+ * What a clock in this zone reads at an instant.
30
+ */
31
+ partsAt(instant) {
32
+ return this.clock(instant);
33
+ }
34
+ /**
35
+ * The instant a clock in this zone reads a wall-clock time at.
36
+ *
37
+ * Two offsets are in play on the day a zone changes one, so both are tried
38
+ * and the answer is whichever of them the zone actually reads back. A time
39
+ * the clocks read twice has two, and the earlier is the answer, which is the
40
+ * one real Scheduler fires at. A time the clocks skip over has neither, and
41
+ * the answer is the later of the two, which is the first instant after the
42
+ * hour that never happened.
43
+ */
44
+ instantOf(year, month, day, hour = 0, minute = 0) {
45
+ const wanted = Date.UTC(year, month, day, hour, minute);
46
+ const candidates = this.candidatesFor(wanted);
47
+ const read = candidates.filter((candidate) => this.wallClockOf(candidate) === wanted);
48
+ return read.length === 0 ? Math.max(...candidates) : Math.min(...read);
49
+ }
50
+ /**
51
+ * Whether an instant is the one this zone puts its own wall-clock reading at.
52
+ *
53
+ * The second of a repeated hour reads the same as the first and is not the
54
+ * instant that reading resolves to, which is what tells the two apart.
55
+ */
56
+ isCanonical(instant) {
57
+ const read = this.clock(instant);
58
+ return (this.instantOf(read.year, read.month, read.day, read.hour, read.minute) === instant);
59
+ }
60
+ /**
61
+ * The instants a wall-clock time could be, under the offsets either side of
62
+ * a change.
63
+ */
64
+ candidatesFor(wanted) {
65
+ const first = wanted - this.offsetAt(wanted);
66
+ const second = wanted - this.offsetAt(first);
67
+ return first === second ? [first] : [first, second];
68
+ }
69
+ /**
70
+ * How far ahead of UTC this zone is at an instant, in milliseconds.
71
+ */
72
+ offsetAt(instant) {
73
+ return (this.wallClockOf(instant) - Math.floor(instant / minuteMs) * minuteMs);
74
+ }
75
+ /**
76
+ * What a clock in this zone reads at an instant, as a UTC instant of the same
77
+ * wall-clock fields, which is what makes two readings comparable.
78
+ */
79
+ wallClockOf(instant) {
80
+ const read = this.clock(instant);
81
+ return Date.UTC(read.year, read.month, read.day, read.hour, read.minute);
82
+ }
83
+ }
@@ -16,7 +16,7 @@ export declare class SimSchedule {
16
16
  /**
17
17
  * Read a schedule expression under a dialect's rules.
18
18
  */
19
- static of(source: string, dialect: SimScheduleDialect): SimSchedule;
19
+ static of(source: string, dialect: SimScheduleDialect, timeZone?: string): SimSchedule;
20
20
  /**
21
21
  * Read the body of whichever form the expression was written in.
22
22
  */
@@ -1,6 +1,7 @@
1
1
  import { SimAtExpression } from "./at/sim-at-expression.js";
2
2
  import { SimCronExpression } from "./cron/sim-cron-expression.js";
3
3
  import { SimRateExpression } from "./rate/sim-rate-expression.js";
4
+ import { SimScheduleZone } from "./sim-schedule-zone.js";
4
5
  import { SimScheduleExpressionError } from "./sim-schedule.error.js";
5
6
  const expression = /^(?<kind>[a-z]+)\((?<body>.*)\)$/su;
6
7
  /**
@@ -23,27 +24,27 @@ export class SimSchedule {
23
24
  /**
24
25
  * Read a schedule expression under a dialect's rules.
25
26
  */
26
- static of(source, dialect) {
27
+ static of(source, dialect, timeZone) {
27
28
  const read = expression.exec(source.trim())?.groups;
28
29
  if (read === undefined) {
29
30
  throw new SimScheduleExpressionError(`a schedule expression is 'rate(<value> <unit>)' or ` +
30
31
  `'cron(<fields>)', and this one is '${source}'`);
31
32
  }
32
- return new this(source, this.occurrencesOf(read, dialect));
33
+ return new this(source, this.occurrencesOf(read, dialect, SimScheduleZone.of(timeZone)));
33
34
  }
34
35
  /**
35
36
  * Read the body of whichever form the expression was written in.
36
37
  */
37
- static occurrencesOf(read, dialect) {
38
+ static occurrencesOf(read, dialect, zone) {
38
39
  const body = read["body"] ?? "";
39
40
  if (read["kind"] === "rate") {
40
41
  return SimRateExpression.of(body, dialect.requiresRateAgreement);
41
42
  }
42
43
  if (read["kind"] === "cron") {
43
- return SimCronExpression.of(dialect.cronFields, body);
44
+ return SimCronExpression.of(dialect.cronFields, body, zone);
44
45
  }
45
46
  if (read["kind"] === "at" && dialect.allowsOneTime) {
46
- return SimAtExpression.of(body);
47
+ return SimAtExpression.of(body, zone);
47
48
  }
48
49
  const forms = dialect.allowsOneTime
49
50
  ? "an 'at(...)', a 'rate(...)' or a 'cron(...)'"
@@ -486,7 +486,7 @@ Every ascending sort is emitted carrying `NULLS LAST`, because Trino orders null
486
486
  direction it sorts and SQLite orders them first ascending. Both were cases where a query answered
487
487
  differently while still succeeding, which is the failure that costs the most to find.
488
488
 
489
- ### Flattening an array or a map
489
+ ### Flattening an array, a map or a split value
490
490
 
491
491
  `UNNEST` runs. An array or a map column is held as its JSON text, and SQLite reads that with
492
492
  `json_each`, so a statement flattening one returns a row per element the way Athena does.
@@ -505,6 +505,22 @@ falls back rather than reading a scalar as a collection.
505
505
  One flattening per statement is what this covers, joined with `CROSS JOIN`. A second `UNNEST`, a
506
506
  `LEFT JOIN UNNEST`, a `SELECT *` beside one, and a position taken from a map all fall back.
507
507
 
508
+ A table with no array column in it can still be flattened. `split` answers an array, and an `UNNEST`
509
+ over the call cuts one stored string into a row per part. This is how a packed access log row is
510
+ read, since Glue builds a log table's columns from what the delivery was configured with.
511
+
512
+ ```sql
513
+ SELECT t.part
514
+ FROM rainlytics.logs
515
+ CROSS JOIN UNNEST(split(url_extract_parameter(cs_uri_stem || '?' || cs_uri_query, 'b'), ';'))
516
+ AS t(part)
517
+ ```
518
+
519
+ Splitting an empty value answers one empty part, as it does on Trino. A row whose value is null
520
+ answers no parts at all and drops out, the way an empty array column does. The call is what says the
521
+ value is a collection (an expression has no schema entry to read). `split` and `slice` are the two
522
+ calls the flattening reaches, and any other expression under an `UNNEST` falls back.
523
+
508
524
  ### The functions a statement can call
509
525
 
510
526
  SQLite carries a much smaller function library than Trino, and the engine fills the gap for the ones
@@ -516,7 +532,7 @@ its declared result.
516
532
  | Date and time | `current_date`, `current_timestamp`, `date_add`, `date_diff`, `date_trunc`, `date_format`, `at_timezone`, `from_unixtime`, `to_unixtime`, `from_iso8601_timestamp`, `from_iso8601_date`, `to_iso8601` |
517
533
  | JSON | `json_extract`, `json_extract_scalar`, `json_parse`, `json_size` |
518
534
  | Array and map | `array_agg`, `cardinality`, `contains`, `element_at`, `array_join`, `slice` |
519
- | String | `regexp_like`, `regexp_extract`, `regexp_replace`, `split_part`, `strpos` |
535
+ | String | `regexp_like`, `regexp_extract`, `regexp_replace`, `split`, `split_part`, `strpos` |
520
536
  | Binary | `md5`, `sha1`, `sha256`, `sha512`, `xxhash64`, `murmur3`, `crc32`, `to_hex`, `from_hex`, `to_base64`, `from_base64`, `to_utf8`, `from_utf8` |
521
537
  | URL | `url_extract_host`, `url_extract_path`, `url_extract_protocol`, `url_extract_port`, `url_extract_query`, `url_extract_fragment`, `url_extract_parameter`, `url_decode`, `url_encode` |
522
538
  | Approximate | `approx_distinct`, `approx_percentile` |
@@ -992,6 +1008,7 @@ own and authorizes work on one against the workgroup it belongs to. This asks th
992
1008
  - `StartQueryExecution`, `GetQueryExecution`, `GetQueryResults` and `StopQueryExecution`
993
1009
  - A `SELECT` run for real over JSON lines and CSV objects in simulated S3, answered by SQLite
994
1010
  - `UNNEST` over an array or a map column, with `WITH ORDINALITY` where a query wants the position
1011
+ - `UNNEST` over a `split` or a `slice` call, flattening one stored string into a row per part
995
1012
  - Trino's date, JSON, array, string and URL functions, with `current_timestamp` reading the
996
1013
  simulated clock
997
1014
  - Declared results, matched on the query text, ahead of the engine for one statement and behind it
@@ -1027,6 +1044,8 @@ Current documented limitations:
1027
1044
  gives a map's keys rather than its positions.
1028
1045
  - `UNNEST` over a `ROW` or a struct array falls back. The element needs field access and the
1029
1046
  flattened column is JSON text here.
1047
+ - `UNNEST` over an expression reaches `split` and `slice`. The engine reads the call to decide that
1048
+ a value is a collection, and a call to anything else falls back.
1030
1049
  - The Trino function library reaches as far as the table under
1031
1050
  [the functions a statement can call](#the-functions-a-statement-can-call). A query reaching for
1032
1051
  anything else Trino has and SQLite lacks falls back.
@@ -1107,6 +1126,11 @@ Current documented limitations:
1107
1126
  - A null in a result row reads as an empty string. Real Athena leaves the value out of the row.
1108
1127
  - A computed boolean reads as `1` and `0`. The Glue column type is what makes a boolean column read
1109
1128
  as `true` and `false`, and an expression has no column type behind it.
1129
+ - `round(x, 1)` keeps the one decimal place it was asked for, so `25` reads as `25.0` the way Trino
1130
+ renders it, and the column reports `double`. The scale is read off the statement, so a `round`
1131
+ whose scale is an expression rather than a number keeps none of it. Real Athena reports `decimal`
1132
+ where the value being rounded came from a decimal literal, and nothing here tracks decimal
1133
+ precision.
1110
1134
  - An expression nobody named is called `_col0` upward, as Athena calls one. An alias that needed
1111
1135
  quotes around it is renamed the same way.
1112
1136
  - The engine reads every object under the prefixes a query reaches and holds the rows in memory.
@@ -1738,8 +1738,8 @@ const { TableDescription } = await simAws.dynamoDb().createTable(
1738
1738
  const streamArn = TableDescription?.LatestStreamArn;
1739
1739
 
1740
1740
  // The projection goes into a second table. A function writing back into the
1741
- // table whose stream invoked it would be delivered its own writes, which the
1742
- // simulator refuses rather than looping on.
1741
+ // table whose stream invoked it is delivered its own writes, and the simulator
1742
+ // refuses a chain of those that keeps going.
1743
1743
  await simAws.dynamoDb().createTable(
1744
1744
  new CreateTableCommand({
1745
1745
  TableName: "order-totals",
@@ -2161,12 +2161,16 @@ created without `FunctionResponseTypes` ignores a report entirely.
2161
2161
 
2162
2162
  ### Writing back to the source table
2163
2163
 
2164
- A handler that writes into the table whose stream invoked it is delivered its own write, which
2165
- writes again. Real Lambda runs that loop for as long as the account is willing to pay for it. The
2166
- simulation refuses instead, with an error naming the function, the stream and the table, before the
2167
- test times out.
2164
+ A handler that writes into the table whose stream invoked it is delivered its own write. Where that
2165
+ write brings on another, and that one another, the simulation is going round a loop. Real Lambda
2166
+ runs the loop for as long as the account is willing to pay for it. Yulin counts the deliveries a
2167
+ handler's own writes bring on and refuses after ten of them in a row, with an error naming the
2168
+ function, the stream, the table and the count.
2168
2169
 
2169
- The write itself succeeds. The refusal comes afterwards, from whatever is waiting for the simulation
2170
+ A handler that settles is left to finish. Writing back once and then finding the work already done
2171
+ ends the chain at one link, and the delivery that link caused writes nothing.
2172
+
2173
+ The write itself succeeds. A refusal comes afterwards, from whatever is waiting for the simulation
2170
2174
  to settle:
2171
2175
 
2172
2176
  ```typescript
@@ -2177,7 +2181,7 @@ Only the handler's own writes count. Items written at the same time by the test,
2177
2181
  in the simulation, are an ordinary batch however many of them there are, because the guard tells them
2178
2182
  apart by where the write came from, and never by when it landed.
2179
2183
 
2180
- Writing the projection into a second table is what the guard is asking for, and is what a real
2184
+ A projection that writes back on every delivery belongs in a second table. That is what a real
2181
2185
  aggregation or search index does anyway.
2182
2186
 
2183
2187
  ### Making a stream event without a table
@@ -2361,9 +2365,11 @@ the mapping's execution-role check is looking for.
2361
2365
 
2362
2366
  `bisectBatchOnError` is simulated, and is covered under
2363
2367
  [Splitting a failed batch around the record that broke it](#splitting-a-failed-batch-around-the-record-that-broke-it).
2364
- The other properties a non-default `DynamoEventSource` adds are recorded rather than acted on. Those
2365
- are `FilterCriteria`, `ParallelizationFactor` and `TumblingWindowInSeconds`.
2366
- The mapping deploys, delivers every record whole and unfiltered, and each property it was created
2368
+ So is `filters`, which is covered under
2369
+ [Filtering the records a mapping delivers](#filtering-the-records-a-mapping-delivers). The other
2370
+ properties a non-default `DynamoEventSource` adds are recorded and left alone. Those are
2371
+ `ParallelizationFactor` and `TumblingWindowInSeconds`.
2372
+ The mapping deploys, delivers every record whole, and each property it was created
2367
2373
  without is listed in
2368
2374
  [`stack.ignoredProperties`](https://yulinsim.dev/services/cloudformation/#properties-a-resource-was-created-without "Properties a Resource was created without")
2369
2375
  with what the mapping does in its place. `CreateEventSourceMapping` still refuses the same
@@ -2531,6 +2537,137 @@ work the same way here, and each shard counts its own attempts.
2531
2537
  A mapping naming an enhanced fan-out consumer ARN is refused, since consumers are unsimulated.
2532
2538
  `AWS::Lambda::EventSourceMapping` deploys a Kinesis mapping the same way it deploys a DynamoDB one.
2533
2539
 
2540
+ ## Filtering the records a mapping delivers
2541
+
2542
+ `FilterCriteria` on an event source mapping decides which records reach the function. A record that
2543
+ matches is delivered. One that does not is treated as handled. A stream moves its checkpoint past it
2544
+ and a queue deletes it, the way real Lambda handles a record it filtered out. A batch that comes back
2545
+ empty invokes nothing at all.
2546
+
2547
+ AWS documents Lambda's filter rules as EventBridge event patterns, and simulated EventBridge's
2548
+ matcher is what evaluates them here. Exact values, `prefix`, `suffix`, `anything-but`, `numeric` and
2549
+ `exists` all work. A mapping naming an operator the matcher has no behaviour for is refused when it
2550
+ is created. A mapping carrying several filters delivers a record any one of them matches.
2551
+
2552
+ What a pattern reads depends on the source:
2553
+
2554
+ - **SQS** filters on `body` alone, as real Lambda does. A body holding JSON is read as the object it
2555
+ parses to, and one holding anything else is read as the string it is. A pattern naming any other
2556
+ key is refused when the mapping is created.
2557
+ - **DynamoDB streams** filter on the record the function would have received, so `eventName` and
2558
+ `dynamodb.NewImage.status.S` both work, with the attribute values in their DynamoDB shapes.
2559
+ - **Kinesis streams** filter on the record with the payload decoded, under a top-level `data` key.
2560
+ The payload has to hold JSON for a pattern to read inside it.
2561
+
2562
+ ```typescript sim-lambda-event-source-filter-criteria
2563
+ /**
2564
+ * Delivering only the queue messages a filter matches.
2565
+ */
2566
+
2567
+ import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";
2568
+ import {
2569
+ CreateEventSourceMappingCommand,
2570
+ CreateFunctionCommand,
2571
+ } from "@aws-sdk/client-lambda";
2572
+ import { CreateQueueCommand, SendMessageCommand } from "@aws-sdk/client-sqs";
2573
+
2574
+ import { SimAws } from "@kensio/yulin";
2575
+ import {
2576
+ makeLambdaZipFileInput,
2577
+ type SimLambdaSqsEvent,
2578
+ } from "@kensio/yulin/lambda";
2579
+
2580
+ const simAws = new SimAws();
2581
+ const queueArn = `arn:aws:sqs:${simAws.defaultRegionName}:${simAws.defaultAccountId}:orders`;
2582
+
2583
+ const { QueueUrl } = await simAws
2584
+ .sqs()
2585
+ .createQueue(new CreateQueueCommand({ QueueName: "orders" }));
2586
+
2587
+ const role = await simAws.iam().createRole(
2588
+ new CreateRoleCommand({
2589
+ RoleName: "ShippedOrdersRole",
2590
+ AssumeRolePolicyDocument: JSON.stringify({
2591
+ Version: "2012-10-17",
2592
+ Statement: {
2593
+ Effect: "Allow",
2594
+ Principal: { Service: "lambda.amazonaws.com" },
2595
+ Action: "sts:AssumeRole",
2596
+ },
2597
+ }),
2598
+ }),
2599
+ );
2600
+
2601
+ await simAws.iam().putRolePolicy(
2602
+ new PutRolePolicyCommand({
2603
+ RoleName: "ShippedOrdersRole",
2604
+ PolicyName: "ConsumeOrders",
2605
+ PolicyDocument: JSON.stringify({
2606
+ Version: "2012-10-17",
2607
+ Statement: {
2608
+ Effect: "Allow",
2609
+ Action: [
2610
+ "sqs:ReceiveMessage",
2611
+ "sqs:DeleteMessage",
2612
+ "sqs:GetQueueAttributes",
2613
+ ],
2614
+ Resource: queueArn,
2615
+ },
2616
+ }),
2617
+ }),
2618
+ );
2619
+
2620
+ const shipped: string[] = [];
2621
+
2622
+ await simAws.lambda().createFunction(
2623
+ new CreateFunctionCommand({
2624
+ FunctionName: "shipped-orders",
2625
+ Role: role.Role.Arn,
2626
+ Code: {
2627
+ ZipFile: makeLambdaZipFileInput((event: SimLambdaSqsEvent) => {
2628
+ for (const record of event.Records) {
2629
+ shipped.push(
2630
+ (JSON.parse(record.body) as { orderId: string }).orderId,
2631
+ );
2632
+ }
2633
+ }),
2634
+ },
2635
+ }),
2636
+ );
2637
+
2638
+ await simAws.lambda().createEventSourceMapping(
2639
+ new CreateEventSourceMappingCommand({
2640
+ EventSourceArn: queueArn,
2641
+ FunctionName: "shipped-orders",
2642
+ FilterCriteria: {
2643
+ Filters: [{ Pattern: JSON.stringify({ body: { status: ["shipped"] } }) }],
2644
+ },
2645
+ }),
2646
+ );
2647
+
2648
+ for (const order of [
2649
+ { orderId: "order-1", status: "placed" },
2650
+ { orderId: "order-2", status: "shipped" },
2651
+ ]) {
2652
+ await simAws
2653
+ .sqs()
2654
+ .sendMessage(
2655
+ new SendMessageCommand({ QueueUrl, MessageBody: JSON.stringify(order) }),
2656
+ );
2657
+ }
2658
+
2659
+ await simAws.backgroundTasksComplete();
2660
+
2661
+ console.log(shipped); // ["order-2"]
2662
+ ```
2663
+
2664
+ `GetEventSourceMappingCommand` reports the patterns back as they were written. A mapping created
2665
+ without filters reports none.
2666
+
2667
+ An `AWS::Lambda::EventSourceMapping` carrying `FilterCriteria` deploys with it, and so does a SAM
2668
+ function's queue or stream event. A CDK `SqsEventSource` or `DynamoEventSource` given `filters`
2669
+ therefore filters here the way it does deployed.
2670
+
2534
2671
  ## Function URLs
2535
2672
 
2536
2673
  A Function URL is an HTTP endpoint for one function. Creating one with
@@ -4199,6 +4336,8 @@ Sim Lambda currently supports:
4199
4336
  `CreateEventSourceMappingCommand` and read with `GetEventSourceMappingCommand`,
4200
4337
  `ListEventSourceMappingsCommand` and `DeleteEventSourceMappingCommand`, delivering real-shaped SQS,
4201
4338
  DynamoDB stream and Kinesis events and honouring `BatchSize`
4339
+ - `FilterCriteria` on any of those mappings, evaluated with EventBridge event patterns, delivering
4340
+ the records that match and handling the rest without an invocation
4202
4341
  - `StartingPosition: "TRIM_HORIZON"` and `"LATEST"` on a stream mapping, and `"AT_TIMESTAMP"` on a
4203
4342
  Kinesis one, with a failing batch blocking its shard until it is through or discarded
4204
4343
  - `MaximumRetryAttempts` and `MaximumRecordAgeInSeconds` on a stream mapping, ending the retries at
@@ -4361,7 +4500,7 @@ Current documented limitations:
4361
4500
  it stands.
4362
4501
  - SQS queues, DynamoDB streams and Kinesis streams are the only event sources. Kafka, DocumentDB and
4363
4502
  Kinesis enhanced fan-out consumers are refused outright. `CreateEventSourceMapping` also refuses
4364
- `FilterCriteria`, `ScalingConfig`, `ParallelizationFactor`, `TumblingWindowInSeconds` and the
4503
+ `ScalingConfig`, `ParallelizationFactor`, `TumblingWindowInSeconds` and the
4365
4504
  other inputs this simulation has no behaviour for. An
4366
4505
  `AWS::Lambda::EventSourceMapping` naming any of them deploys instead, and records each one against
4367
4506
  the Resource (see
@@ -4377,9 +4516,14 @@ Current documented limitations:
4377
4516
  AWS counts a bisected batch's deliveries against the same quota. Without that, the simulator's own
4378
4517
  cap of five attempts would discard a batch of a hundred long before it was down to one record. The
4379
4518
  splitting still ends on its own, because a batch halves at every step.
4380
- - A handler writing into the table whose stream invoked it is refused with
4381
- `SimLambdaStreamCascadeError` rather than being delivered its own writes forever. Real Lambda runs
4382
- that loop.
4519
+ - Ten deliveries in a row, each one brought on by the handler's own writes to the source that
4520
+ invoked it, are refused with `SimLambdaStreamCascadeError`. Real Lambda runs that loop for as long
4521
+ as it is paid for. A handler that writes back and then settles is delivered its own writes and
4522
+ finishes.
4523
+ - `FilterCriteria` is evaluated by simulated EventBridge's pattern matcher, which leaves out the
4524
+ `equals-ignore-case` and `$or` operators real Lambda takes. A pattern naming one is refused when
4525
+ the mapping is created. The limits AWS puts on the number and length of patterns go unenforced,
4526
+ and `KMSKeyArn` is refused, so criteria are held in plaintext.
4383
4527
  - A shard iterator never expires, where a real one is good for 15 minutes.
4384
4528
  - `MaximumBatchingWindowInSeconds` is only simulated as 0. A partial batch is delivered as soon as
4385
4529
  anything is on the event source, leaving a batching window nothing to wait for. A non-zero value
@@ -61,10 +61,38 @@ Scheduler accepts three expression forms:
61
61
  - `rate(<value> <unit>)` runs from when the schedule was created. The unit is `minute`, `hour` or
62
62
  `day`, and Scheduler lets it disagree with its value. `rate(1 hours)` is an hour here and a
63
63
  refusal on an EventBridge rule.
64
- - `cron(<six fields>)` names absolute instants in UTC. Minutes, hours, day-of-month, month,
65
- day-of-week and year, so every day at two in the morning is `cron(0 2 * * ? *)`. The day-of-month
66
- and day-of-week fields cannot both say something. Whichever is not deciding the day is written
67
- `?`.
64
+ - `cron(<six fields>)` names a wall-clock time. Minutes, hours, day-of-month, month, day-of-week and
65
+ year, so every day at two in the morning is `cron(0 2 * * ? *)`. The day-of-month and day-of-week
66
+ fields cannot both say something. Whichever is not deciding the day is written `?`.
67
+
68
+ ### The zone a schedule runs in
69
+
70
+ `ScheduleExpressionTimezone` says which zone the wall-clock time belongs to, and a schedule created
71
+ without one runs in UTC. Any IANA name the host's own `Intl` timezone data recognises works, and a
72
+ name it does not is refused when the schedule is created.
73
+
74
+ ```typescript
75
+ await simAws.scheduler().createSchedule(
76
+ new CreateScheduleCommand({
77
+ Name: "nightly-rollup",
78
+ ScheduleExpression: "cron(0 2 * * ? *)",
79
+ ScheduleExpressionTimezone: "Europe/London",
80
+ FlexibleTimeWindow: { Mode: "OFF" },
81
+ Target: { Arn: functionArn, RoleArn: roleArn },
82
+ }),
83
+ );
84
+ ```
85
+
86
+ That schedule is due at 01:00 UTC through British Summer Time and at 02:00 UTC through the winter,
87
+ which is what a nightly job written for a zone actually does. The two days a year a zone changes
88
+ offset are read the same way. A schedule keeps its hour on the clock across both.
89
+
90
+ The hour the clocks move takes the rule real Scheduler uses. A wall-clock time inside the hour that
91
+ never happens falls due at the first instant after it (02:30 on the morning the clocks go forward
92
+ fires at 03:30). A wall-clock time the clocks read twice falls due once, at the first of the two
93
+ readings.
94
+
95
+ `GetSchedule` reports the zone back as it was written.
68
96
 
69
97
  ## Firing a schedule
70
98
 
@@ -819,8 +847,9 @@ Resources of the same stack.
819
847
  - `FlexibleTimeWindow` with `Mode: "FLEXIBLE"` is refused. Real Scheduler invokes the target at an
820
848
  unpredictable moment inside the window, and firing at the exact due time instead would let a test
821
849
  rely on timing AWS leaves unpromised.
822
- - `ScheduleExpressionTimezone` other than `UTC` is refused outright, since running a schedule in the
823
- wrong zone fires it at the wrong hour.
850
+ - `ScheduleExpressionTimezone` is read from the host's own timezone data through `Intl`, so a zone
851
+ the host has never heard of is refused even where AWS would take it. A schedule that names none
852
+ runs in UTC.
824
853
  - `StartDate` and `EndDate` are refused outright.
825
854
  - Targets are Lambda, SQS, SNS and ECS. The universal target
826
855
  (`arn:aws:scheduler:::aws-sdk:<service>:<action>`) and every other target service are refused when
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kensio/yulin",
3
- "version": "1.21.21",
3
+ "version": "1.21.23",
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/",