@graffiti-garden/api 1.0.2 → 1.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/dist/index.cjs +1 -1
- package/dist/index.cjs.map +3 -3
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +3 -3
- package/dist/src/1-api.d.ts +23 -39
- package/dist/src/1-api.d.ts.map +1 -1
- package/dist/src/2-types.d.ts +38 -1
- package/dist/src/2-types.d.ts.map +1 -1
- package/dist/src/4-utilities.d.ts +3 -5
- package/dist/src/4-utilities.d.ts.map +1 -1
- package/dist/tests/crud.d.ts.map +1 -1
- package/dist/tests/discover.d.ts.map +1 -1
- package/dist/tests.mjs +65 -9
- package/dist/tests.mjs.map +3 -3
- package/package.json +4 -5
- package/src/1-api.ts +24 -38
- package/src/2-types.ts +41 -1
- package/src/4-utilities.ts +38 -34
- package/tests/crud.ts +12 -1
- package/tests/discover.ts +54 -2
- package/tests/media.ts +2 -2
package/src/1-api.ts
CHANGED
|
@@ -6,6 +6,9 @@ import type {
|
|
|
6
6
|
GraffitiPostObject,
|
|
7
7
|
GraffitiObjectStream,
|
|
8
8
|
GraffitiObjectStreamContinue,
|
|
9
|
+
GraffitiMedia,
|
|
10
|
+
GraffitiPostMedia,
|
|
11
|
+
GraffitiMediaAccept,
|
|
9
12
|
} from "./2-types";
|
|
10
13
|
import type { JSONSchema } from "json-schema-to-ts";
|
|
11
14
|
|
|
@@ -150,6 +153,8 @@ export abstract class Graffiti {
|
|
|
150
153
|
*
|
|
151
154
|
* @throws {@link GraffitiErrorSchemaMismatch} if the retrieved object does not match the provided schema.
|
|
152
155
|
*
|
|
156
|
+
* @throws {@link GraffitiErrorInvalidSchema} If an invalid schema is provided.
|
|
157
|
+
*
|
|
153
158
|
* @group 1 - Single-Object Methods
|
|
154
159
|
*/
|
|
155
160
|
abstract get<Schema extends JSONSchema>(
|
|
@@ -194,7 +199,7 @@ export abstract class Graffiti {
|
|
|
194
199
|
* {@link GraffitiObjectBase.actor | `actor`}.
|
|
195
200
|
*/
|
|
196
201
|
session: GraffitiSession,
|
|
197
|
-
): Promise<
|
|
202
|
+
): Promise<GraffitiObjectBase>;
|
|
198
203
|
|
|
199
204
|
/**
|
|
200
205
|
* Discovers objects created by any actor that are contained
|
|
@@ -223,6 +228,9 @@ export abstract class Graffiti {
|
|
|
223
228
|
* Since different implementations may fetch data from multiple sources there is
|
|
224
229
|
* no guarentee on the order that objects are returned in.
|
|
225
230
|
*
|
|
231
|
+
* @throws {@link GraffitiErrorInvalidSchema} if an invalid schema is provided.
|
|
232
|
+
* Discovery is lazy and will not throw until the iterator is consumed.
|
|
233
|
+
*
|
|
226
234
|
* @returns Returns a stream of objects that match the given {@link GraffitiObjectBase.channels | `channels`}
|
|
227
235
|
* and [JSON Schema](https://json-schema.org).
|
|
228
236
|
*
|
|
@@ -263,7 +271,11 @@ export abstract class Graffiti {
|
|
|
263
271
|
* instead, which is returned along with the `cursor` at the
|
|
264
272
|
* end of the original stream.
|
|
265
273
|
*
|
|
266
|
-
* @throws {@link
|
|
274
|
+
* @throws {@link GraffitiErrorNotFound} upon iteration
|
|
275
|
+
* if the cursor is invalid or expired.
|
|
276
|
+
*
|
|
277
|
+
* @throws {@link GraffitiErrorForbidden} upon iteration
|
|
278
|
+
* if the {@link GraffitiObjectBase.actor | `actor`}
|
|
267
279
|
* provided in the `session` is not the same as the `actor`
|
|
268
280
|
* that initiated the original stream.
|
|
269
281
|
*
|
|
@@ -286,24 +298,12 @@ export abstract class Graffiti {
|
|
|
286
298
|
* @group 3 - Media Methods
|
|
287
299
|
*/
|
|
288
300
|
abstract postMedia(
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
data: Blob;
|
|
296
|
-
/**
|
|
297
|
-
* An optional list, identical in function to an object's
|
|
298
|
-
* {@link GraffitiObjectBase.allowed | `allowed`} property,
|
|
299
|
-
* that specifies the {@link GraffitiObjectBase.actor | `actor`}s
|
|
300
|
-
* who are allowed to access the media. If the list is `undefined`
|
|
301
|
-
* or `null`, anyone with the URL can access the media. If the list
|
|
302
|
-
* is empty, only the {@link GraffitiObjectBase.actor | `actor`}
|
|
303
|
-
* who {@link postMedia | `post`ed} the media can access it.
|
|
304
|
-
*/
|
|
305
|
-
allowed?: string[] | null;
|
|
306
|
-
},
|
|
301
|
+
/**
|
|
302
|
+
* The media data to upload, and optionally
|
|
303
|
+
* an {@link GraffitiObjectBase.allowed | `allowed`}
|
|
304
|
+
* list of actors that can view it.
|
|
305
|
+
*/
|
|
306
|
+
media: GraffitiPostMedia,
|
|
307
307
|
/**
|
|
308
308
|
* An implementation-specific object with information to authenticate the
|
|
309
309
|
* {@link GraffitiObjectBase.actor | `actor`}.
|
|
@@ -326,7 +326,7 @@ export abstract class Graffiti {
|
|
|
326
326
|
/**
|
|
327
327
|
* A globally unique identifier and locator for the media.
|
|
328
328
|
*/
|
|
329
|
-
|
|
329
|
+
url: string,
|
|
330
330
|
/**
|
|
331
331
|
* An implementation-specific object with information to authenticate the
|
|
332
332
|
* {@link GraffitiObjectBase.actor | `actor`}.
|
|
@@ -355,29 +355,15 @@ export abstract class Graffiti {
|
|
|
355
355
|
*/
|
|
356
356
|
mediaUrl: string,
|
|
357
357
|
/**
|
|
358
|
-
* A
|
|
358
|
+
* A specification for what types and sizes of media are acceptable.
|
|
359
359
|
*/
|
|
360
|
-
|
|
361
|
-
/**
|
|
362
|
-
* A list of acceptable media types for the retrieved media,
|
|
363
|
-
* formatted as like an [HTTP Accept header](https://httpwg.org/specs/rfc9110.html#field.accept)
|
|
364
|
-
*/
|
|
365
|
-
accept?: string;
|
|
366
|
-
/**
|
|
367
|
-
* The maximum acceptable size, in bytes, of the media.
|
|
368
|
-
*/
|
|
369
|
-
maxBytes?: number;
|
|
370
|
-
},
|
|
360
|
+
accept: GraffitiMediaAccept,
|
|
371
361
|
/**
|
|
372
362
|
* An implementation-specific object with information to authenticate the
|
|
373
363
|
* {@link GraffitiObjectBase.actor | `actor`}.
|
|
374
364
|
*/
|
|
375
365
|
session?: GraffitiSession | null,
|
|
376
|
-
): Promise<
|
|
377
|
-
data: Blob;
|
|
378
|
-
actor: string;
|
|
379
|
-
allowed?: string[] | null;
|
|
380
|
-
}>;
|
|
366
|
+
): Promise<GraffitiMedia>;
|
|
381
367
|
|
|
382
368
|
/**
|
|
383
369
|
* Begins the login process. Depending on the implementation, this may
|
package/src/2-types.ts
CHANGED
|
@@ -350,7 +350,7 @@ export type GraffitiObjectStreamContinue<Schema extends JSONSchema> =
|
|
|
350
350
|
export type GraffitiLoginEvent = CustomEvent<
|
|
351
351
|
| {
|
|
352
352
|
error: Error;
|
|
353
|
-
session?:
|
|
353
|
+
session?: GraffitiSession;
|
|
354
354
|
}
|
|
355
355
|
| {
|
|
356
356
|
error?: undefined;
|
|
@@ -396,3 +396,43 @@ export type GraffitiSessionInitializedEvent = CustomEvent<
|
|
|
396
396
|
| null
|
|
397
397
|
| undefined
|
|
398
398
|
>;
|
|
399
|
+
|
|
400
|
+
export type GraffitiMedia = {
|
|
401
|
+
/**
|
|
402
|
+
* The binary data of the media to be uploaded,
|
|
403
|
+
* along with its [media type](https://www.iana.org/assignments/media-types/media-types.xhtml),
|
|
404
|
+
* formatted as a [Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob).
|
|
405
|
+
*/
|
|
406
|
+
data: Blob;
|
|
407
|
+
/**
|
|
408
|
+
* The {@link GraffitiObjectBase.actor | `actor`} that
|
|
409
|
+
* {@link Graffiti.postMedia | `post`ed} the media.
|
|
410
|
+
*/
|
|
411
|
+
actor: string;
|
|
412
|
+
/**
|
|
413
|
+
* An optional list, identical in function to an object's
|
|
414
|
+
* {@link GraffitiObjectBase.allowed | `allowed`} property,
|
|
415
|
+
* that specifies the {@link GraffitiObjectBase.actor | `actor`}s
|
|
416
|
+
* who are allowed to access the media. If the list is `undefined`
|
|
417
|
+
* or `null`, anyone with the URL can access the media. If the list
|
|
418
|
+
* is empty, only the {@link GraffitiObjectBase.actor | `actor`}
|
|
419
|
+
* who {@link Graffiti.postMedia | `post`ed} the media can access it.
|
|
420
|
+
*/
|
|
421
|
+
allowed?: string[] | null;
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
export type GraffitiPostMedia = Pick<GraffitiMedia, "data" | "allowed">;
|
|
425
|
+
|
|
426
|
+
export type GraffitiMediaAccept = {
|
|
427
|
+
/**
|
|
428
|
+
* A list of acceptable media types for the retrieved media.
|
|
429
|
+
* Each type in the list may be of the form `<type>/<subtype>`,
|
|
430
|
+
* `<type>/*`, or `*/*`, just as types are formatted in
|
|
431
|
+
* an [HTTP Accept header](https://httpwg.org/specs/rfc9110.html#field.accept).
|
|
432
|
+
*/
|
|
433
|
+
types?: string[];
|
|
434
|
+
/**
|
|
435
|
+
* The maximum acceptable size, in bytes, of the media.
|
|
436
|
+
*/
|
|
437
|
+
maxBytes?: number;
|
|
438
|
+
};
|
package/src/4-utilities.ts
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import type { JSONSchema } from "json-schema-to-ts";
|
|
2
|
-
import type { Ajv } from "ajv";
|
|
3
|
-
import { GraffitiErrorInvalidSchema } from "./3-errors";
|
|
4
1
|
import type {
|
|
5
2
|
GraffitiObjectBase,
|
|
6
|
-
GraffitiObject,
|
|
7
3
|
GraffitiObjectUrl,
|
|
8
4
|
GraffitiSession,
|
|
9
5
|
} from "./2-types";
|
|
@@ -12,25 +8,6 @@ export function unpackObjectUrl(url: string | GraffitiObjectUrl) {
|
|
|
12
8
|
return typeof url === "string" ? url : url.url;
|
|
13
9
|
}
|
|
14
10
|
|
|
15
|
-
export function compileGraffitiObjectSchema<Schema extends JSONSchema>(
|
|
16
|
-
ajv: Ajv,
|
|
17
|
-
schema: Schema,
|
|
18
|
-
) {
|
|
19
|
-
try {
|
|
20
|
-
// Force the validation guard because
|
|
21
|
-
// it is too big for the type checker.
|
|
22
|
-
// Fortunately json-schema-to-ts is
|
|
23
|
-
// well tested against ajv.
|
|
24
|
-
return ajv.compile(schema) as (
|
|
25
|
-
data: GraffitiObjectBase,
|
|
26
|
-
) => data is GraffitiObject<Schema>;
|
|
27
|
-
} catch (error) {
|
|
28
|
-
throw new GraffitiErrorInvalidSchema(
|
|
29
|
-
error instanceof Error ? error.message : undefined,
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
11
|
export function isActorAllowedGraffitiObject(
|
|
35
12
|
object: GraffitiObjectBase,
|
|
36
13
|
session?: GraffitiSession | null,
|
|
@@ -50,16 +27,43 @@ export function isActorAllowedGraffitiObject(
|
|
|
50
27
|
export function maskGraffitiObject(
|
|
51
28
|
object: GraffitiObjectBase,
|
|
52
29
|
channels: string[],
|
|
53
|
-
|
|
54
|
-
):
|
|
55
|
-
// If the actor is
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
30
|
+
actor?: string | null,
|
|
31
|
+
): GraffitiObjectBase {
|
|
32
|
+
// If the actor is the creator, return the object as is
|
|
33
|
+
if (actor === object.actor) return object;
|
|
34
|
+
|
|
35
|
+
// If there is an allowed list, mask it to only include the actor
|
|
36
|
+
// (This assumes the actor is already allowed to access the object)
|
|
37
|
+
const allowedMasked = object.allowed && actor ? [actor] : undefined;
|
|
38
|
+
// Mask the channels to only include the channels that are being queried
|
|
39
|
+
const channelsMasked = object.channels.filter((c) => channels.includes(c));
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
...object,
|
|
43
|
+
allowed: allowedMasked,
|
|
44
|
+
channels: channelsMasked,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function isMediaAcceptable(
|
|
49
|
+
mediaType: string,
|
|
50
|
+
acceptableMediaTypes: string[],
|
|
51
|
+
): boolean {
|
|
52
|
+
const [type, subtype] = mediaType.toLowerCase().split(";")[0].split("/");
|
|
53
|
+
|
|
54
|
+
if (!type || !subtype) return false;
|
|
55
|
+
|
|
56
|
+
return acceptableMediaTypes.some((acceptable) => {
|
|
57
|
+
const [accType, accSubtype] = acceptable
|
|
58
|
+
.toLowerCase()
|
|
59
|
+
.split(";")[0]
|
|
60
|
+
.split("/");
|
|
61
|
+
|
|
62
|
+
if (!accType || !accSubtype) return false;
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
(accType === type || accType === "*") &&
|
|
66
|
+
(accSubtype === subtype || accSubtype === "*")
|
|
63
67
|
);
|
|
64
|
-
}
|
|
68
|
+
});
|
|
65
69
|
}
|
package/tests/crud.ts
CHANGED
|
@@ -34,6 +34,12 @@ export const graffitiCRUDTests = (
|
|
|
34
34
|
session2 = await useSession2();
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
+
it("get nonexistant object", async () => {
|
|
38
|
+
await expect(graffiti.get(randomString(), {})).rejects.toThrow(
|
|
39
|
+
GraffitiErrorNotFound,
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
37
43
|
it("post, get, delete", async () => {
|
|
38
44
|
const value = {
|
|
39
45
|
something: "hello, world~ c:",
|
|
@@ -56,7 +62,12 @@ export const graffitiCRUDTests = (
|
|
|
56
62
|
expect(gotten.actor).toEqual(previous.actor);
|
|
57
63
|
|
|
58
64
|
// Delete it
|
|
59
|
-
await graffiti.delete(gotten, session);
|
|
65
|
+
const deleted = await graffiti.delete(gotten, session);
|
|
66
|
+
expect(deleted.value).toEqual(value);
|
|
67
|
+
expect(deleted.channels).toEqual(channels);
|
|
68
|
+
expect(deleted.allowed).toBeUndefined();
|
|
69
|
+
expect(deleted.actor).toEqual(session.actor);
|
|
70
|
+
expect(deleted.url).toEqual(previous.url);
|
|
60
71
|
|
|
61
72
|
// Get is not found
|
|
62
73
|
await expect(graffiti.get(gotten, {})).rejects.toBeInstanceOf(
|
package/tests/discover.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { it, expect, describe, assert, beforeAll } from "vitest";
|
|
2
2
|
import type {
|
|
3
3
|
Graffiti,
|
|
4
|
-
GraffitiObjectBase,
|
|
5
4
|
GraffitiSession,
|
|
6
5
|
JSONSchema,
|
|
7
6
|
} from "@graffiti-garden/api";
|
|
7
|
+
import {
|
|
8
|
+
GraffitiErrorForbidden,
|
|
9
|
+
GraffitiErrorInvalidSchema,
|
|
10
|
+
GraffitiErrorNotFound,
|
|
11
|
+
} from "@graffiti-garden/api";
|
|
8
12
|
import {
|
|
9
13
|
randomString,
|
|
10
14
|
nextStreamValue,
|
|
@@ -103,6 +107,19 @@ export const graffitiDiscoverTests = (
|
|
|
103
107
|
expect(value.actor).toEqual(session1.actor);
|
|
104
108
|
});
|
|
105
109
|
|
|
110
|
+
it("discover bad schema", async () => {
|
|
111
|
+
const iterator = graffiti.discover([], {
|
|
112
|
+
properties: {
|
|
113
|
+
value: {
|
|
114
|
+
//@ts-ignore
|
|
115
|
+
type: "asdf",
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
await expect(iterator.next()).rejects.toThrow(GraffitiErrorInvalidSchema);
|
|
121
|
+
});
|
|
122
|
+
|
|
106
123
|
it("discover for actor", async () => {
|
|
107
124
|
const object1 = randomPostObject();
|
|
108
125
|
const posted1 = await graffiti.post<{}>(object1, session1);
|
|
@@ -396,12 +413,47 @@ export const graffitiDiscoverTests = (
|
|
|
396
413
|
assert(!value.done && !value.value.error, "value is done");
|
|
397
414
|
assert(value.value.tombstone, "value is not tombstone");
|
|
398
415
|
expect(value.value.object.url).toEqual(posted.url);
|
|
399
|
-
await
|
|
416
|
+
const returnValue2 = await tombIterator.next();
|
|
417
|
+
assert(returnValue2.done, "value2 is not done");
|
|
418
|
+
|
|
419
|
+
// Post another object
|
|
420
|
+
const posted2 = await graffiti.post<{}>(object, session);
|
|
421
|
+
const doubleContinueIterator = continueStream<{}>(
|
|
422
|
+
graffiti,
|
|
423
|
+
returnValue2.value,
|
|
424
|
+
continueType,
|
|
425
|
+
);
|
|
426
|
+
const value2 = await doubleContinueIterator.next();
|
|
427
|
+
assert(!value2.done && !value2.value.error, "value2 is done");
|
|
428
|
+
assert(!value2.value.tombstone, "value2 is tombstone");
|
|
429
|
+
expect(value2.value.object.url).toEqual(posted2.url);
|
|
430
|
+
await expect(doubleContinueIterator.next()).resolves.toHaveProperty(
|
|
400
431
|
"done",
|
|
401
432
|
true,
|
|
402
433
|
);
|
|
403
434
|
});
|
|
435
|
+
|
|
436
|
+
it("continue with wrong actor", async () => {
|
|
437
|
+
const iterator = graffiti.discover<{}>([], {}, session1);
|
|
438
|
+
const result = await iterator.next();
|
|
439
|
+
assert(result.done, "iterator is not done");
|
|
440
|
+
|
|
441
|
+
const continuation = continueStream<{}>(
|
|
442
|
+
graffiti,
|
|
443
|
+
result.value,
|
|
444
|
+
continueType,
|
|
445
|
+
session2,
|
|
446
|
+
);
|
|
447
|
+
await expect(continuation.next()).rejects.toThrow(
|
|
448
|
+
GraffitiErrorForbidden,
|
|
449
|
+
);
|
|
450
|
+
});
|
|
404
451
|
});
|
|
405
452
|
}
|
|
453
|
+
|
|
454
|
+
it("lookup non-existant cursor", async () => {
|
|
455
|
+
const iterator = graffiti.continueDiscover(randomString());
|
|
456
|
+
await expect(iterator.next()).rejects.toThrow(GraffitiErrorNotFound);
|
|
457
|
+
});
|
|
406
458
|
});
|
|
407
459
|
};
|
package/tests/media.ts
CHANGED
|
@@ -58,7 +58,7 @@ export const graffitiMediaTests = (
|
|
|
58
58
|
const mediaUrl = await graffiti.postMedia({ data }, session);
|
|
59
59
|
|
|
60
60
|
const media = await graffiti.getMedia(mediaUrl, {
|
|
61
|
-
|
|
61
|
+
types: ["application/json", "text/*"],
|
|
62
62
|
});
|
|
63
63
|
expect(await media.data.text()).toEqual(text);
|
|
64
64
|
expect(media.data.type).toEqual("text/plain");
|
|
@@ -73,7 +73,7 @@ export const graffitiMediaTests = (
|
|
|
73
73
|
|
|
74
74
|
await expect(
|
|
75
75
|
graffiti.getMedia(mediaUrl, {
|
|
76
|
-
|
|
76
|
+
types: ["image/*"],
|
|
77
77
|
}),
|
|
78
78
|
).rejects.toThrow(GraffitiErrorNotAcceptable);
|
|
79
79
|
});
|