@kensio/yulin 1.20.9 → 1.20.11

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 (35) hide show
  1. package/dist/serve/controller/host/sim-aws-service-hosts.d.ts +4 -0
  2. package/dist/serve/controller/host/sim-aws-service-hosts.js +4 -0
  3. package/dist/service/apigatewayv2/domain/sim-http-api-domain-endpoint.d.ts +59 -0
  4. package/dist/service/apigatewayv2/domain/sim-http-api-domain-endpoint.js +67 -0
  5. package/dist/service/apigatewayv2/domain/sim-http-api-domain-name.d.ts +42 -10
  6. package/dist/service/apigatewayv2/domain/sim-http-api-domain-name.js +57 -13
  7. package/dist/service/apigatewayv2/registry/sim-http-api-domain-registry.d.ts +7 -1
  8. package/dist/service/apigatewayv2/registry/sim-http-api-domain-registry.js +15 -3
  9. package/dist/service/apigatewayv2/serve/sim-http-api-serving.js +3 -1
  10. package/dist/service/athena/engine/sim-athena-json-records.d.ts +23 -0
  11. package/dist/service/athena/engine/sim-athena-json-records.js +46 -0
  12. package/dist/service/athena/engine/sim-athena-object-codec.d.ts +13 -0
  13. package/dist/service/athena/engine/sim-athena-object-codec.js +40 -0
  14. package/dist/service/athena/engine/sim-athena-record-reader.js +26 -10
  15. package/dist/service/athena/engine/sim-athena-table-objects.d.ts +1 -1
  16. package/dist/service/athena/engine/sim-athena-table-objects.js +4 -2
  17. package/dist/service/athena/sim-athena-engine.fixture.d.ts +2 -0
  18. package/dist/service/athena/sim-athena-engine.fixture.js +6 -0
  19. package/dist/service/aws/factory/sim-aws-account-service-cache.d.ts +2 -0
  20. package/dist/service/aws/factory/sim-aws-account-service-cache.js +3 -0
  21. package/dist/service/aws/factory/sim-aws-scoped-service-registries.d.ts +7 -0
  22. package/dist/service/aws/factory/sim-aws-scoped-service-registries.js +9 -2
  23. package/dist/service/aws/factory/sim-aws-service-factory.js +3 -1
  24. package/dist/service/cloudformation/resource/cfn/apigatewayv2/sim-http-api-domain-name-cfn.d.ts +4 -4
  25. package/dist/service/cloudformation/resource/cfn/apigatewayv2/sim-http-api-domain-name-cfn.js +4 -4
  26. package/dist/service/route53/resolve/sim-r53-service-target-resolver.d.ts +12 -0
  27. package/dist/service/route53/resolve/sim-r53-service-target-resolver.js +24 -0
  28. package/dist/service/route53/resolve/sim-route53-resolver.d.ts +9 -1
  29. package/dist/service/route53/resolve/sim-route53-resolver.js +10 -2
  30. package/dist/service/route53/sim-route53.d.ts +6 -0
  31. package/dist/service/route53/sim-route53.js +1 -0
  32. package/dist/service/s3/mount/sim-s3-mount.type.d.ts +1 -1
  33. package/dist/service/s3/storage/filesystem/s3-filesystem-allowed.js +1 -0
  34. package/dist/service/s3/storage/filesystem/s3-filesystem-object-metadata.js +1 -0
  35. package/package.json +1 -1
@@ -32,6 +32,10 @@ export declare class SimAwsNoServiceHosts implements SimAwsServiceHosts {
32
32
  * project picked rather than names AWS generated. Resolution asks one thing
33
33
  * about a hostname, so the holders are gathered here rather than each being
34
34
  * threaded separately through it.
35
+ *
36
+ * Resolution asks twice. A hosted-zone record outranks some claims and leaves
37
+ * others alone, and which of these a holder is gathered into is what says when
38
+ * the hostnames it holds are answered for.
35
39
  */
36
40
  export declare class SimAwsAnyServiceHosts implements SimAwsServiceHosts {
37
41
  private readonly holders;
@@ -18,6 +18,10 @@ export class SimAwsNoServiceHosts {
18
18
  * project picked rather than names AWS generated. Resolution asks one thing
19
19
  * about a hostname, so the holders are gathered here rather than each being
20
20
  * threaded separately through it.
21
+ *
22
+ * Resolution asks twice. A hosted-zone record outranks some claims and leaves
23
+ * others alone, and which of these a holder is gathered into is what says when
24
+ * the hostnames it holds are answered for.
21
25
  */
22
26
  export class SimAwsAnyServiceHosts {
23
27
  holders;
@@ -0,0 +1,59 @@
1
+ import type { Brand } from "../../../util/brand.type.js";
2
+ /**
3
+ * The id API Gateway allocates for the regional endpoint of one custom domain,
4
+ * which is the leading DNS label of that endpoint's hostname.
5
+ */
6
+ export type SimHttpApiDomainId = Brand<string, "SimHttpApiDomainId">;
7
+ /**
8
+ * What a custom domain's regional endpoint hostname is built from.
9
+ */
10
+ export interface SimHttpApiDomainEndpointParts {
11
+ readonly domainId: SimHttpApiDomainId;
12
+ readonly regionName: string;
13
+ }
14
+ /**
15
+ * Allocate a domain endpoint id in the shape real API Gateway uses: `d-`
16
+ * followed by 10 lowercase alphanumeric characters, forming one DNS label.
17
+ */
18
+ export declare function makeSimHttpApiDomainId(): SimHttpApiDomainId;
19
+ /**
20
+ * Whether a DNS label is a custom domain endpoint id rather than an API id.
21
+ *
22
+ * A custom domain and an API are issued endpoints of the same shape, so this
23
+ * is what decides which of the two a hostname names. API ids carry no hyphen,
24
+ * which is what leaves the prefix free to say it.
25
+ */
26
+ export declare function isSimHttpApiDomainId(label: string): boolean;
27
+ /**
28
+ * Build the real AWS endpoint hostname for a custom domain, which is what
29
+ * `RegionalDomainName` answers with and what a record for the custom domain
30
+ * name points at.
31
+ */
32
+ export declare function simHttpApiDomainEndpointHost(parts: SimHttpApiDomainEndpointParts): string;
33
+ /**
34
+ * Build the logical hostname for a custom domain endpoint: the AWS endpoint
35
+ * hostname without the real AWS domain, which is the form Yulin serves on
36
+ * localhost.
37
+ */
38
+ export declare function simHttpApiDomainEndpointLogicalHost(parts: SimHttpApiDomainEndpointParts): string;
39
+ /**
40
+ * A custom domain's regional endpoint hostname, as it was read.
41
+ */
42
+ export interface SimHttpApiDomainEndpointHost {
43
+ /**
44
+ * The hostname without the AWS domain, which is the name simulated DNS
45
+ * resolves and the domain registry holds.
46
+ */
47
+ readonly logicalHost: string;
48
+ readonly regionName: string;
49
+ }
50
+ /**
51
+ * Read a custom domain's regional endpoint hostname, in either the form real
52
+ * API Gateway reports it or the local form the AWS domain is dropped from.
53
+ *
54
+ * Both forms are read here because a request arriving over HTTP has had the
55
+ * AWS domain rewritten away and a record value written as `RegionalDomainName`
56
+ * answered has not, and a record pointing the custom domain name at the
57
+ * endpoint is how the domain is reached on AWS.
58
+ */
59
+ export declare function readSimHttpApiDomainEndpointHost(hostname: string): SimHttpApiDomainEndpointHost | undefined;
@@ -0,0 +1,67 @@
1
+ import { faker } from "@faker-js/faker";
2
+ import { executeApiDomain, executeApiHostLabel, } from "../api/sim-http-api-host.js";
3
+ /**
4
+ * The first character of the id API Gateway allocates a custom domain, which
5
+ * is what tells its endpoint hostname apart from the one an API gets.
6
+ */
7
+ const domainIdPrefix = "d-";
8
+ /**
9
+ * Allocate a domain endpoint id in the shape real API Gateway uses: `d-`
10
+ * followed by 10 lowercase alphanumeric characters, forming one DNS label.
11
+ */
12
+ export function makeSimHttpApiDomainId() {
13
+ return `${domainIdPrefix}${faker.helpers.fromRegExp(/[a-z0-9]{10}/)}`;
14
+ }
15
+ /**
16
+ * Whether a DNS label is a custom domain endpoint id rather than an API id.
17
+ *
18
+ * A custom domain and an API are issued endpoints of the same shape, so this
19
+ * is what decides which of the two a hostname names. API ids carry no hyphen,
20
+ * which is what leaves the prefix free to say it.
21
+ */
22
+ export function isSimHttpApiDomainId(label) {
23
+ return label.startsWith(domainIdPrefix);
24
+ }
25
+ /**
26
+ * Build the real AWS endpoint hostname for a custom domain, which is what
27
+ * `RegionalDomainName` answers with and what a record for the custom domain
28
+ * name points at.
29
+ */
30
+ export function simHttpApiDomainEndpointHost(parts) {
31
+ return `${simHttpApiDomainEndpointLogicalHost(parts)}.${executeApiDomain}`;
32
+ }
33
+ /**
34
+ * Build the logical hostname for a custom domain endpoint: the AWS endpoint
35
+ * hostname without the real AWS domain, which is the form Yulin serves on
36
+ * localhost.
37
+ */
38
+ export function simHttpApiDomainEndpointLogicalHost(parts) {
39
+ return `${parts.domainId}.${executeApiHostLabel}.${parts.regionName}`;
40
+ }
41
+ /**
42
+ * Read a custom domain's regional endpoint hostname, in either the form real
43
+ * API Gateway reports it or the local form the AWS domain is dropped from.
44
+ *
45
+ * Both forms are read here because a request arriving over HTTP has had the
46
+ * AWS domain rewritten away and a record value written as `RegionalDomainName`
47
+ * answered has not, and a record pointing the custom domain name at the
48
+ * endpoint is how the domain is reached on AWS.
49
+ */
50
+ export function readSimHttpApiDomainEndpointHost(hostname) {
51
+ const awsDomainSuffix = `.${executeApiDomain}`;
52
+ const logicalHost = hostname.endsWith(awsDomainSuffix)
53
+ ? hostname.slice(0, -awsDomainSuffix.length)
54
+ : hostname;
55
+ const labels = logicalHost.split(".");
56
+ if (labels.length !== 3) {
57
+ return undefined;
58
+ }
59
+ const [domainId, service, regionName] = labels;
60
+ if (domainId === undefined ||
61
+ regionName === undefined ||
62
+ service !== executeApiHostLabel ||
63
+ !isSimHttpApiDomainId(domainId)) {
64
+ return undefined;
65
+ }
66
+ return { logicalHost, regionName };
67
+ }
@@ -1,4 +1,5 @@
1
1
  import type { SimAwsAccountRegionScope } from "../../aws/sim-aws-account-region-scope.js";
2
+ import { type SimHttpApiDomainId } from "./sim-http-api-domain-endpoint.js";
2
3
  import { SimApiMappingStore } from "./sim-api-mapping-store.js";
3
4
  /**
4
5
  * The expression API Gateway selects an API mapping with, which is the request
@@ -12,7 +13,7 @@ export declare const simApiMappingSelectionExpression = "$request.basepath";
12
13
  * Real API Gateway publishes a different id per Region. One fixed value stands
13
14
  * for all of them here, the way `simElbV2CanonicalHostedZoneId` does for a
14
15
  * load balancer: nothing resolves through it, and an alias record reaches the
15
- * domain by the name it points at.
16
+ * domain by the regional endpoint name it points at.
16
17
  */
17
18
  export declare const simHttpApiRegionalHostedZoneId = "Z0000000000001";
18
19
  /**
@@ -29,13 +30,21 @@ export interface SimHttpApiDomainNameConfiguration {
29
30
  EndpointType?: string | undefined;
30
31
  SecurityPolicy?: string | undefined;
31
32
  }
33
+ /**
34
+ * One entry of a domain's `DomainNameConfigurations` as a command reports it,
35
+ * which is what was written plus the endpoint API Gateway made for it.
36
+ */
37
+ export interface SimHttpApiDomainNameConfigurationView extends SimHttpApiDomainNameConfiguration {
38
+ ApiGatewayDomainName: string;
39
+ HostedZoneId: string;
40
+ }
32
41
  /**
33
42
  * Minimal structural domain name view, as the Create and Get commands return.
34
43
  */
35
44
  export interface SimHttpApiDomainNameView {
36
45
  DomainName: string;
37
46
  ApiMappingSelectionExpression: string;
38
- DomainNameConfigurations: readonly SimHttpApiDomainNameConfiguration[];
47
+ DomainNameConfigurations: readonly SimHttpApiDomainNameConfigurationView[];
39
48
  }
40
49
  interface SimHttpApiDomainNameProperties {
41
50
  readonly domainName: string;
@@ -45,9 +54,11 @@ interface SimHttpApiDomainNameProperties {
45
54
  /**
46
55
  * A simulated API Gateway custom domain name.
47
56
  *
48
- * The domain answers on a hostname of its own rather than on one API Gateway
49
- * generated, which is how an HTTP API is reached under a name the project
50
- * owns. It owns its API mappings, because a mapping is addressed by domain on
57
+ * The domain has the two names real API Gateway gives it. One is the hostname
58
+ * the project owns, which is how an HTTP API is reached under a name of its
59
+ * own. The other is the regional endpoint API Gateway issues alongside it,
60
+ * which a record for the custom domain name points at. The domain answers on
61
+ * both. It owns its API mappings, because a mapping is addressed by domain on
51
62
  * real AWS and none of them outlives the domain.
52
63
  *
53
64
  * A domain and the APIs it maps live in one Account and Region, as they do on
@@ -58,6 +69,10 @@ export declare class SimHttpApiDomainName {
58
69
  readonly accountRegionScope: SimAwsAccountRegionScope;
59
70
  readonly configurations: readonly SimHttpApiDomainNameConfiguration[];
60
71
  readonly apiMappings: SimApiMappingStore;
72
+ /**
73
+ * The id API Gateway allocated this domain's regional endpoint.
74
+ */
75
+ readonly domainId: SimHttpApiDomainId;
61
76
  constructor(properties: SimHttpApiDomainNameProperties);
62
77
  /**
63
78
  * The hostname this domain answers requests on.
@@ -67,13 +82,17 @@ export declare class SimHttpApiDomainName {
67
82
  * The hostname a DNS record pointing at this domain names, which is what
68
83
  * `Fn::GetAtt RegionalDomainName` answers with.
69
84
  *
70
- * Real API Gateway issues a separate `d-<id>.execute-api.<region>` name here
71
- * and expects the custom domain to be pointed at it. This answers with the
72
- * custom domain's own hostname instead, so a CloudFront Origin or a Route53
73
- * alias built on it reaches the domain. An `execute-api` name would be read
74
- * as a generated API endpoint by simulated DNS and route to no API at all.
85
+ * This is the domain's published address. A CloudFront Origin points at it
86
+ * directly, and a record for the custom domain name points at it. The custom
87
+ * domain name is reached through that record.
75
88
  */
76
89
  get regionalDomainName(): string;
90
+ /**
91
+ * The hostnames a request can reach this domain by. They are the custom
92
+ * domain name and the regional endpoint, the second without the AWS domain
93
+ * the local URL rewriting drops.
94
+ */
95
+ get hostnames(): readonly string[];
77
96
  /**
78
97
  * The ARN of this domain, which `Fn::GetAtt DomainNameArn` answers with.
79
98
  *
@@ -86,5 +105,18 @@ export declare class SimHttpApiDomainName {
86
105
  * Get the AWS-like view of this domain name.
87
106
  */
88
107
  view(): SimHttpApiDomainNameView;
108
+ /**
109
+ * What this domain's regional endpoint hostname is built from.
110
+ */
111
+ private get endpointParts();
112
+ /**
113
+ * The domain's configurations as a command reports them.
114
+ *
115
+ * Each one is what was written plus the endpoint API Gateway made for it. A
116
+ * domain created without a configuration still reports one, because the
117
+ * endpoint exists whether or not anything was written about the certificate
118
+ * in front of it, and real API Gateway reports it there.
119
+ */
120
+ private configurationViews;
89
121
  }
90
122
  export {};
@@ -1,3 +1,4 @@
1
+ import { makeSimHttpApiDomainId, simHttpApiDomainEndpointHost, simHttpApiDomainEndpointLogicalHost, } from "./sim-http-api-domain-endpoint.js";
1
2
  import { SimApiMappingStore } from "./sim-api-mapping-store.js";
2
3
  /**
3
4
  * The expression API Gateway selects an API mapping with, which is the request
@@ -11,15 +12,17 @@ export const simApiMappingSelectionExpression = "$request.basepath";
11
12
  * Real API Gateway publishes a different id per Region. One fixed value stands
12
13
  * for all of them here, the way `simElbV2CanonicalHostedZoneId` does for a
13
14
  * load balancer: nothing resolves through it, and an alias record reaches the
14
- * domain by the name it points at.
15
+ * domain by the regional endpoint name it points at.
15
16
  */
16
17
  export const simHttpApiRegionalHostedZoneId = "Z0000000000001";
17
18
  /**
18
19
  * A simulated API Gateway custom domain name.
19
20
  *
20
- * The domain answers on a hostname of its own rather than on one API Gateway
21
- * generated, which is how an HTTP API is reached under a name the project
22
- * owns. It owns its API mappings, because a mapping is addressed by domain on
21
+ * The domain has the two names real API Gateway gives it. One is the hostname
22
+ * the project owns, which is how an HTTP API is reached under a name of its
23
+ * own. The other is the regional endpoint API Gateway issues alongside it,
24
+ * which a record for the custom domain name points at. The domain answers on
25
+ * both. It owns its API mappings, because a mapping is addressed by domain on
23
26
  * real AWS and none of them outlives the domain.
24
27
  *
25
28
  * A domain and the APIs it maps live in one Account and Region, as they do on
@@ -30,6 +33,10 @@ export class SimHttpApiDomainName {
30
33
  accountRegionScope;
31
34
  configurations;
32
35
  apiMappings = new SimApiMappingStore();
36
+ /**
37
+ * The id API Gateway allocated this domain's regional endpoint.
38
+ */
39
+ domainId = makeSimHttpApiDomainId();
33
40
  constructor(properties) {
34
41
  this.domainName = properties.domainName;
35
42
  this.accountRegionScope = properties.accountRegionScope;
@@ -45,14 +52,23 @@ export class SimHttpApiDomainName {
45
52
  * The hostname a DNS record pointing at this domain names, which is what
46
53
  * `Fn::GetAtt RegionalDomainName` answers with.
47
54
  *
48
- * Real API Gateway issues a separate `d-<id>.execute-api.<region>` name here
49
- * and expects the custom domain to be pointed at it. This answers with the
50
- * custom domain's own hostname instead, so a CloudFront Origin or a Route53
51
- * alias built on it reaches the domain. An `execute-api` name would be read
52
- * as a generated API endpoint by simulated DNS and route to no API at all.
55
+ * This is the domain's published address. A CloudFront Origin points at it
56
+ * directly, and a record for the custom domain name points at it. The custom
57
+ * domain name is reached through that record.
53
58
  */
54
59
  get regionalDomainName() {
55
- return this.hostname;
60
+ return simHttpApiDomainEndpointHost(this.endpointParts);
61
+ }
62
+ /**
63
+ * The hostnames a request can reach this domain by. They are the custom
64
+ * domain name and the regional endpoint, the second without the AWS domain
65
+ * the local URL rewriting drops.
66
+ */
67
+ get hostnames() {
68
+ return [
69
+ this.hostname,
70
+ simHttpApiDomainEndpointLogicalHost(this.endpointParts),
71
+ ];
56
72
  }
57
73
  /**
58
74
  * The ARN of this domain, which `Fn::GetAtt DomainNameArn` answers with.
@@ -71,9 +87,37 @@ export class SimHttpApiDomainName {
71
87
  return {
72
88
  DomainName: this.domainName,
73
89
  ApiMappingSelectionExpression: simApiMappingSelectionExpression,
74
- DomainNameConfigurations: this.configurations.map((configuration) => ({
75
- ...configuration,
76
- })),
90
+ DomainNameConfigurations: this.configurationViews(),
91
+ };
92
+ }
93
+ /**
94
+ * What this domain's regional endpoint hostname is built from.
95
+ */
96
+ get endpointParts() {
97
+ return {
98
+ domainId: this.domainId,
99
+ regionName: this.accountRegionScope.regionName,
100
+ };
101
+ }
102
+ /**
103
+ * The domain's configurations as a command reports them.
104
+ *
105
+ * Each one is what was written plus the endpoint API Gateway made for it. A
106
+ * domain created without a configuration still reports one, because the
107
+ * endpoint exists whether or not anything was written about the certificate
108
+ * in front of it, and real API Gateway reports it there.
109
+ */
110
+ configurationViews() {
111
+ const endpoint = {
112
+ ApiGatewayDomainName: this.regionalDomainName,
113
+ HostedZoneId: simHttpApiRegionalHostedZoneId,
77
114
  };
115
+ if (this.configurations.length === 0) {
116
+ return [endpoint];
117
+ }
118
+ return this.configurations.map((configuration) => ({
119
+ ...configuration,
120
+ ...endpoint,
121
+ }));
78
122
  }
79
123
  }
@@ -14,6 +14,11 @@ import type { SimHttpApiDomainName } from "../domain/sim-http-api-domain-name.js
14
14
  * and nothing saying it is an API Gateway hostname at all. That makes this the
15
15
  * answer to which simulated service a hostname belongs to, which is what
16
16
  * `SimAwsServiceHosts` asks.
17
+ *
18
+ * Simulated DNS asks a hosted-zone record about a custom domain name first,
19
+ * the way the custom domain name reaches the domain through a record on AWS.
20
+ * This answers where no record names the hostname. That is how a test that
21
+ * creates only a domain reaches it.
17
22
  */
18
23
  export declare class SimHttpApiDomainRegistry implements SimAwsServiceHosts {
19
24
  private readonly domainsByHost;
@@ -28,7 +33,8 @@ export declare class SimHttpApiDomainRegistry implements SimAwsServiceHosts {
28
33
  */
29
34
  deregister(domain: SimHttpApiDomainName): void;
30
35
  /**
31
- * Find the domain a request's hostname reaches.
36
+ * Find the domain a request's hostname reaches, by either of the two names
37
+ * the domain answers on.
32
38
  */
33
39
  findByHost(hostname: string): SimHttpApiDomainName | undefined;
34
40
  /**
@@ -12,6 +12,11 @@ import { SimApiGatewayV2BadRequest } from "../error/sim-api-gateway-v2.error.js"
12
12
  * and nothing saying it is an API Gateway hostname at all. That makes this the
13
13
  * answer to which simulated service a hostname belongs to, which is what
14
14
  * `SimAwsServiceHosts` asks.
15
+ *
16
+ * Simulated DNS asks a hosted-zone record about a custom domain name first,
17
+ * the way the custom domain name reaches the domain through a record on AWS.
18
+ * This answers where no record names the hostname. That is how a test that
19
+ * creates only a domain reaches it.
15
20
  */
16
21
  export class SimHttpApiDomainRegistry {
17
22
  domainsByHost = new Map();
@@ -24,18 +29,25 @@ export class SimHttpApiDomainRegistry {
24
29
  */
25
30
  register(domain) {
26
31
  this.requireUnused(domain);
27
- this.domainsByHost.set(domain.hostname.toLowerCase(), domain);
32
+ for (const hostname of domain.hostnames) {
33
+ this.domainsByHost.set(hostname.toLowerCase(), domain);
34
+ }
35
+ // Only the custom domain name is claimed. The regional endpoint is a name
36
+ // API Gateway made up, and takes no hostname away from another service.
28
37
  this.claims.claim(domain.hostname);
29
38
  }
30
39
  /**
31
40
  * Forget a deleted domain, so its hostname stops resolving.
32
41
  */
33
42
  deregister(domain) {
34
- this.domainsByHost.delete(domain.hostname.toLowerCase());
43
+ for (const hostname of domain.hostnames) {
44
+ this.domainsByHost.delete(hostname.toLowerCase());
45
+ }
35
46
  this.claims.release(domain.hostname);
36
47
  }
37
48
  /**
38
- * Find the domain a request's hostname reaches.
49
+ * Find the domain a request's hostname reaches, by either of the two names
50
+ * the domain answers on.
39
51
  */
40
52
  findByHost(hostname) {
41
53
  return this.domainsByHost.get(hostname.toLowerCase());
@@ -76,7 +76,9 @@ export class SimHttpApiServingResolver {
76
76
  */
77
77
  mapped(target, method, path) {
78
78
  const domain = this.domains.route(target);
79
- /* v8 ignore if -- a domain target only resolves from a registered domain */
79
+ // A regional endpoint hostname is recognised by its shape, as a generated
80
+ // API endpoint is, so one naming no domain arrives here and is answered
81
+ // the way a path no mapping claims is.
80
82
  if (domain === undefined) {
81
83
  return notFound;
82
84
  }
@@ -0,0 +1,23 @@
1
+ import type { SimAthenaEngineRow } from "./sim-athena-engine-row.js";
2
+ /**
3
+ * How one table's JSON records reach its columns.
4
+ *
5
+ * `mappings` holds the column names the OpenX SerDe's `mapping.<column>`
6
+ * parameters declare, each against the record key it reads. A table declaring
7
+ * none leaves it empty, and every column is read by its own name.
8
+ *
9
+ * `caseInsensitive` is the SerDe's own `case.insensitive`, on unless a table
10
+ * turns it off. The SerDe folds a record's keys before looking one up. A
11
+ * mapping matches a key of any case until a table declares `FALSE`.
12
+ */
13
+ export interface SimAthenaJsonFormat {
14
+ readonly mappings: ReadonlyMap<string, string>;
15
+ readonly caseInsensitive: boolean;
16
+ }
17
+ /**
18
+ * One object of JSON lines, read into rows.
19
+ *
20
+ * A nested object or array is kept as its JSON text, which is what makes
21
+ * `json_extract_scalar` and `cardinality` reach into it.
22
+ */
23
+ export declare function simAthenaJsonRows(text: string, format: SimAthenaJsonFormat): readonly SimAthenaEngineRow[];
@@ -0,0 +1,46 @@
1
+ /**
2
+ * One object of JSON lines, read into rows.
3
+ *
4
+ * A nested object or array is kept as its JSON text, which is what makes
5
+ * `json_extract_scalar` and `cardinality` reach into it.
6
+ */
7
+ export function simAthenaJsonRows(text, format) {
8
+ return text
9
+ .split("\n")
10
+ .filter((line) => line.trim().length > 0)
11
+ .map((line) => JSON.parse(line))
12
+ .map((record) => mappedRow(record, format));
13
+ }
14
+ /**
15
+ * One record with its mapped columns laid over it.
16
+ *
17
+ * The record is kept whole underneath. A column no mapping names still reads
18
+ * by its own name. A mapped column is written whether or not the record
19
+ * carries the key, since the mapping is where that column reads from.
20
+ */
21
+ function mappedRow(record, format) {
22
+ if (format.mappings.size === 0) {
23
+ return record;
24
+ }
25
+ const row = { ...record };
26
+ for (const [column, key] of format.mappings) {
27
+ row[column] = mappedValue(record, key, format.caseInsensitive);
28
+ }
29
+ return row;
30
+ }
31
+ function mappedValue(record, key, caseInsensitive) {
32
+ const exact = record[key];
33
+ if (exact !== undefined) {
34
+ return exact;
35
+ }
36
+ if (!caseInsensitive) {
37
+ return null;
38
+ }
39
+ const wanted = key.toLowerCase();
40
+ for (const [held, value] of Object.entries(record)) {
41
+ if (held.toLowerCase() === wanted) {
42
+ return value;
43
+ }
44
+ }
45
+ return null;
46
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * One object's bytes, decompressed by the codec its key names.
3
+ *
4
+ * Athena reads the file extension, and so does this. A key ending `.gz` is
5
+ * gzip whatever its bytes hold, and a key ending anything else is text. The
6
+ * magic number would be the other way to tell, and it would read an object
7
+ * real Athena skips.
8
+ *
9
+ * A key naming a codec this simulation has no decompressor for raises, and the
10
+ * engine turns the query down. The declaration a test wrote answers it, which
11
+ * is what an object the engine cannot open already does.
12
+ */
13
+ export declare function simAthenaDecompressedBytes(key: string, bytes: Buffer): Buffer;
@@ -0,0 +1,40 @@
1
+ import { gunzipSync, inflateSync, zstdDecompressSync } from "node:zlib";
2
+ import { SimAthenaSetUpError } from "../error/sim-athena.error.js";
3
+ /** The file extensions naming a codec, against what decompresses each one. */
4
+ const codecs = new Map([
5
+ ["gz", gunzipSync],
6
+ ["zst", zstdDecompressSync],
7
+ ["deflate", inflateSync],
8
+ ]);
9
+ /**
10
+ * The file extensions naming a codec nothing here decompresses.
11
+ *
12
+ * Node's standard library has gzip, zstd and deflate. The rest would need a
13
+ * dependency, and this package takes none for a test to install.
14
+ */
15
+ const unreadableCodecs = new Set(["bz2", "bzip2", "lz4", "lzo", "snappy"]);
16
+ /**
17
+ * One object's bytes, decompressed by the codec its key names.
18
+ *
19
+ * Athena reads the file extension, and so does this. A key ending `.gz` is
20
+ * gzip whatever its bytes hold, and a key ending anything else is text. The
21
+ * magic number would be the other way to tell, and it would read an object
22
+ * real Athena skips.
23
+ *
24
+ * A key naming a codec this simulation has no decompressor for raises, and the
25
+ * engine turns the query down. The declaration a test wrote answers it, which
26
+ * is what an object the engine cannot open already does.
27
+ */
28
+ export function simAthenaDecompressedBytes(key, bytes) {
29
+ const name = key.slice(key.lastIndexOf("/") + 1);
30
+ const dot = name.lastIndexOf(".");
31
+ if (dot === -1) {
32
+ return bytes;
33
+ }
34
+ const extension = name.slice(dot + 1).toLowerCase();
35
+ if (unreadableCodecs.has(extension)) {
36
+ throw new SimAthenaSetUpError(`Unsupported sim Athena compression: ${extension}`);
37
+ }
38
+ const decompress = codecs.get(extension);
39
+ return decompress === undefined ? bytes : decompress(bytes);
40
+ }
@@ -1,10 +1,15 @@
1
1
  import { simAthenaDelimitedRows, } from "./sim-athena-delimited-records.js";
2
+ import { simAthenaJsonRows, } from "./sim-athena-json-records.js";
3
+ /** The SerDe class name that takes `mapping.<column>` parameters. */
4
+ const openXSerDe = "org.openx.data.jsonserde.jsonserde";
2
5
  /** The SerDe class names that mean JSON lines. */
3
6
  const jsonSerDes = new Set([
4
- "org.openx.data.jsonserde.jsonserde",
7
+ openXSerDe,
5
8
  "org.apache.hive.hcatalog.data.jsonserde",
6
9
  "org.apache.hadoop.hive.serde2.jsonserde",
7
10
  ]);
11
+ /** What a `mapping.<column>` parameter is named with. */
12
+ const mappingPrefix = "mapping.";
8
13
  /**
9
14
  * The SerDe class names that mean delimited text, and what each one reads
10
15
  * before the table's own parameters are applied.
@@ -39,7 +44,8 @@ export function simAthenaRecordReader(table) {
39
44
  return undefined;
40
45
  }
41
46
  if (jsonSerDes.has(library)) {
42
- return jsonRows;
47
+ const format = jsonFormat(table, library);
48
+ return (text) => simAthenaJsonRows(text, format);
43
49
  }
44
50
  const defaults = delimitedSerDes.get(library);
45
51
  if (defaults === undefined) {
@@ -50,16 +56,26 @@ export function simAthenaRecordReader(table) {
50
56
  return (text) => simAthenaDelimitedRows(text, format, columns);
51
57
  }
52
58
  /**
53
- * JSON lines, one record per line.
59
+ * How one table's JSON records reach its columns.
54
60
  *
55
- * A nested object or array is kept as its JSON text, which is what makes
56
- * `json_extract_scalar` and `cardinality` reach into it.
61
+ * Only the OpenX SerDe takes the mappings. The Hive JSON SerDes have no
62
+ * `mapping` property, and a table declaring one against them reads by its
63
+ * column names on real Athena the same as here.
57
64
  */
58
- function jsonRows(text) {
59
- return text
60
- .split("\n")
61
- .filter((line) => line.trim().length > 0)
62
- .map((line) => JSON.parse(line));
65
+ function jsonFormat(table, library) {
66
+ const parameters = library === openXSerDe
67
+ ? (table.storageDescriptor?.SerdeInfo?.Parameters ?? {})
68
+ : {};
69
+ const mappings = new Map();
70
+ for (const [name, key] of Object.entries(parameters)) {
71
+ if (name.toLowerCase().startsWith(mappingPrefix)) {
72
+ mappings.set(name.slice(mappingPrefix.length), key);
73
+ }
74
+ }
75
+ return {
76
+ mappings,
77
+ caseInsensitive: parameters["case.insensitive"]?.toLowerCase() !== "false",
78
+ };
63
79
  }
64
80
  function delimitedFormat(table, defaults) {
65
81
  const parameters = table.storageDescriptor?.SerdeInfo?.Parameters ?? {};
@@ -26,5 +26,5 @@ export interface SimAthenaTableObjects extends SimAthenaScannedObjects {
26
26
  * there because nothing holds the data it would answer from.
27
27
  */
28
28
  export declare function simAthenaTableObjects(s3: Partial<SimAthenaTableObjects> | undefined): SimAthenaTableObjects | undefined;
29
- /** One object's bytes, read as UTF-8 text. */
29
+ /** One object's bytes, decompressed by its key and read as UTF-8 text. */
30
30
  export declare function simAthenaObjectText(objects: SimAthenaTableObjects, bucket: string, key: string, caller: SimAwsCaller | undefined): Promise<string>;
@@ -1,3 +1,4 @@
1
+ import { simAthenaDecompressedBytes } from "./sim-athena-object-codec.js";
1
2
  /**
2
3
  * Whether this simulated S3 can open an object as well as list one.
3
4
  *
@@ -9,9 +10,10 @@ export function simAthenaTableObjects(s3) {
9
10
  ? undefined
10
11
  : s3;
11
12
  }
12
- /** One object's bytes, read as UTF-8 text. */
13
+ /** One object's bytes, decompressed by its key and read as UTF-8 text. */
13
14
  export async function simAthenaObjectText(objects, bucket, key, caller) {
14
15
  const got = await objects.getObject({ input: { Bucket: bucket, Key: key } }, caller === undefined ? undefined : { caller });
15
16
  const chunks = await Array.fromAsync(got.Body ?? []);
16
- return new TextDecoder().decode(Buffer.concat(chunks));
17
+ const bytes = simAthenaDecompressedBytes(key, Buffer.concat(chunks));
18
+ return new TextDecoder().decode(bytes);
17
19
  }
@@ -34,5 +34,7 @@ export declare function anEngineSimulation(clock?: SimClock): Promise<SimAthenaE
34
34
  export declare function aCatalogTable(simAws: SimAws, table: SimAthenaEngineTableInput): void;
35
35
  /** Put one object of literal text under the logs Bucket. */
36
36
  export declare function aSeededObject(simAws: SimAws, key: string, body: string): Promise<void>;
37
+ /** Put one object of raw bytes under the logs Bucket. */
38
+ export declare function aSeededBytes(simAws: SimAws, key: string, body: Uint8Array): Promise<void>;
37
39
  /** Put one object of JSON lines under the logs Bucket. */
38
40
  export declare function aSeededJson(simAws: SimAws, key: string, records: readonly Record<string, unknown>[]): Promise<void>;
@@ -58,6 +58,12 @@ export async function aSeededObject(simAws, key, body) {
58
58
  .s3()
59
59
  .putObject({ input: { Bucket: logsBucket, Key: key, Body: body } });
60
60
  }
61
+ /** Put one object of raw bytes under the logs Bucket. */
62
+ export async function aSeededBytes(simAws, key, body) {
63
+ await simAws
64
+ .s3()
65
+ .putObject({ input: { Bucket: logsBucket, Key: key, Body: body } });
66
+ }
61
67
  /** Put one object of JSON lines under the logs Bucket. */
62
68
  export async function aSeededJson(simAws, key, records) {
63
69
  const lines = records.map((record) => JSON.stringify(record));
@@ -20,6 +20,7 @@ interface SimAwsAccountServiceCacheProperties {
20
20
  readonly route53Registry: SimRoute53Registry;
21
21
  readonly kmsRegistry: SimKmsRegistry;
22
22
  readonly serviceHosts: SimAwsServiceHosts;
23
+ readonly shadowableServiceHosts: SimAwsServiceHosts;
23
24
  readonly iamRegistry: SimIamRegistry;
24
25
  readonly accessKeyRegistry: SimIamAccessKeyRegistry;
25
26
  }
@@ -54,6 +55,7 @@ export declare class SimAwsAccountServiceCache {
54
55
  private readonly route53Registry;
55
56
  private readonly kmsRegistry;
56
57
  private readonly serviceHosts;
58
+ private readonly shadowableServiceHosts;
57
59
  /**
58
60
  * Shared memo for account-scoped service instances.
59
61
  *
@@ -40,6 +40,7 @@ export class SimAwsAccountServiceCache {
40
40
  route53Registry;
41
41
  kmsRegistry;
42
42
  serviceHosts;
43
+ shadowableServiceHosts;
43
44
  /**
44
45
  * Shared memo for account-scoped service instances.
45
46
  *
@@ -58,6 +59,7 @@ export class SimAwsAccountServiceCache {
58
59
  this.route53Registry = properties.route53Registry;
59
60
  this.kmsRegistry = properties.kmsRegistry;
60
61
  this.serviceHosts = properties.serviceHosts;
62
+ this.shadowableServiceHosts = properties.shadowableServiceHosts;
61
63
  }
62
64
  /**
63
65
  * The collaborators every Account/Region scoped service takes.
@@ -128,6 +130,7 @@ export class SimAwsAccountServiceCache {
128
130
  background: this.background,
129
131
  route53Registry: this.route53Registry,
130
132
  serviceHosts: this.serviceHosts,
133
+ shadowableServiceHosts: this.shadowableServiceHosts,
131
134
  // A DNSSEC key-signing key names a KMS key by ARN, in whichever
132
135
  // Region holds it, so Route53 resolves it through the registry.
133
136
  kmsKeys: this.kmsRegistry,
@@ -94,6 +94,13 @@ export declare class SimAwsScopedServiceRegistries {
94
94
  * simulated DNS resolution asks after the built-in hostname shapes.
95
95
  */
96
96
  readonly serviceHosts: SimAwsServiceHosts;
97
+ /**
98
+ * Where a hostname a hosted-zone record outranks is looked up. An API
99
+ * Gateway custom domain name is reached through a record on AWS, so a record
100
+ * for one decides where the name goes and the domain answers where no record
101
+ * names it.
102
+ */
103
+ readonly shadowableServiceHosts: SimAwsServiceHosts;
97
104
  /**
98
105
  * The hostnames the domain registries have handed out. A public hostname is
99
106
  * unique across the whole of AWS rather than within one service, so the two
@@ -95,6 +95,13 @@ export class SimAwsScopedServiceRegistries {
95
95
  * simulated DNS resolution asks after the built-in hostname shapes.
96
96
  */
97
97
  serviceHosts;
98
+ /**
99
+ * Where a hostname a hosted-zone record outranks is looked up. An API
100
+ * Gateway custom domain name is reached through a record on AWS, so a record
101
+ * for one decides where the name goes and the domain answers where no record
102
+ * names it.
103
+ */
104
+ shadowableServiceHosts;
98
105
  /**
99
106
  * The hostnames the domain registries have handed out. A public hostname is
100
107
  * unique across the whole of AWS rather than within one service, so the two
@@ -104,8 +111,8 @@ export class SimAwsScopedServiceRegistries {
104
111
  constructor() {
105
112
  this.cognitoDomains = new SimCognitoDomainRegistry(this.hostnameClaims);
106
113
  this.httpApiDomains = new SimHttpApiDomainRegistry(this.hostnameClaims);
107
- this.serviceHosts = new SimAwsAnyServiceHosts([
108
- this.cognitoDomains,
114
+ this.serviceHosts = new SimAwsAnyServiceHosts([this.cognitoDomains]);
115
+ this.shadowableServiceHosts = new SimAwsAnyServiceHosts([
109
116
  this.httpApiDomains,
110
117
  ]);
111
118
  }
@@ -88,8 +88,10 @@ export class SimAwsServiceFactory {
88
88
  kmsRegistry: this.registries.kms,
89
89
  // A Cognito hosted domain and an API Gateway custom domain each answer
90
90
  // on a hostname of their own, so DNS resolution asks the registries
91
- // holding those domains about it.
91
+ // holding those domains about it. A record outranks the API Gateway one,
92
+ // as it does on AWS, which is what keeps the two apart here.
92
93
  serviceHosts: this.registries.serviceHosts,
94
+ shadowableServiceHosts: this.registries.shadowableServiceHosts,
93
95
  iamRegistry: this.iamRegistry,
94
96
  accessKeyRegistry: this.requestAuth.accessKeyRegistry,
95
97
  });
@@ -19,10 +19,10 @@ export declare class SimHttpApiDomainNameCfn implements SimCfnResourceValueAdapt
19
19
  * AWS::ApiGatewayV2::DomainName publishes RegionalDomainName,
20
20
  * RegionalHostedZoneId and DomainNameArn.
21
21
  *
22
- * `RegionalDomainName` is the hostname the domain answers on, so a
23
- * CloudFront Origin or a Route53 alias built on it reaches the domain. Real
24
- * API Gateway answers with a separate `d-<id>.execute-api.<region>` name and
25
- * expects DNS to point the custom domain at it.
22
+ * `RegionalDomainName` is the `d-<id>.execute-api.<region>` endpoint API
23
+ * Gateway issued the domain, which is the published address a CloudFront
24
+ * Origin reaches directly and a Route53 alias points the custom domain name
25
+ * at.
26
26
  */
27
27
  attributeValue(attributeName: string): SimCfnTemplateValue;
28
28
  }
@@ -18,10 +18,10 @@ export class SimHttpApiDomainNameCfn {
18
18
  * AWS::ApiGatewayV2::DomainName publishes RegionalDomainName,
19
19
  * RegionalHostedZoneId and DomainNameArn.
20
20
  *
21
- * `RegionalDomainName` is the hostname the domain answers on, so a
22
- * CloudFront Origin or a Route53 alias built on it reaches the domain. Real
23
- * API Gateway answers with a separate `d-<id>.execute-api.<region>` name and
24
- * expects DNS to point the custom domain at it.
21
+ * `RegionalDomainName` is the `d-<id>.execute-api.<region>` endpoint API
22
+ * Gateway issued the domain, which is the published address a CloudFront
23
+ * Origin reaches directly and a Route53 alias points the custom domain name
24
+ * at.
25
25
  */
26
26
  attributeValue(attributeName) {
27
27
  if (attributeName === "RegionalDomainName") {
@@ -24,6 +24,18 @@ export declare class SimRoute53ServiceTargetResolver {
24
24
  * name that simulated AWS services expose.
25
25
  */
26
26
  resolve(hostname: string): SimAwsServiceTarget | undefined;
27
+ /**
28
+ * Resolve the regional endpoint hostname of an API Gateway custom domain:
29
+ *
30
+ * d-<id>.execute-api.<region>.amazonaws.com
31
+ *
32
+ * This is read ahead of the endpoints below, because a domain endpoint and
33
+ * the endpoint of an API have the same shape and only the `d-` prefix tells
34
+ * them apart. The whole hostname is the resource name, since the domain
35
+ * registry answers about both of the names a domain has and the custom
36
+ * domain name is a hostname too.
37
+ */
38
+ private apiGatewayDomainServiceTarget;
27
39
  /**
28
40
  * Resolve Yulin's own Route53 introspection hostname.
29
41
  *
@@ -1,3 +1,4 @@
1
+ import { readSimHttpApiDomainEndpointHost } from "../../apigatewayv2/domain/sim-http-api-domain-endpoint.js";
1
2
  import { readSimElbV2LoadBalancerHost } from "../../elbv2/load-balancer/sim-elbv2-load-balancer-host.js";
2
3
  import { simRoute53LogicalName } from "../local-name/sim-route53-local-name.js";
3
4
  import { simRoute53DnsHostName } from "../serve/sim-route53-dns-host.js";
@@ -38,8 +39,31 @@ export class SimRoute53ServiceTargetResolver {
38
39
  this.s3Targets.resolve(logicalName) ??
39
40
  this.cloudFrontServiceTarget(logicalName) ??
40
41
  this.loadBalancerServiceTarget(logicalName) ??
42
+ this.apiGatewayDomainServiceTarget(logicalName) ??
41
43
  this.regionalTargets.resolve(logicalName));
42
44
  }
45
+ /**
46
+ * Resolve the regional endpoint hostname of an API Gateway custom domain:
47
+ *
48
+ * d-<id>.execute-api.<region>.amazonaws.com
49
+ *
50
+ * This is read ahead of the endpoints below, because a domain endpoint and
51
+ * the endpoint of an API have the same shape and only the `d-` prefix tells
52
+ * them apart. The whole hostname is the resource name, since the domain
53
+ * registry answers about both of the names a domain has and the custom
54
+ * domain name is a hostname too.
55
+ */
56
+ apiGatewayDomainServiceTarget(logicalName) {
57
+ const endpoint = readSimHttpApiDomainEndpointHost(logicalName);
58
+ if (endpoint === undefined) {
59
+ return undefined;
60
+ }
61
+ return {
62
+ service: "apiGatewayDomain",
63
+ resourceName: endpoint.logicalHost,
64
+ regionName: endpoint.regionName,
65
+ };
66
+ }
43
67
  /**
44
68
  * Resolve Yulin's own Route53 introspection hostname.
45
69
  *
@@ -8,6 +8,11 @@ interface SimRoute53ResolverProperties {
8
8
  * such as a Cognito user pool custom domain.
9
9
  */
10
10
  readonly serviceHosts?: SimAwsServiceHosts | undefined;
11
+ /**
12
+ * Where a hostname a hosted-zone record outranks is looked up, which is a
13
+ * claim answering only where no record names the hostname.
14
+ */
15
+ readonly shadowableServiceHosts?: SimAwsServiceHosts | undefined;
11
16
  }
12
17
  /**
13
18
  * Resolves Yulin-local hostnames through simulated Route53 hosted-zone records.
@@ -16,6 +21,7 @@ export declare class SimRoute53Resolver {
16
21
  private readonly recordFinder;
17
22
  private readonly serviceTargetResolver;
18
23
  private readonly serviceHosts;
24
+ private readonly shadowableServiceHosts;
19
25
  constructor(properties: SimRoute53ResolverProperties);
20
26
  /**
21
27
  * Resolve an HTTP hostname to a simulated AWS service target.
@@ -26,7 +32,9 @@ export declare class SimRoute53Resolver {
26
32
  * logical Route53 name.
27
33
  * 2. Check whether that name already points at a built-in simulated service.
28
34
  * 3. If not, follow CNAME or alias records to the next hostname.
29
- * 4. Stop when a service target is found, a chain breaks, a cycle appears, or
35
+ * 4. Where no record names it, answer with a hostname a resource claimed for
36
+ * itself, such as an API Gateway custom domain.
37
+ * 5. Stop when a service target is found, a chain breaks, a cycle appears, or
30
38
  * the maximum CNAME depth is reached.
31
39
  *
32
40
  * The suffix is optional because the simulated DNS server answers for logical
@@ -10,9 +10,12 @@ export class SimRoute53Resolver {
10
10
  recordFinder;
11
11
  serviceTargetResolver = new SimRoute53ServiceTargetResolver();
12
12
  serviceHosts;
13
+ shadowableServiceHosts;
13
14
  constructor(properties) {
14
15
  this.recordFinder = new SimRoute53HostedZoneRecordFinder(properties.hostedZones);
15
16
  this.serviceHosts = properties.serviceHosts ?? new SimAwsNoServiceHosts();
17
+ this.shadowableServiceHosts =
18
+ properties.shadowableServiceHosts ?? new SimAwsNoServiceHosts();
16
19
  }
17
20
  /**
18
21
  * Resolve an HTTP hostname to a simulated AWS service target.
@@ -23,7 +26,9 @@ export class SimRoute53Resolver {
23
26
  * logical Route53 name.
24
27
  * 2. Check whether that name already points at a built-in simulated service.
25
28
  * 3. If not, follow CNAME or alias records to the next hostname.
26
- * 4. Stop when a service target is found, a chain breaks, a cycle appears, or
29
+ * 4. Where no record names it, answer with a hostname a resource claimed for
30
+ * itself, such as an API Gateway custom domain.
31
+ * 5. Stop when a service target is found, a chain breaks, a cycle appears, or
27
32
  * the maximum CNAME depth is reached.
28
33
  *
29
34
  * The suffix is optional because the simulated DNS server answers for logical
@@ -63,7 +68,10 @@ export class SimRoute53Resolver {
63
68
  const nextRecord = this.findNextResolutionRecord(logicalName);
64
69
  const nextTarget = nextRecord?.values[0];
65
70
  if (nextTarget === undefined || nextTarget.length === 0) {
66
- return undefined;
71
+ // An API Gateway custom domain is reached through a record on AWS, so
72
+ // the record decides where the name goes and the claim answers only
73
+ // where no record names it.
74
+ return this.shadowableServiceHosts.targetForHost(logicalName);
67
75
  }
68
76
  localName = simRoute53LocalName(nextTarget);
69
77
  }
@@ -23,6 +23,12 @@ interface SimRoute53Properties {
23
23
  * one arrives at the service holding it.
24
24
  */
25
25
  readonly serviceHosts?: SimAwsServiceHosts | undefined;
26
+ /**
27
+ * Where a hostname a hosted-zone record outranks is looked up, such as an
28
+ * API Gateway custom domain, which is reached through a record on AWS. A
29
+ * claim there answers only where no record names the hostname.
30
+ */
31
+ readonly shadowableServiceHosts?: SimAwsServiceHosts | undefined;
26
32
  /**
27
33
  * Where a KMS key ARN is looked up. A DNSSEC key-signing key is built on a
28
34
  * customer managed key, and Route53 checks that key before it takes one.
@@ -24,6 +24,7 @@ export class SimRoute53 {
24
24
  this.resolver = new SimRoute53Resolver({
25
25
  hostedZones: route53Registry.hostedZones,
26
26
  serviceHosts: properties.serviceHosts,
27
+ shadowableServiceHosts: properties.shadowableServiceHosts,
27
28
  });
28
29
  this.commands = new SimRoute53Commands({
29
30
  hostedZones: this.hostedZones,
@@ -35,7 +35,7 @@ export interface SimS3MountFilesystemOptions {
35
35
  * the web's own types, and nothing else — so that pointing a Bucket at a
36
36
  * directory cannot be talked into reading whatever else is in it. A site
37
37
  * serving a data file of its own needs its extension named here: a pinyin
38
- * dictionary's `.freq`, a `.wasm` module, a `.pdf`.
38
+ * dictionary's `.freq`, a `.wasm` module, a `.parquet` table.
39
39
  *
40
40
  * Added to the list rather than replacing it, and a leading dot is optional.
41
41
  */
@@ -61,6 +61,7 @@ export const defaultAllowedObjectFileExtensions = new Set([
61
61
  ".map",
62
62
  ".mjs",
63
63
  ".otf",
64
+ ".pdf",
64
65
  ".png",
65
66
  ".svg",
66
67
  ".ttc",
@@ -41,6 +41,7 @@ const contentTypesByExtension = new Map([
41
41
  [".mjs", "text/javascript"],
42
42
  [".json", "application/json"],
43
43
  [".map", "application/json"],
44
+ [".pdf", "application/pdf"],
44
45
  [".png", "image/png"],
45
46
  [".otf", "font/otf"],
46
47
  [".svg", "image/svg+xml"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kensio/yulin",
3
- "version": "1.20.9",
3
+ "version": "1.20.11",
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/",