@graffiti-garden/api 0.4.4 → 0.5.0

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/2-types.ts CHANGED
@@ -12,9 +12,7 @@ import type { Operation as JSONPatchOperation } from "fast-json-patch";
12
12
  * or properties that emerge in the Graffiti [folksonomy](https://en.wikipedia.org/wiki/Folksonomy)
13
13
  * to promote interoperability.
14
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}.
15
+ * The object is globally addressable via its {@link uri | `uri`}.
18
16
  *
19
17
  * The {@link channels | `channels`} and {@link allowed | `allowed`} properties
20
18
  * enable the object's creator to shape the visibility of and access to their object.
@@ -78,21 +76,23 @@ export interface GraffitiObjectBase {
78
76
  actor: string;
79
77
 
80
78
  /**
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.
79
+ * A globally unique identifier for the object. It can be used to point to
80
+ * an object or to retrieve the object directly with {@link Graffiti.get}.
81
+ * If an object is {@link Graffiti.put | put} with the same URI
82
+ * as an existing object, the existing object will be replaced with the new object.
83
+ *
84
+ * The URI is generated on creation and include sufficient randomness to prevent collisions
85
+ * and guessing. The URI starts with "scheme", just like web URLs start with `http` or `https`, to indicate
86
+ * to indicate the particular Graffiti implementation. This allows for applications
87
+ * to pull from multiple coexisting Graffiti implementations without collision.
88
+ * Existing schemes include `graffiti:local:` for objects stored locally
89
+ * (see the [local implementation](https://github.com/graffiti-garden/implementation-local))
90
+ * and `graffiti:remote:` for objects stored on Graffiti-specific web servers (see the
91
+ * [remote implementation](https://github.com/graffiti-garden/implementation-remote)).
92
+ * Options available in the future might include `graffiti:solid:` for objects stored on Solid servers
93
+ * or `graffiti:p2p:` for objects stored on a peer-to-peer network.
94
94
  */
95
- source: string;
95
+ uri: string;
96
96
 
97
97
  /**
98
98
  * The time the object was last modified, measured in milliseconds since January 1, 1970.
@@ -135,38 +135,23 @@ export const GraffitiObjectJSONSchema = {
135
135
  value: { type: "object" },
136
136
  channels: { type: "array", items: { type: "string" } },
137
137
  allowed: { type: "array", items: { type: "string" }, nullable: true },
138
+ uri: { type: "string" },
138
139
  actor: { type: "string" },
139
- name: { type: "string" },
140
- source: { type: "string" },
141
140
  lastModified: { type: "number" },
142
141
  tombstone: { type: "boolean" },
143
142
  },
144
143
  additionalProperties: false,
145
- required: [
146
- "value",
147
- "channels",
148
- "actor",
149
- "name",
150
- "source",
151
- "lastModified",
152
- "tombstone",
153
- ],
144
+ required: ["value", "channels", "actor", "uri", "lastModified", "tombstone"],
154
145
  } as const satisfies JSONSchema;
155
146
 
156
147
  /**
157
- * This is a subset of properties from {@link GraffitiObjectBase} that uniquely
158
- * identify an object's location: {@link GraffitiObjectBase.actor | `actor`},
159
- * {@link GraffitiObjectBase.name | `name`}, and {@link GraffitiObjectBase.source | `source`}.
160
- * Attempts to create an object with the same `actor`, `name`, and `source`
161
- * as an existing object will replace the existing object (see {@link Graffiti.put}).
162
- *
163
- * This location can be converted to
164
- * a globally unique URI using {@link Graffiti.locationToUri}.
148
+ * This is an object containing only the {@link GraffitiObjectBase.uri | `uri`}
149
+ * property of a {@link GraffitiObjectBase | GraffitiObject}.
150
+ * It is used as a utility type so that users can call {@link Graffiti.get},
151
+ * {@link Graffiti.patch}, or {@link Graffiti.delete} directly on an object
152
+ * rather than on `object.uri`.
165
153
  */
166
- export type GraffitiLocation = Pick<
167
- GraffitiObjectBase,
168
- "actor" | "name" | "source"
169
- >;
154
+ export type GraffitiLocation = Pick<GraffitiObjectBase, "uri">;
170
155
 
171
156
  /**
172
157
  * This object is a subset of {@link GraffitiObjectBase} that a user must construct locally before calling {@link Graffiti.put}.
@@ -176,12 +161,8 @@ export type GraffitiLocation = Pick<
176
161
  * This local object must have a {@link GraffitiObjectBase.value | `value`} and {@link GraffitiObjectBase.channels | `channels`}
177
162
  * and may optionally have an {@link GraffitiObjectBase.allowed | `allowed`} property.
178
163
  *
179
- * It may also contain any of the {@link GraffitiLocation } properties: {@link GraffitiObjectBase.actor | `actor`},
180
- * {@link GraffitiObjectBase.name | `name`}, and {@link GraffitiObjectBase.source | `source`}.
181
- * If the location provided exactly matches an existing object, the existing object will be replaced.
182
- * If no `name` is provided, one will be randomly generated.
183
- * If no `actor` is provided, the `actor` from the supplied {@link GraffitiSession | `session` } will be used.
184
- * If no `source` is provided, one may be inferred by the depending on implementation.
164
+ * It may also include a {@link GraffitiObjectBase.uri | `uri`} property to specify the
165
+ * URI of an existing object to replace. If no `uri` is provided, one will be generated during object creation.
185
166
  *
186
167
  * This object does not need a {@link GraffitiObjectBase.lastModified | `lastModified`} or {@link GraffitiObjectBase.tombstone | `tombstone`}
187
168
  * property since these are automatically generated by the Graffiti system.
@@ -204,8 +185,7 @@ export const GraffitiPutObjectJSONSchema = {
204
185
  } as const satisfies JSONSchema;
205
186
 
206
187
  /**
207
- * This object contains information that
208
- * {@link GraffitiObjectBase.source | `source`}s can
188
+ * This object contains information that the underlying implementation can
209
189
  * use to verify that a user has permission to operate a
210
190
  * particular {@link GraffitiObjectBase.actor | `actor`}.
211
191
  * This object is required of all {@link Graffiti} methods
@@ -289,10 +269,12 @@ export interface GraffitiPatch {
289
269
  *
290
270
  * Errors are returned within the stream rather than as
291
271
  * exceptions that would halt the entire stream. This is because
292
- * some implementations may pull data from multiple
293
- * {@link GraffitiObjectBase.source | `source`}s
272
+ * some implementations may pull data from multiple sources
294
273
  * including some that may be unreliable. In many cases,
295
274
  * these errors can be safely ignored.
275
+ * The `origin` property of the error object indicates the
276
+ * source of the error including its scheme and other
277
+ * implementation-specific details (e.g. domain name).
296
278
  *
297
279
  * The stream is an [`AsyncGenerator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function)
298
280
  * that can be iterated over using `for await` loops or calling `next` on the generator.
@@ -305,7 +287,7 @@ export type GraffitiStream<TValue, TReturn = void> = AsyncGenerator<
305
287
  }
306
288
  | {
307
289
  error: Error;
308
- source: string;
290
+ origin: string;
309
291
  },
310
292
  TReturn
311
293
  >;
package/src/3-errors.ts CHANGED
@@ -61,3 +61,11 @@ export class GraffitiErrorInvalidUri extends Error {
61
61
  Object.setPrototypeOf(this, GraffitiErrorInvalidUri.prototype);
62
62
  }
63
63
  }
64
+
65
+ export class GraffitiErrorUnrecognizedUriScheme extends Error {
66
+ constructor(message?: string) {
67
+ super(message);
68
+ this.name = "GraffitiErrorUnrecognizedUriScheme";
69
+ Object.setPrototypeOf(this, GraffitiErrorUnrecognizedUriScheme.prototype);
70
+ }
71
+ }
package/tests/crud.ts CHANGED
@@ -5,7 +5,6 @@ import type {
5
5
  GraffitiPatch,
6
6
  JSONSchema,
7
7
  } from "@graffiti-garden/api";
8
- import type { FromSchema } from "json-schema-to-ts";
9
8
  import {
10
9
  GraffitiErrorNotFound,
11
10
  GraffitiErrorSchemaMismatch,
@@ -48,7 +47,7 @@ export const graffitiCRUDTests = (
48
47
  const previous = await graffiti.put<{}>({ value, channels }, session);
49
48
  expect(previous.value).toEqual({});
50
49
  expect(previous.channels).toEqual([]);
51
- expect(previous.allowed).toBeUndefined();
50
+ expect(previous.allowed).toEqual([]);
52
51
  expect(previous.actor).toEqual(session.actor);
53
52
 
54
53
  // Get it back
@@ -56,9 +55,8 @@ export const graffitiCRUDTests = (
56
55
  expect(gotten.value).toEqual(value);
57
56
  expect(gotten.channels).toEqual([]);
58
57
  expect(gotten.allowed).toBeUndefined();
59
- expect(gotten.name).toEqual(previous.name);
58
+ expect(gotten.uri).toEqual(previous.uri);
60
59
  expect(gotten.actor).toEqual(previous.actor);
61
- expect(gotten.source).toEqual(previous.source);
62
60
  expect(gotten.lastModified).toEqual(previous.lastModified);
63
61
 
64
62
  // Replace it
@@ -66,14 +64,17 @@ export const graffitiCRUDTests = (
66
64
  something: "goodbye, world~ :c",
67
65
  };
68
66
  const beforeReplaced = await graffiti.put<{}>(
69
- { ...previous, value: newValue, channels: [] },
67
+ {
68
+ uri: previous.uri,
69
+ value: newValue,
70
+ channels: [],
71
+ },
70
72
  session,
71
73
  );
72
74
  expect(beforeReplaced.value).toEqual(value);
73
75
  expect(beforeReplaced.tombstone).toEqual(true);
74
- expect(beforeReplaced.name).toEqual(previous.name);
76
+ expect(beforeReplaced.uri).toEqual(previous.uri);
75
77
  expect(beforeReplaced.actor).toEqual(previous.actor);
76
- expect(beforeReplaced.source).toEqual(previous.source);
77
78
  expect(beforeReplaced.lastModified).toBeGreaterThanOrEqual(
78
79
  gotten.lastModified,
79
80
  );
@@ -95,22 +96,14 @@ export const graffitiCRUDTests = (
95
96
  // Get a tombstone
96
97
  const final = await graffiti.get(afterReplaced, {});
97
98
  expect(final).toEqual(beforeDeleted);
98
- });
99
99
 
100
- it("get non-existant", async () => {
101
- const putted = await graffiti.put<{}>(randomPutObject(), session);
102
- await expect(
103
- graffiti.get(
104
- {
105
- ...putted,
106
- name: randomString(),
107
- },
108
- {},
109
- ),
110
- ).rejects.toBeInstanceOf(GraffitiErrorNotFound);
100
+ // Delete it again
101
+ await expect(graffiti.delete(final, session)).rejects.toThrow(
102
+ GraffitiErrorNotFound,
103
+ );
111
104
  });
112
105
 
113
- it("put, get, delete with wrong actor", async () => {
106
+ it("put, delete, patch with wrong actor", async () => {
114
107
  await expect(
115
108
  graffiti.put<{}>(
116
109
  { value: {}, channels: [], actor: session2.actor },
@@ -123,6 +116,17 @@ export const graffitiCRUDTests = (
123
116
  session2,
124
117
  );
125
118
 
119
+ await expect(
120
+ graffiti.put<{}>(
121
+ {
122
+ uri: putted.uri,
123
+ value: {},
124
+ channels: [],
125
+ },
126
+ session1,
127
+ ),
128
+ ).rejects.toThrow(GraffitiErrorForbidden);
129
+
126
130
  await expect(graffiti.delete(putted, session1)).rejects.toThrow(
127
131
  GraffitiErrorForbidden,
128
132
  );
@@ -132,6 +136,36 @@ export const graffitiCRUDTests = (
132
136
  );
133
137
  });
134
138
 
139
+ it("put, patch, delete object that is not allowed", async () => {
140
+ const putted = await graffiti.put<{}>(
141
+ {
142
+ value: {},
143
+ channels: [],
144
+ allowed: [],
145
+ },
146
+ session1,
147
+ );
148
+
149
+ await expect(
150
+ graffiti.put(
151
+ {
152
+ uri: putted.uri,
153
+ value: {},
154
+ channels: [],
155
+ },
156
+ session2,
157
+ ),
158
+ ).rejects.toThrow(GraffitiErrorNotFound);
159
+
160
+ await expect(graffiti.patch({}, putted, session2)).rejects.toThrow(
161
+ GraffitiErrorNotFound,
162
+ );
163
+
164
+ await expect(graffiti.delete(putted, session2)).rejects.toThrow(
165
+ GraffitiErrorNotFound,
166
+ );
167
+ });
168
+
135
169
  it("put and get with schema", async () => {
136
170
  const schema = {
137
171
  properties: {
package/tests/discover.ts CHANGED
@@ -89,7 +89,7 @@ export const graffitiDiscoverTests = (
89
89
  expect(value.lastModified).toEqual(putted.lastModified);
90
90
  });
91
91
 
92
- for (const prop of ["name", "actor", "lastModified"] as const) {
92
+ for (const prop of ["actor", "lastModified"] as const) {
93
93
  it(`discover for ${prop}`, async () => {
94
94
  const object1 = randomPutObject();
95
95
  const putted1 = await graffiti.put<{}>(object1, session1);
@@ -109,8 +109,8 @@ export const graffitiDiscoverTests = (
109
109
  });
110
110
 
111
111
  const value = await nextStreamValue(iterator);
112
- expect(value.name).toEqual(putted1.name);
113
- expect(value.name).not.toEqual(putted2.name);
112
+ expect(value.uri).toEqual(putted1.uri);
113
+ expect(value.uri).not.toEqual(putted2.uri);
114
114
  expect(value.value).toEqual(object1.value);
115
115
  await expect(iterator.next()).resolves.toHaveProperty("done", true);
116
116
  });
@@ -123,7 +123,7 @@ export const graffitiDiscoverTests = (
123
123
  await new Promise((r) => setTimeout(r, 20));
124
124
  const putted2 = await graffiti.put<{}>(object, session);
125
125
 
126
- expect(putted1.name).not.toEqual(putted2.name);
126
+ expect(putted1.uri).not.toEqual(putted2.uri);
127
127
  expect(putted1.lastModified).toBeLessThan(putted2.lastModified);
128
128
 
129
129
  const gtIterator = graffiti.discover([object.channels[0]], {
@@ -142,7 +142,7 @@ export const graffitiDiscoverTests = (
142
142
  },
143
143
  });
144
144
  const value1 = await nextStreamValue(gtIteratorEpsilon);
145
- expect(value1.name).toEqual(putted2.name);
145
+ expect(value1.uri).toEqual(putted2.uri);
146
146
  expect(await gtIteratorEpsilon.next()).toHaveProperty("done", true);
147
147
  const gteIterator = graffiti.discover(object.channels, {
148
148
  properties: {
@@ -153,7 +153,7 @@ export const graffitiDiscoverTests = (
153
153
  },
154
154
  });
155
155
  const value = await nextStreamValue(gteIterator);
156
- expect(value.name).toEqual(putted2.name);
156
+ expect(value.uri).toEqual(putted2.uri);
157
157
  expect(await gteIterator.next()).toHaveProperty("done", true);
158
158
  const gteIteratorEpsilon = graffiti.discover(object.channels, {
159
159
  properties: {
@@ -181,7 +181,7 @@ export const graffitiDiscoverTests = (
181
181
  },
182
182
  });
183
183
  const value3 = await nextStreamValue(ltIteratorEpsilon);
184
- expect(value3.name).toEqual(putted1.name);
184
+ expect(value3.uri).toEqual(putted1.uri);
185
185
  expect(await ltIteratorEpsilon.next()).toHaveProperty("done", true);
186
186
 
187
187
  const lteIterator = graffiti.discover(object.channels, {
@@ -192,7 +192,7 @@ export const graffitiDiscoverTests = (
192
192
  },
193
193
  });
194
194
  const value2 = await nextStreamValue(lteIterator);
195
- expect(value2.name).toEqual(putted1.name);
195
+ expect(value2.uri).toEqual(putted1.uri);
196
196
  expect(await lteIterator.next()).toHaveProperty("done", true);
197
197
 
198
198
  const lteIteratorEpsilon = graffiti.discover(object.channels, {
@@ -481,8 +481,8 @@ export const graffitiDiscoverTests = (
481
481
  const object2 = randomPutObject();
482
482
  const replaced = await graffiti.put<{}>(
483
483
  {
484
- ...putted,
485
484
  ...object2,
485
+ uri: putted.uri,
486
486
  },
487
487
  session,
488
488
  );
@@ -535,11 +535,21 @@ export const graffitiDiscoverTests = (
535
535
 
536
536
  it("put concurrently and discover one", async () => {
537
537
  const object = randomPutObject();
538
- object.name = randomString();
539
538
 
540
- const putPromises = Array(100)
539
+ // Put a first one to get a URI
540
+ const putted = await graffiti.put<{}>(object, session);
541
+
542
+ const putPromises = Array(99)
541
543
  .fill(0)
542
- .map(() => graffiti.put<{}>(object, session));
544
+ .map(() =>
545
+ graffiti.put<{}>(
546
+ {
547
+ ...object,
548
+ uri: putted.uri,
549
+ },
550
+ session,
551
+ ),
552
+ );
543
553
  await Promise.all(putPromises);
544
554
 
545
555
  const iterator = graffiti.discover(object.channels, {});
package/tests/index.ts CHANGED
@@ -1,4 +1,3 @@
1
- export * from "./location";
2
1
  export * from "./crud";
3
2
  export * from "./discover";
4
3
  export * from "./orphans";
package/tests/orphans.ts CHANGED
@@ -27,7 +27,7 @@ export const graffitiOrphanTests = (
27
27
  const orphanIterator1 = graffiti.recoverOrphans({}, session);
28
28
  for await (const orphan of orphanIterator1) {
29
29
  if (orphan.error) continue;
30
- existingOrphans.push(orphan.value.name);
30
+ existingOrphans.push(orphan.value.uri);
31
31
  }
32
32
 
33
33
  const object = randomPutObject();
@@ -37,9 +37,8 @@ export const graffitiOrphanTests = (
37
37
  let numResults = 0;
38
38
  for await (const orphan of orphanIterator2) {
39
39
  if (orphan.error) continue;
40
- if (orphan.value.name === putted.name) {
40
+ if (orphan.value.uri === putted.uri) {
41
41
  numResults++;
42
- expect(orphan.value.source).toBe(putted.source);
43
42
  expect(orphan.value.lastModified).toBe(putted.lastModified);
44
43
  }
45
44
  }
@@ -62,14 +61,14 @@ export const graffitiOrphanTests = (
62
61
  },
63
62
  session,
64
63
  );
65
- expect(putNotOrphan.name).toBe(putOrphan.name);
64
+ expect(putNotOrphan.uri).toBe(putOrphan.uri);
66
65
  expect(putNotOrphan.lastModified).toBeGreaterThan(putOrphan.lastModified);
67
66
 
68
67
  const orphanIterator = graffiti.recoverOrphans({}, session);
69
68
  let numResults = 0;
70
69
  for await (const orphan of orphanIterator) {
71
70
  if (orphan.error) continue;
72
- if (orphan.value.name === putOrphan.name) {
71
+ if (orphan.value.uri === putOrphan.uri) {
73
72
  numResults++;
74
73
  expect(orphan.value.tombstone).toBe(true);
75
74
  expect(orphan.value.lastModified).toBe(putNotOrphan.lastModified);
@@ -1,3 +0,0 @@
1
- import type { Graffiti } from "@graffiti-garden/api";
2
- export declare const graffitiLocationTests: (useGraffiti: () => Pick<Graffiti, "locationToUri" | "uriToLocation">) => void;
3
- //# sourceMappingURL=location.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"location.d.ts","sourceRoot":"","sources":["../../tests/location.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAIrD,eAAO,MAAM,qBAAqB,GAChC,aAAa,MAAM,IAAI,CAAC,QAAQ,EAAE,eAAe,GAAG,eAAe,CAAC,SAmCrE,CAAC"}
package/tests/location.ts DELETED
@@ -1,42 +0,0 @@
1
- import { it, expect, describe } from "vitest";
2
- import type { Graffiti } from "@graffiti-garden/api";
3
- import { GraffitiErrorInvalidUri } from "@graffiti-garden/api";
4
- import { randomString } from "./utils";
5
-
6
- export const graffitiLocationTests = (
7
- useGraffiti: () => Pick<Graffiti, "locationToUri" | "uriToLocation">,
8
- ) => {
9
- describe.concurrent("URI and location conversion", () => {
10
- it("location to uri and back", async () => {
11
- const graffiti = useGraffiti();
12
- const location = {
13
- name: randomString(),
14
- actor: randomString(),
15
- source: randomString(),
16
- };
17
- const uri = graffiti.locationToUri(location);
18
- const location2 = graffiti.uriToLocation(uri);
19
- expect(location).toEqual(location2);
20
- });
21
-
22
- it("collision resistance", async () => {
23
- const graffiti = useGraffiti();
24
- const location1 = {
25
- name: randomString(),
26
- actor: randomString(),
27
- source: randomString(),
28
- };
29
- for (const prop of ["name", "actor", "source"] as const) {
30
- const location2 = { ...location1, [prop]: randomString() };
31
- const uri1 = graffiti.locationToUri(location1);
32
- const uri2 = graffiti.locationToUri(location2);
33
- expect(uri1).not.toEqual(uri2);
34
- }
35
- });
36
-
37
- it("random URI should not be a valid location", async () => {
38
- const graffiti = useGraffiti();
39
- expect(() => graffiti.uriToLocation("")).toThrow(GraffitiErrorInvalidUri);
40
- });
41
- });
42
- };