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.
Files changed (139) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +261 -2
  3. package/dist/adapter-version.d.ts +3 -0
  4. package/dist/adapter-version.js +3 -0
  5. package/dist/adapter.d.ts +12 -0
  6. package/dist/adapter.js +7 -0
  7. package/dist/cli/io.d.ts +9 -0
  8. package/dist/cli/io.js +10 -0
  9. package/dist/cli/run.d.ts +4 -0
  10. package/dist/cli/run.js +39 -0
  11. package/dist/cli/sql.d.ts +5 -0
  12. package/dist/cli/sql.js +129 -0
  13. package/dist/cli.d.ts +3 -0
  14. package/dist/cli.js +5 -0
  15. package/dist/config/schema.d.ts +64 -0
  16. package/dist/config/schema.js +248 -0
  17. package/dist/config/validate.d.ts +30 -0
  18. package/dist/config/validate.js +68 -0
  19. package/dist/create.d.ts +16 -0
  20. package/dist/create.js +308 -0
  21. package/dist/dialect/common.d.ts +54 -0
  22. package/dist/dialect/common.js +197 -0
  23. package/dist/dialect/index.d.ts +5 -0
  24. package/dist/dialect/index.js +8 -0
  25. package/dist/dialect/mssql.d.ts +3 -0
  26. package/dist/dialect/mssql.js +104 -0
  27. package/dist/dialect/mysql.d.ts +3 -0
  28. package/dist/dialect/mysql.js +140 -0
  29. package/dist/dialect/postgres.d.ts +5 -0
  30. package/dist/dialect/postgres.js +90 -0
  31. package/dist/dialect/types.d.ts +79 -0
  32. package/dist/dialect/types.js +2 -0
  33. package/dist/driver/types.d.ts +74 -0
  34. package/dist/driver/types.js +66 -0
  35. package/dist/errors.d.ts +39 -0
  36. package/dist/errors.js +39 -0
  37. package/dist/index.d.ts +7 -0
  38. package/dist/index.js +3 -0
  39. package/dist/internals.d.ts +20 -0
  40. package/dist/internals.js +8 -0
  41. package/dist/logger.d.ts +10 -0
  42. package/dist/logger.js +24 -0
  43. package/dist/model/column-model.d.ts +7 -0
  44. package/dist/model/column-model.js +240 -0
  45. package/dist/model/identifiers.d.ts +19 -0
  46. package/dist/model/identifiers.js +62 -0
  47. package/dist/model/physical.d.ts +19 -0
  48. package/dist/model/physical.js +70 -0
  49. package/dist/model/types.d.ts +92 -0
  50. package/dist/model/types.js +9 -0
  51. package/dist/protocol.d.ts +222 -0
  52. package/dist/protocol.js +5 -0
  53. package/dist/queue/pipeline.d.ts +46 -0
  54. package/dist/queue/pipeline.js +282 -0
  55. package/dist/read/adapters.d.ts +7 -0
  56. package/dist/read/adapters.js +75 -0
  57. package/dist/read/buckets.d.ts +7 -0
  58. package/dist/read/buckets.js +39 -0
  59. package/dist/read/capabilities.d.ts +39 -0
  60. package/dist/read/capabilities.js +65 -0
  61. package/dist/read/handler.d.ts +48 -0
  62. package/dist/read/handler.js +63 -0
  63. package/dist/read/http.d.ts +18 -0
  64. package/dist/read/http.js +65 -0
  65. package/dist/read/index.d.ts +5 -0
  66. package/dist/read/index.js +3 -0
  67. package/dist/read/lock/authorize.d.ts +22 -0
  68. package/dist/read/lock/authorize.js +32 -0
  69. package/dist/read/lock/base-path.d.ts +11 -0
  70. package/dist/read/lock/base-path.js +12 -0
  71. package/dist/read/lock/gate.d.ts +30 -0
  72. package/dist/read/lock/gate.js +24 -0
  73. package/dist/read/lock/index.d.ts +28 -0
  74. package/dist/read/lock/index.js +46 -0
  75. package/dist/read/lock/options.d.ts +13 -0
  76. package/dist/read/lock/options.js +18 -0
  77. package/dist/read/lock/serve-html.d.ts +6 -0
  78. package/dist/read/lock/serve-html.js +38 -0
  79. package/dist/read/mask.d.ts +12 -0
  80. package/dist/read/mask.js +55 -0
  81. package/dist/read/params.d.ts +56 -0
  82. package/dist/read/params.js +150 -0
  83. package/dist/read/physical.d.ts +11 -0
  84. package/dist/read/physical.js +23 -0
  85. package/dist/read/queries.d.ts +75 -0
  86. package/dist/read/queries.js +316 -0
  87. package/dist/read/report.d.ts +21 -0
  88. package/dist/read/report.js +32 -0
  89. package/dist/read/routes.d.ts +38 -0
  90. package/dist/read/routes.js +261 -0
  91. package/dist/read/scope.d.ts +11 -0
  92. package/dist/read/scope.js +47 -0
  93. package/dist/read/search/fields.d.ts +24 -0
  94. package/dist/read/search/fields.js +67 -0
  95. package/dist/read/search/parse.d.ts +48 -0
  96. package/dist/read/search/parse.js +124 -0
  97. package/dist/read/semaphore.d.ts +6 -0
  98. package/dist/read/semaphore.js +34 -0
  99. package/dist/read/service.d.ts +42 -0
  100. package/dist/read/service.js +266 -0
  101. package/dist/read/types.d.ts +62 -0
  102. package/dist/read/types.js +2 -0
  103. package/dist/redact.d.ts +8 -0
  104. package/dist/redact.js +60 -0
  105. package/dist/report.d.ts +12 -0
  106. package/dist/report.js +39 -0
  107. package/dist/schema/generate.d.ts +46 -0
  108. package/dist/schema/generate.js +71 -0
  109. package/dist/schema/index.d.ts +5 -0
  110. package/dist/schema/index.js +3 -0
  111. package/dist/startup/compare.d.ts +22 -0
  112. package/dist/startup/compare.js +165 -0
  113. package/dist/startup/fix-sql.d.ts +15 -0
  114. package/dist/startup/fix-sql.js +94 -0
  115. package/dist/startup/privileges.d.ts +21 -0
  116. package/dist/startup/privileges.js +63 -0
  117. package/dist/startup/session-timezone.d.ts +10 -0
  118. package/dist/startup/session-timezone.js +19 -0
  119. package/dist/startup/type-families.d.ts +10 -0
  120. package/dist/startup/type-families.js +55 -0
  121. package/dist/startup/unmapped-notice.d.ts +5 -0
  122. package/dist/startup/unmapped-notice.js +28 -0
  123. package/dist/track/build-row.d.ts +50 -0
  124. package/dist/track/build-row.js +208 -0
  125. package/dist/track/custom.d.ts +31 -0
  126. package/dist/track/custom.js +83 -0
  127. package/dist/track/denied-view.d.ts +14 -0
  128. package/dist/track/denied-view.js +21 -0
  129. package/dist/track/first-visit.d.ts +19 -0
  130. package/dist/track/first-visit.js +70 -0
  131. package/dist/track/ip.d.ts +17 -0
  132. package/dist/track/ip.js +87 -0
  133. package/dist/track/limits.d.ts +7 -0
  134. package/dist/track/limits.js +26 -0
  135. package/dist/types.d.ts +136 -0
  136. package/dist/types.js +2 -0
  137. package/dist/version.d.ts +3 -0
  138. package/dist/version.js +3 -0
  139. package/package.json +71 -4
@@ -0,0 +1,79 @@
1
+ import type { Interval } from '../protocol.js';
2
+ import type { TableRef } from '../model/identifiers.js';
3
+ import type { ColumnDef, ColumnModel, Dialect, SqlValue } from '../model/types.js';
4
+ export interface CompiledQuery {
5
+ readonly sql: string;
6
+ readonly params: readonly SqlValue[];
7
+ }
8
+ export type Row = Readonly<Record<string, unknown>>;
9
+ /** Normalised catalog row, identical across dialects. */
10
+ export interface ColumnInfo {
11
+ readonly name: string;
12
+ readonly dataType: string;
13
+ readonly maxLength: number | null;
14
+ readonly nullable: boolean;
15
+ readonly hasDefault: boolean;
16
+ readonly generated: boolean;
17
+ readonly precision: number | null;
18
+ readonly tableType: string;
19
+ /** The schema (database for MySQL) the catalog found the table in; used to qualify names from then on. */
20
+ readonly schema?: string;
21
+ }
22
+ export interface GrantOptions {
23
+ readonly table: TableRef;
24
+ readonly writerRole: string;
25
+ readonly readerRole: string;
26
+ /** Column-level SELECT for the writer, only needed for write-time first-visit detection. */
27
+ readonly writerSelectColumns?: readonly string[];
28
+ }
29
+ /** SQL fragments used by the read API. Inputs that are not column names are validated before use. */
30
+ export interface ReadSyntax {
31
+ /** UTC start of the bucket as ISO text, e.g. 2026-10-25T00:00:00.000Z. Weeks start on Monday, independent of session settings. */
32
+ bucket(col: string, interval: Interval): string;
33
+ /** An exact instant as ISO text (ms precision, UTC). */
34
+ tsText(col: string): string;
35
+ isTrue(col: string): string;
36
+ /** True only for a non-NULL false value. */
37
+ isFalse(col: string): string;
38
+ /** Text value of a JSON key. `key` must match the custom key pattern; it comes from host config, never from a request. */
39
+ jsonKey(col: string, key: string): string;
40
+ castText(col: string, kind: 'string' | 'integer' | 'number' | 'boolean'): string;
41
+ /** A JSON key as a number, NULL when the stored value is not numeric (explorer number filters, spec 18.2). */
42
+ jsonNumber(col: string, key: string): string;
43
+ /** Row count that cannot overflow: COUNT_BIG(*) on SQL Server (COUNT is a 32-bit int there). */
44
+ readonly count: string;
45
+ /** `TOP n ` for SQL Server, '' otherwise. n must be a positive integer. */
46
+ top(n: number): string;
47
+ /** ` LIMIT n` for PostgreSQL and MySQL, '' for SQL Server. */
48
+ limit(n: number): string;
49
+ }
50
+ /** Pure SQL text generation. No driver imports, no I/O. */
51
+ export interface SqlDialect {
52
+ readonly name: Dialect;
53
+ readonly limits: {
54
+ readonly maxParams: number;
55
+ readonly maxRowsPerInsert: number;
56
+ readonly maxIdentifier: number;
57
+ };
58
+ quoteIdent(name: string): string;
59
+ qualifiedTable(table: TableRef): string;
60
+ placeholder(index: number): string;
61
+ columnType(c: ColumnDef): string;
62
+ timestampParam(d: Date): string;
63
+ buildInsert(model: ColumnModel, rows: readonly (readonly SqlValue[])[]): CompiledQuery;
64
+ firstVisitLookup(model: ColumnModel, kind: 'u' | 'i', values: readonly string[]): CompiledQuery;
65
+ introspectSql(table: TableRef): CompiledQuery;
66
+ mapColumnRow(row: Row): ColumnInfo;
67
+ privilegeSql(table: TableRef): readonly CompiledQuery[];
68
+ parsePrivileges(results: readonly (readonly Row[])[], table: TableRef): ReadonlySet<string>;
69
+ createTableSql(model: ColumnModel): string;
70
+ createIndexSql(model: ColumnModel): readonly string[];
71
+ grantSql(opts: GrantOptions): readonly string[];
72
+ addColumnSql(table: TableRef, c: ColumnDef): string;
73
+ dropNotNullSql(table: TableRef, c: ColumnDef): string;
74
+ alterTypeSql(table: TableRef, c: ColumnDef): string;
75
+ readonly read: ReadSyntax;
76
+ /** Renders a role/user for GRANT: placeholders pass through, real names are validated and quoted. */
77
+ renderRole(role: string): string;
78
+ }
79
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,74 @@
1
+ import { DynaliticsError, type SafeCause } from '../errors.js';
2
+ import type { CompiledQuery, Row } from '../dialect/types.js';
3
+ import type { Redactor } from '../redact.js';
4
+ import type { Dialect } from '../model/types.js';
5
+ import type { Credential } from '../types.js';
6
+ import type { ADAPTER_API_VERSION } from '../adapter-version.js';
7
+ export interface DriverOptions {
8
+ readonly credential: Credential;
9
+ /** Pool size for pools dynalitics creates. */
10
+ readonly max: number;
11
+ readonly connectTimeoutMs: number;
12
+ readonly redact: Redactor;
13
+ /** Default request timeout for pools dynalitics creates. */
14
+ readonly requestTimeoutMs?: number;
15
+ }
16
+ /** Runtime part of an adapter: a thin wrapper around a pool. All SQL text comes from the dialect. */
17
+ export interface Driver {
18
+ query(q: CompiledQuery, opts: {
19
+ timeoutMs: number;
20
+ }): Promise<readonly Row[]>;
21
+ isTransient(err: unknown): boolean;
22
+ /** No-op when the pool belongs to the host. */
23
+ close(): Promise<void>;
24
+ }
25
+ /**
26
+ * What an adapter package (dynalitics-postgres, -mysql, -mssql) exports. The only code that imports a driver.
27
+ */
28
+ export interface DriverAdapter {
29
+ readonly apiVersion: typeof ADAPTER_API_VERSION;
30
+ readonly dialect: Dialect;
31
+ /** e.g. 'dynalitics-postgres'; shown in errors only. */
32
+ readonly packageName: string;
33
+ /** Called once per credential. Must build every error with toDriverError and never close a host pool. */
34
+ createDriver(options: DriverOptions): Promise<Driver>;
35
+ }
36
+ /** Cross-copy brand: an adapter that resolved a second copy of dynalitics still produces recognisable errors. */
37
+ declare const DRIVER_ERROR_BRAND: unique symbol;
38
+ /** The only error type drivers throw: redacted message, sanitised cause, never the raw driver error. */
39
+ export declare class DriverError extends DynaliticsError {
40
+ readonly [DRIVER_ERROR_BRAND] = true;
41
+ readonly transient: boolean;
42
+ readonly timeout: boolean;
43
+ /** A data or constraint error caused by ONE row's values (as opposed to one that would fail every row). */
44
+ readonly data: boolean;
45
+ /** The database user lacks a privilege (batch-wide: retrying row by row cannot help). */
46
+ readonly permission: boolean;
47
+ constructor(safeCause: SafeCause, transient: boolean, timeout: boolean, data?: boolean, permission?: boolean);
48
+ }
49
+ export interface Classification {
50
+ readonly transient: boolean;
51
+ readonly timeout: boolean;
52
+ /** Data/constraint error: its message may contain the offending visitor value, so it is never forwarded. */
53
+ readonly data?: boolean;
54
+ readonly permission?: boolean;
55
+ /** Connection, authentication and network classes: the (redacted, scrubbed) message is forwarded. */
56
+ readonly forward?: boolean;
57
+ }
58
+ /** Drops quoted literals and parenthesised values (where databases print offending values) and caps the length. */
59
+ export declare function scrubMessage(message: string): string;
60
+ /**
61
+ * Message policy (F-02): data/constraint errors forward no text (it can contain the offending value), permission
62
+ * errors a fixed sentence, connection/auth/network/timeout classes their redacted and scrubbed message, and every
63
+ * other class only its name and code.
64
+ */
65
+ export declare function toDriverError(err: unknown, redact: Redactor, classify: (err: unknown) => Classification): DriverError;
66
+ /** `instanceof DriverError`, also true for a DriverError built by another copy of this package (spec v0.5, A5). */
67
+ export declare function isDriverError(err: unknown): err is DriverError;
68
+ export declare const isTransientError: (err: unknown) => boolean;
69
+ export declare const errCode: (err: unknown) => string;
70
+ export declare const errMessage: (err: unknown) => string;
71
+ export declare const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
72
+ export declare const IDLE_TIMEOUT_MS = 30000;
73
+ export {};
74
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,66 @@
1
+ import { DynaliticsError } from '../errors.js';
2
+ import { sanitizeError } from '../redact.js';
3
+ /** Cross-copy brand: an adapter that resolved a second copy of dynalitics still produces recognisable errors. */
4
+ const DRIVER_ERROR_BRAND = Symbol.for('dynalitics.DriverError');
5
+ /** The only error type drivers throw: redacted message, sanitised cause, never the raw driver error. */
6
+ export class DriverError extends DynaliticsError {
7
+ [DRIVER_ERROR_BRAND] = true;
8
+ transient;
9
+ timeout;
10
+ /** A data or constraint error caused by ONE row's values (as opposed to one that would fail every row). */
11
+ data;
12
+ /** The database user lacks a privilege (batch-wide: retrying row by row cannot help). */
13
+ permission;
14
+ constructor(safeCause, transient, timeout, data = false, permission = false) {
15
+ super('DYNALITICS_DRIVER', safeCause.message, safeCause);
16
+ this.transient = transient;
17
+ this.timeout = timeout;
18
+ this.data = data;
19
+ this.permission = permission;
20
+ }
21
+ }
22
+ const MAX_MESSAGE = 300;
23
+ const QUOTED = /'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|\([^()]*\)/g;
24
+ /** Drops quoted literals and parenthesised values (where databases print offending values) and caps the length. */
25
+ export function scrubMessage(message) {
26
+ return message
27
+ .replace(QUOTED, (m) => (m.startsWith('(') ? '(…)' : m.startsWith('"') ? '"…"' : "'…'"))
28
+ .slice(0, MAX_MESSAGE);
29
+ }
30
+ /**
31
+ * Message policy (F-02): data/constraint errors forward no text (it can contain the offending value), permission
32
+ * errors a fixed sentence, connection/auth/network/timeout classes their redacted and scrubbed message, and every
33
+ * other class only its name and code.
34
+ */
35
+ export function toDriverError(err, redact, classify) {
36
+ const { transient, timeout, data = false, permission = false, forward = false } = classify(err);
37
+ const base = sanitizeError(err, redact);
38
+ const withCode = base.code === undefined ? '' : ` (${base.code})`;
39
+ const head = { name: base.name, ...(base.code === undefined ? {} : { code: base.code }) };
40
+ let message;
41
+ if (data)
42
+ message = `data or constraint error${withCode}; the offending value is not shown`;
43
+ else if (permission)
44
+ message = 'permission denied; the database user lacks a required privilege';
45
+ else if (forward || timeout || transient)
46
+ message = scrubMessage(base.message);
47
+ else
48
+ message = `database error${withCode}; details are not forwarded`;
49
+ return new DriverError({ ...head, message }, transient, timeout, data, permission);
50
+ }
51
+ /** `instanceof DriverError`, also true for a DriverError built by another copy of this package (spec v0.5, A5). */
52
+ export function isDriverError(err) {
53
+ return (err instanceof DriverError ||
54
+ (typeof err === 'object' &&
55
+ err !== null &&
56
+ err[DRIVER_ERROR_BRAND] === true));
57
+ }
58
+ export const isTransientError = (err) => isDriverError(err) && err.transient;
59
+ export const errCode = (err) => {
60
+ const code = err?.code;
61
+ return typeof code === 'string' || typeof code === 'number' ? String(code) : '';
62
+ };
63
+ export const errMessage = (err) => (err instanceof Error ? err.message : String(err)).toLowerCase();
64
+ export const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
65
+ export const IDLE_TIMEOUT_MS = 30_000;
66
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,39 @@
1
+ /** Sanitised copy of a lower-level error. The raw error is never attached to ours (it may carry credentials). */
2
+ export interface SafeCause {
3
+ readonly name: string;
4
+ readonly code?: string;
5
+ readonly message: string;
6
+ }
7
+ export declare class DynaliticsError extends Error {
8
+ readonly code: string;
9
+ readonly safeCause: SafeCause | undefined;
10
+ constructor(code: string, message: string, safeCause?: SafeCause);
11
+ }
12
+ /** Thrown synchronously for invalid configuration. Lists every issue found. */
13
+ export declare class DynaliticsConfigError extends DynaliticsError {
14
+ readonly issues: readonly string[];
15
+ constructor(issues: readonly string[]);
16
+ }
17
+ /** Thrown when a database cannot be reached or a driver is missing. Message is already redacted. */
18
+ export declare class DynaliticsConnectionError extends DynaliticsError {
19
+ constructor(message: string, safeCause?: SafeCause);
20
+ }
21
+ export type SchemaProblemCode = 'missing_table' | 'not_a_table' | 'missing_column' | 'type_mismatch' | 'too_short' | 'not_nullable' | 'extra_required_column' | 'timestamp_session_timezone' | 'identity_not_discoverable';
22
+ export interface SchemaProblem {
23
+ readonly code: SchemaProblemCode;
24
+ readonly message: string;
25
+ readonly column?: string;
26
+ }
27
+ export interface SchemaWarning {
28
+ readonly code: string;
29
+ readonly message: string;
30
+ readonly column?: string;
31
+ }
32
+ /** Thrown at startup when the table does not match the column model. `fixSql` is printed text, never executed. */
33
+ export declare class DynaliticsSchemaError extends DynaliticsError {
34
+ readonly problems: readonly SchemaProblem[];
35
+ readonly warnings: readonly SchemaWarning[];
36
+ readonly fixSql: string;
37
+ constructor(report: string, problems: readonly SchemaProblem[], warnings: readonly SchemaWarning[], fixSql: string);
38
+ }
39
+ //# sourceMappingURL=errors.d.ts.map
package/dist/errors.js ADDED
@@ -0,0 +1,39 @@
1
+ export class DynaliticsError extends Error {
2
+ code;
3
+ safeCause;
4
+ constructor(code, message, safeCause) {
5
+ super(message);
6
+ this.name = new.target.name;
7
+ this.code = code;
8
+ this.safeCause = safeCause;
9
+ }
10
+ }
11
+ /** Thrown synchronously for invalid configuration. Lists every issue found. */
12
+ export class DynaliticsConfigError extends DynaliticsError {
13
+ issues;
14
+ constructor(issues) {
15
+ super('DYNALITICS_CONFIG', issues.length === 1
16
+ ? `Invalid dynalitics configuration: ${issues[0] ?? ''}`
17
+ : `Invalid dynalitics configuration (${issues.length} issues):\n${issues.map((i) => ` - ${i}`).join('\n')}`);
18
+ this.issues = [...issues];
19
+ }
20
+ }
21
+ /** Thrown when a database cannot be reached or a driver is missing. Message is already redacted. */
22
+ export class DynaliticsConnectionError extends DynaliticsError {
23
+ constructor(message, safeCause) {
24
+ super('DYNALITICS_CONNECTION', message, safeCause);
25
+ }
26
+ }
27
+ /** Thrown at startup when the table does not match the column model. `fixSql` is printed text, never executed. */
28
+ export class DynaliticsSchemaError extends DynaliticsError {
29
+ problems;
30
+ warnings;
31
+ fixSql;
32
+ constructor(report, problems, warnings, fixSql) {
33
+ super('DYNALITICS_SCHEMA', report);
34
+ this.problems = [...problems];
35
+ this.warnings = [...warnings];
36
+ this.fixSql = fixSql;
37
+ }
38
+ }
39
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1,7 @@
1
+ export { createDynalitics } from './create.js';
2
+ export { DynaliticsError, DynaliticsConfigError, DynaliticsConnectionError, DynaliticsSchemaError, } from './errors.js';
3
+ export type { SchemaProblem, SchemaWarning, SafeCause } from './errors.js';
4
+ export type { Logger } from './logger.js';
5
+ export type { Credential, Dynalitics, DynaliticsConfig, DynaliticsStats, ErrorCode, ErrorInfo, FlushResult, TrackResult, Visit, } from './types.js';
6
+ export type { ColumnDef, ColumnModel, CustomFieldDef, CustomValue, Dialect, IpMode, Role, SchemaConfig, } from './model/types.js';
7
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { createDynalitics } from './create.js';
2
+ export { DynaliticsError, DynaliticsConfigError, DynaliticsConnectionError, DynaliticsSchemaError, } from './errors.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,20 @@
1
+ import type { SqlDialect } from './dialect/types.js';
2
+ import type { Driver } from './driver/types.js';
3
+ import type { ColumnModel, IpMode } from './model/types.js';
4
+ import type { Logger } from './logger.js';
5
+ import type { Redactor } from './redact.js';
6
+ import type { Dynalitics, ErrorInfo } from './types.js';
7
+ /** What the read handler needs from a Dynalitics instance. Not part of the public API. */
8
+ export interface Internals {
9
+ readonly reader: Driver | undefined;
10
+ readonly dialect: SqlDialect;
11
+ readonly model: ColumnModel;
12
+ readonly ipMode: IpMode;
13
+ readonly redact: Redactor;
14
+ readonly report: (info: ErrorInfo) => void;
15
+ readonly logger: Logger;
16
+ readonly now: () => Date;
17
+ }
18
+ export declare function registerInternals(dyn: Dynalitics, internals: Internals): void;
19
+ export declare function getInternals(dyn: Dynalitics): Internals | undefined;
20
+ //# sourceMappingURL=internals.d.ts.map
@@ -0,0 +1,8 @@
1
+ const registry = new WeakMap();
2
+ export function registerInternals(dyn, internals) {
3
+ registry.set(dyn, internals);
4
+ }
5
+ export function getInternals(dyn) {
6
+ return registry.get(dyn);
7
+ }
8
+ //# sourceMappingURL=internals.js.map
@@ -0,0 +1,10 @@
1
+ import type { Redactor } from './redact.js';
2
+ export interface Logger {
3
+ info(msg: string): void;
4
+ warn(msg: string): void;
5
+ error(msg: string): void;
6
+ }
7
+ export declare const consoleLogger: Logger;
8
+ /** Redacts every message and swallows logger exceptions, so logging can never leak a secret or break the host. */
9
+ export declare function safeLogger(inner: Logger, redact: Redactor): Logger;
10
+ //# sourceMappingURL=logger.d.ts.map
package/dist/logger.js ADDED
@@ -0,0 +1,24 @@
1
+ /* eslint-disable no-console -- the default logger is the one place that writes to the console */
2
+ export const consoleLogger = {
3
+ info: (msg) => console.info(`[dynalitics] ${msg}`),
4
+ warn: (msg) => console.warn(`[dynalitics] ${msg}`),
5
+ error: (msg) => console.error(`[dynalitics] ${msg}`),
6
+ };
7
+ /* eslint-enable no-console */
8
+ /** Redacts every message and swallows logger exceptions, so logging can never leak a secret or break the host. */
9
+ export function safeLogger(inner, redact) {
10
+ const wrap = (fn) => (msg) => {
11
+ try {
12
+ fn(redact(msg));
13
+ }
14
+ catch {
15
+ // a broken logger must not break tracking
16
+ }
17
+ };
18
+ return {
19
+ info: wrap((m) => inner.info(m)),
20
+ warn: wrap((m) => inner.warn(m)),
21
+ error: wrap((m) => inner.error(m)),
22
+ };
23
+ }
24
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1,7 @@
1
+ import type { ColumnModel, SchemaConfig } from './types.js';
2
+ /**
3
+ * Pure and deterministic: the single source of truth that the generator, row builder, startup validator
4
+ * and (later) the read queries all consume. Never mutates its input. Throws DynaliticsConfigError listing all issues.
5
+ */
6
+ export declare function buildColumnModel(schema: SchemaConfig): ColumnModel;
7
+ //# sourceMappingURL=column-model.d.ts.map
@@ -0,0 +1,240 @@
1
+ import { DynaliticsConfigError } from '../errors.js';
2
+ import { identifierMessage, isCustomKey, isIdentifier, parseTableRef } from './identifiers.js';
3
+ import { OPTIONAL_STORED_ROLES } from './types.js';
4
+ const DEFAULT_PAGE_LENGTH = 512;
5
+ const MAX_PAGE_LENGTH = 700;
6
+ const DEFAULT_CUSTOM_LENGTH = 128;
7
+ const MAX_CUSTOM_LENGTH = 1024;
8
+ const REQUIRED = ['timestamp', 'consent', 'pageVisited'];
9
+ const str = (maxLength) => () => ({ kind: 'string', maxLength });
10
+ /** Recommended-table defaults, in DDL order (spec 3.3). */
11
+ const ROLE_SPECS = [
12
+ ['id', { name: 'id', type: () => ({ kind: 'id' }), nullable: false, personal: false }],
13
+ [
14
+ 'timestamp',
15
+ { name: 'visited_at', type: () => ({ kind: 'timestamp' }), nullable: false, personal: false },
16
+ ],
17
+ [
18
+ 'consent',
19
+ { name: 'consent', type: () => ({ kind: 'boolean' }), nullable: false, personal: false },
20
+ ],
21
+ [
22
+ 'pageVisited',
23
+ {
24
+ name: 'page_visited',
25
+ type: (len) => ({ kind: 'string', maxLength: len }),
26
+ nullable: false,
27
+ personal: false,
28
+ },
29
+ ],
30
+ ['ip', { name: 'ip_address', type: str(64), nullable: true, personal: true }],
31
+ ['userId', { name: 'user_id', type: str(128), nullable: true, personal: true }],
32
+ ['username', { name: 'username', type: str(128), nullable: true, personal: true }],
33
+ ['email', { name: 'email', type: str(320), nullable: true, personal: true }],
34
+ [
35
+ 'isFirstVisit',
36
+ { name: 'is_first_visit', type: () => ({ kind: 'boolean' }), nullable: true, personal: true },
37
+ ],
38
+ ['custom', { name: 'custom', type: () => ({ kind: 'json' }), nullable: true, personal: false }],
39
+ ];
40
+ function resolveTable(schema, issues) {
41
+ try {
42
+ return parseTableRef(schema.table);
43
+ }
44
+ catch (e) {
45
+ issues.push(...(e instanceof DynaliticsConfigError ? e.issues : [String(e)]));
46
+ return { name: 'invalid' };
47
+ }
48
+ }
49
+ function resolveRoleColumns(schema, firstVisitEnabled, pageLength, issues) {
50
+ const overrides = schema.columns ?? {};
51
+ const out = [];
52
+ for (const [role, spec] of ROLE_SPECS) {
53
+ const override = overrides[role];
54
+ if (override === false && REQUIRED.includes(role)) {
55
+ issues.push(`columns.${role} is required and cannot be false (timestamp, consent and pageVisited must be stored).`);
56
+ continue;
57
+ }
58
+ if (role === 'isFirstVisit') {
59
+ if (!firstVisitEnabled) {
60
+ if (typeof override === 'string') {
61
+ issues.push('columns.isFirstVisit is mapped but firstVisit.enabled is false. Enable write-time first-visit detection or unmap the column.');
62
+ }
63
+ continue;
64
+ }
65
+ if (override === false) {
66
+ issues.push('firstVisit.enabled needs columns.isFirstVisit; it cannot be false.');
67
+ continue;
68
+ }
69
+ }
70
+ if (override === false)
71
+ continue;
72
+ const name = override ?? spec.name;
73
+ if (!isIdentifier(name)) {
74
+ issues.push(identifierMessage(name, `columns.${role}`));
75
+ continue;
76
+ }
77
+ out.push({
78
+ role,
79
+ key: role,
80
+ name,
81
+ type: spec.type(pageLength),
82
+ nullable: spec.nullable,
83
+ personal: spec.personal,
84
+ dbGenerated: role === 'id',
85
+ });
86
+ }
87
+ return out;
88
+ }
89
+ const REQUIRED_MESSAGE = (role) => `columns.${role} is required when tableMode is "existing" (timestamp, consent and pageVisited must be mapped).`;
90
+ /**
91
+ * tableMode "existing": `columns` is an explicit mapping. A role that is not listed (or is false) is unmapped and its
92
+ * data is not stored. Only the three required roles are mandatory. Physical types are checked at startup (7.2).
93
+ */
94
+ function resolveExistingRoleColumns(schema, firstVisitEnabled, pageLength, issues) {
95
+ const overrides = schema.columns ?? {};
96
+ const out = [];
97
+ for (const [role, spec] of ROLE_SPECS) {
98
+ const override = overrides[role];
99
+ if (typeof override !== 'string') {
100
+ if (REQUIRED.includes(role))
101
+ issues.push(REQUIRED_MESSAGE(role));
102
+ continue;
103
+ }
104
+ if (role === 'isFirstVisit' && !firstVisitEnabled) {
105
+ issues.push('columns.isFirstVisit is mapped but firstVisit.enabled is false. Enable write-time first-visit detection or unmap the column.');
106
+ continue;
107
+ }
108
+ if (!isIdentifier(override)) {
109
+ issues.push(identifierMessage(override, `columns.${role}`));
110
+ continue;
111
+ }
112
+ out.push({
113
+ role,
114
+ key: role,
115
+ name: override,
116
+ type: spec.type(pageLength),
117
+ nullable: spec.nullable,
118
+ personal: spec.personal,
119
+ dbGenerated: role === 'id',
120
+ });
121
+ }
122
+ return out;
123
+ }
124
+ function promotedType(def, maxLength) {
125
+ return def.type === 'string' ? { kind: 'string', maxLength } : { kind: def.type };
126
+ }
127
+ function resolveCustomFields(schema, hasJsonColumn, issues) {
128
+ const fields = new Map();
129
+ const promoted = [];
130
+ for (const [key, def] of Object.entries(schema.customFields ?? {})) {
131
+ if (!isCustomKey(key)) {
132
+ issues.push(`${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).`);
133
+ continue;
134
+ }
135
+ const maxLength = def.maxLength ?? DEFAULT_CUSTOM_LENGTH;
136
+ if (!Number.isInteger(maxLength) || maxLength < 1 || maxLength > MAX_CUSTOM_LENGTH) {
137
+ issues.push(`customFields.${key}.maxLength must be an integer 1..${MAX_CUSTOM_LENGTH}.`);
138
+ continue;
139
+ }
140
+ const personal = def.personal ?? true;
141
+ if (def.keepWhenDenied === true && personal) {
142
+ issues.push(`customFields.${key}.keepWhenDenied requires personal: false (a personal field is never stored without consent).`);
143
+ continue;
144
+ }
145
+ if (def.breakdown === true && personal) {
146
+ issues.push(`customFields.${key}.breakdown requires personal: false.`);
147
+ continue;
148
+ }
149
+ let column;
150
+ if (def.column !== undefined) {
151
+ const name = def.column === true ? key : def.column;
152
+ if (!isIdentifier(name)) {
153
+ issues.push(identifierMessage(name, `customFields.${key}.column`));
154
+ continue;
155
+ }
156
+ column = {
157
+ role: 'promoted',
158
+ key,
159
+ name,
160
+ type: promotedType(def, maxLength),
161
+ nullable: true,
162
+ personal,
163
+ dbGenerated: false,
164
+ };
165
+ promoted.push(column);
166
+ }
167
+ else if (!hasJsonColumn) {
168
+ issues.push(`customFields.${key} is not promoted to a column and no JSON column is mapped. Set customFields.${key}.column or map columns.custom.`);
169
+ continue;
170
+ }
171
+ fields.set(key, {
172
+ key,
173
+ type: def.type,
174
+ maxLength,
175
+ personal,
176
+ breakdown: def.breakdown === true,
177
+ keepWhenDenied: def.keepWhenDenied === true,
178
+ ...(column === undefined ? {} : { column }),
179
+ });
180
+ }
181
+ return { fields, promoted };
182
+ }
183
+ function findDuplicateNames(columns, issues) {
184
+ const seen = new Map();
185
+ for (const c of columns) {
186
+ const k = c.name.toLowerCase();
187
+ const prev = seen.get(k);
188
+ if (prev !== undefined) {
189
+ issues.push(`${label(prev)} and ${label(c)} both map to "${c.name}"; each role needs its own column (names compare case-insensitively).`);
190
+ }
191
+ else {
192
+ seen.set(k, c);
193
+ }
194
+ }
195
+ }
196
+ const label = (c) => c.role === 'promoted' ? `customFields.${c.key}` : `columns.${c.key}`;
197
+ /**
198
+ * Pure and deterministic: the single source of truth that the generator, row builder, startup validator
199
+ * and (later) the read queries all consume. Never mutates its input. Throws DynaliticsConfigError listing all issues.
200
+ */
201
+ export function buildColumnModel(schema) {
202
+ const issues = [];
203
+ const existing = schema.tableMode === 'existing';
204
+ const table = resolveTable(schema, issues);
205
+ const pageLength = schema.page?.maxLength ?? DEFAULT_PAGE_LENGTH;
206
+ if (!Number.isInteger(pageLength) || pageLength < 1 || pageLength > MAX_PAGE_LENGTH) {
207
+ issues.push(`page.maxLength must be an integer 1..${MAX_PAGE_LENGTH}.`);
208
+ }
209
+ const firstVisitEnabled = schema.firstVisit?.enabled === true;
210
+ const roleColumns = (existing ? resolveExistingRoleColumns : resolveRoleColumns)(schema, firstVisitEnabled, pageLength, issues);
211
+ const hasJson = roleColumns.some((c) => c.role === 'custom');
212
+ const { fields, promoted } = resolveCustomFields(schema, hasJson, issues);
213
+ const columns = [...roleColumns, ...promoted];
214
+ findDuplicateNames(columns, issues);
215
+ const byRole = (role) => columns.find((c) => c.role === role);
216
+ const userId = byRole('userId');
217
+ const ip = byRole('ip');
218
+ if (existing) {
219
+ if (firstVisitEnabled &&
220
+ (byRole('isFirstVisit') === undefined || (userId === undefined && ip === undefined))) {
221
+ issues.push('firstVisit.enabled needs columns.isFirstVisit and at least one of columns.userId, columns.ip.');
222
+ }
223
+ }
224
+ else if (firstVisitEnabled && userId === undefined && ip === undefined) {
225
+ issues.push('firstVisit.enabled needs at least one of columns.userId, columns.ip.');
226
+ }
227
+ if (issues.length > 0)
228
+ throw new DynaliticsConfigError(issues);
229
+ return {
230
+ table,
231
+ mode: existing ? 'existing' : 'recommended',
232
+ columns,
233
+ insertColumns: columns.filter((c) => !c.dbGenerated),
234
+ unmappedRoles: OPTIONAL_STORED_ROLES.filter((r) => byRole(r) === undefined),
235
+ customFields: fields,
236
+ identity: { ...(userId === undefined ? {} : { userId }), ...(ip === undefined ? {} : { ip }) },
237
+ firstVisitEnabled,
238
+ };
239
+ }
240
+ //# sourceMappingURL=column-model.js.map
@@ -0,0 +1,19 @@
1
+ export interface TableRef {
2
+ readonly database?: string;
3
+ readonly schema?: string;
4
+ readonly name: string;
5
+ }
6
+ export declare function isIdentifier(name: unknown): name is string;
7
+ export declare function isCustomKey(name: unknown): name is string;
8
+ export declare function identifierMessage(name: string, what: string): string;
9
+ export declare function assertIdentifier(name: string, what: string): string;
10
+ export declare function assertCustomKey(key: string): string;
11
+ /** Parses 'name', 'schema.name', 'db.schema.name' or an object into a validated TableRef. */
12
+ export declare function parseTableRef(input: string | {
13
+ database?: string;
14
+ schema?: string;
15
+ name: string;
16
+ }): TableRef;
17
+ /** Dialect-specific restrictions on table references (spec 3.7). */
18
+ export declare function assertTableForDialect(ref: TableRef, dialect: 'postgres' | 'mysql' | 'mssql'): void;
19
+ //# sourceMappingURL=identifiers.d.ts.map