@decaf-ts/for-fabric 0.13.9 → 0.13.10

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.
@@ -120,114 +120,104 @@ class FabricContractAdapter extends for_couchdb_1.CouchDBAdapter {
120
120
  .update(FabricContractAdapter.stableStringify(relevant))
121
121
  .digest("base64url");
122
122
  }
123
- static parseSyntheticPrivateBookmark(bookmark) {
124
- if (typeof bookmark !== "string")
125
- return undefined;
126
- if (!bookmark.startsWith(FabricContractAdapter.PRIVATE_BOOKMARK_PREFIX))
127
- return undefined;
128
- const raw = bookmark.slice(FabricContractAdapter.PRIVATE_BOOKMARK_PREFIX.length);
129
- try {
130
- const parsed = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
131
- if (!parsed ||
132
- typeof parsed !== "object" ||
133
- typeof parsed.sortField !== "string" ||
134
- typeof parsed.direction !== "string" ||
135
- typeof parsed.idField !== "string" ||
136
- typeof parsed.lastId !== "string"
137
- || typeof parsed.queryHash !== "string") {
138
- return undefined;
139
- }
140
- return {
141
- sortField: parsed.sortField,
142
- direction: parsed.direction === "desc" ? "desc" : "asc",
143
- idField: parsed.idField,
144
- lastValue: parsed.lastValue,
145
- lastId: parsed.lastId,
146
- queryHash: parsed.queryHash,
147
- };
148
- }
149
- catch {
150
- return undefined;
151
- }
152
- }
153
- static buildSyntheticPrivateBookmark(cursor) {
154
- return `${FabricContractAdapter.PRIVATE_BOOKMARK_PREFIX}${Buffer.from(JSON.stringify(cursor)).toString("base64url")}`;
155
- }
156
- static buildPrivateCursorSelector(selector, sortField, direction, cursor) {
123
+ static buildPrivateContinuationSelector(selector, sortField, direction, idField, lastValue, lastId, variant) {
157
124
  const cmp = direction === "desc" ? "$lt" : "$gt";
158
- if (cursor.idField === sortField) {
125
+ const idCmp = direction === "desc" ? "$lt" : "$gt";
126
+ if (idField === sortField) {
159
127
  const continuation = {
160
128
  [sortField]: {
161
- [cmp]: cursor.lastValue,
129
+ [cmp]: lastValue,
162
130
  },
163
131
  };
164
- if (!Object.keys(selector || {}).length)
132
+ if (!selector || !Object.keys(selector).length) {
165
133
  return continuation;
134
+ }
166
135
  return {
167
136
  $and: [selector, continuation],
168
137
  };
169
138
  }
170
- const continuation = {
171
- $or: [
139
+ const clauses = variant === "same"
140
+ ? [
172
141
  {
173
142
  [sortField]: {
174
- [cmp]: cursor.lastValue,
143
+ $eq: lastValue,
175
144
  },
176
145
  },
177
146
  {
178
- $and: [
179
- {
180
- [sortField]: {
181
- $eq: cursor.lastValue,
182
- },
183
- },
184
- {
185
- [cursor.idField]: {
186
- [cmp]: cursor.lastId,
187
- },
188
- },
189
- ],
147
+ [idField]: {
148
+ [idCmp]: lastId,
149
+ },
190
150
  },
191
- ],
192
- };
193
- if (!Object.keys(selector || {}).length)
194
- return continuation;
151
+ ]
152
+ : [
153
+ {
154
+ [sortField]: {
155
+ [cmp]: lastValue,
156
+ },
157
+ },
158
+ ];
159
+ if (!selector || !Object.keys(selector).length) {
160
+ return clauses.length === 1
161
+ ? clauses[0]
162
+ : {
163
+ $and: clauses,
164
+ };
165
+ }
195
166
  return {
196
- $and: [selector, continuation],
167
+ $and: [selector, ...clauses],
197
168
  };
198
169
  }
199
- static normalizePrivateSort(query, idField = "id") {
200
- if (!Array.isArray(query.sort) || query.sort.length === 0) {
201
- throw new core_1.PagingError("Private collection pagination requires an explicit Mango sort");
170
+ static buildOrlessKeysetContinuationQueries(query, cursor, pageSize) {
171
+ const baseQuery = { ...query };
172
+ delete baseQuery.skip;
173
+ delete baseQuery.bookmark;
174
+ delete baseQuery.limit;
175
+ if (cursor.idField === cursor.sortField) {
176
+ const single = {
177
+ ...baseQuery,
178
+ selector: FabricContractAdapter.buildPrivateContinuationSelector((baseQuery.selector || {}), cursor.sortField, cursor.direction, cursor.idField, cursor.lastValue, cursor.lastId, "same"),
179
+ limit: pageSize + 1,
180
+ };
181
+ return [single];
202
182
  }
203
- const firstSort = Object.entries(query.sort[0] || {})[0];
204
- if (!firstSort || typeof firstSort[0] !== "string") {
205
- throw new core_1.PagingError("Private collection pagination requires a valid first sort field");
183
+ const first = {
184
+ ...baseQuery,
185
+ selector: FabricContractAdapter.buildPrivateContinuationSelector((baseQuery.selector || {}), cursor.sortField, cursor.direction, cursor.idField, cursor.lastValue, cursor.lastId, "same"),
186
+ limit: pageSize + 1,
187
+ };
188
+ const second = {
189
+ ...baseQuery,
190
+ selector: FabricContractAdapter.buildPrivateContinuationSelector((baseQuery.selector || {}), cursor.sortField, cursor.direction, cursor.idField, cursor.lastValue, cursor.lastId, "next"),
191
+ limit: pageSize + 1,
192
+ };
193
+ return [first, second];
194
+ }
195
+ static ensureDeterministicPrivateSort(query, tieBreaker) {
196
+ if (!Array.isArray(query.sort) || !query.sort.length) {
197
+ throw new core_1.PagingError("Private Mango pagination requires an explicit sort. Add orderBy(...) so a stable generated index can be selected.");
206
198
  }
207
- const sortField = firstSort[0];
208
- const direction = String(firstSort[1] || "asc").toLowerCase();
209
- if (direction !== "asc" && direction !== "desc") {
210
- throw new core_1.PagingError(`Unsupported private pagination sort direction: ${direction}`);
199
+ const direction = ((0, for_couchdb_1.getSortDirection)(query) ||
200
+ core_1.OrderDirection.ASC);
201
+ (0, for_couchdb_1.ensureDeterministicSort)(query, tieBreaker, direction);
202
+ const sortFields = (0, for_couchdb_1.getSortFields)(query);
203
+ const sortField = sortFields[0];
204
+ if (!sortField) {
205
+ throw new core_1.PagingError("Private Mango pagination requires a valid first sort field.");
211
206
  }
212
- const hasIdTieBreaker = idField === sortField ||
213
- query.sort.some((entry) => {
214
- const [field, dir] = Object.entries(entry || {})[0] || [];
215
- return (field === idField && String(dir || "").toLowerCase() === direction);
216
- });
217
- if (!hasIdTieBreaker && idField !== sortField) {
218
- query.sort = [...query.sort, { [idField]: direction }];
207
+ if (!sortFields.includes(tieBreaker)) {
208
+ throw new core_1.PagingError(`Private Mango pagination requires tie-breaker sort field "${tieBreaker}".`);
219
209
  }
220
210
  return {
221
211
  sortField,
222
212
  direction: direction,
223
- idField,
213
+ idField: tieBreaker,
224
214
  };
225
215
  }
226
216
  static parsePrivateResultValue(value) {
227
217
  try {
228
218
  const parsed = JSON.parse(value.toString("utf8"));
229
- if (!parsed || typeof parsed !== "object") {
230
- throw new Error("Private query result is not an object");
219
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
220
+ throw new Error("Private query result is not a JSON object");
231
221
  }
232
222
  return parsed;
233
223
  }
@@ -235,6 +225,102 @@ class FabricContractAdapter extends for_couchdb_1.CouchDBAdapter {
235
225
  throw new db_decorators_1.SerializationError(`Failed to parse private query result while building bookmark: ${e}`);
236
226
  }
237
227
  }
228
+ static async executePrivateMangoPageQueries(stub, collection, queries, pageSize, log) {
229
+ const paged = [];
230
+ for (const query of queries) {
231
+ if (paged.length >= pageSize + 1) {
232
+ break;
233
+ }
234
+ const remaining = pageSize + 1 - paged.length;
235
+ const queryForExecution = {
236
+ ...query,
237
+ limit: remaining,
238
+ };
239
+ delete queryForExecution.skip;
240
+ delete queryForExecution.bookmark;
241
+ log.debug(`Querying collection ${collection} for ${JSON.stringify(queryForExecution)}`);
242
+ const response = await stub.getPrivateDataQueryResult(collection, JSON.stringify(queryForExecution));
243
+ const iterator = (response.iterator ||
244
+ response);
245
+ try {
246
+ while (paged.length < pageSize + 1) {
247
+ const res = await iterator.next();
248
+ if (res.done)
249
+ break;
250
+ if (!res.value || !res.value.value)
251
+ continue;
252
+ paged.push({
253
+ key: res.value.key,
254
+ value: Buffer.isBuffer(res.value.value)
255
+ ? res.value.value
256
+ : Buffer.from(res.value.value.toString("utf8")),
257
+ });
258
+ }
259
+ }
260
+ finally {
261
+ await iterator.close();
262
+ }
263
+ }
264
+ return paged;
265
+ }
266
+ static toFabricPaginationResponse(docs, bookmark) {
267
+ let idx = 0;
268
+ const iterator = {
269
+ async next() {
270
+ if (idx < docs.length) {
271
+ return {
272
+ value: docs[idx++],
273
+ done: false,
274
+ };
275
+ }
276
+ return {
277
+ value: undefined,
278
+ done: true,
279
+ };
280
+ },
281
+ async close() { },
282
+ };
283
+ return {
284
+ iterator: iterator,
285
+ metadata: {
286
+ fetchedRecordsCount: docs.length,
287
+ bookmark,
288
+ },
289
+ };
290
+ }
291
+ static parseSyntheticPrivateBookmark(bookmark) {
292
+ if (typeof bookmark !== "string")
293
+ return undefined;
294
+ if (!bookmark.startsWith(FabricContractAdapter.PRIVATE_BOOKMARK_PREFIX))
295
+ return undefined;
296
+ const raw = bookmark.slice(FabricContractAdapter.PRIVATE_BOOKMARK_PREFIX.length);
297
+ try {
298
+ const parsed = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
299
+ if (!parsed ||
300
+ typeof parsed !== "object" ||
301
+ typeof parsed.sortField !== "string" ||
302
+ typeof parsed.direction !== "string" ||
303
+ typeof parsed.idField !== "string" ||
304
+ typeof parsed.lastId !== "string"
305
+ || typeof parsed.queryHash !== "string") {
306
+ return undefined;
307
+ }
308
+ return {
309
+ sortField: parsed.sortField,
310
+ direction: parsed.direction === "desc" ? "desc" : "asc",
311
+ idField: parsed.idField,
312
+ lastValue: parsed.lastValue,
313
+ lastId: parsed.lastId,
314
+ queryHash: parsed.queryHash,
315
+ };
316
+ }
317
+ catch {
318
+ return undefined;
319
+ }
320
+ }
321
+ static buildSyntheticPrivateBookmark(cursor) {
322
+ return `${FabricContractAdapter.PRIVATE_BOOKMARK_PREFIX}${Buffer.from(JSON.stringify(cursor)).toString("base64url")}`;
323
+ }
238
324
  getClient() {
239
325
  throw new core_1.UnsupportedError("Client is not supported in Fabric contracts");
240
326
  }
@@ -627,76 +713,48 @@ class FabricContractAdapter extends for_couchdb_1.CouchDBAdapter {
627
713
  }
628
714
  case "queryResultPaginated": {
629
715
  const [stub, rawInput, limit, skip, bookmark, ...args] = argsList;
630
- const { log } = thisArg["logCtx"](args, prop);
716
+ const { log, ctx } = thisArg["logCtx"](args, prop);
631
717
  if (skip !== undefined && skip !== null && Number(skip) > 0) {
632
- throw new core_1.PagingError("Private collection pagination does not support skip/offset pagination. Use the returned bookmark instead.");
718
+ throw new core_1.PagingError("Private Mango pagination does not support skip/offset pagination. Use the returned synthetic bookmark instead.");
633
719
  }
634
720
  const pageSize = Math.max(1, Number(limit) || 250);
635
721
  const query = { ...rawInput };
636
- const queryPkField = typeof query["__pkField"] === "string" &&
637
- query["__pkField"].trim().length
638
- ? String(query["__pkField"])
639
- : "id";
640
- delete query["__pkField"];
641
- const { sortField, direction: sortDirection, idField, } = FabricContractAdapter.normalizePrivateSort(query, queryPkField);
722
+ const queryInput = query;
723
+ const queryPkField = typeof queryInput["__pkField"] === "string" &&
724
+ queryInput["__pkField"].trim().length
725
+ ? String(queryInput["__pkField"])
726
+ : String(ctx?.getOrUndefined("privatePaginationTieBreaker") ||
727
+ "id");
728
+ delete queryInput["__pkField"];
642
729
  const syntheticCursor = FabricContractAdapter.parseSyntheticPrivateBookmark(bookmark);
643
730
  if (bookmark !== undefined &&
644
731
  bookmark !== null &&
645
732
  bookmark !== "" &&
646
733
  !syntheticCursor) {
647
- throw new core_1.PagingError("Private collection pagination only supports adapter-generated synthetic bookmarks");
734
+ throw new core_1.PagingError("Private Mango pagination only supports adapter-generated synthetic bookmarks.");
735
+ }
736
+ if (!Array.isArray(query.sort) || !query.sort.length) {
737
+ throw new core_1.PagingError("Private Mango pagination requires an explicit sort. Add orderBy(...) so a stable generated index can be selected.");
648
738
  }
739
+ const { sortField, direction: sortDirection, idField } = FabricContractAdapter.ensureDeterministicPrivateSort(query, queryPkField);
740
+ (0, for_couchdb_1.warnScanProneMangoOperators)(query.selector || {}, log);
649
741
  log.debug(`Private paginated query input collection=${collection} limit=${limit} skip=${skip} bookmark=${bookmark} sortField=${sortField} direction=${sortDirection} synthetic=${Boolean(syntheticCursor)}`);
650
- delete query.skip;
651
- delete query.bookmark;
652
742
  query.limit = pageSize + 1;
653
743
  const queryHash = FabricContractAdapter.privateQueryHash(query);
654
744
  if (syntheticCursor) {
655
745
  if (syntheticCursor.queryHash !== queryHash) {
656
- throw new core_1.PagingError("Private collection bookmark does not match the current query");
657
- }
658
- query.selector = FabricContractAdapter.buildPrivateCursorSelector(query.selector || {}, syntheticCursor.sortField, syntheticCursor.direction, syntheticCursor);
659
- }
660
- log.debug(`Querying collection ${collection} for ${JSON.stringify(query)}`);
661
- const response = await stub.getPrivateDataQueryResult(collection, JSON.stringify(query));
662
- const iterator = (response.iterator ||
663
- response);
664
- const responseMetadata = response.metadata ||
665
- iterator.metadata ||
666
- {};
667
- log.debug(`Private paginated response collection=${collection} metadata=${JSON.stringify(responseMetadata)}`);
668
- log.verbose(`iterator from collection ${collection} received`);
669
- const paged = [];
670
- let hasMore = false;
671
- try {
672
- while (true) {
673
- const res = await iterator.next();
674
- if (res.done)
675
- break;
676
- if (res.value && res.value.value) {
677
- const key = res.value.key;
678
- if (paged.length < pageSize) {
679
- paged.push({
680
- key,
681
- value: Buffer.isBuffer(res.value.value)
682
- ? res.value.value
683
- : Buffer.from(res.value.value.toString("utf8")),
684
- });
685
- }
686
- else {
687
- hasMore = true;
688
- break; // early exit
689
- }
690
- }
746
+ throw new core_1.PagingError("Private Mango bookmark does not match the current query.");
691
747
  }
692
748
  }
693
- finally {
694
- await iterator.close();
695
- }
696
- log.debug(`Private paginated collection=${collection} produced ${paged.length} rows hasMore=${hasMore}`);
749
+ const queries = syntheticCursor
750
+ ? FabricContractAdapter.buildOrlessKeysetContinuationQueries(query, syntheticCursor, pageSize)
751
+ : [{ ...query, limit: pageSize + 1 }];
752
+ const paged = await FabricContractAdapter.executePrivateMangoPageQueries(stub, collection, queries, pageSize, log);
753
+ const hasMore = paged.length > pageSize;
754
+ const docs = hasMore ? paged.slice(0, pageSize) : paged;
697
755
  let nextBookmark = "";
698
- if (hasMore && paged.length) {
699
- const last = paged[paged.length - 1];
756
+ if (hasMore && docs.length) {
757
+ const last = docs[docs.length - 1];
700
758
  const lastDoc = FabricContractAdapter.parsePrivateResultValue(last.value);
701
759
  if (!(sortField in lastDoc)) {
702
760
  throw new core_1.PagingError(`Cannot build private pagination bookmark: sorted field "${sortField}" is missing from the last result`);
@@ -716,24 +774,7 @@ class FabricContractAdapter extends for_couchdb_1.CouchDBAdapter {
716
774
  queryHash,
717
775
  });
718
776
  }
719
- // Wrap the page in an async iterator for resultIterator()
720
- let idx = 0;
721
- const arrayIterator = {
722
- async next() {
723
- if (idx < paged.length) {
724
- return { value: paged[idx++], done: false };
725
- }
726
- return { value: undefined, done: true };
727
- },
728
- async close() { },
729
- };
730
- return {
731
- iterator: arrayIterator,
732
- metadata: {
733
- fetchedRecordsCount: paged.length,
734
- bookmark: nextBookmark,
735
- },
736
- };
777
+ return FabricContractAdapter.toFabricPaginationResponse(docs, nextBookmark);
737
778
  }
738
779
  default:
739
780
  throw new db_decorators_1.InternalError(`Unsupported method override ${String(prop)}`);
@@ -803,6 +844,8 @@ class FabricContractAdapter extends for_couchdb_1.CouchDBAdapter {
803
844
  segregated: false,
804
845
  rebuildWithTransient: false,
805
846
  fullySegregated: false,
847
+ strictPrivateMangoPagination: true,
848
+ privatePaginationTieBreaker: "id",
806
849
  };
807
850
  if (flags instanceof ContractContext_js_1.FabricContractContext || flags instanceof core_1.Context) {
808
851
  flags = flags.toOverrides();
@@ -937,6 +980,8 @@ class FabricContractAdapter extends for_couchdb_1.CouchDBAdapter {
937
980
  const originalInput = { ...rawInput };
938
981
  const { skip, limit } = originalInput;
939
982
  const bookmark = originalInput["bookmark"];
983
+ const hasSort = Array.isArray(originalInput.sort) &&
984
+ originalInput.sort.length > 0;
940
985
  const pkField = typeof originalInput["__pkField"] === "string" &&
941
986
  originalInput["__pkField"].trim().length
942
987
  ? String(originalInput["__pkField"])
@@ -944,7 +989,8 @@ class FabricContractAdapter extends for_couchdb_1.CouchDBAdapter {
944
989
  const hasSkip = skip !== undefined && skip !== null && Number(skip) > 0;
945
990
  const hasBookmark = bookmark !== undefined && bookmark !== null && bookmark !== "";
946
991
  const paginationActive = Boolean(limit || hasSkip || hasBookmark);
947
- const shouldPaginate = Boolean(paginationActive && (enableSegregates || hasSkip || hasBookmark));
992
+ const shouldPaginate = Boolean(paginationActive &&
993
+ (enableSegregates || hasSkip || hasBookmark || hasSort));
948
994
  const baseInput = { ...originalInput };
949
995
  delete baseInput["skip"];
950
996
  delete baseInput["bookmark"];