시작하기전에...
nexus-prisma-plugin
을 nexus 팀에서 유지관리를 하지 못하고 Prisma 팀에서 진행하게 되었습니다. https://github.com/graphql-nexus/nexus-plugin-prisma/issues/1039 아직은 초창기이지만, 전에 있었던 타입 이름을 string
으로 사용하는 방식에서 많이 진화되고 있습니다. 즉 Prisma 모델을 그대로 옮겨 올 수 있도록 한 점이 꽤 맘에 들며, Prisma에 AST Node로 작성할 수 있는 description도 그대로 GraphQL description으로 가지고 올 수 있게 됩니다.아직은 Early-preview의 모습이고 Production에서는 사용을 자제하라고 합니다. 하지만 모델 필드의 타입만 지정하거나 하는 경우나 직접 만들어낸 resolver를 사용하거나, 특별한 복잡한 타입을 사용하지 않는 한 Prisma 스키마를 그대로 GraphQL로 옮기는데엔 큰 지장이 없어보입니다.
사실상 Nexus GraphQL을 위한 Prisma의 공식 플러그인이며, 추후 이 플러그인으로 마이그레이션을 해야할 것 으로 보입니다.
![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2Fb69e055d-0651-49a7-87ca-73abde330cb2%2F79902fd5-e2b5-468f-b61d-84e1c453871a%2FUntitled.png%3Ftable%3Dblock%26id%3D42851d4c-16fe-4d03-ad40-e7614f26ca83%26spaceId%3Db69e055d-0651-49a7-87ca-73abde330cb2%26expirationTimestamp%3D1738922400000%26signature%3D3vvHy93B3Ln0VBdQfScahKPIYdh-hMumwmk8PVtg4yI?table=block&id=42851d4c-16fe-4d03-ad40-e7614f26ca83&cache=v2)
- 직접 혹은 스크립트(CI, programmatic, etc.) 실행
$ prisma generate
.
- Prisma 제너레이터 시스템은 Prisma 스키마 파일을 읽습니다.
- Prisma 제너레이터 시스템은 Prisma 스키마의 구조화된 표현인 "DMMF"를 전달하여 Nexus Prisma 제너레이터를 실행합니다.
- Nexus Prisma 제너레이터는 있는 경우 Nexus Prisma 제너레이터 구성을 읽습니다.
- Nexus Prisma 제너레이터는 생성된 소스 코드를 작성합니다. 기본적으로
node_modules
의nexus-prisma
패키지 내의 특정한 위치에 있습니다. 제너레이터 옵션으로 이 위치를 구성할 수 있습니다.
- 코드에서
nexus-prisma
를import
할 때 생성된 타입을을 가져 옵니다.
CRUD model 을 Plain Nexus 함수로 마이그레이션 하기
요약하면 현재 작동중인 API의 Experimental CRUD를 Plain Nexus로 마이그레이션 하기는 거의 불가능해보입니다. Nexus-Prisma 공식 플러그인의 Long-term 로드맵 개발사항으로 기다려야할 것 같습니다.
CashUser
모델을 CRUD로 만들기
추가되는 모델들은
nexus-prisma
를 사용해서 만들어 봅시다.현재 진행중인 스프린트 개발은 DB 통합되면서 Introspection된 Prisma 모델을 사용하게 될 예정입니다. 이때 단순히
CashUser
의 모델이 필요하지만, 이와 연관된 Points
모델도 사용하게 될 예정입니다.Prisma 스키마에는
nexus-prisma
제너레이터를 추가해주어야 합니다.generator nexusPrisma {
provider = "nexus-prisma"
}
추가된 이후엔 Prisma 모델 타입을 Nexus 타입으로 직접 임포트해서 사용할 수 있습니다.
import { CashUser } from "nexus-prisma";
CashUser
를 만들면 아래와 같습니다. CashUser.$name
은 타입이름이 되고,CashUser.$description
은 Prisma 스키마에 있던 description 입니다.
CashUser
라는 넥서스 타입의 필드들은 각각name
,type
,resolve
를 각각 가지고 있습니다. 따라서 Nexus-GraphQL 루틴대로 별개로 추가할 필요 없이 아래와 같이t.field()
에 추가해주기만 하면 됩니다.
- Relation model를 필터링을 하거나 Ordering을 하거나 Pagination, 날짜조건 필터등을 추가해야할 땐 아래와 같이 커스텀 리졸버를 만들어줍니다.
import { intArg, nullable, objectType } from "nexus";
import { CashUser } from "nexus-prisma";
export const CashUserType = objectType({
name: CashUser.$name,
description: CashUser.$description,
definition(t) {
t.field(CashUser.id);
t.field(CashUser.createdAt);
t.field(CashUser.updatedAt);
t.field(CashUser.nickname);
t.field(CashUser.email);
t.field(CashUser.cityId);
t.field(CashUser.isMarried);
t.field(CashUser.referralCode);
t.field(CashUser.deviceId);
t.field(CashUser.point);
t.field(CashUser.referrerId);
t.field(CashUser.isMarketing);
t.field(CashUser.type);
t.field(CashUser.accountId);
t.field(CashUser.status);
t.field(CashUser.deletedAt);
t.field(CashUser.deleteType);
t.field({
name: CashUser.PointActive.name,
type: CashUser.PointActive.type,
args: { take: nullable(intArg()), skip: nullable(intArg()) },
async resolve({ id }, args, { prismaRO, replaceNullsWithUndefineds }) {
const { take, skip } = replaceNullsWithUndefineds(args);
const pointActive = await prismaRO.pointActive.findMany({
where: { userId: { equals: id } },
take,
skip,
});
return pointActive;
},
});
t.field(CashUser.Points_Points_targetUserIdToUsers);
t.field({
name: CashUser.Points_Points_userIdToUsers.name,
type: CashUser.Points_Points_userIdToUsers.type,
args: { take: nullable(intArg()), skip: nullable(intArg()) },
async resolve({ id }, args, { prismaRO, replaceNullsWithUndefineds }) {
const { take, skip } = replaceNullsWithUndefineds(args);
const point = await prismaRO.points.findMany({
where: { userId: { equals: id } },
take,
skip,
});
return point;
},
});
},
});
여기서
CashUser.Points_Points_userIdToUsers
는 타입이 Points
입니다. 따라서 Points
넥서스 타입도 만들어주어야 합니다.Prisma vs Nexus args
Type
경험해 보신분들은 아시겠지만, GraphQL은
undefined
를 허용하지 않습니다. 반면에 Prisma는 null
과 undefined
는 별개로 취급하며 null
을 값으로 취급합니다. 즉, GraphQL argument로 받은 인자는 nullable
인 경우 null
값이 오게되는데 이를 Prisma argument로 넘겨 보낼 땐 undefined
로 변환해주어야 합니다. 그리하여
Context
에 replaceNullsWithUndefineds
라는 함수를 포함시켜주었습니다. 그 반대인 replaceUndefinedWithNulls
도 포함되어있습니다. 이는 반대로 Prisma로 받은 DB데이터의 타입을 GraphQL API로 넘겨줄 때 사용할 수 있습니다.replaceNullsWithUndefineds
이 함수는 객체가 가지고 있는 모든
null
성분을 undefined
로 교체해 줍니다.타입은 아래와 같이 recursive한 Conditional Type을 사용하고 있습니다.
type RecursivelyReplaceNullWithUndefined<T> = T extends null
? undefined
: T extends Record<string, unknown>
? {
[K in keyof T]: T[K] extends (infer U)[]
? RecursivelyReplaceNullWithUndefined<U>[]
: RecursivelyReplaceNullWithUndefined<T[K]>;
}
: T;
함수도 마찬가지로 recursive한 함수로 제작되었습니다.
function replaceNullsWithUndefineds<T>(
obj: T,
): RecursivelyReplaceNullWithUndefined<T> {
const newObj: any = {};
Object.keys(obj).forEach((k) => {
const value: any = (obj as any)[k];
newObj[k as keyof T] =
value === null
? undefined
:
value &&
typeof value === "object" &&
value.__proto__.constructor === Object
? replaceNullsWithUndefineds(value)
: value;
});
return newObj;
}
주의:
Object
타입만 사용해야 합니다. Scalar
타입을 사용하면 빈 {}
값만 나오게 됩니다.replaceUndefinedsWithNulls
이 함수는 객체가 가지고 있는 모든
undefined
성분을 null
로 교체해 줍니다.type RecursivelyReplaceUndefinedWithNull<T> = T extends undefined
? null
: T extends Record<string, unknown>
? {
[K in keyof T]-?: T[K] extends (infer U)[]
? RecursivelyReplaceUndefinedWithNull<U>[]
: RecursivelyReplaceUndefinedWithNull<T[K]>;
}
: T;
함수도 마찬가지로 recursive 함수로 제작되었습니다.
function replaceUndefinedsWithNulls<T>(
obj: T,
): RecursivelyReplaceUndefinedWithNull<T> {
const newObj: any = {};
Object.keys(obj).forEach((k) => {
const value: any = (obj as any)[k];
newObj[k as keyof T] =
typeof value === "undefined"
? null
:
value &&
typeof value === "object" &&
value.__proto__.constructor === Object
? replaceUndefinedsWithNulls(value)
: value;
});
return newObj;
}