@org-quicko/silo-client 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +159 -124
- package/dist/collections/collection-handle.d.cts +15 -23
- package/dist/collections/collection-handle.d.ts +15 -23
- package/dist/collections/collections.d.cts +3 -5
- package/dist/collections/collections.d.ts +3 -5
- package/dist/entries/entry-envelope.d.cts +18 -0
- package/dist/entries/entry-envelope.d.ts +18 -0
- package/dist/entries/entry-read-options.d.cts +16 -0
- package/dist/entries/entry-read-options.d.ts +16 -0
- package/dist/entries/entry-reader.d.cts +16 -16
- package/dist/entries/entry-reader.d.ts +16 -16
- package/dist/entries/entry.d.cts +11 -23
- package/dist/entries/entry.d.ts +11 -23
- package/dist/index.cjs +29 -153
- package/dist/index.d.cts +3 -5
- package/dist/index.d.ts +3 -5
- package/dist/index.js +29 -153
- package/dist/search/search-hit.d.cts +5 -5
- package/dist/search/search-hit.d.ts +5 -5
- package/dist/search/search.d.cts +4 -1
- package/dist/search/search.d.ts +4 -1
- package/package.json +58 -58
- package/dist/collections/reserved-field-names.d.cts +0 -9
- package/dist/collections/reserved-field-names.d.ts +0 -9
- package/dist/entries/entry-base.d.cts +0 -31
- package/dist/entries/entry-base.d.ts +0 -31
- package/dist/entries/entry-mapper.d.cts +0 -13
- package/dist/entries/entry-mapper.d.ts +0 -13
- package/dist/entries/entry-payload.d.cts +0 -12
- package/dist/entries/entry-payload.d.ts +0 -12
- package/dist/entries/resolved-entry.d.cts +0 -11
- package/dist/entries/resolved-entry.d.ts +0 -11
package/README.md
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
# silo-client
|
|
2
2
|
|
|
3
|
-
The typed client for [silo](https://github.com/org-quicko/silo). It
|
|
4
|
-
silo
|
|
5
|
-
environment holds collections, and a collection holds entries.
|
|
3
|
+
The typed client for [silo](https://github.com/org-quicko/silo). It has the same
|
|
4
|
+
shape as silo itself: an instance holds projects, a project holds environments,
|
|
5
|
+
an environment holds collections, and a collection holds entries.
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
8
|
npm install @org-quicko/silo-client
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
Runs on Node 18+, Bun, Deno, browsers and workers.
|
|
11
|
+
Runs on Node 18+, Bun, Deno, browsers and workers. It has no dependencies.
|
|
12
|
+
|
|
13
|
+
The examples below build moviespace, a small film database.
|
|
12
14
|
|
|
13
15
|
## Start
|
|
14
16
|
|
|
@@ -17,115 +19,142 @@ import { Silo } from "@org-quicko/silo-client"
|
|
|
17
19
|
|
|
18
20
|
const silo = new Silo({ url: "http://localhost:8090", key: process.env.SILO_KEY })
|
|
19
21
|
|
|
20
|
-
const
|
|
22
|
+
const movies = silo.project("moviespace").environment("prod").collection("movies")
|
|
21
23
|
|
|
22
|
-
const page = await
|
|
23
|
-
for (const
|
|
24
|
-
console.log(
|
|
24
|
+
const page = await movies.list({ limit: 10 })
|
|
25
|
+
for (const movie of page.entries) {
|
|
26
|
+
console.log(movie.title)
|
|
25
27
|
}
|
|
26
28
|
```
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
so nothing
|
|
30
|
-
|
|
31
|
-
Both scope names are always explicit. There is no default project or
|
|
32
|
-
environment, because a client that guesses reads the wrong environment quietly.
|
|
33
|
-
`silo.scope("acme", "prod")` is the same chain in one call.
|
|
30
|
+
`silo.project("moviespace").environment("prod")` sends no request. It only
|
|
31
|
+
builds the path, so nothing needs an `await` until the read.
|
|
34
32
|
|
|
35
33
|
## Entries
|
|
36
34
|
|
|
37
|
-
|
|
35
|
+
Describe your fields and the collection becomes typed.
|
|
38
36
|
|
|
39
37
|
```ts
|
|
40
|
-
interface
|
|
38
|
+
interface Movie {
|
|
41
39
|
title: string
|
|
40
|
+
year: number
|
|
42
41
|
status: "draft" | "published"
|
|
43
|
-
|
|
42
|
+
genres: string[]
|
|
44
43
|
}
|
|
45
44
|
|
|
46
|
-
const
|
|
45
|
+
const movies = silo.project("moviespace").environment("prod").collection<Movie>("movies")
|
|
46
|
+
|
|
47
|
+
const created = await movies.create({
|
|
48
|
+
title: "Arrival",
|
|
49
|
+
year: 2016,
|
|
50
|
+
status: "draft",
|
|
51
|
+
genres: ["sci-fi"],
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
await movies.replace(created.id, created.rev, {
|
|
55
|
+
title: "Arrival",
|
|
56
|
+
year: 2016,
|
|
57
|
+
status: "published",
|
|
58
|
+
genres: ["sci-fi", "drama"],
|
|
59
|
+
})
|
|
60
|
+
```
|
|
47
61
|
|
|
48
|
-
|
|
62
|
+
A row is exactly what the API sent back: your fields and silo's envelope keys
|
|
63
|
+
together in one flat object.
|
|
49
64
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
65
|
+
```ts
|
|
66
|
+
{
|
|
67
|
+
id: "01M24ZX2ZK60T72CNPCM222E3Z",
|
|
68
|
+
rev: 1,
|
|
69
|
+
title: "Arrival",
|
|
70
|
+
year: 2016,
|
|
71
|
+
status: "published",
|
|
72
|
+
genres: ["sci-fi", "drama"],
|
|
73
|
+
created_at: "2026-09-10T06:25:55.699Z",
|
|
74
|
+
updated_at: "2026-09-10T06:25:55.699Z",
|
|
75
|
+
}
|
|
53
76
|
```
|
|
54
77
|
|
|
55
|
-
An entry keeps its own revision, so you never hold one. `save()` sends the
|
|
56
|
-
revision it read, adopts the one it gets back, and raises `ConflictError` if
|
|
57
|
-
somebody wrote first.
|
|
58
78
|
|
|
59
|
-
|
|
79
|
+
Writes are calls on the collection, and each one takes the revision it expects:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
await movies.create(fields)
|
|
83
|
+
await movies.replace(id, rev, fields)
|
|
84
|
+
await movies.delete(id, rev)
|
|
85
|
+
```
|
|
60
86
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
| `posts.edit(id)` | `Entry<Post>` | left as written | available |
|
|
87
|
+
`rev` is the revision the row reported. If it is out of date the call raises
|
|
88
|
+
`ConflictError`. A fresh read gives you the current one. `replace` needs
|
|
89
|
+
every field, because the route replaces the whole entry.
|
|
65
90
|
|
|
66
|
-
|
|
67
|
-
you read that way would write the substituted value over the reference, so a
|
|
68
|
-
read that substitutes has no `save()` at all, and `edit` is how you read an
|
|
69
|
-
entry you mean to write back.
|
|
91
|
+
### Reading an entry you plan to edit
|
|
70
92
|
|
|
71
|
-
`
|
|
72
|
-
|
|
93
|
+
silo substitutes a `{{VARIABLE}}` in your content on the way out. If you write
|
|
94
|
+
that value back, you replace the reference somebody typed with whatever it
|
|
95
|
+
happened to mean today, and you cannot recover the template from the result. So
|
|
96
|
+
read raw before you edit:
|
|
73
97
|
|
|
74
98
|
```ts
|
|
75
|
-
await
|
|
76
|
-
|
|
77
|
-
|
|
99
|
+
const draft = await movies.get(id, { variables: "raw" })
|
|
100
|
+
draft.trailerUrl // "{{CDN_URL}}/trailers/arrival.mp4", as stored
|
|
101
|
+
|
|
102
|
+
const { id: _, rev, created_at, updated_at, ...fields } = draft
|
|
103
|
+
await movies.replace(draft.id, rev, { ...fields, status: "published" })
|
|
78
104
|
```
|
|
79
105
|
|
|
80
|
-
`
|
|
81
|
-
|
|
82
|
-
field, because the route is a full replace.
|
|
106
|
+
`{ variables: "raw" }` works on every read: `get`, `list`, `all` and `pages`.
|
|
107
|
+
Writes always send raw, so `create` and `replace` answer with what you sent.
|
|
83
108
|
|
|
84
|
-
Five field names
|
|
85
|
-
|
|
86
|
-
|
|
109
|
+
Five field names belong to the envelope: `id`, `rev`, `seq`, `created_at` and
|
|
110
|
+
`updated_at`. silo refuses a schema that declares one when you create the
|
|
111
|
+
collection, and refuses an entry that carries one when you write it. A row can
|
|
112
|
+
never collide with its own envelope.
|
|
87
113
|
|
|
88
114
|
## Queries
|
|
89
115
|
|
|
90
|
-
|
|
116
|
+
You build filters, and a typed collection types them.
|
|
91
117
|
|
|
92
118
|
```ts
|
|
93
119
|
import { Filter, Sort } from "@org-quicko/silo-client"
|
|
94
120
|
|
|
95
|
-
const page = await
|
|
96
|
-
where:
|
|
97
|
-
.and(
|
|
121
|
+
const page = await movies.list({
|
|
122
|
+
where: movies.filter.field("status").equals("published")
|
|
123
|
+
.and(movies.filter.each("genres").equals("sci-fi")),
|
|
98
124
|
sort: Sort.recentlyUpdated(),
|
|
99
125
|
limit: 20,
|
|
100
126
|
})
|
|
101
127
|
```
|
|
102
128
|
|
|
103
|
-
`field` addresses your own fields
|
|
104
|
-
|
|
129
|
+
`field` addresses your own fields. `each` addresses every element of an array.
|
|
130
|
+
`meta` addresses the envelope.
|
|
105
131
|
|
|
106
132
|
```ts
|
|
107
|
-
|
|
108
|
-
|
|
133
|
+
movies.filter.field("title").contains("arrival")
|
|
134
|
+
movies.filter.each("genres").equals("sci-fi")
|
|
109
135
|
Filter.meta("updated_at").greaterThan("2026-01-01T00:00:00Z")
|
|
110
136
|
```
|
|
111
137
|
|
|
138
|
+
A dot reaches inside a nested field, so `field("director.name")` addresses the
|
|
139
|
+
`name` of a `director` object.
|
|
140
|
+
|
|
112
141
|
The operators are `equals`, `notEquals`, `contains`, `greaterThan`, `atLeast`,
|
|
113
|
-
`lessThan`, `atMost`, `oneOf` and `exists
|
|
114
|
-
`Filter`
|
|
115
|
-
and `Filter.raw(node)` takes the wire
|
|
142
|
+
`lessThan`, `atMost`, `oneOf` and `exists`. Join them with `and`, `or` and
|
|
143
|
+
`not`. `Filter` offers the same calls without types, for a filter you assemble
|
|
144
|
+
at runtime, and `Filter.raw(node)` takes the wire format directly.
|
|
116
145
|
|
|
117
|
-
|
|
118
|
-
|
|
146
|
+
`each` is a separate call because the two ways of writing a wildcard ask
|
|
147
|
+
different questions:
|
|
119
148
|
|
|
120
149
|
```ts
|
|
121
|
-
|
|
122
|
-
Filter.not(
|
|
150
|
+
movies.filter.each("genres").notEquals("horror") // some genre is not "horror"
|
|
151
|
+
Filter.not(movies.filter.each("genres").equals("horror")) // no genre is "horror"
|
|
123
152
|
```
|
|
124
153
|
|
|
125
154
|
## Pagination
|
|
126
155
|
|
|
127
156
|
```ts
|
|
128
|
-
const first = await
|
|
157
|
+
const first = await movies.list({ limit: 25 })
|
|
129
158
|
first.total // 137
|
|
130
159
|
first.pageNumber // 1
|
|
131
160
|
first.hasMore // true
|
|
@@ -134,66 +163,67 @@ const second = await first.next()
|
|
|
134
163
|
```
|
|
135
164
|
|
|
136
165
|
A page reports the window silo actually used, which is not always the one you
|
|
137
|
-
asked for
|
|
138
|
-
`next()`
|
|
139
|
-
correctly instead of stepping over entries.
|
|
166
|
+
asked for. silo caps `limit` at 500, and replaces a limit of zero or less with
|
|
167
|
+
50. `next()` moves forward by the window silo reported, so an oversized request
|
|
168
|
+
still pages correctly instead of stepping over entries.
|
|
140
169
|
|
|
141
|
-
Every page
|
|
170
|
+
Every page can be iterated, and two helpers page for you.
|
|
142
171
|
|
|
143
172
|
```ts
|
|
144
173
|
for (const entry of page) { }
|
|
145
174
|
|
|
146
|
-
for await (const entry of
|
|
147
|
-
for await (const page of
|
|
175
|
+
for await (const entry of movies.all({ where })) { }
|
|
176
|
+
for await (const page of movies.pages({ limit: 100 })) { }
|
|
148
177
|
```
|
|
149
178
|
|
|
150
|
-
|
|
151
|
-
stable when that matters.
|
|
179
|
+
Paging by offset over data that is being written is not a snapshot. Sort by
|
|
180
|
+
something stable when that matters.
|
|
152
181
|
|
|
153
182
|
## Media
|
|
154
183
|
|
|
155
184
|
```ts
|
|
156
185
|
import { MediaReference } from "@org-quicko/silo-client"
|
|
157
186
|
|
|
158
|
-
const
|
|
187
|
+
const poster = await silo.media.upload({
|
|
159
188
|
bytes,
|
|
160
|
-
filename: "
|
|
161
|
-
contentType: "image/
|
|
162
|
-
folder: "
|
|
189
|
+
filename: "arrival.jpg",
|
|
190
|
+
contentType: "image/jpeg",
|
|
191
|
+
folder: "posters",
|
|
163
192
|
})
|
|
164
193
|
|
|
165
|
-
await
|
|
166
|
-
title: "
|
|
194
|
+
await movies.create({
|
|
195
|
+
title: "Arrival",
|
|
196
|
+
year: 2016,
|
|
167
197
|
status: "draft",
|
|
168
|
-
|
|
169
|
-
|
|
198
|
+
genres: ["sci-fi"],
|
|
199
|
+
poster: MediaReference.of(poster.id),
|
|
170
200
|
})
|
|
171
201
|
```
|
|
172
202
|
|
|
173
|
-
|
|
174
|
-
nothing. `asset.url`
|
|
175
|
-
store. Storing the URL is
|
|
203
|
+
An entry refers to an asset by id, so renaming or moving a file rewrites
|
|
204
|
+
nothing. Use `asset.url` for the link you serve, and `asset.reference` for the
|
|
205
|
+
value you store. Storing the URL instead is what a later rename breaks.
|
|
176
206
|
|
|
177
207
|
```ts
|
|
178
|
-
await
|
|
179
|
-
await
|
|
180
|
-
await
|
|
181
|
-
await
|
|
182
|
-
await
|
|
183
|
-
|
|
184
|
-
const usage = await
|
|
185
|
-
usage.usages
|
|
186
|
-
usage.total
|
|
187
|
-
usage.visible
|
|
208
|
+
await poster.rename("arrival-2016.jpg")
|
|
209
|
+
await poster.moveTo("posters/2016")
|
|
210
|
+
await poster.setTags(["poster"]) // replaces the whole list
|
|
211
|
+
await poster.delete() // refused while an entry refers to it
|
|
212
|
+
await poster.delete({ force: true })
|
|
213
|
+
|
|
214
|
+
const usage = await poster.usages()
|
|
215
|
+
usage.usages // the referring entries this key may read
|
|
216
|
+
usage.total // the true count
|
|
217
|
+
usage.visible // how many of them this key may see
|
|
188
218
|
```
|
|
189
219
|
|
|
190
|
-
Folders, and a bulk delete
|
|
220
|
+
Folders, and a bulk delete that takes up to 100 ids:
|
|
191
221
|
|
|
192
222
|
```ts
|
|
193
223
|
await silo.media.folders.list()
|
|
194
|
-
await silo.media.folders.create("
|
|
195
|
-
await silo.media.folders.rename("
|
|
196
|
-
await silo.media.folders.delete("
|
|
224
|
+
await silo.media.folders.create("posters/2016")
|
|
225
|
+
await silo.media.folders.rename("posters", "artwork", { merge: true })
|
|
226
|
+
await silo.media.folders.delete("artwork", { recursive: true })
|
|
197
227
|
|
|
198
228
|
const report = await silo.media.deleteMany(ids, { force: true })
|
|
199
229
|
report.deleted
|
|
@@ -202,80 +232,85 @@ report.failed
|
|
|
202
232
|
|
|
203
233
|
## Variables
|
|
204
234
|
|
|
205
|
-
|
|
235
|
+
You declare a variable once per project, and give it a value per environment.
|
|
206
236
|
|
|
207
237
|
```ts
|
|
208
|
-
const
|
|
209
|
-
await
|
|
238
|
+
const moviespace = silo.project("moviespace")
|
|
239
|
+
await moviespace.variables.declare("CDN_URL", {
|
|
240
|
+
environment: "prod",
|
|
241
|
+
value: "https://cdn.moviespace.com",
|
|
242
|
+
})
|
|
210
243
|
|
|
211
|
-
const environment =
|
|
244
|
+
const environment = moviespace.environment("prod")
|
|
212
245
|
await environment.variables.list()
|
|
213
|
-
await environment.variables.set("
|
|
214
|
-
await environment.variables.unset("
|
|
246
|
+
await environment.variables.set("CDN_URL", "https://cdn.moviespace.com")
|
|
247
|
+
await environment.variables.unset("CDN_URL")
|
|
215
248
|
```
|
|
216
249
|
|
|
217
|
-
`variable.value` is `null` when this environment has given it
|
|
218
|
-
not `""`. An empty value substitutes as empty
|
|
219
|
-
standing in the response.
|
|
250
|
+
`variable.value` is `null` when this environment has given it no value, which is
|
|
251
|
+
not the same as `""`. An empty value substitutes as empty. An unset one leaves
|
|
252
|
+
`{{CDN_URL}}` standing in the response.
|
|
220
253
|
|
|
221
254
|
## Search
|
|
222
255
|
|
|
223
|
-
The reach is whatever you call it on, so
|
|
256
|
+
The reach is whatever you call it on, so leaving out an argument cannot widen a
|
|
224
257
|
search.
|
|
225
258
|
|
|
226
259
|
```ts
|
|
227
|
-
await
|
|
228
|
-
await environment.search({ query: "
|
|
229
|
-
await silo.search({ query: "
|
|
260
|
+
await movies.search({ query: "arrival" }) // one collection
|
|
261
|
+
await environment.search({ query: "arrival" }) // one environment
|
|
262
|
+
await silo.search({ query: "arrival" }) // everything the key can read
|
|
230
263
|
```
|
|
231
264
|
|
|
232
|
-
A hit says where it was found and quotes
|
|
265
|
+
A hit says where it was found, and quotes the text that matched.
|
|
233
266
|
|
|
234
267
|
```ts
|
|
235
|
-
const results = await silo.search({ query: "
|
|
236
|
-
results.hits[0].collection // "
|
|
268
|
+
const results = await silo.search({ query: "arrival" })
|
|
269
|
+
results.hits[0].collection // "movies"
|
|
237
270
|
results.hits[0].snippets // [{ path, before, match, after }]
|
|
238
271
|
results.engine // "fts5" when the index answered, "scan" when it walked
|
|
239
272
|
```
|
|
240
273
|
|
|
241
274
|
## Errors
|
|
242
275
|
|
|
243
|
-
|
|
276
|
+
There is one class per failure, so you can branch on the type.
|
|
244
277
|
|
|
245
278
|
```ts
|
|
246
|
-
import { ConflictError, ValidationFailedError
|
|
279
|
+
import { ConflictError, ValidationFailedError } from "@org-quicko/silo-client"
|
|
247
280
|
|
|
248
281
|
try {
|
|
249
|
-
await
|
|
282
|
+
await movies.replace(movie.id, movie.rev, fields)
|
|
250
283
|
} catch (error) {
|
|
251
284
|
if (error instanceof ConflictError) {
|
|
252
|
-
|
|
285
|
+
// Somebody else wrote first. Read again for the current revision.
|
|
286
|
+
const current = await movies.get(movie.id)
|
|
287
|
+
await movies.replace(current.id, current.rev, fields)
|
|
253
288
|
} else if (error instanceof ValidationFailedError) {
|
|
254
289
|
error.details // [{ path: "/title", message }]
|
|
255
290
|
}
|
|
256
291
|
}
|
|
257
292
|
```
|
|
258
293
|
|
|
259
|
-
`SiloError` is the base for anything silo refused: `ValidationFailedError`,
|
|
294
|
+
`SiloError` is the base class for anything silo refused: `ValidationFailedError`,
|
|
260
295
|
`UnauthorizedError`, `ForbiddenError`, `NotFoundError`, `ConflictError`,
|
|
261
296
|
`MediaInUseError`, `MediaDeleteStalledError` and `InternalError`.
|
|
262
297
|
|
|
263
298
|
`NetworkError`, `TimeoutError`, `RequestAbortedError` and `InvalidResponseError`
|
|
264
|
-
are not `SiloError`, because
|
|
265
|
-
not prove the write failed. Read the entry back before
|
|
299
|
+
are not `SiloError`, because silo never answered. A `NetworkError` on a write
|
|
300
|
+
does not prove the write failed. Read the entry back before you decide.
|
|
266
301
|
|
|
267
302
|
## Cancellation
|
|
268
303
|
|
|
269
304
|
Every call takes the same last argument.
|
|
270
305
|
|
|
271
306
|
```ts
|
|
272
|
-
await
|
|
273
|
-
await
|
|
307
|
+
await movies.list({ limit: 20 }, { signal: controller.signal })
|
|
308
|
+
await movies.get(id, { timeoutMilliseconds: 2_000 })
|
|
274
309
|
```
|
|
275
310
|
|
|
276
|
-
`abort()` raises `RequestAbortedError
|
|
277
|
-
Nothing is retried for you
|
|
278
|
-
caller knows whether a call was safe to repeat.
|
|
311
|
+
`abort()` raises `RequestAbortedError`, and the deadline raises `TimeoutError`.
|
|
312
|
+
Nothing is retried for you. A retried `POST` creates a second entry, and only
|
|
313
|
+
the caller knows whether a call was safe to repeat.
|
|
279
314
|
|
|
280
315
|
## Anonymous reads
|
|
281
316
|
|
|
@@ -283,14 +318,14 @@ A key is optional. Without one you reach the collections whose schema does not
|
|
|
283
318
|
set `x-silo-auth`.
|
|
284
319
|
|
|
285
320
|
```ts
|
|
286
|
-
const silo = new Silo({ url: "https://cms.
|
|
321
|
+
const silo = new Silo({ url: "https://cms.moviespace.com" })
|
|
287
322
|
```
|
|
288
323
|
|
|
289
324
|
## What this client does not reach
|
|
290
325
|
|
|
291
326
|
Keys, claims, plugins, export and import, settings, audit and observability.
|
|
292
327
|
Those are operator surfaces, and the admin UI and the CLI own them. There is no
|
|
293
|
-
generic `request()` either
|
|
328
|
+
generic `request()` either. `RouteInventory` lists every route this client
|
|
294
329
|
covers and every route it leaves out, and a test holds that list against the
|
|
295
330
|
server's own registrations.
|
|
296
331
|
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { Entry } from "../entries/entry.cjs";
|
|
1
|
+
import type { Entry } from "../entries/entry.cjs";
|
|
2
2
|
import type { EntryListQuery } from "../entries/entry-list-query.cjs";
|
|
3
3
|
import type { EntryPage } from "../entries/entry-page.cjs";
|
|
4
4
|
import type { EntryPageStream } from "../entries/entry-page-stream.cjs";
|
|
5
|
-
import {
|
|
5
|
+
import type { EntryReadOptions } from "../entries/entry-read-options.cjs";
|
|
6
6
|
import type { EntryStream } from "../entries/entry-stream.cjs";
|
|
7
|
-
import { ResolvedEntry } from "../entries/resolved-entry.cjs";
|
|
8
7
|
import { TypedFilter } from "../query/typed-filter.cjs";
|
|
9
8
|
import type { RequestOptions } from "../request-options.cjs";
|
|
10
9
|
import { RenameReport } from "../scope/rename-report.cjs";
|
|
@@ -17,38 +16,31 @@ import { CollectionSchema } from "./collection-schema.cjs";
|
|
|
17
16
|
* One collection, typed to its fields:
|
|
18
17
|
* `environment.collection<Post>("posts")`.
|
|
19
18
|
*
|
|
20
|
-
* Reads
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
19
|
+
* Reads answer the wire's own flat rows and writes take explicit arguments, so
|
|
20
|
+
* there is one entry shape here and no second read surface. Pass
|
|
21
|
+
* `{ variables: "raw" }` to any read to get the stored `{{NAME}}` templates
|
|
22
|
+
* instead of what they resolve to, which is what editing one requires (D62).
|
|
24
23
|
*/
|
|
25
24
|
export declare class CollectionHandle<Fields = Record<string, unknown>> {
|
|
26
25
|
private readonly scope;
|
|
27
26
|
readonly name: string;
|
|
28
27
|
readonly filter: TypedFilter<Fields>;
|
|
29
28
|
readonly schema: CollectionSchema;
|
|
30
|
-
|
|
31
|
-
* The same collection read for writing back. Every read here keeps the
|
|
32
|
-
* templates as stored and answers an `Entry`, which has `save()`.
|
|
33
|
-
*/
|
|
34
|
-
readonly editable: EntryReader<Entry<Fields>>;
|
|
35
|
-
private readonly context;
|
|
36
|
-
private readonly resolved;
|
|
29
|
+
private readonly reader;
|
|
37
30
|
constructor(scope: ScopeReference, name: string);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
list(query?: EntryListQuery, options?: RequestOptions): Promise<EntryPage<ResolvedEntry<Fields>>>;
|
|
43
|
-
all(query?: EntryListQuery, options?: RequestOptions): EntryStream<ResolvedEntry<Fields>>;
|
|
44
|
-
pages(query?: EntryListQuery, options?: RequestOptions): EntryPageStream<ResolvedEntry<Fields>>;
|
|
31
|
+
get(id: string, options?: EntryReadOptions): Promise<Entry<Fields>>;
|
|
32
|
+
list(query?: EntryListQuery, options?: EntryReadOptions): Promise<EntryPage<Entry<Fields>>>;
|
|
33
|
+
all(query?: EntryListQuery, options?: EntryReadOptions): EntryStream<Entry<Fields>>;
|
|
34
|
+
pages(query?: EntryListQuery, options?: EntryReadOptions): EntryPageStream<Entry<Fields>>;
|
|
45
35
|
create(fields: Fields, options?: RequestOptions): Promise<Entry<Fields>>;
|
|
46
|
-
/** A full replace, which is what the route is: send every field.
|
|
36
|
+
/** A full replace, which is what the route is: send every field. `rev` is
|
|
37
|
+
* the one the entry answered when it was read, and a stale one is a
|
|
38
|
+
* `ConflictError`. */
|
|
47
39
|
replace(id: string, rev: number, fields: Fields, options?: RequestOptions): Promise<Entry<Fields>>;
|
|
48
40
|
delete(id: string, rev: number, options?: RequestOptions): Promise<void>;
|
|
49
41
|
search(query: SearchQuery, options?: RequestOptions): Promise<SearchPage>;
|
|
50
42
|
rename(name: string, options?: RenameOptions): Promise<RenameReport>;
|
|
51
43
|
/** Both writes ask for the stored templates back, so what returns is what
|
|
52
|
-
* was sent
|
|
44
|
+
* was sent rather than a resolved snapshot of it. */
|
|
53
45
|
private write;
|
|
54
46
|
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { Entry } from "../entries/entry.js";
|
|
1
|
+
import type { Entry } from "../entries/entry.js";
|
|
2
2
|
import type { EntryListQuery } from "../entries/entry-list-query.js";
|
|
3
3
|
import type { EntryPage } from "../entries/entry-page.js";
|
|
4
4
|
import type { EntryPageStream } from "../entries/entry-page-stream.js";
|
|
5
|
-
import {
|
|
5
|
+
import type { EntryReadOptions } from "../entries/entry-read-options.js";
|
|
6
6
|
import type { EntryStream } from "../entries/entry-stream.js";
|
|
7
|
-
import { ResolvedEntry } from "../entries/resolved-entry.js";
|
|
8
7
|
import { TypedFilter } from "../query/typed-filter.js";
|
|
9
8
|
import type { RequestOptions } from "../request-options.js";
|
|
10
9
|
import { RenameReport } from "../scope/rename-report.js";
|
|
@@ -17,38 +16,31 @@ import { CollectionSchema } from "./collection-schema.js";
|
|
|
17
16
|
* One collection, typed to its fields:
|
|
18
17
|
* `environment.collection<Post>("posts")`.
|
|
19
18
|
*
|
|
20
|
-
* Reads
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
19
|
+
* Reads answer the wire's own flat rows and writes take explicit arguments, so
|
|
20
|
+
* there is one entry shape here and no second read surface. Pass
|
|
21
|
+
* `{ variables: "raw" }` to any read to get the stored `{{NAME}}` templates
|
|
22
|
+
* instead of what they resolve to, which is what editing one requires (D62).
|
|
24
23
|
*/
|
|
25
24
|
export declare class CollectionHandle<Fields = Record<string, unknown>> {
|
|
26
25
|
private readonly scope;
|
|
27
26
|
readonly name: string;
|
|
28
27
|
readonly filter: TypedFilter<Fields>;
|
|
29
28
|
readonly schema: CollectionSchema;
|
|
30
|
-
|
|
31
|
-
* The same collection read for writing back. Every read here keeps the
|
|
32
|
-
* templates as stored and answers an `Entry`, which has `save()`.
|
|
33
|
-
*/
|
|
34
|
-
readonly editable: EntryReader<Entry<Fields>>;
|
|
35
|
-
private readonly context;
|
|
36
|
-
private readonly resolved;
|
|
29
|
+
private readonly reader;
|
|
37
30
|
constructor(scope: ScopeReference, name: string);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
list(query?: EntryListQuery, options?: RequestOptions): Promise<EntryPage<ResolvedEntry<Fields>>>;
|
|
43
|
-
all(query?: EntryListQuery, options?: RequestOptions): EntryStream<ResolvedEntry<Fields>>;
|
|
44
|
-
pages(query?: EntryListQuery, options?: RequestOptions): EntryPageStream<ResolvedEntry<Fields>>;
|
|
31
|
+
get(id: string, options?: EntryReadOptions): Promise<Entry<Fields>>;
|
|
32
|
+
list(query?: EntryListQuery, options?: EntryReadOptions): Promise<EntryPage<Entry<Fields>>>;
|
|
33
|
+
all(query?: EntryListQuery, options?: EntryReadOptions): EntryStream<Entry<Fields>>;
|
|
34
|
+
pages(query?: EntryListQuery, options?: EntryReadOptions): EntryPageStream<Entry<Fields>>;
|
|
45
35
|
create(fields: Fields, options?: RequestOptions): Promise<Entry<Fields>>;
|
|
46
|
-
/** A full replace, which is what the route is: send every field.
|
|
36
|
+
/** A full replace, which is what the route is: send every field. `rev` is
|
|
37
|
+
* the one the entry answered when it was read, and a stale one is a
|
|
38
|
+
* `ConflictError`. */
|
|
47
39
|
replace(id: string, rev: number, fields: Fields, options?: RequestOptions): Promise<Entry<Fields>>;
|
|
48
40
|
delete(id: string, rev: number, options?: RequestOptions): Promise<void>;
|
|
49
41
|
search(query: SearchQuery, options?: RequestOptions): Promise<SearchPage>;
|
|
50
42
|
rename(name: string, options?: RenameOptions): Promise<RenameReport>;
|
|
51
43
|
/** Both writes ask for the stored templates back, so what returns is what
|
|
52
|
-
* was sent
|
|
44
|
+
* was sent rather than a resolved snapshot of it. */
|
|
53
45
|
private write;
|
|
54
46
|
}
|
|
@@ -9,11 +9,9 @@ export declare class Collections {
|
|
|
9
9
|
private readonly scope;
|
|
10
10
|
constructor(scope: ScopeReference);
|
|
11
11
|
list(options?: RequestOptions): Promise<CollectionSummary[]>;
|
|
12
|
+
/** A schema declaring a field named `id`, `rev`, `seq`, `created_at` or
|
|
13
|
+
* `updated_at` is refused by the server with a `ValidationFailedError`
|
|
14
|
+
* naming it, so there is nothing to warn about here (D62). */
|
|
12
15
|
create(name: string, schema: JsonSchema, options?: RequestOptions): Promise<CollectionDefinition>;
|
|
13
16
|
private static toSummary;
|
|
14
|
-
/** The only `console` use in this package: a schema declaring a reserved
|
|
15
|
-
* field still validates, so this is the one place a caller can learn
|
|
16
|
-
* about it before finding out from a field that is silently never
|
|
17
|
-
* returned. */
|
|
18
|
-
private static warnOnReservedFields;
|
|
19
17
|
}
|
|
@@ -9,11 +9,9 @@ export declare class Collections {
|
|
|
9
9
|
private readonly scope;
|
|
10
10
|
constructor(scope: ScopeReference);
|
|
11
11
|
list(options?: RequestOptions): Promise<CollectionSummary[]>;
|
|
12
|
+
/** A schema declaring a field named `id`, `rev`, `seq`, `created_at` or
|
|
13
|
+
* `updated_at` is refused by the server with a `ValidationFailedError`
|
|
14
|
+
* naming it, so there is nothing to warn about here (D62). */
|
|
12
15
|
create(name: string, schema: JsonSchema, options?: RequestOptions): Promise<CollectionDefinition>;
|
|
13
16
|
private static toSummary;
|
|
14
|
-
/** The only `console` use in this package: a schema declaring a reserved
|
|
15
|
-
* field still validates, so this is the one place a caller can learn
|
|
16
|
-
* about it before finding out from a field that is silently never
|
|
17
|
-
* returned. */
|
|
18
|
-
private static warnOnReservedFields;
|
|
19
17
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The four keys silo puts on every entry alongside the author's own fields.
|
|
3
|
+
*
|
|
4
|
+
* `rev` is here because `replace()` and `delete()` require the revision the
|
|
5
|
+
* caller expects and a mismatch is a `409`, so the number has to survive the
|
|
6
|
+
* trip from a read to a write. The timestamps are ISO-8601 strings rather than
|
|
7
|
+
* `Date`s, and `created_at` keeps the wire's spelling, because a row read from
|
|
8
|
+
* one call and handed to the next unchanged is worth more than a prettier one.
|
|
9
|
+
*
|
|
10
|
+
* No field can collide with these: silo refuses a schema declaring one and an
|
|
11
|
+
* entry carrying one (D62).
|
|
12
|
+
*/
|
|
13
|
+
export interface EntryEnvelope {
|
|
14
|
+
id: string;
|
|
15
|
+
rev: number;
|
|
16
|
+
created_at: string;
|
|
17
|
+
updated_at: string;
|
|
18
|
+
}
|