js-bao 0.12.0-alpha.3 → 0.12.0-alpha.5

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
@@ -1023,6 +1023,12 @@ const electronicsCount = await Product.count({
1023
1023
 
1024
1024
  Perform complex data analysis with grouping, statistical operations, and faceting.
1025
1025
 
1026
+ `aggregate` answers one row per group: the `groupBy` values under `group`, as
1027
+ stored (`null` for a missing field), and each operation's value beside it under
1028
+ `count` or `<type>_<field>` — even when it is the only operation. Rows come back
1029
+ in `sort` order; no matching records gives `[]`. Two `groupBy` entries that
1030
+ would share a key in `group` throw `Ambiguous aggregation group keys`.
1031
+
1026
1032
  ### Basic Aggregation
1027
1033
 
1028
1034
  ```typescript
@@ -1031,7 +1037,11 @@ const categoryCounts = await Product.aggregate({
1031
1037
  groupBy: ["category"],
1032
1038
  operations: [{ type: "count" }],
1033
1039
  });
1034
- // Result: { "Electronics": 25, "Books": 18, "Clothing": 12 }
1040
+ // Result: [
1041
+ // { group: { category: "Electronics" }, count: 25 },
1042
+ // { group: { category: "Books" }, count: 18 },
1043
+ // { group: { category: "Clothing" }, count: 12 }
1044
+ // ]
1035
1045
 
1036
1046
  // Multiple statistical operations
1037
1047
  const categoryStats = await Product.aggregate({
@@ -1044,8 +1054,9 @@ const categoryStats = await Product.aggregate({
1044
1054
  { type: "max", field: "price" },
1045
1055
  ],
1046
1056
  });
1047
- // Result: {
1048
- // "Electronics": {
1057
+ // Result: [
1058
+ // {
1059
+ // group: { category: "Electronics" },
1049
1060
  // count: 25,
1050
1061
  // avg_price: 299.99,
1051
1062
  // sum_price: 7499.75,
@@ -1053,7 +1064,7 @@ const categoryStats = await Product.aggregate({
1053
1064
  // max_price: 1299.99
1054
1065
  // },
1055
1066
  // ...
1056
- // }
1067
+ // ]
1057
1068
  ```
1058
1069
 
1059
1070
  ### Multi-Dimensional Grouping
@@ -1064,15 +1075,11 @@ const salesData = await Product.aggregate({
1064
1075
  groupBy: ["category", "brand"],
1065
1076
  operations: [{ type: "count" }, { type: "sum", field: "revenue" }],
1066
1077
  });
1067
- // Result: {
1068
- // "Electronics": {
1069
- // "Apple": { count: 12, sum_revenue: 15000 },
1070
- // "Samsung": { count: 8, sum_revenue: 9500 }
1071
- // },
1072
- // "Books": {
1073
- // "Penguin": { count: 25, sum_revenue: 450 }
1074
- // }
1075
- // }
1078
+ // Result: [
1079
+ // { group: { category: "Electronics", brand: "Apple" }, count: 12, sum_revenue: 15000 },
1080
+ // { group: { category: "Electronics", brand: "Samsung" }, count: 8, sum_revenue: 9500 },
1081
+ // { group: { category: "Books", brand: "Penguin" }, count: 25, sum_revenue: 450 }
1082
+ // ]
1076
1083
  ```
1077
1084
 
1078
1085
  ### StringSet Aggregation
@@ -1105,14 +1112,21 @@ const tagCounts = await Article.aggregate({
1105
1112
  groupBy: ["tags"], // StringSet faceting
1106
1113
  operations: [{ type: "count" }],
1107
1114
  });
1108
- // Result: { "javascript": 45, "react": 32, "tutorial": 28 }
1115
+ // Result: [
1116
+ // { group: { tags: "javascript" }, count: 45 },
1117
+ // { group: { tags: "react" }, count: 32 },
1118
+ // { group: { tags: "tutorial" }, count: 28 }
1119
+ // ]
1109
1120
 
1110
1121
  // Membership-based grouping (articles that have specific tag vs don't)
1111
1122
  const urgentCounts = await Article.aggregate({
1112
1123
  groupBy: [{ field: "tags", contains: "urgent" }],
1113
1124
  operations: [{ type: "count" }],
1114
1125
  });
1115
- // Result: { "true": 5, "false": 120 }
1126
+ // Result: [
1127
+ // { group: { has_tags_urgent: "true" }, count: 5 },
1128
+ // { group: { has_tags_urgent: "false" }, count: 120 }
1129
+ // ]
1116
1130
 
1117
1131
  // Complex aggregation with filtering and sorting
1118
1132
  const topTags = await Article.aggregate({
@@ -1128,7 +1142,11 @@ const topTags = await Article.aggregate({
1128
1142
 
1129
1143
  ```typescript
1130
1144
  interface AggregationOptions {
1131
- groupBy: (string | { field: string; contains: string })[];
1145
+ groupBy: (
1146
+ | string // a plain field, or a StringSet facet
1147
+ | { field: string; contains: string } // StringSet membership: has_<field>_<contains>
1148
+ | { field: string; period: "month" | "quarter" | "year" } // date period: <field>_<period>
1149
+ )[];
1132
1150
  operations: {
1133
1151
  type: "count" | "sum" | "avg" | "min" | "max";
1134
1152
  field?: string; // Required for sum, avg, min, max
@@ -1143,6 +1161,22 @@ interface AggregationOptions {
1143
1161
  }
1144
1162
  ```
1145
1163
 
1164
+ A period level groups a `date` (or `string`) field by calendar month, quarter
1165
+ or year. Its value in `group` is `YYYY-MM`, `YYYY-Qn` or `YYYY`, read from the
1166
+ stored text with no time-zone conversion (a UTC ISO timestamp buckets by its
1167
+ UTC date); a missing or non-date value is `null`. The keys sort
1168
+ chronologically, so `sort` can name them:
1169
+
1170
+ ```typescript
1171
+ const monthly = await Transaction.aggregate({
1172
+ filter: { date: { $gte: "2025-11-01", $lte: "2026-10-31" } },
1173
+ groupBy: ["categoryId", { field: "date", period: "month" }],
1174
+ operations: [{ type: "sum", field: "amount" }, { type: "count" }],
1175
+ sort: { field: "date_month", direction: 1 },
1176
+ });
1177
+ // [{ group: { categoryId: "food", date_month: "2025-11" }, sum_amount: -50, count: 2 }, ...]
1178
+ ```
1179
+
1146
1180
  ## StringSet Fields
1147
1181
 
1148
1182
  StringSet is a special field type optimized for tag-like data, providing efficient membership queries and faceting capabilities.
@@ -1254,7 +1288,10 @@ const tagStats = await Article.aggregate({
1254
1288
  groupBy: [{ field: "tags", contains: "tutorial" }],
1255
1289
  operations: [{ type: "count" }],
1256
1290
  });
1257
- // Result: { "true": 25, "false": 75 }
1291
+ // Result: [
1292
+ // { group: { has_tags_tutorial: "true" }, count: 25 },
1293
+ // { group: { has_tags_tutorial: "false" }, count: 75 }
1294
+ // ]
1258
1295
  ```
1259
1296
 
1260
1297
  ## Database Engine Specifics
@@ -1556,9 +1593,43 @@ See the `examples/` directory for complete working examples:
1556
1593
 
1557
1594
  ## Release notes
1558
1595
 
1559
- Changes to the on-disk behaviour of `js-bao/cloudflare/do` (`DurableObjectEngine`)
1560
- that a store's owner can observe. Newest first.
1561
-
1596
+ Changes a consumer of this package can observe, including the on-disk
1597
+ behaviour of `js-bao/cloudflare/do` (`DurableObjectEngine`). Newest first.
1598
+
1599
+ - **Breaking: format 2 (large document) is removed** (0.12.0). Every
1600
+ `models/format2*` export that served the epoch overlay is gone — the record
1601
+ store and its asynchronous host, the overlay observer and fold, the epoch
1602
+ handoff and rotation, the cold start and snapshot loader, the offline window
1603
+ (`DocumentOfflineWindowError`) and offline replay — with `utils/largeDocuments`
1604
+ and `utils/overlaySql`. `InitJsBaoOptions` loses `largeDocumentStorage` and
1605
+ `InitJsBaoResult` its `format2*` hooks and `whenFormat2ProjectionSettled`;
1606
+ `connectDocument` refuses `documentFormat: 2` with `DOCUMENT_FORMAT_UNSUPPORTED`.
1607
+ The OPFS worker and broker, `Format2StoreSql`, `createFormat2Sql()`,
1608
+ `largeDocumentStoreScope` and the row cache stay: a record-log document
1609
+ (`documentFormat: 3`) runs on them. The worker answers `serve`,
1610
+ `storedEpoch`, `exportLegacy`, `importLegacy` and `dropLegacy` as unknown
1611
+ commands.
1612
+ - **Breaking: `createModelClass` is removed** (#4285). It is no longer exported
1613
+ from `js-bao` (or re-exported from `js-bao-wss-client`). Define a model as
1614
+ `class X extends BaseModel {}` and attach its schema with
1615
+ `attachAndRegisterModel(X, schema)`, as [Defining Models](#defining-models-schema-first-no-decorators)
1616
+ shows; instance methods, getters and statics go on the class body. The v1
1617
+ codegen (`js-bao-codegen`) no longer recognises a `createModelClass(...)`
1618
+ call as a model definition; it reads the class-based pattern as before.
1619
+ - **New stores get a keyed StringSet table** (#3881). A store the engine
1620
+ creates declares `stringset_index` as `PRIMARY KEY (_type, _record_id, field,
1621
+ value) WITHOUT ROWID` with `idx_ss_type_field_value` as its one named index,
1622
+ so a StringSet value costs two rows written instead of three.
1623
+ `DURABLE_OBJECT_SCHEMA_OPTIONS.memberShape` is `"keyed"`, and
1624
+ `engine.memberTableShape` answers `"keyed"` for such a store. An existing
1625
+ store keeps its record-first or field-first table; all three shapes return
1626
+ the same results. On the platform, a plain document converts at its next
1627
+ projection rebuild; databases and large documents keep their shape.
1628
+ `rebuildMemberTable` now builds the keyed table. Rollback floor: a keyed table
1629
+ has no `rowid`, so once a keyed store exists do not roll back past the
1630
+ release that first returned StringSet values sorted by code point (it reads
1631
+ `ORDER BY value`); an older release fails on a keyed table with `no such
1632
+ column: rowid`.
1562
1633
  - **New stores get a record-first StringSet table** (#3877). A store the
1563
1634
  engine creates declares `stringset_index` with `UNIQUE(_type, _record_id,
1564
1635
  field, value)` (record-first) instead of `UNIQUE(_type, field, _record_id,