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
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { DynaliticsConfigError } from '../errors.js';
|
|
2
|
+
/** The ONLY way identifiers (table, schema, column names) enter SQL. Quoting is added on top as defence in depth. */
|
|
3
|
+
const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]{0,62}$/;
|
|
4
|
+
const CUSTOM_KEY = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/;
|
|
5
|
+
const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
6
|
+
export function isIdentifier(name) {
|
|
7
|
+
return typeof name === 'string' && IDENTIFIER.test(name);
|
|
8
|
+
}
|
|
9
|
+
export function isCustomKey(name) {
|
|
10
|
+
return typeof name === 'string' && CUSTOM_KEY.test(name) && !FORBIDDEN_KEYS.has(name);
|
|
11
|
+
}
|
|
12
|
+
export function identifierMessage(name, what) {
|
|
13
|
+
return `Identifier ${JSON.stringify(name)} (${what}) is not supported. Allowed: letters, digits and underscore, starting with a letter or underscore, at most 63 characters. Create a view or rename the object.`;
|
|
14
|
+
}
|
|
15
|
+
export function assertIdentifier(name, what) {
|
|
16
|
+
if (!isIdentifier(name))
|
|
17
|
+
throw new DynaliticsConfigError([identifierMessage(name, what)]);
|
|
18
|
+
return name;
|
|
19
|
+
}
|
|
20
|
+
export function assertCustomKey(key) {
|
|
21
|
+
if (!isCustomKey(key)) {
|
|
22
|
+
throw new DynaliticsConfigError([
|
|
23
|
+
`${JSON.stringify(key)} is not a valid custom field key. Allowed: ^[A-Za-z_][A-Za-z0-9_]{0,63}$ (not __proto__, constructor or prototype).`,
|
|
24
|
+
]);
|
|
25
|
+
}
|
|
26
|
+
return key;
|
|
27
|
+
}
|
|
28
|
+
/** Parses 'name', 'schema.name', 'db.schema.name' or an object into a validated TableRef. */
|
|
29
|
+
export function parseTableRef(input) {
|
|
30
|
+
const parts = typeof input === 'string' ? splitTableString(input) : input;
|
|
31
|
+
const checked = (value, what) => assertIdentifier(value, what);
|
|
32
|
+
return {
|
|
33
|
+
...(parts.database === undefined
|
|
34
|
+
? {}
|
|
35
|
+
: { database: checked(parts.database, 'table database') }),
|
|
36
|
+
...(parts.schema === undefined ? {} : { schema: checked(parts.schema, 'table schema') }),
|
|
37
|
+
name: checked(parts.name, 'table name'),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function splitTableString(input) {
|
|
41
|
+
const segments = input.split('.');
|
|
42
|
+
if (segments.length > 3) {
|
|
43
|
+
throw new DynaliticsConfigError(['Table name has at most 3 parts: database.schema.name.']);
|
|
44
|
+
}
|
|
45
|
+
const [a = '', b, c] = segments;
|
|
46
|
+
if (segments.length === 1)
|
|
47
|
+
return { name: a };
|
|
48
|
+
if (segments.length === 2)
|
|
49
|
+
return { schema: a, name: b ?? '' };
|
|
50
|
+
return { database: a, schema: b ?? '', name: c ?? '' };
|
|
51
|
+
}
|
|
52
|
+
/** Dialect-specific restrictions on table references (spec 3.7). */
|
|
53
|
+
export function assertTableForDialect(ref, dialect) {
|
|
54
|
+
if (ref.database !== undefined && dialect !== 'mssql') {
|
|
55
|
+
throw new DynaliticsConfigError([
|
|
56
|
+
dialect === 'postgres'
|
|
57
|
+
? 'PostgreSQL tables are addressed as name or schema.name; cross-database names do not exist.'
|
|
58
|
+
: 'MySQL tables are addressed as name or schema.name, where "schema" means the database.',
|
|
59
|
+
]);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=identifiers.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ColumnInfo } from '../dialect/types.js';
|
|
2
|
+
import type { ColumnDef, ColumnModel, Dialect } from './types.js';
|
|
3
|
+
export interface LengthAdoption {
|
|
4
|
+
readonly column: string;
|
|
5
|
+
readonly configured: number;
|
|
6
|
+
readonly real: number;
|
|
7
|
+
}
|
|
8
|
+
/** True if the real column is an integer type, so a boolean has to be bound as 1/0 (and read as a number). */
|
|
9
|
+
export declare const isIntegerColumn: (c: ColumnDef | undefined) => boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Returns a copy of the model that knows the real database types (from the catalog). In `existing` mode string
|
|
12
|
+
* columns also adopt the real length when it is shorter than configured (spec 7.2); unlimited types keep the configured
|
|
13
|
+
* maximum. Recommended tables keep their lengths: a shorter real column is the `too_short` problem there.
|
|
14
|
+
*/
|
|
15
|
+
export declare function adoptPhysical(model: ColumnModel, catalog: readonly ColumnInfo[], dialect: Dialect): {
|
|
16
|
+
readonly model: ColumnModel;
|
|
17
|
+
readonly adopted: readonly LengthAdoption[];
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=physical.d.ts.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const INTEGER_TYPES = new Set([
|
|
2
|
+
'int2',
|
|
3
|
+
'int4',
|
|
4
|
+
'int8',
|
|
5
|
+
'smallint',
|
|
6
|
+
'integer',
|
|
7
|
+
'bigint',
|
|
8
|
+
'tinyint',
|
|
9
|
+
'mediumint',
|
|
10
|
+
'int',
|
|
11
|
+
]);
|
|
12
|
+
/** True if the real column is an integer type, so a boolean has to be bound as 1/0 (and read as a number). */
|
|
13
|
+
export const isIntegerColumn = (c) => c?.physicalType !== undefined && INTEGER_TYPES.has(c.physicalType);
|
|
14
|
+
/**
|
|
15
|
+
* Returns a copy of the model that knows the real database types (from the catalog). In `existing` mode string
|
|
16
|
+
* columns also adopt the real length when it is shorter than configured (spec 7.2); unlimited types keep the configured
|
|
17
|
+
* maximum. Recommended tables keep their lengths: a shorter real column is the `too_short` problem there.
|
|
18
|
+
*/
|
|
19
|
+
export function adoptPhysical(model, catalog, dialect) {
|
|
20
|
+
const key = (n) => (dialect === 'postgres' ? n : n.toLowerCase());
|
|
21
|
+
const byName = new Map(catalog.map((c) => [key(c.name), c]));
|
|
22
|
+
const adopted = [];
|
|
23
|
+
const refine = (c) => {
|
|
24
|
+
const info = byName.get(key(c.name));
|
|
25
|
+
if (info === undefined)
|
|
26
|
+
return c;
|
|
27
|
+
const withType = { ...c, physicalType: info.dataType };
|
|
28
|
+
if (model.mode === 'existing' &&
|
|
29
|
+
c.type.kind === 'string' &&
|
|
30
|
+
info.maxLength !== null &&
|
|
31
|
+
info.maxLength > 0 &&
|
|
32
|
+
info.maxLength < c.type.maxLength) {
|
|
33
|
+
adopted.push({ column: c.name, configured: c.type.maxLength, real: info.maxLength });
|
|
34
|
+
return { ...withType, type: { kind: 'string', maxLength: info.maxLength } };
|
|
35
|
+
}
|
|
36
|
+
return withType;
|
|
37
|
+
};
|
|
38
|
+
const columns = model.columns.map(refine);
|
|
39
|
+
const byColumnName = new Map(columns.map((c) => [c.name, c]));
|
|
40
|
+
const byRole = (role) => columns.find((c) => c.role === role);
|
|
41
|
+
const customFields = new Map();
|
|
42
|
+
for (const [name, field] of model.customFields) {
|
|
43
|
+
const column = field.column === undefined ? undefined : byColumnName.get(field.column.name);
|
|
44
|
+
customFields.set(name, {
|
|
45
|
+
...field,
|
|
46
|
+
...(column === undefined
|
|
47
|
+
? {}
|
|
48
|
+
: {
|
|
49
|
+
column,
|
|
50
|
+
maxLength: column.type.kind === 'string' ? column.type.maxLength : field.maxLength,
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
const userId = byRole('userId');
|
|
55
|
+
const ip = byRole('ip');
|
|
56
|
+
return {
|
|
57
|
+
model: {
|
|
58
|
+
...model,
|
|
59
|
+
columns,
|
|
60
|
+
insertColumns: columns.filter((c) => !c.dbGenerated),
|
|
61
|
+
customFields,
|
|
62
|
+
identity: {
|
|
63
|
+
...(userId === undefined ? {} : { userId }),
|
|
64
|
+
...(ip === undefined ? {} : { ip }),
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
adopted,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=physical.js.map
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { TableRef } from './identifiers.js';
|
|
2
|
+
export type Dialect = 'postgres' | 'mysql' | 'mssql';
|
|
3
|
+
export type IpMode = 'raw' | 'hash' | 'truncate';
|
|
4
|
+
export type SqlValue = string | number | boolean | null;
|
|
5
|
+
export type CustomValue = string | number | boolean | null;
|
|
6
|
+
export type ScalarType = 'string' | 'integer' | 'number' | 'boolean';
|
|
7
|
+
export type Role = 'timestamp' | 'consent' | 'pageVisited' | 'ip' | 'userId' | 'username' | 'email' | 'custom' | 'isFirstVisit' | 'id';
|
|
8
|
+
export type LogicalType = {
|
|
9
|
+
readonly kind: 'id';
|
|
10
|
+
} | {
|
|
11
|
+
readonly kind: 'timestamp';
|
|
12
|
+
} | {
|
|
13
|
+
readonly kind: 'boolean';
|
|
14
|
+
} | {
|
|
15
|
+
readonly kind: 'string';
|
|
16
|
+
readonly maxLength: number;
|
|
17
|
+
} | {
|
|
18
|
+
readonly kind: 'integer';
|
|
19
|
+
} | {
|
|
20
|
+
readonly kind: 'number';
|
|
21
|
+
} | {
|
|
22
|
+
readonly kind: 'json';
|
|
23
|
+
};
|
|
24
|
+
export interface ColumnDef {
|
|
25
|
+
readonly role: Role | 'promoted' | 'extra';
|
|
26
|
+
/** Role name, custom field name, or extra column name. */
|
|
27
|
+
readonly key: string;
|
|
28
|
+
/** Physical column name, already validated against the identifier pattern. */
|
|
29
|
+
readonly name: string;
|
|
30
|
+
readonly type: LogicalType;
|
|
31
|
+
readonly nullable: boolean;
|
|
32
|
+
/** Drives consent stripping, SQL comments and grant comments. */
|
|
33
|
+
readonly personal: boolean;
|
|
34
|
+
/** True only for `id`: never inserted. */
|
|
35
|
+
readonly dbGenerated: boolean;
|
|
36
|
+
/** The real database type, lower-case (`timestamptz`, `int2`, ...). Set after the catalog was read (startup), never from config. */
|
|
37
|
+
readonly physicalType?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface CustomFieldDef {
|
|
40
|
+
type: ScalarType;
|
|
41
|
+
maxLength?: number;
|
|
42
|
+
personal?: boolean;
|
|
43
|
+
column?: true | string;
|
|
44
|
+
breakdown?: boolean;
|
|
45
|
+
/** Keep this (non-personal) field on rows where consent was denied. Default false: denied rows store nothing else. */
|
|
46
|
+
keepWhenDenied?: boolean;
|
|
47
|
+
}
|
|
48
|
+
export interface ResolvedCustomField {
|
|
49
|
+
readonly key: string;
|
|
50
|
+
readonly type: ScalarType;
|
|
51
|
+
readonly maxLength: number;
|
|
52
|
+
readonly personal: boolean;
|
|
53
|
+
readonly breakdown: boolean;
|
|
54
|
+
readonly keepWhenDenied: boolean;
|
|
55
|
+
/** Present when the field is promoted to its own column. */
|
|
56
|
+
readonly column?: ColumnDef;
|
|
57
|
+
}
|
|
58
|
+
/** The part of the config that decides table shape. No secrets, no functions: safe to read from JSON. */
|
|
59
|
+
export interface SchemaConfig {
|
|
60
|
+
table: string | {
|
|
61
|
+
database?: string;
|
|
62
|
+
schema?: string;
|
|
63
|
+
name: string;
|
|
64
|
+
};
|
|
65
|
+
tableMode?: 'recommended' | 'existing';
|
|
66
|
+
columns?: Partial<Record<Role, string | false>>;
|
|
67
|
+
customFields?: Record<string, CustomFieldDef>;
|
|
68
|
+
page?: {
|
|
69
|
+
maxLength?: number;
|
|
70
|
+
};
|
|
71
|
+
firstVisit?: {
|
|
72
|
+
enabled?: boolean;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export interface ColumnModel {
|
|
76
|
+
readonly table: TableRef;
|
|
77
|
+
readonly mode: 'recommended' | 'existing';
|
|
78
|
+
/** Mapped columns, in DDL order. */
|
|
79
|
+
readonly columns: readonly ColumnDef[];
|
|
80
|
+
/** `columns` minus database-generated ones, in insert order. */
|
|
81
|
+
readonly insertColumns: readonly ColumnDef[];
|
|
82
|
+
/** Optional roles that are not stored; drives the startup info log. */
|
|
83
|
+
readonly unmappedRoles: readonly Role[];
|
|
84
|
+
readonly customFields: ReadonlyMap<string, ResolvedCustomField>;
|
|
85
|
+
readonly identity: {
|
|
86
|
+
readonly userId?: ColumnDef;
|
|
87
|
+
readonly ip?: ColumnDef;
|
|
88
|
+
};
|
|
89
|
+
readonly firstVisitEnabled: boolean;
|
|
90
|
+
}
|
|
91
|
+
export declare const OPTIONAL_STORED_ROLES: readonly Role[];
|
|
92
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
export type Interval = 'hour' | 'day' | 'week' | 'month';
|
|
2
|
+
export type ErrorCode = 'forbidden' | 'not_found' | 'method_not_allowed' | 'invalid_parameter' | 'range_too_large' | 'invalid_query' | 'timeout' | 'unavailable' | 'internal';
|
|
3
|
+
export type UnavailableReason = 'no_identity_column' | 'identity_not_reliable' | 'disabled_by_host' | 'column_not_mapped';
|
|
4
|
+
export interface Unavailable {
|
|
5
|
+
reason: UnavailableReason;
|
|
6
|
+
message: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ResponseMeta {
|
|
9
|
+
generatedAt: string;
|
|
10
|
+
from?: string;
|
|
11
|
+
to?: string;
|
|
12
|
+
interval?: Interval;
|
|
13
|
+
piiMasked: boolean;
|
|
14
|
+
cached: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** Why an explorer query was refused (spec 18.4). The message for each is a fixed sentence. */
|
|
17
|
+
export type QueryErrorReason = 'too_long' | 'too_many_clauses' | 'unbalanced_quote' | 'empty_value' | 'value_too_long' | 'unknown_field' | 'bad_value';
|
|
18
|
+
export interface ApiErrorBody {
|
|
19
|
+
code: ErrorCode;
|
|
20
|
+
message: string;
|
|
21
|
+
param?: string;
|
|
22
|
+
/** Only for `invalid_query`. */
|
|
23
|
+
reason?: QueryErrorReason;
|
|
24
|
+
/** Only for `invalid_query`: 0-based character offset in `q`. */
|
|
25
|
+
position?: number;
|
|
26
|
+
}
|
|
27
|
+
export type ApiResponse<T> = {
|
|
28
|
+
success: true;
|
|
29
|
+
data: T;
|
|
30
|
+
unavailable: null;
|
|
31
|
+
error: null;
|
|
32
|
+
meta: ResponseMeta;
|
|
33
|
+
} | {
|
|
34
|
+
success: true;
|
|
35
|
+
data: null;
|
|
36
|
+
unavailable: Unavailable;
|
|
37
|
+
error: null;
|
|
38
|
+
meta: ResponseMeta;
|
|
39
|
+
} | {
|
|
40
|
+
success: false;
|
|
41
|
+
data: null;
|
|
42
|
+
unavailable: null;
|
|
43
|
+
error: ApiErrorBody;
|
|
44
|
+
};
|
|
45
|
+
export type WidgetAvailability = {
|
|
46
|
+
available: true;
|
|
47
|
+
} | {
|
|
48
|
+
available: false;
|
|
49
|
+
reason: UnavailableReason;
|
|
50
|
+
message: string;
|
|
51
|
+
};
|
|
52
|
+
export type NewVsReturningSource = 'computed' | 'column';
|
|
53
|
+
export type NewVsReturningBasis = 'user_id' | 'ip' | 'user_id_then_ip';
|
|
54
|
+
export type NewVsReturningAvailability = {
|
|
55
|
+
available: true;
|
|
56
|
+
source: NewVsReturningSource;
|
|
57
|
+
basis: NewVsReturningBasis;
|
|
58
|
+
approximate: boolean;
|
|
59
|
+
} | {
|
|
60
|
+
available: false;
|
|
61
|
+
reason: UnavailableReason;
|
|
62
|
+
message: string;
|
|
63
|
+
};
|
|
64
|
+
export interface Breakdown {
|
|
65
|
+
key: string;
|
|
66
|
+
label: string;
|
|
67
|
+
}
|
|
68
|
+
export interface MetaData {
|
|
69
|
+
version: 2;
|
|
70
|
+
now: string;
|
|
71
|
+
intervals: readonly Interval[];
|
|
72
|
+
limits: {
|
|
73
|
+
maxRangeDays: number;
|
|
74
|
+
maxBuckets: number;
|
|
75
|
+
topPagesMax: number;
|
|
76
|
+
recentMax: number;
|
|
77
|
+
newVsReturningMaxRangeDays: number;
|
|
78
|
+
};
|
|
79
|
+
defaults: {
|
|
80
|
+
rangeDays: number;
|
|
81
|
+
interval: Interval;
|
|
82
|
+
topPagesLimit: number;
|
|
83
|
+
recentLimit: number;
|
|
84
|
+
};
|
|
85
|
+
breakdowns: readonly Breakdown[];
|
|
86
|
+
widgets: {
|
|
87
|
+
visitsOverTime: WidgetAvailability;
|
|
88
|
+
topPages: WidgetAvailability;
|
|
89
|
+
consentRate: WidgetAvailability;
|
|
90
|
+
newVsReturning: NewVsReturningAvailability;
|
|
91
|
+
recentVisits: WidgetAvailability;
|
|
92
|
+
/** v0.6. Absent from older servers: treat a missing key as unavailable. */
|
|
93
|
+
explorer?: WidgetAvailability;
|
|
94
|
+
};
|
|
95
|
+
/** v0.6 (spec 18.1). Absent when the explorer is disabled or the server predates it. */
|
|
96
|
+
explorer?: ExplorerMeta;
|
|
97
|
+
fields: {
|
|
98
|
+
id: boolean;
|
|
99
|
+
ipAddress: boolean;
|
|
100
|
+
userId: boolean;
|
|
101
|
+
username: boolean;
|
|
102
|
+
email: boolean;
|
|
103
|
+
isFirstVisit: boolean;
|
|
104
|
+
};
|
|
105
|
+
features: {
|
|
106
|
+
recentVisitsCursor: boolean;
|
|
107
|
+
piiRevealable: boolean;
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
export interface VisitPoint {
|
|
111
|
+
bucket: string;
|
|
112
|
+
visits: number;
|
|
113
|
+
}
|
|
114
|
+
export interface BreakdownSeries {
|
|
115
|
+
value: string | null;
|
|
116
|
+
isOther: boolean;
|
|
117
|
+
points: readonly VisitPoint[];
|
|
118
|
+
}
|
|
119
|
+
export interface VisitsOverTimeData {
|
|
120
|
+
interval: Interval;
|
|
121
|
+
points: readonly VisitPoint[];
|
|
122
|
+
total: number;
|
|
123
|
+
breakdown?: {
|
|
124
|
+
key: string;
|
|
125
|
+
series: readonly BreakdownSeries[];
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
export interface TopPage {
|
|
129
|
+
page: string;
|
|
130
|
+
visits: number;
|
|
131
|
+
share: number;
|
|
132
|
+
}
|
|
133
|
+
export interface TopPagesData {
|
|
134
|
+
totalVisits: number;
|
|
135
|
+
pages: readonly TopPage[];
|
|
136
|
+
otherVisits: number;
|
|
137
|
+
}
|
|
138
|
+
export interface NewVsReturningCounts {
|
|
139
|
+
new: number;
|
|
140
|
+
returning: number;
|
|
141
|
+
unknown: number;
|
|
142
|
+
}
|
|
143
|
+
export interface NewVsReturningData {
|
|
144
|
+
interval: Interval;
|
|
145
|
+
source: NewVsReturningSource;
|
|
146
|
+
basis: NewVsReturningBasis;
|
|
147
|
+
approximate: boolean;
|
|
148
|
+
points: readonly (NewVsReturningCounts & {
|
|
149
|
+
bucket: string;
|
|
150
|
+
})[];
|
|
151
|
+
totals: NewVsReturningCounts;
|
|
152
|
+
}
|
|
153
|
+
export interface ConsentCounts {
|
|
154
|
+
granted: number;
|
|
155
|
+
denied: number;
|
|
156
|
+
rate: number | null;
|
|
157
|
+
}
|
|
158
|
+
export interface ConsentRateData {
|
|
159
|
+
interval: Interval;
|
|
160
|
+
points: readonly (ConsentCounts & {
|
|
161
|
+
bucket: string;
|
|
162
|
+
})[];
|
|
163
|
+
totals: ConsentCounts;
|
|
164
|
+
}
|
|
165
|
+
export interface RecentVisit {
|
|
166
|
+
id: string | null;
|
|
167
|
+
timestamp: string;
|
|
168
|
+
pageVisited: string;
|
|
169
|
+
consent: boolean;
|
|
170
|
+
isFirstVisit: boolean | null;
|
|
171
|
+
ipAddress: string | null;
|
|
172
|
+
userId: string | null;
|
|
173
|
+
username: string | null;
|
|
174
|
+
email: string | null;
|
|
175
|
+
custom: Readonly<Record<string, string | number | boolean | null>> | null;
|
|
176
|
+
}
|
|
177
|
+
export interface RecentVisitsData {
|
|
178
|
+
visits: readonly RecentVisit[];
|
|
179
|
+
nextCursor: string | null;
|
|
180
|
+
}
|
|
181
|
+
export type SearchFieldType = 'text' | 'boolean' | 'number';
|
|
182
|
+
export interface SearchFieldInfo {
|
|
183
|
+
key: string;
|
|
184
|
+
type: SearchFieldType;
|
|
185
|
+
/** Bare terms (no `key:`) match this field. */
|
|
186
|
+
search: boolean;
|
|
187
|
+
/** Shown in the field sidebar and usable as `split`. */
|
|
188
|
+
facet: boolean;
|
|
189
|
+
}
|
|
190
|
+
export interface ExplorerMeta {
|
|
191
|
+
fields: readonly SearchFieldInfo[];
|
|
192
|
+
limits: {
|
|
193
|
+
maxQueryChars: number;
|
|
194
|
+
maxClauses: number;
|
|
195
|
+
maxValueChars: number;
|
|
196
|
+
maxRangeDays: number;
|
|
197
|
+
facetTop: number;
|
|
198
|
+
tailMinIntervalMs: number;
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
export interface SearchData {
|
|
202
|
+
visits: readonly RecentVisit[];
|
|
203
|
+
nextCursor: string | null;
|
|
204
|
+
matched: number;
|
|
205
|
+
}
|
|
206
|
+
/** Same shape as visits-over-time; `breakdown` holds the split series when `split` was given. */
|
|
207
|
+
export type SearchHistogramData = VisitsOverTimeData;
|
|
208
|
+
export interface FacetValue {
|
|
209
|
+
value: string;
|
|
210
|
+
count: number;
|
|
211
|
+
}
|
|
212
|
+
export interface FieldFacet {
|
|
213
|
+
key: string;
|
|
214
|
+
values: readonly FacetValue[];
|
|
215
|
+
nullCount: number;
|
|
216
|
+
otherCount: number;
|
|
217
|
+
}
|
|
218
|
+
export interface SearchFieldsData {
|
|
219
|
+
matched: number;
|
|
220
|
+
fields: readonly FieldFacet[];
|
|
221
|
+
}
|
|
222
|
+
//# sourceMappingURL=protocol.d.ts.map
|
package/dist/protocol.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Wire types of the dynalitics read API (docs/SPEC.md section 10, v0.2). Types only.
|
|
2
|
+
// This file is a vendored copy of packages/dynalitics/src/protocol.ts; the dashboard must not
|
|
3
|
+
// depend on the server package. `npm run check:protocol -w dynalitics-dashboard` diffs the two.
|
|
4
|
+
export {};
|
|
5
|
+
//# sourceMappingURL=protocol.js.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { SqlDialect } from '../dialect/types.js';
|
|
2
|
+
import { type Driver } from '../driver/types.js';
|
|
3
|
+
import type { ColumnModel, SqlValue } from '../model/types.js';
|
|
4
|
+
import type { Identity } from '../track/build-row.js';
|
|
5
|
+
import type { FirstVisit } from '../track/first-visit.js';
|
|
6
|
+
import type { ResolvedConfig } from '../config/schema.js';
|
|
7
|
+
import type { ErrorInfo, FlushResult, TrackResult } from '../types.js';
|
|
8
|
+
export interface QueuedRow {
|
|
9
|
+
readonly values: readonly SqlValue[];
|
|
10
|
+
readonly identity: Identity | null;
|
|
11
|
+
}
|
|
12
|
+
export interface PipelineDeps {
|
|
13
|
+
readonly driver: Driver;
|
|
14
|
+
readonly dialect: SqlDialect;
|
|
15
|
+
readonly model: ColumnModel;
|
|
16
|
+
readonly queue: ResolvedConfig['queue'];
|
|
17
|
+
readonly firstVisit: FirstVisit | undefined;
|
|
18
|
+
readonly report: (info: ErrorInfo) => void;
|
|
19
|
+
readonly sleep: (ms: number) => Promise<void>;
|
|
20
|
+
readonly random: () => number;
|
|
21
|
+
readonly now: () => number;
|
|
22
|
+
readonly requestTimeoutMs: number;
|
|
23
|
+
}
|
|
24
|
+
export interface PipelineStats {
|
|
25
|
+
queued: number;
|
|
26
|
+
written: number;
|
|
27
|
+
failed: number;
|
|
28
|
+
droppedFull: number;
|
|
29
|
+
queueLength: number;
|
|
30
|
+
}
|
|
31
|
+
export interface Pipeline {
|
|
32
|
+
enqueue(row: QueuedRow): Promise<TrackResult>;
|
|
33
|
+
flush(opts?: {
|
|
34
|
+
timeoutMs?: number;
|
|
35
|
+
}): Promise<FlushResult>;
|
|
36
|
+
close(opts?: {
|
|
37
|
+
timeoutMs?: number;
|
|
38
|
+
}): Promise<FlushResult>;
|
|
39
|
+
stats(): PipelineStats;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Bounded in-memory queue with batched multi-row INSERTs (spec 9). One batch is in flight at a time, so write-time
|
|
43
|
+
* first-visit resolution is race-free within one process.
|
|
44
|
+
*/
|
|
45
|
+
export declare function createPipeline(deps: PipelineDeps): Pipeline;
|
|
46
|
+
//# sourceMappingURL=pipeline.d.ts.map
|