/** * Copyright (c) 2020-present, Goldman Sachs * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { createModelSchema, list, object, optional, primitive, } from 'serializr'; import { type PlainObject, type RequestHeaders, assertNonNullable, guaranteeNonEmptyString, SerializationFactory, usingModelSchema, } from '@finos/legend-shared'; import { type LegendApplicationConfigurationInput, type LegendApplicationConfigurationData, type LegendApplicationBackendEntry, LegendApplicationConfig, } from '@finos/legend-application'; import { QueryBuilderConfig } from '@finos/legend-query-builder'; import { DataProductConfig } from '@finos/legend-extension-dsl-data-product'; import { OIDCConfiguration, LegendIngestionConfiguration, validateIngestionDeploymentConfiguration, } from './LegendIngestionConfiguration.js'; export class ServiceRegistrationEnvironmentConfig { env!: string; executionUrl!: string; modes: string[] = []; managementUrl!: string; static readonly serialization = new SerializationFactory( createModelSchema(ServiceRegistrationEnvironmentConfig, { env: primitive(), executionUrl: primitive(), managementUrl: primitive(), modes: list(primitive()), }), ); } export class UserSearchConfig { applicationDirectoryUrl!: string; userProfileImageUrl!: string; static readonly serialization = new SerializationFactory( createModelSchema(UserSearchConfig, { applicationDirectoryUrl: primitive(), userProfileImageUrl: primitive(), }), ); } class LegendStudioApplicationCoreOptions { /** * Indicates if we should enable strict-mode for graph builder * * Default to `false` */ enableGraphBuilderStrictMode = false; /** * Indicates whether we enable type ahead in all editors. * * Default to `false` because of performance reasons for large models. */ typeAheadEnabled = false; projectCreationGroupIdSuggestion = 'org.finos.legend.*'; /** * This flag is for any feature that is not production ready. * Used to iterate over features until they are ready for production. */ NonProductionFeatureFlag = false; /** * Indicates if we should keep section index and do not rewrite/flatten the paths shortened by section * imports. * * This flag will be kept until we have full support for section index * See https://github.com/finos/legend-studio/issues/1067 */ TEMPORARY__preserveSectionIndex = false; /** * This flag can be removed when the support for local connection is official * Right now it's done to support the SnowflakeApp creation demo * See https://github.com/finos/legend-engine/pull/1819 */ TEMPORARY__enableLocalConnectionBuilder = false; /** * This flag enables creating a sandbox project via engine. * Remove this flag once workflow is finalized. */ TEMPORARY__enableCreationOfSandboxProjects = false; /** * Provides service registration environment configs. * * TODO: when we modularize service, we can move this config to DSL Service preset. Then, we can remove * the TEMPORARY__ prefix. * * @modularize * See https://github.com/finos/legend-studio/issues/65 */ TEMPORARY__serviceRegistrationConfig: ServiceRegistrationEnvironmentConfig[] = []; /** * Config specific to query builder */ queryBuilderConfig: QueryBuilderConfig | undefined; ingestDeploymentConfig: LegendIngestionConfiguration | undefined; dataProductConfig: DataProductConfig | undefined; userSearchConfig: UserSearchConfig | undefined; reconciliationOidcConfig: OIDCConfiguration | undefined; /** * Indicates if we should enable oauth flow * * Default to `false` */ enableOauthFlow = false; private static readonly serialization = new SerializationFactory( createModelSchema(LegendStudioApplicationCoreOptions, { enableGraphBuilderStrictMode: optional(primitive()), typeAheadEnabled: optional(primitive()), projectCreationGroupIdSuggestion: optional(primitive()), TEMPORARY__preserveSectionIndex: optional(primitive()), TEMPORARY__enableCreationOfSandboxProjects: optional(primitive()), TEMPORARY__enableLocalConnectionBuilder: optional(primitive()), NonProductionFeatureFlag: optional(primitive()), TEMPORARY__serviceRegistrationConfig: list( object(ServiceRegistrationEnvironmentConfig), ), queryBuilderConfig: optional( usingModelSchema(QueryBuilderConfig.serialization.schema), ), ingestDeploymentConfig: optional( usingModelSchema(LegendIngestionConfiguration.serialization.schema), ), dataProductConfig: optional( usingModelSchema(DataProductConfig.serialization.schema), ), userSearchConfig: optional( usingModelSchema(UserSearchConfig.serialization.schema), ), reconciliationOidcConfig: optional( usingModelSchema(OIDCConfiguration.serialization.schema), ), enableOauthFlow: optional(primitive()), }), ); static create( configData: PlainObject, ): LegendStudioApplicationCoreOptions { return LegendStudioApplicationCoreOptions.serialization.fromJson( configData, ); } } export interface LegendStudioApplicationConfigurationData extends LegendApplicationConfigurationData { sdlc: { url: string; baseHeaders?: RequestHeaders; client?: string; /** * Enable the popup-based SDLC re-authentication flow. * * When `true`, Studio surfaces a "Re-authenticate with SDLC" action * and (on a mid-session 401) automatically opens a popup that completes * the OAuth dance against the SDLC server — preserving all in-memory * editor state instead of doing a top-level navigation that wipes it. * * The popup uses a same-origin static page as its OAuth `redirect_uri`, * derived at runtime from Studio's own origin + base URL (the same way * the boot-time `/auth/authorize` flow constructs its `redirect_uri`). * The deployment ships that page at `/popup-callback.html`; * deployments that enable this flag must register that derived URL on * the SDLC server's OAuth client allow-list. */ enablePopupReAuth?: boolean; /** * When `true`, the SDLC server is authenticated exclusively via the * session cookie, never via an `Authorization: Bearer` header. * * See `SDLCServerClientConfig.useCookieAuthOnly`. */ useCookieAuthOnly?: boolean; }; depot: { url: string }; engine: { url: string; queryUrl?: string; useCookieAuthOnly?: boolean; queryClientName?: string; }; query?: { url: string }; marketplace?: { url: string }; showcase?: { url: string }; pct?: { reportUrl: string }; legendAI?: { url: string }; /** * The lakehouse contract server, as Legend Query and Legend Marketplace * configure it: Studio reads the viewer's lakehouse environment from its * entitlements */ lakehouse?: { url: string }; } export class LegendStudioApplicationConfig extends LegendApplicationConfig { readonly options = new LegendStudioApplicationCoreOptions(); readonly engineServerUrl: string; readonly engineQueryServerUrl?: string | undefined; readonly engineQueryClientName?: string | undefined; readonly engineUseCookieAuthOnly: boolean; readonly depotServerUrl: string; readonly sdlcServerUrl: string; readonly sdlcServerBaseHeaders?: RequestHeaders | undefined; readonly sdlcServerClient?: string | undefined; readonly sdlcEnablePopupReAuth: boolean; readonly sdlcUseCookieAuthOnly: boolean; readonly queryApplicationUrl?: string | undefined; readonly marketplaceApplicationUrl?: string | undefined; readonly showcaseServerUrl?: string | undefined; readonly pctReportUrl?: string | undefined; readonly legendAIUrl?: string | undefined; readonly lakehouseContractUrl?: string | undefined; constructor( input: LegendApplicationConfigurationInput, ) { super(input); // engine assertNonNullable( input.configData.engine, `Can't configure application: 'engine' field is missing`, ); this.engineServerUrl = LegendApplicationConfig.resolveAbsoluteUrl( guaranteeNonEmptyString( input.configData.engine.url, `Can't configure application: 'engine.url' field is missing or empty`, ), ); if (input.configData.engine.queryUrl) { this.engineQueryServerUrl = LegendApplicationConfig.resolveAbsoluteUrl( input.configData.engine.queryUrl, ); } this.engineQueryClientName = input.configData.engine.queryClientName; this.engineUseCookieAuthOnly = Boolean( input.configData.engine.useCookieAuthOnly, ); // depot assertNonNullable( input.configData.depot, `Can't configure application: 'depot' field is missing`, ); this.depotServerUrl = LegendApplicationConfig.resolveAbsoluteUrl( guaranteeNonEmptyString( input.configData.depot.url, `Can't configure application: 'depot.url' field is missing or empty`, ), ); // sdlc assertNonNullable( input.configData.sdlc, `Can't configure application: 'sdlc' field is missing`, ); this.sdlcServerUrl = LegendApplicationConfig.resolveAbsoluteUrl( guaranteeNonEmptyString( input.configData.sdlc.url, `Can't configure application: 'sdlc.url' field is missing or empty`, ), ); this.sdlcServerBaseHeaders = input.configData.sdlc.baseHeaders; this.sdlcServerClient = input.configData.sdlc.client; this.sdlcEnablePopupReAuth = Boolean( input.configData.sdlc.enablePopupReAuth, ); this.sdlcUseCookieAuthOnly = Boolean( input.configData.sdlc.useCookieAuthOnly, ); // query if (input.configData.query?.url) { this.queryApplicationUrl = LegendApplicationConfig.resolveAbsoluteUrl( input.configData.query.url, ); } // marketplace if (input.configData.marketplace?.url) { this.marketplaceApplicationUrl = LegendApplicationConfig.resolveAbsoluteUrl( input.configData.marketplace.url, ); } // showcase if (input.configData.showcase?.url) { this.showcaseServerUrl = LegendApplicationConfig.resolveAbsoluteUrl( input.configData.showcase.url, ); } // pct if (input.configData.pct?.reportUrl) { this.pctReportUrl = LegendApplicationConfig.resolveAbsoluteUrl( input.configData.pct.reportUrl, ); } // legend AI if (input.configData.legendAI?.url) { this.legendAIUrl = LegendApplicationConfig.resolveAbsoluteUrl( input.configData.legendAI.url, ); } // lakehouse if (input.configData.lakehouse?.url) { this.lakehouseContractUrl = LegendApplicationConfig.resolveAbsoluteUrl( input.configData.lakehouse.url, ); } // options this.options = LegendStudioApplicationCoreOptions.create( input.configData.extensions?.core ?? {}, ); if (this.options.ingestDeploymentConfig) { validateIngestionDeploymentConfiguration( this.options.ingestDeploymentConfig, ); } } override getDefaultApplicationStorageKey(): string { return 'legend-studio'; } override getBackendEntries(): LegendApplicationBackendEntry[] { return [ { key: 'sdlc', label: 'SDLC Server', configPath: 'sdlc.url', field: 'sdlcServerUrl', }, { key: 'engine', label: 'Engine Server', configPath: 'engine.url', field: 'engineServerUrl', }, { key: 'engine-query', label: 'Engine Query Server', configPath: 'engine.queryUrl', field: 'engineQueryServerUrl', }, { key: 'depot', label: 'Depot Server', configPath: 'depot.url', field: 'depotServerUrl', }, { key: 'showcase', label: 'Showcase Server', configPath: 'showcase.url', field: 'showcaseServerUrl', }, { key: 'lakehouse-contract', label: 'Lakehouse Contract Server', configPath: 'lakehouse.url', field: 'lakehouseContractUrl', }, { key: 'legend-ai', label: 'Legend AI', configPath: 'legendAI.url', field: 'legendAIUrl', }, ]; } }