create basic user to organization db

This commit is contained in:
Nate Kelley 2025-07-15 16:20:40 -06:00
parent 74993ca556
commit 2e73d97ffc
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
4 changed files with 226 additions and 0 deletions

View File

@ -7,6 +7,8 @@ const app = new Hono()
// Apply authentication globally to ALL routes in this router
.use('*', requireAuth)
.get('/', (c) => {
const _user = c.get('busterUser');
// Stub data for user listing (only accessible to authenticated users)
const stubUsers = [
{ id: '1', name: 'John Doe', email: 'john@example.com', role: 'admin' },

View File

@ -1 +1,2 @@
export * from './user';
export * from './users-to-organizations';

View File

@ -0,0 +1,133 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { getUserOrganizationId } from '../organizations/organizations';
import { getUserToOrganization } from './users-to-organizations';
// Mock the organizations module
vi.mock('../organizations/organizations', () => ({
getUserOrganizationId: vi.fn(),
}));
// Mock the database connection
vi.mock('../../connection', () => ({
db: {
select: vi.fn(() => ({
from: vi.fn(() => ({
innerJoin: vi.fn(() => ({
where: vi.fn(() => Promise.resolve([])),
})),
})),
})),
},
}));
describe('getUserToOrganization', () => {
const mockGetUserOrganizationId = vi.mocked(getUserOrganizationId);
beforeEach(() => {
vi.clearAllMocks();
});
test('should throw error when user is not found in any organization', async () => {
// Arrange
mockGetUserOrganizationId.mockResolvedValue(null);
// Act & Assert
await expect(
getUserToOrganization({ userId: '123e4567-e89b-12d3-a456-426614174000' })
).rejects.toThrow('User not found in any organization');
});
test('should return users in the same organization without filters', async () => {
// Arrange
const mockUsers = [
{
id: '123e4567-e89b-12d3-a456-426614174000',
name: 'John Doe',
email: 'john@example.com',
avatarUrl: null,
role: 'querier',
status: 'active',
},
{
id: '123e4567-e89b-12d3-a456-426614174001',
name: 'Jane Smith',
email: 'jane@example.com',
avatarUrl: 'https://example.com/avatar.jpg',
role: 'workspace_admin',
status: 'active',
},
];
mockGetUserOrganizationId.mockResolvedValue({
organizationId: 'org-123',
role: 'querier',
});
const { db } = await import('../../connection');
const mockDb = vi.mocked(db);
mockDb.select.mockReturnValue({
from: vi.fn().mockReturnValue({
innerJoin: vi.fn().mockReturnValue({
where: vi.fn().mockResolvedValue(mockUsers),
}),
}),
} as unknown as ReturnType<typeof db.select>);
// Act
const result = await getUserToOrganization({
userId: '123e4567-e89b-12d3-a456-426614174000',
});
// Assert
expect(result).toEqual(mockUsers);
expect(mockGetUserOrganizationId).toHaveBeenCalledWith('123e4567-e89b-12d3-a456-426614174000');
});
test('should apply userName filter when provided', async () => {
// Arrange
const mockUsers = [
{
id: '123e4567-e89b-12d3-a456-426614174000',
name: 'John Doe',
email: 'john@example.com',
avatarUrl: null,
role: 'querier',
status: 'active',
},
];
mockGetUserOrganizationId.mockResolvedValue({
organizationId: 'org-123',
role: 'querier',
});
const { db } = await import('../../connection');
const mockDb = vi.mocked(db);
mockDb.select.mockReturnValue({
from: vi.fn().mockReturnValue({
innerJoin: vi.fn().mockReturnValue({
where: vi.fn().mockResolvedValue(mockUsers),
}),
}),
} as unknown as ReturnType<typeof db.select>);
// Act
const result = await getUserToOrganization({
userId: '123e4567-e89b-12d3-a456-426614174000',
filters: { userName: 'John' },
});
// Assert
expect(result).toEqual(mockUsers);
expect(mockGetUserOrganizationId).toHaveBeenCalledWith('123e4567-e89b-12d3-a456-426614174000');
});
test('should validate input with invalid UUID', async () => {
// Act & Assert
await expect(getUserToOrganization({ userId: 'invalid-uuid' })).rejects.toThrow(
'User ID must be a valid UUID'
);
});
});

View File

@ -0,0 +1,90 @@
import { and, eq, isNull, like } from 'drizzle-orm';
import { z } from 'zod';
import { db } from '../../connection';
import { users, usersToOrganizations } from '../../schema';
import { getUserOrganizationId } from '../organizations/organizations';
// Input schema for type safety
const GetUserToOrganizationInputSchema = z.object({
userId: z.string().uuid('User ID must be a valid UUID'),
filters: z
.object({
userName: z.string().optional(),
email: z.string().optional(),
})
.optional(),
});
export type GetUserToOrganizationInput = z.infer<typeof GetUserToOrganizationInputSchema>;
// Output schema for type safety
export const UserOutputSchema = z.object({
id: z.string().uuid(),
name: z.string().nullable(),
email: z.string(),
avatarUrl: z.string().nullable(),
role: z.string(),
status: z.string(),
});
export type UserWithRole = z.infer<typeof UserOutputSchema>;
export const getUserToOrganization = async ({
userId,
filters,
}: GetUserToOrganizationInput): Promise<UserWithRole[]> => {
// Validate input
const validated = GetUserToOrganizationInputSchema.parse({ userId, filters });
try {
// First, get the user's organization ID using the existing function
const userOrg = await getUserOrganizationId(validated.userId);
if (!userOrg) {
throw new Error('User not found in any organization');
}
const organizationId = userOrg.organizationId;
// Build the where conditions for the join
const joinConditions = and(
eq(users.id, usersToOrganizations.userId),
eq(usersToOrganizations.organizationId, organizationId),
isNull(usersToOrganizations.deletedAt)
);
// Build the where conditions for filtering
const conditions = [];
if (validated.filters?.userName) {
conditions.push(like(users.name, `%${validated.filters.userName}%`));
}
if (validated.filters?.email) {
conditions.push(like(users.email, `%${validated.filters.email}%`));
}
const baseQuery = db
.select({
id: users.id,
name: users.name,
email: users.email,
avatarUrl: users.avatarUrl,
role: usersToOrganizations.role,
status: usersToOrganizations.status,
})
.from(users)
.innerJoin(usersToOrganizations, joinConditions);
const results =
conditions.length > 0
? await baseQuery.where(conditions.length === 1 ? conditions[0] : and(...conditions))
: await baseQuery;
// Validate and return results
return results.map((user) => UserOutputSchema.parse(user));
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(`Invalid input: ${error.errors.map((e) => e.message).join(', ')}`);
}
throw error;
}
};