@enbox/dwn-sdk-js 0.4.17 → 0.4.18

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 (77) hide show
  1. package/dist/browser.mjs +1 -1
  2. package/dist/browser.mjs.map +4 -4
  3. package/dist/esm/generated/precompiled-validators.js +349 -198
  4. package/dist/esm/generated/precompiled-validators.js.map +1 -1
  5. package/dist/esm/src/handlers/records-collection.js +200 -0
  6. package/dist/esm/src/handlers/records-collection.js.map +1 -0
  7. package/dist/esm/src/handlers/records-count.js +19 -196
  8. package/dist/esm/src/handlers/records-count.js.map +1 -1
  9. package/dist/esm/src/handlers/records-query.js +19 -376
  10. package/dist/esm/src/handlers/records-query.js.map +1 -1
  11. package/dist/esm/src/handlers/records-subscribe.js +21 -341
  12. package/dist/esm/src/handlers/records-subscribe.js.map +1 -1
  13. package/dist/esm/src/interfaces/records-count.js +1 -1
  14. package/dist/esm/src/interfaces/records-count.js.map +1 -1
  15. package/dist/esm/src/interfaces/records-query.js +1 -1
  16. package/dist/esm/src/interfaces/records-query.js.map +1 -1
  17. package/dist/esm/src/interfaces/records-subscribe.js +3 -1
  18. package/dist/esm/src/interfaces/records-subscribe.js.map +1 -1
  19. package/dist/esm/src/store/index-level.js +84 -32
  20. package/dist/esm/src/store/index-level.js.map +1 -1
  21. package/dist/esm/src/store/message-store-level.js +45 -11
  22. package/dist/esm/src/store/message-store-level.js.map +1 -1
  23. package/dist/esm/src/utils/record-limit-occupancy.js +5 -2
  24. package/dist/esm/src/utils/record-limit-occupancy.js.map +1 -1
  25. package/dist/esm/src/utils/records.js +14 -47
  26. package/dist/esm/src/utils/records.js.map +1 -1
  27. package/dist/esm/tests/features/records-nested-query-scope.spec.js +15 -1
  28. package/dist/esm/tests/features/records-nested-query-scope.spec.js.map +1 -1
  29. package/dist/esm/tests/features/records-record-limit.spec.js +116 -0
  30. package/dist/esm/tests/features/records-record-limit.spec.js.map +1 -1
  31. package/dist/esm/tests/handlers/records-count.spec.js +52 -1
  32. package/dist/esm/tests/handlers/records-count.spec.js.map +1 -1
  33. package/dist/esm/tests/handlers/records-subscribe.spec.js +76 -1
  34. package/dist/esm/tests/handlers/records-subscribe.spec.js.map +1 -1
  35. package/dist/esm/tests/store/index-level.spec.js +95 -1
  36. package/dist/esm/tests/store/index-level.spec.js.map +1 -1
  37. package/dist/esm/tests/store/message-store-level.spec.js +70 -1
  38. package/dist/esm/tests/store/message-store-level.spec.js.map +1 -1
  39. package/dist/types/generated/precompiled-validators.d.ts.map +1 -1
  40. package/dist/types/src/handlers/records-collection.d.ts +32 -0
  41. package/dist/types/src/handlers/records-collection.d.ts.map +1 -0
  42. package/dist/types/src/handlers/records-count.d.ts +0 -35
  43. package/dist/types/src/handlers/records-count.d.ts.map +1 -1
  44. package/dist/types/src/handlers/records-query.d.ts +0 -61
  45. package/dist/types/src/handlers/records-query.d.ts.map +1 -1
  46. package/dist/types/src/handlers/records-subscribe.d.ts +1 -37
  47. package/dist/types/src/handlers/records-subscribe.d.ts.map +1 -1
  48. package/dist/types/src/interfaces/records-subscribe.d.ts.map +1 -1
  49. package/dist/types/src/store/index-level.d.ts +27 -3
  50. package/dist/types/src/store/index-level.d.ts.map +1 -1
  51. package/dist/types/src/store/message-store-level.d.ts +1 -0
  52. package/dist/types/src/store/message-store-level.d.ts.map +1 -1
  53. package/dist/types/src/types/message-store.d.ts +6 -1
  54. package/dist/types/src/types/message-store.d.ts.map +1 -1
  55. package/dist/types/src/types/records-types.d.ts +1 -1
  56. package/dist/types/src/types/records-types.d.ts.map +1 -1
  57. package/dist/types/src/utils/record-limit-occupancy.d.ts.map +1 -1
  58. package/dist/types/src/utils/records.d.ts +6 -25
  59. package/dist/types/src/utils/records.d.ts.map +1 -1
  60. package/dist/types/tests/features/records-nested-query-scope.spec.d.ts.map +1 -1
  61. package/dist/types/tests/features/records-record-limit.spec.d.ts.map +1 -1
  62. package/dist/types/tests/handlers/records-count.spec.d.ts.map +1 -1
  63. package/dist/types/tests/handlers/records-subscribe.spec.d.ts.map +1 -1
  64. package/package.json +1 -1
  65. package/src/handlers/records-collection.ts +269 -0
  66. package/src/handlers/records-count.ts +21 -236
  67. package/src/handlers/records-query.ts +22 -447
  68. package/src/handlers/records-subscribe.ts +26 -404
  69. package/src/interfaces/records-count.ts +1 -1
  70. package/src/interfaces/records-query.ts +1 -1
  71. package/src/interfaces/records-subscribe.ts +5 -2
  72. package/src/store/index-level.ts +145 -37
  73. package/src/store/message-store-level.ts +46 -11
  74. package/src/types/message-store.ts +6 -1
  75. package/src/types/records-types.ts +1 -1
  76. package/src/utils/record-limit-occupancy.ts +5 -2
  77. package/src/utils/records.ts +17 -52
@@ -23,6 +23,13 @@ export type IndexLevelConfig = {
23
23
 
24
24
  export type IndexedItem = { messageCid: string, indexes: KeyValues };
25
25
 
26
+ type RecordLimitParentGroupsInput = {
27
+ candidateFilter: Filter;
28
+ filters: Filter[];
29
+ max: number;
30
+ parentIds: readonly string[];
31
+ };
32
+
26
33
  /** A value that can be encoded into a lexicographically sortable index key. */
27
34
  type IndexableValue = string | number | boolean;
28
35
 
@@ -429,27 +436,106 @@ export class IndexLevel {
429
436
 
430
437
  const indexName = parentId === undefined ? 'protocolPath' : 'parentId';
431
438
  const indexValue = parentId ?? protocolPath;
432
- const matchPrefix = IndexLevel.keySegmentJoin(IndexLevel.encodeValue(indexValue), '');
433
- const partition = await this.getIndexPartition(tenant, indexName);
434
439
  const rankHeap: string[] = [];
435
440
 
436
- for await (const [key, value] of partition.iterator({ gt: matchPrefix }, options)) {
437
- if (!key.startsWith(matchPrefix)) {
438
- break;
439
- }
440
-
441
- const item = JSON.parse(value) as IndexedItem;
441
+ for await (const item of this.iterateExactMatches(tenant, indexName, indexValue, options)) {
442
442
  if ((parentId === undefined && item.indexes.parentId !== undefined) ||
443
443
  !FilterUtility.matchFilter(item.indexes, candidateFilter)) {
444
444
  continue;
445
445
  }
446
446
 
447
- IndexLevel.addToBoundedMaxHeap(rankHeap, IndexLevel.createRecordLimitRankKey(item.indexes), max);
447
+ IndexLevel.addToRecordLimitRankHeap(
448
+ rankHeap,
449
+ IndexLevel.createRecordLimitRankKey(item.indexes),
450
+ max,
451
+ );
448
452
  }
449
453
 
450
454
  return rankHeap[0];
451
455
  }
452
456
 
457
+ /**
458
+ * Queries the bounded occupants of explicitly selected direct-parent groups.
459
+ * Occupancy is resolved before caller filters, then the survivors are globally
460
+ * sorted and paginated using ordinary IndexLevel cursor semantics.
461
+ */
462
+ public async queryRecordLimitParentGroups(
463
+ tenant: string,
464
+ input: RecordLimitParentGroupsInput & { queryOptions: QueryOptions },
465
+ options?: IndexLevelOptions,
466
+ ): Promise<IndexedItem[]> {
467
+ assertValidSubtreeFilters([input.candidateFilter, ...input.filters]);
468
+ const { filters, queryOptions } = input;
469
+ const { cursor, limit, sortDirection = SortDirection.Ascending, sortProperty } = queryOptions;
470
+ if (limit !== undefined && limit <= 0) {
471
+ return [];
472
+ }
473
+
474
+ const cursorStartingKey = cursor === undefined ? undefined : this.createStartingKeyFromCursor(cursor);
475
+ const compareItems = (left: IndexedItem, right: IndexedItem): number =>
476
+ this.sortItems(left, right, sortProperty, sortDirection);
477
+ const matches: IndexedItem[] = [];
478
+
479
+ for await (const item of this.iterateRecordLimitParentGroupOccupants(tenant, input, options)) {
480
+ if (item.indexes[sortProperty] === undefined ||
481
+ !FilterUtility.matchAnyFilter(item.indexes, filters) ||
482
+ (cursorStartingKey !== undefined &&
483
+ !this.isItemAfterCursor(item, sortDirection, sortProperty, cursorStartingKey))) {
484
+ continue;
485
+ }
486
+
487
+ matches.push(item);
488
+ }
489
+
490
+ matches.sort(compareItems);
491
+ return limit === undefined ? matches : matches.slice(0, limit);
492
+ }
493
+
494
+ /** Counts the bounded occupants of explicitly selected direct-parent groups. */
495
+ public async countRecordLimitParentGroups(
496
+ tenant: string,
497
+ input: RecordLimitParentGroupsInput & { sortProperty: string },
498
+ options?: IndexLevelOptions,
499
+ ): Promise<number> {
500
+ assertValidSubtreeFilters([input.candidateFilter, ...input.filters]);
501
+ let count = 0;
502
+ for await (const item of this.iterateRecordLimitParentGroupOccupants(tenant, input, options)) {
503
+ if (item.indexes[input.sortProperty] !== undefined &&
504
+ FilterUtility.matchAnyFilter(item.indexes, input.filters)) {
505
+ count++;
506
+ }
507
+ }
508
+ return count;
509
+ }
510
+
511
+ /** Streams occupants from exact parentId ranges without opening the requested-sort index. */
512
+ private async * iterateRecordLimitParentGroupOccupants(
513
+ tenant: string,
514
+ input: RecordLimitParentGroupsInput,
515
+ options?: IndexLevelOptions,
516
+ ): AsyncGenerator<IndexedItem> {
517
+ for (const parentId of new Set(input.parentIds)) {
518
+ const candidateFilter: Filter = { ...input.candidateFilter, parentId };
519
+ const cutoff = await this.findRecordLimitRankCutoff(
520
+ tenant,
521
+ candidateFilter,
522
+ parentId,
523
+ input.max,
524
+ options,
525
+ );
526
+ if (cutoff === undefined) {
527
+ continue;
528
+ }
529
+
530
+ for await (const item of this.iterateExactMatches(tenant, 'parentId', parentId, options)) {
531
+ if (FilterUtility.matchFilter(item.indexes, candidateFilter) &&
532
+ lexicographicalCompare(IndexLevel.createRecordLimitRankKey(item.indexes), cutoff) <= 0) {
533
+ yield item;
534
+ }
535
+ }
536
+ }
537
+ }
538
+
453
539
  /**
454
540
  * Builds the deterministic `$recordLimit` rank key: oldest creation time,
455
541
  * then record ID. Message CID deliberately does not participate because
@@ -464,10 +550,14 @@ export class IndexLevel {
464
550
  return IndexLevel.keySegmentJoin(IndexLevel.encodeValue(dateCreated), IndexLevel.encodeValue(recordId));
465
551
  }
466
552
 
467
- private static addToBoundedMaxHeap(heap: string[], value: string, max: number): void {
553
+ private static addToRecordLimitRankHeap(
554
+ heap: string[],
555
+ value: string,
556
+ max: number,
557
+ ): void {
468
558
  if (heap.length < max) {
469
559
  heap.push(value);
470
- IndexLevel.siftRecordLimitRankUp(heap, heap.length - 1);
560
+ IndexLevel.siftRecordLimitRankHeapUp(heap, heap.length - 1);
471
561
  return;
472
562
  }
473
563
 
@@ -476,10 +566,13 @@ export class IndexLevel {
476
566
  }
477
567
 
478
568
  heap[0] = value;
479
- IndexLevel.siftRecordLimitRankDown(heap, 0);
569
+ IndexLevel.siftRecordLimitRankHeapDown(heap, 0);
480
570
  }
481
571
 
482
- private static siftRecordLimitRankUp(heap: string[], startIndex: number): void {
572
+ private static siftRecordLimitRankHeapUp(
573
+ heap: string[],
574
+ startIndex: number,
575
+ ): void {
483
576
  let index = startIndex;
484
577
  while (index > 0) {
485
578
  const parentIndex = (index - 1) >>> 1;
@@ -492,7 +585,10 @@ export class IndexLevel {
492
585
  }
493
586
  }
494
587
 
495
- private static siftRecordLimitRankDown(heap: string[], startIndex: number): void {
588
+ private static siftRecordLimitRankHeapDown(
589
+ heap: string[],
590
+ startIndex: number,
591
+ ): void {
496
592
  let index = startIndex;
497
593
  while (true) {
498
594
  const leftIndex = index * 2 + 1;
@@ -501,7 +597,8 @@ export class IndexLevel {
501
597
  }
502
598
 
503
599
  const rightIndex = leftIndex + 1;
504
- const largerChildIndex = rightIndex < heap.length && lexicographicalCompare(heap[rightIndex], heap[leftIndex]) > 0
600
+ const largerChildIndex = rightIndex < heap.length &&
601
+ lexicographicalCompare(heap[rightIndex], heap[leftIndex]) > 0
505
602
  ? rightIndex
506
603
  : leftIndex;
507
604
  if (lexicographicalCompare(heap[index], heap[largerChildIndex]) >= 0) {
@@ -774,22 +871,29 @@ export class IndexLevel {
774
871
  propertyValue: EqualFilter,
775
872
  options?: IndexLevelOptions
776
873
  ): Promise<IndexedItem[]> {
874
+ const matches: IndexedItem[] = [];
875
+ for await (const item of this.iterateExactMatches(tenant, propertyName, propertyValue, options)) {
876
+ matches.push(item);
877
+ }
878
+ return matches;
879
+ }
777
880
 
881
+ /** Streams one exact property-value range without materializing the range. */
882
+ private async * iterateExactMatches(
883
+ tenant: string,
884
+ propertyName: string,
885
+ propertyValue: EqualFilter,
886
+ options?: IndexLevelOptions,
887
+ ): AsyncGenerator<IndexedItem> {
778
888
  const matchPrefix = IndexLevel.keySegmentJoin(IndexLevel.encodeValue(propertyValue));
779
- const iteratorOptions: LevelWrapperIteratorOptions<string> = {
780
- gt: matchPrefix
781
- };
782
-
783
889
  const filterPartition = await this.getIndexPartition(tenant, propertyName);
784
- const matches: IndexedItem[] = [];
785
- for await (const [ key, value ] of filterPartition.iterator(iteratorOptions, options)) {
786
- // immediately stop if we arrive at an index that contains a different property value
890
+
891
+ for await (const [key, value] of filterPartition.iterator({ gt: matchPrefix }, options)) {
787
892
  if (!key.startsWith(matchPrefix)) {
788
- break;
893
+ return;
789
894
  }
790
- matches.push(JSON.parse(value) as IndexedItem);
895
+ yield JSON.parse(value) as IndexedItem;
791
896
  }
792
- return matches;
793
897
  }
794
898
 
795
899
  /**
@@ -891,24 +995,13 @@ export class IndexLevel {
891
995
  * Since the array is already sorted, binary search provides O(log n) performance instead of O(n).
892
996
  */
893
997
  private findCursorStartingIndex(items: IndexedItem[], sortDirection: SortDirection, sortProperty: string, cursorStartingKey: string): number {
894
-
895
- const isAfterCursor = (item: IndexedItem): boolean => {
896
- const { messageCid, indexes } = item;
897
- const sortValue = indexes[sortProperty] as string | number;
898
- const itemCompareValue = IndexLevel.keySegmentJoin(IndexLevel.encodeValue(sortValue), messageCid);
899
-
900
- return sortDirection === SortDirection.Ascending ?
901
- itemCompareValue > cursorStartingKey :
902
- itemCompareValue < cursorStartingKey;
903
- };
904
-
905
998
  // binary search for the first item after the cursor
906
999
  let low = 0;
907
1000
  let high = items.length;
908
1001
 
909
1002
  while (low < high) {
910
1003
  const mid = (low + high) >>> 1;
911
- if (isAfterCursor(items[mid])) {
1004
+ if (this.isItemAfterCursor(items[mid], sortDirection, sortProperty, cursorStartingKey)) {
912
1005
  high = mid;
913
1006
  } else {
914
1007
  low = mid + 1;
@@ -918,6 +1011,21 @@ export class IndexLevel {
918
1011
  return low < items.length ? low : -1;
919
1012
  }
920
1013
 
1014
+ private isItemAfterCursor(
1015
+ item: IndexedItem,
1016
+ sortDirection: SortDirection,
1017
+ sortProperty: string,
1018
+ cursorStartingKey: string,
1019
+ ): boolean {
1020
+ const { messageCid, indexes } = item;
1021
+ const sortValue = indexes[sortProperty] as string | number;
1022
+ const itemCompareValue = IndexLevel.keySegmentJoin(IndexLevel.encodeValue(sortValue), messageCid);
1023
+
1024
+ return sortDirection === SortDirection.Ascending ?
1025
+ itemCompareValue > cursorStartingKey :
1026
+ itemCompareValue < cursorStartingKey;
1027
+ }
1028
+
921
1029
  /**
922
1030
  * Gets the indexes given an messageCid. This is a reverse lookup to construct starting keys, as well as deleting indexed items.
923
1031
  */
@@ -347,15 +347,28 @@ export class MessageStoreLevel implements MessageStore, ReplicationFeedReader {
347
347
  // this adds 1 to the limit if provided, that way we can check to see if there are additional results and provide a return cursor.
348
348
  const queryOptions = MessageStoreLevel.buildQueryOptions(messageSort, pagination);
349
349
  const index = await this.index();
350
- const results = options?.recordLimit === undefined
351
- ? await index.query(tenant, filters, queryOptions, options)
352
- : await this.collectRecordLimitPopulation({
353
- tenant,
354
- filters,
355
- queryOptions,
356
- recordLimit: options.recordLimit,
357
- options,
358
- });
350
+ const recordLimit = options?.recordLimit;
351
+ let results: IndexedItem[];
352
+ if (recordLimit === undefined) {
353
+ results = await index.query(tenant, filters, queryOptions, options);
354
+ } else {
355
+ const parentIds = MessageStoreLevel.getRecordLimitParentIds(recordLimit);
356
+ results = parentIds === undefined
357
+ ? await this.collectRecordLimitPopulation({
358
+ tenant,
359
+ filters,
360
+ queryOptions,
361
+ recordLimit,
362
+ options,
363
+ })
364
+ : await index.queryRecordLimitParentGroups(tenant, {
365
+ candidateFilter : MessageStoreLevel.buildRecordLimitCandidateFilter(recordLimit),
366
+ filters,
367
+ max : recordLimit.max,
368
+ parentIds,
369
+ queryOptions,
370
+ }, options);
371
+ }
359
372
 
360
373
  let cursor: PaginationCursor | undefined;
361
374
  // checks to see if the returned results are greater than the limit, which would indicate additional results.
@@ -387,16 +400,28 @@ export class MessageStoreLevel implements MessageStore, ReplicationFeedReader {
387
400
 
388
401
  const queryOptions = MessageStoreLevel.buildQueryOptions(messageSort);
389
402
  const index = await this.index();
390
- if (options?.recordLimit === undefined) {
403
+ const recordLimit = options?.recordLimit;
404
+ if (recordLimit === undefined) {
391
405
  return index.count(tenant, filters, queryOptions, options);
392
406
  }
393
407
 
408
+ const parentIds = MessageStoreLevel.getRecordLimitParentIds(recordLimit);
409
+ if (parentIds !== undefined) {
410
+ return index.countRecordLimitParentGroups(tenant, {
411
+ candidateFilter : MessageStoreLevel.buildRecordLimitCandidateFilter(recordLimit),
412
+ filters,
413
+ max : recordLimit.max,
414
+ parentIds,
415
+ sortProperty : queryOptions.sortProperty,
416
+ }, options);
417
+ }
418
+
394
419
  let count = 0;
395
420
  for await (const _item of this.iterateRecordLimitPopulation({
396
421
  tenant,
397
422
  filters,
398
423
  queryOptions,
399
- recordLimit: options.recordLimit,
424
+ recordLimit,
400
425
  options,
401
426
  })) {
402
427
  count++;
@@ -544,9 +569,19 @@ export class MessageStoreLevel implements MessageStore, ReplicationFeedReader {
544
569
  if (recordLimit.contextId !== undefined) {
545
570
  filter.contextId = { subtree: recordLimit.contextId };
546
571
  }
572
+ if (recordLimit.parentId !== undefined) {
573
+ filter.parentId = recordLimit.parentId;
574
+ }
547
575
  return filter;
548
576
  }
549
577
 
578
+ private static getRecordLimitParentIds(recordLimit: RecordLimitOccupancy): readonly string[] | undefined {
579
+ if (recordLimit.parentId === undefined) {
580
+ return undefined;
581
+ }
582
+ return Array.isArray(recordLimit.parentId) ? recordLimit.parentId : [recordLimit.parentId];
583
+ }
584
+
550
585
  /**
551
586
  * Builds the IndexLevel QueryOptions object given MessageStore sort and pagination parameters.
552
587
  */
@@ -19,9 +19,14 @@ export type RecordLimitOccupancy = {
19
19
  /**
20
20
  * Root of the candidate context subtree. An exact-record request supplies
21
21
  * its direct-parent context here so sibling candidates still consume slots.
22
- * Omitted for root protocol paths.
22
+ * Omitted for root protocol paths and selections scoped only by direct parent.
23
23
  */
24
24
  contextId?: string;
25
+ /**
26
+ * Direct-parent groups selected by the Records filter. Including this in
27
+ * the candidate population keeps each selected group's ranking complete.
28
+ */
29
+ parentId?: string | string[];
25
30
  max: number;
26
31
  };
27
32
 
@@ -161,7 +161,7 @@ export type RecordsFilter = {
161
161
  schema?: string;
162
162
  tags?: { [property:string]: RecordsWriteTagsFilter }
163
163
  recordId?: string;
164
- parentId?: string;
164
+ parentId?: string | string[];
165
165
  dataFormat?: string;
166
166
  dataSize?: RangeFilter;
167
167
  dataCid?: string;
@@ -110,7 +110,7 @@ export async function resolveRecordLimitOccupancy(input: RecordLimitPolicyDepend
110
110
  recordsFilter: RecordsFilter;
111
111
  messageTimestamp: string;
112
112
  }): Promise<RecordLimitOccupancy | undefined> {
113
- const { contextId, protocol, protocolPath } = input.recordsFilter;
113
+ const { contextId, parentId, protocol, protocolPath } = input.recordsFilter;
114
114
  if (protocol === undefined || protocolPath === undefined) {
115
115
  return undefined;
116
116
  }
@@ -134,8 +134,11 @@ export async function resolveRecordLimitOccupancy(input: RecordLimitPolicyDepend
134
134
  if (!protocolPath.includes('/')) {
135
135
  return recordLimit;
136
136
  }
137
+ if (parentId !== undefined) {
138
+ recordLimit.parentId = parentId;
139
+ }
137
140
  if (contextId === undefined) {
138
- return undefined;
141
+ return recordLimit;
139
142
  }
140
143
 
141
144
  const contextDepth = contextId.split('/').length;
@@ -280,16 +280,19 @@ export class Records {
280
280
  }
281
281
 
282
282
  /**
283
- * Nested protocol-path queries must select the target path itself or one of
284
- * its ancestor contexts. An omitted, malformed, or deeper context cannot
285
- * identify a bounded subtree of the selected protocol path.
283
+ * Nested protocol-path collection requests must select one or more direct
284
+ * parents, the target path itself, or one of its ancestor contexts. A
285
+ * bounded path-wide subscription may omit that scope because its initial
286
+ * page is explicitly capped; record-limited results still project each
287
+ * direct-parent population independently.
286
288
  */
287
- public static validateNestedProtocolPathQueryScope(
289
+ public static validateNestedProtocolPathScope(
288
290
  filter: RecordsFilter,
289
291
  errorCode: DwnErrorCode,
290
292
  operationName: string,
293
+ allowBoundedPathWideSelection = false,
291
294
  ): void {
292
- const { contextId, protocolPath } = filter;
295
+ const { contextId, parentId, protocolPath } = filter;
293
296
  if (!protocolPath?.includes('/')) {
294
297
  return;
295
298
  }
@@ -298,6 +301,14 @@ export class Records {
298
301
  return;
299
302
  }
300
303
 
304
+ if (parentId !== undefined && contextId === undefined) {
305
+ return;
306
+ }
307
+
308
+ if (contextId === undefined && allowBoundedPathWideSelection) {
309
+ return;
310
+ }
311
+
301
312
  const protocolPathDepth = protocolPath.split('/').length;
302
313
  const contextIdSegments = contextId?.split('/');
303
314
  if (
@@ -310,7 +321,7 @@ export class Records {
310
321
 
311
322
  throw new DwnError(
312
323
  errorCode,
313
- `${operationName} for nested protocol path '${protocolPath}' must include a contextId selecting that path or one of its ancestors`
324
+ `${operationName} for nested protocol path '${protocolPath}' must include parentId or a contextId selecting that path or one of its ancestors`
314
325
  );
315
326
  }
316
327
 
@@ -405,16 +416,6 @@ export class Records {
405
416
  return filterCopy;
406
417
  }
407
418
 
408
- public static buildUnpublishedControlRecordsFilter(filter: RecordsFilter, dateSort?: DateSort): Filter {
409
- return {
410
- ...Records.convertFilter(filter, dateSort),
411
- interface : DwnInterfaceName.Records,
412
- method : DwnMethodName.Write,
413
- isLatestBaseState : true,
414
- published : false,
415
- };
416
- }
417
-
418
419
  public static buildControlRecordsFilters(filters: Filter[]): Filter[] {
419
420
  const controlFilters: Filter[] = [];
420
421
  for (const filter of filters) {
@@ -584,13 +585,6 @@ export class Records {
584
585
  }
585
586
  }
586
587
 
587
- /**
588
- * Determines if signature payload contains a protocolRole and should be authorized as such.
589
- */
590
- public static shouldProtocolAuthorize(signaturePayload: GenericSignaturePayload): boolean {
591
- return signaturePayload.protocolRole !== undefined;
592
- }
593
-
594
588
  /**
595
589
  * Checks if the filter supports returning published records.
596
590
  */
@@ -639,33 +633,4 @@ export class Records {
639
633
  return !incomingDeleteIsNewest;
640
634
  }
641
635
 
642
- /**
643
- * Checks whether or not the incoming records query filter should build an unpublished recipient MessageStore filter.
644
- *
645
- * @param filter The incoming RecordsFilter to evaluate against.
646
- * @param recipient The recipient to check against the filter, typically the query/subscribe message author.
647
- * @returns {boolean} True if the filter contains the recipient, or if the recipient filter is undefined/empty.
648
- */
649
- static shouldBuildUnpublishedRecipientFilter(filter: RecordsFilter, recipient: string): boolean {
650
- const { recipient: recipientFilter } = filter;
651
-
652
- return Array.isArray(recipientFilter) ?
653
- recipientFilter.length === 0 || recipientFilter.includes(recipient) :
654
- recipientFilter === undefined || recipientFilter === recipient;
655
- }
656
-
657
- /**
658
- * Checks whether or not the incoming records query filter should build an unpublished author MessageStore filter.
659
- *
660
- * @param filter The incoming RecordsFilter to evaluate against.
661
- * @param author The author to check against the filter, typically the query/subscribe message author.
662
- * @returns {boolean} True if the filter contains the author, or if the author filter is undefined/empty.
663
- */
664
- static shouldBuildUnpublishedAuthorFilter(filter: RecordsFilter, author: string): boolean {
665
- const { author: authorFilter } = filter;
666
-
667
- return Array.isArray(authorFilter) ?
668
- authorFilter.length === 0 || authorFilter.includes(author) :
669
- authorFilter === undefined || authorFilter === author;
670
- }
671
636
  }