feat: Add fresh fucking entities

This commit is contained in:
Wanjohi
2025-10-26 04:39:36 +03:00
parent 6f6616ce6c
commit 16a1c8c145
14 changed files with 245 additions and 166 deletions

View File

@@ -6,6 +6,8 @@ export namespace Actor {
export interface Member {
type: "member";
properties: {
email: string;
teamID: string;
memberID: string;
};
}
@@ -22,7 +24,6 @@ export namespace Actor {
type: "system";
properties: {
teamID: string;
memberID: string;
};
}

View File

@@ -1,6 +1,6 @@
import { char, timestamp as rawTs } from "drizzle-orm/pg-core";
export const ulid = (name: string) => char(name, { length: 26 + 5 });
export const ulid = (name: string) => char(name, { length: 26 + 4 });
export const id = {
get id() {

View File

@@ -4,17 +4,37 @@ export namespace Examples {
export const Id = (prefix: keyof typeof prefixes) =>
`${prefixes[prefix]}_XXXXXXXXXXXXXXXXXXXXXXXXX`;
export const Team = {
id: Id("team"),
export const User = {
id: Id("user"),
name: "Jane Doe",
email: "email@example.com",
polarCustomerID: "0bfcb712-df13-4454-81a8-fbee66eddca4",
customerID: "0bfcb712-df13-4454-81a8-fbee66eddca4",
};
export const Member = {
id: Id("member"),
name: "John Doe",
role: "adult" as const, // Role on the team, adult or child
teamID: Team.id, // FK to Teams
export const Organisation = {
id: Id("organisation"),
name: "John",
slug: "johns_org", // Team slug (not null, unique)
ownerID: User.id,
};
export const SteamAccount = {
id: "74839300282033", // Steam ID
ownerID: User.id,
name: "JD The 65th",
username: "jdoe",
realName: "John Doe",
memberSince: new Date("2010-01-26T21:00:00.000Z"),
avatarHash: "3a5e805fd4c1e04e26a97af0b9c6fab2dee91a19",
limitations: {
isLimited: false,
tradeBanState: "none" as const,
isVacBanned: false,
visibilityState: 3,
privacyState: "public" as const,
},
profileUrl: "The65thJD", //"https://steamcommunity.com/id/XXXXXXXXXXXXXXXX/",
lastSyncedAt: new Date("2025-04-26T20:11:08.155Z"),
};
export const Token = {
@@ -24,7 +44,7 @@ export namespace Examples {
};
export const App = {
id: Id("apiApp"),
id: Id("app"),
secret: "sec_******XXXX",
name: "Example App",
redirectURI: "https://web.example.com/v1/callback",

View File

@@ -1,32 +0,0 @@
import { z } from "zod/v4";
import { Examples } from "../examples.js";
import { MemberRole } from "./member.sql.js";
export namespace Member {
export const Info = z
.object({
id: z.string().meta({
description: "The unique identifier of the member",
example: Examples.Member.id,
}),
name: z.string().meta({
description: "The name of the member",
example: Examples.Member.name,
}),
teamID: z.string().meta({
description: "The unique identifier of the team this member belongs to",
example: Examples.Member.teamID,
}),
role: z.enum(MemberRole.enumValues).meta({
description: "The role of the member within the team",
example: Examples.Member.role,
}),
})
.meta({
ref: "Member",
description: "Represents a member of a team",
example: Examples.Member,
});
export type Info = z.infer<typeof Info>;
}

View File

@@ -1,15 +0,0 @@
import { teamTable } from "../team/team.sql.js";
import { pgEnum, pgTable } from "drizzle-orm/pg-core";
import { id, ulid, timestamps } from "../drizzle/types.js";
export const MemberRole = pgEnum("role", ["adult", "child"]);
export const memberTable = pgTable("members", {
...id,
...timestamps,
name: ulid("name").notNull(),
teamID: ulid("team_id").references(() => teamTable.id, {
onDelete: "cascade",
}),
role: MemberRole("role").notNull(),
});

View File

@@ -0,0 +1,31 @@
import { z } from "zod/v4";
import { Examples } from "../examples.js";
export namespace Organisation {
export const Info = z
.object({
id: z.string().meta({
description: "The unique identifier of the organisation",
example: Examples.Organisation.id,
}),
name: z.string().meta({
description: "The name of the organisation",
example: Examples.Organisation.name,
}),
slug: z.string().meta({
description: "The unique slug identifier for the organisation",
example: Examples.Organisation.slug,
}),
ownerID: z.string().meta({
description: "The unique identifier of the organisation owner",
example: Examples.Organisation.ownerID,
}),
})
.meta({
ref: "Organisation",
description: "Represents an organisation entity within Nestri",
example: Examples.Organisation,
});
export type Info = z.infer<typeof Info>;
}

View File

@@ -0,0 +1,13 @@
import { id, timestamps, ulid } from "../drizzle/types.js";
import { pgTable, varchar } from "drizzle-orm/pg-core";
import { userTable } from "../user/user.sql.js";
export const orgTable = pgTable("organisations", {
...id,
...timestamps,
ownerID: ulid("owner_id").references(() => userTable.id, {
onDelete: "cascade",
}),
name: varchar("name", { length: 255 }).unique().notNull(),
slug: varchar("slug", { length: 255 }).unique().notNull(),
});

View File

@@ -0,0 +1,57 @@
import { z } from "zod/v4";
import { Examples } from "../examples.js";
import { Limitations } from "./steam.sql.js";
export namespace Steam {
export const Info = z
.object({
id: z.string().meta({
description: "The unique Steam ID of the account",
example: Examples.SteamAccount.id,
}),
ownerID: z.string().meta({
description:
"The unique identifier of the user who owns this Steam account",
example: Examples.SteamAccount.ownerID,
}),
name: z.string().meta({
description: "The display name of the Steam account",
example: Examples.SteamAccount.name,
}),
username: z.string().meta({
description: "The username of the Steam account",
example: Examples.SteamAccount.username,
}),
realName: z.string().nullable().meta({
description: "The real name associated with the Steam account",
example: Examples.SteamAccount.realName,
}),
memberSince: z.date().meta({
description: "The date when the Steam account was created",
example: Examples.SteamAccount.memberSince,
}),
avatarHash: z.string().meta({
description: "The hash of the Steam account's avatar",
example: Examples.SteamAccount.avatarHash,
}),
limitations: Limitations.meta({
description: "Account limitations and restrictions",
example: Examples.SteamAccount.limitations,
}),
profileUrl: z.string().meta({
description: "The Steam community profile URL or identifier",
example: Examples.SteamAccount.profileUrl,
}),
lastSyncedAt: z.date().meta({
description: "The timestamp when the account was last synced",
example: Examples.SteamAccount.lastSyncedAt,
}),
})
.meta({
ref: "SteamAccount",
description: "Represents a Steam account entity",
example: Examples.SteamAccount,
});
export type Info = z.infer<typeof Info>;
}

View File

@@ -0,0 +1,45 @@
import { z } from "zod/v4";
import { Examples } from "../examples.js";
import { userTable } from "../user/user.sql.js";
import { timestamps, ulid, utc } from "../drizzle/types.js";
import { pgTable, varchar, json } from "drizzle-orm/pg-core";
export const Limitations = z.object({
isLimited: z.boolean().meta({
description: "Whether the account has limited functionality",
example: Examples.SteamAccount.limitations.isLimited,
}),
tradeBanState: z.string().meta({
description: "The current trade ban status",
example: Examples.SteamAccount.limitations.tradeBanState,
}),
isVacBanned: z.boolean().meta({
description: "Whether the account is VAC banned",
example: Examples.SteamAccount.limitations.isVacBanned,
}),
visibilityState: z.number().meta({
description: "The visibility state of the profile",
example: Examples.SteamAccount.limitations.visibilityState,
}),
privacyState: z.string().meta({
description: "The privacy state of the profile",
example: Examples.SteamAccount.limitations.privacyState,
}),
});
export type Limitations = z.infer<typeof Limitations>;
export const steamTable = pgTable("steam_accounts", {
...timestamps,
id: varchar("id", { length: 255 }).primaryKey().notNull(),
ownerID: ulid("owner_id").references(() => userTable.id, {
onDelete: "cascade",
}),
memberSince: utc("member_since").notNull(),
name: varchar("name", { length: 255 }).notNull(),
lastSyncedAt: utc("last_synced_at").notNull(),
realName: varchar("real_name", { length: 255 }),
profileUrl: varchar("profile_url", { length: 255 }),
avatarHash: varchar("avatar_hash", { length: 255 }).notNull(),
limitations: json("limitations").$type<Limitations>().notNull(),
});

View File

@@ -1,100 +0,0 @@
import { z } from "zod/v4";
import { fn } from "../utils/fn.js";
import { Examples } from "../examples.js";
import { teamTable } from "./team.sql.js";
import { createID } from "../utils/id.js";
import { Polar } from "../polar/index.js";
import { Database } from "../drizzle/index.js";
import { VisibleError, ErrorCodes } from "../error.js";
import { and, asc, eq, isNull, sql } from "drizzle-orm";
export namespace Team {
export const Info = z
.object({
id: z.string().meta({
description: "The unique identifier of the team",
example: Examples.Team.id,
}),
email: z.email().meta({
description: "The contact email address for the team",
example: Examples.Team.email,
}),
polarCustomerID: z.string().nullable().meta({
description: "Associated Polar.sh customer identifier",
example: Examples.Team.polarCustomerID,
}),
})
.meta({
ref: "Team",
description: "Represents a team entity",
example: Examples.Team,
});
export type Info = z.infer<typeof Info>;
export class TeamExistsError extends VisibleError {
constructor(email: string) {
super(
"already_exists",
ErrorCodes.Validation.ALREADY_EXISTS,
`A team with this email ${email} already exists`,
);
}
}
export const create = fn(
Info.omit({ polarCustomerID: true }).partial({ id: true }),
async (input) => {
const userID = createID("team");
const polarCustomerID = await Polar.fromUserEmail(input.email).then(
(polarUser) => polarUser?.id || null,
);
const id = input.id ?? userID;
await Database.transaction((tx) => {
return tx
.insert(teamTable)
.values({
id,
email: input.email,
polarCustomerID,
})
.onConflictDoUpdate({
target: teamTable.email,
set: {
timeDeleted: null,
id: sql.raw(`excluded.${teamTable.id.name}`),
polarCustomerID: sql.raw(
`excluded.${teamTable.polarCustomerID.name}`,
),
},
});
});
return id;
},
);
export const fromEmail = fn(Info.shape.email, async (email) =>
Database.transaction(async (tx) =>
tx
.select()
.from(teamTable)
.where(and(eq(teamTable.email, email), isNull(teamTable.timeDeleted)))
.orderBy(asc(teamTable.timeCreated))
.execute()
.then((rows) => rows.map(serialize).at(0)),
),
);
export function serialize(
input: typeof teamTable.$inferSelect,
): z.infer<typeof Info> {
return {
id: input.id,
email: input.email,
polarCustomerID: input.polarCustomerID,
};
}
}

View File

@@ -0,0 +1,31 @@
import { z } from "zod/v4";
import { Examples } from "../examples.js";
export namespace User {
export const Info = z
.object({
id: z.string().meta({
description: "The unique identifier of the user",
example: Examples.User.id,
}),
name: z.string().optional().meta({
description: "The full name of the user",
example: Examples.User.name,
}),
email: z.email().meta({
description: "The email address of the user",
example: Examples.User.email,
}),
customerID: z.string().nullable().meta({
description: "Associated Polar.sh customer identifier",
example: Examples.User.customerID,
}),
})
.meta({
ref: "User",
description: "Represents a user entity",
example: Examples.User,
});
export type Info = z.infer<typeof Info>;
}

View File

@@ -1,9 +1,10 @@
import { id, timestamps } from "../drizzle/types.js";
import { pgTable, varchar } from "drizzle-orm/pg-core";
export const teamTable = pgTable("team", {
export const userTable = pgTable("users", {
...id,
...timestamps,
name: varchar("name", { length: 255 }),
email: varchar("email", { length: 255 }).unique().notNull(),
polarCustomerID: varchar("polar_customer_id", { length: 255 }),
customerID: varchar("customer_id", { length: 255 }).notNull(),
});

View File

@@ -1,11 +1,14 @@
import { ulid } from "ulid";
export const prefixes = {
apiApp: "app",
user: "usr",
steam: "stm",
team: "tm",
app: "app",
member: "mbr",
apiSecret: "sec",
apiPersonal: "pat",
team: "team",
member: "mebr",
organisation: "org",
} as const;
/**

View File

@@ -2,6 +2,7 @@ import { subjects } from "@/subjects";
import { MiddlewareHandler } from "hono";
import { Actor } from "@nestri/core/actor";
import { Api } from "@nestri/core/api/index";
import { Member } from "@nestri/core/member/index";
import { createClient } from "@openauthjs/openauth/client";
import { VisibleError, ErrorCodes } from "@nestri/core/error";
@@ -52,6 +53,9 @@ export const auth: MiddlewareHandler = async (c, next) => {
"Invalid bearer token",
);
if (result.subject.type === "team") {
const memberID =
c.req.header("x-nestri-member") || c.req.query("memberID");
if (!memberID) {
return Actor.provide(
"team",
{
@@ -61,6 +65,26 @@ export const auth: MiddlewareHandler = async (c, next) => {
next,
);
}
return Actor.provide(
"system",
{
teamID: result.subject.properties.teamID,
},
async () => {
const member = await Member.fromID(email);
if (!member || member.timeDeleted) {
c.status(401);
return c.text("Unauthorized: User not found");
}
return Actor.provide(
"member",
{ memberID: member.id, teamID: member.workspaceID },
next,
);
},
);
}
}
return Actor.provide("public", {}, next);