Organize db exports into client and server versions (#32)

This commit is contained in:
Chris Estreich 2025-05-18 13:19:07 -07:00 committed by GitHub
parent b167eaec1d
commit 67c63b5b46
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 130 additions and 112 deletions

View file

@ -3,7 +3,7 @@
import { eq } from 'drizzle-orm';
import { logger } from '@/lib/server/logger';
import { client as db, users, orgs } from '@/db';
import { client as db, users, orgs } from '@/db/server';
import { syncCurrentUser } from '../sync';

View file

@ -4,7 +4,7 @@ import { eq } from 'drizzle-orm';
import { clerkClient } from '@clerk/nextjs/server';
import { logger } from '@/lib/server/logger';
import { client as db, orgs } from '@/db';
import { client as db, orgs } from '@/db/server';
import { syncOrg } from '../sync';

View file

@ -9,8 +9,7 @@ import {
import type { TimePeriod } from '@/types';
import { analytics } from '@/lib/server';
import * as db from '@/db';
import { inArray } from 'drizzle-orm';
import { type User, getUsersById } from '@/db/server';
/**
* captureEvent
@ -106,7 +105,7 @@ const developerUsageSchema = z.object({
});
export type DeveloperUsage = z.infer<typeof developerUsageSchema> & {
user: db.User;
user: User;
};
export const getDeveloperUsage = async ({
@ -140,25 +139,9 @@ export const getDeveloperUsage = async ({
.array(developerUsageSchema)
.parse(await resultSet.json());
const users = (
await db.client
.select()
.from(db.users)
.where(
inArray(
db.users.id,
developerUsages.map(({ userId }) => userId),
),
)
).reduce(
(acc, user) => ({ ...acc, [user.id]: user }),
{} as Record<string, db.User>,
);
const users = await getUsersById(developerUsages.map(({ userId }) => userId));
return developerUsages
.map((usage) => ({
...usage,
user: users[usage.userId],
}))
.map((usage) => ({ ...usage, user: users[usage.userId] }))
.filter((usage): usage is DeveloperUsage => !!usage.user);
};

View file

@ -9,7 +9,7 @@ import {
type CreateAuditLog,
client as db,
auditLogs,
} from '@/db';
} from '@/db/server';
export const getAuditLogs = async ({
orgId,

View file

@ -4,7 +4,7 @@ import { sql } from 'drizzle-orm';
import { z } from 'zod';
import { type ApiResponse, ORGANIZATION_ALLOW_ALL } from '@/types';
import { client as db, AuditLogTargetType, orgSettings } from '@/db';
import { AuditLogTargetType, client as db, orgSettings } from '@/db/server';
import { isAuthSuccess, handleError } from '@/lib/server';
import { validateAuth } from './auth';

View file

@ -12,7 +12,7 @@ import {
organizationAllowListSchema,
organizationDefaultSettingsSchema,
} from '@/types';
import { client as db, AuditLogTargetType, orgSettings } from '@/db';
import { AuditLogTargetType, client as db, orgSettings } from '@/db/server';
import { handleError, isAuthSuccess } from '@/lib/server';
import { validateAuth } from './auth';

View file

@ -4,7 +4,7 @@ import { z } from 'zod';
import type { ApiResponse } from '@/types';
import { handleError, isAuthSuccess } from '@/lib/server';
import { AuditLogTargetType } from '@/db';
import { AuditLogTargetType } from '@/db/server';
import { validateAuth } from './auth';
import { createAuditLog } from './auditLogs';

View file

@ -9,7 +9,7 @@ import {
client as db,
users,
orgs,
} from '@/db';
} from '@/db/server';
import { logger } from '@/lib/server';
export async function syncCurrentUser({

View file

@ -1,46 +1,9 @@
'use client';
import { ArrowRight, Calendar, Clock, User } from 'lucide-react';
import Link from 'next/link';
import { ArrowRight, Calendar, Clock, User } from 'lucide-react';
import type { AuditLog } from '@/db';
type AuditLogDetailsProps = {
log: AuditLog;
};
const formatValue = (value: unknown) => {
if (value === null || value === undefined) {
return <span className="italic text-muted-foreground">None</span>;
}
if (Array.isArray(value)) {
return (
<ul className="list-disc pl-5">
{value.map((item, index) => (
<li key={index}>{String(item)}</li>
))}
</ul>
);
}
if (typeof value === 'object') {
return (
<div className="space-y-1">
{Object.entries(value).map(([key, val]) => (
<div key={key} className="grid grid-cols-2 gap-2">
<span className="text-sm font-medium">{key}:</span>
<span className="text-sm">{String(val)}</span>
</div>
))}
</div>
);
}
return String(value);
};
export const AuditLogDetails = ({ log }: AuditLogDetailsProps) => (
export const AuditLogDetails = ({ log }: { log: AuditLog }) => (
<div className="space-y-6">
<div className="space-y-4">
<div className="flex items-center space-x-2 text-sm text-muted-foreground">
@ -78,3 +41,34 @@ export const AuditLogDetails = ({ log }: AuditLogDetailsProps) => (
</div>
</div>
);
const formatValue = (value: unknown) => {
if (value === null || value === undefined) {
return <span className="italic text-muted-foreground">None</span>;
}
if (Array.isArray(value)) {
return (
<ul className="list-disc pl-5">
{value.map((item, index) => (
<li key={index}>{String(item)}</li>
))}
</ul>
);
}
if (typeof value === 'object') {
return (
<div className="space-y-1">
{Object.entries(value).map(([key, val]) => (
<div key={key} className="grid grid-cols-2 gap-2">
<span className="text-sm font-medium">{key}:</span>
<span className="text-sm">{String(val)}</span>
</div>
))}
</div>
);
}
return String(value);
};

View file

@ -1,26 +1,13 @@
import { formatDistance } from 'date-fns';
import { Settings, Sliders, Users } from 'lucide-react';
import { type AuditLog, AuditLogTargetType } from '@/db/schema';
import { type AuditLog, AuditLogTargetType } from '@/db';
type AuditLogEntryProps = {
log: AuditLog;
onClick: (log: AuditLog) => void;
};
const getIconByType = (type: AuditLogTargetType) => {
switch (type) {
case AuditLogTargetType.PROVIDER_WHITELIST:
return <Settings className="size-4 text-purple-500" />;
case AuditLogTargetType.DEFAULT_PARAMETERS:
return <Sliders className="size-4 text-green-500" />;
case AuditLogTargetType.MEMBER_CHANGE:
return <Users className="size-4 text-amber-500" />;
default:
return <Settings className="size-4 text-gray-500" />;
}
};
export const AuditLogEntry = ({ log, onClick }: AuditLogEntryProps) => (
<div
onClick={() => onClick(log)}
@ -40,3 +27,16 @@ export const AuditLogEntry = ({ log, onClick }: AuditLogEntryProps) => (
</div>
</div>
);
const getIconByType = (type: AuditLogTargetType) => {
switch (type) {
case AuditLogTargetType.PROVIDER_WHITELIST:
return <Settings className="size-4 text-purple-500" />;
case AuditLogTargetType.DEFAULT_PARAMETERS:
return <Sliders className="size-4 text-green-500" />;
case AuditLogTargetType.MEMBER_CHANGE:
return <Users className="size-4 text-amber-500" />;
default:
return <Settings className="size-4 text-gray-500" />;
}
};

5
src/db/enums.ts Normal file
View file

@ -0,0 +1,5 @@
export enum AuditLogTargetType {
PROVIDER_WHITELIST = 1,
DEFAULT_PARAMETERS = 2,
MEMBER_CHANGE = 3, // TODO: Currently no logs of this type are collected.
}

View file

@ -1,2 +1,4 @@
export * from './client';
export * from './schema';
// These can safely be imported in client or server environments.
export * from './enums';
export * from './types';

1
src/db/queries/index.ts Normal file
View file

@ -0,0 +1 @@
export * from './users';

13
src/db/queries/users.ts Normal file
View file

@ -0,0 +1,13 @@
import { inArray } from 'drizzle-orm';
import { client as db } from '../db';
import { users } from '../schema';
import type { User } from '../types';
export const getUsersById = async (
ids: string[],
): Promise<Record<string, User>> =>
(await db.select().from(users).where(inArray(users.id, ids))).reduce(
(acc, user) => ({ ...acc, [user.id]: user }),
{} as Record<string, User>,
);

View file

@ -15,6 +15,8 @@ import {
ORGANIZATION_ALLOW_ALL,
} from '@/types';
import { AuditLogTargetType } from './enums';
/**
* users
*/
@ -48,10 +50,6 @@ export const userRelations = relations(users, ({ one }) => ({
}),
}));
export type User = typeof users.$inferSelect;
export type CreateUser = Omit<typeof users.$inferInsert, 'id' | 'createdAt'>;
/**
* organizations
*/
@ -83,10 +81,6 @@ export const orgsRelations = relations(orgs, ({ many, one }) => ({
}),
}));
export type Org = typeof orgs.$inferSelect;
export type CreateOrg = Omit<typeof orgs.$inferInsert, 'id' | 'createdAt'>;
/**
* organization_settings
*/
@ -122,8 +116,6 @@ export const orgSettingsRelations = relations(orgSettings, ({ one }) => ({
}),
}));
export type OrgSettings = typeof orgSettings.$inferSelect;
/**
* audit_logs
*/
@ -138,7 +130,7 @@ export const auditLogs = pgTable(
orgId: text('organization_id')
.notNull()
.references(() => orgs.id),
targetType: integer('target_type').notNull(), // AuditLogTargetType
targetType: integer('target_type').$type<AuditLogTargetType>().notNull(),
targetId: text('target_id').notNull(),
newValue: jsonb('new_value').notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
@ -162,20 +154,3 @@ export const auditLogsRelations = relations(auditLogs, ({ one }) => ({
references: [orgs.id],
}),
}));
export enum AuditLogTargetType {
PROVIDER_WHITELIST = 1,
DEFAULT_PARAMETERS = 2,
MEMBER_CHANGE = 3, // TODO: Currently no logs of this type are collected.
}
export type AuditLog = typeof auditLogs.$inferSelect & {
targetType: AuditLogTargetType;
};
export type CreateAuditLog = Omit<
typeof auditLogs.$inferInsert,
'id' | 'createdAt'
> & {
targetType: AuditLogTargetType;
};

7
src/db/server.ts Normal file
View file

@ -0,0 +1,7 @@
// These can only be imported in a server (node.js) environment.
export * from './index';
export * from './db';
export * from './schema';
export * from './queries';

38
src/db/types.ts Normal file
View file

@ -0,0 +1,38 @@
import type { users, orgs, orgSettings, auditLogs } from './schema';
type Generated = 'id' | 'createdAt' | 'updatedAt';
/**
* users
*/
export type User = typeof users.$inferSelect;
export type CreateUser = Omit<typeof users.$inferInsert, Generated>;
/**
* orgs
*/
export type Org = typeof orgs.$inferSelect;
export type CreateOrg = Omit<typeof orgs.$inferInsert, Generated>;
/**
* orgSettings
*/
export type OrgSettings = typeof orgSettings.$inferSelect;
export type CreateOrgSettings = Omit<
typeof orgSettings.$inferInsert,
Generated
>;
/**
* auditLogs
*/
export type AuditLog = typeof auditLogs.$inferSelect;
export type CreateAuditLog = Omit<typeof auditLogs.$inferInsert, Generated>;

View file

@ -1,7 +1,7 @@
import { sql } from 'drizzle-orm';
import { Env } from '@/lib/server';
import { testDb, disconnect } from '@/db';
import { testDb, disconnect } from '@/db/server';
async function resetTestDatabase() {
const db = testDb!;