-
Type:
Story
-
Resolution: Unresolved
-
Priority:
Major - P3
-
Affects Version/s: None
-
Component/s: None
-
None
Implement a new PreferencesStorage, AtlasPreferencesStorage, that GETs/PUTs the singleton AppPreferences doc. Inject it into CompassWebPreferencesAccess in place of InMemoryStorage, wired up in compass-web's preferences loading.
Implementation detail (moved here from the technical design, WRITING-39309)
AppPreferences is user-scoped by design on the backend: per adding-a-user-data-type.md it is deliberately not a member of USER_DATA_TYPES; it is served on the monolith's global route /ui/userData/
{type}with no org or project in the path and no document id, and it supports only GET and PUT. AtlasUserData therefore requires the orgId/projectId to be optional for user-scoped types.
packages/compass-user-data/src/user-data.ts uses a closed UserDataType union to enforce, at the type level, which arguments are valid for which type (no runtime checks):
- Add a type-level distinction alongside UserDataType:
type UserScopedUserDataType = 'AppPreferences';
- Derive the id argument shape from it once:
type ScopedId<T extends UserDataType> = T extends UserScopedUserDataType ? undefined : string;
- Make orgId/projectId conditional on the constructor options, keyed off the same T:
export type AtlasUserDataOptions<T extends UserDataType, Input> = (T extends UserScopedUserDataType ? unknown : { orgId: string; projectId: string }) & { atlasService: AtlasServiceLike; serialize?: SerializeContent<Input>; deserialize?: DeserializeContent; };
- Update all four methods to take id via ScopedId (id stays a required parameter — for 'AppPreferences' the only legal value is undefined):
write(id: ScopedId<T>, content: z.input<S>): Promise<boolean> updateAttributes(id: ScopedId<T>, data: Partial<z.input<S>>): Promise<boolean> readOne(id: ScopedId<T>): Promise<z.output<S> | undefined> delete(id: ScopedId<T>): Promise<boolean>
- Replace per-method URL construction with one endpointFor, and add userScopedUserDataEndpoint(type) to AtlasService:
function isUserScopedUserDataType(type: UserDataType): type is UserScopedUserDataType { return type === 'AppPreferences'; } private endpointFor(id?: string): string { return isUserScopedUserDataType(this.dataType) ? this.atlasService.userScopedUserDataEndpoint(this.dataType) : this.atlasService.userDataEndpoint(this.orgId, this.projectId, this.dataType, id); }
- Build AtlasPreferencesStorage in packages/compass-preferences-model/src/preferences-atlas.ts on top of AtlasUserData (constructor takes an AtlasServiceLike — no orgId/projectId). It is created in packages/compass-web/src/preferences.tsx within the initial-preferences loader, with the injected AtlasService; setup() preloads the stored AppPreferences doc.
Bootstrap / initial fetch (react-tree-driven — no module-scoped prefetch or cache)
Initial settings load entirely within the React tree. The hook useCompassWebPreferences (packages/compass-web/src/preferences.tsx:253) performs the initial load:
- the cloud preferences endpoint *GET /explorer/v1/groups/
{projectId}
/preferences* — fetched via the injected AtlasService (atlasService.cloudEndpoint + authenticatedFetch). It supplies permission-derived prefs (readOnly/readWrite), timezone, GenAI opt-ins, and cloud feature flags / project & org overrides (getPreferencesFromCloudApi).
- AtlasPreferencesStorage (AtlasUserData over AppPreferences) — the persisted user settings doc.
The two sources load in parallel, and nothing renders until they resolve: CompassWebPreferencesProvider (packages/compass-web/src/entrypoint.tsx:498) gates on the hook — spinner while loading, throws on error, then mounts the PreferencesProvider. The load is react-lifecycle-driven; there is no module-scoped prefetch and no module cache.
{note}Remaining refactor on the branch: remove the module-scope compassWebPreferencesCache and the _fetchAndCachePreferences / loadCompassWebPreferences helpers from packages/compass-web/src/preferences.tsx so the loader lives entirely in the hook, and rework setCompassWebPreferencesAccess / getAnyCompassWebPreferencesAccess (sandbox support) accordingly.{note}Subsequent updates: CompassWebPreferencesAccess.savePreferences → AtlasPreferencesStorage.updatePreferences → userData.write(undefined, ...) (read-modify-write of the singleton doc).
AtlasServiceProvider (packages/atlas-service/src/provider.tsx:34) is mounted above the preferences provider; compass-web passes atlasServiceBackendPreset explicitly (host-derived via getAtlasServiceBackendPreset), and the AtlasService constructor no longer takes PreferencesAccess (packages/atlas-service/src/atlas-service.ts:55).