diff --git a/packages/cubejs-server-core/src/core/CompilerApi.js b/packages/cubejs-server-core/src/core/CompilerApi.ts similarity index 61% rename from packages/cubejs-server-core/src/core/CompilerApi.js rename to packages/cubejs-server-core/src/core/CompilerApi.ts index 0a40db37339b4..5998b394958de 100644 --- a/packages/cubejs-server-core/src/core/CompilerApi.js +++ b/packages/cubejs-server-core/src/core/CompilerApi.ts @@ -5,25 +5,153 @@ import { queryClass, PreAggregations, QueryFactory, - prepareCompiler + prepareCompiler, + BaseQuery, PrepareCompilerOptions, } from '@cubejs-backend/schema-compiler'; import { v4 as uuidv4, parse as uuidParse } from 'uuid'; import { LRUCache } from 'lru-cache'; import { NativeInstance } from '@cubejs-backend/native'; +import { SchemaFileRepository } from '@cubejs-backend/shared'; +import { BaseDriver } from '@cubejs-backend/query-orchestrator'; +import { + DbTypeAsyncFn, + DatabaseType, + DialectFactoryFn, + DriverContext, + DialectContext, + RequestContext, + LoggerFn, +} from './types'; + +export type CompilerApiOptions = PrepareCompilerOptions & { + dialectClass?: DialectFactoryFn; + logger?: LoggerFn; + preAggregationsSchema?: (context: RequestContext) => string | Promise; + allowUngroupedWithoutPrimaryKey?: boolean; + convertTzForRawTimeDimension?: boolean; + schemaVersion?: () => string | Promise; + contextToRoles?: (context: RequestContext) => string[] | Promise; + compileContext?: any; + sqlCache?: boolean; + standalone?: boolean; + compilerCacheSize?: number; + maxCompilerCacheKeepAlive?: number; + updateCompilerCacheKeepAlive?: boolean; + externalDialectClass?: typeof BaseQuery; + externalDbType?: DatabaseType; + devServer?: boolean; + fastReload?: boolean; +}; + +interface CompilersResult { + compiler: any; + metaTransformer: any; + cubeEvaluator: any; + contextEvaluator: any; + joinGraph: any; + compilerCache: any; + headCommitId: string; + compilerId: string; +} + +interface SqlGeneratorResult { + external: any; + sql: any; + lambdaQueries: any; + timeDimensionAlias?: string; + timeDimensionField?: string; + order: any; + cacheKeyQueries: any; + preAggregations: any; + dataSource: string; + aliasNameToMember: any; + rollupMatchResults?: any; + canUseTransformedQuery: any; + memberNames: string[]; +} + +interface ApplicablePolicy { + role: string; + conditions?: Array<{ if: any }>; + rowLevel?: { + filters?: any[]; + allowAll?: boolean; + }; + memberLevel?: { + includesMembers: string[]; + excludesMembers: string[]; + }; +} + +interface NestedFilter { + memberReference?: string; + member?: string; + operator?: string; + values?: any; + or?: NestedFilter[]; + and?: NestedFilter[]; +} + +interface CubeConfig { + name: string; + measures?: Array<{ name: string; isVisible?: boolean; public?: boolean }>; + dimensions?: Array<{ name: string; isVisible?: boolean; public?: boolean }>; + segments?: Array<{ name: string; isVisible?: boolean; public?: boolean }>; + hierarchies?: Array<{ name: string; isVisible?: boolean; public?: boolean }>; +} + +interface CubeWithConfig { + config: CubeConfig; +} export class CompilerApi { - /** - * Class constructor. - * @param {SchemaFileRepository} repository - * @param {DbTypeAsyncFn} dbType - * @param {*} options - */ - - constructor(repository, dbType, options) { + protected dialectClass?: DialectFactoryFn; + + protected options: CompilerApiOptions; + + protected allowNodeRequire: boolean; + + protected logger?: LoggerFn; + + protected preAggregationsSchema?: (context: RequestContext) => string | Promise; + + protected allowUngroupedWithoutPrimaryKey?: boolean; + + protected convertTzForRawTimeDimension?: boolean; + + protected schemaVersion?: () => string | Promise; + + protected contextToRoles?: (context: RequestContext) => string[] | Promise; + + protected allowJsDuplicatePropsInSchema?: boolean; + + protected sqlCache?: boolean; + + protected standalone?: boolean; + + protected nativeInstance: NativeInstance; + + protected compiledScriptCache: LRUCache; + + protected compiledScriptCacheInterval?: NodeJS.Timeout; + + protected graphqlSchema?: any; + + protected compilers?: Promise; + + protected compilerVersion?: string; + + protected queryFactory?: QueryFactory; + + public constructor( + protected readonly repository: SchemaFileRepository, + protected readonly dbType: DbTypeAsyncFn, + options: CompilerApiOptions = {} + ) { this.repository = repository; this.dbType = dbType; this.dialectClass = options.dialectClass; - this.options = options || {}; + this.options = options; this.allowNodeRequire = options.allowNodeRequire == null ? true : options.allowNodeRequire; this.logger = this.options.logger; this.preAggregationsSchema = this.options.preAggregationsSchema; @@ -31,7 +159,6 @@ export class CompilerApi { this.convertTzForRawTimeDimension = this.options.convertTzForRawTimeDimension; this.schemaVersion = this.options.schemaVersion; this.contextToRoles = this.options.contextToRoles; - this.compileContext = options.compileContext; this.allowJsDuplicatePropsInSchema = options.allowJsDuplicatePropsInSchema; this.sqlCache = options.sqlCache; this.standalone = options.standalone; @@ -42,7 +169,6 @@ export class CompilerApi { updateAgeOnGet: options.updateCompilerCacheKeepAlive }); - // proactively free up old cache values occasionally if (this.options.maxCompilerCacheKeepAlive) { this.compiledScriptCacheInterval = setInterval( () => this.compiledScriptCache.purgeStale(), @@ -51,25 +177,33 @@ export class CompilerApi { } } - dispose() { + public dispose(): void { if (this.compiledScriptCacheInterval) { clearInterval(this.compiledScriptCacheInterval); } } - setGraphQLSchema(schema) { + public setGraphQLSchema(schema: any): void { this.graphqlSchema = schema; } - getGraphQLSchema() { + public getGraphQLSchema(): any { return this.graphqlSchema; } - createNativeInstance() { + public setSchemaVersion(schemaVersion: () => string | Promise) { + this.schemaVersion = schemaVersion; + } + + public getOptions(): CompilerApiOptions { + return this.options; + } + + public createNativeInstance(): NativeInstance { return new NativeInstance(); } - async getCompilers({ requestId } = {}) { + public async getCompilers({ requestId }: { requestId?: string } = {}): Promise { let compilerVersion = ( this.schemaVersion && await this.schemaVersion() || 'default_schema_version' @@ -95,14 +229,10 @@ export class CompilerApi { return this.compilers; } - /** - * Creates the compilers instances without model compilation, - * because it could fail and no compilers will be returned. - */ - createCompilerInstances() { + public createCompilerInstances(): CompilersResult { return prepareCompiler(this.repository, { allowNodeRequire: this.allowNodeRequire, - compileContext: this.compileContext, + compileContext: this.options.compileContext, allowJsDuplicatePropsInSchema: this.allowJsDuplicatePropsInSchema, standalone: this.standalone, nativeInstance: this.nativeInstance, @@ -110,17 +240,17 @@ export class CompilerApi { }); } - async compileSchema(compilerVersion, requestId) { + public async compileSchema(compilerVersion: string, requestId?: string): Promise { const startCompilingTime = new Date().getTime(); try { - this.logger(this.compilers ? 'Recompiling schema' : 'Compiling schema', { + this.logger?.(this.compilers ? 'Recompiling schema' : 'Compiling schema', { version: compilerVersion, requestId }); const compilers = await compile(this.repository, { allowNodeRequire: this.allowNodeRequire, - compileContext: this.compileContext, + compileContext: this.options.compileContext, allowJsDuplicatePropsInSchema: this.allowJsDuplicatePropsInSchema, standalone: this.standalone, nativeInstance: this.nativeInstance, @@ -128,15 +258,15 @@ export class CompilerApi { }); this.queryFactory = await this.createQueryFactory(compilers); - this.logger('Compiling schema completed', { + this.logger?.('Compiling schema completed', { version: compilerVersion, requestId, duration: ((new Date()).getTime() - startCompilingTime), }); return compilers; - } catch (e) { - this.logger('Compiling schema error', { + } catch (e: any) { + this.logger?.('Compiling schema error', { version: compilerVersion, requestId, duration: ((new Date()).getTime() - startCompilingTime), @@ -146,7 +276,7 @@ export class CompilerApi { } } - async createQueryFactory(compilers) { + public async createQueryFactory(compilers: CompilersResult): Promise { const { cubeEvaluator } = compilers; const cubeToQueryClass = Object.fromEntries( @@ -162,15 +292,15 @@ export class CompilerApi { return new QueryFactory(cubeToQueryClass); } - async getDbType(dataSource = 'default') { - return this.dbType({ dataSource, }); + public async getDbType(dataSource: string = 'default'): Promise { + return this.dbType({ dataSource } as DriverContext); } - getDialectClass(dataSource = 'default', dbType) { - return this.dialectClass?.({ dataSource, dbType }); + public getDialectClass(dataSource: string = 'default', dbType: DatabaseType): any { + return this.dialectClass?.({ dataSource, dbType } as DialectContext); } - async getSqlGenerator(query, dataSource) { + public async getSqlGenerator(query: any, dataSource?: string): Promise<{ sqlGenerator: any; compilers: CompilersResult }> { const dbType = await this.getDbType(dataSource); const compilers = await this.getCompilers({ requestId: query.requestId }); let sqlGenerator = await this.createQueryByDataSource(compilers, query, dataSource, dbType); @@ -179,14 +309,10 @@ export class CompilerApi { throw new Error(`Unknown dbType: ${dbType}`); } - // sqlGenerator.dataSource can return undefined for query without members - // Queries like this are used by api-gateway to initialize SQL API - // At the same time, those queries should use concrete dataSource, so we should be good to go with it dataSource = compilers.compiler.withQuery(sqlGenerator, () => sqlGenerator.dataSource); if (dataSource !== undefined) { const _dbType = await this.getDbType(dataSource); if (dataSource !== 'default' && dbType !== _dbType) { - // TODO consider more efficient way than instantiating query sqlGenerator = await this.createQueryByDataSource( compilers, query, @@ -205,7 +331,10 @@ export class CompilerApi { return { sqlGenerator, compilers }; } - async getSql(query, options = {}) { + public async getSql( + query: any, + options: { includeDebugInfo?: boolean; exportAnnotatedSql?: boolean; requestId?: string } = {} + ): Promise { const { includeDebugInfo, exportAnnotatedSql } = options; const { sqlGenerator, compilers } = await this.getSqlGenerator(query); @@ -227,8 +356,7 @@ export class CompilerApi { })); if (this.sqlCache) { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { requestId, ...keyOptions } = query; + const { requestId: _requestId, ...keyOptions } = query; const key = { query: keyOptions, options }; return compilers.compilerCache.getQueryCache(key).cache(['sql'], getSqlFn); } else { @@ -236,18 +364,18 @@ export class CompilerApi { } } - async getRolesFromContext(context) { + public async getRolesFromContext(context: RequestContext): Promise> { if (!this.contextToRoles) { return new Set(); } return new Set(await this.contextToRoles(context)); } - userHasRole(userRoles, role) { + public userHasRole(userRoles: Set, role: string): boolean { return userRoles.has(role) || role === '*'; } - roleMeetsConditions(evaluatedConditions) { + public roleMeetsConditions(evaluatedConditions?: any[]): boolean { if (evaluatedConditions?.length) { return evaluatedConditions.reduce((a, b) => { if (typeof b !== 'boolean') { @@ -259,24 +387,28 @@ export class CompilerApi { return true; } - async getCubesFromQuery(query, context) { + public async getCubesFromQuery(query: any, context: RequestContext): Promise> { const sql = await this.getSql(query, { requestId: context.requestId }); return new Set(sql.memberNames.map(memberName => memberName.split('.')[0])); } - hashRequestContext(context) { + public hashRequestContext(context: any): string { if (!context.__hash) { context.__hash = crypto.createHash('md5').update(JSON.stringify(context)).digest('hex'); } return context.__hash; } - async getApplicablePolicies(cube, context, compilers) { + public async getApplicablePolicies( + cube: any, + context: RequestContext, + compilers: CompilersResult + ): Promise { const cache = compilers.compilerCache.getRbacCacheInstance(); const cacheKey = `${cube.name}_${this.hashRequestContext(context)}`; if (!cache.has(cacheKey)) { const userRoles = await this.getRolesFromContext(context); - const policies = cube.accessPolicy.filter(policy => { + const policies = cube.accessPolicy.filter((policy: ApplicablePolicy) => { const evaluatedConditions = (policy.conditions || []).map( condition => compilers.cubeEvaluator.evaluateContextFunction(cube, condition.if, context) ); @@ -288,9 +420,14 @@ export class CompilerApi { return cache.get(cacheKey); } - evaluateNestedFilter(filter, cube, context, cubeEvaluator) { - const result = { - }; + public evaluateNestedFilter( + filter: any, + cube: any, + context: RequestContext, + cubeEvaluator: any + ): NestedFilter { + const result: NestedFilter = {}; + if (filter.memberReference) { const evaluatedValues = cubeEvaluator.evaluateContextFunction( cube, @@ -302,25 +439,19 @@ export class CompilerApi { result.values = evaluatedValues; } if (filter.or) { - result.or = filter.or.map(f => this.evaluateNestedFilter(f, cube, context, cubeEvaluator)); + result.or = filter.or.map((f: any) => this.evaluateNestedFilter(f, cube, context, cubeEvaluator)); } if (filter.and) { - result.and = filter.and.map(f => this.evaluateNestedFilter(f, cube, context, cubeEvaluator)); + result.and = filter.and.map((f: any) => this.evaluateNestedFilter(f, cube, context, cubeEvaluator)); } return result; } - /** - * This method rewrites the query according to RBAC row level security policies. - * - * If RBAC is enabled, it looks at all the Cubes from the query with accessPolicy defined. - * It extracts all policies applicable to for the current user context (contextToRoles() + conditions). - * It then generates an rls filter by - * - combining all filters for the same role with AND - * - combining all filters for different roles with OR - * - combining cube and view filters with AND - */ - async applyRowLevelSecurity(query, evaluatedQuery, context) { + public async applyRowLevelSecurity( + query: any, + evaluatedQuery: any, + context: RequestContext + ): Promise<{ query: any; denied: boolean }> { const compilers = await this.getCompilers({ requestId: context.requestId }); const { cubeEvaluator } = compilers; @@ -330,11 +461,9 @@ export class CompilerApi { const queryCubes = await this.getCubesFromQuery(evaluatedQuery, context); - // We collect Cube and View filters separately because they have to be - // applied in "two layers": first Cube filters, then View filters on top - const cubeFiltersPerCubePerRole = {}; - const viewFiltersPerCubePerRole = {}; - const hasAllowAllForCube = {}; + const cubeFiltersPerCubePerRole: Record> = {}; + const viewFiltersPerCubePerRole: Record> = {}; + const hasAllowAllForCube: Record = {}; for (const cubeName of queryCubes) { const cube = cubeEvaluator.cubeFromPath(cubeName); @@ -346,7 +475,7 @@ export class CompilerApi { for (const policy of userPolicies) { hasRoleWithAccess = true; - (policy?.rowLevel?.filters || []).forEach(filter => { + (policy?.rowLevel?.filters || []).forEach((filter: any) => { filtersMap[cubeName] = filtersMap[cubeName] || {}; filtersMap[cubeName][policy.role] = filtersMap[cubeName][policy.role] || []; filtersMap[cubeName][policy.role].push( @@ -355,22 +484,17 @@ export class CompilerApi { }); if (!policy?.rowLevel || policy?.rowLevel?.allowAll) { hasAllowAllForCube[cubeName] = true; - // We don't have a way to add an "all alloed" filter like `WHERE 1 = 1` or something. - // Instead, we'll just mark that the user has "all" access to a given cube and remove - // all filters later break; } } if (!hasRoleWithAccess) { - // This is a hack that will make sure that the query returns no result query.segments = query.segments || []; query.segments.push({ expression: () => '1 = 0', cubeName: cube.name, name: 'rlsAccessDenied', }); - // If we hit this condition there's no need to evaluate the rest of the policy return { query, denied: true }; } } @@ -388,26 +512,27 @@ export class CompilerApi { return { query, denied: false }; } - removeEmptyFilters(filter) { + public removeEmptyFilters(filter: any): any { if (filter?.and) { - const and = filter.and.map(f => this.removeEmptyFilters(f)).filter(f => f); + const and = filter.and.map((f: any) => this.removeEmptyFilters(f)).filter((f: any) => f); return and.length > 1 ? { and } : and.at(0) || null; } if (filter?.or) { - const or = filter.or.map(f => this.removeEmptyFilters(f)).filter(f => f); + const or = filter.or.map((f: any) => this.removeEmptyFilters(f)).filter((f: any) => f); return or.length > 1 ? { or } : or.at(0) || null; } return filter; } - buildFinalRlsFilter(cubeFiltersPerCubePerRole, viewFiltersPerCubePerRole, hasAllowAllForCube) { - // - delete all filters for cubes where the user has allowAll - // - combine the rest into per role maps - // - join all filters for the same role with AND - // - join all filters for different roles with OR - // - join cube and view filters with AND - - const roleReducer = (filtersMap) => (acc, cubeName) => { + public buildFinalRlsFilter( + cubeFiltersPerCubePerRole: Record>, + viewFiltersPerCubePerRole: Record>, + hasAllowAllForCube: Record + ): any { + const roleReducer = (filtersMap: Record>) => ( + acc: Record, + cubeName: string + ): Record => { if (!hasAllowAllForCube[cubeName]) { Object.keys(filtersMap[cubeName]).forEach(role => { acc[role] = (acc[role] || []).concat(filtersMap[cubeName][role]); @@ -438,39 +563,48 @@ export class CompilerApi { }); } - async compilerCacheFn(requestId, key, path) { + public async compilerCacheFn( + requestId: string, + key: any, + path: string[] + ): Promise<(subKey: string[], cacheFn: () => any) => any> { const compilers = await this.getCompilers({ requestId }); if (this.sqlCache) { - return (subKey, cacheFn) => compilers.compilerCache.getQueryCache(key).cache(path.concat(subKey), cacheFn); + return (subKey: string[], cacheFn: () => any) => compilers.compilerCache.getQueryCache(key).cache(path.concat(subKey), cacheFn); } else { - return (subKey, cacheFn) => cacheFn(); + return (subKey: string[], cacheFn: () => any) => cacheFn(); } } - /** - * - * @param {unknown} filter - * @returns {Promise>} - */ - async preAggregations(filter) { + public async preAggregations(filter: any): Promise { const { cubeEvaluator } = await this.getCompilers(); return cubeEvaluator.preAggregations(filter); } - async scheduledPreAggregations() { + public async scheduledPreAggregations(): Promise { const { cubeEvaluator } = await this.getCompilers(); return cubeEvaluator.scheduledPreAggregations(); } - async createQueryByDataSource(compilers, query, dataSource, dbType) { + public async createQueryByDataSource( + compilers: CompilersResult, + query: any, + dataSource?: string, + dbType?: DatabaseType + ): Promise { if (!dbType) { - dbType = await this.getDbType(dataSource); + dbType = await this.getDbType(dataSource || 'default'); } - return this.createQuery(compilers, dbType, this.getDialectClass(dataSource, dbType), query); + return this.createQuery(compilers, dbType, this.getDialectClass(dataSource || 'default', dbType), query); } - createQuery(compilers, dbType, dialectClass, query) { + public createQuery( + compilers: CompilersResult, + dbType: DatabaseType, + dialectClass: any, + query: any + ): any { return createQuery( compilers, dbType, @@ -487,12 +621,12 @@ export class CompilerApi { ); } - /** - * if RBAC is enabled, this method is used to patch isVisible property of cube members - * based on access policies. - */ - async patchVisibilityByAccessPolicy(compilers, context, cubes) { - const isMemberVisibleInContext = {}; + public async patchVisibilityByAccessPolicy( + compilers: CompilersResult, + context: RequestContext, + cubes: CubeWithConfig[] + ): Promise<{ cubes: CubeWithConfig[]; visibilityMaskHash: string | null }> { + const isMemberVisibleInContext: Record = {}; const { cubeEvaluator } = compilers; if (!cubeEvaluator.isRbacEnabled()) { @@ -504,7 +638,7 @@ export class CompilerApi { if (cubeEvaluator.isRbacEnabledForCube(evaluatedCube)) { const applicablePolicies = await this.getApplicablePolicies(evaluatedCube, context, compilers); - const computeMemberVisibility = (item) => { + const computeMemberVisibility = (item: { name: string }): boolean => { for (const policy of applicablePolicies) { if (policy.memberLevel) { if (policy.memberLevel.includesMembers.includes(item.name) && @@ -512,37 +646,36 @@ export class CompilerApi { return true; } } else { - // If there's no memberLevel policy, we assume that all members are visible return true; } } return false; }; - for (const dimension of cube.config.dimensions) { + for (const dimension of cube.config.dimensions || []) { isMemberVisibleInContext[dimension.name] = computeMemberVisibility(dimension); } - for (const measure of cube.config.measures) { + for (const measure of cube.config.measures || []) { isMemberVisibleInContext[measure.name] = computeMemberVisibility(measure); } - for (const segment of cube.config.segments) { + for (const segment of cube.config.segments || []) { isMemberVisibleInContext[segment.name] = computeMemberVisibility(segment); } - for (const hierarchy of cube.config.hierarchies) { + for (const hierarchy of cube.config.hierarchies || []) { isMemberVisibleInContext[hierarchy.name] = computeMemberVisibility(hierarchy); } } } - const visibilityPatcherForCube = (cube) => { + const visibilityPatcherForCube = (cube: CubeWithConfig) => { const evaluatedCube = cubeEvaluator.cubeFromPath(cube.config.name); if (!cubeEvaluator.isRbacEnabledForCube(evaluatedCube)) { - return (item) => item; + return (item: any) => item; } - return (item) => ({ + return (item: any) => ({ ...item, isVisible: item.isVisible && isMemberVisibleInContext[item.name], public: item.public && isMemberVisibleInContext[item.name] @@ -550,8 +683,6 @@ export class CompilerApi { }; const visibiliyMask = JSON.stringify(isMemberVisibleInContext, Object.keys(isMemberVisibleInContext).sort()); - // This hash will be returned along the modified meta config and can be used - // to distinguish between different "schema versions" after DAP visibility is applied const visibilityMaskHash = crypto.createHash('sha256').update(visibiliyMask).digest('hex'); return { @@ -569,14 +700,23 @@ export class CompilerApi { }; } - mixInVisibilityMaskHash(compilerId, visibilityMaskHash) { + public mixInVisibilityMaskHash(compilerId: string, visibilityMaskHash: string): string { const uuidBytes = uuidParse(compilerId); const hashBytes = Buffer.from(visibilityMaskHash, 'hex'); - return uuidv4({ random: crypto.createHash('sha256').update(uuidBytes).update(hashBytes).digest() - .subarray(0, 16) }); + + return uuidv4({ + random: crypto.createHash('sha256') + .update(uuidBytes as any) + .update(hashBytes) + .digest() + .subarray(0, 16) + }); } - async metaConfig(requestContext, options = {}) { + public async metaConfig( + requestContext: RequestContext, + options: { includeCompilerId?: boolean; requestId?: string } = {} + ): Promise { const { includeCompilerId, ...restOptions } = options; const compilers = await this.getCompilers(restOptions); const { cubes } = compilers.metaTransformer; @@ -588,17 +728,18 @@ export class CompilerApi { if (includeCompilerId) { return { cubes: patchedCubes, - // This compilerId is primarily used by the cubejs-backend-native or caching purposes. - // By default it doesn't account for member visibility changes introduced above by DAP. - // Here we're modifying the originila compilerId in a way that it's distinct for - // distinct schema versions while still being a valid UUID. - compilerId: visibilityMaskHash ? this.mixInVisibilityMaskHash(compilers.compilerId, visibilityMaskHash) : compilers.compilerId, + compilerId: visibilityMaskHash ? + this.mixInVisibilityMaskHash(compilers.compilerId, visibilityMaskHash) : + compilers.compilerId, }; } return patchedCubes; } - async metaConfigExtended(requestContext, options) { + public async metaConfigExtended( + requestContext: RequestContext, + options: { requestId?: string } + ): Promise<{ metaConfig: CubeWithConfig[]; cubeDefinitions: any }> { const compilers = await this.getCompilers(options); const { cubes: patchedCubes } = await this.patchVisibilityByAccessPolicy( compilers, @@ -611,11 +752,11 @@ export class CompilerApi { }; } - async compilerId(options = {}) { + public async compilerId(options: { requestId?: string } = {}): Promise { return (await this.getCompilers(options)).compilerId; } - async cubeNameToDataSource(query) { + public async cubeNameToDataSource(query: { requestId?: string }): Promise> { const { cubeEvaluator } = await this.getCompilers({ requestId: query.requestId }); return cubeEvaluator .cubeNames() @@ -624,7 +765,7 @@ export class CompilerApi { ).reduce((a, b) => ({ ...a, ...b }), {}); } - async memberToDataSource(query) { + public async memberToDataSource(query: { requestId?: string }): Promise> { const { cubeEvaluator } = await this.getCompilers({ requestId: query.requestId }); const entries = cubeEvaluator @@ -633,7 +774,7 @@ export class CompilerApi { const cubeDef = cubeEvaluator.cubeFromPath(cube); if (cubeDef.isView) { const viewName = cubeDef.name; - return cubeDef.includedMembers.map(included => { + return cubeDef.includedMembers.map((included: any) => { const memberName = `${viewName}.${included.name}`; const refCubeDef = cubeEvaluator.cubeFromPath(included.memberPath); const dataSource = refCubeDef.dataSource ?? 'default'; @@ -652,14 +793,17 @@ export class CompilerApi { return Object.fromEntries(entries); } - async dataSources(orchestratorApi, query) { + public async dataSources( + orchestratorApi: { driverFactory: (dataSource: string) => Promise }, + query?: { requestId?: string } + ): Promise<{ dataSources: Array<{ dataSource: string; dbType: DatabaseType }> }> { const cubeNameToDataSource = await this.cubeNameToDataSource(query || { requestId: `datasources-${uuidv4()}` }); let dataSources = Object.keys(cubeNameToDataSource).map(c => cubeNameToDataSource[c]); dataSources = [...new Set(dataSources)]; - dataSources = await Promise.all( + const dataSourcesWithTypes = await Promise.all( dataSources.map(async (dataSource) => { try { await orchestratorApi.driverFactory(dataSource); @@ -672,11 +816,11 @@ export class CompilerApi { ); return { - dataSources: dataSources.filter((source) => source), + dataSources: dataSourcesWithTypes.filter((source): source is { dataSource: string; dbType: DatabaseType } => source !== null), }; } - canUsePreAggregationForTransformedQuery(transformedQuery, refs) { + public canUsePreAggregationForTransformedQuery(transformedQuery: any, refs: any): any { return PreAggregations.canUsePreAggregationForTransformedQueryFn(transformedQuery, refs); } } diff --git a/packages/cubejs-server-core/src/core/server.ts b/packages/cubejs-server-core/src/core/server.ts index 1db5966d5af31..7e22477b5e9bb 100644 --- a/packages/cubejs-server-core/src/core/server.ts +++ b/packages/cubejs-server-core/src/core/server.ts @@ -548,7 +548,8 @@ export class CubejsServerCore { this.compilerCache.set(appId, compilerApi); } - compilerApi.schemaVersion = currentSchemaVersion; + compilerApi.setSchemaVersion(currentSchemaVersion); + return compilerApi; } diff --git a/packages/cubejs-server-core/src/core/types.ts b/packages/cubejs-server-core/src/core/types.ts index a4b5c749144f1..bdb831789744c 100644 --- a/packages/cubejs-server-core/src/core/types.ts +++ b/packages/cubejs-server-core/src/core/types.ts @@ -128,6 +128,7 @@ export type ContextToCubeStoreRouterIdFn = (context: RequestContext) => string | export type OrchestratorOptionsFn = (context: RequestContext) => OrchestratorOptions | Promise; export type PreAggregationsSchemaFn = (context: RequestContext) => string | Promise; +export type SchemaVersionFn = (context: RequestContext) => string | Promise; export type ScheduledRefreshTimeZonesFn = (context: RequestContext) => string[] | Promise; @@ -208,7 +209,7 @@ export interface CreateOptions { queryTransformer?: QueryRewriteFn; queryRewrite?: QueryRewriteFn; preAggregationsSchema?: string | PreAggregationsSchemaFn; - schemaVersion?: (context: RequestContext) => string | Promise; + schemaVersion?: SchemaVersionFn; extendContext?: ExtendContextFn; scheduledRefreshTimer?: boolean | number; scheduledRefreshTimeZones?: string[] | ScheduledRefreshTimeZonesFn; diff --git a/packages/cubejs-server-core/test/unit/index.test.ts b/packages/cubejs-server-core/test/unit/index.test.ts index 5ff8398240763..2a8c6f1ab10c4 100644 --- a/packages/cubejs-server-core/test/unit/index.test.ts +++ b/packages/cubejs-server-core/test/unit/index.test.ts @@ -338,7 +338,7 @@ describe('index.test', () => { securityContext: null, requestId: 'XXX' }); - expect(compilerApi.options.allowNodeRequire).toStrictEqual(false); + expect(compilerApi.getOptions().allowNodeRequire).toStrictEqual(false); await cubejsServerCore.releaseConnections(); }); @@ -354,7 +354,7 @@ describe('index.test', () => { const metaConfigExtendedSpy = jest.spyOn(compilerApi, 'metaConfigExtended'); test('CompilerApi metaConfig', async () => { - const metaConfig = await compilerApi.metaConfig({ securityContext: {} }, { requestId: 'XXX' }); + const metaConfig = await compilerApi.metaConfig({ securityContext: {}, requestId: 'XXX' }, { requestId: 'XXX' }); expect((metaConfig)?.length).toBeGreaterThan(0); expect(metaConfig[0]).toHaveProperty('config'); expect(metaConfig[0].config.hasOwnProperty('sql')).toBe(false); @@ -363,7 +363,7 @@ describe('index.test', () => { }); test('CompilerApi metaConfigExtended', async () => { - const metaConfigExtended = await compilerApi.metaConfigExtended({ securityContext: {} }, { requestId: 'XXX' }); + const metaConfigExtended = await compilerApi.metaConfigExtended({ securityContext: {}, requestId: 'XXX' }, { requestId: 'XXX' }); expect(metaConfigExtended).toHaveProperty('metaConfig'); expect(metaConfigExtended.metaConfig.length).toBeGreaterThan(0); expect(metaConfigExtended).toHaveProperty('cubeDefinitions'); @@ -383,14 +383,14 @@ describe('index.test', () => { const metaConfigExtendedSpy = jest.spyOn(compilerApi, 'metaConfigExtended'); test('CompilerApi metaConfig', async () => { - const metaConfig = await compilerApi.metaConfig({ securityContext: {} }, { requestId: 'XXX' }); + const metaConfig = await compilerApi.metaConfig({ securityContext: {}, requestId: 'XXX' }, { requestId: 'XXX' }); expect(metaConfig).toEqual([]); expect(metaConfigSpy).toHaveBeenCalled(); metaConfigSpy.mockClear(); }); test('CompilerApi metaConfigExtended', async () => { - const metaConfigExtended = await compilerApi.metaConfigExtended({ securityContext: {} }, { requestId: 'XXX' }); + const metaConfigExtended = await compilerApi.metaConfigExtended({ securityContext: {}, requestId: 'XXX' }, { requestId: 'XXX' }); expect(metaConfigExtended).toHaveProperty('metaConfig'); expect(metaConfigExtended.metaConfig).toEqual([]); expect(metaConfigExtended).toHaveProperty('cubeDefinitions'); @@ -401,7 +401,7 @@ describe('index.test', () => { test('CompilerApi dataSources default', async () => { const dataSources = await compilerApi.dataSources({ - driverFactory: jest.fn(async () => true) + driverFactory: jest.fn(async () => ({} as any)) }); expect(dataSources).toHaveProperty('dataSources'); @@ -420,7 +420,7 @@ describe('index.test', () => { const dataSourcesSpy = jest.spyOn(compilerApi, 'dataSources'); test('CompilerApi dataSources', async () => { const dataSources = await compilerApi.dataSources({ - driverFactory: jest.fn(async () => true) + driverFactory: jest.fn(async () => ({} as any)) }); expect(dataSources).toHaveProperty('dataSources');