@graffiti-garden/api 0.0.4 → 0.0.5

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 CHANGED
@@ -20,3 +20,28 @@ Then run a local server to view the documentation:
20
20
  cd docs
21
21
  npx http-server
22
22
  ```
23
+
24
+ ## Testing
25
+
26
+ We have written a number of unit tests to verify implementations of the API with [vitest](https://vitest.dev/).
27
+ To use them, create a test file in that ends in `*.spec.ts` and format it as follows:
28
+
29
+ ```typescript
30
+ import { graffitiCRUDTests } from "@graffiti-garden/api/tests";
31
+
32
+ const useGraffiti = () => new MyGraffitiImplementation();
33
+ // Fill in with implementation-specific information
34
+ // to provide to valid actor sessions for the tests
35
+ // to use as identities.
36
+ const useSession1 = () => ({ actor: "someone" });
37
+ const useSession2 = () => ({ actor: "someoneelse" });
38
+
39
+ // Run the tests
40
+ graffitiCRUDTests(useGraffiti, useSession1, useSession2);
41
+ ```
42
+
43
+ Then run the tests in the root of your directory with:
44
+
45
+ ```bash
46
+ npx vitest
47
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graffiti-garden/api",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "The heart of Graffiti",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
package/src/1-api.ts CHANGED
@@ -3,7 +3,7 @@ import type {
3
3
  GraffitiObject,
4
4
  GraffitiObjectBase,
5
5
  GraffitiPatch,
6
- GraffitiSessionBase,
6
+ GraffitiSession,
7
7
  GraffitiPutObject,
8
8
  GraffitiStream,
9
9
  } from "./2-types";
@@ -26,10 +26,10 @@ import type { JSONSchema4 } from "json-schema";
26
26
  * other important properties (e.g. privacy, security, scalability), those properties
27
27
  * are useless if the system as a whole is unusable. Build APIs before protocols!
28
28
  *
29
- * The first group of methods are like standard CRUD operations that
29
+ * The first group of methods are like standard CRUD methods that
30
30
  * allow applications to {@link put}, {@link get}, {@link patch}, and {@link delete}
31
31
  * {@link GraffitiObjectBase} objects. The main difference between these
32
- * methods and standard database operations is that an {@link GraffitiObjectBase.actor | `actor`}
32
+ * methods and standard database methods is that an {@link GraffitiObjectBase.actor | `actor`}
33
33
  * (essentially a user) can only modify objects that they created.
34
34
  * Applications may also specify an an array of actors that are {@link GraffitiObjectBase.allowed | `allowed`}
35
35
  * to access the object and an array of {@link GraffitiObjectBase.channels | `channels`}
@@ -51,11 +51,13 @@ import type { JSONSchema4 } from "json-schema";
51
51
  * Finally, other utility functions provide simple type conversions and
52
52
  * allow users to find objects "lost" to forgotten or misspelled channels.
53
53
  *
54
- * @groupDescription CRUD Operations
54
+ * @groupDescription CRUD Methods
55
55
  * Methods for {@link put | creating}, {@link get | reading}, {@link patch | updating},
56
56
  * and {@link delete | deleting} {@link GraffitiObjectBase | Graffiti objects}.
57
- * @groupDescription Query Operations
57
+ * @groupDescription Query Methods
58
58
  * Methods for retrieving multiple {@link GraffitiObjectBase | Graffiti objects} at a time.
59
+ * @groupDescription Session Management
60
+ * Methods and properties for logging in and out of a Graffiti implementation.
59
61
  * @groupDescription Utilities
60
62
  * Methods for for converting Graffiti objects to and from URIs
61
63
  * and for finding lost objects.
@@ -103,27 +105,27 @@ export abstract class Graffiti {
103
105
  * and {@link GraffitiObjectBase.source | `source`}) exactly match the location of an existing object.
104
106
  *
105
107
  * @returns The object that was replaced if one exists or an object with
106
- * with a `null` {@link GraffitiObjectBase.value | `value`} if this operation
108
+ * with a `null` {@link GraffitiObjectBase.value | `value`} if this method
107
109
  * created a new object.
108
110
  * The object will have a {@link GraffitiObjectBase.tombstone | `tombstone`}
109
111
  * field set to `true` and a {@link GraffitiObjectBase.lastModified | `lastModified`}
110
112
  * field updated to the time of replacement/creation.
111
113
  *
112
- * @group CRUD Operations
114
+ * @group CRUD Methods
113
115
  */
114
116
  abstract put<Schema>(
115
117
  /**
116
118
  * 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
119
  * as the generic type parameter. We highly recommend providing a schema to
118
120
  * ensure that the PUT object matches subsequent {@link get} or {@link discover}
119
- * operations.
121
+ * methods.
120
122
  */
121
123
  object: GraffitiPutObject<Schema>,
122
124
  /**
123
125
  * An implementation-specific object with information to authenticate the
124
126
  * {@link GraffitiObjectBase.actor | `actor`}.
125
127
  */
126
- session: GraffitiSessionBase,
128
+ session: GraffitiSession,
127
129
  ): Promise<GraffitiObjectBase>;
128
130
 
129
131
  /**
@@ -134,9 +136,9 @@ export abstract class Graffiti {
134
136
  * a {@link GraffitiErrorNotFound} is thrown.
135
137
  *
136
138
  * The retrieved object is also type-checked against the provided [JSON schema](https://json-schema.org/)
137
- * otherwise an error is thrown.
139
+ * otherwise a {@link GraffitiErrorSchemaMismatch} is thrown.
138
140
  *
139
- * @group CRUD Operations
141
+ * @group CRUD Methods
140
142
  */
141
143
  abstract get<Schema extends JSONSchema4>(
142
144
  /**
@@ -153,7 +155,7 @@ export abstract class Graffiti {
153
155
  * the retrieved object's {@link GraffitiObjectBase.allowed | `allowed`}
154
156
  * property must be `undefined`.
155
157
  */
156
- session?: GraffitiSessionBase,
158
+ session?: GraffitiSession,
157
159
  ): Promise<GraffitiObject<Schema>>;
158
160
 
159
161
  /**
@@ -167,7 +169,7 @@ export abstract class Graffiti {
167
169
  * field set to `true` and a {@link GraffitiObjectBase.lastModified | `lastModified`}
168
170
  * field updated to the time of deletion.
169
171
  *
170
- * @group CRUD Operations
172
+ * @group CRUD Methods
171
173
  */
172
174
  abstract patch(
173
175
  /**
@@ -183,7 +185,7 @@ export abstract class Graffiti {
183
185
  * An implementation-specific object with information to authenticate the
184
186
  * {@link GraffitiObjectBase.actor | `actor`}.
185
187
  */
186
- session: GraffitiSessionBase,
188
+ session: GraffitiSession,
187
189
  ): Promise<GraffitiObjectBase>;
188
190
 
189
191
  /**
@@ -191,7 +193,7 @@ export abstract class Graffiti {
191
193
  * The deleting {@link GraffitiObjectBase.actor | `actor`} must be the same as the
192
194
  * `actor` that created the object.
193
195
  *
194
- * If the object does not exist or has already been deleted, a
196
+ * If the object does not exist or has already been deleted,
195
197
  * {@link GraffitiErrorNotFound} is thrown.
196
198
  *
197
199
  * @returns The object that was deleted if one exists or an object with
@@ -200,7 +202,7 @@ export abstract class Graffiti {
200
202
  * field set to `true` and a {@link GraffitiObjectBase.lastModified | `lastModified`}
201
203
  * field updated to the time of deletion.
202
204
  *
203
- * @group CRUD Operations
205
+ * @group CRUD Methods
204
206
  */
205
207
  abstract delete(
206
208
  /**
@@ -211,7 +213,7 @@ export abstract class Graffiti {
211
213
  * An implementation-specific object with information to authenticate the
212
214
  * {@link GraffitiObjectBase.actor | `actor`}.
213
215
  */
214
- session: GraffitiSessionBase,
216
+ session: GraffitiSession,
215
217
  ): Promise<GraffitiObjectBase>;
216
218
 
217
219
  /**
@@ -229,11 +231,11 @@ export abstract class Graffiti {
229
231
  * the allowed list will be masked to only contain the querying actor if the
230
232
  * allowed list is not `undefined` (public). Additionally, if the actor is not the
231
233
  * creator of a discovered object, any {@link GraffitiObjectBase.channels | `channels`}
232
- * not specified by the `discover` operation will not be revealed. This masking happens
234
+ * not specified by the `discover` method will not be revealed. This masking happens
233
235
  * before the supplied schema is applied.
234
236
  *
235
237
  * Since different implementations may fetch data from multiple sources there is
236
- * no guarentee on the order that objects are returned in. Additionally, the operation
238
+ * no guarentee on the order that objects are returned in. Additionally, the method
237
239
  * may return objects that have been deleted but with a
238
240
  * {@link GraffitiObjectBase.tombstone | `tombstone`} field set to `true` for
239
241
  * cache invalidation purposes. Implementations must make aware when, if ever,
@@ -245,7 +247,7 @@ export abstract class Graffiti {
245
247
  * @returns A stream of objects that match the given {@link GraffitiObjectBase.channels | `channels`}
246
248
  * and [JSON Schema](https://json-schema.org).
247
249
  *
248
- * @group Query Operations
250
+ * @group Query Methods
249
251
  */
250
252
  abstract discover<Schema extends JSONSchema4>(
251
253
  /**
@@ -262,7 +264,7 @@ export abstract class Graffiti {
262
264
  * only objects that have no {@link GraffitiObjectBase.allowed | `allowed`}
263
265
  * property will be returned.
264
266
  */
265
- session?: GraffitiSessionBase,
267
+ session?: GraffitiSession,
266
268
  ): GraffitiStream<GraffitiObject<Schema>>;
267
269
 
268
270
  /**
@@ -286,7 +288,7 @@ export abstract class Graffiti {
286
288
  * all instance's of that friend's name in the user's application instantly,
287
289
  * providing a consistent user experience.
288
290
  *
289
- * @group Query Operations
291
+ * @group Query Methods
290
292
  */
291
293
  abstract synchronize<Schema extends JSONSchema4>(
292
294
  /**
@@ -303,7 +305,7 @@ export abstract class Graffiti {
303
305
  * only objects that have no {@link GraffitiObjectBase.allowed | `allowed`}
304
306
  * property will be returned.
305
307
  */
306
- session?: GraffitiSessionBase,
308
+ session?: GraffitiSession,
307
309
  ): GraffitiStream<GraffitiObject<Schema>>;
308
310
 
309
311
  /**
@@ -327,7 +329,7 @@ export abstract class Graffiti {
327
329
  * An implementation-specific object with information to authenticate the
328
330
  * {@link GraffitiObjectBase.actor | `actor`}.
329
331
  */
330
- session: GraffitiSessionBase,
332
+ session: GraffitiSession,
331
333
  ): GraffitiStream<{
332
334
  channel: string;
333
335
  source: string;
@@ -353,12 +355,77 @@ export abstract class Graffiti {
353
355
  * {@link GraffitiObjectBase.tombstone | `tombstone`} field is `true`
354
356
  * if the object has been deleted.
355
357
  */
356
- abstract listOrphans(session: GraffitiSessionBase): GraffitiStream<{
358
+ abstract listOrphans(session: GraffitiSession): GraffitiStream<{
357
359
  name: string;
358
360
  source: string;
359
361
  lastModified: Date;
360
362
  tombstone: boolean;
361
363
  }>;
364
+
365
+ /**
366
+ * Begins the login process. Depending on the implementation, this may
367
+ * involve redirecting the user to a login page or opening a popup,
368
+ * so it should always be called in response to a user action.
369
+ *
370
+ * The {@link GraffitiSession | session} object is returned
371
+ * asynchronously via {@link Graffiti.sessionEvents | sessionEvents}
372
+ * as a {@link GraffitiLoginEvent}.
373
+ *
374
+ * @group Session Management
375
+ */
376
+ abstract login(
377
+ /**
378
+ * An optional actor to prompt the user to login as. For example,
379
+ * if a session expired and the user is trying to reauthenticate,
380
+ * or if the user entered their username in an application-side login form.
381
+ *
382
+ * If not provided, the implementation should prompt the user to
383
+ * supply an actor ID along with their other login information
384
+ * (e.g. password).
385
+ */
386
+ actor?: string,
387
+ /**
388
+ * An arbitrary string that will be returned with the
389
+ * {@link GraffitiSession | session} object
390
+ * when the login process is complete.
391
+ * See {@link GraffitiLoginEvent}.
392
+ */
393
+ state?: string,
394
+ ): Promise<void>;
395
+
396
+ /**
397
+ * Begins the logout process. Depending on the implementation, this may
398
+ * involve redirecting the user to a logout page or opening a popup,
399
+ * so it should always be called in response to a user action.
400
+ *
401
+ * A confirmation will be returned asynchronously via
402
+ * {@link Graffiti.sessionEvents | sessionEvents}
403
+ * as a {@link GraffitiLogoutEvent}.
404
+ *
405
+ * @group Session Management
406
+ */
407
+ abstract logout(
408
+ /**
409
+ * The {@link GraffitiSession | session} object to logout.
410
+ */
411
+ session: GraffitiSession,
412
+ /**
413
+ * An arbitrary string that will be returned with the
414
+ * when the logout process is complete.
415
+ * See {@link GraffitiLogoutEvent}.
416
+ */
417
+ state?: string,
418
+ ): Promise<void>;
419
+
420
+ /**
421
+ * An event target that can be used to listen for `login`
422
+ * and `logout` events. They are custom events of types
423
+ * {@link GraffitiLoginEvent`} and {@link GraffitiLogoutEvent }
424
+ * respectively.
425
+ *
426
+ * @group Session Management
427
+ */
428
+ abstract readonly sessionEvents: EventTarget;
362
429
  }
363
430
 
364
431
  /**
package/src/2-types.ts CHANGED
@@ -146,7 +146,7 @@ export type GraffitiLocation = Pick<
146
146
  * {@link GraffitiObjectBase.name | `name`}, and {@link GraffitiObjectBase.source | `source`}.
147
147
  * If the location provided exactly matches an existing object, the existing object will be replaced.
148
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.
149
+ * If no `actor` is provided, the `actor` from the supplied {@link GraffitiSession | `session` } will be used.
150
150
  * If no `source` is provided, one may be inferred by the depending on implementation.
151
151
  *
152
152
  * This object does not need a {@link GraffitiObjectBase.lastModified | `lastModified`} or {@link GraffitiObjectBase.tombstone | `tombstone`}
@@ -168,7 +168,7 @@ export type GraffitiPutObject<Schema> = Pick<
168
168
  * that modify objects and optional for methods that read objects.
169
169
  *
170
170
  * At a minimum the `session` object must contain the
171
- * {@link GraffitiSessionBase.actor | `actor`} URI the user wants to authenticate with.
171
+ * {@link GraffitiSession.actor | `actor`} URI the user wants to authenticate with.
172
172
  * However it is likely that the `session` object must contain other
173
173
  * implementation-specific properties.
174
174
  * For example, a Solid implementation might include a
@@ -179,7 +179,7 @@ export type GraffitiPutObject<Schema> = Pick<
179
179
  * It may also include other implementation specific properties
180
180
  * that provide hints for performance or security.
181
181
  */
182
- export interface GraffitiSessionBase {
182
+ export interface GraffitiSession {
183
183
  /**
184
184
  * The {@link GraffitiObjectBase.actor | `actor`} a user wants to authenticate with.
185
185
  */
@@ -241,14 +241,45 @@ export interface GraffitiPatch {
241
241
  */
242
242
  export type GraffitiStream<T> = AsyncGenerator<
243
243
  | {
244
- error: false;
244
+ error?: undefined;
245
245
  value: T;
246
246
  }
247
247
  | {
248
- error: true;
249
- value: Error;
248
+ error: Error;
250
249
  source: string;
251
250
  },
252
- void,
253
251
  void
254
252
  >;
253
+
254
+ /**
255
+ * The event type produced in {@link Graffiti.sessionEvents}
256
+ * when a user logs in manually from {@link Graffiti.login}
257
+ * or when their session is restored from a previous login.
258
+ * The event name to listen for is `login`.
259
+ */
260
+ export type GraffitiLoginEvent = CustomEvent<
261
+ {
262
+ state?: string;
263
+ } & (
264
+ | {
265
+ error: Error;
266
+ session?: undefined;
267
+ }
268
+ | {
269
+ error?: undefined;
270
+ session: GraffitiSession;
271
+ }
272
+ )
273
+ >;
274
+
275
+ /**
276
+ * The event type produced in {@link Graffiti.sessionEvents}
277
+ * when a user logs out either manually with {@link Graffiti.logout}
278
+ * or when their session times out or otherwise becomes invalid.
279
+ * The event name to listen for is `logout`.
280
+ */
281
+ export type GraffitiLogoutEvent = CustomEvent<{
282
+ actor: string;
283
+ state?: string;
284
+ error?: Error;
285
+ }>;
package/src/3-errors.ts CHANGED
@@ -1,7 +1,63 @@
1
+ export class GraffitiErrorUnauthorized extends Error {
2
+ constructor(message?: string) {
3
+ super(message);
4
+ this.name = "GraffitiErrorUnauthorized";
5
+ Object.setPrototypeOf(this, GraffitiErrorUnauthorized.prototype);
6
+ }
7
+ }
8
+
9
+ export class GraffitiErrorForbidden extends Error {
10
+ constructor(message?: string) {
11
+ super(message);
12
+ this.name = "GraffitiErrorForbidden";
13
+ Object.setPrototypeOf(this, GraffitiErrorForbidden.prototype);
14
+ }
15
+ }
16
+
1
17
  export class GraffitiErrorNotFound extends Error {
2
- constructor(message: string) {
18
+ constructor(message?: string) {
3
19
  super(message);
4
20
  this.name = "GraffitiErrorNotFound";
5
21
  Object.setPrototypeOf(this, GraffitiErrorNotFound.prototype);
6
22
  }
7
23
  }
24
+
25
+ export class GraffitiErrorInvalidSchema extends Error {
26
+ constructor(message?: string) {
27
+ super(message);
28
+ this.name = "GraffitiErrorInvalidSchema";
29
+ Object.setPrototypeOf(this, GraffitiErrorInvalidSchema.prototype);
30
+ }
31
+ }
32
+
33
+ export class GraffitiErrorSchemaMismatch extends Error {
34
+ constructor(message?: string) {
35
+ super(message);
36
+ this.name = "GraffitiErrorSchemaMismatch";
37
+ Object.setPrototypeOf(this, GraffitiErrorSchemaMismatch.prototype);
38
+ }
39
+ }
40
+
41
+ export class GraffitiErrorPatchTestFailed extends Error {
42
+ constructor(message?: string) {
43
+ super(message);
44
+ this.name = "GraffitiErrorPatchTestFailed";
45
+ Object.setPrototypeOf(this, GraffitiErrorPatchTestFailed.prototype);
46
+ }
47
+ }
48
+
49
+ export class GraffitiErrorPatchError extends Error {
50
+ constructor(message?: string) {
51
+ super(message);
52
+ this.name = "GraffitiErrorPatchError";
53
+ Object.setPrototypeOf(this, GraffitiErrorPatchError.prototype);
54
+ }
55
+ }
56
+
57
+ export class GraffitiErrorInvalidUri extends Error {
58
+ constructor(message?: string) {
59
+ super(message);
60
+ this.name = "GraffitiErrorInvalidUri";
61
+ Object.setPrototypeOf(this, GraffitiErrorInvalidUri.prototype);
62
+ }
63
+ }