mirror of
https://github.com/willchen96/mike.git
synced 2026-07-12 22:42:11 +02:00
Refactor ProjectPageParts and ProjectPageHeader components for improved loading states and skeleton UI. Update Modal and PageHeader components to support loading states. Enhance RenameableTitle for better caret positioning. Adjust DisplayWorkflowModal to utilize the new Modal component structure. Update WorkflowList to include loading indicators and improve sticky header behavior.
This commit is contained in:
parent
444d1d38e4
commit
1fa0554ea5
49 changed files with 3623 additions and 1587 deletions
|
|
@ -15,7 +15,11 @@ import {
|
|||
type ChatMessage,
|
||||
} from "../lib/chatTools";
|
||||
import { completeText } from "../lib/llm";
|
||||
import { getUserApiKeys, getUserModelSettings } from "../lib/userSettings";
|
||||
import {
|
||||
getLegalResearchUsEnabled,
|
||||
getUserApiKeys,
|
||||
getUserModelSettings,
|
||||
} from "../lib/userSettings";
|
||||
import { checkProjectAccess } from "../lib/access";
|
||||
import { safeErrorLog, safeErrorMessage } from "../lib/safeError";
|
||||
|
||||
|
|
@ -552,7 +556,14 @@ chatRouter.post("/", requireAuth, async (req, res) => {
|
|||
db,
|
||||
docIndex,
|
||||
);
|
||||
const apiMessages = buildMessages(enrichedMessages, docAvailability);
|
||||
const legalResearchUs = await getLegalResearchUsEnabled(userId, db);
|
||||
const apiMessages = buildMessages(
|
||||
enrichedMessages,
|
||||
docAvailability,
|
||||
undefined,
|
||||
undefined,
|
||||
legalResearchUs,
|
||||
);
|
||||
|
||||
const workflowStore = await buildWorkflowStore(userId, userEmail, db);
|
||||
|
||||
|
|
@ -588,6 +599,7 @@ chatRouter.post("/", requireAuth, async (req, res) => {
|
|||
db,
|
||||
write,
|
||||
workflowStore,
|
||||
includeResearchTools: legalResearchUs,
|
||||
model,
|
||||
apiKeys,
|
||||
signal: streamAbort.signal,
|
||||
|
|
|
|||
|
|
@ -364,7 +364,7 @@ documentsRouter.get("/:documentId/versions", requireAuth, async (req, res) => {
|
|||
const { data: rows } = await db
|
||||
.from("document_versions")
|
||||
.select(
|
||||
"id, version_number, source, created_at, filename, file_type, size_bytes, page_count",
|
||||
"id, version_number, source, created_at, filename, file_type, size_bytes, page_count, deleted_at, deleted_by",
|
||||
)
|
||||
.eq("document_id", documentId)
|
||||
.order("created_at", { ascending: true });
|
||||
|
|
@ -433,19 +433,12 @@ documentsRouter.post(
|
|||
});
|
||||
}
|
||||
|
||||
const targetActive = await loadActiveVersion(documentId, db);
|
||||
const targetType = targetActive?.file_type ?? "";
|
||||
const active = await loadActiveVersion(sourceDocumentId, db);
|
||||
if (!active)
|
||||
return void res
|
||||
.status(404)
|
||||
.json({ detail: "Source document has no active version." });
|
||||
const sourceType = active.file_type ?? "";
|
||||
if (targetType && sourceType && targetType !== sourceType) {
|
||||
return void res.status(400).json({
|
||||
detail: `Source document type (${sourceType}) does not match document type (${targetType}).`,
|
||||
});
|
||||
}
|
||||
|
||||
const bytes = await downloadFile(active.storage_path);
|
||||
if (!bytes)
|
||||
|
|
@ -603,8 +596,6 @@ documentsRouter.post(
|
|||
if (!access.ok)
|
||||
return void res.status(404).json({ detail: "Document not found" });
|
||||
|
||||
// Reject if the uploaded file's extension doesn't match the document's
|
||||
// declared type — otherwise every downstream viewer/extractor breaks.
|
||||
const suffix = file.originalname.includes(".")
|
||||
? file.originalname.split(".").pop()!.toLowerCase()
|
||||
: "";
|
||||
|
|
@ -614,14 +605,6 @@ documentsRouter.post(
|
|||
});
|
||||
}
|
||||
|
||||
const currentActive = await loadActiveVersion(documentId, db);
|
||||
const expectedType = currentActive?.file_type ?? "";
|
||||
if (expectedType && expectedType !== suffix) {
|
||||
return void res.status(400).json({
|
||||
detail: `Uploaded file type (${suffix}) does not match document type (${expectedType}).`,
|
||||
});
|
||||
}
|
||||
|
||||
// Peg the new version into a predictable /versions/:id path under the
|
||||
// existing document folder so ops can spot the history in storage.
|
||||
const versionSlug = crypto.randomUUID().replace(/-/g, "");
|
||||
|
|
@ -777,6 +760,7 @@ documentsRouter.patch(
|
|||
.update({ filename })
|
||||
.eq("id", versionId)
|
||||
.eq("document_id", documentId)
|
||||
.is("deleted_at", null)
|
||||
.select(
|
||||
"id, version_number, source, created_at, filename, file_type, size_bytes, page_count",
|
||||
)
|
||||
|
|
@ -788,6 +772,160 @@ documentsRouter.patch(
|
|||
},
|
||||
);
|
||||
|
||||
// PUT /single-documents/:documentId/versions/:versionId/file
|
||||
// Replace the file bytes and metadata for an existing version while keeping
|
||||
// its version number and id. This is destructive and owner-only.
|
||||
documentsRouter.put(
|
||||
"/:documentId/versions/:versionId/file",
|
||||
requireAuth,
|
||||
singleFileUpload("file"),
|
||||
async (req, res) => {
|
||||
const userId = res.locals.userId as string;
|
||||
const userEmail = res.locals.userEmail as string | undefined;
|
||||
const { documentId, versionId } = req.params;
|
||||
const db = createServerSupabase();
|
||||
|
||||
const file = req.file;
|
||||
if (!file)
|
||||
return void res.status(400).json({ detail: "file is required" });
|
||||
|
||||
const { data: doc } = await db
|
||||
.from("documents")
|
||||
.select("id, user_id, project_id")
|
||||
.eq("id", documentId)
|
||||
.single();
|
||||
if (!doc)
|
||||
return void res.status(404).json({ detail: "Document not found" });
|
||||
const access = await ensureDocAccess(doc, userId, userEmail, db);
|
||||
if (!access.ok || !access.isOwner)
|
||||
return void res.status(404).json({ detail: "Document not found" });
|
||||
|
||||
const { data: target, error: targetErr } = await db
|
||||
.from("document_versions")
|
||||
.select("id, storage_path, pdf_storage_path, file_type, deleted_at")
|
||||
.eq("id", versionId)
|
||||
.eq("document_id", documentId)
|
||||
.single();
|
||||
if (targetErr || !target)
|
||||
return void res.status(404).json({ detail: "Version not found" });
|
||||
if (target.deleted_at)
|
||||
return void res.status(400).json({ detail: "Version is deleted." });
|
||||
|
||||
const suffix = file.originalname.includes(".")
|
||||
? file.originalname.split(".").pop()!.toLowerCase()
|
||||
: "";
|
||||
if (!ALLOWED_TYPES.has(suffix)) {
|
||||
return void res.status(400).json({
|
||||
detail: `Unsupported file type: ${suffix}. Allowed: pdf, docx, doc`,
|
||||
});
|
||||
}
|
||||
if (target.file_type && target.file_type !== suffix) {
|
||||
return void res.status(400).json({
|
||||
detail: `Uploaded file type (${suffix}) does not match version type (${target.file_type}).`,
|
||||
});
|
||||
}
|
||||
|
||||
const versionSlug = crypto.randomUUID().replace(/-/g, "");
|
||||
const key = versionStorageKey(
|
||||
userId,
|
||||
documentId,
|
||||
versionSlug,
|
||||
file.originalname,
|
||||
);
|
||||
const contentType =
|
||||
suffix === "pdf"
|
||||
? "application/pdf"
|
||||
: "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
||||
|
||||
try {
|
||||
await uploadFile(
|
||||
key,
|
||||
file.buffer.buffer.slice(
|
||||
file.buffer.byteOffset,
|
||||
file.buffer.byteOffset + file.buffer.byteLength,
|
||||
) as ArrayBuffer,
|
||||
contentType,
|
||||
);
|
||||
} catch (e) {
|
||||
console.error("[versions/replace] storage write failed", e);
|
||||
return void res
|
||||
.status(500)
|
||||
.json({ detail: "Failed to upload replacement version." });
|
||||
}
|
||||
|
||||
let pdfStoragePath: string | null = null;
|
||||
if (suffix === "docx" || suffix === "doc") {
|
||||
try {
|
||||
const pdfBuf = await docxToPdf(file.buffer);
|
||||
const pdfKey = `converted-pdfs/${userId}/${documentId}/${versionSlug}.pdf`;
|
||||
await uploadFile(
|
||||
pdfKey,
|
||||
pdfBuf.buffer.slice(
|
||||
pdfBuf.byteOffset,
|
||||
pdfBuf.byteOffset + pdfBuf.byteLength,
|
||||
) as ArrayBuffer,
|
||||
"application/pdf",
|
||||
);
|
||||
pdfStoragePath = pdfKey;
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`[versions/replace] DOCX→PDF conversion failed for ${file.originalname}:`,
|
||||
err,
|
||||
);
|
||||
}
|
||||
} else if (suffix === "pdf") {
|
||||
pdfStoragePath = key;
|
||||
}
|
||||
|
||||
const rawBuf = file.buffer.buffer.slice(
|
||||
file.buffer.byteOffset,
|
||||
file.buffer.byteOffset + file.buffer.byteLength,
|
||||
) as ArrayBuffer;
|
||||
const pageCount = suffix === "pdf" ? await countPdfPages(rawBuf) : null;
|
||||
const requestedFilename =
|
||||
typeof req.body?.filename === "string" && req.body.filename.trim()
|
||||
? req.body.filename.trim().slice(0, 200)
|
||||
: file.originalname;
|
||||
const uploadedAt = new Date().toISOString();
|
||||
|
||||
const { data: updated, error: updateErr } = await db
|
||||
.from("document_versions")
|
||||
.update({
|
||||
storage_path: key,
|
||||
pdf_storage_path: pdfStoragePath,
|
||||
filename: requestedFilename,
|
||||
file_type: suffix,
|
||||
size_bytes: file.buffer.byteLength,
|
||||
page_count: pageCount,
|
||||
created_at: uploadedAt,
|
||||
})
|
||||
.eq("id", versionId)
|
||||
.eq("document_id", documentId)
|
||||
.select(
|
||||
"id, version_number, source, created_at, filename, file_type, size_bytes, page_count",
|
||||
)
|
||||
.single();
|
||||
if (updateErr || !updated) {
|
||||
await Promise.all(
|
||||
[key, pdfStoragePath]
|
||||
.filter((path): path is string => !!path)
|
||||
.map((path) => deleteFile(path).catch(() => {})),
|
||||
);
|
||||
return void res.status(500).json({
|
||||
detail: updateErr?.message ?? "Failed to replace version.",
|
||||
});
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
[target.storage_path, target.pdf_storage_path]
|
||||
.filter((path): path is string => !!path)
|
||||
.map((path) => deleteFile(path).catch(() => {})),
|
||||
);
|
||||
|
||||
res.json(updated);
|
||||
},
|
||||
);
|
||||
|
||||
// DELETE /single-documents/:documentId/versions/:versionId
|
||||
// Delete one version. The last remaining version cannot be deleted; if the
|
||||
// deleted version is current, the newest remaining version becomes current.
|
||||
|
|
@ -813,8 +951,11 @@ documentsRouter.delete(
|
|||
|
||||
const { data: versions, error: versionsErr } = await db
|
||||
.from("document_versions")
|
||||
.select("id, storage_path, pdf_storage_path, version_number, created_at")
|
||||
.eq("document_id", documentId);
|
||||
.select(
|
||||
"id, storage_path, pdf_storage_path, version_number, created_at, deleted_at",
|
||||
)
|
||||
.eq("document_id", documentId)
|
||||
.is("deleted_at", null);
|
||||
if (versionsErr) {
|
||||
return void res.status(500).json({ detail: versionsErr.message });
|
||||
}
|
||||
|
|
@ -825,6 +966,7 @@ documentsRouter.delete(
|
|||
pdf_storage_path: string | null;
|
||||
version_number: number | null;
|
||||
created_at: string | null;
|
||||
deleted_at?: string | null;
|
||||
}[];
|
||||
const target = rows.find((row) => row.id === versionId);
|
||||
if (!target)
|
||||
|
|
@ -850,6 +992,7 @@ documentsRouter.delete(
|
|||
doc.current_version_id === versionId
|
||||
? (remaining[0]?.id ?? null)
|
||||
: doc.current_version_id;
|
||||
const deletedAt = new Date().toISOString();
|
||||
|
||||
if (doc.current_version_id === versionId) {
|
||||
const { error: updateErr } = await db
|
||||
|
|
@ -866,9 +1009,15 @@ documentsRouter.delete(
|
|||
|
||||
const { error: deleteErr } = await db
|
||||
.from("document_versions")
|
||||
.delete()
|
||||
.update({
|
||||
storage_path: null,
|
||||
pdf_storage_path: null,
|
||||
deleted_at: deletedAt,
|
||||
deleted_by: userId,
|
||||
})
|
||||
.eq("id", versionId)
|
||||
.eq("document_id", documentId);
|
||||
.eq("document_id", documentId)
|
||||
.is("deleted_at", null);
|
||||
if (deleteErr) {
|
||||
return void res.status(500).json({ detail: deleteErr.message });
|
||||
}
|
||||
|
|
@ -882,6 +1031,7 @@ documentsRouter.delete(
|
|||
res.json({
|
||||
deleted_version_id: versionId,
|
||||
current_version_id: nextCurrentVersionId,
|
||||
deleted_at: deletedAt,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ downloadsRouter.get("/:token", requireAuth, async (req, res) => {
|
|||
.from("document_versions")
|
||||
.select("id, document_id")
|
||||
.eq("storage_path", info.path)
|
||||
.is("deleted_at", null)
|
||||
.maybeSingle();
|
||||
if (byStoragePath) {
|
||||
version = byStoragePath as { id: string; document_id: string };
|
||||
|
|
|
|||
|
|
@ -15,7 +15,10 @@ import {
|
|||
PROJECT_EXTRA_TOOLS,
|
||||
type ChatMessage,
|
||||
} from "../lib/chatTools";
|
||||
import { getUserApiKeys } from "../lib/userSettings";
|
||||
import {
|
||||
getLegalResearchUsEnabled,
|
||||
getUserApiKeys,
|
||||
} from "../lib/userSettings";
|
||||
import { checkProjectAccess } from "../lib/access";
|
||||
import { safeErrorLog, safeErrorMessage } from "../lib/safeError";
|
||||
|
||||
|
|
@ -141,10 +144,13 @@ projectChatRouter.post("/", requireAuth, async (req, res) => {
|
|||
systemPromptExtra += `\n\nUSER-ATTACHED DOCUMENTS FOR THIS TURN:\nThe user has attached the following document(s) directly to their latest message. Treat these as the primary focus of the request unless their message clearly says otherwise.\n${lines.join("\n")}`;
|
||||
}
|
||||
|
||||
const legalResearchUs = await getLegalResearchUsEnabled(userId, db);
|
||||
const apiMessages = buildMessages(
|
||||
messagesForLLM,
|
||||
docAvailability,
|
||||
systemPromptExtra,
|
||||
undefined,
|
||||
legalResearchUs,
|
||||
);
|
||||
|
||||
const workflowStore = await buildWorkflowStore(userId, userEmail, db);
|
||||
|
|
@ -176,6 +182,7 @@ projectChatRouter.post("/", requireAuth, async (req, res) => {
|
|||
write,
|
||||
extraTools: PROJECT_EXTRA_TOOLS,
|
||||
workflowStore,
|
||||
includeResearchTools: legalResearchUs,
|
||||
model,
|
||||
apiKeys,
|
||||
signal: streamAbort.signal,
|
||||
|
|
|
|||
|
|
@ -60,6 +60,59 @@ async function deleteProjectDocumentsAndVersionFiles(
|
|||
return error ?? null;
|
||||
}
|
||||
|
||||
async function attachDocumentOwnerLabels(
|
||||
db: ReturnType<typeof createServerSupabase>,
|
||||
docs: { user_id?: string | null }[],
|
||||
) {
|
||||
const ownerIds = docs
|
||||
.map((doc) => doc.user_id)
|
||||
.filter((id): id is string => typeof id === "string" && id.length > 0)
|
||||
.filter((id, index, arr) => arr.indexOf(id) === index);
|
||||
if (ownerIds.length === 0) return;
|
||||
|
||||
const emailByUserId = new Map<string, string>();
|
||||
const userResults = await Promise.allSettled(
|
||||
ownerIds.map(async (id) => {
|
||||
const { data, error } = await db.auth.admin.getUserById(id);
|
||||
if (error) throw error;
|
||||
return { id, email: data.user?.email ?? null };
|
||||
}),
|
||||
);
|
||||
for (const result of userResults) {
|
||||
if (result.status === "fulfilled" && result.value.email) {
|
||||
emailByUserId.set(result.value.id, result.value.email);
|
||||
}
|
||||
}
|
||||
|
||||
const displayNameByUserId = new Map<string, string>();
|
||||
const { data: profiles, error: profilesError } = await db
|
||||
.from("user_profiles")
|
||||
.select("user_id, display_name")
|
||||
.in("user_id", ownerIds);
|
||||
if (profilesError) {
|
||||
console.warn("[projects] failed to load document owner profiles", profilesError);
|
||||
}
|
||||
for (const profile of profiles ?? []) {
|
||||
const displayName =
|
||||
typeof profile.display_name === "string"
|
||||
? profile.display_name.trim()
|
||||
: "";
|
||||
if (displayName) {
|
||||
displayNameByUserId.set(profile.user_id as string, displayName);
|
||||
}
|
||||
}
|
||||
|
||||
for (const doc of docs as ({
|
||||
user_id?: string | null;
|
||||
owner_email?: string | null;
|
||||
owner_display_name?: string | null;
|
||||
})[]) {
|
||||
if (!doc.user_id) continue;
|
||||
doc.owner_email = emailByUserId.get(doc.user_id) ?? null;
|
||||
doc.owner_display_name = displayNameByUserId.get(doc.user_id) ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
// GET /projects
|
||||
projectsRouter.get("/", requireAuth, async (req, res) => {
|
||||
const userId = res.locals.userId as string;
|
||||
|
|
@ -190,10 +243,12 @@ projectsRouter.get("/:projectId", requireAuth, async (req, res) => {
|
|||
]);
|
||||
const docsTyped = (docs ?? []) as unknown as {
|
||||
id: string;
|
||||
user_id?: string | null;
|
||||
current_version_id?: string | null;
|
||||
}[];
|
||||
await attachLatestVersionNumbers(db, docsTyped);
|
||||
await attachActiveVersionPaths(db, docsTyped);
|
||||
await attachDocumentOwnerLabels(db, docsTyped);
|
||||
res.json({
|
||||
...project,
|
||||
is_owner: project.user_id === userId,
|
||||
|
|
@ -335,9 +390,11 @@ projectsRouter.patch("/:projectId", requireAuth, async (req, res) => {
|
|||
]);
|
||||
const docsTyped = (docs ?? []) as unknown as {
|
||||
id: string;
|
||||
user_id?: string | null;
|
||||
current_version_id?: string | null;
|
||||
}[];
|
||||
await attachActiveVersionPaths(db, docsTyped);
|
||||
await attachDocumentOwnerLabels(db, docsTyped);
|
||||
res.json({ ...data, documents: docsTyped, folders: folderData ?? [] });
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ type UserProfileRow = {
|
|||
title_model: string | null;
|
||||
tabular_model: string;
|
||||
mfa_on_login: boolean | null;
|
||||
legal_research_us: boolean | null;
|
||||
};
|
||||
|
||||
function errorMessage(error: unknown): string {
|
||||
|
|
@ -65,6 +66,8 @@ function errorMessage(error: unknown): string {
|
|||
}
|
||||
|
||||
const PROFILE_SELECT =
|
||||
"display_name, organisation, message_credits_used, credits_reset_date, tier, title_model, tabular_model, mfa_on_login, legal_research_us";
|
||||
const PROFILE_SELECT_NO_LEGAL =
|
||||
"display_name, organisation, message_credits_used, credits_reset_date, tier, title_model, tabular_model, mfa_on_login";
|
||||
const LEGACY_PROFILE_SELECT =
|
||||
"display_name, organisation, message_credits_used, credits_reset_date, tier, tabular_model";
|
||||
|
|
@ -80,14 +83,43 @@ function isMissingProfileColumn(error: unknown, column: string): boolean {
|
|||
return record.code === "42703" && message.includes(column);
|
||||
}
|
||||
|
||||
// Loads a profile while tolerating older databases that lack the
|
||||
// legal_research_us column. Tries the full select first, then falls back to
|
||||
// the legacy cascade (which also handles missing title_model / mfa_on_login)
|
||||
// and defaults the feature flag to enabled.
|
||||
async function selectProfile(
|
||||
db: ReturnType<typeof createServerSupabase>,
|
||||
userId: string,
|
||||
mode: "maybe" | "single",
|
||||
) {
|
||||
const fullQuery = db
|
||||
.from("user_profiles")
|
||||
.select(PROFILE_SELECT)
|
||||
.eq("user_id", userId);
|
||||
const full =
|
||||
mode === "single"
|
||||
? await fullQuery.single()
|
||||
: await fullQuery.maybeSingle();
|
||||
if (!full.error) return full;
|
||||
|
||||
const legacy = await selectProfileLegacy(db, userId, mode);
|
||||
if (legacy.data && typeof legacy.data === "object") {
|
||||
const row = legacy.data as Record<string, unknown>;
|
||||
if (!("legal_research_us" in row)) {
|
||||
Object.assign(row, { legal_research_us: true });
|
||||
}
|
||||
}
|
||||
return legacy;
|
||||
}
|
||||
|
||||
async function selectProfileLegacy(
|
||||
db: ReturnType<typeof createServerSupabase>,
|
||||
userId: string,
|
||||
mode: "maybe" | "single",
|
||||
) {
|
||||
const query = db
|
||||
.from("user_profiles")
|
||||
.select(PROFILE_SELECT)
|
||||
.select(PROFILE_SELECT_NO_LEGAL)
|
||||
.eq("user_id", userId);
|
||||
const result =
|
||||
mode === "single" ? await query.single() : await query.maybeSingle();
|
||||
|
|
@ -166,6 +198,7 @@ function serializeProfile(row: UserProfileRow, apiKeyStatus?: ApiKeyStatus) {
|
|||
titleModel: resolveModel(row.title_model, titleFallback),
|
||||
tabularModel: resolveModel(row.tabular_model, DEFAULT_TABULAR_MODEL),
|
||||
mfaOnLogin: row.mfa_on_login === true,
|
||||
legalResearchUs: row.legal_research_us !== false,
|
||||
...(apiKeyStatus ? { apiKeyStatus } : {}),
|
||||
};
|
||||
}
|
||||
|
|
@ -178,6 +211,7 @@ function validateProfilePayload(body: unknown):
|
|||
organisation?: string | null;
|
||||
title_model?: string;
|
||||
tabular_model?: string;
|
||||
legal_research_us?: boolean;
|
||||
updated_at: string;
|
||||
};
|
||||
}
|
||||
|
|
@ -192,6 +226,7 @@ function validateProfilePayload(body: unknown):
|
|||
"organisation",
|
||||
"titleModel",
|
||||
"tabularModel",
|
||||
"legalResearchUs",
|
||||
]);
|
||||
const invalidField = Object.keys(raw).find(
|
||||
(key) => !allowedFields.has(key),
|
||||
|
|
@ -208,6 +243,7 @@ function validateProfilePayload(body: unknown):
|
|||
organisation?: string | null;
|
||||
title_model?: string;
|
||||
tabular_model?: string;
|
||||
legal_research_us?: boolean;
|
||||
updated_at: string;
|
||||
} = { updated_at: new Date().toISOString() };
|
||||
|
||||
|
|
@ -253,6 +289,16 @@ function validateProfilePayload(body: unknown):
|
|||
update.title_model = resolved;
|
||||
}
|
||||
|
||||
if ("legalResearchUs" in raw) {
|
||||
if (typeof raw.legalResearchUs !== "boolean") {
|
||||
return {
|
||||
ok: false,
|
||||
detail: "legalResearchUs must be a boolean",
|
||||
};
|
||||
}
|
||||
update.legal_research_us = raw.legalResearchUs;
|
||||
}
|
||||
|
||||
return { ok: true, update };
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue