@enbox/api 0.2.3 → 0.3.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.
Files changed (73) hide show
  1. package/README.md +235 -35
  2. package/dist/browser.mjs +13 -13
  3. package/dist/browser.mjs.map +4 -4
  4. package/dist/esm/dwn-api.js +24 -10
  5. package/dist/esm/dwn-api.js.map +1 -1
  6. package/dist/esm/index.js +6 -0
  7. package/dist/esm/index.js.map +1 -1
  8. package/dist/esm/live-query.js +34 -5
  9. package/dist/esm/live-query.js.map +1 -1
  10. package/dist/esm/permission-grant.js +3 -6
  11. package/dist/esm/permission-grant.js.map +1 -1
  12. package/dist/esm/permission-request.js +4 -7
  13. package/dist/esm/permission-request.js.map +1 -1
  14. package/dist/esm/record-data.js +131 -0
  15. package/dist/esm/record-data.js.map +1 -0
  16. package/dist/esm/record-types.js +9 -0
  17. package/dist/esm/record-types.js.map +1 -0
  18. package/dist/esm/record.js +58 -184
  19. package/dist/esm/record.js.map +1 -1
  20. package/dist/esm/repository-types.js +13 -0
  21. package/dist/esm/repository-types.js.map +1 -0
  22. package/dist/esm/repository.js +347 -0
  23. package/dist/esm/repository.js.map +1 -0
  24. package/dist/esm/typed-live-query.js +129 -0
  25. package/dist/esm/typed-live-query.js.map +1 -0
  26. package/dist/esm/typed-record.js +227 -0
  27. package/dist/esm/typed-record.js.map +1 -0
  28. package/dist/esm/typed-web5.js +134 -23
  29. package/dist/esm/typed-web5.js.map +1 -1
  30. package/dist/esm/web5.js +83 -22
  31. package/dist/esm/web5.js.map +1 -1
  32. package/dist/types/dwn-api.d.ts.map +1 -1
  33. package/dist/types/index.d.ts +6 -0
  34. package/dist/types/index.d.ts.map +1 -1
  35. package/dist/types/live-query.d.ts +43 -4
  36. package/dist/types/live-query.d.ts.map +1 -1
  37. package/dist/types/permission-grant.d.ts +1 -1
  38. package/dist/types/permission-grant.d.ts.map +1 -1
  39. package/dist/types/permission-request.d.ts +1 -1
  40. package/dist/types/permission-request.d.ts.map +1 -1
  41. package/dist/types/record-data.d.ts +49 -0
  42. package/dist/types/record-data.d.ts.map +1 -0
  43. package/dist/types/record-types.d.ts +145 -0
  44. package/dist/types/record-types.d.ts.map +1 -0
  45. package/dist/types/record.d.ts +13 -144
  46. package/dist/types/record.d.ts.map +1 -1
  47. package/dist/types/repository-types.d.ts +137 -0
  48. package/dist/types/repository-types.d.ts.map +1 -0
  49. package/dist/types/repository.d.ts +59 -0
  50. package/dist/types/repository.d.ts.map +1 -0
  51. package/dist/types/typed-live-query.d.ts +111 -0
  52. package/dist/types/typed-live-query.d.ts.map +1 -0
  53. package/dist/types/typed-record.d.ts +179 -0
  54. package/dist/types/typed-record.d.ts.map +1 -0
  55. package/dist/types/typed-web5.d.ts +55 -24
  56. package/dist/types/typed-web5.d.ts.map +1 -1
  57. package/dist/types/web5.d.ts +54 -4
  58. package/dist/types/web5.d.ts.map +1 -1
  59. package/package.json +8 -7
  60. package/src/dwn-api.ts +30 -13
  61. package/src/index.ts +6 -0
  62. package/src/live-query.ts +71 -7
  63. package/src/permission-grant.ts +2 -3
  64. package/src/permission-request.ts +3 -4
  65. package/src/record-data.ts +155 -0
  66. package/src/record-types.ts +188 -0
  67. package/src/record.ts +86 -389
  68. package/src/repository-types.ts +249 -0
  69. package/src/repository.ts +391 -0
  70. package/src/typed-live-query.ts +200 -0
  71. package/src/typed-record.ts +309 -0
  72. package/src/typed-web5.ts +202 -49
  73. package/src/web5.ts +162 -27
@@ -0,0 +1,200 @@
1
+ /**
2
+ * A type-safe wrapper around {@link LiveQuery} that carries the data type `T`
3
+ * through initial snapshot records and real-time change events.
4
+ *
5
+ * `TypedLiveQuery<T>` wraps a `LiveQuery` so that:
6
+ * - `.records` returns `TypedRecord<T>[]` instead of `Record[]`.
7
+ * - `.on('create', handler)` provides `TypedRecord<T>` in the callback.
8
+ * - `.on('change', handler)` provides `TypedRecordChange<T>` in the callback.
9
+ * - `.on('disconnected' | 'reconnecting' | 'reconnected' | 'eose', handler)`
10
+ * forwards transport lifecycle events from the underlying `LiveQuery`.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * const { liveQuery } = await typed.records.subscribe('friend');
15
+ * // liveQuery is TypedLiveQuery<FriendData>
16
+ *
17
+ * for (const record of liveQuery.records) {
18
+ * const data = await record.data.json(); // FriendData
19
+ * }
20
+ *
21
+ * liveQuery.on('create', async (record) => {
22
+ * const data = await record.data.json(); // FriendData
23
+ * });
24
+ *
25
+ * // Connection lifecycle events
26
+ * liveQuery.on('disconnected', () => showOfflineIndicator());
27
+ * liveQuery.on('reconnected', () => hideOfflineIndicator());
28
+ * liveQuery.on('eose', () => console.log('catch-up complete'));
29
+ * ```
30
+ */
31
+
32
+ import type { PaginationCursor } from '@enbox/dwn-sdk-js';
33
+ import type { LiveQuery, RecordChange } from './live-query.js';
34
+
35
+ import { TypedRecord } from './typed-record.js';
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Typed change event
39
+ // ---------------------------------------------------------------------------
40
+
41
+ /**
42
+ * Describes a change to a record in a {@link TypedLiveQuery}.
43
+ *
44
+ * Same shape as {@link RecordChange} but with the record wrapped
45
+ * in a {@link TypedRecord}.
46
+ */
47
+ export type TypedRecordChange<T> = {
48
+ /** Whether the record was created, updated, or deleted. */
49
+ type: RecordChange['type'];
50
+
51
+ /** The typed record affected by the change. */
52
+ record: TypedRecord<T>;
53
+ };
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // TypedLiveQuery class
57
+ // ---------------------------------------------------------------------------
58
+
59
+ /**
60
+ * A type-safe wrapper around {@link LiveQuery} that preserves the data type `T`
61
+ * through the initial snapshot and all subsequent change events.
62
+ *
63
+ * Obtain instances through `TypedWeb5.records.subscribe()` — never construct
64
+ * directly.
65
+ */
66
+ export class TypedLiveQuery<T> {
67
+ /** The underlying untyped LiveQuery instance. */
68
+ private _liveQuery: LiveQuery;
69
+
70
+ /** Cached typed record array, built lazily from the underlying records. */
71
+ private _typedRecords?: TypedRecord<T>[];
72
+
73
+ constructor(liveQuery: LiveQuery) {
74
+ this._liveQuery = liveQuery;
75
+ }
76
+
77
+ // -------------------------------------------------------------------------
78
+ // Escape hatch
79
+ // -------------------------------------------------------------------------
80
+
81
+ /** Access the underlying untyped {@link LiveQuery} for advanced use cases. */
82
+ public get rawLiveQuery(): LiveQuery {
83
+ return this._liveQuery;
84
+ }
85
+
86
+ // -------------------------------------------------------------------------
87
+ // Typed initial snapshot
88
+ // -------------------------------------------------------------------------
89
+
90
+ /** The initial snapshot of matching records, typed as `TypedRecord<T>[]`. */
91
+ public get records(): TypedRecord<T>[] {
92
+ if (!this._typedRecords) {
93
+ this._typedRecords = this._liveQuery.records.map(
94
+ (record) => new TypedRecord<T>(record),
95
+ );
96
+ }
97
+ return this._typedRecords;
98
+ }
99
+
100
+ /**
101
+ * Pagination cursor for fetching the next page of initial results.
102
+ *
103
+ * `undefined` when there are no more results.
104
+ */
105
+ public get cursor(): PaginationCursor | undefined {
106
+ return this._liveQuery.cursor;
107
+ }
108
+
109
+ /**
110
+ * Whether the transport connection is currently active.
111
+ * Delegates to the underlying `LiveQuery.isConnected`.
112
+ */
113
+ public get isConnected(): boolean {
114
+ return this._liveQuery.isConnected;
115
+ }
116
+
117
+ // -------------------------------------------------------------------------
118
+ // Typed event handlers
119
+ // -------------------------------------------------------------------------
120
+
121
+ /**
122
+ * Register a typed event handler. Returns an unsubscribe function.
123
+ *
124
+ * Supports both record change events (`change`, `create`, `update`, `delete`)
125
+ * and transport lifecycle events (`disconnected`, `reconnecting`,
126
+ * `reconnected`, `eose`).
127
+ *
128
+ * @param event - The event type to listen for.
129
+ * @param handler - The handler function with typed record(s) or lifecycle data.
130
+ * @returns A function that removes the handler when called.
131
+ *
132
+ * @example
133
+ * ```ts
134
+ * const off = liveQuery.on('create', (record) => {
135
+ * // record is TypedRecord<T>
136
+ * });
137
+ * off(); // stop listening
138
+ *
139
+ * liveQuery.on('disconnected', () => showOfflineIndicator());
140
+ * liveQuery.on('reconnected', () => hideOfflineIndicator());
141
+ * ```
142
+ */
143
+ public on(event: 'change', handler: (change: TypedRecordChange<T>) => void): () => void;
144
+ public on(event: 'create', handler: (record: TypedRecord<T>) => void): () => void;
145
+ public on(event: 'update', handler: (record: TypedRecord<T>) => void): () => void;
146
+ public on(event: 'delete', handler: (record: TypedRecord<T>) => void): () => void;
147
+ public on(event: 'disconnected', handler: () => void): () => void;
148
+ public on(event: 'reconnecting', handler: (detail: { attempt: number }) => void): () => void;
149
+ public on(event: 'reconnected', handler: () => void): () => void;
150
+ public on(event: 'eose', handler: () => void): () => void;
151
+ public on(
152
+ event: 'change' | 'create' | 'update' | 'delete' | 'disconnected' | 'reconnecting' | 'reconnected' | 'eose',
153
+ handler: ((change: TypedRecordChange<T>) => void)
154
+ | ((record: TypedRecord<T>) => void)
155
+ | ((detail: { attempt: number }) => void)
156
+ | (() => void),
157
+ ): () => void {
158
+ // Lifecycle events: delegate directly to the underlying LiveQuery.
159
+ if (event === 'disconnected') {
160
+ return this._liveQuery.on('disconnected', handler as () => void);
161
+ }
162
+ if (event === 'reconnected') {
163
+ return this._liveQuery.on('reconnected', handler as () => void);
164
+ }
165
+ if (event === 'eose') {
166
+ return this._liveQuery.on('eose', handler as () => void);
167
+ }
168
+ if (event === 'reconnecting') {
169
+ return this._liveQuery.on('reconnecting', handler as (detail: { attempt: number }) => void);
170
+ }
171
+
172
+ // Record change events: wrap records in TypedRecord.
173
+ if (event === 'change') {
174
+ return this._liveQuery.on('change', (change: RecordChange): void => {
175
+ (handler as (change: TypedRecordChange<T>) => void)({
176
+ type : change.type,
177
+ record : new TypedRecord<T>(change.record),
178
+ });
179
+ });
180
+ }
181
+
182
+ // For create/update/delete, the underlying LiveQuery handler receives a Record.
183
+ // Cast event to 'create' to satisfy the overload resolver — the actual value
184
+ // is always one of 'create' | 'update' | 'delete' at this point.
185
+ return this._liveQuery.on(event as 'create', (record): void => {
186
+ (handler as (record: TypedRecord<T>) => void)(new TypedRecord<T>(record));
187
+ });
188
+ }
189
+
190
+ // -------------------------------------------------------------------------
191
+ // Lifecycle
192
+ // -------------------------------------------------------------------------
193
+
194
+ /**
195
+ * Close the underlying subscription and stop dispatching events.
196
+ */
197
+ public async close(): Promise<void> {
198
+ return this._liveQuery.close();
199
+ }
200
+ }
@@ -0,0 +1,309 @@
1
+ /**
2
+ * A type-safe wrapper around {@link Record} that carries the data type `T`
3
+ * through its entire lifecycle — from write to read, query, update, and
4
+ * subscribe.
5
+ *
6
+ * `TypedRecord<T>` uses composition (not inheritance) to wrap the underlying
7
+ * untyped `Record` class. All read-only getters and lifecycle methods are
8
+ * forwarded, while data-access and mutation methods are enhanced with the
9
+ * generic `T`:
10
+ *
11
+ * - `.data.json()` returns `Promise<T>` instead of `Promise<unknown>`.
12
+ * - `.update({ data })` accepts `Partial<T>` for the data payload.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * const { record } = await typed.records.write('friend', {
17
+ * data: { did: 'did:example:alice', alias: 'Alice' },
18
+ * });
19
+ *
20
+ * // record is TypedRecord<FriendData>
21
+ * const data = await record.data.json(); // FriendData — no manual cast
22
+ * ```
23
+ */
24
+
25
+ import type { Record } from './record.js';
26
+ import type { RecordData } from './record-data.js';
27
+ import type { DwnDateSort, DwnMessage, DwnPaginationCursor, DwnResponseStatus } from '@enbox/agent';
28
+ import type { RecordDeleteParams, RecordModel, RecordUpdateParams } from './record-types.js';
29
+
30
+ import type { DwnInterface } from '@enbox/agent';
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Typed data accessor
34
+ // ---------------------------------------------------------------------------
35
+
36
+ /**
37
+ * A data accessor that preserves the record's type parameter `T` on
38
+ * the `json()` method.
39
+ *
40
+ * All other methods (`blob`, `bytes`, `text`, `stream`, `then`, `catch`)
41
+ * are forwarded unchanged from the underlying {@link RecordData}.
42
+ */
43
+ export type TypedRecordData<T> = Omit<RecordData, 'json'> & {
44
+ /** Parse the data as JSON, returning the typed data shape `T`. */
45
+ json: () => Promise<T>;
46
+ };
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Typed update params
50
+ // ---------------------------------------------------------------------------
51
+
52
+ /**
53
+ * Update parameters for a {@link TypedRecord}.
54
+ *
55
+ * Extends the base `RecordUpdateParams` but narrows `data` from `unknown`
56
+ * to `Partial<T>`.
57
+ */
58
+ export type TypedRecordUpdateParams<T> = Omit<RecordUpdateParams, 'data'> & {
59
+ /** The new data for the record. Type-checked against the schema map. */
60
+ data?: Partial<T>;
61
+ };
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Typed update / delete results
65
+ // ---------------------------------------------------------------------------
66
+
67
+ /**
68
+ * Result of a {@link TypedRecord.update} operation.
69
+ */
70
+ export type TypedRecordUpdateResult<T> = DwnResponseStatus & {
71
+ /** The updated record, carrying the same type parameter. */
72
+ record: TypedRecord<T>;
73
+ };
74
+
75
+ /**
76
+ * Result of a {@link TypedRecord.delete} operation.
77
+ */
78
+ export type TypedRecordDeleteResult<T> = DwnResponseStatus & {
79
+ /** The deleted record, carrying the same type parameter. */
80
+ record: TypedRecord<T>;
81
+ };
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // TypedRecord class
85
+ // ---------------------------------------------------------------------------
86
+
87
+ /**
88
+ * A type-safe wrapper around {@link Record} that preserves the data type `T`.
89
+ *
90
+ * Obtain instances through `TypedWeb5.records.write()`, `.query()`, `.read()`,
91
+ * or `.subscribe()` — never construct directly.
92
+ */
93
+ export class TypedRecord<T> {
94
+ /** The underlying untyped Record instance. */
95
+ private _record: Record;
96
+
97
+ constructor(record: Record) {
98
+ this._record = record;
99
+ }
100
+
101
+ // -------------------------------------------------------------------------
102
+ // Escape hatch
103
+ // -------------------------------------------------------------------------
104
+
105
+ /** Access the underlying untyped {@link Record} for advanced use cases. */
106
+ public get rawRecord(): Record {
107
+ return this._record;
108
+ }
109
+
110
+ // -------------------------------------------------------------------------
111
+ // Typed data accessor
112
+ // -------------------------------------------------------------------------
113
+
114
+ /**
115
+ * Returns the data of the current record with type-safe accessors.
116
+ *
117
+ * The `json()` method returns `Promise<T>` — no manual generic needed.
118
+ *
119
+ * @throws `Error` if the record has been deleted.
120
+ */
121
+ public get data(): TypedRecordData<T> {
122
+ const underlying = this._record.data;
123
+ return {
124
+ blob : (): Promise<Blob> => underlying.blob(),
125
+ bytes : (): Promise<Uint8Array> => underlying.bytes(),
126
+ json : (): Promise<T> => underlying.json<T>(),
127
+ text : (): Promise<string> => underlying.text(),
128
+ stream : (): Promise<ReadableStream> => underlying.stream(),
129
+ then : underlying.then.bind(underlying),
130
+ catch : underlying.catch.bind(underlying),
131
+ };
132
+ }
133
+
134
+ // -------------------------------------------------------------------------
135
+ // Typed mutation methods
136
+ // -------------------------------------------------------------------------
137
+
138
+ /**
139
+ * Update the current record on the DWN.
140
+ *
141
+ * @param params - Parameters including the typed `data` payload.
142
+ * @returns The status and an updated {@link TypedRecord}.
143
+ * @throws `Error` if the record has been deleted.
144
+ */
145
+ public async update(params: TypedRecordUpdateParams<T>): Promise<TypedRecordUpdateResult<T>> {
146
+ const { status, record } = await this._record.update(params as RecordUpdateParams);
147
+ return { status, record: new TypedRecord<T>(record) };
148
+ }
149
+
150
+ /**
151
+ * Delete the current record on the DWN.
152
+ *
153
+ * @param params - Delete parameters.
154
+ * @returns The status and a {@link TypedRecord} reflecting the deleted state.
155
+ */
156
+ public async delete(params?: RecordDeleteParams): Promise<TypedRecordDeleteResult<T>> {
157
+ const { status, record } = await this._record.delete(params);
158
+ return { status, record: new TypedRecord<T>(record) };
159
+ }
160
+
161
+ // -------------------------------------------------------------------------
162
+ // Forwarded lifecycle methods
163
+ // -------------------------------------------------------------------------
164
+
165
+ /**
166
+ * Stores the current record state to the owner's DWN.
167
+ *
168
+ * @param importRecord - If true, sign as owner before storing. Defaults to false.
169
+ */
170
+ public async store(importRecord: boolean = false): Promise<DwnResponseStatus> {
171
+ return this._record.store(importRecord);
172
+ }
173
+
174
+ /**
175
+ * Signs and optionally stores the record to the owner's DWN.
176
+ * Useful when importing a record signed by someone else.
177
+ *
178
+ * @param store - If true, store after signing. Defaults to true.
179
+ */
180
+ public async import(store: boolean = true): Promise<DwnResponseStatus> {
181
+ return this._record.import(store);
182
+ }
183
+
184
+ /**
185
+ * Send the current record to a remote DWN.
186
+ *
187
+ * @param target - Optional DID of the target DWN. Defaults to the connected DID.
188
+ */
189
+ public async send(target?: string): Promise<DwnResponseStatus> {
190
+ return this._record.send(target);
191
+ }
192
+
193
+ /**
194
+ * Returns a JSON representation of the Record instance.
195
+ */
196
+ public toJSON(): RecordModel {
197
+ return this._record.toJSON();
198
+ }
199
+
200
+ /**
201
+ * Returns a string representation of the Record instance.
202
+ */
203
+ public toString(): string {
204
+ return this._record.toString();
205
+ }
206
+
207
+ /**
208
+ * Returns a pagination cursor for the current record given a sort order.
209
+ */
210
+ public async paginationCursor(sort: DwnDateSort): Promise<DwnPaginationCursor | undefined> {
211
+ return this._record.paginationCursor(sort);
212
+ }
213
+
214
+ // -------------------------------------------------------------------------
215
+ // Forwarded immutable property getters
216
+ // -------------------------------------------------------------------------
217
+
218
+ /** Record's ID. */
219
+ public get id(): string { return this._record.id; }
220
+
221
+ /** Record's context ID. */
222
+ public get contextId(): string | undefined { return this._record.contextId; }
223
+
224
+ /** Record's creation date. */
225
+ public get dateCreated(): string { return this._record.dateCreated; }
226
+
227
+ /** Record's parent ID. */
228
+ public get parentId(): string | undefined { return this._record.parentId; }
229
+
230
+ /** Record's protocol. */
231
+ public get protocol(): string | undefined { return this._record.protocol; }
232
+
233
+ /** Record's protocol path. */
234
+ public get protocolPath(): string | undefined { return this._record.protocolPath; }
235
+
236
+ /** Record's recipient. */
237
+ public get recipient(): string | undefined { return this._record.recipient; }
238
+
239
+ /** Record's schema. */
240
+ public get schema(): string | undefined { return this._record.schema; }
241
+
242
+ // -------------------------------------------------------------------------
243
+ // Forwarded mutable property getters
244
+ // -------------------------------------------------------------------------
245
+
246
+ /** Record's data format. */
247
+ public get dataFormat(): string | undefined { return this._record.dataFormat; }
248
+
249
+ /** Record's data CID. */
250
+ public get dataCid(): string | undefined { return this._record.dataCid; }
251
+
252
+ /** Record's data size. */
253
+ public get dataSize(): number | undefined { return this._record.dataSize; }
254
+
255
+ /** Record's published date. */
256
+ public get datePublished(): string | undefined { return this._record.datePublished; }
257
+
258
+ /** Record's published status. */
259
+ public get published(): boolean | undefined { return this._record.published; }
260
+
261
+ /** Tags of the record. */
262
+ public get tags(): DwnMessage[DwnInterface.RecordsWrite]['descriptor']['tags'] | undefined {
263
+ return this._record.tags;
264
+ }
265
+
266
+ // -------------------------------------------------------------------------
267
+ // Forwarded state-dependent property getters
268
+ // -------------------------------------------------------------------------
269
+
270
+ /** DID that is the logical author of the Record. */
271
+ public get author(): string { return this._record.author; }
272
+
273
+ /** DID that is the original creator of the Record. */
274
+ public get creator(): string { return this._record.creator; }
275
+
276
+ /** Record's message timestamp. */
277
+ public get timestamp(): string { return this._record.timestamp; }
278
+
279
+ /** Record's encryption details. */
280
+ public get encryption(): DwnMessage[DwnInterface.RecordsWrite]['encryption'] {
281
+ return this._record.encryption;
282
+ }
283
+
284
+ /** Record's authorization. */
285
+ public get authorization(): DwnMessage[DwnInterface.RecordsWrite | DwnInterface.RecordsDelete]['authorization'] {
286
+ return this._record.authorization;
287
+ }
288
+
289
+ /** Record's attestation. */
290
+ public get attestation(): DwnMessage[DwnInterface.RecordsWrite]['attestation'] | undefined {
291
+ return this._record.attestation;
292
+ }
293
+
294
+ /** Role under which the author is writing the record. */
295
+ public get protocolRole(): string | undefined { return this._record.protocolRole; }
296
+
297
+ /** Record's deleted state. */
298
+ public get deleted(): boolean { return this._record.deleted; }
299
+
300
+ /** Record's initial write if the record has been updated. */
301
+ public get initialWrite(): DwnMessage[DwnInterface.RecordsWrite] | undefined {
302
+ return this._record.initialWrite;
303
+ }
304
+
305
+ /** The raw DWN message backing this record. */
306
+ public get rawMessage(): DwnMessage[DwnInterface.RecordsWrite] | DwnMessage[DwnInterface.RecordsDelete] {
307
+ return this._record.rawMessage;
308
+ }
309
+ }