@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.
- package/dist/browser.mjs +1 -1
- package/dist/browser.mjs.map +4 -4
- package/dist/esm/generated/precompiled-validators.js +349 -198
- package/dist/esm/generated/precompiled-validators.js.map +1 -1
- package/dist/esm/src/handlers/records-collection.js +200 -0
- package/dist/esm/src/handlers/records-collection.js.map +1 -0
- package/dist/esm/src/handlers/records-count.js +19 -196
- package/dist/esm/src/handlers/records-count.js.map +1 -1
- package/dist/esm/src/handlers/records-query.js +19 -376
- package/dist/esm/src/handlers/records-query.js.map +1 -1
- package/dist/esm/src/handlers/records-subscribe.js +21 -341
- package/dist/esm/src/handlers/records-subscribe.js.map +1 -1
- package/dist/esm/src/interfaces/records-count.js +1 -1
- package/dist/esm/src/interfaces/records-count.js.map +1 -1
- package/dist/esm/src/interfaces/records-query.js +1 -1
- package/dist/esm/src/interfaces/records-query.js.map +1 -1
- package/dist/esm/src/interfaces/records-subscribe.js +3 -1
- package/dist/esm/src/interfaces/records-subscribe.js.map +1 -1
- package/dist/esm/src/store/index-level.js +84 -32
- package/dist/esm/src/store/index-level.js.map +1 -1
- package/dist/esm/src/store/message-store-level.js +45 -11
- package/dist/esm/src/store/message-store-level.js.map +1 -1
- package/dist/esm/src/utils/record-limit-occupancy.js +5 -2
- package/dist/esm/src/utils/record-limit-occupancy.js.map +1 -1
- package/dist/esm/src/utils/records.js +14 -47
- package/dist/esm/src/utils/records.js.map +1 -1
- package/dist/esm/tests/features/records-nested-query-scope.spec.js +15 -1
- package/dist/esm/tests/features/records-nested-query-scope.spec.js.map +1 -1
- package/dist/esm/tests/features/records-record-limit.spec.js +116 -0
- package/dist/esm/tests/features/records-record-limit.spec.js.map +1 -1
- package/dist/esm/tests/handlers/records-count.spec.js +52 -1
- package/dist/esm/tests/handlers/records-count.spec.js.map +1 -1
- package/dist/esm/tests/handlers/records-subscribe.spec.js +76 -1
- package/dist/esm/tests/handlers/records-subscribe.spec.js.map +1 -1
- package/dist/esm/tests/store/index-level.spec.js +95 -1
- package/dist/esm/tests/store/index-level.spec.js.map +1 -1
- package/dist/esm/tests/store/message-store-level.spec.js +70 -1
- package/dist/esm/tests/store/message-store-level.spec.js.map +1 -1
- package/dist/types/generated/precompiled-validators.d.ts.map +1 -1
- package/dist/types/src/handlers/records-collection.d.ts +32 -0
- package/dist/types/src/handlers/records-collection.d.ts.map +1 -0
- package/dist/types/src/handlers/records-count.d.ts +0 -35
- package/dist/types/src/handlers/records-count.d.ts.map +1 -1
- package/dist/types/src/handlers/records-query.d.ts +0 -61
- package/dist/types/src/handlers/records-query.d.ts.map +1 -1
- package/dist/types/src/handlers/records-subscribe.d.ts +1 -37
- package/dist/types/src/handlers/records-subscribe.d.ts.map +1 -1
- package/dist/types/src/interfaces/records-subscribe.d.ts.map +1 -1
- package/dist/types/src/store/index-level.d.ts +27 -3
- package/dist/types/src/store/index-level.d.ts.map +1 -1
- package/dist/types/src/store/message-store-level.d.ts +1 -0
- package/dist/types/src/store/message-store-level.d.ts.map +1 -1
- package/dist/types/src/types/message-store.d.ts +6 -1
- package/dist/types/src/types/message-store.d.ts.map +1 -1
- package/dist/types/src/types/records-types.d.ts +1 -1
- package/dist/types/src/types/records-types.d.ts.map +1 -1
- package/dist/types/src/utils/record-limit-occupancy.d.ts.map +1 -1
- package/dist/types/src/utils/records.d.ts +6 -25
- package/dist/types/src/utils/records.d.ts.map +1 -1
- package/dist/types/tests/features/records-nested-query-scope.spec.d.ts.map +1 -1
- package/dist/types/tests/features/records-record-limit.spec.d.ts.map +1 -1
- package/dist/types/tests/handlers/records-count.spec.d.ts.map +1 -1
- package/dist/types/tests/handlers/records-subscribe.spec.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/handlers/records-collection.ts +269 -0
- package/src/handlers/records-count.ts +21 -236
- package/src/handlers/records-query.ts +22 -447
- package/src/handlers/records-subscribe.ts +26 -404
- package/src/interfaces/records-count.ts +1 -1
- package/src/interfaces/records-query.ts +1 -1
- package/src/interfaces/records-subscribe.ts +5 -2
- package/src/store/index-level.ts +145 -37
- package/src/store/message-store-level.ts +46 -11
- package/src/types/message-store.ts +6 -1
- package/src/types/records-types.ts +1 -1
- package/src/utils/record-limit-occupancy.ts +5 -2
- package/src/utils/records.ts +17 -52
package/src/store/index-level.ts
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
569
|
+
IndexLevel.siftRecordLimitRankHeapDown(heap, 0);
|
|
480
570
|
}
|
|
481
571
|
|
|
482
|
-
private static
|
|
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
|
|
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 &&
|
|
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
|
-
|
|
785
|
-
for await (const [
|
|
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
|
-
|
|
893
|
+
return;
|
|
789
894
|
}
|
|
790
|
-
|
|
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 (
|
|
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
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
141
|
+
return recordLimit;
|
|
139
142
|
}
|
|
140
143
|
|
|
141
144
|
const contextDepth = contextId.split('/').length;
|
package/src/utils/records.ts
CHANGED
|
@@ -280,16 +280,19 @@ export class Records {
|
|
|
280
280
|
}
|
|
281
281
|
|
|
282
282
|
/**
|
|
283
|
-
* Nested protocol-path
|
|
284
|
-
*
|
|
285
|
-
*
|
|
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
|
|
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
|
}
|