@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 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
@@ -0,0 +1,3 @@
1
+ export * from "./types";
2
+ export type { JSONSchema4 } from "json-schema";
3
+ export * from "./api";
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
+ }