1
19import { describe, expect, it } from "vitest";
20import { PrismaClient } from "@prisma/client";
21import type { PrismaService } from "../../../common/prisma.service.js";
22import { CacheService } from "../../../common/cache.js";
23import { setPermissionCache } from "../../../lib/authorize.js";
24import { ActorContext } from "../../../common/actor-context.js";
25import type { AuthContext } from "../../../common/types.js";
26import { optionIdForName } from "@factory/ai";
27import { DatabaseQueryService } from "../query.service.js";
28
29const WS = "ws_opt";
30const USER = "user_opt";
31const DB = "db_opt";
32const REC = "rec_opt";
33const STATUS_PROP = "prop_status";
34
35const actor: AuthContext = { kind: "user", userId: USER };
36
37
39function legacyPrisma() {
40 const calls: string[] = [];
41 const prisma = {
42 read(): PrismaClient {
43 return prisma as unknown as PrismaClient;
44 },
45 write(): PrismaClient {
46 return prisma as unknown as PrismaClient;
47 },
48 database: {
49 async findUnique(): Promise<unknown> {
50 calls.push("database.findUnique");
51 return { id: DB, workspaceId: WS, isArchived: false, pageId: "pg_opt", title: "Legacy AI DB" };
52 }
53 },
54 databaseProperty: {
55 async findMany(): Promise<unknown[]> {
56 calls.push("databaseProperty.findMany");
57 return [
58 { id: "prop_title", databaseId: DB, name: "Name", type: "TITLE", position: "a0", isTitle: true, config: {} },
59
60 {
61 id: STATUS_PROP,
62 databaseId: DB,
63 name: "Status",
64 type: "STATUS",
65 position: "a1",
66 isTitle: false,
67 config: { options: [{ name: "To Do", color: "default" }, { name: "Done", color: "green" }] }
68 }
69 ];
70 }
71 },
72 databaseRecord: {
73 async findMany(): Promise<unknown[]> {
74 calls.push("databaseRecord.findMany");
75 return [
76 {
77 id: REC,
78 workspaceId: WS,
79 databaseId: DB,
80 pageId: "pg_rec",
81 icon: null,
82 coverUrl: null,
83 archivedAt: null,
84 version: 1,
85 position: "a5",
86 createdTime: new Date(),
87 lastEditedTime: new Date(),
88 createdBy: USER,
89 lastEditedBy: USER,
90 createdAt: new Date(),
91 page: { title: "First task", icon: null },
92
93 properties: [{ propertyId: STATUS_PROP, value: { optionId: "To Do" } }]
94 }
95 ];
96 }
97 }
98 };
99 return { prisma, calls };
100}
101
102function makeService(prisma: unknown) {
103 return new DatabaseQueryService(
104 prisma as unknown as PrismaService,
105 { write: async (): Promise<void> => {}, audit: async (): Promise<void> => {} } as never,
106 { publish: async (): Promise<void> => {} } as never,
107 new CacheService(undefined, undefined),
108 {} as never
109 );
110}
111
112describe("option-id canonicalization (legacy AI DBs)", () => {
113 it("READ: resolves a name-stored option ref to the deterministic id in the DTO", async () => {
114 setPermissionCache(async () => ({ level: "EDIT", cached: true }));
115 const { prisma } = legacyPrisma();
116 const service = makeService(prisma);
117
118 const res = await ActorContext.run(actor, () =>
119 service.list(DB, USER, { limit: "50" })
120 );
121 const records = res.data as Array<{ properties: Record<string, { optionId?: string }> }>;
122 const status = records[0]?.properties[STATUS_PROP];
123
124 expect(status?.optionId).toBe(optionIdForName("To Do"));
125 expect(status?.optionId).not.toBe("To Do");
126 });
127
128 it("READ: leaves an already-canonical id ref unchanged", async () => {
129 setPermissionCache(async () => ({ level: "EDIT", cached: true }));
130 const id = optionIdForName("To Do");
131 const { prisma } = legacyPrisma();
132
133 const orig = prisma.databaseRecord.findMany.bind(prisma.databaseRecord);
134 prisma.databaseRecord.findMany = (async () => [
135 {
136 id: REC,
137 workspaceId: WS,
138 databaseId: DB,
139 pageId: "pg_rec",
140 icon: null,
141 coverUrl: null,
142 archivedAt: null,
143 version: 1,
144 position: "a5",
145 createdTime: new Date(),
146 lastEditedTime: new Date(),
147 createdBy: USER,
148 lastEditedBy: USER,
149 createdAt: new Date(),
150 page: { title: "First task", icon: null },
151 properties: [{ propertyId: STATUS_PROP, value: { optionId: id } }]
152 }
153 ]) as unknown as typeof orig;
154
155 const service = makeService(prisma);
156 const res = await ActorContext.run(actor, () => service.list(DB, USER, { limit: "50" }));
157 const records = res.data as Array<{ properties: Record<string, { optionId?: string }> }>;
158 expect(records[0]?.properties[STATUS_PROP]?.optionId).toBe(id);
159 });
160
161 it("READ: a name ref with NO matching option is preserved verbatim (no data loss)", async () => {
162 setPermissionCache(async () => ({ level: "EDIT", cached: true }));
163 const { prisma } = legacyPrisma();
164
165
166 prisma.databaseRecord.findMany = (async () => [
167 {
168 id: REC,
169 workspaceId: WS,
170 databaseId: DB,
171 pageId: "pg_rec",
172 icon: null,
173 coverUrl: null,
174 archivedAt: null,
175 version: 1,
176 position: "a5",
177 createdTime: new Date(),
178 lastEditedTime: new Date(),
179 createdBy: USER,
180 lastEditedBy: USER,
181 createdAt: new Date(),
182 page: { title: "First task", icon: null },
183 properties: [{ propertyId: STATUS_PROP, value: { optionId: "Renamed Later" } }]
184 }
185 ]) as unknown as typeof prisma.databaseRecord.findMany;
186
187 const service = makeService(prisma);
188 const res = await ActorContext.run(actor, () => service.list(DB, USER, { limit: "50" }));
189 const records = res.data as Array<{ properties: Record<string, { optionId?: string }> }>;
190 expect(records[0]?.properties[STATUS_PROP]?.optionId).toBe("Renamed Later");
191 });
192});
193