@graffiti-garden/api 0.6.4 → 1.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.
Files changed (44) hide show
  1. package/README.md +0 -7
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.cjs.map +4 -4
  4. package/dist/index.mjs +1 -1
  5. package/dist/index.mjs.map +4 -4
  6. package/dist/src/1-api.d.ts +172 -178
  7. package/dist/src/1-api.d.ts.map +1 -1
  8. package/dist/src/2-types.d.ts +39 -152
  9. package/dist/src/2-types.d.ts.map +1 -1
  10. package/dist/src/3-errors.d.ts +2 -8
  11. package/dist/src/3-errors.d.ts.map +1 -1
  12. package/dist/src/4-utilities.d.ts +8 -0
  13. package/dist/src/4-utilities.d.ts.map +1 -0
  14. package/dist/src/index.d.ts +1 -0
  15. package/dist/src/index.d.ts.map +1 -1
  16. package/dist/tests/crud.d.ts +1 -1
  17. package/dist/tests/crud.d.ts.map +1 -1
  18. package/dist/tests/discover.d.ts +1 -1
  19. package/dist/tests/discover.d.ts.map +1 -1
  20. package/dist/tests/index.d.ts +1 -2
  21. package/dist/tests/index.d.ts.map +1 -1
  22. package/dist/tests/media.d.ts +3 -0
  23. package/dist/tests/media.d.ts.map +1 -0
  24. package/dist/tests/utils.d.ts +3 -3
  25. package/dist/tests/utils.d.ts.map +1 -1
  26. package/dist/tests.mjs +207 -790
  27. package/dist/tests.mjs.map +4 -4
  28. package/package.json +6 -6
  29. package/src/1-api.ts +182 -186
  30. package/src/2-types.ts +42 -157
  31. package/src/3-errors.ts +6 -22
  32. package/src/4-utilities.ts +65 -0
  33. package/src/index.ts +1 -0
  34. package/tests/crud.ts +39 -332
  35. package/tests/discover.ts +51 -349
  36. package/tests/index.ts +1 -2
  37. package/tests/media.ts +158 -0
  38. package/tests/utils.ts +4 -4
  39. package/dist/tests/channel-stats.d.ts +0 -3
  40. package/dist/tests/channel-stats.d.ts.map +0 -1
  41. package/dist/tests/orphans.d.ts +0 -3
  42. package/dist/tests/orphans.d.ts.map +0 -1
  43. package/tests/channel-stats.ts +0 -129
  44. package/tests/orphans.ts +0 -128
package/tests/media.ts ADDED
@@ -0,0 +1,158 @@
1
+ import {
2
+ GraffitiErrorNotAcceptable,
3
+ GraffitiErrorNotFound,
4
+ GraffitiErrorTooLarge,
5
+ type Graffiti,
6
+ type GraffitiSession,
7
+ } from "@graffiti-garden/api";
8
+ import { it, expect, describe, beforeAll } from "vitest";
9
+ import { randomString } from "./utils";
10
+
11
+ export const graffitiMediaTests = (
12
+ useGraffiti: () => Pick<Graffiti, "postMedia" | "getMedia" | "deleteMedia">,
13
+ useSession1: () => GraffitiSession | Promise<GraffitiSession>,
14
+ useSession2: () => GraffitiSession | Promise<GraffitiSession>,
15
+ ) => {
16
+ describe.concurrent(
17
+ "media",
18
+ {
19
+ timeout: 20000,
20
+ },
21
+ () => {
22
+ let graffiti: ReturnType<typeof useGraffiti>;
23
+ let session: GraffitiSession;
24
+ let session1: GraffitiSession;
25
+ let session2: GraffitiSession;
26
+ beforeAll(async () => {
27
+ graffiti = useGraffiti();
28
+ session1 = await useSession1();
29
+ session = session1;
30
+ session2 = await useSession2();
31
+ });
32
+
33
+ it("post, get, delete media", async () => {
34
+ // Post media
35
+ const text = randomString();
36
+ const data = new Blob([text], { type: "text/plain" });
37
+ const mediaUrl = await graffiti.postMedia({ data }, session);
38
+
39
+ // Get the media back
40
+ const media = await graffiti.getMedia(mediaUrl, {});
41
+ expect(await media.data.text()).toEqual(text);
42
+ expect(media.data.type).toEqual("text/plain");
43
+ expect(media.allowed).toBeUndefined();
44
+ expect(media.actor).toBe(session.actor);
45
+
46
+ // Delete the media
47
+ await graffiti.deleteMedia(mediaUrl, session);
48
+
49
+ // Try to get the media again
50
+ await expect(graffiti.getMedia(mediaUrl, {})).rejects.toThrow(
51
+ GraffitiErrorNotFound,
52
+ );
53
+ });
54
+
55
+ it("acceptable type", async () => {
56
+ const text = randomString();
57
+ const data = new Blob([text], { type: "text/plain" });
58
+ const mediaUrl = await graffiti.postMedia({ data }, session);
59
+
60
+ const media = await graffiti.getMedia(mediaUrl, {
61
+ accept: "text/*",
62
+ });
63
+ expect(await media.data.text()).toEqual(text);
64
+ expect(media.data.type).toEqual("text/plain");
65
+ expect(media.allowed).toBeUndefined();
66
+ expect(media.actor).toBe(session.actor);
67
+ });
68
+
69
+ it("unacceptable type", async () => {
70
+ const text = randomString();
71
+ const data = new Blob([text], { type: "text/plain" });
72
+ const mediaUrl = await graffiti.postMedia({ data }, session);
73
+
74
+ await expect(
75
+ graffiti.getMedia(mediaUrl, {
76
+ accept: "image/*",
77
+ }),
78
+ ).rejects.toThrow(GraffitiErrorNotAcceptable);
79
+ });
80
+
81
+ it("acceptable size", async () => {
82
+ const text = randomString();
83
+ const data = new Blob([text], { type: "text/plain" });
84
+ const mediaUrl = await graffiti.postMedia({ data }, session);
85
+
86
+ const media = await graffiti.getMedia(mediaUrl, {
87
+ maxBytes: data.size,
88
+ });
89
+ expect(await media.data.text()).toEqual(text);
90
+ expect(media.data.type).toEqual("text/plain");
91
+ expect(media.allowed).toBeUndefined();
92
+ expect(media.actor).toBe(session.actor);
93
+ });
94
+
95
+ it("unacceptable size", async () => {
96
+ const text = randomString();
97
+ const data = new Blob([text], { type: "text/plain" });
98
+ const mediaUrl = await graffiti.postMedia({ data }, session);
99
+
100
+ await expect(
101
+ graffiti.getMedia(mediaUrl, {
102
+ maxBytes: data.size - 1,
103
+ }),
104
+ ).rejects.toThrow(GraffitiErrorTooLarge);
105
+ });
106
+
107
+ it("empty allowed", async () => {
108
+ const text = randomString();
109
+ const data = new Blob([text], { type: "text/plain" });
110
+ const allowed: string[] = [];
111
+ const mediaUrl = await graffiti.postMedia({ data, allowed }, session1);
112
+
113
+ // Get it with the authorized user
114
+ const media = await graffiti.getMedia(mediaUrl, {}, session1);
115
+ expect(await media.data.text()).toEqual(text);
116
+ expect(media.data.type).toEqual("text/plain");
117
+ expect(media.allowed).toEqual([]);
118
+ expect(media.actor).toBe(session1.actor);
119
+
120
+ // Get it with the unauthorized user
121
+ await expect(graffiti.getMedia(mediaUrl, {}, session2)).rejects.toThrow(
122
+ GraffitiErrorNotFound,
123
+ );
124
+
125
+ // Get it without authorization
126
+ await expect(graffiti.getMedia(mediaUrl, {})).rejects.toThrow(
127
+ GraffitiErrorNotFound,
128
+ );
129
+ });
130
+
131
+ it("allowed", async () => {
132
+ const text = randomString();
133
+ const data = new Blob([text], { type: "text/plain" });
134
+ const allowed = [randomString(), session2.actor, randomString()];
135
+ const mediaUrl = await graffiti.postMedia({ data, allowed }, session1);
136
+
137
+ // Get it with the authorized user
138
+ const media = await graffiti.getMedia(mediaUrl, {}, session1);
139
+ expect(await media.data.text()).toEqual(text);
140
+ expect(media.data.type).toEqual("text/plain");
141
+ expect(media.allowed).toEqual(allowed);
142
+ expect(media.actor).toBe(session1.actor);
143
+
144
+ // Get it with the allowed user
145
+ const media2 = await graffiti.getMedia(mediaUrl, {}, session2);
146
+ expect(await media2.data.text()).toEqual(text);
147
+ expect(media2.data.type).toEqual("text/plain");
148
+ expect(media2.allowed).toEqual([session2.actor]);
149
+ expect(media2.actor).toBe(session1.actor);
150
+
151
+ // Get it without authorization
152
+ await expect(graffiti.getMedia(mediaUrl, {})).rejects.toThrow(
153
+ GraffitiErrorNotFound,
154
+ );
155
+ });
156
+ },
157
+ );
158
+ };
package/tests/utils.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { assert } from "vitest";
2
2
  import type {
3
- GraffitiPutObject,
3
+ GraffitiPostObject,
4
4
  GraffitiObjectStream,
5
5
  JSONSchema,
6
6
  GraffitiObject,
@@ -27,7 +27,7 @@ export function randomValue() {
27
27
  };
28
28
  }
29
29
 
30
- export function randomPutObject(): GraffitiPutObject<{}> {
30
+ export function randomPostObject(): GraffitiPostObject<{}> {
31
31
  return {
32
32
  value: randomValue(),
33
33
  channels: [randomString(), randomString()],
@@ -44,13 +44,13 @@ export async function nextStreamValue<Schema extends JSONSchema>(
44
44
  }
45
45
 
46
46
  export function continueStream<Schema extends JSONSchema>(
47
- graffiti: Pick<Graffiti, "continueObjectStream">,
47
+ graffiti: Pick<Graffiti, "continueDiscover">,
48
48
  streamReturn: GraffitiObjectStreamReturn<Schema>,
49
49
  type: "cursor" | "continue",
50
50
  session?: GraffitiSession | null,
51
51
  ): GraffitiObjectStreamContinue<Schema> {
52
52
  if (type === "cursor") {
53
- return graffiti.continueObjectStream(
53
+ return graffiti.continueDiscover(
54
54
  streamReturn.cursor,
55
55
  session,
56
56
  ) as unknown as GraffitiObjectStreamContinue<Schema>;
@@ -1,3 +0,0 @@
1
- import type { Graffiti, GraffitiSession } from "@graffiti-garden/api";
2
- export declare const graffitiChannelStatsTests: (useGraffiti: () => Pick<Graffiti, "channelStats" | "put" | "delete" | "patch">, useSession1: () => GraffitiSession | Promise<GraffitiSession>, useSession2: () => GraffitiSession | Promise<GraffitiSession>) => void;
3
- //# sourceMappingURL=channel-stats.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"channel-stats.d.ts","sourceRoot":"","sources":["../../tests/channel-stats.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAGtE,eAAO,MAAM,yBAAyB,GACpC,aAAa,MAAM,IAAI,CACrB,QAAQ,EACR,cAAc,GAAG,KAAK,GAAG,QAAQ,GAAG,OAAO,CAC5C,EACD,aAAa,MAAM,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC,EAC7D,aAAa,MAAM,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC,SAsH9D,CAAC"}
@@ -1,3 +0,0 @@
1
- import type { Graffiti, GraffitiSession } from "@graffiti-garden/api";
2
- export declare const graffitiOrphanTests: (useGraffiti: () => Pick<Graffiti, "recoverOrphans" | "put" | "delete" | "patch" | "continueObjectStream">, useSession1: () => GraffitiSession | Promise<GraffitiSession>, useSession2: () => GraffitiSession | Promise<GraffitiSession>) => void;
3
- //# sourceMappingURL=orphans.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"orphans.d.ts","sourceRoot":"","sources":["../../tests/orphans.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAQtE,eAAO,MAAM,mBAAmB,GAC9B,aAAa,MAAM,IAAI,CACrB,QAAQ,EACR,gBAAgB,GAAG,KAAK,GAAG,QAAQ,GAAG,OAAO,GAAG,sBAAsB,CACvE,EACD,aAAa,MAAM,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC,EAC7D,aAAa,MAAM,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC,SAgH9D,CAAC"}
@@ -1,129 +0,0 @@
1
- import { it, expect, describe, assert, beforeAll } from "vitest";
2
- import type { Graffiti, GraffitiSession } from "@graffiti-garden/api";
3
- import { randomString } from "./utils";
4
-
5
- export const graffitiChannelStatsTests = (
6
- useGraffiti: () => Pick<
7
- Graffiti,
8
- "channelStats" | "put" | "delete" | "patch"
9
- >,
10
- useSession1: () => GraffitiSession | Promise<GraffitiSession>,
11
- useSession2: () => GraffitiSession | Promise<GraffitiSession>,
12
- ) => {
13
- describe("channel stats", { timeout: 20000 }, () => {
14
- let graffiti: ReturnType<typeof useGraffiti>;
15
- let session: GraffitiSession;
16
- let session1: GraffitiSession;
17
- let session2: GraffitiSession;
18
- beforeAll(async () => {
19
- graffiti = useGraffiti();
20
- session1 = await useSession1();
21
- session = session1;
22
- session2 = await useSession2();
23
- });
24
-
25
- it("list channels", async () => {
26
- const existingChannels: Map<string, number> = new Map();
27
- const channelIterator1 = graffiti.channelStats(session);
28
- for await (const channel of channelIterator1) {
29
- if (channel.error) continue;
30
- existingChannels.set(channel.value.channel, channel.value.count);
31
- }
32
-
33
- const channels = [randomString(), randomString(), randomString()];
34
-
35
- // Add one value to channels[0],
36
- // two values to both channels[0] and channels[1],
37
- // three values to all channels
38
- // one value to channels[2]
39
- for (let i = 0; i < 3; i++) {
40
- for (let j = 0; j < i + 1; j++) {
41
- await graffiti.put<{}>(
42
- {
43
- value: {
44
- index: j,
45
- },
46
- channels: channels.slice(0, i + 1),
47
- },
48
- session,
49
- );
50
- }
51
- }
52
- await graffiti.put<{}>(
53
- { value: { index: 3 }, channels: [channels[2]] },
54
- session,
55
- );
56
-
57
- const channelIterator2 = graffiti.channelStats(session);
58
- let newChannels: Map<string, number> = new Map();
59
- for await (const channel of channelIterator2) {
60
- if (channel.error) continue;
61
- newChannels.set(channel.value.channel, channel.value.count);
62
- }
63
- // Filter out existing channels
64
- newChannels = new Map(
65
- Array.from(newChannels).filter(
66
- ([channel, count]) => !existingChannels.has(channel),
67
- ),
68
- );
69
- expect(newChannels.size).toBe(3);
70
- expect(newChannels.get(channels[0])).toBe(6);
71
- expect(newChannels.get(channels[1])).toBe(5);
72
- expect(newChannels.get(channels[2])).toBe(4);
73
- });
74
-
75
- it("list channels with deleted channel", async () => {
76
- const channels = [randomString(), randomString(), randomString()];
77
-
78
- // Add an item with two channels
79
- const before = await graffiti.put<{}>(
80
- {
81
- value: { index: 2 },
82
- channels: channels.slice(1),
83
- },
84
- session,
85
- );
86
-
87
- // Add an item with all channels
88
- const first = await graffiti.put<{}>(
89
- { value: { index: 0 }, channels },
90
- session,
91
- );
92
- // But then delete it
93
- await graffiti.delete(first, session);
94
-
95
- // Create a new object with only one channel
96
- const second = await graffiti.put<{}>(
97
- {
98
- value: { index: 1 },
99
- channels: channels.slice(2),
100
- },
101
- session,
102
- );
103
-
104
- const channelIterator = graffiti.channelStats(session);
105
-
106
- let got1 = 0;
107
- let got2 = 0;
108
- for await (const result of channelIterator) {
109
- if (result.error) continue;
110
- const { channel, count, lastModified } = result.value;
111
- assert(
112
- channel !== channels[0],
113
- "There should not be an object in channel[0]",
114
- );
115
- if (channel === channels[1]) {
116
- expect(count).toBe(1);
117
- expect(lastModified).toBe(before.lastModified);
118
- got1++;
119
- } else if (channel === channels[2]) {
120
- expect(count).toBe(2);
121
- expect(lastModified).toBe(second.lastModified);
122
- got2++;
123
- }
124
- }
125
- expect(got1).toBe(1);
126
- expect(got2).toBe(1);
127
- });
128
- });
129
- };
package/tests/orphans.ts DELETED
@@ -1,128 +0,0 @@
1
- import { it, expect, describe, assert, beforeAll } from "vitest";
2
- import type { Graffiti, GraffitiSession } from "@graffiti-garden/api";
3
- import {
4
- randomPutObject,
5
- randomString,
6
- nextStreamValue,
7
- continueStream,
8
- } from "./utils";
9
-
10
- export const graffitiOrphanTests = (
11
- useGraffiti: () => Pick<
12
- Graffiti,
13
- "recoverOrphans" | "put" | "delete" | "patch" | "continueObjectStream"
14
- >,
15
- useSession1: () => GraffitiSession | Promise<GraffitiSession>,
16
- useSession2: () => GraffitiSession | Promise<GraffitiSession>,
17
- ) => {
18
- describe("recoverOrphans", { timeout: 20000 }, () => {
19
- let graffiti: ReturnType<typeof useGraffiti>;
20
- let session: GraffitiSession;
21
- let session1: GraffitiSession;
22
- let session2: GraffitiSession;
23
- beforeAll(async () => {
24
- graffiti = useGraffiti();
25
- session1 = await useSession1();
26
- session = session1;
27
- session2 = await useSession2();
28
- });
29
-
30
- it("list orphans", async () => {
31
- const existingOrphans: string[] = [];
32
- const orphanIterator1 = graffiti.recoverOrphans({}, session);
33
- for await (const orphan of orphanIterator1) {
34
- if (orphan.error) continue;
35
- existingOrphans.push(orphan.object.url);
36
- }
37
-
38
- const object = randomPutObject();
39
- object.channels = [];
40
- const putted = await graffiti.put<{}>(object, session);
41
- const orphanIterator2 = graffiti.recoverOrphans({}, session);
42
- let numResults = 0;
43
- for await (const orphan of orphanIterator2) {
44
- if (orphan.error) continue;
45
- assert(!orphan.tombstone, "orphan is tombstone");
46
- if (orphan.object.url === putted.url) {
47
- numResults++;
48
- expect(orphan.object.lastModified).toBe(putted.lastModified);
49
- }
50
- }
51
- expect(numResults).toBe(1);
52
- });
53
-
54
- for (const continueType of ["continue", "cursor"] as const) {
55
- describe(`continue orphans with ${continueType}`, () => {
56
- it("replaced orphan, no longer", async () => {
57
- const object = randomPutObject();
58
- object.channels = [];
59
- const putOrphan = await graffiti.put<{}>(object, session);
60
-
61
- // Wait for the put to be processed
62
- await new Promise((resolve) => setTimeout(resolve, 10));
63
-
64
- expect(Object.keys(object.value).length).toBeGreaterThanOrEqual(1);
65
- expect(Object.keys(object.value)[0]).toBeTypeOf("string");
66
- const iterator1 = graffiti.recoverOrphans<{}>(
67
- {
68
- properties: {
69
- value: {
70
- properties: {
71
- [Object.keys(object.value)[0]]: {
72
- type: "string",
73
- },
74
- },
75
- required: [Object.keys(object.value)[0]],
76
- },
77
- },
78
- },
79
- session,
80
- );
81
- const value1 = await nextStreamValue<{}>(iterator1);
82
- expect(value1.value).toEqual(object.value);
83
- const returnValue = await iterator1.next();
84
- assert(returnValue.done, "value2 is not done");
85
-
86
- const putNotOrphan = await graffiti.put<{}>(
87
- {
88
- ...putOrphan,
89
- ...object,
90
- channels: [randomString()],
91
- },
92
- session,
93
- );
94
- expect(putNotOrphan.url).toBe(putOrphan.url);
95
- expect(putNotOrphan.lastModified).toBeGreaterThan(
96
- putOrphan.lastModified,
97
- );
98
-
99
- // The tombstone will not appear to a fresh iterator
100
- const orphanIterator = graffiti.recoverOrphans({}, session);
101
- let numResults = 0;
102
- for await (const orphan of orphanIterator) {
103
- if (orphan.error) continue;
104
- if (orphan.object.url === putOrphan.url) {
105
- numResults++;
106
- }
107
- }
108
- expect(numResults).toBe(0);
109
-
110
- const iterator2 = continueStream<{}>(
111
- graffiti,
112
- returnValue.value,
113
- continueType,
114
- session,
115
- );
116
- const value2 = await iterator2.next();
117
- assert(
118
- !value2.done && !value2.value.error,
119
- "value2 is done or has error",
120
- );
121
- assert(value2.value.tombstone, "value2 is not tombstone");
122
- expect(value2.value.object.url).toBe(putOrphan.url);
123
- await expect(iterator2.next()).resolves.toHaveProperty("done", true);
124
- });
125
- });
126
- }
127
- });
128
- };