@valbuild/server 0.120.4 → 0.122.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/CHANGELOG.md +242 -0
- package/dist/declarations/src/Service.d.ts +15 -0
- package/dist/declarations/src/ValOps.d.ts +156 -4
- package/dist/declarations/src/ValOpsFS.d.ts +16 -2
- package/dist/declarations/src/ValOpsHttp.d.ts +186 -5
- package/dist/declarations/src/ValServer.d.ts +106 -1
- package/dist/declarations/src/externalRecords.d.ts +366 -0
- package/dist/declarations/src/fixHandlers.d.ts +13 -0
- package/dist/declarations/src/history/HistoryError.d.ts +90 -0
- package/dist/declarations/src/history/types.d.ts +126 -0
- package/dist/declarations/src/index.d.ts +3 -1
- package/dist/declarations/src/tools/types.d.ts +22 -31
- package/dist/declarations/src/valServerConfig.d.ts +17 -14
- package/dist/valbuild-server.cjs.dev.js +2704 -306
- package/dist/valbuild-server.cjs.prod.js +2704 -306
- package/dist/valbuild-server.esm.js +2701 -309
- package/package.json +4 -4
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import type { ExternalItemOf, ExternalLabelOf, ExternalReadonlyOf, ExternalRecordSrc, InferValModuleType, Json, JsonOf, ModuleFilePath, ValModule } from "@valbuild/core";
|
|
2
|
+
/**
|
|
3
|
+
* The type surface of an external record's adapter.
|
|
4
|
+
*
|
|
5
|
+
* Nothing here runs: phase 0 is the contract, and the registry that executes it
|
|
6
|
+
* arrives with the read endpoints. What the contract has to get right now is the
|
|
7
|
+
* shape, because every adapter written against it is a compatibility promise.
|
|
8
|
+
*
|
|
9
|
+
* Four kinds of method, and each is required or not for one reason:
|
|
10
|
+
*
|
|
11
|
+
* - **Read** — `keys`, `get`. Cannot be built out of anything else, so always
|
|
12
|
+
* required.
|
|
13
|
+
* - **Write** — `put`, `delete`. Likewise, and they move together: required on a
|
|
14
|
+
* writable record, forbidden on a `.readonly()` one.
|
|
15
|
+
* - **Derived** — `count`, `search`. Computable from the read methods, so
|
|
16
|
+
* omitting one costs performance, never capability. `false` declines the
|
|
17
|
+
* fallback; that is the only thing `false` ever means here.
|
|
18
|
+
* - **Media** — `putFile`, `getFile`. A pair, and required by the item SCHEMA
|
|
19
|
+
* rather than by this type (see `hasMediaSchema`).
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Brands the result envelope so a bare value can be accepted alongside it.
|
|
23
|
+
*
|
|
24
|
+
* A symbol rather than a property name because `get` returns
|
|
25
|
+
* `Record<key, Item>` — a record that can perfectly well contain a key called
|
|
26
|
+
* `kind`. The envelope never crosses the wire (it travels in-process between
|
|
27
|
+
* adapter and Val), so there is no serialization cost to it.
|
|
28
|
+
*/
|
|
29
|
+
export declare const EXTERNAL_RESULT: unique symbol;
|
|
30
|
+
export type ExternalIssue = {
|
|
31
|
+
message: string;
|
|
32
|
+
/**
|
|
33
|
+
* Whether repeating the operation could succeed.
|
|
34
|
+
*
|
|
35
|
+
* Only the adapter can tell a transient 429 from a permanent 404, so only the
|
|
36
|
+
* adapter sets this. A THROWN error is never retried: Val cannot tell a rate
|
|
37
|
+
* limit from a `TypeError`, and retrying a bug three times only fails slower.
|
|
38
|
+
*/
|
|
39
|
+
retryable?: boolean;
|
|
40
|
+
/** Names the entry when the problem is one key's, not the whole call's. */
|
|
41
|
+
key?: string;
|
|
42
|
+
cause?: unknown;
|
|
43
|
+
};
|
|
44
|
+
export type ExternalResult<T> = {
|
|
45
|
+
readonly [EXTERNAL_RESULT]: true;
|
|
46
|
+
} & ({
|
|
47
|
+
kind: "ok";
|
|
48
|
+
value: T;
|
|
49
|
+
warnings?: ExternalIssue[];
|
|
50
|
+
} | {
|
|
51
|
+
kind: "err";
|
|
52
|
+
error: ExternalIssue;
|
|
53
|
+
});
|
|
54
|
+
/**
|
|
55
|
+
* What every adapter method may return: the value on its own, or the envelope.
|
|
56
|
+
*
|
|
57
|
+
* A bare value means "ok, nothing to report", which keeps the common path one
|
|
58
|
+
* line. Errors can be thrown instead of returned — the envelope is for a failure
|
|
59
|
+
* worth classifying, or a success worth annotating.
|
|
60
|
+
*/
|
|
61
|
+
export type Returns<T> = T | ExternalResult<T>;
|
|
62
|
+
/**
|
|
63
|
+
* Wrap a value as a successful result, optionally with warnings.
|
|
64
|
+
*
|
|
65
|
+
* `NoInfer` is load-bearing, not decoration. Without it `T` is inferred FROM the
|
|
66
|
+
* argument, and an object literal that infers its own type is not contextually
|
|
67
|
+
* typed by the adapter's contract — so `title` inside `ok({ a: { title } })` is a
|
|
68
|
+
* fresh property that happens to be named `title`, with no declaration link back
|
|
69
|
+
* to `title: s.string()` in the schema. "Find all references" on the schema field
|
|
70
|
+
* then stops at the page that reads it and never reaches the adapter that
|
|
71
|
+
* produces it. `NoInfer` blocks that inference, leaving the contextual return
|
|
72
|
+
* type as the only source for `T`, which is what restores the link (and, as a
|
|
73
|
+
* bonus, makes a typo report the offending property rather than the whole
|
|
74
|
+
* return value).
|
|
75
|
+
*
|
|
76
|
+
* `externalNavigation.test.ts` guards this; nothing else would notice.
|
|
77
|
+
*
|
|
78
|
+
* The cost: `ok(...)` in a position with NO contextual type — a helper without a
|
|
79
|
+
* return type annotation — infers `unknown` instead of the argument's type, and
|
|
80
|
+
* fails where the helper is used rather than where it is written. Annotate the
|
|
81
|
+
* helper's return type, or inline it.
|
|
82
|
+
*/
|
|
83
|
+
export declare function ok<T>(value: NoInfer<T>, warnings?: ExternalIssue[]): ExternalResult<T>;
|
|
84
|
+
export declare function err(issue: ExternalIssue): ExternalResult<never>;
|
|
85
|
+
export declare function isExternalResult<T>(value: Returns<T>): value is ExternalResult<T>;
|
|
86
|
+
export type ExternalAuthor = {
|
|
87
|
+
/** Always present. */
|
|
88
|
+
id: string;
|
|
89
|
+
/**
|
|
90
|
+
* Present when Val knows it — the profile shape has it optional, so an
|
|
91
|
+
* adapter keying only on email would break for a user without one.
|
|
92
|
+
*/
|
|
93
|
+
email?: string;
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* What every adapter method is told about the call it is serving.
|
|
97
|
+
*
|
|
98
|
+
* `tx` is present only when the adapter declared an `around`; a store with no
|
|
99
|
+
* transaction seam has nothing to put there and nothing to ignore.
|
|
100
|
+
*/
|
|
101
|
+
export type ExternalCtx<Tx> = {
|
|
102
|
+
moduleFilePath: ModuleFilePath;
|
|
103
|
+
/** Who is editing. Anything richer is the adapter's to resolve. */
|
|
104
|
+
author?: ExternalAuthor;
|
|
105
|
+
/**
|
|
106
|
+
* 1 on the first call, 2 on the first retry.
|
|
107
|
+
*
|
|
108
|
+
* Not only for logging: an adapter can widen a timeout, skip a read replica
|
|
109
|
+
* that may be lagging, bypass a cache, or give up on its own terms.
|
|
110
|
+
*/
|
|
111
|
+
attempt: number;
|
|
112
|
+
} & ([Tx] extends [never] ? {
|
|
113
|
+
tx?: undefined;
|
|
114
|
+
} : {
|
|
115
|
+
tx: Tx;
|
|
116
|
+
});
|
|
117
|
+
export type ExternalKeyPage = {
|
|
118
|
+
keys: string[];
|
|
119
|
+
/** `null` means this was the last page. */
|
|
120
|
+
cursor: string | null;
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* How a page should be ordered.
|
|
124
|
+
*
|
|
125
|
+
* Records are unordered today and sorting is coming; the parameter lands now
|
|
126
|
+
* because adding one to `keys` later would break every adapter that exists by
|
|
127
|
+
* then. Val passes `undefined` until sorting ships, and an adapter may ignore it.
|
|
128
|
+
*
|
|
129
|
+
* Two rules for whoever implements it:
|
|
130
|
+
*
|
|
131
|
+
* - **A cursor is only valid for the sort that issued it.** Key-ordered paging
|
|
132
|
+
* is `where key > cursor`; sorted paging is `where (field, key) > (…, …)`.
|
|
133
|
+
* Val pairs the cursor with a hash of the sort and restarts rather than
|
|
134
|
+
* replaying a mismatched one.
|
|
135
|
+
* - **The key is always the last sort term.** Ordering by a non-unique field
|
|
136
|
+
* without a tiebreaker lets rows shift between pages, so page 2 can skip or
|
|
137
|
+
* repeat an entry while both pages look fine on their own.
|
|
138
|
+
*/
|
|
139
|
+
export type ExternalSort = {
|
|
140
|
+
/**
|
|
141
|
+
* A path into the ITEM: `["publishedAt"]`, `["author", "name"]`. Absent means
|
|
142
|
+
* order by the entry key, which is the only order there is today.
|
|
143
|
+
*/
|
|
144
|
+
path?: string[];
|
|
145
|
+
direction: "asc" | "desc";
|
|
146
|
+
};
|
|
147
|
+
export type ExternalSearchHit<Item> = {
|
|
148
|
+
key: string;
|
|
149
|
+
/**
|
|
150
|
+
* Where it matched, relative to the item. A store that only knows "this row
|
|
151
|
+
* matched" omits it; the Studio uses it to show the field, not just the row.
|
|
152
|
+
*/
|
|
153
|
+
path?: string[];
|
|
154
|
+
/**
|
|
155
|
+
* The entry as the store has it.
|
|
156
|
+
*
|
|
157
|
+
* Val applies pending patches on top before deciding the hit still stands —
|
|
158
|
+
* which is the whole reason to return it. A delegated search sees only
|
|
159
|
+
* PUBLISHED content, so without the value there is no way to drop a hit whose
|
|
160
|
+
* draft edit removed the matching text.
|
|
161
|
+
*/
|
|
162
|
+
value?: Item;
|
|
163
|
+
};
|
|
164
|
+
export type ExternalSearchPage<Item> = {
|
|
165
|
+
hits: ExternalSearchHit<Item>[];
|
|
166
|
+
cursor: string | null;
|
|
167
|
+
};
|
|
168
|
+
export type ExternalFile = {
|
|
169
|
+
/**
|
|
170
|
+
* The path Val chose — `${directory}/${createFilename(...)}` — which is the
|
|
171
|
+
* same name a local or remote file would have got.
|
|
172
|
+
*
|
|
173
|
+
* An INPUT, not something the adapter invents: the stored ref embeds this
|
|
174
|
+
* path, so a gallery can be moved back to local storage by writing the bytes
|
|
175
|
+
* where the ref already says they belong.
|
|
176
|
+
*/
|
|
177
|
+
path: string;
|
|
178
|
+
bytes: Uint8Array;
|
|
179
|
+
filename: string;
|
|
180
|
+
mimeType: string;
|
|
181
|
+
/** Content address. Make `putFile` idempotent on it, so a retry re-uses. */
|
|
182
|
+
sha256: string;
|
|
183
|
+
};
|
|
184
|
+
type AnyExternalModule = ValModule<ExternalRecordSrc>;
|
|
185
|
+
/** Read methods. Always required. */
|
|
186
|
+
type ReadMethods<Item, Tx> = {
|
|
187
|
+
keys(args: {
|
|
188
|
+
cursor: string | null;
|
|
189
|
+
limit: number;
|
|
190
|
+
sort?: ExternalSort;
|
|
191
|
+
}, ctx: ExternalCtx<Tx>): Promise<Returns<ExternalKeyPage>>;
|
|
192
|
+
get(keys: string[], ctx: ExternalCtx<Tx>): Promise<Returns<Record<string, Item | null>>>;
|
|
193
|
+
};
|
|
194
|
+
/**
|
|
195
|
+
* Write methods. Required together, and forbidden together on `.readonly()`.
|
|
196
|
+
*
|
|
197
|
+
* `put` must be an UPSERT keyed by the entry key and `delete` must tolerate an
|
|
198
|
+
* absent key, because a publish may be replayed: retry re-runs the whole scope
|
|
199
|
+
* where there is a transaction, and the individual call where there is not.
|
|
200
|
+
*/
|
|
201
|
+
type WriteMethods<Item, Tx> = {
|
|
202
|
+
put(entries: Record<string, Item>, ctx: ExternalCtx<Tx> & {
|
|
203
|
+
/**
|
|
204
|
+
* What `putFile` returned, keyed by the path it was given.
|
|
205
|
+
*
|
|
206
|
+
* Path rather than sha256 because the entry references its file by path
|
|
207
|
+
* and carries no hash — keying by hash would need a second map. The sha is
|
|
208
|
+
* a field, for an adapter doing content-addressed bookkeeping.
|
|
209
|
+
*/
|
|
210
|
+
uploads: Record<string, {
|
|
211
|
+
sha256: string;
|
|
212
|
+
data?: Json;
|
|
213
|
+
}>;
|
|
214
|
+
}): Promise<Returns<void>>;
|
|
215
|
+
delete(keys: string[], ctx: ExternalCtx<Tx>): Promise<Returns<void>>;
|
|
216
|
+
};
|
|
217
|
+
/**
|
|
218
|
+
* Named so the compiler prints the reason. A bare `never` would report only
|
|
219
|
+
* "not assignable to type 'undefined'", which tells nobody anything.
|
|
220
|
+
*/
|
|
221
|
+
export type ReadonlyRecordHasNoWrites = {
|
|
222
|
+
readonly __val_error: "this record is .readonly(): remove put and delete, or remove .readonly() from the schema";
|
|
223
|
+
};
|
|
224
|
+
/** Derived and media methods, and the sort declaration. */
|
|
225
|
+
type OptionalMethods<Item, Tx> = {
|
|
226
|
+
/**
|
|
227
|
+
* Three modes. A function delegates; `false` declines; omitted means Val
|
|
228
|
+
* counts by paging `keys` and summing, cached and bounded.
|
|
229
|
+
*/
|
|
230
|
+
count?: false | ((ctx: ExternalCtx<Tx>) => Promise<Returns<number>>);
|
|
231
|
+
/**
|
|
232
|
+
* Three modes, as `count`. Omitted, Val answers from the entries it has
|
|
233
|
+
* already seen and labels the result partial — it does not fetch a whole store
|
|
234
|
+
* on the editor's behalf. `false` declines even that.
|
|
235
|
+
*/
|
|
236
|
+
search?: false | ((query: {
|
|
237
|
+
text: string;
|
|
238
|
+
cursor: string | null;
|
|
239
|
+
limit: number;
|
|
240
|
+
sort?: ExternalSort;
|
|
241
|
+
}, ctx: ExternalCtx<Tx>) => Promise<Returns<ExternalSearchPage<Item>>>);
|
|
242
|
+
/**
|
|
243
|
+
* Which item paths this store can order by. `true` for any field. Absent
|
|
244
|
+
* means key order only — all any store can promise, and all a bucket can do.
|
|
245
|
+
*/
|
|
246
|
+
sortable?: true | string[][];
|
|
247
|
+
/**
|
|
248
|
+
* The most keys this store will list per call, and the most one `get` may
|
|
249
|
+
* carry.
|
|
250
|
+
*
|
|
251
|
+
* Chunking hints for the fetcher, NOT ceilings the UI sees: if the Studio asks
|
|
252
|
+
* for 50 and the store lists 10 at a time, Val makes five calls and returns
|
|
253
|
+
* 50. Page size belongs to the Studio, which must not be able to tell how a
|
|
254
|
+
* record is stored.
|
|
255
|
+
*/
|
|
256
|
+
maxPageSize?: number;
|
|
257
|
+
maxBatchSize?: number;
|
|
258
|
+
/**
|
|
259
|
+
* A token that changes when anything in this record changes — `max(updated_at)`,
|
|
260
|
+
* a sequence number, an ETag. Polled on the poll that already exists. Absent
|
|
261
|
+
* costs freshness, never function: Val re-reads on navigation.
|
|
262
|
+
*/
|
|
263
|
+
version?(ctx: ExternalCtx<Tx>): Promise<Returns<string>>;
|
|
264
|
+
/** Stores bytes at the path Val chose. Paired with `getFile`. */
|
|
265
|
+
putFile?(file: ExternalFile, ctx: ExternalCtx<Tx>): Promise<Returns<{
|
|
266
|
+
data?: Json;
|
|
267
|
+
}>>;
|
|
268
|
+
/** Reads them back. If an adapter can store bytes it must return them. */
|
|
269
|
+
getFile?(path: string, ctx: ExternalCtx<Tx>): Promise<Returns<Uint8Array | null>>;
|
|
270
|
+
};
|
|
271
|
+
/** The item type an external module's entries hold, loosened as JSON allows. */
|
|
272
|
+
export type ItemOfModule<M extends AnyExternalModule> = JsonOf<ExternalItemOf<InferValModuleType<M>>>;
|
|
273
|
+
/**
|
|
274
|
+
* The adapter a given external module needs.
|
|
275
|
+
*
|
|
276
|
+
* Writes are required or forbidden by the module's own `.readonly()`, read off
|
|
277
|
+
* the source marker's phantom.
|
|
278
|
+
*/
|
|
279
|
+
export type AdapterFor<M extends AnyExternalModule, Tx> = ReadMethods<ItemOfModule<M>, Tx> & OptionalMethods<ItemOfModule<M>, Tx> & (ExternalReadonlyOf<InferValModuleType<M>> extends true ? {
|
|
280
|
+
put?: ReadonlyRecordHasNoWrites;
|
|
281
|
+
delete?: ReadonlyRecordHasNoWrites;
|
|
282
|
+
} : WriteMethods<ItemOfModule<M>, Tx>);
|
|
283
|
+
declare const BoundTag: unique symbol;
|
|
284
|
+
/** What `entry()` returns: a module and its adapter, checked against each other. */
|
|
285
|
+
export type BoundExternalRecord<M> = {
|
|
286
|
+
readonly [BoundTag]: M;
|
|
287
|
+
};
|
|
288
|
+
export type ExternalRecords = {
|
|
289
|
+
readonly __brand: "ExternalRecords";
|
|
290
|
+
};
|
|
291
|
+
export type ExternalBuilder<Tx> = {
|
|
292
|
+
/**
|
|
293
|
+
* Bind an adapter to a module.
|
|
294
|
+
*
|
|
295
|
+
* A call rather than an object literal on purpose: `module` is inferred from
|
|
296
|
+
* the first argument, so `AdapterFor` RESOLVES for the second and TypeScript
|
|
297
|
+
* records the declaration link between a schema field and the adapter that
|
|
298
|
+
* produces it. Inside a single object literal the module stays a deferred type
|
|
299
|
+
* parameter, the adapter type never resolves, and "find all references" on a
|
|
300
|
+
* schema field stops reaching the adapter.
|
|
301
|
+
*/
|
|
302
|
+
entry<M extends AnyExternalModule>(module: M, adapter: AdapterFor<M, Tx>): BoundExternalRecord<M>;
|
|
303
|
+
/**
|
|
304
|
+
* Collect the bindings. The key must equal the schema's own `.external(label)`.
|
|
305
|
+
*/
|
|
306
|
+
modules<E extends Record<string, BoundExternalRecord<AnyExternalModule>>>(entries: E & {
|
|
307
|
+
[K in keyof E]: E[K] extends BoundExternalRecord<infer M extends AnyExternalModule> ? ExternalLabelOf<InferValModuleType<M>> extends K ? E[K] : BoundExternalRecord<ValModule<ExternalRecordSrc<unknown, K & string>>> : never;
|
|
308
|
+
}): ExternalRecords;
|
|
309
|
+
};
|
|
310
|
+
export type ExternalDefinition<Tx> = {
|
|
311
|
+
/**
|
|
312
|
+
* One scope per request, so a 50-key batch is one transaction and one query
|
|
313
|
+
* rather than fifty. Composes with any `withTransaction(cb)` API.
|
|
314
|
+
*
|
|
315
|
+
* Optional: a store with no transaction seam — a bucket, a REST API — simply
|
|
316
|
+
* omits it, and `ctx` then has no `tx`.
|
|
317
|
+
*
|
|
318
|
+
* Retry scope follows from this. WITH `around`, Val re-enters the whole scope,
|
|
319
|
+
* because a database aborts a transaction on its first error and answers
|
|
320
|
+
* everything after it with "current transaction is aborted". WITHOUT it, Val
|
|
321
|
+
* repeats the single operation, which is both correct and far cheaper.
|
|
322
|
+
*/
|
|
323
|
+
around?: <R>(run: (tx: Tx) => Promise<R>) => Promise<R>;
|
|
324
|
+
/**
|
|
325
|
+
* Retry policy. Return the delay in ms, or `false` to give up.
|
|
326
|
+
*
|
|
327
|
+
* A policy function, not a re-implementation of the loop. Defaults to three
|
|
328
|
+
* attempts with exponential backoff. `false` disables retries entirely.
|
|
329
|
+
*/
|
|
330
|
+
retry?: false | {
|
|
331
|
+
attempts: number;
|
|
332
|
+
backoff?: (attempt: number) => number;
|
|
333
|
+
} | ((attempt: number, issue: ExternalIssue) => number | false);
|
|
334
|
+
/** Fired after a publish, so an app that caches can purge what changed. */
|
|
335
|
+
onPublished?: (event: {
|
|
336
|
+
label: string;
|
|
337
|
+
keys: string[];
|
|
338
|
+
}) => Promise<void> | void;
|
|
339
|
+
/**
|
|
340
|
+
* How far the derived `count` walk may page before answering "200,000+".
|
|
341
|
+
* Keys only — it never fetches entry content.
|
|
342
|
+
*/
|
|
343
|
+
countPageLimit?: number;
|
|
344
|
+
};
|
|
345
|
+
/**
|
|
346
|
+
* Declare the adapters for this project's external records.
|
|
347
|
+
*
|
|
348
|
+
* `Tx` is given explicitly: `around` offers the compiler no inference site,
|
|
349
|
+
* since `run` is a callback you CALL rather than one whose signature you write.
|
|
350
|
+
* One type argument, in one place, and every inline adapter then gets `tx`,
|
|
351
|
+
* `cursor`, `limit` and the row shape correctly typed.
|
|
352
|
+
*
|
|
353
|
+
* @example
|
|
354
|
+
* const { entry, modules } = defineExternal<typeof sql>({
|
|
355
|
+
* around: (run) => sql.begin(run),
|
|
356
|
+
* });
|
|
357
|
+
*
|
|
358
|
+
* export default modules({
|
|
359
|
+
* posts: entry(postsVal, { keys, get, put, delete: del, search: false }),
|
|
360
|
+
* });
|
|
361
|
+
*
|
|
362
|
+
* @example a store with no transaction
|
|
363
|
+
* const { entry, modules } = defineExternal();
|
|
364
|
+
*/
|
|
365
|
+
export declare function defineExternal<Tx = never>(definition?: ExternalDefinition<Tx>): ExternalBuilder<Tx>;
|
|
366
|
+
export {};
|
|
@@ -164,6 +164,19 @@ export declare function handleRemoteFileCheck(): Promise<FixHandlerResult>;
|
|
|
164
164
|
export declare function handleUniqueFolderCheck(ctx: FixHandlerContext): Promise<FixHandlerResult>;
|
|
165
165
|
export declare function handleCheckAllFiles(ctx: FixHandlerContext): Promise<FixHandlerResult>;
|
|
166
166
|
export declare function handleJsonValuesExtractEntry(ctx: FixHandlerContext): Promise<FixHandlerResult>;
|
|
167
|
+
/**
|
|
168
|
+
* `external:upload` under `val validate --fix`: refuse, and say what to run.
|
|
169
|
+
*
|
|
170
|
+
* Every other fix in this registry rewrites something inside the repository —
|
|
171
|
+
* reversible, visible in a diff, wrong by at most one commit. This one would
|
|
172
|
+
* write entries into a live external store: not in a diff, not undone by
|
|
173
|
+
* `git revert`, and against production indistinguishable from an editor's
|
|
174
|
+
* publish. So a blanket `--fix` must never apply it.
|
|
175
|
+
*
|
|
176
|
+
* `fixableErrorMessage` rather than a plain error, because the error IS fixable
|
|
177
|
+
* — just not by this command.
|
|
178
|
+
*/
|
|
179
|
+
export declare function handleExternalUpload(): Promise<FixHandlerResult>;
|
|
167
180
|
export declare const currentFixHandlers: Record<Exclude<ValidationFix, "keyof:check-keys" | "router:check-route">, FixHandler>;
|
|
168
181
|
export declare const fixHandlers: Record<string, FixHandler>;
|
|
169
182
|
export declare function createDefaultValFSHost(): IValFSHost;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { ModuleFilePath } from "@valbuild/core";
|
|
2
|
+
/**
|
|
3
|
+
* Everything that can go wrong reading, replaying or restoring history.
|
|
4
|
+
*
|
|
5
|
+
* There is a lot of it, which is the point of making it one closed union rather
|
|
6
|
+
* than strings: reading a commit means reaching a content host, finding the
|
|
7
|
+
* commit, reading what was recorded for each of its modules, and asking whether
|
|
8
|
+
* this version of Val can make sense of the schema it was stored under. Each of
|
|
9
|
+
* those fails differently, and a caller deciding what to SHOW needs to know
|
|
10
|
+
* which.
|
|
11
|
+
*
|
|
12
|
+
* Every member has a producer. The union once carried five more - a module
|
|
13
|
+
* removed from the project, an op that would not replay, a value that no longer
|
|
14
|
+
* fits today's schema, a field the schema no longer defines, and ops from a
|
|
15
|
+
* core version known to replay wrongly. All five belonged to reconstructing a
|
|
16
|
+
* commit by replaying patches against a parsed source; storing the data ended
|
|
17
|
+
* that, and a member nothing can produce is a state the Studio writes handling
|
|
18
|
+
* for and never sees.
|
|
19
|
+
*
|
|
20
|
+
* ## The rule
|
|
21
|
+
*
|
|
22
|
+
* Whole-commit failures are the `err` channel. Per-module and per-patch
|
|
23
|
+
* failures ride along inside the `ok` payload.
|
|
24
|
+
*
|
|
25
|
+
* A commit that cannot be found or read at all has nothing to show. But one
|
|
26
|
+
* unparseable module out of ten does not make the other nine unreadable, and
|
|
27
|
+
* collapsing the whole view because of it would hide exactly the information
|
|
28
|
+
* someone needs to see - that this module is the broken one.
|
|
29
|
+
*/
|
|
30
|
+
export type HistoryError =
|
|
31
|
+
/** No such commit, or not one Val created. Nothing will make it readable. */
|
|
32
|
+
{
|
|
33
|
+
kind: "commit-not-found";
|
|
34
|
+
commitSha: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The commit says it has an archive and the archive is missing or malformed.
|
|
38
|
+
* Distinct from a commit that predates archiving, which is not an error.
|
|
39
|
+
*/
|
|
40
|
+
| {
|
|
41
|
+
kind: "archive-unreadable";
|
|
42
|
+
commitSha: string;
|
|
43
|
+
message: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Nothing was stored for this module at this commit - a commit made before
|
|
47
|
+
* history was recorded, or by a Val too old to send it. NOT the same as an
|
|
48
|
+
* empty module, which is why it is reported rather than defaulted.
|
|
49
|
+
*/
|
|
50
|
+
| {
|
|
51
|
+
kind: "source-unavailable";
|
|
52
|
+
moduleFilePath: ModuleFilePath;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The schema stored with this commit is not one this version of Val can read.
|
|
56
|
+
*
|
|
57
|
+
* Expected, and NOT anyone's mistake: schemas are stored as written, and Val's
|
|
58
|
+
* schema format is allowed to move. An older project opened in a newer Val -
|
|
59
|
+
* or the reverse - can hit this, and the honest thing is to say the commit
|
|
60
|
+
* cannot be shown HERE rather than to imply the data is damaged. Everything
|
|
61
|
+
* else about the commit still reads.
|
|
62
|
+
*/
|
|
63
|
+
| {
|
|
64
|
+
kind: "schema-unreadable";
|
|
65
|
+
moduleFilePath: ModuleFilePath;
|
|
66
|
+
message: string;
|
|
67
|
+
}
|
|
68
|
+
/** A binary file or `*.val.json` entry could not be read at that commit. */
|
|
69
|
+
| {
|
|
70
|
+
kind: "file-unavailable";
|
|
71
|
+
gitPath: string;
|
|
72
|
+
message: string;
|
|
73
|
+
}
|
|
74
|
+
/** History needs the content host; local FS mode has git instead. */
|
|
75
|
+
| {
|
|
76
|
+
kind: "not-supported-in-fs-mode";
|
|
77
|
+
}
|
|
78
|
+
/** Could not reach the content host at all. */
|
|
79
|
+
| {
|
|
80
|
+
kind: "transport";
|
|
81
|
+
message: string;
|
|
82
|
+
};
|
|
83
|
+
export declare function historyErrorMessage(error: HistoryError): string;
|
|
84
|
+
/**
|
|
85
|
+
* Whether an error is about the whole commit rather than one part of it.
|
|
86
|
+
*
|
|
87
|
+
* The `err`/`ok`-payload split above, as a predicate, so callers do not
|
|
88
|
+
* re-derive it and disagree.
|
|
89
|
+
*/
|
|
90
|
+
export declare function isWholeCommitError(error: HistoryError): boolean;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { JSONValue } from "@valbuild/core/patch";
|
|
2
|
+
import type { ModuleFilePath, PatchId, SerializedSchema, SourcePath } from "@valbuild/core";
|
|
3
|
+
import type { HistoryError } from "./HistoryError.js";
|
|
4
|
+
/** One commit, as history lists it. */
|
|
5
|
+
export type HistoricalCommit = {
|
|
6
|
+
commitSha: string;
|
|
7
|
+
parentCommitSha: string;
|
|
8
|
+
clientCommitSha: string;
|
|
9
|
+
branch: string;
|
|
10
|
+
createdBranch: string | null;
|
|
11
|
+
creator: string | null;
|
|
12
|
+
message: string | null;
|
|
13
|
+
createdAt: string;
|
|
14
|
+
seqNum: string;
|
|
15
|
+
patchCount: number;
|
|
16
|
+
/**
|
|
17
|
+
* Whether this commit's record was stored. False for commits made before
|
|
18
|
+
* history was recorded: their patches are still readable, but not the
|
|
19
|
+
* pre-commit sources a restore replays against.
|
|
20
|
+
*/
|
|
21
|
+
hasArchive: boolean;
|
|
22
|
+
};
|
|
23
|
+
export type CommitPage = {
|
|
24
|
+
commits: HistoricalCommit[];
|
|
25
|
+
/** Pass as `cursor` for the next page. `null` when there are no more. */
|
|
26
|
+
nextCursor: string | null;
|
|
27
|
+
};
|
|
28
|
+
export type CommitPatch = {
|
|
29
|
+
patchId: PatchId;
|
|
30
|
+
moduleFilePath: ModuleFilePath;
|
|
31
|
+
patch: unknown;
|
|
32
|
+
authorId: string | null;
|
|
33
|
+
createdAt: string;
|
|
34
|
+
baseSha: string;
|
|
35
|
+
coreVersion: string;
|
|
36
|
+
};
|
|
37
|
+
export type FileChange = "added" | "modified" | "deleted";
|
|
38
|
+
export type AffectedFile = {
|
|
39
|
+
kind: "module-source" | "json-entry" | "binary";
|
|
40
|
+
gitPath: string;
|
|
41
|
+
change: FileChange;
|
|
42
|
+
} | {
|
|
43
|
+
kind: "remote-binary";
|
|
44
|
+
ref: string;
|
|
45
|
+
change: FileChange;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* A binary file this commit touched - named, not fetched.
|
|
49
|
+
*
|
|
50
|
+
* `url` is where the bytes are IF something needs them. Nothing downloads a
|
|
51
|
+
* commit's images to show that the commit changed them; the descriptor is
|
|
52
|
+
* enough to render a row, and only an `<img src>` that actually mounts pays.
|
|
53
|
+
*/
|
|
54
|
+
export type BinaryFileRef = {
|
|
55
|
+
gitPath: string;
|
|
56
|
+
change: FileChange;
|
|
57
|
+
remote: boolean;
|
|
58
|
+
url: string;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* A module as one commit left it.
|
|
62
|
+
*
|
|
63
|
+
* Both halves are stored rather than derived. `source` is the module's data,
|
|
64
|
+
* recorded at the commit - not recovered by parsing the `.val.ts` git holds,
|
|
65
|
+
* because that is code and parsing code is best-effort and rots. `schema` is
|
|
66
|
+
* the schema the data was written against, which nothing in the current
|
|
67
|
+
* checkout has once the schema has moved on, and without which the module
|
|
68
|
+
* cannot be RENDERED as it was - only guessed at.
|
|
69
|
+
*/
|
|
70
|
+
export type HistoricalModule = {
|
|
71
|
+
/** The module's data at this commit. `null` if deleted, or unreadable — see `failures`. */
|
|
72
|
+
source: JSONValue | null;
|
|
73
|
+
/**
|
|
74
|
+
* The schema at this commit, if this version of Val can read it.
|
|
75
|
+
*
|
|
76
|
+
* `null` with a `schema-unreadable` failure means the stored schema is from a
|
|
77
|
+
* Val whose schema format this one does not know. Expected rather than
|
|
78
|
+
* broken, and reported so the Studio can say so kindly.
|
|
79
|
+
*/
|
|
80
|
+
schema: SerializedSchema | null;
|
|
81
|
+
patchIds: PatchId[];
|
|
82
|
+
/**
|
|
83
|
+
* What this commit changed here, taken from the ops themselves.
|
|
84
|
+
*
|
|
85
|
+
* The ops ARE the change, so this needs no before-state and no replay - which
|
|
86
|
+
* is what let the static parser and the patch replay go.
|
|
87
|
+
*/
|
|
88
|
+
changedPaths: SourcePath[];
|
|
89
|
+
/** Per-module problems. Collected, never thrown - see HistoryError. */
|
|
90
|
+
failures: HistoryError[];
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* A commit, reconstructed.
|
|
94
|
+
*
|
|
95
|
+
* Deliberately says nothing about the CURRENT source or schema, so it can never
|
|
96
|
+
* change for a given commit sha - which is what lets it be cached forever. The
|
|
97
|
+
* comparison against today lives in `HistoricalComparison`.
|
|
98
|
+
*/
|
|
99
|
+
export type HistoricalPatchSet = {
|
|
100
|
+
commit: HistoricalCommit;
|
|
101
|
+
modules: Record<ModuleFilePath, HistoricalModule>;
|
|
102
|
+
patches: CommitPatch[];
|
|
103
|
+
/** `*.val.json` entry contents at this commit, keyed by git path. */
|
|
104
|
+
jsonEntries: Record<string, JSONValue>;
|
|
105
|
+
binaryFiles: BinaryFileRef[];
|
|
106
|
+
/** Problems that are about the commit but did not stop it being read. */
|
|
107
|
+
warnings: HistoryError[];
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* One module's stored version, exactly as the content service returns it.
|
|
111
|
+
*
|
|
112
|
+
* `schema` is `unknown` on purpose: the content service stores it opaquely and
|
|
113
|
+
* cannot vouch for it, so it arrives unvalidated and is checked HERE, where the
|
|
114
|
+
* schema format is actually known. That check is what turns "a project written
|
|
115
|
+
* by a different Val" into a sentence rather than a crash.
|
|
116
|
+
*/
|
|
117
|
+
export type StoredModuleVersion = {
|
|
118
|
+
moduleFilePath: ModuleFilePath;
|
|
119
|
+
commitSha: string;
|
|
120
|
+
sourceSha: string | null;
|
|
121
|
+
schemaSha: string;
|
|
122
|
+
source: JSONValue | null;
|
|
123
|
+
schema: unknown;
|
|
124
|
+
/** We hold the hash but not the object. Distinct from a deleted module. */
|
|
125
|
+
unavailable: boolean;
|
|
126
|
+
};
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { createService, Service } from "./Service.js";
|
|
2
|
+
export { defineExternal, ok, err, isExternalResult, EXTERNAL_RESULT, } from "./externalRecords.js";
|
|
3
|
+
export type { AdapterFor, BoundExternalRecord, ExternalAuthor, ExternalBuilder, ExternalCtx, ExternalDefinition, ExternalFile, ExternalIssue, ExternalKeyPage, ExternalRecords, ExternalResult, ExternalSearchHit, ExternalSearchPage, ExternalSort, ItemOfModule, ReadonlyRecordHasNoWrites, Returns, } from "./externalRecords.js";
|
|
2
4
|
export { createValApiRouter, createValServer, safeReadGit } from "./ValRouter.js";
|
|
3
5
|
export { createValTools } from "./tools/index.js";
|
|
4
6
|
export { VAL_SCOPE_READ, VAL_SCOPE_WRITE, authorIdFromVerifiedSubject, } from "./tools/index.js";
|
|
@@ -15,7 +17,7 @@ export { formatSyntaxErrorTree } from "./patch/ts/syntax.js";
|
|
|
15
17
|
export { analyzeValModule } from "./patch/ts/valModule.js";
|
|
16
18
|
export type { ValModuleAnalysis } from "./patch/ts/valModule.js";
|
|
17
19
|
export { createFixPatch } from "./createFixPatch.js";
|
|
18
|
-
export { fixHandlers, currentFixHandlers, createDefaultValFSHost, handleFileMetadata, handleRemoteFileUpload, handleRemoteGalleryFileUpload, handleRemoteFileDownload, handleRemoteFileCheck, handleUniqueFolderCheck, handleCheckAllFiles, handleJsonValuesExtractEntry, } from "./fixHandlers.js";
|
|
20
|
+
export { fixHandlers, currentFixHandlers, createDefaultValFSHost, handleFileMetadata, handleRemoteFileUpload, handleRemoteGalleryFileUpload, handleRemoteFileDownload, handleRemoteFileCheck, handleUniqueFolderCheck, handleCheckAllFiles, handleJsonValuesExtractEntry, handleExternalUpload, } from "./fixHandlers.js";
|
|
19
21
|
export type { FixHandler, FixHandlerContext, FixHandlerResult, IValRemote, ValidationEvent, ValidationError, ValModule, } from "./fixHandlers.js";
|
|
20
22
|
export * from "./jwt.js";
|
|
21
23
|
export type { ValServer } from "./ValServer.js";
|