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.
Files changed (131) hide show
  1. package/README.md +162 -0
  2. package/dist/adapters/duckdb-ingest.d.ts +76 -0
  3. package/dist/adapters/duckdb-ingest.js +173 -0
  4. package/dist/adapters/duckdb-query.d.ts +56 -0
  5. package/dist/adapters/duckdb-query.js +80 -0
  6. package/dist/adapters/duckdb-session.d.ts +168 -0
  7. package/dist/adapters/duckdb-session.js +330 -0
  8. package/dist/app/_app/immutable/assets/0.BTQrrh5B.css +1 -0
  9. package/dist/app/_app/immutable/assets/2.CZSK3rT8.css +1 -0
  10. package/dist/app/_app/immutable/assets/BrushContext.D7c8UPey.css +1 -0
  11. package/dist/app/_app/immutable/assets/ChartAnnotations.CPxIG7Mw.css +1 -0
  12. package/dist/app/_app/immutable/assets/Circle.C5MKzgk2.css +1 -0
  13. package/dist/app/_app/immutable/assets/DefaultTooltip.C5-uctZ7.css +1 -0
  14. package/dist/app/_app/immutable/assets/Group.DV48xipa.css +1 -0
  15. package/dist/app/_app/immutable/assets/Labels.BxZ4NUVz.css +1 -0
  16. package/dist/app/_app/immutable/assets/Legend.CxnrE4Ye.css +1 -0
  17. package/dist/app/_app/immutable/assets/Line.fkmsECm9.css +1 -0
  18. package/dist/app/_app/immutable/assets/Path.CvpwNZ6g.css +1 -0
  19. package/dist/app/_app/immutable/assets/Rect.CtRaGMmQ.css +1 -0
  20. package/dist/app/_app/immutable/assets/Text.j9l35qB0.css +1 -0
  21. package/dist/app/_app/immutable/assets/TransformContext.Bs_HkpAk.css +1 -0
  22. package/dist/app/_app/immutable/assets/Voronoi.ce7atosu.css +1 -0
  23. package/dist/app/_app/immutable/chunks/-aNGNaBT.js +1 -0
  24. package/dist/app/_app/immutable/chunks/6djn-yLs.js +1 -0
  25. package/dist/app/_app/immutable/chunks/B1amyutE.js +1 -0
  26. package/dist/app/_app/immutable/chunks/B3vZDoek.js +1 -0
  27. package/dist/app/_app/immutable/chunks/B5KRA4hC.js +1 -0
  28. package/dist/app/_app/immutable/chunks/BClnVG6H.js +1 -0
  29. package/dist/app/_app/immutable/chunks/BID1NNRh.js +1 -0
  30. package/dist/app/_app/immutable/chunks/BR2LaRms.js +1 -0
  31. package/dist/app/_app/immutable/chunks/Bd1gDe3Y.js +1 -0
  32. package/dist/app/_app/immutable/chunks/Bjy-W4x2.js +81 -0
  33. package/dist/app/_app/immutable/chunks/Bl052uUt.js +1 -0
  34. package/dist/app/_app/immutable/chunks/Bye3lL0c.js +1 -0
  35. package/dist/app/_app/immutable/chunks/C58PZtCD.js +4 -0
  36. package/dist/app/_app/immutable/chunks/CAzydqEO.js +1 -0
  37. package/dist/app/_app/immutable/chunks/CCch3uox.js +1 -0
  38. package/dist/app/_app/immutable/chunks/CIlSMUH9.js +1 -0
  39. package/dist/app/_app/immutable/chunks/CO1vUXfR.js +1 -0
  40. package/dist/app/_app/immutable/chunks/CPbD8C65.js +5 -0
  41. package/dist/app/_app/immutable/chunks/CRTcXoMo.js +1 -0
  42. package/dist/app/_app/immutable/chunks/CjjyIQAO.js +1 -0
  43. package/dist/app/_app/immutable/chunks/CuXAxjvF.js +1 -0
  44. package/dist/app/_app/immutable/chunks/CvyVA_jC.js +1 -0
  45. package/dist/app/_app/immutable/chunks/CxGCFVdy.js +1 -0
  46. package/dist/app/_app/immutable/chunks/D0Ty6LN0.js +1 -0
  47. package/dist/app/_app/immutable/chunks/D2AaQUUW.js +1 -0
  48. package/dist/app/_app/immutable/chunks/D2BnX0Uk.js +3 -0
  49. package/dist/app/_app/immutable/chunks/DJc8C0NK.js +1 -0
  50. package/dist/app/_app/immutable/chunks/DKMlMI4a.js +1 -0
  51. package/dist/app/_app/immutable/chunks/DVXZkpbf.js +1 -0
  52. package/dist/app/_app/immutable/chunks/DVt8ukQ_.js +1 -0
  53. package/dist/app/_app/immutable/chunks/DZPlYdq_.js +1 -0
  54. package/dist/app/_app/immutable/chunks/Db0q5_zr.js +1 -0
  55. package/dist/app/_app/immutable/chunks/Dfvzj6n2.js +1 -0
  56. package/dist/app/_app/immutable/chunks/Dh958be7.js +1 -0
  57. package/dist/app/_app/immutable/chunks/DjKLLdnY.js +15 -0
  58. package/dist/app/_app/immutable/chunks/Doz7YX1W.js +1 -0
  59. package/dist/app/_app/immutable/chunks/DthYhn6Y.js +2 -0
  60. package/dist/app/_app/immutable/chunks/DtuTIrAM.js +1 -0
  61. package/dist/app/_app/immutable/chunks/HclGiUj8.js +1 -0
  62. package/dist/app/_app/immutable/chunks/Hx0TNsV3.js +1 -0
  63. package/dist/app/_app/immutable/chunks/RobXhXPM.js +1 -0
  64. package/dist/app/_app/immutable/chunks/V9ZjaxiY.js +1 -0
  65. package/dist/app/_app/immutable/chunks/Y5urAfNy.js +1 -0
  66. package/dist/app/_app/immutable/chunks/caXkbKD3.js +1 -0
  67. package/dist/app/_app/immutable/chunks/devYm2ud.js +1 -0
  68. package/dist/app/_app/immutable/chunks/mtZWP0zR.js +1 -0
  69. package/dist/app/_app/immutable/chunks/vDgBJUjM.js +1 -0
  70. package/dist/app/_app/immutable/chunks/xIq_fFFM.js +1 -0
  71. package/dist/app/_app/immutable/chunks/xihTtKlq.js +1 -0
  72. package/dist/app/_app/immutable/chunks/z05MoCFz.js +1 -0
  73. package/dist/app/_app/immutable/entry/app.CLAerUAN.js +2 -0
  74. package/dist/app/_app/immutable/entry/start.D3MqnNci.js +1 -0
  75. package/dist/app/_app/immutable/nodes/0.UTMEigHJ.js +1 -0
  76. package/dist/app/_app/immutable/nodes/1.Cn4f11bT.js +1 -0
  77. package/dist/app/_app/immutable/nodes/2.B39cIcr2.js +6 -0
  78. package/dist/app/_app/version.json +1 -0
  79. package/dist/app/index.html +82 -0
  80. package/dist/aws/clients.d.ts +70 -0
  81. package/dist/aws/clients.js +52 -0
  82. package/dist/aws/errors.d.ts +41 -0
  83. package/dist/aws/errors.js +70 -0
  84. package/dist/aws/firehose.d.ts +228 -0
  85. package/dist/aws/firehose.js +347 -0
  86. package/dist/aws/glue.d.ts +103 -0
  87. package/dist/aws/glue.js +225 -0
  88. package/dist/aws/lambda.d.ts +132 -0
  89. package/dist/aws/lambda.js +339 -0
  90. package/dist/aws/s3tables.d.ts +120 -0
  91. package/dist/aws/s3tables.js +281 -0
  92. package/dist/backfill.d.ts +100 -0
  93. package/dist/backfill.js +294 -0
  94. package/dist/commands.d.ts +124 -0
  95. package/dist/commands.js +336 -0
  96. package/dist/config.d.ts +162 -0
  97. package/dist/config.js +317 -0
  98. package/dist/fixture-ingest.d.ts +49 -0
  99. package/dist/fixture-ingest.js +43 -0
  100. package/dist/fixture-query.d.ts +39 -0
  101. package/dist/fixture-query.js +70 -0
  102. package/dist/index.d.ts +35 -0
  103. package/dist/index.js +35 -0
  104. package/dist/nodes.d.ts +404 -0
  105. package/dist/nodes.js +2708 -0
  106. package/dist/paths.d.ts +45 -0
  107. package/dist/paths.js +47 -0
  108. package/dist/plugin.d.ts +102 -0
  109. package/dist/plugin.js +248 -0
  110. package/dist/ports.d.ts +113 -0
  111. package/dist/ports.js +35 -0
  112. package/dist/queries.d.ts +301 -0
  113. package/dist/queries.js +414 -0
  114. package/dist/schema.d.ts +240 -0
  115. package/dist/schema.js +154 -0
  116. package/dist/server.d.ts +150 -0
  117. package/dist/server.js +499 -0
  118. package/dist/transform/bots.d.ts +47 -0
  119. package/dist/transform/bots.js +73 -0
  120. package/dist/transform/handler.d.ts +135 -0
  121. package/dist/transform/handler.js +177 -0
  122. package/dist/transform/map-record.d.ts +110 -0
  123. package/dist/transform/map-record.js +275 -0
  124. package/dist/transform/visitor-key.d.ts +83 -0
  125. package/dist/transform/visitor-key.js +120 -0
  126. package/dist/transform-bundle/index.mjs +21456 -0
  127. package/dist/transform-bundle/transform-manifest.json +4 -0
  128. package/dist/transform-hash.d.ts +135 -0
  129. package/dist/transform-hash.js +186 -0
  130. package/dist/write-transform-manifest.mjs +365 -0
  131. 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
+ }