From 4739e2b195512e278c44672d960cb95d3113cccf Mon Sep 17 00:00:00 2001 From: theia Date: Fri, 10 Jan 2025 12:10:34 -0500 Subject: [PATCH] added login to the api --- README.md | 25 ++++++++++++++ src/1-api.ts | 77 ++++++++++++++++++++++++++++++++++++++------ src/2-types.ts | 44 +++++++++++++++++++++---- tests/crud.ts | 6 ++-- tests/discover.ts | 6 ++-- tests/synchronize.ts | 6 ++-- 6 files changed, 140 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 37fec8a..107e2e7 100644 --- a/README.md +++ b/README.md @@ -20,3 +20,28 @@ Then run a local server to view the documentation: cd docs npx http-server ``` + +## Testing + +We have written a number of unit tests to verify implementations of the API with [vitest](https://vitest.dev/). +Use them as follows: + +```typescript +import { graffitiCRUDTests } from "@graffiti-garden/api/tests"; + +const useGraffiti = () => new MyGraffitiImplementation(); +// Fill in with implementation-specific information +// to provide to valid actor sessions for the tests +// to use as identities. +const useSession1 = () => ({ actor: "someone" }); +const useSession2 = () => ({ actor: "someoneelse" }); + +// Run the tests +graffitiCRUDTests(useGraffiti, useSession1, useSession2); +``` + +Then run the tests with: + +```bash +npx vitest +``` diff --git a/src/1-api.ts b/src/1-api.ts index 96c599e..f6bbeee 100644 --- a/src/1-api.ts +++ b/src/1-api.ts @@ -3,7 +3,7 @@ import type { GraffitiObject, GraffitiObjectBase, GraffitiPatch, - GraffitiSessionBase, + GraffitiSession, GraffitiPutObject, GraffitiStream, } from "./2-types"; @@ -123,7 +123,7 @@ export abstract class Graffiti { * An implementation-specific object with information to authenticate the * {@link GraffitiObjectBase.actor | `actor`}. */ - session: GraffitiSessionBase, + session: GraffitiSession, ): Promise; /** @@ -153,7 +153,7 @@ export abstract class Graffiti { * the retrieved object's {@link GraffitiObjectBase.allowed | `allowed`} * property must be `undefined`. */ - session?: GraffitiSessionBase, + session?: GraffitiSession, ): Promise>; /** @@ -183,7 +183,7 @@ export abstract class Graffiti { * An implementation-specific object with information to authenticate the * {@link GraffitiObjectBase.actor | `actor`}. */ - session: GraffitiSessionBase, + session: GraffitiSession, ): Promise; /** @@ -211,7 +211,7 @@ export abstract class Graffiti { * An implementation-specific object with information to authenticate the * {@link GraffitiObjectBase.actor | `actor`}. */ - session: GraffitiSessionBase, + session: GraffitiSession, ): Promise; /** @@ -262,7 +262,7 @@ export abstract class Graffiti { * only objects that have no {@link GraffitiObjectBase.allowed | `allowed`} * property will be returned. */ - session?: GraffitiSessionBase, + session?: GraffitiSession, ): GraffitiStream>; /** @@ -303,7 +303,7 @@ export abstract class Graffiti { * only objects that have no {@link GraffitiObjectBase.allowed | `allowed`} * property will be returned. */ - session?: GraffitiSessionBase, + session?: GraffitiSession, ): GraffitiStream>; /** @@ -327,7 +327,7 @@ export abstract class Graffiti { * An implementation-specific object with information to authenticate the * {@link GraffitiObjectBase.actor | `actor`}. */ - session: GraffitiSessionBase, + session: GraffitiSession, ): GraffitiStream<{ channel: string; source: string; @@ -353,12 +353,71 @@ export abstract class Graffiti { * {@link GraffitiObjectBase.tombstone | `tombstone`} field is `true` * if the object has been deleted. */ - abstract listOrphans(session: GraffitiSessionBase): GraffitiStream<{ + abstract listOrphans(session: GraffitiSession): GraffitiStream<{ name: string; source: string; lastModified: Date; tombstone: boolean; }>; + + /** + * Begins the login process. Depending on the implementation, this may + * involve redirecting the user to a login page or opening a popup, + * so it should always be called in response to a user action. + * + * The {@link GraffitiSession | session} object is returned + * asynchronously via the {@link Graffiti.sessionEvents | sessionEvents} + * event target as a {@link GraffitiLoginEvent}. + */ + abstract login( + /** + * An optional actor to prompt the user to login as. For example, + * if a session expired and the user is trying to reauthenticate, + * or if the user entered their username in a login form. + * + * If not provided, the implementation should prompt the user to + * supply an actor ID along with their other login information + * (e.g. password). + */ + actor?: string, + /** + * An arbitrary string that will be returned with the + * {@link GraffitiSession | session} object + * when the login process is complete. + * See {@link GraffitiLoginEvent}. + */ + state?: string, + ): Promise; + + /** + * Begins the logout process. Depending on the implementation, this may + * involve redirecting the user to a logout page or opening a popup, + * so it should always be called in response to a user action. + * + * A confirmation will be returned asynchronously via the + * {@link Graffiti.sessionEvents | sessionEvents} event target + * as a {@link GraffitiLogoutEvent}. + */ + abstract logout( + /** + * The {@link GraffitiSession | session} object to logout. + */ + session: GraffitiSession, + /** + * An arbitrary string that will be returned with the + * when the logout process is complete. + * See {@link GraffitiLogoutEvent}. + */ + state?: string, + ): Promise; + + /** + * An event target that can be used to listen for `login` + * and `logout` events. They are custom events of types + * {@link GraffitiLoginEvent`} and {@link GraffitiLogoutEvent } + * respectively. + */ + abstract readonly sessionEvents: EventTarget; } /** diff --git a/src/2-types.ts b/src/2-types.ts index 2815d10..bb2e187 100644 --- a/src/2-types.ts +++ b/src/2-types.ts @@ -146,7 +146,7 @@ export type GraffitiLocation = Pick< * {@link GraffitiObjectBase.name | `name`}, and {@link GraffitiObjectBase.source | `source`}. * If the location provided exactly matches an existing object, the existing object will be replaced. * If no `name` is provided, one will be randomly generated. - * If no `actor` is provided, the `actor` from the supplied {@link GraffitiSessionBase | `session` } will be used. + * If no `actor` is provided, the `actor` from the supplied {@link GraffitiSession | `session` } will be used. * If no `source` is provided, one may be inferred by the depending on implementation. * * This object does not need a {@link GraffitiObjectBase.lastModified | `lastModified`} or {@link GraffitiObjectBase.tombstone | `tombstone`} @@ -168,7 +168,7 @@ export type GraffitiPutObject = Pick< * that modify objects and optional for methods that read objects. * * At a minimum the `session` object must contain the - * {@link GraffitiSessionBase.actor | `actor`} URI the user wants to authenticate with. + * {@link GraffitiSession.actor | `actor`} URI the user wants to authenticate with. * However it is likely that the `session` object must contain other * implementation-specific properties. * For example, a Solid implementation might include a @@ -179,7 +179,7 @@ export type GraffitiPutObject = Pick< * It may also include other implementation specific properties * that provide hints for performance or security. */ -export interface GraffitiSessionBase { +export interface GraffitiSession { /** * The {@link GraffitiObjectBase.actor | `actor`} a user wants to authenticate with. */ @@ -241,13 +241,45 @@ export interface GraffitiPatch { */ export type GraffitiStream = AsyncGenerator< | { - error: false; + error?: undefined; value: T; } | { - error: true; - value: Error; + error: Error; source: string; }, void >; + +/** + * The event type produced in {@link Graffiti.sessionEvents} + * when a user logs in manually from {@link Graffiti.login} + * or when their session is restored from a previous login. + * The event name to listen for is `login`. + */ +export type GraffitiLoginEvent = CustomEvent< + { + state?: string; + } & ( + | { + error: Error; + session?: undefined; + } + | { + error?: undefined; + session: GraffitiSession; + } + ) +>; + +/** + * The event type produced in {@link Graffiti.sessionEvents} + * when a user logs out either manually with {@link Graffiti.logout} + * or when their session times out or otherwise becomes invalid. + * The event name to listen for is `logout`. + */ +export type GraffitiLogoutEvent = CustomEvent<{ + actor: string; + state?: string; + error?: Error; +}>; diff --git a/tests/crud.ts b/tests/crud.ts index 46529f2..a3be222 100644 --- a/tests/crud.ts +++ b/tests/crud.ts @@ -1,7 +1,7 @@ import { it, expect, describe } from "vitest"; import { type GraffitiFactory, - type GraffitiSessionBase, + type GraffitiSession, type GraffitiPatch, GraffitiErrorNotFound, GraffitiErrorSchemaMismatch, @@ -14,8 +14,8 @@ import { randomPutObject, randomString } from "./utils"; export const graffitiCRUDTests = ( useGraffiti: GraffitiFactory, - useSession1: () => GraffitiSessionBase, - useSession2: () => GraffitiSessionBase, + useSession1: () => GraffitiSession, + useSession2: () => GraffitiSession, ) => { describe("CRUD", () => { it("put, get, delete", async () => { diff --git a/tests/discover.ts b/tests/discover.ts index 8c2fcc1..c04fddd 100644 --- a/tests/discover.ts +++ b/tests/discover.ts @@ -1,11 +1,11 @@ import { it, expect, describe } from "vitest"; -import { type GraffitiFactory, type GraffitiSessionBase } from "../src/index"; +import { type GraffitiFactory, type GraffitiSession } from "../src/index"; import { randomString, randomValue, randomPutObject } from "./utils"; export const graffitiDiscoverTests = ( useGraffiti: GraffitiFactory, - useSession1: () => GraffitiSessionBase, - useSession2: () => GraffitiSessionBase, + useSession1: () => GraffitiSession, + useSession2: () => GraffitiSession, ) => { describe("discover", () => { it("discover single", async () => { diff --git a/tests/synchronize.ts b/tests/synchronize.ts index 7fea4fe..7d5c6e4 100644 --- a/tests/synchronize.ts +++ b/tests/synchronize.ts @@ -1,12 +1,12 @@ import { it, expect, describe } from "vitest"; -import { type GraffitiFactory, type GraffitiSessionBase } from "../src/index"; +import { type GraffitiFactory, type GraffitiSession } from "../src/index"; import { randomPutObject, randomString } from "./utils"; import { randomInt } from "crypto"; export const graffitiSynchronizeTests = ( useGraffiti: GraffitiFactory, - useSession1: () => GraffitiSessionBase, - useSession2: () => GraffitiSessionBase, + useSession1: () => GraffitiSession, + useSession2: () => GraffitiSession, ) => { describe("synchronize", () => { it("get", async () => {