edgedb-js
edgedb-js copied to clipboard
Allow better query composition with Typescript builder
The TS query builder is amazingly composable as long as you don't use the e.params function because you can't add that query to another, you can only execute the query with the client
here is an example from an app I am building
export const getTrackQuery = e.params(
{
trackId: e.uuid,
},
({ trackId }) =>
e.select(e.Track, () => ({
filter_single: { id: trackId },
})),
);
export const createRevisionQuery = e.params(
{ trackId: e.uuid, assetKey: e.str },
(params) => {
// You can't do this :(
const t = getTrackQuery({
trackId: params.trackId,
});
// can't even do this if you had a client :(
const t = getTrackQuery.run({} as Executor,{
trackId: params.trackId,
});
...
},
);
This really hurt my brain so I built a wrapper that allowed me to do this
export const currentUserQuery = query((e) => {
return e.global.current_user;
});
export const getTrackQuery = query.params((e, trackId: string) => {
return e.select(e.Track, () => ({
filter_single: { id: trackId },
}));
});
export const createRevisionQuery = query.params(
(e, params: { trackId: string; assetKey: string }) => {
const track = getTrackQuery(params.trackId);
const name = e.op(
e.str("Revision "),
"++",
e.cast(e.str, e.op(e.count(track.revisions), "+", 1)),
);
const asset = e.insert(e.Asset, {
key: params.assetKey,
});
return e.insert(e.Revision, {
created_by: currentUserQuery(),
track,
asset,
name,
});
},
);
which I can also then call like this
// with a client
await createRevisionQuery({ trackId, assetKey }, client);
// partial apply (this is how you compose them)
const runnable = createRevisionQuery({ trackId, assetKey })
// can use the runnable in another query or call run like all other queries
e.run(client)
this is implmented like this
import { Executor } from "gel";
import b, { $infer, TypeSet } from "./builder";
interface QueryBuilder {
<T extends TypeSet>(fn: (e: typeof b) => T): Query<T>;
}
interface Query<T extends TypeSet = TypeSet> {
(): T;
(client: Executor): Promise<$infer<T>>;
}
const _query = ((fn) => {
return (client) => {
if (!client) {
return fn(b);
}
// @ts-ignore
return fn(b).run(client);
};
}) as QueryBuilder;
interface ParamerterizedQueryBuilder {
<P, T extends TypeSet = TypeSet>(
fn: (e: typeof b, params: P) => T,
): ParamerterizedQuery<P, T>;
}
interface ParamerterizedQuery<P, T extends TypeSet> {
(params: P): T;
(params: P, client: Executor): Promise<$infer<T>>;
}
const _pQuery = ((fn) => {
return (params, client) => {
if (!client) {
return fn(b, params);
}
// @ts-ignore
return fn(b, params).run(client);
};
}) as ParamerterizedQueryBuilder;
export const query = Object.assign(_query, {
params: _pQuery,
});
Having a way to do this in the query builder itself would be amazing, the API doesn't need to be the same even just a sibling to run called build or something that just takes the second arg of run of a param query
Happy to help with implementation with this if its something that you think would be useful for everyone