ExportXMLWordPrintableJSON

    • Type: Story
    • Resolution: Unresolved
    • Priority: Major - P3
    • No version
    • Affects Version/s: None
    • Component/s: None
    • None
    • Not Needed
    • None
    • Developer Tools

      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):

      1. Add a type-level distinction alongside UserDataType:
        type UserScopedUserDataType = 'AppPreferences';
        
      1. Derive the id argument shape from it once:
        type ScopedId<T extends UserDataType> = T extends UserScopedUserDataType ? undefined : string;
        
      1. 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;
          };
        
      1. 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>
        
      1. 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);
        }
        
      1. 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:

      1. 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).

      2. 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).

            Assignee:
            Kevin Mas Ruiz
            Reporter:
            Derek Cho (Inactive)
            Votes:
            0 Vote for this issue
            Watchers:
            1 Start watching this issue

              Created:
              Updated: