@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,249 @@
1
+ /**
2
+ * Type-level machinery for the protocol repository pattern.
3
+ *
4
+ * These types produce a statically-typed CRUD API shape from a
5
+ * `ProtocolDefinition` + `SchemaMap`, with singleton detection
6
+ * (via `$recordLimit: { max: 1 }`) and nested-path awareness.
7
+ *
8
+ * All types are purely compile-time — they produce no runtime code.
9
+ *
10
+ * @module
11
+ */
12
+
13
+ import type { SchemaMap } from './protocol-types.js';
14
+ import type { TypedLiveQuery } from './typed-live-query.js';
15
+ import type { TypedRecord } from './typed-record.js';
16
+ import type {
17
+ DataForPath,
18
+ TypedCreateRequest,
19
+ TypedQueryRequest,
20
+ TypedSubscribeRequest,
21
+ } from './typed-web5.js';
22
+ import type { DwnPaginationCursor, DwnResponseStatus } from '@enbox/agent';
23
+ import type { ProtocolDefinition, ProtocolRuleSet } from '@enbox/dwn-sdk-js';
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Structure navigation
27
+ // ---------------------------------------------------------------------------
28
+
29
+ /**
30
+ * Navigates a `ProtocolRuleSet` tree to the node at a slash-delimited path.
31
+ */
32
+ type RuleSetAtPath<R, Path extends string> =
33
+ Path extends `${infer Head}/${infer Tail}`
34
+ ? Head extends keyof R
35
+ ? R[Head] extends ProtocolRuleSet
36
+ ? RuleSetAtPath<R[Head], Tail>
37
+ : never
38
+ : never
39
+ : Path extends keyof R
40
+ ? R[Path] extends ProtocolRuleSet
41
+ ? R[Path]
42
+ : never
43
+ : never;
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Singleton detection
47
+ // ---------------------------------------------------------------------------
48
+
49
+ /**
50
+ * Extracts the `$recordLimit` from a rule set node, if present.
51
+ */
52
+ type RecordLimitAtRuleSet<RS> =
53
+ RS extends { $recordLimit: infer L } ? L : never;
54
+
55
+ /**
56
+ * `true` when the rule set at `Path` has `$recordLimit: { max: 1 }`.
57
+ */
58
+ export type IsSingleton<D extends ProtocolDefinition, Path extends string> =
59
+ RecordLimitAtRuleSet<RuleSetAtPath<D['structure'], Path>> extends { max: 1 }
60
+ ? true
61
+ : false;
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Data type resolution
65
+ // ---------------------------------------------------------------------------
66
+
67
+ /** Resolves the TypeScript data type for a given path. */
68
+ type DataAt<
69
+ D extends ProtocolDefinition,
70
+ M extends SchemaMap,
71
+ Path extends string,
72
+ > = DataForPath<D, M, Path>;
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // Common option types
76
+ // ---------------------------------------------------------------------------
77
+
78
+ /**
79
+ * Write options for a collection `create()` call.
80
+ * Omits `data` (passed separately) and protocol-injected fields.
81
+ */
82
+ export type CollectionCreateOptions<
83
+ D extends ProtocolDefinition,
84
+ M extends SchemaMap,
85
+ Path extends string,
86
+ > = Omit<TypedCreateRequest<D, M, Path>, 'data' | 'parentContextId'> & {
87
+ data: DataAt<D, M, Path>;
88
+ };
89
+
90
+ /**
91
+ * Write options for a singleton `set()` call.
92
+ */
93
+ export type SingletonSetOptions<
94
+ D extends ProtocolDefinition,
95
+ M extends SchemaMap,
96
+ Path extends string,
97
+ > = Omit<TypedCreateRequest<D, M, Path>, 'data' | 'parentContextId'> & {
98
+ data: DataAt<D, M, Path>;
99
+ };
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // CRUD shapes
103
+ // ---------------------------------------------------------------------------
104
+
105
+ /** CRUD API for a root-level collection (unbounded or max > 1). */
106
+ export type CollectionCRUD<
107
+ D extends ProtocolDefinition,
108
+ M extends SchemaMap,
109
+ Path extends string,
110
+ > = {
111
+ create(options: CollectionCreateOptions<D, M, Path>): Promise<DwnResponseStatus & { record: TypedRecord<DataAt<D, M, Path>> }>;
112
+ query(options?: TypedQueryRequest): Promise<DwnResponseStatus & { records: TypedRecord<DataAt<D, M, Path>>[]; cursor?: DwnPaginationCursor }>;
113
+ get(recordId: string): Promise<TypedRecord<DataAt<D, M, Path>>>;
114
+ delete(recordId: string): Promise<DwnResponseStatus>;
115
+ subscribe(options?: TypedSubscribeRequest): Promise<TypedLiveQuery<DataAt<D, M, Path>> | undefined>;
116
+ };
117
+
118
+ /** CRUD API for a root-level singleton ($recordLimit max: 1). */
119
+ export type SingletonCRUD<
120
+ D extends ProtocolDefinition,
121
+ M extends SchemaMap,
122
+ Path extends string,
123
+ > = {
124
+ set(options: SingletonSetOptions<D, M, Path>): Promise<DwnResponseStatus & { record: TypedRecord<DataAt<D, M, Path>> }>;
125
+ get(): Promise<TypedRecord<DataAt<D, M, Path>> | undefined>;
126
+ delete(recordId: string): Promise<DwnResponseStatus>;
127
+ };
128
+
129
+ /** CRUD API for a nested collection (parent context required). */
130
+ export type NestedCollectionCRUD<
131
+ D extends ProtocolDefinition,
132
+ M extends SchemaMap,
133
+ Path extends string,
134
+ > = {
135
+ create(
136
+ parentContextId: string,
137
+ options: CollectionCreateOptions<D, M, Path>,
138
+ ): Promise<DwnResponseStatus & { record: TypedRecord<DataAt<D, M, Path>> }>;
139
+ query(
140
+ parentContextId: string,
141
+ options?: TypedQueryRequest,
142
+ ): Promise<DwnResponseStatus & { records: TypedRecord<DataAt<D, M, Path>>[]; cursor?: DwnPaginationCursor }>;
143
+ get(recordId: string): Promise<TypedRecord<DataAt<D, M, Path>>>;
144
+ delete(recordId: string): Promise<DwnResponseStatus>;
145
+ subscribe(
146
+ parentContextId: string,
147
+ options?: TypedSubscribeRequest,
148
+ ): Promise<TypedLiveQuery<DataAt<D, M, Path>> | undefined>;
149
+ };
150
+
151
+ /** CRUD API for a nested singleton ($recordLimit max: 1). */
152
+ export type NestedSingletonCRUD<
153
+ D extends ProtocolDefinition,
154
+ M extends SchemaMap,
155
+ Path extends string,
156
+ > = {
157
+ set(
158
+ parentContextId: string,
159
+ options: SingletonSetOptions<D, M, Path>,
160
+ ): Promise<DwnResponseStatus & { record: TypedRecord<DataAt<D, M, Path>> }>;
161
+ get(parentContextId: string): Promise<TypedRecord<DataAt<D, M, Path>> | undefined>;
162
+ delete(recordId: string): Promise<DwnResponseStatus>;
163
+ };
164
+
165
+ // ---------------------------------------------------------------------------
166
+ // Recursive repository node
167
+ // ---------------------------------------------------------------------------
168
+
169
+ /**
170
+ * Extracts the child type names (non-`$`-prefixed keys that extend ProtocolRuleSet)
171
+ * from a rule set node.
172
+ */
173
+ type ChildKeys<RS> = {
174
+ [K in Extract<keyof RS, string>]: K extends `$${string}`
175
+ ? never
176
+ : RS[K] extends ProtocolRuleSet
177
+ ? K
178
+ : never;
179
+ }[Extract<keyof RS, string>];
180
+
181
+ /**
182
+ * Builds nested repository nodes for all children of a given rule set.
183
+ */
184
+ type ChildNodes<
185
+ D extends ProtocolDefinition,
186
+ M extends SchemaMap,
187
+ RS,
188
+ ParentPath extends string,
189
+ > = {
190
+ [K in ChildKeys<RS>]: RepositoryNode<D, M, RS[K], `${ParentPath}/${K}`, true>;
191
+ };
192
+
193
+ /**
194
+ * A single node in the repository tree. Provides CRUD methods appropriate
195
+ * to whether the path is root/nested and singleton/collection, plus child
196
+ * nodes for further nesting.
197
+ */
198
+ export type RepositoryNode<
199
+ D extends ProtocolDefinition,
200
+ M extends SchemaMap,
201
+ RS,
202
+ Path extends string,
203
+ IsNested extends boolean = false,
204
+ > =
205
+ // Choose CRUD shape based on singleton + nested status
206
+ (IsNested extends true
207
+ ? (IsSingleton<D, Path> extends true
208
+ ? NestedSingletonCRUD<D, M, Path>
209
+ : NestedCollectionCRUD<D, M, Path>)
210
+ : (IsSingleton<D, Path> extends true
211
+ ? SingletonCRUD<D, M, Path>
212
+ : CollectionCRUD<D, M, Path>))
213
+ // Plus child nodes for deeper nesting
214
+ & ChildNodes<D, M, RS, Path>;
215
+
216
+ // ---------------------------------------------------------------------------
217
+ // Top-level repository type
218
+ // ---------------------------------------------------------------------------
219
+
220
+ /**
221
+ * The top-level repository type for a protocol definition.
222
+ *
223
+ * Maps each root-level type name in the protocol's `structure` to a
224
+ * `RepositoryNode` with the appropriate CRUD methods and nested children.
225
+ *
226
+ * @example
227
+ * ```ts
228
+ * const social: Repository<typeof SocialGraphDef, SocialGraphSchemaMap>;
229
+ *
230
+ * // Root collection
231
+ * await social.friend.create({ data: { did: '...' } });
232
+ * await social.friend.query();
233
+ *
234
+ * // Nested under group
235
+ * await social.group.member.create(groupCtxId, { data: { did: '...' } });
236
+ * ```
237
+ */
238
+ export type Repository<
239
+ D extends ProtocolDefinition,
240
+ M extends SchemaMap,
241
+ > = {
242
+ [K in Extract<keyof D['structure'], string> as K extends `$${string}` ? never : K]:
243
+ D['structure'][K] extends ProtocolRuleSet
244
+ ? RepositoryNode<D, M, D['structure'][K], K>
245
+ : never;
246
+ } & {
247
+ /** Install (configure) the protocol. Idempotent — no-op if already installed. */
248
+ configure(options?: { encryption?: boolean }): Promise<DwnResponseStatus>;
249
+ };
@@ -0,0 +1,391 @@
1
+ /**
2
+ * Protocol-aware repository factory.
3
+ *
4
+ * `repository()` takes a `TypedWeb5` instance and returns a Proxy-backed
5
+ * object whose shape mirrors the protocol's `structure` tree with
6
+ * ergonomic CRUD methods on each node.
7
+ *
8
+ * - **Collections** (default): `create`, `query`, `get`, `delete`, `subscribe`
9
+ * - **Singletons** (`$recordLimit: { max: 1 }`): `set`, `get`, `delete`
10
+ * - **Nested types**: first argument is `parentContextId`
11
+ * - **`configure()`**: idempotent protocol installation
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * const social = repository(web5.using(SocialGraphProtocol));
16
+ * await social.configure();
17
+ *
18
+ * // Root collection
19
+ * const { record } = await social.friend.create({
20
+ * data: { did: 'did:example:alice' },
21
+ * });
22
+ *
23
+ * // Nested under group
24
+ * const members = await social.group.member.query(groupContextId);
25
+ * ```
26
+ *
27
+ * @module
28
+ */
29
+
30
+ import type { DwnResponseStatus } from '@enbox/agent';
31
+ import type { Repository } from './repository-types.js';
32
+ import type { SchemaMap } from './protocol-types.js';
33
+ import type { TypedWeb5 } from './typed-web5.js';
34
+ import type { ProtocolDefinition, ProtocolRuleSet } from '@enbox/dwn-sdk-js';
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Runtime helpers
38
+ // ---------------------------------------------------------------------------
39
+
40
+ /**
41
+ * Checks whether a DWN response status indicates that the record limit
42
+ * has been exceeded. The DWN engine returns a 400 status with a detail
43
+ * message containing "record limit" when `$recordLimit` rejects a write.
44
+ */
45
+ function isRecordLimitExceeded(status: { code: number; detail: string }): boolean {
46
+ return status.code === 400 && status.detail.includes('record limit');
47
+ }
48
+
49
+ /**
50
+ * Checks whether a protocol rule set at a given path is a singleton
51
+ * (has `$recordLimit: { max: 1 }`).
52
+ */
53
+ function isSingletonPath(definition: ProtocolDefinition, path: string): boolean {
54
+ const segments = path.split('/');
55
+ let node: ProtocolRuleSet | undefined = definition.structure as unknown as ProtocolRuleSet;
56
+
57
+ for (const seg of segments) {
58
+ if (!node || typeof node !== 'object') { return false; }
59
+ node = (node as Record<string, ProtocolRuleSet>)[seg];
60
+ }
61
+
62
+ if (!node || typeof node !== 'object') { return false; }
63
+ const limit = (node as Record<string, unknown>)['$recordLimit'];
64
+ return limit !== undefined
65
+ && typeof limit === 'object'
66
+ && limit !== null
67
+ && (limit as Record<string, unknown>)['max'] === 1;
68
+ }
69
+
70
+ /**
71
+ * Returns the child type keys (non-`$`-prefixed) of a rule set node
72
+ * reached by the given path.
73
+ */
74
+ function getChildKeys(definition: ProtocolDefinition, path: string): string[] {
75
+ const segments = path.split('/');
76
+ let node: Record<string, unknown> = definition.structure as unknown as Record<string, unknown>;
77
+
78
+ for (const seg of segments) {
79
+ if (!node || typeof node !== 'object') { return []; }
80
+ node = node[seg] as Record<string, unknown>;
81
+ }
82
+
83
+ if (!node || typeof node !== 'object') { return []; }
84
+ return Object.keys(node).filter((k) => !k.startsWith('$'));
85
+ }
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // CRUD method builders
89
+ // ---------------------------------------------------------------------------
90
+
91
+ /**
92
+ * Build collection CRUD methods for a root-level path.
93
+ */
94
+ function buildRootCollectionMethods(
95
+ typed: TypedWeb5<ProtocolDefinition, SchemaMap>,
96
+ path: string,
97
+ ): Record<string, Function> {
98
+ return {
99
+ async create(options: Record<string, unknown>): Promise<unknown> {
100
+ const { status, record } = await typed.records.create(path, options as never);
101
+ return { status, record };
102
+ },
103
+
104
+ async query(options?: Record<string, unknown>): Promise<unknown> {
105
+ const { status, records, cursor } = await typed.records.query(path, options as never);
106
+ return { status, records, cursor };
107
+ },
108
+
109
+ async get(recordId: string): Promise<unknown> {
110
+ const { record } = await typed.records.read(path, {
111
+ filter: { recordId },
112
+ });
113
+ return record;
114
+ },
115
+
116
+ async delete(recordId: string): Promise<DwnResponseStatus> {
117
+ return typed.records.delete(path, { recordId });
118
+ },
119
+
120
+ async subscribe(options?: Record<string, unknown>): Promise<unknown> {
121
+ const { liveQuery } = await typed.records.subscribe(path, options as never);
122
+ return liveQuery;
123
+ },
124
+ };
125
+ }
126
+
127
+ /**
128
+ * Build singleton CRUD methods for a root-level path.
129
+ *
130
+ * Uses a create-first strategy: attempts to create a new record, and if the
131
+ * DWN rejects it because the record limit is reached, falls back to querying
132
+ * the existing record and updating it. This avoids the race condition inherent
133
+ * in query-then-create/update.
134
+ */
135
+ function buildRootSingletonMethods(
136
+ typed: TypedWeb5<ProtocolDefinition, SchemaMap>,
137
+ path: string,
138
+ ): Record<string, Function> {
139
+ return {
140
+ async set(options: Record<string, unknown>): Promise<unknown> {
141
+ // Attempt to create — if limit not yet reached, this succeeds.
142
+ const createResult = await typed.records.create(path, options as never);
143
+
144
+ if (isRecordLimitExceeded(createResult.status)) {
145
+ // Record limit hit — query existing and update.
146
+ const { records } = await typed.records.query(path);
147
+ if (records.length > 0) {
148
+ const { status, record } = await records[0].update({
149
+ data: options.data,
150
+ ...(options.tags !== undefined ? { tags: options.tags } : {}),
151
+ } as never);
152
+ return { status, record };
153
+ }
154
+ }
155
+
156
+ return { status: createResult.status, record: createResult.record };
157
+ },
158
+
159
+ async get(): Promise<unknown> {
160
+ const { records } = await typed.records.query(path);
161
+ return records.length > 0 ? records[0] : undefined;
162
+ },
163
+
164
+ async delete(recordId: string): Promise<DwnResponseStatus> {
165
+ return typed.records.delete(path, { recordId });
166
+ },
167
+ };
168
+ }
169
+
170
+ /**
171
+ * Build collection CRUD methods for a nested path.
172
+ */
173
+ function buildNestedCollectionMethods(
174
+ typed: TypedWeb5<ProtocolDefinition, SchemaMap>,
175
+ path: string,
176
+ ): Record<string, Function> {
177
+ return {
178
+ async create(parentContextId: string, options: Record<string, unknown>): Promise<unknown> {
179
+ const { status, record } = await typed.records.create(path, {
180
+ ...options,
181
+ parentContextId,
182
+ } as never);
183
+ return { status, record };
184
+ },
185
+
186
+ async query(parentContextId: string, options?: Record<string, unknown>): Promise<unknown> {
187
+ const { status, records, cursor } = await typed.records.query(path, {
188
+ ...options,
189
+ filter: {
190
+ ...(options as Record<string, Record<string, unknown>>)?.filter,
191
+ contextId: parentContextId,
192
+ },
193
+ } as never);
194
+ return { status, records, cursor };
195
+ },
196
+
197
+ async get(recordId: string): Promise<unknown> {
198
+ const { record } = await typed.records.read(path, {
199
+ filter: { recordId },
200
+ });
201
+ return record;
202
+ },
203
+
204
+ async delete(recordId: string): Promise<DwnResponseStatus> {
205
+ return typed.records.delete(path, { recordId });
206
+ },
207
+
208
+ async subscribe(parentContextId: string, options?: Record<string, unknown>): Promise<unknown> {
209
+ const { liveQuery } = await typed.records.subscribe(path, {
210
+ ...options,
211
+ filter: {
212
+ ...(options as Record<string, Record<string, unknown>>)?.filter,
213
+ contextId: parentContextId,
214
+ },
215
+ } as never);
216
+ return liveQuery;
217
+ },
218
+ };
219
+ }
220
+
221
+ /**
222
+ * Build singleton CRUD methods for a nested path.
223
+ *
224
+ * Uses the same create-first strategy as root singletons to avoid race
225
+ * conditions between concurrent set() calls.
226
+ */
227
+ function buildNestedSingletonMethods(
228
+ typed: TypedWeb5<ProtocolDefinition, SchemaMap>,
229
+ path: string,
230
+ ): Record<string, Function> {
231
+ return {
232
+ async set(parentContextId: string, options: Record<string, unknown>): Promise<unknown> {
233
+ // Attempt to create under this parent — succeeds if no record exists yet.
234
+ const createResult = await typed.records.create(path, {
235
+ ...options,
236
+ parentContextId,
237
+ } as never);
238
+
239
+ if (isRecordLimitExceeded(createResult.status)) {
240
+ // Record limit hit — query existing under this parent and update.
241
+ const { records } = await typed.records.query(path, {
242
+ filter: { contextId: parentContextId },
243
+ } as never);
244
+
245
+ if (records.length > 0) {
246
+ const { status, record } = await records[0].update({
247
+ data: options.data,
248
+ ...(options.tags !== undefined ? { tags: options.tags } : {}),
249
+ } as never);
250
+ return { status, record };
251
+ }
252
+ }
253
+
254
+ return { status: createResult.status, record: createResult.record };
255
+ },
256
+
257
+ async get(parentContextId: string): Promise<unknown> {
258
+ const { records } = await typed.records.query(path, {
259
+ filter: { contextId: parentContextId },
260
+ } as never);
261
+ return records.length > 0 ? records[0] : undefined;
262
+ },
263
+
264
+ async delete(recordId: string): Promise<DwnResponseStatus> {
265
+ return typed.records.delete(path, { recordId });
266
+ },
267
+ };
268
+ }
269
+
270
+ // ---------------------------------------------------------------------------
271
+ // Node builder
272
+ // ---------------------------------------------------------------------------
273
+
274
+ /**
275
+ * Build a repository node for a given path, including CRUD methods
276
+ * and Proxy-based child nodes.
277
+ */
278
+ function buildNode(
279
+ typed: TypedWeb5<ProtocolDefinition, SchemaMap>,
280
+ definition: ProtocolDefinition,
281
+ path: string,
282
+ isNested: boolean,
283
+ ): Record<string, unknown> {
284
+ const singleton = isSingletonPath(definition, path);
285
+
286
+ // Build CRUD methods based on root/nested and singleton/collection
287
+ let methods: Record<string, Function>;
288
+ if (isNested) {
289
+ methods = singleton
290
+ ? buildNestedSingletonMethods(typed, path)
291
+ : buildNestedCollectionMethods(typed, path);
292
+ } else {
293
+ methods = singleton
294
+ ? buildRootSingletonMethods(typed, path)
295
+ : buildRootCollectionMethods(typed, path);
296
+ }
297
+
298
+ // Use a Proxy to lazily build child nodes
299
+ const childKeys = getChildKeys(definition, path);
300
+ const childCache: Record<string, unknown> = {};
301
+
302
+ return new Proxy(methods, {
303
+ get(target: Record<string, unknown>, prop: string | symbol): unknown {
304
+ if (typeof prop !== 'string') { return undefined; }
305
+
306
+ // CRUD methods take priority
307
+ if (prop in target) { return target[prop]; }
308
+
309
+ // Lazily build child nodes
310
+ if (childKeys.includes(prop)) {
311
+ if (!(prop in childCache)) {
312
+ childCache[prop] = buildNode(typed, definition, `${path}/${prop}`, true);
313
+ }
314
+ return childCache[prop];
315
+ }
316
+
317
+ return undefined;
318
+ },
319
+ });
320
+ }
321
+
322
+ // ---------------------------------------------------------------------------
323
+ // Public API
324
+ // ---------------------------------------------------------------------------
325
+
326
+ /**
327
+ * Creates a protocol-aware repository from a `TypedWeb5` instance.
328
+ *
329
+ * The returned object provides domain-specific CRUD methods that mirror
330
+ * the protocol's structure tree:
331
+ *
332
+ * - Root types: `repo.friend.create()`, `repo.friend.query()`
333
+ * - Nested types: `repo.group.member.create(groupCtxId, { data })`
334
+ * - Singletons: `repo.profile.set()`, `repo.profile.get()`
335
+ * - Protocol install: `repo.configure()`
336
+ *
337
+ * @param typed - A `TypedWeb5` instance from `web5.using(protocol)`.
338
+ * @returns A typed repository object.
339
+ *
340
+ * @example
341
+ * ```ts
342
+ * const social = repository(web5.using(SocialGraphProtocol));
343
+ * await social.configure();
344
+ *
345
+ * const rec = await social.friend.create({
346
+ * data: { did: 'did:example:alice' },
347
+ * });
348
+ * const friends = await social.friend.query();
349
+ * ```
350
+ */
351
+ export function repository<
352
+ D extends ProtocolDefinition,
353
+ M extends SchemaMap,
354
+ >(typed: TypedWeb5<D, M>): Repository<D, M> {
355
+ const definition = typed.definition as ProtocolDefinition;
356
+
357
+ // Get root-level type keys from the structure
358
+ const rootKeys = Object.keys(definition.structure).filter((k) => !k.startsWith('$'));
359
+ const nodeCache: Record<string, unknown> = {};
360
+
361
+ const proxy = new Proxy({} as Record<string, unknown>, {
362
+ get(_target: Record<string, unknown>, prop: string | symbol): unknown {
363
+ if (typeof prop !== 'string') { return undefined; }
364
+
365
+ // configure() method
366
+ if (prop === 'configure') {
367
+ return async (options?: { encryption?: boolean }): Promise<DwnResponseStatus> => {
368
+ const result = await typed.configure(options);
369
+ return result;
370
+ };
371
+ }
372
+
373
+ // Root-level type nodes
374
+ if (rootKeys.includes(prop)) {
375
+ if (!(prop in nodeCache)) {
376
+ nodeCache[prop] = buildNode(
377
+ typed as unknown as TypedWeb5<ProtocolDefinition, SchemaMap>,
378
+ definition,
379
+ prop,
380
+ false,
381
+ );
382
+ }
383
+ return nodeCache[prop];
384
+ }
385
+
386
+ return undefined;
387
+ },
388
+ });
389
+
390
+ return proxy as unknown as Repository<D, M>;
391
+ }