-
Replying to https://twitter.com/flybayer/status/1558105078787198977 I do not understand how this structure will uphold the end-to-end type safety provided by Prisma for free. { post
{ followers
{ profileImage
// etc. If you have a view that requires this, Prisma will give you the types for free. Of course, you can then transform, or even type cast the data as you like, but based on existing types. The only other alternative to re-using Prisma types is using GraphQL auto-generated types, as GraphQL solves a similar problem, just one layer above (keeping in mind that Prisma was once called GraphCool and built GraphQL SaaS). Of course, you could define your own types manually, but doing this for every single view is a pain. Especially when actually using a Backend-For-Frontend-Approach, e.g., using RPC like Blitz.js or loaders like Remix, For everything above that level, a separate GraphQL-API is the fare more mature solution, wouldn't use Blitz.js for that though. Maybe call a type-safe GraphQL API via Blitz in the resolvers, separating the frontend from the API. For the bullet points:
→ This is currently happening at the resolver/input level anyway. As soon as data enters my backend functions, I want them to be validated and type-safely parsed, using something like Zod.
Yes, true enough. We just use a constant
Validation: See above. Type safety: This should be definitely solved on the Prisma level, and is tracked in this issue: prisma/prisma#3219. We work around that as described here: prisma/prisma#3219 (comment). But it's admittedly a bit ugly. But you know what's even uglier? Rebuilding nested type-safe data structures from scratch, i.e. doing what Prisma does for you...
I don't like the term "easy", when talking about swapping the data backbone of your app. But I think it's actually Prisma that makes this "easier", if any. Of course there should be an abstraction layer between Prisma and your app, but it should be very thin, re-using existing Prisma types and enhancing them, instead of building some TypeORM-like layer on top of Prisma... If one has really large-scale scalability and isolation in mind, one should directly resort to GraphQL, which allows you to re-use your stuff at multiple services. What's your take on that? |
Beta Was this translation helpful? Give feedback.
Replies: 4 comments 5 replies
-
Hey Nick! Firstly, I want to make it clear that current way to use Blitz will not change. Any advantage architecture stuff we bring will be an addition for people who want it.
If using Prisma types directly work great for you, by all means keep at it. Our Flightcontrol codebase has grown enough that we're feeling a lot of friction using the Prisma types everywhere. And that's what prompted me to add a domain layer between Prisma and our app logic. |
Beta Was this translation helpful? Give feedback.
-
Hey Brandon, thanks for the reply 💙 My post maybe came across a bit "defendish" regarding the Prisma types, but actually I see some of the same issues in our largish codebase (1 full-stack Blitz.js app > 100 resolvers, 1 remix app, 1 GraphQL-API, 4 asynchronous backend services…). I'd happily drop Prisma types as single source of truth and switch to some other "more sophisticated" architecture, if it convincingly solves these things. I just see some serious issues with your proposed model (at least for us), and was super curious, how you'd overcome them. Take a look at this view for example: UI data requirementsIgnoring for a moment the real requirements, let's just assume you'd need something like this as a data requirement for this view, using a GraphQL-ish syntax, to express nested requirements in connections/relations: {
getEnvironmentById(id: "7") {
id
stages {
id
name
region {
id
name
...
}
resources {
id
name
type
... on Database {
engine
maxStorage
...
}
... on App {
type
vCPU
...
}
}
}
project {
id
name
customIcon {
id
height
width
}
}
deployments {
id
createdAt
status
commit {
id
message
}
}
}
} Auto-generated partial & connected typesBoth Prisma and GraphQL are well known for enabling us to express such both partial (a selection of the fields of a table = SQL- Much more, with both we are able to auto-generate the types for such data. That's the current state, whether you use Prisma or GraphQL (or some other DSL) as the source of your types. Application LayerNow - following the new model - in the Application Layer, there's a resolver / query called That Application Layer feels very much like a Backend For Frontend, expressing, use-case-specific data requirements, where Blitz.js resolvers are defined. Let's see how we can translate that into data + types. Database LayerOur little resolver In the new model, though, we are supposed to use Repositories / Stores instead of calling Prisma directly. Let's do that. Luckily, there's an My question now is: What kind of data does this method return?Return only the primitive fields of → Following my data requirements, will I then have to call other stores like Return all or most of the data required? → Now what if another Return all data, but rename it → This means, the Store provides a method per use-case/view. This at least would solve all data requirements in a sane and efficient way, but it's no longer an abstraction at all, but just Domain Layer (Types)Let's assume we somehow solved the issues in the Database Layer. Now we're not supposed to re-use the types as returned by the DB-layer, but those we manually defined in our Domain Layer. We definitely will need a type // Only primitive types
interface Environment {
id: string;
createdAt: Date;
updatedAt: Date;
name: string;
description: string;
}
// Including relationships / connections
interface Environment {
id: string;
// ... etc.
stages: Stage[];
resources: Resource[];
}
type Resource = DatabaseResource | AppResource; // etc. No matter how we define it, our resolver type EnvironmentForShow = Pick<Environment, "id" | "createdAt" | "name"> & {
stages: Pick<Stage, "id" | "name"> & {
region: Pick<Region, "id" | "name">;
resources: Array<
Pick<
Resource,
"id" | "name" | "type" | "engine" | "maxStorage" | "vCPU"
> & {
config: Pick<ResourceConfiguration, "id" | "name">;
awsResources: Array<Pick<AWSResource, "id" | "createdAt">>;
}
>;
// etc.
};
// etc.
};
If you found ways to mitigate these issues, I'd be really curious to know about, as I'm always looking for superiour architectures to organize your code. If these issues just don't apply to Flightcontrol / the Blitz apps you run, please tell me, too. Thanks in advance 💙 |
Beta Was this translation helpful? Give feedback.
-
Wow, thanks for sharing a real life example. This shows how an architecture really works and helps me and others learn and make informed decisions. 👌
For the interested reader, here's a great article about this store pattern from a domain driven design perspective. It's using Sequelize instead of Prisma, but that's interchangeable. |
Beta Was this translation helpful? Give feedback.
-
Here's what I have for the store with prisma import { Project as PrismaProject } from "@prisma/client"
import { db, prismaUtils } from "application/libs/prisma"
import { Project } from "../entities/Project"
const mapper = {
toEntity(raw: PrismaProject) {
return Project.new(prismaUtils.clean(raw))
},
toPrisma(model: Project) {
return {
...model.data,
config: prismaUtils.jsonNullable(model.data.config),
}
},
}
export const makeProjectStore = (deps = { db }) => ({
async get({ id, organizationId }: { id: string; organizationId: string | undefined }) {
const raw = await deps.db.project.findFirst({
where: { id, organizationId },
})
return raw ? mapper.toEntity(raw) : null
},
async getIds({
ids,
organizationId,
}: {
ids: (string | null)[]
organizationId: string | undefined
}) {
const rawProjects = await deps.db.project.findMany({
where: { id: { in: ids.filter(Boolean) as any }, organizationId },
})
return rawProjects.map(mapper.toEntity)
},
async getByEnvironmentId({
environmentId,
organizationId,
}: {
environmentId: string
organizationId: string | undefined
}) {
const raw = await deps.db.project.findFirst({
where: { organizationId, environments: { some: { id: environmentId } } },
})
return raw ? mapper.toEntity(raw) : null
},
async getAllInOrg({ organizationId }: { organizationId: string }) {
const rawProjects = await deps.db.project.findMany({ where: { organizationId } })
return rawProjects.map(mapper.toEntity)
},
async save(project: Project) {
const saved = await deps.db.project.upsert({
where: { id: project.id },
create: mapper.toPrisma(project),
update: mapper.toPrisma(project),
})
project.replace(mapper.toEntity(saved).data)
},
async delete(project: Project) {
await deps.db.project.delete({ where: { id: project.id } })
},
})
export const ProjectStore = makeProjectStore() |
Beta Was this translation helpful? Give feedback.
Here's what I have for the store with prisma