🐜 fix: Make sure the db uses the same name (#199)

## Description
This fixes the issue where Cloudflare fails

## Related Issues
<!-- List any related issues (e.g., "Closes #123", "Fixes #456") -->

## Type of Change

- [x] Bug fix (non-breaking change)
- [ ] New feature (non-breaking change)
- [ ] Breaking change (fix or feature that changes existing
functionality)
- [ ] Documentation update
- [ ] Other (please describe):

## Checklist

- [x] I have updated relevant documentation
- [x] My code follows the project's coding style
- [x] My changes generate no new warnings/errors
This commit is contained in:
Wanjohi
2025-03-02 00:01:25 +03:00
committed by GitHub
parent b18b08b822
commit 178c612f0f
34 changed files with 5 additions and 2835 deletions

View File

@@ -1,86 +0,0 @@
import { createContext } from "../src/context";
import { VisibleError } from "./error";
export interface UserActor {
type: "user";
properties: {
accessToken: string;
userID: string;
auth?:
| {
type: "personal";
token: string;
}
| {
type: "oauth";
clientID: string;
};
};
}
export interface DeviceActor {
type: "device";
properties: {
teamSlug: string;
hostname: string;
auth?:
| {
type: "personal";
token: string;
}
| {
type: "oauth";
clientID: string;
};
};
}
export interface PublicActor {
type: "public";
properties: {};
}
type Actor = UserActor | PublicActor | DeviceActor;
export const ActorContext = createContext<Actor>();
export function useCurrentUser() {
const actor = ActorContext.use();
if (actor.type === "user") return {
id:actor.properties.userID,
token: actor.properties.accessToken,
};
throw new VisibleError(
"auth",
"unauthorized",
`You don't have permission to access this resource`,
);
}
export function useCurrentDevice() {
const actor = ActorContext.use();
if (actor.type === "device") return {
hostname:actor.properties.hostname,
teamSlug: actor.properties.teamSlug
};
throw new VisibleError(
"auth",
"unauthorized",
`You don't have permission to access this resource`,
);
}
export function useActor() {
try {
return ActorContext.use();
} catch {
return { type: "public", properties: {} } as PublicActor;
}
}
export function assertActor<T extends Actor["type"]>(type: T) {
const actor = useActor();
if (actor.type !== type)
throw new VisibleError("auth", "actor.invalid", `Actor is not "${type}"`);
return actor as Extract<Actor, { type: T }>;
}

View File

@@ -1,90 +0,0 @@
import { z } from "zod"
import { Resource } from "sst";
import { doubleFn, fn } from "../utils";
import { AwsClient } from "aws4fetch";
import { DescribeTasksCommandOutput, StopTaskCommandOutput, type RunTaskCommandOutput } from "@aws-sdk/client-ecs";
export module Aws {
export const client = async () => {
return new AwsClient({
accessKeyId: Resource.AwsAccessKey.value,
secretAccessKey: Resource.AwsSecretKey.value,
region: "us-east-1",
});
}
export const EcsRunTask = fn(z.object({
cluster: z.string(),
count: z.number(),
taskDefinition: z.string(),
launchType: z.enum(["EC2", "FARGATE"]),
overrides: z.object({
containerOverrides: z.object({
name: z.string(),
environment: z.object({
name: z.string(),
value: z.string().or(z.number())
}).array()
}).array()
})
}), async (body) => {
const c = await client();
const url = new URL(`https://ecs.${c.region}.amazonaws.com/`)
const res = await c.fetch(url, {
method: "POST",
headers: {
"X-Amz-Target": "AmazonEC2ContainerServiceV20141113.RunTask",
"Content-Type": "application/x-amz-json-1.1",
},
body: JSON.stringify(body)
})
return await res.json() as RunTaskCommandOutput
})
export const EcsDescribeTasks = fn(z.object({ tasks: z.string().array(), cluster: z.string() }), async (body) => {
const c = await client();
const url = new URL(`https://ecs.${c.region}.amazonaws.com/`)
const res = await c.fetch(url, {
method: "POST",
headers: {
"X-Amz-Target": "AmazonEC2ContainerServiceV20141113.DescribeTasks",
"Content-Type": "application/x-amz-json-1.1",
},
body: JSON.stringify(body)
})
return await res.json() as DescribeTasksCommandOutput
})
export const EcsStopTask = fn(z.object({
cluster: z.string().optional(),
reason: z.string().optional(),
task: z.string()
}), async (body) => {
const c = await client();
const url = new URL(`https://ecs.${c.region}.amazonaws.com/`)
const res = await c.fetch(url, {
method: "POST",
headers: {
"X-Amz-Target": "AmazonEC2ContainerServiceV20141113.StopTask",
"Content-Type": "application/x-amz-json-1.1",
},
body: JSON.stringify(body)
})
return await res.json() as StopTaskCommandOutput
})
}

View File

@@ -1,7 +0,0 @@
import { z } from "zod";
import "zod-openapi/extend";
export module Common {
export const IdDescription = `Unique object identifier.
The format and length of IDs may change over time.`;
}

View File

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

View File

@@ -1,45 +0,0 @@
import { LoopsClient } from "loops";
import { Resource } from "sst/resource"
export namespace Email {
export const Client = () => new LoopsClient(Resource.LoopsApiKey.value);
export async function send(
to: string,
body: string,
) {
try {
await Client().sendTransactionalEmail(
{
transactionalId: "cm58pdf8d03upb5ecirnmvrfb",
email: to,
dataVariables: {
logincode: body
}
}
);
} catch (error) {
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

@@ -1,9 +0,0 @@
export class VisibleError extends Error {
constructor(
public kind: "input" | "auth",
public code: string,
public message: string,
) {
super(message);
}
}

View File

@@ -1,75 +0,0 @@
export module Examples {
export const User = {
id: "0bfcc712-df13-4454-81a8-fbee66eddca4",
email: "john@example.com",
};
export const Task = {
id: "0bfcc712-df13-4454-81a8-fbee66eddca4",
taskID: "b8302fca2d224d91ab342a2e4ab926d3",
type: "AWS" as const, //or "on-premises",
lastStatus: "RUNNING" as const,
healthStatus: "UNKNOWN" as const,
startedAt: '2025-01-09T01:56:23.902Z',
lastUpdated: '2025-01-09T01:56:23.902Z',
stoppedAt: '2025-01-09T04:46:23.902Z'
}
export const Profile = {
id: "0bfcb712-df13-4454-81a8-fbee66eddca4",
username: "janedoe47",
status: "active" as const,
avatarUrl: "https://cdn.discordapp.com/avatars/xxxxxxx/xxxxxxx.png",
discriminator: 12, //it needs to be two digits
createdAt: '2025-01-04T11:56:23.902Z',
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",
fingerprint: "fc27f428f9ca47d4b41b70889ae0c62090",
createdAt: '2025-01-04T11:56:23.902Z',
deletedAt: '2025-01-09T01:56:23.902Z'
}
export const Instance = {
id: "0bfcb712-df13-4454-81a8-fbee66eddca4",
hostname: "a955e059f05d",
createdAt: '2025-01-04T11:56:23.902Z',
lastActive: '2025-01-09T01:56:23.902Z'
}
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,
startedAt: '2025-01-04T11:56:23.902Z',
endedAt: '2025-01-04T12:36:23.902Z'
}
}

View File

@@ -1,151 +0,0 @@
// 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

@@ -1,83 +0,0 @@
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"
export module Instances {
export const Info = z
.object({
id: z.string().openapi({
description: Common.IdDescription,
example: Examples.Instance.id,
}),
hostname: z.string().openapi({
description: "The container's hostname",
example: Examples.Instance.hostname,
}),
createdAt: z.string().or(z.number()).openapi({
description: "The time this instances was registered on the network",
example: Examples.Instance.createdAt,
}),
lastActive: z.string().or(z.number()).optional().openapi({
description: "The time this instance was last seen on the network",
example: Examples.Instance.lastActive,
})
})
.openapi({
ref: "Instance",
description: "Represents a running container that is connected to the Nestri network..",
example: Examples.Instance,
});
export type Info = z.infer<typeof Info>;
export const create = fn(z.object({ hostname: z.string(), teamID: z.string() }), async (input) => {
const id = createID()
const now = new Date().toISOString()
const db = databaseClient()
await db.transact(
db.tx.instances[id]!.update({
hostname: input.hostname,
createdAt: now,
}).link({ owners: input.teamID })
)
return "ok"
})
export const fromTeamID = fn(z.string(), async (teamID) => {
const db = databaseClient()
const query = {
instances: {
$: {
where: {
owners: teamID
}
}
}
}
const res = await db.query(query)
const data = res.instances
if (data && data.length > 0) {
const result = pipe(
data,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
lastActive: group[0].lastActive,
hostname: group[0].hostname,
createdAt: group[0].createdAt
}))
)
return result
}
return null
})
}

View File

@@ -1,232 +0,0 @@
// import { z } from "zod"
// import { fn } from "../utils";
// import { Games } from "../game"
// import { Common } from "../common";
// import { Examples } from "../examples";
// import { useCurrentUser } from "../actor";
// import databaseClient from "../database"
// import { id as createID } from "@instantdb/admin";
// import { groupBy, map, pipe, values } from "remeda"
// export module Machines {
// export const Info = z
// .object({
// id: z.string().openapi({
// description: Common.IdDescription,
// example: Examples.Machine.id,
// }),
// hostname: z.string().openapi({
// description: "The Linux hostname that identifies this machine",
// example: Examples.Machine.hostname,
// }),
// fingerprint: z.string().openapi({
// description: "A unique identifier derived from the machine's Linux machine ID.",
// example: Examples.Machine.fingerprint,
// }),
// 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: "Represents a physical or virtual machine connected to the Nestri network..",
// example: Examples.Machine,
// });
// 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().toISOString()
// const db = databaseClient()
// await db.transact(
// db.tx.machines[id]!.update({
// fingerprint: input.fingerprint,
// hostname: input.hostname,
// createdAt: now,
// //Just in case it had been previously deleted
// deletedAt: undefined
// })
// )
// return id
// })
// // export const fromID = fn(z.string(), async (id) => {
// const db = databaseClient()
// const query = {
// machines: {
// $: {
// where: {
// id: id,
// deletedAt: { $isNull: true }
// }
// }
// }
// }
// const res = await db.query(query)
// const machines = res.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 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) => {
// const db = databaseClient()
// const query = {
// machines: {
// $: {
// where: {
// fingerprint: input,
// deletedAt: { $isNull: true }
// }
// }
// }
// }
// const res = await db.query(query)
// 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()
// const query = {
// $users: {
// $: { where: { id: user.id } },
// machines: {
// $: {
// where: {
// deletedAt: { $isNull: true }
// }
// }
// }
// },
// }
// const res = await db.query(query)
// 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 linkToCurrentUser = fn(z.string(), async (id) => {
// const user = useCurrentUser()
// const db = databaseClient()
// 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

@@ -1,412 +0,0 @@
import { z } from "zod"
import { fn } from "../utils";
import { Common } from "../common";
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 const userStatus = z.enum([
"active", //online and playing a game
"idle", //online and not playing
"offline",
]);
export module Profiles {
const MAX_ATTEMPTS = 50;
export const Info = z
.object({
id: z.string().openapi({
description: Common.IdDescription,
example: Examples.Machine.id,
}),
username: z.string().openapi({
description: "The user's unique username",
example: Examples.Profile.username,
}),
avatarUrl: z.string().or(z.undefined()).openapi({
description: "The url to the profile picture.",
example: Examples.Profile.username,
}),
status: userStatus.openapi({
description: "Whether the user is active, idle or offline",
example: Examples.Profile.status
}),
discriminator: z.string().or(z.number()).openapi({
description: "The number discriminator for each username",
example: Examples.Profile.discriminator,
}),
createdAt: z.string().or(z.number()).openapi({
description: "The time when this profile was first created",
example: Examples.Profile.createdAt,
}),
updatedAt: z.string().or(z.number()).openapi({
description: "The time when this profile was last edited",
example: Examples.Profile.updatedAt,
})
})
.openapi({
ref: "Profile",
description: "Represents a profile of a user on Nestri",
example: Examples.Profile,
});
export type Info = z.infer<typeof Info>;
export type userStatus = z.infer<typeof userStatus>;
export const sanitizeUsername = (username: string): string => {
// Remove spaces and numbers
return username.replace(/[\s0-9]/g, '');
};
export const generateDiscriminator = (): string => {
return Math.floor(Math.random() * 100).toString().padStart(2, '0');
};
export const isValidDiscriminator = (discriminator: string): boolean => {
return /^\d{2}$/.test(discriminator);
};
export const fromUsername = fn(z.string(), async (input) => {
const sanitizedUsername = sanitizeUsername(input);
const db = databaseClient()
const query = {
profiles: {
$: {
where: {
username: sanitizedUsername,
}
}
}
}
const res = await db.query(query)
const profiles = res.profiles
if (!profiles || profiles.length == 0) {
return null
}
return pipe(
profiles,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
username: group[0].username,
createdAt: group[0].createdAt,
discriminator: group[0].discriminator,
updatedAt: group[0].updatedAt,
status: group[0].status as userStatus
}))
)
})
export const findAvailableDiscriminator = fn(z.string(), async (input) => {
const db = databaseClient()
const username = sanitizeUsername(input);
for (let i = 0; i < MAX_ATTEMPTS; i++) {
const discriminator = generateDiscriminator();
const query = {
profiles: {
$: {
where: {
username,
discriminator
}
}
}
}
const res = await db.query(query)
const profiles = res.profiles
if (profiles.length === 0) {
return discriminator;
}
}
return null; // No available discriminators
})
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);
const db = databaseClient()
const id = createID()
const now = new Date().toISOString()
let discriminator: string | null;
if (input.customDiscriminator) {
if (!isValidDiscriminator(input.customDiscriminator)) {
console.error('Invalid discriminator format')
return null
// throw new Error('Invalid discriminator format');
}
const query = {
profiles: {
$: {
where: {
username,
discriminator: input.customDiscriminator
}
}
}
}
const res = await db.query(query)
const profiles = res.profiles
if (profiles.length != 0) {
console.error("Username and discriminator combination already taken ")
return null
// throw new Error('Username and discriminator combination already taken');
}
discriminator = input.customDiscriminator
} else {
// Generate a random available discriminator
discriminator = await findAvailableDiscriminator(username);
if (!discriminator) {
console.error("No available discriminators for this username ")
return null
// throw new Error('No available discriminators for this username');
}
}
return await db.transact(
db.tx.profiles[id]!.update({
username,
avatarUrl: input.avatarUrl,
createdAt: now,
updatedAt: now,
discriminator,
status: "idle"
}).link({ owner: input.owner })
)
})
export const getFullUsername = async (username: string) => {
const db = databaseClient()
const query = {
profiles: {
$: {
where: {
username,
}
}
}
}
const res = await db.query(query)
const profiles = res.profiles
if (!profiles || profiles.length === 0) {
console.error('User not found')
return null
// throw new Error('User not found');
}
return `${profiles[0]?.username}#${profiles[0]?.discriminator}`;
}
export const fromOwnerID = async (ownerID: string) => {
try {
const db = databaseClient()
const query = {
profiles: {
$: {
where: {
owner: ownerID
}
},
}
}
const res = await db.query(query)
const profiles = res.profiles
if (!profiles || profiles.length === 0) {
throw new Error("No profiles were found");
}
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,
status: group[0].status as userStatus
}))
)
return profile[0]
} catch (error) {
console.log("user fromOwnerID", error)
return null
}
}
export const fromID = async (id: string) => {
try {
const db = databaseClient()
const query = {
profiles: {
$: {
where: {
id
}
},
}
}
const res = await db.query(query)
const profiles = res.profiles
if (!profiles || profiles.length === 0) {
throw new Error("No profiles were found");
}
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,
status: group[0].status as userStatus
}))
)
return profile[0]
} catch (error) {
console.log("user fromID", error)
return null
}
}
export const fromIDToOwner = async (id: string) => {
try {
const db = databaseClient()
const query = {
profiles: {
$: {
where: {
id
}
},
}
}
const res = await db.query(query)
const profiles = res.profiles as any
if (!profiles || profiles.length === 0) {
throw new Error("No profiles were found");
}
return profiles[0]!.owner as string
} catch (error) {
console.log("user fromID", error)
return null
}
}
export const getCurrentProfile = async () => {
const user = useCurrentUser()
const currentProfile = await fromOwnerID(user.id);
return currentProfile
}
export const setStatus = fn(userStatus, async (status) => {
try {
const user = useCurrentUser()
const db = databaseClient()
const now = new Date().toISOString()
await db.transact(
db.tx.profiles[user.id]!.update({
status,
updatedAt: now
})
)
} catch (error) {
console.log("user setStatus error", error)
return null
}
})
export const list = async () => {
try {
const db = databaseClient()
// const ago = new Date(Date.now() - (60 * 1000 * 30)).toISOString()
const ago = new Date(Date.now() - (24 * 60 * 60 * 1000)).toISOString()
const query = {
profiles: {
$: {
limit: 10,
where: {
updatedAt: { $gt: ago },
},
order: {
updatedAt: "desc" as const,
},
}
}
}
const res = await db.query(query)
const profiles = res.profiles
if (!profiles || profiles.length === 0) {
throw new Error("No profiles were found");
}
const result = 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,
status: group[0].status as userStatus
}))
)
return result
} catch (error) {
console.log("user list error", error)
return null
}
}
}

View File

@@ -1,251 +0,0 @@
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 { 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,
}),
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({ public: z.boolean() }), async (input) => {
try {
const id = createID()
const db = databaseClient()
const user = useCurrentUser()
const now = new Date().toISOString()
await db.transact(
db.tx.sessions[id]!.update({
public: input.public,
startedAt: now,
}).link({ owner: user.id })
)
return id
} catch (err) {
return null
}
})
export const getActive = async () => {
try {
const db = databaseClient()
const query = {
sessions: {
$: {
where: {
endedAt: { $isNull: true }
}
}
}
}
const res = await db.query(query)
const sessions = res.sessions
if (!sessions || sessions.length === 0) {
throw new Error("No active sessions found")
}
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,
}))
)
return result
} catch (error) {
return null
}
}
export const fromID = fn(z.string(), async (id) => {
try {
const db = databaseClient()
const query = {
sessions: {
$: {
where: {
id: id,
}
}
}
}
const res = await db.query(query)
const sessions = res.sessions
if (!sessions || sessions.length === 0) {
throw new Error("No sessions were found");
}
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,
}))
)
return result
} catch (err) {
console.log("sessions error", err)
return null
}
})
export const fromTaskID = fn(z.string(), async (taskID) => {
try {
const db = databaseClient()
const query = {
sessions: {
$: {
where: {
task: taskID,
endedAt: { $isNull: true }
}
}
}
}
const res = await db.query(query)
const sessions = res.sessions
if (!sessions || sessions.length === 0) {
throw new Error("No sessions were found");
}
console.log("sessions", sessions)
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,
}))
)
return result[0]
} catch (err) {
console.log("sessions error", err)
return null
}
})
export const end = fn(z.string(), async (id) => {
const user = useCurrentUser()
try {
const db = databaseClient()
const now = new Date().toISOString()
const query = {
sessions: {
$: {
where: {
owner: user.id,
id,
}
}
},
}
const res = await db.query(query)
const sessions = res.sessions
if (!sessions || sessions.length === 0) {
throw new Error("No sessions were found");
}
await db.transact(db.tx.sessions[sessions[0]!.id]!.update({ endedAt: now }))
return "ok"
} catch (error) {
return null
}
})
export const fromOwnerID = fn(z.string(), async (id) => {
try {
const db = databaseClient()
const query = {
sessions: {
$: {
where: {
owner: id,
endedAt: { $isNull: true }
}
}
}
}
const res = await db.query(query)
const sessions = res.sessions
if (!sessions || sessions.length === 0) {
throw new Error("No sessions were found");
}
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,
}))
)
return result[0]
} catch (err) {
console.log("session owner error", err)
return null
}
})
}

View File

@@ -1,205 +0,0 @@
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 = fn(z.string().optional(), async (userID) => {
const db = databaseClient()
const user = userID ? userID : useCurrentUser().id
const query = {
subscriptions: {
$: {
where: {
owner: user,
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.fromOwnerID(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().toISOString()
}))
})
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

@@ -1,331 +0,0 @@
import { z } from "zod";
import { fn } from "../utils";
import { Resource } from "sst";
import { Aws } from "../aws/client";
import { Common } from "../common";
import { Examples } from "../examples";
import databaseClient from "../database"
import { useCurrentUser } from "../actor";
import { id as createID } from "@instantdb/admin";
import { groupBy, map, pipe, values } from "remeda"
import { Sessions } from "../session";
export const lastStatus = z.enum([
"RUNNING",
"PENDING",
"UNKNOWN",
"STOPPED",
]);
export const taskType = z.enum([
"AWS",
"ON_PREMISES",
"UNKNOWN"
]);
export const healthStatus = z.enum([
"HEALTHY",
"UNHEALTHY",
"UNKNOWN",
]);
export type taskType = z.infer<typeof taskType>;
export type lastStatus = z.infer<typeof lastStatus>;
export type healthStatus = z.infer<typeof healthStatus>;
export module Tasks {
export const Info = z
.object({
id: z.string().openapi({
description: Common.IdDescription,
example: Examples.Task.id,
}),
type: taskType.openapi({
description: "Where this task is hosted on",
example: Examples.Task.type,
}),
taskID: z.string().openapi({
description: "The id of this task as seen on AWS",
example: Examples.Task.taskID,
}),
startedAt: z.string().or(z.number()).openapi({
description: "The time this task was started",
example: Examples.Task.startedAt,
}),
lastUpdated: z.string().or(z.number()).openapi({
description: "The time the information about this task was last updated",
example: Examples.Task.lastUpdated,
}),
stoppedAt: z.string().or(z.number()).optional().openapi({
description: "The time this task was stopped or quit",
example: Examples.Task.lastUpdated,
}),
lastStatus: lastStatus.openapi({
description: "The last registered status of this task",
example: Examples.Task.lastStatus,
}),
healthStatus: healthStatus.openapi({
description: "The health status of this task",
example: Examples.Task.healthStatus,
})
})
.openapi({
ref: "Subscription",
description: "Subscription to a Nestri product.",
example: Examples.Task,
});
export type Info = z.infer<typeof Info>;
export const list = async () => {
const db = databaseClient()
const user = useCurrentUser()
try {
const query = {
tasks: {
$: {
where: {
stoppedAt: { $isNull: true },
owner: user.id
}
},
}
}
const data = await db.query(query)
const response = data.tasks
if (!response || response.length === 0) {
throw new Error("No task for this user were found");
}
const result = pipe(
response,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
taskID: group[0].taskID,
type: group[0].type as taskType,
lastStatus: group[0].lastStatus as lastStatus,
healthStatus: group[0].healthStatus as healthStatus,
startedAt: group[0].startedAt,
stoppedAt: group[0].stoppedAt,
lastUpdated: group[0].lastUpdated,
}))
)
return result
} catch (e) {
return null
}
}
export const create = async () => {
const user = useCurrentUser()
try {
//TODO: Use a simpler way to set the session ID
// const sessionID = createID()
const sessionID = await Sessions.create({ public: true })
if (!sessionID) throw new Error("No session id was given");
const run = await Aws.EcsRunTask({
count: 1,
cluster: Resource.NestriGPUCluster.value,
taskDefinition: Resource.NestriGPUTask.value,
launchType: "EC2",
overrides: {
containerOverrides: [
{
name: "nestri",
environment: [
{
name: "NESTRI_ROOM",
value: sessionID
}
]
}
]
}
})
if (!run.tasks || run.tasks.length === 0) {
throw new Error(`No tasks were started`);
}
// Extract task details
const task = run.tasks[0];
const taskArn = task?.taskArn!;
const taskId = taskArn.split('/').pop()!; // Extract task ID from ARN
const taskStatus = task?.lastStatus;
const taskHealthStatus = task?.healthStatus;
const startedAt = task?.startedAt!;
const id = createID()
const db = databaseClient()
const now = new Date().toISOString()
await db.transact(db.tx.tasks[id]!.update({
taskID: taskId,
type: "AWS",
healthStatus: taskHealthStatus ? taskHealthStatus.toString() : "UNKNOWN",
startedAt: startedAt ? startedAt.toISOString() : now,
lastStatus: taskStatus,
lastUpdated: now,
}).link({ owner: user.id, sessions: sessionID }))
return id
} catch (e) {
console.error("error", e)
return null
}
}
export const fromID = fn(z.string(), async (taskID) => {
const db = databaseClient()
try {
const query = {
tasks: {
$: {
where: {
id: taskID,
stoppedAt: { $isNull: true }
}
},
}
}
const data = await db.query(query)
const response = data.tasks
if (!response || response.length === 0) {
throw new Error("No task with the given id was found");
}
const result = pipe(
response,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
taskID: group[0].taskID,
type: group[0].type as taskType,
lastStatus: group[0].lastStatus as lastStatus,
healthStatus: group[0].healthStatus as healthStatus,
startedAt: group[0].startedAt,
stoppedAt: group[0].stoppedAt,
lastUpdated: group[0].lastUpdated,
}))
)
return result[0]
} catch (error) {
return null
}
})
export const update = fn(z.string(), async (taskID) => {
try {
const db = databaseClient()
const query = {
tasks: {
$: {
where: {
id: taskID,
stoppedAt: { $isNull: true }
}
},
}
}
const data = await db.query(query)
const response = data.tasks
if (!response || response.length === 0) {
throw new Error("No task with the given taskID was found");
}
const now = new Date().toISOString()
const describeResponse = await Aws.EcsDescribeTasks({
tasks: [response[0]!.taskID],
cluster: Resource.NestriGPUCluster.value
})
if (!describeResponse.tasks || describeResponse.tasks.length === 0) {
throw new Error("No tasks were found");
}
const task = describeResponse.tasks[0]!
const updatedDb = {
healthStatus: task.healthStatus ? task.healthStatus : "UNKNOWN",
lastStatus: task.lastStatus ? task.lastStatus : "UNKNOWN",
lastUpdated: now,
}
await db.transact(db.tx.tasks[response[0]!.id]!.update({
...updatedDb
}))
const updatedRes = [{ ...response[0]!, ...updatedDb }]
const result = pipe(
updatedRes,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
taskID: group[0].taskID,
type: group[0].type as taskType,
lastStatus: group[0].lastStatus as lastStatus,
healthStatus: group[0].healthStatus as healthStatus,
startedAt: group[0].startedAt,
stoppedAt: group[0].stoppedAt,
lastUpdated: group[0].lastUpdated,
}))
)
return result
} catch (error) {
console.error("update error", error)
return null
}
})
export const stop = fn(z.object({ taskID: z.string(), id: z.string() }), async (input) => {
const db = databaseClient()
const now = new Date().toISOString()
try {
//TODO:Check whether they own this task first
const stopResponse = await Aws.EcsStopTask({
task: input.taskID,
cluster: Resource.NestriGPUCluster.value,
reason: "Client requested a shutdown"
})
if (!stopResponse.task) {
throw new Error(`No task was stopped`);
}
await db.transact(db.tx.tasks[input.id]!.update({
stoppedAt: now,
lastUpdated: now,
lastStatus: "STOPPED",
healthStatus: "UNKNOWN"
}))
return "ok"
} catch (error) {
console.error("stop error", error)
return null
}
})
}

View File

@@ -1,164 +0,0 @@
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,
slug: group[0].slug,
createdAt: group[0].createdAt,
updatedAt: group[0].updatedAt,
// 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

@@ -1,17 +0,0 @@
export interface CloudflareCF {
colo: string;
continent: string;
country: string,
city: string;
region: string;
longitude: number;
latitude: number;
metroCode: string;
postalCode: string;
timezone: string;
regionCode: number;
}
export interface CFRequest extends Request {
cf: CloudflareCF
}

View File

@@ -1,37 +0,0 @@
import { z } from "zod";
import databaseClient from "../database"
import { fn } from "../utils";
import { Common } from "../common";
import { Examples } from "../examples";
export module Users {
export const Info = z
.object({
id: z.string().openapi({
description: Common.IdDescription,
example: Examples.User.id,
}),
email: z.string().nullable().openapi({
description: "Email address of the user.",
example: Examples.User.email,
}),
})
.openapi({
ref: "User",
description: "A Nestri console user.",
example: Examples.User,
});
export const fromEmail = fn(z.string(), async (email) => {
const db = databaseClient()
const res = await db.auth.getUser({ email })
return res
})
export const create = fn(z.string(), async (email) => {
const db = databaseClient()
const token = await db.auth.createToken(email)
return token
})
}

View File

@@ -1,27 +0,0 @@
import { ZodSchema, z } from "zod";
export function fn<
Arg1 extends ZodSchema,
Callback extends (arg1: z.output<Arg1>) => any,
>(arg1: Arg1, cb: Callback) {
const result = function (input: z.input<typeof arg1>): ReturnType<Callback> {
const parsed = arg1.parse(input);
return cb.apply(cb, [parsed as any]);
};
result.schema = arg1;
return result;
}
export function doubleFn<
Arg1 extends ZodSchema,
Arg2 extends ZodSchema,
Callback extends (arg1: z.output<Arg1>, arg2: z.output<Arg2>) => any,
>(arg1: Arg1, arg2: Arg2, cb: Callback) {
const result = function (input: z.input<typeof arg1>, input2: z.input<typeof arg2>): ReturnType<Callback> {
const parsed = arg1.parse(input);
const parsed2 = arg2.parse(input2);
return cb.apply(cb, [parsed as any, parsed2 as any]);
};
result.schema = arg1;
return result;
}

View File

@@ -1,9 +0,0 @@
import { ulid } from "ulid";
export const prefixes = {
user: "usr",
} as const;
export function createID(prefix: keyof typeof prefixes): string {
return [prefixes[prefix], ulid()].join("_");
}

View File

@@ -1,2 +0,0 @@
export * from "./fn"
export * from "./id"