@enbox/dwn-sdk-js 0.4.16 → 0.4.17
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 +734 -747
- 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-count.js +18 -10
- package/dist/esm/src/handlers/records-count.js.map +1 -1
- package/dist/esm/src/handlers/records-query.js +10 -7
- package/dist/esm/src/handlers/records-query.js.map +1 -1
- package/dist/esm/src/handlers/records-subscribe.js +10 -7
- 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/store/index-level.js +143 -6
- package/dist/esm/src/store/index-level.js.map +1 -1
- package/dist/esm/src/store/message-store-level.js +158 -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 +70 -202
- package/dist/esm/src/utils/record-limit-occupancy.js.map +1 -1
- package/dist/esm/src/utils/records.js +9 -9
- 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 +138 -73
- package/dist/esm/tests/features/records-nested-query-scope.spec.js.map +1 -1
- package/dist/esm/tests/features/records-record-limit.spec.js +264 -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-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 +114 -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 +6 -0
- 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-count.d.ts +1 -0
- package/dist/types/src/handlers/records-count.d.ts.map +1 -1
- package/dist/types/src/handlers/records-query.d.ts.map +1 -1
- 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/store/index-level.d.ts +37 -0
- package/dist/types/src/store/index-level.d.ts.map +1 -1
- package/dist/types/src/store/message-store-level.d.ts +23 -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 +23 -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 +2 -1
- 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 +3 -2
- 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-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-count.ts +34 -13
- package/src/handlers/records-query.ts +14 -11
- package/src/handlers/records-subscribe.ts +18 -11
- package/src/handlers/records-write.ts +17 -32
- package/src/index.ts +3 -3
- package/src/interfaces/protocols-configure.ts +4 -13
- package/src/store/index-level.ts +197 -10
- package/src/store/message-store-level.ts +216 -25
- package/src/store/storage-controller.ts +1 -3
- package/src/types/message-store.ts +25 -2
- package/src/types/protocols-types.ts +7 -26
- package/src/types/query-types.ts +10 -2
- package/src/types/records-types.ts +2 -1
- 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 +105 -293
- package/src/utils/records.ts +9 -9
|
@@ -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,134 @@ 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, 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 (contextId === undefined) {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const contextDepth = contextId.split('/').length;
|
|
142
|
+
const protocolPathDepth = protocolPath.split('/').length;
|
|
143
|
+
recordLimit.contextId = contextDepth === protocolPathDepth
|
|
144
|
+
? Records.getParentContextFromOfContextId(contextId)
|
|
145
|
+
: contextId;
|
|
146
|
+
|
|
147
|
+
return recordLimit;
|
|
186
148
|
}
|
|
187
149
|
|
|
188
|
-
async function
|
|
150
|
+
async function getRecordLimitForMessage(input: RecordLimitOccupancyDependencies & {
|
|
189
151
|
tenant: string;
|
|
190
|
-
|
|
152
|
+
message: RecordsWriteMessage;
|
|
191
153
|
messageTimestamp: string;
|
|
192
|
-
recordLimitDefinitions: Map<string, ProtocolRecordLimitDefinition | undefined>;
|
|
193
154
|
}): 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') {
|
|
155
|
+
const { protocol, protocolPath } = input.message.descriptor;
|
|
156
|
+
if (protocol === undefined || protocolPath === undefined) {
|
|
203
157
|
return undefined;
|
|
204
158
|
}
|
|
205
159
|
|
|
206
160
|
return getRecordLimit({
|
|
207
|
-
validationStateReader
|
|
208
|
-
tenant
|
|
161
|
+
validationStateReader : input.validationStateReader,
|
|
162
|
+
tenant : input.tenant,
|
|
209
163
|
protocol,
|
|
210
164
|
protocolPath,
|
|
211
|
-
messageTimestamp
|
|
212
|
-
recordLimitDefinitions : input.recordLimitDefinitions,
|
|
165
|
+
messageTimestamp : input.messageTimestamp,
|
|
213
166
|
});
|
|
214
167
|
}
|
|
215
168
|
|
|
@@ -219,171 +172,30 @@ async function getRecordLimit(input: {
|
|
|
219
172
|
protocol: string;
|
|
220
173
|
protocolPath: string;
|
|
221
174
|
messageTimestamp: string;
|
|
222
|
-
recordLimitDefinitions: Map<string, ProtocolRecordLimitDefinition | undefined>;
|
|
223
175
|
}): 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(
|
|
176
|
+
let protocolDefinition;
|
|
177
|
+
try {
|
|
178
|
+
protocolDefinition = await input.validationStateReader.fetchProtocolDefinition(
|
|
281
179
|
input.tenant,
|
|
282
|
-
|
|
283
|
-
|
|
180
|
+
input.protocol,
|
|
181
|
+
input.messageTimestamp,
|
|
284
182
|
);
|
|
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;
|
|
183
|
+
} catch (error) {
|
|
184
|
+
if (error instanceof DwnError && error.code === DwnErrorCode.ProtocolAuthorizationProtocolNotFound) {
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
throw error;
|
|
382
188
|
}
|
|
383
189
|
|
|
384
|
-
|
|
190
|
+
const ruleSet = getRuleSetAtPath(input.protocolPath, protocolDefinition.structure);
|
|
191
|
+
const recordLimit = ruleSet?.$recordLimit;
|
|
192
|
+
return recordLimit;
|
|
385
193
|
}
|
|
386
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Selects a bounded occupant set for an in-memory projection whose grouping
|
|
197
|
+
* and ranking policy is supplied by the caller.
|
|
198
|
+
*/
|
|
387
199
|
export function selectOccupantRecordIds<T extends RecordsWriteMessage>(input: OccupancyProjectionInput<T>): Set<string> {
|
|
388
200
|
const occupants = new Set<string>();
|
|
389
201
|
const groups = new Map<string, T[]>();
|
package/src/utils/records.ts
CHANGED
|
@@ -280,8 +280,9 @@ export class Records {
|
|
|
280
280
|
}
|
|
281
281
|
|
|
282
282
|
/**
|
|
283
|
-
* Nested protocol-path queries must
|
|
284
|
-
*
|
|
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.
|
|
285
286
|
*/
|
|
286
287
|
public static validateNestedProtocolPathQueryScope(
|
|
287
288
|
filter: RecordsFilter,
|
|
@@ -297,10 +298,11 @@ export class Records {
|
|
|
297
298
|
return;
|
|
298
299
|
}
|
|
299
300
|
|
|
300
|
-
const
|
|
301
|
+
const protocolPathDepth = protocolPath.split('/').length;
|
|
301
302
|
const contextIdSegments = contextId?.split('/');
|
|
302
303
|
if (
|
|
303
|
-
contextIdSegments
|
|
304
|
+
contextIdSegments !== undefined &&
|
|
305
|
+
contextIdSegments.length <= protocolPathDepth &&
|
|
304
306
|
contextIdSegments.every(segment => segment.length > 0)
|
|
305
307
|
) {
|
|
306
308
|
return;
|
|
@@ -308,7 +310,7 @@ export class Records {
|
|
|
308
310
|
|
|
309
311
|
throw new DwnError(
|
|
310
312
|
errorCode,
|
|
311
|
-
`${operationName} for nested protocol path '${protocolPath}' must include
|
|
313
|
+
`${operationName} for nested protocol path '${protocolPath}' must include a contextId selecting that path or one of its ancestors`
|
|
312
314
|
);
|
|
313
315
|
}
|
|
314
316
|
|
|
@@ -386,10 +388,8 @@ export class Records {
|
|
|
386
388
|
delete filterCopy.dateUpdated;
|
|
387
389
|
}
|
|
388
390
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
if (contextIdPrefixFilter) {
|
|
392
|
-
filterCopy.contextId = contextIdPrefixFilter;
|
|
391
|
+
if (contextId !== undefined) {
|
|
392
|
+
filterCopy.contextId = { subtree: contextId };
|
|
393
393
|
}
|
|
394
394
|
|
|
395
395
|
// 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.
|