@graffiti-garden/api 0.0.3 → 0.0.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graffiti-garden/api",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "The heart of Graffiti",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -17,7 +17,8 @@
17
17
  "author": "Theia Henderson",
18
18
  "license": "GPL-3.0-or-later",
19
19
  "scripts": {
20
- "docs": "typedoc --options typedoc.json"
20
+ "docs": "typedoc --options typedoc.json",
21
+ "prepublishOnly": "npm install"
21
22
  },
22
23
  "repository": {
23
24
  "type": "git",
@@ -6,7 +6,7 @@ import type {
6
6
  GraffitiSessionBase,
7
7
  GraffitiPutObject,
8
8
  GraffitiStream,
9
- } from "./types";
9
+ } from "./2-types";
10
10
  import type { JSONSchema4 } from "json-schema";
11
11
 
12
12
  /**
@@ -131,7 +131,7 @@ export abstract class Graffiti {
131
131
  * If no object exists at that location or if the retrieving
132
132
  * {@link GraffitiObjectBase.actor | `actor`} is not the creator or included in
133
133
  * the object's {@link GraffitiObjectBase.allowed | `allowed`} property,
134
- * an error is thrown.
134
+ * a {@link GraffitiErrorNotFound} is thrown.
135
135
  *
136
136
  * The retrieved object is also type-checked against the provided [JSON schema](https://json-schema.org/)
137
137
  * otherwise an error is thrown.
@@ -191,6 +191,9 @@ export abstract class Graffiti {
191
191
  * The deleting {@link GraffitiObjectBase.actor | `actor`} must be the same as the
192
192
  * `actor` that created the object.
193
193
  *
194
+ * If the object does not exist or has already been deleted, a
195
+ * {@link GraffitiErrorNotFound} is thrown.
196
+ *
194
197
  * @returns The object that was deleted if one exists or an object with
195
198
  * with a `null` {@link GraffitiObjectBase.value | `value`} otherwise.
196
199
  * The object will have a {@link GraffitiObjectBase.tombstone | `tombstone`}
@@ -212,17 +215,36 @@ export abstract class Graffiti {
212
215
  ): Promise<GraffitiObjectBase>;
213
216
 
214
217
  /**
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
+ * Discovers objects created by any user that are contained
219
+ * in at least one of the given {@link GraffitiObjectBase.channels | `channels`}
220
+ * and match the given [JSON Schema](https://json-schema.org).
218
221
  *
219
222
  * Objects are returned asynchronously as they are discovered but the stream
220
223
  * will end once all leads have been exhausted.
221
224
  * The method must be polled again for new objects.
222
225
  *
226
+ * `discover` will not return objects that the {@link GraffitiObjectBase.actor | `actor`}
227
+ * is not {@link GraffitiObjectBase.allowed | `allowed`} to access.
228
+ * If the actor is not the creator of a discovered object,
229
+ * the allowed list will be masked to only contain the querying actor if the
230
+ * allowed list is not `undefined` (public). Additionally, if the actor is not the
231
+ * creator of a discovered object, any {@link GraffitiObjectBase.channels | `channels`}
232
+ * not specified by the `discover` operation will not be revealed. This masking happens
233
+ * before the supplied schema is applied.
234
+ *
235
+ * 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
237
+ * may return objects that have been deleted but with a
238
+ * {@link GraffitiObjectBase.tombstone | `tombstone`} field set to `true` for
239
+ * cache invalidation purposes. Implementations must make aware when, if ever,
240
+ * tombstoned objects are removed.
241
+ *
223
242
  * {@link discover} can be used in conjunction with {@link synchronize}
224
243
  * to provide a responsive and consistent user experience.
225
244
  *
245
+ * @returns A stream of objects that match the given {@link GraffitiObjectBase.channels | `channels`}
246
+ * and [JSON Schema](https://json-schema.org).
247
+ *
226
248
  * @group Query Operations
227
249
  */
228
250
  abstract discover<Schema extends JSONSchema4>(
@@ -0,0 +1,7 @@
1
+ export class GraffitiErrorNotFound extends Error {
2
+ constructor(message: string) {
3
+ super(message);
4
+ this.name = "GraffitiErrorNotFound";
5
+ Object.setPrototypeOf(this, GraffitiErrorNotFound.prototype);
6
+ }
7
+ }
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
- export * from "./types";
1
+ export * from "./1-api";
2
+ export * from "./2-types";
3
+ export * from "./3-errors";
2
4
  export type { JSONSchema4 } from "json-schema";
3
- export * from "./api";
package/tests/crud.ts ADDED
@@ -0,0 +1,250 @@
1
+ import { it, expect } from "vitest";
2
+ import type {
3
+ GraffitiFactory,
4
+ GraffitiSessionBase,
5
+ GraffitiPatch,
6
+ } from "../src/index";
7
+
8
+ export const graffitiCRUDTests = (
9
+ useGraffiti: GraffitiFactory,
10
+ useSession1: () => GraffitiSessionBase,
11
+ useSession2: () => GraffitiSessionBase,
12
+ ) => {
13
+ it("put, get, delete", async () => {
14
+ const graffiti = useGraffiti();
15
+ const session = useSession1();
16
+ const value = {
17
+ something: "hello, world~ c:",
18
+ };
19
+ const channels = ["world"];
20
+
21
+ // Put the object
22
+ const previous = await graffiti.put({ value, channels }, session);
23
+ expect(previous.value).toEqual({});
24
+ expect(previous.channels).toEqual([]);
25
+ expect(previous.allowed).toBeUndefined();
26
+ expect(previous.actor).toEqual(session.actor);
27
+
28
+ // Get it back
29
+ const gotten = await graffiti.get(previous, {});
30
+ expect(gotten.value).toEqual(value);
31
+ expect(gotten.channels).toEqual([]);
32
+ expect(gotten.allowed).toBeUndefined();
33
+ expect(gotten.name).toEqual(previous.name);
34
+ expect(gotten.actor).toEqual(previous.actor);
35
+ expect(gotten.source).toEqual(previous.source);
36
+ expect(gotten.lastModified.getTime()).toEqual(
37
+ previous.lastModified.getTime(),
38
+ );
39
+
40
+ // Replace it
41
+ const newValue = {
42
+ something: "goodbye, world~ :c",
43
+ };
44
+ const beforeReplaced = await graffiti.put(
45
+ { ...previous, value: newValue, channels: [] },
46
+ session,
47
+ );
48
+ expect(beforeReplaced.value).toEqual(value);
49
+ expect(beforeReplaced.tombstone).toEqual(true);
50
+ expect(beforeReplaced.name).toEqual(previous.name);
51
+ expect(beforeReplaced.actor).toEqual(previous.actor);
52
+ expect(beforeReplaced.source).toEqual(previous.source);
53
+ expect(beforeReplaced.lastModified.getTime()).toBeGreaterThan(
54
+ gotten.lastModified.getTime(),
55
+ );
56
+
57
+ // Get it again
58
+ const afterReplaced = await graffiti.get(previous, {});
59
+ expect(afterReplaced.value).toEqual(newValue);
60
+ expect(afterReplaced.lastModified.getTime()).toEqual(
61
+ beforeReplaced.lastModified.getTime(),
62
+ );
63
+ expect(afterReplaced.tombstone).toEqual(false);
64
+
65
+ // Delete it
66
+ const beforeDeleted = await graffiti.delete(afterReplaced, session);
67
+ expect(beforeDeleted.tombstone).toEqual(true);
68
+ expect(beforeDeleted.value).toEqual(newValue);
69
+ expect(beforeDeleted.lastModified.getTime()).toBeGreaterThan(
70
+ beforeReplaced.lastModified.getTime(),
71
+ );
72
+
73
+ // Try to get it and fail
74
+ await expect(graffiti.get(afterReplaced, {})).rejects.toThrow();
75
+ });
76
+
77
+ it("put and get with schema", async () => {
78
+ const graffiti = useGraffiti();
79
+ const session = useSession1();
80
+
81
+ const schema = {
82
+ properties: {
83
+ value: {
84
+ properties: {
85
+ something: {
86
+ type: "string",
87
+ },
88
+ another: {
89
+ type: "integer",
90
+ },
91
+ },
92
+ },
93
+ },
94
+ } as const;
95
+
96
+ const goodValue = {
97
+ something: "hello",
98
+ another: 42,
99
+ } as const;
100
+
101
+ const putted = await graffiti.put<typeof schema>(
102
+ {
103
+ value: goodValue,
104
+ channels: [],
105
+ },
106
+ session,
107
+ );
108
+
109
+ const gotten = await graffiti.get(putted, schema);
110
+ expect(gotten.value.something).toEqual(goodValue.something);
111
+ expect(gotten.value.another).toEqual(goodValue.another);
112
+ });
113
+
114
+ it("put and get with bad schema", async () => {
115
+ const graffiti = useGraffiti();
116
+ const session = useSession1();
117
+
118
+ const putted = await graffiti.put(
119
+ {
120
+ value: {
121
+ hello: "world",
122
+ },
123
+ channels: [],
124
+ },
125
+ session,
126
+ );
127
+
128
+ await expect(
129
+ graffiti.get(putted, {
130
+ properties: {
131
+ value: {
132
+ properties: {
133
+ hello: {
134
+ type: "number",
135
+ },
136
+ },
137
+ },
138
+ },
139
+ }),
140
+ ).rejects.toThrow();
141
+ });
142
+
143
+ it("put and get with empty access control", async () => {
144
+ const graffiti = useGraffiti();
145
+ const session1 = useSession1();
146
+ const session2 = useSession2();
147
+
148
+ const value = {
149
+ um: "hi",
150
+ };
151
+ const allowed = ["asdf"];
152
+ const channels = ["helloooo"];
153
+ const putted = await graffiti.put({ value, allowed, channels }, session1);
154
+
155
+ // Get it with authenticated session
156
+ const gotten = await graffiti.get(putted, {}, session1);
157
+ expect(gotten.value).toEqual(value);
158
+ expect(gotten.allowed).toEqual(allowed);
159
+ expect(gotten.channels).toEqual(channels);
160
+
161
+ // But not without session
162
+ await expect(graffiti.get(putted, {})).rejects.toThrow();
163
+
164
+ // Or the wrong session
165
+ await expect(graffiti.get(putted, {}, session2)).rejects.toThrow();
166
+ });
167
+
168
+ it("put and get with specific access control", async () => {
169
+ const graffiti = useGraffiti();
170
+ const session1 = useSession1();
171
+ const session2 = useSession2();
172
+
173
+ const value = {
174
+ um: "hi",
175
+ };
176
+ const allowed = ["asdf", session2.actor, "1234"];
177
+ const channels = ["helloooo"];
178
+ const putted = await graffiti.put(
179
+ {
180
+ value,
181
+ allowed,
182
+ channels,
183
+ },
184
+ session1,
185
+ );
186
+
187
+ // Get it with authenticated session
188
+ const gotten = await graffiti.get(putted, {}, session1);
189
+ expect(gotten.value).toEqual(value);
190
+ expect(gotten.allowed).toEqual(allowed);
191
+ expect(gotten.channels).toEqual(channels);
192
+
193
+ // But not without session
194
+ await expect(graffiti.get(putted, {})).rejects.toThrow();
195
+
196
+ const gotten2 = await graffiti.get(putted, {}, session2);
197
+ expect(gotten2.value).toEqual(value);
198
+ // They should only see that is is private to them
199
+ expect(gotten2.allowed).toEqual([session2.actor]);
200
+ // And not see any channels
201
+ expect(gotten2.channels).toEqual([]);
202
+ });
203
+
204
+ it("patch value", async () => {
205
+ const graffiti = useGraffiti();
206
+ const session = useSession1();
207
+
208
+ const value = {
209
+ something: "hello, world~ c:",
210
+ };
211
+ const putted = await graffiti.put({ value, channels: [] }, session);
212
+
213
+ const patch: GraffitiPatch = {
214
+ value: [
215
+ { op: "replace", path: "/something", value: "goodbye, world~ :c" },
216
+ ],
217
+ };
218
+ const beforePatched = await graffiti.patch(patch, putted, session);
219
+ expect(beforePatched.value).toEqual(value);
220
+ expect(beforePatched.tombstone).toBe(true);
221
+
222
+ const gotten = await graffiti.get(putted, {});
223
+ expect(gotten.value).toEqual({
224
+ something: "goodbye, world~ :c",
225
+ });
226
+ expect(beforePatched.lastModified.getTime()).toBe(
227
+ gotten.lastModified.getTime(),
228
+ );
229
+
230
+ await graffiti.delete(putted, session);
231
+ });
232
+
233
+ it("patch channels", async () => {
234
+ const graffiti = useGraffiti();
235
+ const session = useSession1();
236
+
237
+ const putted = await graffiti.put(
238
+ { value: {}, channels: ["helloooo"] },
239
+ session,
240
+ );
241
+
242
+ const patch: GraffitiPatch = {
243
+ channels: [{ op: "replace", path: "/0", value: "goodbye" }],
244
+ };
245
+ await graffiti.patch(patch, putted, session);
246
+ const gotten = await graffiti.get(putted, {}, session);
247
+ expect(gotten.channels).toEqual(["goodbye"]);
248
+ await graffiti.delete(putted, session);
249
+ });
250
+ };
package/tests/index.ts CHANGED
@@ -1 +1,2 @@
1
1
  export * from "./location";
2
+ export * from "./crud";
package/src/errors.ts DELETED
@@ -1,14 +0,0 @@
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
- }
File without changes