@enbox/dwn-sdk-js 0.4.16 → 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 +978 -840
- package/dist/esm/generated/precompiled-validators.js.map +1 -1
- package/dist/esm/json-schemas/definitions.json +5 -0
- package/dist/esm/src/core/dwn-constant.js +2 -2
- package/dist/esm/src/core/dwn-error.js +0 -1
- package/dist/esm/src/core/dwn-error.js.map +1 -1
- package/dist/esm/src/core/protocol-authorization-validation.js +1 -24
- package/dist/esm/src/core/protocol-authorization-validation.js.map +1 -1
- package/dist/esm/src/core/protocol-authorization.js +1 -5
- package/dist/esm/src/core/protocol-authorization.js.map +1 -1
- package/dist/esm/src/core/validation-state-reader.js +2 -3
- package/dist/esm/src/core/validation-state-reader.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 +26 -195
- package/dist/esm/src/handlers/records-count.js.map +1 -1
- package/dist/esm/src/handlers/records-query.js +19 -373
- package/dist/esm/src/handlers/records-query.js.map +1 -1
- package/dist/esm/src/handlers/records-subscribe.js +21 -338
- package/dist/esm/src/handlers/records-subscribe.js.map +1 -1
- package/dist/esm/src/handlers/records-write.js +8 -26
- package/dist/esm/src/handlers/records-write.js.map +1 -1
- package/dist/esm/src/index.js +1 -1
- package/dist/esm/src/index.js.map +1 -1
- package/dist/esm/src/interfaces/protocols-configure.js +4 -9
- package/dist/esm/src/interfaces/protocols-configure.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 +210 -21
- package/dist/esm/src/store/index-level.js.map +1 -1
- package/dist/esm/src/store/message-store-level.js +192 -21
- package/dist/esm/src/store/message-store-level.js.map +1 -1
- package/dist/esm/src/store/storage-controller.js +1 -3
- package/dist/esm/src/store/storage-controller.js.map +1 -1
- package/dist/esm/src/types/protocols-types.js +0 -10
- package/dist/esm/src/types/protocols-types.js.map +1 -1
- package/dist/esm/src/utils/filter.js +35 -1
- package/dist/esm/src/utils/filter.js.map +1 -1
- package/dist/esm/src/utils/grant-key-coverage.js +4 -6
- package/dist/esm/src/utils/grant-key-coverage.js.map +1 -1
- package/dist/esm/src/utils/messages.js +4 -14
- package/dist/esm/src/utils/messages.js.map +1 -1
- package/dist/esm/src/utils/permission-scope.js +3 -9
- package/dist/esm/src/utils/permission-scope.js.map +1 -1
- package/dist/esm/src/utils/record-limit-occupancy.js +73 -202
- package/dist/esm/src/utils/record-limit-occupancy.js.map +1 -1
- package/dist/esm/src/utils/records.js +19 -52
- package/dist/esm/src/utils/records.js.map +1 -1
- package/dist/esm/tests/core/records-grant-authorization.spec.js +13 -0
- package/dist/esm/tests/core/records-grant-authorization.spec.js.map +1 -1
- package/dist/esm/tests/core/replication-replay-property.spec.js +2 -2
- package/dist/esm/tests/core/replication-replay-property.spec.js.map +1 -1
- package/dist/esm/tests/core/validation-read-closure.spec.js +1 -1
- package/dist/esm/tests/core/validation-read-closure.spec.js.map +1 -1
- package/dist/esm/tests/dwn.spec.js +2 -2
- package/dist/esm/tests/dwn.spec.js.map +1 -1
- package/dist/esm/tests/features/records-delivery.spec.js +2 -5
- package/dist/esm/tests/features/records-delivery.spec.js.map +1 -1
- package/dist/esm/tests/features/records-nested-query-scope.spec.js +149 -70
- package/dist/esm/tests/features/records-nested-query-scope.spec.js.map +1 -1
- package/dist/esm/tests/features/records-record-limit.spec.js +380 -95
- package/dist/esm/tests/features/records-record-limit.spec.js.map +1 -1
- package/dist/esm/tests/fuzz/filter.fuzz.spec.js +19 -4
- package/dist/esm/tests/fuzz/filter.fuzz.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/handlers/records-write.spec.js +57 -3
- package/dist/esm/tests/handlers/records-write.spec.js.map +1 -1
- package/dist/esm/tests/store/index-level.spec.js +208 -1
- package/dist/esm/tests/store/index-level.spec.js.map +1 -1
- package/dist/esm/tests/store/message-store-cross-context.spec.js +46 -0
- package/dist/esm/tests/store/message-store-cross-context.spec.js.map +1 -0
- package/dist/esm/tests/store/message-store-level.spec.js +76 -1
- package/dist/esm/tests/store/message-store-level.spec.js.map +1 -1
- package/dist/esm/tests/store/message-store.spec.js +81 -0
- package/dist/esm/tests/store/message-store.spec.js.map +1 -1
- package/dist/esm/tests/utils/filters.spec.js +25 -4
- package/dist/esm/tests/utils/filters.spec.js.map +1 -1
- package/dist/esm/tests/utils/permission-scope.spec.js +4 -0
- package/dist/esm/tests/utils/permission-scope.spec.js.map +1 -1
- package/dist/esm/tests/utils/records.spec.js +12 -0
- package/dist/esm/tests/utils/records.spec.js.map +1 -1
- package/dist/esm/tests/validation/json-schemas/records/records-write.spec.js +3 -1
- package/dist/esm/tests/validation/json-schemas/records/records-write.spec.js.map +1 -1
- package/dist/types/generated/precompiled-validators.d.ts.map +1 -1
- package/dist/types/src/core/dwn-constant.d.ts +2 -2
- package/dist/types/src/core/dwn-error.d.ts +0 -1
- package/dist/types/src/core/dwn-error.d.ts.map +1 -1
- package/dist/types/src/core/protocol-authorization-validation.d.ts +0 -10
- package/dist/types/src/core/protocol-authorization-validation.d.ts.map +1 -1
- package/dist/types/src/core/protocol-authorization.d.ts.map +1 -1
- package/dist/types/src/core/validation-state-reader.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 -34
- 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/handlers/records-write.d.ts +3 -0
- package/dist/types/src/handlers/records-write.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +3 -3
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/interfaces/protocols-configure.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 +61 -0
- package/dist/types/src/store/index-level.d.ts.map +1 -1
- package/dist/types/src/store/message-store-level.d.ts +24 -6
- package/dist/types/src/store/message-store-level.d.ts.map +1 -1
- package/dist/types/src/store/storage-controller.d.ts.map +1 -1
- package/dist/types/src/types/message-store.d.ts +28 -2
- package/dist/types/src/types/message-store.d.ts.map +1 -1
- package/dist/types/src/types/protocols-types.d.ts +7 -24
- package/dist/types/src/types/protocols-types.d.ts.map +1 -1
- package/dist/types/src/types/query-types.d.ts +8 -1
- package/dist/types/src/types/query-types.d.ts.map +1 -1
- package/dist/types/src/types/records-types.d.ts +3 -2
- package/dist/types/src/types/records-types.d.ts.map +1 -1
- package/dist/types/src/utils/filter.d.ts +14 -1
- package/dist/types/src/utils/filter.d.ts.map +1 -1
- package/dist/types/src/utils/grant-key-coverage.d.ts.map +1 -1
- package/dist/types/src/utils/messages.d.ts.map +1 -1
- package/dist/types/src/utils/permission-scope.d.ts +0 -2
- package/dist/types/src/utils/permission-scope.d.ts.map +1 -1
- package/dist/types/src/utils/record-limit-occupancy.d.ts +25 -12
- package/dist/types/src/utils/record-limit-occupancy.d.ts.map +1 -1
- package/dist/types/src/utils/records.d.ts +6 -24
- package/dist/types/src/utils/records.d.ts.map +1 -1
- package/dist/types/tests/features/records-delivery.spec.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/dist/types/tests/handlers/records-write.spec.d.ts.map +1 -1
- package/dist/types/tests/store/message-store-cross-context.spec.d.ts +2 -0
- package/dist/types/tests/store/message-store-cross-context.spec.d.ts.map +1 -0
- package/dist/types/tests/store/message-store.spec.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/core/dwn-constant.ts +2 -2
- package/src/core/dwn-error.ts +0 -2
- package/src/core/protocol-authorization-validation.ts +1 -33
- package/src/core/protocol-authorization.ts +0 -7
- package/src/core/validation-state-reader.ts +2 -3
- package/src/handlers/records-collection.ts +269 -0
- package/src/handlers/records-count.ts +38 -232
- package/src/handlers/records-query.ts +22 -444
- package/src/handlers/records-subscribe.ts +26 -397
- package/src/handlers/records-write.ts +17 -32
- package/src/index.ts +3 -3
- package/src/interfaces/protocols-configure.ts +4 -13
- 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 +323 -28
- package/src/store/message-store-level.ts +251 -25
- package/src/store/storage-controller.ts +1 -3
- package/src/types/message-store.ts +30 -2
- package/src/types/protocols-types.ts +7 -26
- package/src/types/query-types.ts +10 -2
- package/src/types/records-types.ts +3 -2
- package/src/utils/filter.ts +40 -3
- package/src/utils/grant-key-coverage.ts +4 -7
- package/src/utils/messages.ts +4 -14
- package/src/utils/permission-scope.ts +4 -11
- package/src/utils/record-limit-occupancy.ts +108 -293
- package/src/utils/records.ts +22 -57
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { FilterUtility } from './filter.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Minimal protocol-bound scope shape used by permission grant authorization.
|
|
3
5
|
*
|
|
@@ -38,22 +40,13 @@ export class PermissionScopeMatcher {
|
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
if (scope.protocolPath !== undefined) {
|
|
41
|
-
return
|
|
43
|
+
return FilterUtility.matchesSubtree(scope.protocolPath, target.protocolPath);
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
if (scope.contextId !== undefined) {
|
|
45
|
-
return
|
|
47
|
+
return FilterUtility.matchesSubtree(scope.contextId, target.contextId);
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
return true;
|
|
49
51
|
}
|
|
50
|
-
|
|
51
|
-
private static matchesContextId(scopeContextId: string, candidateContextId: unknown): boolean {
|
|
52
|
-
return PermissionScopeMatcher.matchesSubtree(scopeContextId, candidateContextId);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
private static matchesSubtree(scopeValue: string, candidateValue: unknown): boolean {
|
|
56
|
-
return typeof candidateValue === 'string' &&
|
|
57
|
-
(candidateValue === scopeValue || candidateValue.startsWith(scopeValue + '/'));
|
|
58
|
-
}
|
|
59
52
|
}
|
|
@@ -1,42 +1,32 @@
|
|
|
1
|
-
import type { MessageStore } from '../types/message-store.js';
|
|
2
1
|
import type { ProtocolRecordLimitDefinition } from '../types/protocols-types.js';
|
|
3
|
-
import type { RecordsWriteMessage } from '../types/records-types.js';
|
|
4
2
|
import type { ValidationStateReader } from '../types/validation-state-reader.js';
|
|
5
3
|
import type { Filter, PaginationCursor } from '../types/query-types.js';
|
|
6
4
|
import type { MessageSort, Pagination } from '../types/message-types.js';
|
|
5
|
+
import type { MessageStore, RecordLimitOccupancy } from '../types/message-store.js';
|
|
6
|
+
import type { RecordsFilter, RecordsWriteMessage } from '../types/records-types.js';
|
|
7
7
|
|
|
8
|
-
import { FilterUtility } from './filter.js';
|
|
9
8
|
import { getRuleSetAtPath } from './protocols.js';
|
|
10
|
-
import { lexicographicalCompare } from './string.js';
|
|
11
|
-
import { ProtocolRecordLimitStrategy } from '../types/protocols-types.js';
|
|
12
9
|
import { Records } from './records.js';
|
|
13
|
-
import { SortDirection } from '../types/query-types.js';
|
|
14
10
|
import { DwnError, DwnErrorCode } from '../core/dwn-error.js';
|
|
15
11
|
import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js';
|
|
16
12
|
|
|
17
|
-
type
|
|
18
|
-
messageStore: MessageStore;
|
|
13
|
+
type RecordLimitPolicyDependencies = {
|
|
19
14
|
validationStateReader: ValidationStateReader;
|
|
20
15
|
};
|
|
21
16
|
|
|
22
|
-
type
|
|
17
|
+
type RecordLimitOccupancyDependencies = RecordLimitPolicyDependencies & {
|
|
18
|
+
messageStore: MessageStore;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type RecordLimitOccupancyQueryInput = {
|
|
22
|
+
messageStore: MessageStore;
|
|
23
23
|
tenant: string;
|
|
24
24
|
filters: Filter[];
|
|
25
|
-
|
|
25
|
+
recordLimit?: RecordLimitOccupancy;
|
|
26
26
|
messageSort?: MessageSort;
|
|
27
27
|
pagination?: Pagination;
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
-
type RecordLimitScope = {
|
|
31
|
-
protocol: string;
|
|
32
|
-
protocolPath: string;
|
|
33
|
-
parentContextId: string;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
type RecordLimitFilterResolution = {
|
|
37
|
-
projectedFilters: Filter[];
|
|
38
|
-
};
|
|
39
|
-
|
|
40
30
|
type OccupancyProjectionInput<T extends RecordsWriteMessage> = {
|
|
41
31
|
records: T[];
|
|
42
32
|
max: number;
|
|
@@ -45,171 +35,137 @@ type OccupancyProjectionInput<T extends RecordsWriteMessage> = {
|
|
|
45
35
|
};
|
|
46
36
|
|
|
47
37
|
/**
|
|
48
|
-
* Queries
|
|
49
|
-
*
|
|
50
|
-
* Broad filters keep the store's native query path. Projecting those without a store-level grouping primitive would require
|
|
51
|
-
* scanning the full matching set, which is worse than leaving them unprojected until a bounded broad-query strategy exists.
|
|
38
|
+
* Queries the caller-visible portion of the deterministic `$recordLimit`
|
|
39
|
+
* occupant population for one concrete protocol path.
|
|
52
40
|
*/
|
|
53
41
|
export async function queryRecordsWithRecordLimitOccupancy(
|
|
54
42
|
input: RecordLimitOccupancyQueryInput
|
|
55
43
|
): Promise<{ messages: RecordsWriteMessage[]; cursor?: PaginationCursor }> {
|
|
56
|
-
const filterResolution = await resolveRecordLimitFilters(input);
|
|
57
|
-
if (filterResolution === undefined) {
|
|
58
|
-
const { messages, cursor } = await input.messageStore.query(input.tenant, input.filters, input.messageSort, input.pagination);
|
|
59
|
-
return { messages: messages.filter(Records.isRecordsWrite), cursor };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (filterResolution.projectedFilters.length === 0) {
|
|
63
|
-
return { messages: [] };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
44
|
const { messages, cursor } = await input.messageStore.query(
|
|
67
45
|
input.tenant,
|
|
68
|
-
|
|
46
|
+
input.filters,
|
|
69
47
|
input.messageSort,
|
|
70
|
-
input.pagination
|
|
48
|
+
input.pagination,
|
|
49
|
+
input.recordLimit === undefined ? undefined : { recordLimit: input.recordLimit },
|
|
71
50
|
);
|
|
72
51
|
|
|
73
52
|
return { messages: messages.filter(Records.isRecordsWrite), cursor };
|
|
74
53
|
}
|
|
75
54
|
|
|
76
55
|
/**
|
|
77
|
-
* Counts
|
|
56
|
+
* Counts the same deterministic `$recordLimit` occupant population returned
|
|
57
|
+
* by {@link queryRecordsWithRecordLimitOccupancy}.
|
|
78
58
|
*/
|
|
79
|
-
export async function countRecordsWithRecordLimitOccupancy(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
return input.messageStore.count(input.tenant, filterResolution.projectedFilters, input.messageSort);
|
|
59
|
+
export async function countRecordsWithRecordLimitOccupancy(
|
|
60
|
+
input: Omit<RecordLimitOccupancyQueryInput, 'pagination'>
|
|
61
|
+
): Promise<number> {
|
|
62
|
+
return input.messageStore.count(
|
|
63
|
+
input.tenant,
|
|
64
|
+
input.filters,
|
|
65
|
+
input.messageSort,
|
|
66
|
+
input.recordLimit === undefined ? undefined : { recordLimit: input.recordLimit },
|
|
67
|
+
);
|
|
90
68
|
}
|
|
91
69
|
|
|
92
70
|
/**
|
|
93
|
-
* Returns true when the latest RecordsWrite is
|
|
71
|
+
* Returns true when the latest RecordsWrite is part of its path's current
|
|
72
|
+
* `$recordLimit` occupant population.
|
|
94
73
|
*/
|
|
95
74
|
export async function isRecordLimitOccupant(input: RecordLimitOccupancyDependencies & {
|
|
96
75
|
tenant: string;
|
|
97
76
|
message: RecordsWriteMessage;
|
|
98
77
|
messageTimestamp: string;
|
|
99
78
|
}): Promise<boolean> {
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
validationStateReader : input.validationStateReader,
|
|
103
|
-
tenant : input.tenant,
|
|
104
|
-
message : input.message,
|
|
105
|
-
messageTimestamp : input.messageTimestamp,
|
|
106
|
-
recordLimitDefinitions : new Map(),
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
if (recordLimit === undefined) {
|
|
79
|
+
const recordLimitDefinition = await getRecordLimitForMessage(input);
|
|
80
|
+
if (recordLimitDefinition === undefined) {
|
|
110
81
|
return true;
|
|
111
82
|
}
|
|
112
83
|
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
async function resolveRecordLimitFilters(
|
|
124
|
-
input: Omit<RecordLimitOccupancyQueryInput, 'pagination'>
|
|
125
|
-
): Promise<RecordLimitFilterResolution | undefined> {
|
|
126
|
-
const recordLimitDefinitions = new Map<string, ProtocolRecordLimitDefinition | undefined>();
|
|
127
|
-
const projectedFilters: Filter[] = [];
|
|
128
|
-
let projectionApplied = false;
|
|
129
|
-
|
|
130
|
-
for (const filter of input.filters) {
|
|
131
|
-
const recordLimit = await getRecordLimitForFilter({
|
|
132
|
-
messageStore : input.messageStore,
|
|
133
|
-
validationStateReader : input.validationStateReader,
|
|
134
|
-
tenant : input.tenant,
|
|
135
|
-
filter,
|
|
136
|
-
messageTimestamp : input.messageTimestamp,
|
|
137
|
-
recordLimitDefinitions,
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
if (recordLimit === undefined) {
|
|
141
|
-
projectedFilters.push(filter);
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const scope = getRecordLimitScopeFromFilter(filter);
|
|
146
|
-
if (scope === undefined) {
|
|
147
|
-
return undefined;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const occupantRecordIds = await findOccupantRecordIds({
|
|
151
|
-
messageStore : input.messageStore,
|
|
152
|
-
tenant : input.tenant,
|
|
153
|
-
scope,
|
|
154
|
-
recordLimit,
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
const projectedFilter = buildProjectedFilter(filter, occupantRecordIds);
|
|
158
|
-
if (projectedFilter !== undefined) {
|
|
159
|
-
projectedFilters.push(projectedFilter);
|
|
160
|
-
}
|
|
161
|
-
projectionApplied = true;
|
|
84
|
+
const { protocol, protocolPath } = input.message.descriptor;
|
|
85
|
+
const recordLimit: RecordLimitOccupancy = {
|
|
86
|
+
protocol,
|
|
87
|
+
protocolPath,
|
|
88
|
+
max: recordLimitDefinition.max,
|
|
89
|
+
};
|
|
90
|
+
const parentContextId = Records.getParentContextFromOfContextId(input.message.contextId);
|
|
91
|
+
if (parentContextId !== undefined && parentContextId !== '') {
|
|
92
|
+
recordLimit.contextId = parentContextId;
|
|
162
93
|
}
|
|
163
94
|
|
|
164
|
-
|
|
95
|
+
const count = await input.messageStore.count(input.tenant, [{
|
|
96
|
+
interface : DwnInterfaceName.Records,
|
|
97
|
+
method : DwnMethodName.Write,
|
|
98
|
+
isLatestBaseState : true,
|
|
99
|
+
protocol,
|
|
100
|
+
protocolPath,
|
|
101
|
+
recordId : input.message.recordId,
|
|
102
|
+
}], undefined, { recordLimit });
|
|
103
|
+
|
|
104
|
+
return count > 0;
|
|
165
105
|
}
|
|
166
106
|
|
|
167
|
-
|
|
107
|
+
/** Resolves one incoming Records filter to its store-level occupancy policy. */
|
|
108
|
+
export async function resolveRecordLimitOccupancy(input: RecordLimitPolicyDependencies & {
|
|
168
109
|
tenant: string;
|
|
169
|
-
|
|
110
|
+
recordsFilter: RecordsFilter;
|
|
170
111
|
messageTimestamp: string;
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const { protocol, protocolPath } = input.message.descriptor;
|
|
112
|
+
}): Promise<RecordLimitOccupancy | undefined> {
|
|
113
|
+
const { contextId, parentId, protocol, protocolPath } = input.recordsFilter;
|
|
174
114
|
if (protocol === undefined || protocolPath === undefined) {
|
|
175
115
|
return undefined;
|
|
176
116
|
}
|
|
177
117
|
|
|
178
|
-
|
|
179
|
-
validationStateReader
|
|
180
|
-
tenant
|
|
118
|
+
const recordLimitDefinition = await getRecordLimit({
|
|
119
|
+
validationStateReader : input.validationStateReader,
|
|
120
|
+
tenant : input.tenant,
|
|
181
121
|
protocol,
|
|
182
122
|
protocolPath,
|
|
183
|
-
messageTimestamp
|
|
184
|
-
recordLimitDefinitions : input.recordLimitDefinitions,
|
|
123
|
+
messageTimestamp : input.messageTimestamp,
|
|
185
124
|
});
|
|
125
|
+
if (recordLimitDefinition === undefined) {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const recordLimit: RecordLimitOccupancy = {
|
|
130
|
+
protocol,
|
|
131
|
+
protocolPath,
|
|
132
|
+
max: recordLimitDefinition.max,
|
|
133
|
+
};
|
|
134
|
+
if (!protocolPath.includes('/')) {
|
|
135
|
+
return recordLimit;
|
|
136
|
+
}
|
|
137
|
+
if (parentId !== undefined) {
|
|
138
|
+
recordLimit.parentId = parentId;
|
|
139
|
+
}
|
|
140
|
+
if (contextId === undefined) {
|
|
141
|
+
return recordLimit;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const contextDepth = contextId.split('/').length;
|
|
145
|
+
const protocolPathDepth = protocolPath.split('/').length;
|
|
146
|
+
recordLimit.contextId = contextDepth === protocolPathDepth
|
|
147
|
+
? Records.getParentContextFromOfContextId(contextId)
|
|
148
|
+
: contextId;
|
|
149
|
+
|
|
150
|
+
return recordLimit;
|
|
186
151
|
}
|
|
187
152
|
|
|
188
|
-
async function
|
|
153
|
+
async function getRecordLimitForMessage(input: RecordLimitOccupancyDependencies & {
|
|
189
154
|
tenant: string;
|
|
190
|
-
|
|
155
|
+
message: RecordsWriteMessage;
|
|
191
156
|
messageTimestamp: string;
|
|
192
|
-
recordLimitDefinitions: Map<string, ProtocolRecordLimitDefinition | undefined>;
|
|
193
157
|
}): Promise<ProtocolRecordLimitDefinition | undefined> {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
input.filter.isLatestBaseState !== true
|
|
197
|
-
) {
|
|
198
|
-
return undefined;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const { protocol, protocolPath } = input.filter;
|
|
202
|
-
if (typeof protocol !== 'string' || typeof protocolPath !== 'string') {
|
|
158
|
+
const { protocol, protocolPath } = input.message.descriptor;
|
|
159
|
+
if (protocol === undefined || protocolPath === undefined) {
|
|
203
160
|
return undefined;
|
|
204
161
|
}
|
|
205
162
|
|
|
206
163
|
return getRecordLimit({
|
|
207
|
-
validationStateReader
|
|
208
|
-
tenant
|
|
164
|
+
validationStateReader : input.validationStateReader,
|
|
165
|
+
tenant : input.tenant,
|
|
209
166
|
protocol,
|
|
210
167
|
protocolPath,
|
|
211
|
-
messageTimestamp
|
|
212
|
-
recordLimitDefinitions : input.recordLimitDefinitions,
|
|
168
|
+
messageTimestamp : input.messageTimestamp,
|
|
213
169
|
});
|
|
214
170
|
}
|
|
215
171
|
|
|
@@ -219,171 +175,30 @@ async function getRecordLimit(input: {
|
|
|
219
175
|
protocol: string;
|
|
220
176
|
protocolPath: string;
|
|
221
177
|
messageTimestamp: string;
|
|
222
|
-
recordLimitDefinitions: Map<string, ProtocolRecordLimitDefinition | undefined>;
|
|
223
178
|
}): Promise<ProtocolRecordLimitDefinition | undefined> {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
try {
|
|
228
|
-
protocolDefinition = await input.validationStateReader.fetchProtocolDefinition(
|
|
229
|
-
input.tenant,
|
|
230
|
-
input.protocol,
|
|
231
|
-
input.messageTimestamp,
|
|
232
|
-
);
|
|
233
|
-
} catch (error) {
|
|
234
|
-
if (error instanceof DwnError && error.code === DwnErrorCode.ProtocolAuthorizationProtocolNotFound) {
|
|
235
|
-
input.recordLimitDefinitions.set(key, undefined);
|
|
236
|
-
return undefined;
|
|
237
|
-
}
|
|
238
|
-
throw error;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const ruleSet = getRuleSetAtPath(input.protocolPath, protocolDefinition.structure);
|
|
242
|
-
const recordLimit = ruleSet?.$recordLimit;
|
|
243
|
-
input.recordLimitDefinitions.set(
|
|
244
|
-
key,
|
|
245
|
-
recordLimit?.strategy === ProtocolRecordLimitStrategy.Reject ? recordLimit : undefined
|
|
246
|
-
);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
return input.recordLimitDefinitions.get(key);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
async function findOccupantRecordIds(input: {
|
|
253
|
-
messageStore: MessageStore;
|
|
254
|
-
tenant: string;
|
|
255
|
-
scope: RecordLimitScope;
|
|
256
|
-
recordLimit: ProtocolRecordLimitDefinition;
|
|
257
|
-
}): Promise<Set<string>> {
|
|
258
|
-
const scopeFilter = buildRecordLimitScopeFilter(input.scope);
|
|
259
|
-
const messageSort = { dateCreated: SortDirection.Ascending };
|
|
260
|
-
const { messages: firstPage } = await input.messageStore.query(
|
|
261
|
-
input.tenant,
|
|
262
|
-
[scopeFilter],
|
|
263
|
-
messageSort,
|
|
264
|
-
{ limit: input.recordLimit.max }
|
|
265
|
-
);
|
|
266
|
-
const firstPageCandidates = firstPage.filter(Records.isRecordsWrite);
|
|
267
|
-
if (firstPageCandidates.length === 0) {
|
|
268
|
-
return new Set();
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const boundaryDateCreated = firstPageCandidates.at(-1)!.descriptor.dateCreated;
|
|
272
|
-
const beforeBoundary = firstPageCandidates.filter(
|
|
273
|
-
(message): boolean => message.descriptor.dateCreated < boundaryDateCreated
|
|
274
|
-
);
|
|
275
|
-
let boundaryCandidates = firstPageCandidates.filter(
|
|
276
|
-
(message): boolean => message.descriptor.dateCreated === boundaryDateCreated
|
|
277
|
-
);
|
|
278
|
-
|
|
279
|
-
if (firstPageCandidates.length === input.recordLimit.max) {
|
|
280
|
-
const { messages } = await input.messageStore.query(
|
|
179
|
+
let protocolDefinition;
|
|
180
|
+
try {
|
|
181
|
+
protocolDefinition = await input.validationStateReader.fetchProtocolDefinition(
|
|
281
182
|
input.tenant,
|
|
282
|
-
|
|
283
|
-
|
|
183
|
+
input.protocol,
|
|
184
|
+
input.messageTimestamp,
|
|
284
185
|
);
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const rankedCandidates = [
|
|
291
|
-
...beforeBoundary,
|
|
292
|
-
...boundaryCandidates,
|
|
293
|
-
];
|
|
294
|
-
|
|
295
|
-
return selectOccupantRecordIds({
|
|
296
|
-
records : rankedCandidates,
|
|
297
|
-
max : input.recordLimit.max,
|
|
298
|
-
getScopeKey : (): string => '',
|
|
299
|
-
compareRecords : compareRecordLimitCandidates,
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function buildRecordLimitScopeFilter(scope: RecordLimitScope): Filter {
|
|
304
|
-
const filter: Filter = {
|
|
305
|
-
interface : DwnInterfaceName.Records,
|
|
306
|
-
method : DwnMethodName.Write,
|
|
307
|
-
isLatestBaseState : true,
|
|
308
|
-
protocol : scope.protocol,
|
|
309
|
-
protocolPath : scope.protocolPath,
|
|
310
|
-
};
|
|
311
|
-
|
|
312
|
-
if (scope.parentContextId !== '') {
|
|
313
|
-
filter.contextId = FilterUtility.constructPrefixFilterAsRangeFilter(scope.parentContextId);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
return filter;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function getRecordLimitScopeFromFilter(filter: Filter): RecordLimitScope | undefined {
|
|
320
|
-
const { protocol, protocolPath } = filter;
|
|
321
|
-
if (typeof protocol !== 'string' || typeof protocolPath !== 'string') {
|
|
322
|
-
return undefined;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
if (!protocolPath.includes('/')) {
|
|
326
|
-
return { protocol, protocolPath, parentContextId: '' };
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const parentContextId = getExactParentContextIdFromFilter(filter);
|
|
330
|
-
if (parentContextId === undefined) {
|
|
331
|
-
return undefined;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
return { protocol, protocolPath, parentContextId };
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function getRecordLimitScope(message: RecordsWriteMessage): RecordLimitScope {
|
|
338
|
-
return {
|
|
339
|
-
protocol : message.descriptor.protocol,
|
|
340
|
-
protocolPath : message.descriptor.protocolPath,
|
|
341
|
-
parentContextId : Records.getParentContextFromOfContextId(message.contextId) ?? '',
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
function getExactParentContextIdFromFilter(filter: Filter): string | undefined {
|
|
346
|
-
const { contextId } = filter;
|
|
347
|
-
if (contextId === undefined || !FilterUtility.isRangeFilter(contextId)) {
|
|
348
|
-
return undefined;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
if (typeof contextId.gte !== 'string' || contextId.lt !== `${contextId.gte}\uffff`) {
|
|
352
|
-
return undefined;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
return contextId.gte;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function buildProjectedFilter(filter: Filter, occupantRecordIds: Set<string>): Filter | undefined {
|
|
359
|
-
let projectedRecordIds = Array.from(occupantRecordIds);
|
|
360
|
-
const existingRecordIdFilter = filter.recordId;
|
|
361
|
-
if (typeof existingRecordIdFilter === 'string') {
|
|
362
|
-
projectedRecordIds = occupantRecordIds.has(existingRecordIdFilter) ? [existingRecordIdFilter] : [];
|
|
363
|
-
} else if (Array.isArray(existingRecordIdFilter)) {
|
|
364
|
-
projectedRecordIds = existingRecordIdFilter
|
|
365
|
-
.filter((recordId): recordId is string => typeof recordId === 'string' && occupantRecordIds.has(recordId));
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
if (projectedRecordIds.length === 0) {
|
|
369
|
-
return undefined;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
return {
|
|
373
|
-
...filter,
|
|
374
|
-
recordId: projectedRecordIds,
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function compareRecordLimitCandidates(left: RecordsWriteMessage, right: RecordsWriteMessage): number {
|
|
379
|
-
const dateComparison = lexicographicalCompare(left.descriptor.dateCreated, right.descriptor.dateCreated);
|
|
380
|
-
if (dateComparison !== 0) {
|
|
381
|
-
return dateComparison;
|
|
186
|
+
} catch (error) {
|
|
187
|
+
if (error instanceof DwnError && error.code === DwnErrorCode.ProtocolAuthorizationProtocolNotFound) {
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
throw error;
|
|
382
191
|
}
|
|
383
192
|
|
|
384
|
-
|
|
193
|
+
const ruleSet = getRuleSetAtPath(input.protocolPath, protocolDefinition.structure);
|
|
194
|
+
const recordLimit = ruleSet?.$recordLimit;
|
|
195
|
+
return recordLimit;
|
|
385
196
|
}
|
|
386
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Selects a bounded occupant set for an in-memory projection whose grouping
|
|
200
|
+
* and ranking policy is supplied by the caller.
|
|
201
|
+
*/
|
|
387
202
|
export function selectOccupantRecordIds<T extends RecordsWriteMessage>(input: OccupancyProjectionInput<T>): Set<string> {
|
|
388
203
|
const occupants = new Set<string>();
|
|
389
204
|
const groups = new Map<string, T[]>();
|
package/src/utils/records.ts
CHANGED
|
@@ -280,15 +280,19 @@ export class Records {
|
|
|
280
280
|
}
|
|
281
281
|
|
|
282
282
|
/**
|
|
283
|
-
* Nested protocol-path
|
|
284
|
-
*
|
|
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.
|
|
285
288
|
*/
|
|
286
|
-
public static
|
|
289
|
+
public static validateNestedProtocolPathScope(
|
|
287
290
|
filter: RecordsFilter,
|
|
288
291
|
errorCode: DwnErrorCode,
|
|
289
292
|
operationName: string,
|
|
293
|
+
allowBoundedPathWideSelection = false,
|
|
290
294
|
): void {
|
|
291
|
-
const { contextId, protocolPath } = filter;
|
|
295
|
+
const { contextId, parentId, protocolPath } = filter;
|
|
292
296
|
if (!protocolPath?.includes('/')) {
|
|
293
297
|
return;
|
|
294
298
|
}
|
|
@@ -297,10 +301,19 @@ export class Records {
|
|
|
297
301
|
return;
|
|
298
302
|
}
|
|
299
303
|
|
|
300
|
-
|
|
304
|
+
if (parentId !== undefined && contextId === undefined) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (contextId === undefined && allowBoundedPathWideSelection) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const protocolPathDepth = protocolPath.split('/').length;
|
|
301
313
|
const contextIdSegments = contextId?.split('/');
|
|
302
314
|
if (
|
|
303
|
-
contextIdSegments
|
|
315
|
+
contextIdSegments !== undefined &&
|
|
316
|
+
contextIdSegments.length <= protocolPathDepth &&
|
|
304
317
|
contextIdSegments.every(segment => segment.length > 0)
|
|
305
318
|
) {
|
|
306
319
|
return;
|
|
@@ -308,7 +321,7 @@ export class Records {
|
|
|
308
321
|
|
|
309
322
|
throw new DwnError(
|
|
310
323
|
errorCode,
|
|
311
|
-
`${operationName} for nested protocol path '${protocolPath}' must include
|
|
324
|
+
`${operationName} for nested protocol path '${protocolPath}' must include parentId or a contextId selecting that path or one of its ancestors`
|
|
312
325
|
);
|
|
313
326
|
}
|
|
314
327
|
|
|
@@ -386,10 +399,8 @@ export class Records {
|
|
|
386
399
|
delete filterCopy.dateUpdated;
|
|
387
400
|
}
|
|
388
401
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
if (contextIdPrefixFilter) {
|
|
392
|
-
filterCopy.contextId = contextIdPrefixFilter;
|
|
402
|
+
if (contextId !== undefined) {
|
|
403
|
+
filterCopy.contextId = { subtree: contextId };
|
|
393
404
|
}
|
|
394
405
|
|
|
395
406
|
// if the author filter is an array and it's empty, we should remove it from the filter as it will always return no results.
|
|
@@ -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
|
}
|