Skip to content

Commit

Permalink
Membership, #61
Browse files Browse the repository at this point in the history
  • Loading branch information
fabiancook committed Jul 28, 2023
1 parent 9218aab commit 638c283
Show file tree
Hide file tree
Showing 11 changed files with 234 additions and 9 deletions.
3 changes: 2 additions & 1 deletion src/data/authentication-state/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ export type AuthenticationStateType =
| "exchange"
| "challenge"
| "credential"
| "anonymous";
| "anonymous"
| "membership";

export interface AuthenticationStateFromData {
type: AuthenticationStateType | string;
Expand Down
3 changes: 2 additions & 1 deletion src/data/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ export * from "./offer";
export * from "./user-credential";
export * from "./task";
export * from "./appointment";
export * from "./change";
export * from "./change";
export * from "./membership";
6 changes: 6 additions & 0 deletions src/data/membership/delete-membership.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import {getMembershipStore} from "./store";

export async function deleteMembership(membershipId: string) {
const store = getMembershipStore();
return store.delete(membershipId);
}
6 changes: 6 additions & 0 deletions src/data/membership/get-membership.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import {getMembershipStore} from "./store";

export function getMembership(membershipId: string) {
const store = getMembershipStore();
return store.get(membershipId);
}
32 changes: 32 additions & 0 deletions src/data/membership/get-referenced-memberships.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {setMembership} from "./set-membership";
import {Membership, MembershipData} from "./types";

export async function createMembershipReferences(input: (string | MembershipData)[]): Promise<Membership[]> {
const membershipInput = parseMembershipReferences(input);
return membershipInput.length ? await Promise.all(
membershipInput.map(setMembership)
) : [];
}

export function getMembershipReferenceMap(memberships: Membership[]) {
return new Map(
memberships.map(membership => [membership.reference, membership])
);
}

export function parseMembershipReferences(memberships: (MembershipData | string)[]): MembershipData[] {
return [
...(memberships ?? []).map(membership => {
if (typeof membership === "string") {
return { reference: membership }
}
return membership;
})
]
.filter(
(value, index, array) => {
const before = array.slice(0, index);
return !before.find(other => other.reference === value.reference);
}
)
}
6 changes: 6 additions & 0 deletions src/data/membership/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export * from "./types";
export * from "./get-membership";
export * from "./set-membership";
export * from "./store";
export * from "./delete-membership";
export * as membershipSchema from "./schema";
47 changes: 47 additions & 0 deletions src/data/membership/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
export const membershipData = {
type: "object",
properties: {
reference: {
type: "string"
},
name: {
type: "string",
nullable: true
},
email: {
type: "string",
nullable: true
},
membershipId: {
type: "string",
nullable: true
},
createdAt: {
type: "string",
nullable: true
},
},
additionalProperties: true,
required: [
"reference"
]
}

export const membership = {
type: "object",
properties: {
...membershipData.properties,
membershipId: {
type: "string",
},
createdAt: {
type: "string",
},
},
additionalProperties: true,
required: [
...membershipData.required,
"membershipId",
"createdAt"
]
}
86 changes: 86 additions & 0 deletions src/data/membership/set-membership.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import {getMembershipStore} from "./store";
import {Membership, PartialMembership} from "./types";
import {createHash} from "crypto";
import {getMembership} from "./get-membership";
import {entries} from "../entries";
import {getMaybePartner, getMaybeUser, isUnauthenticated} from "../../authentication";
import {v4} from "uuid";
import {ok} from "../../is";

const {
ATTENDEE_DISABLE_PARTITION,
ATTENDEE_PARTITION,
} = process.env;

function getPartitionPrefix() {
if (ATTENDEE_PARTITION) {
return ATTENDEE_PARTITION;
}
const partner = getMaybePartner();
// If authenticated, membership information will be retained across happenings
if (partner?.partnerId) {
return `partner:${partner.partnerId}:`
}
const user = getMaybeUser();
if (user?.userId) {
// Users can create their own memberships
return `user:${user.userId}:`
}
ok(isUnauthenticated(), "Expected user or partner if not anonymous");
// Random every time if no authentication :)
// If creating a happening tree, each new tree request will have a new set of memberships
return v4();
}

/**
* Allows partial update of an membership, retains existing properties
* @param data
*/
export async function setMembership(data: PartialMembership) {
const store = getMembershipStore();
let reference = data.reference,
membershipId = data.membershipId;
// Allows for either reference or membershipId to be provided as a reference string
if (!membershipId) {
const existing = await getMembership(reference);
if (existing) {
reference = existing.reference;
membershipId = existing.membershipId;
}
}
if (!membershipId) {
membershipId = createMembershipId();
}
const existing = await getMembership(membershipId);
if (existing && !isDifferent(existing)) {
return existing;
}
const createdAt = data.createdAt || new Date().toISOString();
const createdByPartnerId = getMaybePartner()?.partnerId;
const createdByUserId = getMaybeUser()?.userId;
const membership: Membership = {
...existing,
...data,
reference,
membershipId,
createdAt,
createdByPartnerId,
createdByUserId
};
await store.set(membershipId, membership);
return membership;

function isDifferent(value: Membership) {
return !!entries(data).find(entry => value[entry[0]] !== entry[1]);
}

function createMembershipId() {
if (ATTENDEE_DISABLE_PARTITION) {
return data.reference;
}
const hash = createHash("sha512");
hash.update(getPartitionPrefix());
hash.update(data.reference);
return hash.digest().toString("hex");
}
}
10 changes: 10 additions & 0 deletions src/data/membership/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {getKeyValueStore} from "../kv";
import {Membership} from "./types";

const STORE_NAME = "membership" as const;

export function getMembershipStore() {
return getKeyValueStore<Membership>(STORE_NAME, {
counter: true
});
}
14 changes: 14 additions & 0 deletions src/data/membership/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export interface MembershipData extends Record<string, unknown> {
reference: string;
name?: string;
email?: string;
}

export interface Membership extends MembershipData {
membershipId: string;
createdAt: string;
createdByPartnerId?: string;
createdByUserId?: string;
}

export type PartialMembership = MembershipData & Partial<Membership>;
30 changes: 23 additions & 7 deletions src/react/server/paths/appointment/view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,8 @@ export async function handler(request: FastifyRequest<Schema>) {
return getAppointmentTree(appointmentId);
}

export function AppointmentPage() {
const appointment = useInput<AppointmentTree>();
export function getAppointmentDate(appointment: AppointmentTree) {
const timezone = appointment.timezone || DEFAULT_TIMEZONE;

const { url } = useData();
const { pathname } = new URL(url);
const { AppointmentActions } = useConfig();

let date,
dateString,
time,
Expand Down Expand Up @@ -91,6 +85,28 @@ export function AppointmentPage() {
dateString = `Ends at ${endInstance.toFormat(TIME_FORMAT)}, ${endInstance.toFormat(DATE_FORMAT)}`;
}

return {
date,
dateString,
time,
timeString
}
}

export function AppointmentPage() {
const appointment = useInput<AppointmentTree>();

const { url } = useData();
const { pathname } = new URL(url);
const { AppointmentActions } = useConfig();

const {
date,
dateString,
time,
timeString,
} = getAppointmentDate(appointment);

const status = appointment.status || "scheduled";
const isFinished = status === "completed";
const statusString = (appointment.status || "scheduled").replace(/^([a-z])/, (string) => string.toUpperCase());
Expand Down

0 comments on commit 638c283

Please sign in to comment.