feat: Sync to OSS repo

This commit is contained in:
Wanjohi
2026-08-06 22:13:51 +03:00
parent 46d2a56180
commit 3faac3008f
144 changed files with 27561 additions and 0 deletions

View File

@@ -0,0 +1,143 @@
import { eq, and, isNull, sql } from 'drizzle-orm';
import z from 'zod';
import { Actor } from '../actor.js';
import { Database } from '../db/index.js';
import { Examples } from '../examples.js';
import { fn } from '../fn.js';
import { Identifier } from '../id.js';
import { TeamMemberTable } from './member.sql.js';
import { TeamTable } from './team.sql.js';
export namespace Team {
export const Info = z
.object({
id: z.string().meta({
description: 'Unique identifier for the team record',
example: Examples.Team.id
}),
name: z.string().meta({
description: 'Display name of the team',
example: Examples.Team.name
}),
slug: z.string().meta({
description: 'URL-friendly unique slug for the team',
example: Examples.Team.slug
}),
ownerId: z.string().meta({
description: 'The user who owns/created this team',
example: Examples.Team.ownerId
}),
billingEmail: z.email().nullable().optional().meta({
description: 'Email address used for billing and invoices',
example: Examples.Team.billingEmail
}),
plan: z.string().optional().meta({
description: 'Current billing plan (free, pro, team, enterprise)',
example: Examples.Team.plan
}),
subscriptionStatus: z.string().optional().meta({
description: 'Current subscription status (active, past_due, canceled, etc.)',
example: Examples.Team.subscriptionStatus
}),
metadata: z.record(z.string(), z.unknown()).nullable().optional().meta({
description: 'Arbitrary metadata attached to the team',
example: Examples.Team.metadata
})
})
.meta({
ref: 'Team',
description:
'A team/organization for collaboration and billing. Users join teams via memberships.',
example: Examples.Team
});
export type Info = z.infer<typeof Info>;
export const create = fn(Info.pick({ id: true, name: true, slug: true }), async (input) => {
const ownerId = Actor.userID;
await Database.use(async (tx) => {
await tx.insert(TeamTable).values({
id: input.id,
name: input.name,
slug: input.slug,
ownerId
});
await tx.insert(TeamMemberTable).values({
id: Identifier.ascending('teamMember'),
teamId: input.id,
userId: ownerId,
role: 'owner'
});
});
return input.id;
});
export const fromID = fn(Info.shape.id, async (id) => {
return Database.use(async (tx) => {
return tx
.select()
.from(TeamTable)
.where(and(eq(TeamTable.id, id), isNull(TeamTable.timeDeleted)))
.then((rows) => rows.at(0) ?? null);
});
});
export const fromSlug = fn(Info.shape.slug, async (slug) => {
return Database.use(async (tx) => {
return tx
.select()
.from(TeamTable)
.where(and(eq(TeamTable.slug, slug), isNull(TeamTable.timeDeleted)))
.then((rows) => rows.at(0) ?? null);
});
});
export async function list() {
return Database.use(async (tx) => {
return tx
.select()
.from(TeamTable)
.where(isNull(TeamTable.timeDeleted))
.orderBy(TeamTable.timeCreated);
});
}
export const remove = fn(Info.shape.id, async (id) => {
await Database.use(async (tx) => {
await tx
.update(TeamTable)
.set({ timeDeleted: sql`now()` })
.where(eq(TeamTable.id, id));
});
});
export const createPersonal = fn(z.object({ displayName: z.string() }), async (input) => {
const baseSlug = input.displayName
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '')
.slice(0, 50);
const existing = await fromSlug(baseSlug);
const slug = existing
? `${baseSlug}-${String(Math.floor(Math.random() * 9999)).padStart(4, '0')}`
: baseSlug;
const id = Identifier.ascending('team');
return create({ id, name: `${input.displayName}'s Team`, slug });
});
export function serialize(input: typeof TeamTable.$inferSelect): z.infer<typeof Info> {
return {
id: input.id,
name: input.name,
slug: input.slug,
ownerId: input.ownerId,
billingEmail: input.billingEmail,
plan: input.plan,
subscriptionStatus: input.subscriptionStatus,
metadata: input.metadata
};
}
}

View File

@@ -0,0 +1,27 @@
import { index, pgTable, pgEnum, uniqueIndex } from 'drizzle-orm/pg-core';
import { id, timestamps, ulid } from '../db/types.js';
import { UserTable } from '../user/user.sql.js';
import { TeamTable } from './team.sql.js';
export const TeamMemberRole = pgEnum('team_member_role', ['owner', 'admin', 'member']);
export const TeamMemberTable = pgTable(
'team_member',
{
...id,
...timestamps,
teamId: ulid('team_id')
.notNull()
.references(() => TeamTable.id, { onDelete: 'cascade' }),
userId: ulid('user_id')
.notNull()
.references(() => UserTable.id, { onDelete: 'cascade' }),
role: TeamMemberRole('role').notNull().default('member')
},
(t) => [
uniqueIndex('team_member_team_user_unique').on(t.teamId, t.userId),
index('team_member_team_idx').on(t.teamId),
index('team_member_user_idx').on(t.userId)
]
);

View File

@@ -0,0 +1,114 @@
import { eq, and, isNull, sql } from 'drizzle-orm';
import z from 'zod';
import { Database } from '../db/index.js';
import { Examples } from '../examples.js';
import { fn } from '../fn.js';
import { TeamMemberRole, TeamMemberTable } from './member.sql.js';
export namespace Member {
export const Info = z
.object({
id: z.string().meta({
description: 'Unique identifier for the membership record',
example: Examples.Member.id
}),
teamId: z.string().meta({
description: 'The team this membership belongs to',
example: Examples.Member.teamId
}),
userId: z.string().meta({
description: 'The user who is a member of the team',
example: Examples.Member.userId
}),
role: z.enum(TeamMemberRole.enumValues).meta({
description: 'Role within the team (owner, admin, member)',
example: Examples.Member.role
})
})
.meta({
ref: 'Member',
description: 'Links a user to a team with a specific role',
example: Examples.Member
});
export type Info = z.infer<typeof Info>;
export const create = fn(
Info.pick({ id: true, teamId: true, userId: true, role: true }),
async (input) => {
await Database.use(async (tx) => {
await tx.insert(TeamMemberTable).values({
id: input.id,
teamId: input.teamId,
userId: input.userId,
role: input.role ?? 'member'
});
});
return input.id;
}
);
export const findByTeamAndUser = fn(Info.pick({ teamId: true, userId: true }), async (input) => {
return Database.use(async (tx) => {
return tx
.select()
.from(TeamMemberTable)
.where(
and(
eq(TeamMemberTable.teamId, input.teamId),
eq(TeamMemberTable.userId, input.userId),
isNull(TeamMemberTable.timeDeleted)
)
)
.then((rows) => rows.at(0) ?? null);
});
});
export const listByTeam = fn(Info.shape.teamId, async (teamId) => {
return Database.use(async (tx) => {
return tx
.select()
.from(TeamMemberTable)
.where(and(eq(TeamMemberTable.teamId, teamId), isNull(TeamMemberTable.timeDeleted)))
.orderBy(TeamMemberTable.timeCreated);
});
});
export const listByUser = fn(Info.shape.userId, async (userId) => {
return Database.use(async (tx) => {
return tx
.select()
.from(TeamMemberTable)
.where(and(eq(TeamMemberTable.userId, userId), isNull(TeamMemberTable.timeDeleted)))
.orderBy(TeamMemberTable.timeCreated);
});
});
export const updateRole = fn(Info.pick({ id: true, role: true }), async (input) => {
await Database.use(async (tx) => {
await tx
.update(TeamMemberTable)
.set({ role: input.role })
.where(eq(TeamMemberTable.id, input.id));
});
});
export const remove = fn(Info.shape.id, async (id) => {
await Database.use(async (tx) => {
await tx
.update(TeamMemberTable)
.set({ timeDeleted: sql`now()` })
.where(eq(TeamMemberTable.id, id));
});
});
export function serialize(input: typeof TeamMemberTable.$inferSelect): z.infer<typeof Info> {
return {
id: input.id,
teamId: input.teamId,
userId: input.userId,
role: input.role
};
}
}

View File

@@ -0,0 +1,18 @@
import { jsonb, pgTable, text } from 'drizzle-orm/pg-core';
import { id, timestamps, ulid } from '../db/types.js';
import { UserTable } from '../user/user.sql.js';
export const TeamTable = pgTable('team', {
...id,
...timestamps,
name: text('name').notNull(),
slug: text('slug').notNull().unique(),
ownerId: ulid('owner_id')
.notNull()
.references(() => UserTable.id, { onDelete: 'cascade' }),
billingEmail: text('billing_email'),
plan: text('plan').notNull().default('free'),
subscriptionStatus: text('subscription_status').notNull().default('active'),
metadata: jsonb('metadata').$type<{}>()
});