@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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,247 @@
|
|
|
1
1
|
# @valbuild/server
|
|
2
2
|
|
|
3
|
+
## 0.122.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#563](https://github.com/valbuild/val/pull/563) [`be32261`](https://github.com/valbuild/val/commit/be32261af19db8018bc37b180d903416018c0b79) Thanks [@freekh](https://github.com/freekh)! - See how a module looked at any past commit, and restore from it by pointing at it
|
|
8
|
+
|
|
9
|
+
Val could publish edits but never look back. Now every commit is a durable
|
|
10
|
+
record you can open, and any part of it can be put back.
|
|
11
|
+
|
|
12
|
+
**Open a commit and the Studio splits in two.** The left half is the Studio
|
|
13
|
+
itself — same navigation, same fields, same everything, because it _is_ the
|
|
14
|
+
editor rather than a copy of it. The right half shows the project as that commit
|
|
15
|
+
left it. On a phone the two become one pane and a toggle.
|
|
16
|
+
|
|
17
|
+
**Restoring is directed: you point at the old value, then at where it goes.**
|
|
18
|
+
Val does not try to work out which of today's fields corresponds to which of the
|
|
19
|
+
commit's. It cannot be sure — array items splice, schemas move — and a restore
|
|
20
|
+
that guesses wrong writes into the wrong place and looks like it worked. Two
|
|
21
|
+
picks leave nothing to guess. You can restore across paths, so last month's
|
|
22
|
+
headline can become today's tagline.
|
|
23
|
+
|
|
24
|
+
Before you click, every field on the "now" side says whether it can hold the
|
|
25
|
+
value you picked, and a field that cannot explains why when you click it rather
|
|
26
|
+
than doing nothing. A changed union is not itself a blocker: what matters is
|
|
27
|
+
whether the value's own shape is still allowed, so a union that gained a case
|
|
28
|
+
restores fine and one that lost the case you are restoring does not.
|
|
29
|
+
|
|
30
|
+
Rich text can be restored but is marked "probably fits" rather than confirmed —
|
|
31
|
+
comparing every mark and block against the options a schema allows is not done
|
|
32
|
+
yet, and saying so is better than a confident answer we cannot back. It is
|
|
33
|
+
checked properly the moment you commit to it: before anything is staged, the old
|
|
34
|
+
value is checked against the field it is going into, and a value that cannot be
|
|
35
|
+
that field is refused with the reason. A value that is the right shape but
|
|
36
|
+
breaks a rule about its content — a name too short for its `minLength` — is
|
|
37
|
+
staged and then held at publish, the same as if you had typed it, because a
|
|
38
|
+
restore should not be stricter than typing.
|
|
39
|
+
|
|
40
|
+
**A whole module can be put back on its own**, from a commit that changed
|
|
41
|
+
several, without reverting the rest of the commit.
|
|
42
|
+
|
|
43
|
+
**Restores are staged, not applied.** They land in pending changes, are reviewed
|
|
44
|
+
beside every other edit, and go out with the next publish. There is also "put
|
|
45
|
+
everything back", for when a whole publish was the mistake.
|
|
46
|
+
|
|
47
|
+
To make this possible, publishing now records each changed module's data and the
|
|
48
|
+
schema it was written against. Not the `.val.ts` — git already keeps that, but
|
|
49
|
+
it is code, and turning code back into data means parsing it, which is
|
|
50
|
+
best-effort and stops working as TypeScript, your runtime and Val move on. The
|
|
51
|
+
schema is kept because a value on its own cannot be drawn: showing a module as it
|
|
52
|
+
was at a commit whose schema has since changed needs _that commit's_ schema, and
|
|
53
|
+
nothing in your current checkout has it.
|
|
54
|
+
|
|
55
|
+
Things it will not pretend about: a module the commit did not touch says so
|
|
56
|
+
rather than showing today's value; a module saved by a different version of Val
|
|
57
|
+
says the version differs and that nothing is lost; a commit made before Val
|
|
58
|
+
started recording history disables restore with the reason next to it. Images
|
|
59
|
+
and files are restored by re-uploading them, since the bytes at an old commit
|
|
60
|
+
may no longer be on your branch.
|
|
61
|
+
|
|
62
|
+
History requires the Val content service. In filesystem mode it reports
|
|
63
|
+
`not-supported-in-fs-mode` rather than faking it from git, which has the files
|
|
64
|
+
but not which of a commit's changes were one editor's work.
|
|
65
|
+
|
|
66
|
+
- [#597](https://github.com/valbuild/val/pull/597) [`5d14612`](https://github.com/valbuild/val/commit/5d14612f612d657a37338136188f2b3c02b28fe7) Thanks [@freekh](https://github.com/freekh)! - MCP: remove personal access token auth. The endpoint now needs an `oauth`
|
|
67
|
+
config, or local filesystem mode.
|
|
68
|
+
|
|
69
|
+
Until now, an MCP endpoint with no `oauth` config accepted whatever bearer token
|
|
70
|
+
a caller presented and relayed it to the Val content backend unread. The
|
|
71
|
+
reasoning was that without an issuer the app has no key to check a token
|
|
72
|
+
against, so it should not pretend to be the authority on what that token may
|
|
73
|
+
do — and that much was right. The shape was not: a credential the app cannot
|
|
74
|
+
check is one it cannot refuse either, so "a deployed endpoint that authenticates
|
|
75
|
+
nobody" was a supported configuration, and an app could serve content-rewriting
|
|
76
|
+
tools without ever being told where its callers should authorize.
|
|
77
|
+
|
|
78
|
+
**If you run Val in proxy mode**, MCP now requires the `oauth` config that
|
|
79
|
+
shipped in `0.120.0`. Callers authorize as themselves against the Val
|
|
80
|
+
authorization server, this app verifies the token's signature, issuer, audience
|
|
81
|
+
and expiry itself, and patches carry the verified profile as their author:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
initValMcp(valModules, config, {
|
|
85
|
+
oauth: {
|
|
86
|
+
issuer: "https://admin.val.build",
|
|
87
|
+
resource: "https://your-app.com/api/mcp",
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Leave it out and the endpoint answers `500` naming the missing config, rather
|
|
93
|
+
than serving the request.
|
|
94
|
+
|
|
95
|
+
**If you run Val in local filesystem mode**, nothing changes. Local development
|
|
96
|
+
still needs no `oauth` config and no authorization server: there is no backend
|
|
97
|
+
to authenticate to, and patches are written with no author. A token presented
|
|
98
|
+
to such a project is still refused rather than ignored — the endpoint answers
|
|
99
|
+
`400` and says to take the credential out of the client's configuration, since
|
|
100
|
+
what it reached was a working tree with no permission check in front of it.
|
|
101
|
+
|
|
102
|
+
Two API changes if you built your own host on `createValTools`:
|
|
103
|
+
|
|
104
|
+
- `ValToolContext.auth` no longer has a `{ type: "pat", pat }` variant.
|
|
105
|
+
`{ type: "verified-profile", profileId, scopes }` is the only credential the
|
|
106
|
+
registry accepts, and `null` still means local filesystem mode.
|
|
107
|
+
- `createValOps` no longer takes an `auth` argument. `ValOpsHttp` still accepts
|
|
108
|
+
a personal access token directly — that is how `val debug` uses the token from
|
|
109
|
+
`val login` — but no server request builds one.
|
|
110
|
+
|
|
111
|
+
Proxy mode also stops keeping one data layer per credential. Each personal
|
|
112
|
+
access token needed its own `ValOpsHttp` to hold it, each of those cached the
|
|
113
|
+
project's evaluated modules, and the bounded cache that kept the memory in
|
|
114
|
+
check turned an eviction into a re-evaluation of every module on the next call.
|
|
115
|
+
Verified callers all share one instance, because they all reach the backend
|
|
116
|
+
under the app's own API key.
|
|
117
|
+
|
|
118
|
+
### Patch Changes
|
|
119
|
+
|
|
120
|
+
- [#618](https://github.com/valbuild/val/pull/618) [`da6794f`](https://github.com/valbuild/val/commit/da6794f3dbd77d49ccfe780b359bab1689ee1b11) Thanks [@freekh](https://github.com/freekh)! - Remove the unused `GET /api/val/session` endpoint.
|
|
121
|
+
|
|
122
|
+
Nothing called it. The Studio reads the profile id from `/stat`, and in proxy
|
|
123
|
+
mode the route proxied to `${VAL_BUILD_URL}/api/val/${project}/auth/session`,
|
|
124
|
+
an upstream route that no longer exists — so calling it by hand returned a 500
|
|
125
|
+
rather than a session. It is gone from both the route declarations in
|
|
126
|
+
`@valbuild/shared` and the implementation in `@valbuild/server`.
|
|
127
|
+
|
|
128
|
+
Session cookie handling itself is unchanged: `/authorize`, `/callback` and
|
|
129
|
+
`/logout` still set and clear `val_session` as before.
|
|
130
|
+
|
|
131
|
+
- Updated dependencies [[`be32261`](https://github.com/valbuild/val/commit/be32261af19db8018bc37b180d903416018c0b79), [`da6794f`](https://github.com/valbuild/val/commit/da6794f3dbd77d49ccfe780b359bab1689ee1b11), [`1c8b7fd`](https://github.com/valbuild/val/commit/1c8b7fda1e84cd8bd32a03a85d2789598b98c3fb)]:
|
|
132
|
+
- @valbuild/shared@0.122.0
|
|
133
|
+
- @valbuild/ui@0.122.0
|
|
134
|
+
|
|
135
|
+
## 0.121.0
|
|
136
|
+
|
|
137
|
+
### Minor Changes
|
|
138
|
+
|
|
139
|
+
- [#464](https://github.com/valbuild/val/pull/464) [`2bcc6fd`](https://github.com/valbuild/val/commit/2bcc6fdff8d668123e07e3c5e81ac6fa1436e47b) Thanks [@freekh](https://github.com/freekh)! - Add staging and unstaging of pending changes, so one person can publish a small fix without shipping somebody else's unfinished work.
|
|
140
|
+
|
|
141
|
+
A **patch group** is the set of patches one user has chosen to publish. It is not a patch _set_: a patch set is computed from the schema and says which patches must move together, while a patch group is curated and says which ones you want live.
|
|
142
|
+
|
|
143
|
+
A group holds its owner's own work plus whatever the closure entangled with it — not everything pending. **So Publish changes meaning on a shared branch: it ships your changes and what they depend on, instead of everything anybody has pending.** That is the feature. Unstaging goes further: hold one of your own changes back and it leaves both your preview and your publish, while still existing for everyone else.
|
|
144
|
+
|
|
145
|
+
The rule relating the two is that for every group and every patch set, the group's members within that patch set must form a prefix in patch-chain order. Staging a change therefore pulls in whatever preceded it in the same patch set; unstaging drops whatever was built on top of it. The compare view names what a toggle moves, and whose it is, rather than quietly enlarging or shrinking a publish.
|
|
146
|
+
|
|
147
|
+
Editing inside a region you are holding back is allowed, and the patches you were holding are loaded back in rather than the edit being refused. An earlier design made such a region read-only until it was staged again, because an author picks an array index while looking at their own view — so re-staging patches afterwards can shift the content under the path they just chose, and their edit lands on the wrong element cleanly, with every invariant intact and only the content wrong. That guard is not what ships. It is a rare shape in practice, since two people's edits mostly land in different routes, and refusing an edit for a reason the author cannot see is a worse everyday experience than the case it prevents. Instead the real result is shown immediately: the widened set is what the editor renders and what the compare view lists.
|
|
148
|
+
|
|
149
|
+
Also fixes a pre-existing bug in patch set grouping: patch set paths were compared with a raw string prefix test, and nothing terminates a path segment, so `?foobar/title` matched `?foo`. Deleting record key `foo` and retitling record key `foobar` were treated as one inseparable change. Previously that over-grouped two unrelated edits in the review screen; with staging it would have meant publishing a deletion nobody asked for.
|
|
150
|
+
|
|
151
|
+
The `/patches` routes gain optional patch group fields and `/patch-groups/~/patches` is new. This needs a content API that has patch groups. Filesystem mode keeps the group in the client, since it has a single author and already sends an explicit patch id list when publishing.
|
|
152
|
+
|
|
153
|
+
When a save pulls other people's changes in, you are told: a toast names how many and whose. There is no undo, because your edit was written against the view those changes produce and now depends on them — the compare view shows the widened set.
|
|
154
|
+
|
|
155
|
+
Two other things keep a session honest about a shared branch. `/stat` now says which pending changes have already been published, so another author's publish stops looking pending in your Studio the moment it lands rather than when the site redeploys. And Publish refuses, without writing anything, if somebody published while you were reviewing — the review screen you acted on described a branch that has since moved.
|
|
156
|
+
|
|
157
|
+
Two things this does **not** do yet, both of which need the group annotation to refresh on its own rather than only inside a fetch for missing patch ids:
|
|
158
|
+
|
|
159
|
+
- a stage or unstage in one tab does not reach another tab;
|
|
160
|
+
- if persisting a stage fails, the local view keeps it until the page is reloaded.
|
|
161
|
+
|
|
162
|
+
`docs/independent-publish/DESIGN.md` describes the model and lists what is still a judgement call.
|
|
163
|
+
|
|
164
|
+
- [#605](https://github.com/valbuild/val/pull/605) [`6794d29`](https://github.com/valbuild/val/commit/6794d2980bc81284ab7f2cc667f01cc21c9e3a79) Thanks [@freekh](https://github.com/freekh)! - `s.settings()`: the project's settings, as content.
|
|
165
|
+
|
|
166
|
+
A settings module is one per project, at the root of the content tree:
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
// settings.val.ts
|
|
170
|
+
export default c.define("/settings.val.ts", s.settings(), {});
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Register it in `val.modules.ts` like any other module, and it shows up in the
|
|
174
|
+
Studio under the cog at the foot of the left rail. Everything in it is content:
|
|
175
|
+
it is edited as a draft, it appears in the publish diff, and it is the same for
|
|
176
|
+
everyone working on the project.
|
|
177
|
+
|
|
178
|
+
Every key is optional, at every level, so `{}` is a complete settings module —
|
|
179
|
+
and stays one as sections are added. What it holds today is the assistant:
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
export default c.define("/settings.val.ts", s.settings(), {
|
|
183
|
+
assistant: {
|
|
184
|
+
enabled: true,
|
|
185
|
+
context: "A CMS for developers, run by a team of four in Oslo.",
|
|
186
|
+
tone: "Plain and direct. British English, sentence case in headings.",
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
`context` is background the assistant would otherwise guess at; `tone` is how it
|
|
192
|
+
should write when it writes content. Both are sent with every message it makes.
|
|
193
|
+
|
|
194
|
+
`enabled` decides whether editors have an assistant, and it has **three** states
|
|
195
|
+
rather than two:
|
|
196
|
+
|
|
197
|
+
- `true` — they do.
|
|
198
|
+
- `false` — they do not, and every trace of it goes: no button in the top bar,
|
|
199
|
+
no row in the quick actions, no panel, nothing sent.
|
|
200
|
+
- unset — nobody has decided. The assistant is still **shown**, and asks to be
|
|
201
|
+
turned on before it is used. Hiding an assistant nobody has decided about
|
|
202
|
+
means nobody discovers it; quietly enabling one means a project starts sending
|
|
203
|
+
its content to a model because it did not know to say no.
|
|
204
|
+
|
|
205
|
+
A project with no settings module at all has an assistant, as before: there is
|
|
206
|
+
nowhere to record a decision, and nowhere for the prompt to write the answer.
|
|
207
|
+
|
|
208
|
+
**Breaking: `ai.chat` is gone from `val.config.ts`.** Whether the assistant is
|
|
209
|
+
available is a decision about the project's content, made by the people who edit
|
|
210
|
+
it, so it moved to settings — turning the chat on used to take a developer, a
|
|
211
|
+
deploy and a code review of a boolean. Remove the whole block:
|
|
212
|
+
|
|
213
|
+
```diff
|
|
214
|
+
const { s, c, val, config } = initVal({
|
|
215
|
+
- ai: {
|
|
216
|
+
- chat: {
|
|
217
|
+
- experimental: { enable: true },
|
|
218
|
+
- suggestions: ["Summarize", "Fix typos at this page"],
|
|
219
|
+
- title: "Ask me anything",
|
|
220
|
+
- description: "Val can answer questions about the content.",
|
|
221
|
+
- },
|
|
222
|
+
- },
|
|
223
|
+
});
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
`experimental.enable` becomes `assistant.enabled` in the settings module.
|
|
227
|
+
`suggestions`, `title` and `description` are removed with nothing replacing
|
|
228
|
+
them: the assistant now opens with its own copy. A project that had the chat
|
|
229
|
+
enabled and wants it to stay on for everyone should write
|
|
230
|
+
`assistant: { enabled: true }` — otherwise editors are offered it and asked.
|
|
231
|
+
|
|
232
|
+
`ai.commitMessages` stays in `val.config.ts`, and is unaffected.
|
|
233
|
+
|
|
234
|
+
Two settings modules, or one in a subdirectory, is a module error: the dev
|
|
235
|
+
server refuses to serve sources, `npx val validate` reports it against the file,
|
|
236
|
+
and the Studio says so rather than picking one.
|
|
237
|
+
|
|
238
|
+
### Patch Changes
|
|
239
|
+
|
|
240
|
+
- Updated dependencies [[`105479b`](https://github.com/valbuild/val/commit/105479b84a08846f1fe5971916f6a54275198d12), [`55ec736`](https://github.com/valbuild/val/commit/55ec73651394908b6f440e360d181b95a91c0a93), [`2bcc6fd`](https://github.com/valbuild/val/commit/2bcc6fdff8d668123e07e3c5e81ac6fa1436e47b), [`2bcbee1`](https://github.com/valbuild/val/commit/2bcbee1be682c2bbd5b7bc7d152ddd4204162fd2), [`6794d29`](https://github.com/valbuild/val/commit/6794d2980bc81284ab7f2cc667f01cc21c9e3a79), [`2db27d5`](https://github.com/valbuild/val/commit/2db27d555441bee2dd31817acc8c92b7b718ee55)]:
|
|
241
|
+
- @valbuild/ui@0.121.0
|
|
242
|
+
- @valbuild/shared@0.121.0
|
|
243
|
+
- @valbuild/core@0.121.0
|
|
244
|
+
|
|
3
245
|
## 0.120.4
|
|
4
246
|
|
|
5
247
|
### Patch Changes
|
|
@@ -14,6 +14,21 @@ export declare class Service {
|
|
|
14
14
|
* The module file paths that are registered in the project's val.modules.
|
|
15
15
|
*/
|
|
16
16
|
getModuleFilePaths(): ModuleFilePath[];
|
|
17
|
+
/**
|
|
18
|
+
* Everything that is wrong with how the project's modules are DECLARED, as
|
|
19
|
+
* opposed to what is in them.
|
|
20
|
+
*
|
|
21
|
+
* A module that failed to load is here, and so is a rule that spans the whole
|
|
22
|
+
* set — "one settings module, at the root" cannot be checked while looking at
|
|
23
|
+
* a single module, so `extractValModules` appends it with the offending path.
|
|
24
|
+
* `get` only surfaces these when the module is missing entirely, which a
|
|
25
|
+
* misplaced settings module is not: `val validate` reads them from here and
|
|
26
|
+
* reports them against the file.
|
|
27
|
+
*/
|
|
28
|
+
getModuleErrors(): {
|
|
29
|
+
message: string;
|
|
30
|
+
path?: ModuleFilePath;
|
|
31
|
+
}[];
|
|
17
32
|
private serializedSchemaOf;
|
|
18
33
|
get(moduleFilePath: ModuleFilePath, modulePath: ModulePath, options?: {
|
|
19
34
|
validate: boolean;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { MediaSource, FileMetadata, FileSource, ImageMetadata, ModuleFilePath, PatchId, Schema, SelectorSource, SerializedSchema, Source, SourcePath, ValConfig, ValModules, ValidationError } from "@valbuild/core";
|
|
2
2
|
import { result } from "@valbuild/core/fp";
|
|
3
3
|
import { JSONValue, ParentRef, Patch, PatchError } from "@valbuild/core/patch";
|
|
4
|
+
import type { HistoryError } from "./history/HistoryError.js";
|
|
5
|
+
import type { AffectedFile, StoredModuleVersion, CommitPage, CommitPatch, HistoricalCommit } from "./history/types.js";
|
|
4
6
|
import { ValSyntaxError, ValSyntaxErrorTree } from "./patch/ts/syntax.js";
|
|
5
7
|
import { ParentPatchId } from "@valbuild/core";
|
|
6
8
|
import type { ReifiedPreview } from "@valbuild/core";
|
|
@@ -220,8 +222,11 @@ export declare abstract class ValOps {
|
|
|
220
222
|
* in-flight client patches the server has not seen) must pass
|
|
221
223
|
* `applyPatches: false` or the same edits would be applied twice.
|
|
222
224
|
*/
|
|
223
|
-
getJsonEntry(moduleFilePath: ModuleFilePath, entryKey: string,
|
|
225
|
+
getJsonEntry(moduleFilePath: ModuleFilePath, entryKey: string,
|
|
226
|
+
/** Passed straight through — see {@link getJsonEntries}. */
|
|
227
|
+
opts?: {
|
|
224
228
|
applyPatches?: boolean;
|
|
229
|
+
patchIds?: PatchId[];
|
|
225
230
|
}): Promise<{
|
|
226
231
|
status: "success";
|
|
227
232
|
content: JSONValue | null;
|
|
@@ -260,6 +265,15 @@ export declare abstract class ValOps {
|
|
|
260
265
|
limit: number;
|
|
261
266
|
}, opts?: {
|
|
262
267
|
applyPatches?: boolean;
|
|
268
|
+
/**
|
|
269
|
+
* Only these pending patches, or every one when `undefined`.
|
|
270
|
+
*
|
|
271
|
+
* A draft render is scoped to the caller's own groups, and a page renders
|
|
272
|
+
* `jsonValues` entries beside module content — so without this the two
|
|
273
|
+
* halves of one page disagreed about whose unpublished work they showed.
|
|
274
|
+
* `undefined` is what every other caller passes and must keep getting.
|
|
275
|
+
*/
|
|
276
|
+
patchIds?: PatchId[];
|
|
263
277
|
}): Promise<{
|
|
264
278
|
status: "success";
|
|
265
279
|
entries: {
|
|
@@ -362,10 +376,25 @@ export declare abstract class ValOps {
|
|
|
362
376
|
readProjectFile(path: string): Promise<WithGenericError<{
|
|
363
377
|
data: string;
|
|
364
378
|
}>>;
|
|
365
|
-
createPatch(path: ModuleFilePath, patch: Patch, patchId: PatchId, parentRef: ParentRef, sessionId: string | null, authorId: AuthorId | null
|
|
379
|
+
createPatch(path: ModuleFilePath, patch: Patch, patchId: PatchId, parentRef: ParentRef, sessionId: string | null, authorId: AuthorId | null,
|
|
380
|
+
/**
|
|
381
|
+
* Which patch group this patch joins, recorded in the SAME request.
|
|
382
|
+
*
|
|
383
|
+
* Atomic on purpose. The content API runs every refusal before its insert,
|
|
384
|
+
* so an invalid closure is a 400 with nothing written. Recording membership
|
|
385
|
+
* in a second call would let a patch exist outside its author's group if
|
|
386
|
+
* that call failed — and a patch outside your own group is one you cannot
|
|
387
|
+
* publish until a repair puts it back.
|
|
388
|
+
*
|
|
389
|
+
* Optional: `fs` mode has no groups, and a client that predates them sends
|
|
390
|
+
* nothing.
|
|
391
|
+
*/
|
|
392
|
+
patchGroup?: PatchGroupMembership): Promise<result.Result<{
|
|
366
393
|
error?: undefined;
|
|
367
394
|
patchId: PatchId;
|
|
368
395
|
createdAt: string;
|
|
396
|
+
/** See {@link SaveSourceFilePatchResult} — absent where there are no groups. */
|
|
397
|
+
patchGroupId?: string;
|
|
369
398
|
}, {
|
|
370
399
|
errorType: "other";
|
|
371
400
|
error: GenericErrorMessage;
|
|
@@ -377,14 +406,36 @@ export declare abstract class ValOps {
|
|
|
377
406
|
patchIds?: PatchId[];
|
|
378
407
|
excludePatchOps: ExcludePatchOps;
|
|
379
408
|
}): Promise<ExcludePatchOps extends true ? OrderedPatchesMetadata : OrderedPatches>;
|
|
380
|
-
protected abstract saveSourceFilePatch(path: ModuleFilePath, patch: Patch, patchId: PatchId, parentRef: ParentRef | null, authorId: AuthorId | null, sessionId: string | null): Promise<SaveSourceFilePatchResult>;
|
|
409
|
+
protected abstract saveSourceFilePatch(path: ModuleFilePath, patch: Patch, patchId: PatchId, parentRef: ParentRef | null, authorId: AuthorId | null, sessionId: string | null, patchGroup?: PatchGroupMembership): Promise<SaveSourceFilePatchResult>;
|
|
381
410
|
protected abstract getSourceFile(path: string): Promise<WithGenericError<{
|
|
382
411
|
data: string;
|
|
383
412
|
}>>;
|
|
413
|
+
/**
|
|
414
|
+
* Save a patch's binary file from a `data:...;base64,...` URL.
|
|
415
|
+
*
|
|
416
|
+
* The wire form: `FileReader.readAsDataURL` is what the browser produces, and
|
|
417
|
+
* published `@valbuild/server` versions send it. Code that already HAS bytes
|
|
418
|
+
* should call {@link saveBinaryFileFromPatch} instead of wrapping them in a
|
|
419
|
+
* data URL just to have this unwrap them again.
|
|
420
|
+
*
|
|
421
|
+
* A `null` `data` records a DELETION, which is why this cannot simply be
|
|
422
|
+
* replaced by the byte-taking sibling: there is nothing to hand it.
|
|
423
|
+
*/
|
|
384
424
|
abstract saveBase64EncodedBinaryFileFromPatch(filePath: string, parentRef: ParentRef, patchId: PatchId, data: string | null, type: "file" | "image", metadata: MetadataOfType<"file" | "image"> | undefined): Promise<WithGenericError<{
|
|
385
425
|
patchId: PatchId;
|
|
386
426
|
filePath: string;
|
|
387
427
|
}>>;
|
|
428
|
+
/**
|
|
429
|
+
* The same, for a caller that already has the bytes.
|
|
430
|
+
*
|
|
431
|
+
* Default implementation wraps them back into a data URL so every backend
|
|
432
|
+
* gets this for free; a backend that can take bytes straight through should
|
|
433
|
+
* override it.
|
|
434
|
+
*/
|
|
435
|
+
saveBinaryFileFromPatch(filePath: string, parentRef: ParentRef, patchId: PatchId, bytes: Buffer, mimeType: string, type: "file" | "image", metadata: MetadataOfType<"file" | "image"> | undefined): Promise<WithGenericError<{
|
|
436
|
+
patchId: PatchId;
|
|
437
|
+
filePath: string;
|
|
438
|
+
}>>;
|
|
388
439
|
abstract getBase64EncodedBinaryFileFromPatch(filePath: string, patchId: PatchId, remote: boolean): Promise<Buffer | null>;
|
|
389
440
|
protected abstract getBase64EncodedBinaryFileMetadataFromPatch<T extends "file" | "image">(filePath: string, type: T, patchId: PatchId, remote: boolean): Promise<OpsMetadata<T>>;
|
|
390
441
|
abstract getBinaryFile(filePathOrRef: string): Promise<Buffer | null>;
|
|
@@ -401,6 +452,40 @@ export declare abstract class ValOps {
|
|
|
401
452
|
errors?: undefined;
|
|
402
453
|
deleted?: undefined;
|
|
403
454
|
}>;
|
|
455
|
+
/** One page of a branch's commits, newest first. See history/listCommits. */
|
|
456
|
+
abstract listCommits(branch: string, options?: {
|
|
457
|
+
limit?: number;
|
|
458
|
+
cursor?: string;
|
|
459
|
+
}): Promise<result.Result<CommitPage, HistoryError>>;
|
|
460
|
+
/** The patches that produced one commit, with their ops. */
|
|
461
|
+
abstract getCommitPatches(commitSha: string): Promise<result.Result<{
|
|
462
|
+
commit: HistoricalCommit;
|
|
463
|
+
patches: CommitPatch[];
|
|
464
|
+
}, HistoryError>>;
|
|
465
|
+
/**
|
|
466
|
+
* How each `.val.ts` the commit changed looked BEFORE it, keyed by module
|
|
467
|
+
* file path. Empty for a commit made before this was recorded - which the
|
|
468
|
+
* caller reports as `source-unavailable` rather than as an empty module.
|
|
469
|
+
*/
|
|
470
|
+
/**
|
|
471
|
+
* Each module a commit changed: its data, and the schema it was under.
|
|
472
|
+
*
|
|
473
|
+
* `asOf` widens it from "what this commit changed" to "the whole project as
|
|
474
|
+
* this commit left it", which is what reverting everything to a point in time
|
|
475
|
+
* needs; `moduleFilePath` narrows it to one module, for navigating the
|
|
476
|
+
* history pane off the changed set.
|
|
477
|
+
*/
|
|
478
|
+
abstract getCommitModules(commitSha: string, options?: {
|
|
479
|
+
asOf?: boolean;
|
|
480
|
+
moduleFilePath?: ModuleFilePath;
|
|
481
|
+
}): Promise<result.Result<{
|
|
482
|
+
modules: StoredModuleVersion[];
|
|
483
|
+
complete: boolean;
|
|
484
|
+
}, HistoryError>>;
|
|
485
|
+
/** Which files the commit touched, and how. Names them; does not fetch them. */
|
|
486
|
+
abstract getCommitAffectedFiles(commitSha: string): Promise<result.Result<AffectedFile[], HistoryError>>;
|
|
487
|
+
/** One file's bytes as they were at one commit. */
|
|
488
|
+
abstract getFileAtCommit(commitSha: string, filePath: string, remote: boolean): Promise<result.Result<Buffer, HistoryError>>;
|
|
404
489
|
}
|
|
405
490
|
export type WithGenericError<T extends Record<string, unknown>> = (T & {
|
|
406
491
|
error?: undefined;
|
|
@@ -414,8 +499,37 @@ export type GenericErrorMessage = {
|
|
|
414
499
|
message: string;
|
|
415
500
|
details?: unknown;
|
|
416
501
|
};
|
|
502
|
+
/**
|
|
503
|
+
* The patch group a newly created patch joins.
|
|
504
|
+
*
|
|
505
|
+
* `withPatchIds` is the CLOSURE the client computed — the patches that share
|
|
506
|
+
* a patch set with this one and must move with it. It is not derived here and
|
|
507
|
+
* must not be: the closure needs the content schema, and the service that
|
|
508
|
+
* stores groups does not have it. One implementation of that rule, on the side
|
|
509
|
+
* that can actually compute it.
|
|
510
|
+
*
|
|
511
|
+
* Membership rows are stamped with `coreVersion` on the content side, the same
|
|
512
|
+
* stamp the patch row itself carries, so which client wrote a row stays legible
|
|
513
|
+
* after the fact.
|
|
514
|
+
*/
|
|
515
|
+
export type PatchGroupMembership = {
|
|
516
|
+
/**
|
|
517
|
+
* Absent means "the author's open group, created if absent" — the content API
|
|
518
|
+
* resolves it. The client does not hold an id across publishes, because a
|
|
519
|
+
* published group is refused and the stale id would lose the write.
|
|
520
|
+
*/
|
|
521
|
+
patchGroupId?: string;
|
|
522
|
+
withPatchIds: PatchId[];
|
|
523
|
+
};
|
|
417
524
|
export type SaveSourceFilePatchResult = result.Result<{
|
|
418
525
|
patchId: PatchId;
|
|
526
|
+
/**
|
|
527
|
+
* The group the patch ended up in, where the store has groups at all.
|
|
528
|
+
*
|
|
529
|
+
* Absent in `fs` mode and against a content API that predates groups. The
|
|
530
|
+
* client uses it to learn the id of the group its own first write created.
|
|
531
|
+
*/
|
|
532
|
+
patchGroupId?: string;
|
|
419
533
|
}, ({
|
|
420
534
|
errorType: "other";
|
|
421
535
|
} & GenericErrorMessage) | {
|
|
@@ -477,6 +591,15 @@ export type PreparedCommit = {
|
|
|
477
591
|
* Previous source files that were patched
|
|
478
592
|
*/
|
|
479
593
|
previousSourceFiles: Record<ModuleFilePath, string>;
|
|
594
|
+
/**
|
|
595
|
+
* Each changed module's Source after this commit, and its schema.
|
|
596
|
+
*
|
|
597
|
+
* This is what makes a commit restorable. See the comment where it is built.
|
|
598
|
+
*/
|
|
599
|
+
moduleVersions: Record<ModuleFilePath, {
|
|
600
|
+
source: JSONValue | null;
|
|
601
|
+
schema: SerializedSchema;
|
|
602
|
+
}>;
|
|
480
603
|
/**
|
|
481
604
|
* Diagnosis only: what the source file looks like with the appliable patches
|
|
482
605
|
* applied, for modules that had at least one unappliable patch. Populated
|
|
@@ -523,6 +646,36 @@ export type PatchReadError = {
|
|
|
523
646
|
parentPatchId: ParentPatchId;
|
|
524
647
|
message: string;
|
|
525
648
|
};
|
|
649
|
+
/**
|
|
650
|
+
* The patches a json entry render should apply, out of the whole chain.
|
|
651
|
+
*
|
|
652
|
+
* Three rules, and the second is the one that was missing. A draft page renders
|
|
653
|
+
* `jsonValues` entries beside module content, and only the modules were scoped
|
|
654
|
+
* — so one screen showed the caller's own view for its modules and base plus
|
|
655
|
+
* EVERY pending patch on the branch for the entries beside them, including
|
|
656
|
+
* another author's half-finished edit rendered as though it were live.
|
|
657
|
+
*
|
|
658
|
+
* 1. this module's, since the chain is branch-wide;
|
|
659
|
+
* 2. this caller's, when they asked to be scoped. `undefined` is "everything",
|
|
660
|
+
* which is what every unscoped caller gets and must keep getting;
|
|
661
|
+
* 3. not already applied — a fact about this path rather than about scoping,
|
|
662
|
+
* and true with or without a scope.
|
|
663
|
+
*
|
|
664
|
+
* Filtered here rather than by asking `fetchPatches` for a list, and that is
|
|
665
|
+
* load-bearing: both implementations read an empty `patchIds` as "no filter"
|
|
666
|
+
* and return the whole chain. That is the right default for a caller that
|
|
667
|
+
* cannot mean "none", and the most dangerous possible reading of a group that
|
|
668
|
+
* is genuinely empty — it would render every unpublished patch on the branch
|
|
669
|
+
* instead of base. It costs no round trip either: the whole chain is what the
|
|
670
|
+
* unscoped path fetches anyway.
|
|
671
|
+
*/
|
|
672
|
+
export declare function scopedModulePatches<T extends {
|
|
673
|
+
path: ModuleFilePath;
|
|
674
|
+
patchId: PatchId;
|
|
675
|
+
appliedAt: {
|
|
676
|
+
commitSha: CommitSha;
|
|
677
|
+
} | null;
|
|
678
|
+
}>(patches: T[], moduleFilePath: ModuleFilePath, patchIds: PatchId[] | undefined): T[];
|
|
526
679
|
export type OrderedPatches = {
|
|
527
680
|
patches: {
|
|
528
681
|
path: ModuleFilePath;
|
|
@@ -554,6 +707,5 @@ export type OrderedPatchesMetadata = {
|
|
|
554
707
|
};
|
|
555
708
|
export declare function getFieldsForType<T extends BinaryFileType>(type: T): (keyof MetadataOfType<T> & string)[];
|
|
556
709
|
export declare function createMetadataFromBuffer<T extends BinaryFileType>(type: BinaryFileType, mimeType: string, buffer: Buffer): OpsMetadata<T>;
|
|
557
|
-
export declare function getMimeTypeFromBase64(content: string): string | null;
|
|
558
710
|
export declare function guessMimeTypeFromPath(filePath: string): string | null;
|
|
559
711
|
export declare function bufferFromDataUrl(dataUrl: string): Buffer | undefined;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { PatchId, ModuleFilePath, ValModules } from "@valbuild/core";
|
|
2
|
-
import { AuthorId, BaseSha, BinaryFileType, GenericErrorMessage, MetadataOfType, OpsMetadata, PreparedCommit, ValOps, ValOpsOptions, WithGenericError, SaveSourceFilePatchResult, SchemaSha, CommitSha, OrderedPatches, OrderedPatchesMetadata, SourcesSha } from "./ValOps.js";
|
|
2
|
+
import { AuthorId, BaseSha, BinaryFileType, GenericErrorMessage, MetadataOfType, OpsMetadata, PreparedCommit, ValOps, ValOpsOptions, WithGenericError, SaveSourceFilePatchResult, type PatchGroupMembership, SchemaSha, CommitSha, OrderedPatches, OrderedPatchesMetadata, SourcesSha } from "./ValOps.js";
|
|
3
3
|
import { Patch, ParentRef, ValCommit } from "@valbuild/shared/internal";
|
|
4
|
+
import type { HistoryError } from "./history/HistoryError.js";
|
|
5
|
+
import type { AffectedFile, StoredModuleVersion, CommitPage, CommitPatch, HistoricalCommit } from "./history/types.js";
|
|
6
|
+
import { result } from "@valbuild/core/fp";
|
|
4
7
|
import { Buffer } from "buffer";
|
|
5
8
|
export declare class ValOpsFS extends ValOps {
|
|
6
9
|
private readonly contentUrl;
|
|
@@ -102,7 +105,7 @@ export declare class ValOpsFS extends ValOps {
|
|
|
102
105
|
excludePatchOps: ExcludePatchOps;
|
|
103
106
|
}): Promise<ExcludePatchOps extends true ? OrderedPatchesMetadata : OrderedPatches>;
|
|
104
107
|
private parseJsonFile;
|
|
105
|
-
protected saveSourceFilePatch(path: ModuleFilePath, patch: Patch, patchId: PatchId, _parentRef: ParentRef, authorId: AuthorId | null, sessionId: string | null): Promise<SaveSourceFilePatchResult>;
|
|
108
|
+
protected saveSourceFilePatch(path: ModuleFilePath, patch: Patch, patchId: PatchId, _parentRef: ParentRef, authorId: AuthorId | null, sessionId: string | null, _patchGroup?: PatchGroupMembership): Promise<SaveSourceFilePatchResult>;
|
|
106
109
|
protected getSourceFile(path: string): Promise<WithGenericError<{
|
|
107
110
|
data: string;
|
|
108
111
|
}>>;
|
|
@@ -158,4 +161,15 @@ export declare class ValOpsFS extends ValOps {
|
|
|
158
161
|
* whole directory, and a lock that moves away with it is not holding anything.
|
|
159
162
|
*/
|
|
160
163
|
private getPatchLockFile;
|
|
164
|
+
listCommits(): Promise<result.Result<CommitPage, HistoryError>>;
|
|
165
|
+
getCommitPatches(): Promise<result.Result<{
|
|
166
|
+
commit: HistoricalCommit;
|
|
167
|
+
patches: CommitPatch[];
|
|
168
|
+
}, HistoryError>>;
|
|
169
|
+
getCommitModules(): Promise<result.Result<{
|
|
170
|
+
modules: StoredModuleVersion[];
|
|
171
|
+
complete: boolean;
|
|
172
|
+
}, HistoryError>>;
|
|
173
|
+
getCommitAffectedFiles(): Promise<result.Result<AffectedFile[], HistoryError>>;
|
|
174
|
+
getFileAtCommit(): Promise<result.Result<Buffer, HistoryError>>;
|
|
161
175
|
}
|