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(
|
const workflow = readFileSync(
|
||||||
resolve(process.cwd(), ".gitea/workflows/ci.yaml"),
|
resolve(process.cwd(), ".gitea/workflows/ci.yaml"),
|
||||||
"utf8",
|
"utf8",
|
||||||
);
|
).replace(/\r\n/g, "\n");
|
||||||
|
|
||||||
it("runs check then production deploy on push to main", () => {
|
it("runs check then production deploy on push to main", () => {
|
||||||
expect(workflow).toContain("pnpm biome:lint");
|
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,
|
rel: any,
|
||||||
opts: any,
|
opts: any,
|
||||||
keyValues: unknown[],
|
keyValues: unknown[],
|
||||||
|
mode: "one" | "many",
|
||||||
): Promise<Map<string, any>> {
|
): Promise<Map<string, any>> {
|
||||||
const join = resolveJoin(table, rel);
|
const join = resolveJoin(table, rel);
|
||||||
const byCol = join.fkCols[0];
|
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 sel = projection(rel.referencedTable, opts.select);
|
||||||
const orderBy = buildOrderBy(rel.referencedTable, opts.orderBy);
|
const orderBy = buildOrderBy(rel.referencedTable, opts.orderBy);
|
||||||
|
|
||||||
let stmt: any = sel
|
let stmt: any = sel
|
||||||
? client.select({ ...sel, __fk: byCol }).from(rel.referencedTable)
|
? client.select({ ...sel, __fk: byCol }).from(rel.referencedTable)
|
||||||
: client.select().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);
|
if (orderBy.length) stmt = stmt.orderBy(...orderBy);
|
||||||
|
|
||||||
const rows = (await stmt) as any[];
|
const rows = (await stmt) as any[];
|
||||||
const map = new Map<string, any>();
|
const map = new Map<string, any>();
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const key = row.__fk ?? row[colKey(byCol)];
|
const key = String(row.__fk ?? row[colKey(byCol)]);
|
||||||
map.set(String(key), sel ? pickRow(row, opts.select) : row);
|
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;
|
return map;
|
||||||
}
|
}
|
||||||
@@ -386,13 +398,25 @@ async function attachIncludes(
|
|||||||
for (const row of rows) row[rel.fieldName] = null;
|
for (const row of rows) row[rel.fieldName] = null;
|
||||||
continue;
|
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 join = resolveJoin(table, rel);
|
||||||
const refCol = join.refCols[0];
|
const refCol = join.refCols[0];
|
||||||
|
const related: any[] = [];
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const refValue = row[colKey(refCol)];
|
const refValue = row[colKey(refCol)];
|
||||||
row[rel.fieldName] =
|
const hit = refValue == null ? null : (map.get(String(refValue)) ?? null);
|
||||||
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] = [];
|
for (const row of rows) row[rel.fieldName] = [];
|
||||||
continue;
|
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 join = resolveJoin(table, rel);
|
||||||
const refCol = join.refCols[0];
|
const refCol = join.refCols[0];
|
||||||
|
const related: any[] = [];
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const refValue = row[colKey(refCol)];
|
const refValue = row[colKey(refCol)];
|
||||||
row[rel.fieldName] =
|
const list = refValue == null ? [] : (map.get(String(refValue)) ?? []);
|
||||||
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