@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 CHANGED
@@ -1,14 +1,16 @@
1
1
  # silo-client
2
2
 
3
- The typed client for [silo](https://github.com/org-quicko/silo). It follows
4
- silo's own shape: an instance holds projects, a project holds environments, an
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. No dependencies.
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 posts = silo.project("acme").environment("prod").collection("posts")
22
+ const movies = silo.project("moviespace").environment("prod").collection("movies")
21
23
 
22
- const page = await posts.list({ limit: 10 })
23
- for (const post of page.entries) {
24
- console.log(post.fields.title)
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
- Handles are cheap. `silo.project("acme").environment("prod")` makes no request,
29
- so nothing above needs an `await` until the read.
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
- Name your fields and the collection is typed.
35
+ Describe your fields and the collection becomes typed.
38
36
 
39
37
  ```ts
40
- interface Post {
38
+ interface Movie {
41
39
  title: string
40
+ year: number
42
41
  status: "draft" | "published"
43
- tags: string[]
42
+ genres: string[]
44
43
  }
45
44
 
46
- const posts = silo.project("acme").environment("prod").collection<Post>("posts")
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
- const created = await posts.create({ title: "Hello", status: "draft", tags: [] })
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
- const draft = await posts.edit(created.id)
51
- draft.fields.status = "published"
52
- await draft.save()
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
- `get` and `edit` answer different things, and the difference matters.
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
- | Call | Answers | Variables | `save()` |
62
- |------|---------|-----------|----------|
63
- | `posts.get(id)` | `ResolvedEntry<Post>` | substituted | not available |
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
- A `{{VARIABLE}}` in your content is substituted on the way out. Saving an entry
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
- `posts.editable` is the whole read surface with the same rule, for a tool that
72
- edits more than one entry at a time.
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 posts.editable.get(id)
76
- await posts.editable.list({ limit: 50 })
77
- for await (const post of posts.editable.all()) { }
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
- `replace(id, rev, fields)` and `delete(id, rev)` are there for when you hold an
81
- id and a revision from somewhere else. Both `replace` and `save` send every
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 never survive a round trip, because silo strips them before it
85
- answers: `id`, `rev`, `seq`, `created_at` and `updated_at`. `ReservedFieldNames`
86
- holds the list, and creating a collection warns if its schema declares one.
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
- Filters are built, and a typed collection types them.
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 posts.list({
96
- where: posts.filter.field("status").equals("published")
97
- .and(posts.filter.each("tags").equals("release")),
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, `each` addresses every element of an array,
104
- and `meta` addresses the envelope.
129
+ `field` addresses your own fields. `each` addresses every element of an array.
130
+ `meta` addresses the envelope.
105
131
 
106
132
  ```ts
107
- posts.filter.field("author.name").contains("ada")
108
- posts.filter.each("tags").equals("release")
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`, joined with `and`, `or` and `not`.
114
- `Filter` has the same surface untyped, for a filter you assemble at runtime,
115
- and `Filter.raw(node)` takes the wire AST.
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
- Two spellings of a wildcard mean two different things, which is why `each` is
118
- its own call:
146
+ `each` is a separate call because the two ways of writing a wildcard ask
147
+ different questions:
119
148
 
120
149
  ```ts
121
- posts.filter.each("tags").notEquals("draft") // some tag is not "draft"
122
- Filter.not(posts.filter.each("tags").equals("draft")) // no tag is "draft"
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 posts.list({ limit: 25 })
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: silo caps `limit` at 500 and replaces a nonpositive one with 50.
138
- `next()` advances by the answered window, so an oversized request pages
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 is iterable, and two iterators page for you.
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 posts.all({ where })) { }
147
- for await (const page of posts.pages({ limit: 100 })) { }
175
+ for await (const entry of movies.all({ where })) { }
176
+ for await (const page of movies.pages({ limit: 100 })) { }
148
177
  ```
149
178
 
150
- Offset paging over data being written is not a snapshot. Sort by something
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 asset = await silo.media.upload({
187
+ const poster = await silo.media.upload({
159
188
  bytes,
160
- filename: "hero.png",
161
- contentType: "image/png",
162
- folder: "heroes",
189
+ filename: "arrival.jpg",
190
+ contentType: "image/jpeg",
191
+ folder: "posters",
163
192
  })
164
193
 
165
- await posts.create({
166
- title: "Hello",
194
+ await movies.create({
195
+ title: "Arrival",
196
+ year: 2016,
167
197
  status: "draft",
168
- tags: [],
169
- cover: MediaReference.of(asset.id),
198
+ genres: ["sci-fi"],
199
+ poster: MediaReference.of(poster.id),
170
200
  })
171
201
  ```
172
202
 
173
- Entries reference an asset by id, so renaming or moving a file rewrites
174
- nothing. `asset.url` is the link to serve; `asset.reference` is the value to
175
- store. Storing the URL is the mistake a later rename breaks.
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 asset.rename("hero-2.png")
179
- await asset.moveTo("heroes/2026")
180
- await asset.setTags(["banner"]) // replaces the list
181
- await asset.delete() // refused while an entry references it
182
- await asset.delete({ force: true })
183
-
184
- const usage = await asset.usages()
185
- usage.usages // the referrers this key may read
186
- usage.total // the true count
187
- usage.visible // what this key may see of it
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 capped at 100 ids:
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("heroes/2026")
195
- await silo.media.folders.rename("heroes", "banners", { merge: true })
196
- await silo.media.folders.delete("banners", { recursive: true })
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
- A variable is declared once per project and valued per environment.
235
+ You declare a variable once per project, and give it a value per environment.
206
236
 
207
237
  ```ts
208
- const acme = silo.project("acme")
209
- await acme.variables.declare("API_URL", { environment: "prod", value: "https://api.acme.com" })
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 = acme.environment("prod")
244
+ const environment = moviespace.environment("prod")
212
245
  await environment.variables.list()
213
- await environment.variables.set("API_URL", "https://api.acme.com")
214
- await environment.variables.unset("API_URL")
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 nothing, which is
218
- not `""`. An empty value substitutes as empty; an unset one leaves `{{API_URL}}`
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 a missing argument cannot widen a
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 posts.search({ query: "pricing" }) // one collection
228
- await environment.search({ query: "pricing" }) // one environment
229
- await silo.search({ query: "pricing" }) // everything the key can read
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 why it matched.
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: "pricing" })
236
- results.hits[0].collection // "posts"
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
- One class per failure, so you branch on the type.
276
+ There is one class per failure, so you can branch on the type.
244
277
 
245
278
  ```ts
246
- import { ConflictError, ValidationFailedError, NetworkError } from "@org-quicko/silo-client"
279
+ import { ConflictError, ValidationFailedError } from "@org-quicko/silo-client"
247
280
 
248
281
  try {
249
- await draft.save()
282
+ await movies.replace(movie.id, movie.rev, fields)
250
283
  } catch (error) {
251
284
  if (error instanceof ConflictError) {
252
- await draft.refresh() // somebody else wrote first
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 nothing answered. A `NetworkError` on a write does
265
- not prove the write failed. Read the entry back before deciding.
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 posts.list({ limit: 20 }, { signal: controller.signal })
273
- await posts.get(id, { timeoutMilliseconds: 2_000 })
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` and the deadline raises `TimeoutError`.
277
- Nothing is retried for you: a retried `POST` is a duplicate entry, and only the
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.example.com" })
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: `RouteInventory` lists every route this client
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 { EntryReader } from "../entries/entry-reader.cjs";
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 here substitute every `{{NAME}}` an entry holds and answer a
21
- * `ResolvedEntry`, which has no `save()`. Writing one back would replace the
22
- * reference somebody typed with whatever it happened to mean, so reading for
23
- * a write goes through `edit` or `editable`.
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
- /** One entry, with its variables substituted. Read-only: see `edit`. */
39
- get(id: string, options?: RequestOptions): Promise<ResolvedEntry<Fields>>;
40
- /** One entry as stored, ready to change and `save()`. */
41
- edit(id: string, options?: RequestOptions): Promise<Entry<Fields>>;
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 and is safe to hold and `save()` straight away. */
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 { EntryReader } from "../entries/entry-reader.js";
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 here substitute every `{{NAME}}` an entry holds and answer a
21
- * `ResolvedEntry`, which has no `save()`. Writing one back would replace the
22
- * reference somebody typed with whatever it happened to mean, so reading for
23
- * a write goes through `edit` or `editable`.
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
- /** One entry, with its variables substituted. Read-only: see `edit`. */
39
- get(id: string, options?: RequestOptions): Promise<ResolvedEntry<Fields>>;
40
- /** One entry as stored, ready to change and `save()`. */
41
- edit(id: string, options?: RequestOptions): Promise<Entry<Fields>>;
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 and is safe to hold and `save()` straight away. */
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
+ }