Proposal for Extending schema.ts
in Drizzle ORM with Runtime Middlewares
#1513
Replies: 14 comments 19 replies
-
Thank you for taking the time to write this. The following opinions are my own, and mine only. I personally think that the core drizzle-orm package should stay dependency-free. That is one of the selling point that brought me to it. I think the approach we should follow is improve the experience of the plug-ins we have. |
Beta Was this translation helpful? Give feedback.
-
I agree with you about keeping drizzle dependency-free, and I think that it can still be achieved with the API example I provided. What I miss from |
Beta Was this translation helpful? Give feedback.
-
Your clarification just changed my mind! I think this is a great idea!
Having said this, I think we should convert this discussion to a feature request. I think it is a nice idea. |
Beta Was this translation helpful? Give feedback.
-
Hello again, After further reflection on the proposed enhancements for Drizzle ORM, I've developed an additional perspective that might further expand our capabilities: replacing the This concept of middleware aligns more closely with the pattern seen in many modern web frameworks. It opens up a plethora of possibilities, going beyond mere validation. Here's how it could look: export const mySchemaUsers = mySchema.table('users', {
id: serial('id').primaryKey().use(myIdMiddleware),
name: text('name').use(myNameMiddleware),
}); Expanded Capabilities with Middleware:
By conceptualizing these functions as middleware, we not only retain the initial idea of enhanced validation but also open the door to a more powerful and flexible data-handling paradigm within Drizzle ORM. I'm curious to know what the community and maintainers think about this expanded idea. Does this middleware approach align with the goals and architecture of Drizzle ORM? Looking forward to hearing your thoughts and suggestions! |
Beta Was this translation helpful? Give feedback.
-
The functionality that developers can extend their drizzle client like prisma client extension. Scenarios that I have faced:
Sorry for spamming. |
Beta Was this translation helpful? Give feedback.
-
I've been thinking about this. There is a lot to consider. I think the middleware should be at the table level only to avoid any race condition/complications around the order of operations. To be able to make it useful enough, const usersMiddleware = {
beforeSelect: ({table: CurrentTable, selectedFields: SelectFields, where: SQL, logger: CustomLogger}) => ({ SelectedFields, where }),
afterSelect: ({result: DependsOnTheSelect[], logger: CustomLogger }) => CustomReturn,
beforeInsert: ({ table: CurrentTable, insertedFields: Partial<InsertedFields>, logger: CustomLogger }) => ({ insertedFields, returnedObj }),
afterInsert: ({result: ResultTypeFromTheDriver, returnedObject: WhateverTheUserWants, logger: CustomLogger}) => unknown,
beforeDelete: ({table: CurrentTable, where: SQL, logger: CustomLoger}) => boolean | ((db: Database) => unknown),
afterDelete: ({result: ResultTypeFromTheDriver, logger: CustomLogger}) => unknown,
beforeUpdate: ({ table: CurrentTable, insertedFields: InsertedFields, logger: CustomLogger }) => InsertedFields,
afterUpdate: ({result: ResultTypeFromTheDriver, logger: CustomLogger}) => unknown,
} Not sure if logger is a good fit here, or the user should have it in the scope. As you might see, the names are very descriptive and make the intent very easy to guess. This avoid the if-else nightmare of an event-like api like Here is an annotated example of what this could allow: const users = pgTable('users', {
id: serial('id').primaryKey(),
tenantId: bigint('tenant_id').notNull(),
name: varchar('name', { length: 150 }).notNull(),
password: varchar('password', { length: 300 }).notNull(),
lastName: varchar('last_name', { length: 150 }).notNull(),
fullName: varchar('full_name', { length: 300 }).notNull(),
createdAt: timestamp('created_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at').notNull().defaultNow(),
deletedAt: timestamp('deleted_at'),
randomId: varchar('random_id').notNull()
}).$use({
beforeSelect({table, where, selectedFields }) {
// Here we make sure that if the query is db.select().from(users), the password won't come out.
// But if we do db.select({ password: users.password }).from(users), it would work.
if (!selectedFields) {
const { password, rest: selectedFields } = getTableColumns(table);
}
// You could implement multi tenant applications like this
return { selectedFields, where: and(where, eq(table.tenantId, getTenantId()), isNull(table.deletedAt)) };
},
beforeInsert({table, insertedFields, logger}) {
// Here you can modify the data, validate. The sky is the limit.
if (Array.isArray(insertedFields) {
insertedFields = insertedFields.map((field) => userSchema.parse({
...field,
randomId: generateRandomId(),
fullName: `${field.name} ${field.lastName}`
})
} else {
insertedFields.randomId = generateRandomId();
insertedFields.fullName = `${insertedFields.name} ${insertedFields.lastName}`
insertedFields = userSchema.parse(insertedFields);
}
// Here we can define a custom object to return after the insert:
return {
insertedFields,
generatedRandomId: Array.isArray(insertedFields) ? insertedFields.map(field => field.randomId) : insertedFields.randomId
}
},
afterInsert({ result, returnedObject, logger }) {
logger("inserted random id(s): ", returnedObject.generatedRandomId)
return { result, randomId: returnedObject.generatedRandomId }
},
beforeDelete({table, where }) {
// Here we can do soft deletes:
return async (db) => await db.update(users).set({ deletedAt: new Date() }).where(and(where, eq(table.tenantId, getTenantId()))
}
} This are just the examples I came up with and that I see people asking about. |
Beta Was this translation helpful? Give feedback.
-
Middleware is a must have in my opinion. It's actually preventing me from migrating a current project to drizzle. I think Sequelize offers a strong solution. Some important aspects for me:
|
Beta Was this translation helpful? Give feedback.
-
For anyone needing to plug into Drizzle's lifecycle, check this gist. It's not perfect but it's a good workaround until this is officially supported. |
Beta Was this translation helpful? Give feedback.
-
Is there an implementation plan? I really need this feature. |
Beta Was this translation helpful? Give feedback.
-
We currently wrap drizzle in a base repository https://gist.github.com/cayter/49d5c256a885d90c399ca6c1eca19f51 which it will trigger the before and after hooks. Note that we only made it working with postgres. |
Beta Was this translation helpful? Give feedback.
-
No update 😕 |
Beta Was this translation helpful? Give feedback.
-
I like that Drizzle takes its time for this, because it's very easy to get this one wrong. On Django, we have the same issue: the core team is slower to add things, particularly when they're not really core. Which is a good thing. But in Django there is also a great ecosystem of third party packages, in particular, middlewares. Then the Django core team eventually decides to integrate these packages into Django based on how successful they are. The sad thing though, is that THIS feature WOULD allows a package ecosystem to thrive ! haha! So it's a bit like a chicken and egg problem. WE CAN implement a middleware/proxy pattern on top of the existing DSL, but we're missing the primary hooks to do so. I tried to think about what's the best way to hook into this and I ended up with the same thing as the solution above: https://gist.github.com/ikupenov/10bc89d92d92eaba8cc5569013e04069 so I invite you to look into it. So the drizzle team could give us functions built on top of this proxy pattern and "let us play with it". Because right now we're all building our own proxy system, which doesn't really feel right to me (it's also a lot of nasty work and type checks) If these hooks are built properly, this might foster an ecosystem as big as Vite on the DB side of things (BTW, the api could be very similar to what Vite offers, this would facilitate adoption). I think a good start is to JUST give the function that's called AFTER the proxy, and the function simply passes exactly the context in which drizzle was called (all the db.insert. etc, etc... plus their arguments). No after_save, no after_update, just something very low level that could allow these higher level hooks to be built, this way it's easier to get it right for the Drizzle team. Implementing this is safer than coming up with the perfect "vite-like" api. Which is hard to get right. |
Beta Was this translation helpful? Give feedback.
-
There are some improvements in the proxy solution. It’s now a lot more flexible and hooks can easily be added. Bear in mind that if you go with this approach you’ll need to maintain it and there are some breaking changes coming with V1. Shouldn’t be too bad but worth keeping in mind. That’s the updated gist: https://gist.github.com/ikupenov/26f3775821c05f17b6f8b7a037fb2c7a. |
Beta Was this translation helpful? Give feedback.
-
Is there any way i can implement live query feature with sqlite using drizzle or maybe just get table name passed to the SQL query in drizzle so that I can trigger render when changes are made to that tables and queries associated with that table |
Beta Was this translation helpful? Give feedback.
-
Hello Drizzle Maintainers and Community,
I am excited to propose an enhancement to the
schema.ts
in Drizzle ORM, aiming to integrate runtime validations using libraries like Zod or Joi. This addition is poised to bring a new level of data integrity and flexibility to Drizzle ORM.Currently,
schema.ts
is structured as follows:While effective in defining the table structure, it lacks capabilities for in-depth data validation. Here's how I envision enhancing this:
Column-Level Validation:
This approach allows each column to have bespoke validation logic, offering precise control over the constraints of each field.
Example:
Table-Level Validation:
This holistic approach enables validation of the entire data structure, accommodating complex inter-field validations and business rules.
Example:
Advantages of This Approach:
Enhanced Validation Beyond DB Capabilities: This feature surpasses the typical database-level validations, enabling more sophisticated checks like string lengths, array validations, and other complex data integrity checks not typically available at the database level.
Robust Defaults: With libraries like Zod, we can define strong defaults, such as using nanoid, uuid, or even namespaced IDs (e.g.,
post_abcd12345
), providing more flexibility and robustness in data handling.Shared Validation Logic with Frontend: The proposed validation functions can be exported and reused in the frontend. This consistency ensures that both backend and frontend validations are aligned, reducing redundancy and enhancing overall application integrity.
I believe these enhancements will significantly benefit Drizzle ORM users by offering more powerful and flexible validation mechanisms, ultimately leading to more robust and reliable applications.
I'm eager to hear the community's thoughts on this proposal and am ready to actively contribute towards the implementation of this feature. Your feedback will be invaluable in shaping this potential addition to Drizzle ORM.
Thank you for considering this suggestion, and I look forward to a fruitful discussion.
Beta Was this translation helpful? Give feedback.
All reactions