@pepys/kit 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/README.md +63 -2
- package/package.json +34 -4
- package/src/constants.ts +1 -0
- package/src/consumers/define-consumer.ts +20 -0
- package/src/consumers/types.ts +48 -0
- package/src/delivery-checks/build-delivery-proof.ts +12 -0
- package/src/delivery-checks/is-delivery-authentic.ts +49 -0
- package/src/deployments/check-deployment.ts +445 -0
- package/src/deployments/define-deployment.ts +7 -0
- package/src/deployments/define-plugin.ts +56 -0
- package/src/deployments/list-receive-routes.ts +27 -0
- package/src/deployments/types.ts +112 -0
- package/src/entity-ids/build-entity-id.ts +16 -0
- package/src/entity-ids/entity-id-failure.ts +6 -0
- package/src/entity-ids/entity-id-parts.ts +5 -0
- package/src/entity-ids/fact-kinds.ts +3 -0
- package/src/entity-ids/find-entity-id-failure.ts +25 -0
- package/src/entity-ids/parse-entity-id.ts +27 -0
- package/src/fact-envelope-cases.json +464 -0
- package/src/facts/build-enrichment-fact-id.ts +15 -0
- package/src/facts/build-event-fact-id.ts +11 -0
- package/src/facts/build-fact-hash.ts +6 -0
- package/src/facts/build-manual-fact-id.ts +7 -0
- package/src/facts/build-observation-fact-id.ts +15 -0
- package/src/facts/build-webhook-fact-id.ts +11 -0
- package/src/facts/parse-fact-envelope.ts +44 -0
- package/src/facts/parse-stream-name.ts +33 -0
- package/src/generators/build-payload-shape.ts +80 -0
- package/src/generators/collect-view-kinds.ts +8 -0
- package/src/generators/format-column-type.ts +16 -0
- package/src/generators/format-entity-ids.ts +46 -0
- package/src/generators/format-fact-reader.ts +194 -0
- package/src/generators/format-literal.ts +13 -0
- package/src/generators/format-model-contract.ts +23 -0
- package/src/generators/format-operations.ts +109 -0
- package/src/generators/format-payload-shape.ts +181 -0
- package/src/generators/format-property-access.ts +6 -0
- package/src/generators/format-property-name.ts +6 -0
- package/src/generators/format-row-readers.ts +116 -0
- package/src/generators/generate-boundary-glue.ts +60 -0
- package/src/generators/generate-sqlite-schema.ts +78 -0
- package/src/generators/is-identifier.ts +5 -0
- package/src/generators/quote-sql-identifier.ts +3 -0
- package/src/generators/to-entity-id-type-name.ts +6 -0
- package/src/generators/to-pascal-case.ts +7 -0
- package/src/generators.ts +4 -0
- package/src/ids/types.ts +25 -0
- package/src/index.ts +37 -0
- package/src/link-types/define-link-type.ts +9 -0
- package/src/link-types/types.ts +15 -0
- package/src/links/build-link-registry.ts +7 -0
- package/src/links/find-link-problem.ts +53 -0
- package/src/links/link-check-cases.json +274 -0
- package/src/links/types.ts +27 -0
- package/src/queries/define-query.ts +14 -0
- package/src/queries/types.ts +61 -0
- package/src/send/create-fact-batch-sender.ts +143 -0
- package/src/send/types.ts +25 -0
- package/src/send.ts +2 -0
- package/src/sources/define-fact-family.ts +10 -0
- package/src/sources/define-source.ts +7 -0
- package/src/sources/read-fact-batch.ts +145 -0
- package/src/sources/receive-fact-batch.ts +11 -0
- package/src/sources/types.ts +170 -0
- package/src/store-reader/store-reader.ts +76 -0
- package/src/tables/define-table.ts +13 -0
- package/src/tables/table-row.ts +20 -0
- package/src/types.ts +47 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { isRecord, toTagged } from '@zgeoff/std';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { buildEventFactID } from '../facts/build-event-fact-id.ts';
|
|
4
|
+
import type { EntityID, StreamName } from '../ids/types.ts';
|
|
5
|
+
import type { Result } from '../types.ts';
|
|
6
|
+
import type {
|
|
7
|
+
Delivery,
|
|
8
|
+
FactBatchBody,
|
|
9
|
+
FactFamily,
|
|
10
|
+
PendingFact,
|
|
11
|
+
Received,
|
|
12
|
+
Sender,
|
|
13
|
+
SourceContext,
|
|
14
|
+
} from './types.ts';
|
|
15
|
+
|
|
16
|
+
const batchFactSchema = z.strictObject({
|
|
17
|
+
type: z.string().min(1),
|
|
18
|
+
event: z.string().min(1),
|
|
19
|
+
entity: z.string(),
|
|
20
|
+
actor: z.string().optional(),
|
|
21
|
+
at: z.string(),
|
|
22
|
+
payload: z.unknown(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const factBatchBodySchema = z.strictObject({
|
|
26
|
+
v: z.literal(1),
|
|
27
|
+
facts: z.array(batchFactSchema),
|
|
28
|
+
}) satisfies z.ZodType<FactBatchBody>;
|
|
29
|
+
|
|
30
|
+
// The read of a fact batch path. The testing package builds its envelopes with it, as ingest does.
|
|
31
|
+
export function readFactBatch(delivery: Delivery, source: SourceContext): Received {
|
|
32
|
+
const json = parseJSON(new TextDecoder().decode(delivery.body));
|
|
33
|
+
|
|
34
|
+
if (!json.ok) {
|
|
35
|
+
return { kind: 'refused', status: 400, reason: 'the body is not JSON' };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const body = factBatchBodySchema.safeParse(json.value);
|
|
39
|
+
|
|
40
|
+
if (!body.success) {
|
|
41
|
+
return { kind: 'refused', status: 422, reason: z.prettifyError(body.error) };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (body.data.facts.length === 0) {
|
|
45
|
+
return { kind: 'ignored' };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const facts: PendingFact[] = [];
|
|
49
|
+
|
|
50
|
+
for (const [index, fact] of body.data.facts.entries()) {
|
|
51
|
+
const built = buildPendingFact(fact, source, delivery.sender);
|
|
52
|
+
|
|
53
|
+
if (!built.ok) {
|
|
54
|
+
return { kind: 'refused', status: 422, reason: `fact ${index}: ${built.failure}` };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
facts.push(built.value);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { kind: 'batch', batch: { facts } };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseJSON(text: string): Result<unknown, null> {
|
|
64
|
+
try {
|
|
65
|
+
const value: unknown = JSON.parse(text);
|
|
66
|
+
|
|
67
|
+
return { ok: true, value };
|
|
68
|
+
} catch {
|
|
69
|
+
return { ok: false, failure: null };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
type BatchFact = FactBatchBody['facts'][number];
|
|
74
|
+
|
|
75
|
+
function buildPendingFact(
|
|
76
|
+
fact: BatchFact,
|
|
77
|
+
source: SourceContext,
|
|
78
|
+
sender: Sender | undefined,
|
|
79
|
+
): Result<PendingFact, string> {
|
|
80
|
+
const family = findFamily(source.families, fact.type);
|
|
81
|
+
|
|
82
|
+
if (family === undefined) {
|
|
83
|
+
return { ok: false, failure: `the source has no fact type ${fact.type}` };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!fact.entity.startsWith(`${family.entityKind}:`)) {
|
|
87
|
+
return { ok: false, failure: `the entity is not of the kind ${family.entityKind}` };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const sent = buildSentPayload(fact.payload, sender);
|
|
91
|
+
|
|
92
|
+
if (!sent.ok) {
|
|
93
|
+
return sent;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const payload = family.types[fact.type]?.safeParse(sent.value);
|
|
97
|
+
|
|
98
|
+
if (payload?.success !== true) {
|
|
99
|
+
return { ok: false, failure: `the payload does not fit the type ${fact.type}` };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
ok: true,
|
|
104
|
+
value: {
|
|
105
|
+
stream: toTagged<StreamName>(family.stream),
|
|
106
|
+
fact: {
|
|
107
|
+
id: buildEventFactID({ source: source.name, event: fact.event }),
|
|
108
|
+
type: fact.type,
|
|
109
|
+
v: 1,
|
|
110
|
+
api: family.api,
|
|
111
|
+
entity: toTagged<EntityID>(fact.entity),
|
|
112
|
+
...(fact.actor === undefined ? {} : { actor: toTagged<EntityID>(fact.actor) }),
|
|
113
|
+
at: fact.at,
|
|
114
|
+
|
|
115
|
+
// the payload as sent: a schema may strip or reshape, and the log keeps what arrived
|
|
116
|
+
payload: sent.value,
|
|
117
|
+
},
|
|
118
|
+
blobs: [],
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function findFamily(families: readonly FactFamily[], type: string): FactFamily | undefined {
|
|
124
|
+
return families.find((family) => Object.hasOwn(family.types, type));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// On a path with a proven sender, the stored `origin` is that sender, whatever the body holds: the
|
|
128
|
+
// route's secret proves the batch, and only the proof says which sender wrote it.
|
|
129
|
+
function buildSentPayload(payload: unknown, sender: Sender | undefined): Result<unknown, string> {
|
|
130
|
+
if (sender === undefined) {
|
|
131
|
+
return { ok: true, value: payload };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!isRecord(payload)) {
|
|
135
|
+
return {
|
|
136
|
+
ok: false,
|
|
137
|
+
failure: 'the payload is not an object, so it cannot hold the proven sender',
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
ok: true,
|
|
143
|
+
value: { ...payload, origin: { issuer: sender.issuer, subject: sender.subject } },
|
|
144
|
+
};
|
|
145
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { readFactBatch } from './read-fact-batch.ts';
|
|
2
|
+
import type { ReceivePath } from './types.ts';
|
|
3
|
+
|
|
4
|
+
type FactBatchReceiver = Pick<
|
|
5
|
+
ReceivePath,
|
|
6
|
+
'route' | 'check' | 'bodyByteLimit' | 'factLimit' | 'reach' | 'sender'
|
|
7
|
+
>;
|
|
8
|
+
|
|
9
|
+
export function receiveFactBatch(receiver: FactBatchReceiver): ReceivePath {
|
|
10
|
+
return { ...receiver, kind: 'receive', read: readFactBatch };
|
|
11
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
import type { BlobHash, EntityID, StreamName } from '../ids/types.ts';
|
|
3
|
+
import type { EntityKind } from '../link-types/types.ts';
|
|
4
|
+
import type { DeclaredStreamName, FactEnvelope } from '../types.ts';
|
|
5
|
+
|
|
6
|
+
export interface FactFamily<
|
|
7
|
+
Types extends Readonly<Record<string, z.ZodType>> = Readonly<Record<string, z.ZodType>>,
|
|
8
|
+
Kind extends EntityKind = EntityKind,
|
|
9
|
+
> {
|
|
10
|
+
readonly stream: DeclaredStreamName;
|
|
11
|
+
readonly api: string;
|
|
12
|
+
|
|
13
|
+
// the kind of the `entity` of every fact of the family
|
|
14
|
+
readonly entityKind: Kind;
|
|
15
|
+
|
|
16
|
+
// one zod schema per fact type; a fact type belongs to one family in the deployment
|
|
17
|
+
readonly types: Types;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// A source holds no secret and reads no environment.
|
|
21
|
+
export interface SourceDefinition<Family extends FactFamily = FactFamily> {
|
|
22
|
+
readonly name: string;
|
|
23
|
+
readonly families: readonly Family[];
|
|
24
|
+
readonly live?: ReceivePath | PullPath | undefined;
|
|
25
|
+
readonly recovery?: PullPath | undefined;
|
|
26
|
+
readonly backfill?: PullPath | undefined;
|
|
27
|
+
readonly resync?: PullPath | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Ingest owns every step of a delivery but `read`: the method, the size, the check, the sender, the
|
|
31
|
+
// decoding, the fact limit, the appends and the answer.
|
|
32
|
+
export interface ReceivePath {
|
|
33
|
+
readonly kind: 'receive';
|
|
34
|
+
readonly route: string;
|
|
35
|
+
|
|
36
|
+
// More routes that pass the same check and sender, answer as an ignored delivery, and store
|
|
37
|
+
// nothing. An OTLP exporter sends metrics and traces beside its logs, and retries a refusal.
|
|
38
|
+
readonly ignoredRoutes?: readonly string[] | undefined;
|
|
39
|
+
readonly check: DeliveryCheck;
|
|
40
|
+
|
|
41
|
+
// Default 1 MiB, for the body as sent and for the body after decoding.
|
|
42
|
+
readonly bodyByteLimit?: number | undefined;
|
|
43
|
+
|
|
44
|
+
// Default 100 facts in one delivery.
|
|
45
|
+
readonly factLimit?: number | undefined;
|
|
46
|
+
|
|
47
|
+
// The `content-encoding` values the path takes besides none. Default none.
|
|
48
|
+
readonly encodings?: readonly 'gzip'[] | undefined;
|
|
49
|
+
|
|
50
|
+
// 'status' (default): 200 {"status":"stored","stored":<n>}, or 202 {"status":"ignored"}.
|
|
51
|
+
// 'empty': 200 {} for stored and for ignored, which an OTLP exporter expects.
|
|
52
|
+
readonly answer?: 'status' | 'empty' | undefined;
|
|
53
|
+
|
|
54
|
+
// A mark for the deployment's network rules. Default 'private'. It changes no answer.
|
|
55
|
+
readonly reach?: 'public' | 'private' | undefined;
|
|
56
|
+
|
|
57
|
+
// The path needs a proven sender: ingest answers 401 and stores nothing without one.
|
|
58
|
+
readonly sender?: SenderRequirement | undefined;
|
|
59
|
+
|
|
60
|
+
// Pure: it writes nothing.
|
|
61
|
+
readonly read: (delivery: Delivery, source: SourceContext) => Received;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// `hmac-sha256`: the header holds `sha256=<hex of HMAC-SHA256(secret, body as sent)>`. `token`: it
|
|
65
|
+
// holds the secret, or `Bearer <secret>`. Neither signs a time, so a fact ID must come from the
|
|
66
|
+
// signed body, and a replay is then a copy that every consumer drops.
|
|
67
|
+
export interface DeliveryCheck {
|
|
68
|
+
readonly kind: 'hmac-sha256' | 'token';
|
|
69
|
+
readonly header: string;
|
|
70
|
+
readonly secretEnv: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// The proof a path needs, as data. `kind` picks the adapter of the `SenderProof` port. The audience
|
|
74
|
+
// and the folder of trust documents belong to the deployment, so a plug-in names only the
|
|
75
|
+
// variables that hold them.
|
|
76
|
+
export interface SenderRequirement {
|
|
77
|
+
readonly kind: 'imp-identity';
|
|
78
|
+
readonly audienceEnv: string;
|
|
79
|
+
readonly issuersEnv: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface Delivery {
|
|
83
|
+
headers: Headers;
|
|
84
|
+
|
|
85
|
+
// after the check passed, and after decoding
|
|
86
|
+
body: Uint8Array;
|
|
87
|
+
|
|
88
|
+
// Present on every call for a path with a `sender`. `read` never takes a sender from `headers`.
|
|
89
|
+
sender?: Sender | undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface Sender {
|
|
93
|
+
readonly issuer: string;
|
|
94
|
+
readonly subject: string;
|
|
95
|
+
readonly name: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// The port ingest calls for a path with a `sender`. It answers null when nothing proves a sender.
|
|
99
|
+
export interface SenderProof {
|
|
100
|
+
prove: (requirement: SenderRequirement, headers: Headers) => Promise<Sender | null>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface SourceContext {
|
|
104
|
+
name: string;
|
|
105
|
+
families: readonly FactFamily[];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Ingest answers 401, 405, 413 and 415 itself, so `read` refuses with 400 or 422 only.
|
|
109
|
+
export type Received =
|
|
110
|
+
| { kind: 'refused'; status: 400 | 422; reason: string }
|
|
111
|
+
| { kind: 'ignored' }
|
|
112
|
+
| { kind: 'batch'; batch: Batch };
|
|
113
|
+
|
|
114
|
+
// Ingest reads the variables in `env` at start and passes their values.
|
|
115
|
+
export interface PullPath {
|
|
116
|
+
readonly kind: 'pull';
|
|
117
|
+
readonly name: string;
|
|
118
|
+
readonly intervalMs: number | null;
|
|
119
|
+
readonly env?: readonly string[] | undefined;
|
|
120
|
+
readonly pull: (cursor: string | null, context: PullContext) => Promise<Pulled>;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface PullContext {
|
|
124
|
+
env: Readonly<Record<string, string>>;
|
|
125
|
+
|
|
126
|
+
// For a `resync` pull, the source's marked entities when the pull starts; empty for any other
|
|
127
|
+
// pull. Ingest removes each of these marks once the batch the pull returns is stored, so a pull
|
|
128
|
+
// that cannot read every marked entity answers `unavailable` and every mark stays.
|
|
129
|
+
marks: readonly ResyncMark[];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// An entity whose row may be behind the source, with the locator of the last fact ingest stored
|
|
133
|
+
// for it, if that fact had one.
|
|
134
|
+
export interface ResyncMark {
|
|
135
|
+
entity: EntityID;
|
|
136
|
+
locator: string | undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export type Pulled =
|
|
140
|
+
| { kind: 'batch'; batch: Batch; cursor: string; isDone: boolean }
|
|
141
|
+
| { kind: 'expired' }
|
|
142
|
+
| { kind: 'unavailable'; retryAfterMs: number }
|
|
143
|
+
| { kind: 'rejected'; reason: string };
|
|
144
|
+
|
|
145
|
+
export interface Batch {
|
|
146
|
+
facts: readonly PendingFact[];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// The body of a signed fact batch. Ingest builds each fact ID as `event:<source>:<event>`.
|
|
150
|
+
export interface FactBatchBody {
|
|
151
|
+
v: 1;
|
|
152
|
+
facts: readonly {
|
|
153
|
+
type: string;
|
|
154
|
+
event: string;
|
|
155
|
+
entity: string;
|
|
156
|
+
actor?: string | undefined;
|
|
157
|
+
at: string;
|
|
158
|
+
payload: unknown;
|
|
159
|
+
}[];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface PendingFact {
|
|
163
|
+
stream: StreamName;
|
|
164
|
+
fact: FactEnvelope<unknown>;
|
|
165
|
+
blobs: readonly { hash: BlobHash; bytes: Uint8Array }[];
|
|
166
|
+
|
|
167
|
+
// What a source's resync pull needs to read the entity again, such as an API path. Ingest keeps
|
|
168
|
+
// it beside the entity's mark, and stores it in no fact.
|
|
169
|
+
locator?: string | undefined;
|
|
170
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Kysely } from 'kysely';
|
|
2
|
+
import type { ConsumerDeclaration } from '../consumers/types.ts';
|
|
3
|
+
import type { StoreTables } from '../queries/types.ts';
|
|
4
|
+
import type { Result } from '../types.ts';
|
|
5
|
+
|
|
6
|
+
// One row of the `link` table that the host keeps in each consumer file.
|
|
7
|
+
export interface LinkRow {
|
|
8
|
+
type: string;
|
|
9
|
+
from_id: string;
|
|
10
|
+
to_id: string;
|
|
11
|
+
scope: string | null;
|
|
12
|
+
origin: string;
|
|
13
|
+
confidence: number;
|
|
14
|
+
evidence: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// The two views of every connection. They cover the attached consumers only.
|
|
18
|
+
export interface SharedViews {
|
|
19
|
+
// one row per entity of every entity view of every attached consumer
|
|
20
|
+
entity: {
|
|
21
|
+
id: string;
|
|
22
|
+
kind: string;
|
|
23
|
+
title: string;
|
|
24
|
+
url: string | null;
|
|
25
|
+
actor: string | null;
|
|
26
|
+
updated_at: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// every link of every attached consumer
|
|
30
|
+
link_effective: LinkRow;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// The host's own tables of a consumer file, reached as `<consumer>.<table>`.
|
|
34
|
+
export type HostTables<Consumers extends readonly ConsumerDeclaration[]> = {
|
|
35
|
+
[Name in Consumers[number]['name'] as `${Name}.offsets`]: {
|
|
36
|
+
stream: string;
|
|
37
|
+
last_seq_num: number;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type ConsumersDB<Consumers extends readonly ConsumerDeclaration[]> = StoreTables<Consumers> &
|
|
42
|
+
HostTables<Consumers> &
|
|
43
|
+
SharedViews;
|
|
44
|
+
|
|
45
|
+
export interface AttachedCall {
|
|
46
|
+
// the version folder each attached consumer was read from, fixed for the whole call
|
|
47
|
+
attached: Readonly<Record<string, { version: string }>>;
|
|
48
|
+
|
|
49
|
+
// the `whenCurrent` consumers that had no current version
|
|
50
|
+
skipped: readonly string[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// The work did not run: these consumers have no current version.
|
|
54
|
+
export interface StoreReaderFailure {
|
|
55
|
+
reason: 'no-current-version';
|
|
56
|
+
consumers: readonly string[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface WithConsumersOptions {
|
|
60
|
+
// More consumers to attach, each only when it has a current version. `db` has no typed table for
|
|
61
|
+
// them, and their rows show in the two shared views.
|
|
62
|
+
whenCurrent?: readonly ConsumerDeclaration[] | undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The port a query reads through. Adapters: Turso in the API, one in-memory database in the
|
|
66
|
+
// testing package.
|
|
67
|
+
export interface StoreReader {
|
|
68
|
+
// One read-only connection with the current file of each of `consumers` attached, and `work` in
|
|
69
|
+
// one transaction on it.
|
|
70
|
+
withConsumers: <const Consumers extends readonly ConsumerDeclaration[], Value>(
|
|
71
|
+
consumers: Consumers,
|
|
72
|
+
work: (db: Kysely<ConsumersDB<Consumers>>, call: AttachedCall) => Promise<Value>,
|
|
73
|
+
options?: WithConsumersOptions,
|
|
74
|
+
) => Promise<Result<Value, StoreReaderFailure>>;
|
|
75
|
+
close: () => Promise<void>;
|
|
76
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ColumnDeclaration, IndexDeclaration, TableDeclaration } from '../types.ts';
|
|
2
|
+
|
|
3
|
+
interface TableInput<Columns extends Readonly<Record<string, ColumnDeclaration>>> {
|
|
4
|
+
readonly columns: Columns;
|
|
5
|
+
readonly primaryKey: readonly NoInfer<keyof Columns & string>[];
|
|
6
|
+
readonly indexes: readonly IndexDeclaration<NoInfer<keyof Columns & string>>[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function defineTable<const Columns extends Readonly<Record<string, ColumnDeclaration>>>(
|
|
10
|
+
table: TableInput<Columns>,
|
|
11
|
+
): TableDeclaration<Columns> {
|
|
12
|
+
return table;
|
|
13
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { EntityID } from '../ids/types.ts';
|
|
2
|
+
import type { ColumnDeclaration, TableDeclaration } from '../types.ts';
|
|
3
|
+
|
|
4
|
+
interface KindTypes {
|
|
5
|
+
text: string;
|
|
6
|
+
integer: number;
|
|
7
|
+
real: number;
|
|
8
|
+
boolean: boolean;
|
|
9
|
+
json: unknown;
|
|
10
|
+
entityID: EntityID;
|
|
11
|
+
timestamp: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type ColumnValue<Column extends ColumnDeclaration> = Column['nullable'] extends true
|
|
15
|
+
? KindTypes[Column['kind']] | null
|
|
16
|
+
: KindTypes[Column['kind']];
|
|
17
|
+
|
|
18
|
+
export type TableRow<Table extends TableDeclaration> = {
|
|
19
|
+
-readonly [Name in keyof Table['columns']]: ColumnValue<Table['columns'][Name]>;
|
|
20
|
+
};
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { EntityID, FactID } from './ids/types.ts';
|
|
2
|
+
|
|
3
|
+
export type Result<Value, Failure> = { ok: true; value: Value } | { ok: false; failure: Failure };
|
|
4
|
+
|
|
5
|
+
export interface FactEnvelope<Payload> {
|
|
6
|
+
id: FactID;
|
|
7
|
+
type: string;
|
|
8
|
+
v: 1;
|
|
9
|
+
api: string;
|
|
10
|
+
entity: EntityID;
|
|
11
|
+
actor?: EntityID | undefined;
|
|
12
|
+
at: string;
|
|
13
|
+
cursor?: string | undefined;
|
|
14
|
+
payload: Payload;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// The name of a stream as a declaration writes it. A plain literal fits it, where the branded
|
|
18
|
+
// `StreamName` needs a parse.
|
|
19
|
+
export type DeclaredStreamName = `facts.${string}.${string}`;
|
|
20
|
+
|
|
21
|
+
export type ColumnKind =
|
|
22
|
+
| 'text'
|
|
23
|
+
| 'integer'
|
|
24
|
+
| 'real'
|
|
25
|
+
| 'boolean'
|
|
26
|
+
| 'json'
|
|
27
|
+
| 'entityID'
|
|
28
|
+
| 'timestamp';
|
|
29
|
+
|
|
30
|
+
export interface ColumnDeclaration {
|
|
31
|
+
readonly kind: ColumnKind;
|
|
32
|
+
readonly nullable?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface IndexDeclaration<ColumnName extends string = string> {
|
|
36
|
+
readonly columns: readonly ColumnName[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface TableDeclaration<
|
|
40
|
+
Columns extends Readonly<Record<string, ColumnDeclaration>> = Readonly<
|
|
41
|
+
Record<string, ColumnDeclaration>
|
|
42
|
+
>,
|
|
43
|
+
> {
|
|
44
|
+
readonly columns: Columns;
|
|
45
|
+
readonly primaryKey: readonly string[];
|
|
46
|
+
readonly indexes: readonly IndexDeclaration[];
|
|
47
|
+
}
|