1
32import { afterEach, describe, expect, it } from "vitest";
33import { z } from "zod";
34import { ActorContext } from "../../../common/actor-context.js";
35import type { AuthContext } from "../../../common/types.js";
36import { CacheService } from "../../../common/cache.js";
37import { setPermissionCache } from "../../../lib/authorize.js";
38import { PrismaClient } from "@prisma/client";
39import type { PrismaService } from "../../../common/prisma.service.js";
40import { DatabaseQueryService } from "../query.service.js";
41
42const WORKSPACE_ID = "ws_reco";
43const USER_ID = "user_reco";
44const DB_ID = "db_reco";
45const RECORD_ID = "rec_reco";
46const PAGE_ID = "pg_reco";
47
48
51function countingPrisma(opts: { withLinks: boolean }) {
52 const calls: string[] = [];
53 const targetDb = { id: "db_target", title: "Linked DB" };
54 const targetRecord = { id: "rec_target", page: { title: "Linked Record" } };
55 const prisma = {
56
57
58
59
60
61
62
63
64 read(): PrismaClient {
65 return prisma as unknown as PrismaClient;
66 },
67 write(): PrismaClient {
68 return prisma as unknown as PrismaClient;
69 },
70
71
72
73
74
75 relation: {
76 async findMany(): Promise<unknown[]> {
77 calls.push("relation.findMany");
78 return [];
79 }
80 },
81 workspaceMember: {
82 async findUnique(): Promise<unknown> {
83 calls.push("workspaceMember.findUnique");
84 return { workspaceId: WORKSPACE_ID, userId: USER_ID, role: "MEMBER" };
85 }
86 },
87 teamMember: {
88 async findMany(): Promise<unknown[]> {
89 calls.push("teamMember.findMany");
90 return [];
91 }
92 },
93 permission: {
94 async findMany(): Promise<unknown[]> {
95 calls.push("permission.findMany");
96 return [];
97 }
98 },
99 databaseProperty: {
100 async findMany(): Promise<unknown[]> {
101 calls.push("databaseProperty.findMany");
102 return [
103 { id: "prop_title", databaseId: DB_ID, name: "Name", type: "TITLE", position: "a0", isTitle: true, isVisible: true, required: false, config: {}, default: null, deletedAt: null, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() },
104 { id: "prop_text", databaseId: DB_ID, name: "Notes", type: "TEXT", position: "a1", isTitle: false, isVisible: true, required: false, config: {}, default: null, deletedAt: null, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }
105 ];
106 }
107 },
108 databaseRecord: {
109 async findUnique(): Promise<unknown> {
110 calls.push("databaseRecord.findUnique");
111
112
113 return {
114 id: RECORD_ID,
115 workspaceId: WORKSPACE_ID,
116 databaseId: DB_ID,
117 pageId: PAGE_ID,
118 icon: null,
119 coverUrl: null,
120 archivedAt: null,
121 version: 3,
122 position: "a5",
123 createdTime: new Date(),
124 lastEditedTime: new Date(),
125 createdBy: USER_ID,
126 lastEditedBy: USER_ID,
127 createdAt: new Date(),
128 page: { title: "A 50-block record", icon: null },
129 properties: [{ propertyId: "prop_text", value: { text: "body", marks: [] } }]
130 };
131 },
132 async findMany(): Promise<unknown[]> {
133 calls.push("databaseRecord.findMany");
134 return [targetRecord];
135 }
136 },
137 relationLink: {
138 async findMany(): Promise<unknown[]> {
139 calls.push("relationLink.findMany");
140 return opts.withLinks
141 ? [{ recordId: RECORD_ID, targetRecordId: targetRecord.id, targetDatabaseId: targetDb.id }]
142 : [];
143 }
144 },
145 database: {
146 async findMany(): Promise<unknown[]> {
147 calls.push("database.findMany");
148 return [targetDb];
149 }
150 }
151 };
152 return { prisma, calls };
153}
154
155
157function makeOffCache(): CacheService {
158 return new CacheService(undefined, undefined);
159}
160
161function makeService(prisma: unknown, cache: CacheService) {
162 return new DatabaseQueryService(
163 prisma as unknown as PrismaService,
164 { write: async (): Promise<void> => {}, audit: async (): Promise<void> => {} } as never,
165 { publish: async (): Promise<void> => {} } as never,
166 cache,
167 {} as never
168 );
169}
170
171const actor: AuthContext = { kind: "user", userId: USER_ID };
172
173afterEach(() => {
174 setPermissionCache(null);
175});
176
177describe("record-open query shape (TASK-0135, hot path #5)", () => {
178 it("issues ≤ 4 PG queries for a 50-block record page (no links, caches live)", async () => {
179
180
181 setPermissionCache(async () => ({ level: "EDIT", cached: true }));
182 const { prisma, calls } = countingPrisma({ withLinks: false });
183 const service = makeService(prisma, makeOffCache());
184
185 const dto = await ActorContext.run(actor, () => service.getRecord(RECORD_ID, USER_ID));
186 expect(dto.id).toBe(RECORD_ID);
187 expect(dto.title).toBe("A 50-block record");
188
189 expect(calls).toEqual([
190 "databaseRecord.findUnique",
191 "databaseProperty.findMany",
192 "relationLink.findMany"
193 ]);
194
195 expect(calls.length).toBeLessThanOrEqual(4);
196
197 expect(calls.filter((c) => c === "databaseRecord.findUnique").length).toBe(1);
198 });
199
200 it("stays ≤ 5 with relation links (the target lookups are batched, not per-link)", async () => {
201 setPermissionCache(async () => ({ level: "EDIT", cached: true }));
202 const { prisma, calls } = countingPrisma({ withLinks: true });
203 const service = makeService(prisma, makeOffCache());
204
205 const dto = await ActorContext.run(actor, () => service.getRecord(RECORD_ID, USER_ID));
206 expect((dto as { relations?: Record<string, string[]> }).relations?.["Linked DB"]).toEqual(["Linked Record"]);
207
208 expect(calls.length).toBeLessThanOrEqual(5);
209 expect(calls.filter((c) => c === "relationLink.findMany").length).toBe(1);
210 expect(calls.filter((c) => c === "database.findMany").length).toBe(1);
211 expect(calls.filter((c) => c === "databaseRecord.findMany").length).toBe(1);
212 });
213
214 it("the un-cached permission path is the control (6 queries) — the cache is what bounds it", async () => {
215
216
217
218
219
220 const { prisma, calls } = countingPrisma({ withLinks: false });
221 const service = makeService(prisma, makeOffCache());
222 await ActorContext.run(actor, () => service.getRecord(RECORD_ID, USER_ID));
223 expect(calls).toHaveLength(6);
224 expect(calls.filter((c) => c === "databaseRecord.findUnique").length).toBe(1);
225 });
226});
227