@graffiti-garden/api 0.0.1
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 +22 -0
- package/package.json +37 -0
- package/src/api.ts +348 -0
- package/src/errors.ts +14 -0
- package/src/index.ts +3 -0
- package/src/sync.ts +58 -0
- package/src/types.ts +254 -0
- package/tests/location.spec.ts +16 -0
- package/tsconfig.json +13 -0
package/README.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Graffiti API
|
|
2
|
+
|
|
3
|
+
The Graffiti API makes it possible to build social applications that are flexible and interoperable.
|
|
4
|
+
This repository contains the abstract API and it's documentation.
|
|
5
|
+
|
|
6
|
+
[View the Documentation](https://api.graffiti.garden/classes/Graffiti.html)
|
|
7
|
+
|
|
8
|
+
## Building the Documentation
|
|
9
|
+
|
|
10
|
+
To build the [TypeDoc](https://typedoc.org/) documentation, run the following commands:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm run install
|
|
14
|
+
npm run docs
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Then run a local server to view the documentation:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
cd docs
|
|
21
|
+
npx http-server
|
|
22
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@graffiti-garden/api",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "The heart of Graffiti",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.ts",
|
|
8
|
+
"./tests": "./tests/index.ts"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"tests",
|
|
13
|
+
"package.json",
|
|
14
|
+
"tsconfig.json"
|
|
15
|
+
],
|
|
16
|
+
"author": "Theia Henderson",
|
|
17
|
+
"license": "GPL-3.0-or-later",
|
|
18
|
+
"scripts": {
|
|
19
|
+
"docs": "typedoc --options typedoc.json"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/graffiti-garden/api.git"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/graffiti-garden/api/issues"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://api.graffiti.garden/classes/Graffiti.html",
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/json-schema": "^7.0.15",
|
|
31
|
+
"ajv": "^8.17.1",
|
|
32
|
+
"fast-json-patch": "^3.1.1",
|
|
33
|
+
"tslib": "^2.8.1",
|
|
34
|
+
"typedoc": "^0.26.11",
|
|
35
|
+
"vitest": "^2.1.8"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
GraffitiLocation,
|
|
3
|
+
GraffitiObject,
|
|
4
|
+
GraffitiObjectBase,
|
|
5
|
+
GraffitiPatch,
|
|
6
|
+
GraffitiSessionBase,
|
|
7
|
+
GraffitiPutObject,
|
|
8
|
+
GraffitiStream,
|
|
9
|
+
} from "./types";
|
|
10
|
+
import type { JSONSchema4 } from "json-schema";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* This API describes a small but mighty set of methods that
|
|
14
|
+
* can be used to create many different kinds of social media applications,
|
|
15
|
+
* all of which can interoperate.
|
|
16
|
+
* These methods should satisfy all of an application's needs for
|
|
17
|
+
* the communication, storage, and access management of social data.
|
|
18
|
+
* The rest of the application can be built with standard client-side
|
|
19
|
+
* user interface tools to present and interact with the data.
|
|
20
|
+
*
|
|
21
|
+
* There are several different implementations of this Graffiti API available,
|
|
22
|
+
* including a decentralized implementation and a local implementation
|
|
23
|
+
* that can be used for testing. In the design of Graffiti we prioritized
|
|
24
|
+
* the design of this API first as it is the layer that shapes the experience
|
|
25
|
+
* of developing applications. While different implementations provide tradeoffs between
|
|
26
|
+
* other important properties (e.g. privacy, security, scalability), those properties
|
|
27
|
+
* are useless if the system as a whole is unusable. Build APIs before protocols!
|
|
28
|
+
*
|
|
29
|
+
* The first group of methods are like standard CRUD operations that
|
|
30
|
+
* allow applications to {@link put}, {@link get}, {@link patch}, and {@link delete}
|
|
31
|
+
* {@link GraffitiObjectBase} objects. The main difference between these
|
|
32
|
+
* methods and standard database operations is that an {@link GraffitiObjectBase.actor | `actor`}
|
|
33
|
+
* (essentially a user) can only modify objects that they created.
|
|
34
|
+
* Applications may also specify an an array of actors that are {@link GraffitiObjectBase.allowed | `allowed`}
|
|
35
|
+
* to access the object and an array of {@link GraffitiObjectBase.channels | `channels`}
|
|
36
|
+
* that the object is associated with.
|
|
37
|
+
*
|
|
38
|
+
* The "social" part of the API is the {@link discover} method, which allows
|
|
39
|
+
* an application to query for objects made by other users.
|
|
40
|
+
* This function only returns objects that are associated with one or more
|
|
41
|
+
* of the {@link GraffitiObjectBase.channels | `channels`}
|
|
42
|
+
* provided by a querying application. This helps to prevent
|
|
43
|
+
* [context collapse](https://en.wikipedia.org/wiki/Context_collapse) and
|
|
44
|
+
* allows users to express their intended audience, even in an interoperable
|
|
45
|
+
* environment.
|
|
46
|
+
*
|
|
47
|
+
* Additionally, {@link synchronize} keeps track of changes to data
|
|
48
|
+
* from any of the aforementioned methods and routes these changes internally
|
|
49
|
+
* to provide a consistent user experience.
|
|
50
|
+
*
|
|
51
|
+
* Finally, other utility functions provide simple type conversions and
|
|
52
|
+
* allow users to find objects "lost" to forgotten or misspelled channels.
|
|
53
|
+
*
|
|
54
|
+
* @groupDescription CRUD Operations
|
|
55
|
+
* Methods for {@link put | creating}, {@link get | reading}, {@link patch | updating},
|
|
56
|
+
* and {@link delete | deleting} {@link GraffitiObjectBase | Graffiti objects}.
|
|
57
|
+
* @groupDescription Query Operations
|
|
58
|
+
* Methods for retrieving multiple {@link GraffitiObjectBase | Graffiti objects} at a time.
|
|
59
|
+
* @groupDescription Utilities
|
|
60
|
+
* Methods for for converting Graffiti objects to and from URIs
|
|
61
|
+
* and for finding lost objects.
|
|
62
|
+
*/
|
|
63
|
+
export abstract class Graffiti {
|
|
64
|
+
/**
|
|
65
|
+
* Converts a {@link GraffitiLocation} object containing a
|
|
66
|
+
* {@link GraffitiObjectBase.name | `name`}, {@link GraffitiObjectBase.actor | `actor`},
|
|
67
|
+
* and {@link GraffitiObjectBase.source | `source`} into a globally unique URI.
|
|
68
|
+
* The form of this URI is implementation dependent.
|
|
69
|
+
*
|
|
70
|
+
* Its exact inverse is {@link uriToLocation}.
|
|
71
|
+
*
|
|
72
|
+
* @group Utilities
|
|
73
|
+
*/
|
|
74
|
+
abstract locationToUri(location: GraffitiLocation): string;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Parses a globally unique Graffiti URI into a {@link GraffitiLocation}
|
|
78
|
+
* object containing a {@link GraffitiObjectBase.name | `name`},
|
|
79
|
+
* {@link GraffitiObjectBase.actor | `actor`}, and {@link GraffitiObjectBase.source | `source`}.
|
|
80
|
+
*
|
|
81
|
+
* Its exact inverse is {@link locationToUri}.
|
|
82
|
+
*
|
|
83
|
+
* @group Utilities
|
|
84
|
+
*/
|
|
85
|
+
abstract uriToLocation(uri: string): GraffitiLocation;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* An alias of {@link locationToUri}
|
|
89
|
+
*
|
|
90
|
+
* @group Utilities
|
|
91
|
+
*/
|
|
92
|
+
objectToUri(object: GraffitiObjectBase) {
|
|
93
|
+
return this.locationToUri(object);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Creates a new {@link GraffitiObjectBase | object} or replaces an existing object.
|
|
98
|
+
* An object can only be replaced by the same {@link GraffitiObjectBase.actor | `actor`}
|
|
99
|
+
* that created it.
|
|
100
|
+
*
|
|
101
|
+
* Replacement occurs when the {@link GraffitiLocation} properties of the supplied object
|
|
102
|
+
* ({@link GraffitiObjectBase.name | `name`}, {@link GraffitiObjectBase.actor | `actor`},
|
|
103
|
+
* and {@link GraffitiObjectBase.source | `source`}) exactly match the location of an existing object.
|
|
104
|
+
*
|
|
105
|
+
* @returns The object that was replaced if one exists or an object with
|
|
106
|
+
* with a `null` {@link GraffitiObjectBase.value | `value`} if this operation
|
|
107
|
+
* created a new object.
|
|
108
|
+
* The object will have a {@link GraffitiObjectBase.tombstone | `tombstone`}
|
|
109
|
+
* field set to `true` and a {@link GraffitiObjectBase.lastModified | `lastModified`}
|
|
110
|
+
* field updated to the time of replacement/creation.
|
|
111
|
+
*
|
|
112
|
+
* @group CRUD Operations
|
|
113
|
+
*/
|
|
114
|
+
abstract put<Schema>(
|
|
115
|
+
/**
|
|
116
|
+
* The object to be put. This object is statically type-checked against the [JSON schema](https://json-schema.org/) that can be optionally provided
|
|
117
|
+
* as the generic type parameter. We highly recommend providing a schema to
|
|
118
|
+
* ensure that the PUT object matches subsequent {@link get} or {@link discover}
|
|
119
|
+
* operations.
|
|
120
|
+
*/
|
|
121
|
+
object: GraffitiPutObject<Schema>,
|
|
122
|
+
/**
|
|
123
|
+
* An implementation-specific object with information to authenticate the
|
|
124
|
+
* {@link GraffitiObjectBase.actor | `actor`}.
|
|
125
|
+
*/
|
|
126
|
+
session: GraffitiSessionBase,
|
|
127
|
+
): Promise<GraffitiObjectBase>;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Retrieves an object from a given location.
|
|
131
|
+
* If no object exists at that location or if the retrieving
|
|
132
|
+
* {@link GraffitiObjectBase.actor | `actor`} is not the creator or included in
|
|
133
|
+
* the object's {@link GraffitiObjectBase.allowed | `allowed`} property,
|
|
134
|
+
* an error is thrown.
|
|
135
|
+
*
|
|
136
|
+
* The retrieved object is also type-checked against the provided [JSON schema](https://json-schema.org/)
|
|
137
|
+
* otherwise an error is thrown.
|
|
138
|
+
*
|
|
139
|
+
* @group CRUD Operations
|
|
140
|
+
*/
|
|
141
|
+
abstract get<Schema extends JSONSchema4>(
|
|
142
|
+
/**
|
|
143
|
+
* The location of the object to get.
|
|
144
|
+
*/
|
|
145
|
+
locationOrUri: GraffitiLocation | string,
|
|
146
|
+
/**
|
|
147
|
+
* The JSON schema to validate the retrieved object against.
|
|
148
|
+
*/
|
|
149
|
+
schema: Schema,
|
|
150
|
+
/**
|
|
151
|
+
* An implementation-specific object with information to authenticate the
|
|
152
|
+
* {@link GraffitiObjectBase.actor | `actor`}. If no `session` is provided,
|
|
153
|
+
* the retrieved object's {@link GraffitiObjectBase.allowed | `allowed`}
|
|
154
|
+
* property must be `undefined`.
|
|
155
|
+
*/
|
|
156
|
+
session?: GraffitiSessionBase,
|
|
157
|
+
): Promise<GraffitiObject<Schema>>;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Patches an existing object at a given location.
|
|
161
|
+
* The patching {@link GraffitiObjectBase.actor | `actor`} must be the same as the
|
|
162
|
+
* `actor` that created the object.
|
|
163
|
+
*
|
|
164
|
+
* @returns The object that was deleted if one exists or an object with
|
|
165
|
+
* with a `null` {@link GraffitiObjectBase.value | `value`} otherwise.
|
|
166
|
+
* The object will have a {@link GraffitiObjectBase.tombstone | `tombstone`}
|
|
167
|
+
* field set to `true` and a {@link GraffitiObjectBase.lastModified | `lastModified`}
|
|
168
|
+
* field updated to the time of deletion.
|
|
169
|
+
*
|
|
170
|
+
* @group CRUD Operations
|
|
171
|
+
*/
|
|
172
|
+
abstract patch(
|
|
173
|
+
/**
|
|
174
|
+
* A collection of [JSON Patch](https://jsonpatch.com) operations
|
|
175
|
+
* to apply to the object. See {@link GraffitiPatch} for more information.
|
|
176
|
+
*/
|
|
177
|
+
patch: GraffitiPatch,
|
|
178
|
+
/**
|
|
179
|
+
* The location of the object to patch.
|
|
180
|
+
*/
|
|
181
|
+
locationOrUri: GraffitiLocation | string,
|
|
182
|
+
/**
|
|
183
|
+
* An implementation-specific object with information to authenticate the
|
|
184
|
+
* {@link GraffitiObjectBase.actor | `actor`}.
|
|
185
|
+
*/
|
|
186
|
+
session: GraffitiSessionBase,
|
|
187
|
+
): Promise<GraffitiObjectBase>;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Deletes an object from a given location.
|
|
191
|
+
* The deleting {@link GraffitiObjectBase.actor | `actor`} must be the same as the
|
|
192
|
+
* `actor` that created the object.
|
|
193
|
+
*
|
|
194
|
+
* @returns The object that was deleted if one exists or an object with
|
|
195
|
+
* with a `null` {@link GraffitiObjectBase.value | `value`} otherwise.
|
|
196
|
+
* The object will have a {@link GraffitiObjectBase.tombstone | `tombstone`}
|
|
197
|
+
* field set to `true` and a {@link GraffitiObjectBase.lastModified | `lastModified`}
|
|
198
|
+
* field updated to the time of deletion.
|
|
199
|
+
*
|
|
200
|
+
* @group CRUD Operations
|
|
201
|
+
*/
|
|
202
|
+
abstract delete(
|
|
203
|
+
/**
|
|
204
|
+
* The location of the object to delete.
|
|
205
|
+
*/
|
|
206
|
+
locationOrUri: GraffitiLocation | string,
|
|
207
|
+
/**
|
|
208
|
+
* An implementation-specific object with information to authenticate the
|
|
209
|
+
* {@link GraffitiObjectBase.actor | `actor`}.
|
|
210
|
+
*/
|
|
211
|
+
session: GraffitiSessionBase,
|
|
212
|
+
): Promise<GraffitiObjectBase>;
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Returns a stream of {@link GraffitiObjectBase | objects}
|
|
216
|
+
* that are contained in at least one of the given {@link GraffitiObjectBase.channels | `channels`}
|
|
217
|
+
* and match the given [JSON Schema](https://json-schema.org)
|
|
218
|
+
*
|
|
219
|
+
* Objects are returned asynchronously as they are discovered but the stream
|
|
220
|
+
* will end once all leads have been exhausted.
|
|
221
|
+
* The method must be polled again for new objects.
|
|
222
|
+
*
|
|
223
|
+
* {@link discover} can be used in conjunction with {@link synchronize}
|
|
224
|
+
* to provide a responsive and consistent user experience.
|
|
225
|
+
*
|
|
226
|
+
* @group Query Operations
|
|
227
|
+
*/
|
|
228
|
+
abstract discover<Schema extends JSONSchema4>(
|
|
229
|
+
/**
|
|
230
|
+
* The {@link GraffitiObjectBase.channels | `channels`} that objects must be associated with.
|
|
231
|
+
*/
|
|
232
|
+
channels: string[],
|
|
233
|
+
/**
|
|
234
|
+
* A [JSON Schema](https://json-schema.org) that objects must satisfy.
|
|
235
|
+
*/
|
|
236
|
+
schema: Schema,
|
|
237
|
+
/**
|
|
238
|
+
* An implementation-specific object with information to authenticate the
|
|
239
|
+
* {@link GraffitiObjectBase.actor | `actor`}. If no `session` is provided,
|
|
240
|
+
* only objects that have no {@link GraffitiObjectBase.allowed | `allowed`}
|
|
241
|
+
* property will be returned.
|
|
242
|
+
*/
|
|
243
|
+
session?: GraffitiSessionBase,
|
|
244
|
+
): GraffitiStream<GraffitiObject<Schema>>;
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* This method has the same signature as {@link discover} but listens for
|
|
248
|
+
* changes made via {@link put}, {@link patch}, and {@link delete} or
|
|
249
|
+
* fetched from {@link get} or {@link discover} and then streams appropriate
|
|
250
|
+
* changes to provide a responsive and consistent user experience.
|
|
251
|
+
*
|
|
252
|
+
* Unlike {@link discover}, this method continuously listens for changes
|
|
253
|
+
* and will not terminate unless the user calls the `return` method on the iterator
|
|
254
|
+
* or `break`s out of the loop.
|
|
255
|
+
*
|
|
256
|
+
* Example 1: Suppose a user publishes a post using {@link put}. If the feed
|
|
257
|
+
* displaying that user's posts is using {@link synchronize} to listen for changes,
|
|
258
|
+
* then the user's new post will instantly appear in their feed, giving the UI a
|
|
259
|
+
* responsive feel.
|
|
260
|
+
*
|
|
261
|
+
* Example 2: Suppose one of a user's friends changes their name. As soon as the
|
|
262
|
+
* user's application receives one notice of that change (using {@link get}
|
|
263
|
+
* or {@link discover}), then {@link synchronize} listeners can be used to update
|
|
264
|
+
* all instance's of that friend's name in the user's application instantly,
|
|
265
|
+
* providing a consistent user experience.
|
|
266
|
+
*
|
|
267
|
+
* @group Query Operations
|
|
268
|
+
*/
|
|
269
|
+
abstract synchronize<Schema extends JSONSchema4>(
|
|
270
|
+
/**
|
|
271
|
+
* The {@link GraffitiObjectBase.channels | `channels`} that the objects must be associated with.
|
|
272
|
+
*/
|
|
273
|
+
channels: string[],
|
|
274
|
+
/**
|
|
275
|
+
* A [JSON Schema](https://json-schema.org) that objects must satisfy.
|
|
276
|
+
*/
|
|
277
|
+
schema: Schema,
|
|
278
|
+
/**
|
|
279
|
+
* An implementation-specific object with information to authenticate the
|
|
280
|
+
* {@link GraffitiObjectBase.actor | `actor`}. If no `session` is provided,
|
|
281
|
+
* only objects that have no {@link GraffitiObjectBase.allowed | `allowed`}
|
|
282
|
+
* property will be returned.
|
|
283
|
+
*/
|
|
284
|
+
session?: GraffitiSessionBase,
|
|
285
|
+
): GraffitiStream<GraffitiObject<Schema>>;
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Returns a list of all {@link GraffitiObjectBase.channels | `channels`}
|
|
289
|
+
* that an {@link GraffitiObjectBase.actor | `actor`} has posted to.
|
|
290
|
+
* This is not very useful for most applications, but
|
|
291
|
+
* necessary for certain applications where a user wants a
|
|
292
|
+
* global view of all their Graffiti data or to debug
|
|
293
|
+
* channel usage.
|
|
294
|
+
*
|
|
295
|
+
* @group Utilities
|
|
296
|
+
*
|
|
297
|
+
* @returns A stream the {@link GraffitiObjectBase.channels | `channel`}s
|
|
298
|
+
* that the {@link GraffitiObjectBase.actor | `actor`} has posted to.
|
|
299
|
+
* The `lastModified` field is the time that the user last modified an
|
|
300
|
+
* object in that channel. The `count` field is the number of objects
|
|
301
|
+
* that the user has posted to that channel.
|
|
302
|
+
*/
|
|
303
|
+
abstract listChannels(
|
|
304
|
+
/**
|
|
305
|
+
* An implementation-specific object with information to authenticate the
|
|
306
|
+
* {@link GraffitiObjectBase.actor | `actor`}.
|
|
307
|
+
*/
|
|
308
|
+
session: GraffitiSessionBase,
|
|
309
|
+
): GraffitiStream<{
|
|
310
|
+
channel: string;
|
|
311
|
+
source: string;
|
|
312
|
+
lastModified: Date;
|
|
313
|
+
count: number;
|
|
314
|
+
}>;
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Returns a list of all {@link GraffitiObjectBase | objects} a user has posted that are
|
|
318
|
+
* not associated with any {@link GraffitiObjectBase.channels | `channel`}, i.e. orphaned objects.
|
|
319
|
+
* This is not very useful for most applications, but
|
|
320
|
+
* necessary for certain applications where a user wants a
|
|
321
|
+
* global view of all their Graffiti data or to debug
|
|
322
|
+
* channel usage.
|
|
323
|
+
*
|
|
324
|
+
* @group Utilities
|
|
325
|
+
*
|
|
326
|
+
* @returns A stream of the {@link GraffitiObjectBase.name | `name`}
|
|
327
|
+
* and {@link GraffitiObjectBase.source | `source`} of the orphaned objects
|
|
328
|
+
* that the {@link GraffitiObjectBase.actor | `actor`} has posted to.
|
|
329
|
+
* The {@link GraffitiObjectBase.lastModified | lastModified} field is the
|
|
330
|
+
* time that the user last modified the orphan and the
|
|
331
|
+
* {@link GraffitiObjectBase.tombstone | `tombstone`} field is `true`
|
|
332
|
+
* if the object has been deleted.
|
|
333
|
+
*/
|
|
334
|
+
abstract listOrphans(session: GraffitiSessionBase): GraffitiStream<{
|
|
335
|
+
name: string;
|
|
336
|
+
source: string;
|
|
337
|
+
lastModified: Date;
|
|
338
|
+
tombstone: boolean;
|
|
339
|
+
}>;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* This is a factory function that produces an instance of
|
|
344
|
+
* the {@link Graffiti} class. Since the Graffiti class is
|
|
345
|
+
* abstract, factory functions provide an easy way to
|
|
346
|
+
* swap out different implementations.
|
|
347
|
+
*/
|
|
348
|
+
export type UseGraffiti = () => Graffiti;
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class GraffitiUnauthorizedError extends Error {
|
|
2
|
+
constructor(message: string) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "UnauthorizedError";
|
|
5
|
+
Object.setPrototypeOf(this, GraffitiUnauthorizedError.prototype);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
class ForbiddenError extends Error {
|
|
10
|
+
constructor(message: string) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "ForbiddenError";
|
|
13
|
+
}
|
|
14
|
+
}
|
package/src/index.ts
ADDED
package/src/sync.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { JSONSchema4 } from "json-schema";
|
|
2
|
+
import { Graffiti } from "./api";
|
|
3
|
+
import type {
|
|
4
|
+
GraffitiObject,
|
|
5
|
+
GraffitiObjectBase,
|
|
6
|
+
GraffitiSessionBase,
|
|
7
|
+
} from "./types";
|
|
8
|
+
import Ajv from "ajv";
|
|
9
|
+
import { applyPatch } from "fast-json-patch";
|
|
10
|
+
|
|
11
|
+
export abstract class GraffitiSynchronized extends Graffiti {
|
|
12
|
+
protected readonly ajv = new Ajv();
|
|
13
|
+
protected readonly changes = new EventTarget();
|
|
14
|
+
protected dispatchChanges(
|
|
15
|
+
oldObject: GraffitiObjectBase,
|
|
16
|
+
newObject?: GraffitiObjectBase,
|
|
17
|
+
) {
|
|
18
|
+
this.changes.dispatchEvent(
|
|
19
|
+
new CustomEvent("change", {
|
|
20
|
+
detail: {
|
|
21
|
+
oldObject,
|
|
22
|
+
newObject,
|
|
23
|
+
},
|
|
24
|
+
}),
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
protected abstract _patch(
|
|
29
|
+
...args: Parameters<Graffiti["patch"]>
|
|
30
|
+
): ReturnType<Graffiti["patch"]>;
|
|
31
|
+
|
|
32
|
+
async patch(
|
|
33
|
+
...args: Parameters<Graffiti["patch"]>
|
|
34
|
+
): ReturnType<Graffiti["patch"]> {
|
|
35
|
+
const oldObject = await this._patch(...args);
|
|
36
|
+
const newObject: GraffitiObjectBase = { ...oldObject, tombstone: false };
|
|
37
|
+
for (const prop of ["value", "channels", "allowed"] as const) {
|
|
38
|
+
const ops = args[0][prop];
|
|
39
|
+
if (!ops || !ops.length) continue;
|
|
40
|
+
const result = applyPatch(newObject[prop], ops, false, false).newDocument;
|
|
41
|
+
}
|
|
42
|
+
this.dispatchChanges(oldObject, newObject);
|
|
43
|
+
return oldObject;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// synchronize<Schema extends JSONSchema4>(
|
|
47
|
+
// ...args: Parameters<Graffiti["synchronize"]>
|
|
48
|
+
// ): ReturnType<Graffiti["synchronize"]> {
|
|
49
|
+
// const validate = this.ajv.compile(schema);
|
|
50
|
+
// const matchOptions = {
|
|
51
|
+
// ifModifiedSince: options?.ifModifiedSince,
|
|
52
|
+
// channels,
|
|
53
|
+
// };
|
|
54
|
+
// const repeater = new Repeater < {
|
|
55
|
+
// }
|
|
56
|
+
// GraffitiObject<Schema>>(
|
|
57
|
+
// }
|
|
58
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { type JTDDataType } from "ajv/dist/core";
|
|
2
|
+
import type { Operation as JSONPatchOperation } from "fast-json-patch";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Objects are the atomic unit in Graffiti that can represent both data (*e.g.* a social media post or profile)
|
|
6
|
+
* and activities (*e.g.* a like or follow).
|
|
7
|
+
* Objects are created and modified by a single {@link actor | `actor`}.
|
|
8
|
+
*
|
|
9
|
+
* Most of an object's content is stored in its {@link value | `value`} property, which can be any JSON
|
|
10
|
+
* object. However, we recommend using properties from the
|
|
11
|
+
* [Activity Vocabulary](https://www.w3.org/TR/activitystreams-vocabulary/)
|
|
12
|
+
* or properties that emerge in the Graffiti [folksonomy](https://en.wikipedia.org/wiki/Folksonomy)
|
|
13
|
+
* to promote interoperability.
|
|
14
|
+
*
|
|
15
|
+
* The {@link name | `name`}, {@link actor | `actor`}, and {@link source | `source`}
|
|
16
|
+
* properties together uniquely describe the {@link GraffitiLocation | object's location}
|
|
17
|
+
* and can be {@link Graffiti.locationToUri | converted to a globally unique URI}.
|
|
18
|
+
*
|
|
19
|
+
* The {@link channels | `channels`} and {@link allowed | `allowed`} properties
|
|
20
|
+
* enable the object's creator to shape the visibility of and access to their object.
|
|
21
|
+
*
|
|
22
|
+
* The {@link tombstone | `tombstone`} and {@link lastModified | `lastModified`} properties are for
|
|
23
|
+
* caching and synchronization.
|
|
24
|
+
*/
|
|
25
|
+
export interface GraffitiObjectBase {
|
|
26
|
+
/**
|
|
27
|
+
* The object's content as freeform JSON. We recommend using properties from the
|
|
28
|
+
* [Activity Vocabulary](https://www.w3.org/TR/activitystreams-vocabulary/)
|
|
29
|
+
* or properties that emerge in the Graffiti [folksonomy](https://en.wikipedia.org/wiki/Folksonomy)
|
|
30
|
+
* to promote interoperability.
|
|
31
|
+
*/
|
|
32
|
+
value: {};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* An array of URIs the creator associates with the object. Objects can only be found by querying
|
|
36
|
+
* one of the object's channels using the
|
|
37
|
+
* {@link Graffiti.discover} method. This allows creators to express the intended audience of their object
|
|
38
|
+
* which helps to prevent [context collapse](https://en.wikipedia.org/wiki/Context_collapse) even
|
|
39
|
+
* in the highly interoperable ecosystem that Graffiti envisions. For example, channel URIs may be:
|
|
40
|
+
* - A user's own {@link actor | `actor`} URI. Putting an object in this channel is a way to broadcast
|
|
41
|
+
* the object to the user's followers, like posting a tweet.
|
|
42
|
+
* - The URI of a Graffiti post. Putting an object in this channel is a way to broadcast to anyone viewing
|
|
43
|
+
* the post, like commenting on a tweet.
|
|
44
|
+
* - A URI representing a topic. Putting an object in this channel is a way to broadcast to anyone interested
|
|
45
|
+
* in that topic, like posting in a subreddit.
|
|
46
|
+
*/
|
|
47
|
+
channels: string[];
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* An optional array of {@link actor | `actor`} URIs that the creator allows to access the object.
|
|
51
|
+
* If no `allowed` array is provided, the object can be accessed by anyone (so long as they
|
|
52
|
+
* also know the right {@link channels | `channel` } to look in). An object can always be accessed by its creator, even if
|
|
53
|
+
* the `allowed` array is empty.
|
|
54
|
+
*
|
|
55
|
+
* The `allowed` array is not revealed to users other than the creator, like
|
|
56
|
+
* a BCC email. A user may choose to add a `to` property to the object's {@link value | `value`} to indicate
|
|
57
|
+
* other recipients, however this is not enforced by Graffiti and may not accurately reflect the actual `allowed` array.
|
|
58
|
+
*
|
|
59
|
+
* `allowed` can be combined with {@link channels | `channels`}. For example, to send someone a direct message
|
|
60
|
+
* the sender should put their object in the channel of the recipient's {@link actor | `actor`} URI to notify them of the message and also add
|
|
61
|
+
* the recipient's {@link actor | `actor`} URI to the `allowed` array to prevent others from seeing the message.
|
|
62
|
+
*/
|
|
63
|
+
allowed?: string[];
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The URI of the `actor` that {@link Graffiti.put | created } the object. This `actor` also has the unique permission to
|
|
67
|
+
* {@link Graffiti.patch | modify} or {@link Graffiti.delete | delete} the object.
|
|
68
|
+
*
|
|
69
|
+
* We borrow the term actor from the ActivityPub because
|
|
70
|
+
* [like in ActivityPub](https://www.w3.org/TR/activitypub/#h-note-0)
|
|
71
|
+
* there is not necessarily a one-to-one mapping between actors and people/users.
|
|
72
|
+
* Multiple people can share the same actor or one person can have multiple actors.
|
|
73
|
+
* Actors can also be bots.
|
|
74
|
+
*
|
|
75
|
+
* In Graffiti, actors are always globally unique URIs which
|
|
76
|
+
* allows them to also function as {@link channels | `channels`}.
|
|
77
|
+
*/
|
|
78
|
+
actor: string;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* A name for the object. This name is not globally unique but it is unique when
|
|
82
|
+
* combined with the {@link actor | `actor`} and {@link source | `source`}.
|
|
83
|
+
* Often times it is not specified by the user and randomly generated during {@link Graffiti.put | creation}.
|
|
84
|
+
* If an object is created with the same `name`, `actor`, and `source` as an existing object,
|
|
85
|
+
* the existing object will be replaced with the new object.
|
|
86
|
+
*/
|
|
87
|
+
name: string;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The URI of the source that stores the object. In some decentralized implementations,
|
|
91
|
+
* it can represent the server or [pod](https://en.wikipedia.org/wiki/Solid_(web_decentralization_project)#Design)
|
|
92
|
+
* that a user has delegated to store their objects. In others it may represent the distributed
|
|
93
|
+
* storage network that the object is stored on.
|
|
94
|
+
*/
|
|
95
|
+
source: string;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The time the object was last modified. This is used for caching and synchronization.
|
|
99
|
+
* It can also be used to sort objects in a user interface but in many cases it would be better to
|
|
100
|
+
* use a `createdAt` property in the object's {@link value | `value`} to indicate when the object was created
|
|
101
|
+
* rather than when it was modified.
|
|
102
|
+
*/
|
|
103
|
+
lastModified: Date;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A boolean indicating whether the object has been deleted.
|
|
107
|
+
* Depending on implementation, objects stay available for some time after deletion to allow for synchronization.
|
|
108
|
+
*/
|
|
109
|
+
tombstone: boolean;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* This type constrains the {@link GraffitiObjectBase} type to adhere to a
|
|
114
|
+
* particular [JSON schema](https://json-schema.org/).
|
|
115
|
+
* This allows for static type-checking of an object's {@link GraffitiObjectBase.value | `value`}
|
|
116
|
+
* which is otherwise a freeform JSON object.
|
|
117
|
+
*
|
|
118
|
+
* Schema-aware objects are returned by {@link Graffiti.get} and {@link Graffiti.discover}.
|
|
119
|
+
*/
|
|
120
|
+
export type GraffitiObject<Schema> = GraffitiObjectBase & JTDDataType<Schema>;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* This is a subset of properties from {@link GraffitiObjectBase} that uniquely
|
|
124
|
+
* identify an object's location: {@link GraffitiObjectBase.actor | `actor`},
|
|
125
|
+
* {@link GraffitiObjectBase.name | `name`}, and {@link GraffitiObjectBase.source | `source`}.
|
|
126
|
+
* Attempts to create an object with the same `actor`, `name`, and `source`
|
|
127
|
+
* as an existing object will replace the existing object (see {@link Graffiti.put}).
|
|
128
|
+
*
|
|
129
|
+
* This location can be converted to
|
|
130
|
+
* a globally unique URI using {@link Graffiti.locationToUri}.
|
|
131
|
+
*/
|
|
132
|
+
export type GraffitiLocation = Pick<
|
|
133
|
+
GraffitiObjectBase,
|
|
134
|
+
"actor" | "name" | "source"
|
|
135
|
+
>;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* This object is a subset of {@link GraffitiObjectBase} that a user must construct locally before calling {@link Graffiti.put}.
|
|
139
|
+
* This local copy does not require system-generated properties and may be statically typed with
|
|
140
|
+
* a [JSON schema](https://json-schema.org/) to prevent the accidental creation of erroneous objects.
|
|
141
|
+
*
|
|
142
|
+
* This local object must have a {@link GraffitiObjectBase.value | `value`} and {@link GraffitiObjectBase.channels | `channels`}
|
|
143
|
+
* and may optionally have an {@link GraffitiObjectBase.allowed | `allowed`} property.
|
|
144
|
+
*
|
|
145
|
+
* It may also contain any of the {@link GraffitiLocation } properties: {@link GraffitiObjectBase.actor | `actor`},
|
|
146
|
+
* {@link GraffitiObjectBase.name | `name`}, and {@link GraffitiObjectBase.source | `source`}.
|
|
147
|
+
* If the location provided exactly matches an existing object, the existing object will be replaced.
|
|
148
|
+
* If no `name` is provided, one will be randomly generated.
|
|
149
|
+
* If no `actor` is provided, the `actor` from the supplied {@link GraffitiSessionBase | `session` } will be used.
|
|
150
|
+
* If no `source` is provided, one may be inferred by the depending on implementation.
|
|
151
|
+
*
|
|
152
|
+
* This object does not need a {@link GraffitiObjectBase.lastModified | `lastModified`} or {@link GraffitiObjectBase.tombstone | `tombstone`}
|
|
153
|
+
* property since these are automatically generated by the Graffiti system.
|
|
154
|
+
*/
|
|
155
|
+
export type GraffitiPutObject<Schema> = Pick<
|
|
156
|
+
GraffitiObjectBase,
|
|
157
|
+
"value" | "channels" | "allowed"
|
|
158
|
+
> &
|
|
159
|
+
Partial<GraffitiLocation> &
|
|
160
|
+
JTDDataType<Schema>;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* This object contains information that
|
|
164
|
+
* {@link GraffitiObjectBase.source | `source`}s can
|
|
165
|
+
* use to verify that a user has permission to operate a
|
|
166
|
+
* particular {@link GraffitiObjectBase.actor | `actor`}.
|
|
167
|
+
* This object is required of all {@link Graffiti} methods
|
|
168
|
+
* that modify objects and optional for methods that read objects.
|
|
169
|
+
*
|
|
170
|
+
* At a minimum the `session` object must contain the
|
|
171
|
+
* {@link GraffitiSessionBase.actor | `actor`} URI the user wants to authenticate with.
|
|
172
|
+
* However it is likely that the `session` object must contain other
|
|
173
|
+
* implementation-specific properties.
|
|
174
|
+
* For example, a Solid implementation might include a
|
|
175
|
+
* [`fetch`](https://docs.inrupt.com/developer-tools/api/javascript/solid-client-authn-browser/functions.html#fetch)
|
|
176
|
+
* function. A distributed implementation may include
|
|
177
|
+
* a cryptographic signature.
|
|
178
|
+
*
|
|
179
|
+
* It may also include other implementation specific properties
|
|
180
|
+
* that provide hints for performance or security.
|
|
181
|
+
*/
|
|
182
|
+
export interface GraffitiSessionBase {
|
|
183
|
+
/**
|
|
184
|
+
* The {@link GraffitiObjectBase.actor | `actor`} a user wants to authenticate with.
|
|
185
|
+
*/
|
|
186
|
+
actor: string;
|
|
187
|
+
/**
|
|
188
|
+
* Other implementation-specific properties go here.
|
|
189
|
+
*/
|
|
190
|
+
[key: string]: any;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* This is the format for patches that modify {@link GraffitiObjectBase} objects
|
|
195
|
+
* using the {@link Graffiti.patch} method. The patches must
|
|
196
|
+
* be an array of [JSON Patch](https://jsonpatch.com) operations.
|
|
197
|
+
* Patches can only be applied to the
|
|
198
|
+
* {@link GraffitiObjectBase.value | `value`}, {@link GraffitiObjectBase.channels | `channels`},
|
|
199
|
+
* and {@link GraffitiObjectBase.allowed | `allowed`} properties since the other
|
|
200
|
+
* properties either describe the object's location or are automatically generated.
|
|
201
|
+
* (See also {@link GraffitiPutObject}).
|
|
202
|
+
*/
|
|
203
|
+
export interface GraffitiPatch {
|
|
204
|
+
/**
|
|
205
|
+
* An array of [JSON Patch](https://jsonpatch.com) operations to
|
|
206
|
+
* modify the object's {@link GraffitiObjectBase.value | `value`}. The resulting
|
|
207
|
+
* `value` must still be a JSON object.
|
|
208
|
+
*/
|
|
209
|
+
value?: JSONPatchOperation[];
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* An array of [JSON Patch](https://jsonpatch.com) operations to
|
|
213
|
+
* modify the object's {@link GraffitiObjectBase.channels | `channels`}. The resulting
|
|
214
|
+
* `channels` must still be an array of strings.
|
|
215
|
+
*/
|
|
216
|
+
channels?: JSONPatchOperation[];
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* An array of [JSON Patch](https://jsonpatch.com) operations to
|
|
220
|
+
* modify the object's {@link GraffitiObjectBase.allowed | `allowed`} property. The resulting
|
|
221
|
+
* `allowed` property must still be an array of strings or `undefined`.
|
|
222
|
+
*/
|
|
223
|
+
allowed?: JSONPatchOperation[];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* This type represents a stream of data that are
|
|
228
|
+
* returned by Graffiti's query-like operations such as
|
|
229
|
+
* {@link Graffiti.discover} and {@link Graffiti.listChannels}.
|
|
230
|
+
*
|
|
231
|
+
* Errors are returned within the stream rather than as
|
|
232
|
+
* exceptions that would halt the entire stream. This is because
|
|
233
|
+
* some implementations may pull data from multiple
|
|
234
|
+
* {@link GraffitiObjectBase.source | `source`}s
|
|
235
|
+
* including some that may be unreliable. In many cases,
|
|
236
|
+
* these errors can be safely ignored.
|
|
237
|
+
*
|
|
238
|
+
* The stream is an [`AsyncGenerator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function)
|
|
239
|
+
* that can be iterated over using `for await` loops or calling `next` on the generator.
|
|
240
|
+
* The stream can be terminated by breaking out of a loop calling `return` on the generator.
|
|
241
|
+
*/
|
|
242
|
+
export type GraffitiStream<T> = AsyncGenerator<
|
|
243
|
+
| {
|
|
244
|
+
error: false;
|
|
245
|
+
value: T;
|
|
246
|
+
}
|
|
247
|
+
| {
|
|
248
|
+
error: true;
|
|
249
|
+
value: Error;
|
|
250
|
+
source: string;
|
|
251
|
+
},
|
|
252
|
+
void,
|
|
253
|
+
void
|
|
254
|
+
>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { it, expect } from "vitest";
|
|
2
|
+
import type { UseGraffiti } from "../src/index";
|
|
3
|
+
|
|
4
|
+
export const locationTests = (useGraffiti: UseGraffiti) => {
|
|
5
|
+
it("url and location", async () => {
|
|
6
|
+
const graffiti = useGraffiti();
|
|
7
|
+
const location = {
|
|
8
|
+
name: "12345",
|
|
9
|
+
actor: "https://example.com/actor",
|
|
10
|
+
source: "https://example.com/source",
|
|
11
|
+
};
|
|
12
|
+
const uri = graffiti.locationToUri(location);
|
|
13
|
+
const location2 = graffiti.uriToLocation(uri);
|
|
14
|
+
expect(location).toEqual(location2);
|
|
15
|
+
});
|
|
16
|
+
};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "esnext",
|
|
4
|
+
"module": "esnext",
|
|
5
|
+
"lib": ["esnext", "dom"],
|
|
6
|
+
"moduleResolution": "node",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"resolveJsonModule": true,
|
|
10
|
+
"verbatimModuleSyntax": true
|
|
11
|
+
},
|
|
12
|
+
"include": ["src"]
|
|
13
|
+
}
|