@toa.io/extensions.chronicles 0.0.0-stage → 1.0.0-alpha.333

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 (83) hide show
  1. package/LICENSE +22 -0
  2. package/components/chronicles.encounters/manifest.toa.yaml +108 -0
  3. package/components/chronicles.encounters/operations/anonymous.js +4 -0
  4. package/components/chronicles.encounters/operations/describe.js +4 -0
  5. package/components/chronicles.encounters/operations/identified.js +4 -0
  6. package/components/chronicles.encounters/types/index.d.ts +3 -0
  7. package/components/chronicles.encounters/types/toa.d.ts +36 -0
  8. package/components/chronicles.queries/manifest.toa.yaml +139 -0
  9. package/components/chronicles.queries/operations/define.js +3 -0
  10. package/components/chronicles.queries/operations/describe.js +3 -0
  11. package/components/chronicles.queries/operations/drop.js +3 -0
  12. package/components/chronicles.queries/operations/query.js +3 -0
  13. package/components/chronicles.queries/types/index.d.ts +3 -0
  14. package/components/chronicles.queries/types/toa.d.ts +49 -0
  15. package/components/chronicles.relay/manifest.toa.yaml +43 -0
  16. package/components/chronicles.relay/operations/encounter.js +4 -0
  17. package/components/chronicles.relay/operations/record.js +4 -0
  18. package/components/chronicles.relay/types/index.d.ts +3 -0
  19. package/components/chronicles.relay/types/toa.d.ts +24 -0
  20. package/guides/clickhouse.md +19 -0
  21. package/guides/log.md +52 -0
  22. package/package.json +39 -4
  23. package/readme.md +686 -0
  24. package/store/clickhouse.scope.sql +29 -0
  25. package/store/clickhouse.sql +99 -0
  26. package/transpiled/Client.d.ts +32 -0
  27. package/transpiled/Client.js +173 -0
  28. package/transpiled/Client.js.map +1 -0
  29. package/transpiled/Factory.d.ts +24 -0
  30. package/transpiled/Factory.js +62 -0
  31. package/transpiled/Factory.js.map +1 -0
  32. package/transpiled/Provider.d.ts +142 -0
  33. package/transpiled/Provider.js +4 -0
  34. package/transpiled/Provider.js.map +1 -0
  35. package/transpiled/Publisher.d.ts +50 -0
  36. package/transpiled/Publisher.js +169 -0
  37. package/transpiled/Publisher.js.map +1 -0
  38. package/transpiled/Reader.d.ts +28 -0
  39. package/transpiled/Reader.js +166 -0
  40. package/transpiled/Reader.js.map +1 -0
  41. package/transpiled/Recorder.d.ts +38 -0
  42. package/transpiled/Recorder.js +107 -0
  43. package/transpiled/Recorder.js.map +1 -0
  44. package/transpiled/Relay.d.ts +14 -0
  45. package/transpiled/Relay.js +27 -0
  46. package/transpiled/Relay.js.map +1 -0
  47. package/transpiled/Sender.d.ts +25 -0
  48. package/transpiled/Sender.js +86 -0
  49. package/transpiled/Sender.js.map +1 -0
  50. package/transpiled/Service.d.ts +26 -0
  51. package/transpiled/Service.js +70 -0
  52. package/transpiled/Service.js.map +1 -0
  53. package/transpiled/address.d.ts +7 -0
  54. package/transpiled/address.js +27 -0
  55. package/transpiled/address.js.map +1 -0
  56. package/transpiled/agent.d.ts +16 -0
  57. package/transpiled/agent.js +48 -0
  58. package/transpiled/agent.js.map +1 -0
  59. package/transpiled/clickhouse/Client.d.ts +35 -0
  60. package/transpiled/clickhouse/Client.js +71 -0
  61. package/transpiled/clickhouse/Client.js.map +1 -0
  62. package/transpiled/clickhouse/Provider.d.ts +35 -0
  63. package/transpiled/clickhouse/Provider.js +169 -0
  64. package/transpiled/clickhouse/Provider.js.map +1 -0
  65. package/transpiled/clickhouse/ddl.d.ts +9 -0
  66. package/transpiled/clickhouse/ddl.js +15 -0
  67. package/transpiled/clickhouse/ddl.js.map +1 -0
  68. package/transpiled/clickhouse/views.d.ts +11 -0
  69. package/transpiled/clickhouse/views.js +97 -0
  70. package/transpiled/clickhouse/views.js.map +1 -0
  71. package/transpiled/contract.d.ts +12 -0
  72. package/transpiled/contract.js +58 -0
  73. package/transpiled/contract.js.map +1 -0
  74. package/transpiled/index.d.ts +15 -0
  75. package/transpiled/index.js +2 -0
  76. package/transpiled/index.js.map +1 -0
  77. package/transpiled/settings.d.ts +13 -0
  78. package/transpiled/settings.js +48 -0
  79. package/transpiled/settings.js.map +1 -0
  80. package/transpiled/store.d.ts +5 -0
  81. package/transpiled/store.js +11 -0
  82. package/transpiled/store.js.map +1 -0
  83. package/README.md +0 -3
@@ -0,0 +1,86 @@
1
+ import { current, encode } from 'openspan';
2
+ import { Connector, Locator, entities, exceptions } from '@toa.io/core';
3
+ import { NAMESPACE, RELAY } from '@toa.io/definitions/extensions.chronicles';
4
+ import { Mismatch, project } from './contract.js';
5
+ /**
6
+ * `context.chronicles.send(name, payload, { identity })` — a fact that changes no state, written under
7
+ * `<component>.<name>`, of which what the component declares of it is sent. It is a task to the
8
+ * component that writes facts, so it resolves once the broker has it, and is kept until that
9
+ * component takes it.
10
+ */
11
+ export class Sender extends Connector {
12
+ name = 'chronicles';
13
+ host;
14
+ locator;
15
+ facts;
16
+ remote;
17
+ /** `facts` are the contracts of the facts the component declares, by name */
18
+ constructor(host, locator, facts) {
19
+ super();
20
+ this.host = host;
21
+ this.locator = locator;
22
+ this.facts = facts;
23
+ }
24
+ // named rather than forwarded, so that the connector's lifecycle is not reachable from an
25
+ // algorithm
26
+ async invoke(method, ...args) {
27
+ if (method !== 'send')
28
+ throw new exceptions.MisuseException(`Chronicles have no '${method}'`);
29
+ await this.send(args[0], args[1], args[2]);
30
+ }
31
+ async send(name, payload, options) {
32
+ if (typeof name !== 'string' || !NAME.test(name))
33
+ throw new exceptions.MisuseException(`A chronicle is named by segments of letters and digits separated by dots, not '${String(name)}'`);
34
+ if (payload !== undefined &&
35
+ (typeof payload !== 'object' || payload === null || Array.isArray(payload)))
36
+ throw new exceptions.MisuseException('The payload of a chronicle is an object');
37
+ const contract = this.facts[name];
38
+ if (contract === undefined)
39
+ throw new exceptions.MisuseException(`'${this.locator.id}' declares no fact '${name}'`);
40
+ const context = current();
41
+ const row = {
42
+ id: entities.newid(),
43
+ label: `${this.locator.id}.${name}`,
44
+ component: this.locator.id,
45
+ time: Date.now(),
46
+ entity: null,
47
+ version: null,
48
+ identity: typeof options?.identity === 'string' ? options.identity : null,
49
+ trace: context === undefined ? null : encode(context),
50
+ payload: declared(name, payload ?? {}, contract)
51
+ };
52
+ const remote = await this.recorder();
53
+ await remote.invoke('record', { input: row, task: true });
54
+ }
55
+ /**
56
+ * Resolved on the first fact and not before: an aspect is made ahead of the components of its
57
+ * composition, and one that could not be found is looked for again by the next fact.
58
+ */
59
+ async recorder() {
60
+ this.remote ??= (async () => {
61
+ const remote = await this.host.remote(LOCATOR, SOURCE);
62
+ this.depends(remote);
63
+ await remote.connect();
64
+ return remote;
65
+ })().catch((error) => {
66
+ this.remote = undefined;
67
+ throw error;
68
+ });
69
+ return await this.remote;
70
+ }
71
+ }
72
+ /** What the contract declares of the payload of a fact; one it does not admit is not sent. */
73
+ function declared(name, payload, contract) {
74
+ try {
75
+ return project(payload, contract.properties);
76
+ }
77
+ catch (exception) {
78
+ if (exception instanceof Mismatch)
79
+ throw new exceptions.MisuseException(`The fact '${name}' is not what its chronicles declare: ${exception.message}`);
80
+ throw exception;
81
+ }
82
+ }
83
+ const LOCATOR = new Locator(RELAY, NAMESPACE);
84
+ const SOURCE = { service: 'chronicles' };
85
+ const NAME = /^[a-zA-Z][a-zA-Z0-9]*(\.[a-zA-Z][a-zA-Z0-9]*)*$/;
86
+ //# sourceMappingURL=Sender.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Sender.js","sourceRoot":"","sources":["../source/Sender.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ import { Connector } from '@toa.io/core';
2
+ import type { Publisher } from './Publisher.ts';
3
+ import type { Settings } from '@toa.io/definitions/extensions.chronicles';
4
+ import type { extensions } from '@toa.io/core/types';
5
+ /**
6
+ * The extension's own service: a consumer of every event the log is kept of — one group for every
7
+ * replica, so each event is published once — and the components it ships: the queries, the relay,
8
+ * and the encounters where the annotation asks for them. Nothing of it waits for the broker the
9
+ * rows are published to.
10
+ */
11
+ export declare class Service extends Connector {
12
+ private readonly host;
13
+ private readonly settings;
14
+ private readonly publisher;
15
+ constructor(host: extensions.Host, settings: Settings, publisher: Publisher);
16
+ /** first of all, so what waits for the broker of the log holds up none of what closes after */
17
+ protected close(): Promise<void>;
18
+ protected open(): Promise<void>;
19
+ }
20
+ /** The components this extension ships, which run inside its own service. */
21
+ export declare function components(): Components;
22
+ interface Components {
23
+ labels: string[];
24
+ paths: string[];
25
+ }
26
+ export {};
@@ -0,0 +1,70 @@
1
+ import { readdirSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { Connector } from '@toa.io/core';
4
+ import { ENCOUNTERS, NAMESPACE, SERVICE } from '@toa.io/definitions/extensions.chronicles';
5
+ import { Recorder } from './Recorder.js';
6
+ /**
7
+ * The extension's own service: a consumer of every event the log is kept of — one group for every
8
+ * replica, so each event is published once — and the components it ships: the queries, the relay,
9
+ * and the encounters where the annotation asks for them. Nothing of it waits for the broker the
10
+ * rows are published to.
11
+ */
12
+ export class Service extends Connector {
13
+ host;
14
+ settings;
15
+ publisher;
16
+ // eslint-disable-next-line max-params
17
+ constructor(host, settings, publisher) {
18
+ super();
19
+ this.host = host;
20
+ this.settings = settings;
21
+ this.publisher = publisher;
22
+ this.depends(publisher);
23
+ }
24
+ /** first of all, so what waits for the broker of the log holds up none of what closes after */
25
+ async close() {
26
+ this.publisher.stop();
27
+ }
28
+ async open() {
29
+ const consumed = [
30
+ ...Object.entries(this.settings.events).map(([label, contract]) => [label, 'event', contract]),
31
+ ...Object.entries(this.settings.states).map(([label, contract]) => [label, 'state', contract])
32
+ ];
33
+ for (const [label, kind, contract] of consumed) {
34
+ // an event that is both a row and a state is two consumers, each of a group of its own
35
+ const group = kind === 'state' ? `${GROUP}.states` : GROUP;
36
+ const of = {
37
+ contract,
38
+ identity: this.settings.identities[component(label)] ?? null,
39
+ timestamp: this.settings.timestamps[component(label)] ?? null
40
+ };
41
+ const recorder = new Recorder(label, kind, of, this.publisher);
42
+ const consumer = await this.host.receive(label, group, recorder);
43
+ this.depends(consumer);
44
+ await consumer.connect();
45
+ }
46
+ const composition = await this.host.composition(paths(this.settings));
47
+ this.depends(composition);
48
+ await composition.connect();
49
+ }
50
+ }
51
+ /** The components this extension ships, which run inside its own service. */
52
+ export function components() {
53
+ const labels = [];
54
+ const paths = [];
55
+ for (const entry of readdirSync(ROOT, { withFileTypes: true }))
56
+ if (entry.isDirectory()) {
57
+ labels.push(entry.name.replace('.', '-'));
58
+ paths.push(resolve(ROOT, entry.name));
59
+ }
60
+ return { labels, paths };
61
+ }
62
+ function component(label) {
63
+ return label.slice(0, label.lastIndexOf('.'));
64
+ }
65
+ function paths(settings) {
66
+ return components().paths.filter((path) => settings.encounters || !path.endsWith(`${NAMESPACE}.${ENCOUNTERS}`));
67
+ }
68
+ const ROOT = resolve(import.meta.dirname, '../components/');
69
+ const GROUP = `${NAMESPACE}.${SERVICE}`;
70
+ //# sourceMappingURL=Service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Service.js","sourceRoot":"","sources":["../source/Service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import type { Address } from '@toa.io/definitions/extensions.chronicles';
2
+ /**
3
+ * How much of a browser's address the log keeps. An address is personal data, and the log erases
4
+ * nothing, so by default it keeps the network the address is in — a /24 of IPv4, a /48 of IPv6 —
5
+ * which is as much as a question of where a client is asks for. What is not an address is not kept.
6
+ */
7
+ export declare function keep(address: unknown, how: Address): string | null;
@@ -0,0 +1,27 @@
1
+ import { isIPv4, isIPv6 } from 'node:net';
2
+ /**
3
+ * How much of a browser's address the log keeps. An address is personal data, and the log erases
4
+ * nothing, so by default it keeps the network the address is in — a /24 of IPv4, a /48 of IPv6 —
5
+ * which is as much as a question of where a client is asks for. What is not an address is not kept.
6
+ */
7
+ export function keep(address, how) {
8
+ if (typeof address !== 'string')
9
+ return null;
10
+ if (isIPv4(address))
11
+ return how === 'full' ? address : address.replace(/\.\d+$/, '.0');
12
+ if (isIPv6(address))
13
+ return how === 'full' ? address : network(address);
14
+ return null;
15
+ }
16
+ /** The first three groups of an IPv6 address, and none of the rest. */
17
+ function network(address) {
18
+ const [head, tail = ''] = address.split('::');
19
+ const left = head === '' ? [] : head.split(':');
20
+ const right = tail === '' ? [] : tail.split(':');
21
+ const groups = [...left, ...Array(8 - left.length - right.length).fill('0'), ...right];
22
+ return (groups
23
+ .slice(0, 3)
24
+ .map((group) => group.replace(/^0+(?=.)/, ''))
25
+ .join(':') + '::');
26
+ }
27
+ //# sourceMappingURL=address.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"address.js","sourceRoot":"","sources":["../source/address.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AAGzC;;;;GAIG;AACH,MAAM,UAAU,IAAI,CAAC,OAAgB,EAAE,GAAY;IACjD,IAAI,OAAO,OAAO,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAA;IAE5C,IAAI,MAAM,CAAC,OAAO,CAAC;QAAE,OAAO,GAAG,KAAK,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;IAEtF,IAAI,MAAM,CAAC,OAAO,CAAC;QAAE,OAAO,GAAG,KAAK,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;IAEvE,OAAO,IAAI,CAAA;AACb,CAAC;AAED,uEAAuE;AACvE,SAAS,OAAO,CAAC,OAAe;IAC9B,MAAM,CAAC,IAAI,EAAE,IAAI,GAAG,EAAE,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IAC7C,MAAM,IAAI,GAAG,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAC/C,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAChD,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,EAAE,GAAG,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,KAAK,CAAC,CAAA;IAEtF,OAAO,CACL,MAAM;SACH,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;SACX,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;SAC7C,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,CACpB,CAAA;AACH,CAAC"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * What a client is, as its `user-agent` says, in the few words a question of who visits asks for:
3
+ * the browser, the operating system and the kind of device, by name and without versions — which
4
+ * browsers freeze or leave out, and which make an agent more of a fingerprint. Each is the first of
5
+ * its list that matches, so a name another agent copies comes after the one that copies it: Edge
6
+ * says Chrome and Safari, Chrome says Safari. What matches nothing is `null`.
7
+ *
8
+ * Safari on an iPad says it is a Mac, as it has since iPadOS 13, and nothing the request carries
9
+ * says otherwise: it is a desktop running macOS here.
10
+ */
11
+ export declare function classify(agent: unknown): Classified;
12
+ export interface Classified {
13
+ browser: string | null;
14
+ os: string | null;
15
+ device: string | null;
16
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * What a client is, as its `user-agent` says, in the few words a question of who visits asks for:
3
+ * the browser, the operating system and the kind of device, by name and without versions — which
4
+ * browsers freeze or leave out, and which make an agent more of a fingerprint. Each is the first of
5
+ * its list that matches, so a name another agent copies comes after the one that copies it: Edge
6
+ * says Chrome and Safari, Chrome says Safari. What matches nothing is `null`.
7
+ *
8
+ * Safari on an iPad says it is a Mac, as it has since iPadOS 13, and nothing the request carries
9
+ * says otherwise: it is a desktop running macOS here.
10
+ */
11
+ export function classify(agent) {
12
+ if (typeof agent !== 'string' || agent === '')
13
+ return { browser: null, os: null, device: null };
14
+ return {
15
+ browser: first(BROWSERS, agent),
16
+ os: first(SYSTEMS, agent),
17
+ device: first(DEVICES, agent) ?? 'desktop'
18
+ };
19
+ }
20
+ function first(list, agent) {
21
+ return list.find(([, pattern]) => pattern.test(agent))?.[0] ?? null;
22
+ }
23
+ const BROWSERS = [
24
+ ['Edge', /\bEdg(e|A|iOS)?\//],
25
+ ['Opera', /\bOPR\/|\bOPT\/|\bOpera\b/],
26
+ ['Samsung Internet', /\bSamsungBrowser\//],
27
+ ['Firefox', /\bFirefox\/|\bFxiOS\//],
28
+ ['Chrome', /(\b|Headless)Chrome\/|\bCriOS\//],
29
+ ['Safari', /\bVersion\/[\d.]+.*\bSafari\//]
30
+ ];
31
+ const SYSTEMS = [
32
+ ['Windows', /\bWindows\b/],
33
+ ['iOS', /\b(iPhone|iPad|iPod)\b/],
34
+ ['macOS', /\bMac OS X\b|\bMacintosh\b/],
35
+ ['Android', /\bAndroid\b/],
36
+ ['ChromeOS', /\bCrOS\b/],
37
+ ['Linux', /\bLinux\b/]
38
+ ];
39
+ // a desktop is what is none of these, so it is not listed
40
+ const DEVICES = [
41
+ [
42
+ 'bot',
43
+ /bot\b|crawl|spider|slurp|preview|headless|^curl\/|^wget\/|^python-|^go-http|^okhttp/i
44
+ ],
45
+ ['tablet', /\biPad\b|\bTablet\b|\bAndroid\b(?!.*\bMobile\b)/],
46
+ ['mobile', /\bMobile\b|\biPhone\b|\biPod\b/]
47
+ ];
48
+ //# sourceMappingURL=agent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent.js","sourceRoot":"","sources":["../source/agent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,UAAU,QAAQ,CAAC,KAAc;IACrC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,EAAE;QAC3C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAA;IAElD,OAAO;QACL,OAAO,EAAE,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC;QAC/B,EAAE,EAAE,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC;QACzB,MAAM,EAAE,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,IAAI,SAAS;KAC3C,CAAA;AACH,CAAC;AAQD,SAAS,KAAK,CAAC,IAA6B,EAAE,KAAa;IACzD,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,CAAA;AACrE,CAAC;AAED,MAAM,QAAQ,GAA4B;IACxC,CAAC,MAAM,EAAE,mBAAmB,CAAC;IAC7B,CAAC,OAAO,EAAE,2BAA2B,CAAC;IACtC,CAAC,kBAAkB,EAAE,oBAAoB,CAAC;IAC1C,CAAC,SAAS,EAAE,uBAAuB,CAAC;IACpC,CAAC,QAAQ,EAAE,iCAAiC,CAAC;IAC7C,CAAC,QAAQ,EAAE,+BAA+B,CAAC;CAC5C,CAAA;AAED,MAAM,OAAO,GAA4B;IACvC,CAAC,SAAS,EAAE,aAAa,CAAC;IAC1B,CAAC,KAAK,EAAE,wBAAwB,CAAC;IACjC,CAAC,OAAO,EAAE,4BAA4B,CAAC;IACvC,CAAC,SAAS,EAAE,aAAa,CAAC;IAC1B,CAAC,UAAU,EAAE,UAAU,CAAC;IACxB,CAAC,OAAO,EAAE,WAAW,CAAC;CACvB,CAAA;AAED,0DAA0D;AAC1D,MAAM,OAAO,GAA4B;IACvC;QACE,KAAK;QACL,sFAAsF;KACvF;IACD,CAAC,QAAQ,EAAE,iDAAiD,CAAC;IAC7D,CAAC,QAAQ,EAAE,gCAAgC,CAAC;CAC7C,CAAA"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * ClickHouse over its HTTP interface, as one user: `fetch` and nothing else, because what the log
3
+ * needs of it is a statement and its answer.
4
+ */
5
+ export declare class ClickHouse {
6
+ private readonly endpoint;
7
+ private readonly headers;
8
+ /** The statements run in the user's default database, which is what the store gives it to read. */
9
+ constructor(address: string, user: string, key: string);
10
+ /** A statement that answers nothing worth reading. */
11
+ run(sql: string, settings?: Record<string, string>): Promise<void>;
12
+ /**
13
+ * A query, answered as `JSONCompact`: the columns, and the rows as arrays. The format is the
14
+ * default rather than a clause, which a statement like `EXPLAIN` would take as its own.
15
+ */
16
+ select(sql: string, settings?: Record<string, string>): Promise<Answer>;
17
+ /** Each row as an object, for what this extension reads of the store itself. */
18
+ rows<T = Record<string, unknown>>(sql: string, settings?: Record<string, string>): Promise<T[]>;
19
+ private request;
20
+ }
21
+ export interface Answer {
22
+ meta: Array<{
23
+ name: string;
24
+ type: string;
25
+ }>;
26
+ data: unknown[][];
27
+ rows: number;
28
+ }
29
+ /** What ClickHouse refused a statement with, as it said it. */
30
+ export declare class ClickHouseError extends Error {
31
+ readonly status: number;
32
+ /** ClickHouse's own code, where its message carries one */
33
+ readonly code: number | undefined;
34
+ constructor(status: number, message: string);
35
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * ClickHouse over its HTTP interface, as one user: `fetch` and nothing else, because what the log
3
+ * needs of it is a statement and its answer.
4
+ */
5
+ export class ClickHouse {
6
+ endpoint;
7
+ headers;
8
+ /** The statements run in the user's default database, which is what the store gives it to read. */
9
+ constructor(address, user, key) {
10
+ const url = new URL(address);
11
+ this.endpoint = `${url.protocol}//${url.host}/`;
12
+ this.headers = { 'x-clickhouse-user': user, 'x-clickhouse-key': key };
13
+ }
14
+ /** A statement that answers nothing worth reading. */
15
+ async run(sql, settings = {}) {
16
+ await this.request(sql, settings);
17
+ }
18
+ /**
19
+ * A query, answered as `JSONCompact`: the columns, and the rows as arrays. The format is the
20
+ * default rather than a clause, which a statement like `EXPLAIN` would take as its own.
21
+ */
22
+ async select(sql, settings = {}) {
23
+ const text = await this.request(sql, { ...settings, default_format: 'JSONCompact' });
24
+ return parse(text);
25
+ }
26
+ /** Each row as an object, for what this extension reads of the store itself. */
27
+ async rows(sql, settings = {}) {
28
+ const text = await this.request(sql, {
29
+ ...settings,
30
+ ...NUMBERS,
31
+ default_format: 'JSON'
32
+ });
33
+ return parse(text).data;
34
+ }
35
+ async request(sql, settings) {
36
+ const search = new URLSearchParams(settings);
37
+ const response = await fetch(`${this.endpoint}?${search.toString()}`, {
38
+ method: 'POST',
39
+ headers: this.headers,
40
+ body: sql
41
+ });
42
+ const text = await response.text();
43
+ if (!response.ok)
44
+ throw new ClickHouseError(response.status, text.trim());
45
+ return text;
46
+ }
47
+ }
48
+ /** What ClickHouse refused a statement with, as it said it. */
49
+ export class ClickHouseError extends Error {
50
+ status;
51
+ /** ClickHouse's own code, where its message carries one */
52
+ code;
53
+ constructor(status, message) {
54
+ super(message);
55
+ this.status = status;
56
+ const code = /^Code: (\d+)\./.exec(message)?.[1];
57
+ this.code = code === undefined ? undefined : Number(code);
58
+ }
59
+ }
60
+ /** An answer in another format is one the statement asked for itself, with a `FORMAT`. */
61
+ function parse(text) {
62
+ try {
63
+ return JSON.parse(text);
64
+ }
65
+ catch {
66
+ throw new ClickHouseError(200, 'The answer is not JSON: a statement leaves `FORMAT` out');
67
+ }
68
+ }
69
+ /** UInt64 as numbers rather than strings: what this extension reads of them is counts. */
70
+ const NUMBERS = { output_format_json_quote_64bit_integers: '0' };
71
+ //# sourceMappingURL=Client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Client.js","sourceRoot":"","sources":["../../source/clickhouse/Client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ import type { Definition, Encountered, Field, Label, Provider, Result, Summary, View } from '../Provider.ts';
2
+ import type { User } from '../settings.ts';
3
+ import type { Property } from '@toa.io/definitions/extensions.chronicles';
4
+ /**
5
+ * The log in ClickHouse, read as the context's user of it: `events`, `states` and `encounters` in
6
+ * the user's default database, which the store lets it read the rows of its scope of, under limits
7
+ * of its own. A query is asked to be read-only besides; only a view is created, in the database
8
+ * named after the scope, where the store lets the user create one.
9
+ *
10
+ * A view says what it is in its comment, as JSON: the `title` and the `description` it was defined
11
+ * with.
12
+ */
13
+ export declare class ClickHouseProvider implements Provider {
14
+ readonly guide: string;
15
+ readonly tables: string[];
16
+ private readonly user;
17
+ private readonly scope;
18
+ /** `scope` is the context's, which its views are kept under */
19
+ constructor(url: string, user: User, scope: string);
20
+ fields(properties: Record<string, Property>): Field[];
21
+ states(): Promise<Summary[]>;
22
+ labels(): Promise<Label[]>;
23
+ encounters(): Promise<Encountered[]>;
24
+ /**
25
+ * One row more than is answered, cut by the `limit` setting — the result's own, outside whatever
26
+ * the statement says — so a result of the limit exactly reads as whole, and one past it as cut.
27
+ */
28
+ query(sql: string): Promise<Result>;
29
+ validate(sql: string): Promise<void>;
30
+ views(): Promise<View[]>;
31
+ define(definition: Definition): Promise<void>;
32
+ drop(name: string): Promise<void>;
33
+ private view;
34
+ private read;
35
+ }
@@ -0,0 +1,169 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { ClickHouse, ClickHouseError } from './Client.js';
4
+ import * as ddl from './ddl.js';
5
+ import * as views from './views.js';
6
+ import { Refusal } from '../Provider.js';
7
+ /**
8
+ * The log in ClickHouse, read as the context's user of it: `events`, `states` and `encounters` in
9
+ * the user's default database, which the store lets it read the rows of its scope of, under limits
10
+ * of its own. A query is asked to be read-only besides; only a view is created, in the database
11
+ * named after the scope, where the store lets the user create one.
12
+ *
13
+ * A view says what it is in its comment, as JSON: the `title` and the `description` it was defined
14
+ * with.
15
+ */
16
+ export class ClickHouseProvider {
17
+ guide = GUIDE;
18
+ tables = ddl.TABLES;
19
+ user;
20
+ scope;
21
+ /** `scope` is the context's, which its views are kept under */
22
+ constructor(url, user, scope) {
23
+ this.user = new ClickHouse(url, user.username, user.password);
24
+ this.scope = scope;
25
+ }
26
+ fields(properties) {
27
+ return views.fields(properties);
28
+ }
29
+ async states() {
30
+ return await this.read(`SELECT component, uniqExact(entity) AS entities, toString(max(time)) AS last
31
+ FROM states GROUP BY component ORDER BY component`);
32
+ }
33
+ async labels() {
34
+ return await this.read(`SELECT label, count() AS rows, toString(min(time)) AS first, toString(max(time)) AS last
35
+ FROM events GROUP BY label ORDER BY label`);
36
+ }
37
+ async encounters() {
38
+ const names = await this.read(`SELECT name, count() AS rows, toString(min(time)) AS first, toString(max(time)) AS last,
39
+ toString(max(received)) AS received
40
+ FROM encounters GROUP BY name ORDER BY name`);
41
+ // the fields of the latest payloads of each name, as they were sent: two types of one path
42
+ // are two clients that disagree
43
+ const fields = await this.read(`SELECT name, path, arraySort(groupUniqArray(type)) AS types
44
+ FROM (
45
+ SELECT name, typed.1 AS path, typed.2 AS type
46
+ FROM (SELECT name, payload FROM encounters ORDER BY received DESC LIMIT ${SAMPLE} BY name)
47
+ ARRAY JOIN arrayZip(
48
+ mapKeys(JSONAllPathsWithTypes(payload)),
49
+ mapValues(JSONAllPathsWithTypes(payload))
50
+ ) AS typed
51
+ )
52
+ GROUP BY name, path ORDER BY name, path`);
53
+ return names.map((encountered) => ({
54
+ ...encountered,
55
+ fields: fields
56
+ .filter(({ name }) => name === encountered.name)
57
+ .map(({ path, types }) => ({ path, types }))
58
+ }));
59
+ }
60
+ /**
61
+ * One row more than is answered, cut by the `limit` setting — the result's own, outside whatever
62
+ * the statement says — so a result of the limit exactly reads as whole, and one past it as cut.
63
+ */
64
+ async query(sql) {
65
+ const settings = { ...READONLY, ...NUMBERS, limit: String(ddl.LIMITS.rows + 1) };
66
+ const answer = await refusing(async () => await this.user.select(sql, settings));
67
+ return {
68
+ columns: answer.meta,
69
+ rows: answer.data.slice(0, ddl.LIMITS.rows),
70
+ truncated: answer.data.length > ddl.LIMITS.rows
71
+ };
72
+ }
73
+ // parsed and planned as the user, which is what refuses a view that reads what a query could not
74
+ async validate(sql) {
75
+ await refusing(async () => {
76
+ const [ast] = await this.read(`EXPLAIN AST ${sql}`);
77
+ if (!ast.explain.startsWith('SelectWithUnionQuery'))
78
+ throw new Refusal('A view is one SELECT');
79
+ await this.user.run(`EXPLAIN PLAN ${sql}`, READONLY);
80
+ });
81
+ }
82
+ async views() {
83
+ const tables = await this.read(`SELECT name, as_select, create_table_query, comment FROM system.tables
84
+ WHERE database = ${ddl.literal(this.scope)} AND NOT startsWith(name, '.inner')
85
+ ORDER BY name`);
86
+ const refreshes = await this.read(`SELECT view, toString(last_success_time) AS refreshed FROM system.view_refreshes
87
+ WHERE database = ${ddl.literal(this.scope)} AND last_success_time IS NOT NULL`);
88
+ const refreshed = new Map(refreshes.map(({ view, refreshed }) => [view, refreshed]));
89
+ return tables.map((table) => {
90
+ const { title, description } = said(table.comment);
91
+ const view = {
92
+ name: table.name,
93
+ read: `${ddl.id(this.scope)}.${ddl.id(table.name)}`,
94
+ sql: table.as_select
95
+ };
96
+ if (title !== undefined)
97
+ view.title = title;
98
+ if (description !== undefined)
99
+ view.description = description;
100
+ // stated in seconds, and kept in the largest unit that divides them
101
+ const every = /REFRESH EVERY (\d+) (SECOND|MINUTE|HOUR|DAY|WEEK)/.exec(table.create_table_query);
102
+ if (every !== null) {
103
+ view.refresh = Number(every[1]) * UNITS[every[2]];
104
+ const at = refreshed.get(table.name);
105
+ if (at !== undefined)
106
+ view.refreshed = at;
107
+ }
108
+ return view;
109
+ });
110
+ }
111
+ // the comment is on a line of its own: a statement that ends in a comment of its own would
112
+ // take it in otherwise
113
+ async define(definition) {
114
+ const { name, sql, refresh, title, description } = definition;
115
+ const comment = ddl.literal(JSON.stringify({ title, description }));
116
+ const view = this.view(name);
117
+ await refusing(async () => {
118
+ await this.user.run(`DROP TABLE IF EXISTS ${view} SYNC`);
119
+ if (refresh === undefined)
120
+ await this.user.run(`CREATE VIEW ${view} SQL SECURITY INVOKER AS ${sql}\nCOMMENT ${comment}`);
121
+ else
122
+ await this.user.run(`CREATE MATERIALIZED VIEW ${view} REFRESH EVERY ${refresh} SECOND
123
+ ENGINE = MergeTree ORDER BY tuple() AS ${sql}\nCOMMENT ${comment}`);
124
+ });
125
+ }
126
+ async drop(name) {
127
+ await refusing(async () => await this.user.run(`DROP TABLE IF EXISTS ${this.view(name)} SYNC`));
128
+ }
129
+ view(name) {
130
+ return `${ddl.id(this.scope)}.${ddl.id(name)}`;
131
+ }
132
+ async read(sql) {
133
+ return await this.user.rows(sql, READONLY);
134
+ }
135
+ }
136
+ /** What ClickHouse refused is the provider's refusal, in ClickHouse's words. */
137
+ async function refusing(fn) {
138
+ try {
139
+ return await fn();
140
+ }
141
+ catch (exception) {
142
+ if (exception instanceof ClickHouseError)
143
+ throw new Refusal(exception.message);
144
+ throw exception;
145
+ }
146
+ }
147
+ /** What a view's comment says of it; nothing, of one made otherwise. */
148
+ function said(comment) {
149
+ try {
150
+ const parsed = JSON.parse(comment);
151
+ return typeof parsed === 'object' && parsed !== null ? parsed : {};
152
+ }
153
+ catch {
154
+ return {};
155
+ }
156
+ }
157
+ const UNITS = {
158
+ SECOND: 1,
159
+ MINUTE: 60,
160
+ HOUR: 3600,
161
+ DAY: 86400,
162
+ WEEK: 604800
163
+ };
164
+ const SAMPLE = 1000;
165
+ const GUIDE = readFileSync(resolve(import.meta.dirname, '../../guides/clickhouse.md'), 'utf8');
166
+ const NUMBERS = { output_format_json_quote_64bit_integers: '0' };
167
+ /** what a request asks besides its user's rights: to change nothing */
168
+ const READONLY = { readonly: '1' };
169
+ //# sourceMappingURL=Provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Provider.js","sourceRoot":"","sources":["../../source/clickhouse/Provider.ts"],"names":[],"mappings":"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"}