dynalitics 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +261 -2
- package/dist/adapter-version.d.ts +3 -0
- package/dist/adapter-version.js +3 -0
- package/dist/adapter.d.ts +12 -0
- package/dist/adapter.js +7 -0
- package/dist/cli/io.d.ts +9 -0
- package/dist/cli/io.js +10 -0
- package/dist/cli/run.d.ts +4 -0
- package/dist/cli/run.js +39 -0
- package/dist/cli/sql.d.ts +5 -0
- package/dist/cli/sql.js +129 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +5 -0
- package/dist/config/schema.d.ts +64 -0
- package/dist/config/schema.js +248 -0
- package/dist/config/validate.d.ts +30 -0
- package/dist/config/validate.js +68 -0
- package/dist/create.d.ts +16 -0
- package/dist/create.js +308 -0
- package/dist/dialect/common.d.ts +54 -0
- package/dist/dialect/common.js +197 -0
- package/dist/dialect/index.d.ts +5 -0
- package/dist/dialect/index.js +8 -0
- package/dist/dialect/mssql.d.ts +3 -0
- package/dist/dialect/mssql.js +104 -0
- package/dist/dialect/mysql.d.ts +3 -0
- package/dist/dialect/mysql.js +140 -0
- package/dist/dialect/postgres.d.ts +5 -0
- package/dist/dialect/postgres.js +90 -0
- package/dist/dialect/types.d.ts +79 -0
- package/dist/dialect/types.js +2 -0
- package/dist/driver/types.d.ts +74 -0
- package/dist/driver/types.js +66 -0
- package/dist/errors.d.ts +39 -0
- package/dist/errors.js +39 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +3 -0
- package/dist/internals.d.ts +20 -0
- package/dist/internals.js +8 -0
- package/dist/logger.d.ts +10 -0
- package/dist/logger.js +24 -0
- package/dist/model/column-model.d.ts +7 -0
- package/dist/model/column-model.js +240 -0
- package/dist/model/identifiers.d.ts +19 -0
- package/dist/model/identifiers.js +62 -0
- package/dist/model/physical.d.ts +19 -0
- package/dist/model/physical.js +70 -0
- package/dist/model/types.d.ts +92 -0
- package/dist/model/types.js +9 -0
- package/dist/protocol.d.ts +222 -0
- package/dist/protocol.js +5 -0
- package/dist/queue/pipeline.d.ts +46 -0
- package/dist/queue/pipeline.js +282 -0
- package/dist/read/adapters.d.ts +7 -0
- package/dist/read/adapters.js +75 -0
- package/dist/read/buckets.d.ts +7 -0
- package/dist/read/buckets.js +39 -0
- package/dist/read/capabilities.d.ts +39 -0
- package/dist/read/capabilities.js +65 -0
- package/dist/read/handler.d.ts +48 -0
- package/dist/read/handler.js +63 -0
- package/dist/read/http.d.ts +18 -0
- package/dist/read/http.js +65 -0
- package/dist/read/index.d.ts +5 -0
- package/dist/read/index.js +3 -0
- package/dist/read/lock/authorize.d.ts +22 -0
- package/dist/read/lock/authorize.js +32 -0
- package/dist/read/lock/base-path.d.ts +11 -0
- package/dist/read/lock/base-path.js +12 -0
- package/dist/read/lock/gate.d.ts +30 -0
- package/dist/read/lock/gate.js +24 -0
- package/dist/read/lock/index.d.ts +28 -0
- package/dist/read/lock/index.js +46 -0
- package/dist/read/lock/options.d.ts +13 -0
- package/dist/read/lock/options.js +18 -0
- package/dist/read/lock/serve-html.d.ts +6 -0
- package/dist/read/lock/serve-html.js +38 -0
- package/dist/read/mask.d.ts +12 -0
- package/dist/read/mask.js +55 -0
- package/dist/read/params.d.ts +56 -0
- package/dist/read/params.js +150 -0
- package/dist/read/physical.d.ts +11 -0
- package/dist/read/physical.js +23 -0
- package/dist/read/queries.d.ts +75 -0
- package/dist/read/queries.js +316 -0
- package/dist/read/report.d.ts +21 -0
- package/dist/read/report.js +32 -0
- package/dist/read/routes.d.ts +38 -0
- package/dist/read/routes.js +261 -0
- package/dist/read/scope.d.ts +11 -0
- package/dist/read/scope.js +47 -0
- package/dist/read/search/fields.d.ts +24 -0
- package/dist/read/search/fields.js +67 -0
- package/dist/read/search/parse.d.ts +48 -0
- package/dist/read/search/parse.js +124 -0
- package/dist/read/semaphore.d.ts +6 -0
- package/dist/read/semaphore.js +34 -0
- package/dist/read/service.d.ts +42 -0
- package/dist/read/service.js +266 -0
- package/dist/read/types.d.ts +62 -0
- package/dist/read/types.js +2 -0
- package/dist/redact.d.ts +8 -0
- package/dist/redact.js +60 -0
- package/dist/report.d.ts +12 -0
- package/dist/report.js +39 -0
- package/dist/schema/generate.d.ts +46 -0
- package/dist/schema/generate.js +71 -0
- package/dist/schema/index.d.ts +5 -0
- package/dist/schema/index.js +3 -0
- package/dist/startup/compare.d.ts +22 -0
- package/dist/startup/compare.js +165 -0
- package/dist/startup/fix-sql.d.ts +15 -0
- package/dist/startup/fix-sql.js +94 -0
- package/dist/startup/privileges.d.ts +21 -0
- package/dist/startup/privileges.js +63 -0
- package/dist/startup/session-timezone.d.ts +10 -0
- package/dist/startup/session-timezone.js +19 -0
- package/dist/startup/type-families.d.ts +10 -0
- package/dist/startup/type-families.js +55 -0
- package/dist/startup/unmapped-notice.d.ts +5 -0
- package/dist/startup/unmapped-notice.js +28 -0
- package/dist/track/build-row.d.ts +50 -0
- package/dist/track/build-row.js +208 -0
- package/dist/track/custom.d.ts +31 -0
- package/dist/track/custom.js +83 -0
- package/dist/track/denied-view.d.ts +14 -0
- package/dist/track/denied-view.js +21 -0
- package/dist/track/first-visit.d.ts +19 -0
- package/dist/track/first-visit.js +70 -0
- package/dist/track/ip.d.ts +17 -0
- package/dist/track/ip.js +87 -0
- package/dist/track/limits.d.ts +7 -0
- package/dist/track/limits.js +26 -0
- package/dist/types.d.ts +136 -0
- package/dist/types.js +2 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.js +3 -0
- package/package.json +71 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dynalitics contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,262 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dynalitics
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Server-side analytics that write **one row per visit into your own SQL database** (PostgreSQL, MySQL, SQL Server). There is no dynalitics service, no tracking script and no cookie (the optional [`dynalitics-consent`](https://www.npmjs.com/package/dynalitics-consent) banner sets one, holding only the visitor's choice): your server calls `track()`, and a row lands in a table you own. The same package also serves a read API for the dashboard.
|
|
4
|
+
|
|
5
|
+
> **Early release (`0.1.x`; the API may still change before 1.0).** Everything below has run against real PostgreSQL 17 and MySQL 8.4 in local integration tests. `0.1.0` is published from a maintainer's machine, so it has no npm provenance; later versions are published by GitHub Actions with provenance.
|
|
6
|
+
>
|
|
7
|
+
> **SQL Server support: TBD.** The first real run (GitHub Actions) found a bug in how SQL Server formats times, now fixed but not yet confirmed. `dynalitics-mssql` is published automatically, with the same version as the other packages, only once the SQL Server CI job is green; until then it is not on npm.
|
|
8
|
+
|
|
9
|
+
## Which packages you need
|
|
10
|
+
|
|
11
|
+
`dynalitics` is the core. It has no dependencies and never loads a database driver. You add **one adapter** for your database, and optionally the dashboard:
|
|
12
|
+
|
|
13
|
+
| Package | What it does | You need it when |
|
|
14
|
+
| ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | -------------------------------------- |
|
|
15
|
+
| `dynalitics` | `track()`, table validation, the `dynalitics sql` CLI, the read API (handler) | always |
|
|
16
|
+
| [`dynalitics-postgres`](https://www.npmjs.com/package/dynalitics-postgres) | connects to PostgreSQL through `pg` | your database is PostgreSQL |
|
|
17
|
+
| [`dynalitics-mysql`](https://www.npmjs.com/package/dynalitics-mysql) | connects to MySQL through `mysql2` | your database is MySQL |
|
|
18
|
+
| [`dynalitics-mssql`](https://www.npmjs.com/package/dynalitics-mssql) | **TBD:** connects to SQL Server through `mssql`; published once the SQL Server CI job is green | your database is SQL Server |
|
|
19
|
+
| [`dynalitics-consent`](https://www.npmjs.com/package/dynalitics-consent) | optional consent banner with your message; `consentFromRequest(req)` gives `track()` its `consent` | you want dynalitics to ask for consent |
|
|
20
|
+
| [`dynalitics-dashboard`](https://www.npmjs.com/package/dynalitics-dashboard) | the browser dashboard (HTML page or React components); talks to the read API | you want charts, a table, an explorer |
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
npm install dynalitics dynalitics-postgres # Node 22+, ESM only
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Only the driver of the adapter you install ends up in `node_modules`. `dynalitics sql` and `dynalitics/schema` work without any adapter.
|
|
27
|
+
|
|
28
|
+
## How the pieces fit
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
your web process your admin process (can be the same one)
|
|
32
|
+
createDynalitics({ writer }) createDynalitics({ reader })
|
|
33
|
+
| track() -> queue -> INSERT | createHandler() -> GET /api/* -> SELECT
|
|
34
|
+
v v
|
|
35
|
+
+---------------------- your table in your database ----------------------+
|
|
36
|
+
^
|
|
37
|
+
dynalitics-dashboard (browser) calls /api/*
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The recommended setup uses **two database users**: a writer that can only `INSERT` and a reader that can only `SELECT`. A compromised web process then cannot read back the personal data it wrote.
|
|
41
|
+
|
|
42
|
+
## Step 1: create the table (`dynalitics sql`)
|
|
43
|
+
|
|
44
|
+
dynalitics never creates, alters or drops anything at runtime. The CLI **prints** SQL for you to review and run. It never connects.
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
npx dynalitics sql --dialect postgres --table public.dynalitics_events \
|
|
48
|
+
--grants --writer dyn_writer --reader dyn_reader --out dynalitics.sql
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
| Flag | Meaning |
|
|
52
|
+
| ----------------------------------- | --------------------------------------------------------------------------------- |
|
|
53
|
+
| `--dialect postgres\|mysql\|mssql` | required |
|
|
54
|
+
| `--table <name>` | `name`, `schema.name` or `db.schema.name` |
|
|
55
|
+
| `--config <file.json>` | table shape as JSON: `columns`, `customFields`, `page`, `firstVisit` (no secrets) |
|
|
56
|
+
| `--no-indexes` | skip the recommended indexes |
|
|
57
|
+
| `--grants` / `--grants-only` | append the least-privilege GRANTs / print only them (works for any table) |
|
|
58
|
+
| `--writer <role>` `--reader <role>` | replace the `<writer_role>` / `<reader_role>` placeholders |
|
|
59
|
+
| `--out <file>` `--force` | write to a file; refuses to overwrite without `--force` |
|
|
60
|
+
|
|
61
|
+
The same output is available in code from `dynalitics/schema`: `generateSql({ dialect, schema, indexes, grants })` and `generateGrants({ dialect, table, writerRole, readerRole })`.
|
|
62
|
+
|
|
63
|
+
The recommended table has these columns. Rename any with `columns`, and drop optional ones with `columns.<role> = false`:
|
|
64
|
+
|
|
65
|
+
| Role | Column | Notes |
|
|
66
|
+
| -------------- | ---------------- | --------------------------------------------------- |
|
|
67
|
+
| `id` | `id` | generated by the database; used for paging |
|
|
68
|
+
| `timestamp` | `visited_at` | set at `track()` time, UTC |
|
|
69
|
+
| `consent` | `consent` | boolean |
|
|
70
|
+
| `pageVisited` | `page_visited` | up to `page.maxLength` (512) |
|
|
71
|
+
| `ip` | `ip_address` | **personal**; how it is stored depends on `ip.mode` |
|
|
72
|
+
| `userId` | `user_id` | **personal** |
|
|
73
|
+
| `username` | `username` | **personal** |
|
|
74
|
+
| `email` | `email` | **personal** |
|
|
75
|
+
| `custom` | `custom` | JSON for your declared custom fields |
|
|
76
|
+
| `isFirstVisit` | `is_first_visit` | only with `firstVisit.enabled` |
|
|
77
|
+
|
|
78
|
+
## Step 2: track visits
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { createDynalitics } from 'dynalitics';
|
|
82
|
+
import { postgres } from 'dynalitics-postgres';
|
|
83
|
+
|
|
84
|
+
const dyn = await createDynalitics({
|
|
85
|
+
adapter: postgres,
|
|
86
|
+
connection: { writer: { connectionString: process.env.ANALYTICS_WRITER_URL! } },
|
|
87
|
+
table: 'public.dynalitics_events',
|
|
88
|
+
ip: { mode: 'hash', salt: process.env.ANALYTICS_IP_SALT! },
|
|
89
|
+
customFields: { plan: { type: 'string', maxLength: 32, personal: false, breakdown: true } },
|
|
90
|
+
onError: (e) => logger.warn(e), // already redacted: never contains connection details
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// In a request handler. Fire and forget: it never throws and never waits for the database.
|
|
94
|
+
void dyn.track({
|
|
95
|
+
pageVisited: req.originalUrl, // query string and fragment are stripped unless page.keepQuery
|
|
96
|
+
consent: req.cookies.analytics === 'yes', // YOU decide; anything but `true` counts as denied
|
|
97
|
+
ip: req.ip, // you pass the trusted address; dynalitics never reads X-Forwarded-For
|
|
98
|
+
userId: session?.userId,
|
|
99
|
+
username: session?.name,
|
|
100
|
+
email: session?.email,
|
|
101
|
+
custom: { plan: 'pro' },
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
await dyn.close(); // on shutdown: flushes the queue (5 s by default), then closes the pool
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`createDynalitics` is async: it connects, then checks that your table matches the configuration. If it does not, it throws a `DynaliticsSchemaError` whose message contains the SQL that would fix it, for you to run (it is never executed). It can also throw `DynaliticsConfigError` (lists every problem at once) and `DynaliticsConnectionError`. No error ever contains a connection string or password.
|
|
108
|
+
|
|
109
|
+
### The instance
|
|
110
|
+
|
|
111
|
+
| Member | What it does |
|
|
112
|
+
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
|
113
|
+
| `track(visit)` | queues one visit. Resolves `{ status: 'queued' }`, `{ status: 'dropped', reason }` or `{ status: 'rejected', issues }`. Never rejects. |
|
|
114
|
+
| `flush({ timeoutMs? })` | waits until everything queued so far is written or has failed. Call it before a serverless function returns. |
|
|
115
|
+
| `close({ timeoutMs? })` | stops accepting visits, flushes, closes the pools dynalitics created. Idempotent. |
|
|
116
|
+
| `stats()` | counters: `queued`, `written`, `droppedFull`, `rejected`, `failed`, `queueLength`, `lruSize`, `lastErrorAt` |
|
|
117
|
+
| `capabilities` | `{ canWrite, canRead }`: which credentials are configured |
|
|
118
|
+
| `model`, `table` | the resolved column model and table, for diagnostics |
|
|
119
|
+
|
|
120
|
+
### What is stored (consent)
|
|
121
|
+
|
|
122
|
+
| `consent` you pass | Stored |
|
|
123
|
+
| ---------------------------------- | ----------------------------------------------------------------------------- |
|
|
124
|
+
| `true` | page, time, and the IP, user id, username, email and custom fields you passed |
|
|
125
|
+
| `false`, or missing and no default | page, time and `consent = false` only |
|
|
126
|
+
| anything else (`'yes'`, `1`) | never coerced: stored like `false` and reported as `invalid_visit` |
|
|
127
|
+
|
|
128
|
+
A custom field survives a denial only with `personal: false` **and** `keepWhenDenied: true`. A synchronous default is possible with `consent: { default: (visit) => boolean }`. To have dynalitics ask the visitor, use [`dynalitics-consent`](https://www.npmjs.com/package/dynalitics-consent) and pass `consent: consentFromRequest(req)`. The full matrix is in [`SECURITY.md`](https://github.com/lucsoftsl/dynalitics/blob/main/SECURITY.md).
|
|
129
|
+
|
|
130
|
+
### Configuration reference
|
|
131
|
+
|
|
132
|
+
| Option | Default | Meaning |
|
|
133
|
+
| ------------------------------------------------------ | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
134
|
+
| `adapter` | **required** | `postgres`, `mysql` or `mssql` from the adapter package |
|
|
135
|
+
| `connection` | **required** | `{ writer, reader }` (recommended), one of them, or a single credential. A credential is `{ connectionString }` or `{ pool }` (your own pool; dynalitics never closes it) |
|
|
136
|
+
| `acknowledgeSingleCredential` | `false` | silences the startup warning when one credential does both jobs |
|
|
137
|
+
| `table` | **required** | `'events'`, `'schema.events'` or `{ schema, name }` |
|
|
138
|
+
| `tableMode` | `'recommended'` | `'existing'` maps your own table through an explicit `columns` mapping (partly built: [`docs/SPEC.md`](https://github.com/lucsoftsl/dynalitics/blob/main/docs/SPEC.md) section 17.5) |
|
|
139
|
+
| `columns` | standard names | role → column name, or `false` to not store that role |
|
|
140
|
+
| `customFields` | none | `{ name: { type, maxLength?, personal?, breakdown?, keepWhenDenied?, column? } }`, see below |
|
|
141
|
+
| `page.keepQuery` / `page.maxLength` | `false` / `512` | keep the query string; maximum stored length (up to 700) |
|
|
142
|
+
| `page.normalize` | none | `(path) => string`, e.g. turn `/users/42` into `/users/:id` |
|
|
143
|
+
| `ip.mode` | `'truncate'` | `'truncate'` (keep the network part), `'hash'` (needs `ip.salt`, at least 16 characters), `'raw'` |
|
|
144
|
+
| `ip.ipv4Prefix` / `ip.ipv6Prefix` | `24` / `48` | bits kept by `truncate` |
|
|
145
|
+
| `consent.default` | none | synchronous `(visit) => boolean`, used when `consent` is missing |
|
|
146
|
+
| `firstVisit.enabled` / `firstVisit.cacheSize` | `false` / `50000` | write-time "first visit" detection (the writer then needs a column-level SELECT) |
|
|
147
|
+
| `queue.maxSize` / `queue.batchSize` | `10000` / `200` | bounded in-memory queue, and rows per `INSERT` |
|
|
148
|
+
| `queue.flushIntervalMs` | `1000` | how often a partial batch is written |
|
|
149
|
+
| `queue.backpressure` / `queue.maxBlockMs` | `'drop'` / `2000` | when the queue is full: drop the visit, or `'block'` the caller for up to `maxBlockMs` |
|
|
150
|
+
| `queue.retry` | 3 tries, 200 to 5000 ms | retry with backoff for transient database errors |
|
|
151
|
+
| `limits.customMaxKeys` / `customMaxBytes` / `oversize` | `20` / `4096` / `'truncate'` | caps on custom data; `'reject'` refuses oversize visits instead |
|
|
152
|
+
| `poolSize` | writer `4`, reader `2` | per credential, 1 to 20; ignored for your own pools |
|
|
153
|
+
| `connectTimeoutMs` | `10000` | |
|
|
154
|
+
| `validateOnStart` | `'strict'` | `'skip'` skips the table check (logged loudly; not allowed with `tableMode: 'existing'`) |
|
|
155
|
+
| `onError` / `logger` | none / `console` | error reports (redacted) and log output |
|
|
156
|
+
|
|
157
|
+
**Custom fields.** `type` is `'string' | 'integer' | 'number' | 'boolean'`. `personal` defaults to **true** (fails closed): such a field is masked in the dashboard, dropped on denied rows and never searchable, unless you declare `personal: false`. `breakdown: true` (needs `personal: false`) lets the dashboard group by the field. `column: true` (or a column name) stores the field in its own column instead of the JSON column.
|
|
158
|
+
|
|
159
|
+
## Step 3: serve the read API (`dynalitics/handler`)
|
|
160
|
+
|
|
161
|
+
The dashboard never talks to your database. It calls a small, fixed read API that you mount in your app. The API needs a **reader** credential, so it uses its own instance (or one instance that has both credentials).
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
import { createDynalitics } from 'dynalitics';
|
|
165
|
+
import { createHandler, toNodeListener } from 'dynalitics/handler';
|
|
166
|
+
import { dashboardHtml } from 'dynalitics-dashboard/html';
|
|
167
|
+
import { postgres } from 'dynalitics-postgres';
|
|
168
|
+
|
|
169
|
+
const reader = await createDynalitics({
|
|
170
|
+
adapter: postgres,
|
|
171
|
+
connection: { reader: { connectionString: process.env.ANALYTICS_READER_URL! } },
|
|
172
|
+
table: 'public.dynalitics_events',
|
|
173
|
+
ip: { mode: 'hash', salt: process.env.ANALYTICS_IP_SALT! }, // repeat the writer's table shape
|
|
174
|
+
customFields: { plan: { type: 'string', maxLength: 32, personal: false, breakdown: true } },
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const handler = createHandler(reader, {
|
|
178
|
+
authorize: (req) => isAdmin(req.raw), // REQUIRED. Return true, or { allowed: true, revealPii? }
|
|
179
|
+
basePath: '/admin/analytics',
|
|
180
|
+
dashboardHtml, // optional: also serve the HTML dashboard at GET /admin/analytics/
|
|
181
|
+
scope: 'none', // or { column, value } when several sites share the table
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
app.use('/admin/analytics', toNodeListener(handler)); // Express, plain node:http
|
|
185
|
+
// For Next.js route handlers, Hono, Bun or Deno: toFetchHandler(handler) gives (Request) => Promise<Response>.
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
| Option | Default | Meaning |
|
|
189
|
+
| ------------------------------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
|
190
|
+
| `authorize(req)` | **required** | runs first on every request. Only `true` or `{ allowed: true }` lets a request through; anything else is `403` |
|
|
191
|
+
| `basePath` | `'/'` | where the handler is mounted |
|
|
192
|
+
| `dashboardHtml` | none | the string from `dynalitics-dashboard/html`; served with a per-request CSP nonce |
|
|
193
|
+
| `scope` | logs a warning | `{ column, value }` adds `AND column = value` to every read; `'none'` means reading the whole table is intended |
|
|
194
|
+
| `pii.reveal` / `pii.maskUserId` | `false` / `true` | personal values are masked unless `pii.reveal` is set, `authorize` returned `revealPii: true` **and** the request asks for `?pii=reveal` |
|
|
195
|
+
| `limits` | 92 days, 800 buckets, 100 rows, 10 s, 4 queries | `maxRangeDays`, `maxBuckets`, `topPagesMax`, `recentMax`, `timeoutMs` (per query, cancelled in the database), `concurrency` |
|
|
196
|
+
| `newVsReturning` | enabled, 31 days | `enabled`, `source: 'computed' \| 'column'`, `ipIdentity: 'auto' \| 'always' \| 'never'`, `maxRangeDays` |
|
|
197
|
+
| `explorer` | enabled, 31 days | `enabled`, `maxRangeDays` for the search routes below |
|
|
198
|
+
| `authorizeTimeoutMs` | `5000` | an `authorize` that takes longer fails closed with `500` |
|
|
199
|
+
| `cacheTtlMs` | `0` | short micro-cache for identical requests |
|
|
200
|
+
| `onError` | none | read-side error reports (route, status, code; never the request) |
|
|
201
|
+
|
|
202
|
+
### Routes (all `GET`, under `basePath`)
|
|
203
|
+
|
|
204
|
+
| Route | Returns |
|
|
205
|
+
| ----------------------- | -------------------------------------------------------------------------------- |
|
|
206
|
+
| `/` | the dashboard page (only if `dashboardHtml` was given) |
|
|
207
|
+
| `/api/meta` | what this setup supports: widgets, fields, limits, explorer fields |
|
|
208
|
+
| `/api/visits-over-time` | visits per bucket, with an optional `breakdown` by a `breakdown: true` field |
|
|
209
|
+
| `/api/top-pages` | most visited pages |
|
|
210
|
+
| `/api/new-vs-returning` | new, returning and unknown visitors per bucket (an estimate when IP is involved) |
|
|
211
|
+
| `/api/consent-rate` | granted vs declined per bucket |
|
|
212
|
+
| `/api/recent-visits` | newest visits, masked, with cursor paging |
|
|
213
|
+
| `/api/search` | explorer: visits matching `q`, plus the match count |
|
|
214
|
+
| `/api/search/histogram` | explorer: matches per bucket, with an optional `split` by a facet field |
|
|
215
|
+
| `/api/search/fields` | explorer: the top values per field among the matches |
|
|
216
|
+
|
|
217
|
+
Unknown or repeated parameters are `400` before any SQL runs; a request can never pass SQL, a column name or a sort order. Every response uses the same JSON envelope: `{ success, data, unavailable, error, meta }`. The exact shapes are in `dynalitics/protocol` (types only) and [`docs/SPEC.md`](https://github.com/lucsoftsl/dynalitics/blob/main/docs/SPEC.md) sections 10 and 18.
|
|
218
|
+
|
|
219
|
+
### The explorer's query language (`q`)
|
|
220
|
+
|
|
221
|
+
```
|
|
222
|
+
pricing any searchable field contains "pricing" (case-insensitive)
|
|
223
|
+
"two words" the exact phrase
|
|
224
|
+
page:/blog* starts with /blog (also *end and *contains*)
|
|
225
|
+
plan:pro equals pro, case-insensitive; inside quotes * is literal: plan:"a*b"
|
|
226
|
+
-consent:false leave these out; works on any part, rows without the field are kept
|
|
227
|
+
amount:>=10 numbers: > >= < <=
|
|
228
|
+
plan:* has a value
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Parts are combined with AND. The searchable fields are `page`, `consent`, `isFirstVisit` (only with the write-time column) and custom fields declared `personal: false`. **Personal data is never searchable:** `email:x` gets the same "unknown field" answer as a typo, so a search cannot be used to find out whether an address exists. Limits: 512 characters, 12 parts, 128 characters per value. A bad query is `400 invalid_query` with a fixed message, a `reason` and the character `position`.
|
|
232
|
+
|
|
233
|
+
Case-insensitive matching uses the database's `LOWER()`: exact for ASCII, while other scripts depend on your collation (untested beyond Latin-1 accents). A search reads every row in the chosen range, which is why the explorer's range is capped at 31 days by default.
|
|
234
|
+
|
|
235
|
+
## Entry points
|
|
236
|
+
|
|
237
|
+
| Import | Contents |
|
|
238
|
+
| --------------------- | --------------------------------------------------------------------------- |
|
|
239
|
+
| `dynalitics` | `createDynalitics`, error classes, types |
|
|
240
|
+
| `dynalitics/handler` | `createHandler`, `toNodeListener`, `toFetchHandler` |
|
|
241
|
+
| `dynalitics/schema` | `generateSql`, `generateGrants`, `buildColumnModel` (no driver, no network) |
|
|
242
|
+
| `dynalitics/protocol` | wire types of the read API (types only) |
|
|
243
|
+
| `dynalitics/adapter` | the contract for adapter packages (see below) |
|
|
244
|
+
| `npx dynalitics sql` | the SQL generator CLI |
|
|
245
|
+
|
|
246
|
+
## Writing your own adapter
|
|
247
|
+
|
|
248
|
+
An adapter is a frozen object `{ apiVersion: 1, dialect, packageName, createDriver(options) }`. `createDriver` receives `{ credential, max, connectTimeoutMs, redact, requestTimeoutMs? }` and returns a `Driver` with `query(compiled, { timeoutMs })`, `isTransient(err)` and `close()`. The SQL text always comes from dynalitics; an adapter only runs it. Build every error with `toDriverError` from `dynalitics/adapter`, so that values a database echoes in its error messages never leak, and never close a pool the host passed in. `dialect` must be one of the three supported dialects. The three official adapters are about 150 lines each and are the best reference ([source](https://github.com/lucsoftsl/dynalitics/tree/main/packages)).
|
|
249
|
+
|
|
250
|
+
## Know this before you rely on it
|
|
251
|
+
|
|
252
|
+
- **Delivery is in memory and at least once.** A crash loses queued visits; a retried batch can write a duplicate row.
|
|
253
|
+
- **Retention and deletion are your job.** There is no delete path.
|
|
254
|
+
- **Truncated and hashed IPs are still personal data.** So can page paths be (`/reset/<token>`); use `page.normalize`.
|
|
255
|
+
- **New vs returning is an estimate** whenever IP is part of the identity.
|
|
256
|
+
- **With your own pool**, query cancellation is best effort: set `statement_timeout` / `max_execution_time` on the reader role.
|
|
257
|
+
- `track()` does no rate limiting; protect public endpoints that call it.
|
|
258
|
+
- Not built: `extraColumns`, `dynalitics inspect`. Not supported: MariaDB, SQLite, Oracle.
|
|
259
|
+
|
|
260
|
+
More: the [repository README](https://github.com/lucsoftsl/dynalitics#readme) (guided tour), [`SECURITY.md`](https://github.com/lucsoftsl/dynalitics/blob/main/SECURITY.md) (what is guaranteed and which test backs it) and [`docs/SPEC.md`](https://github.com/lucsoftsl/dynalitics/blob/main/docs/SPEC.md) (the full contract).
|
|
261
|
+
|
|
262
|
+
MIT licensed.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 'dynalitics/adapter': the contract an adapter package implements (spec 5.2, v0.5).
|
|
3
|
+
* Imports no driver and no network code.
|
|
4
|
+
*/
|
|
5
|
+
export { ADAPTER_API_VERSION } from './adapter-version.js';
|
|
6
|
+
export { DEFAULT_REQUEST_TIMEOUT_MS, DriverError, IDLE_TIMEOUT_MS, errCode, errMessage, isDriverError, isTransientError, scrubMessage, toDriverError, } from './driver/types.js';
|
|
7
|
+
export type { Classification, Driver, DriverAdapter, DriverOptions } from './driver/types.js';
|
|
8
|
+
export type { CompiledQuery, Row } from './dialect/types.js';
|
|
9
|
+
export type { Dialect } from './model/types.js';
|
|
10
|
+
export type { Redactor } from './redact.js';
|
|
11
|
+
export type { Credential } from './types.js';
|
|
12
|
+
//# sourceMappingURL=adapter.d.ts.map
|
package/dist/adapter.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 'dynalitics/adapter': the contract an adapter package implements (spec 5.2, v0.5).
|
|
3
|
+
* Imports no driver and no network code.
|
|
4
|
+
*/
|
|
5
|
+
export { ADAPTER_API_VERSION } from './adapter-version.js';
|
|
6
|
+
export { DEFAULT_REQUEST_TIMEOUT_MS, DriverError, IDLE_TIMEOUT_MS, errCode, errMessage, isDriverError, isTransientError, scrubMessage, toDriverError, } from './driver/types.js';
|
|
7
|
+
//# sourceMappingURL=adapter.js.map
|
package/dist/cli/io.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface CliIo {
|
|
2
|
+
stdout(text: string): void;
|
|
3
|
+
stderr(text: string): void;
|
|
4
|
+
readFile(path: string): string;
|
|
5
|
+
exists(path: string): boolean;
|
|
6
|
+
writeFile(path: string, content: string, overwrite: boolean): void;
|
|
7
|
+
}
|
|
8
|
+
export declare const nodeIo: CliIo;
|
|
9
|
+
//# sourceMappingURL=io.d.ts.map
|
package/dist/cli/io.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
export const nodeIo = {
|
|
3
|
+
stdout: (text) => void process.stdout.write(text),
|
|
4
|
+
stderr: (text) => void process.stderr.write(text),
|
|
5
|
+
readFile: (path) => readFileSync(path, 'utf8'),
|
|
6
|
+
exists: (path) => existsSync(path),
|
|
7
|
+
// 'wx' fails if the file appeared since the exists() check, so a race cannot overwrite it.
|
|
8
|
+
writeFile: (path, content, overwrite) => writeFileSync(path, content, { flag: overwrite ? 'w' : 'wx' }),
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=io.js.map
|
package/dist/cli/run.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { VERSION } from '../version.js';
|
|
2
|
+
import { SQL_USAGE, runSql } from './sql.js';
|
|
3
|
+
const HELP = `dynalitics ${VERSION}
|
|
4
|
+
|
|
5
|
+
Commands
|
|
6
|
+
sql Prints the recommended table, indexes and least-privilege GRANTs.
|
|
7
|
+
It only prints (or writes a file): it never connects to a database and never executes anything.
|
|
8
|
+
inspect (planned) Read-only look at a table you already have.
|
|
9
|
+
|
|
10
|
+
Usage
|
|
11
|
+
${SQL_USAGE.split('\n').join('\n ')}
|
|
12
|
+
dynalitics --help | --version
|
|
13
|
+
`;
|
|
14
|
+
export async function runCli(argv, io) {
|
|
15
|
+
const [command, ...rest] = argv;
|
|
16
|
+
switch (command) {
|
|
17
|
+
case undefined:
|
|
18
|
+
io.stderr(HELP);
|
|
19
|
+
return 1;
|
|
20
|
+
case '--help':
|
|
21
|
+
case '-h':
|
|
22
|
+
case 'help':
|
|
23
|
+
io.stdout(HELP);
|
|
24
|
+
return 0;
|
|
25
|
+
case '--version':
|
|
26
|
+
case '-v':
|
|
27
|
+
io.stdout(`${VERSION}\n`);
|
|
28
|
+
return 0;
|
|
29
|
+
case 'sql':
|
|
30
|
+
return Promise.resolve(runSql(rest, io));
|
|
31
|
+
case 'inspect':
|
|
32
|
+
io.stderr('dynalitics inspect is not available yet (planned for phase 2).\n');
|
|
33
|
+
return 1;
|
|
34
|
+
default:
|
|
35
|
+
io.stderr(`Unknown command ${JSON.stringify(command)}.\n\n${HELP}`);
|
|
36
|
+
return 1;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=run.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { CliIo } from './io.js';
|
|
2
|
+
export declare const SQL_USAGE = "dynalitics sql --dialect postgres|mysql|mssql --table <name | schema.name | db.schema.name>\n [--config <file.json>] table-shape settings as JSON (columns, customFields, page, firstVisit)\n [--no-indexes]\n [--grants] append least-privilege GRANTs (placeholders <writer_role>/<reader_role>)\n [--grants-only] print ONLY the GRANTs (works for a table you already have)\n [--writer <name>] [--reader <name>] replace the placeholders (requires --grants or --grants-only)\n [--out <file>] [--force] write to a file; refuses to overwrite without --force\n";
|
|
3
|
+
export declare function runSql(argv: readonly string[], io: CliIo): number;
|
|
4
|
+
export declare function reportError(err: unknown, io: CliIo): number;
|
|
5
|
+
//# sourceMappingURL=sql.d.ts.map
|
package/dist/cli/sql.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { parseArgs } from 'node:util';
|
|
2
|
+
import { runRule } from '../config/validate.js';
|
|
3
|
+
import { schemaFileRule } from '../config/schema.js';
|
|
4
|
+
import { DynaliticsConfigError } from '../errors.js';
|
|
5
|
+
import { buildColumnModel } from '../model/column-model.js';
|
|
6
|
+
import { generateGrants, generateSql } from '../schema/generate.js';
|
|
7
|
+
export const SQL_USAGE = `dynalitics sql --dialect postgres|mysql|mssql --table <name | schema.name | db.schema.name>
|
|
8
|
+
[--config <file.json>] table-shape settings as JSON (columns, customFields, page, firstVisit)
|
|
9
|
+
[--no-indexes]
|
|
10
|
+
[--grants] append least-privilege GRANTs (placeholders <writer_role>/<reader_role>)
|
|
11
|
+
[--grants-only] print ONLY the GRANTs (works for a table you already have)
|
|
12
|
+
[--writer <name>] [--reader <name>] replace the placeholders (requires --grants or --grants-only)
|
|
13
|
+
[--out <file>] [--force] write to a file; refuses to overwrite without --force
|
|
14
|
+
`;
|
|
15
|
+
const DIALECTS = ['postgres', 'mysql', 'mssql'];
|
|
16
|
+
const OPTIONS = {
|
|
17
|
+
dialect: { type: 'string' },
|
|
18
|
+
table: { type: 'string' },
|
|
19
|
+
config: { type: 'string' },
|
|
20
|
+
'no-indexes': { type: 'boolean' },
|
|
21
|
+
grants: { type: 'boolean' },
|
|
22
|
+
'grants-only': { type: 'boolean' },
|
|
23
|
+
writer: { type: 'string' },
|
|
24
|
+
reader: { type: 'string' },
|
|
25
|
+
out: { type: 'string' },
|
|
26
|
+
force: { type: 'boolean' },
|
|
27
|
+
help: { type: 'boolean', short: 'h' },
|
|
28
|
+
};
|
|
29
|
+
class UsageError extends Error {
|
|
30
|
+
}
|
|
31
|
+
function loadSchemaFile(path, io) {
|
|
32
|
+
let text;
|
|
33
|
+
try {
|
|
34
|
+
text = io.readFile(path);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
throw new UsageError(`Cannot read ${path}.`);
|
|
38
|
+
}
|
|
39
|
+
let parsed;
|
|
40
|
+
try {
|
|
41
|
+
parsed = JSON.parse(text);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
throw new UsageError(`${path} is not valid JSON. Only JSON is accepted; code is never executed.`);
|
|
45
|
+
}
|
|
46
|
+
// Only table-shape keys are allowed: this is how connection strings and functions are kept out.
|
|
47
|
+
const issues = runRule(schemaFileRule, parsed, 'config file');
|
|
48
|
+
if (issues.length > 0)
|
|
49
|
+
throw new DynaliticsConfigError(issues);
|
|
50
|
+
return parsed;
|
|
51
|
+
}
|
|
52
|
+
export function runSql(argv, io) {
|
|
53
|
+
try {
|
|
54
|
+
const { values } = parseArgs({
|
|
55
|
+
args: [...argv],
|
|
56
|
+
options: OPTIONS,
|
|
57
|
+
strict: true,
|
|
58
|
+
allowPositionals: false,
|
|
59
|
+
});
|
|
60
|
+
if (values.help === true) {
|
|
61
|
+
io.stdout(SQL_USAGE);
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
const dialect = values.dialect;
|
|
65
|
+
if (dialect === undefined || !DIALECTS.includes(dialect)) {
|
|
66
|
+
throw new UsageError('--dialect must be one of: postgres, mysql, mssql.');
|
|
67
|
+
}
|
|
68
|
+
if (values.table === undefined)
|
|
69
|
+
throw new UsageError('--table is required.');
|
|
70
|
+
const wantsGrants = values.grants === true || values['grants-only'] === true;
|
|
71
|
+
if ((values.writer !== undefined || values.reader !== undefined) && !wantsGrants) {
|
|
72
|
+
throw new UsageError('--writer and --reader need --grants or --grants-only.');
|
|
73
|
+
}
|
|
74
|
+
const file = values.config === undefined ? {} : loadSchemaFile(values.config, io);
|
|
75
|
+
const schema = { table: values.table, ...file };
|
|
76
|
+
const roles = {
|
|
77
|
+
...(values.writer === undefined ? {} : { writerRole: values.writer }),
|
|
78
|
+
...(values.reader === undefined ? {} : { readerRole: values.reader }),
|
|
79
|
+
};
|
|
80
|
+
let text;
|
|
81
|
+
if (values['grants-only'] === true) {
|
|
82
|
+
const identity = file.firstVisit?.enabled === true ? buildColumnModel(schema).identity : undefined;
|
|
83
|
+
const columns = identity === undefined
|
|
84
|
+
? undefined
|
|
85
|
+
: [identity.userId, identity.ip].filter((c) => c !== undefined).map((c) => c.name);
|
|
86
|
+
text = `${generateGrants({
|
|
87
|
+
dialect: dialect,
|
|
88
|
+
table: values.table,
|
|
89
|
+
...roles,
|
|
90
|
+
...(columns === undefined ? {} : { writerSelectColumns: columns }),
|
|
91
|
+
}).join('\n')}\n`;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
text = generateSql({
|
|
95
|
+
dialect: dialect,
|
|
96
|
+
schema,
|
|
97
|
+
...(values['no-indexes'] === true ? { indexes: false } : {}),
|
|
98
|
+
...(values.grants === true ? { grants: roles } : {}),
|
|
99
|
+
}).full;
|
|
100
|
+
}
|
|
101
|
+
if (values.out === undefined) {
|
|
102
|
+
io.stdout(text);
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
if (io.exists(values.out) && values.force !== true) {
|
|
106
|
+
throw new UsageError(`Refusing to overwrite ${values.out}; pass --force to replace it.`);
|
|
107
|
+
}
|
|
108
|
+
io.writeFile(values.out, text, values.force === true);
|
|
109
|
+
io.stderr(`Wrote ${values.out}\n`);
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
return reportError(err, io);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export function reportError(err, io) {
|
|
117
|
+
if (err instanceof DynaliticsConfigError)
|
|
118
|
+
io.stderr(`${err.issues.join('\n')}\n`);
|
|
119
|
+
else if (err instanceof UsageError)
|
|
120
|
+
io.stderr(`${err.message}\n`);
|
|
121
|
+
else if (err instanceof TypeError &&
|
|
122
|
+
err.code?.startsWith('ERR_PARSE_ARGS')) {
|
|
123
|
+
io.stderr(`${err.message}\n\n${SQL_USAGE}`);
|
|
124
|
+
}
|
|
125
|
+
else
|
|
126
|
+
io.stderr(`Unexpected error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
127
|
+
return 1;
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=sql.js.map
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { DriverAdapter } from '../driver/types.js';
|
|
2
|
+
import type { Logger } from '../logger.js';
|
|
3
|
+
import type { Dialect, IpMode, SchemaConfig } from '../model/types.js';
|
|
4
|
+
import type { Credential, DynaliticsConfig, ErrorInfo, Visit } from '../types.js';
|
|
5
|
+
import { type Rule } from './validate.js';
|
|
6
|
+
/** Rule for the JSON file accepted by `dynalitics sql --config`: table-shape settings only, no secrets, no functions. */
|
|
7
|
+
export declare const schemaFileRule: Rule;
|
|
8
|
+
/** Shape and cross-field validation. Throws one DynaliticsConfigError listing every issue. */
|
|
9
|
+
export declare function validateConfig(input: unknown): DynaliticsConfig;
|
|
10
|
+
export interface ResolvedConfig {
|
|
11
|
+
readonly adapter: DriverAdapter;
|
|
12
|
+
readonly dialect: Dialect;
|
|
13
|
+
readonly credentials: {
|
|
14
|
+
readonly single: boolean;
|
|
15
|
+
readonly writer?: Credential;
|
|
16
|
+
readonly reader?: Credential;
|
|
17
|
+
};
|
|
18
|
+
readonly acknowledgeSingleCredential: boolean;
|
|
19
|
+
readonly poolSize: {
|
|
20
|
+
readonly writer: number;
|
|
21
|
+
readonly reader: number;
|
|
22
|
+
};
|
|
23
|
+
readonly connectTimeoutMs: number;
|
|
24
|
+
readonly schema: SchemaConfig;
|
|
25
|
+
readonly page: {
|
|
26
|
+
readonly keepQuery: boolean;
|
|
27
|
+
readonly maxLength: number;
|
|
28
|
+
readonly normalize: ((path: string) => string) | undefined;
|
|
29
|
+
};
|
|
30
|
+
readonly ip: {
|
|
31
|
+
readonly mode: IpMode;
|
|
32
|
+
readonly salt: string | undefined;
|
|
33
|
+
readonly ipv4Prefix: number;
|
|
34
|
+
readonly ipv6Prefix: number;
|
|
35
|
+
};
|
|
36
|
+
readonly consentDefault: ((visit: Readonly<Visit>) => boolean) | undefined;
|
|
37
|
+
readonly firstVisit: {
|
|
38
|
+
readonly enabled: boolean;
|
|
39
|
+
readonly cacheSize: number;
|
|
40
|
+
};
|
|
41
|
+
readonly queue: {
|
|
42
|
+
readonly maxSize: number;
|
|
43
|
+
readonly batchSize: number;
|
|
44
|
+
readonly flushIntervalMs: number;
|
|
45
|
+
readonly backpressure: 'drop' | 'block';
|
|
46
|
+
readonly maxBlockMs: number;
|
|
47
|
+
readonly retry: {
|
|
48
|
+
readonly attempts: number;
|
|
49
|
+
readonly baseDelayMs: number;
|
|
50
|
+
readonly maxDelayMs: number;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
readonly limits: {
|
|
54
|
+
readonly customMaxKeys: number;
|
|
55
|
+
readonly customMaxBytes: number;
|
|
56
|
+
readonly oversize: 'truncate' | 'reject';
|
|
57
|
+
};
|
|
58
|
+
readonly validateOnStart: 'strict' | 'skip';
|
|
59
|
+
readonly onError: ((info: ErrorInfo) => void) | undefined;
|
|
60
|
+
readonly logger: Logger | undefined;
|
|
61
|
+
}
|
|
62
|
+
/** Applies defaults (spec 4.3). Pure: returns a new object, never mutates the config. */
|
|
63
|
+
export declare function resolveConfig(c: DynaliticsConfig): ResolvedConfig;
|
|
64
|
+
//# sourceMappingURL=schema.d.ts.map
|