feat: Add more API endpoints (#150)

This commit is contained in:
Wanjohi
2025-01-05 01:06:34 +03:00
committed by GitHub
parent dede878c3c
commit b47448255f
18 changed files with 1271 additions and 269 deletions

View File

@@ -49,6 +49,7 @@ export function useCurrentUser() {
id:actor.properties.userID,
token: actor.properties.accessToken
};
throw new VisibleError(
"auth",
"unauthorized",

View File

@@ -3,10 +3,10 @@ import { init } from "@instantdb/admin";
import schema from "../instant.schema";
const databaseClient = () => init({
appId: Resource.InstantAppId.value,
adminToken: Resource.InstantAdminToken.value,
schema
})
appId: Resource.InstantAppId.value,
adminToken: Resource.InstantAdminToken.value,
schema
})
export default databaseClient

View File

@@ -7,39 +7,23 @@ export module Examples {
export const Machine = {
id: "0bfcb712-df13-4454-81a8-fbee66eddca4",
hostname: "desktopeuo8vsf",
hostname: "DESKTOP-EUO8VSF",
fingerprint: "fc27f428f9ca47d4b41b70889ae0c62090",
location: "KE, AF"
createdAt: '2025-01-04T11:56:23.902Z',
deletedAt: '2025-01-09T01:56:23.902Z'
}
// export const Team = {
// id: createID(),
// name: "Jane's Family",
// type: "Family"
// }
// export const ProductVariant = {
// id: createID(),
// name: "FamilySM",
// price: 10,
// };
// export const Product = {
// id: createID(),
// name: "Family",
// description: "The ideal subscription tier for dedicated gamers who crave more flexibility and social gaming experiences.",
// variants: [ProductVariant],
// subscription: "allowed" as const,
// };
// export const Subscription = {
// id: createID(),
// productVariant: ProductVariant,
// quantity: 1,
// polarOrderID: createID(),
// frequency: "monthly" as const,
// next: new Date("2024-02-01 19:36:19.000").getTime(),
// owner: User
// };
export const Game = {
id: '0bfcb712-df13-4454-81a8-fbee66eddca4',
name: "Control Ultimate Edition",
steamID: 870780,
}
export const Session = {
id: "0bfcb712-df13-4454-81a8-fbee66eddca4",
public: true,
name: 'Late night chilling with the squad',
startedAt: '2025-01-04T11:56:23.902Z',
endedAt: '2025-01-04T11:56:23.902Z'
}
}

View File

@@ -0,0 +1,151 @@
import { z } from "zod"
import { fn } from "../utils";
import { Common } from "../common";
import { Examples } from "../examples";
import databaseClient from "../database"
import { id as createID } from "@instantdb/admin";
import { groupBy, map, pipe, values } from "remeda"
import { useCurrentDevice, useCurrentUser } from "../actor";
export module Games {
export const Info = z
.object({
id: z.string().openapi({
description: Common.IdDescription,
example: Examples.Game.id,
}),
name: z.string().openapi({
description: "A human-readable name for the game, used for easy identification.",
example: Examples.Game.name,
}),
steamID: z.number().openapi({
description: "The Steam ID of the game, used to identify it during installation and runtime.",
example: Examples.Game.steamID,
})
})
.openapi({
ref: "Game",
description: "Represents a Steam game that can be installed and played on a machine.",
example: Examples.Game,
});
export type Info = z.infer<typeof Info>;
export const create = fn(Info.pick({ name: true, steamID: true }), async (input) => {
const id = createID()
const db = databaseClient()
const device = useCurrentDevice()
await db.transact(
db.tx.games[id]!.update({
name: input.name,
steamID: input.steamID,
}).link({ machines: device.id })
)
return id
})
export const list = async () => {
const db = databaseClient()
const user = useCurrentUser()
const query = {
$users: {
$: { where: { id: user.id } },
games: {}
},
}
const res = await db.query(query)
const games = res.$users[0]?.games
if (games && games.length > 0) {
const result = pipe(
games,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
name: group[0].name,
steamID: group[0].steamID,
}))
)
return result
}
return null
}
export const fromSteamID = fn(z.number(), async (steamID) => {
const db = databaseClient()
const query = {
games: {
$: {
where: {
steamID,
}
}
}
}
const res = await db.query(query)
const games = res.games
if (games.length > 0) {
const result = pipe(
games,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
name: group[0].name,
steamID: group[0].steamID,
}))
)
return result[0]
}
return null
})
export const linkToCurrentUser = fn(z.string(), async (steamID) => {
const user = useCurrentUser()
const db = databaseClient()
await db.transact(db.tx.games[steamID]!.link({ owners: user.id }))
return "ok"
})
export const unLinkFromCurrentUser = fn(z.number(), async (steamID) => {
const user = useCurrentUser()
const db = databaseClient()
const query = {
$users: {
$: { where: { id: user.id } },
games: {
$: {
where: {
steamID,
}
}
}
},
}
const res = await db.query(query)
const games = res.$users[0]?.games
if (games && games.length > 0) {
const game = games[0] as Info
await db.transact(db.tx.games[game.id]!.unlink({ owners: user.id }))
return "ok"
}
return null
})
}

View File

@@ -2,11 +2,12 @@ import { z } from "zod"
import { fn } from "../utils";
import { Common } from "../common";
import { Examples } from "../examples";
import databaseClient from "../database"
import { useCurrentUser } from "../actor";
import databaseClient from "../database"
import { id as createID } from "@instantdb/admin";
export module Machine {
import { groupBy, map, pipe, values } from "remeda"
import { Games } from "../game"
export module Machines {
export const Info = z
.object({
id: z.string().openapi({
@@ -14,63 +15,45 @@ export module Machine {
example: Examples.Machine.id,
}),
hostname: z.string().openapi({
description: "Hostname of the machine",
description: "The Linux hostname that identifies this machine",
example: Examples.Machine.hostname,
}),
fingerprint: z.string().openapi({
description: "The machine's fingerprint, derived from the machine's Linux machine ID.",
description: "A unique identifier derived from the machine's Linux machine ID.",
example: Examples.Machine.fingerprint,
}),
location: z.string().openapi({
description: "The machine's approximate location; country and continent.",
example: Examples.Machine.location,
createdAt: z.string().or(z.number()).openapi({
description: "Represents a machine running on the Nestri network, containing its identifying information and metadata.",
example: Examples.Machine.createdAt,
})
})
.openapi({
ref: "Machine",
description: "A machine running on the Nestri network.",
description: "Represents a a physical or virtual machine connected to the Nestri network..",
example: Examples.Machine,
});
export const create = fn(z.object({
fingerprint: z.string(),
hostname: z.string(),
location: z.string()
}), async (input) => {
export type Info = z.infer<typeof Info>;
export const create = fn(Info.pick({ fingerprint: true, hostname: true }), async (input) => {
const id = createID()
const now = new Date().getTime()
const now = new Date().toISOString()
const db = databaseClient()
await db.transact(
db.tx.machines[id]!.update({
fingerprint: input.fingerprint,
hostname: input.hostname,
location: input.location,
createdAt: now,
//Just in case it had been previously deleted
deletedAt: undefined
})
)
return id
})
export const remove = fn(z.string(), async (id) => {
const now = new Date().getTime()
// const device = useCurrentDevice()
// const db = databaseClient()
// if (device.id) { // the machine can delete itself
// await db.transact(db.tx.machines[device.id]!.update({ deletedAt: now }))
// } else {// the user can delete it manually
const user = useCurrentUser()
const db = databaseClient().asUser({ token: user.token })
await db.transact(db.tx.machines[id]!.update({ deletedAt: now }))
// }
return "ok"
})
export const fromID = fn(z.string(), async (id) => {
const user = useCurrentUser()
const db = databaseClient().asUser({ token: user.token })
const db = databaseClient()
const query = {
machines: {
@@ -84,8 +67,53 @@ export module Machine {
}
const res = await db.query(query)
const machines = res.machines
return res.machines[0]
if (machines && machines.length > 0) {
const result = pipe(
machines,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
fingerprint: group[0].fingerprint,
hostname: group[0].hostname,
createdAt: group[0].createdAt
}))
)
return result
}
return null
})
export const installedGames = fn(z.string(), async (id) => {
const db = databaseClient()
const query = {
machines: {
$: {
where: {
id: id,
deletedAt: { $isNull: true }
}
},
games: {}
}
}
const res = await db.query(query)
const machines = res.machines
if (machines && machines.length > 0) {
const games = machines[0]?.games as any
if (games.length > 0) {
return games as Games.Info[]
}
return null
}
return null
})
export const fromFingerprint = fn(z.string(), async (input) => {
@@ -104,19 +132,38 @@ export module Machine {
const res = await db.query(query)
return res.machines[0]
const machines = res.machines
if (machines.length > 0) {
const result = pipe(
machines,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
fingerprint: group[0].fingerprint,
hostname: group[0].hostname,
createdAt: group[0].createdAt
}))
)
return result[0]
}
return null
})
export const list = async () => {
const user = useCurrentUser()
const db = databaseClient().asUser({ token: user.token })
const db = databaseClient()
const query = {
$users: {
$: { where: { id: user.id } },
machines: {
$: {
deletedAt: { $isNull: true }
where: {
deletedAt: { $isNull: true }
}
}
}
},
@@ -124,17 +171,62 @@ export module Machine {
const res = await db.query(query)
return res.$users[0]?.machines
const machines = res.$users[0]?.machines
if (machines && machines.length > 0) {
const result = pipe(
machines,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
fingerprint: group[0].fingerprint,
hostname: group[0].hostname,
createdAt: group[0].createdAt
}))
)
return result
}
return null
}
export const link = fn(z.object({
machineId: z.string()
}), async (input) => {
export const linkToCurrentUser = fn(z.string(), async (id) => {
const user = useCurrentUser()
const db = databaseClient()
await db.transact(db.tx.machines[input.machineId]!.link({ owner: user.id }))
await db.transact(db.tx.machines[id]!.link({ owner: user.id }))
return "ok"
})
export const unLinkFromCurrentUser = fn(z.string(), async (id) => {
const user = useCurrentUser()
const db = databaseClient()
const now = new Date().toISOString()
const query = {
$users: {
$: { where: { id: user.id } },
machines: {
$: {
where: {
id,
deletedAt: { $isNull: true }
}
}
}
},
}
const res = await db.query(query)
const machines = res.$users[0]?.machines
if (machines && machines.length > 0) {
const machine = machines[0] as Info
await db.transact(db.tx.machines[machine.id]!.update({ deletedAt: now }))
return "ok"
}
return null
})
}

View File

@@ -0,0 +1,292 @@
import { z } from "zod"
import { fn } from "../utils";
import { Machines } from "../machine";
import { Common } from "../common";
import { Examples } from "../examples";
import databaseClient from "../database"
import { useCurrentUser } from "../actor";
import { groupBy, map, pipe, values } from "remeda"
import { id as createID } from "@instantdb/admin";
export module Sessions {
export const Info = z
.object({
id: z.string().openapi({
description: Common.IdDescription,
example: Examples.Session.id,
}),
name: z.string().openapi({
description: "A human-readable name for the session to help identify it",
example: Examples.Session.name,
}),
public: z.boolean().openapi({
description: "If true, the session is publicly viewable by all users. If false, only authorized users can access it",
example: Examples.Session.public,
}),
endedAt: z.string().or(z.number()).or(z.undefined()).openapi({
description: "The timestamp indicating when this session was completed or terminated. Null if session is still active.",
example: Examples.Session.endedAt,
}),
startedAt: z.string().or(z.number()).openapi({
description: "The timestamp indicating when this session started.",
example: Examples.Session.startedAt,
})
})
.openapi({
ref: "Session",
description: "Represents a single game play session, tracking its lifetime and accessibility settings.",
example: Examples.Session,
});
export type Info = z.infer<typeof Info>;
export const create = fn(z.object({ name: z.string(), public: z.boolean(), fingerprint: z.string(), steamID: z.number() }), async (input) => {
const id = createID()
const now = new Date().toISOString()
const db = databaseClient()
const user = useCurrentUser()
const machine = await Machines.fromFingerprint(input.fingerprint)
if (!machine) {
return { error: "Such a machine does not exist" }
}
const games = await Machines.installedGames(machine.id)
if (!games) {
return { error: "The machine has no installed games" }
}
const result = pipe(
games,
groupBy(x => x.steamID === input.steamID ? "similar" : undefined),
)
if (!result.similar || result.similar.length == 0) {
return { error: "The machine does not have this game installed" }
}
await db.transact(
db.tx.sessions[id]!.update({
name: input.name,
public: input.public,
startedAt: now,
}).link({ owner: user.id, machine: machine.id, game: result.similar[0].id })
)
return { data: id }
})
export const list = async () => {
const user = useCurrentUser()
const db = databaseClient()
const query = {
$users: {
$: { where: { id: user.id } },
sessions: {}
},
}
const res = await db.query(query)
const sessions = res.$users[0]?.sessions
if (sessions && sessions.length > 0) {
const result = pipe(
sessions,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
endedAt: group[0].endedAt,
startedAt: group[0].startedAt,
public: group[0].public,
name: group[0].name
}))
)
return result
}
return null
}
export const getActive = async () => {
const user = useCurrentUser()
const db = databaseClient()
const query = {
$users: {
$: { where: { id: user.id } },
sessions: {
$: {
where: {
endedAt: { $isNull: true }
}
}
}
},
}
const res = await db.query(query)
const sessions = res.$users[0]?.sessions
if (sessions && sessions.length > 0) {
const result = pipe(
sessions,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
endedAt: group[0].endedAt,
startedAt: group[0].startedAt,
public: group[0].public,
name: group[0].name
}))
)
return result
}
return null
}
export const getPublicActive = async () => {
const db = databaseClient()
const query = {
sessions: {
$: {
where: {
endedAt: { $isNull: true },
public: true
}
}
}
}
const res = await db.query(query)
const sessions = res.sessions
if (sessions && sessions.length > 0) {
const result = pipe(
sessions,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
endedAt: group[0].endedAt,
startedAt: group[0].startedAt,
public: group[0].public,
name: group[0].name
}))
)
return result
}
return null
}
export const fromSteamID = fn(z.number(), async (steamID) => {
const db = databaseClient()
const query = {
games: {
$: {
where: {
steamID
}
},
sessions: {
$: {
where: {
endedAt: { $isNull: true },
public: true
}
}
}
}
}
const res = await db.query(query)
const sessions = res.games[0]?.sessions
if (sessions && sessions.length > 0) {
const result = pipe(
sessions,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
endedAt: group[0].endedAt,
startedAt: group[0].startedAt,
public: group[0].public,
name: group[0].name
}))
)
return result
}
return null
})
export const fromID = fn(z.string(), async (id) => {
const db = databaseClient()
useCurrentUser()
const query = {
sessions: {
$: {
where: {
id: id,
}
}
}
}
const res = await db.query(query)
const sessions = res.sessions
if (sessions && sessions.length > 0) {
const result = pipe(
sessions,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
endedAt: group[0].endedAt,
startedAt: group[0].startedAt,
public: group[0].public,
name: group[0].name
}))
)
return result
}
return null
})
export const end = fn(z.string(), async (id) => {
const user = useCurrentUser()
const db = databaseClient()
const now = new Date().toISOString()
const query = {
$users: {
$: { where: { id: user.id } },
sessions: {
$: {
where: {
id,
}
}
}
},
}
const res = await db.query(query)
const sessions = res.$users[0]?.sessions
if (sessions && sessions.length > 0) {
const session = sessions[0] as Info
await db.transact(db.tx.sessions[session.id]!.update({ endedAt: now }))
return "ok"
}
return null
})
}

View File

@@ -1,48 +0,0 @@
import databaseClient from "../database"
import { z } from "zod"
import { Common } from "../common";
import { createID, fn } from "../utils";
import { Examples } from "../examples";
export module Team {
export const Info = z
.object({
id: z.string().openapi({
description: Common.IdDescription,
example: Examples.Team.id,
}),
name: z.string().openapi({
description: "Name of the machine",
example: Examples.Team.name,
}),
type: z.string().nullable().openapi({
description: "Whether this is a personal or family type of team",
example: Examples.Team.type,
})
})
.openapi({
ref: "Team",
description: "A group of Nestri user's who share the same machine",
example: Examples.Team,
});
export const create = fn(z.object({
name: z.string(),
type: z.enum(["personal", "family"]),
owner: z.string(),
}), async (input) => {
const id = createID("machine")
const now = new Date().getTime()
const db = databaseClient()
await db.transact(db.tx.teams[id]!.update({
name: input.name,
type: input.type,
createdAt: now
}).link({
owner: input.owner,
}))
return id
})
}