From 3e7ae9cac7e2fe18b0390e1e4e5f616dce121890 Mon Sep 17 00:00:00 2001 From: Aarav Sharma Date: Tue, 4 Aug 2026 22:33:43 -0600 Subject: [PATCH 1/4] fix(cli): planner create/schedule send correct class and calendar refs Two bugs that prevented CLI-created todos from appearing in the right places: 1. action create: resolveEmployeeId() returns either an Employee or Person ref depending on the workspace model, but createAction hardcoded attachedToClass as 'contact:class:Person'. Mismatch rejected by addCollection or routed the todo to the wrong collection. Now returns { ref, class } and mirrors the resolved class into attachedToClass, probing the actual class when the lookup helper returns just a ref. 2. action schedule: set WorkSlot.calendar to todo.user (an Employee ref), but Event.calendar is a Ref. The Schedule Calendar UI filters by the user's PersonalCalendar ref, so slots were invisible. Now resolves the PersonalCalendar via findPrimaryCalendar logic (Calendar docs for user + PrimaryCalendar preference). --- packages/cli/src/resources/todo.ts | 98 ++++++++++++++++++++++++------ 1 file changed, 81 insertions(+), 17 deletions(-) diff --git a/packages/cli/src/resources/todo.ts b/packages/cli/src/resources/todo.ts index aedf3ad..1887099 100644 --- a/packages/cli/src/resources/todo.ts +++ b/packages/cli/src/resources/todo.ts @@ -43,6 +43,47 @@ const CALENDAR_SPACE = 'calendar:space:Calendar' as Ref const TODO_PRIORITIES = new Set(['High', 'Medium', 'Low', 'NoPriority', 'Urgent']) const TODO_VISIBILITIES = new Set(['public', 'busy', 'private']) +const CALENDAR_CLASS = 'calendar:class:Calendar' as Ref> +const PRIMARY_CALENDAR_PREF = 'calendar:class:PrimaryCalendar' as Ref> + +/** + * Resolve the user's PersonalCalendar the same way the web UI's + * `findPrimaryCalendar` does (plugins/time-resources/src/utils.ts). The + * preference document's `attachedTo` is the user-selected Calendar; if + * none, fall back to the first Calendar owned or writable by the current + * user. Returns `undefined` if the workspace has no Calendar for this + * user (rare — fresh workspace). + */ +async function resolvePrimaryCalendar( + client: Awaited>, + primarySocialId: string, + accountUuid: string +): Promise | undefined> { + let calendars: Array; user?: string; hidden?: boolean; access?: string }> + try { + calendars = (await client.findAll(CALENDAR_CLASS, { + user: primarySocialId, + hidden: false, + access: { $in: ['owner', 'writer'] } + })) as typeof calendars + } catch { + return undefined + } + if (calendars.length === 0) return undefined + + // PrimaryCalendar preference (single instance) names the chosen Calendar + // via its `attachedTo` field. See models/calendar PrimaryCalendar model. + try { + const pref = (await client.findOne(PRIMARY_CALENDAR_PREF, {})) as (Doc & { attachedTo?: Ref }) | undefined + if (pref?.attachedTo !== undefined) { + const match = calendars.find((c) => c._id === pref.attachedTo) + if (match !== undefined) return match._id + } + } catch { + // preference not yet created for this workspace + } + return calendars[0]._id +} function parseDate(value: string, field: string): number { const t = new Date(value).getTime() @@ -64,16 +105,22 @@ async function readBodyText(opts: { body?: string; bodyFile?: string }): Promise /** * Resolves a workspace user reference for an email address or the current account. * + * Returns both the doc `_id` and the class it belongs to. Callers that build + * `attachedTo` / `attachedToClass` pairs (e.g. `addCollection`) MUST use the + * returned class — ToDo `user` accepts either Employee or Person refs depending + * on the workspace model, and a mismatch between ref and class will be + * rejected by the server or land the todo in the wrong collection. + * * @param email - The person to resolve * @param resolveOpts - Optional `--url` / `--workspace` to thread through to the account-service fallback - * @returns The matching person or employee reference, or the current account UUID when `email` is omitted + * @returns The matching `_id` paired with its class, or the current account UUID with `contact:class:Person` when `email` is omitted * @throws {CliError} When no matching person is found in the workspace */ async function resolveEmployeeId( client: Awaited>, email?: string, resolveOpts: ResolveOpts = {} -): Promise> { +): Promise<{ ref: Ref, class: Ref> }> { if (email) { // Todo `user` accepts either an Employee or a Person ref depending on // the workspace model, so try Employee first then Person via the shared @@ -85,33 +132,43 @@ async function resolveEmployeeId( ['contact:class:Employee', 'contact:class:Person'], resolveOpts ) - if (id !== undefined) return id + if (id !== undefined) { + // The helper returns only the _id, not the class. Probe to find + // which class it belongs to so `attachedToClass` matches the ref. + for (const classId of ['contact:class:Employee', 'contact:class:Person']) { + try { + const doc = await client.findOne(classId as Ref>, { _id: id }) + if (doc) return { ref: id, class: classId as Ref> } + } catch { + // class not in this workspace's model + } + } + } } // Workspace-local fallback for name-based lookups or when the // cross-workspace lookup doesn't match anything in this workspace. // Scan both Employee and Person so users who exist only as Employee - // are still matched by name. + // are still matched by name. Track the class each candidate came from + // so callers can mirror it into `attachedToClass`. const lower = email.toLowerCase() - const candidates: Array = [] for (const classId of ['contact:class:Employee', 'contact:class:Person']) { try { const docs = (await client.findAll( classId as Ref>, {}, { limit: 500 } )) as Array - candidates.push(...docs) + const hit = docs.find( + (p) => (p.name ?? '').toLowerCase() === lower || (p.email ?? '').toLowerCase() === lower + ) + if (hit) return { ref: hit._id, class: classId as Ref> } } catch { // class not in this workspace's model; try the next one } } - const hit = candidates.find( - (p) => (p.name ?? '').toLowerCase() === lower || (p.email ?? '').toLowerCase() === lower - ) - if (!hit) throw new CliError(ExitCode.NotFound, `no person matching ${email} in this workspace`) - return hit._id + throw new CliError(ExitCode.NotFound, `no person matching ${email} in this workspace`) } // Default: current user const account = await client.getAccount() - return account.uuid as Ref + return { ref: account.uuid as Ref, class: 'contact:class:Person' as Ref> } } // ---- list ---- @@ -137,7 +194,7 @@ export async function listActions(opts: ListActionsOpts = {}): Promise { const client = await connectCli({ url: opts.url, workspace: opts.workspace }) try { const query: Record = {} - if (opts.owner) query.user = await resolveEmployeeId(client, opts.owner, { url: opts.url, workspace: opts.workspace }) + if (opts.owner) query.user = (await resolveEmployeeId(client, opts.owner, { url: opts.url, workspace: opts.workspace })).ref if (opts.priority) { if (!TODO_PRIORITIES.has(opts.priority)) { throw new CliError(ExitCode.Validation, `invalid --priority: ${opts.priority}`, `expected one of ${[...TODO_PRIORITIES].join(' | ')}`) @@ -275,7 +332,7 @@ export async function createAction(opts: CreateActionOpts): Promise { : (opts.description ? opts.description : '') const client = await connectCli({ url: opts.url, workspace: opts.workspace }) try { - const user = await resolveEmployeeId(client, opts.owner, { url: opts.url, workspace: opts.workspace }) + const { ref: user, class: userClass } = await resolveEmployeeId(client, opts.owner, { url: opts.url, workspace: opts.workspace }) if (opts.priority && !TODO_PRIORITIES.has(opts.priority)) { throw new CliError(ExitCode.Validation, `invalid --priority: ${opts.priority}`, `expected one of ${[...TODO_PRIORITIES].join(' | ')}`) } @@ -295,7 +352,7 @@ export async function createAction(opts: CreateActionOpts): Promise { attachedToClass = opts.attachedToClass as Ref> } else { attachedTo = user - attachedToClass = 'contact:class:Person' as Ref> + attachedToClass = userClass } const data: Record = { @@ -373,7 +430,7 @@ export async function updateAction(ref: string, opts: UpdateActionOpts): Promise } ops.visibility = opts.visibility } - if (opts.owner) ops.user = await resolveEmployeeId(client, opts.owner, { url: opts.url, workspace: opts.workspace }) + if (opts.owner) ops.user = (await resolveEmployeeId(client, opts.owner, { url: opts.url, workspace: opts.workspace })).ref if (Object.keys(ops).length === 0) { throw new CliError(ExitCode.Validation, 'nothing to update', 'pass --title, --description, --due, --priority, --visibility, or --owner') @@ -526,12 +583,19 @@ export async function scheduleAction(ref: string, opts: ScheduleActionOpts): Pro const account = await client.getAccount() const startMs = parseDate(opts.start, '--start') const dueMs = startMs + opts.duration * 60 * 1000 + // Resolve the user's PersonalCalendar the same way the web UI does + // (see time-resources/utils.ts: findPrimaryCalendar). `todo.user` is an + // Employee ref — using it as the `calendar` field makes the WorkSlot + // invisible to the Schedule Calendar UI, which filters by Calendar ref. + const calendarRef = account.primarySocialId !== undefined + ? await resolvePrimaryCalendar(client, account.primarySocialId, account.uuid) + : undefined const data: Record = { title: todo.title, date: startMs, dueDate: dueMs, allDay: !!opts.allDay, - calendar: todo.user, + calendar: calendarRef ?? todo.user, eventId: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`, access: 'owner', visibility: todo.visibility ?? 'public', From 0e2044c5aaa240611e8f1e6c67ef386fff58d855 Mon Sep 17 00:00:00 2001 From: Aarav Sharma Date: Tue, 4 Aug 2026 22:44:13 -0600 Subject: [PATCH 2/4] =?UTF-8?q?fix(cli):=20address=20review=20=E2=80=94=20?= =?UTF-8?q?query=20Person=20by=20personUuid,=20match=20UI=20primary-calend?= =?UTF-8?q?ar=20order?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. resolveEmployeeId default-owner branch previously returned account.uuid as if it were a workspace-local Person _id. Now queries contact:class:Person with { personUuid: account.uuid }, mirroring resolveEmailToLocalId, and throws an explicit error if no Person was provisioned for the current account. 2. resolvePrimaryCalendar previously fell back to the first writable Calendar when neither the preference nor a Person-local match was found. The UI scheduler's getPrimaryCalendar (plugins/calendar/src/utils.ts:432) instead returns the first eligible ExternalCalendar (default: true, hidden: false) and, failing that, the synthetic ${accountUuid}_calendar. Match that order so CLI-scheduled WorkSlots land in the same calendar the UI would use. Shared helper is inlined because @hcengineering/calendar is not a CLI dependency. --- packages/cli/src/resources/todo.ts | 62 ++++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 16 deletions(-) diff --git a/packages/cli/src/resources/todo.ts b/packages/cli/src/resources/todo.ts index 1887099..277868b 100644 --- a/packages/cli/src/resources/todo.ts +++ b/packages/cli/src/resources/todo.ts @@ -44,22 +44,30 @@ const CALENDAR_SPACE = 'calendar:space:Calendar' as Ref const TODO_PRIORITIES = new Set(['High', 'Medium', 'Low', 'NoPriority', 'Urgent']) const TODO_VISIBILITIES = new Set(['public', 'busy', 'private']) const CALENDAR_CLASS = 'calendar:class:Calendar' as Ref> +const EXTERNAL_CALENDAR_CLASS = 'calendar:class:ExternalCalendar' as Ref> const PRIMARY_CALENDAR_PREF = 'calendar:class:PrimaryCalendar' as Ref> /** * Resolve the user's PersonalCalendar the same way the web UI's - * `findPrimaryCalendar` does (plugins/time-resources/src/utils.ts). The - * preference document's `attachedTo` is the user-selected Calendar; if - * none, fall back to the first Calendar owned or writable by the current - * user. Returns `undefined` if the workspace has no Calendar for this - * user (rare — fresh workspace). + * `findPrimaryCalendar` / `getPrimaryCalendar` does + * (plugins/time-resources/src/utils.ts, plugins/calendar/src/utils.ts). + * Mirrors the platform's selection order: + * 1. PrimaryCalendar preference's `attachedTo` (the user-picked Calendar). + * 2. First ExternalCalendar with `default: true` and `hidden: false`. + * 3. Synthetic `${accountUuid}_calendar` (matches the platform's fallback + * so the WorkSlot lands in the same calendar the UI would use). + * + * The shared `getPrimaryCalendar` helper from `@hcengineering/calendar` + * is not reused because the CLI does not depend on that package; the + * logic is small enough to inline and the platform contract is the source + * of truth. */ async function resolvePrimaryCalendar( client: Awaited>, primarySocialId: string, accountUuid: string -): Promise | undefined> { - let calendars: Array; user?: string; hidden?: boolean; access?: string }> +): Promise> { + let calendars: Array; _class?: Ref>; user?: string; hidden?: boolean; access?: string; default?: boolean }> try { calendars = (await client.findAll(CALENDAR_CLASS, { user: primarySocialId, @@ -67,12 +75,10 @@ async function resolvePrimaryCalendar( access: { $in: ['owner', 'writer'] } })) as typeof calendars } catch { - return undefined + return `${accountUuid}_calendar` as Ref } - if (calendars.length === 0) return undefined - // PrimaryCalendar preference (single instance) names the chosen Calendar - // via its `attachedTo` field. See models/calendar PrimaryCalendar model. + // 1. PrimaryCalendar preference names the chosen Calendar via attachedTo. try { const pref = (await client.findOne(PRIMARY_CALENDAR_PREF, {})) as (Doc & { attachedTo?: Ref }) | undefined if (pref?.attachedTo !== undefined) { @@ -82,7 +88,17 @@ async function resolvePrimaryCalendar( } catch { // preference not yet created for this workspace } - return calendars[0]._id + + // 2. Eligible ExternalCalendar default. + for (const c of calendars) { + if (c._class === EXTERNAL_CALENDAR_CLASS && !c.hidden && c.default === true) { + return c._id + } + } + + // 3. Synthetic account-default Calendar — matches what getPrimaryCalendar + // returns so the WorkSlot lands in the same calendar the UI would use. + return `${accountUuid}_calendar` as Ref } function parseDate(value: string, field: string): number { @@ -166,9 +182,23 @@ async function resolveEmployeeId( } throw new CliError(ExitCode.NotFound, `no person matching ${email} in this workspace`) } - // Default: current user + // Default: current user. Look up the workspace-local Person linked to the + // current account by `personUuid` — account.uuid is the account-level UUID + // and may not be a valid Person doc _id in this workspace. Throw if no + // Person was provisioned (the bootstrap step should create one). const account = await client.getAccount() - return { ref: account.uuid as Ref, class: 'contact:class:Person' as Ref> } + const person = (await client.findOne( + 'contact:class:Person' as Ref>, + { personUuid: account.uuid } + )) as Doc | undefined + if (person === undefined) { + throw new CliError( + ExitCode.NotFound, + `no contact:class:Person provisioned for current account`, + 'open the workspace once in the browser, or re-run without omitting --owner' + ) + } + return { ref: person._id, class: 'contact:class:Person' as Ref> } } // ---- list ---- @@ -589,13 +619,13 @@ export async function scheduleAction(ref: string, opts: ScheduleActionOpts): Pro // invisible to the Schedule Calendar UI, which filters by Calendar ref. const calendarRef = account.primarySocialId !== undefined ? await resolvePrimaryCalendar(client, account.primarySocialId, account.uuid) - : undefined + : (`${account.uuid}_calendar` as Ref) const data: Record = { title: todo.title, date: startMs, dueDate: dueMs, allDay: !!opts.allDay, - calendar: calendarRef ?? todo.user, + calendar: calendarRef, eventId: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`, access: 'owner', visibility: todo.visibility ?? 'public', From 5ab1bb4552001f9c4d6138cb0dcad1f57562e074 Mon Sep 17 00:00:00 2001 From: Aarav Sharma Date: Wed, 5 Aug 2026 07:42:26 -0600 Subject: [PATCH 3/4] =?UTF-8?q?fix(cli):=20address=20second=20review=20?= =?UTF-8?q?=E2=80=94=20narrow=20domain-not-found=20catches,=20probe=20Empl?= =?UTF-8?q?oyee=20in=20default=20owner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address 3 of 6 review findings; skip 3 with reasons: Fixes: - resolvePrimaryCalendar.findAll: catch now only swallows 'domain not found' errors so network/auth failures surface instead of silently falling back to the synthetic ${accountUuid}_calendar. - resolveEmployeeId probe loop (email branch): same narrow-to-domain- not-found fix so a transient findOne failure doesn't masquerade as 'class not in this workspace'. - resolveEmployeeId workspace-local fallback: same narrow-to-domain- not-found fix. - resolveEmployeeId default-owner branch: previously only queried contact:class:Person; if the workspace models the current user only as Employee, it threw NotFound. Now probes both Person and Employee, mirroring the email branch. Skipped: - findOne(PrimaryCalendar, {}) empty filter: matches platform UI's findPrimaryCalendar exactly; Preference is treated as a workspace singleton there. - ExternalCalendar strict-equality check: OSS platform defines no ExternalCalendar subclasses (only ExternalCalendar extends Calendar); the UI uses the same strict equality. Revisit if subclasses land. - resolveEmployeeId workspace-local limit: 500: matches the resolveEmailToLocalId pattern; pagination refactor out of scope. --- packages/cli/src/resources/todo.ts | 69 +++++++++++++++++++++--------- 1 file changed, 48 insertions(+), 21 deletions(-) diff --git a/packages/cli/src/resources/todo.ts b/packages/cli/src/resources/todo.ts index 277868b..06a7608 100644 --- a/packages/cli/src/resources/todo.ts +++ b/packages/cli/src/resources/todo.ts @@ -47,6 +47,16 @@ const CALENDAR_CLASS = 'calendar:class:Calendar' as Ref> const EXTERNAL_CALENDAR_CLASS = 'calendar:class:ExternalCalendar' as Ref> const PRIMARY_CALENDAR_PREF = 'calendar:class:PrimaryCalendar' as Ref> +/** + * True when the SDK error indicates the queried class doesn't exist in the + * workspace model (`Hierarchy` throws `Error('domain not found: ')`). + * Used to distinguish "this workspace doesn't model Employee/Person/Calendar" + * from real network/auth failures, which must surface to the caller. + */ +function isDomainNotFound (err: unknown): boolean { + return err instanceof Error && err.message.includes('domain not found') +} + /** * Resolve the user's PersonalCalendar the same way the web UI's * `findPrimaryCalendar` / `getPrimaryCalendar` does @@ -74,19 +84,27 @@ async function resolvePrimaryCalendar( hidden: false, access: { $in: ['owner', 'writer'] } })) as typeof calendars - } catch { + } catch (err) { + // "domain not found" means this workspace doesn't model Calendar — fall + // through to the synthetic default so the slot still lands somewhere + // visible. Any other error (network, auth, server) must propagate. + if (!isDomainNotFound(err)) throw err return `${accountUuid}_calendar` as Ref } // 1. PrimaryCalendar preference names the chosen Calendar via attachedTo. + // The platform UI queries with an empty filter (Preference is treated as a + // workspace singleton there), so we match that contract instead of + // guessing a user-scoped filter. try { const pref = (await client.findOne(PRIMARY_CALENDAR_PREF, {})) as (Doc & { attachedTo?: Ref }) | undefined if (pref?.attachedTo !== undefined) { const match = calendars.find((c) => c._id === pref.attachedTo) if (match !== undefined) return match._id } - } catch { - // preference not yet created for this workspace + } catch (err) { + if (!isDomainNotFound(err)) throw err + // preference class not in this workspace — fall through to ExternalCalendar scan } // 2. Eligible ExternalCalendar default. @@ -155,8 +173,9 @@ async function resolveEmployeeId( try { const doc = await client.findOne(classId as Ref>, { _id: id }) if (doc) return { ref: id, class: classId as Ref> } - } catch { - // class not in this workspace's model + } catch (err) { + if (!isDomainNotFound(err)) throw err + // class not in this workspace's model; try the next one } } } @@ -176,29 +195,37 @@ async function resolveEmployeeId( (p) => (p.name ?? '').toLowerCase() === lower || (p.email ?? '').toLowerCase() === lower ) if (hit) return { ref: hit._id, class: classId as Ref> } - } catch { + } catch (err) { + if (!isDomainNotFound(err)) throw err // class not in this workspace's model; try the next one } } throw new CliError(ExitCode.NotFound, `no person matching ${email} in this workspace`) } - // Default: current user. Look up the workspace-local Person linked to the - // current account by `personUuid` — account.uuid is the account-level UUID - // and may not be a valid Person doc _id in this workspace. Throw if no - // Person was provisioned (the bootstrap step should create one). + // Default: current user. Look up the workspace-local Person/Employee linked + // to the current account by `personUuid` — account.uuid is the + // account-level UUID and may not be a valid Person/Employee doc _id in + // this workspace. Probe both classes, matching the email branch above. + // Throws if neither class is provisioned (the bootstrap step should + // create one). const account = await client.getAccount() - const person = (await client.findOne( - 'contact:class:Person' as Ref>, - { personUuid: account.uuid } - )) as Doc | undefined - if (person === undefined) { - throw new CliError( - ExitCode.NotFound, - `no contact:class:Person provisioned for current account`, - 'open the workspace once in the browser, or re-run without omitting --owner' - ) + for (const classId of ['contact:class:Person', 'contact:class:Employee']) { + try { + const doc = (await client.findOne( + classId as Ref>, + { personUuid: account.uuid } + )) as Doc | undefined + if (doc !== undefined) return { ref: doc._id, class: classId as Ref> } + } catch (err) { + if (!isDomainNotFound(err)) throw err + // class not in this workspace's model; try the next one + } } - return { ref: person._id, class: 'contact:class:Person' as Ref> } + throw new CliError( + ExitCode.NotFound, + `no contact:class:Person or contact:class:Employee provisioned for current account`, + 'open the workspace once in the browser, or re-run without omitting --owner' + ) } // ---- list ---- From 4851233ed89de9fe26bc7c734b770f83b7b3401b Mon Sep 17 00:00:00 2001 From: Aarav Sharma Date: Wed, 5 Aug 2026 07:58:15 -0600 Subject: [PATCH 4/4] =?UTF-8?q?fix(cli):=20address=20remaining=20review=20?= =?UTF-8?q?=E2=80=94=20case-insensitive=20domain=20check,=20Employee-first?= =?UTF-8?q?=20probe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two leftover SUGGESTION findings from the second review pass: 1. isDomainNotFound: switch err.message.includes('domain not found') to /domain not found/i so a future SDK message tweak (casing, translation) doesn't silently break the discriminator. The platform's Hierarchy error is exact-cased today; the change is purely defensive. 2. resolveEmployeeId default-owner branch was probing ['contact:class:Person', 'contact:class:Employee'] while the email branch (and the post-ff2ae6f preference) probes Employee first. Reorder for consistency. Behaviorally a no-op when only one class is provisioned (findOne returns undefined, loop falls through), but matches the comment and the email branch. --- packages/cli/src/resources/todo.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/resources/todo.ts b/packages/cli/src/resources/todo.ts index 06a7608..c54a112 100644 --- a/packages/cli/src/resources/todo.ts +++ b/packages/cli/src/resources/todo.ts @@ -54,7 +54,11 @@ const PRIMARY_CALENDAR_PREF = 'calendar:class:PrimaryCalendar' as Ref * from real network/auth failures, which must surface to the caller. */ function isDomainNotFound (err: unknown): boolean { - return err instanceof Error && err.message.includes('domain not found') + // Hierarchy throws `Error('domain not found: ')` when the queried + // class isn't in the workspace model. Match case-insensitively so a + // future SDK message tweak (casing, translation) doesn't silently break + // the discriminator. + return err instanceof Error && /domain not found/i.test(err.message) } /** @@ -209,7 +213,7 @@ async function resolveEmployeeId( // Throws if neither class is provisioned (the bootstrap step should // create one). const account = await client.getAccount() - for (const classId of ['contact:class:Person', 'contact:class:Employee']) { + for (const classId of ['contact:class:Employee', 'contact:class:Person']) { try { const doc = (await client.findOne( classId as Ref>,