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
package/dist/config.js
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This package owns the `analytics` config key end to end: its shape, its
|
|
3
|
+
* defaults, and the validator core's `parseConfig` deliberately does not run
|
|
4
|
+
* (`packages/core/src/config.ts:242` merges an `analytics` block through
|
|
5
|
+
* untouched, so a config carrying one is valid and inert when the plugin is
|
|
6
|
+
* not installed).
|
|
7
|
+
*
|
|
8
|
+
* **Two shapes, and why the seam between them is where it is.**
|
|
9
|
+
* `validateAnalyticsConfig` is the function this package's
|
|
10
|
+
* `Plugin.validateConfig` hands core's raw block to, so whatever it returns is
|
|
11
|
+
* what the host puts on `ctx.pluginConfig` (`packages/core/src/plugin.ts:85-93`).
|
|
12
|
+
* It applies every default it can: `namespace`, `table`, `bots` and
|
|
13
|
+
* `dashboard.port` default to plain literals, so they come back total and no
|
|
14
|
+
* reader downstream re-checks them for `undefined`.
|
|
15
|
+
*
|
|
16
|
+
* `tableBucket` and `saltSecretName` cannot be defaulted there. Both carry the
|
|
17
|
+
* environment, and the SPI hands `validateConfig` the block and nothing else -
|
|
18
|
+
* no `env`, no `siteName`. So an operator's overrides for those two are
|
|
19
|
+
* carried under {@link ENV_DERIVED}, a symbol this module does not export, and
|
|
20
|
+
* {@link resolveAnalyticsConfig} - which takes the plugin context, and
|
|
21
|
+
* therefore always has the environment - is the only way to a bucket name or a
|
|
22
|
+
* salt name.
|
|
23
|
+
*
|
|
24
|
+
* That sealing is load-bearing, not tidiness. Were `tableBucket` merely
|
|
25
|
+
* optional on the validated block, a node downstream could write
|
|
26
|
+
* ``ctx.pluginConfig.tableBucket ?? `${ctx.config.siteName}-analytics` `` and
|
|
27
|
+
* it would compile, typecheck and pass its own tests - and staging would then
|
|
28
|
+
* resolve to production's bucket. With the field absent from the type, that
|
|
29
|
+
* read is `TS2339: Property 'tableBucket' does not exist on type
|
|
30
|
+
* 'AnalyticsConfig'`, which `pnpm typecheck` fails on; `config.test.ts` pins
|
|
31
|
+
* the diagnostic so the seal cannot be opened silently.
|
|
32
|
+
*
|
|
33
|
+
* **Every derived default carries the environment**, matching `deriveNames`'
|
|
34
|
+
* `<env>-<siteName>` prefix (`packages/core/src/config.ts:352`):
|
|
35
|
+
* `tableBucket` is `<env>-<siteName>-analytics` and `saltSecretName` is
|
|
36
|
+
* `<siteName>/<env>/analytics-salt`. Do not "simplify" the environment out of
|
|
37
|
+
* either one. Without it staging and production resolve to the *same* Iceberg
|
|
38
|
+
* table and the same salt, and `blogwright analytics destroy --yes` run in
|
|
39
|
+
* staging issues `DeleteTableBucket` against production's data. Nothing in the
|
|
40
|
+
* system catches that: state is scoped per environment
|
|
41
|
+
* (`state/<env>.analytics.json`), so each environment's state file correctly
|
|
42
|
+
* records the bucket it was told to use and neither can see the collision.
|
|
43
|
+
* AWS gives a second, quieter reason - it "does not recommend using multiple
|
|
44
|
+
* Firehose streams to write data to the same Apache Iceberg table", because
|
|
45
|
+
* Iceberg's optimistic concurrency makes the streams contend.
|
|
46
|
+
*
|
|
47
|
+
* Pure data and pure functions only: no `node:` builtin, no vendor SDK, no
|
|
48
|
+
* `fetch`. See [the change spec's §Configuration → The `analytics`
|
|
49
|
+
* block](../../../.specs/changes/merged/2026-07-26-analytics_plugin.md).
|
|
50
|
+
*/
|
|
51
|
+
/**
|
|
52
|
+
* The key an operator's `tableBucket`/`saltSecretName` overrides ride on. A
|
|
53
|
+
* module-private `unique symbol`: TypeScript emits it into `config.d.ts`
|
|
54
|
+
* unexported, so no module outside this one can name it, and neither
|
|
55
|
+
* `ctx.pluginConfig.tableBucket` nor `ctx.pluginConfig[ENV_DERIVED]` compiles
|
|
56
|
+
* anywhere else. That is the point - see this module's doc comment for the
|
|
57
|
+
* `?? <env-less fallback>` line it exists to make unwritable.
|
|
58
|
+
*/
|
|
59
|
+
const ENV_DERIVED = Symbol('analytics env-derived overrides');
|
|
60
|
+
/** Default Iceberg namespace. */
|
|
61
|
+
const DEFAULT_NAMESPACE = 'web';
|
|
62
|
+
/** Default Iceberg table - the one `schema.ts` describes. */
|
|
63
|
+
const DEFAULT_TABLE = 'page_views';
|
|
64
|
+
/** Default bot handling: keep bot rows, mark them, and leave filtering to the query. */
|
|
65
|
+
const DEFAULT_BOTS = 'flag';
|
|
66
|
+
/**
|
|
67
|
+
* Default port for the local dashboard server. Exported because the dashboard
|
|
68
|
+
* server binds this exact number: it imports the constant rather than
|
|
69
|
+
* restating it, so the default has one home.
|
|
70
|
+
*/
|
|
71
|
+
export const DEFAULT_DASHBOARD_PORT = 4317;
|
|
72
|
+
/** Lowest port the dashboard may bind - anything below it needs root on Unix. */
|
|
73
|
+
const MIN_DASHBOARD_PORT = 1024;
|
|
74
|
+
/** Highest port there is. */
|
|
75
|
+
const MAX_DASHBOARD_PORT = 65535;
|
|
76
|
+
/** Shortest S3 bucket name S3 accepts. */
|
|
77
|
+
const TABLE_BUCKET_MIN_LENGTH = 3;
|
|
78
|
+
/** Longest S3 bucket name S3 accepts - the same limit `deriveNames` enforces. */
|
|
79
|
+
const TABLE_BUCKET_MAX_LENGTH = 63;
|
|
80
|
+
/**
|
|
81
|
+
* S3 bucket naming, narrowed to what an S3 Tables bucket takes: lowercase
|
|
82
|
+
* alphanumerics and dashes, {@link TABLE_BUCKET_MIN_LENGTH} to
|
|
83
|
+
* {@link TABLE_BUCKET_MAX_LENGTH} characters. Composed from those two
|
|
84
|
+
* constants rather than hard-coded inside the pattern, so the bounds have one
|
|
85
|
+
* home and the rejection message quotes the same numbers the pattern enforces.
|
|
86
|
+
*/
|
|
87
|
+
const TABLE_BUCKET_PATTERN = new RegExp(`^[0-9a-z-]{${TABLE_BUCKET_MIN_LENGTH},${TABLE_BUCKET_MAX_LENGTH}}$`);
|
|
88
|
+
/** Iceberg identifier: an S3 Tables catalog rejects anything else. */
|
|
89
|
+
const IDENTIFIER_PATTERN = /^[a-z0-9_]+$/;
|
|
90
|
+
/** Characters permitted in a Secrets Manager secret name - the class `pds.secretName` uses. */
|
|
91
|
+
const SECRET_NAME_PATTERN = /^[\w/+=.@-]+$/;
|
|
92
|
+
/** The `bots` union, as data, so the validator and its message read one list. */
|
|
93
|
+
const BOTS_MODES = ['flag', 'filter'];
|
|
94
|
+
/** Every key the block allows - the schema's `additionalProperties: false`, as data. */
|
|
95
|
+
const ANALYTICS_KEYS = [
|
|
96
|
+
'tableBucket',
|
|
97
|
+
'namespace',
|
|
98
|
+
'table',
|
|
99
|
+
'bots',
|
|
100
|
+
'saltSecretName',
|
|
101
|
+
'dashboard',
|
|
102
|
+
];
|
|
103
|
+
/** Every key the `dashboard` sub-block allows. */
|
|
104
|
+
const DASHBOARD_KEYS = ['port'];
|
|
105
|
+
/**
|
|
106
|
+
* Narrow an `unknown` to an object. The `!== null` is the `typeof` guard's
|
|
107
|
+
* usual companion, not a domain value: nothing in this module returns or
|
|
108
|
+
* accepts `null` for a setting - absence is `undefined`.
|
|
109
|
+
*/
|
|
110
|
+
function isRecord(value) {
|
|
111
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
112
|
+
}
|
|
113
|
+
/** Render a rejected value for a message: strings quoted, as core's messages quote them. */
|
|
114
|
+
function formatValue(value) {
|
|
115
|
+
if (typeof value === 'string')
|
|
116
|
+
return `"${value}"`;
|
|
117
|
+
if (isRecord(value) || Array.isArray(value))
|
|
118
|
+
return JSON.stringify(value);
|
|
119
|
+
return String(value);
|
|
120
|
+
}
|
|
121
|
+
function isBotHandling(value) {
|
|
122
|
+
return BOTS_MODES.some((mode) => mode === value);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The `tableBucket` default. Carries `env` first, matching `deriveNames`'
|
|
126
|
+
* `<env>-<siteName>` prefix - see this module's doc comment for what dropping
|
|
127
|
+
* it destroys.
|
|
128
|
+
*/
|
|
129
|
+
function defaultTableBucket(site) {
|
|
130
|
+
return `${site.env}-${site.siteName}-analytics`;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The `saltSecretName` default. Carries `env` as a path segment, so two
|
|
134
|
+
* environments hash `visitor_key` with two different salts.
|
|
135
|
+
*/
|
|
136
|
+
function defaultSaltSecretName(site) {
|
|
137
|
+
return `${site.siteName}/${site.env}/analytics-salt`;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Validate a raw `analytics` config block, boundary-checked as `unknown`
|
|
141
|
+
* because it comes off `parseConfigDocument`'s `raw` half (a plugin's block
|
|
142
|
+
* has no type until its own package narrows it). An absent block validates as
|
|
143
|
+
* an empty one, so installing the plugin without writing an `analytics` key is
|
|
144
|
+
* valid.
|
|
145
|
+
*
|
|
146
|
+
* Applies the four literal defaults, so `namespace`, `table`, `bots` and
|
|
147
|
+
* `dashboard.port` are total on the returned block. `tableBucket` and
|
|
148
|
+
* `saltSecretName` are validated here too, but sealed under
|
|
149
|
+
* {@link ENV_DERIVED}: their defaults carry the environment, which this
|
|
150
|
+
* signature does not, so {@link resolveAnalyticsConfig} is where they resolve.
|
|
151
|
+
*
|
|
152
|
+
* Raises in the repo's vocabulary (`packages/core/src/config.ts:274-340`),
|
|
153
|
+
* naming the offending key and value.
|
|
154
|
+
*/
|
|
155
|
+
export function validateAnalyticsConfig(raw) {
|
|
156
|
+
const block = raw === undefined ? {} : raw;
|
|
157
|
+
if (!isRecord(block)) {
|
|
158
|
+
throw new Error(`config.analytics must be an object, got ${formatValue(block)}`);
|
|
159
|
+
}
|
|
160
|
+
for (const key of Object.keys(block)) {
|
|
161
|
+
if (!ANALYTICS_KEYS.some((known) => known === key)) {
|
|
162
|
+
throw new Error(`config.analytics.${key} is not a known setting - allowed keys are ${ANALYTICS_KEYS.join(', ')}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
namespace: validateIdentifier(block['namespace'], 'namespace') ?? DEFAULT_NAMESPACE,
|
|
167
|
+
table: validateIdentifier(block['table'], 'table') ?? DEFAULT_TABLE,
|
|
168
|
+
bots: validateBots(block['bots']) ?? DEFAULT_BOTS,
|
|
169
|
+
dashboard: { port: validateDashboardPort(block['dashboard']) },
|
|
170
|
+
[ENV_DERIVED]: validateEnvDerivedOverrides(block),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Validate the two settings whose defaults need the environment, returning
|
|
175
|
+
* them as the sealed overrides {@link resolveAnalyticsConfig} later reads. An
|
|
176
|
+
* absent setting stays absent: this function has no environment to default it
|
|
177
|
+
* with, which is the whole reason the seal exists.
|
|
178
|
+
*/
|
|
179
|
+
function validateEnvDerivedOverrides(raw) {
|
|
180
|
+
const overrides = {};
|
|
181
|
+
const tableBucket = raw['tableBucket'];
|
|
182
|
+
if (tableBucket !== undefined) {
|
|
183
|
+
if (typeof tableBucket !== 'string' || !TABLE_BUCKET_PATTERN.test(tableBucket)) {
|
|
184
|
+
throw new Error(`config.analytics.tableBucket must be ${TABLE_BUCKET_MIN_LENGTH}..${TABLE_BUCKET_MAX_LENGTH} lowercase alphanumeric/dash characters, got ${formatValue(tableBucket)}`);
|
|
185
|
+
}
|
|
186
|
+
overrides.tableBucket = tableBucket;
|
|
187
|
+
}
|
|
188
|
+
const saltSecretName = raw['saltSecretName'];
|
|
189
|
+
if (saltSecretName !== undefined) {
|
|
190
|
+
// Same character class and same message shape as `config.pds.secretName`
|
|
191
|
+
// (`packages/core/src/config.ts:337`): both name a Secrets Manager secret,
|
|
192
|
+
// so an operator who has hit one message recognises the other.
|
|
193
|
+
if (typeof saltSecretName !== 'string' || !SECRET_NAME_PATTERN.test(saltSecretName)) {
|
|
194
|
+
throw new Error(`config.analytics.saltSecretName has invalid characters: ${formatValue(saltSecretName)}`);
|
|
195
|
+
}
|
|
196
|
+
overrides.saltSecretName = saltSecretName;
|
|
197
|
+
}
|
|
198
|
+
return overrides;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Validate an Iceberg identifier (`namespace`, `table`), returning `undefined`
|
|
202
|
+
* when the setting is absent so the caller applies its own named default.
|
|
203
|
+
*/
|
|
204
|
+
function validateIdentifier(raw, key) {
|
|
205
|
+
if (raw === undefined)
|
|
206
|
+
return undefined;
|
|
207
|
+
if (typeof raw !== 'string' || !IDENTIFIER_PATTERN.test(raw)) {
|
|
208
|
+
throw new Error(`config.analytics.${key} must be lowercase alphanumeric/underscores, got ${formatValue(raw)}`);
|
|
209
|
+
}
|
|
210
|
+
return raw;
|
|
211
|
+
}
|
|
212
|
+
/** Validate the `bots` setting, returning `undefined` when it is absent. */
|
|
213
|
+
function validateBots(raw) {
|
|
214
|
+
if (raw === undefined)
|
|
215
|
+
return undefined;
|
|
216
|
+
if (!isBotHandling(raw)) {
|
|
217
|
+
throw new Error(`config.analytics.bots must be one of ${BOTS_MODES.join(', ')}, got ${formatValue(raw)}`);
|
|
218
|
+
}
|
|
219
|
+
return raw;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Validate the `dashboard` sub-block and return the port it settles on -
|
|
223
|
+
* {@link DEFAULT_DASHBOARD_PORT} when the sub-block or the setting is absent.
|
|
224
|
+
* Split out to keep the validator at one level of detail.
|
|
225
|
+
*/
|
|
226
|
+
function validateDashboardPort(raw) {
|
|
227
|
+
if (raw === undefined)
|
|
228
|
+
return DEFAULT_DASHBOARD_PORT;
|
|
229
|
+
if (!isRecord(raw)) {
|
|
230
|
+
throw new Error(`config.analytics.dashboard must be an object, got ${formatValue(raw)}`);
|
|
231
|
+
}
|
|
232
|
+
for (const key of Object.keys(raw)) {
|
|
233
|
+
if (!DASHBOARD_KEYS.some((known) => known === key)) {
|
|
234
|
+
throw new Error(`config.analytics.dashboard.${key} is not a known setting - allowed keys are ${DASHBOARD_KEYS.join(', ')}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const port = raw['port'];
|
|
238
|
+
if (port === undefined)
|
|
239
|
+
return DEFAULT_DASHBOARD_PORT;
|
|
240
|
+
// One message for one validity condition: an integer inside the range. A
|
|
241
|
+
// fractional or non-numeric port is outside that range as much as `80` is.
|
|
242
|
+
// The `typeof` does the narrowing `Number.isInteger` does not - its lib
|
|
243
|
+
// signature returns `boolean`, not a predicate - so at runtime it is
|
|
244
|
+
// redundant with the next clause, which already rejects every non-number.
|
|
245
|
+
if (typeof port !== 'number' ||
|
|
246
|
+
!Number.isInteger(port) ||
|
|
247
|
+
port < MIN_DASHBOARD_PORT ||
|
|
248
|
+
port > MAX_DASHBOARD_PORT) {
|
|
249
|
+
throw new Error(`config.analytics.dashboard.port must be in ${MIN_DASHBOARD_PORT}..${MAX_DASHBOARD_PORT}, got ${formatValue(port)}`);
|
|
250
|
+
}
|
|
251
|
+
return port;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Unseal the environment-carrying overrides off a validated block, rejecting a
|
|
255
|
+
* block that never went through {@link validateAnalyticsConfig}.
|
|
256
|
+
*
|
|
257
|
+
* The type says that cannot happen and the runtime says otherwise, because the
|
|
258
|
+
* one seam that fills `ctx.pluginConfig` erases `TConfig`: the CLI dispatches
|
|
259
|
+
* over `Plugin<unknown>` and `toPluginContext(ops)` returns
|
|
260
|
+
* `PluginContext<unknown>` (`packages/cli/src/plugin-commands.ts:241,466`), so
|
|
261
|
+
* an unvalidated block passes the compiler exactly there - and
|
|
262
|
+
* {@link resolveAnalyticsConfig} is exported, so a caller can hand one over
|
|
263
|
+
* directly too. Such a block carries neither this symbol nor the four literal
|
|
264
|
+
* defaults, and without this check the first read raised `TypeError: Cannot
|
|
265
|
+
* read properties of undefined (reading 'tableBucket')` - an internal field
|
|
266
|
+
* name and no fix.
|
|
267
|
+
*
|
|
268
|
+
* Nothing is recovered here, deliberately. The literal defaults are missing on
|
|
269
|
+
* that block as well, so a fabricated one would carry `undefined` at runtime
|
|
270
|
+
* for `namespace`, `table`, `bots` and `dashboard.port` - fields whose type
|
|
271
|
+
* says total - and every reader downstream trusts that type. A throw naming
|
|
272
|
+
* the function that must run first is loud; a recovered block is silently
|
|
273
|
+
* wrong.
|
|
274
|
+
*/
|
|
275
|
+
function unsealEnvDerivedOverrides(block) {
|
|
276
|
+
// Justified by the line below, which is the validation: the declared type
|
|
277
|
+
// has no `undefined` here precisely because a validated block always has it.
|
|
278
|
+
const overrides = block[ENV_DERIVED];
|
|
279
|
+
if (overrides === undefined) {
|
|
280
|
+
throw new Error('analytics config was not validated: ctx.pluginConfig must come from validateAnalyticsConfig, which applies the defaults this resolver reads');
|
|
281
|
+
}
|
|
282
|
+
return overrides;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Resolve the environment-carrying settings a validated block sealed, giving
|
|
286
|
+
* the total config every reader downstream holds. Takes the plugin context
|
|
287
|
+
* rather than a `{ env, siteName }` pair, so the site identity is read out of
|
|
288
|
+
* `ctx` in one place instead of at each call site and no caller can supply an
|
|
289
|
+
* environment other than the one it is running in.
|
|
290
|
+
*
|
|
291
|
+
* Raises when handed a block the validator did not produce - see
|
|
292
|
+
* {@link unsealEnvDerivedOverrides}.
|
|
293
|
+
*
|
|
294
|
+
* The derived bucket name is length-checked here, where it is derived, exactly
|
|
295
|
+
* as `deriveNames` checks the site bucket it derives
|
|
296
|
+
* (`packages/core/src/config.ts:355`). Only the length is checked: `env` and
|
|
297
|
+
* `siteName` are already held to `^[a-z0-9-]+$` by `deriveNames` and
|
|
298
|
+
* `validateConfig`, and a second character check here would be a second home
|
|
299
|
+
* for a rule core already owns.
|
|
300
|
+
*/
|
|
301
|
+
export function resolveAnalyticsConfig(ctx) {
|
|
302
|
+
const site = { env: ctx.env, siteName: ctx.config.siteName };
|
|
303
|
+
const block = ctx.pluginConfig;
|
|
304
|
+
const overrides = unsealEnvDerivedOverrides(block);
|
|
305
|
+
const tableBucket = overrides.tableBucket ?? defaultTableBucket(site);
|
|
306
|
+
if (tableBucket.length > TABLE_BUCKET_MAX_LENGTH) {
|
|
307
|
+
throw new Error(`derived analytics table bucket "${tableBucket}" exceeds S3's ${TABLE_BUCKET_MAX_LENGTH}-char limit; shorten env or siteName`);
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
tableBucket,
|
|
311
|
+
namespace: block.namespace,
|
|
312
|
+
table: block.table,
|
|
313
|
+
bots: block.bots,
|
|
314
|
+
saltSecretName: overrides.saltSecretName ?? defaultSaltSecretName(site),
|
|
315
|
+
dashboard: { port: block.dashboard.port },
|
|
316
|
+
};
|
|
317
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The recording {@link AnalyticsIngest} the backfill's tests substitute at the
|
|
3
|
+
* write port, so no test in this package starts DuckDB to prove what was
|
|
4
|
+
* written. It is the write-side counterpart of `fixture-query.ts`, and it is
|
|
5
|
+
* shipped beside the interface it implements for the same reason
|
|
6
|
+
* `createMemoryFileSystem` (`packages/core/src/adapters/memory-fs.ts`) is: a
|
|
7
|
+
* real implementation of the port, not a stub that agrees with anything.
|
|
8
|
+
*
|
|
9
|
+
* **It records rather than judges, with two exceptions, and both are the
|
|
10
|
+
* contract rather than a convenience.** {@link AnalyticsIngest.insertDay}
|
|
11
|
+
* documents that a batch is one whole day and that an empty batch is a caller
|
|
12
|
+
* error; a fake that quietly accepted a row belonging to another day, or an
|
|
13
|
+
* insert of nothing, would let a backfill defect pass through it and land on
|
|
14
|
+
* an assertion about something else. Everything a test actually asserts - which
|
|
15
|
+
* days were written, in which order, with which rows - it reads off
|
|
16
|
+
* {@link RecordingAnalyticsIngest.calls}, and the fake takes no view on it.
|
|
17
|
+
* The bound (no day at or after the recorded `createdDay`) in particular is
|
|
18
|
+
* deliberately NOT enforced here: it is the property the backfill exists to
|
|
19
|
+
* hold, so a fake that enforced it would answer the question under test.
|
|
20
|
+
*/
|
|
21
|
+
import type { AnalyticsIngest } from './ports.js';
|
|
22
|
+
import type { PageView } from './schema.js';
|
|
23
|
+
/**
|
|
24
|
+
* One accepted {@link AnalyticsIngest.insertDay}, exactly as it arrived.
|
|
25
|
+
*
|
|
26
|
+
* Not exported, following `ports.ts`' `QueryValue`: no consumer needs the
|
|
27
|
+
* shape by name today - a test reaches it as
|
|
28
|
+
* `RecordingAnalyticsIngest['calls'][number]` or, in practice, by reading
|
|
29
|
+
* `call.day` and `call.rows` off it. Export it once something names it.
|
|
30
|
+
*/
|
|
31
|
+
interface RecordedInsert {
|
|
32
|
+
/** The UTC day the call named. */
|
|
33
|
+
readonly day: string;
|
|
34
|
+
/** The rows it carried, in the order the caller handed them over. */
|
|
35
|
+
readonly rows: readonly PageView[];
|
|
36
|
+
}
|
|
37
|
+
/** The fake, plus the record of what it was asked to write. */
|
|
38
|
+
export interface RecordingAnalyticsIngest extends AnalyticsIngest {
|
|
39
|
+
/** Every accepted insert, in call order. */
|
|
40
|
+
readonly calls: readonly RecordedInsert[];
|
|
41
|
+
/** The days of {@link calls}, in call order - the shape most assertions want. */
|
|
42
|
+
readonly days: readonly string[];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Build a {@link RecordingAnalyticsIngest}. Takes nothing: an ingest port has
|
|
46
|
+
* no result to seed, so the only thing a test needs from it is what it saw.
|
|
47
|
+
*/
|
|
48
|
+
export declare function createRecordingAnalyticsIngest(): RecordingAnalyticsIngest;
|
|
49
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The recording {@link AnalyticsIngest} the backfill's tests substitute at the
|
|
3
|
+
* write port, so no test in this package starts DuckDB to prove what was
|
|
4
|
+
* written. It is the write-side counterpart of `fixture-query.ts`, and it is
|
|
5
|
+
* shipped beside the interface it implements for the same reason
|
|
6
|
+
* `createMemoryFileSystem` (`packages/core/src/adapters/memory-fs.ts`) is: a
|
|
7
|
+
* real implementation of the port, not a stub that agrees with anything.
|
|
8
|
+
*
|
|
9
|
+
* **It records rather than judges, with two exceptions, and both are the
|
|
10
|
+
* contract rather than a convenience.** {@link AnalyticsIngest.insertDay}
|
|
11
|
+
* documents that a batch is one whole day and that an empty batch is a caller
|
|
12
|
+
* error; a fake that quietly accepted a row belonging to another day, or an
|
|
13
|
+
* insert of nothing, would let a backfill defect pass through it and land on
|
|
14
|
+
* an assertion about something else. Everything a test actually asserts - which
|
|
15
|
+
* days were written, in which order, with which rows - it reads off
|
|
16
|
+
* {@link RecordingAnalyticsIngest.calls}, and the fake takes no view on it.
|
|
17
|
+
* The bound (no day at or after the recorded `createdDay`) in particular is
|
|
18
|
+
* deliberately NOT enforced here: it is the property the backfill exists to
|
|
19
|
+
* hold, so a fake that enforced it would answer the question under test.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Build a {@link RecordingAnalyticsIngest}. Takes nothing: an ingest port has
|
|
23
|
+
* no result to seed, so the only thing a test needs from it is what it saw.
|
|
24
|
+
*/
|
|
25
|
+
export function createRecordingAnalyticsIngest() {
|
|
26
|
+
const calls = [];
|
|
27
|
+
const days = [];
|
|
28
|
+
return {
|
|
29
|
+
calls,
|
|
30
|
+
days,
|
|
31
|
+
async insertDay(day, rows) {
|
|
32
|
+
if (rows.length === 0) {
|
|
33
|
+
throw new Error(`analytics ingest was asked to insert day ${day} with no rows`);
|
|
34
|
+
}
|
|
35
|
+
const foreign = rows.find((row) => row.day !== day);
|
|
36
|
+
if (foreign !== undefined) {
|
|
37
|
+
throw new Error(`analytics ingest was asked to insert day ${day} carrying a row for day ${foreign.day}`);
|
|
38
|
+
}
|
|
39
|
+
calls.push({ day, rows });
|
|
40
|
+
days.push(day);
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fixture-backed {@link AnalyticsQuery} every consumer's test substitutes
|
|
3
|
+
* at the port, so no test in this package starts DuckDB. It is the analytics
|
|
4
|
+
* counterpart of `packages/core/src/adapters/memory-fs.ts`: a real
|
|
5
|
+
* implementation of the port, seeded with data, shipped beside the interface
|
|
6
|
+
* it implements.
|
|
7
|
+
*
|
|
8
|
+
* It is not a stub that answers anything. It runs the *same*
|
|
9
|
+
* {@link prepareQuery} the DuckDB adapter runs, so an unknown name and an
|
|
10
|
+
* absent or inverted range are refused here with the same messages a real
|
|
11
|
+
* dashboard would produce, and it refuses at construction to hold rows that do
|
|
12
|
+
* not match the query's declared result columns. That second check is the
|
|
13
|
+
* point: a fake seeded with rows shaped like nothing the query returns lets a
|
|
14
|
+
* consumer's assertions pass for the wrong reason, which is the commonest way
|
|
15
|
+
* a test stops being able to fail.
|
|
16
|
+
*
|
|
17
|
+
* `calls` records what each accepted call resolved to, so a consumer can
|
|
18
|
+
* assert *what was bound* - that its date range reached the query, that its
|
|
19
|
+
* bot flag defaulted from `config.analytics.bots` - without a spy or a mock.
|
|
20
|
+
*/
|
|
21
|
+
import { type AnalyticsConfig } from './config.js';
|
|
22
|
+
import type { AnalyticsQuery, QueryRow } from './ports.js';
|
|
23
|
+
import { type PreparedQuery, type QueryName } from './queries.js';
|
|
24
|
+
/** Rows to answer with, per query name. A name with no entry raises when asked for. */
|
|
25
|
+
export type QueryFixtures = Partial<Record<QueryName, readonly QueryRow[]>>;
|
|
26
|
+
/** The fake, plus the record of what it was asked. */
|
|
27
|
+
export interface FixtureAnalyticsQuery extends AnalyticsQuery {
|
|
28
|
+
/** Every accepted call, in order, as the shared lookup resolved it. */
|
|
29
|
+
readonly calls: readonly PreparedQuery[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Build an {@link AnalyticsQuery} that answers from `fixtures`.
|
|
33
|
+
*
|
|
34
|
+
* `config` defaults to a validated empty `analytics` block, so a consumer that
|
|
35
|
+
* does not care about bot handling gets task 44's own default rather than a
|
|
36
|
+
* second copy of it stated here; a consumer that does care passes a block
|
|
37
|
+
* validated from the raw config it is exercising.
|
|
38
|
+
*/
|
|
39
|
+
export declare function createFixtureAnalyticsQuery(fixtures: QueryFixtures, config?: Pick<AnalyticsConfig, 'bots'>): FixtureAnalyticsQuery;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fixture-backed {@link AnalyticsQuery} every consumer's test substitutes
|
|
3
|
+
* at the port, so no test in this package starts DuckDB. It is the analytics
|
|
4
|
+
* counterpart of `packages/core/src/adapters/memory-fs.ts`: a real
|
|
5
|
+
* implementation of the port, seeded with data, shipped beside the interface
|
|
6
|
+
* it implements.
|
|
7
|
+
*
|
|
8
|
+
* It is not a stub that answers anything. It runs the *same*
|
|
9
|
+
* {@link prepareQuery} the DuckDB adapter runs, so an unknown name and an
|
|
10
|
+
* absent or inverted range are refused here with the same messages a real
|
|
11
|
+
* dashboard would produce, and it refuses at construction to hold rows that do
|
|
12
|
+
* not match the query's declared result columns. That second check is the
|
|
13
|
+
* point: a fake seeded with rows shaped like nothing the query returns lets a
|
|
14
|
+
* consumer's assertions pass for the wrong reason, which is the commonest way
|
|
15
|
+
* a test stops being able to fail.
|
|
16
|
+
*
|
|
17
|
+
* `calls` records what each accepted call resolved to, so a consumer can
|
|
18
|
+
* assert *what was bound* - that its date range reached the query, that its
|
|
19
|
+
* bot flag defaulted from `config.analytics.bots` - without a spy or a mock.
|
|
20
|
+
*/
|
|
21
|
+
import { validateAnalyticsConfig } from './config.js';
|
|
22
|
+
import { prepareQuery, queryDefinition, } from './queries.js';
|
|
23
|
+
/** Render a list for a message, saying so when it is empty rather than trailing off. */
|
|
24
|
+
function formatList(items) {
|
|
25
|
+
return items.length === 0 ? 'none' : items.join(', ');
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Refuse fixture rows that are not shaped like the query's own result. Checked
|
|
29
|
+
* once, at construction, so the failure names the test's own fixture rather
|
|
30
|
+
* than surfacing later as an assertion that passed against the wrong keys.
|
|
31
|
+
*/
|
|
32
|
+
function checkFixtureShape(name, rows) {
|
|
33
|
+
const expected = [...queryDefinition(name).resultColumns].sort();
|
|
34
|
+
rows.forEach((row, index) => {
|
|
35
|
+
const actual = Object.keys(row).sort();
|
|
36
|
+
if (actual.join(',') !== expected.join(',')) {
|
|
37
|
+
throw new Error(`fixture row ${index} for analytics query "${name}" must carry exactly its result columns ${formatList(expected)}, got ${formatList(actual)}`);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Build an {@link AnalyticsQuery} that answers from `fixtures`.
|
|
43
|
+
*
|
|
44
|
+
* `config` defaults to a validated empty `analytics` block, so a consumer that
|
|
45
|
+
* does not care about bot handling gets task 44's own default rather than a
|
|
46
|
+
* second copy of it stated here; a consumer that does care passes a block
|
|
47
|
+
* validated from the raw config it is exercising.
|
|
48
|
+
*/
|
|
49
|
+
export function createFixtureAnalyticsQuery(fixtures, config = validateAnalyticsConfig({})) {
|
|
50
|
+
const recorded = new Map();
|
|
51
|
+
for (const [name, rows] of Object.entries(fixtures)) {
|
|
52
|
+
if (rows === undefined)
|
|
53
|
+
continue;
|
|
54
|
+
checkFixtureShape(name, rows);
|
|
55
|
+
recorded.set(name, rows);
|
|
56
|
+
}
|
|
57
|
+
const calls = [];
|
|
58
|
+
return {
|
|
59
|
+
calls,
|
|
60
|
+
async run(name, params) {
|
|
61
|
+
const prepared = prepareQuery(name, params, config);
|
|
62
|
+
calls.push(prepared);
|
|
63
|
+
const rows = recorded.get(prepared.name);
|
|
64
|
+
if (rows === undefined) {
|
|
65
|
+
throw new Error(`no fixture rows recorded for analytics query "${prepared.name}" - recorded queries are ${formatList([...recorded.keys()])}`);
|
|
66
|
+
}
|
|
67
|
+
return rows;
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public surface of blogwright-analytics: CloudFront access logs routed
|
|
3
|
+
* through Firehose into an Iceberg table, with a local dashboard over it.
|
|
4
|
+
* The plugin is installed on demand with `blogwright plugin add analytics`,
|
|
5
|
+
* never shipped with the CLI - see DEVELOPMENT.md §Hexagonal architecture
|
|
6
|
+
* ("Features live in their own packages"). This module carries the package's
|
|
7
|
+
* own AWS service clients (built over core's shared SigV4 transport through
|
|
8
|
+
* the plugin-supplied `ServiceDescriptor` seam) and, since task 47, the
|
|
9
|
+
* `Plugin` default export the CLI's discovery loads.
|
|
10
|
+
*
|
|
11
|
+
* The default export and the package's `blogwright.plugin` manifest field
|
|
12
|
+
* landed together, deliberately: a package carrying the manifest without a
|
|
13
|
+
* conforming default export is a discovery error naming this package, so
|
|
14
|
+
* neither half may ship ahead of the other.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* The plugin's own four AWS service clients and the shapes they return. They
|
|
18
|
+
* live here rather than in `blogwright-core` because core enumerates no
|
|
19
|
+
* `firehose`, `glue`, `lambda` or `s3tables` signing name: each signs through
|
|
20
|
+
* the plugin-supplied `ServiceDescriptor` seam over the host's us-east-1
|
|
21
|
+
* signer. Each module carries the client's own documentation.
|
|
22
|
+
*/
|
|
23
|
+
export * from './aws/firehose.js';
|
|
24
|
+
export * from './aws/glue.js';
|
|
25
|
+
export * from './aws/lambda.js';
|
|
26
|
+
export * from './aws/s3tables.js';
|
|
27
|
+
/**
|
|
28
|
+
* The `Plugin` object the CLI discovers, and the namespace constant its
|
|
29
|
+
* `name` is built from. Both live in `plugin.ts`; `ANALYTICS_NAMESPACE`
|
|
30
|
+
* moved there from this module when the default export arrived to consume
|
|
31
|
+
* it, so the namespace has one home rather than a constant here and a
|
|
32
|
+
* literal there. Its conformance to core's `PLUGIN_NAME_PATTERN` is pinned
|
|
33
|
+
* by `index.test.ts` and again, through `validatePlugin`, by `plugin.test.ts`.
|
|
34
|
+
*/
|
|
35
|
+
export { default, ANALYTICS_NAMESPACE } from './plugin.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public surface of blogwright-analytics: CloudFront access logs routed
|
|
3
|
+
* through Firehose into an Iceberg table, with a local dashboard over it.
|
|
4
|
+
* The plugin is installed on demand with `blogwright plugin add analytics`,
|
|
5
|
+
* never shipped with the CLI - see DEVELOPMENT.md §Hexagonal architecture
|
|
6
|
+
* ("Features live in their own packages"). This module carries the package's
|
|
7
|
+
* own AWS service clients (built over core's shared SigV4 transport through
|
|
8
|
+
* the plugin-supplied `ServiceDescriptor` seam) and, since task 47, the
|
|
9
|
+
* `Plugin` default export the CLI's discovery loads.
|
|
10
|
+
*
|
|
11
|
+
* The default export and the package's `blogwright.plugin` manifest field
|
|
12
|
+
* landed together, deliberately: a package carrying the manifest without a
|
|
13
|
+
* conforming default export is a discovery error naming this package, so
|
|
14
|
+
* neither half may ship ahead of the other.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* The plugin's own four AWS service clients and the shapes they return. They
|
|
18
|
+
* live here rather than in `blogwright-core` because core enumerates no
|
|
19
|
+
* `firehose`, `glue`, `lambda` or `s3tables` signing name: each signs through
|
|
20
|
+
* the plugin-supplied `ServiceDescriptor` seam over the host's us-east-1
|
|
21
|
+
* signer. Each module carries the client's own documentation.
|
|
22
|
+
*/
|
|
23
|
+
export * from './aws/firehose.js';
|
|
24
|
+
export * from './aws/glue.js';
|
|
25
|
+
export * from './aws/lambda.js';
|
|
26
|
+
export * from './aws/s3tables.js';
|
|
27
|
+
/**
|
|
28
|
+
* The `Plugin` object the CLI discovers, and the namespace constant its
|
|
29
|
+
* `name` is built from. Both live in `plugin.ts`; `ANALYTICS_NAMESPACE`
|
|
30
|
+
* moved there from this module when the default export arrived to consume
|
|
31
|
+
* it, so the namespace has one home rather than a constant here and a
|
|
32
|
+
* literal there. Its conformance to core's `PLUGIN_NAME_PATTERN` is pinned
|
|
33
|
+
* by `index.test.ts` and again, through `validatePlugin`, by `plugin.test.ts`.
|
|
34
|
+
*/
|
|
35
|
+
export { default, ANALYTICS_NAMESPACE } from './plugin.js';
|