@graffiti-garden/api 0.2.0 → 0.2.2

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/src/1-api.ts CHANGED
@@ -6,6 +6,7 @@ import type {
6
6
  GraffitiSession,
7
7
  GraffitiPutObject,
8
8
  GraffitiStream,
9
+ ChannelStats,
9
10
  } from "./2-types";
10
11
  import type { JSONSchema4 } from "json-schema";
11
12
 
@@ -23,6 +24,7 @@ import type { JSONSchema4 } from "json-schema";
23
24
  *
24
25
  * There are several different implementations of this Graffiti API available,
25
26
  * including a [federated implementation](https://github.com/graffiti-garden/implementation-federated),
27
+ * that lets users choose where their data is stored,
26
28
  * and a [local implementation](https://github.com/graffiti-garden/implementation-local)
27
29
  * that can be used for testing and development. In our design of Graffiti, this API is our
28
30
  * primary focus as it is the layer that shapes the experience
@@ -36,22 +38,28 @@ import type { JSONSchema4 } from "json-schema";
36
38
  *
37
39
  * ## Overview
38
40
  *
39
- * This API tries to draw from well-known concepts and standards wherever possible.
40
- * JSON objects, representing social artifacts (e.g. posts, profiles) and activities
41
- * (e.g. likes, follows) can be interacted with through standard CRUD operations:
41
+ * Graffiti provides applications with methods to create and store data
42
+ * on behalf of their users using standard CRUD operations:
42
43
  * {@link put}, {@link get}, {@link patch}, and {@link delete}.
43
- * Objects can be typed with [JSON Schema](https://json-schema.org/) and patches
44
- * can be applied with [JSON Patch](https://jsonpatch.com).
45
- * For interoperability between Graffiti applications, we recommend using established properties from the
46
- * [Activity Vocabulary](https://www.w3.org/TR/activitystreams-vocabulary/) when available.
44
+ * This data can represent both social artifacts (e.g. posts, profiles) and
45
+ * activities (e.g. likes, follows) and is stored as JSON.
47
46
  *
48
- * The social aspect of Graffiti comes from the {@link discover} operation
47
+ * The social aspect of Graffiti comes from the {@link discover} method
49
48
  * which allows applications to find objects that other users made.
50
49
  * It is a lot like a traditional query operation, but it only
51
50
  * returns objects that have been placed in particular
52
51
  * {@link GraffitiObjectBase.channels | `channels`}
53
52
  * specified by the discovering application.
54
53
  *
54
+ * Graffiti builds on well known concepts and standards wherever possible.
55
+ * JSON Objects can be typed with [JSON Schema](https://json-schema.org/) and patches
56
+ * can be applied with [JSON Patch](https://jsonpatch.com).
57
+ * For interoperability between Graffiti applications, we recommend that
58
+ * objects use established properties from the
59
+ * [Activity Vocabulary](https://www.w3.org/TR/activitystreams-vocabulary/) when available,
60
+ * however it is always possible to create additional properties, contributing
61
+ * to the broader [folksonomy](https://en.wikipedia.org/wiki/Folksonomy).
62
+ *
55
63
  * {@link GraffitiObjectBase.channels | `channels`} are one of the major concepts
56
64
  * unique to Graffiti along with *interaction relativity*.
57
65
  * Channels create boundaries between public spaces and work to prevent
@@ -231,19 +239,17 @@ import type { JSONSchema4 } from "json-schema";
231
239
  *
232
240
  * ## TODO
233
241
  *
234
- * - Test for listChannels and listOrphans,
235
242
  * - Implement scope.
236
243
  *
237
244
  * @groupDescription CRUD Methods
238
245
  * Methods for {@link put | creating}, {@link get | reading}, {@link patch | updating},
239
246
  * and {@link delete | deleting} {@link GraffitiObjectBase | Graffiti objects}.
240
247
  * @groupDescription Query Methods
241
- * Methods for retrieving multiple {@link GraffitiObjectBase | Graffiti objects} at a time.
248
+ * Methods that retrieve or accumulate information about multiple {@link GraffitiObjectBase | Graffiti objects} at a time.
242
249
  * @groupDescription Session Management
243
250
  * Methods and properties for logging in and out of a Graffiti implementation.
244
251
  * @groupDescription Utilities
245
- * Methods for for converting Graffiti objects to and from URIs
246
- * and for finding lost objects.
252
+ * Methods for for converting Graffiti objects to and from URIs.
247
253
  */
248
254
  export abstract class Graffiti {
249
255
  /**
@@ -313,14 +319,26 @@ export abstract class Graffiti {
313
319
 
314
320
  /**
315
321
  * Retrieves an object from a given location.
316
- * If no object exists at that location or if the retrieving
317
- * {@link GraffitiObjectBase.actor | `actor`} is not the creator or included in
318
- * the object's {@link GraffitiObjectBase.allowed | `allowed`} property,
319
- * a {@link GraffitiErrorNotFound} is thrown.
320
322
  *
321
- * The retrieved object is also type-checked against the provided [JSON schema](https://json-schema.org/)
323
+ * The retrieved object is type-checked against the provided [JSON schema](https://json-schema.org/)
322
324
  * otherwise a {@link GraffitiErrorSchemaMismatch} is thrown.
323
325
  *
326
+ * If the object existed but has since been deleted,
327
+ * or the retrieving {@link GraffitiObjectBase.actor | `actor`}
328
+ * was {@link GraffitiObjectBase.allowed | `allowed`} to access
329
+ * the object but now isn't, this method will return the latest
330
+ * version of the object that the {@link GraffitiObjectBase.actor | `actor`}
331
+ * was allowed to access with its {@link GraffitiObjectBase.tombstone | `tombstone`}
332
+ * set to `true`, so long as that version is still cached.
333
+ *
334
+ * Otherwise, if the object never existed, or the
335
+ * retrieving {@link GraffitiObjectBase.actor | `actor`} was never
336
+ * {@link GraffitiObjectBase.allowed | `allowed`} to access it, or if
337
+ * the object was changed long enough ago that its history has been
338
+ * purged from the cache, a {@link GraffitiErrorNotFound} is thrown.
339
+ * The rate at which the cache is purged is implementation dependent.
340
+ * See the `tombstoneReturn` property returned by {@link discover}.
341
+ *
324
342
  * @group CRUD Methods
325
343
  */
326
344
  abstract get<Schema extends JSONSchema4>(
@@ -417,7 +435,7 @@ export abstract class Graffiti {
417
435
  * not specified by the `discover` method will not be revealed. This masking happens
418
436
  * before the supplied schema is applied.
419
437
  *
420
- * {@link discover} can be used in conjunction with {@link synchronize}
438
+ * {@link discover} can be used in conjunction with {@link synchronizeDiscover}
421
439
  * to provide a responsive and consistent user experience.
422
440
  *
423
441
  * Since different implementations may fetch data from multiple sources there is
@@ -487,129 +505,57 @@ export abstract class Graffiti {
487
505
  >;
488
506
 
489
507
  /**
490
- * This method has the same signature as {@link discover} but listens for
491
- * changes made via {@link put}, {@link patch}, and {@link delete} or
492
- * fetched from {@link get} or {@link discover} and then streams appropriate
493
- * changes to provide a responsive and consistent user experience.
494
- *
495
- * Unlike {@link discover}, this method continuously listens for changes
496
- * and will not terminate unless the user calls the `return` method on the iterator
497
- * or `break`s out of the loop.
498
- *
499
- * Example 1: Suppose a user publishes a post using {@link put}. If the feed
500
- * displaying that user's posts is using {@link synchronize} to listen for changes,
501
- * then the user's new post will instantly appear in their feed, giving the UI a
502
- * responsive feel.
503
- *
504
- * Example 2: Suppose one of a user's friends changes their name. As soon as the
505
- * user's application receives one notice of that change (using {@link get}
506
- * or {@link discover}), then {@link synchronize} listeners can be used to update
507
- * all instance's of that friend's name in the user's application instantly,
508
- * providing a consistent user experience.
508
+ * Discovers objects **not** contained in any
509
+ * {@link GraffitiObjectBase.channels | `channels`}
510
+ * that were created by the querying {@link GraffitiObjectBase.actor | `actor`}
511
+ * and match the given [JSON Schema](https://json-schema.org).
512
+ * Unlike {@link discover}, this method will not return objects created by other users.
509
513
  *
510
- * @group Synchronize Methods
511
- */
512
- abstract synchronizeDiscover<Schema extends JSONSchema4>(
513
- /**
514
- * The {@link GraffitiObjectBase.channels | `channels`} that the objects must be associated with.
515
- */
516
- channels: string[],
517
- /**
518
- * A [JSON Schema](https://json-schema.org) that objects must satisfy.
519
- */
520
- schema: Schema,
521
- /**
522
- * An implementation-specific object with information to authenticate the
523
- * {@link GraffitiObjectBase.actor | `actor`}. If no `session` is provided,
524
- * only objects that have no {@link GraffitiObjectBase.allowed | `allowed`}
525
- * property will be returned.
526
- */
527
- session?: GraffitiSession | null,
528
- ): GraffitiStream<GraffitiObject<Schema>>;
529
-
530
- /**
531
- * This method has the same signature as {@link get} but, like {@link synchronizeDiscover},
532
- * it listens for changes made via {@link put}, {@link patch}, and {@link delete} or
533
- * fetched from {@link get} or {@link discover} and then streams appropriate
534
- * changes to provide a responsive and consistent user experience.
514
+ * This method is not useful for most applications, but necessary for
515
+ * getting a global view of all a user's Graffiti data or debugging
516
+ * channel usage.
535
517
  *
536
- * Unlike {@link get}, which returns a single result, this method continuously
537
- * listens for changes which are output as an asynchronous {@link GraffitiStream}.
518
+ * It's return value is the same as {@link discover}.
538
519
  *
539
- * @group Synchronize Methods
520
+ * @group Query Methods
540
521
  */
541
- abstract synchronizeGet<Schema extends JSONSchema4>(
542
- /**
543
- * The location of the object to get.
544
- */
545
- locationOrUri: GraffitiLocation | string,
522
+ abstract recoverOrphans<Schema extends JSONSchema4>(
546
523
  /**
547
- * The JSON schema to validate the retrieved object against.
524
+ * A [JSON Schema](https://json-schema.org) that orphaned objects must satisfy.
548
525
  */
549
526
  schema: Schema,
550
527
  /**
551
528
  * An implementation-specific object with information to authenticate the
552
- * {@link GraffitiObjectBase.actor | `actor`}. If no `session` is provided,
553
- * the retrieved object's {@link GraffitiObjectBase.allowed | `allowed`}
554
- * property must be `undefined`.
529
+ * {@link GraffitiObjectBase.actor | `actor`}.
555
530
  */
556
- session?: GraffitiSession | null,
557
- ): GraffitiStream<GraffitiObject<Schema>>;
531
+ session: GraffitiSession,
532
+ ): GraffitiStream<
533
+ GraffitiObject<Schema>,
534
+ {
535
+ tombstoneRetention: number;
536
+ }
537
+ >;
558
538
 
559
539
  /**
560
- * Returns a list of all {@link GraffitiObjectBase.channels | `channels`}
540
+ * Returns statistics about all the {@link GraffitiObjectBase.channels | `channels`}
561
541
  * that an {@link GraffitiObjectBase.actor | `actor`} has posted to.
562
542
  * This is not very useful for most applications, but
563
543
  * necessary for certain applications where a user wants a
564
544
  * global view of all their Graffiti data or to debug
565
545
  * channel usage.
566
546
  *
567
- * @group Utilities
547
+ * @group Query Methods
568
548
  *
569
- * @returns A stream the {@link GraffitiObjectBase.channels | `channel`}s
549
+ * @returns A stream of statistics for each {@link GraffitiObjectBase.channels | `channel`}
570
550
  * that the {@link GraffitiObjectBase.actor | `actor`} has posted to.
571
- * The `lastModified` field is the time that the user last modified an
572
- * object in that channel. The `count` field is the number of objects
573
- * that the user has posted to that channel.
574
551
  */
575
- abstract listChannels(
552
+ abstract channelStats(
576
553
  /**
577
554
  * An implementation-specific object with information to authenticate the
578
555
  * {@link GraffitiObjectBase.actor | `actor`}.
579
556
  */
580
557
  session: GraffitiSession,
581
- ): GraffitiStream<{
582
- channel: string;
583
- lastModified: number;
584
- count: number;
585
- }>;
586
-
587
- /**
588
- * Returns a list of all {@link GraffitiObjectBase | objects} a user has posted that are
589
- * not associated with any {@link GraffitiObjectBase.channels | `channel`}, i.e. orphaned objects.
590
- * This is not very useful for most applications, but
591
- * necessary for certain applications where a user wants a
592
- * global view of all their Graffiti data or to debug
593
- * channel usage.
594
- *
595
- * @group Utilities
596
- *
597
- * @returns A stream of the {@link GraffitiObjectBase.name | `name`}
598
- * and {@link GraffitiObjectBase.source | `source`} of the orphaned objects
599
- * that the {@link GraffitiObjectBase.actor | `actor`} has posted to.
600
- * The {@link GraffitiObjectBase.lastModified | lastModified} field is the
601
- * time that the user last modified the orphan.
602
- */
603
- abstract listOrphans(session: GraffitiSession): GraffitiStream<{
604
- name: string;
605
- source: string;
606
- lastModified: string;
607
- }>;
608
-
609
- /**
610
- * The age at which a query for a session will be considered expired.
611
- */
612
- // abstract readonly maxAge: number;
558
+ ): GraffitiStream<ChannelStats>;
613
559
 
614
560
  /**
615
561
  * Begins the login process. Depending on the implementation, this may
@@ -682,6 +628,102 @@ export abstract class Graffiti {
682
628
  * @group Session Management
683
629
  */
684
630
  abstract readonly sessionEvents: EventTarget;
631
+
632
+ /**
633
+ * This method has the same signature as {@link discover} but listens for
634
+ * changes made via {@link put}, {@link patch}, and {@link delete} or
635
+ * fetched from {@link get}, {@link discover}, and {@link recoverOrphans}
636
+ * and then streams appropriate changes to provide a responsive and
637
+ * consistent user experience.
638
+ *
639
+ * Unlike {@link discover}, this method continuously listens for changes
640
+ * and will not terminate unless the user calls the `return` method on the iterator
641
+ * or `break`s out of the loop.
642
+ *
643
+ * Example 1: Suppose a user publishes a post using {@link put}. If the feed
644
+ * displaying that user's posts is using {@link synchronizeDiscover} to listen for changes,
645
+ * then the user's new post will instantly appear in their feed, giving the UI a
646
+ * responsive feel.
647
+ *
648
+ * Example 2: Suppose one of a user's friends changes their name. As soon as the
649
+ * user's application receives one notice of that change (using {@link get}
650
+ * or {@link discover}), then {@link synchronizeDiscover} listeners can be used to update
651
+ * all instance's of that friend's name in the user's application instantly,
652
+ * providing a consistent user experience.
653
+ *
654
+ * @group Synchronize Methods
655
+ */
656
+ abstract synchronizeDiscover<Schema extends JSONSchema4>(
657
+ /**
658
+ * The {@link GraffitiObjectBase.channels | `channels`} that the objects must be associated with.
659
+ */
660
+ channels: string[],
661
+ /**
662
+ * A [JSON Schema](https://json-schema.org) that objects must satisfy.
663
+ */
664
+ schema: Schema,
665
+ /**
666
+ * An implementation-specific object with information to authenticate the
667
+ * {@link GraffitiObjectBase.actor | `actor`}. If no `session` is provided,
668
+ * only objects that have no {@link GraffitiObjectBase.allowed | `allowed`}
669
+ * property will be returned.
670
+ */
671
+ session?: GraffitiSession | null,
672
+ ): GraffitiStream<GraffitiObject<Schema>>;
673
+
674
+ /**
675
+ * This method has the same signature as {@link get} but, like {@link synchronizeDiscover},
676
+ * it listens for changes made via {@link put}, {@link patch}, and {@link delete} or
677
+ * fetched from {@link get}, {@link discover}, and {@link recoverOrphans} and then
678
+ * streams appropriate changes to provide a responsive and consistent user experience.
679
+ *
680
+ * Unlike {@link get}, which returns a single result, this method continuously
681
+ * listens for changes which are output as an asynchronous {@link GraffitiStream}.
682
+ *
683
+ * @group Synchronize Methods
684
+ */
685
+ abstract synchronizeGet<Schema extends JSONSchema4>(
686
+ /**
687
+ * The location of the object to get.
688
+ */
689
+ locationOrUri: GraffitiLocation | string,
690
+ /**
691
+ * The JSON schema to validate the retrieved object against.
692
+ */
693
+ schema: Schema,
694
+ /**
695
+ * An implementation-specific object with information to authenticate the
696
+ * {@link GraffitiObjectBase.actor | `actor`}. If no `session` is provided,
697
+ * the retrieved object's {@link GraffitiObjectBase.allowed | `allowed`}
698
+ * property must be `undefined`.
699
+ */
700
+ session?: GraffitiSession | null,
701
+ ): GraffitiStream<GraffitiObject<Schema>>;
702
+
703
+ /**
704
+ * This method has the same signature as {@link recoverOrphans} but,
705
+ * like {@link synchronizeDiscover}, it listens for changes made via
706
+ * {@link put}, {@link patch}, and {@link delete} or fetched from
707
+ * {@link get}, {@link discover}, and {@link recoverOrphans} and then
708
+ * streams appropriate changes to provide a responsive and consistent user experience.
709
+ *
710
+ * Unlike {@link recoverOrphans}, this method continuously listens for changes
711
+ * and will not terminate unless the user calls the `return` method on the iterator
712
+ * or `break`s out of the loop.
713
+ *
714
+ * @group Synchronize Methods
715
+ */
716
+ abstract synchronizeRecoverOrphans<Schema extends JSONSchema4>(
717
+ /**
718
+ * A [JSON Schema](https://json-schema.org) that orphaned objects must satisfy.
719
+ */
720
+ schema: Schema,
721
+ /**
722
+ * An implementation-specific object with information to authenticate the
723
+ * {@link GraffitiObjectBase.actor | `actor`}.
724
+ */
725
+ session: GraffitiSession,
726
+ ): GraffitiStream<GraffitiObject<Schema>>;
685
727
  }
686
728
 
687
729
  /**
package/src/2-types.ts CHANGED
@@ -245,7 +245,7 @@ export interface GraffitiPatch {
245
245
  /**
246
246
  * This type represents a stream of data that are
247
247
  * returned by Graffiti's query-like operations such as
248
- * {@link Graffiti.discover} and {@link Graffiti.listChannels}.
248
+ * {@link Graffiti.discover} and {@link Graffiti.recoverOrphans}.
249
249
  *
250
250
  * Errors are returned within the stream rather than as
251
251
  * exceptions that would halt the entire stream. This is because
@@ -270,6 +270,29 @@ export type GraffitiStream<TValue, TReturn = void> = AsyncGenerator<
270
270
  TReturn
271
271
  >;
272
272
 
273
+ /**
274
+ * Statistic about single channel returned by {@link Graffiti.channelStats}.
275
+ * These statistics only account for contributions made by the
276
+ * querying actor.
277
+ */
278
+ export type ChannelStats = {
279
+ /**
280
+ * The URI of the channel.
281
+ */
282
+ channel: string;
283
+ /**
284
+ * The number of non-{@link GraffitiObjectBase.tombstone | `tombstone`}d objects
285
+ * that the actor has posted to the channel.
286
+ */
287
+ count: number;
288
+ /**
289
+ * The time that the actor {@link Graffiti.lastModified | last modified} an object in the channel,
290
+ * measured in milliseconds since January 1, 1970.
291
+ * {@link GraffitiObjectBase.tombstone | Tombstone}d objects do not effect this modification time.
292
+ */
293
+ lastModified: number;
294
+ };
295
+
273
296
  /**
274
297
  * The event type produced in {@link Graffiti.sessionEvents}
275
298
  * when a user logs in manually from {@link Graffiti.login}
@@ -310,8 +333,11 @@ export type GraffitiLogoutEvent = CustomEvent<
310
333
  * and restore any previously active sessions.
311
334
  * Successful session restores will be returned in parallel as
312
335
  * their own {@link GraffitiLoginEvent} events.
313
- * This event optionally return an `href` property
314
- * if there were any redirects during the restoration process.
336
+ *
337
+ * This event optionally returns an `href` property
338
+ * representing the URL the user originated a login request
339
+ * from, which may be useful for redirecting the user back to
340
+ * the page they were on after login.
315
341
  * The event name to listen for is `initialized`.
316
342
  */
317
343
  export type GraffitiSessionInitializedEvent = CustomEvent<
@@ -0,0 +1,124 @@
1
+ import { it, expect, describe, assert } from "vitest";
2
+ import type { Graffiti, GraffitiSession } from "@graffiti-garden/api";
3
+ import { randomPutObject, randomString } from "./utils";
4
+
5
+ export const graffitiChannelStatsTests = (
6
+ useGraffiti: () => Pick<
7
+ Graffiti,
8
+ "channelStats" | "put" | "delete" | "patch"
9
+ >,
10
+ useSession1: () => GraffitiSession,
11
+ useSession2: () => GraffitiSession,
12
+ ) => {
13
+ describe("channel stats", () => {
14
+ it("list channels", async () => {
15
+ const graffiti = useGraffiti();
16
+ const session = useSession1();
17
+
18
+ const existingChannels: Map<string, number> = new Map();
19
+ const channelIterator1 = graffiti.channelStats(session);
20
+ for await (const channel of channelIterator1) {
21
+ if (channel.error) continue;
22
+ existingChannels.set(channel.value.channel, channel.value.count);
23
+ }
24
+
25
+ const channels = [randomString(), randomString(), randomString()];
26
+
27
+ // Add one value to channels[0],
28
+ // two values to both channels[0] and channels[1],
29
+ // three values to all channels
30
+ // one value to channels[2]
31
+ for (let i = 0; i < 3; i++) {
32
+ for (let j = 0; j < i + 1; j++) {
33
+ await graffiti.put(
34
+ {
35
+ value: {
36
+ index: j,
37
+ },
38
+ channels: channels.slice(0, i + 1),
39
+ },
40
+ session,
41
+ );
42
+ }
43
+ }
44
+ await graffiti.put(
45
+ { value: { index: 3 }, channels: [channels[2]] },
46
+ session,
47
+ );
48
+
49
+ const channelIterator2 = graffiti.channelStats(session);
50
+ let newChannels: Map<string, number> = new Map();
51
+ for await (const channel of channelIterator2) {
52
+ if (channel.error) continue;
53
+ newChannels.set(channel.value.channel, channel.value.count);
54
+ }
55
+ // Filter out existing channels
56
+ newChannels = new Map(
57
+ Array.from(newChannels).filter(
58
+ ([channel, count]) => !existingChannels.has(channel),
59
+ ),
60
+ );
61
+ expect(newChannels.size).toBe(3);
62
+ expect(newChannels.get(channels[0])).toBe(6);
63
+ expect(newChannels.get(channels[1])).toBe(5);
64
+ expect(newChannels.get(channels[2])).toBe(4);
65
+ });
66
+
67
+ it("list channels with deleted channel", async () => {
68
+ const graffiti = useGraffiti();
69
+ const session = useSession1();
70
+
71
+ const channels = [randomString(), randomString(), randomString()];
72
+
73
+ // Add an item with two channels
74
+ const before = await graffiti.put(
75
+ {
76
+ value: { index: 2 },
77
+ channels: channels.slice(1),
78
+ },
79
+ session,
80
+ );
81
+
82
+ // Add an item with all channels
83
+ const first = await graffiti.put(
84
+ { value: { index: 0 }, channels },
85
+ session,
86
+ );
87
+ // But then delete it
88
+ await graffiti.delete(first, session);
89
+
90
+ // Create a new object with only one channel
91
+ const second = await graffiti.put(
92
+ {
93
+ value: { index: 1 },
94
+ channels: channels.slice(2),
95
+ },
96
+ session,
97
+ );
98
+
99
+ const channelIterator = graffiti.channelStats(session);
100
+
101
+ let got1 = 0;
102
+ let got2 = 0;
103
+ for await (const result of channelIterator) {
104
+ if (result.error) continue;
105
+ const { channel, count, lastModified } = result.value;
106
+ assert(
107
+ channel !== channels[0],
108
+ "There should not be an object in channel[0]",
109
+ );
110
+ if (channel === channels[1]) {
111
+ expect(count).toBe(1);
112
+ expect(lastModified).toBe(before.lastModified);
113
+ got1++;
114
+ } else if (channel === channels[2]) {
115
+ expect(count).toBe(2);
116
+ expect(lastModified).toBe(second.lastModified);
117
+ got2++;
118
+ }
119
+ }
120
+ expect(got1).toBe(1);
121
+ expect(got2).toBe(1);
122
+ });
123
+ });
124
+ };
package/tests/src/crud.ts CHANGED
@@ -63,7 +63,7 @@ export const graffitiCRUDTests = (
63
63
  expect(beforeReplaced.name).toEqual(previous.name);
64
64
  expect(beforeReplaced.actor).toEqual(previous.actor);
65
65
  expect(beforeReplaced.source).toEqual(previous.source);
66
- expect(beforeReplaced.lastModified).toBeGreaterThanOrEqual(
66
+ expect(beforeReplaced.lastModified).toBeGreaterThan(
67
67
  gotten.lastModified,
68
68
  );
69
69
 
@@ -77,14 +77,29 @@ export const graffitiCRUDTests = (
77
77
  const beforeDeleted = await graffiti.delete(afterReplaced, session);
78
78
  expect(beforeDeleted.tombstone).toEqual(true);
79
79
  expect(beforeDeleted.value).toEqual(newValue);
80
- expect(beforeDeleted.lastModified).toBeGreaterThanOrEqual(
80
+ expect(beforeDeleted.lastModified).toBeGreaterThan(
81
81
  beforeReplaced.lastModified,
82
82
  );
83
83
 
84
- // Try to get it and fail
85
- await expect(graffiti.get(afterReplaced, {})).rejects.toThrow(
86
- GraffitiErrorNotFound,
87
- );
84
+ // Get a tombstone
85
+ const final = await graffiti.get(afterReplaced, {});
86
+ expect(final).toEqual(beforeDeleted);
87
+ });
88
+
89
+ it("get non-existant", async () => {
90
+ const graffiti = useGraffiti();
91
+ const session = useSession1();
92
+
93
+ const putted = await graffiti.put(randomPutObject(), session);
94
+ await expect(
95
+ graffiti.get(
96
+ {
97
+ ...putted,
98
+ name: randomString(),
99
+ },
100
+ {},
101
+ ),
102
+ ).rejects.toBeInstanceOf(GraffitiErrorNotFound);
88
103
  });
89
104
 
90
105
  it("put, get, delete with wrong actor", async () => {
@@ -218,10 +233,14 @@ export const graffitiCRUDTests = (
218
233
  expect(gotten.channels).toEqual(channels);
219
234
 
220
235
  // But not without session
221
- await expect(graffiti.get(putted, {})).rejects.toThrow();
236
+ await expect(graffiti.get(putted, {})).rejects.toBeInstanceOf(
237
+ GraffitiErrorNotFound,
238
+ );
222
239
 
223
240
  // Or the wrong session
224
- await expect(graffiti.get(putted, {}, session2)).rejects.toThrow();
241
+ await expect(graffiti.get(putted, {}, session2)).rejects.toBeInstanceOf(
242
+ GraffitiErrorNotFound,
243
+ );
225
244
  });
226
245
 
227
246
  it("put and get with specific access control", async () => {
@@ -250,7 +269,9 @@ export const graffitiCRUDTests = (
250
269
  expect(gotten.channels).toEqual(channels);
251
270
 
252
271
  // But not without session
253
- await expect(graffiti.get(putted, {})).rejects.toThrow();
272
+ await expect(graffiti.get(putted, {})).rejects.toBeInstanceOf(
273
+ GraffitiErrorNotFound,
274
+ );
254
275
 
255
276
  const gotten2 = await graffiti.get(putted, {}, session2);
256
277
  expect(gotten2.value).toEqual(value);
@@ -287,6 +308,17 @@ export const graffitiCRUDTests = (
287
308
  await graffiti.delete(putted, session);
288
309
  });
289
310
 
311
+ it("patch deleted object", async () => {
312
+ const graffiti = useGraffiti();
313
+ const session = useSession1();
314
+
315
+ const putted = await graffiti.put(randomPutObject(), session);
316
+ const deleted = await graffiti.delete(putted, session);
317
+ await expect(
318
+ graffiti.patch({}, putted, session),
319
+ ).rejects.toBeInstanceOf(GraffitiErrorNotFound);
320
+ });
321
+
290
322
  it("deep patch", async () => {
291
323
  const graffiti = useGraffiti();
292
324
  const session = useSession1();
@@ -2,3 +2,5 @@ export * from "./location";
2
2
  export * from "./crud";
3
3
  export * from "./synchronize";
4
4
  export * from "./discover";
5
+ export * from "./orphans";
6
+ export * from "./channel-stats";