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/paths.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plugin's composition root for *paths*: the one module in
|
|
3
|
+
* `packages/analytics/src/` permitted to touch `import.meta.url`.
|
|
4
|
+
*
|
|
5
|
+
* `analytics-transform-function` (`nodes.ts`) deploys an artifact this package
|
|
6
|
+
* ships inside itself - the rolldown bundle and the manifest stamped beside it
|
|
7
|
+
* under `transform-hash.ts`'s `TRANSFORM_BUNDLE_DIR` - so something has to know where the
|
|
8
|
+
* installed package is on disk. The CLI answers the identical question for the
|
|
9
|
+
* build-agent at `packages/cli/src/context.ts` (`cliPackageDir()`, joined to
|
|
10
|
+
* `agent` and put on the context as `agentDir`), and the rule that keeps that
|
|
11
|
+
* honest is DEVELOPMENT.md §Hexagonal architecture: a location is resolved at
|
|
12
|
+
* a composition root and handed to domain code as *data*.
|
|
13
|
+
*
|
|
14
|
+
* `nodes.ts` has no wiring step of its own to resolve it in - the SPI reaches
|
|
15
|
+
* it through `plugin.nodes(ctx)` with nothing in between - so this module is
|
|
16
|
+
* that step. Resolving it inline in `nodes.ts` would satisfy the package's ban
|
|
17
|
+
* on Node's own filesystem module (`import.meta.url` is not a restricted
|
|
18
|
+
* import) while breaking the composition-root rule anyway, and it would put a
|
|
19
|
+
* second `import.meta.url` in the package the day a second artifact needs
|
|
20
|
+
* locating.
|
|
21
|
+
*
|
|
22
|
+
* This module resolves a directory and nothing else. The *bytes* under it are
|
|
23
|
+
* still read through `ctx.ports.fs`, never here: this package imports Node's
|
|
24
|
+
* filesystem module nowhere at all, and no `packages/analytics/src/` path joins
|
|
25
|
+
* the `no-restricted-imports` override list in `.oxlintrc.json`. (Named in
|
|
26
|
+
* prose rather than spelled as the specifier, so the definition of done's grep
|
|
27
|
+
* for it over this tree does not trip over a comment - the same care
|
|
28
|
+
* `ports.ts` takes with its own vendor name.)
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* The `blogwright-analytics` package root, resolved from this module's own
|
|
32
|
+
* location - `<package>/dist/paths.js` once compiled, `<package>/src/paths.ts`
|
|
33
|
+
* under vitest, and one level up from either.
|
|
34
|
+
*
|
|
35
|
+
* `resolve` is here for the reason `cliPackageDir()` states: `new URL('..', …)`
|
|
36
|
+
* yields a trailing separator, unlike every other directory value in the repo,
|
|
37
|
+
* so a caller writing `${ANALYTICS_PACKAGE_DIR}/x` would get a doubled one - in
|
|
38
|
+
* the path and in any error message built from it.
|
|
39
|
+
*
|
|
40
|
+
* The relative location of the transform artifacts under this directory is not
|
|
41
|
+
* restated here: `transform-hash.ts` owns `TRANSFORM_BUNDLE_DIR` as "where
|
|
42
|
+
* the build puts the Lambda artifacts, relative to the package root", and
|
|
43
|
+
* `nodes.ts` joins the two.
|
|
44
|
+
*/
|
|
45
|
+
export declare const ANALYTICS_PACKAGE_DIR: string;
|
package/dist/paths.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plugin's composition root for *paths*: the one module in
|
|
3
|
+
* `packages/analytics/src/` permitted to touch `import.meta.url`.
|
|
4
|
+
*
|
|
5
|
+
* `analytics-transform-function` (`nodes.ts`) deploys an artifact this package
|
|
6
|
+
* ships inside itself - the rolldown bundle and the manifest stamped beside it
|
|
7
|
+
* under `transform-hash.ts`'s `TRANSFORM_BUNDLE_DIR` - so something has to know where the
|
|
8
|
+
* installed package is on disk. The CLI answers the identical question for the
|
|
9
|
+
* build-agent at `packages/cli/src/context.ts` (`cliPackageDir()`, joined to
|
|
10
|
+
* `agent` and put on the context as `agentDir`), and the rule that keeps that
|
|
11
|
+
* honest is DEVELOPMENT.md §Hexagonal architecture: a location is resolved at
|
|
12
|
+
* a composition root and handed to domain code as *data*.
|
|
13
|
+
*
|
|
14
|
+
* `nodes.ts` has no wiring step of its own to resolve it in - the SPI reaches
|
|
15
|
+
* it through `plugin.nodes(ctx)` with nothing in between - so this module is
|
|
16
|
+
* that step. Resolving it inline in `nodes.ts` would satisfy the package's ban
|
|
17
|
+
* on Node's own filesystem module (`import.meta.url` is not a restricted
|
|
18
|
+
* import) while breaking the composition-root rule anyway, and it would put a
|
|
19
|
+
* second `import.meta.url` in the package the day a second artifact needs
|
|
20
|
+
* locating.
|
|
21
|
+
*
|
|
22
|
+
* This module resolves a directory and nothing else. The *bytes* under it are
|
|
23
|
+
* still read through `ctx.ports.fs`, never here: this package imports Node's
|
|
24
|
+
* filesystem module nowhere at all, and no `packages/analytics/src/` path joins
|
|
25
|
+
* the `no-restricted-imports` override list in `.oxlintrc.json`. (Named in
|
|
26
|
+
* prose rather than spelled as the specifier, so the definition of done's grep
|
|
27
|
+
* for it over this tree does not trip over a comment - the same care
|
|
28
|
+
* `ports.ts` takes with its own vendor name.)
|
|
29
|
+
*/
|
|
30
|
+
import { resolve } from 'node:path';
|
|
31
|
+
import { fileURLToPath } from 'node:url';
|
|
32
|
+
/**
|
|
33
|
+
* The `blogwright-analytics` package root, resolved from this module's own
|
|
34
|
+
* location - `<package>/dist/paths.js` once compiled, `<package>/src/paths.ts`
|
|
35
|
+
* under vitest, and one level up from either.
|
|
36
|
+
*
|
|
37
|
+
* `resolve` is here for the reason `cliPackageDir()` states: `new URL('..', …)`
|
|
38
|
+
* yields a trailing separator, unlike every other directory value in the repo,
|
|
39
|
+
* so a caller writing `${ANALYTICS_PACKAGE_DIR}/x` would get a doubled one - in
|
|
40
|
+
* the path and in any error message built from it.
|
|
41
|
+
*
|
|
42
|
+
* The relative location of the transform artifacts under this directory is not
|
|
43
|
+
* restated here: `transform-hash.ts` owns `TRANSFORM_BUNDLE_DIR` as "where
|
|
44
|
+
* the build puts the Lambda artifacts, relative to the package root", and
|
|
45
|
+
* `nodes.ts` joins the two.
|
|
46
|
+
*/
|
|
47
|
+
export const ANALYTICS_PACKAGE_DIR = resolve(fileURLToPath(new URL('..', import.meta.url)));
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The package's `Plugin` default export: the object the CLI's discovery
|
|
3
|
+
* loads once `package.json`'s `{ "blogwright": { "plugin": "analytics" } }`
|
|
4
|
+
* manifest field marks this package as a plugin. Everything the host learns
|
|
5
|
+
* about this plugin - its namespace, its help text, the config key it owns,
|
|
6
|
+
* its validator, its actions and its `init` contributor - it learns from
|
|
7
|
+
* here.
|
|
8
|
+
*
|
|
9
|
+
* **The command table, and what is deliberately missing from it.** Task 16's
|
|
10
|
+
* precedence rules (`packages/cli/src/plugin-commands.ts`, its TASK 13 and
|
|
11
|
+
* TASK 16 PRECEDENCE sections) decide which verbs a plugin may declare:
|
|
12
|
+
*
|
|
13
|
+
* - `bootstrap` and `destroy` are ALWAYS the CLI's generic verbs. They run
|
|
14
|
+
* `applyGraph`/`destroyGraph` over `nodes(ctx)`, and a plugin may not
|
|
15
|
+
* import the CLI, so a plugin cannot run that engine itself. Declaring
|
|
16
|
+
* either is not merely redundant, it is rejected at discovery -
|
|
17
|
+
* `rejectDeclaredLifecycleCollisions` (`packages/cli/src/plugins.ts`)
|
|
18
|
+
* turns the whole package into a load failure naming the colliding
|
|
19
|
+
* action. They are absent here for that reason, not for tidiness.
|
|
20
|
+
* - `status` is the generic verb UNLESS the plugin declares its own, and
|
|
21
|
+
* this one does: `analytics status` reports strictly more than the
|
|
22
|
+
* generic verb can - the stream's delivery health and the table's
|
|
23
|
+
* current row count on top of the node listing - and needs no engine
|
|
24
|
+
* call to do it, because `read()` lives on the plugin's own nodes.
|
|
25
|
+
* - `init` is ABSENT from the table on purpose, and this is the trap
|
|
26
|
+
* worth stating plainly: a declared command takes precedence over the
|
|
27
|
+
* generic action, so an `init` entry here would shadow the generic
|
|
28
|
+
* config-block splice. `blogwright analytics init` would then ask the
|
|
29
|
+
* operator every question below and write nothing at all. The `init`
|
|
30
|
+
* contributor ({@link Plugin.init}) is the ONLY way this plugin supplies
|
|
31
|
+
* `init`. (pds is the opposite case: it declares a real `init` command
|
|
32
|
+
* that creates the publication record and writes no config block, which
|
|
33
|
+
* precedence permits - a declared `init` owns the action, not an
|
|
34
|
+
* obligation to write config.) Declaring both is itself a discovery
|
|
35
|
+
* rejection, `rejectDeclaredInitCollisions` in the same collision pass.
|
|
36
|
+
* - `backfill` is legal to declare precisely because only `bootstrap` and
|
|
37
|
+
* `destroy` are reserved; it is the spec's optional, run-by-hand,
|
|
38
|
+
* one-shot action.
|
|
39
|
+
*
|
|
40
|
+
* None of the three declared actions is destructive, so no summary here
|
|
41
|
+
* states `--yes`. The one destructive verb this namespace answers is
|
|
42
|
+
* `analytics destroy --yes`, which is generic: its refusal without `--yes`
|
|
43
|
+
* is `runGenericDestroy`'s, and its summary is the CLI's own.
|
|
44
|
+
*
|
|
45
|
+
* **`nodes` is declared, and declaring it is what turns the generic verbs
|
|
46
|
+
* on.** `genericLifecycleCommand` and `genericLifecycleActions`
|
|
47
|
+
* (`packages/cli/src/plugin-commands.ts`) both gate all three generic verbs
|
|
48
|
+
* on `plugin.nodes` being declared at all, so before task 54 wired
|
|
49
|
+
* {@link buildAnalyticsNodes} to it, `analytics bootstrap` and `analytics
|
|
50
|
+
* destroy` were neither answered nor advertised in `blogwright --help`.
|
|
51
|
+
* They are both now, and `analytics status` still runs the declared command
|
|
52
|
+
* above rather than the generic verb, because a declared action wins.
|
|
53
|
+
*
|
|
54
|
+
* The property `nodes` carries is not just "twelve nodes": it is that the
|
|
55
|
+
* CLI's engine reconciles them against the store `toPluginContext` scoped to
|
|
56
|
+
* this plugin's name, `state/<env>.analytics.json` - never the site's
|
|
57
|
+
* `state/<env>.json`. `blogwright bootstrap` therefore provisions none of
|
|
58
|
+
* these twelve (the site's `buildNodes` does not consult discovery), and
|
|
59
|
+
* `blogwright destroy --yes` removes none of them, because
|
|
60
|
+
* `assertNoScopedState` (`packages/cli/src/commands.ts`) refuses the site
|
|
61
|
+
* teardown for as long as that scoped object exists and names
|
|
62
|
+
* `blogwright analytics destroy <env> --yes` as the way through.
|
|
63
|
+
*
|
|
64
|
+
* The builder itself is passed by reference - `nodes: buildAnalyticsNodes`,
|
|
65
|
+
* no wrapper. The array is not shared: each call builds a fresh one. There is
|
|
66
|
+
* nothing for a wrapper to do either, because the set does not vary with
|
|
67
|
+
* the context, and re-ordering it here would only hide that the order the
|
|
68
|
+
* builder returns is already a topological one.
|
|
69
|
+
*
|
|
70
|
+
* **`validateConfig` is task 44's validator, bound - not wrapped.** The
|
|
71
|
+
* property below is `validateAnalyticsConfig` itself. That matters twice
|
|
72
|
+
* over. It is the function that applies the four literal defaults and stamps
|
|
73
|
+
* the module-private symbol `resolveAnalyticsConfig` unseals
|
|
74
|
+
* (`config.ts`'s `unsealEnvDerivedOverrides`), so any wrapper that reshaped
|
|
75
|
+
* or re-defaulted the block would produce one the resolver rejects at
|
|
76
|
+
* runtime. And the CLI calls it with `undefined` when the operator's config
|
|
77
|
+
* carries no `analytics` key at all (`resolvePluginConfig`,
|
|
78
|
+
* `packages/cli/src/plugins.ts`, over `pluginBlock`'s plain property read),
|
|
79
|
+
* so the plugin's own defaults - not a bare `{}` - are what such an operator
|
|
80
|
+
* gets; `validateAnalyticsConfig` treats an absent block as an empty one for
|
|
81
|
+
* exactly that reason. Passing the function by reference keeps both
|
|
82
|
+
* properties instead of restating them.
|
|
83
|
+
*/
|
|
84
|
+
import type { Plugin } from 'blogwright-core';
|
|
85
|
+
import { type AnalyticsConfig } from './config.js';
|
|
86
|
+
/**
|
|
87
|
+
* The CLI namespace this plugin claims (`blogwright analytics <action>`),
|
|
88
|
+
* and - deliberately the same string - the single top-level config key it
|
|
89
|
+
* owns (`config.analytics`), the way pds's namespace and key coincide too.
|
|
90
|
+
* Re-exported from `index.ts`, where it lived before the default export
|
|
91
|
+
* existed to consume it.
|
|
92
|
+
*/
|
|
93
|
+
export declare const ANALYTICS_NAMESPACE = "analytics";
|
|
94
|
+
/**
|
|
95
|
+
* The plugin the CLI discovers. `Plugin<AnalyticsConfig>` names the block
|
|
96
|
+
* `validateConfig` returns, which is what the host puts on
|
|
97
|
+
* `ctx.pluginConfig` - the only route to this plugin's settings, and (for
|
|
98
|
+
* the two environment-carrying ones) only through
|
|
99
|
+
* `resolveAnalyticsConfig(ctx)`.
|
|
100
|
+
*/
|
|
101
|
+
declare const analyticsPlugin: Plugin<AnalyticsConfig>;
|
|
102
|
+
export default analyticsPlugin;
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The package's `Plugin` default export: the object the CLI's discovery
|
|
3
|
+
* loads once `package.json`'s `{ "blogwright": { "plugin": "analytics" } }`
|
|
4
|
+
* manifest field marks this package as a plugin. Everything the host learns
|
|
5
|
+
* about this plugin - its namespace, its help text, the config key it owns,
|
|
6
|
+
* its validator, its actions and its `init` contributor - it learns from
|
|
7
|
+
* here.
|
|
8
|
+
*
|
|
9
|
+
* **The command table, and what is deliberately missing from it.** Task 16's
|
|
10
|
+
* precedence rules (`packages/cli/src/plugin-commands.ts`, its TASK 13 and
|
|
11
|
+
* TASK 16 PRECEDENCE sections) decide which verbs a plugin may declare:
|
|
12
|
+
*
|
|
13
|
+
* - `bootstrap` and `destroy` are ALWAYS the CLI's generic verbs. They run
|
|
14
|
+
* `applyGraph`/`destroyGraph` over `nodes(ctx)`, and a plugin may not
|
|
15
|
+
* import the CLI, so a plugin cannot run that engine itself. Declaring
|
|
16
|
+
* either is not merely redundant, it is rejected at discovery -
|
|
17
|
+
* `rejectDeclaredLifecycleCollisions` (`packages/cli/src/plugins.ts`)
|
|
18
|
+
* turns the whole package into a load failure naming the colliding
|
|
19
|
+
* action. They are absent here for that reason, not for tidiness.
|
|
20
|
+
* - `status` is the generic verb UNLESS the plugin declares its own, and
|
|
21
|
+
* this one does: `analytics status` reports strictly more than the
|
|
22
|
+
* generic verb can - the stream's delivery health and the table's
|
|
23
|
+
* current row count on top of the node listing - and needs no engine
|
|
24
|
+
* call to do it, because `read()` lives on the plugin's own nodes.
|
|
25
|
+
* - `init` is ABSENT from the table on purpose, and this is the trap
|
|
26
|
+
* worth stating plainly: a declared command takes precedence over the
|
|
27
|
+
* generic action, so an `init` entry here would shadow the generic
|
|
28
|
+
* config-block splice. `blogwright analytics init` would then ask the
|
|
29
|
+
* operator every question below and write nothing at all. The `init`
|
|
30
|
+
* contributor ({@link Plugin.init}) is the ONLY way this plugin supplies
|
|
31
|
+
* `init`. (pds is the opposite case: it declares a real `init` command
|
|
32
|
+
* that creates the publication record and writes no config block, which
|
|
33
|
+
* precedence permits - a declared `init` owns the action, not an
|
|
34
|
+
* obligation to write config.) Declaring both is itself a discovery
|
|
35
|
+
* rejection, `rejectDeclaredInitCollisions` in the same collision pass.
|
|
36
|
+
* - `backfill` is legal to declare precisely because only `bootstrap` and
|
|
37
|
+
* `destroy` are reserved; it is the spec's optional, run-by-hand,
|
|
38
|
+
* one-shot action.
|
|
39
|
+
*
|
|
40
|
+
* None of the three declared actions is destructive, so no summary here
|
|
41
|
+
* states `--yes`. The one destructive verb this namespace answers is
|
|
42
|
+
* `analytics destroy --yes`, which is generic: its refusal without `--yes`
|
|
43
|
+
* is `runGenericDestroy`'s, and its summary is the CLI's own.
|
|
44
|
+
*
|
|
45
|
+
* **`nodes` is declared, and declaring it is what turns the generic verbs
|
|
46
|
+
* on.** `genericLifecycleCommand` and `genericLifecycleActions`
|
|
47
|
+
* (`packages/cli/src/plugin-commands.ts`) both gate all three generic verbs
|
|
48
|
+
* on `plugin.nodes` being declared at all, so before task 54 wired
|
|
49
|
+
* {@link buildAnalyticsNodes} to it, `analytics bootstrap` and `analytics
|
|
50
|
+
* destroy` were neither answered nor advertised in `blogwright --help`.
|
|
51
|
+
* They are both now, and `analytics status` still runs the declared command
|
|
52
|
+
* above rather than the generic verb, because a declared action wins.
|
|
53
|
+
*
|
|
54
|
+
* The property `nodes` carries is not just "twelve nodes": it is that the
|
|
55
|
+
* CLI's engine reconciles them against the store `toPluginContext` scoped to
|
|
56
|
+
* this plugin's name, `state/<env>.analytics.json` - never the site's
|
|
57
|
+
* `state/<env>.json`. `blogwright bootstrap` therefore provisions none of
|
|
58
|
+
* these twelve (the site's `buildNodes` does not consult discovery), and
|
|
59
|
+
* `blogwright destroy --yes` removes none of them, because
|
|
60
|
+
* `assertNoScopedState` (`packages/cli/src/commands.ts`) refuses the site
|
|
61
|
+
* teardown for as long as that scoped object exists and names
|
|
62
|
+
* `blogwright analytics destroy <env> --yes` as the way through.
|
|
63
|
+
*
|
|
64
|
+
* The builder itself is passed by reference - `nodes: buildAnalyticsNodes`,
|
|
65
|
+
* no wrapper. The array is not shared: each call builds a fresh one. There is
|
|
66
|
+
* nothing for a wrapper to do either, because the set does not vary with
|
|
67
|
+
* the context, and re-ordering it here would only hide that the order the
|
|
68
|
+
* builder returns is already a topological one.
|
|
69
|
+
*
|
|
70
|
+
* **`validateConfig` is task 44's validator, bound - not wrapped.** The
|
|
71
|
+
* property below is `validateAnalyticsConfig` itself. That matters twice
|
|
72
|
+
* over. It is the function that applies the four literal defaults and stamps
|
|
73
|
+
* the module-private symbol `resolveAnalyticsConfig` unseals
|
|
74
|
+
* (`config.ts`'s `unsealEnvDerivedOverrides`), so any wrapper that reshaped
|
|
75
|
+
* or re-defaulted the block would produce one the resolver rejects at
|
|
76
|
+
* runtime. And the CLI calls it with `undefined` when the operator's config
|
|
77
|
+
* carries no `analytics` key at all (`resolvePluginConfig`,
|
|
78
|
+
* `packages/cli/src/plugins.ts`, over `pluginBlock`'s plain property read),
|
|
79
|
+
* so the plugin's own defaults - not a bare `{}` - are what such an operator
|
|
80
|
+
* gets; `validateAnalyticsConfig` treats an absent block as an empty one for
|
|
81
|
+
* exactly that reason. Passing the function by reference keeps both
|
|
82
|
+
* properties instead of restating them.
|
|
83
|
+
*/
|
|
84
|
+
import { backfill, dashboard, status } from './commands.js';
|
|
85
|
+
import { validateAnalyticsConfig } from './config.js';
|
|
86
|
+
import { buildAnalyticsNodes } from './nodes.js';
|
|
87
|
+
/**
|
|
88
|
+
* The CLI namespace this plugin claims (`blogwright analytics <action>`),
|
|
89
|
+
* and - deliberately the same string - the single top-level config key it
|
|
90
|
+
* owns (`config.analytics`), the way pds's namespace and key coincide too.
|
|
91
|
+
* Re-exported from `index.ts`, where it lived before the default export
|
|
92
|
+
* existed to consume it.
|
|
93
|
+
*/
|
|
94
|
+
export const ANALYTICS_NAMESPACE = 'analytics';
|
|
95
|
+
/** The answers {@link isYes} accepts, matching `confirm`'s (`packages/cli/src/logger.ts`). */
|
|
96
|
+
const YES_ANSWERS = new Set(['y', 'yes']);
|
|
97
|
+
/** True when the operator agreed. Empty input never reaches here - `ask` substitutes the default first. */
|
|
98
|
+
function isYes(answer) {
|
|
99
|
+
return YES_ANSWERS.has(answer.trim().toLowerCase());
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Run `block` past {@link validateAnalyticsConfig} and return the message it
|
|
103
|
+
* raised, or `undefined` when it holds - the shape `PluginQuestion.validate`
|
|
104
|
+
* asks for. Every prompt below validates through this, so the wizard rejects
|
|
105
|
+
* exactly what the config validator rejects, with the validator's own
|
|
106
|
+
* message, and neither list of rules nor set of messages has a second home.
|
|
107
|
+
*
|
|
108
|
+
* These validators are also the only thing standing between a typed answer
|
|
109
|
+
* and the JSON text the entries below interpolate it into. An answer carrying
|
|
110
|
+
* a quote would close its own string and open a setting the wizard never
|
|
111
|
+
* asked about - `tableBucket` above all, the field task 44 sealed behind a
|
|
112
|
+
* module-private symbol precisely so that no code path can produce an
|
|
113
|
+
* env-less bucket name (`config.ts`, and what an env-less one destroys). Such
|
|
114
|
+
* a splice would survive the host's re-parse AND `validateAnalyticsConfig`,
|
|
115
|
+
* because the key it opens is a legitimate one; what refuses it is the
|
|
116
|
+
* `table` prompt's own rule, that an Iceberg identifier has no quote in it.
|
|
117
|
+
*/
|
|
118
|
+
function rejectionOf(block) {
|
|
119
|
+
try {
|
|
120
|
+
validateAnalyticsConfig(block);
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
return err.message;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Validate a typed port answer. A non-numeric answer is handed to the
|
|
129
|
+
* validator as the string it is, so the rejection quotes what the operator
|
|
130
|
+
* typed rather than `NaN`.
|
|
131
|
+
*/
|
|
132
|
+
function portRejection(answer) {
|
|
133
|
+
const numeric = Number(answer);
|
|
134
|
+
return rejectionOf({ dashboard: { port: Number.isNaN(numeric) ? answer : numeric } });
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Ask the operator for this plugin's config block and return it as the
|
|
138
|
+
* property/comment entries `renderConfigBlock` (`packages/cli/src/config-block.ts`)
|
|
139
|
+
* renders - the entry shape `renderConfig` (`packages/cli/src/init.ts`)
|
|
140
|
+
* mirrors at the top level. Both paths that reach a contributor - the
|
|
141
|
+
* first-run wizard (`blogwright init`) and the generic per-plugin action
|
|
142
|
+
* (`blogwright analytics init`) - call this one function.
|
|
143
|
+
*
|
|
144
|
+
* Only the four settings whose defaults are plain literals are asked. The
|
|
145
|
+
* other two the block accepts - `tableBucket` and `saltSecretName` - are
|
|
146
|
+
* deliberately not asked: their defaults carry the environment (`config.ts`),
|
|
147
|
+
* a contributor is handed no environment (`PluginInitIo` is terminal-shaped
|
|
148
|
+
* and nothing more), and a prompt whose default is wrong for every
|
|
149
|
+
* environment but one is worse than no prompt at all. An operator overriding
|
|
150
|
+
* either writes it into the block by hand, where the validator still checks
|
|
151
|
+
* it.
|
|
152
|
+
*
|
|
153
|
+
* The defaults offered come from `validateAnalyticsConfig(undefined)` -
|
|
154
|
+
* task 44's validator applied to an absent block - rather than from
|
|
155
|
+
* constants restated here, so the value the wizard offers is by construction
|
|
156
|
+
* the value an operator gets by leaving the setting out. That is also the
|
|
157
|
+
* exact call the CLI makes for an operator with no `analytics` key.
|
|
158
|
+
*
|
|
159
|
+
* No question carries `required`. Every default below is a non-empty string,
|
|
160
|
+
* and `ask` (`packages/cli/src/init.ts`) substitutes the default before it
|
|
161
|
+
* consults `required`, so the flag has no reachable effect on any of them.
|
|
162
|
+
*
|
|
163
|
+
* Returns an empty array - never `undefined` - when the operator declines or
|
|
164
|
+
* the session cannot ask, so the composing caller writes no key, no block
|
|
165
|
+
* and no stray comma. Touches no filesystem: `PluginInitIo` carries no `fs`,
|
|
166
|
+
* and writing the answers is the host's job on both paths.
|
|
167
|
+
*/
|
|
168
|
+
async function askAnalyticsBlock(io) {
|
|
169
|
+
if (!io.isInteractive) {
|
|
170
|
+
io.logger.warn('analytics: not an interactive session - skipping the analytics block; run `blogwright analytics init` later, or write it by hand');
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
const defaults = validateAnalyticsConfig(undefined);
|
|
174
|
+
const enable = await io.ask({
|
|
175
|
+
prompt: 'set up analytics now? CloudFront access logs into an Iceberg table (y/n)',
|
|
176
|
+
defaultValue: 'y',
|
|
177
|
+
});
|
|
178
|
+
if (!isYes(enable))
|
|
179
|
+
return [];
|
|
180
|
+
const namespace = await io.ask({
|
|
181
|
+
prompt: 'Iceberg namespace holding the table',
|
|
182
|
+
defaultValue: defaults.namespace,
|
|
183
|
+
validate: (answer) => rejectionOf({ namespace: answer }),
|
|
184
|
+
});
|
|
185
|
+
const table = await io.ask({
|
|
186
|
+
prompt: 'Iceberg table the page views land in',
|
|
187
|
+
defaultValue: defaults.table,
|
|
188
|
+
validate: (answer) => rejectionOf({ table: answer }),
|
|
189
|
+
});
|
|
190
|
+
const bots = await io.ask({
|
|
191
|
+
prompt: 'bot traffic in dashboard queries - flag (keep, marked) or filter (excluded)',
|
|
192
|
+
defaultValue: defaults.bots,
|
|
193
|
+
validate: (answer) => rejectionOf({ bots: answer }),
|
|
194
|
+
});
|
|
195
|
+
const port = await io.ask({
|
|
196
|
+
prompt: 'port the local dashboard listens on',
|
|
197
|
+
defaultValue: String(defaults.dashboard.port),
|
|
198
|
+
validate: portRejection,
|
|
199
|
+
});
|
|
200
|
+
return [
|
|
201
|
+
{ property: `"namespace": "${namespace}"`, comment: 'Iceberg namespace holding the table' },
|
|
202
|
+
{ property: `"table": "${table}"`, comment: 'Iceberg table the page views land in' },
|
|
203
|
+
{
|
|
204
|
+
property: `"bots": "${bots}"`,
|
|
205
|
+
comment: 'flag keeps bot rows and marks them; filter excludes them from queries',
|
|
206
|
+
},
|
|
207
|
+
// The number is rendered from the parsed value, not the typed text: a
|
|
208
|
+
// `04317` an operator types validates fine as a number but is not legal
|
|
209
|
+
// JSON, and this block is re-parsed before it reaches disk.
|
|
210
|
+
{
|
|
211
|
+
property: `"dashboard": { "port": ${Number(port)} }`,
|
|
212
|
+
comment: 'the dashboard binds 127.0.0.1 on this port',
|
|
213
|
+
},
|
|
214
|
+
];
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* The plugin the CLI discovers. `Plugin<AnalyticsConfig>` names the block
|
|
218
|
+
* `validateConfig` returns, which is what the host puts on
|
|
219
|
+
* `ctx.pluginConfig` - the only route to this plugin's settings, and (for
|
|
220
|
+
* the two environment-carrying ones) only through
|
|
221
|
+
* `resolveAnalyticsConfig(ctx)`.
|
|
222
|
+
*/
|
|
223
|
+
const analyticsPlugin = {
|
|
224
|
+
name: ANALYTICS_NAMESPACE,
|
|
225
|
+
description: 'CloudFront access logs in an Iceberg table, with a local dashboard',
|
|
226
|
+
configKey: ANALYTICS_NAMESPACE,
|
|
227
|
+
validateConfig: validateAnalyticsConfig,
|
|
228
|
+
init: askAnalyticsBlock,
|
|
229
|
+
nodes: buildAnalyticsNodes,
|
|
230
|
+
commands: [
|
|
231
|
+
{
|
|
232
|
+
action: 'status',
|
|
233
|
+
summary: "show resources, stream delivery health and the table's row count",
|
|
234
|
+
run: status,
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
action: 'dashboard',
|
|
238
|
+
summary: 'serve the local dashboard over the table on 127.0.0.1',
|
|
239
|
+
run: dashboard,
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
action: 'backfill',
|
|
243
|
+
summary: "one-shot fill of pre-Firehose days from the site's logs",
|
|
244
|
+
run: backfill,
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
};
|
|
248
|
+
export default analyticsPlugin;
|
package/dist/ports.d.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics-owned ports. Shared ports (`FileSystem`, `Terminal`) come from
|
|
3
|
+
* blogwright-core; the ports here serve only this package, following the CLI's
|
|
4
|
+
* private-ports precedent (`packages/cli/src/ports.ts:1-6`).
|
|
5
|
+
*
|
|
6
|
+
* **The vendor library lives only behind these interfaces.** DuckDB is what
|
|
7
|
+
* actually answers a named query - it attaches the S3 Tables catalog in
|
|
8
|
+
* read-only mode and runs the statement - and what writes a backfilled day,
|
|
9
|
+
* through a second attach of the same catalog that is not read-only. DuckDB's
|
|
10
|
+
* node-api package is imported nowhere in this package except the two adapters
|
|
11
|
+
* under `adapters/` that implement {@link AnalyticsQuery} and
|
|
12
|
+
* {@link AnalyticsIngest}. (Named in prose rather than spelled as the package
|
|
13
|
+
* specifier, so the definition of done's grep for that specifier over this
|
|
14
|
+
* tree does not trip over a comment.) Nothing in the signatures below names a
|
|
15
|
+
* DuckDB type either: a result row is this package's own {@link QueryRow} and
|
|
16
|
+
* an inserted row is {@link PageView} off `schema.ts`, neither of them a
|
|
17
|
+
* vendor object, so the named query set, the local server, the dashboard's
|
|
18
|
+
* data shaping and the backfill command all compile with no knowledge that
|
|
19
|
+
* DuckDB exists.
|
|
20
|
+
*
|
|
21
|
+
* That containment is load-bearing rather than tidy. The change spec records
|
|
22
|
+
* DuckDB's iceberg extension as *preview*, so its attach syntax may move, and
|
|
23
|
+
* it records that the "no Lake Formation grant" assumption holds only while
|
|
24
|
+
* the table bucket stays in IAM access-control mode. The port is what keeps
|
|
25
|
+
* either of those turning into an edit spread across the dashboard: both land
|
|
26
|
+
* in one adapter, which is also where DuckDB's errors are mapped into the
|
|
27
|
+
* repo's own vocabulary.
|
|
28
|
+
*
|
|
29
|
+
* Tests never start DuckDB. They substitute at these ports with the
|
|
30
|
+
* fixture-backed fake in `fixture-query.ts`, which answers the same named set
|
|
31
|
+
* through the same lookup and range validation the real adapter uses, and with
|
|
32
|
+
* the recording fake in `fixture-ingest.ts`, which keeps every day it was
|
|
33
|
+
* handed so a test can assert what was written rather than that something was.
|
|
34
|
+
*/
|
|
35
|
+
import type { QueryName, QueryParams } from './queries.js';
|
|
36
|
+
import type { PageView } from './schema.js';
|
|
37
|
+
/**
|
|
38
|
+
* One cell of a result row. `null` is deliberately not among them: a SQL NULL
|
|
39
|
+
* reaches a caller as an absent key, so no reader ever has to tell "the column
|
|
40
|
+
* was null" from "the column means null" - the repo's rule that no `null`
|
|
41
|
+
* stands for a domain value, applied at the read boundary.
|
|
42
|
+
*
|
|
43
|
+
* Not exported: no consumer needs the bare union today - one can reach the
|
|
44
|
+
* same type as `QueryRow[string]`. Export it once a real consumer needs it by
|
|
45
|
+
* name.
|
|
46
|
+
*/
|
|
47
|
+
type QueryValue = string | number | boolean;
|
|
48
|
+
/**
|
|
49
|
+
* One result row, keyed by the result column names the query's definition
|
|
50
|
+
* declares (`resultColumns` in `queries.ts`). A generic record rather than a
|
|
51
|
+
* per-query row type: the seven definitions' shapes are data, and a caller
|
|
52
|
+
* that wants a narrower type narrows it at its own boundary.
|
|
53
|
+
*/
|
|
54
|
+
export type QueryRow = Readonly<Record<string, QueryValue>>;
|
|
55
|
+
/**
|
|
56
|
+
* Reading the `page_views` table, in domain vocabulary: one operation, "answer
|
|
57
|
+
* this named query over this date range".
|
|
58
|
+
*
|
|
59
|
+
* `name` is a {@link QueryName} and not a `string`, which is the port's half of
|
|
60
|
+
* the spec's *Named queries, never client-supplied SQL* decision - a caller
|
|
61
|
+
* cannot ask this port for a statement, only for one of the seven the package
|
|
62
|
+
* defines. An implementation still validates at run time, because the one seam
|
|
63
|
+
* that feeds it is an HTTP path off the local server, where the compiler's
|
|
64
|
+
* guarantee has already been erased: an unknown name raises an error listing
|
|
65
|
+
* the available names, and an absent or inverted range raises rather than
|
|
66
|
+
* quietly standing in a default (see `prepareQuery`).
|
|
67
|
+
*/
|
|
68
|
+
export interface AnalyticsQuery {
|
|
69
|
+
/**
|
|
70
|
+
* Answer one of the named queries. Rows come back in the definition's own
|
|
71
|
+
* order (the `ORDER BY` it carries), so a caller never re-sorts to get a
|
|
72
|
+
* stable chart.
|
|
73
|
+
*/
|
|
74
|
+
run(name: QueryName, params: QueryParams): Promise<readonly QueryRow[]>;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Writing whole days into the `page_views` table, in domain vocabulary: one
|
|
78
|
+
* operation, "insert these rows for this UTC day".
|
|
79
|
+
*
|
|
80
|
+
* **It exists for the one-shot `analytics backfill` action and nothing else.**
|
|
81
|
+
* The steady-state pipeline never reaches it: CloudFront's logs arrive through
|
|
82
|
+
* Firehose, which writes the table itself (§Analytics pipeline → Shape), so
|
|
83
|
+
* the only writer on this side of the port is the hand-run pull of history
|
|
84
|
+
* that predates the Firehose delivery. The dashboard's read path is never
|
|
85
|
+
* handed an implementation of this interface - `createDashboardServer` takes
|
|
86
|
+
* an {@link AnalyticsQuery} and has no member to put one in - so a named query
|
|
87
|
+
* cannot become a write however the server is called.
|
|
88
|
+
*
|
|
89
|
+
* `day` is the `YYYY-MM-DD` UTC day every row in `rows` carries in its own
|
|
90
|
+
* `day` column, passed separately because it is the unit of the operation: an
|
|
91
|
+
* implementation inserts the whole day or none of it. Passing it also lets an
|
|
92
|
+
* implementation refuse a batch whose rows do not all belong to the day it was
|
|
93
|
+
* asked to write, which is a mistake no row-shaped signature could express.
|
|
94
|
+
*
|
|
95
|
+
* There is deliberately no `close()`. Task 55 measured the read side - an
|
|
96
|
+
* unclosed in-memory DuckDB instance holds no libuv handle and the process
|
|
97
|
+
* exits - and the write side adds nothing that outlives a call: each
|
|
98
|
+
* {@link insertDay} is its own transaction, committed or rolled back before it
|
|
99
|
+
* returns, so a command that stops between days leaves no open transaction and
|
|
100
|
+
* no half-written day behind. A `close()` here would be an interface member
|
|
101
|
+
* with no caller, which `pnpm knip` cannot see and a reader would take for a
|
|
102
|
+
* resource that needs releasing.
|
|
103
|
+
*/
|
|
104
|
+
export interface AnalyticsIngest {
|
|
105
|
+
/**
|
|
106
|
+
* Insert one whole UTC day's rows, atomically. An empty `rows` is a caller
|
|
107
|
+
* error rather than a no-op: a day with nothing to write is a day the caller
|
|
108
|
+
* should not have asked to insert, and treating it as success would make an
|
|
109
|
+
* "inserted" report line true of a day that got nothing.
|
|
110
|
+
*/
|
|
111
|
+
insertDay(day: string, rows: readonly PageView[]): Promise<void>;
|
|
112
|
+
}
|
|
113
|
+
export {};
|
package/dist/ports.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics-owned ports. Shared ports (`FileSystem`, `Terminal`) come from
|
|
3
|
+
* blogwright-core; the ports here serve only this package, following the CLI's
|
|
4
|
+
* private-ports precedent (`packages/cli/src/ports.ts:1-6`).
|
|
5
|
+
*
|
|
6
|
+
* **The vendor library lives only behind these interfaces.** DuckDB is what
|
|
7
|
+
* actually answers a named query - it attaches the S3 Tables catalog in
|
|
8
|
+
* read-only mode and runs the statement - and what writes a backfilled day,
|
|
9
|
+
* through a second attach of the same catalog that is not read-only. DuckDB's
|
|
10
|
+
* node-api package is imported nowhere in this package except the two adapters
|
|
11
|
+
* under `adapters/` that implement {@link AnalyticsQuery} and
|
|
12
|
+
* {@link AnalyticsIngest}. (Named in prose rather than spelled as the package
|
|
13
|
+
* specifier, so the definition of done's grep for that specifier over this
|
|
14
|
+
* tree does not trip over a comment.) Nothing in the signatures below names a
|
|
15
|
+
* DuckDB type either: a result row is this package's own {@link QueryRow} and
|
|
16
|
+
* an inserted row is {@link PageView} off `schema.ts`, neither of them a
|
|
17
|
+
* vendor object, so the named query set, the local server, the dashboard's
|
|
18
|
+
* data shaping and the backfill command all compile with no knowledge that
|
|
19
|
+
* DuckDB exists.
|
|
20
|
+
*
|
|
21
|
+
* That containment is load-bearing rather than tidy. The change spec records
|
|
22
|
+
* DuckDB's iceberg extension as *preview*, so its attach syntax may move, and
|
|
23
|
+
* it records that the "no Lake Formation grant" assumption holds only while
|
|
24
|
+
* the table bucket stays in IAM access-control mode. The port is what keeps
|
|
25
|
+
* either of those turning into an edit spread across the dashboard: both land
|
|
26
|
+
* in one adapter, which is also where DuckDB's errors are mapped into the
|
|
27
|
+
* repo's own vocabulary.
|
|
28
|
+
*
|
|
29
|
+
* Tests never start DuckDB. They substitute at these ports with the
|
|
30
|
+
* fixture-backed fake in `fixture-query.ts`, which answers the same named set
|
|
31
|
+
* through the same lookup and range validation the real adapter uses, and with
|
|
32
|
+
* the recording fake in `fixture-ingest.ts`, which keeps every day it was
|
|
33
|
+
* handed so a test can assert what was written rather than that something was.
|
|
34
|
+
*/
|
|
35
|
+
export {};
|