DT
Drizzle Teamโ€ข14mo 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โ€ข14mo 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โ€ข14mo ago
I can't think of anything other than noUndeckedIndexedAccess. Other than that, type casting but it's just as silly
francis
francisOPโ€ข14mo 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โ€ข14mo 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โ€ข14mo 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โ€ข14mo 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โ€ข14mo 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โ€ข14mo ago
justOneOrThrow
francis
francisOPโ€ข14mo 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โ€ข14mo ago
I wrote the eslint plugin to avoid that
francis
francisOPโ€ข14mo 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โ€ข14mo ago
I would like to make it so that it would only work if it is a drizzle object
francis
francisOPโ€ข14mo 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โ€ข14mo ago
For now we released it like that, it is not an easy one exactly
francis
francisOPโ€ข14mo 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โ€ข14mo ago
Yeah, you just have to be consistent
francis
francisOPโ€ข14mo 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โ€ข14mo ago
The other option that I thought was to provide a list of names on which NOT to work on
francis
francisOPโ€ข14mo 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โ€ข14mo ago
Just put rlsDrizzle in the list
francis
francisOPโ€ข14mo 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โ€ข14mo ago
Interesting... I think every js runtime treats proxies differently
francis
francisOPโ€ข14mo 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โ€ข14mo 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โ€ข14mo 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โ€ข14mo ago
I will look later - thank you!
Angelelz
Angelelzโ€ข14mo 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โ€ข14mo 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โ€ข14mo ago
busy dude
francis
francisOPโ€ข14mo 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โ€ข14mo 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

Did you find this page helpful?