@hearthlog/ledger 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +235 -0
- package/README.md +173 -2
- package/contracts/append-hash.json +30 -0
- package/docs/database-structure.md +176 -0
- package/docs/event-store-service-design.md +124 -0
- package/docs/event-store-service.http +182 -0
- package/docs/event-store-service.openapi.yaml +1651 -0
- package/package.json +43 -3
- package/scripts/logical-backup.ts +656 -0
- package/src/action-batches.ts +65 -0
- package/src/app-registry.ts +479 -0
- package/src/auth.ts +186 -0
- package/src/contracts.ts +251 -0
- package/src/domain.ts +245 -0
- package/src/errors.ts +50 -0
- package/src/event-validation.ts +181 -0
- package/src/fence.ts +76 -0
- package/src/http.ts +656 -0
- package/src/index.ts +3 -0
- package/src/ledger-app.ts +2057 -0
- package/src/rpc.ts +42 -0
- package/src/schema.ts +236 -0
- package/src/schemas.ts +429 -0
- package/src/sqlite-export.ts +136 -0
- package/src/transfer.ts +219 -0
package/src/rpc.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LedgerError,
|
|
3
|
+
normalizeError,
|
|
4
|
+
type ErrorBody,
|
|
5
|
+
} from "./errors";
|
|
6
|
+
|
|
7
|
+
export interface RpcSuccess<T> {
|
|
8
|
+
ok: true;
|
|
9
|
+
status: number;
|
|
10
|
+
body: T;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface RpcFailure {
|
|
14
|
+
ok: false;
|
|
15
|
+
status: number;
|
|
16
|
+
body: ErrorBody;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type RpcResult<T> = RpcSuccess<T> | RpcFailure;
|
|
20
|
+
|
|
21
|
+
export function rpcSuccess<T>(body: T, status = 200): RpcSuccess<T> {
|
|
22
|
+
return { ok: true, status, body };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function rpcCall<T>(
|
|
26
|
+
operation: () => RpcSuccess<T> | Promise<RpcSuccess<T>>,
|
|
27
|
+
): Promise<RpcResult<T>> {
|
|
28
|
+
try {
|
|
29
|
+
return await operation();
|
|
30
|
+
} catch (cause) {
|
|
31
|
+
if (cause instanceof LedgerError) {
|
|
32
|
+
return {
|
|
33
|
+
ok: false,
|
|
34
|
+
status: cause.status,
|
|
35
|
+
body: cause.body,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
// Anything else is a server fault. Reporting it as invalid input would
|
|
39
|
+
// make clients permanently reject changes that a retry could accept.
|
|
40
|
+
throw normalizeError(cause);
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/schema.ts
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
const MIGRATIONS_TABLE = `
|
|
2
|
+
CREATE TABLE IF NOT EXISTS _sql_schema_migrations (
|
|
3
|
+
id INTEGER PRIMARY KEY,
|
|
4
|
+
applied_at TEXT NOT NULL
|
|
5
|
+
);
|
|
6
|
+
`;
|
|
7
|
+
|
|
8
|
+
const LEDGER_APP_SCHEMA = `
|
|
9
|
+
CREATE TABLE IF NOT EXISTS app_metadata (
|
|
10
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
11
|
+
app_id TEXT NOT NULL,
|
|
12
|
+
name TEXT NOT NULL,
|
|
13
|
+
principal_id TEXT NOT NULL,
|
|
14
|
+
principal_generation INTEGER NOT NULL,
|
|
15
|
+
created_at TEXT NOT NULL,
|
|
16
|
+
token_created_at TEXT NOT NULL,
|
|
17
|
+
revoked_at TEXT
|
|
18
|
+
);
|
|
19
|
+
CREATE TABLE IF NOT EXISTS logs (
|
|
20
|
+
id TEXT PRIMARY KEY,
|
|
21
|
+
name TEXT NOT NULL,
|
|
22
|
+
version INTEGER NOT NULL DEFAULT 0,
|
|
23
|
+
created_at TEXT NOT NULL,
|
|
24
|
+
last_updated_at TEXT
|
|
25
|
+
);
|
|
26
|
+
CREATE TABLE IF NOT EXISTS log_events (
|
|
27
|
+
log_id TEXT NOT NULL,
|
|
28
|
+
version INTEGER NOT NULL,
|
|
29
|
+
event_id TEXT NOT NULL,
|
|
30
|
+
event_type TEXT NOT NULL,
|
|
31
|
+
entity_id TEXT NOT NULL,
|
|
32
|
+
entity_version INTEGER NOT NULL,
|
|
33
|
+
event_timestamp TEXT NOT NULL,
|
|
34
|
+
payload TEXT NOT NULL,
|
|
35
|
+
PRIMARY KEY (log_id, version),
|
|
36
|
+
UNIQUE (log_id, event_id)
|
|
37
|
+
);
|
|
38
|
+
CREATE INDEX IF NOT EXISTS log_events_entity_idx
|
|
39
|
+
ON log_events(log_id, entity_id, entity_version);
|
|
40
|
+
CREATE TABLE IF NOT EXISTS entity_claims (
|
|
41
|
+
log_id TEXT NOT NULL,
|
|
42
|
+
entity_id TEXT NOT NULL,
|
|
43
|
+
current_version INTEGER NOT NULL,
|
|
44
|
+
version_created INTEGER NOT NULL,
|
|
45
|
+
created_at TEXT NOT NULL,
|
|
46
|
+
updated_at TEXT NOT NULL,
|
|
47
|
+
PRIMARY KEY (log_id, entity_id)
|
|
48
|
+
);
|
|
49
|
+
CREATE TABLE IF NOT EXISTS append_requests (
|
|
50
|
+
request_id TEXT NOT NULL,
|
|
51
|
+
log_id TEXT NOT NULL,
|
|
52
|
+
request_hash TEXT NOT NULL,
|
|
53
|
+
result TEXT NOT NULL,
|
|
54
|
+
created_at TEXT NOT NULL,
|
|
55
|
+
PRIMARY KEY (request_id, log_id)
|
|
56
|
+
);
|
|
57
|
+
CREATE TABLE IF NOT EXISTS socket_tickets (
|
|
58
|
+
nonce TEXT PRIMARY KEY,
|
|
59
|
+
log_id TEXT NOT NULL,
|
|
60
|
+
principal_id TEXT NOT NULL,
|
|
61
|
+
principal_generation INTEGER NOT NULL,
|
|
62
|
+
after_version INTEGER NOT NULL,
|
|
63
|
+
origin TEXT,
|
|
64
|
+
expires_at INTEGER NOT NULL,
|
|
65
|
+
consumed_at INTEGER
|
|
66
|
+
);
|
|
67
|
+
`;
|
|
68
|
+
|
|
69
|
+
const DROP_SOCKET_TICKETS = `
|
|
70
|
+
DROP TABLE IF EXISTS socket_tickets;
|
|
71
|
+
DROP INDEX IF EXISTS log_events_entity_idx;
|
|
72
|
+
`;
|
|
73
|
+
|
|
74
|
+
const APP_REGISTRY_SCHEMA = `
|
|
75
|
+
CREATE TABLE IF NOT EXISTS applications (
|
|
76
|
+
id TEXT PRIMARY KEY,
|
|
77
|
+
name TEXT NOT NULL UNIQUE,
|
|
78
|
+
created_at TEXT NOT NULL,
|
|
79
|
+
revoked_at TEXT,
|
|
80
|
+
active_principal_id TEXT NOT NULL,
|
|
81
|
+
principal_generation INTEGER NOT NULL,
|
|
82
|
+
active_token_created_at TEXT,
|
|
83
|
+
provisioning_state TEXT NOT NULL
|
|
84
|
+
);
|
|
85
|
+
CREATE INDEX IF NOT EXISTS applications_created_idx ON applications(created_at);
|
|
86
|
+
`;
|
|
87
|
+
|
|
88
|
+
interface Migration {
|
|
89
|
+
id: number;
|
|
90
|
+
sql: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const LEDGER_APP_MIGRATIONS: readonly Migration[] = [
|
|
94
|
+
{ id: 1, sql: LEDGER_APP_SCHEMA },
|
|
95
|
+
{ id: 2, sql: DROP_SOCKET_TICKETS },
|
|
96
|
+
{ id: 3, sql: `
|
|
97
|
+
ALTER TABLE log_events ADD COLUMN action_id TEXT;
|
|
98
|
+
ALTER TABLE log_events ADD COLUMN action_index INTEGER;
|
|
99
|
+
ALTER TABLE log_events ADD COLUMN action_size INTEGER;
|
|
100
|
+
CREATE UNIQUE INDEX log_events_action_position
|
|
101
|
+
ON log_events(log_id, action_id, action_index) WHERE action_id IS NOT NULL;
|
|
102
|
+
` },
|
|
103
|
+
{ id: 4, sql: `
|
|
104
|
+
ALTER TABLE logs ADD COLUMN source_log_id TEXT;
|
|
105
|
+
ALTER TABLE logs ADD COLUMN source_version INTEGER;
|
|
106
|
+
ALTER TABLE logs ADD COLUMN status TEXT NOT NULL DEFAULT 'ready';
|
|
107
|
+
CREATE TABLE log_fork_requests (
|
|
108
|
+
request_id TEXT PRIMARY KEY,
|
|
109
|
+
source_log_id TEXT NOT NULL,
|
|
110
|
+
target_version INTEGER NOT NULL,
|
|
111
|
+
expected_source_head_version INTEGER NOT NULL,
|
|
112
|
+
name TEXT NOT NULL,
|
|
113
|
+
fork_log_id TEXT NOT NULL UNIQUE
|
|
114
|
+
);
|
|
115
|
+
CREATE INDEX logs_source_idx ON logs(source_log_id);
|
|
116
|
+
` },
|
|
117
|
+
// Replay compatibility checks run on every read, append and socket
|
|
118
|
+
// delivery. This partial index holds only restore and removal markers.
|
|
119
|
+
{ id: 5, sql: `
|
|
120
|
+
CREATE INDEX log_events_replay_markers
|
|
121
|
+
ON log_events(log_id, event_type)
|
|
122
|
+
WHERE event_type IN ('WORKSPACE_RESTORED', 'EVENTS_EXCISED');
|
|
123
|
+
` },
|
|
124
|
+
{ id: 6, sql: `
|
|
125
|
+
CREATE TABLE gallery_photo_references (
|
|
126
|
+
log_id TEXT NOT NULL,
|
|
127
|
+
photo_id TEXT NOT NULL,
|
|
128
|
+
file_version_id TEXT NOT NULL,
|
|
129
|
+
source_version INTEGER NOT NULL,
|
|
130
|
+
PRIMARY KEY (log_id, photo_id, file_version_id)
|
|
131
|
+
);
|
|
132
|
+
CREATE INDEX gallery_photo_references_file_idx
|
|
133
|
+
ON gallery_photo_references(photo_id, file_version_id);
|
|
134
|
+
` },
|
|
135
|
+
// Clients close their stream while hidden, so live sockets alone rarely
|
|
136
|
+
// show who uses an application. The administrator console lists the most
|
|
137
|
+
// recent sessions; LedgerApp keeps this table bounded.
|
|
138
|
+
{ id: 7, sql: `
|
|
139
|
+
CREATE TABLE socket_sessions (
|
|
140
|
+
connection_id TEXT PRIMARY KEY,
|
|
141
|
+
log_id TEXT NOT NULL,
|
|
142
|
+
principal_id TEXT NOT NULL,
|
|
143
|
+
principal_generation INTEGER NOT NULL,
|
|
144
|
+
connected_at TEXT NOT NULL,
|
|
145
|
+
disconnected_at TEXT,
|
|
146
|
+
close_code INTEGER,
|
|
147
|
+
cursor INTEGER NOT NULL,
|
|
148
|
+
last_sent_at TEXT,
|
|
149
|
+
user_agent TEXT,
|
|
150
|
+
ip TEXT,
|
|
151
|
+
country TEXT,
|
|
152
|
+
colo TEXT
|
|
153
|
+
);
|
|
154
|
+
CREATE INDEX socket_sessions_connected_idx
|
|
155
|
+
ON socket_sessions(connected_at);
|
|
156
|
+
` },
|
|
157
|
+
// Hiding is a presentation choice for the log tree. A null value keeps
|
|
158
|
+
// older Workers and restored backups showing every log.
|
|
159
|
+
{ id: 8, sql: `
|
|
160
|
+
ALTER TABLE logs ADD COLUMN hidden_at TEXT;
|
|
161
|
+
` },
|
|
162
|
+
// An accepted append's events already live in log_events; its replay
|
|
163
|
+
// needs only their version range. Older rows keep the full response.
|
|
164
|
+
{ id: 9, sql: `
|
|
165
|
+
ALTER TABLE append_requests ADD COLUMN first_version INTEGER;
|
|
166
|
+
ALTER TABLE append_requests ADD COLUMN last_version INTEGER;
|
|
167
|
+
` },
|
|
168
|
+
// Who wrote an event: the identity the frontend Worker verified. Null for
|
|
169
|
+
// events accepted before this column existed or without a stamped identity;
|
|
170
|
+
// earlier code ignores the column during a rollback.
|
|
171
|
+
{ id: 10, sql: `
|
|
172
|
+
ALTER TABLE log_events ADD COLUMN actor TEXT;
|
|
173
|
+
` },
|
|
174
|
+
// What the application may append (see event-validation.ts). Null keeps
|
|
175
|
+
// the behavior applications had before policies, and earlier code ignores the column.
|
|
176
|
+
{ id: 11, sql: `
|
|
177
|
+
ALTER TABLE app_metadata ADD COLUMN event_policy TEXT;
|
|
178
|
+
` },
|
|
179
|
+
// 'importing' while an identity-preserving import fills the application;
|
|
180
|
+
// null otherwise, so earlier code never sees a value it does not know.
|
|
181
|
+
{ id: 12, sql: `
|
|
182
|
+
ALTER TABLE app_metadata ADD COLUMN import_state TEXT;
|
|
183
|
+
` },
|
|
184
|
+
// The write fence of a tenant move (see fence.ts). Null means writable, and
|
|
185
|
+
// earlier code ignores the columns.
|
|
186
|
+
{ id: 13, sql: `
|
|
187
|
+
ALTER TABLE app_metadata ADD COLUMN fence_mode TEXT;
|
|
188
|
+
ALTER TABLE app_metadata ADD COLUMN fence_destination TEXT;
|
|
189
|
+
` },
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
const APP_REGISTRY_MIGRATIONS: readonly Migration[] = [
|
|
193
|
+
{ id: 1, sql: APP_REGISTRY_SCHEMA },
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
export function migrateLedgerApp(storage: DurableObjectStorage): void {
|
|
197
|
+
migrate(storage, LEDGER_APP_MIGRATIONS);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function migrateAppRegistry(storage: DurableObjectStorage): void {
|
|
201
|
+
migrate(storage, APP_REGISTRY_MIGRATIONS);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function migrate(
|
|
205
|
+
storage: DurableObjectStorage,
|
|
206
|
+
migrations: readonly Migration[],
|
|
207
|
+
): void {
|
|
208
|
+
storage.transactionSync(() => {
|
|
209
|
+
storage.sql.exec(MIGRATIONS_TABLE);
|
|
210
|
+
const applied = new Set(
|
|
211
|
+
storage.sql
|
|
212
|
+
.exec<{ id: number }>(
|
|
213
|
+
"SELECT id FROM _sql_schema_migrations ORDER BY id",
|
|
214
|
+
)
|
|
215
|
+
.toArray()
|
|
216
|
+
.map(({ id }) => id),
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
let previousId = 0;
|
|
220
|
+
for (const migration of migrations) {
|
|
221
|
+
if (migration.id <= previousId) {
|
|
222
|
+
throw new Error("schema migrations must be strictly ordered");
|
|
223
|
+
}
|
|
224
|
+
previousId = migration.id;
|
|
225
|
+
if (applied.has(migration.id)) continue;
|
|
226
|
+
|
|
227
|
+
storage.sql.exec(migration.sql);
|
|
228
|
+
storage.sql.exec(
|
|
229
|
+
`INSERT INTO _sql_schema_migrations (id, applied_at)
|
|
230
|
+
VALUES (?, ?)`,
|
|
231
|
+
migration.id,
|
|
232
|
+
new Date().toISOString(),
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
}
|
package/src/schemas.ts
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import { t } from "elysia";
|
|
2
|
+
import type {
|
|
3
|
+
AdminConnection as AdminConnectionContract,
|
|
4
|
+
AdminConnectionsResponse as AdminConnectionsResponseContract,
|
|
5
|
+
AdminRecentConnection as AdminRecentConnectionContract,
|
|
6
|
+
} from "@hearthlog/contracts/ledger/admin";
|
|
7
|
+
|
|
8
|
+
type JsonPrimitive = null | boolean | number | string;
|
|
9
|
+
// A bounded depth keeps Cloudflare's RPC serializability inference finite.
|
|
10
|
+
type JsonValue0 = JsonPrimitive;
|
|
11
|
+
type JsonValue1 = JsonPrimitive | JsonValue0[] | Record<string, JsonValue0>;
|
|
12
|
+
type JsonValue2 = JsonPrimitive | JsonValue1[] | Record<string, JsonValue1>;
|
|
13
|
+
type JsonValue3 = JsonPrimitive | JsonValue2[] | Record<string, JsonValue2>;
|
|
14
|
+
type JsonValue4 = JsonPrimitive | JsonValue3[] | Record<string, JsonValue3>;
|
|
15
|
+
type JsonValue5 = JsonPrimitive | JsonValue4[] | Record<string, JsonValue4>;
|
|
16
|
+
type JsonValue6 = JsonPrimitive | JsonValue5[] | Record<string, JsonValue5>;
|
|
17
|
+
|
|
18
|
+
export type JsonValue = JsonValue6;
|
|
19
|
+
export type JsonObject = Record<string, JsonValue>;
|
|
20
|
+
|
|
21
|
+
const uuid = (label: string) =>
|
|
22
|
+
t.String({
|
|
23
|
+
format: "uuid",
|
|
24
|
+
error: `${label} must be a UUID`,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const jsonObject = t.Unsafe<JsonObject>(t.Record(t.String(), t.Unknown()));
|
|
28
|
+
|
|
29
|
+
const strictObject = { additionalProperties: false } as const;
|
|
30
|
+
const queryInteger = {
|
|
31
|
+
minimum: 0,
|
|
32
|
+
maximum: Number.MAX_SAFE_INTEGER,
|
|
33
|
+
multipleOf: 1,
|
|
34
|
+
} as const;
|
|
35
|
+
const queryLimit = {
|
|
36
|
+
...queryInteger,
|
|
37
|
+
minimum: 1,
|
|
38
|
+
} as const;
|
|
39
|
+
|
|
40
|
+
/** What an application may append; see `EventPolicy` in event-validation.ts. */
|
|
41
|
+
export const eventPolicyBody = t.Object(
|
|
42
|
+
{
|
|
43
|
+
contracts: t.Union([t.Array(t.String({ pattern: "^(core|[a-z0-9]+(\\.[a-z0-9-]+)+)$" }), { maxItems: 64 }), t.Null()]),
|
|
44
|
+
dynamicEvents: t.Boolean(),
|
|
45
|
+
validation: t.Union([t.Literal("report"), t.Literal("enforce"), t.Null()]),
|
|
46
|
+
},
|
|
47
|
+
strictObject,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
export const importBeginBody = t.Object(
|
|
51
|
+
{
|
|
52
|
+
id: uuid("application id"),
|
|
53
|
+
name: t.String(),
|
|
54
|
+
createdAt: t.String(),
|
|
55
|
+
policy: t.Optional(t.Union([eventPolicyBody, t.Null()])),
|
|
56
|
+
},
|
|
57
|
+
strictObject,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
/** Records are validated strictly by `parseTransferRecords`, not by the route. */
|
|
61
|
+
export const importRecordsBody = t.Object({ records: t.Array(t.Any(), { minItems: 1, maxItems: 2000 }) }, strictObject);
|
|
62
|
+
|
|
63
|
+
export const importFinishBody = t.Object(
|
|
64
|
+
{ logs: t.Array(t.Object({ id: uuid("log id"), headVersion: t.Integer({ minimum: 0 }) }, strictObject), { maxItems: 10000 }) },
|
|
65
|
+
strictObject,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
export const fenceBody = t.Object(
|
|
69
|
+
{
|
|
70
|
+
mode: t.Union([t.Literal("off"), t.Literal("tenant_migrating"), t.Literal("moved")]),
|
|
71
|
+
destination: t.Optional(t.Nullable(t.String())),
|
|
72
|
+
},
|
|
73
|
+
strictObject,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
export const createApplicationBody = t.Object(
|
|
77
|
+
{
|
|
78
|
+
name: t.String(),
|
|
79
|
+
/** Omitted: the application starts with the policy new applications get. */
|
|
80
|
+
policy: t.Optional(eventPolicyBody),
|
|
81
|
+
},
|
|
82
|
+
strictObject,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
export const applicationParams = t.Object(
|
|
86
|
+
{
|
|
87
|
+
appId: uuid("application id"),
|
|
88
|
+
},
|
|
89
|
+
strictObject,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
export const createLogBody = t.Object(
|
|
93
|
+
{
|
|
94
|
+
name: t.Optional(t.Nullable(t.String())),
|
|
95
|
+
},
|
|
96
|
+
strictObject,
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
export const logVisibilityBody = t.Object(
|
|
100
|
+
{
|
|
101
|
+
hidden: t.Boolean(),
|
|
102
|
+
},
|
|
103
|
+
strictObject,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
export const forkLogBody = t.Object(
|
|
107
|
+
{
|
|
108
|
+
name: t.String({ minLength: 1 }),
|
|
109
|
+
targetVersion: t.Integer(queryInteger),
|
|
110
|
+
expectedSourceHeadVersion: t.Integer(queryInteger),
|
|
111
|
+
requestId: uuid('fork request id'),
|
|
112
|
+
},
|
|
113
|
+
strictObject,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
export const logLineageBody = t.Object(
|
|
117
|
+
{
|
|
118
|
+
sourceLogId: uuid('source log id'),
|
|
119
|
+
sourceVersion: t.Integer(queryInteger),
|
|
120
|
+
},
|
|
121
|
+
strictObject,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
export const logParams = t.Object(
|
|
125
|
+
{
|
|
126
|
+
logId: uuid("logId"),
|
|
127
|
+
},
|
|
128
|
+
strictObject,
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
export const galleryPhotoReferenceParams = t.Object(
|
|
132
|
+
{
|
|
133
|
+
logId: uuid('log id'),
|
|
134
|
+
photoId: uuid('gallery photo id'),
|
|
135
|
+
fileVersionId: uuid('gallery file version id'),
|
|
136
|
+
},
|
|
137
|
+
strictObject,
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
export const eventsQuery = t.Object(
|
|
141
|
+
{
|
|
142
|
+
afterVersion: t.Optional(t.Numeric(queryInteger)),
|
|
143
|
+
limit: t.Optional(t.Numeric(queryLimit)),
|
|
144
|
+
},
|
|
145
|
+
strictObject,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
export const socketQuery = t.Object(
|
|
149
|
+
{
|
|
150
|
+
replayVersion: t.Optional(t.Numeric(queryInteger)),
|
|
151
|
+
afterVersion: t.Optional(t.Numeric(queryInteger)),
|
|
152
|
+
},
|
|
153
|
+
strictObject,
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
export const adminEventParams = t.Object(
|
|
157
|
+
{
|
|
158
|
+
appId: uuid("application id"),
|
|
159
|
+
logId: uuid("log id"),
|
|
160
|
+
},
|
|
161
|
+
strictObject,
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
export const adminEventsQuery = t.Object(
|
|
165
|
+
{
|
|
166
|
+
beforeVersion: t.Optional(t.Numeric({ ...queryInteger, minimum: 1 })),
|
|
167
|
+
limit: t.Optional(t.Numeric({ ...queryLimit, maximum: 100 })),
|
|
168
|
+
search: t.Optional(t.String({ maxLength: 200 })),
|
|
169
|
+
},
|
|
170
|
+
strictObject,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
export const adminMetrics = t.Object(
|
|
174
|
+
{
|
|
175
|
+
sampledAt: t.String(),
|
|
176
|
+
logCount: t.Integer({ minimum: 0 }),
|
|
177
|
+
eventCount: t.Integer({ minimum: 0 }),
|
|
178
|
+
entityCount: t.Integer({ minimum: 0 }),
|
|
179
|
+
databaseBytes: t.Integer({ minimum: 0 }),
|
|
180
|
+
connectedClients: t.Integer({ minimum: 0 }),
|
|
181
|
+
lastUpdatedAt: t.Nullable(t.String()),
|
|
182
|
+
},
|
|
183
|
+
strictObject,
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
export const adminConnectionsQuery = t.Object(
|
|
187
|
+
{ afterId: t.Optional(uuid("connection id")) },
|
|
188
|
+
strictObject,
|
|
189
|
+
);
|
|
190
|
+
const adminConnectionClient = t.Object(
|
|
191
|
+
{
|
|
192
|
+
userAgent: t.Nullable(t.String()),
|
|
193
|
+
ip: t.Nullable(t.String()),
|
|
194
|
+
country: t.Nullable(t.String()),
|
|
195
|
+
colo: t.Nullable(t.String()),
|
|
196
|
+
},
|
|
197
|
+
strictObject,
|
|
198
|
+
);
|
|
199
|
+
export const adminConnection = t.Object(
|
|
200
|
+
{
|
|
201
|
+
id: uuid("connection id"),
|
|
202
|
+
logId: uuid("log id"),
|
|
203
|
+
logName: t.Nullable(t.String()),
|
|
204
|
+
principalId: uuid("principal id"),
|
|
205
|
+
generation: t.Integer({ minimum: 1 }),
|
|
206
|
+
state: t.Union([
|
|
207
|
+
t.Literal("connecting"),
|
|
208
|
+
t.Literal("open"),
|
|
209
|
+
t.Literal("closing"),
|
|
210
|
+
t.Literal("closed"),
|
|
211
|
+
]),
|
|
212
|
+
cursor: t.Integer({ minimum: 0 }),
|
|
213
|
+
headVersion: t.Nullable(t.Integer({ minimum: 0 })),
|
|
214
|
+
connectedAt: t.Nullable(t.String()),
|
|
215
|
+
lastSentAt: t.Nullable(t.String()),
|
|
216
|
+
client: adminConnectionClient,
|
|
217
|
+
},
|
|
218
|
+
strictObject,
|
|
219
|
+
);
|
|
220
|
+
export const adminRecentConnection = t.Object(
|
|
221
|
+
{
|
|
222
|
+
id: uuid("connection id"),
|
|
223
|
+
logId: uuid("log id"),
|
|
224
|
+
logName: t.Nullable(t.String()),
|
|
225
|
+
principalId: uuid("principal id"),
|
|
226
|
+
generation: t.Integer({ minimum: 1 }),
|
|
227
|
+
cursor: t.Integer({ minimum: 0 }),
|
|
228
|
+
connectedAt: t.String(),
|
|
229
|
+
disconnectedAt: t.Nullable(t.String()),
|
|
230
|
+
closeCode: t.Nullable(t.Integer({ minimum: 0 })),
|
|
231
|
+
lastSentAt: t.Nullable(t.String()),
|
|
232
|
+
client: adminConnectionClient,
|
|
233
|
+
},
|
|
234
|
+
strictObject,
|
|
235
|
+
);
|
|
236
|
+
export const adminConnectionsResponse = t.Object(
|
|
237
|
+
{
|
|
238
|
+
sampledAt: t.String(),
|
|
239
|
+
total: t.Integer({ minimum: 0 }),
|
|
240
|
+
unavailable: t.Integer({ minimum: 0 }),
|
|
241
|
+
connections: t.Array(adminConnection),
|
|
242
|
+
nextAfterId: t.Nullable(uuid("connection id")),
|
|
243
|
+
recent: t.Array(adminRecentConnection),
|
|
244
|
+
},
|
|
245
|
+
strictObject,
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
export const incomingEvent = t.Object(
|
|
249
|
+
{
|
|
250
|
+
id: t.Optional(t.Nullable(uuid("event id"))),
|
|
251
|
+
type: t.String(),
|
|
252
|
+
timestamp: t.Optional(t.Nullable(t.Integer({ minimum: 0 }))),
|
|
253
|
+
entityId: t.String(),
|
|
254
|
+
baseEntityVersion: t.Optional(t.Nullable(t.Integer({ minimum: 0 }))),
|
|
255
|
+
payload: jsonObject,
|
|
256
|
+
},
|
|
257
|
+
strictObject,
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
export const appendEventsBody = t.Object(
|
|
261
|
+
{
|
|
262
|
+
requestId: uuid("requestId"),
|
|
263
|
+
actionId: t.Optional(uuid("actionId")),
|
|
264
|
+
expectedLogVersion: t.Optional(t.Integer({ minimum: 0 })),
|
|
265
|
+
events: t.Array(incomingEvent, { maxItems: 512 }),
|
|
266
|
+
},
|
|
267
|
+
strictObject,
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
export const storedEvent = t.Object(
|
|
271
|
+
{
|
|
272
|
+
action: t.Optional(t.Object({
|
|
273
|
+
id: uuid("action id"),
|
|
274
|
+
index: t.Integer({ minimum: 0 }),
|
|
275
|
+
size: t.Integer({ minimum: 1 }),
|
|
276
|
+
}, strictObject)),
|
|
277
|
+
version: t.Integer({ minimum: 1 }),
|
|
278
|
+
id: uuid("stored event id"),
|
|
279
|
+
type: t.String(),
|
|
280
|
+
entityId: t.String(),
|
|
281
|
+
entityVersion: t.Integer({ minimum: 1 }),
|
|
282
|
+
timestamp: t.Integer({ minimum: 0 }),
|
|
283
|
+
payload: jsonObject,
|
|
284
|
+
/** Identity the gateway verified for the request that wrote the event. */
|
|
285
|
+
actor: t.Optional(t.String({ minLength: 1, maxLength: 320 })),
|
|
286
|
+
},
|
|
287
|
+
strictObject,
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
export const logSummary = t.Object(
|
|
291
|
+
{
|
|
292
|
+
id: uuid("stored log id"),
|
|
293
|
+
name: t.String(),
|
|
294
|
+
headVersion: t.Integer({ minimum: 0 }),
|
|
295
|
+
createdAt: t.String(),
|
|
296
|
+
lastUpdatedAt: t.Nullable(t.String()),
|
|
297
|
+
sourceLogId: t.Nullable(uuid('source log id')),
|
|
298
|
+
sourceLogName: t.Nullable(t.String()),
|
|
299
|
+
sourceVersion: t.Nullable(t.Integer({ minimum: 0 })),
|
|
300
|
+
sourceTimestamp: t.Nullable(t.String()),
|
|
301
|
+
status: t.Union([t.Literal('ready'), t.Literal('preparing')]),
|
|
302
|
+
hiddenAt: t.Nullable(t.String()),
|
|
303
|
+
},
|
|
304
|
+
strictObject,
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
export const applicationSummary = t.Object(
|
|
308
|
+
{
|
|
309
|
+
id: uuid("stored application id"),
|
|
310
|
+
name: t.String(),
|
|
311
|
+
createdAt: t.String(),
|
|
312
|
+
revokedAt: t.Nullable(t.String()),
|
|
313
|
+
activeTokenCreatedAt: t.Nullable(t.String()),
|
|
314
|
+
},
|
|
315
|
+
strictObject,
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
export const apiModels = {
|
|
319
|
+
AdminConnectionsResponse: adminConnectionsResponse,
|
|
320
|
+
AdminMetricsResponse: adminMetrics,
|
|
321
|
+
AdminEventsResponse: t.Object(
|
|
322
|
+
{
|
|
323
|
+
events: t.Array(storedEvent),
|
|
324
|
+
headVersion: t.Integer({ minimum: 0 }),
|
|
325
|
+
nextBeforeVersion: t.Nullable(t.Integer({ minimum: 1 })),
|
|
326
|
+
},
|
|
327
|
+
strictObject,
|
|
328
|
+
),
|
|
329
|
+
ApplicationResponse: applicationSummary,
|
|
330
|
+
ApplicationListResponse: t.Object(
|
|
331
|
+
{
|
|
332
|
+
apps: t.Array(applicationSummary),
|
|
333
|
+
},
|
|
334
|
+
strictObject,
|
|
335
|
+
),
|
|
336
|
+
ApplicationTokenResponse: t.Object(
|
|
337
|
+
{
|
|
338
|
+
app: applicationSummary,
|
|
339
|
+
token: t.String(),
|
|
340
|
+
},
|
|
341
|
+
strictObject,
|
|
342
|
+
),
|
|
343
|
+
AppendEventsResponse: t.Object(
|
|
344
|
+
{
|
|
345
|
+
newVersion: t.Integer({ minimum: 0 }),
|
|
346
|
+
events: t.Array(storedEvent),
|
|
347
|
+
},
|
|
348
|
+
strictObject,
|
|
349
|
+
),
|
|
350
|
+
CreateLogResponse: t.Object(
|
|
351
|
+
{
|
|
352
|
+
id: uuid("stored log id"),
|
|
353
|
+
name: t.String(),
|
|
354
|
+
headVersion: t.Literal(0),
|
|
355
|
+
createdAt: t.String(),
|
|
356
|
+
},
|
|
357
|
+
strictObject,
|
|
358
|
+
),
|
|
359
|
+
EmptyResponse: t.Null(),
|
|
360
|
+
EventBatchResponse: t.Object(
|
|
361
|
+
{
|
|
362
|
+
events: t.Array(storedEvent),
|
|
363
|
+
},
|
|
364
|
+
strictObject,
|
|
365
|
+
),
|
|
366
|
+
HealthResponse: t.Null(),
|
|
367
|
+
LogListResponse: t.Object(
|
|
368
|
+
{
|
|
369
|
+
logs: t.Array(logSummary),
|
|
370
|
+
lineage: t.Object({
|
|
371
|
+
rootIds: t.Array(uuid('root log id')),
|
|
372
|
+
childIdsByLogId: t.Record(t.String(), t.Array(uuid('child log id'))),
|
|
373
|
+
}, strictObject),
|
|
374
|
+
},
|
|
375
|
+
strictObject,
|
|
376
|
+
),
|
|
377
|
+
LogResponse: logSummary,
|
|
378
|
+
ForkLogResponse: logSummary,
|
|
379
|
+
GalleryPhotoReferenceResponse: t.Object({ present: t.Boolean() }, strictObject),
|
|
380
|
+
SessionResponse: t.Object(
|
|
381
|
+
{
|
|
382
|
+
appId: uuid("appId"),
|
|
383
|
+
principalId: uuid("principalId"),
|
|
384
|
+
capabilities: t.Optional(t.Array(t.String())),
|
|
385
|
+
fence: t.Optional(t.Object({
|
|
386
|
+
mode: t.Union([t.Literal("tenant_migrating"), t.Literal("moved")]),
|
|
387
|
+
destination: t.Nullable(t.String()),
|
|
388
|
+
}, strictObject)),
|
|
389
|
+
},
|
|
390
|
+
strictObject,
|
|
391
|
+
),
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
export type CreateApplicationRequest = typeof createApplicationBody.static;
|
|
395
|
+
export type EventPolicyRequest = typeof eventPolicyBody.static;
|
|
396
|
+
export type CreateLogRequest = typeof createLogBody.static;
|
|
397
|
+
export type ForkLogRequest = typeof forkLogBody.static;
|
|
398
|
+
export type LogVisibilityRequest = typeof logVisibilityBody.static;
|
|
399
|
+
export type EventsQuery = typeof eventsQuery.static;
|
|
400
|
+
export type IncomingEvent = typeof incomingEvent.static;
|
|
401
|
+
export type AppendEventsRequest = typeof appendEventsBody.static;
|
|
402
|
+
export type StoredEvent = typeof storedEvent.static;
|
|
403
|
+
export type ApplicationSummary = typeof applicationSummary.static;
|
|
404
|
+
export type ApplicationListResponse =
|
|
405
|
+
typeof apiModels.ApplicationListResponse.static;
|
|
406
|
+
export type ApplicationTokenResponse =
|
|
407
|
+
typeof apiModels.ApplicationTokenResponse.static;
|
|
408
|
+
export type AppendEventsResponse = typeof apiModels.AppendEventsResponse.static;
|
|
409
|
+
export type CreateLogResponse = typeof apiModels.CreateLogResponse.static;
|
|
410
|
+
export type EventBatchResponse = typeof apiModels.EventBatchResponse.static;
|
|
411
|
+
export type LogSummary = typeof logSummary.static;
|
|
412
|
+
export type LogListResponse = typeof apiModels.LogListResponse.static;
|
|
413
|
+
export type SessionResponse = typeof apiModels.SessionResponse.static;
|
|
414
|
+
export type AdminMetrics = typeof adminMetrics.static;
|
|
415
|
+
export type AdminEventsQuery = typeof adminEventsQuery.static;
|
|
416
|
+
export type AdminEventsResponse = typeof apiModels.AdminEventsResponse.static;
|
|
417
|
+
export type AdminConnection = typeof adminConnection.static;
|
|
418
|
+
export type AdminRecentConnection = typeof adminRecentConnection.static;
|
|
419
|
+
export type AdminConnectionsQuery = typeof adminConnectionsQuery.static;
|
|
420
|
+
export type AdminConnectionsResponse = typeof adminConnectionsResponse.static;
|
|
421
|
+
|
|
422
|
+
// The browser's view of these responses is `@hearthlog/contracts/ledger/admin`; this fails to
|
|
423
|
+
// compile when the schemas and those types stop describing the same shape.
|
|
424
|
+
type Same<A, B> = [A] extends [B] ? ([B] extends [A] ? true : never) : never;
|
|
425
|
+
export const adminContractsMatch: [
|
|
426
|
+
Same<AdminConnection, AdminConnectionContract>,
|
|
427
|
+
Same<AdminRecentConnection, AdminRecentConnectionContract>,
|
|
428
|
+
Same<AdminConnectionsResponse, AdminConnectionsResponseContract>,
|
|
429
|
+
] = [true, true, true];
|