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

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({
@@ -1254,7 +1268,10 @@ const tagStats = await Article.aggregate({
1254
1268
  groupBy: [{ field: "tags", contains: "tutorial" }],
1255
1269
  operations: [{ type: "count" }],
1256
1270
  });
1257
- // Result: { "true": 25, "false": 75 }
1271
+ // Result: [
1272
+ // { group: { has_tags_tutorial: "true" }, count: 25 },
1273
+ // { group: { has_tags_tutorial: "false" }, count: 75 }
1274
+ // ]
1258
1275
  ```
1259
1276
 
1260
1277
  ## Database Engine Specifics
@@ -1556,9 +1573,30 @@ See the `examples/` directory for complete working examples:
1556
1573
 
1557
1574
  ## Release notes
1558
1575
 
1559
- Changes to the on-disk behaviour of `js-bao/cloudflare/do` (`DurableObjectEngine`)
1560
- that a store's owner can observe. Newest first.
1561
-
1576
+ Changes a consumer of this package can observe, including the on-disk
1577
+ behaviour of `js-bao/cloudflare/do` (`DurableObjectEngine`). Newest first.
1578
+
1579
+ - **Breaking: `createModelClass` is removed** (#4285). It is no longer exported
1580
+ from `js-bao` (or re-exported from `js-bao-wss-client`). Define a model as
1581
+ `class X extends BaseModel {}` and attach its schema with
1582
+ `attachAndRegisterModel(X, schema)`, as [Defining Models](#defining-models-schema-first-no-decorators)
1583
+ shows; instance methods, getters and statics go on the class body. The v1
1584
+ codegen (`js-bao-codegen`) no longer recognises a `createModelClass(...)`
1585
+ call as a model definition; it reads the class-based pattern as before.
1586
+ - **New stores get a keyed StringSet table** (#3881). A store the engine
1587
+ creates declares `stringset_index` as `PRIMARY KEY (_type, _record_id, field,
1588
+ value) WITHOUT ROWID` with `idx_ss_type_field_value` as its one named index,
1589
+ so a StringSet value costs two rows written instead of three.
1590
+ `DURABLE_OBJECT_SCHEMA_OPTIONS.memberShape` is `"keyed"`, and
1591
+ `engine.memberTableShape` answers `"keyed"` for such a store. An existing
1592
+ store keeps its record-first or field-first table; all three shapes return
1593
+ the same results. On the platform, a plain document converts at its next
1594
+ projection rebuild; databases and large documents keep their shape.
1595
+ `rebuildMemberTable` now builds the keyed table. Rollback floor: a keyed table
1596
+ has no `rowid`, so once a keyed store exists do not roll back past the
1597
+ release that first returned StringSet values sorted by code point (it reads
1598
+ `ORDER BY value`); an older release fails on a keyed table with `no such
1599
+ column: rowid`.
1562
1600
  - **New stores get a record-first StringSet table** (#3877). A store the
1563
1601
  engine creates declares `stringset_index` with `UNIQUE(_type, _record_id,
1564
1602
  field, value)` (record-first) instead of `UNIQUE(_type, field, _record_id,
package/dist/browser.cjs CHANGED
@@ -8672,6 +8672,51 @@ var init_format2ClientWrite = __esm({
8672
8672
  }
8673
8673
  });
8674
8674
 
8675
+ // src/utils/aggregationRows.ts
8676
+ function aggregationGroupKey(level) {
8677
+ return typeof level === "string" ? level : `has_${level.field}_${level.contains}`;
8678
+ }
8679
+ function aggregationOperationKey(operation) {
8680
+ return operation.type === "count" ? "count" : `${operation.type}_${operation.field}`;
8681
+ }
8682
+ function positionalGroupAlias(index) {
8683
+ return `_g${index}`;
8684
+ }
8685
+ function ambiguousAggregationGroupKeysMessage(key) {
8686
+ return `Ambiguous aggregation group keys: '${key}' names more than one groupBy level`;
8687
+ }
8688
+ function findAmbiguousAggregationGroupKey(groupBy) {
8689
+ const seen = /* @__PURE__ */ new Set();
8690
+ for (const level of groupBy) {
8691
+ const key = aggregationGroupKey(level);
8692
+ if (seen.has(key)) return key;
8693
+ seen.add(key);
8694
+ }
8695
+ return null;
8696
+ }
8697
+ function aggregationRow(sqlRow, groupBy, operations, readLevel) {
8698
+ const group = {};
8699
+ groupBy.forEach((level, index) => {
8700
+ Object.defineProperty(group, aggregationGroupKey(level), {
8701
+ value: readLevel(level, index),
8702
+ enumerable: true,
8703
+ writable: true,
8704
+ configurable: true
8705
+ });
8706
+ });
8707
+ const row = { group };
8708
+ for (const operation of operations) {
8709
+ const key = aggregationOperationKey(operation);
8710
+ row[key] = sqlRow[key] ?? null;
8711
+ }
8712
+ return row;
8713
+ }
8714
+ var init_aggregationRows = __esm({
8715
+ "src/utils/aggregationRows.ts"() {
8716
+ "use strict";
8717
+ }
8718
+ });
8719
+
8675
8720
  // src/models/relationshipManager.ts
8676
8721
  function warnIfOrderFieldNullable(modelClass, orderByField, relationshipDescription) {
8677
8722
  if (ALWAYS_PRESENT_ORDER_FIELDS.has(orderByField)) return;
@@ -8957,7 +9002,7 @@ var init_ModelRegistry = __esm({
8957
9002
  * A registry for ONE `initJsBao` instance (#3039).
8958
9003
  *
8959
9004
  * The global singleton stays the registration target for
8960
- * `attachAndRegisterModel` / `createModelClass`; this is the per-instance
9005
+ * `attachAndRegisterModel`; this is the per-instance
8961
9006
  * view the ORM actually resolves schemas through, so two clients that
8962
9007
  * register different models under the same name no longer overwrite each
8963
9008
  * other's include translation and relationship validation.
@@ -9005,7 +9050,7 @@ var init_ModelRegistry = __esm({
9005
9050
  const modelName = modelClass?.modelName ?? schema?.options?.name;
9006
9051
  if (!modelName || !schema?.options?.name || !schema?.resolvedUniqueConstraints) {
9007
9052
  throw new Error(
9008
- `[initJsBao] Cannot register a model class with no name. Build it with defineModelSchema and attachAndRegisterModel (or createModelClass) so it carries a static modelName and a schema with resolvedUniqueConstraints.`
9053
+ `[initJsBao] Cannot register a model class with no name. Build it with defineModelSchema and attachAndRegisterModel so it carries a static modelName and a schema with resolvedUniqueConstraints.`
9009
9054
  );
9010
9055
  }
9011
9056
  const existing = this.models.get(modelName);
@@ -9054,7 +9099,7 @@ var init_ModelRegistry = __esm({
9054
9099
  registerModel(modelClass, options, fields) {
9055
9100
  if (!options.name) {
9056
9101
  throw new Error(
9057
- `[ModelRegistry] Model class is missing a name in its options. Ensure the schema passed to defineModelSchema/createModelClass/attachAndRegisterModel includes a 'name' property.`
9102
+ `[ModelRegistry] Model class is missing a name in its options. Ensure the schema passed to defineModelSchema/attachAndRegisterModel includes a 'name' property.`
9058
9103
  );
9059
9104
  }
9060
9105
  if (this.models.has(options.name)) {
@@ -9098,7 +9143,7 @@ var init_ModelRegistry = __esm({
9098
9143
  }
9099
9144
  return infos;
9100
9145
  }
9101
- // Helper to get model name from class (set by attachAndRegisterModel/createModelClass)
9146
+ // Helper to get model name from class (set by attachAndRegisterModel)
9102
9147
  getModelNameFromClass(modelClass) {
9103
9148
  return modelClass.modelName;
9104
9149
  }
@@ -9109,13 +9154,13 @@ var init_ModelRegistry = __esm({
9109
9154
  const modelName = this.getModelNameFromClass(modelClass);
9110
9155
  if (!modelName) {
9111
9156
  console.warn(
9112
- `[ModelRegistry] A model class provided to setExplicitModelsForSession does not have a static 'modelName' property or it's undefined. It will be ignored. Ensure attachAndRegisterModel (or createModelClass) was called for this model.`
9157
+ `[ModelRegistry] A model class provided to setExplicitModelsForSession does not have a static 'modelName' property or it's undefined. It will be ignored. Ensure attachAndRegisterModel was called for this model.`
9113
9158
  );
9114
9159
  return;
9115
9160
  }
9116
9161
  if (!this.models.has(modelName) || this.models.get(modelName) !== modelClass) {
9117
9162
  console.warn(
9118
- `[ModelRegistry] Model class with name ${modelName} provided to setExplicitModelsForSession was not found in the global registry or does not match. It will be ignored. Ensure the model file is imported and attachAndRegisterModel (or createModelClass) has run.`
9163
+ `[ModelRegistry] Model class with name ${modelName} provided to setExplicitModelsForSession was not found in the global registry or does not match. It will be ignored. Ensure the model file is imported and attachAndRegisterModel has run.`
9119
9164
  );
9120
9165
  return;
9121
9166
  }
@@ -9917,6 +9962,7 @@ var init_BaseModel = __esm({
9917
9962
  init_format2OfflineWindow();
9918
9963
  init_logger();
9919
9964
  init_queryTableKey();
9965
+ init_aggregationRows();
9920
9966
  FORMAT2_ROW_PAGE = 500;
9921
9967
  UniqueConstraintViolationError = class _UniqueConstraintViolationError extends Error {
9922
9968
  modelName;
@@ -13209,7 +13255,7 @@ var init_BaseModel = __esm({
13209
13255
  /**
13210
13256
  * Main aggregation API - performs grouping, faceting, and statistical operations
13211
13257
  * @param options Aggregation configuration with groupBy, operations, filter, limit, and sort
13212
- * @returns Nested object structure with aggregation results
13258
+ * @returns One row per group, `{ group, ...operationValues }`, in SQL order
13213
13259
  *
13214
13260
  * @example
13215
13261
  * // Simple facet count
@@ -13217,7 +13263,7 @@ var init_BaseModel = __esm({
13217
13263
  * groupBy: ['tags'],
13218
13264
  * operations: [{ type: 'count' }]
13219
13265
  * });
13220
- * // Result: { red: 15, blue: 8, green: 12 }
13266
+ * // Result: [{ group: { tags: 'red' }, count: 15 }, { group: { tags: 'blue' }, count: 8 }]
13221
13267
  *
13222
13268
  * @example
13223
13269
  * // Multi-dimensional grouping with multiple operations
@@ -13229,9 +13275,10 @@ var init_BaseModel = __esm({
13229
13275
  * { type: 'avg', field: 'score' }
13230
13276
  * ],
13231
13277
  * filter: { active: true },
13232
- * sort: { field: 'count', direction: 'desc' },
13278
+ * sort: { field: 'count', direction: -1 },
13233
13279
  * limit: 10
13234
13280
  * });
13281
+ * // Result: [{ group: { category: 'work', status: 'open' }, count: 4, sum_amount: 120, avg_score: 3.5 }, ...]
13235
13282
  *
13236
13283
  * @example
13237
13284
  * // StringSet membership grouping
@@ -13239,7 +13286,7 @@ var init_BaseModel = __esm({
13239
13286
  * groupBy: [{ field: 'tags', contains: 'urgent' }],
13240
13287
  * operations: [{ type: 'count' }]
13241
13288
  * });
13242
- * // Result: { true: 5, false: 23 }
13289
+ * // Result: [{ group: { has_tags_urgent: 'true' }, count: 5 }, { group: { has_tags_urgent: 'false' }, count: 23 }]
13243
13290
  */
13244
13291
  static async aggregate(options) {
13245
13292
  const ormContext = resolveOrmContext(this);
@@ -13289,6 +13336,10 @@ var init_BaseModel = __esm({
13289
13336
  }
13290
13337
  }
13291
13338
  }
13339
+ const ambiguousKey = findAmbiguousAggregationGroupKey(options.groupBy);
13340
+ if (ambiguousKey !== null) {
13341
+ throw new Error(ambiguousAggregationGroupKeysMessage(ambiguousKey));
13342
+ }
13292
13343
  const aggregateScope = this.documentsInScope(options.documents);
13293
13344
  const connected = new Set(
13294
13345
  this.documentsInScope()
@@ -13322,7 +13373,8 @@ var init_BaseModel = __esm({
13322
13373
  const processedResult = this.processAggregationResults(
13323
13374
  results,
13324
13375
  options,
13325
- aggregationQuery.aliasMetadata
13376
+ aggregationQuery.aliasMetadata,
13377
+ aggregationQuery.regularGroupBy
13326
13378
  );
13327
13379
  if (aggregationQuery.aliasMetadata && aggregationQuery.aliasMetadata.length > 0) {
13328
13380
  const debugMap = {};
@@ -13485,9 +13537,13 @@ var init_BaseModel = __esm({
13485
13537
  const selectParts = [];
13486
13538
  const aliasMetadata = [];
13487
13539
  const usedAliases = /* @__PURE__ */ new Set();
13488
- for (const field of regularGroupBy) {
13489
- selectParts.push(`${quotedMainTable}.${quoteIdentifier(field)}`);
13490
- }
13540
+ regularGroupBy.forEach((field, index) => {
13541
+ selectParts.push(
13542
+ `${quotedMainTable}.${quoteIdentifier(field)} AS ${quoteIdentifier(
13543
+ positionalGroupAlias(index)
13544
+ )}`
13545
+ );
13546
+ });
13491
13547
  for (const membership of stringSetMemberships) {
13492
13548
  const membershipKey = this.buildMembershipKey(
13493
13549
  membership.field,
@@ -13597,7 +13653,9 @@ var init_BaseModel = __esm({
13597
13653
  }
13598
13654
  aliasMetadata.forEach((detail) => {
13599
13655
  const quotedAlias = quoteIdentifier(detail.alias);
13600
- groupByParts.push(`${quotedAlias}.${quotedRecordIdColumn}`);
13656
+ groupByParts.push(
13657
+ `CASE WHEN ${quotedAlias}.${quotedRecordIdColumn} IS NOT NULL THEN 'true' ELSE 'false' END`
13658
+ );
13601
13659
  });
13602
13660
  if (groupByParts.length > 0) {
13603
13661
  sql += ` GROUP BY ${groupByParts.join(", ")}`;
@@ -13613,7 +13671,9 @@ var init_BaseModel = __esm({
13613
13671
  );
13614
13672
  if (membershipDetail) {
13615
13673
  sortExpression = quoteIdentifier(membershipDetail.alias);
13616
- } else if (requestedField.startsWith("sum_") || requestedField.startsWith("avg_") || requestedField.startsWith("min_") || requestedField.startsWith("max_")) {
13674
+ } else if (options.operations.some(
13675
+ (operation) => aggregationOperationKey(operation) === requestedField
13676
+ )) {
13617
13677
  sortExpression = quoteIdentifier(requestedField);
13618
13678
  } else {
13619
13679
  sortExpression = `${quotedMainTable}.${quoteIdentifier(
@@ -13627,99 +13687,50 @@ var init_BaseModel = __esm({
13627
13687
  if (options.limit) {
13628
13688
  sql += ` LIMIT ${options.limit}`;
13629
13689
  }
13630
- return { sql, params, aliasMetadata };
13631
- }
13632
- /**
13633
- * Read one row's operation values, collapsing a single-operation aggregate
13634
- * to the bare value of that operation.
13635
- *
13636
- * This is the platform's one aggregate leaf rule, shared with the DO engine's
13637
- * `_extractOpValue` (`engines/cloudflare/createDocumentDO.ts`, behind
13638
- * database aggregates, registered `aggregate` operations, workflow pipeline
13639
- * steps and `connectDoDb` bindings) and its mirror `aggregateRowsToResult`
13640
- * in the platform's `src/document-projection.ts` (the document HTTP
13641
- * aggregate). These model statics used to collapse a lone `count` only and
13642
- * keep `<type>_<field>` for a lone `sum`/`avg`/`min`/`max`, so the same
13643
- * declared aggregation answered differently depending on the path taken;
13644
- * they were the outlier and moved to the shared rule in #3076 (js-bao 0.10.0).
13645
- *
13646
- * Both result-shaping branches below call this, so the StringSet-facet path
13647
- * and the general path cannot drift apart again. Pinned by
13648
- * `cli/tests/unit/aggregate-shaper-divergence-3076.test.ts` and, end to end
13649
- * against the HTTP aggregate, by
13650
- * `tests/api/http/aggregate-shape-convergence-3076.test.ts`.
13651
- */
13652
- static extractAggregationLeaf(row, operations) {
13653
- const valueFor = (operation) => operation.type === "count" ? row.count : row[`${operation.type}_${operation.field}`];
13654
- if (operations.length === 1) return valueFor(operations[0]);
13655
- const leaf = {};
13656
- for (const operation of operations) {
13657
- if (operation.type === "count") leaf.count = row.count;
13658
- else leaf[`${operation.type}_${operation.field}`] = valueFor(operation);
13659
- }
13660
- return leaf;
13690
+ return { sql, params, aliasMetadata, regularGroupBy };
13661
13691
  }
13662
13692
  /**
13663
- * Process aggregation results into nested structure.
13693
+ * Shape SQL rows into aggregation rows: `{ group, ...operationValues }`, in
13694
+ * the order SQL returned them.
13664
13695
  *
13665
- * The leaf under the last group value follows the one platform rule — see
13666
- * {@link BaseModel.extractAggregationLeaf}.
13696
+ * A facet plan's rows carry the member value as `group_key`; a regular
13697
+ * plan's carry each plain level under its positional alias (`_g<i>`, `i`
13698
+ * its index in `regularGroupBy`, the plan's selected plain fields) and each
13699
+ * membership under the alias `aliasMetadata` records for it. A level the
13700
+ * plan did not select (a StringSet facet beside other levels) is `null`.
13701
+ * Without `regularGroupBy`, every string level counts as a plain one. Shared with the DO engine's
13702
+ * `_processAggregationResults` (`engines/cloudflare/createDocumentDO.ts`) and
13703
+ * the platform's document HTTP aggregate (`aggregateRowsToResult`); the three
13704
+ * are pinned against each other by
13705
+ * `cli/tests/unit/aggregate-shaper-divergence-3076.test.ts`.
13667
13706
  */
13668
- static processAggregationResults(results, options, aliasMetadata) {
13669
- if (results.length === 0) {
13670
- return {};
13671
- }
13707
+ static processAggregationResults(results, options, aliasMetadata, regularGroupBy) {
13672
13708
  const membershipAliasLookup = /* @__PURE__ */ new Map();
13673
13709
  if (aliasMetadata) {
13674
13710
  for (const detail of aliasMetadata) {
13675
13711
  membershipAliasLookup.set(detail.membershipKey, detail.alias);
13676
- membershipAliasLookup.set(detail.originalAlias, detail.alias);
13677
13712
  }
13678
13713
  }
13679
- if (options.groupBy.length === 1 && typeof options.groupBy[0] === "string" && results[0].hasOwnProperty("group_key")) {
13680
- const facetResult = {};
13681
- for (const row of results) {
13682
- facetResult[row.group_key] = this.extractAggregationLeaf(
13683
- row,
13684
- options.operations
13714
+ const facet = options.groupBy.length === 1 && typeof options.groupBy[0] === "string" && results.length > 0 && Object.prototype.hasOwnProperty.call(results[0], "group_key");
13715
+ return results.map((row) => {
13716
+ let plainIndex = 0;
13717
+ return aggregationRow(row, options.groupBy, options.operations, (level) => {
13718
+ if (facet) return row.group_key ?? null;
13719
+ if (typeof level === "string") {
13720
+ const index = regularGroupBy ? regularGroupBy.indexOf(level) : plainIndex++;
13721
+ return index < 0 ? null : row[positionalGroupAlias(index)] ?? null;
13722
+ }
13723
+ const resolvedAlias = membershipAliasLookup.get(
13724
+ this.buildMembershipKey(level.field, level.contains)
13685
13725
  );
13686
- }
13687
- return facetResult;
13688
- }
13689
- const nestedResult = {};
13690
- for (const row of results) {
13691
- let current = nestedResult;
13692
- for (let i = 0; i < options.groupBy.length; i++) {
13693
- const groupBy = options.groupBy[i];
13694
- let key;
13695
- if (typeof groupBy === "string") {
13696
- key = String(row[groupBy]);
13697
- } else {
13698
- const membershipKey = this.buildMembershipKey(
13699
- groupBy.field,
13700
- groupBy.contains
13701
- );
13702
- const resolvedAlias = membershipAliasLookup.get(membershipKey) || membershipAliasLookup.get(
13703
- `has_${groupBy.field}_${groupBy.contains}`
13726
+ if (!resolvedAlias) {
13727
+ throw new Error(
13728
+ `Missing alias metadata for StringSet membership ${level.field}:${level.contains}`
13704
13729
  );
13705
- if (!resolvedAlias) {
13706
- throw new Error(
13707
- `Missing alias metadata for StringSet membership ${groupBy.field}:${groupBy.contains}`
13708
- );
13709
- }
13710
- key = row[resolvedAlias];
13711
13730
  }
13712
- if (i === options.groupBy.length - 1) {
13713
- current[key] = this.extractAggregationLeaf(row, options.operations);
13714
- } else {
13715
- if (!current[key]) {
13716
- current[key] = {};
13717
- }
13718
- current = current[key];
13719
- }
13720
- }
13721
- }
13722
- return nestedResult;
13731
+ return row[resolvedAlias] ?? null;
13732
+ });
13733
+ });
13723
13734
  }
13724
13735
  /**
13725
13736
  * Document-style query API - returns single result or null
@@ -14568,7 +14579,6 @@ __export(browser_exports, {
14568
14579
  compareRecordIds: () => compareRecordIds,
14569
14580
  completeOverlayEntries: () => completeOverlayEntries,
14570
14581
  configuredOfflineWindowDays: () => configuredOfflineWindowDays,
14571
- createModelClass: () => createModelClass,
14572
14582
  decideDeferredReplay: () => decideDeferredReplay,
14573
14583
  decodeFoldedState: () => decodeFoldedState,
14574
14584
  decodeFormat2StoreError: () => decodeFormat2StoreError,
@@ -20422,7 +20432,7 @@ var DocumentManagerImpl = class {
20422
20432
  const boundTo = modelClass?._ormContext;
20423
20433
  if (boundTo && boundTo !== this.ormContext && !boundTo.destroyed) {
20424
20434
  throw new Error(
20425
- `[initJsBao] Model '${modelName}' is bound to live js-bao instance #${boundTo.instanceId}. Give instance #${this.ormContext.instanceId} its own model class (a fresh createModelClass / attachAndRegisterModel result).`
20435
+ `[initJsBao] Model '${modelName}' is bound to live js-bao instance #${boundTo.instanceId}. Give instance #${this.ormContext.instanceId} its own model class (a fresh class from defineModelSchema + attachAndRegisterModel).`
20426
20436
  );
20427
20437
  }
20428
20438
  const { added } = this.modelRegistry.adoptModel(modelClass, {
@@ -20575,7 +20585,7 @@ function bindModelClass(ctx, modelName, modelClass, onLiveConflict) {
20575
20585
  }
20576
20586
  if (onLiveConflict === "throw") {
20577
20587
  throw new Error(
20578
- `[initJsBao] Model '${modelName}' is bound to live js-bao instance #${current.instanceId}. Give instance #${ctx.instanceId} its own model class (a fresh createModelClass / attachAndRegisterModel result).`
20588
+ `[initJsBao] Model '${modelName}' is bound to live js-bao instance #${current.instanceId}. Give instance #${ctx.instanceId} its own model class (a fresh class from defineModelSchema + attachAndRegisterModel).`
20579
20589
  );
20580
20590
  }
20581
20591
  console.warn(
@@ -20812,36 +20822,6 @@ function buildDefinedModelSchema(input) {
20812
20822
  };
20813
20823
  return schema;
20814
20824
  }
20815
- function createModelClass(config) {
20816
- const { schema, baseClass = BaseModel2, methods, statics } = config;
20817
- const ParentClass = baseClass;
20818
- class GeneratedModel extends ParentClass {
20819
- }
20820
- Object.defineProperty(GeneratedModel, "name", {
20821
- value: schema.options.name,
20822
- configurable: true
20823
- });
20824
- const methodDefinitions = methods?.();
20825
- if (methodDefinitions) {
20826
- const descriptors = Object.getOwnPropertyDescriptors(methodDefinitions);
20827
- for (const [key, descriptor] of Object.entries(descriptors)) {
20828
- if (key === "constructor") continue;
20829
- Object.defineProperty(GeneratedModel.prototype, key, descriptor);
20830
- }
20831
- }
20832
- if (statics) {
20833
- const staticDefinitions = statics(
20834
- GeneratedModel
20835
- );
20836
- const descriptors = Object.getOwnPropertyDescriptors(staticDefinitions);
20837
- for (const [key, descriptor] of Object.entries(descriptors)) {
20838
- if (key === "prototype") continue;
20839
- Object.defineProperty(GeneratedModel, key, descriptor);
20840
- }
20841
- }
20842
- attachAndRegisterModel(GeneratedModel, schema);
20843
- return GeneratedModel;
20844
- }
20845
20825
  function attachSchemaToClass(modelClass, schema) {
20846
20826
  modelClass.schema = schema;
20847
20827
  modelClass.modelName = schema.options.name;