Custom SQL function (json_agg & json_build_object)

Rrphlmr4/1/2023
Hello there.

I'm new to Drizzle and I love it 😍.

I have successfully made a jsonAgg helper.

function jsonAgg<T extends PgTable<TableConfig>>(table: T) {
  return sql<InferModel<T>[]>`json_agg(${table})`;
}


Now I have trouble to make a jsonAggBuildObject 😅 that should produce something like this:

sql`json_agg(json_build_object('key1', ${table.col1}, 'key2', ${table.col2}, ...))`


I try something but it end with an error error: could not determine data type of parameter $1

function jsonAggBuildObject<T extends Record<string, AnyColumn>>(shape: T) {
  const shapeString = Object.entries(shape)
    .map(([key, value]) => {
      return `'${key}', ${value}`;
    })
    .join(",");

  return sql<
    InferColumnsDataTypes<T>[]
  >`json_agg(json_build_object(${shapeString}))`;
}


Any idea ?
Rrphlmr4/1/2023
It's used like that:

.select({
   ...other,
   themes: jsonAggBuildObject({
      id: themes.id,      
      label: themes.label,
    }),
 })
Rrphlmr4/1/2023
Note: if it doesn't optimize the underlying request (selecting only themes.id and themes.label, forget that, I'm fine with the first helper and some mapping 😄
Bbloberenober4/1/2023
Your function should return an object, not sql, because that's what .select() accepts. So you need to return key-value pairs, where the key is just to map the result object, and the value is what you want to select for that particular key.
Bbloberenober4/1/2023
No, wait, that's not correct
Bbloberenober4/1/2023
Could you show how you expect the resulting SQL to look like?
Rrphlmr4/1/2023
the resulting SQL looks like that:
json_agg(
    json_build_object(
      'id', "themes"."id",
      'label', "themes"."label"
    )
  ) AS "themes"
Rrphlmr4/1/2023
using this "manually" in the select works 😉 But I wanted to explore the "helper" way haha.

sql`json_agg(json_build_object('id', ${themes.id}, 'label', ${themes.label}))`
Bbloberenober4/1/2023
OK I see. In that case, you need to escape the column names with the name() function, which will tell the ORM that it needs to be treated as a column name instead of a parameter.
Bbloberenober4/1/2023
When you use name(), you don't need to manually add quotes to the query, it will be done by the ORM.
Bbloberenober4/1/2023
So something like sql`${name(key)}, ${value}`
Bbloberenober4/1/2023
Also, you always need to use sql when you're building the query.
Bbloberenober4/1/2023
You can compose sql parts inside other sql.
Bbloberenober4/1/2023
You might take a look at how we map the selection objects to the SQL ourselves: https://github.com/drizzle-team/drizzle-orm/blob/main/drizzle-orm/src/pg-core/dialect.ts#L121
Bbloberenober4/1/2023
But this is an interesting helper! We might explore the possibility to add it to the library.
Rrphlmr4/1/2023
yeah it's more "easy" than to reduce results later :p
Bbloberenober4/1/2023
btw, I've experimented a bit with this type of query and here's what I got that works (this example uses cities 1:m users):
SELECT
    c.id AS city_id,
    c.name,
    count(u.id) as users_count,
    coalesce(json_agg(
        json_build_array(u.id, u.name)
    ) filter (where u.id is not null and u.name is not null), '[]') AS users
FROM
    cities c
left JOIN
    users2 u ON c.id = u.city_id
GROUP BY
    c.id;


(just replace the json_build_array with json_build_object)
might help you a bit to build your helper 🙂

the joined count is for an edge case when you're joining nullable columns only and no right rows were joined for a certain left row - the result will have a single right row with all columns as nulls, but the joined count will be zero.
Bbloberenober4/1/2023
Also, check the filter clause in json_agg, it needs to have a check that one of non-nullable columns in the joined table is not null (for automated generation, it can just use a first non-nullable column from the joined table).
Rrphlmr4/1/2023
Thanks for your help!

It takes me 5h but it works 😂

// ⚠️ Potential for SQL injections, so you shouldn't allow user-specified key names
function jsonAggBuildObject<T extends Record<string, AnyColumn>>(shape: T) {
  const chunks: SQL[] = [];

  Object.entries(shape).forEach(([key, value]) => {
    if (chunks.length > 0) {
      chunks.push(sql.raw(`,`));
    }
    chunks.push(sql.raw(`'${key}',`));
    chunks.push(sql`${value}`);
  });

  return sql<
    InferColumnsDataTypes<T>[]
  >`json_agg(json_build_object(${sql.fromList(chunks)}))`;
}
Rrphlmr4/1/2023
don't tell me there was an easier way, please 🤣
Bbloberenober4/1/2023
1. You shouldn't use inlineParams here, it's only intended for static queries, you're introducing a SQL injection vulnerability. As I wrote earlier, you should use the name() function.
2. [] as SQL[] is an incorrect usage, it should be const chunks: SQL[] = []

Other then that, looks good
Rrphlmr4/1/2023
name() produces double " 😢
Bbloberenober4/1/2023
But you're escaping a column name, no?
Bbloberenober4/1/2023
It should be double quotes
Rrphlmr4/1/2023
json_build_object is a mapper to make a custom json from a group result.

db
      .select({
        timelineModel,
        postModel,
        eventModel,
        // here 👇
        themes: jsonAggBuildObject({
          test: themeModel.id,
          test2: themeModel.label,
        }),
      })
      .from(timelineModel)
      .leftJoin(postModel, eq(timelineModel.pub_id, postModel.pub_id))
      .leftJoin(eventModel, eq(timelineModel.pub_id, eventModel.pub_id))
      .leftJoin(
        themesInTimeline,
        eq(themesInTimeline.pub_id, timelineModel.pub_id)
      )
      .leftJoin(themeModel, eq(themeModel.id, themesInTimeline.theme_id))
      .where(inArray(timelineModel.club_id, clubsId))
      .groupBy(timelineModel.pub_id, postModel.pub_id, eventModel.pub_id)
      .orderBy(desc(timelineModel.created_at))
      .limit(20);
Rrphlmr4/1/2023
name() produces this sql : json_agg(json_build_object("test","themes_2"."id","test2","themes_2"."label"))

should be json_agg(json_build_object('test',"themes_2"."id",'test2',"themes_2"."label")) (single quotes for key name)
Rrphlmr4/1/2023
I know that's maybe overthinking 😅
Bbloberenober4/1/2023
ahh I see
then you can use sql.raw(`'${key}'`)
but you should be careful, since it still has potential for SQL injections, so you shouldn't allow user-specified key names
Rrphlmr4/1/2023
Thank you! I now have everything I need to continue my journey