DT
Drizzle Team•12mo ago
francis

Is there a way to explicitly type certain drizzle results as `Array<T | undefined>`?

For example, I am making a query which I know will sometimes return undefined, since I am accessing a row that may or may not be present. I'm not sure how to type this such that I have type safety through the rest of the code to ensure that I never access properties without undefined checks. I'm aware of Typescript noUncheckedIndexedAccess but that will change the behavior of all arrays everywhere in my codebase, which I do not want. I just want the ability to indicate that a certain Drizzle select result will sometimes have no items returned. e.g. const [maybeExistingEntity] = await drizzle.select(.....).limit(1) now strongly types maybeExistingEntity where I would like the type to be T | undefined.
31 Replies
francis
francisOP•12mo ago
my current solution is to wrap drizzle calls where this applies in:
export function maybeDrizzleResult<T>(input: T[]): Array<T | undefined> {
return input;
}
export function maybeDrizzleResult<T>(input: T[]): Array<T | undefined> {
return input;
}
But this seems silly. thoughts?
Angelelz
Angelelz•12mo ago
I can't think of anything other than noUndeckedIndexedAccess. Other than that, type casting but it's just as silly
francis
francisOP•12mo ago
yeah, that tsconfig option makes a lot of patterns fail, e.g. if a.length > 1 a[0].something... since it could be a sparse array, theoretically, the compiler can't prove that accessing the first element is safe using it isn't realistic, so I'll just use my funky function for now. Would be great to have a dot method on drizzle that just transforms the return type into undefined for these cases (or maybe just... have it be undefined all the time? since any query can fail to return results, after all) or actually, what would be very nice in drizzle, that won't break any existing queries, is to add a new function analogous to limit(1) that returns either the element or undefined. Like fetchFirst() or something and instead of returning a Result[] it can return Result | undefined
Angelelz
Angelelz•12mo ago
The problem with Array<T | undefined> is that now ALL the elements of the array are literally T or undefined, so even if you .map() you'll have to type cast it or something like this, which is just not correct
francis
francisOP•12mo ago
yeah I think this only makes sense for the case where you are accessing the array entries via index, which I only do for when I expect there to be exactly zero or one results oh hey, that's smart. Instead of const [existingEntity] = maybeDrizzleResult(...), I'll make my function do just that
Angelelz
Angelelz•12mo ago
Name your function justOne and just add a limit(1) to the query, access it and return T | undefined There you go
francis
francisOP•12mo ago
yes well, there are two potential use cases here sometimes you want to select the first entry, if present. Sometimes you want to select the first entry and assert that there is only one matching row
Angelelz
Angelelz•12mo ago
justOneOrThrow
francis
francisOP•12mo ago
I already have a utility function for assert that this update query modifies only one row because... I forgot a WHERE and overwrote my whole table, that was fun
export async function assertSingleUpdate(input: unknown[] | Promise<unknown[]>) {
assert((await input).length === 1, "Expected exactly one row to be updated");
}
export async function assertSingleUpdate(input: unknown[] | Promise<unknown[]>) {
assert((await input).length === 1, "Expected exactly one row to be updated");
}
Angelelz
Angelelz•12mo ago
I wrote the eslint plugin to avoid that
francis
francisOP•12mo ago
ah, I didn't realize there was a plugin now! excellent feature request: would be nice if your plugin also throw an error if you renamed your drizzle object to something else 🙂 not sure if that is possible e.g. if you switch from const db = drizzle(...) to const somethingElse = drizzle(...) and somethingElse is not in your drizzleObjectName, explode similarly for the variable names used in transactions (having never worked with eslint plugins, I don't know if that is possible)
Angelelz
Angelelz•12mo ago
I would like to make it so that it would only work if it is a drizzle object
francis
francisOP•12mo ago
sure, but how would you tell what is a drizzle object at parse time? you don't have access to the type information, I'm guessing idk
Angelelz
Angelelz•12mo ago
For now we released it like that, it is not an easy one exactly
francis
francisOP•12mo ago
yeah, I totally understand hence my fake solution of "if you specify an object list, we will enforce it on creation and on use" e.g. if you assign the output of drizzle() to something not in that list, explode, since the list must be wrong though... that won't work in most cases, actually. since I assign drizzle through multiple levels of constructors, etc import renames, etc
Angelelz
Angelelz•12mo ago
Yeah, you just have to be consistent
francis
francisOP•12mo ago
re: "work only if it is a drizzle object": would you be able to handle the case I have where for row level security I actually access all of my drizzle instances through a proxy object that wraps the transaction to set transaction variables for user id and claims?
Angelelz
Angelelz•12mo ago
The other option that I thought was to provide a list of names on which NOT to work on
francis
francisOP•12mo ago
(I am not sure if there is a better way to do that now, when I spun up this project, there was no way to do that)
export function createRlsDrizzle(claimsFn: ClaimsFunction) {
return new Proxy<typeof rlsDb>(rlsDb, {
get(target, prop, receiver) {
if (prop === "transaction") {
return async (
first: DrizzleTransactionFunctionFirst,
...rest: DrizzleTransactionFunctionRest
) => {
const claims = claimsFn ? JSON.stringify(claimsFn() || {}) : "";
return target.transaction(async (tx) => {
await tx.execute(sql.raw(`SELECT set_config('request.jwt.claims', '${claims}', TRUE)`));
return first(tx);
}, ...rest);
};
}
return Reflect.get(target, prop, receiver);
},
});
}
export function createRlsDrizzle(claimsFn: ClaimsFunction) {
return new Proxy<typeof rlsDb>(rlsDb, {
get(target, prop, receiver) {
if (prop === "transaction") {
return async (
first: DrizzleTransactionFunctionFirst,
...rest: DrizzleTransactionFunctionRest
) => {
const claims = claimsFn ? JSON.stringify(claimsFn() || {}) : "";
return target.transaction(async (tx) => {
await tx.execute(sql.raw(`SELECT set_config('request.jwt.claims', '${claims}', TRUE)`));
return first(tx);
}, ...rest);
};
}
return Reflect.get(target, prop, receiver);
},
});
}
usage: request.locals.rlsDrizzle = createRlsDrizzle(...some claims function based on request user...) and then all database queries made in request handling are automatically limited to only that user's permissions in row level security. it's quite nice.
Angelelz
Angelelz•12mo ago
Just put rlsDrizzle in the list
francis
francisOP•12mo ago
ah yeah I was speaking more for your desired future of "detect drizzle instances and only error on those" since I'm not sure this will detect as a drizzle instance, as it is a Proxy
Angelelz
Angelelz•12mo ago
Interesting... I think every js runtime treats proxies differently
francis
francisOP•12mo ago
is there a better way now to do what I am trying here, to hook into transactions to set variables on each one? I couldn't find one a few months ago but you said there was more RLS support now, so I am hoping... 🙂
Angelelz
Angelelz•12mo ago
No, not now Actually, I would love to have your input in the issue I still have it as a draft PR because I would like some input from somebody using the feature
Angelelz
Angelelz•12mo ago
GitHub
[FEATURE]: Support PostgreSQL's Row Level Security (RLS) · Issue #5...
Describe want to want Supabase is really nicely using Row Level Secruity for granular authorization rules. 🔗 Here's the link to their docs: https://supabase.com/docs/guides/auth/row-level-secur...
francis
francisOP•12mo ago
I will look later - thank you!
Angelelz
Angelelz•12mo ago
I just thought of another way:
const justOne = <T extends any[]>(val: T) => val[0] as T[number] | undefined
db.select(...).from(...).limit(1).then(justOne)
const justOne = <T extends any[]>(val: T) => val[0] as T[number] | undefined
db.select(...).from(...).limit(1).then(justOne)
francis
francisOP•12mo ago
(I haven't forgotten about this btw - have been v busy with work but will get to it. the tab is open!)
Angelelz
Angelelz•12mo ago
busy dude
francis
francisOP•12mo ago
@Angelelz (not urgent) added some feedback on https://github.com/drizzle-team/drizzle-orm/issues/594 - my only real concern is the ability to bypass RLS by accident, where really it should fail loudly if you attempt to use an RLS client without the proper creds
GitHub
[FEATURE]: Support PostgreSQL's Row Level Security (RLS) · Issue #5...
Describe want to want Supabase is really nicely using Row Level Secruity for granular authorization rules. 🔗 Here's the link to their docs: https://supabase.com/docs/guides/auth/row-level-secur...
francis
francisOP•12mo ago
I know that this feature isn't so much an RLS client as it is RLS claims support for transactions, but it would be great to have an RLS client as well. That way, we could pass the client object around safely and be confident that the consumers of it can't bypass RLS by mistake
Want results from more Discord servers?
Add your server