@kedem/okdb 1.1.0 → 1.1.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/docs/views.md CHANGED
@@ -166,6 +166,129 @@ stats.orders.revenue.value; // → total revenue
166
166
 
167
167
  ---
168
168
 
169
+ ## Bucketing
170
+
171
+ Bucketed views partition their aggregations by a time period (or a custom grouping field), producing a per-bucket series of incremental results instead of a single global document.
172
+
173
+ ### Bucket config
174
+
175
+ Add a `bucket` object to the view definition:
176
+
177
+ ```js
178
+ {
179
+ type: string, // source type (required)
180
+ filter: object, // optional
181
+ reduce: object, // required
182
+ bucket: {
183
+ field: string, // required — dot path to the timestamp or grouping field
184
+ preset: 'time', // use preset OR project, not both
185
+ granularity: string, // required — bucket size (see below)
186
+ },
187
+ }
188
+ ```
189
+
190
+ #### `preset: 'time'` granularities
191
+
192
+ | `granularity` | Bucket size |
193
+ | ------------- | -------------------- |
194
+ | `minute` | 1 minute |
195
+ | `hour` | 1 hour |
196
+ | `day` | 1 calendar day (UTC) |
197
+ | `week` | ISO week |
198
+ | `month` | Calendar month |
199
+ | `quarter` | Calendar quarter |
200
+ | `year` | Calendar year |
201
+
202
+ The `field` value is interpreted as a Unix timestamp in milliseconds or an ISO date string.
203
+
204
+ #### Custom projection (JavaScript only)
205
+
206
+ Supply a `project` function instead of `preset` to map each field value to an arbitrary string bucket key:
207
+
208
+ ```js
209
+ bucket: {
210
+ field: 'category',
211
+ granularity: 'category',
212
+ project(value) {
213
+ return String(value ?? 'unknown').toLowerCase();
214
+ },
215
+ }
216
+ ```
217
+
218
+ Custom projections are not JSON-serializable and cannot be used in synced environments (`BUCKET_PROJECT_NOT_SYNCABLE`).
219
+
220
+ ### Create example
221
+
222
+ ```js
223
+ await env.views.create('salesByDay', {
224
+ type: 'orders',
225
+ filter: { status: 'completed' },
226
+ reduce: {
227
+ revenue: { $sum: 'amount' },
228
+ count: { $count: true },
229
+ },
230
+ bucket: {
231
+ field: 'completedAt', // Unix ms or ISO date string on each source doc
232
+ preset: 'time',
233
+ granularity: 'day',
234
+ },
235
+ });
236
+ ```
237
+
238
+ ### `range(name, options?)` — per-bucket aggregates
239
+
240
+ Returns a sparse ordered array of bucket entries. Only buckets with at least one document appear.
241
+
242
+ ```js
243
+ const rows = env.views.range('salesByDay', {
244
+ from: '2024-01-01', // ISO date / epoch ms / Date — optional lower bound
245
+ to: '2024-03-31', // inclusive upper bound — optional
246
+ granularity: 'day', // must match the view's granularity, or omit
247
+ includePartial: false, // true = include the bucket containing Date.now()
248
+ });
249
+ // → [
250
+ // { bucketKey: '2024-01-01', granularity: 'day', reducers: { revenue: { value: 1200 }, count: { value: 4 } }, refs: {} },
251
+ // { bucketKey: '2024-01-02', granularity: 'day', reducers: { revenue: { value: 890 }, count: { value: 3 } }, refs: {} },
252
+ // ]
253
+ ```
254
+
255
+ Each entry:
256
+
257
+ - `bucketKey` — canonical string key for the bucket (ISO date string for `preset:'time'`)
258
+ - `granularity` — the view's configured granularity
259
+ - `reducers` — `{ [outputField]: { value: N } }` for scalar reducers; `{ [outputField]: { [groupKey]: { value: N } } }` for `$countBy`
260
+ - `refs` — `{ [refName]: { [subField]: { value: N } } }` for `$ref` sub-views
261
+
262
+ ### `listBuckets(name, options)` — enumerate populated buckets
263
+
264
+ Returns an ordered list of bucket keys that have at least one document, with per-bucket counts.
265
+
266
+ ```js
267
+ const buckets = env.views.listBuckets('salesByDay', {
268
+ granularity: 'day', // required — must equal the view's configured granularity
269
+ from: '2024-01-01', // optional
270
+ to: '2024-12-31', // optional
271
+ limit: 10, // optional — cap result count
272
+ reverse: true, // optional — true = latest-first
273
+ });
274
+ // → [
275
+ // { bucketKey: '2024-12-31', count: 5 },
276
+ // { bucketKey: '2024-12-30', count: 12 },
277
+ // ]
278
+ ```
279
+
280
+ ### Items scoped to a bucket
281
+
282
+ When a reducer has `items: true`, pass a `bucket` option to `.items()` to restrict results to a single bucket:
283
+
284
+ ```js
285
+ const view = env.views.get('salesByDay');
286
+ const items = view.count.items({ bucket: { granularity: 'day', key: '2024-01-15' } });
287
+ // → Order[] (only orders in the 2024-01-15 bucket)
288
+ ```
289
+
290
+ ---
291
+
169
292
  ## Lifecycle
170
293
 
171
294
  ### States
@@ -255,6 +378,10 @@ env.views.getMeta(name) // → { state, clock, error, refs
255
378
  await env.views.getDefinition(name) // → stored definition | null
256
379
  await env.views.list() // → string[]
257
380
 
381
+ // Bucketing (bucketed views only)
382
+ env.views.range(name, options?) // → BucketEntry[] | null (synchronous)
383
+ env.views.listBuckets(name, options) // → { bucketKey, count }[] (synchronous)
384
+
258
385
  // Lifecycle
259
386
  await env.views.rebuild(name)
260
387
  await env.views.stop(name)
@@ -271,19 +398,22 @@ env.views.registerReducer(name, { apply })
271
398
 
272
399
  Views are scoped per environment. The `{env}` segment is the environment name (e.g. `default`).
273
400
 
274
- | Method | Path | Description |
275
- | -------- | ---------------------------------------- | ----------------------- |
276
- | `GET` | `/api/env/{env}/views` | List view names |
277
- | `POST` | `/api/env/{env}/views` | Create a view |
278
- | `GET` | `/api/env/{env}/views/{name}` | Get current view output |
279
- | `GET` | `/api/env/{env}/views/{name}/meta` | Get view state/meta |
280
- | `GET` | `/api/env/{env}/views/{name}/definition` | Get stored definition |
281
- | `DELETE` | `/api/env/{env}/views/{name}` | Remove a view |
282
- | `POST` | `/api/env/{env}/views/{name}/rebuild` | Rebuild from scratch |
283
- | `POST` | `/api/env/{env}/views/{name}/stop` | Stop processing |
284
- | `POST` | `/api/env/{env}/views/{name}/start` | Start / resume |
285
-
286
- **Create body:**
401
+ | Method | Path | Description |
402
+ | -------- | ---------------------------------------- | ------------------------------------------------ |
403
+ | `GET` | `/api/env/{env}/views` | List view names |
404
+ | `POST` | `/api/env/{env}/views` | Create a view |
405
+ | `GET` | `/api/env/{env}/views/{name}` | Get current view output |
406
+ | `GET` | `/api/env/{env}/views/{name}/meta` | Get view state/meta |
407
+ | `GET` | `/api/env/{env}/views/{name}/definition` | Get stored definition |
408
+ | `DELETE` | `/api/env/{env}/views/{name}` | Remove a view |
409
+ | `POST` | `/api/env/{env}/views/{name}/rebuild` | Rebuild from scratch |
410
+ | `POST` | `/api/env/{env}/views/{name}/stop` | Stop processing |
411
+ | `POST` | `/api/env/{env}/views/{name}/start` | Start / resume |
412
+ | `GET` | `/api/env/{env}/views/{name}/range` | Per-bucket aggregates (bucketed views only) |
413
+ | `GET` | `/api/env/{env}/views/{name}/buckets` | List populated bucket keys (bucketed views only) |
414
+ | `POST` | `/api/env/{env}/views/{name}/items` | Paginated source items for a view or sub-scope |
415
+
416
+ **Create body (standard view):**
287
417
 
288
418
  ```json
289
419
  {
@@ -297,6 +427,25 @@ Views are scoped per environment. The `{env}` segment is the environment name (e
297
427
  }
298
428
  ```
299
429
 
430
+ **Create body (bucketed view):**
431
+
432
+ ```json
433
+ {
434
+ "name": "salesByDay",
435
+ "type": "orders",
436
+ "filter": { "status": "completed" },
437
+ "reduce": {
438
+ "revenue": { "$sum": "amount" },
439
+ "count": { "$count": true }
440
+ },
441
+ "bucket": {
442
+ "field": "completedAt",
443
+ "preset": "time",
444
+ "granularity": "day"
445
+ }
446
+ }
447
+ ```
448
+
300
449
  **Delete with managed indexes:**
301
450
 
302
451
  ```json
@@ -307,7 +456,7 @@ Views are scoped per environment. The `{env}` segment is the environment name (e
307
456
 
308
457
  ## MCP
309
458
 
310
- Views are available as the `views` capability. Actions: `list_views`, `create_view`, `get_view`, `get_view_meta`, `get_view_definition`, `remove_view`, `rebuild_view`, `stop_view`, `start_view`.
459
+ Views are available as the `views` capability. Actions: `list_views`, `create_view`, `get_view`, `get_view_meta`, `get_view_definition`, `remove_view`, `rebuild_view`, `stop_view`, `start_view`, `get_view_range`, `list_view_buckets`, `list_view_items`.
311
460
 
312
461
  ---
313
462
 
@@ -335,17 +484,30 @@ View definitions are stored in the `~views` system type within their environment
335
484
 
336
485
  ## Error codes
337
486
 
338
- | Code | Cause |
339
- | ------------------------------ | ---------------------------------------------------------------------------------------------- |
340
- | `VIEW_INVALID_NAME` | Name is empty, contains whitespace, or is not a string |
341
- | `VIEW_INVALID_DEFINITION` | Definition is not an object |
342
- | `VIEW_MISSING_TYPE` | `type` field missing or not a string |
343
- | `VIEW_MISSING_REDUCE` | `reduce` field missing or empty |
344
- | `VIEW_TYPE_NOT_REGISTERED` | The source type does not exist in this environment |
345
- | `VIEW_ALREADY_EXISTS` | A view with that name already exists |
346
- | `VIEW_NOT_FOUND` | No view with that name |
347
- | `VIEW_HAS_ORPHANED_INDEXES` | Removing the view would leave auto-created indexes with no owner; pass `managedIndexes` option |
348
- | `VIEW_REF_UNSUPPORTED_REDUCER` | `$min`/`$max` used inside a `$ref` sub-view |
349
- | `VIEW_INVALID_REDUCER_NAME` | Custom reducer name does not start with `$` |
350
- | `VIEW_REDUCER_CONFLICT` | Custom reducer name conflicts with a built-in |
351
- | `VIEW_INVALID_REDUCER` | Custom reducer missing an `apply` function |
487
+ | Code | Cause |
488
+ | --------------------------------- | ----------------------------------------------------------------------------------------------------- |
489
+ | `VIEW_INVALID_NAME` | Name is empty, contains whitespace, or is not a string |
490
+ | `VIEW_INVALID_DEFINITION` | Definition is not an object |
491
+ | `VIEW_MISSING_TYPE` | `type` field missing or not a string |
492
+ | `VIEW_MISSING_REDUCE` | `reduce` field missing or empty |
493
+ | `VIEW_TYPE_NOT_REGISTERED` | The source type does not exist in this environment |
494
+ | `VIEW_ALREADY_EXISTS` | A view with that name already exists |
495
+ | `VIEW_NOT_FOUND` | No view with that name |
496
+ | `VIEW_HAS_ORPHANED_INDEXES` | Removing the view would leave auto-created indexes with no owner; pass `managedIndexes` option |
497
+ | `VIEW_REF_UNSUPPORTED_REDUCER` | `$min`/`$max` used inside a `$ref` sub-view |
498
+ | `VIEW_INVALID_REDUCER_NAME` | Custom reducer name does not start with `$` |
499
+ | `VIEW_REDUCER_CONFLICT` | Custom reducer name conflicts with a built-in |
500
+ | `VIEW_INVALID_REDUCER` | Custom reducer missing an `apply` function |
501
+ | `BUCKET_CONFIG_INVALID` | `bucket` config is missing required fields, uses an unsupported preset, or has an invalid granularity |
502
+ | `BUCKET_PROJECT_NOT_SYNCABLE` | `bucket.project` is a function and cannot be used in a synced environment; use `preset:'time'` |
503
+ | `BUCKET_INVALID_TIME_VALUE` | A document's bucket field could not be parsed as a time value |
504
+ | `VIEW_NOT_BUCKETED` | `range()` or `listBuckets()` was called on a view that has no `bucket` config |
505
+ | `VIEW_RANGE_GRANULARITY_MISMATCH` | The `granularity` option does not match the view's configured granularity |
506
+ | `VIEW_RANGE_GRANULARITY_REQUIRED` | `listBuckets()` was called without a `granularity` option |
507
+ | `VIEW_ITEMS_SCOPE_INVALID` | `scope` is not one of `view`, `reducer`, `ref`, `ref-reducer` |
508
+ | `VIEW_ITEMS_REDUCER_NOT_FOUND` | The named reducer does not exist on the view |
509
+ | `VIEW_ITEMS_REF_NOT_FOUND` | The named `$ref` slot does not exist on the view |
510
+ | `VIEW_ITEMS_GROUP_NOT_FOUND` | The named group key does not exist on the `$countBy` reducer |
511
+ | `VIEW_ITEMS_NOT_AVAILABLE` | `items()` was requested but the reducer does not have `items: true` |
512
+ | `VIEW_ITEMS_LIMIT_INVALID` | `limit` is not a non-negative integer |
513
+ | `VIEW_ITEMS_OFFSET_INVALID` | `offset` is not a non-negative integer |