blogwright-analytics 0.3.3
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.
- package/README.md +162 -0
- package/dist/adapters/duckdb-ingest.d.ts +76 -0
- package/dist/adapters/duckdb-ingest.js +173 -0
- package/dist/adapters/duckdb-query.d.ts +56 -0
- package/dist/adapters/duckdb-query.js +80 -0
- package/dist/adapters/duckdb-session.d.ts +168 -0
- package/dist/adapters/duckdb-session.js +330 -0
- package/dist/app/_app/immutable/assets/0.BTQrrh5B.css +1 -0
- package/dist/app/_app/immutable/assets/2.CZSK3rT8.css +1 -0
- package/dist/app/_app/immutable/assets/BrushContext.D7c8UPey.css +1 -0
- package/dist/app/_app/immutable/assets/ChartAnnotations.CPxIG7Mw.css +1 -0
- package/dist/app/_app/immutable/assets/Circle.C5MKzgk2.css +1 -0
- package/dist/app/_app/immutable/assets/DefaultTooltip.C5-uctZ7.css +1 -0
- package/dist/app/_app/immutable/assets/Group.DV48xipa.css +1 -0
- package/dist/app/_app/immutable/assets/Labels.BxZ4NUVz.css +1 -0
- package/dist/app/_app/immutable/assets/Legend.CxnrE4Ye.css +1 -0
- package/dist/app/_app/immutable/assets/Line.fkmsECm9.css +1 -0
- package/dist/app/_app/immutable/assets/Path.CvpwNZ6g.css +1 -0
- package/dist/app/_app/immutable/assets/Rect.CtRaGMmQ.css +1 -0
- package/dist/app/_app/immutable/assets/Text.j9l35qB0.css +1 -0
- package/dist/app/_app/immutable/assets/TransformContext.Bs_HkpAk.css +1 -0
- package/dist/app/_app/immutable/assets/Voronoi.ce7atosu.css +1 -0
- package/dist/app/_app/immutable/chunks/-aNGNaBT.js +1 -0
- package/dist/app/_app/immutable/chunks/6djn-yLs.js +1 -0
- package/dist/app/_app/immutable/chunks/B1amyutE.js +1 -0
- package/dist/app/_app/immutable/chunks/B3vZDoek.js +1 -0
- package/dist/app/_app/immutable/chunks/B5KRA4hC.js +1 -0
- package/dist/app/_app/immutable/chunks/BClnVG6H.js +1 -0
- package/dist/app/_app/immutable/chunks/BID1NNRh.js +1 -0
- package/dist/app/_app/immutable/chunks/BR2LaRms.js +1 -0
- package/dist/app/_app/immutable/chunks/Bd1gDe3Y.js +1 -0
- package/dist/app/_app/immutable/chunks/Bjy-W4x2.js +81 -0
- package/dist/app/_app/immutable/chunks/Bl052uUt.js +1 -0
- package/dist/app/_app/immutable/chunks/Bye3lL0c.js +1 -0
- package/dist/app/_app/immutable/chunks/C58PZtCD.js +4 -0
- package/dist/app/_app/immutable/chunks/CAzydqEO.js +1 -0
- package/dist/app/_app/immutable/chunks/CCch3uox.js +1 -0
- package/dist/app/_app/immutable/chunks/CIlSMUH9.js +1 -0
- package/dist/app/_app/immutable/chunks/CO1vUXfR.js +1 -0
- package/dist/app/_app/immutable/chunks/CPbD8C65.js +5 -0
- package/dist/app/_app/immutable/chunks/CRTcXoMo.js +1 -0
- package/dist/app/_app/immutable/chunks/CjjyIQAO.js +1 -0
- package/dist/app/_app/immutable/chunks/CuXAxjvF.js +1 -0
- package/dist/app/_app/immutable/chunks/CvyVA_jC.js +1 -0
- package/dist/app/_app/immutable/chunks/CxGCFVdy.js +1 -0
- package/dist/app/_app/immutable/chunks/D0Ty6LN0.js +1 -0
- package/dist/app/_app/immutable/chunks/D2AaQUUW.js +1 -0
- package/dist/app/_app/immutable/chunks/D2BnX0Uk.js +3 -0
- package/dist/app/_app/immutable/chunks/DJc8C0NK.js +1 -0
- package/dist/app/_app/immutable/chunks/DKMlMI4a.js +1 -0
- package/dist/app/_app/immutable/chunks/DVXZkpbf.js +1 -0
- package/dist/app/_app/immutable/chunks/DVt8ukQ_.js +1 -0
- package/dist/app/_app/immutable/chunks/DZPlYdq_.js +1 -0
- package/dist/app/_app/immutable/chunks/Db0q5_zr.js +1 -0
- package/dist/app/_app/immutable/chunks/Dfvzj6n2.js +1 -0
- package/dist/app/_app/immutable/chunks/Dh958be7.js +1 -0
- package/dist/app/_app/immutable/chunks/DjKLLdnY.js +15 -0
- package/dist/app/_app/immutable/chunks/Doz7YX1W.js +1 -0
- package/dist/app/_app/immutable/chunks/DthYhn6Y.js +2 -0
- package/dist/app/_app/immutable/chunks/DtuTIrAM.js +1 -0
- package/dist/app/_app/immutable/chunks/HclGiUj8.js +1 -0
- package/dist/app/_app/immutable/chunks/Hx0TNsV3.js +1 -0
- package/dist/app/_app/immutable/chunks/RobXhXPM.js +1 -0
- package/dist/app/_app/immutable/chunks/V9ZjaxiY.js +1 -0
- package/dist/app/_app/immutable/chunks/Y5urAfNy.js +1 -0
- package/dist/app/_app/immutable/chunks/caXkbKD3.js +1 -0
- package/dist/app/_app/immutable/chunks/devYm2ud.js +1 -0
- package/dist/app/_app/immutable/chunks/mtZWP0zR.js +1 -0
- package/dist/app/_app/immutable/chunks/vDgBJUjM.js +1 -0
- package/dist/app/_app/immutable/chunks/xIq_fFFM.js +1 -0
- package/dist/app/_app/immutable/chunks/xihTtKlq.js +1 -0
- package/dist/app/_app/immutable/chunks/z05MoCFz.js +1 -0
- package/dist/app/_app/immutable/entry/app.CLAerUAN.js +2 -0
- package/dist/app/_app/immutable/entry/start.D3MqnNci.js +1 -0
- package/dist/app/_app/immutable/nodes/0.UTMEigHJ.js +1 -0
- package/dist/app/_app/immutable/nodes/1.Cn4f11bT.js +1 -0
- package/dist/app/_app/immutable/nodes/2.B39cIcr2.js +6 -0
- package/dist/app/_app/version.json +1 -0
- package/dist/app/index.html +82 -0
- package/dist/aws/clients.d.ts +70 -0
- package/dist/aws/clients.js +52 -0
- package/dist/aws/errors.d.ts +41 -0
- package/dist/aws/errors.js +70 -0
- package/dist/aws/firehose.d.ts +228 -0
- package/dist/aws/firehose.js +347 -0
- package/dist/aws/glue.d.ts +103 -0
- package/dist/aws/glue.js +225 -0
- package/dist/aws/lambda.d.ts +132 -0
- package/dist/aws/lambda.js +339 -0
- package/dist/aws/s3tables.d.ts +120 -0
- package/dist/aws/s3tables.js +281 -0
- package/dist/backfill.d.ts +100 -0
- package/dist/backfill.js +294 -0
- package/dist/commands.d.ts +124 -0
- package/dist/commands.js +336 -0
- package/dist/config.d.ts +162 -0
- package/dist/config.js +317 -0
- package/dist/fixture-ingest.d.ts +49 -0
- package/dist/fixture-ingest.js +43 -0
- package/dist/fixture-query.d.ts +39 -0
- package/dist/fixture-query.js +70 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +35 -0
- package/dist/nodes.d.ts +404 -0
- package/dist/nodes.js +2708 -0
- package/dist/paths.d.ts +45 -0
- package/dist/paths.js +47 -0
- package/dist/plugin.d.ts +102 -0
- package/dist/plugin.js +248 -0
- package/dist/ports.d.ts +113 -0
- package/dist/ports.js +35 -0
- package/dist/queries.d.ts +301 -0
- package/dist/queries.js +414 -0
- package/dist/schema.d.ts +240 -0
- package/dist/schema.js +154 -0
- package/dist/server.d.ts +150 -0
- package/dist/server.js +499 -0
- package/dist/transform/bots.d.ts +47 -0
- package/dist/transform/bots.js +73 -0
- package/dist/transform/handler.d.ts +135 -0
- package/dist/transform/handler.js +177 -0
- package/dist/transform/map-record.d.ts +110 -0
- package/dist/transform/map-record.js +275 -0
- package/dist/transform/visitor-key.d.ts +83 -0
- package/dist/transform/visitor-key.js +120 -0
- package/dist/transform-bundle/index.mjs +21456 -0
- package/dist/transform-bundle/transform-manifest.json +4 -0
- package/dist/transform-hash.d.ts +135 -0
- package/dist/transform-hash.js +186 -0
- package/dist/write-transform-manifest.mjs +365 -0
- package/package.json +59 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { type SigningClient } from 'blogwright-core';
|
|
2
|
+
/**
|
|
3
|
+
* A catalog federation as this plugin reads it, mapped out of Glue's `Catalog` shape
|
|
4
|
+
* so the catalog-integration node decides whether to adopt without re-reading the raw
|
|
5
|
+
* response. `sourceIdentifier` and `connectionName` are what make that decision
|
|
6
|
+
* possible: a catalog of the right name that is not federated at all, or federated
|
|
7
|
+
* somewhere else, is not something to adopt, and both keys are absent in that case.
|
|
8
|
+
*/
|
|
9
|
+
export interface CatalogFederation {
|
|
10
|
+
/** The catalog's name (`s3tablescatalog` for this plugin's federation). */
|
|
11
|
+
readonly name: string;
|
|
12
|
+
/**
|
|
13
|
+
* The catalog's own ARN (`Catalog.ResourceArn`), in the
|
|
14
|
+
* `arn:aws:glue:<region>:<account-id>:catalog/<name>` form. Empty string when the
|
|
15
|
+
* service reports none.
|
|
16
|
+
*/
|
|
17
|
+
readonly resourceArn: string;
|
|
18
|
+
/**
|
|
19
|
+
* `FederatedCatalog.Identifier` - the S3 Tables resource the catalog federates, as
|
|
20
|
+
* `arn:aws:s3tables:<region>:<account-id>:bucket/*`. Undefined when the catalog
|
|
21
|
+
* exists but carries no `FederatedCatalog` at all.
|
|
22
|
+
*/
|
|
23
|
+
readonly sourceIdentifier: string | undefined;
|
|
24
|
+
/** `FederatedCatalog.ConnectionName` - `aws:s3tables` for this federation. Undefined when the catalog is not federated. */
|
|
25
|
+
readonly connectionName: string | undefined;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* AWS Glue Data Catalog client for the S3 Tables federation, over the shared SigV4
|
|
29
|
+
* transport.
|
|
30
|
+
*
|
|
31
|
+
* Both narrowings below use `AwsError`'s predicates unmodified, unlike this module's
|
|
32
|
+
* two siblings, and that is a property of the service rather than luck: Glue is
|
|
33
|
+
* AWS-JSON, so its error body carries `{"__type":"EntityNotFoundException",...}` and
|
|
34
|
+
* core's `parseError` (`packages/core/src/aws/signer.ts`) reads a real exception name
|
|
35
|
+
* into `AwsError.code` - where `s3tables.ts` gets `Http<status>` because S3 Tables
|
|
36
|
+
* puts the name in an `x-amzn-ErrorType` header the parser never reads. And Glue's
|
|
37
|
+
* names happen to fall inside both patterns: `EntityNotFoundException` matches
|
|
38
|
+
* `isNotFound`'s `/NotFound/i` (`packages/core/src/aws/errors.ts:24`), while both
|
|
39
|
+
* `AlreadyExistsException` *and* `FederatedResourceAlreadyExistsException` - the two
|
|
40
|
+
* distinct duplicates `CreateCatalog` documents - match `isAlreadyExists`'s
|
|
41
|
+
* `/AlreadyExists/i` (`errors.ts:32`). So no local predicate is needed here and core's
|
|
42
|
+
* regex, which the site's own bootstrap shares, is left alone.
|
|
43
|
+
*
|
|
44
|
+
* `statusCode` is no help either way and is never narrowed on: every Glue exception is
|
|
45
|
+
* HTTP 400 except `InternalServiceException` at 500, so `isNotFound`'s `=== 404` limb
|
|
46
|
+
* and any `=== 409` limb are dead on this service - the code is the only signal. Glue
|
|
47
|
+
* also returns its request id only in the `x-amzn-requestid` header, which
|
|
48
|
+
* `parseError` does not read, so `AwsError.requestId` is always `undefined` here.
|
|
49
|
+
*/
|
|
50
|
+
export declare class GlueClient {
|
|
51
|
+
private readonly client;
|
|
52
|
+
constructor(client: SigningClient);
|
|
53
|
+
private call;
|
|
54
|
+
/**
|
|
55
|
+
* Create the `s3tablescatalog` federation over an S3 Tables bucket resource.
|
|
56
|
+
* `tableBucketArn` is the `FederatedCatalog.Identifier`, and AWS's integration
|
|
57
|
+
* procedure passes the account-and-region wildcard
|
|
58
|
+
* `arn:aws:s3tables:<region>:<account-id>:bucket/*` there: one catalog federates
|
|
59
|
+
* every table bucket in the account and Region, mounting each as a child catalog.
|
|
60
|
+
* This client sends whatever the caller passes rather than building the wildcard
|
|
61
|
+
* itself - the node owns the account id and Region.
|
|
62
|
+
*
|
|
63
|
+
* Idempotent: an already-existing federation resolves rather than throwing, because
|
|
64
|
+
* the integration is **account-and-region-scoped shared state**. Two blogwright
|
|
65
|
+
* environments in one account share the single `s3tablescatalog`, as does anything
|
|
66
|
+
* else in that account that enabled the integration, so a second environment's
|
|
67
|
+
* bootstrap must adopt what is there instead of fighting over it. Both duplicate
|
|
68
|
+
* exceptions are covered: `AlreadyExistsException` when a catalog of that name
|
|
69
|
+
* exists, and `FederatedResourceAlreadyExistsException` when the federation itself
|
|
70
|
+
* is already registered (which is what a second run against a console-enabled
|
|
71
|
+
* account hits).
|
|
72
|
+
*
|
|
73
|
+
* Nothing else is swallowed. In particular `EntityNotFoundException`, which
|
|
74
|
+
* `CreateCatalog` also documents, means something entirely different here than it
|
|
75
|
+
* does on `getCatalogFederation` - not "no such catalog" but "the entity this
|
|
76
|
+
* catalog would federate does not exist", i.e. the S3 Tables resource in
|
|
77
|
+
* `tableBucketArn` is wrong. Treating that as success would report a federation that
|
|
78
|
+
* was never created.
|
|
79
|
+
*
|
|
80
|
+
* Returns `void`: `CreateCatalog`'s success response is an empty body, so there is
|
|
81
|
+
* nothing to return, and a caller that needs the catalog's ARN reads it back with
|
|
82
|
+
* `getCatalogFederation` - which is the call it made before creating anyway.
|
|
83
|
+
*/
|
|
84
|
+
createCatalogFederation(name: string, tableBucketArn: string): Promise<void>;
|
|
85
|
+
/**
|
|
86
|
+
* Look the federation up by catalog name; `undefined` when it does not exist, so the
|
|
87
|
+
* catalog-integration node adopts an existing federation instead of creating one, in
|
|
88
|
+
* the `packages/core/src/aws/secretsmanager.ts:78-89` shape.
|
|
89
|
+
*
|
|
90
|
+
* The name *is* the id: `GetCatalog` is keyed by `CatalogId`, and for a catalog
|
|
91
|
+
* created directly under the account that id is the catalog's own name (AWS's
|
|
92
|
+
* verification step for the integration is literally
|
|
93
|
+
* `aws glue get-catalog --catalog-id s3tablescatalog`). It takes no bucket ARN -
|
|
94
|
+
* `GetCatalogRequest` has exactly one member.
|
|
95
|
+
*
|
|
96
|
+
* Only `EntityNotFoundException` reads as absent. `FederationSourceException` in
|
|
97
|
+
* particular is rethrown even though its `FederationSourceErrorCode` can itself be
|
|
98
|
+
* `EntityNotFoundException`: `parseError` reads the outer `__type`, so `code` is the
|
|
99
|
+
* wrapper name, and a federation source that failed is a broken federation to
|
|
100
|
+
* surface, not a missing one to silently re-create.
|
|
101
|
+
*/
|
|
102
|
+
getCatalogFederation(name: string): Promise<CatalogFederation | undefined>;
|
|
103
|
+
}
|
package/dist/aws/glue.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { AwsError } from 'blogwright-core';
|
|
2
|
+
import { rethrowWithContext } from './errors.js';
|
|
3
|
+
/**
|
|
4
|
+
* AWS Glue Data Catalog client, cut down to the one thing this plugin needs: the
|
|
5
|
+
* `s3tablescatalog` federation Firehose writes the `page_views` table through.
|
|
6
|
+
* Firehose reaches an S3 Tables table only via a Glue catalog
|
|
7
|
+
* (`IcebergDestinationConfiguration.CatalogConfiguration.CatalogARN`), never via
|
|
8
|
+
* the S3 Tables API directly, so without this federation the delivery stream has
|
|
9
|
+
* nothing to point at. It exposes exactly two operations - create the federation
|
|
10
|
+
* and look one up - because `analytics-catalog-integration` is a read-then-adopt
|
|
11
|
+
* node and needs nothing else; no update, no delete (the change spec makes that
|
|
12
|
+
* node's `delete()` a no-op, since the federation is account-and-region scoped
|
|
13
|
+
* shared state that other environments and other tools also depend on).
|
|
14
|
+
*
|
|
15
|
+
* It lives in `blogwright-analytics`, not in core: core's `SIGNING_NAMES` gains no
|
|
16
|
+
* `glue` key, and every request signs through the `{ service: 'glue', signingName:
|
|
17
|
+
* 'glue' }` descriptor the plugin transport seam accepts (see
|
|
18
|
+
* `packages/core/src/aws/endpoint.ts`'s `ServiceDescriptor`), which resolves to the
|
|
19
|
+
* canonical `glue.<region>.amazonaws.com` host.
|
|
20
|
+
*
|
|
21
|
+
* Protocol: AWS JSON 1.1 - `POST /` with `content-type: application/x-amz-json-1.1`
|
|
22
|
+
* and an `x-amz-target: AWSGlue.<Operation>` header, exactly as
|
|
23
|
+
* `packages/core/src/aws/secretsmanager.ts` and `logs.ts` do for their services.
|
|
24
|
+
* `AWSGlue` is the service's own target prefix (`glue-2017-03-31`'s `targetPrefix`),
|
|
25
|
+
* and both operations are `POST /` despite Glue's reference calling them "Get" and
|
|
26
|
+
* "Create".
|
|
27
|
+
*
|
|
28
|
+
* Operation names and body keys below are verified against the Glue API reference
|
|
29
|
+
* (`CreateCatalog`, `GetCatalog`, and the `CatalogInput`, `FederatedCatalog`,
|
|
30
|
+
* `PrincipalPermissions` and `Catalog` shapes they nest) and against AWS's own
|
|
31
|
+
* documented S3 Tables integration procedure. No SDK validates them here and a
|
|
32
|
+
* transport-mocked test can only assert the body this module itself builds, so the
|
|
33
|
+
* reference is the only thing that catches a wrong or missing key - and here a wrong
|
|
34
|
+
* key produces a *silently misconfigured federation*, not an error: `CatalogInput`
|
|
35
|
+
* has no required members at all, so a `CreateCatalog` that omitted
|
|
36
|
+
* `FederatedCatalog` entirely would return HTTP 200 and leave an empty non-federated
|
|
37
|
+
* catalog behind under the right name, which `getCatalogFederation` would then
|
|
38
|
+
* happily adopt.
|
|
39
|
+
*
|
|
40
|
+
* Two easy-to-miss spellings are pinned deliberately:
|
|
41
|
+
* - `Name` is a *sibling* of `CatalogInput`, not a member of it. `CatalogInput`
|
|
42
|
+
* carries no name; putting one inside it is silently ignored.
|
|
43
|
+
* - `CatalogNameString`'s pattern is `^(?!(.*[.\/\\]|aws:)).*$` - a catalog name may
|
|
44
|
+
* not start with `aws:`. That restriction does *not* apply to
|
|
45
|
+
* `FederatedCatalog.ConnectionName`, which is exactly `aws:s3tables`; the two are
|
|
46
|
+
* different fields with different patterns.
|
|
47
|
+
*
|
|
48
|
+
* The floci emulator does not implement this service, so it is covered by transport
|
|
49
|
+
* mocks in tests.
|
|
50
|
+
*/
|
|
51
|
+
const SERVICE = { service: 'glue', signingName: 'glue' };
|
|
52
|
+
/** The service's AWS-JSON target prefix; every `x-amz-target` is `${TARGET}.<Operation>`. */
|
|
53
|
+
const TARGET = 'AWSGlue';
|
|
54
|
+
/**
|
|
55
|
+
* The AWS-managed connection that federates S3 Tables into the Data Catalog, sent as
|
|
56
|
+
* `FederatedCatalog.ConnectionName`. Hard-coded rather than a parameter because this
|
|
57
|
+
* client exists only for the S3 Tables federation; any other connection would make
|
|
58
|
+
* `createCatalogFederation` a different operation. `FederatedCatalog.ConnectionType`
|
|
59
|
+
* is deliberately not sent - AWS's documented S3 Tables integration omits it, and the
|
|
60
|
+
* connection name alone identifies the source.
|
|
61
|
+
*/
|
|
62
|
+
const S3_TABLES_CONNECTION = 'aws:s3tables';
|
|
63
|
+
/**
|
|
64
|
+
* The default permissions that put the federated catalog under **IAM access control**
|
|
65
|
+
* rather than Lake Formation grants, sent as both
|
|
66
|
+
* `CatalogInput.CreateDatabaseDefaultPermissions` and `CreateTableDefaultPermissions`.
|
|
67
|
+
*
|
|
68
|
+
* Load-bearing, not decoration. `CatalogInput` marks both fields optional, so omitting
|
|
69
|
+
* them succeeds - and leaves the catalog under Lake Formation control, where every
|
|
70
|
+
* read and write needs a grant this plugin never provisions. Firehose's delivery role
|
|
71
|
+
* would be denied on write and the change spec's whole "requires `s3tables`
|
|
72
|
+
* permissions but no Lake Formation grant" assumption would fail, with the only
|
|
73
|
+
* symptom a stream quietly routing every record to the error bucket. AWS's own S3
|
|
74
|
+
* Tables integration procedure sets both to `IAM_ALLOWED_PRINCIPALS` with `ALL` for
|
|
75
|
+
* exactly this reason.
|
|
76
|
+
*
|
|
77
|
+
* `AllowFullTableExternalDataAccess` is deliberately *not* sent alongside them. It
|
|
78
|
+
* only opens Lake-Formation-registered S3 locations to third-party engines, and the
|
|
79
|
+
* one third-party reader here (DuckDB) attaches through S3 Tables' own endpoint, not
|
|
80
|
+
* through Glue credential vending - so setting it would widen access this pipeline
|
|
81
|
+
* never uses. Note if it is ever needed that it is a **string** enum, `"True"` or
|
|
82
|
+
* `"False"`, not a boolean.
|
|
83
|
+
*/
|
|
84
|
+
const IAM_DEFAULT_PERMISSIONS = [
|
|
85
|
+
{ Principal: { DataLakePrincipalIdentifier: 'IAM_ALLOWED_PRINCIPALS' }, Permissions: ['ALL'] },
|
|
86
|
+
];
|
|
87
|
+
/**
|
|
88
|
+
* Map `GetCatalog`'s response onto the domain value.
|
|
89
|
+
*
|
|
90
|
+
* A 200 is the existence signal: `GetCatalogResponse.Catalog` is optional in the
|
|
91
|
+
* service model, but a 200 carrying no catalog is neither documented nor observed, so
|
|
92
|
+
* an absent field falls back rather than inventing a second "does not exist" answer
|
|
93
|
+
* next to `EntityNotFoundException` - the same call this module's siblings make
|
|
94
|
+
* (`s3tables.ts`'s `normalizeTable`, `firehose.ts`'s `describeDeliveryStream`).
|
|
95
|
+
*/
|
|
96
|
+
function normalizeCatalog(out, fallbackName) {
|
|
97
|
+
const catalog = out.Catalog;
|
|
98
|
+
return {
|
|
99
|
+
name: catalog?.Name ?? fallbackName,
|
|
100
|
+
resourceArn: catalog?.ResourceArn ?? '',
|
|
101
|
+
sourceIdentifier: catalog?.FederatedCatalog?.Identifier,
|
|
102
|
+
connectionName: catalog?.FederatedCatalog?.ConnectionName,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* AWS Glue Data Catalog client for the S3 Tables federation, over the shared SigV4
|
|
107
|
+
* transport.
|
|
108
|
+
*
|
|
109
|
+
* Both narrowings below use `AwsError`'s predicates unmodified, unlike this module's
|
|
110
|
+
* two siblings, and that is a property of the service rather than luck: Glue is
|
|
111
|
+
* AWS-JSON, so its error body carries `{"__type":"EntityNotFoundException",...}` and
|
|
112
|
+
* core's `parseError` (`packages/core/src/aws/signer.ts`) reads a real exception name
|
|
113
|
+
* into `AwsError.code` - where `s3tables.ts` gets `Http<status>` because S3 Tables
|
|
114
|
+
* puts the name in an `x-amzn-ErrorType` header the parser never reads. And Glue's
|
|
115
|
+
* names happen to fall inside both patterns: `EntityNotFoundException` matches
|
|
116
|
+
* `isNotFound`'s `/NotFound/i` (`packages/core/src/aws/errors.ts:24`), while both
|
|
117
|
+
* `AlreadyExistsException` *and* `FederatedResourceAlreadyExistsException` - the two
|
|
118
|
+
* distinct duplicates `CreateCatalog` documents - match `isAlreadyExists`'s
|
|
119
|
+
* `/AlreadyExists/i` (`errors.ts:32`). So no local predicate is needed here and core's
|
|
120
|
+
* regex, which the site's own bootstrap shares, is left alone.
|
|
121
|
+
*
|
|
122
|
+
* `statusCode` is no help either way and is never narrowed on: every Glue exception is
|
|
123
|
+
* HTTP 400 except `InternalServiceException` at 500, so `isNotFound`'s `=== 404` limb
|
|
124
|
+
* and any `=== 409` limb are dead on this service - the code is the only signal. Glue
|
|
125
|
+
* also returns its request id only in the `x-amzn-requestid` header, which
|
|
126
|
+
* `parseError` does not read, so `AwsError.requestId` is always `undefined` here.
|
|
127
|
+
*/
|
|
128
|
+
export class GlueClient {
|
|
129
|
+
client;
|
|
130
|
+
constructor(client) {
|
|
131
|
+
this.client = client;
|
|
132
|
+
}
|
|
133
|
+
async call(op, payload) {
|
|
134
|
+
const res = await this.client.send({
|
|
135
|
+
service: SERVICE,
|
|
136
|
+
method: 'POST',
|
|
137
|
+
path: '/',
|
|
138
|
+
headers: {
|
|
139
|
+
'content-type': 'application/x-amz-json-1.1',
|
|
140
|
+
'x-amz-target': `${TARGET}.${op}`,
|
|
141
|
+
},
|
|
142
|
+
body: JSON.stringify(payload),
|
|
143
|
+
});
|
|
144
|
+
const text = res.text();
|
|
145
|
+
return (text ? JSON.parse(text) : {});
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Create the `s3tablescatalog` federation over an S3 Tables bucket resource.
|
|
149
|
+
* `tableBucketArn` is the `FederatedCatalog.Identifier`, and AWS's integration
|
|
150
|
+
* procedure passes the account-and-region wildcard
|
|
151
|
+
* `arn:aws:s3tables:<region>:<account-id>:bucket/*` there: one catalog federates
|
|
152
|
+
* every table bucket in the account and Region, mounting each as a child catalog.
|
|
153
|
+
* This client sends whatever the caller passes rather than building the wildcard
|
|
154
|
+
* itself - the node owns the account id and Region.
|
|
155
|
+
*
|
|
156
|
+
* Idempotent: an already-existing federation resolves rather than throwing, because
|
|
157
|
+
* the integration is **account-and-region-scoped shared state**. Two blogwright
|
|
158
|
+
* environments in one account share the single `s3tablescatalog`, as does anything
|
|
159
|
+
* else in that account that enabled the integration, so a second environment's
|
|
160
|
+
* bootstrap must adopt what is there instead of fighting over it. Both duplicate
|
|
161
|
+
* exceptions are covered: `AlreadyExistsException` when a catalog of that name
|
|
162
|
+
* exists, and `FederatedResourceAlreadyExistsException` when the federation itself
|
|
163
|
+
* is already registered (which is what a second run against a console-enabled
|
|
164
|
+
* account hits).
|
|
165
|
+
*
|
|
166
|
+
* Nothing else is swallowed. In particular `EntityNotFoundException`, which
|
|
167
|
+
* `CreateCatalog` also documents, means something entirely different here than it
|
|
168
|
+
* does on `getCatalogFederation` - not "no such catalog" but "the entity this
|
|
169
|
+
* catalog would federate does not exist", i.e. the S3 Tables resource in
|
|
170
|
+
* `tableBucketArn` is wrong. Treating that as success would report a federation that
|
|
171
|
+
* was never created.
|
|
172
|
+
*
|
|
173
|
+
* Returns `void`: `CreateCatalog`'s success response is an empty body, so there is
|
|
174
|
+
* nothing to return, and a caller that needs the catalog's ARN reads it back with
|
|
175
|
+
* `getCatalogFederation` - which is the call it made before creating anyway.
|
|
176
|
+
*/
|
|
177
|
+
async createCatalogFederation(name, tableBucketArn) {
|
|
178
|
+
try {
|
|
179
|
+
await this.call('CreateCatalog', {
|
|
180
|
+
Name: name,
|
|
181
|
+
CatalogInput: {
|
|
182
|
+
FederatedCatalog: {
|
|
183
|
+
Identifier: tableBucketArn,
|
|
184
|
+
ConnectionName: S3_TABLES_CONNECTION,
|
|
185
|
+
},
|
|
186
|
+
CreateDatabaseDefaultPermissions: IAM_DEFAULT_PERMISSIONS,
|
|
187
|
+
CreateTableDefaultPermissions: IAM_DEFAULT_PERMISSIONS,
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
if (err instanceof AwsError && err.isAlreadyExists)
|
|
193
|
+
return;
|
|
194
|
+
rethrowWithContext(err, 'createCatalogFederation', name);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Look the federation up by catalog name; `undefined` when it does not exist, so the
|
|
199
|
+
* catalog-integration node adopts an existing federation instead of creating one, in
|
|
200
|
+
* the `packages/core/src/aws/secretsmanager.ts:78-89` shape.
|
|
201
|
+
*
|
|
202
|
+
* The name *is* the id: `GetCatalog` is keyed by `CatalogId`, and for a catalog
|
|
203
|
+
* created directly under the account that id is the catalog's own name (AWS's
|
|
204
|
+
* verification step for the integration is literally
|
|
205
|
+
* `aws glue get-catalog --catalog-id s3tablescatalog`). It takes no bucket ARN -
|
|
206
|
+
* `GetCatalogRequest` has exactly one member.
|
|
207
|
+
*
|
|
208
|
+
* Only `EntityNotFoundException` reads as absent. `FederationSourceException` in
|
|
209
|
+
* particular is rethrown even though its `FederationSourceErrorCode` can itself be
|
|
210
|
+
* `EntityNotFoundException`: `parseError` reads the outer `__type`, so `code` is the
|
|
211
|
+
* wrapper name, and a federation source that failed is a broken federation to
|
|
212
|
+
* surface, not a missing one to silently re-create.
|
|
213
|
+
*/
|
|
214
|
+
async getCatalogFederation(name) {
|
|
215
|
+
try {
|
|
216
|
+
const out = await this.call('GetCatalog', { CatalogId: name });
|
|
217
|
+
return normalizeCatalog(out, name);
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
if (err instanceof AwsError && err.isNotFound)
|
|
221
|
+
return undefined;
|
|
222
|
+
rethrowWithContext(err, 'getCatalogFederation', name);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { type SigningClient } from 'blogwright-core';
|
|
2
|
+
/**
|
|
3
|
+
* The version-specific settings shared by `CreateFunction` and
|
|
4
|
+
* `UpdateFunctionConfiguration` - the two operations that carry exactly these keys,
|
|
5
|
+
* one nested in a create body and one as the whole body of the configuration PUT.
|
|
6
|
+
* Expressed once so the pair cannot drift.
|
|
7
|
+
*/
|
|
8
|
+
export interface FunctionConfigurationInput {
|
|
9
|
+
/** ARN of the function's execution role (`Role`). */
|
|
10
|
+
readonly roleArn: string;
|
|
11
|
+
/** Runtime identifier, e.g. `nodejs22.x` (`Runtime`). */
|
|
12
|
+
readonly runtime: string;
|
|
13
|
+
/** Entry point, `<file>.<export>` for a Node runtime (`Handler`). */
|
|
14
|
+
readonly handler: string;
|
|
15
|
+
/** Memory available to the function in **MB** (`MemorySize`; 128-32768, any multiple of 1 MB). */
|
|
16
|
+
readonly memoryMb: number;
|
|
17
|
+
/** Maximum run time in **seconds** (`Timeout`; the service caps it at 900). */
|
|
18
|
+
readonly timeoutSeconds: number;
|
|
19
|
+
/**
|
|
20
|
+
* Environment variables (`Environment.Variables`). Absent and empty mean different
|
|
21
|
+
* things to the service on an update and are sent differently: omitting the key
|
|
22
|
+
* leaves the function's existing variables untouched, while an explicit map
|
|
23
|
+
* replaces them wholesale - so an empty map *clears* them.
|
|
24
|
+
*/
|
|
25
|
+
readonly environment?: Record<string, string> | undefined;
|
|
26
|
+
}
|
|
27
|
+
/** Everything `CreateFunction` needs on top of the shared configuration. */
|
|
28
|
+
export interface CreateFunctionInput extends FunctionConfigurationInput {
|
|
29
|
+
/** The function's name (`FunctionName`), also the name every other operation addresses it by. */
|
|
30
|
+
readonly name: string;
|
|
31
|
+
/**
|
|
32
|
+
* The deployment package as raw zip bytes, sent inline. This client base64-encodes
|
|
33
|
+
* them into the wire's blob member itself - that translation is its concern, not
|
|
34
|
+
* its callers'. Inline rather than an S3 code location because Lambda requires the
|
|
35
|
+
* code bucket to sit in the function's own region while the site's bucket is in
|
|
36
|
+
* `config.region` and this function is pinned to us-east-1; the node that reads the
|
|
37
|
+
* bundle owns the size guard against the inline-payload limit.
|
|
38
|
+
*/
|
|
39
|
+
readonly zipFile: Uint8Array;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A function's lifecycle state in this repo's vocabulary rather than the service's
|
|
43
|
+
* mixed case, mirroring how `firehose.ts` maps `DeliveryStreamStatus`. It is not
|
|
44
|
+
* decoration: a function that exists but is `failed` is not something the
|
|
45
|
+
* transform-function node should adopt as reconciled (the pipeline would then route
|
|
46
|
+
* every record to the error bucket with nothing surfacing as an error), and an update
|
|
47
|
+
* against a `pending` function is answered with a 409. `unknown` covers a state the
|
|
48
|
+
* service adds later - reporting an unrecognised state as unknown is honest, where
|
|
49
|
+
* mapping it onto one of the eight would not be.
|
|
50
|
+
*/
|
|
51
|
+
export type FunctionState = 'pending' | 'active' | 'inactive' | 'failed' | 'deactivating' | 'deactivated' | 'active-non-invocable' | 'deleting' | 'unknown';
|
|
52
|
+
/** The narrow view of `GetFunction` the transform-function node needs. */
|
|
53
|
+
export interface LambdaFunction {
|
|
54
|
+
readonly name: string;
|
|
55
|
+
/** The function's ARN (`Configuration.FunctionArn`). Empty string when the service reports none. */
|
|
56
|
+
readonly arn: string;
|
|
57
|
+
readonly state: FunctionState;
|
|
58
|
+
}
|
|
59
|
+
/** AWS Lambda function client, over the shared SigV4 transport. */
|
|
60
|
+
export declare class LambdaClient {
|
|
61
|
+
private readonly client;
|
|
62
|
+
constructor(client: SigningClient);
|
|
63
|
+
private call;
|
|
64
|
+
/**
|
|
65
|
+
* Create the function with its code and configuration in one request. Idempotent:
|
|
66
|
+
* a function of the same name already existing is not an error (see
|
|
67
|
+
* `isFunctionAlreadyExists`). Its code and configuration are *not* reconciled
|
|
68
|
+
* against `input` on that path - that is what `updateFunctionCode` and
|
|
69
|
+
* `updateFunctionConfiguration` are for, and the node that owns the function
|
|
70
|
+
* decides between them on its recorded source hash.
|
|
71
|
+
*
|
|
72
|
+
* A 404 is deliberately **not** swallowed here even though `getFunction` and
|
|
73
|
+
* `deleteFunction` read one as absence. On this operation
|
|
74
|
+
* `ResourceNotFoundException` does not mean "no such function" - the function is
|
|
75
|
+
* the thing being created - it means the execution role in `roleArn` does not
|
|
76
|
+
* exist. Treating it as success would report a function that was never created,
|
|
77
|
+
* the same asymmetry `glue.ts`'s `createCatalogFederation` documents for
|
|
78
|
+
* `EntityNotFoundException`.
|
|
79
|
+
*
|
|
80
|
+
* Returns `void`, discarding the response's `FunctionArn`: it is unavailable on the
|
|
81
|
+
* already-exists path (the error body carries no ARN), so returning it from only
|
|
82
|
+
* one of the two branches would be a false economy. A caller needing the ARN either
|
|
83
|
+
* reads it back with `getFunction` - the call it made before creating anyway - or
|
|
84
|
+
* builds the fixed `arn:aws:lambda:<region>:<account-id>:function:<name>` form from
|
|
85
|
+
* what a `PluginContext` already carries, exactly as `s3tables.ts`'s
|
|
86
|
+
* `createTableBucket` argues.
|
|
87
|
+
*
|
|
88
|
+
* Tags are not accepted. The transform-function node does not tag the function, and
|
|
89
|
+
* tagging it later is `TagResource`, a separate operation this client can grow when
|
|
90
|
+
* a node needs it, rather than a parameter carried unused today.
|
|
91
|
+
*/
|
|
92
|
+
createFunction(input: CreateFunctionInput): Promise<void>;
|
|
93
|
+
/**
|
|
94
|
+
* The function's name, ARN and lifecycle state; `undefined` when no such function
|
|
95
|
+
* exists, so the transform-function node's `read` reports absence without throwing
|
|
96
|
+
* and its `create` runs instead. Lambda answers an absent function with
|
|
97
|
+
* `ResourceNotFoundException` at HTTP 404, which `AwsError.isNotFound` matches on
|
|
98
|
+
* its status limb.
|
|
99
|
+
*/
|
|
100
|
+
getFunction(name: string): Promise<LambdaFunction | undefined>;
|
|
101
|
+
/**
|
|
102
|
+
* Replace the function's deployment package. The zip bytes go on the wire as a
|
|
103
|
+
* **top-level** `ZipFile`, not nested under `Code` the way `CreateFunction` takes
|
|
104
|
+
* them - the two operations do not share a body shape.
|
|
105
|
+
*
|
|
106
|
+
* Nothing is swallowed. A 409 here means a previous update has not finished, not
|
|
107
|
+
* that the code is already what was sent, and a 404 means the function is gone -
|
|
108
|
+
* both are the node's to see.
|
|
109
|
+
*/
|
|
110
|
+
updateFunctionCode(name: string, zipFile: Uint8Array): Promise<void>;
|
|
111
|
+
/**
|
|
112
|
+
* Replace the function's version-specific settings - role, runtime, handler, memory,
|
|
113
|
+
* timeout and environment. The name travels in the path, never in the body, which
|
|
114
|
+
* is why this takes it as a separate argument rather than reusing
|
|
115
|
+
* `CreateFunctionInput`.
|
|
116
|
+
*
|
|
117
|
+
* Nothing is swallowed, for the same reasons as `updateFunctionCode`.
|
|
118
|
+
*/
|
|
119
|
+
updateFunctionConfiguration(name: string, input: FunctionConfigurationInput): Promise<void>;
|
|
120
|
+
/**
|
|
121
|
+
* Delete the function and every version and alias of it. No-op when it does not
|
|
122
|
+
* exist, so teardown is re-runnable.
|
|
123
|
+
*
|
|
124
|
+
* Every other failure is rethrown with context - including the 409
|
|
125
|
+
* `ResourceConflictException`, which on this operation means the function is
|
|
126
|
+
* mid-operation and cannot be deleted yet, not that it is already gone. Swallowing
|
|
127
|
+
* it (by reusing `isFunctionAlreadyExists` here) would report a live function as
|
|
128
|
+
* torn down, leaving the Firehose stream pointing at a processor the teardown
|
|
129
|
+
* claimed to have removed.
|
|
130
|
+
*/
|
|
131
|
+
deleteFunction(name: string): Promise<void>;
|
|
132
|
+
}
|