@enbox/api 0.1.1 → 0.2.0
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/README.md +140 -159
- package/dist/browser.mjs +23 -13
- package/dist/browser.mjs.map +4 -4
- package/dist/esm/advanced.js +11 -0
- package/dist/esm/advanced.js.map +1 -0
- package/dist/esm/define-protocol.js +3 -3
- package/dist/esm/dwn-api.js +55 -107
- package/dist/esm/dwn-api.js.map +1 -1
- package/dist/esm/dwn-reader-api.js +128 -0
- package/dist/esm/dwn-reader-api.js.map +1 -0
- package/dist/esm/index.js +3 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/live-query.js +5 -4
- package/dist/esm/live-query.js.map +1 -1
- package/dist/esm/read-only-record.js +255 -0
- package/dist/esm/read-only-record.js.map +1 -0
- package/dist/esm/record.js +46 -57
- package/dist/esm/record.js.map +1 -1
- package/dist/esm/typed-web5.js +242 -0
- package/dist/esm/typed-web5.js.map +1 -0
- package/dist/esm/web5.js +71 -3
- package/dist/esm/web5.js.map +1 -1
- package/dist/types/advanced.d.ts +12 -0
- package/dist/types/advanced.d.ts.map +1 -0
- package/dist/types/define-protocol.d.ts +3 -3
- package/dist/types/dwn-api.d.ts +12 -89
- package/dist/types/dwn-api.d.ts.map +1 -1
- package/dist/types/dwn-reader-api.d.ts +138 -0
- package/dist/types/dwn-reader-api.d.ts.map +1 -0
- package/dist/types/index.d.ts +3 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/live-query.d.ts +13 -1
- package/dist/types/live-query.d.ts.map +1 -1
- package/dist/types/protocol-types.d.ts +2 -2
- package/dist/types/read-only-record.d.ts +133 -0
- package/dist/types/read-only-record.d.ts.map +1 -0
- package/dist/types/record.d.ts +37 -17
- package/dist/types/record.d.ts.map +1 -1
- package/dist/types/{typed-dwn-api.d.ts → typed-web5.d.ts} +70 -73
- package/dist/types/typed-web5.d.ts.map +1 -0
- package/dist/types/web5.d.ts +79 -3
- package/dist/types/web5.d.ts.map +1 -1
- package/package.json +9 -5
- package/src/advanced.ts +29 -0
- package/src/define-protocol.ts +3 -3
- package/src/dwn-api.ts +88 -222
- package/src/dwn-reader-api.ts +255 -0
- package/src/index.ts +3 -2
- package/src/live-query.ts +20 -4
- package/src/protocol-types.ts +2 -2
- package/src/read-only-record.ts +328 -0
- package/src/record.ts +116 -86
- package/src/typed-web5.ts +445 -0
- package/src/web5.ts +104 -5
- package/dist/esm/typed-dwn-api.js +0 -182
- package/dist/esm/typed-dwn-api.js.map +0 -1
- package/dist/types/typed-dwn-api.d.ts.map +0 -1
- package/src/typed-dwn-api.ts +0 -370
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NOTE: Added reference types here to avoid a `pnpm` bug during build.
|
|
3
|
+
* https://github.com/enboxorg/enbox/pull/507
|
|
4
|
+
*/
|
|
5
|
+
/// <reference types="@enbox/dwn-sdk-js" />
|
|
6
|
+
|
|
7
|
+
import type { AnonymousDwnApi, DwnPaginationCursor, DwnResponseStatus } from '@enbox/agent';
|
|
8
|
+
import type {
|
|
9
|
+
DateSort,
|
|
10
|
+
Pagination,
|
|
11
|
+
ProtocolDefinition,
|
|
12
|
+
ProtocolsQueryFilter,
|
|
13
|
+
RecordsFilter,
|
|
14
|
+
} from '@enbox/dwn-sdk-js';
|
|
15
|
+
|
|
16
|
+
import { ReadOnlyRecord } from './read-only-record.js';
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Request / Response types for records
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Request to query public records from a remote DWN.
|
|
24
|
+
*
|
|
25
|
+
* @beta
|
|
26
|
+
*/
|
|
27
|
+
export type ReaderRecordsQueryRequest = {
|
|
28
|
+
/** The DID of the remote DWN to query (required — reader is remote-only). */
|
|
29
|
+
from: string;
|
|
30
|
+
/** Filter criteria for the query. */
|
|
31
|
+
filter: RecordsFilter;
|
|
32
|
+
/** Sort order for results. */
|
|
33
|
+
dateSort?: DateSort;
|
|
34
|
+
/** Pagination options. */
|
|
35
|
+
pagination?: Pagination;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Response from a reader records query.
|
|
40
|
+
*
|
|
41
|
+
* @beta
|
|
42
|
+
*/
|
|
43
|
+
export type ReaderRecordsQueryResponse = DwnResponseStatus & {
|
|
44
|
+
/** Array of read-only records matching the query. */
|
|
45
|
+
records: ReadOnlyRecord[];
|
|
46
|
+
/** Pagination cursor for fetching the next page. */
|
|
47
|
+
cursor?: DwnPaginationCursor;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Request to read a specific public record from a remote DWN.
|
|
52
|
+
*
|
|
53
|
+
* @beta
|
|
54
|
+
*/
|
|
55
|
+
export type ReaderRecordsReadRequest = {
|
|
56
|
+
/** The DID of the remote DWN to read from (required — reader is remote-only). */
|
|
57
|
+
from: string;
|
|
58
|
+
/** Filter to identify the record (typically `{ recordId: '...' }`). */
|
|
59
|
+
filter: RecordsFilter;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Response from a reader records read.
|
|
64
|
+
*
|
|
65
|
+
* @beta
|
|
66
|
+
*/
|
|
67
|
+
export type ReaderRecordsReadResponse = DwnResponseStatus & {
|
|
68
|
+
/** The read-only record, if found. */
|
|
69
|
+
record?: ReadOnlyRecord;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Request to count public records on a remote DWN.
|
|
74
|
+
*
|
|
75
|
+
* @beta
|
|
76
|
+
*/
|
|
77
|
+
export type ReaderRecordsCountRequest = {
|
|
78
|
+
/** The DID of the remote DWN to count records in (required). */
|
|
79
|
+
from: string;
|
|
80
|
+
/** Filter criteria for counting. */
|
|
81
|
+
filter: RecordsFilter;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Response from a reader records count.
|
|
86
|
+
*
|
|
87
|
+
* @beta
|
|
88
|
+
*/
|
|
89
|
+
export type ReaderRecordsCountResponse = DwnResponseStatus & {
|
|
90
|
+
/** The number of matching public records. */
|
|
91
|
+
count?: number;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Request / Response types for protocols
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Request to query published protocols from a remote DWN.
|
|
100
|
+
*
|
|
101
|
+
* @beta
|
|
102
|
+
*/
|
|
103
|
+
export type ReaderProtocolsQueryRequest = {
|
|
104
|
+
/** The DID of the remote DWN to query protocols from (required). */
|
|
105
|
+
from: string;
|
|
106
|
+
/** Optional filter for the protocol query. */
|
|
107
|
+
filter?: ProtocolsQueryFilter;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Response from a reader protocols query.
|
|
112
|
+
*
|
|
113
|
+
* @beta
|
|
114
|
+
*/
|
|
115
|
+
export type ReaderProtocolsQueryResponse = DwnResponseStatus & {
|
|
116
|
+
/** Array of published protocol definitions. */
|
|
117
|
+
protocols: ProtocolDefinition[];
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// DwnReaderApi
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* A read-only API for querying public data on remote DWNs without any identity or signing keys.
|
|
126
|
+
*
|
|
127
|
+
* This class mirrors the shape of {@link DwnApi}'s `records` and `protocols`
|
|
128
|
+
* namespaces, but restricts to read-path operations and requires a `from` DID
|
|
129
|
+
* on every call (remote-only). All messages are unsigned, so only published
|
|
130
|
+
* records and protocols are accessible.
|
|
131
|
+
*
|
|
132
|
+
* Obtain an instance via {@link Web5.anonymous | `Web5.anonymous()`}.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```ts
|
|
136
|
+
* const { dwn } = Web5.anonymous();
|
|
137
|
+
*
|
|
138
|
+
* const { records } = await dwn.records.query({
|
|
139
|
+
* from: 'did:dht:alice...',
|
|
140
|
+
* filter: { protocol: 'https://social.example/posts', protocolPath: 'post' },
|
|
141
|
+
* });
|
|
142
|
+
*
|
|
143
|
+
* for (const record of records) {
|
|
144
|
+
* console.log(record.id, await record.data.text());
|
|
145
|
+
* }
|
|
146
|
+
* ```
|
|
147
|
+
*
|
|
148
|
+
* @beta
|
|
149
|
+
*/
|
|
150
|
+
export class DwnReaderApi {
|
|
151
|
+
private _anonymousDwn: AnonymousDwnApi;
|
|
152
|
+
|
|
153
|
+
constructor(anonymousDwn: AnonymousDwnApi) {
|
|
154
|
+
this._anonymousDwn = anonymousDwn;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* API to interact with public DWN records (query, read, count).
|
|
159
|
+
*/
|
|
160
|
+
get records(): {
|
|
161
|
+
query: (request: ReaderRecordsQueryRequest) => Promise<ReaderRecordsQueryResponse>;
|
|
162
|
+
read: (request: ReaderRecordsReadRequest) => Promise<ReaderRecordsReadResponse>;
|
|
163
|
+
count: (request: ReaderRecordsCountRequest) => Promise<ReaderRecordsCountResponse>;
|
|
164
|
+
} {
|
|
165
|
+
return {
|
|
166
|
+
/**
|
|
167
|
+
* Query public records from a remote DWN.
|
|
168
|
+
* Only published records are returned.
|
|
169
|
+
*/
|
|
170
|
+
query: async (request: ReaderRecordsQueryRequest): Promise<ReaderRecordsQueryResponse> => {
|
|
171
|
+
const reply = await this._anonymousDwn.recordsQuery(request.from, {
|
|
172
|
+
filter : request.filter,
|
|
173
|
+
dateSort : request.dateSort,
|
|
174
|
+
pagination : request.pagination,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const { entries = [], status, cursor } = reply;
|
|
178
|
+
|
|
179
|
+
const records = entries.map((entry) => new ReadOnlyRecord({
|
|
180
|
+
rawMessage : entry,
|
|
181
|
+
initialWrite : entry.initialWrite,
|
|
182
|
+
encodedData : entry.encodedData,
|
|
183
|
+
remoteOrigin : request.from,
|
|
184
|
+
anonymousDwn : this._anonymousDwn,
|
|
185
|
+
}));
|
|
186
|
+
|
|
187
|
+
return { records, status, cursor };
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Read a specific public record from a remote DWN.
|
|
192
|
+
* Succeeds for published records and protocol records with `{ who: 'anyone', can: ['read'] }`.
|
|
193
|
+
*/
|
|
194
|
+
read: async (request: ReaderRecordsReadRequest): Promise<ReaderRecordsReadResponse> => {
|
|
195
|
+
const reply = await this._anonymousDwn.recordsRead(request.from, {
|
|
196
|
+
filter: request.filter,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const { entry, status } = reply;
|
|
200
|
+
|
|
201
|
+
let record: ReadOnlyRecord | undefined;
|
|
202
|
+
if (200 <= status.code && status.code <= 299 && entry?.recordsWrite) {
|
|
203
|
+
record = new ReadOnlyRecord({
|
|
204
|
+
rawMessage : entry.recordsWrite,
|
|
205
|
+
initialWrite : entry.initialWrite,
|
|
206
|
+
data : entry.data,
|
|
207
|
+
remoteOrigin : request.from,
|
|
208
|
+
anonymousDwn : this._anonymousDwn,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return { record, status };
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Count public records on a remote DWN.
|
|
217
|
+
* Only published records are counted.
|
|
218
|
+
*/
|
|
219
|
+
count: async (request: ReaderRecordsCountRequest): Promise<ReaderRecordsCountResponse> => {
|
|
220
|
+
const reply = await this._anonymousDwn.recordsCount(request.from, {
|
|
221
|
+
filter: request.filter,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const { count, status } = reply;
|
|
225
|
+
|
|
226
|
+
return { count, status };
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* API to query published protocol definitions from remote DWNs.
|
|
233
|
+
*/
|
|
234
|
+
get protocols(): {
|
|
235
|
+
query: (request: ReaderProtocolsQueryRequest) => Promise<ReaderProtocolsQueryResponse>;
|
|
236
|
+
} {
|
|
237
|
+
return {
|
|
238
|
+
/**
|
|
239
|
+
* Query published protocols from a remote DWN.
|
|
240
|
+
* Only protocol definitions with `published: true` are returned.
|
|
241
|
+
*/
|
|
242
|
+
query: async (request: ReaderProtocolsQueryRequest): Promise<ReaderProtocolsQueryResponse> => {
|
|
243
|
+
const reply = await this._anonymousDwn.protocolsQuery(request.from, {
|
|
244
|
+
filter: request.filter,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const { entries = [], status } = reply;
|
|
248
|
+
|
|
249
|
+
const protocols = entries.map((entry) => entry.descriptor.definition);
|
|
250
|
+
|
|
251
|
+
return { protocols, status };
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -23,15 +23,16 @@
|
|
|
23
23
|
|
|
24
24
|
export * from './define-protocol.js';
|
|
25
25
|
export * from './did-api.js';
|
|
26
|
-
export * from './dwn-api.js';
|
|
26
|
+
export * from './dwn-reader-api.js';
|
|
27
27
|
export * from './grant-revocation.js';
|
|
28
28
|
export * from './live-query.js';
|
|
29
29
|
export * from './permission-grant.js';
|
|
30
30
|
export * from './permission-request.js';
|
|
31
31
|
export * from './protocol.js';
|
|
32
32
|
export * from './protocol-types.js';
|
|
33
|
+
export * from './read-only-record.js';
|
|
33
34
|
export * from './record.js';
|
|
34
|
-
export * from './typed-
|
|
35
|
+
export * from './typed-web5.js';
|
|
35
36
|
export * from './vc-api.js';
|
|
36
37
|
export * from './web5.js';
|
|
37
38
|
|
package/src/live-query.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { RecordsQueryReplyEntry } from '@enbox/dwn-sdk-js';
|
|
2
1
|
import type { DwnMessageSubscription, PermissionsApi, Web5Agent } from '@enbox/agent';
|
|
2
|
+
import type { PaginationCursor, RecordsQueryReplyEntry } from '@enbox/dwn-sdk-js';
|
|
3
3
|
|
|
4
4
|
import { getRecordAuthor } from '@enbox/agent';
|
|
5
5
|
|
|
@@ -60,6 +60,9 @@ export type LiveQueryOptions = {
|
|
|
60
60
|
/** The initial snapshot entries from the subscribe reply. */
|
|
61
61
|
initialEntries: RecordsQueryReplyEntry[];
|
|
62
62
|
|
|
63
|
+
/** Pagination cursor for fetching the next page of initial results. */
|
|
64
|
+
cursor?: PaginationCursor;
|
|
65
|
+
|
|
63
66
|
/** The underlying DWN subscription handle. */
|
|
64
67
|
subscription: DwnMessageSubscription;
|
|
65
68
|
};
|
|
@@ -116,6 +119,17 @@ export class LiveQuery extends EventTarget {
|
|
|
116
119
|
/** The initial snapshot of matching records. */
|
|
117
120
|
readonly records: Record[];
|
|
118
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Pagination cursor for fetching the next page of initial results.
|
|
124
|
+
*
|
|
125
|
+
* When the initial snapshot was limited (via `pagination.limit`), this
|
|
126
|
+
* cursor can be passed in a subsequent `records.subscribe()` call to
|
|
127
|
+
* continue from where the previous snapshot left off.
|
|
128
|
+
*
|
|
129
|
+
* `undefined` when there are no more results.
|
|
130
|
+
*/
|
|
131
|
+
readonly cursor?: PaginationCursor;
|
|
132
|
+
|
|
119
133
|
/** The underlying DWN subscription handle. */
|
|
120
134
|
private _subscription: DwnMessageSubscription;
|
|
121
135
|
|
|
@@ -131,6 +145,7 @@ export class LiveQuery extends EventTarget {
|
|
|
131
145
|
const {
|
|
132
146
|
agent,
|
|
133
147
|
connectedDid,
|
|
148
|
+
cursor,
|
|
134
149
|
delegateDid,
|
|
135
150
|
protocolRole,
|
|
136
151
|
remoteOrigin,
|
|
@@ -140,6 +155,7 @@ export class LiveQuery extends EventTarget {
|
|
|
140
155
|
} = options;
|
|
141
156
|
|
|
142
157
|
this._subscription = subscription;
|
|
158
|
+
this.cursor = cursor;
|
|
143
159
|
|
|
144
160
|
// Build Record objects from the initial snapshot entries (same logic as records.query()).
|
|
145
161
|
this.records = initialEntries.map((entry) => new Record(agent, {
|
|
@@ -154,7 +170,7 @@ export class LiveQuery extends EventTarget {
|
|
|
154
170
|
// Seed the known-records map with recordId -> messageTimestamp for dedup.
|
|
155
171
|
this._knownRecords = new Map();
|
|
156
172
|
for (const record of this.records) {
|
|
157
|
-
this._knownRecords.set(record.id, record.
|
|
173
|
+
this._knownRecords.set(record.id, record.timestamp);
|
|
158
174
|
}
|
|
159
175
|
}
|
|
160
176
|
|
|
@@ -179,7 +195,7 @@ export class LiveQuery extends EventTarget {
|
|
|
179
195
|
|
|
180
196
|
if (knownTimestamp !== undefined) {
|
|
181
197
|
// We've seen this recordId before (either from snapshot or a prior event).
|
|
182
|
-
if (record.
|
|
198
|
+
if (record.timestamp <= knownTimestamp) {
|
|
183
199
|
// Duplicate or stale event from the overlap window — skip.
|
|
184
200
|
return;
|
|
185
201
|
}
|
|
@@ -189,7 +205,7 @@ export class LiveQuery extends EventTarget {
|
|
|
189
205
|
}
|
|
190
206
|
|
|
191
207
|
// Update the known state.
|
|
192
|
-
this._knownRecords.set(record.id, record.
|
|
208
|
+
this._knownRecords.set(record.id, record.timestamp);
|
|
193
209
|
}
|
|
194
210
|
|
|
195
211
|
const change: RecordChange = { type: changeType, record };
|
package/src/protocol-types.ts
CHANGED
|
@@ -133,7 +133,7 @@ export type TagKeys<Tags extends ProtocolTagsDefinition> = Exclude<
|
|
|
133
133
|
/**
|
|
134
134
|
* A mapping from protocol type names to their TypeScript data shapes.
|
|
135
135
|
*
|
|
136
|
-
* Used as a type parameter to `defineProtocol()` and `
|
|
136
|
+
* Used as a type parameter to `defineProtocol()` and `TypedWeb5` so that
|
|
137
137
|
* the protocol definition JSON stays JSON-compatible while TypeScript types
|
|
138
138
|
* are tracked separately.
|
|
139
139
|
*
|
|
@@ -154,7 +154,7 @@ export type SchemaMap = Record<string, unknown>;
|
|
|
154
154
|
/**
|
|
155
155
|
* The return type of `defineProtocol()`. Bundles the raw protocol definition
|
|
156
156
|
* with its inferred path strings and schema type map for downstream use
|
|
157
|
-
* by `
|
|
157
|
+
* by `TypedWeb5`.
|
|
158
158
|
*/
|
|
159
159
|
export type TypedProtocol<
|
|
160
160
|
D extends ProtocolDefinition = ProtocolDefinition,
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NOTE: Added reference types here to avoid a `pnpm` bug during build.
|
|
3
|
+
* https://github.com/enboxorg/enbox/pull/507
|
|
4
|
+
*/
|
|
5
|
+
/// <reference types="@enbox/dwn-sdk-js" />
|
|
6
|
+
|
|
7
|
+
import type { AnonymousDwnApi } from '@enbox/agent';
|
|
8
|
+
import type { RecordsWriteDescriptor, RecordsWriteMessage, RecordsWriteTags } from '@enbox/dwn-sdk-js';
|
|
9
|
+
|
|
10
|
+
import { getRecordAuthor } from '@enbox/agent';
|
|
11
|
+
import { Convert, Stream } from '@enbox/common';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Construction options for a {@link ReadOnlyRecord}.
|
|
15
|
+
*
|
|
16
|
+
* @beta
|
|
17
|
+
*/
|
|
18
|
+
export type ReadOnlyRecordOptions = {
|
|
19
|
+
/** The raw `RecordsWriteMessage` returned from a query or read reply. */
|
|
20
|
+
rawMessage: RecordsWriteMessage;
|
|
21
|
+
/** The initial write message, if the record has been updated. */
|
|
22
|
+
initialWrite?: RecordsWriteMessage;
|
|
23
|
+
/** Encoded data (Base64URL string) if the data was small enough to be inlined in the query reply. */
|
|
24
|
+
encodedData?: string;
|
|
25
|
+
/** A readable data stream, present when the record comes from a `RecordsRead` reply. */
|
|
26
|
+
data?: ReadableStream;
|
|
27
|
+
/** The DID of the remote DWN this record was fetched from. Used for data re-fetch. */
|
|
28
|
+
remoteOrigin: string;
|
|
29
|
+
/** The {@link AnonymousDwnApi} instance used to re-fetch data when needed. */
|
|
30
|
+
anonymousDwn: AnonymousDwnApi;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* An immutable, read-only view of a DWN record.
|
|
35
|
+
*
|
|
36
|
+
* `ReadOnlyRecord` is returned by {@link DwnReaderApi} methods and provides
|
|
37
|
+
* access to the record's metadata and data without any mutation capabilities.
|
|
38
|
+
* There are no `update()`, `delete()`, `send()`, `store()`, or `import()`
|
|
39
|
+
* methods — the compiler prevents accidental writes.
|
|
40
|
+
*
|
|
41
|
+
* Data access works identically to the full {@link Record} class:
|
|
42
|
+
* - If the data was inlined (small payloads from query replies), it is
|
|
43
|
+
* available immediately.
|
|
44
|
+
* - If the data was not inlined, `data.stream()` / `data.text()` / etc.
|
|
45
|
+
* automatically perform an anonymous `RecordsRead` to fetch it.
|
|
46
|
+
*
|
|
47
|
+
* @beta
|
|
48
|
+
*/
|
|
49
|
+
export class ReadOnlyRecord {
|
|
50
|
+
// Private backing fields.
|
|
51
|
+
private _anonymousDwn: AnonymousDwnApi;
|
|
52
|
+
private _remoteOrigin: string;
|
|
53
|
+
private _author: string;
|
|
54
|
+
private _creator: string;
|
|
55
|
+
private _descriptor: RecordsWriteDescriptor;
|
|
56
|
+
private _recordId: string;
|
|
57
|
+
private _contextId?: string;
|
|
58
|
+
private _initialWrite?: RecordsWriteMessage;
|
|
59
|
+
private _encodedData?: Blob;
|
|
60
|
+
private _readableStream?: ReadableStream;
|
|
61
|
+
private _authorization: RecordsWriteMessage['authorization'];
|
|
62
|
+
private _attestation?: RecordsWriteMessage['attestation'];
|
|
63
|
+
private _encryption?: RecordsWriteMessage['encryption'];
|
|
64
|
+
|
|
65
|
+
constructor(options: ReadOnlyRecordOptions) {
|
|
66
|
+
const { rawMessage, initialWrite, encodedData, data, remoteOrigin, anonymousDwn } = options;
|
|
67
|
+
|
|
68
|
+
this._anonymousDwn = anonymousDwn;
|
|
69
|
+
this._remoteOrigin = remoteOrigin;
|
|
70
|
+
|
|
71
|
+
// Extract the author DID from the authorization signature. The author is
|
|
72
|
+
// the DID that signed the most recent RecordsWrite message. For records
|
|
73
|
+
// returned from anonymous queries, the authorization will be present (the
|
|
74
|
+
// remote DWN always returns the full message), but we guard against
|
|
75
|
+
// malformed messages gracefully.
|
|
76
|
+
try {
|
|
77
|
+
this._author = getRecordAuthor(rawMessage) ?? 'unknown';
|
|
78
|
+
} catch {
|
|
79
|
+
this._author = 'unknown';
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
this._creator = initialWrite ? (getRecordAuthor(initialWrite) ?? this._author) : this._author;
|
|
83
|
+
} catch {
|
|
84
|
+
this._creator = this._author;
|
|
85
|
+
}
|
|
86
|
+
this._descriptor = rawMessage.descriptor;
|
|
87
|
+
this._recordId = rawMessage.recordId;
|
|
88
|
+
this._contextId = rawMessage.contextId;
|
|
89
|
+
this._initialWrite = initialWrite;
|
|
90
|
+
this._authorization = rawMessage.authorization;
|
|
91
|
+
this._attestation = rawMessage.attestation;
|
|
92
|
+
this._encryption = rawMessage.encryption;
|
|
93
|
+
|
|
94
|
+
if (encodedData) {
|
|
95
|
+
this._encodedData = new Blob(
|
|
96
|
+
[Convert.base64Url(encodedData).toUint8Array()],
|
|
97
|
+
{ type: this.dataFormat },
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (data) {
|
|
102
|
+
this._readableStream = data;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Immutable record properties
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
/** Record's unique identifier. */
|
|
111
|
+
get id(): string { return this._recordId; }
|
|
112
|
+
|
|
113
|
+
/** Record's context ID. */
|
|
114
|
+
get contextId(): string | undefined { return this._contextId; }
|
|
115
|
+
|
|
116
|
+
/** Record's creation date. */
|
|
117
|
+
get dateCreated(): string { return this._descriptor.dateCreated; }
|
|
118
|
+
|
|
119
|
+
/** Record's parent ID. */
|
|
120
|
+
get parentId(): string | undefined { return this._descriptor.parentId; }
|
|
121
|
+
|
|
122
|
+
/** Record's protocol URI. */
|
|
123
|
+
get protocol(): string | undefined { return this._descriptor.protocol; }
|
|
124
|
+
|
|
125
|
+
/** Record's protocol path. */
|
|
126
|
+
get protocolPath(): string | undefined { return this._descriptor.protocolPath; }
|
|
127
|
+
|
|
128
|
+
/** Record's recipient. */
|
|
129
|
+
get recipient(): string | undefined { return this._descriptor.recipient; }
|
|
130
|
+
|
|
131
|
+
/** Record's schema. */
|
|
132
|
+
get schema(): string | undefined { return this._descriptor.schema; }
|
|
133
|
+
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// Mutable descriptor properties
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
/** Record's data format / MIME type. */
|
|
139
|
+
get dataFormat(): string { return this._descriptor.dataFormat; }
|
|
140
|
+
|
|
141
|
+
/** Record's data CID. */
|
|
142
|
+
get dataCid(): string { return this._descriptor.dataCid; }
|
|
143
|
+
|
|
144
|
+
/** Record's data size in bytes. */
|
|
145
|
+
get dataSize(): number { return this._descriptor.dataSize; }
|
|
146
|
+
|
|
147
|
+
/** Record's published date. */
|
|
148
|
+
get datePublished(): string | undefined { return this._descriptor.datePublished; }
|
|
149
|
+
|
|
150
|
+
/** Whether the record is published. */
|
|
151
|
+
get published(): boolean | undefined { return this._descriptor.published; }
|
|
152
|
+
|
|
153
|
+
/** Tags associated with the record. */
|
|
154
|
+
get tags(): RecordsWriteTags | undefined { return this._descriptor.tags; }
|
|
155
|
+
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// State-dependent properties
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
/** DID that is the logical author of the record. */
|
|
161
|
+
get author(): string { return this._author; }
|
|
162
|
+
|
|
163
|
+
/** DID that originally created the record. */
|
|
164
|
+
get creator(): string { return this._creator; }
|
|
165
|
+
|
|
166
|
+
/** Record's message timestamp (time of most recent create/update). */
|
|
167
|
+
get timestamp(): string { return this._descriptor.messageTimestamp; }
|
|
168
|
+
|
|
169
|
+
/** Record's encryption metadata, if encrypted. */
|
|
170
|
+
get encryption(): RecordsWriteMessage['encryption'] { return this._encryption; }
|
|
171
|
+
|
|
172
|
+
/** Record's authorization. */
|
|
173
|
+
get authorization(): RecordsWriteMessage['authorization'] { return this._authorization; }
|
|
174
|
+
|
|
175
|
+
/** Record's attestation signatures. */
|
|
176
|
+
get attestation(): RecordsWriteMessage['attestation'] { return this._attestation; }
|
|
177
|
+
|
|
178
|
+
/** The initial write message, if the record has been updated. */
|
|
179
|
+
get initialWrite(): RecordsWriteMessage | undefined { return this._initialWrite; }
|
|
180
|
+
|
|
181
|
+
/** The DID of the remote DWN this record was fetched from. */
|
|
182
|
+
get remoteOrigin(): string { return this._remoteOrigin; }
|
|
183
|
+
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// Data access
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Returns the data of the current record.
|
|
190
|
+
* If the data is not available in-memory, it is fetched from the remote DWN
|
|
191
|
+
* using an anonymous `RecordsRead`.
|
|
192
|
+
*
|
|
193
|
+
* @returns A data accessor with `blob()`, `bytes()`, `json()`, `text()`, and `stream()` methods.
|
|
194
|
+
*
|
|
195
|
+
* @beta
|
|
196
|
+
*/
|
|
197
|
+
get data(): {
|
|
198
|
+
blob: () => Promise<Blob>;
|
|
199
|
+
bytes: () => Promise<Uint8Array>;
|
|
200
|
+
json: <T = unknown>() => Promise<T>;
|
|
201
|
+
text: () => Promise<string>;
|
|
202
|
+
stream: () => Promise<ReadableStream>;
|
|
203
|
+
then: (
|
|
204
|
+
onFulfilled?: (value: ReadableStream) => ReadableStream | PromiseLike<ReadableStream>,
|
|
205
|
+
onRejected?: (reason: any) => PromiseLike<never>,
|
|
206
|
+
) => Promise<ReadableStream>;
|
|
207
|
+
catch: (onRejected?: (reason: any) => PromiseLike<never>) => Promise<ReadableStream>;
|
|
208
|
+
} {
|
|
209
|
+
const self = this;
|
|
210
|
+
const dataObj = {
|
|
211
|
+
async blob(): Promise<Blob> {
|
|
212
|
+
return new Blob([await Stream.consumeToBytes({ readableStream: await this.stream() })], { type: self.dataFormat });
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
async bytes(): Promise<Uint8Array> {
|
|
216
|
+
return await Stream.consumeToBytes({ readableStream: await this.stream() });
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
async json<T = unknown>(): Promise<T> {
|
|
220
|
+
return await Stream.consumeToJson({ readableStream: await this.stream() }) as T;
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
async text(): Promise<string> {
|
|
224
|
+
return await Stream.consumeToText({ readableStream: await this.stream() });
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
async stream(): Promise<ReadableStream> {
|
|
228
|
+
if (self._encodedData) {
|
|
229
|
+
return Stream.fromBlob(self._encodedData);
|
|
230
|
+
} else if (self._readableStream) {
|
|
231
|
+
const currentStream = self._readableStream;
|
|
232
|
+
self._readableStream = undefined;
|
|
233
|
+
return currentStream;
|
|
234
|
+
} else {
|
|
235
|
+
// Re-fetch the data from the remote DWN using an anonymous RecordsRead.
|
|
236
|
+
return await self.readRecordData();
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
then(
|
|
241
|
+
onFulfilled?: (value: ReadableStream) => ReadableStream | PromiseLike<ReadableStream>,
|
|
242
|
+
onRejected?: (reason: any) => PromiseLike<never>,
|
|
243
|
+
): Promise<ReadableStream> {
|
|
244
|
+
return this.stream().then(onFulfilled, onRejected);
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
catch(onRejected?: (reason: any) => PromiseLike<never>): Promise<ReadableStream> {
|
|
248
|
+
return this.stream().catch(onRejected);
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
return dataObj;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
// Serialization
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Returns a JSON representation of the record.
|
|
261
|
+
* Called by `JSON.stringify(...)` automatically.
|
|
262
|
+
*/
|
|
263
|
+
toJSON(): Record<string, unknown> {
|
|
264
|
+
return {
|
|
265
|
+
attestation : this.attestation,
|
|
266
|
+
author : this.author,
|
|
267
|
+
authorization : this.authorization,
|
|
268
|
+
contextId : this.contextId,
|
|
269
|
+
dataCid : this.dataCid,
|
|
270
|
+
dataFormat : this.dataFormat,
|
|
271
|
+
dataSize : this.dataSize,
|
|
272
|
+
dateCreated : this.dateCreated,
|
|
273
|
+
datePublished : this.datePublished,
|
|
274
|
+
encryption : this.encryption,
|
|
275
|
+
parentId : this.parentId,
|
|
276
|
+
protocol : this.protocol,
|
|
277
|
+
protocolPath : this.protocolPath,
|
|
278
|
+
published : this.published,
|
|
279
|
+
recipient : this.recipient,
|
|
280
|
+
recordId : this.id,
|
|
281
|
+
schema : this.schema,
|
|
282
|
+
tags : this.tags,
|
|
283
|
+
timestamp : this.timestamp,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Convenience string representation.
|
|
289
|
+
*/
|
|
290
|
+
toString(): string {
|
|
291
|
+
let str = 'ReadOnlyRecord: {\n';
|
|
292
|
+
str += ` ID: ${this.id}\n`;
|
|
293
|
+
str += this.contextId ? ` Context ID: ${this.contextId}\n` : '';
|
|
294
|
+
str += this.protocol ? ` Protocol: ${this.protocol}\n` : '';
|
|
295
|
+
str += this.schema ? ` Schema: ${this.schema}\n` : '';
|
|
296
|
+
str += ` Data CID: ${this.dataCid}\n`;
|
|
297
|
+
str += ` Data Format: ${this.dataFormat}\n`;
|
|
298
|
+
str += ` Data Size: ${this.dataSize}\n`;
|
|
299
|
+
str += ` Created: ${this.dateCreated}\n`;
|
|
300
|
+
str += ` Timestamp: ${this.timestamp}\n`;
|
|
301
|
+
str += '}';
|
|
302
|
+
return str;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
// Private helpers
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Fetches the record's data from the remote DWN using an anonymous `RecordsRead`.
|
|
311
|
+
*/
|
|
312
|
+
private async readRecordData(): Promise<ReadableStream> {
|
|
313
|
+
try {
|
|
314
|
+
const reply = await this._anonymousDwn.recordsRead(this._remoteOrigin, {
|
|
315
|
+
filter: { recordId: this._recordId },
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
if (reply.status.code !== 200 || !reply.entry?.data) {
|
|
319
|
+
throw new Error(`${reply.status.code}: ${reply.status.detail}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return reply.entry.data;
|
|
323
|
+
} catch (error: unknown) {
|
|
324
|
+
const message = (error instanceof Error) ? error.message : 'Unknown error';
|
|
325
|
+
throw new Error(`ReadOnlyRecord: Error reading data for record '${this._recordId}': ${message}`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|