@org-quicko/silo-client 1.0.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.
Files changed (202) hide show
  1. package/README.md +313 -0
  2. package/dist/collections/collection-definition.d.cts +10 -0
  3. package/dist/collections/collection-definition.d.ts +10 -0
  4. package/dist/collections/collection-handle.d.cts +54 -0
  5. package/dist/collections/collection-handle.d.ts +54 -0
  6. package/dist/collections/collection-schema.d.cts +16 -0
  7. package/dist/collections/collection-schema.d.ts +16 -0
  8. package/dist/collections/collection-summary.d.cts +10 -0
  9. package/dist/collections/collection-summary.d.ts +10 -0
  10. package/dist/collections/collections.d.cts +19 -0
  11. package/dist/collections/collections.d.ts +19 -0
  12. package/dist/collections/json-schema.d.cts +9 -0
  13. package/dist/collections/json-schema.d.ts +9 -0
  14. package/dist/collections/reserved-field-names.d.cts +9 -0
  15. package/dist/collections/reserved-field-names.d.ts +9 -0
  16. package/dist/entries/entry-base.d.cts +31 -0
  17. package/dist/entries/entry-base.d.ts +31 -0
  18. package/dist/entries/entry-list-query.d.cts +11 -0
  19. package/dist/entries/entry-list-query.d.ts +11 -0
  20. package/dist/entries/entry-mapper.d.cts +13 -0
  21. package/dist/entries/entry-mapper.d.ts +13 -0
  22. package/dist/entries/entry-page-stream.d.cts +14 -0
  23. package/dist/entries/entry-page-stream.d.ts +14 -0
  24. package/dist/entries/entry-page.d.cts +16 -0
  25. package/dist/entries/entry-page.d.ts +16 -0
  26. package/dist/entries/entry-payload.d.cts +12 -0
  27. package/dist/entries/entry-payload.d.ts +12 -0
  28. package/dist/entries/entry-reader.d.cts +26 -0
  29. package/dist/entries/entry-reader.d.ts +26 -0
  30. package/dist/entries/entry-stream.d.cts +5 -0
  31. package/dist/entries/entry-stream.d.ts +5 -0
  32. package/dist/entries/entry.d.cts +25 -0
  33. package/dist/entries/entry.d.ts +25 -0
  34. package/dist/entries/resolved-entry.d.cts +11 -0
  35. package/dist/entries/resolved-entry.d.ts +11 -0
  36. package/dist/errors/conflict-error.d.cts +6 -0
  37. package/dist/errors/conflict-error.d.ts +6 -0
  38. package/dist/errors/error-code.d.cts +4 -0
  39. package/dist/errors/error-code.d.ts +4 -0
  40. package/dist/errors/error-factory.d.cts +15 -0
  41. package/dist/errors/error-factory.d.ts +15 -0
  42. package/dist/errors/error-payload.d.cts +13 -0
  43. package/dist/errors/error-payload.d.ts +13 -0
  44. package/dist/errors/forbidden-error.d.cts +5 -0
  45. package/dist/errors/forbidden-error.d.ts +5 -0
  46. package/dist/errors/internal-error.d.cts +6 -0
  47. package/dist/errors/internal-error.d.ts +6 -0
  48. package/dist/errors/invalid-response-error.d.cts +11 -0
  49. package/dist/errors/invalid-response-error.d.ts +11 -0
  50. package/dist/errors/media-delete-stalled-error.d.cts +14 -0
  51. package/dist/errors/media-delete-stalled-error.d.ts +14 -0
  52. package/dist/errors/media-in-use-error.d.cts +21 -0
  53. package/dist/errors/media-in-use-error.d.ts +21 -0
  54. package/dist/errors/network-error.d.cts +11 -0
  55. package/dist/errors/network-error.d.ts +11 -0
  56. package/dist/errors/not-found-error.d.cts +6 -0
  57. package/dist/errors/not-found-error.d.ts +6 -0
  58. package/dist/errors/request-aborted-error.d.cts +10 -0
  59. package/dist/errors/request-aborted-error.d.ts +10 -0
  60. package/dist/errors/silo-error.d.cts +12 -0
  61. package/dist/errors/silo-error.d.ts +12 -0
  62. package/dist/errors/timeout-error.d.cts +11 -0
  63. package/dist/errors/timeout-error.d.ts +11 -0
  64. package/dist/errors/unauthorized-error.d.cts +5 -0
  65. package/dist/errors/unauthorized-error.d.ts +5 -0
  66. package/dist/errors/validation-detail.d.cts +8 -0
  67. package/dist/errors/validation-detail.d.ts +8 -0
  68. package/dist/errors/validation-failed-error.d.cts +11 -0
  69. package/dist/errors/validation-failed-error.d.ts +11 -0
  70. package/dist/index.cjs +2095 -0
  71. package/dist/index.d.cts +96 -0
  72. package/dist/index.d.ts +96 -0
  73. package/dist/index.js +2053 -0
  74. package/dist/instance/health-report.d.cts +5 -0
  75. package/dist/instance/health-report.d.ts +5 -0
  76. package/dist/media/media-asset-mapper.d.cts +31 -0
  77. package/dist/media/media-asset-mapper.d.ts +31 -0
  78. package/dist/media/media-asset-payload.d.cts +20 -0
  79. package/dist/media/media-asset-payload.d.ts +20 -0
  80. package/dist/media/media-asset.d.cts +63 -0
  81. package/dist/media/media-asset.d.ts +63 -0
  82. package/dist/media/media-delete-failure.d.cts +16 -0
  83. package/dist/media/media-delete-failure.d.ts +16 -0
  84. package/dist/media/media-delete-options.d.cts +9 -0
  85. package/dist/media/media-delete-options.d.ts +9 -0
  86. package/dist/media/media-delete-report.d.cts +13 -0
  87. package/dist/media/media-delete-report.d.ts +13 -0
  88. package/dist/media/media-folder-delete.d.cts +10 -0
  89. package/dist/media/media-folder-delete.d.ts +10 -0
  90. package/dist/media/media-folder-move.d.cts +9 -0
  91. package/dist/media/media-folder-move.d.ts +9 -0
  92. package/dist/media/media-folders.d.cts +22 -0
  93. package/dist/media/media-folders.d.ts +22 -0
  94. package/dist/media/media-page-stream.d.cts +10 -0
  95. package/dist/media/media-page-stream.d.ts +10 -0
  96. package/dist/media/media-page.d.cts +19 -0
  97. package/dist/media/media-page.d.ts +19 -0
  98. package/dist/media/media-query.d.cts +24 -0
  99. package/dist/media/media-query.d.ts +24 -0
  100. package/dist/media/media-reference.d.cts +18 -0
  101. package/dist/media/media-reference.d.ts +18 -0
  102. package/dist/media/media-stream.d.cts +7 -0
  103. package/dist/media/media-stream.d.ts +7 -0
  104. package/dist/media/media-upload.d.cts +18 -0
  105. package/dist/media/media-upload.d.ts +18 -0
  106. package/dist/media/media-usage-page.d.cts +35 -0
  107. package/dist/media/media-usage-page.d.ts +35 -0
  108. package/dist/media/media-usage.d.cts +13 -0
  109. package/dist/media/media-usage.d.ts +13 -0
  110. package/dist/media/media.d.cts +37 -0
  111. package/dist/media/media.d.ts +37 -0
  112. package/dist/pagination/page-window.d.cts +14 -0
  113. package/dist/pagination/page-window.d.ts +14 -0
  114. package/dist/pagination/page.d.cts +27 -0
  115. package/dist/pagination/page.d.ts +27 -0
  116. package/dist/pagination/row-stream.d.cts +28 -0
  117. package/dist/pagination/row-stream.d.ts +28 -0
  118. package/dist/query/field-path.d.cts +14 -0
  119. package/dist/query/field-path.d.ts +14 -0
  120. package/dist/query/filter-expression.d.cts +15 -0
  121. package/dist/query/filter-expression.d.ts +15 -0
  122. package/dist/query/filter-field.d.cts +19 -0
  123. package/dist/query/filter-field.d.ts +19 -0
  124. package/dist/query/filter-node.d.cts +9 -0
  125. package/dist/query/filter-node.d.ts +9 -0
  126. package/dist/query/filter-operator.d.cts +7 -0
  127. package/dist/query/filter-operator.d.ts +7 -0
  128. package/dist/query/filter.d.cts +18 -0
  129. package/dist/query/filter.d.ts +18 -0
  130. package/dist/query/sort-term.d.cts +13 -0
  131. package/dist/query/sort-term.d.ts +13 -0
  132. package/dist/query/sort.d.cts +15 -0
  133. package/dist/query/sort.d.ts +15 -0
  134. package/dist/query/typed-filter.d.cts +32 -0
  135. package/dist/query/typed-filter.d.ts +32 -0
  136. package/dist/request-options.d.cts +9 -0
  137. package/dist/request-options.d.ts +9 -0
  138. package/dist/scope/delete-options.d.cts +9 -0
  139. package/dist/scope/delete-options.d.ts +9 -0
  140. package/dist/scope/environment-handle.d.cts +29 -0
  141. package/dist/scope/environment-handle.d.ts +29 -0
  142. package/dist/scope/environment.d.cts +8 -0
  143. package/dist/scope/environment.d.ts +8 -0
  144. package/dist/scope/environments.d.cts +11 -0
  145. package/dist/scope/environments.d.ts +11 -0
  146. package/dist/scope/project-handle.d.cts +22 -0
  147. package/dist/scope/project-handle.d.ts +22 -0
  148. package/dist/scope/project.d.cts +8 -0
  149. package/dist/scope/project.d.ts +8 -0
  150. package/dist/scope/projects.d.cts +10 -0
  151. package/dist/scope/projects.d.ts +10 -0
  152. package/dist/scope/rename-options.d.cts +10 -0
  153. package/dist/scope/rename-options.d.ts +10 -0
  154. package/dist/scope/rename-report.d.cts +21 -0
  155. package/dist/scope/rename-report.d.ts +21 -0
  156. package/dist/scope/scope-reference.d.cts +12 -0
  157. package/dist/scope/scope-reference.d.ts +12 -0
  158. package/dist/search/search-engine.d.cts +3 -0
  159. package/dist/search/search-engine.d.ts +3 -0
  160. package/dist/search/search-hit.d.cts +18 -0
  161. package/dist/search/search-hit.d.ts +18 -0
  162. package/dist/search/search-page.d.cts +19 -0
  163. package/dist/search/search-page.d.ts +19 -0
  164. package/dist/search/search-query.d.cts +11 -0
  165. package/dist/search/search-query.d.ts +11 -0
  166. package/dist/search/search-reach.d.cts +12 -0
  167. package/dist/search/search-reach.d.ts +12 -0
  168. package/dist/search/search-snippet.d.cts +8 -0
  169. package/dist/search/search-snippet.d.ts +8 -0
  170. package/dist/search/search.d.cts +18 -0
  171. package/dist/search/search.d.ts +18 -0
  172. package/dist/silo-options.d.cts +12 -0
  173. package/dist/silo-options.d.ts +12 -0
  174. package/dist/silo.d.cts +42 -0
  175. package/dist/silo.d.ts +42 -0
  176. package/dist/transport/abort-signals.d.cts +23 -0
  177. package/dist/transport/abort-signals.d.ts +23 -0
  178. package/dist/transport/api-path.d.cts +36 -0
  179. package/dist/transport/api-path.d.ts +36 -0
  180. package/dist/transport/fetch-function.d.cts +11 -0
  181. package/dist/transport/fetch-function.d.ts +11 -0
  182. package/dist/transport/page-payload.d.cts +15 -0
  183. package/dist/transport/page-payload.d.ts +15 -0
  184. package/dist/transport/query-string.d.cts +10 -0
  185. package/dist/transport/query-string.d.ts +10 -0
  186. package/dist/transport/response-decoder.d.cts +14 -0
  187. package/dist/transport/response-decoder.d.ts +14 -0
  188. package/dist/transport/route-inventory.d.cts +22 -0
  189. package/dist/transport/route-inventory.d.ts +22 -0
  190. package/dist/transport/transport-request.d.cts +18 -0
  191. package/dist/transport/transport-request.d.ts +18 -0
  192. package/dist/transport/transport.d.cts +45 -0
  193. package/dist/transport/transport.d.ts +45 -0
  194. package/dist/variables/environment-variables.d.cts +14 -0
  195. package/dist/variables/environment-variables.d.ts +14 -0
  196. package/dist/variables/project-variables.d.cts +38 -0
  197. package/dist/variables/project-variables.d.ts +38 -0
  198. package/dist/variables/variable-declaration.d.cts +10 -0
  199. package/dist/variables/variable-declaration.d.ts +10 -0
  200. package/dist/variables/variable.d.cts +17 -0
  201. package/dist/variables/variable.d.ts +17 -0
  202. package/package.json +58 -0
package/README.md ADDED
@@ -0,0 +1,313 @@
1
+ # silo-client
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.
6
+
7
+ ```sh
8
+ npm install @org-quicko/silo-client
9
+ ```
10
+
11
+ Runs on Node 18+, Bun, Deno, browsers and workers. No dependencies.
12
+
13
+ ## Start
14
+
15
+ ```ts
16
+ import { Silo } from "@org-quicko/silo-client"
17
+
18
+ const silo = new Silo({ url: "http://localhost:8090", key: process.env.SILO_KEY })
19
+
20
+ const posts = silo.project("acme").environment("prod").collection("posts")
21
+
22
+ const page = await posts.list({ limit: 10 })
23
+ for (const post of page.entries) {
24
+ console.log(post.fields.title)
25
+ }
26
+ ```
27
+
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.
34
+
35
+ ## Entries
36
+
37
+ Name your fields and the collection is typed.
38
+
39
+ ```ts
40
+ interface Post {
41
+ title: string
42
+ status: "draft" | "published"
43
+ tags: string[]
44
+ }
45
+
46
+ const posts = silo.project("acme").environment("prod").collection<Post>("posts")
47
+
48
+ const created = await posts.create({ title: "Hello", status: "draft", tags: [] })
49
+
50
+ const draft = await posts.edit(created.id)
51
+ draft.fields.status = "published"
52
+ await draft.save()
53
+ ```
54
+
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
+
59
+ `get` and `edit` answer different things, and the difference matters.
60
+
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 |
65
+
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.
70
+
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.
73
+
74
+ ```ts
75
+ await posts.editable.get(id)
76
+ await posts.editable.list({ limit: 50 })
77
+ for await (const post of posts.editable.all()) { }
78
+ ```
79
+
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.
83
+
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.
87
+
88
+ ## Queries
89
+
90
+ Filters are built, and a typed collection types them.
91
+
92
+ ```ts
93
+ import { Filter, Sort } from "@org-quicko/silo-client"
94
+
95
+ const page = await posts.list({
96
+ where: posts.filter.field("status").equals("published")
97
+ .and(posts.filter.each("tags").equals("release")),
98
+ sort: Sort.recentlyUpdated(),
99
+ limit: 20,
100
+ })
101
+ ```
102
+
103
+ `field` addresses your own fields, `each` addresses every element of an array,
104
+ and `meta` addresses the envelope.
105
+
106
+ ```ts
107
+ posts.filter.field("author.name").contains("ada")
108
+ posts.filter.each("tags").equals("release")
109
+ Filter.meta("updated_at").greaterThan("2026-01-01T00:00:00Z")
110
+ ```
111
+
112
+ 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.
116
+
117
+ Two spellings of a wildcard mean two different things, which is why `each` is
118
+ its own call:
119
+
120
+ ```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"
123
+ ```
124
+
125
+ ## Pagination
126
+
127
+ ```ts
128
+ const first = await posts.list({ limit: 25 })
129
+ first.total // 137
130
+ first.pageNumber // 1
131
+ first.hasMore // true
132
+
133
+ const second = await first.next()
134
+ ```
135
+
136
+ 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.
140
+
141
+ Every page is iterable, and two iterators page for you.
142
+
143
+ ```ts
144
+ for (const entry of page) { }
145
+
146
+ for await (const entry of posts.all({ where })) { }
147
+ for await (const page of posts.pages({ limit: 100 })) { }
148
+ ```
149
+
150
+ Offset paging over data being written is not a snapshot. Sort by something
151
+ stable when that matters.
152
+
153
+ ## Media
154
+
155
+ ```ts
156
+ import { MediaReference } from "@org-quicko/silo-client"
157
+
158
+ const asset = await silo.media.upload({
159
+ bytes,
160
+ filename: "hero.png",
161
+ contentType: "image/png",
162
+ folder: "heroes",
163
+ })
164
+
165
+ await posts.create({
166
+ title: "Hello",
167
+ status: "draft",
168
+ tags: [],
169
+ cover: MediaReference.of(asset.id),
170
+ })
171
+ ```
172
+
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.
176
+
177
+ ```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
188
+ ```
189
+
190
+ Folders, and a bulk delete capped at 100 ids:
191
+
192
+ ```ts
193
+ 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 })
197
+
198
+ const report = await silo.media.deleteMany(ids, { force: true })
199
+ report.deleted
200
+ report.failed
201
+ ```
202
+
203
+ ## Variables
204
+
205
+ A variable is declared once per project and valued per environment.
206
+
207
+ ```ts
208
+ const acme = silo.project("acme")
209
+ await acme.variables.declare("API_URL", { environment: "prod", value: "https://api.acme.com" })
210
+
211
+ const environment = acme.environment("prod")
212
+ await environment.variables.list()
213
+ await environment.variables.set("API_URL", "https://api.acme.com")
214
+ await environment.variables.unset("API_URL")
215
+ ```
216
+
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.
220
+
221
+ ## Search
222
+
223
+ The reach is whatever you call it on, so a missing argument cannot widen a
224
+ search.
225
+
226
+ ```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
230
+ ```
231
+
232
+ A hit says where it was found and quotes why it matched.
233
+
234
+ ```ts
235
+ const results = await silo.search({ query: "pricing" })
236
+ results.hits[0].collection // "posts"
237
+ results.hits[0].snippets // [{ path, before, match, after }]
238
+ results.engine // "fts5" when the index answered, "scan" when it walked
239
+ ```
240
+
241
+ ## Errors
242
+
243
+ One class per failure, so you branch on the type.
244
+
245
+ ```ts
246
+ import { ConflictError, ValidationFailedError, NetworkError } from "@org-quicko/silo-client"
247
+
248
+ try {
249
+ await draft.save()
250
+ } catch (error) {
251
+ if (error instanceof ConflictError) {
252
+ await draft.refresh() // somebody else wrote first
253
+ } else if (error instanceof ValidationFailedError) {
254
+ error.details // [{ path: "/title", message }]
255
+ }
256
+ }
257
+ ```
258
+
259
+ `SiloError` is the base for anything silo refused: `ValidationFailedError`,
260
+ `UnauthorizedError`, `ForbiddenError`, `NotFoundError`, `ConflictError`,
261
+ `MediaInUseError`, `MediaDeleteStalledError` and `InternalError`.
262
+
263
+ `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.
266
+
267
+ ## Cancellation
268
+
269
+ Every call takes the same last argument.
270
+
271
+ ```ts
272
+ await posts.list({ limit: 20 }, { signal: controller.signal })
273
+ await posts.get(id, { timeoutMilliseconds: 2_000 })
274
+ ```
275
+
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.
279
+
280
+ ## Anonymous reads
281
+
282
+ A key is optional. Without one you reach the collections whose schema does not
283
+ set `x-silo-auth`.
284
+
285
+ ```ts
286
+ const silo = new Silo({ url: "https://cms.example.com" })
287
+ ```
288
+
289
+ ## What this client does not reach
290
+
291
+ Keys, claims, plugins, export and import, settings, audit and observability.
292
+ 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
294
+ covers and every route it leaves out, and a test holds that list against the
295
+ server's own registrations.
296
+
297
+ ## Development
298
+
299
+ ```sh
300
+ bun install # from the repo root
301
+ bun test
302
+ bun run typecheck
303
+ bun run build
304
+ bun run test:packaged # packs the tarball and consumes it from Node, Bun and a browser build
305
+ ```
306
+
307
+ The design and the reasoning behind it are in
308
+ [docs/design/api-client.md](../../docs/design/api-client.md). The routes are in
309
+ [docs/guide/http-api.md](../../docs/guide/http-api.md).
310
+
311
+ ## License
312
+
313
+ AGPL-3.0-or-later
@@ -0,0 +1,10 @@
1
+ import type { JsonSchema } from "./json-schema.cjs";
2
+ /** One collection with its schema, bundled so `silo://` refs are already
3
+ * resolved. What `create`, `schema.get()`, `schema.put()` and
4
+ * `environment.schemas()` all answer. No mapping needed: every key here is
5
+ * already the wire's own spelling. */
6
+ export interface CollectionDefinition {
7
+ readonly id: string;
8
+ readonly name: string;
9
+ readonly schema: JsonSchema;
10
+ }
@@ -0,0 +1,10 @@
1
+ import type { JsonSchema } from "./json-schema.js";
2
+ /** One collection with its schema, bundled so `silo://` refs are already
3
+ * resolved. What `create`, `schema.get()`, `schema.put()` and
4
+ * `environment.schemas()` all answer. No mapping needed: every key here is
5
+ * already the wire's own spelling. */
6
+ export interface CollectionDefinition {
7
+ readonly id: string;
8
+ readonly name: string;
9
+ readonly schema: JsonSchema;
10
+ }
@@ -0,0 +1,54 @@
1
+ import { Entry } from "../entries/entry.cjs";
2
+ import type { EntryListQuery } from "../entries/entry-list-query.cjs";
3
+ import type { EntryPage } from "../entries/entry-page.cjs";
4
+ import type { EntryPageStream } from "../entries/entry-page-stream.cjs";
5
+ import { EntryReader } from "../entries/entry-reader.cjs";
6
+ import type { EntryStream } from "../entries/entry-stream.cjs";
7
+ import { ResolvedEntry } from "../entries/resolved-entry.cjs";
8
+ import { TypedFilter } from "../query/typed-filter.cjs";
9
+ import type { RequestOptions } from "../request-options.cjs";
10
+ import { RenameReport } from "../scope/rename-report.cjs";
11
+ import type { RenameOptions } from "../scope/rename-options.cjs";
12
+ import type { ScopeReference } from "../scope/scope-reference.cjs";
13
+ import type { SearchPage } from "../search/search-page.cjs";
14
+ import type { SearchQuery } from "../search/search-query.cjs";
15
+ import { CollectionSchema } from "./collection-schema.cjs";
16
+ /**
17
+ * One collection, typed to its fields:
18
+ * `environment.collection<Post>("posts")`.
19
+ *
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`.
24
+ */
25
+ export declare class CollectionHandle<Fields = Record<string, unknown>> {
26
+ private readonly scope;
27
+ readonly name: string;
28
+ readonly filter: TypedFilter<Fields>;
29
+ 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;
37
+ 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>>;
45
+ create(fields: Fields, options?: RequestOptions): Promise<Entry<Fields>>;
46
+ /** A full replace, which is what the route is: send every field. */
47
+ replace(id: string, rev: number, fields: Fields, options?: RequestOptions): Promise<Entry<Fields>>;
48
+ delete(id: string, rev: number, options?: RequestOptions): Promise<void>;
49
+ search(query: SearchQuery, options?: RequestOptions): Promise<SearchPage>;
50
+ rename(name: string, options?: RenameOptions): Promise<RenameReport>;
51
+ /** 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. */
53
+ private write;
54
+ }
@@ -0,0 +1,54 @@
1
+ import { Entry } from "../entries/entry.js";
2
+ import type { EntryListQuery } from "../entries/entry-list-query.js";
3
+ import type { EntryPage } from "../entries/entry-page.js";
4
+ import type { EntryPageStream } from "../entries/entry-page-stream.js";
5
+ import { EntryReader } from "../entries/entry-reader.js";
6
+ import type { EntryStream } from "../entries/entry-stream.js";
7
+ import { ResolvedEntry } from "../entries/resolved-entry.js";
8
+ import { TypedFilter } from "../query/typed-filter.js";
9
+ import type { RequestOptions } from "../request-options.js";
10
+ import { RenameReport } from "../scope/rename-report.js";
11
+ import type { RenameOptions } from "../scope/rename-options.js";
12
+ import type { ScopeReference } from "../scope/scope-reference.js";
13
+ import type { SearchPage } from "../search/search-page.js";
14
+ import type { SearchQuery } from "../search/search-query.js";
15
+ import { CollectionSchema } from "./collection-schema.js";
16
+ /**
17
+ * One collection, typed to its fields:
18
+ * `environment.collection<Post>("posts")`.
19
+ *
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`.
24
+ */
25
+ export declare class CollectionHandle<Fields = Record<string, unknown>> {
26
+ private readonly scope;
27
+ readonly name: string;
28
+ readonly filter: TypedFilter<Fields>;
29
+ 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;
37
+ 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>>;
45
+ create(fields: Fields, options?: RequestOptions): Promise<Entry<Fields>>;
46
+ /** A full replace, which is what the route is: send every field. */
47
+ replace(id: string, rev: number, fields: Fields, options?: RequestOptions): Promise<Entry<Fields>>;
48
+ delete(id: string, rev: number, options?: RequestOptions): Promise<void>;
49
+ search(query: SearchQuery, options?: RequestOptions): Promise<SearchPage>;
50
+ rename(name: string, options?: RenameOptions): Promise<RenameReport>;
51
+ /** 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. */
53
+ private write;
54
+ }
@@ -0,0 +1,16 @@
1
+ import type { RequestOptions } from "../request-options.cjs";
2
+ import type { DeleteOptions } from "../scope/delete-options.cjs";
3
+ import type { ScopeReference } from "../scope/scope-reference.cjs";
4
+ import type { CollectionDefinition } from "./collection-definition.cjs";
5
+ import type { JsonSchema } from "./json-schema.cjs";
6
+ /** One collection's schema, bundled on every read: `get`, `put`,
7
+ * `delete`. Deleting the schema deletes the collection — the only path the
8
+ * server exposes for that. */
9
+ export declare class CollectionSchema {
10
+ private readonly scope;
11
+ private readonly name;
12
+ constructor(scope: ScopeReference, name: string);
13
+ get(options?: RequestOptions): Promise<CollectionDefinition>;
14
+ put(schema: JsonSchema, options?: RequestOptions): Promise<CollectionDefinition>;
15
+ delete(options?: DeleteOptions): Promise<void>;
16
+ }
@@ -0,0 +1,16 @@
1
+ import type { RequestOptions } from "../request-options.js";
2
+ import type { DeleteOptions } from "../scope/delete-options.js";
3
+ import type { ScopeReference } from "../scope/scope-reference.js";
4
+ import type { CollectionDefinition } from "./collection-definition.js";
5
+ import type { JsonSchema } from "./json-schema.js";
6
+ /** One collection's schema, bundled on every read: `get`, `put`,
7
+ * `delete`. Deleting the schema deletes the collection — the only path the
8
+ * server exposes for that. */
9
+ export declare class CollectionSchema {
10
+ private readonly scope;
11
+ private readonly name;
12
+ constructor(scope: ScopeReference, name: string);
13
+ get(options?: RequestOptions): Promise<CollectionDefinition>;
14
+ put(schema: JsonSchema, options?: RequestOptions): Promise<CollectionDefinition>;
15
+ delete(options?: DeleteOptions): Promise<void>;
16
+ }
@@ -0,0 +1,10 @@
1
+ /** One collection as the listing answers it: no schema, which is the one
2
+ * thing a list of collections never needs to draw. */
3
+ export interface CollectionSummary {
4
+ readonly id: string;
5
+ readonly name: string;
6
+ readonly entries: number;
7
+ readonly requiresAuth: boolean;
8
+ readonly createdAt: Date;
9
+ readonly updatedAt: Date;
10
+ }
@@ -0,0 +1,10 @@
1
+ /** One collection as the listing answers it: no schema, which is the one
2
+ * thing a list of collections never needs to draw. */
3
+ export interface CollectionSummary {
4
+ readonly id: string;
5
+ readonly name: string;
6
+ readonly entries: number;
7
+ readonly requiresAuth: boolean;
8
+ readonly createdAt: Date;
9
+ readonly updatedAt: Date;
10
+ }
@@ -0,0 +1,19 @@
1
+ import type { RequestOptions } from "../request-options.cjs";
2
+ import type { ScopeReference } from "../scope/scope-reference.cjs";
3
+ import type { CollectionDefinition } from "./collection-definition.cjs";
4
+ import type { CollectionSummary } from "./collection-summary.cjs";
5
+ import type { JsonSchema } from "./json-schema.cjs";
6
+ /** Collections within one environment: `list` answers summaries and no
7
+ * schema, `create` answers the new collection's bundled definition. */
8
+ export declare class Collections {
9
+ private readonly scope;
10
+ constructor(scope: ScopeReference);
11
+ list(options?: RequestOptions): Promise<CollectionSummary[]>;
12
+ create(name: string, schema: JsonSchema, options?: RequestOptions): Promise<CollectionDefinition>;
13
+ 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
+ }
@@ -0,0 +1,19 @@
1
+ import type { RequestOptions } from "../request-options.js";
2
+ import type { ScopeReference } from "../scope/scope-reference.js";
3
+ import type { CollectionDefinition } from "./collection-definition.js";
4
+ import type { CollectionSummary } from "./collection-summary.js";
5
+ import type { JsonSchema } from "./json-schema.js";
6
+ /** Collections within one environment: `list` answers summaries and no
7
+ * schema, `create` answers the new collection's bundled definition. */
8
+ export declare class Collections {
9
+ private readonly scope;
10
+ constructor(scope: ScopeReference);
11
+ list(options?: RequestOptions): Promise<CollectionSummary[]>;
12
+ create(name: string, schema: JsonSchema, options?: RequestOptions): Promise<CollectionDefinition>;
13
+ 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
+ }
@@ -0,0 +1,9 @@
1
+ /** A JSON Schema document, loosely typed: this client validates nothing
2
+ * locally and passes a schema through to the server exactly as given. */
3
+ export interface JsonSchema {
4
+ type?: string;
5
+ properties?: Record<string, JsonSchema>;
6
+ required?: string[];
7
+ items?: JsonSchema | JsonSchema[];
8
+ [keyword: string]: unknown;
9
+ }
@@ -0,0 +1,9 @@
1
+ /** A JSON Schema document, loosely typed: this client validates nothing
2
+ * locally and passes a schema through to the server exactly as given. */
3
+ export interface JsonSchema {
4
+ type?: string;
5
+ properties?: Record<string, JsonSchema>;
6
+ required?: string[];
7
+ items?: JsonSchema | JsonSchema[];
8
+ [keyword: string]: unknown;
9
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The five field names the server strips from every entry response before
3
+ * answering, verified against `EntryUtils.toApiResponse`. A schema
4
+ * declaring one still validates and stores it; it just never comes back.
5
+ */
6
+ export declare class ReservedFieldNames {
7
+ static readonly all: readonly string[];
8
+ static isReserved(name: string): boolean;
9
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The five field names the server strips from every entry response before
3
+ * answering, verified against `EntryUtils.toApiResponse`. A schema
4
+ * declaring one still validates and stores it; it just never comes back.
5
+ */
6
+ export declare class ReservedFieldNames {
7
+ static readonly all: readonly string[];
8
+ static isReserved(name: string): boolean;
9
+ }
@@ -0,0 +1,31 @@
1
+ import type { RequestOptions } from "../request-options.cjs";
2
+ import type { Transport } from "../transport/transport.cjs";
3
+ import type { EntryPayload } from "./entry-payload.cjs";
4
+ /** What an entry needs to address itself: the transport, and the scope and
5
+ * collection its id lives in. */
6
+ export interface EntryContext {
7
+ readonly transport: Transport;
8
+ readonly project: string;
9
+ readonly environment: string;
10
+ readonly collection: string;
11
+ }
12
+ /**
13
+ * What every entry has regardless of whether it is editable: identity,
14
+ * timestamps, its fields, a plain snapshot, and `delete()` — which needs
15
+ * only an id and a rev, not the content. `rev`/`createdAt`/`updatedAt`/
16
+ * `fields` are declared `readonly` here; {@link Entry} is the one subclass
17
+ * that redeclares them mutable, since it is the one subclass with something
18
+ * that legitimately changes them.
19
+ */
20
+ export declare abstract class EntryBase<Fields> {
21
+ protected readonly context: EntryContext;
22
+ readonly id: string;
23
+ readonly rev: number;
24
+ readonly createdAt: Date;
25
+ readonly updatedAt: Date;
26
+ readonly fields: Fields;
27
+ constructor(context: EntryContext, payload: EntryPayload);
28
+ /** The flat wire shape, for logging or React state. */
29
+ toJSON(): EntryPayload;
30
+ delete(options?: RequestOptions): Promise<void>;
31
+ }
@@ -0,0 +1,31 @@
1
+ import type { RequestOptions } from "../request-options.js";
2
+ import type { Transport } from "../transport/transport.js";
3
+ import type { EntryPayload } from "./entry-payload.js";
4
+ /** What an entry needs to address itself: the transport, and the scope and
5
+ * collection its id lives in. */
6
+ export interface EntryContext {
7
+ readonly transport: Transport;
8
+ readonly project: string;
9
+ readonly environment: string;
10
+ readonly collection: string;
11
+ }
12
+ /**
13
+ * What every entry has regardless of whether it is editable: identity,
14
+ * timestamps, its fields, a plain snapshot, and `delete()` — which needs
15
+ * only an id and a rev, not the content. `rev`/`createdAt`/`updatedAt`/
16
+ * `fields` are declared `readonly` here; {@link Entry} is the one subclass
17
+ * that redeclares them mutable, since it is the one subclass with something
18
+ * that legitimately changes them.
19
+ */
20
+ export declare abstract class EntryBase<Fields> {
21
+ protected readonly context: EntryContext;
22
+ readonly id: string;
23
+ readonly rev: number;
24
+ readonly createdAt: Date;
25
+ readonly updatedAt: Date;
26
+ readonly fields: Fields;
27
+ constructor(context: EntryContext, payload: EntryPayload);
28
+ /** The flat wire shape, for logging or React state. */
29
+ toJSON(): EntryPayload;
30
+ delete(options?: RequestOptions): Promise<void>;
31
+ }
@@ -0,0 +1,11 @@
1
+ import type { FilterExpression } from "../query/filter-expression.cjs";
2
+ import type { SortTerm } from "../query/sort-term.cjs";
3
+ /** Everything `collection.list()` accepts, all optional. `where`
4
+ * builds through `collection.filter`; `sort` takes a built `SortTerm` or a
5
+ * raw string. */
6
+ export interface EntryListQuery {
7
+ where?: FilterExpression;
8
+ sort?: SortTerm | string;
9
+ limit?: number;
10
+ offset?: number;
11
+ }