fix(db): accumulate many-includes and nest relations in prisma facade
Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
e5ff7ec9e5
commit
20b85381fe
3 files changed
+97
-11
No files matched your search
@@ -6,7 +6,7 @@ describe("CI workflow", () => {
|
||||
const workflow = readFileSync(
|
||||
resolve(process.cwd(), ".gitea/workflows/ci.yaml"),
|
||||
"utf8",
|
||||
);
|
||||
).replace(/\r\n/g, "\n");
|
||||
|
||||
it("runs check then production deploy on push to main", () => {
|
||||
expect(workflow).toContain("pnpm biome:lint");
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
/**
|
||||
* Mirrors the many-relation grouping fix in prisma-facade queryRelationRows.
|
||||
* Keeps the contract local so we don't export private helpers.
|
||||
*/
|
||||
function groupRelationRows(
|
||||
rows: Array<{ __fk: string; id: number }>,
|
||||
mode: "one" | "many",
|
||||
): Map<string, unknown> {
|
||||
const map = new Map<string, unknown>();
|
||||
for (const row of rows) {
|
||||
const key = String(row.__fk);
|
||||
const cleaned = { id: row.id };
|
||||
if (mode === "many") {
|
||||
const list = map.get(key);
|
||||
if (Array.isArray(list)) list.push(cleaned);
|
||||
else map.set(key, [cleaned]);
|
||||
} else if (!map.has(key)) {
|
||||
map.set(key, cleaned);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
describe("prisma-facade relation grouping", () => {
|
||||
it("accumulates many rows per parent key", () => {
|
||||
const map = groupRelationRows(
|
||||
[
|
||||
{ __fk: "1", id: 10 },
|
||||
{ __fk: "1", id: 11 },
|
||||
{ __fk: "2", id: 20 },
|
||||
],
|
||||
"many",
|
||||
);
|
||||
expect(map.get("1")).toEqual([{ id: 10 }, { id: 11 }]);
|
||||
expect(map.get("2")).toEqual([{ id: 20 }]);
|
||||
});
|
||||
|
||||
it("keeps a single row for one relations", () => {
|
||||
const map = groupRelationRows(
|
||||
[
|
||||
{ __fk: "1", id: 10 },
|
||||
{ __fk: "1", id: 11 },
|
||||
],
|
||||
"one",
|
||||
);
|
||||
expect(map.get("1")).toEqual({ id: 10 });
|
||||
});
|
||||
});
|
||||
+46
-10
@@ -270,24 +270,36 @@ async function queryRelationRows(
|
||||
rel: any,
|
||||
opts: any,
|
||||
keyValues: unknown[],
|
||||
mode: "one" | "many",
|
||||
): Promise<Map<string, any>> {
|
||||
const join = resolveJoin(table, rel);
|
||||
const byCol = join.fkCols[0];
|
||||
const condition = inArray(byCol, coerceArray(byCol, keyValues));
|
||||
const parts = [
|
||||
inArray(byCol, coerceArray(byCol, keyValues)),
|
||||
buildCondition(rel.referencedTable, opts.where),
|
||||
].filter(Boolean);
|
||||
const condition = parts.length === 1 ? parts[0] : and(...parts);
|
||||
const sel = projection(rel.referencedTable, opts.select);
|
||||
const orderBy = buildOrderBy(rel.referencedTable, opts.orderBy);
|
||||
|
||||
let stmt: any = sel
|
||||
? client.select({ ...sel, __fk: byCol }).from(rel.referencedTable)
|
||||
: client.select().from(rel.referencedTable);
|
||||
stmt = stmt.where(condition);
|
||||
if (condition) stmt = stmt.where(condition);
|
||||
if (orderBy.length) stmt = stmt.orderBy(...orderBy);
|
||||
|
||||
const rows = (await stmt) as any[];
|
||||
const map = new Map<string, any>();
|
||||
for (const row of rows) {
|
||||
const key = row.__fk ?? row[colKey(byCol)];
|
||||
map.set(String(key), sel ? pickRow(row, opts.select) : row);
|
||||
const key = String(row.__fk ?? row[colKey(byCol)]);
|
||||
const cleaned = sel ? pickRow(row, opts.select) : row;
|
||||
if (mode === "many") {
|
||||
const list = map.get(key);
|
||||
if (Array.isArray(list)) list.push(cleaned);
|
||||
else map.set(key, [cleaned]);
|
||||
} else if (!map.has(key)) {
|
||||
map.set(key, cleaned);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
@@ -386,13 +398,25 @@ async function attachIncludes(
|
||||
for (const row of rows) row[rel.fieldName] = null;
|
||||
continue;
|
||||
}
|
||||
const map = await queryRelationRows(client, table, rel, opts, values);
|
||||
const map = await queryRelationRows(
|
||||
client,
|
||||
table,
|
||||
rel,
|
||||
opts,
|
||||
values,
|
||||
"one",
|
||||
);
|
||||
const join = resolveJoin(table, rel);
|
||||
const refCol = join.refCols[0];
|
||||
const related: any[] = [];
|
||||
for (const row of rows) {
|
||||
const refValue = row[colKey(refCol)];
|
||||
row[rel.fieldName] =
|
||||
refValue == null ? null : (map.get(String(refValue)) ?? null);
|
||||
const hit = refValue == null ? null : (map.get(String(refValue)) ?? null);
|
||||
row[rel.fieldName] = hit;
|
||||
if (hit) related.push(hit);
|
||||
}
|
||||
if (opts.include && related.length > 0) {
|
||||
await attachIncludes(client, rel.referencedTable, related, opts.include);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,13 +426,25 @@ async function attachIncludes(
|
||||
for (const row of rows) row[rel.fieldName] = [];
|
||||
continue;
|
||||
}
|
||||
const map = await queryRelationRows(client, table, rel, opts, values);
|
||||
const map = await queryRelationRows(
|
||||
client,
|
||||
table,
|
||||
rel,
|
||||
opts,
|
||||
values,
|
||||
"many",
|
||||
);
|
||||
const join = resolveJoin(table, rel);
|
||||
const refCol = join.refCols[0];
|
||||
const related: any[] = [];
|
||||
for (const row of rows) {
|
||||
const refValue = row[colKey(refCol)];
|
||||
row[rel.fieldName] =
|
||||
refValue == null ? [] : (map.get(String(refValue)) ?? []);
|
||||
const list = refValue == null ? [] : (map.get(String(refValue)) ?? []);
|
||||
row[rel.fieldName] = list;
|
||||
for (const item of list) related.push(item);
|
||||
}
|
||||
if (opts.include && related.length > 0) {
|
||||
await attachIncludes(client, rel.referencedTable, related, opts.include);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user