@pepys/kit 0.0.1 → 0.1.1
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 +2 -1
- package/package.json +1 -1
- package/src/deployments/check-deployment.ts +1 -0
- package/src/deployments/types.ts +4 -2
- package/src/generators/build-payload-shape.ts +5 -0
- package/src/ids/to-stream-name.ts +7 -0
- package/src/ids/to-stream-offset.ts +6 -0
- package/src/index.ts +3 -0
- package/src/links/build-link-registry.ts +7 -0
- package/src/links/types.ts +5 -0
- package/src/queries/types.ts +17 -12
- package/src/send/types.ts +3 -3
- package/src/sources/read-fact-batch.ts +36 -5
- package/src/sources/types.ts +17 -1
package/README.md
CHANGED
|
@@ -34,7 +34,8 @@ Test the plug-in with [`@pepys/testing`](../testing/README.md), and build it wit
|
|
|
34
34
|
|
|
35
35
|
## Features
|
|
36
36
|
|
|
37
|
-
- `@pepys/kit`: the `define*` declarations and the deployment check.
|
|
37
|
+
- `@pepys/kit`: the `define*` declarations and the deployment check. `toStreamName` and
|
|
38
|
+
`toStreamOffset` give a family's `stream` and a number as the branded types that a fact log takes.
|
|
38
39
|
- `@pepys/kit/generators`: the SQL schema and glue generators.
|
|
39
40
|
- `@pepys/kit/send`: the client that signs and sends a fact batch.
|
|
40
41
|
|
package/package.json
CHANGED
package/src/deployments/types.ts
CHANGED
|
@@ -40,9 +40,11 @@ export interface Deployment<Plugins extends readonly Plugin[] = readonly Plugin[
|
|
|
40
40
|
readonly settings?: Readonly<Record<string, Readonly<Record<string, SettingValue>>>> | undefined;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
// The typed client of one plug-in's queries. A type only, so the kit needs no oRPC.
|
|
43
|
+
// The typed client of one plug-in's queries. A type only, so the kit needs no oRPC. A plug-in with
|
|
44
|
+
// no queries, written inside `defineDeployment`, takes its query type from the call's context, so
|
|
45
|
+
// a query name that is no literal gives no method rather than an index signature.
|
|
44
46
|
export type PluginClient<Target extends Plugin> = {
|
|
45
|
-
[Query in Target['queries'][number] as Query['name']]: (
|
|
47
|
+
[Query in Target['queries'][number] as string extends Query['name'] ? never : Query['name']]: (
|
|
46
48
|
input: z.input<Query['input']>,
|
|
47
49
|
) => Promise<z.output<Query['output']>>;
|
|
48
50
|
};
|
|
@@ -60,6 +60,11 @@ function buildPayloadField(name: string, schema: z.core.$ZodType): PayloadField
|
|
|
60
60
|
shape: buildPayloadShape(optional.unwrap()),
|
|
61
61
|
isOptional: true,
|
|
62
62
|
}))
|
|
63
|
+
.with(P.instanceOf(z.ZodExactOptional), (optional) => ({
|
|
64
|
+
name,
|
|
65
|
+
shape: buildPayloadShape(optional.unwrap()),
|
|
66
|
+
isOptional: true,
|
|
67
|
+
}))
|
|
63
68
|
.with(P.instanceOf(z.ZodDefault), (withDefault) => ({
|
|
64
69
|
name,
|
|
65
70
|
shape: buildPayloadShape(withDefault.unwrap()),
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { buildEnrichmentFactID } from './facts/build-enrichment-fact-id.ts';
|
|
2
|
+
export { buildLinkRegistry } from './links/build-link-registry.ts';
|
|
2
3
|
export { buildEntityID } from './entity-ids/build-entity-id.ts';
|
|
3
4
|
export { buildEventFactID } from './facts/build-event-fact-id.ts';
|
|
4
5
|
export { buildManualFactID } from './facts/build-manual-fact-id.ts';
|
|
@@ -24,6 +25,8 @@ export { parseStreamName } from './facts/parse-stream-name.ts';
|
|
|
24
25
|
export { readFactBatch } from './sources/read-fact-batch.ts';
|
|
25
26
|
export { receiveFactBatch } from './sources/receive-fact-batch.ts';
|
|
26
27
|
export type { TableRow } from './tables/table-row.ts';
|
|
28
|
+
export { toStreamName } from './ids/to-stream-name.ts';
|
|
29
|
+
export { toStreamOffset } from './ids/to-stream-offset.ts';
|
|
27
30
|
export type * from './consumers/types.ts';
|
|
28
31
|
export type * from './deployments/types.ts';
|
|
29
32
|
export type * from './ids/types.ts';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { LinkTypeDeclaration } from '../link-types/types.ts';
|
|
2
|
+
import type { LinkRegistry } from './types.ts';
|
|
3
|
+
|
|
4
|
+
// The deployment check refuses one name with two shapes, so a name maps to one declaration.
|
|
5
|
+
export function buildLinkRegistry(linkTypes: readonly LinkTypeDeclaration[]): LinkRegistry {
|
|
6
|
+
return new Map(linkTypes.map((linkType) => [linkType.name, linkType]));
|
|
7
|
+
}
|
package/src/links/types.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import type { LinkTypeDeclaration } from '../link-types/types.ts';
|
|
2
|
+
|
|
3
|
+
// Each link type of a deployment, by name.
|
|
4
|
+
export type LinkRegistry = ReadonlyMap<string, LinkTypeDeclaration>;
|
|
5
|
+
|
|
1
6
|
// What is wrong with one link that a consumer returns. The host has no deployment registry, so a
|
|
2
7
|
// type is declared by the consumer that writes it, or by nothing.
|
|
3
8
|
export type LinkProblem =
|
package/src/queries/types.ts
CHANGED
|
@@ -5,18 +5,23 @@ import type { ConsumerDeclaration } from '../consumers/types.ts';
|
|
|
5
5
|
import type { ConsumersDB } from '../store-reader/store-reader.ts';
|
|
6
6
|
import type { TableRow } from '../tables/table-row.ts';
|
|
7
7
|
|
|
8
|
-
// The Kysely tables of a set of consumers, from their declarations.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
8
|
+
// The Kysely tables of a set of consumers, from their declarations. No consumers give `unknown`,
|
|
9
|
+
// which adds no table to an intersection, where the intersection of an empty union would give
|
|
10
|
+
// `never`.
|
|
11
|
+
export type StoreTables<Consumers extends readonly ConsumerDeclaration[]> =
|
|
12
|
+
Consumers extends readonly []
|
|
13
|
+
? unknown
|
|
14
|
+
: Simplify<
|
|
15
|
+
UnionToIntersection<
|
|
16
|
+
{
|
|
17
|
+
[Index in keyof Consumers]: {
|
|
18
|
+
-readonly [Table in keyof Consumers[Index]['tables']]: TableRow<
|
|
19
|
+
Consumers[Index]['tables'][Table]
|
|
20
|
+
>;
|
|
21
|
+
};
|
|
22
|
+
}[number]
|
|
23
|
+
>
|
|
24
|
+
>;
|
|
20
25
|
|
|
21
26
|
export interface QueryCall<Input, Consumers extends readonly ConsumerDeclaration[]> {
|
|
22
27
|
input: Input;
|
package/src/send/types.ts
CHANGED
|
@@ -17,9 +17,9 @@ export type OutgoingFact<Source extends SourceDefinition> = FactBatchBody['facts
|
|
|
17
17
|
}[keyof Family['types'] & string];
|
|
18
18
|
}[Source['families'][number]['stream']];
|
|
19
19
|
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
20
|
+
// A 502, 503 or 504 answer, a timeout and no connection are a `retry`: send the same batch again,
|
|
21
|
+
// unchanged. Every other answer that is no success is `refused`, 500 included: ingest answers 500
|
|
22
|
+
// for a batch that no resend of the same bytes can store.
|
|
23
23
|
export type SendFailure =
|
|
24
24
|
| { kind: 'refused'; status: number; reason: string }
|
|
25
25
|
| { kind: 'retry'; reason: string };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { toTagged } from '@zgeoff/std';
|
|
1
|
+
import { isRecord, toTagged } from '@zgeoff/std';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { buildEventFactID } from '../facts/build-event-fact-id.ts';
|
|
4
4
|
import type { EntityID, StreamName } from '../ids/types.ts';
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
FactFamily,
|
|
10
10
|
PendingFact,
|
|
11
11
|
Received,
|
|
12
|
+
Sender,
|
|
12
13
|
SourceContext,
|
|
13
14
|
} from './types.ts';
|
|
14
15
|
|
|
@@ -47,7 +48,7 @@ export function readFactBatch(delivery: Delivery, source: SourceContext): Receiv
|
|
|
47
48
|
const facts: PendingFact[] = [];
|
|
48
49
|
|
|
49
50
|
for (const [index, fact] of body.data.facts.entries()) {
|
|
50
|
-
const built = buildPendingFact(fact, source);
|
|
51
|
+
const built = buildPendingFact(fact, source, delivery.sender);
|
|
51
52
|
|
|
52
53
|
if (!built.ok) {
|
|
53
54
|
return { kind: 'refused', status: 422, reason: `fact ${index}: ${built.failure}` };
|
|
@@ -71,7 +72,11 @@ function parseJSON(text: string): Result<unknown, null> {
|
|
|
71
72
|
|
|
72
73
|
type BatchFact = FactBatchBody['facts'][number];
|
|
73
74
|
|
|
74
|
-
function buildPendingFact(
|
|
75
|
+
function buildPendingFact(
|
|
76
|
+
fact: BatchFact,
|
|
77
|
+
source: SourceContext,
|
|
78
|
+
sender: Sender | undefined,
|
|
79
|
+
): Result<PendingFact, string> {
|
|
75
80
|
const family = findFamily(source.families, fact.type);
|
|
76
81
|
|
|
77
82
|
if (family === undefined) {
|
|
@@ -82,7 +87,13 @@ function buildPendingFact(fact: BatchFact, source: SourceContext): Result<Pendin
|
|
|
82
87
|
return { ok: false, failure: `the entity is not of the kind ${family.entityKind}` };
|
|
83
88
|
}
|
|
84
89
|
|
|
85
|
-
const
|
|
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);
|
|
86
97
|
|
|
87
98
|
if (payload?.success !== true) {
|
|
88
99
|
return { ok: false, failure: `the payload does not fit the type ${fact.type}` };
|
|
@@ -102,7 +113,7 @@ function buildPendingFact(fact: BatchFact, source: SourceContext): Result<Pendin
|
|
|
102
113
|
at: fact.at,
|
|
103
114
|
|
|
104
115
|
// the payload as sent: a schema may strip or reshape, and the log keeps what arrived
|
|
105
|
-
payload:
|
|
116
|
+
payload: sent.value,
|
|
106
117
|
},
|
|
107
118
|
blobs: [],
|
|
108
119
|
},
|
|
@@ -112,3 +123,23 @@ function buildPendingFact(fact: BatchFact, source: SourceContext): Result<Pendin
|
|
|
112
123
|
function findFamily(families: readonly FactFamily[], type: string): FactFamily | undefined {
|
|
113
124
|
return families.find((family) => Object.hasOwn(family.types, type));
|
|
114
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
|
+
}
|
package/src/sources/types.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { z } from 'zod';
|
|
2
|
-
import type { BlobHash, StreamName } from '../ids/types.ts';
|
|
2
|
+
import type { BlobHash, EntityID, StreamName } from '../ids/types.ts';
|
|
3
3
|
import type { EntityKind } from '../link-types/types.ts';
|
|
4
4
|
import type { DeclaredStreamName, FactEnvelope } from '../types.ts';
|
|
5
5
|
|
|
@@ -122,6 +122,18 @@ export interface PullPath {
|
|
|
122
122
|
|
|
123
123
|
export interface PullContext {
|
|
124
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;
|
|
125
137
|
}
|
|
126
138
|
|
|
127
139
|
export type Pulled =
|
|
@@ -151,4 +163,8 @@ export interface PendingFact {
|
|
|
151
163
|
stream: StreamName;
|
|
152
164
|
fact: FactEnvelope<unknown>;
|
|
153
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;
|
|
154
170
|
}
|