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,339 @@
|
|
|
1
|
+
import { AwsError } from 'blogwright-core';
|
|
2
|
+
import { rethrowWithContext } from './errors.js';
|
|
3
|
+
/**
|
|
4
|
+
* AWS Lambda control-plane client for the *standard* function API - create, get,
|
|
5
|
+
* update (code and configuration) and delete for the one function this plugin owns,
|
|
6
|
+
* the record-transform Lambda that Firehose runs over every CloudFront log record.
|
|
7
|
+
*
|
|
8
|
+
* **Its relationship to core's `MicrovmsClient`
|
|
9
|
+
* (`packages/core/src/aws/microvms.ts`).** The two clients share a host and a SigV4
|
|
10
|
+
* signing name, and share nothing else. Both resolve to
|
|
11
|
+
* `lambda.<region>.amazonaws.com` and both sign as `lambda` - Lambda MicroVMs is
|
|
12
|
+
* served off the standard Lambda endpoint - and what separates them is the path
|
|
13
|
+
* prefix. `MicrovmsClient` owns `/2025-09-09/` (`microvm-images`, `microvms`,
|
|
14
|
+
* `auth-token`) and owns it exclusively: not one request this module issues carries
|
|
15
|
+
* that prefix, which `lambda.test.ts` asserts over every recorded URL of every
|
|
16
|
+
* operation. This module owns `/2015-03-31/functions`, the function API that has
|
|
17
|
+
* been Lambda's since 2015. Neither module's `PATHS` table can collide with the
|
|
18
|
+
* other's, and neither needs to know the other exists at runtime.
|
|
19
|
+
*
|
|
20
|
+
* It lives in `blogwright-analytics`, not in core: core's `SIGNING_NAMES` gains no
|
|
21
|
+
* `lambda` key (`microvms` is already keyed there and maps to the `lambda` signing
|
|
22
|
+
* name), and every request here signs through the `{ service: 'lambda', signingName:
|
|
23
|
+
* 'lambda' }` descriptor the plugin transport seam accepts (see
|
|
24
|
+
* `packages/core/src/aws/endpoint.ts`'s `ServiceDescriptor`), which resolves to the
|
|
25
|
+
* canonical `lambda.<region>.amazonaws.com` host through `canonicalHost`'s default
|
|
26
|
+
* `${service}.${region}.amazonaws.com` branch.
|
|
27
|
+
*
|
|
28
|
+
* Protocol: REST-JSON - a method and a path per operation with
|
|
29
|
+
* `content-type: application/json`, the shape `packages/core/src/aws/microvms.ts`
|
|
30
|
+
* and `s3tables.ts` next door use, not the AWS-JSON `POST /` + `x-amz-target` of the
|
|
31
|
+
* `firehose.ts` and `glue.ts` siblings. Methods and paths below are verified
|
|
32
|
+
* operation by operation against the Lambda API reference; two of them are easy to
|
|
33
|
+
* get wrong and are pinned by their own tests:
|
|
34
|
+
*
|
|
35
|
+
* - The deployment package is nested as `Code.ZipFile` in `CreateFunction`'s body
|
|
36
|
+
* but is a **top-level** `ZipFile` in `UpdateFunctionCode`'s. The two operations
|
|
37
|
+
* do not share a body shape, and a create-shaped body sent to `/code` would set no
|
|
38
|
+
* code at all.
|
|
39
|
+
* - `GetFunction`'s response nests the function's configuration under
|
|
40
|
+
* `Configuration` (beside `Code`, `Tags` and `Concurrency`), while
|
|
41
|
+
* `CreateFunction`, `UpdateFunctionCode` and `UpdateFunctionConfiguration` return a
|
|
42
|
+
* `FunctionConfiguration` at the top level. Reading `FunctionArn` off the root of a
|
|
43
|
+
* `GetFunction` response yields `undefined`, not an error.
|
|
44
|
+
*
|
|
45
|
+
* `Qualifier` is deliberately never sent, so every operation addresses the
|
|
46
|
+
* unpublished `$LATEST` version. Nothing in this pipeline publishes a version or an
|
|
47
|
+
* alias, and `Publish` is likewise never sent: a published version would freeze code
|
|
48
|
+
* and configuration that the transform-function node reconciles in place.
|
|
49
|
+
*
|
|
50
|
+
* **Error regime (rest-json, header-only exception names).** This service is in the
|
|
51
|
+
* same regime as the `s3tables.ts` sibling and not the `firehose.ts`/`glue.ts` ones.
|
|
52
|
+
* Lambda puts the exception name in an `x-amzn-ErrorType` header and its request id
|
|
53
|
+
* in `x-amzn-RequestId`, and its error *body* carries at most `{"Type": ...,
|
|
54
|
+
* "message": ...}` - where `Type` is the category, `"User"` or `"Service"`, never the
|
|
55
|
+
* exception name, and is absent altogether on the unmodelled failures. Core's `parseError` (`packages/core/src/aws/signer.ts`) reads a code from the
|
|
56
|
+
* body alone (`__type`/`code`/`Code`), so for every Lambda failure `AwsError.code`
|
|
57
|
+
* arrives as `"Http<status>"` and `AwsError.requestId` is always `undefined`. Probed
|
|
58
|
+
* against the live endpoint, not read off a documentation page: an unauthenticated
|
|
59
|
+
* `GET https://lambda.us-east-1.amazonaws.com/2015-03-31/functions/<name>` answers
|
|
60
|
+
* `403` with `x-amzn-errortype: MissingAuthenticationTokenException`,
|
|
61
|
+
* `x-amzn-requestid: <uuid>` and a body of exactly
|
|
62
|
+
* `{"message":"Missing Authentication Token"}`.
|
|
63
|
+
*
|
|
64
|
+
* The status code is what survives, and unlike S3 Tables' single generic
|
|
65
|
+
* `ConflictException` the statuses here are genuinely discriminating - each of the
|
|
66
|
+
* two this client narrows on belongs to exactly one exception:
|
|
67
|
+
*
|
|
68
|
+
* | status | exception | meaning on this client's operations |
|
|
69
|
+
* | ------ | ----------------------------- | ----------------------------------- |
|
|
70
|
+
* | 404 | `ResourceNotFoundException` | the function is absent - *except on create*, where it is the execution role (or a layer, or a code-signing config) that is absent |
|
|
71
|
+
* | 409 | `ResourceConflictException` | "already exists" on create; "another operation is in progress" on update and delete |
|
|
72
|
+
* | 412 | `PreconditionFailedException` | a `RevisionId` mismatch; unreachable here, no `RevisionId` is ever sent |
|
|
73
|
+
* | 400 | `InvalidParameterValueException`, `CodeStorageExceededException`, `CodeVerificationFailedException`, `InvalidCodeSignatureException` | |
|
|
74
|
+
* | 429 | `TooManyRequestsException` | |
|
|
75
|
+
* | 500 | `ServiceException` | |
|
|
76
|
+
*
|
|
77
|
+
* So `AwsError.isNotFound` works on this service through its `statusCode === 404`
|
|
78
|
+
* limb (its `code` regex never matches `"Http404"`), while `isAlreadyExists` matches
|
|
79
|
+
* nothing at all and `isFunctionAlreadyExists` below bridges the gap. Both
|
|
80
|
+
* overloadings in the table above are load-bearing and are handled where they arise:
|
|
81
|
+
* see `createFunction` for the 404 one and `deleteFunction` for the 409 one.
|
|
82
|
+
*
|
|
83
|
+
* The durable fix is core-level and deliberately not made here (this task must not
|
|
84
|
+
* touch `packages/core`): `parseError` should read `x-amzn-errortype` and
|
|
85
|
+
* `x-amzn-requestid` from the headers it already receives, which would hand this
|
|
86
|
+
* client a real exception name and a request id to quote to AWS support. It is
|
|
87
|
+
* recorded as a follow-up in the plan's open questions and would subsume both this
|
|
88
|
+
* module's 409 limb and `s3tables.ts`'s.
|
|
89
|
+
*
|
|
90
|
+
* The floci emulator does not implement this service, so it is covered by transport
|
|
91
|
+
* mocks in tests.
|
|
92
|
+
*/
|
|
93
|
+
const SERVICE = { service: 'lambda', signingName: 'lambda' };
|
|
94
|
+
/**
|
|
95
|
+
* The standard Lambda function API's date prefix. Distinct from
|
|
96
|
+
* `packages/core/src/aws/microvms.ts`'s `API = '/2025-09-09'`, which that module owns
|
|
97
|
+
* and this one never emits.
|
|
98
|
+
*/
|
|
99
|
+
const API = '/2015-03-31';
|
|
100
|
+
const PATHS = {
|
|
101
|
+
functions: `${API}/functions`,
|
|
102
|
+
// The function name is percent-encoded into its path segment the way
|
|
103
|
+
// `microvms.ts:17` encodes an image id. Lambda's own name pattern would not need
|
|
104
|
+
// it, but the encoding is what keeps a caller-supplied name inside its segment
|
|
105
|
+
// rather than letting it address a different operation.
|
|
106
|
+
function: (name) => `${API}/functions/${encodeURIComponent(name)}`,
|
|
107
|
+
functionCode: (name) => `${API}/functions/${encodeURIComponent(name)}/code`,
|
|
108
|
+
functionConfiguration: (name) => `${API}/functions/${encodeURIComponent(name)}/configuration`,
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* The only `PackageType` this client deploys. Sent explicitly on create rather than
|
|
112
|
+
* relying on the service default, so the request states the packaging model its
|
|
113
|
+
* `Code.ZipFile` and its `Handler`/`Runtime` depend on - the same call `firehose.ts`
|
|
114
|
+
* makes for `DeliveryStreamType`.
|
|
115
|
+
*/
|
|
116
|
+
const PACKAGE_TYPE = 'Zip';
|
|
117
|
+
function toFunctionState(state) {
|
|
118
|
+
switch (state) {
|
|
119
|
+
case 'Pending':
|
|
120
|
+
return 'pending';
|
|
121
|
+
case 'Active':
|
|
122
|
+
return 'active';
|
|
123
|
+
case 'Inactive':
|
|
124
|
+
return 'inactive';
|
|
125
|
+
case 'Failed':
|
|
126
|
+
return 'failed';
|
|
127
|
+
case 'Deactivating':
|
|
128
|
+
return 'deactivating';
|
|
129
|
+
case 'Deactivated':
|
|
130
|
+
return 'deactivated';
|
|
131
|
+
case 'ActiveNonInvocable':
|
|
132
|
+
return 'active-non-invocable';
|
|
133
|
+
case 'Deleting':
|
|
134
|
+
return 'deleting';
|
|
135
|
+
default:
|
|
136
|
+
return 'unknown';
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Map `GetFunction`'s response onto the domain value. A 200 is the existence signal:
|
|
141
|
+
* `Configuration` is optional in the service model, but a 200 carrying none is
|
|
142
|
+
* neither documented nor observed, so an absent field falls back rather than
|
|
143
|
+
* inventing a second "does not exist" answer next to `ResourceNotFoundException` -
|
|
144
|
+
* the same call `glue.ts`'s `normalizeCatalog` and `s3tables.ts`'s `normalizeTable`
|
|
145
|
+
* make.
|
|
146
|
+
*/
|
|
147
|
+
function normalizeFunction(out, fallbackName) {
|
|
148
|
+
const configuration = out.Configuration;
|
|
149
|
+
return {
|
|
150
|
+
name: configuration?.FunctionName ?? fallbackName,
|
|
151
|
+
arn: configuration?.FunctionArn ?? '',
|
|
152
|
+
state: toFunctionState(configuration?.State),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/** Encode the deployment package for the wire's blob member; the one place the base64 step is spelled out. */
|
|
156
|
+
function encodeZip(zipFile) {
|
|
157
|
+
return Buffer.from(zipFile).toString('base64');
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Build the version-specific settings shared by `CreateFunction` and
|
|
161
|
+
* `UpdateFunctionConfiguration`. `Environment` is omitted entirely when the input
|
|
162
|
+
* carries none, which on the update path leaves the function's existing variables
|
|
163
|
+
* alone; an explicit map (empty included) is sent as `{ Variables }` and replaces
|
|
164
|
+
* them.
|
|
165
|
+
*/
|
|
166
|
+
function configurationBody(input) {
|
|
167
|
+
return {
|
|
168
|
+
Role: input.roleArn,
|
|
169
|
+
Runtime: input.runtime,
|
|
170
|
+
Handler: input.handler,
|
|
171
|
+
MemorySize: input.memoryMb,
|
|
172
|
+
Timeout: input.timeoutSeconds,
|
|
173
|
+
...(input.environment !== undefined ? { Environment: { Variables: input.environment } } : {}),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* True when a `CreateFunction` failure means the function already exists, so a re-run
|
|
178
|
+
* of the create is a no-op rather than an error.
|
|
179
|
+
*
|
|
180
|
+
* The `statusCode === 409` limb is what actually does the work, and it is not
|
|
181
|
+
* belt-and-braces. Lambda answers a duplicate function name with
|
|
182
|
+
* `ResourceConflictException`, whose only status is 409 and which is the only Lambda
|
|
183
|
+
* exception at 409 - but the name never reaches `AwsError.code`, because it travels
|
|
184
|
+
* in the `x-amzn-ErrorType` header and core's `parseError` reads the body alone (see
|
|
185
|
+
* the module doc comment). So `code` is `"Http409"`, `AwsError.isAlreadyExists` -
|
|
186
|
+
* which tests `code` against `/AlreadyExists|BucketAlreadyOwnedByYou|EntityAlreadyExists|Conflict/i`
|
|
187
|
+
* - matches nothing, and a create written against it alone would report every
|
|
188
|
+
* re-bootstrap as a failure. This is a header-parsing gap and not, as in
|
|
189
|
+
* `firehose.ts`, a predicate-breadth one: `ResourceConflictException` *would* match
|
|
190
|
+
* core's regex on its `Conflict` alternative the day `parseError` learns to read the
|
|
191
|
+
* header, which is why `isAlreadyExists` is kept as the first limb rather than
|
|
192
|
+
* replaced.
|
|
193
|
+
*
|
|
194
|
+
* Named for the create path rather than as a general `isAlreadyExists` because the
|
|
195
|
+
* same exception means something else on every other operation. The service documents
|
|
196
|
+
* it as "the resource already exists, **or another operation is in progress**": on
|
|
197
|
+
* `DeleteFunction` a 409 means the function is mid-update and cannot be deleted yet,
|
|
198
|
+
* and on the two update operations it means a previous update has not finished. This
|
|
199
|
+
* predicate is applied to `createFunction` and nowhere else, so those three surface
|
|
200
|
+
* as failures instead of being reported as a torn-down or reconciled function.
|
|
201
|
+
*/
|
|
202
|
+
function isFunctionAlreadyExists(err) {
|
|
203
|
+
return err instanceof AwsError && (err.isAlreadyExists || err.statusCode === 409);
|
|
204
|
+
}
|
|
205
|
+
/** AWS Lambda function client, over the shared SigV4 transport. */
|
|
206
|
+
export class LambdaClient {
|
|
207
|
+
client;
|
|
208
|
+
constructor(client) {
|
|
209
|
+
this.client = client;
|
|
210
|
+
}
|
|
211
|
+
async call(method, path, payload) {
|
|
212
|
+
const res = await this.client.send({
|
|
213
|
+
service: SERVICE,
|
|
214
|
+
method,
|
|
215
|
+
path,
|
|
216
|
+
headers: { 'content-type': 'application/json' },
|
|
217
|
+
...(payload !== undefined ? { body: JSON.stringify(payload) } : {}),
|
|
218
|
+
});
|
|
219
|
+
const text = res.text();
|
|
220
|
+
return (text ? JSON.parse(text) : {});
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Create the function with its code and configuration in one request. Idempotent:
|
|
224
|
+
* a function of the same name already existing is not an error (see
|
|
225
|
+
* `isFunctionAlreadyExists`). Its code and configuration are *not* reconciled
|
|
226
|
+
* against `input` on that path - that is what `updateFunctionCode` and
|
|
227
|
+
* `updateFunctionConfiguration` are for, and the node that owns the function
|
|
228
|
+
* decides between them on its recorded source hash.
|
|
229
|
+
*
|
|
230
|
+
* A 404 is deliberately **not** swallowed here even though `getFunction` and
|
|
231
|
+
* `deleteFunction` read one as absence. On this operation
|
|
232
|
+
* `ResourceNotFoundException` does not mean "no such function" - the function is
|
|
233
|
+
* the thing being created - it means the execution role in `roleArn` does not
|
|
234
|
+
* exist. Treating it as success would report a function that was never created,
|
|
235
|
+
* the same asymmetry `glue.ts`'s `createCatalogFederation` documents for
|
|
236
|
+
* `EntityNotFoundException`.
|
|
237
|
+
*
|
|
238
|
+
* Returns `void`, discarding the response's `FunctionArn`: it is unavailable on the
|
|
239
|
+
* already-exists path (the error body carries no ARN), so returning it from only
|
|
240
|
+
* one of the two branches would be a false economy. A caller needing the ARN either
|
|
241
|
+
* reads it back with `getFunction` - the call it made before creating anyway - or
|
|
242
|
+
* builds the fixed `arn:aws:lambda:<region>:<account-id>:function:<name>` form from
|
|
243
|
+
* what a `PluginContext` already carries, exactly as `s3tables.ts`'s
|
|
244
|
+
* `createTableBucket` argues.
|
|
245
|
+
*
|
|
246
|
+
* Tags are not accepted. The transform-function node does not tag the function, and
|
|
247
|
+
* tagging it later is `TagResource`, a separate operation this client can grow when
|
|
248
|
+
* a node needs it, rather than a parameter carried unused today.
|
|
249
|
+
*/
|
|
250
|
+
async createFunction(input) {
|
|
251
|
+
try {
|
|
252
|
+
await this.call('POST', PATHS.functions, {
|
|
253
|
+
FunctionName: input.name,
|
|
254
|
+
PackageType: PACKAGE_TYPE,
|
|
255
|
+
// Nested under `Code` here; `UpdateFunctionCode` takes the same bytes as a
|
|
256
|
+
// top-level `ZipFile` instead.
|
|
257
|
+
Code: { ZipFile: encodeZip(input.zipFile) },
|
|
258
|
+
...configurationBody(input),
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
if (isFunctionAlreadyExists(err))
|
|
263
|
+
return;
|
|
264
|
+
rethrowWithContext(err, 'createFunction', input.name);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* The function's name, ARN and lifecycle state; `undefined` when no such function
|
|
269
|
+
* exists, so the transform-function node's `read` reports absence without throwing
|
|
270
|
+
* and its `create` runs instead. Lambda answers an absent function with
|
|
271
|
+
* `ResourceNotFoundException` at HTTP 404, which `AwsError.isNotFound` matches on
|
|
272
|
+
* its status limb.
|
|
273
|
+
*/
|
|
274
|
+
async getFunction(name) {
|
|
275
|
+
try {
|
|
276
|
+
const out = await this.call('GET', PATHS.function(name));
|
|
277
|
+
return normalizeFunction(out, name);
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
if (err instanceof AwsError && err.isNotFound)
|
|
281
|
+
return undefined;
|
|
282
|
+
rethrowWithContext(err, 'getFunction', name);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Replace the function's deployment package. The zip bytes go on the wire as a
|
|
287
|
+
* **top-level** `ZipFile`, not nested under `Code` the way `CreateFunction` takes
|
|
288
|
+
* them - the two operations do not share a body shape.
|
|
289
|
+
*
|
|
290
|
+
* Nothing is swallowed. A 409 here means a previous update has not finished, not
|
|
291
|
+
* that the code is already what was sent, and a 404 means the function is gone -
|
|
292
|
+
* both are the node's to see.
|
|
293
|
+
*/
|
|
294
|
+
async updateFunctionCode(name, zipFile) {
|
|
295
|
+
try {
|
|
296
|
+
await this.call('PUT', PATHS.functionCode(name), { ZipFile: encodeZip(zipFile) });
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
rethrowWithContext(err, 'updateFunctionCode', name);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Replace the function's version-specific settings - role, runtime, handler, memory,
|
|
304
|
+
* timeout and environment. The name travels in the path, never in the body, which
|
|
305
|
+
* is why this takes it as a separate argument rather than reusing
|
|
306
|
+
* `CreateFunctionInput`.
|
|
307
|
+
*
|
|
308
|
+
* Nothing is swallowed, for the same reasons as `updateFunctionCode`.
|
|
309
|
+
*/
|
|
310
|
+
async updateFunctionConfiguration(name, input) {
|
|
311
|
+
try {
|
|
312
|
+
await this.call('PUT', PATHS.functionConfiguration(name), configurationBody(input));
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
rethrowWithContext(err, 'updateFunctionConfiguration', name);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Delete the function and every version and alias of it. No-op when it does not
|
|
320
|
+
* exist, so teardown is re-runnable.
|
|
321
|
+
*
|
|
322
|
+
* Every other failure is rethrown with context - including the 409
|
|
323
|
+
* `ResourceConflictException`, which on this operation means the function is
|
|
324
|
+
* mid-operation and cannot be deleted yet, not that it is already gone. Swallowing
|
|
325
|
+
* it (by reusing `isFunctionAlreadyExists` here) would report a live function as
|
|
326
|
+
* torn down, leaving the Firehose stream pointing at a processor the teardown
|
|
327
|
+
* claimed to have removed.
|
|
328
|
+
*/
|
|
329
|
+
async deleteFunction(name) {
|
|
330
|
+
try {
|
|
331
|
+
await this.call('DELETE', PATHS.function(name));
|
|
332
|
+
}
|
|
333
|
+
catch (err) {
|
|
334
|
+
if (err instanceof AwsError && err.isNotFound)
|
|
335
|
+
return;
|
|
336
|
+
rethrowWithContext(err, 'deleteFunction', name);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { type SigningClient } from 'blogwright-core';
|
|
2
|
+
/**
|
|
3
|
+
* An S3 Tables bucket, as {@link S3TablesClient.getTableBucket} reports it.
|
|
4
|
+
* Narrowed from the API response on purpose: the bucket's creation timestamp
|
|
5
|
+
* and owner account are facts about the account, not about whether the node is
|
|
6
|
+
* present.
|
|
7
|
+
*
|
|
8
|
+
* The `analytics-table-bucket` node reads neither field. It decides presence on
|
|
9
|
+
* whether this resolves at all (`bucket === undefined`), and `recordTableBucket`
|
|
10
|
+
* then derives both the recorded name and the ARN from config rather than from
|
|
11
|
+
* the response - so the two agree with what the node would create, not with
|
|
12
|
+
* whatever the account happens to hold.
|
|
13
|
+
*/
|
|
14
|
+
export interface TableBucket {
|
|
15
|
+
arn: string;
|
|
16
|
+
name: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A namespace inside a table bucket. `tableBucketArn` is carried alongside the
|
|
20
|
+
* name because `GetNamespace`'s own response omits it (only `CreateNamespace`
|
|
21
|
+
* returns it), so the value here is the one the caller looked the namespace up
|
|
22
|
+
* by - see `normalizeNamespace`.
|
|
23
|
+
*/
|
|
24
|
+
export interface Namespace {
|
|
25
|
+
name: string;
|
|
26
|
+
tableBucketArn: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A table inside a namespace. The `analytics-table` node decides presence on
|
|
30
|
+
* whether this resolves at all and records `name` and `arn`; it does not read
|
|
31
|
+
* `metadataLocation`, and nothing else in the package does either - the field
|
|
32
|
+
* is carried because the API returns it, not because a caller needs it. Worth
|
|
33
|
+
* deciding whether to drop it: knip cannot see an unused interface member, so
|
|
34
|
+
* nothing here would ever flag it.
|
|
35
|
+
*/
|
|
36
|
+
export interface Table {
|
|
37
|
+
arn: string;
|
|
38
|
+
name: string;
|
|
39
|
+
/** Absent until the table has been written to at least once. */
|
|
40
|
+
metadataLocation: string | undefined;
|
|
41
|
+
}
|
|
42
|
+
/** One column of an Iceberg table's schema, in the shape `createTable` accepts. */
|
|
43
|
+
export interface IcebergSchemaField {
|
|
44
|
+
readonly name: string;
|
|
45
|
+
/** An Apache Iceberg primitive type name (e.g. `"string"`, `"timestamp"`, `"date"`, `"int"`, `"long"`, `"double"`, `"boolean"`). */
|
|
46
|
+
readonly type: string;
|
|
47
|
+
/** Explicit field id, required here (see the module doc comment) so a partition field can reference it via `sourceId`. */
|
|
48
|
+
readonly id: number;
|
|
49
|
+
readonly required?: boolean | undefined;
|
|
50
|
+
}
|
|
51
|
+
/** The Iceberg partition transforms `IcebergPartitionField.transform` accepts. */
|
|
52
|
+
export type PartitionTransform = 'identity' | 'year' | 'month' | 'day' | 'hour' | 'bucket' | 'truncate';
|
|
53
|
+
/** One partition field, deriving its partition value from a schema field's `id`. */
|
|
54
|
+
export interface IcebergPartitionField {
|
|
55
|
+
readonly name: string;
|
|
56
|
+
/** The schema field (`IcebergSchemaField.id`) this partition is derived from. */
|
|
57
|
+
readonly sourceId: number;
|
|
58
|
+
readonly transform: PartitionTransform;
|
|
59
|
+
readonly fieldId?: number | undefined;
|
|
60
|
+
}
|
|
61
|
+
/** The Iceberg schema and (optional) partition spec `createTable` writes into `metadata.iceberg`. */
|
|
62
|
+
export interface IcebergTableSchema {
|
|
63
|
+
readonly fields: readonly IcebergSchemaField[];
|
|
64
|
+
readonly partitionSpec?: readonly IcebergPartitionField[] | undefined;
|
|
65
|
+
}
|
|
66
|
+
/** S3 Tables control-plane client, over the shared SigV4 transport. */
|
|
67
|
+
export declare class S3TablesClient {
|
|
68
|
+
private readonly client;
|
|
69
|
+
constructor(client: SigningClient);
|
|
70
|
+
private call;
|
|
71
|
+
/**
|
|
72
|
+
* Create a table bucket. Idempotent: an already-existing bucket of the same
|
|
73
|
+
* name is not an error.
|
|
74
|
+
*
|
|
75
|
+
* Deliberately returns `void`, discarding the response's `arn`, rather than
|
|
76
|
+
* surfacing it: `getTableBucket` is ARN-keyed with no name-based lookup, so a
|
|
77
|
+
* caller that wants the ARN has to compute it before calling `createTableBucket`
|
|
78
|
+
* anyway - to run its own `getTableBucket` existence check first, per the usual
|
|
79
|
+
* read-then-create reconcile pattern - using the fixed
|
|
80
|
+
* `arn:aws:s3tables:<region>:<accountId>:bucket/<name>` form and the account id
|
|
81
|
+
* a `PluginContext` already carries. By the time `createTableBucket` runs, the
|
|
82
|
+
* caller already holds the ARN it needs; echoing the response's `arn` back would
|
|
83
|
+
* be redundant on the happy path, and unavailable on the already-exists path
|
|
84
|
+
* (the error body carries no `arn`), so returning it from only one of the two
|
|
85
|
+
* branches would be a false economy. `createNamespace` needs no such lookup at
|
|
86
|
+
* all (its identity is exactly its inputs); `createTable`'s identity is genuinely
|
|
87
|
+
* unrecoverable from its inputs (a table ARN carries an opaque generated id, not
|
|
88
|
+
* a name), but `getTable` is already name-keyed via `/get-table`'s query
|
|
89
|
+
* parameters, so a caller hydrates a table's ARN with a lookup, not by
|
|
90
|
+
* reconstructing it.
|
|
91
|
+
*/
|
|
92
|
+
createTableBucket(name: string): Promise<void>;
|
|
93
|
+
/** Fetch a table bucket by ARN; undefined when it does not exist. */
|
|
94
|
+
getTableBucket(tableBucketArn: string): Promise<TableBucket | undefined>;
|
|
95
|
+
/** Delete a table bucket. No-op when it does not exist, so teardown is re-runnable. */
|
|
96
|
+
deleteTableBucket(tableBucketArn: string): Promise<void>;
|
|
97
|
+
/** Create a namespace in a table bucket. Idempotent: an already-existing namespace is not an error. */
|
|
98
|
+
createNamespace(tableBucketArn: string, namespace: string): Promise<void>;
|
|
99
|
+
/** Fetch a namespace by bucket ARN and name; undefined when it does not exist. */
|
|
100
|
+
getNamespace(tableBucketArn: string, namespace: string): Promise<Namespace | undefined>;
|
|
101
|
+
/** Delete a namespace. No-op when it does not exist, so teardown is re-runnable. */
|
|
102
|
+
deleteNamespace(tableBucketArn: string, namespace: string): Promise<void>;
|
|
103
|
+
/**
|
|
104
|
+
* Create an Iceberg table in a namespace, carrying its schema (and, when given,
|
|
105
|
+
* its partition spec) so the table is never created schema-less. This matters
|
|
106
|
+
* beyond correctness-in-general: Firehose matches incoming record keys to
|
|
107
|
+
* Iceberg column names *exactly* and silently routes anything that does not
|
|
108
|
+
* match to the error bucket (see the analytics plugin spec's §Record
|
|
109
|
+
* transformation), so a schema-less table here fails every subsequent record
|
|
110
|
+
* with no error surfacing anywhere - the corruption this parameter exists to
|
|
111
|
+
* prevent. Idempotent: an already-existing table is not an error (its schema is
|
|
112
|
+
* not reconciled against `schema` on that path - S3 Tables has no
|
|
113
|
+
* update-schema-on-conflict operation for `CreateTable` to fall back to).
|
|
114
|
+
*/
|
|
115
|
+
createTable(tableBucketArn: string, namespace: string, name: string, schema: IcebergTableSchema): Promise<void>;
|
|
116
|
+
/** Fetch a table by bucket ARN, namespace and name; undefined when it does not exist. */
|
|
117
|
+
getTable(tableBucketArn: string, namespace: string, name: string): Promise<Table | undefined>;
|
|
118
|
+
/** Delete a table. No-op when it does not exist, so teardown is re-runnable. */
|
|
119
|
+
deleteTable(tableBucketArn: string, namespace: string, name: string): Promise<void>;
|
|
120
|
+
}
|