feat: Connect the frontend to the API (#160)

This commit is contained in:
Wanjohi
2025-01-18 07:12:47 +03:00
committed by GitHub
parent dfe37a6cec
commit f480ced756
56 changed files with 2109 additions and 743 deletions

View File

@@ -14,11 +14,17 @@ const _schema = i.schema({
profiles: i.entity({
avatarUrl: i.string().optional(),
username: i.string().indexed(),
ownerID: i.string().unique().indexed(),
updatedAt: i.date(),
createdAt: i.date(),
discriminator: i.string().indexed()
}),
teams: i.entity({
name: i.string(),
slug: i.string().unique().indexed(),
deletedAt: i.date().optional().indexed(),
updatedAt: i.date(),
createdAt: i.date(),
}),
games: i.entity({
name: i.string(),
steamID: i.number().unique().indexed(),
@@ -29,12 +35,31 @@ const _schema = i.schema({
endedAt: i.date().optional().indexed(),
public: i.boolean().indexed(),
}),
subscriptions: i.entity({
checkoutID: i.string(),
// quantity: i.number(),
// frequency: i.string(),
canceledAt: i.date(),
// next: i.date()
})
},
links: {
UserSubscriptions: {
forward: { on: "subscriptions", has: "one", label: "owner" },
reverse: { on: "$users", has: "many", label: "subscriptions" }
},
UserProfiles: {
forward: { on: "profiles", has: "one", label: "owner" },
reverse: { on: "$users", has: "one", label: "profile" }
},
TeamsOwned: {
forward: { on: "teams", has: "one", label: "owner" },
reverse: { on: "$users", has: "many", label: "teamsOwned" },
},
TeamsJoined: {
forward: { on: "teams", has: "many", label: "members" },
reverse: { on: "$users", has: "many", label: "teamsJoined" },
},
UserMachines: {
forward: { on: "machines", has: "one", label: "owner" },
reverse: { on: "$users", has: "many", label: "machines" }

View File

@@ -22,4 +22,24 @@ export namespace Email {
console.log("error sending email", error)
}
}
export async function sendWelcome(
to: string,
name: string,
) {
try {
await Client().sendTransactionalEmail(
{
transactionalId: "cm61jrbbx02twlstfwfcywt5u",
email: to,
dataVariables: {
name
}
}
);
} catch (error) {
console.log("error sending email", error)
}
}
}

View File

@@ -14,6 +14,25 @@ export module Examples {
updatedAt: '2025-01-09T01:56:23.902Z'
}
export const Subscription = {
id: "0bfcb712-df13-4454-81a8-fbee66eddca4",
checkoutID: "0bfcb712-df43-4454-81a8-fbee66eddca4",
// productID: "0bfcb712-df43-4454-81a8-fbee66eddca4",
// quantity: 1,
// frequency: "monthly" as const,
// next: '2025-01-09T01:56:23.902Z',
canceledAt: '2025-02-09T01:56:23.902Z'
}
export const Team = {
id: "0bfcb712-df13-4454-81a8-fbee66eddca4",
owner: true,
name: "Jane Doe's Games",
slug: "jane-does-games",
createdAt: '2025-01-04T11:56:23.902Z',
updatedAt: '2025-01-09T01:56:23.902Z'
}
export const Machine = {
id: "0bfcb712-df13-4454-81a8-fbee66eddca4",
hostname: "DESKTOP-EUO8VSF",

View File

@@ -5,6 +5,7 @@ import { Examples } from "../examples";
import databaseClient from "../database";
import { groupBy, map, pipe, values } from "remeda"
import { id as createID } from "@instantdb/admin";
import { useCurrentUser } from "../actor";
export module Profiles {
const MAX_ATTEMPTS = 50;
@@ -124,10 +125,6 @@ export module Profiles {
export const create = fn(z.object({ username: z.string(), customDiscriminator: z.string().optional(), avatarUrl: z.string().optional(), owner: z.string() }), async (input) => {
const username = sanitizeUsername(input.username);
// if (!username || username.length < 2 || username.length > 32) {
// // throw new Error('Invalid username length');
// }
const db = databaseClient()
const id = createID()
const now = new Date().toISOString()
@@ -150,6 +147,7 @@ export module Profiles {
}
}
}
const res = await db.query(query)
const profiles = res.profiles
if (profiles.length != 0) {
@@ -176,7 +174,6 @@ export module Profiles {
avatarUrl: input.avatarUrl,
createdAt: now,
updatedAt: now,
ownerID: input.owner,
discriminator,
}).link({ owner: input.owner })
)
@@ -214,7 +211,7 @@ export module Profiles {
profiles: {
$: {
where: {
ownerID
owner: ownerID
}
},
}
@@ -227,6 +224,27 @@ export module Profiles {
return null
}
return profiles
const profile = pipe(
profiles,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
username: group[0].username,
createdAt: group[0].createdAt,
updatedAt: group[0].updatedAt,
avatarUrl: group[0].avatarUrl,
discriminator: group[0].discriminator
}))
)
return profile[0]
}
export const getCurrentProfile = async () => {
const user = useCurrentUser()
const currentProfile = await getProfile(user.id);
return currentProfile
}
};

View File

@@ -0,0 +1,205 @@
import { z } from "zod";
import databaseClient from "../database"
import { fn } from "../utils";
import { groupBy, map, pipe, values } from "remeda"
import { Common } from "../common";
import { Examples } from "../examples";
import { useCurrentUser } from "../actor";
import { id as createID } from "@instantdb/admin";
import { Email } from "../email";
import { Profiles } from "../profile";
export const SubscriptionFrequency = z.enum([
"fixed",
"daily",
"weekly",
"monthly",
"yearly",
]);
export type SubscriptionFrequency = z.infer<typeof SubscriptionFrequency>;
export namespace Subscriptions {
export const Info = z
.object({
id: z.string().openapi({
description: Common.IdDescription,
example: Examples.Subscription.id,
}),
checkoutID: z.string().openapi({
description: "The polar.sh checkout id",
example: Examples.Subscription.checkoutID,
}),
// productID: z.string().openapi({
// description: "ID of the product being subscribed to.",
// example: Examples.Subscription.productID,
// }),
// quantity: z.number().int().openapi({
// description: "Quantity of the subscription.",
// example: Examples.Subscription.quantity,
// }),
// frequency: SubscriptionFrequency.openapi({
// description: "Frequency of the subscription.",
// example: Examples.Subscription.frequency,
// }),
// next: z.string().or(z.number()).openapi({
// description: "Next billing date for the subscription.",
// example: Examples.Subscription.next,
// }),
canceledAt: z.string().or(z.number()).optional().openapi({
description: "Cancelled date for the subscription.",
example: Examples.Subscription.canceledAt,
}),
})
.openapi({
ref: "Subscription",
description: "Subscription to a Nestri product.",
example: Examples.Subscription,
});
export type Info = z.infer<typeof Info>;
export const list = async () => {
const db = databaseClient()
const user = useCurrentUser()
const query = {
subscriptions: {
$: {
where: {
owner: user.id,
canceledAt: { $isNull: true }
}
},
}
}
const res = await db.query(query)
const response = res.subscriptions
if (!response || response.length === 0) {
return null
}
const result = pipe(
response,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
// next: group[0].next,
// frequency: group[0].frequency as any,
// quantity: group[0].quantity,
// productID: group[0].productID,
checkoutID: group[0].checkoutID,
}))
)
return result
}
export const create = fn(Info.omit({ id: true, canceledAt: true }), async (input) => {
// const id = createID()
const id = createID()
const db = databaseClient()
const user = useCurrentUser()
//Use the polar.sh ID
await db.transact(db.tx.subscriptions[id]!.update({
// next: input.next,
// frequency: input.frequency,
// quantity: input.quantity,
checkoutID: input.checkoutID,
}).link({ owner: user.id }))
const res = await db.auth.getUser({ id: user.id })
const profile = await Profiles.getProfile(user.id)
if (profile) {
await Email.sendWelcome(res.email, profile.username)
}
})
export const remove = fn(z.string(), async (id) => {
const db = databaseClient()
await db.transact(db.tx.subscriptions[id]!.update({
canceledAt: new Date().toString()
}))
})
export const fromID = fn(z.string(), async (id) => {
const db = databaseClient()
const user = useCurrentUser()
const query = {
subscriptions: {
$: {
where: {
id,
//Make sure they can only get subscriptions they own
owner: user.id,
canceledAt: { $isNull: true }
}
},
}
}
const res = await db.query(query)
const response = res.subscriptions
if (!response || response.length === 0) {
return null
}
const result = pipe(
response,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
checkoutID: group[0].checkoutID,
// next: group[0].next,
// frequency: group[0].frequency as any,
// quantity: group[0].quantity,
// productID: group[0].productID,
}))
)
return result[0]
})
export const fromCheckoutID = fn(z.string(), async (id) => {
const db = databaseClient()
const user = useCurrentUser()
const query = {
subscriptions: {
$: {
where: {
id,
//Make sure they can only get subscriptions they own
checkoutID: id,
canceledAt: { $isNull: true }
}
},
}
}
const res = await db.query(query)
const response = res.subscriptions
if (!response || response.length === 0) {
return null
}
const result = pipe(
response,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
checkoutID: group[0].checkoutID,
}))
)
return result[0]
})
}

View File

@@ -0,0 +1,165 @@
import { z } from "zod";
import databaseClient from "../database"
import { fn } from "../utils";
import { groupBy, map, pipe, values } from "remeda"
import { Common } from "../common";
import { Examples } from "../examples";
import { useCurrentUser } from "../actor";
import { id as createID } from "@instantdb/admin";
export namespace Teams {
export const Info = z
.object({
id: z.string().openapi({
description: Common.IdDescription,
example: Examples.Team.id,
}),
name: z.string().openapi({
description: "Name of the team",
example: Examples.Team.name,
}),
createdAt: z.string().or(z.number()).openapi({
description: "The time when this team was first created",
example: Examples.Team.createdAt,
}),
updatedAt: z.string().or(z.number()).openapi({
description: "The time when this team was last edited",
example: Examples.Team.updatedAt,
}),
owner: z.boolean().openapi({
description: "Whether this team is owned by this user",
example: Examples.Team.owner,
}),
slug: z.string().openapi({
description: "This is the unique name identifier for the team",
example: Examples.Team.slug
})
})
.openapi({
ref: "Team",
description: "A group of users sharing the same machines for gaming.",
example: Examples.Team,
});
export type Info = z.infer<typeof Info>;
export const list = async () => {
const db = databaseClient()
const user = useCurrentUser()
const query = {
teams: {
$: {
where: {
members: user.id,
deletedAt: { $isNull: true }
}
},
}
}
const res = await db.query(query)
const teams = res.teams
if (!teams || teams.length === 0) {
return null
}
const result = pipe(
teams,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
name: group[0].name,
createdAt: group[0].createdAt,
updatedAt: group[0].updatedAt,
slug: group[0].slug,
//@ts-expect-error
owner: group[0].owner === user.id
}))
)
return result
}
export const fromSlug = fn(z.string(), async (slug) => {
const db = databaseClient()
const query = {
teams: {
$: {
where: {
slug,
deletedAt: { $isNull: true }
}
},
}
}
const res = await db.query(query)
const teams = res.teams
if (!teams || teams.length === 0) {
return null
}
const result = pipe(
teams,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
name: group[0].name,
createdAt: group[0].createdAt,
slug: group[0].slug,
updatedAt: group[0].updatedAt,
//@ts-expect-error
owner: group[0].owner === user.id
}))
)
return result[0]
})
export const create = fn(Info.pick({ name: true, slug: true }), async (input) => {
const id = createID()
const db = databaseClient()
const user = useCurrentUser()
const now = new Date().toISOString()
await db.transact(db.tx.teams[id]!.update({
name: input.name,
slug: input.slug,
createdAt: now,
updatedAt: now,
}).link({ owner: user.id, members: user.id }))
return id
})
export const remove = fn(z.string(), async (id) => {
const db = databaseClient()
const now = new Date().toISOString()
await db.transact(db.tx.teams[id]!.update({
deletedAt: now
}))
return "ok"
})
export const invite = fn(z.object({email:z.string(), id: z.string()}), async (input) => {
//TODO:
// const db = databaseClient()
// const now = new Date().toISOString()
// await db.transact(db.tx.teams[id]!.update({
// deletedAt: now
// }))
return "ok"
})
}

View File

@@ -4,7 +4,7 @@ import { fn } from "../utils";
import { Common } from "../common";
import { Examples } from "../examples";
export module User {
export module Users {
export const Info = z
.object({
id: z.string().openapi({