@enbox/api 0.3.1 → 0.4.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 +63 -0
- package/dist/browser.mjs +11 -28
- package/dist/browser.mjs.map +4 -4
- package/dist/esm/advanced.js +1 -1
- package/dist/esm/define-protocol.js +3 -3
- package/dist/esm/did-api.js +1 -1
- package/dist/esm/did-api.js.map +1 -1
- package/dist/esm/dwn-api.js +6 -6
- package/dist/esm/dwn-api.js.map +1 -1
- package/dist/esm/dwn-reader-api.js +2 -2
- package/dist/esm/enbox.js +205 -0
- package/dist/esm/enbox.js.map +1 -0
- package/dist/esm/index.js +16 -15
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/protocol.js +2 -2
- package/dist/esm/protocol.js.map +1 -1
- package/dist/esm/record-data.js +79 -5
- package/dist/esm/record-data.js.map +1 -1
- package/dist/esm/record.js +49 -10
- package/dist/esm/record.js.map +1 -1
- package/dist/esm/repository.js +7 -7
- package/dist/esm/repository.js.map +1 -1
- package/dist/esm/typed-enbox.js +583 -0
- package/dist/esm/typed-enbox.js.map +1 -0
- package/dist/esm/typed-live-query.js +1 -1
- package/dist/esm/typed-record.js +370 -46
- package/dist/esm/typed-record.js.map +1 -1
- package/dist/esm/utils.js +25 -0
- package/dist/esm/utils.js.map +1 -1
- package/dist/esm/vc-api.js.map +1 -1
- package/dist/types/advanced.d.ts +1 -1
- package/dist/types/define-protocol.d.ts +3 -3
- package/dist/types/did-api.d.ts +4 -4
- package/dist/types/did-api.d.ts.map +1 -1
- package/dist/types/dwn-api.d.ts +12 -7
- package/dist/types/dwn-api.d.ts.map +1 -1
- package/dist/types/dwn-reader-api.d.ts +2 -2
- package/dist/types/enbox.d.ts +202 -0
- package/dist/types/enbox.d.ts.map +1 -0
- package/dist/types/grant-revocation.d.ts +2 -2
- package/dist/types/grant-revocation.d.ts.map +1 -1
- package/dist/types/index.d.ts +16 -15
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/live-query.d.ts +2 -2
- package/dist/types/live-query.d.ts.map +1 -1
- package/dist/types/permission-grant.d.ts +2 -2
- package/dist/types/permission-grant.d.ts.map +1 -1
- package/dist/types/permission-request.d.ts +2 -2
- package/dist/types/permission-request.d.ts.map +1 -1
- package/dist/types/protocol-types.d.ts +2 -2
- package/dist/types/protocol.d.ts +7 -7
- package/dist/types/protocol.d.ts.map +1 -1
- package/dist/types/record-data.d.ts +17 -0
- package/dist/types/record-data.d.ts.map +1 -1
- package/dist/types/record.d.ts +24 -10
- package/dist/types/record.d.ts.map +1 -1
- package/dist/types/repository-types.d.ts +19 -11
- package/dist/types/repository-types.d.ts.map +1 -1
- package/dist/types/repository.d.ts +7 -7
- package/dist/types/repository.d.ts.map +1 -1
- package/dist/types/typed-enbox.d.ts +613 -0
- package/dist/types/typed-enbox.d.ts.map +1 -0
- package/dist/types/typed-live-query.d.ts +1 -1
- package/dist/types/typed-record.d.ts +427 -53
- package/dist/types/typed-record.d.ts.map +1 -1
- package/dist/types/utils.d.ts +23 -0
- package/dist/types/utils.d.ts.map +1 -1
- package/dist/types/vc-api.d.ts +3 -3
- package/dist/types/vc-api.d.ts.map +1 -1
- package/package.json +12 -11
- package/src/advanced.ts +1 -1
- package/src/define-protocol.ts +3 -3
- package/src/did-api.ts +5 -5
- package/src/dwn-api.ts +22 -17
- package/src/dwn-reader-api.ts +2 -2
- package/src/enbox.ts +281 -0
- package/src/grant-revocation.ts +3 -3
- package/src/index.ts +17 -16
- package/src/live-query.ts +2 -2
- package/src/permission-grant.ts +4 -4
- package/src/permission-request.ts +3 -3
- package/src/protocol-types.ts +2 -2
- package/src/protocol.ts +8 -8
- package/src/record-data.ts +86 -5
- package/src/record.ts +54 -13
- package/src/repository-types.ts +19 -7
- package/src/repository.ts +15 -15
- package/src/typed-enbox.ts +1169 -0
- package/src/typed-live-query.ts +1 -1
- package/src/typed-record.ts +431 -53
- package/src/utils.ts +27 -0
- package/src/vc-api.ts +4 -4
- package/dist/esm/typed-web5.js +0 -339
- package/dist/esm/typed-web5.js.map +0 -1
- package/dist/esm/web5.js +0 -410
- package/dist/esm/web5.js.map +0 -1
- package/dist/types/typed-web5.d.ts +0 -221
- package/dist/types/typed-web5.d.ts.map +0 -1
- package/dist/types/web5.d.ts +0 -346
- package/dist/types/web5.d.ts.map +0 -1
- package/src/typed-web5.ts +0 -598
- package/src/web5.ts +0 -754
|
@@ -0,0 +1,1169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A protocol-scoped API returned by {@link Enbox.using}.
|
|
3
|
+
*
|
|
4
|
+
* `TypedEnbox` is the **primary developer interface** for interacting with
|
|
5
|
+
* protocol-backed records. It auto-injects the protocol URI, protocolPath,
|
|
6
|
+
* and schema into every operation, and provides compile-time path
|
|
7
|
+
* autocompletion plus typed data payloads via the schema map.
|
|
8
|
+
*
|
|
9
|
+
* All record-returning methods wrap the underlying `Record` instances in
|
|
10
|
+
* {@link TypedRecord} so that type information flows through reads, queries,
|
|
11
|
+
* updates, and subscriptions without manual casts.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* const social = enbox.using(SocialProtocol);
|
|
16
|
+
*
|
|
17
|
+
* // Install the protocol
|
|
18
|
+
* await social.configure();
|
|
19
|
+
*
|
|
20
|
+
* // Create — path and data type are checked at compile time
|
|
21
|
+
* const { record } = await social.records.create('thread', {
|
|
22
|
+
* data: { title: 'Hello World', body: '...' },
|
|
23
|
+
* });
|
|
24
|
+
* // record is TypedRecord<ThreadData>
|
|
25
|
+
*
|
|
26
|
+
* const data = await record.data.json(); // ThreadData — no cast needed
|
|
27
|
+
*
|
|
28
|
+
* // Query — protocol and protocolPath are auto-injected
|
|
29
|
+
* const { records } = await social.records.query('thread');
|
|
30
|
+
* // records is TypedRecord<ThreadData>[]
|
|
31
|
+
*
|
|
32
|
+
* // Subscribe — real-time changes via TypedLiveQuery
|
|
33
|
+
* const { liveQuery } = await social.records.subscribe('thread/reply');
|
|
34
|
+
* liveQuery.on('create', (record) => {
|
|
35
|
+
* // record is TypedRecord<ReplyData>
|
|
36
|
+
* });
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import type { DwnApi } from './dwn-api.js';
|
|
41
|
+
import type { Protocol } from './protocol.js';
|
|
42
|
+
|
|
43
|
+
import type { DateSort, ProtocolDefinition, ProtocolType, RecordsFilter } from '@enbox/dwn-sdk-js';
|
|
44
|
+
import type { DwnPaginationCursor, DwnResponseStatus } from '@enbox/agent';
|
|
45
|
+
import type { ProtocolPaths, SchemaMap, TypedProtocol, TypeNameAtPath } from './protocol-types.js';
|
|
46
|
+
|
|
47
|
+
import { TypedLiveQuery } from './typed-live-query.js';
|
|
48
|
+
import { TypedRecord } from './typed-record.js';
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Helper types
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Resolves the TypeScript data type for a given protocol path.
|
|
56
|
+
*
|
|
57
|
+
* If the schema map contains a mapping for the type name at the given path,
|
|
58
|
+
* that type is returned. Otherwise falls back to `unknown`.
|
|
59
|
+
*/
|
|
60
|
+
export type DataForPath<
|
|
61
|
+
_D extends ProtocolDefinition,
|
|
62
|
+
M extends SchemaMap,
|
|
63
|
+
Path extends string,
|
|
64
|
+
> = TypeNameAtPath<Path> extends keyof M ? M[TypeNameAtPath<Path>] : unknown;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Resolves the `ProtocolType` entry for a given protocol path.
|
|
68
|
+
*/
|
|
69
|
+
type ProtocolTypeForPath<
|
|
70
|
+
D extends ProtocolDefinition,
|
|
71
|
+
Path extends string,
|
|
72
|
+
> = TypeNameAtPath<Path> extends keyof D['types']
|
|
73
|
+
? D['types'][TypeNameAtPath<Path>] extends ProtocolType
|
|
74
|
+
? D['types'][TypeNameAtPath<Path>]
|
|
75
|
+
: undefined
|
|
76
|
+
: undefined;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Resolves a `dataFormat` string literal union for a path, or `string` if none.
|
|
80
|
+
*/
|
|
81
|
+
type DataFormatForPath<
|
|
82
|
+
D extends ProtocolDefinition,
|
|
83
|
+
Path extends string,
|
|
84
|
+
> = ProtocolTypeForPath<D, Path> extends { dataFormats: infer F }
|
|
85
|
+
? F extends readonly string[]
|
|
86
|
+
? F[number]
|
|
87
|
+
: string
|
|
88
|
+
: string;
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// Request / response types
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Options for {@link TypedEnbox} `records.create()`.
|
|
96
|
+
*
|
|
97
|
+
* The `data` field is type-checked against the protocol's schema map for
|
|
98
|
+
* the given path, providing compile-time safety for record payloads.
|
|
99
|
+
*
|
|
100
|
+
* @typeParam D - The protocol definition type.
|
|
101
|
+
* @typeParam M - The schema map mapping type names to TypeScript types.
|
|
102
|
+
* @typeParam Path - The protocol path string literal.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```ts
|
|
106
|
+
* await proto.records.create('notebook/page', {
|
|
107
|
+
* data: { title: 'My Page', body: '...' }, // type-checked as PageData
|
|
108
|
+
* parentContextId: notebook.contextId, // link to parent
|
|
109
|
+
* tags: { category: 'draft' },
|
|
110
|
+
* });
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
export type TypedCreateRequest<
|
|
114
|
+
D extends ProtocolDefinition,
|
|
115
|
+
M extends SchemaMap,
|
|
116
|
+
Path extends string,
|
|
117
|
+
> = {
|
|
118
|
+
/** The data payload. Type-checked against the schema map for the given path. */
|
|
119
|
+
data: DataForPath<D, M, Path>;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The context ID of the parent record.
|
|
123
|
+
*
|
|
124
|
+
* Required when creating a child record under a parent in a hierarchical
|
|
125
|
+
* protocol structure. Use the parent record's {@link TypedRecord.contextId | contextId}
|
|
126
|
+
* as this value.
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```ts
|
|
130
|
+
* const { record: notebook } = await proto.records.create('notebook', {
|
|
131
|
+
* data: { name: 'My Notebook' },
|
|
132
|
+
* });
|
|
133
|
+
*
|
|
134
|
+
* // Create a page under the notebook
|
|
135
|
+
* await proto.records.create('notebook/page', {
|
|
136
|
+
* data: { title: 'Page 1' },
|
|
137
|
+
* parentContextId: notebook.contextId,
|
|
138
|
+
* });
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
parentContextId?: string;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Whether the record should be publicly published.
|
|
145
|
+
*
|
|
146
|
+
* Published records can be read by anyone without authorization.
|
|
147
|
+
*/
|
|
148
|
+
published?: boolean;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* ISO 8601 timestamp for when the record is considered published.
|
|
152
|
+
* Only meaningful when `published` is `true`.
|
|
153
|
+
*/
|
|
154
|
+
datePublished?: string;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The DID of the intended recipient.
|
|
158
|
+
*
|
|
159
|
+
* Sets the recipient for permission-scoped records. The recipient may
|
|
160
|
+
* have special read/write permissions as defined by the protocol's
|
|
161
|
+
* `$actions` rules.
|
|
162
|
+
*/
|
|
163
|
+
recipient?: string;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The protocol role under which to create this record.
|
|
167
|
+
*
|
|
168
|
+
* The DWN will verify that the author is authorized to write under
|
|
169
|
+
* this role per the protocol's `$actions` configuration.
|
|
170
|
+
*/
|
|
171
|
+
protocolRole?: string;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* The MIME type / data format for the record.
|
|
175
|
+
*
|
|
176
|
+
* If omitted, defaults to the first entry in the protocol type's
|
|
177
|
+
* `dataFormats` array (typically `'application/json'`).
|
|
178
|
+
*/
|
|
179
|
+
dataFormat?: DataFormatForPath<D, Path>;
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Key-value metadata tags to attach to the record.
|
|
183
|
+
*
|
|
184
|
+
* Tags are indexed by the DWN and can be used in query filters for
|
|
185
|
+
* efficient lookups. Values can be strings, numbers, booleans, or
|
|
186
|
+
* arrays of strings/numbers.
|
|
187
|
+
*/
|
|
188
|
+
tags?: globalThis.Record<string, string | number | boolean | string[] | number[]>;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Whether to persist the record to the local DWN immediately.
|
|
192
|
+
*
|
|
193
|
+
* Defaults to `true`. Set to `false` to create the record in memory
|
|
194
|
+
* only — you can call {@link TypedRecord.store | record.store()} later.
|
|
195
|
+
*/
|
|
196
|
+
store?: boolean;
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Whether to auto-encrypt the record.
|
|
200
|
+
*
|
|
201
|
+
* If omitted, encryption follows the protocol definition. Set to `true`
|
|
202
|
+
* to force encryption or `false` to skip it.
|
|
203
|
+
*/
|
|
204
|
+
encryption?: boolean;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Response from {@link TypedEnbox} `records.create()`.
|
|
209
|
+
*
|
|
210
|
+
* Uses a discriminated union so that TypeScript narrows `record` to
|
|
211
|
+
* `TypedRecord<T>` after a `status.code` check:
|
|
212
|
+
*
|
|
213
|
+
* ```ts
|
|
214
|
+
* const result = await proto.records.create('notebook', { data });
|
|
215
|
+
* if (result.record) {
|
|
216
|
+
* // TypeScript knows `record` is TypedRecord<NotebookData> here
|
|
217
|
+
* console.log(result.record.id);
|
|
218
|
+
* }
|
|
219
|
+
* ```
|
|
220
|
+
*
|
|
221
|
+
* @typeParam T - The data type of the created record.
|
|
222
|
+
*/
|
|
223
|
+
export type TypedCreateResponse<T = unknown> =
|
|
224
|
+
| (DwnResponseStatus & { record: TypedRecord<T> })
|
|
225
|
+
| (DwnResponseStatus & { record: undefined });
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Filter options for {@link TypedEnbox} `records.query()` and `records.subscribe()`.
|
|
229
|
+
*
|
|
230
|
+
* The `protocol`, `protocolPath`, and `schema` fields are automatically
|
|
231
|
+
* injected by {@link TypedEnbox} — you only need to supply additional
|
|
232
|
+
* filter criteria.
|
|
233
|
+
*
|
|
234
|
+
* Common filter fields inherited from `RecordsFilter`:
|
|
235
|
+
*
|
|
236
|
+
* - **`parentId`** — Filter by parent context ID. Despite the name, this
|
|
237
|
+
* filters on the parent record's **context ID** (i.e. pass
|
|
238
|
+
* `parent.contextId`, not `parent.id`). Use this to find child records
|
|
239
|
+
* under a specific parent in a hierarchical protocol.
|
|
240
|
+
* - **`recordId`** — Match a specific record by its unique ID.
|
|
241
|
+
* - **`recipient`** — Filter by recipient DID.
|
|
242
|
+
* - **`dataFormat`** — Filter by MIME type.
|
|
243
|
+
* - **`dateCreated`** — Range filter on creation date.
|
|
244
|
+
* - **`contextId`** — Filter by context ID directly.
|
|
245
|
+
*
|
|
246
|
+
* @example
|
|
247
|
+
* ```ts
|
|
248
|
+
* // Find pages under a specific notebook
|
|
249
|
+
* const { records } = await proto.records.query('notebook/page', {
|
|
250
|
+
* filter: {
|
|
251
|
+
* parentId: notebook.contextId, // filters by parent's context ID
|
|
252
|
+
* tags: { status: 'published' },
|
|
253
|
+
* },
|
|
254
|
+
* });
|
|
255
|
+
* ```
|
|
256
|
+
*/
|
|
257
|
+
export type TypedQueryFilter = Omit<RecordsFilter, 'protocol' | 'protocolPath' | 'schema'> & {
|
|
258
|
+
/**
|
|
259
|
+
* Filter records by tag values.
|
|
260
|
+
*
|
|
261
|
+
* Only records whose tags match **all** specified key-value pairs are
|
|
262
|
+
* returned. Array values match if the record's tag contains any of the
|
|
263
|
+
* specified values.
|
|
264
|
+
*/
|
|
265
|
+
tags?: globalThis.Record<string, string | number | boolean | (string | number)[]>;
|
|
266
|
+
/** Alias for `parentId` — filters records by parent context ID. */
|
|
267
|
+
parentContextId?: string;
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Options for {@link TypedEnbox} `records.query()`.
|
|
272
|
+
*
|
|
273
|
+
* All fields are optional — calling `query(path)` with no request object
|
|
274
|
+
* returns all records at that path.
|
|
275
|
+
*
|
|
276
|
+
* @example
|
|
277
|
+
* ```ts
|
|
278
|
+
* const { records, cursor } = await proto.records.query('notebook', {
|
|
279
|
+
* filter: { tags: { archived: false } },
|
|
280
|
+
* dateSort: DateSort.CreatedDescending,
|
|
281
|
+
* pagination: { limit: 25 },
|
|
282
|
+
* });
|
|
283
|
+
*
|
|
284
|
+
* // Paginate for more
|
|
285
|
+
* if (cursor) {
|
|
286
|
+
* const { records: next } = await proto.records.query('notebook', {
|
|
287
|
+
* pagination: { limit: 25, cursor },
|
|
288
|
+
* });
|
|
289
|
+
* }
|
|
290
|
+
* ```
|
|
291
|
+
*/
|
|
292
|
+
export type TypedQueryRequest = {
|
|
293
|
+
/**
|
|
294
|
+
* A remote DWN DID to query from.
|
|
295
|
+
*
|
|
296
|
+
* When set, the query is sent to the specified DID's remote DWN instead
|
|
297
|
+
* of the local DWN.
|
|
298
|
+
*/
|
|
299
|
+
from?: string;
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Filter criteria for the query.
|
|
303
|
+
*
|
|
304
|
+
* The `protocol`, `protocolPath`, and `schema` fields are auto-injected.
|
|
305
|
+
* See {@link TypedQueryFilter} for available filter fields.
|
|
306
|
+
*/
|
|
307
|
+
filter?: TypedQueryFilter;
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Sort order for the returned records.
|
|
311
|
+
*
|
|
312
|
+
* Use `DateSort.CreatedAscending`, `DateSort.CreatedDescending`,
|
|
313
|
+
* `DateSort.PublishedAscending`, or `DateSort.PublishedDescending`.
|
|
314
|
+
*/
|
|
315
|
+
dateSort?: DateSort;
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Pagination options.
|
|
319
|
+
*
|
|
320
|
+
* - `limit` — Maximum number of records to return.
|
|
321
|
+
* - `cursor` — A pagination cursor from a previous query response to
|
|
322
|
+
* resume from where the last page left off.
|
|
323
|
+
*/
|
|
324
|
+
pagination?: { limit?: number; cursor?: DwnPaginationCursor };
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* The protocol role under which to execute the query.
|
|
328
|
+
*
|
|
329
|
+
* Required when the protocol's `$actions` rules restrict read access
|
|
330
|
+
* to specific roles.
|
|
331
|
+
*/
|
|
332
|
+
protocolRole?: string;
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* When `true`, automatically decrypts encrypted records in the results.
|
|
336
|
+
*
|
|
337
|
+
* If omitted, encrypted records are returned as-is (data accessors will
|
|
338
|
+
* return encrypted bytes).
|
|
339
|
+
*/
|
|
340
|
+
encryption?: boolean;
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Response from {@link TypedEnbox} `records.query()`.
|
|
345
|
+
*
|
|
346
|
+
* @typeParam T - The data type of the queried records.
|
|
347
|
+
*/
|
|
348
|
+
export type TypedQueryResponse<T = unknown> = DwnResponseStatus & {
|
|
349
|
+
/**
|
|
350
|
+
* The matching records, each wrapped as {@link TypedRecord | TypedRecord<T>}.
|
|
351
|
+
*
|
|
352
|
+
* The array is empty if no records match the filter criteria.
|
|
353
|
+
*/
|
|
354
|
+
records: TypedRecord<T>[];
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* A pagination cursor for fetching the next page of results.
|
|
358
|
+
*
|
|
359
|
+
* Pass this to a subsequent `query()` call's `pagination.cursor` to
|
|
360
|
+
* continue from where this page ended. `undefined` when there are no
|
|
361
|
+
* more results.
|
|
362
|
+
*/
|
|
363
|
+
cursor?: DwnPaginationCursor;
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Options for {@link TypedEnbox} `records.read()`.
|
|
368
|
+
*
|
|
369
|
+
* A `filter` is required to identify which record to read. The most common
|
|
370
|
+
* approach is to filter by `recordId`.
|
|
371
|
+
*
|
|
372
|
+
* @example
|
|
373
|
+
* ```ts
|
|
374
|
+
* // Read a specific record by ID
|
|
375
|
+
* const { record } = await proto.records.read('notebook', {
|
|
376
|
+
* filter: { recordId: notebookId },
|
|
377
|
+
* });
|
|
378
|
+
*
|
|
379
|
+
* // Read from a remote DWN
|
|
380
|
+
* const { record: remote } = await proto.records.read('notebook', {
|
|
381
|
+
* from: 'did:example:alice',
|
|
382
|
+
* filter: { recordId: notebookId },
|
|
383
|
+
* });
|
|
384
|
+
* ```
|
|
385
|
+
*/
|
|
386
|
+
export type TypedReadRequest = {
|
|
387
|
+
/**
|
|
388
|
+
* A remote DWN DID to read from.
|
|
389
|
+
*
|
|
390
|
+
* When set, the read is performed against the specified DID's remote
|
|
391
|
+
* DWN instead of the local DWN.
|
|
392
|
+
*/
|
|
393
|
+
from?: string;
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Filter to identify the record to read.
|
|
397
|
+
*
|
|
398
|
+
* The `protocol`, `protocolPath`, and `schema` fields are auto-injected.
|
|
399
|
+
* Typically you filter by `recordId` to read a specific record. Other
|
|
400
|
+
* fields from `RecordsFilter` (like `contextId`, `parentId`, `recipient`)
|
|
401
|
+
* are also available.
|
|
402
|
+
*/
|
|
403
|
+
filter: Omit<RecordsFilter, 'protocol' | 'protocolPath' | 'schema'> & {
|
|
404
|
+
/** Alias for `parentId` — filters records by parent context ID. */
|
|
405
|
+
parentContextId?: string;
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* When `true`, automatically decrypts the record if it is encrypted.
|
|
410
|
+
*
|
|
411
|
+
* If omitted, encrypted records are returned as-is.
|
|
412
|
+
*/
|
|
413
|
+
encryption?: boolean;
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Response from {@link TypedEnbox} `records.read()`.
|
|
418
|
+
*
|
|
419
|
+
* Uses a discriminated union so that TypeScript narrows `record` to
|
|
420
|
+
* `TypedRecord<T>` after a truthiness check:
|
|
421
|
+
*
|
|
422
|
+
* ```ts
|
|
423
|
+
* const result = await proto.records.read('notebook', { filter: { recordId } });
|
|
424
|
+
* if (result.record) {
|
|
425
|
+
* const data = await result.record.data.json(); // NotebookData
|
|
426
|
+
* }
|
|
427
|
+
* ```
|
|
428
|
+
*
|
|
429
|
+
* @typeParam T - The data type of the read record.
|
|
430
|
+
*/
|
|
431
|
+
export type TypedReadResponse<T = unknown> =
|
|
432
|
+
| (DwnResponseStatus & { record: TypedRecord<T> })
|
|
433
|
+
| (DwnResponseStatus & { record: undefined });
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Options for {@link TypedEnbox} `records.delete()`.
|
|
437
|
+
*
|
|
438
|
+
* @example
|
|
439
|
+
* ```ts
|
|
440
|
+
* const { status } = await proto.records.delete('notebook', {
|
|
441
|
+
* recordId: notebook.id,
|
|
442
|
+
* });
|
|
443
|
+
* ```
|
|
444
|
+
*/
|
|
445
|
+
export type TypedDeleteRequest = {
|
|
446
|
+
/**
|
|
447
|
+
* A remote DWN DID to delete from.
|
|
448
|
+
*
|
|
449
|
+
* When set, the delete is performed on the specified DID's remote DWN.
|
|
450
|
+
*/
|
|
451
|
+
from?: string;
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* The unique `recordId` of the record to delete.
|
|
455
|
+
*
|
|
456
|
+
* Use {@link TypedRecord.id | record.id} to obtain this value.
|
|
457
|
+
*/
|
|
458
|
+
recordId: string;
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Options for {@link TypedEnbox} `records.subscribe()`.
|
|
463
|
+
*
|
|
464
|
+
* @example
|
|
465
|
+
* ```ts
|
|
466
|
+
* const { liveQuery } = await proto.records.subscribe('notebook/page', {
|
|
467
|
+
* filter: { parentId: notebook.contextId },
|
|
468
|
+
* });
|
|
469
|
+
*
|
|
470
|
+
* liveQuery.on('create', (record) => {
|
|
471
|
+
* console.log('New page:', await record.data.json());
|
|
472
|
+
* });
|
|
473
|
+
* ```
|
|
474
|
+
*/
|
|
475
|
+
export type TypedSubscribeRequest = {
|
|
476
|
+
/**
|
|
477
|
+
* A remote DWN DID to subscribe to.
|
|
478
|
+
*
|
|
479
|
+
* When set, the subscription listens to the specified DID's remote DWN.
|
|
480
|
+
*/
|
|
481
|
+
from?: string;
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Filter criteria for the subscription.
|
|
485
|
+
*
|
|
486
|
+
* The `protocol`, `protocolPath`, and `schema` fields are auto-injected.
|
|
487
|
+
* See {@link TypedQueryFilter} for available filter fields.
|
|
488
|
+
*/
|
|
489
|
+
filter?: TypedQueryFilter;
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* The protocol role under which to subscribe.
|
|
493
|
+
*
|
|
494
|
+
* Required when the protocol's `$actions` rules restrict read access
|
|
495
|
+
* to specific roles.
|
|
496
|
+
*/
|
|
497
|
+
protocolRole?: string;
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Response from {@link TypedEnbox} `records.subscribe()`.
|
|
502
|
+
*
|
|
503
|
+
* @typeParam T - The data type of records in the subscription.
|
|
504
|
+
*/
|
|
505
|
+
export type TypedSubscribeResponse<T = unknown> = DwnResponseStatus & {
|
|
506
|
+
/**
|
|
507
|
+
* The typed live query instance for receiving real-time record changes.
|
|
508
|
+
*
|
|
509
|
+
* `undefined` if the subscription request failed (check `status.code`).
|
|
510
|
+
* When defined, use `liveQuery.on('create' | 'update' | 'delete', callback)`
|
|
511
|
+
* to react to changes. Call `liveQuery.close()` to stop the subscription.
|
|
512
|
+
*/
|
|
513
|
+
liveQuery?: TypedLiveQuery<T>;
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
// ---------------------------------------------------------------------------
|
|
517
|
+
// TypedEnbox class
|
|
518
|
+
// ---------------------------------------------------------------------------
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* A protocol-scoped API that auto-injects `protocol`, `protocolPath`, and
|
|
522
|
+
* `schema` into every DWN operation.
|
|
523
|
+
*
|
|
524
|
+
* All record-returning methods wrap results in {@link TypedRecord} so that
|
|
525
|
+
* the data type `T` (resolved from the schema map) flows end-to-end — from
|
|
526
|
+
* write through read, query, update, and subscribe — without manual casts.
|
|
527
|
+
*
|
|
528
|
+
* Obtain an instance via `enbox.using(typedProtocol)`.
|
|
529
|
+
*
|
|
530
|
+
* @example
|
|
531
|
+
* ```ts
|
|
532
|
+
* const social = enbox.using(SocialProtocol);
|
|
533
|
+
*
|
|
534
|
+
* await social.configure();
|
|
535
|
+
*
|
|
536
|
+
* const { record } = await social.records.create('friend', {
|
|
537
|
+
* data: { did: 'did:example:alice', alias: 'Alice' },
|
|
538
|
+
* });
|
|
539
|
+
* const data = await record.data.json(); // FriendData — no cast
|
|
540
|
+
*
|
|
541
|
+
* const { records } = await social.records.query('friend', {
|
|
542
|
+
* filter: { tags: { did: 'did:example:alice' } },
|
|
543
|
+
* });
|
|
544
|
+
* for (const r of records) {
|
|
545
|
+
* const d = await r.data.json(); // FriendData
|
|
546
|
+
* }
|
|
547
|
+
* ```
|
|
548
|
+
*/
|
|
549
|
+
export class TypedEnbox<
|
|
550
|
+
D extends ProtocolDefinition = ProtocolDefinition,
|
|
551
|
+
M extends SchemaMap = SchemaMap,
|
|
552
|
+
> {
|
|
553
|
+
/** @internal */
|
|
554
|
+
private _dwn: DwnApi;
|
|
555
|
+
/** @internal */
|
|
556
|
+
private _definition: D;
|
|
557
|
+
/** @internal */
|
|
558
|
+
private _configured: boolean = false;
|
|
559
|
+
/** @internal */
|
|
560
|
+
private _ensureReadyPromise: Promise<void> | null = null;
|
|
561
|
+
/** @internal */
|
|
562
|
+
private _validPaths: Set<string>;
|
|
563
|
+
/** @internal */
|
|
564
|
+
private _records?: TypedEnbox<D, M>['records'];
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* @internal Create a new `TypedEnbox` instance. Use `enbox.using(protocol)` instead.
|
|
568
|
+
* @param dwn - The underlying DWN API instance.
|
|
569
|
+
* @param protocol - The typed protocol containing the definition and schema map.
|
|
570
|
+
*/
|
|
571
|
+
constructor(dwn: DwnApi, protocol: TypedProtocol<D, M>) {
|
|
572
|
+
this._dwn = dwn;
|
|
573
|
+
this._definition = protocol.definition;
|
|
574
|
+
this._validPaths = collectPaths(this._definition.structure);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* The protocol URI string (e.g. `'https://example.com/social'`).
|
|
579
|
+
*
|
|
580
|
+
* This is the globally unique identifier for the protocol and is
|
|
581
|
+
* auto-injected into every record operation.
|
|
582
|
+
*/
|
|
583
|
+
public get protocol(): string {
|
|
584
|
+
return this._definition.protocol;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* The raw protocol definition object.
|
|
589
|
+
*
|
|
590
|
+
* Contains the full `protocol`, `types`, and `structure` that define
|
|
591
|
+
* the protocol's schema and permission rules.
|
|
592
|
+
*/
|
|
593
|
+
public get definition(): D {
|
|
594
|
+
return this._definition;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Configures (installs) this protocol on the local DWN.
|
|
599
|
+
*
|
|
600
|
+
* If the protocol is already installed with an identical definition,
|
|
601
|
+
* this is a no-op and returns the existing protocol with status `200`.
|
|
602
|
+
* If the definition has changed (e.g. new types, modified structure),
|
|
603
|
+
* the protocol is re-configured with the updated definition and returns
|
|
604
|
+
* status `202`.
|
|
605
|
+
*
|
|
606
|
+
* **Must be called before any record operations.** Methods like
|
|
607
|
+
* `records.create()`, `records.query()`, etc. will throw if the protocol
|
|
608
|
+
* has not been configured.
|
|
609
|
+
*
|
|
610
|
+
* @param options - Optional configuration overrides.
|
|
611
|
+
* @param options.encryption - Whether to enable auto-encryption for the
|
|
612
|
+
* protocol. If omitted, follows the protocol definition defaults.
|
|
613
|
+
* @returns The DWN response status and the installed protocol object.
|
|
614
|
+
*
|
|
615
|
+
* @example
|
|
616
|
+
* ```ts
|
|
617
|
+
* const proto = enbox.using(NotebookProtocol);
|
|
618
|
+
*
|
|
619
|
+
* const { status, protocol } = await proto.configure();
|
|
620
|
+
* console.log(status.code); // 202 (first install) or 200 (already installed)
|
|
621
|
+
*
|
|
622
|
+
* // Now you can use records.create(), records.query(), etc.
|
|
623
|
+
* ```
|
|
624
|
+
*/
|
|
625
|
+
public async configure(options?: { encryption?: boolean }): Promise<DwnResponseStatus & { protocol?: Protocol }> {
|
|
626
|
+
// Query for an existing installation of this protocol.
|
|
627
|
+
const { protocols } = await this._dwn.protocols.query({
|
|
628
|
+
filter: { protocol: this._definition.protocol },
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
// If already installed with the same definition, return it as-is.
|
|
632
|
+
if (protocols.length > 0) {
|
|
633
|
+
const existing = protocols[0];
|
|
634
|
+
if (definitionsEqual(existing.definition, this._definition)) {
|
|
635
|
+
this._configured = true;
|
|
636
|
+
return { status: { code: 200, detail: 'OK' }, protocol: existing };
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Not installed or definition has changed — configure the new version.
|
|
641
|
+
const result = await this._dwn.protocols.configure({
|
|
642
|
+
definition : this._definition,
|
|
643
|
+
encryption : options?.encryption,
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
if (result.status.code === 202) {
|
|
647
|
+
this._configured = true;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return result;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Whether the protocol has been configured (installed) on the local DWN.
|
|
655
|
+
*
|
|
656
|
+
* Returns `true` after a successful call to {@link TypedEnbox.configure | configure()}.
|
|
657
|
+
* Record operations will throw if this is `false`.
|
|
658
|
+
*/
|
|
659
|
+
public get isConfigured(): boolean {
|
|
660
|
+
return this._configured;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Validates that the path is recognized.
|
|
665
|
+
* Throws a descriptive error if the path is not a valid protocol path.
|
|
666
|
+
*/
|
|
667
|
+
private _assertValidPath(path: string): void {
|
|
668
|
+
if (!this._validPaths.has(path)) {
|
|
669
|
+
throw new Error(
|
|
670
|
+
`TypedEnbox: invalid protocol path '${path}'. ` +
|
|
671
|
+
`Valid paths are: ${[...this._validPaths].join(', ')}.`,
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Ensures the protocol is configured before performing record operations.
|
|
678
|
+
*
|
|
679
|
+
* On first call, queries for an existing protocol installation:
|
|
680
|
+
* - If found with an identical definition → marks as configured.
|
|
681
|
+
* - If found with a different definition → marks as configured but warns
|
|
682
|
+
* that the local definition differs from the installed one.
|
|
683
|
+
* - If not found → installs the protocol via `protocols.configure()`.
|
|
684
|
+
*
|
|
685
|
+
* Concurrent calls are deduplicated via a shared Promise so the network
|
|
686
|
+
* call only happens once.
|
|
687
|
+
*/
|
|
688
|
+
private async _ensureReady(path: string): Promise<void> {
|
|
689
|
+
if (this._configured) {
|
|
690
|
+
this._assertValidPath(path);
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (this._ensureReadyPromise === null) {
|
|
695
|
+
this._ensureReadyPromise = this._autoConfigureOnce();
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
await this._ensureReadyPromise;
|
|
699
|
+
this._assertValidPath(path);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Performs the one-time auto-configuration check. Called at most once;
|
|
704
|
+
* subsequent calls reuse the same Promise via `_ensureReadyPromise`.
|
|
705
|
+
*/
|
|
706
|
+
private async _autoConfigureOnce(): Promise<void> {
|
|
707
|
+
const { protocols } = await this._dwn.protocols.query({
|
|
708
|
+
filter: { protocol: this._definition.protocol },
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
if (protocols.length > 0) {
|
|
712
|
+
const existing = protocols[0];
|
|
713
|
+
if (definitionsEqual(existing.definition, this._definition)) {
|
|
714
|
+
this._configured = true;
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Installed but definitions differ — allow operations but warn.
|
|
719
|
+
console.warn(
|
|
720
|
+
`TypedEnbox: installed protocol '${this._definition.protocol}' differs from the provided definition. ` +
|
|
721
|
+
'Call configure() to update it.',
|
|
722
|
+
);
|
|
723
|
+
this._configured = true;
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Not installed — configure it now.
|
|
728
|
+
const result = await this._dwn.protocols.configure({
|
|
729
|
+
definition: this._definition,
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
if (result.status.code === 202) {
|
|
733
|
+
this._configured = true;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Protocol-scoped record operations.
|
|
739
|
+
*
|
|
740
|
+
* Every method auto-injects the `protocol`, `protocolPath`, and `schema`
|
|
741
|
+
* from the protocol definition — you never need to specify them manually.
|
|
742
|
+
* Path parameters provide **compile-time autocompletion** via
|
|
743
|
+
* `ProtocolPaths<D>`, and data types are resolved from the schema map.
|
|
744
|
+
*
|
|
745
|
+
* All methods return {@link TypedRecord} or {@link TypedLiveQuery} instances
|
|
746
|
+
* that carry the resolved data type from the schema map, providing
|
|
747
|
+
* end-to-end type safety.
|
|
748
|
+
*
|
|
749
|
+
* Available methods:
|
|
750
|
+
* - {@link TypedEnbox.records.create | create(path, request)} — Create a new record
|
|
751
|
+
* - {@link TypedEnbox.records.query | query(path, request?)} — Query records with filters
|
|
752
|
+
* - {@link TypedEnbox.records.read | read(path, request)} — Read a single record
|
|
753
|
+
* - {@link TypedEnbox.records.delete | delete(path, request)} — Delete a record by ID
|
|
754
|
+
* - {@link TypedEnbox.records.subscribe | subscribe(path, request?)} — Real-time subscription
|
|
755
|
+
*/
|
|
756
|
+
public get records(): {
|
|
757
|
+
create: <Path extends ProtocolPaths<D> & string>(
|
|
758
|
+
path: Path,
|
|
759
|
+
request: TypedCreateRequest<D, M, Path>,
|
|
760
|
+
) => Promise<TypedCreateResponse<DataForPath<D, M, Path>>>;
|
|
761
|
+
|
|
762
|
+
query: <Path extends ProtocolPaths<D> & string>(
|
|
763
|
+
path: Path,
|
|
764
|
+
request?: TypedQueryRequest,
|
|
765
|
+
) => Promise<TypedQueryResponse<DataForPath<D, M, Path>>>;
|
|
766
|
+
|
|
767
|
+
read: <Path extends ProtocolPaths<D> & string>(
|
|
768
|
+
path: Path,
|
|
769
|
+
request: TypedReadRequest,
|
|
770
|
+
) => Promise<TypedReadResponse<DataForPath<D, M, Path>>>;
|
|
771
|
+
|
|
772
|
+
delete: <Path extends ProtocolPaths<D> & string>(
|
|
773
|
+
path: Path,
|
|
774
|
+
request: TypedDeleteRequest,
|
|
775
|
+
) => Promise<DwnResponseStatus>;
|
|
776
|
+
|
|
777
|
+
subscribe: <Path extends ProtocolPaths<D> & string>(
|
|
778
|
+
path: Path,
|
|
779
|
+
request?: TypedSubscribeRequest,
|
|
780
|
+
) => Promise<TypedSubscribeResponse<DataForPath<D, M, Path>>>;
|
|
781
|
+
} {
|
|
782
|
+
if (this._records !== undefined) {
|
|
783
|
+
return this._records;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const cached = {
|
|
787
|
+
/**
|
|
788
|
+
* Create a new record at the given protocol path.
|
|
789
|
+
*
|
|
790
|
+
* The `protocol`, `protocolPath`, and `schema` are auto-injected from
|
|
791
|
+
* the protocol definition. The `data` field is type-checked against
|
|
792
|
+
* the schema map for the given path.
|
|
793
|
+
*
|
|
794
|
+
* @param path - The protocol path (e.g. `'notebook'`, `'notebook/page'`).
|
|
795
|
+
* Provides compile-time autocompletion for valid paths.
|
|
796
|
+
* @param request - Create options including the typed `data` payload
|
|
797
|
+
* and optional fields like `parentContextId`, `tags`, `recipient`.
|
|
798
|
+
* @returns A {@link TypedCreateResponse} containing the DWN response
|
|
799
|
+
* `status` and the created {@link TypedRecord}.
|
|
800
|
+
*
|
|
801
|
+
* @example
|
|
802
|
+
* ```ts
|
|
803
|
+
* const { status, record } = await proto.records.create('notebook', {
|
|
804
|
+
* data: { name: 'My Notebook' },
|
|
805
|
+
* });
|
|
806
|
+
*
|
|
807
|
+
* // Create a child page under the notebook's context
|
|
808
|
+
* const { record: page } = await proto.records.create('notebook/page', {
|
|
809
|
+
* data: { title: 'First Page', body: '' },
|
|
810
|
+
* parentContextId: record.contextId,
|
|
811
|
+
* });
|
|
812
|
+
* ```
|
|
813
|
+
*/
|
|
814
|
+
create: async <Path extends ProtocolPaths<D> & string>(
|
|
815
|
+
path: Path,
|
|
816
|
+
request: TypedCreateRequest<D, M, Path>,
|
|
817
|
+
): Promise<TypedCreateResponse<DataForPath<D, M, Path>>> => {
|
|
818
|
+
const normalizedPath = normalizePath(path);
|
|
819
|
+
await this._ensureReady(normalizedPath);
|
|
820
|
+
const typeName = lastSegment(normalizedPath);
|
|
821
|
+
const typeEntry = this._definition.types[typeName] as ProtocolType | undefined;
|
|
822
|
+
|
|
823
|
+
const { status, record } = await this._dwn.records.write({
|
|
824
|
+
data : request.data,
|
|
825
|
+
store : request.store,
|
|
826
|
+
encryption : request.encryption,
|
|
827
|
+
parentContextId : request.parentContextId,
|
|
828
|
+
published : request.published,
|
|
829
|
+
datePublished : request.datePublished,
|
|
830
|
+
recipient : request.recipient,
|
|
831
|
+
protocolRole : request.protocolRole,
|
|
832
|
+
tags : request.tags,
|
|
833
|
+
protocol : this._definition.protocol,
|
|
834
|
+
protocolPath : normalizedPath,
|
|
835
|
+
...(typeEntry?.schema !== undefined ? { schema: typeEntry.schema } : {}),
|
|
836
|
+
dataFormat : request.dataFormat ?? typeEntry?.dataFormats?.[0],
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
return {
|
|
840
|
+
status,
|
|
841
|
+
record: record ? new TypedRecord<DataForPath<D, M, Path>>(record) : undefined,
|
|
842
|
+
};
|
|
843
|
+
},
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Query records at the given protocol path.
|
|
847
|
+
*
|
|
848
|
+
* Returns all matching records as an array of typed records, with
|
|
849
|
+
* an optional pagination cursor for fetching additional pages.
|
|
850
|
+
*
|
|
851
|
+
* @param path - The protocol path to query (e.g. `'notebook'`,
|
|
852
|
+
* `'notebook/page'`).
|
|
853
|
+
* @param request - Optional filter, sort, and pagination options.
|
|
854
|
+
* Omit entirely to return all records at the path.
|
|
855
|
+
* @returns A {@link TypedQueryResponse} containing `status`, `records`
|
|
856
|
+
* (as {@link TypedRecord | TypedRecord<T>[]}), and an optional
|
|
857
|
+
* `cursor` for pagination.
|
|
858
|
+
*
|
|
859
|
+
* @example
|
|
860
|
+
* ```ts
|
|
861
|
+
* // Query all notebooks
|
|
862
|
+
* const { records } = await proto.records.query('notebook');
|
|
863
|
+
*
|
|
864
|
+
* // Query pages under a specific notebook
|
|
865
|
+
* const { records: pages } = await proto.records.query('notebook/page', {
|
|
866
|
+
* filter: { parentId: notebook.contextId },
|
|
867
|
+
* });
|
|
868
|
+
*
|
|
869
|
+
* for (const page of pages) {
|
|
870
|
+
* const data = await page.data.json(); // PageData
|
|
871
|
+
* }
|
|
872
|
+
*
|
|
873
|
+
* // Paginated query
|
|
874
|
+
* const { records: batch, cursor } = await proto.records.query('notebook', {
|
|
875
|
+
* pagination: { limit: 10 },
|
|
876
|
+
* dateSort: DateSort.CreatedDescending,
|
|
877
|
+
* });
|
|
878
|
+
* ```
|
|
879
|
+
*/
|
|
880
|
+
query: async <Path extends ProtocolPaths<D> & string>(
|
|
881
|
+
path: Path,
|
|
882
|
+
request?: TypedQueryRequest,
|
|
883
|
+
): Promise<TypedQueryResponse<DataForPath<D, M, Path>>> => {
|
|
884
|
+
const normalizedPath = normalizePath(path);
|
|
885
|
+
await this._ensureReady(normalizedPath);
|
|
886
|
+
const typeName = lastSegment(normalizedPath);
|
|
887
|
+
const typeEntry = this._definition.types[typeName] as ProtocolType | undefined;
|
|
888
|
+
|
|
889
|
+
const queryFilter = mapParentContextId(request?.filter);
|
|
890
|
+
|
|
891
|
+
const { status, records, cursor } = await this._dwn.records.query({
|
|
892
|
+
from : request?.from,
|
|
893
|
+
encryption : request?.encryption,
|
|
894
|
+
filter : {
|
|
895
|
+
...queryFilter,
|
|
896
|
+
protocol : this._definition.protocol,
|
|
897
|
+
protocolPath : normalizedPath,
|
|
898
|
+
...(typeEntry?.schema !== undefined ? { schema: typeEntry.schema } : {}),
|
|
899
|
+
},
|
|
900
|
+
dateSort : request?.dateSort,
|
|
901
|
+
pagination : request?.pagination,
|
|
902
|
+
protocolRole : request?.protocolRole,
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
return {
|
|
906
|
+
status,
|
|
907
|
+
records: records.map((r) => new TypedRecord<DataForPath<D, M, Path>>(r)),
|
|
908
|
+
cursor,
|
|
909
|
+
};
|
|
910
|
+
},
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* Read a single record at the given protocol path.
|
|
914
|
+
*
|
|
915
|
+
* Unlike `query()`, which returns an array, `read()` returns exactly
|
|
916
|
+
* one record. Use `filter.recordId` to target a specific record.
|
|
917
|
+
*
|
|
918
|
+
* @param path - The protocol path to read from.
|
|
919
|
+
* @param request - Read options including a `filter` to identify the
|
|
920
|
+
* record. See {@link TypedReadRequest} for details.
|
|
921
|
+
* @returns A {@link TypedReadResponse} containing `status` and the
|
|
922
|
+
* matching {@link TypedRecord}.
|
|
923
|
+
*
|
|
924
|
+
* @example
|
|
925
|
+
* ```ts
|
|
926
|
+
* const { record } = await proto.records.read('notebook', {
|
|
927
|
+
* filter: { recordId: notebookId },
|
|
928
|
+
* });
|
|
929
|
+
*
|
|
930
|
+
* const data = await record.data.json(); // NotebookData
|
|
931
|
+
* console.log(data.name);
|
|
932
|
+
* ```
|
|
933
|
+
*/
|
|
934
|
+
read: async <Path extends ProtocolPaths<D> & string>(
|
|
935
|
+
path: Path,
|
|
936
|
+
request: TypedReadRequest,
|
|
937
|
+
): Promise<TypedReadResponse<DataForPath<D, M, Path>>> => {
|
|
938
|
+
const normalizedPath = normalizePath(path);
|
|
939
|
+
await this._ensureReady(normalizedPath);
|
|
940
|
+
const typeName = lastSegment(normalizedPath);
|
|
941
|
+
const typeEntry = this._definition.types[typeName] as ProtocolType | undefined;
|
|
942
|
+
|
|
943
|
+
const readFilter = mapParentContextId(request.filter);
|
|
944
|
+
|
|
945
|
+
const { status, record } = await this._dwn.records.read({
|
|
946
|
+
from : request.from,
|
|
947
|
+
encryption : request.encryption,
|
|
948
|
+
protocol : this._definition.protocol,
|
|
949
|
+
filter : {
|
|
950
|
+
...readFilter,
|
|
951
|
+
protocol : this._definition.protocol,
|
|
952
|
+
protocolPath : normalizedPath,
|
|
953
|
+
...(typeEntry?.schema !== undefined ? { schema: typeEntry.schema } : {}),
|
|
954
|
+
},
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
return {
|
|
958
|
+
status,
|
|
959
|
+
record: record ? new TypedRecord<DataForPath<D, M, Path>>(record) : undefined,
|
|
960
|
+
};
|
|
961
|
+
},
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* Delete a record at the given protocol path.
|
|
965
|
+
*
|
|
966
|
+
* The path is used for protocol validation and permission scoping,
|
|
967
|
+
* while the actual record is identified by `recordId`.
|
|
968
|
+
*
|
|
969
|
+
* @param path - The protocol path (used for permission scoping and
|
|
970
|
+
* path validation).
|
|
971
|
+
* @param request - Delete options. `recordId` is required; `from` is
|
|
972
|
+
* optional for remote deletes.
|
|
973
|
+
* @returns The DWN response status.
|
|
974
|
+
*
|
|
975
|
+
* @example
|
|
976
|
+
* ```ts
|
|
977
|
+
* const { status } = await proto.records.delete('notebook', {
|
|
978
|
+
* recordId: notebook.id,
|
|
979
|
+
* });
|
|
980
|
+
*
|
|
981
|
+
* if (status.code === 202) {
|
|
982
|
+
* console.log('Notebook deleted');
|
|
983
|
+
* }
|
|
984
|
+
* ```
|
|
985
|
+
*/
|
|
986
|
+
delete: async <Path extends ProtocolPaths<D> & string>(
|
|
987
|
+
_path: Path,
|
|
988
|
+
request: TypedDeleteRequest,
|
|
989
|
+
): Promise<DwnResponseStatus> => {
|
|
990
|
+
await this._ensureReady(normalizePath(_path));
|
|
991
|
+
return this._dwn.records.delete({
|
|
992
|
+
from : request.from,
|
|
993
|
+
protocol : this._definition.protocol,
|
|
994
|
+
recordId : request.recordId,
|
|
995
|
+
});
|
|
996
|
+
},
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* Subscribe to real-time changes at the given protocol path.
|
|
1000
|
+
*
|
|
1001
|
+
* Returns a {@link TypedLiveQuery} that atomically provides an initial
|
|
1002
|
+
* snapshot and a real-time stream of deduplicated change events, with
|
|
1003
|
+
* all records typed as `TypedRecord<T>`.
|
|
1004
|
+
*
|
|
1005
|
+
* @param path - The protocol path to subscribe to.
|
|
1006
|
+
* @param request - Optional filter and role. Use `filter.parentId`
|
|
1007
|
+
* to scope the subscription to children of a specific parent.
|
|
1008
|
+
* @returns A {@link TypedSubscribeResponse} containing `status` and
|
|
1009
|
+
* a {@link TypedLiveQuery} for receiving events.
|
|
1010
|
+
*
|
|
1011
|
+
* @example
|
|
1012
|
+
* ```ts
|
|
1013
|
+
* const { liveQuery } = await proto.records.subscribe('notebook/page', {
|
|
1014
|
+
* filter: { parentId: notebook.contextId },
|
|
1015
|
+
* });
|
|
1016
|
+
*
|
|
1017
|
+
* liveQuery.on('create', (record) => {
|
|
1018
|
+
* // record is TypedRecord<PageData>
|
|
1019
|
+
* console.log('New page created');
|
|
1020
|
+
* });
|
|
1021
|
+
*
|
|
1022
|
+
* liveQuery.on('update', (record) => {
|
|
1023
|
+
* const data = await record.data.json(); // PageData
|
|
1024
|
+
* });
|
|
1025
|
+
*
|
|
1026
|
+
* liveQuery.on('delete', (record) => {
|
|
1027
|
+
* console.log('Page deleted:', record.id);
|
|
1028
|
+
* });
|
|
1029
|
+
*
|
|
1030
|
+
* // Stop listening
|
|
1031
|
+
* liveQuery.close();
|
|
1032
|
+
* ```
|
|
1033
|
+
*/
|
|
1034
|
+
subscribe: async <Path extends ProtocolPaths<D> & string>(
|
|
1035
|
+
path: Path,
|
|
1036
|
+
request?: TypedSubscribeRequest,
|
|
1037
|
+
): Promise<TypedSubscribeResponse<DataForPath<D, M, Path>>> => {
|
|
1038
|
+
const normalizedPath = normalizePath(path);
|
|
1039
|
+
await this._ensureReady(normalizedPath);
|
|
1040
|
+
const typeName = lastSegment(normalizedPath);
|
|
1041
|
+
const typeEntry = this._definition.types[typeName] as ProtocolType | undefined;
|
|
1042
|
+
|
|
1043
|
+
const subFilter = mapParentContextId(request?.filter);
|
|
1044
|
+
|
|
1045
|
+
const { status, liveQuery } = await this._dwn.records.subscribe({
|
|
1046
|
+
from : request?.from,
|
|
1047
|
+
filter : {
|
|
1048
|
+
...subFilter,
|
|
1049
|
+
protocol : this._definition.protocol,
|
|
1050
|
+
protocolPath : normalizedPath,
|
|
1051
|
+
...(typeEntry?.schema !== undefined ? { schema: typeEntry.schema } : {}),
|
|
1052
|
+
},
|
|
1053
|
+
protocolRole: request?.protocolRole,
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
return {
|
|
1057
|
+
status,
|
|
1058
|
+
liveQuery: liveQuery ? new TypedLiveQuery<DataForPath<D, M, Path>>(liveQuery) : undefined,
|
|
1059
|
+
};
|
|
1060
|
+
},
|
|
1061
|
+
};
|
|
1062
|
+
|
|
1063
|
+
this._records = cached;
|
|
1064
|
+
return cached;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// ---------------------------------------------------------------------------
|
|
1069
|
+
// Helpers
|
|
1070
|
+
// ---------------------------------------------------------------------------
|
|
1071
|
+
|
|
1072
|
+
/**
|
|
1073
|
+
* Maps the `parentContextId` alias to the underlying `parentId` field
|
|
1074
|
+
* expected by the DWN SDK. If both are provided, `parentId` takes precedence.
|
|
1075
|
+
* Returns a new object (or `undefined` if the input was `undefined`).
|
|
1076
|
+
*/
|
|
1077
|
+
function mapParentContextId<T extends Record<string, unknown>>(
|
|
1078
|
+
filter: T | undefined,
|
|
1079
|
+
): Omit<T, 'parentContextId'> | undefined {
|
|
1080
|
+
if (!filter) { return undefined; }
|
|
1081
|
+
const { parentContextId, ...rest } = filter as Record<string, unknown>;
|
|
1082
|
+
if (parentContextId !== undefined && rest.parentId === undefined) {
|
|
1083
|
+
rest.parentId = parentContextId;
|
|
1084
|
+
}
|
|
1085
|
+
return rest as Omit<T, 'parentContextId'>;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
/**
|
|
1089
|
+
* Compares two protocol definitions for deep equality using deterministic
|
|
1090
|
+
* JSON serialization.
|
|
1091
|
+
*
|
|
1092
|
+
* Keys are sorted recursively so that semantically identical definitions
|
|
1093
|
+
* with different key ordering are treated as equal.
|
|
1094
|
+
*/
|
|
1095
|
+
function definitionsEqual(a: unknown, b: unknown): boolean {
|
|
1096
|
+
return stableStringify(a) === stableStringify(b);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/**
|
|
1100
|
+
* Strips leading and trailing slashes from a path.
|
|
1101
|
+
*
|
|
1102
|
+
* `'friend/'` → `'friend'`, `'/group/member/'` → `'group/member'`.
|
|
1103
|
+
*/
|
|
1104
|
+
function normalizePath(path: string): string {
|
|
1105
|
+
return path.replace(/^\/+|\/+$/g, '');
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
/**
|
|
1109
|
+
* Returns the last segment of a slash-delimited path.
|
|
1110
|
+
*/
|
|
1111
|
+
function lastSegment(path: string): string {
|
|
1112
|
+
const parts = path.split('/');
|
|
1113
|
+
return parts[parts.length - 1];
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
/**
|
|
1117
|
+
* Recursively collects all valid protocol path strings from a structure object.
|
|
1118
|
+
*
|
|
1119
|
+
* Given `{ foo: { bar: { $actions: [...] } } }`, returns `Set(['foo', 'foo/bar'])`.
|
|
1120
|
+
* Keys starting with `$` are skipped.
|
|
1121
|
+
*/
|
|
1122
|
+
function collectPaths(
|
|
1123
|
+
structure: Record<string, unknown>,
|
|
1124
|
+
prefix: string = '',
|
|
1125
|
+
): Set<string> {
|
|
1126
|
+
const paths = new Set<string>();
|
|
1127
|
+
|
|
1128
|
+
for (const key of Object.keys(structure)) {
|
|
1129
|
+
if (key.startsWith('$')) { continue; }
|
|
1130
|
+
|
|
1131
|
+
const fullPath = prefix ? `${prefix}/${key}` : key;
|
|
1132
|
+
paths.add(fullPath);
|
|
1133
|
+
|
|
1134
|
+
const child = structure[key];
|
|
1135
|
+
if (child !== null && typeof child === 'object') {
|
|
1136
|
+
for (const nested of collectPaths(child as Record<string, unknown>, fullPath)) {
|
|
1137
|
+
paths.add(nested);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
return paths;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
/**
|
|
1146
|
+
* Deterministic JSON serialization with sorted keys.
|
|
1147
|
+
*/
|
|
1148
|
+
function stableStringify(value: unknown): string {
|
|
1149
|
+
if (value === null || value === undefined || typeof value !== 'object') {
|
|
1150
|
+
return JSON.stringify(value);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
if (Array.isArray(value)) {
|
|
1154
|
+
return '[' + value.map((item) => stableStringify(item)).join(',') + ']';
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
const keys = Object.keys(value as globalThis.Record<string, unknown>).sort();
|
|
1158
|
+
const pairs = keys.map((key) =>
|
|
1159
|
+
JSON.stringify(key) + ':' + stableStringify((value as globalThis.Record<string, unknown>)[key])
|
|
1160
|
+
);
|
|
1161
|
+
return '{' + pairs.join(',') + '}';
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// ---------------------------------------------------------------------------
|
|
1165
|
+
// Deprecated alias — migration aid
|
|
1166
|
+
// ---------------------------------------------------------------------------
|
|
1167
|
+
|
|
1168
|
+
/** @deprecated Use {@link TypedEnbox} instead. Will be removed in a future version. */
|
|
1169
|
+
export const TypedWeb5 = TypedEnbox;
|