@enbox/api 0.2.3 → 0.2.4
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 +235 -35
- package/dist/browser.mjs +13 -13
- package/dist/browser.mjs.map +4 -4
- package/dist/esm/dwn-api.js +24 -10
- package/dist/esm/dwn-api.js.map +1 -1
- package/dist/esm/index.js +6 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/live-query.js +34 -5
- package/dist/esm/live-query.js.map +1 -1
- package/dist/esm/permission-grant.js +3 -6
- package/dist/esm/permission-grant.js.map +1 -1
- package/dist/esm/permission-request.js +4 -7
- package/dist/esm/permission-request.js.map +1 -1
- package/dist/esm/record-data.js +131 -0
- package/dist/esm/record-data.js.map +1 -0
- package/dist/esm/record-types.js +9 -0
- package/dist/esm/record-types.js.map +1 -0
- package/dist/esm/record.js +58 -184
- package/dist/esm/record.js.map +1 -1
- package/dist/esm/repository-types.js +13 -0
- package/dist/esm/repository-types.js.map +1 -0
- package/dist/esm/repository.js +347 -0
- package/dist/esm/repository.js.map +1 -0
- package/dist/esm/typed-live-query.js +101 -0
- package/dist/esm/typed-live-query.js.map +1 -0
- package/dist/esm/typed-record.js +227 -0
- package/dist/esm/typed-record.js.map +1 -0
- package/dist/esm/typed-web5.js +134 -23
- package/dist/esm/typed-web5.js.map +1 -1
- package/dist/esm/web5.js +78 -20
- package/dist/esm/web5.js.map +1 -1
- package/dist/types/dwn-api.d.ts.map +1 -1
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/live-query.d.ts +43 -4
- package/dist/types/live-query.d.ts.map +1 -1
- package/dist/types/permission-grant.d.ts +1 -1
- package/dist/types/permission-grant.d.ts.map +1 -1
- package/dist/types/permission-request.d.ts +1 -1
- package/dist/types/permission-request.d.ts.map +1 -1
- package/dist/types/record-data.d.ts +49 -0
- package/dist/types/record-data.d.ts.map +1 -0
- package/dist/types/record-types.d.ts +145 -0
- package/dist/types/record-types.d.ts.map +1 -0
- package/dist/types/record.d.ts +13 -144
- package/dist/types/record.d.ts.map +1 -1
- package/dist/types/repository-types.d.ts +137 -0
- package/dist/types/repository-types.d.ts.map +1 -0
- package/dist/types/repository.d.ts +59 -0
- package/dist/types/repository.d.ts.map +1 -0
- package/dist/types/typed-live-query.d.ts +86 -0
- package/dist/types/typed-live-query.d.ts.map +1 -0
- package/dist/types/typed-record.d.ts +179 -0
- package/dist/types/typed-record.d.ts.map +1 -0
- package/dist/types/typed-web5.d.ts +55 -24
- package/dist/types/typed-web5.d.ts.map +1 -1
- package/dist/types/web5.d.ts +47 -2
- package/dist/types/web5.d.ts.map +1 -1
- package/package.json +8 -7
- package/src/dwn-api.ts +30 -13
- package/src/index.ts +6 -0
- package/src/live-query.ts +71 -7
- package/src/permission-grant.ts +2 -3
- package/src/permission-request.ts +3 -4
- package/src/record-data.ts +155 -0
- package/src/record-types.ts +188 -0
- package/src/record.ts +86 -389
- package/src/repository-types.ts +249 -0
- package/src/repository.ts +391 -0
- package/src/typed-live-query.ts +156 -0
- package/src/typed-record.ts +309 -0
- package/src/typed-web5.ts +202 -49
- package/src/web5.ts +150 -23
|
@@ -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
|
+
}
|