Kent C. Dodds
Your brian needs this 🧠
Client
Server
type LoaderData = Await<ReturnType<typeof getLoaderData>> async function getLoaderData() { const users = await prisma.user.findMany({ select: { id: true, email: true, firstName: true, team: true, postReads: { select: { postSlug: true, }, }, }, }) return {users} } export const loader: LoaderFunction = async ({request}) => { return json(await getLoaderData()) } export default function UsersPage() { const data = useLoaderData<LoaderData>() return ( <div> <h1>Users</h1> <ul> {/* all this auto-completes and type checks!! */} {data.users.map(user => ( <li key={user.id}> <div>{user.firstName}</div> </li> ))} </ul> </div> ) }
import type {LinksFunction} from 'remix' import aboutStyles from '~/styles/routes/about.css' export const links: LinksFunction = () => { return [{rel: 'stylesheet', href: aboutStyles}] } export default function AboutScreen() { return <stuff /> }
node .
node --require ./mocks .
MSW to the rescue!
Caching to the rescue!
// here's an example of the cachified credits.yml // that powers the /credits page: async function getPeople({ request, forceFresh, }: { request?: Request forceFresh?: boolean }) { const allPeople = await cachified({ cache: redisCache, key: 'content:data:credits.yml', request, forceFresh, maxAge: 1000 * 60 * 60 * 24 * 30, getFreshValue: async () => { const creditsString = await downloadFile( 'content/data/credits.yml', ) const rawCredits = YAML.parse(creditsString) if (!Array.isArray(rawCredits)) { console.error('Credits is not an array', rawCredits) throw new Error('Credits is not an array.') } return rawCredits.map(mapPerson).filter(typedBoolean) }, checkValue: (value: unknown) => Array.isArray(value), }) return allPeople }
type CacheMetadata = { createdTime: number maxAge: number | null } type VNUP<Value> = | Value | null | undefined | Promise<Value | null | undefined> async function cachified< Value, Cache extends { name: string get: (key: string) => VNUP<{ metadata: CacheMetadata value: Value }> set: ( key: string, value: { metadata: CacheMetadata value: Value }, ) => unknown | Promise<unknown> del: (key: string) => unknown | Promise<unknown> }, >(options: { key: string cache: Cache getFreshValue: () => Promise<Value> checkValue?: (value: Value) => boolean | string forceFresh?: boolean request?: Request fallbackToCache?: boolean timings?: Timings timingType?: string maxAge?: number }): Promise<Value> { // do the stuff }
<img {...otherProps} src="https://res.cloudinary.com/kentcdodds-com/image/upload/w_1517,q_auto,f_auto,b_rgb:e6e9ee/unsplash/photo-1459262838948-3e2de6c1ec80" srcset=" https://res.cloudinary.com/kentcdodds-com/image/upload/w_280,q_auto,f_auto,b_rgb:e6e9ee/unsplash/photo-1459262838948-3e2de6c1ec80 280w, https://res.cloudinary.com/kentcdodds-com/image/upload/w_560,q_auto,f_auto,b_rgb:e6e9ee/unsplash/photo-1459262838948-3e2de6c1ec80 560w, https://res.cloudinary.com/kentcdodds-com/image/upload/w_840,q_auto,f_auto,b_rgb:e6e9ee/unsplash/photo-1459262838948-3e2de6c1ec80 840w, https://res.cloudinary.com/kentcdodds-com/image/upload/w_1100,q_auto,f_auto,b_rgb:e6e9ee/unsplash/photo-1459262838948-3e2de6c1ec80 1100w, https://res.cloudinary.com/kentcdodds-com/image/upload/w_1650,q_auto,f_auto,b_rgb:e6e9ee/unsplash/photo-1459262838948-3e2de6c1ec80 1650w, https://res.cloudinary.com/kentcdodds-com/image/upload/w_2500,q_auto,f_auto,b_rgb:e6e9ee/unsplash/photo-1459262838948-3e2de6c1ec80 2500w, https://res.cloudinary.com/kentcdodds-com/image/upload/w_2100,q_auto,f_auto,b_rgb:e6e9ee/unsplash/photo-1459262838948-3e2de6c1ec80 2100w, https://res.cloudinary.com/kentcdodds-com/image/upload/w_3100,q_auto,f_auto,b_rgb:e6e9ee/unsplash/photo-1459262838948-3e2de6c1ec80 3100w " sizes=" (max-width:1023px) 80vw, (min-width:1024px) and (max-width:1620px) 67vw, 1100px " />
<meta name="twitter:image" content=" https://res.cloudinary.com/kentcdodds-com/image/upload /$th_1256,$tw_2400,$gw_$tw_div_24,$gh_$th_div_12 /co_rgb:a9adc1,c_fit,g_north_west,w_$gw_mul_14,h_$gh,x_$gw_mul_1.5,y_$gh_mul_1.3,l_text:kentcdodds.com:Matter-Regular.woff2_50:Checkout%2520this%2520article /co_white,c_fit,g_north_west,w_$gw_mul_13.5,h_$gh_mul_7,x_$gw_mul_1.5,y_$gh_mul_2.3,l_text:kentcdodds.com:Matter-Regular.woff2_110:Don't%2520Solve%2520Problems%252C%2520Eliminate%2520Them /c_fit,g_north_west,r_max,w_$gw_mul_4,h_$gh_mul_3,x_$gw,y_$gh_mul_8,l_kent:profile-transparent /co_rgb:a9adc1,c_fit,g_north_west,w_$gw_mul_5.5,h_$gh_mul_4,x_$gw_mul_4.5,y_$gh_mul_9,l_text:kentcdodds.com:Matter-Regular.woff2_70:Kent%20C.%20Dodds /co_rgb:a9adc1,c_fit,g_north_west,w_$gw_mul_9,x_$gw_mul_4.5,y_$gh_mul_9.8,l_text:kentcdodds.com:Matter-Regular.woff2_40:kentcdodds.com%252Fblog /c_fill,ar_3:4,r_12,g_east,h_$gh_mul_10,x_$gw,l_unsplash:photo-1459262838948-3e2de6c1ec80 /c_fill,w_$tw,h_$th/kentcdodds.com/social-background.png " />
=
+
-- prisma migrate dev --name user_roles -- CreateEnum CREATE TYPE "Role" AS ENUM ('ADMIN', 'MEMBER'); -- AlterTable ALTER TABLE "User" ADD COLUMN "role" "Role" DEFAULT E'MEMBER'; -- Manually written stuff: -- Update all users to be members: update "User" set role = E'MEMBER'; -- update me@kentcdodds.com to be ADMIN: update "User" set role = E'ADMIN' where email = 'me@kentcdodds.com'; -- make role required ALTER TABLE "User" ALTER COLUMN "role" SET NOT NULL;
-- prisma migrate dev --name init -- CreateEnum CREATE TYPE "Team" AS ENUM ('BLUE', 'RED', 'YELLOW'); -- CreateTable CREATE TABLE "User" ( "id" TEXT NOT NULL, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, "email" TEXT NOT NULL, "firstName" TEXT NOT NULL, "team" "Team" NOT NULL, PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "Session" ( "id" TEXT NOT NULL, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "userId" TEXT NOT NULL, "expirationDate" TIMESTAMP(3) NOT NULL, PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "Call" ( "id" TEXT NOT NULL, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, "title" TEXT NOT NULL, "description" TEXT NOT NULL, "userId" TEXT NOT NULL, "base64" TEXT NOT NULL, PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "PostRead" ( "id" TEXT NOT NULL, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "userId" TEXT NOT NULL, "postSlug" TEXT NOT NULL, PRIMARY KEY ("id") ); -- CreateIndex CREATE UNIQUE INDEX "User.email_unique" ON "User"("email"); -- AddForeignKey ALTER TABLE "Session" ADD FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "Call" ADD FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "PostRead" ADD FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- prisma migrate dev --name call_kent_episode_id -- AlterTable ALTER TABLE "Call" ADD COLUMN "episodeId" TEXT;
-- prisma migrate dev --name remove_episode_id_and_add_keywords /* Warnings: - You are about to drop the column `episodeId` on the `Call` table. All the data in the column will be lost. */ -- AlterTable ALTER TABLE "Call" DROP COLUMN "episodeId", ADD COLUMN "keywords" TEXT DEFAULT E'', -- first autofill all keywords with an empty string ALTER COLUMN "keywords" SET NOT NULL; -- then set it to not null
// source
const users = await prisma.user.findMany({
select: {
id: true,
email: true,
firstName: true,
},
})
// types
const users: Array<{
id: string
email: string
firstName: string
}>
// source
const users = await prisma.user.findMany({
select: {
id: true,
email: true,
firstName: true,
team: true, // <-- new field
},
})
// types
const users: Array<{
id: string
email: string
firstName: string
team: Team // <-- field appears!
}>
// source
const users = await prisma.user.findMany({
select: {
id: true,
email: true,
firstName: true,
team: true,
postReads: {
select: {
postSlug: true,
},
},
},
})
// types
const users: Array<{
id: string
email: string
firstName: string
team: Team
postReads: Array<{
postSlug: string
}>
}>
You also get:
distinct, include, skip, take, orderBy, where, etc.
All with autocomplete/TypeSafety 🤯