javascript - Vad är ett arkitektoniskt rent sätt att utelämna fält från ett databassvar i ett API med Typescript och Kysely


Jag använder Kysely och Typescript. Jag kan få ett User-modell-svar genom att göra:

interface User {
  id: Generated<number>
  first_name: string
  last_name: string
  email: string
  password: string
  is_email_verified: boolean
  role: 'admin' | 'user'
}

interface Database {
  user: User
}

dbClient = dbClient || new Kysely<Database>({
  dialect: new PlanetScaleDialect({url: config.mysql.url})
})

const getUserById = async (id: number) => {
  const user = await db
    .selectFrom('user')
    .selectAll()
    .where('user.id', '=', id)
    .executeTakeFirst()
  return user
}

Mitt API kommer att få svarobjektet på följande sätt:

const getUser: Handler<{ Bindings: Bindings }> = async (c) => {
  const { params } = userValidation.getUser.parse(c.req.parseBody());
  const user = await userService.getUserById(params.userId);
  if (!user) {
    throw new ApiError(httpStatus.NOT_FOUND, 'User not found');
  }
  return c.json(user, httpStatus.OK as StatusCode);
};

Jag kommer att göra något liknande ovan för alla mina modeller/tabeller. Problemet jag har är att vissa av dessa modeller kommer att ha icke-offentliga fält, t.ex. lösenord på Användare. Vad är det bästa sättet att utelämna dessa?

Jag skulle kunna göra en userResponse-funktion t.ex. i user.model.ts:

interface User {
  id: Generated<number>
  first_name: string
  last_name: string
  email: string
  password: string
  is_email_verified: boolean
  role: 'admin' | 'user'
}

interface UserResponse {
  id: number
  first_name: string
  last_name: string
  email: string
  is_email_verified: boolean
  role: 'admin' | 'user'
}

const userResponse = (body: Selectable<User>) => {
  return {
    id: body.id,
    first_name: body.first_name,
    last_name: body.last_name,
    email: body.email,
    is_email_verified: body.is_email_verified,
    role: body.role
  } as UserResponse
}

export {
  User,
  UserResponse,
  userResponse
}

Gör då:

return c.json(userResponse(user), httpStatus.OK as StatusCode);

Eller så kan jag skapa en UserResponse-klass med en lista av privata fält som implementerar User-gränssnittet och överskrider toJSON()-metoden och tar bort alla fält som ingår i arrayen för privata medlemmar. Men på något sätt känns detta fel eftersom Kysely ger mig en User-klass direkt från DB-frågan. Om jag inte konverterar Kysely-svaret till min egen modell vid varje fråga, till exempel.

const getUserById = async (id: number) => {
  const user = await db
    .selectFrom('user')
    .selectAll()
    .where('user.id', '=', id)
    .executeTakeFirst()
  return UserModel(user)
}

Någon idé om vad den rena/optimala lösningen är?

Svar #1

Kysely är en frågebyggare, inte ett ORM. Den har bara kunskap om ditt schema vid kompileringsstid (inte under körning).


Du kan hålla en lista över tabellens kolumnnamn vid körning och endast skicka icke-känsliga sådana till .select(keys)/.returning(keys) istället för att använda .selectAll()/.returningAll(). Du drar också nytta av att bara hämta den data du behöver.

const nonSensitiveUserColumns = [
  "id",
  "first_name",
  "last_name",
  "email",
  "role",
] as const satisfies ReadonlyArray<keyof User>

const getUserById = async (id: number) => {
  const user = await db
    .selectFrom("user")
    .where("user.id", "=", id)
    .select(nonSensitiveUserColumns)
    .executeTakeFirst()
  return user
}

const user = await getUserById(15)
//      ^? { id: number; first_name: string; last_name: string; email: string; role: 'admin' | 'user' }

Lekplatslänk


Ett annat alternativ skulle vara att använda validering/parsing av outputschema. Ett bibliotek som zod kan vara användbart, eftersom det som standard utelämnar alla okända nycklar eller tillåter att utöka befintliga scheman. För att hålla allt DRY kan du göra zod till sanningskällan för dina scheman och skicka deras härledda typer till Kysely.

import { z } from 'zod'
import { Kysely, type Generated, type Selectable } from 'kysely'
import { PlanetScaleDialect } from 'kysely-planetscale'

const userSchema = z.object({
    id: z.custom<Generated<number>>(
        (thing) => z.number().safeParse(thing).success
    ),
    first_name: z.string(),
    last_name: z.string(),
    email: z.string(),
    password: z.string(),
    is_email_verified: z.coerce.boolean(),
    role: z.enum(['admin', 'user'])
})

const safeUserSchema = userSchema.omit({ password: true }).extend({ id: z.number() })

type User = z.infer<typeof userSchema>
type SafeUser = Selectable<z.infer<typeof safeUserSchema>>

interface Database {
    user: User
}

const db = new Kysely<Database>({
  dialect: new PlanetScaleDialect({url: '<url>'})
})

const getUserById = async (id: number) => {
  const user = await db
    .selectFrom('user')
    .where('user.id', '=', id)
    .selectAll()
    .executeTakeFirst()
  return user
}

const user = await getUserById(15);

const safeUser = safeUserSchema.parse(user);
//       ^? SafeUser

Lekplatslänk

Relaterade artiklar