Skip to content

Commit

Permalink
added login to the api
Browse files Browse the repository at this point in the history
  • Loading branch information
sportdeath committed Jan 10, 2025
1 parent 1d2756e commit 4739e2b
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 24 deletions.
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
77 changes: 68 additions & 9 deletions src/1-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type {
GraffitiObject,
GraffitiObjectBase,
GraffitiPatch,
GraffitiSessionBase,
GraffitiSession,
GraffitiPutObject,
GraffitiStream,
} from "./2-types";
Expand Down Expand Up @@ -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<GraffitiObjectBase>;

/**
Expand Down Expand Up @@ -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<GraffitiObject<Schema>>;

/**
Expand Down Expand Up @@ -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<GraffitiObjectBase>;

/**
Expand Down Expand Up @@ -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<GraffitiObjectBase>;

/**
Expand Down Expand Up @@ -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<GraffitiObject<Schema>>;

/**
Expand Down Expand Up @@ -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<GraffitiObject<Schema>>;

/**
Expand All @@ -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;
Expand All @@ -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<void>;

/**
* 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<void>;

/**
* 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;
}

/**
Expand Down
44 changes: 38 additions & 6 deletions src/2-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`}
Expand All @@ -168,7 +168,7 @@ export type GraffitiPutObject<Schema> = 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
Expand All @@ -179,7 +179,7 @@ export type GraffitiPutObject<Schema> = 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.
*/
Expand Down Expand Up @@ -241,13 +241,45 @@ export interface GraffitiPatch {
*/
export type GraffitiStream<T> = 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;
}>;
6 changes: 3 additions & 3 deletions tests/crud.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { it, expect, describe } from "vitest";
import {
type GraffitiFactory,
type GraffitiSessionBase,
type GraffitiSession,
type GraffitiPatch,
GraffitiErrorNotFound,
GraffitiErrorSchemaMismatch,
Expand All @@ -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 () => {
Expand Down
6 changes: 3 additions & 3 deletions tests/discover.ts
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand Down
6 changes: 3 additions & 3 deletions tests/synchronize.ts
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand Down

0 comments on commit 4739e2b

Please sign in to comment.