@enbox/api 0.1.0 → 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 +84 -130
- 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 +4 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/live-query.js +154 -0
- package/dist/esm/live-query.js.map +1 -0
- 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 +20 -104
- 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 +4 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/live-query.d.ts +148 -0
- package/dist/types/live-query.d.ts.map +1 -0
- 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} +75 -76
- 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 +10 -6
- package/src/advanced.ts +29 -0
- package/src/define-protocol.ts +3 -3
- package/src/dwn-api.ts +141 -266
- package/src/dwn-reader-api.ts +255 -0
- package/src/index.ts +4 -2
- package/src/live-query.ts +261 -0
- 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/subscription-util.js +0 -35
- package/dist/esm/subscription-util.js.map +0 -1
- package/dist/esm/typed-dwn-api.js +0 -181
- package/dist/esm/typed-dwn-api.js.map +0 -1
- package/dist/types/subscription-util.d.ts +0 -19
- package/dist/types/subscription-util.d.ts.map +0 -1
- package/dist/types/typed-dwn-api.d.ts.map +0 -1
- package/src/subscription-util.ts +0 -44
- package/src/typed-dwn-api.ts +0 -370
|
@@ -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
|
+
}
|
package/src/record.ts
CHANGED
|
@@ -60,8 +60,8 @@ export type RecordModel = ImmutableRecordProperties & OptionalRecordProperties &
|
|
|
60
60
|
/** The unique identifier of the record. */
|
|
61
61
|
recordId?: string;
|
|
62
62
|
|
|
63
|
-
/** The timestamp
|
|
64
|
-
|
|
63
|
+
/** The message timestamp (time of creation, most recent update, or deletion). */
|
|
64
|
+
timestamp?: string;
|
|
65
65
|
|
|
66
66
|
/** The protocol role under which this record is written. */
|
|
67
67
|
protocolRole?: RecordOptions['protocolRole'];
|
|
@@ -152,8 +152,8 @@ export type RecordUpdateParams = {
|
|
|
152
152
|
/** The size of the data in bytes. */
|
|
153
153
|
dataSize?: DwnMessageDescriptor[DwnInterface.RecordsWrite]['dataSize'];
|
|
154
154
|
|
|
155
|
-
/** The timestamp
|
|
156
|
-
|
|
155
|
+
/** The timestamp of the update message. */
|
|
156
|
+
timestamp?: DwnMessageDescriptor[DwnInterface.RecordsWrite]['messageTimestamp'];
|
|
157
157
|
|
|
158
158
|
/** The timestamp indicating when the record was published. */
|
|
159
159
|
datePublished?: DwnMessageDescriptor[DwnInterface.RecordsWrite]['datePublished'];
|
|
@@ -196,13 +196,33 @@ export type RecordDeleteParams = {
|
|
|
196
196
|
/** Whether or not to prune any children this record may have. */
|
|
197
197
|
prune?: DwnMessageDescriptor[DwnInterface.RecordsDelete]['prune'];
|
|
198
198
|
|
|
199
|
-
/** The timestamp
|
|
200
|
-
|
|
199
|
+
/** The timestamp of the delete message. */
|
|
200
|
+
timestamp?: DwnMessageDescriptor[DwnInterface.RecordsDelete]['messageTimestamp'];
|
|
201
201
|
|
|
202
202
|
/** The protocol role under which this record will be deleted. */
|
|
203
203
|
protocolRole?: string;
|
|
204
204
|
};
|
|
205
205
|
|
|
206
|
+
/**
|
|
207
|
+
* The result of a {@link Record.update} operation.
|
|
208
|
+
*
|
|
209
|
+
* @beta
|
|
210
|
+
*/
|
|
211
|
+
export type RecordUpdateResult = DwnResponseStatus & {
|
|
212
|
+
/** The updated Record instance reflecting the new state. */
|
|
213
|
+
record: Record;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* The result of a {@link Record.delete} operation.
|
|
218
|
+
*
|
|
219
|
+
* @beta
|
|
220
|
+
*/
|
|
221
|
+
export type RecordDeleteResult = DwnResponseStatus & {
|
|
222
|
+
/** The deleted Record instance reflecting the deleted state. */
|
|
223
|
+
record: Record;
|
|
224
|
+
};
|
|
225
|
+
|
|
206
226
|
/**
|
|
207
227
|
* The `Record` class encapsulates a single record's data and metadata, providing a more
|
|
208
228
|
* developer-friendly interface for working with Decentralized Web Node (DWN) records.
|
|
@@ -210,12 +230,9 @@ export type RecordDeleteParams = {
|
|
|
210
230
|
* Methods are provided to read, update, and manage the record's lifecycle, including writing to
|
|
211
231
|
* remote DWNs.
|
|
212
232
|
*
|
|
213
|
-
* Note: The `messageTimestamp`
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
* intended to simplify the developer experience of working with
|
|
217
|
-
* logical records (and not individual DWN messages) the
|
|
218
|
-
* `messageTimestamp` is mapped to `dateModified`.
|
|
233
|
+
* Note: The DWN SDK's `messageTimestamp` is exposed as `timestamp` on
|
|
234
|
+
* the Record class. It represents the time of the most recent
|
|
235
|
+
* message (create, update, or delete) for this logical record.
|
|
219
236
|
*
|
|
220
237
|
* @beta
|
|
221
238
|
*/
|
|
@@ -270,9 +287,14 @@ export class Record implements RecordModel {
|
|
|
270
287
|
/** Role under which the record is written. */
|
|
271
288
|
private _protocolRole?: RecordOptions['protocolRole'];
|
|
272
289
|
|
|
290
|
+
/** Cached reconstructed raw message, invalidated when record state changes. */
|
|
291
|
+
private _rawMessageCache?: DwnMessage[DwnInterface.RecordsWrite] | DwnMessage[DwnInterface.RecordsDelete];
|
|
292
|
+
/** Dirty flag indicating the cached raw message needs to be rebuilt. */
|
|
293
|
+
private _rawMessageDirty: boolean = true;
|
|
294
|
+
|
|
273
295
|
/** The `RecordsWriteMessage` descriptor unless the record is in a deleted state */
|
|
274
296
|
private get _recordsWriteDescriptor(): DwnMessageDescriptor[DwnInterface.RecordsWrite] | undefined {
|
|
275
|
-
if (
|
|
297
|
+
if (!this.isRecordsDeleteDescriptor(this._descriptor)) {
|
|
276
298
|
return this._descriptor as DwnMessageDescriptor[DwnInterface.RecordsWrite];
|
|
277
299
|
}
|
|
278
300
|
|
|
@@ -338,8 +360,8 @@ export class Record implements RecordModel {
|
|
|
338
360
|
/** DID that is the original creator of the Record. */
|
|
339
361
|
get creator(): string { return this._creator; }
|
|
340
362
|
|
|
341
|
-
/** Record's
|
|
342
|
-
get
|
|
363
|
+
/** Record's message timestamp (time of creation, most recent update, or deletion). */
|
|
364
|
+
get timestamp(): string { return this._descriptor.messageTimestamp; }
|
|
343
365
|
|
|
344
366
|
/** Record's encryption */
|
|
345
367
|
get encryption(): DwnMessage[DwnInterface.RecordsWrite]['encryption'] { return this._encryption; }
|
|
@@ -354,15 +376,20 @@ export class Record implements RecordModel {
|
|
|
354
376
|
get protocolRole(): string | undefined { return this._protocolRole; }
|
|
355
377
|
|
|
356
378
|
/** Record's deleted state (true/false) */
|
|
357
|
-
get deleted(): boolean { return
|
|
379
|
+
get deleted(): boolean { return this.isRecordsDeleteDescriptor(this._descriptor); }
|
|
358
380
|
|
|
359
381
|
/** Record's initial write if the record has been updated */
|
|
360
382
|
get initialWrite(): RecordOptions['initialWrite'] { return this._initialWrite; }
|
|
361
383
|
|
|
362
384
|
/**
|
|
363
385
|
* Returns a copy of the raw `RecordsWriteMessage` that was used to create the current `Record` instance.
|
|
386
|
+
* The result is cached and only rebuilt when the record's state changes (via `update()` or `delete()`).
|
|
364
387
|
*/
|
|
365
388
|
get rawMessage(): DwnMessage[DwnInterface.RecordsWrite] | DwnMessage[DwnInterface.RecordsDelete] {
|
|
389
|
+
if (!this._rawMessageDirty && this._rawMessageCache) {
|
|
390
|
+
return this._rawMessageCache;
|
|
391
|
+
}
|
|
392
|
+
|
|
366
393
|
const messageType = this._descriptor.interface + this._descriptor.method;
|
|
367
394
|
let message: DwnMessage[DwnInterface.RecordsWrite] | DwnMessage[DwnInterface.RecordsDelete];
|
|
368
395
|
if (messageType === DwnInterface.RecordsWrite) {
|
|
@@ -382,6 +409,9 @@ export class Record implements RecordModel {
|
|
|
382
409
|
}
|
|
383
410
|
|
|
384
411
|
removeUndefinedProperties(message);
|
|
412
|
+
|
|
413
|
+
this._rawMessageCache = message;
|
|
414
|
+
this._rawMessageDirty = false;
|
|
385
415
|
return message;
|
|
386
416
|
}
|
|
387
417
|
|
|
@@ -661,7 +691,7 @@ export class Record implements RecordModel {
|
|
|
661
691
|
messageType : DwnInterface.RecordsWrite,
|
|
662
692
|
author : this._connectedDid,
|
|
663
693
|
target : target,
|
|
664
|
-
dataStream : await this.data.blob(),
|
|
694
|
+
dataStream : this._encodedData ?? await this.data.blob(),
|
|
665
695
|
rawMessage : { ...this.rawMessage }
|
|
666
696
|
};
|
|
667
697
|
}
|
|
@@ -677,26 +707,26 @@ export class Record implements RecordModel {
|
|
|
677
707
|
*/
|
|
678
708
|
toJSON(): RecordModel {
|
|
679
709
|
return {
|
|
680
|
-
attestation
|
|
681
|
-
author
|
|
682
|
-
authorization
|
|
683
|
-
contextId
|
|
684
|
-
dataCid
|
|
685
|
-
dataFormat
|
|
686
|
-
dataSize
|
|
687
|
-
dateCreated
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
710
|
+
attestation : this.attestation,
|
|
711
|
+
author : this.author,
|
|
712
|
+
authorization : this.authorization,
|
|
713
|
+
contextId : this.contextId,
|
|
714
|
+
dataCid : this.dataCid,
|
|
715
|
+
dataFormat : this.dataFormat,
|
|
716
|
+
dataSize : this.dataSize,
|
|
717
|
+
dateCreated : this.dateCreated,
|
|
718
|
+
datePublished : this.datePublished,
|
|
719
|
+
encryption : this.encryption,
|
|
720
|
+
parentId : this.parentId,
|
|
721
|
+
protocol : this.protocol,
|
|
722
|
+
protocolPath : this.protocolPath,
|
|
723
|
+
protocolRole : this.protocolRole,
|
|
724
|
+
published : this.published,
|
|
725
|
+
recipient : this.recipient,
|
|
726
|
+
recordId : this.id,
|
|
727
|
+
schema : this.schema,
|
|
728
|
+
tags : this.tags,
|
|
729
|
+
timestamp : this.timestamp,
|
|
700
730
|
};
|
|
701
731
|
}
|
|
702
732
|
|
|
@@ -720,7 +750,7 @@ export class Record implements RecordModel {
|
|
|
720
750
|
|
|
721
751
|
str += ` Deleted: ${this.deleted}\n`;
|
|
722
752
|
str += ` Created: ${this.dateCreated}\n`;
|
|
723
|
-
str += `
|
|
753
|
+
str += ` Timestamp: ${this.timestamp}\n`;
|
|
724
754
|
str += `}`;
|
|
725
755
|
return str;
|
|
726
756
|
}
|
|
@@ -743,7 +773,7 @@ export class Record implements RecordModel {
|
|
|
743
773
|
*
|
|
744
774
|
* @beta
|
|
745
775
|
*/
|
|
746
|
-
async update({
|
|
776
|
+
async update({ timestamp, data, encryption, protocolRole, store = true, ...params }: RecordUpdateParams): Promise<RecordUpdateResult> {
|
|
747
777
|
|
|
748
778
|
if (this.deleted) {
|
|
749
779
|
throw new Error('Record: Cannot revive a deleted record.');
|
|
@@ -763,7 +793,7 @@ export class Record implements RecordModel {
|
|
|
763
793
|
...params,
|
|
764
794
|
parentContextId,
|
|
765
795
|
protocolRole : protocolRole ?? this._protocolRole, // Use the current protocolRole if not provided.
|
|
766
|
-
messageTimestamp :
|
|
796
|
+
messageTimestamp : timestamp, // Map Record class `timestamp` property to DWN SDK `messageTimestamp`
|
|
767
797
|
recordId : this._recordId
|
|
768
798
|
};
|
|
769
799
|
|
|
@@ -785,7 +815,7 @@ export class Record implements RecordModel {
|
|
|
785
815
|
}
|
|
786
816
|
|
|
787
817
|
// Throw an error if an attempt is made to modify immutable properties.
|
|
788
|
-
// Note: `data` and `
|
|
818
|
+
// Note: `data` and `timestamp` have already been handled.
|
|
789
819
|
const mutableDescriptorProperties = new Set(['data', 'dataCid', 'dataFormat', 'dataSize', 'datePublished', 'messageTimestamp', 'published', 'tags']);
|
|
790
820
|
Record.verifyPermittedMutation(Object.keys(params), mutableDescriptorProperties);
|
|
791
821
|
|
|
@@ -820,41 +850,38 @@ export class Record implements RecordModel {
|
|
|
820
850
|
|
|
821
851
|
const agentResponse = await this._agent.processDwnRequest(requestOptions);
|
|
822
852
|
|
|
823
|
-
const { message, reply: { status } } = agentResponse;
|
|
824
|
-
const responseMessage = message;
|
|
825
|
-
|
|
826
|
-
if (200 <= status.code && status.code <= 299) {
|
|
827
|
-
// copy the original raw message to the initial write before we update the values.
|
|
828
|
-
if (!this._initialWrite) {
|
|
829
|
-
// If there is no initial write, we need to create one from the current record state.
|
|
830
|
-
// We checked in the beginning of the function that the rawMessage is a RecordsWrite message.
|
|
831
|
-
this._initialWrite = { ...this.rawMessage as DwnMessage[DwnInterface.RecordsWrite] };
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
// Only update the local Record instance mutable properties if the record was successfully (over)written.
|
|
835
|
-
this._authorization = responseMessage.authorization;
|
|
836
|
-
this._encryption = responseMessage.encryption;
|
|
837
|
-
this._protocolRole = updateMessage.protocolRole;
|
|
838
|
-
mutableDescriptorProperties.forEach(property => {
|
|
839
|
-
this._descriptor[property] = responseMessage.descriptor[property];
|
|
840
|
-
});
|
|
853
|
+
const { message: responseMessage, reply: { status } } = agentResponse;
|
|
841
854
|
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
}
|
|
855
|
+
if (!(200 <= status.code && status.code <= 299)) {
|
|
856
|
+
// Return a shallow copy of this record on failure — no state change.
|
|
857
|
+
return { status, record: this };
|
|
846
858
|
}
|
|
847
859
|
|
|
848
|
-
|
|
860
|
+
// Determine the initial write for the new Record instance.
|
|
861
|
+
const initialWrite = this._initialWrite ?? { ...this.rawMessage as DwnMessage[DwnInterface.RecordsWrite] };
|
|
862
|
+
|
|
863
|
+
// Construct a new Record instance reflecting the updated state.
|
|
864
|
+
const updatedRecord = new Record(this._agent, {
|
|
865
|
+
author : this._author,
|
|
866
|
+
connectedDid : this._connectedDid,
|
|
867
|
+
delegateDid : this._delegateDid,
|
|
868
|
+
remoteOrigin : this._remoteOrigin,
|
|
869
|
+
protocolRole : protocolRole ?? this._protocolRole,
|
|
870
|
+
initialWrite,
|
|
871
|
+
encodedData : data !== undefined ? dataBlob : this._encodedData,
|
|
872
|
+
...responseMessage as DwnMessage[DwnInterface.RecordsWrite],
|
|
873
|
+
}, this._permissionsApi);
|
|
874
|
+
|
|
875
|
+
return { status, record: updatedRecord };
|
|
849
876
|
}
|
|
850
877
|
|
|
851
878
|
/**
|
|
852
879
|
* Delete the current record on the DWN.
|
|
853
880
|
* @param params - Parameters to delete the record.
|
|
854
|
-
* @returns the status
|
|
881
|
+
* @returns the status and a new Record instance reflecting the deleted state
|
|
855
882
|
*/
|
|
856
|
-
async delete(deleteParams?: RecordDeleteParams): Promise<
|
|
857
|
-
const { store = true, signAsOwner,
|
|
883
|
+
async delete(deleteParams?: RecordDeleteParams): Promise<RecordDeleteResult> {
|
|
884
|
+
const { store = true, signAsOwner, timestamp, prune = false } = deleteParams || {};
|
|
858
885
|
|
|
859
886
|
const signAsOwnerValue = signAsOwner && this._delegateDid === undefined;
|
|
860
887
|
const signAsOwnerDelegate = signAsOwner && this._delegateDid !== undefined;
|
|
@@ -893,7 +920,7 @@ export class Record implements RecordModel {
|
|
|
893
920
|
deleteOptions.messageParams = {
|
|
894
921
|
prune : prune,
|
|
895
922
|
recordId : this._recordId,
|
|
896
|
-
messageTimestamp :
|
|
923
|
+
messageTimestamp : timestamp,
|
|
897
924
|
protocolRole : deleteParams?.protocolRole ?? this._protocolRole // if no protocolRole is provided, use the current protocolRole
|
|
898
925
|
};
|
|
899
926
|
}
|
|
@@ -920,22 +947,23 @@ export class Record implements RecordModel {
|
|
|
920
947
|
const { message, reply: { status } } = agentResponse;
|
|
921
948
|
|
|
922
949
|
if (status.code !== 202) {
|
|
923
|
-
// If the delete was not successful, return
|
|
924
|
-
return { status };
|
|
950
|
+
// If the delete was not successful, return this record unchanged.
|
|
951
|
+
return { status, record: this };
|
|
925
952
|
}
|
|
926
953
|
|
|
927
|
-
//
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
954
|
+
// Construct a new Record instance reflecting the deleted state.
|
|
955
|
+
const initialWrite = this._initialWrite;
|
|
956
|
+
const deletedRecord = new Record(this._agent, {
|
|
957
|
+
author : getRecordAuthor(message),
|
|
958
|
+
connectedDid : this._connectedDid,
|
|
959
|
+
delegateDid : this._delegateDid,
|
|
960
|
+
remoteOrigin : this._remoteOrigin,
|
|
961
|
+
protocolRole : deleteParams?.protocolRole ?? this._protocolRole,
|
|
962
|
+
initialWrite,
|
|
963
|
+
...message as DwnMessage[DwnInterface.RecordsDelete],
|
|
964
|
+
}, this._permissionsApi);
|
|
965
|
+
|
|
966
|
+
return { status, record: deletedRecord };
|
|
939
967
|
}
|
|
940
968
|
|
|
941
969
|
/**
|
|
@@ -1051,7 +1079,10 @@ export class Record implements RecordModel {
|
|
|
1051
1079
|
if (200 <= status.code && status.code <= 299) {
|
|
1052
1080
|
// If we are signing as the owner, make sure to update the current record state's
|
|
1053
1081
|
// authorization, because now it will have the owner's signature on it.
|
|
1054
|
-
if (signAsOwner) {
|
|
1082
|
+
if (signAsOwner) {
|
|
1083
|
+
this._authorization = responseMessage.authorization;
|
|
1084
|
+
this._rawMessageDirty = true;
|
|
1085
|
+
}
|
|
1055
1086
|
}
|
|
1056
1087
|
|
|
1057
1088
|
return { status };
|
|
@@ -1088,9 +1119,8 @@ export class Record implements RecordModel {
|
|
|
1088
1119
|
// When reading the data as a delegate, if we don't find a grant we will attempt to read it with the delegate DID as the author.
|
|
1089
1120
|
// This allows users to read publicly available data without needing explicit grants.
|
|
1090
1121
|
//
|
|
1091
|
-
// NOTE:
|
|
1092
|
-
//
|
|
1093
|
-
// TODO: https://github.com/enboxorg/enbox/issues/898
|
|
1122
|
+
// NOTE: For anonymous/public record data access, callers can use `ReadOnlyRecord` via `Web5.anonymous()`.
|
|
1123
|
+
// See: https://github.com/enboxorg/enbox/issues/898
|
|
1094
1124
|
try {
|
|
1095
1125
|
const { message: delegatedGrant } = await this._permissionsApi.getPermissionForRequest({
|
|
1096
1126
|
connectedDid : this._connectedDid,
|