diff --git a/packages/cubejs-schema-compiler/src/compiler/ContextEvaluator.js b/packages/cubejs-schema-compiler/src/compiler/ContextEvaluator.js deleted file mode 100644 index a3a03b3652cd1..0000000000000 --- a/packages/cubejs-schema-compiler/src/compiler/ContextEvaluator.js +++ /dev/null @@ -1,35 +0,0 @@ -import R from 'ramda'; - -export class ContextEvaluator { - /** - * @param {import('./CubeEvaluator').CubeEvaluator} cubeEvaluator - */ - constructor(cubeEvaluator) { - this.cubeEvaluator = cubeEvaluator; - this.contextDefinitions = {}; - } - - // eslint-disable-next-line no-unused-vars - compile(contexts, _errorReporter) { - if (contexts.length === 0) { - return; - } - - // TODO: handle duplications, context names must be uniq - this.contextDefinitions = R.compose( - R.fromPairs, - R.map(v => [v.name, this.compileContext(v)]) - )(contexts); - } - - compileContext(context) { - return { - name: context.name, - contextMembers: this.cubeEvaluator.evaluateReferences(null, context.contextMembers) - }; - } - - get contextList() { - return R.keys(this.contextDefinitions); - } -} diff --git a/packages/cubejs-schema-compiler/src/compiler/ContextEvaluator.ts b/packages/cubejs-schema-compiler/src/compiler/ContextEvaluator.ts new file mode 100644 index 0000000000000..c4cb1a7e2b14e --- /dev/null +++ b/packages/cubejs-schema-compiler/src/compiler/ContextEvaluator.ts @@ -0,0 +1,50 @@ +import type { CubeEvaluator } from './CubeEvaluator'; +import type { ErrorReporter } from './ErrorReporter'; +import { CompilerInterface } from './PrepareCompiler'; + +export interface ContextInput { + name: string; + contextMembers: any; +} + +export interface CompiledContext { + name: string; + contextMembers: any; +} + +export class ContextEvaluator implements CompilerInterface { + private readonly cubeEvaluator: CubeEvaluator; + + private contextDefinitions: Map; + + public constructor(cubeEvaluator: CubeEvaluator) { + this.cubeEvaluator = cubeEvaluator; + this.contextDefinitions = new Map(); + } + + public compile(contexts: ContextInput[], errorReporter?: ErrorReporter): void { + if (contexts.length === 0) { + return; + } + + this.contextDefinitions = new Map(); + for (const context of contexts) { + if (errorReporter && this.contextDefinitions.has(context.name)) { + errorReporter.error(`Context "${context.name}" already exists!`); + } else { + this.contextDefinitions.set(context.name, this.compileContext(context)); + } + } + } + + private compileContext(context: ContextInput): CompiledContext { + return { + name: context.name, + contextMembers: this.cubeEvaluator.evaluateReferences(null, context.contextMembers) + }; + } + + public get contextList(): string[] { + return Array.from(this.contextDefinitions.keys()); + } +} diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeDictionary.js b/packages/cubejs-schema-compiler/src/compiler/CubeDictionary.js deleted file mode 100644 index d2691f35a932a..0000000000000 --- a/packages/cubejs-schema-compiler/src/compiler/CubeDictionary.js +++ /dev/null @@ -1,20 +0,0 @@ -import R from 'ramda'; - -export class CubeDictionary { - constructor() { - this.byId = {}; - } - - // eslint-disable-next-line no-unused-vars - compile(cubes, errorReporter) { - this.byId = R.fromPairs(cubes.map(v => [v.name, v])); - } - - resolveCube(cubeName) { - return this.byId[cubeName]; - } - - free() { - this.byId = {}; - } -} diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeDictionary.ts b/packages/cubejs-schema-compiler/src/compiler/CubeDictionary.ts new file mode 100644 index 0000000000000..ec6fabfb9cfb3 --- /dev/null +++ b/packages/cubejs-schema-compiler/src/compiler/CubeDictionary.ts @@ -0,0 +1,35 @@ +import type { ErrorReporter } from './ErrorReporter'; +import { TranspilerCubeResolver } from './transpilers'; +import { CompilerInterface } from './PrepareCompiler'; + +export interface Cube { + name: string; + [key: string]: any; +} + +export class CubeDictionary implements TranspilerCubeResolver, CompilerInterface { + private byId: Map; + + public constructor() { + this.byId = new Map(); + } + + public compile(cubes: Cube[], _errorReporter?: ErrorReporter): void { + this.byId = new Map(); + for (const cube of cubes) { + this.byId.set(cube.name, cube); + } + } + + public resolveCube(name: string): Cube | undefined { + return this.byId.get(name); + } + + public free(): void { + this.byId = new Map(); + } + + public cubeNames(): string[] { + return Array.from(this.byId.keys()); + } +} diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts index 49aac8d7ea388..cac241dc0eaa3 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts @@ -8,11 +8,13 @@ import { camelizeCube } from './utils'; import type { ErrorReporter } from './ErrorReporter'; import { TranspilerSymbolResolver } from './transpilers'; +import { CompilerInterface } from './PrepareCompiler'; export type ToString = { toString(): string }; export type GranularityDefinition = { sql?: (...args: any[]) => string; + name?: string; title?: string; interval?: string; offset?: string; @@ -139,6 +141,17 @@ export type ViewIncludedMember = { name: string; }; +export type FolderMember = { + type?: 'folder'; + name: string; + includes?: FolderMember[]; +}; + +export type Folder = { + name: string; + includes: FolderMember[]; +}; + export interface CubeDefinition { name: string; extends?: (...args: Array) => { __cubeName: string }; @@ -158,7 +171,7 @@ export interface CubeDefinition { accessPolicy?: AccessPolicyDefinition[]; // eslint-disable-next-line camelcase access_policy?: any[]; - folders?: any[]; + folders?: Folder[]; includes?: any; excludes?: any; cubes?: any; @@ -215,7 +228,7 @@ export const CONTEXT_SYMBOLS = { export const CURRENT_CUBE_CONSTANTS = ['CUBE', 'TABLE']; -export class CubeSymbols implements TranspilerSymbolResolver { +export class CubeSymbols implements TranspilerSymbolResolver, CompilerInterface { public symbols: Record; private builtCubes: Record; @@ -913,9 +926,7 @@ export class CubeSymbols implements TranspilerSymbolResolver { * refactoring. */ protected evaluateContextFunction(cube: any, contextFn: any, context: any = {}) { - const cubeEvaluator = this; - - return cubeEvaluator.resolveSymbolsCall(contextFn, (name: string) => { + return this.resolveSymbolsCall(contextFn, (name: string) => { const resolvedSymbol = this.resolveSymbol(cube, name); if (resolvedSymbol) { return resolvedSymbol; @@ -930,7 +941,7 @@ export class CubeSymbols implements TranspilerSymbolResolver { }); } - protected evaluateReferences>( + public evaluateReferences>( cube: string | null, referencesFn: (...args: Array) => T, options: { collectJoinHints?: boolean, originalSorting?: boolean } = {} diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeToMetaTransformer.js b/packages/cubejs-schema-compiler/src/compiler/CubeToMetaTransformer.js deleted file mode 100644 index 91bfe840cc74b..0000000000000 --- a/packages/cubejs-schema-compiler/src/compiler/CubeToMetaTransformer.js +++ /dev/null @@ -1,243 +0,0 @@ -import inflection from 'inflection'; -import R from 'ramda'; -import camelCase from 'camelcase'; - -import { getEnv } from '@cubejs-backend/shared'; -import { CubeSymbols } from './CubeSymbols'; -import { UserError } from './UserError'; -import { BaseMeasure } from '../adapter'; - -export class CubeToMetaTransformer { - /** - * @param {import('./CubeValidator').CubeValidator} cubeValidator - * @param {import('./CubeEvaluator').CubeEvaluator} cubeEvaluator - * @param {import('./ContextEvaluator').ContextEvaluator} contextEvaluator - * @param {import('./JoinGraph').JoinGraph} joinGraph - */ - constructor(cubeValidator, cubeEvaluator, contextEvaluator, joinGraph) { - this.cubeValidator = cubeValidator; - this.cubeSymbols = cubeEvaluator; - this.cubeEvaluator = cubeEvaluator; - this.contextEvaluator = contextEvaluator; - this.joinGraph = joinGraph; - this.cubes = []; - } - - compile(cubes, errorReporter) { - this.cubes = this.cubeSymbols.cubeList - .filter(this.cubeValidator.isCubeValid.bind(this.cubeValidator)) - .map((v) => this.transform(v, errorReporter.inContext(`${v.name} cube`))) - .filter(Boolean); - - /** - * @deprecated - * @protected - */ - this.queries = this.cubes; - } - - /** - * @protected - */ - transform(cube) { - const cubeTitle = cube.title || this.titleize(cube.name); - - const isCubeVisible = this.isVisible(cube, true); - - const flatFolderSeparator = getEnv('nestedFoldersDelimiter'); - const flatFolders = []; - - const processFolder = (folder, path = [], mergedMembers = []) => { - const flatMembers = []; - const nestedMembers = folder.includes.map(member => { - if (member.type === 'folder') { - return processFolder(member, [...path, folder.name], flatMembers); - } - const memberName = `${cube.name}.${member.name}`; - flatMembers.push(memberName); - - return memberName; - }); - - if (flatFolderSeparator !== '') { - flatFolders.push({ - name: [...path, folder.name].join(flatFolderSeparator), - members: flatMembers, - }); - } else if (path.length > 0) { - mergedMembers.push(...flatMembers); - } else { // We're at the root level - flatFolders.push({ - name: folder.name, - members: [...new Set(flatMembers)], - }); - } - - return { - name: folder.name, - members: nestedMembers, - }; - }; - - const nestedFolders = (cube.folders || []).map(f => processFolder(f)); - - return { - config: { - name: cube.name, - type: cube.isView ? 'view' : 'cube', - title: cubeTitle, - isVisible: isCubeVisible, - public: isCubeVisible, - description: cube.description, - connectedComponent: this.joinGraph.connectedComponents()[cube.name], - meta: cube.meta, - measures: R.compose( - R.map((nameToMetric) => ({ - ...this.measureConfig(cube.name, cubeTitle, nameToMetric), - isVisible: isCubeVisible ? this.isVisible(nameToMetric[1], true) : false, - public: isCubeVisible ? this.isVisible(nameToMetric[1], true) : false, - })), - R.toPairs - )(cube.measures || {}), - dimensions: R.compose( - R.map((nameToDimension) => ({ - name: `${cube.name}.${nameToDimension[0]}`, - title: this.title(cubeTitle, nameToDimension), - type: this.dimensionDataType(nameToDimension[1].type), - description: nameToDimension[1].description, - shortTitle: this.title(cubeTitle, nameToDimension, true), - suggestFilterValues: - nameToDimension[1].suggestFilterValues == null - ? true - : nameToDimension[1].suggestFilterValues, - format: nameToDimension[1].format, - meta: nameToDimension[1].meta, - isVisible: isCubeVisible - ? this.isVisible(nameToDimension[1], !nameToDimension[1].primaryKey) - : false, - public: isCubeVisible - ? this.isVisible(nameToDimension[1], !nameToDimension[1].primaryKey) - : false, - primaryKey: !!nameToDimension[1].primaryKey, - aliasMember: nameToDimension[1].aliasMember, - granularities: - nameToDimension[1].granularities - ? R.compose(R.map((g) => ({ - name: g[0], - title: this.title(cubeTitle, g, true), - interval: g[1].interval, - offset: g[1].offset, - origin: g[1].origin, - })), R.toPairs)(nameToDimension[1].granularities) - : undefined, - })), - R.toPairs - )(cube.dimensions || {}), - segments: R.compose( - R.map((nameToSegment) => ({ - name: `${cube.name}.${nameToSegment[0]}`, - title: this.title(cubeTitle, nameToSegment), - shortTitle: this.title(cubeTitle, nameToSegment, true), - description: nameToSegment[1].description, - meta: nameToSegment[1].meta, - isVisible: isCubeVisible ? this.isVisible(nameToSegment[1], true) : false, - public: isCubeVisible ? this.isVisible(nameToSegment[1], true) : false, - })), - R.toPairs - )(cube.segments || {}), - hierarchies: (cube.evaluatedHierarchies || []).map((it) => ({ - ...it, - aliasMember: it.aliasMember, - public: it.public ?? true, - name: `${cube.name}.${it.name}`, - })), - folders: flatFolders, - nestedFolders, - }, - }; - } - - queriesForContext(contextId) { - // return All queries if no context pass - if (R.isNil(contextId) || R.isEmpty(contextId)) { - return this.queries; - } - - const context = this.contextEvaluator.contextDefinitions[contextId]; - - // If contextId is wrong - if (R.isNil(context)) { - throw new UserError(`Context ${contextId} doesn't exist`); - } - - // As for now context works on the cubes level - return R.filter( - (query) => R.includes(query.config.name, context.contextMembers) - )(this.queries); - } - - /** - * @protected - */ - isVisible(symbol, defaultValue) { - if (symbol.public != null) { - return symbol.public; - } - - // TODO: Deprecated, should be removed in the future - if (symbol.visible != null) { - return symbol.visible; - } - - // TODO: Deprecated, should be removed in the futur - if (symbol.shown != null) { - return symbol.shown; - } - - return defaultValue; - } - - dimensionDataType(dimensionType) { - return dimensionType === 'switch' ? 'string' : dimensionType; - } - - measureConfig(cubeName, cubeTitle, nameToMetric) { - const name = `${cubeName}.${nameToMetric[0]}`; - // Support both old 'drillMemberReferences' and new 'drillMembers' keys - const drillMembers = nameToMetric[1].drillMembers || nameToMetric[1].drillMemberReferences; - - const drillMembersArray = (drillMembers && this.cubeEvaluator.evaluateReferences( - cubeName, drillMembers, { originalSorting: true } - )) || []; - - const type = CubeSymbols.toMemberDataType(nameToMetric[1].type); - - return { - name, - title: this.title(cubeTitle, nameToMetric), - description: nameToMetric[1].description, - shortTitle: this.title(cubeTitle, nameToMetric, true), - format: nameToMetric[1].format, - cumulativeTotal: nameToMetric[1].cumulative || BaseMeasure.isCumulative(nameToMetric[1]), - cumulative: nameToMetric[1].cumulative || BaseMeasure.isCumulative(nameToMetric[1]), - type, - aggType: nameToMetric[1].aggType || nameToMetric[1].type, - drillMembers: drillMembersArray, - drillMembersGrouped: { - measures: drillMembersArray.filter((member) => this.cubeEvaluator.isMeasure(member)), - dimensions: drillMembersArray.filter((member) => this.cubeEvaluator.isDimension(member)), - }, - aliasMember: nameToMetric[1].aliasMember, - meta: nameToMetric[1].meta - }; - } - - title(cubeTitle, nameToDef, short) { - // eslint-disable-next-line prefer-template - return `${short ? '' : cubeTitle + ' '}${nameToDef[1].title || this.titleize(nameToDef[0])}`; - } - - titleize(name) { - return inflection.titleize(inflection.underscore(camelCase(name, { pascalCase: true }))); - } -} diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeToMetaTransformer.ts b/packages/cubejs-schema-compiler/src/compiler/CubeToMetaTransformer.ts new file mode 100644 index 0000000000000..036a102f680d8 --- /dev/null +++ b/packages/cubejs-schema-compiler/src/compiler/CubeToMetaTransformer.ts @@ -0,0 +1,393 @@ +import inflection from 'inflection'; +import camelCase from 'camelcase'; + +import { getEnv } from '@cubejs-backend/shared'; +import { + CubeSymbolDefinition, + CubeSymbols, + Folder, + FolderMember, + GranularityDefinition, +} from './CubeSymbols'; +import { UserError } from './UserError'; +import { BaseMeasure } from '../adapter'; +import type { CubeDefinitionExtended } from './CubeSymbols'; +import type { CubeValidator } from './CubeValidator'; +import type { CubeEvaluator } from './CubeEvaluator'; +import type { ContextEvaluator } from './ContextEvaluator'; +import type { JoinGraph } from './JoinGraph'; +import type { ErrorReporter } from './ErrorReporter'; +import { CompilerInterface } from './PrepareCompiler'; + +// Extended types for cube symbols with all runtime properties +export interface ExtendedCubeSymbolDefinition extends CubeSymbolDefinition { + description?: string; + meta?: any; + title?: string; + public?: boolean; + visible?: boolean; + shown?: boolean; + suggestFilterValues?: boolean; + aliasMember?: string; + drillMembers?: any; + drillMemberReferences?: any; + cumulative?: boolean; + aggType?: string; +} + +interface ExtendedCubeDefinition extends CubeDefinitionExtended { + title?: string; + description?: string; + meta?: any; + evaluatedHierarchies?: Array<{ + name: string; + public?: boolean; + [key: string]: any; + }>; +} + +export type FlatFolder = { + name: string; + members: string[]; +}; + +export type NestedFolder = { + name: string; + members: Array; +}; + +export type MeasureConfig = { + name: string; + title: string; + description?: string; + shortTitle: string; + format?: string; + cumulativeTotal: boolean; + cumulative: boolean; + type: string; + aggType: string; + drillMembers: string[]; + drillMembersGrouped: { + measures: string[]; + dimensions: string[]; + }; + aliasMember?: string; + meta?: any; + isVisible: boolean; + public: boolean; +}; + +export type DimensionConfig = { + name: string; + title: string; + type: string; + description?: string; + shortTitle: string; + suggestFilterValues: boolean; + format?: string; + meta?: any; + isVisible: boolean; + public: boolean; + primaryKey: boolean; + aliasMember?: string; + granularities?: GranularityDefinition[]; +}; + +export type SegmentConfig = { + name: string; + title: string; + shortTitle: string; + description?: string; + meta?: any; + isVisible: boolean; + public: boolean; +}; + +export type HierarchyConfig = { + name: string; + public: boolean; + [key: string]: any; +}; + +export type CubeConfig = { + name: string; + type: 'view' | 'cube'; + title: string; + isVisible: boolean; + public: boolean; + description?: string; + connectedComponent: number; + meta?: any; + measures: MeasureConfig[]; + dimensions: DimensionConfig[]; + segments: SegmentConfig[]; + hierarchies: HierarchyConfig[]; + folders: FlatFolder[]; + nestedFolders: NestedFolder[]; +}; + +export type TransformedCube = { + config: CubeConfig; +}; + +export class CubeToMetaTransformer implements CompilerInterface { + private readonly cubeValidator: CubeValidator; + + private readonly cubeSymbols: CubeEvaluator; + + private readonly cubeEvaluator: CubeEvaluator; + + private readonly contextEvaluator: ContextEvaluator; + + private readonly joinGraph: JoinGraph; + + public cubes: TransformedCube[]; + + /** + * @deprecated + */ + public queries: TransformedCube[]; + + public constructor( + cubeValidator: CubeValidator, + cubeEvaluator: CubeEvaluator, + contextEvaluator: ContextEvaluator, + joinGraph: JoinGraph + ) { + this.cubeValidator = cubeValidator; + this.cubeSymbols = cubeEvaluator; + this.cubeEvaluator = cubeEvaluator; + this.contextEvaluator = contextEvaluator; + this.joinGraph = joinGraph; + this.cubes = []; + this.queries = []; + } + + public compile(_cubes: any[], errorReporter: ErrorReporter): void { + this.cubes = this.cubeSymbols.cubeList + .filter(this.cubeValidator.isCubeValid.bind(this.cubeValidator)) + .map((v) => this.transform(v, errorReporter.inContext(`${v.name} cube`))); + + this.queries = this.cubes; + } + + protected transform(cube: CubeDefinitionExtended, _errorReporter?: ErrorReporter): TransformedCube { + const extendedCube = cube as ExtendedCubeDefinition; + const cubeName = extendedCube.name; + const cubeTitle = extendedCube.title || this.titleize(cubeName); + + const isCubeVisible = this.isVisible(extendedCube, true); + + const flatFolderSeparator = getEnv('nestedFoldersDelimiter'); + const flatFolders: FlatFolder[] = []; + + const processFolder = (folder: Folder, path: string[] = [], mergedMembers: string[] = []): NestedFolder => { + const flatMembers: string[] = []; + const nestedMembers: Array = folder.includes.map((member: FolderMember) => { + if (member.type === 'folder' && member.includes) { + return processFolder(member as Folder, [...path, folder.name], flatMembers); + } + const memberName = `${cubeName}.${member.name}`; + flatMembers.push(memberName); + + return memberName; + }); + + if (flatFolderSeparator !== '') { + flatFolders.push({ + name: [...path, folder.name].join(flatFolderSeparator), + members: flatMembers, + }); + } else if (path.length > 0) { + mergedMembers.push(...flatMembers); + } else { // We're at the root level + flatFolders.push({ + name: folder.name, + members: [...new Set(flatMembers)], + }); + } + + return { + name: folder.name, + members: nestedMembers, + }; + }; + + const nestedFolders: NestedFolder[] = (extendedCube.folders || []).map((f: Folder) => processFolder(f)); + + return { + config: { + name: cubeName, + type: extendedCube.isView ? 'view' : 'cube', + title: cubeTitle, + isVisible: isCubeVisible, + public: isCubeVisible, + description: extendedCube.description, + connectedComponent: this.joinGraph.connectedComponents()[cubeName], + meta: extendedCube.meta, + measures: Object.entries(extendedCube.measures || {}).map((nameToMetric: [string, any]) => { + const metricDef = nameToMetric[1] as ExtendedCubeSymbolDefinition; + const measureVisibility = isCubeVisible ? this.isVisible(metricDef, true) : false; + return { + ...this.measureConfig(cubeName, cubeTitle, nameToMetric), + isVisible: measureVisibility, + public: measureVisibility, + }; + }), + dimensions: Object.entries(extendedCube.dimensions || {}).map((nameToDimension: [string, any]) => { + const [dimensionName, dimDef] = nameToDimension; + const extendedDimDef = dimDef as ExtendedCubeSymbolDefinition; + const dimensionVisibility = isCubeVisible + ? this.isVisible(extendedDimDef, !extendedDimDef.primaryKey) + : false; + const granularitiesObj = extendedDimDef.granularities; + + return { + name: `${cubeName}.${dimensionName}`, + title: this.title(cubeTitle, nameToDimension, false), + type: this.dimensionDataType(extendedDimDef.type || 'string'), + description: extendedDimDef.description, + shortTitle: this.title(cubeTitle, nameToDimension, true), + suggestFilterValues: + extendedDimDef.suggestFilterValues == null + ? true + : extendedDimDef.suggestFilterValues, + format: extendedDimDef.format, + meta: extendedDimDef.meta, + isVisible: dimensionVisibility, + public: dimensionVisibility, + primaryKey: !!extendedDimDef.primaryKey, + aliasMember: extendedDimDef.aliasMember, + granularities: + granularitiesObj + ? Object.entries(granularitiesObj).map(([gName, gDef]: [string, any]) => ({ + name: gName, + title: this.title(cubeTitle, [gName, gDef], true), + interval: gDef.interval, + offset: gDef.offset, + origin: gDef.origin, + })) + : undefined, + }; + }), + segments: Object.entries(extendedCube.segments || {}).map((nameToSegment: [string, any]) => { + const [segmentName, segmentDef] = nameToSegment; + const extendedSegmentDef = segmentDef as ExtendedCubeSymbolDefinition; + const segmentVisibility = isCubeVisible ? this.isVisible(extendedSegmentDef, true) : false; + return { + name: `${cubeName}.${segmentName}`, + title: this.title(cubeTitle, nameToSegment, false), + shortTitle: this.title(cubeTitle, nameToSegment, true), + description: extendedSegmentDef.description, + meta: extendedSegmentDef.meta, + isVisible: segmentVisibility, + public: segmentVisibility, + }; + }), + hierarchies: (extendedCube.evaluatedHierarchies || []).map((it: any) => ({ + ...it, + public: it.public ?? true, + name: `${cubeName}.${it.name}`, + })), + folders: flatFolders, + nestedFolders, + }, + }; + } + + public queriesForContext(contextId: string | null | undefined): TransformedCube[] { + // return All queries if no context pass + if (contextId == null || contextId.length === 0) { + return this.queries; + } + + const context = (this.contextEvaluator as any).contextDefinitions[contextId]; + + // If contextId is wrong + if (context == null) { + throw new UserError(`Context ${contextId} doesn't exist`); + } + + // As for now context works on the cubes level + return this.queries.filter( + (query) => context.contextMembers.includes(query.config.name) + ); + } + + /** + * @protected + */ + protected isVisible(symbol: any, defaultValue: boolean): boolean { + if (symbol.public != null) { + return symbol.public; + } + + // TODO: Deprecated, should be removed in the future + if (symbol.visible != null) { + return symbol.visible; + } + + // TODO: Deprecated, should be removed in the futur + if (symbol.shown != null) { + return symbol.shown; + } + + return defaultValue; + } + + private dimensionDataType(dimensionType: string): string { + return dimensionType === 'switch' ? 'string' : dimensionType; + } + + private measureConfig(cubeName: string, cubeTitle: string, nameToMetric: [string, any]): Omit { + const [metricName, metricDef] = nameToMetric; + const extendedMetricDef = metricDef as ExtendedCubeSymbolDefinition; + const name = `${cubeName}.${metricName}`; + + // Support both old 'drillMemberReferences' and new 'drillMembers' keys + const drillMembers = extendedMetricDef.drillMembers || extendedMetricDef.drillMemberReferences; + + const drillMembersArray: string[] = (drillMembers && this.cubeEvaluator.evaluateReferences( + cubeName, drillMembers, { originalSorting: true } + )) || []; + + const type = CubeSymbols.toMemberDataType(extendedMetricDef.type || 'number'); + const isCumulative = extendedMetricDef.cumulative || BaseMeasure.isCumulative(extendedMetricDef); + + const drillMembersGrouped: { measures: string[]; dimensions: string[] } = { measures: [], dimensions: [] }; + for (const member of drillMembersArray) { + if (this.cubeEvaluator.isMeasure(member)) { + drillMembersGrouped.measures.push(member); + } else if (this.cubeEvaluator.isDimension(member)) { + drillMembersGrouped.dimensions.push(member); + } + } + + return { + name, + title: this.title(cubeTitle, nameToMetric, false), + description: extendedMetricDef.description, + shortTitle: this.title(cubeTitle, nameToMetric, true), + format: extendedMetricDef.format, + cumulativeTotal: isCumulative, + cumulative: isCumulative, + type, + aggType: extendedMetricDef.aggType || extendedMetricDef.type || '', + drillMembers: drillMembersArray, + drillMembersGrouped, + aliasMember: extendedMetricDef.aliasMember, + meta: extendedMetricDef.meta + }; + } + + private title(cubeTitle: string, nameToDef: [string, any], short: boolean): string { + const prefix = short ? '' : `${cubeTitle} `; + const def = nameToDef[1] as ExtendedCubeSymbolDefinition; + const suffix = def.title || this.titleize(nameToDef[0]); + return `${prefix}${suffix}`; + } + + private titleize(name: string): string { + return inflection.titleize(inflection.underscore(camelCase(name, { pascalCase: true }))); + } +} diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts b/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts index a5887e8356c9f..9df65fd95fc98 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts @@ -3,6 +3,7 @@ import cronParser from 'cron-parser'; import type { CubeSymbols, CubeDefinition } from './CubeSymbols'; import type { ErrorReporter } from './ErrorReporter'; +import { CompilerInterface } from './PrepareCompiler'; /* ***************************** * ATTENTION: @@ -978,7 +979,7 @@ export function functionFieldsPatterns(): string[] { return Array.from(functionPatterns); } -export class CubeValidator { +export class CubeValidator implements CompilerInterface { protected readonly validCubes: Map = new Map(); public constructor( diff --git a/packages/cubejs-schema-compiler/src/compiler/DataSchemaCompiler.ts b/packages/cubejs-schema-compiler/src/compiler/DataSchemaCompiler.ts index c0f569f265ca4..b49795ce607db 100644 --- a/packages/cubejs-schema-compiler/src/compiler/DataSchemaCompiler.ts +++ b/packages/cubejs-schema-compiler/src/compiler/DataSchemaCompiler.ts @@ -560,7 +560,7 @@ export class DataSchemaCompiler { } private prepareTranspileSymbols() { - const cubeNames: string[] = Object.keys(this.cubeDictionary.byId); + const cubeNames: string[] = this.cubeDictionary.cubeNames(); // We need only cubes and all its member names for transpiling. // Cubes doesn't change during transpiling, but are changed during compilation phase, // so we can prepare them once for every phase. diff --git a/packages/cubejs-schema-compiler/src/compiler/JoinGraph.ts b/packages/cubejs-schema-compiler/src/compiler/JoinGraph.ts index 505b10f952bf9..58cd1360b6e43 100644 --- a/packages/cubejs-schema-compiler/src/compiler/JoinGraph.ts +++ b/packages/cubejs-schema-compiler/src/compiler/JoinGraph.ts @@ -6,6 +6,7 @@ import type { CubeValidator } from './CubeValidator'; import type { CubeEvaluator, MeasureDefinition } from './CubeEvaluator'; import type { CubeDefinition, JoinDefinition } from './CubeSymbols'; import type { ErrorReporter } from './ErrorReporter'; +import { CompilerInterface } from './PrepareCompiler'; export type JoinEdge = { join: JoinDefinition, @@ -30,7 +31,7 @@ export type JoinHint = string | string[]; export type JoinHints = JoinHint[]; -export class JoinGraph { +export class JoinGraph implements CompilerInterface { private readonly cubeValidator: CubeValidator; private readonly cubeEvaluator: CubeEvaluator; diff --git a/packages/cubejs-schema-compiler/src/compiler/ViewCompilationGate.ts b/packages/cubejs-schema-compiler/src/compiler/ViewCompilationGate.ts index 0479c21e087ab..cc5c87996b102 100644 --- a/packages/cubejs-schema-compiler/src/compiler/ViewCompilationGate.ts +++ b/packages/cubejs-schema-compiler/src/compiler/ViewCompilationGate.ts @@ -1,9 +1,7 @@ -export class ViewCompilationGate { - private shouldCompile: any; +import { CompilerInterface } from './PrepareCompiler'; - public constructor() { - this.shouldCompile = false; - } +export class ViewCompilationGate implements CompilerInterface { + private shouldCompile: boolean = false; public compile(cubes: any[]) { // When developing Data Access Policies feature, we've come across a diff --git a/packages/cubejs-schema-compiler/src/compiler/transpilers/transpiler.interface.ts b/packages/cubejs-schema-compiler/src/compiler/transpilers/transpiler.interface.ts index 292256ead97b0..33722eba199ce 100644 --- a/packages/cubejs-schema-compiler/src/compiler/transpilers/transpiler.interface.ts +++ b/packages/cubejs-schema-compiler/src/compiler/transpilers/transpiler.interface.ts @@ -8,12 +8,12 @@ export interface TranspilerInterface { } export interface TranspilerSymbolResolver { - resolveSymbol(cubeName, name): any; - isCurrentCube(name): boolean; + resolveSymbol(cubeName: string | null | undefined, name: string): any; + isCurrentCube(name: string): boolean; } export interface TranspilerCubeResolver { - resolveCube(name): boolean; + resolveCube(name: string): any; } export type SymbolResolver = (name: string) => any; diff --git a/packages/cubejs-schema-compiler/test/integration/postgres/cube-views.test.ts b/packages/cubejs-schema-compiler/test/integration/postgres/cube-views.test.ts index 4b69ca05e9d25..436c9597ac629 100644 --- a/packages/cubejs-schema-compiler/test/integration/postgres/cube-views.test.ts +++ b/packages/cubejs-schema-compiler/test/integration/postgres/cube-views.test.ts @@ -444,13 +444,13 @@ view(\`OrdersViewDrillMembersWithPrefix\`, { it('check includes are exposed in meta', async () => { await compiler.compile(); const cube = metaTransformer.cubes.find(c => c.config.name === 'OrdersView'); - expect(cube.config.measures.find((({ name }) => name === 'OrdersView.count')).name).toBe('OrdersView.count'); + expect(cube?.config.measures.find((({ name }) => name === 'OrdersView.count'))?.name).toBe('OrdersView.count'); }); it('orders are hidden', async () => { await compiler.compile(); const cube = metaTransformer.cubes.find(c => c.config.name === 'Orders'); - expect(cube.config.measures.filter((({ isVisible }) => isVisible)).length).toBe(0); + expect(cube?.config.measures.filter((({ isVisible }) => isVisible)).length).toBe(0); }); it('split views', async () => runQueryTest({ @@ -465,9 +465,9 @@ view(\`OrdersViewDrillMembersWithPrefix\`, { it('check drillMembers are inherited in views', async () => { await compiler.compile(); const cube = metaTransformer.cubes.find(c => c.config.name === 'OrdersView'); - const countMeasure = cube.config.measures.find((m) => m.name === 'OrdersView.count'); - expect(countMeasure.drillMembers).toEqual(['OrdersView.id', 'OrdersView.ProductCategories_name']); - expect(countMeasure.drillMembersGrouped).toEqual({ + const countMeasure = cube?.config.measures.find((m) => m.name === 'OrdersView.count'); + expect(countMeasure?.drillMembers).toEqual(['OrdersView.id', 'OrdersView.ProductCategories_name']); + expect(countMeasure?.drillMembersGrouped).toEqual({ measures: [], dimensions: ['OrdersView.id', 'OrdersView.ProductCategories_name'] }); @@ -478,18 +478,18 @@ view(\`OrdersViewDrillMembersWithPrefix\`, { // Check that the source Orders cube has drill members const sourceOrdersCube = metaTransformer.cubes.find(c => c.config.name === 'Orders'); - const sourceCountMeasure = sourceOrdersCube.config.measures.find((m) => m.name === 'Orders.count'); - expect(sourceCountMeasure.drillMembers).toEqual(['Orders.id', 'Orders.createdAt', 'ProductCategories.name']); + const sourceCountMeasure = sourceOrdersCube?.config.measures.find((m) => m.name === 'Orders.count'); + expect(sourceCountMeasure?.drillMembers).toEqual(['Orders.id', 'Orders.createdAt', 'ProductCategories.name']); // Check that the OrdersView cube inherits these drill members with correct naming const viewCube = metaTransformer.cubes.find(c => c.config.name === 'OrdersView'); - const viewCountMeasure = viewCube.config.measures.find((m) => m.name === 'OrdersView.count'); + const viewCountMeasure = viewCube?.config.measures.find((m) => m.name === 'OrdersView.count'); - expect(viewCountMeasure.drillMembers).toBeDefined(); - expect(Array.isArray(viewCountMeasure.drillMembers)).toBe(true); - expect(viewCountMeasure.drillMembers.length).toBeGreaterThan(0); - expect(viewCountMeasure.drillMembers).toContain('OrdersView.id'); - expect(viewCountMeasure.drillMembersGrouped).toBeDefined(); + expect(viewCountMeasure?.drillMembers).toBeDefined(); + expect(Array.isArray(viewCountMeasure?.drillMembers)).toBe(true); + expect(viewCountMeasure?.drillMembers.length).toBeGreaterThan(0); + expect(viewCountMeasure?.drillMembers).toContain('OrdersView.id'); + expect(viewCountMeasure?.drillMembersGrouped).toBeDefined(); }); it('check drill member inheritance with limited includes in OrdersSimpleView', async () => { @@ -530,17 +530,17 @@ view(\`OrdersViewDrillMembersWithPrefix\`, { // Check that the OrdersView cube inherits these drill members with correct naming const viewCube = metaTransformer.cubes.find(c => c.config.name === 'OrdersViewDrillMembers'); - const viewCountMeasure = viewCube.config.measures.find((m) => m.name === 'OrdersViewDrillMembers.count'); - expect(viewCountMeasure.drillMembers).toBeDefined(); - expect(Array.isArray(viewCountMeasure.drillMembers)).toBe(true); - expect(viewCountMeasure.drillMembers.length).toEqual(2); - expect(viewCountMeasure.drillMembers).toEqual(['OrdersViewDrillMembers.createdAt', 'OrdersViewDrillMembers.name']); - - const viewCount2Measure = viewCube.config.measures.find((m) => m.name === 'OrdersViewDrillMembers.count2'); - expect(viewCount2Measure.drillMembers).toBeDefined(); - expect(Array.isArray(viewCount2Measure.drillMembers)).toBe(true); - expect(viewCount2Measure.drillMembers.length).toEqual(1); - expect(viewCount2Measure.drillMembers).toContain('OrdersViewDrillMembers.name'); + const viewCountMeasure = viewCube?.config.measures.find((m) => m.name === 'OrdersViewDrillMembers.count'); + expect(viewCountMeasure?.drillMembers).toBeDefined(); + expect(Array.isArray(viewCountMeasure?.drillMembers)).toBe(true); + expect(viewCountMeasure?.drillMembers.length).toEqual(2); + expect(viewCountMeasure?.drillMembers).toEqual(['OrdersViewDrillMembers.createdAt', 'OrdersViewDrillMembers.name']); + + const viewCount2Measure = viewCube?.config.measures.find((m) => m.name === 'OrdersViewDrillMembers.count2'); + expect(viewCount2Measure?.drillMembers).toBeDefined(); + expect(Array.isArray(viewCount2Measure?.drillMembers)).toBe(true); + expect(viewCount2Measure?.drillMembers.length).toEqual(1); + expect(viewCount2Measure?.drillMembers).toContain('OrdersViewDrillMembers.name'); }); it('verify drill member inheritance functionality (with transitive joins + prefix)', async () => { @@ -549,16 +549,16 @@ view(\`OrdersViewDrillMembersWithPrefix\`, { // Check that the OrdersView cube inherits these drill members with correct naming const viewCube = metaTransformer.cubes.find(c => c.config.name === 'OrdersViewDrillMembersWithPrefix'); - const viewCountMeasure = viewCube.config.measures.find((m) => m.name === 'OrdersViewDrillMembersWithPrefix.count'); - expect(viewCountMeasure.drillMembers).toBeDefined(); - expect(Array.isArray(viewCountMeasure.drillMembers)).toBe(true); - expect(viewCountMeasure.drillMembers.length).toEqual(2); - expect(viewCountMeasure.drillMembers).toEqual(['OrdersViewDrillMembersWithPrefix.createdAt', 'OrdersViewDrillMembersWithPrefix.ProductCategories_name']); - - const viewCount2Measure = viewCube.config.measures.find((m) => m.name === 'OrdersViewDrillMembersWithPrefix.ProductCategories_count2'); - expect(viewCount2Measure.drillMembers).toBeDefined(); - expect(Array.isArray(viewCount2Measure.drillMembers)).toBe(true); - expect(viewCount2Measure.drillMembers.length).toEqual(1); - expect(viewCount2Measure.drillMembers).toContain('OrdersViewDrillMembersWithPrefix.ProductCategories_name'); + const viewCountMeasure = viewCube?.config.measures.find((m) => m.name === 'OrdersViewDrillMembersWithPrefix.count'); + expect(viewCountMeasure?.drillMembers).toBeDefined(); + expect(Array.isArray(viewCountMeasure?.drillMembers)).toBe(true); + expect(viewCountMeasure?.drillMembers.length).toEqual(2); + expect(viewCountMeasure?.drillMembers).toEqual(['OrdersViewDrillMembersWithPrefix.createdAt', 'OrdersViewDrillMembersWithPrefix.ProductCategories_name']); + + const viewCount2Measure = viewCube?.config.measures.find((m) => m.name === 'OrdersViewDrillMembersWithPrefix.ProductCategories_count2'); + expect(viewCount2Measure?.drillMembers).toBeDefined(); + expect(Array.isArray(viewCount2Measure?.drillMembers)).toBe(true); + expect(viewCount2Measure?.drillMembers.length).toEqual(1); + expect(viewCount2Measure?.drillMembers).toContain('OrdersViewDrillMembersWithPrefix.ProductCategories_name'); }); }); diff --git a/packages/cubejs-schema-compiler/test/unit/folders.test.ts b/packages/cubejs-schema-compiler/test/unit/folders.test.ts index 22b0bdb89257f..1d14c1c58bbce 100644 --- a/packages/cubejs-schema-compiler/test/unit/folders.test.ts +++ b/packages/cubejs-schema-compiler/test/unit/folders.test.ts @@ -25,20 +25,20 @@ describe('Cube Folders', () => { (it) => it.config.name === 'test_view' ); - expect(emptyView.config.folders.length).toBe(2); + expect(emptyView?.config.folders.length).toBe(2); - const folder1 = emptyView.config.folders.find( + const folder1 = emptyView?.config.folders.find( (it) => it.name === 'folder1' ); - expect(folder1.members).toEqual([ + expect(folder1?.members).toEqual([ 'test_view.age', 'test_view.renamed_gender', ]); - const folder2 = emptyView.config.folders.find( + const folder2 = emptyView?.config.folders.find( (it) => it.name === 'folder2' ); - expect(folder2.members).toEqual( + expect(folder2?.members).toEqual( expect.arrayContaining(['test_view.age', 'test_view.renamed_gender']) ); }); @@ -48,29 +48,29 @@ describe('Cube Folders', () => { (it) => it.config.name === 'test_view4' ); - expect(testView.config.folders.length).toBe(3); + expect(testView?.config.folders.length).toBe(3); - const folder1 = testView.config.folders.find( + const folder1 = testView?.config.folders.find( (it) => it.name === 'folder1' ); - expect(folder1.members).toEqual([ + expect(folder1?.members).toEqual([ 'test_view4.users_age', 'test_view4.users_state', 'test_view4.renamed_orders_status', ]); - const folder2 = testView.config.folders.find( + const folder2 = testView?.config.folders.find( (it) => it.name === 'folder2' ); - expect(folder2.members).toEqual( + expect(folder2?.members).toEqual( expect.arrayContaining(['test_view4.users_city', 'test_view4.users_renamed_in_view3_gender']) ); - const folder3 = testView.config.folders.find( + const folder3 = testView?.config.folders.find( (it) => it.name === 'folder3' ); - expect(folder3.members.length).toBe(9); - expect(folder3.members).toEqual([ + expect(folder3?.members.length).toBe(9); + expect(folder3?.members).toEqual([ 'test_view4.users_city', 'test_view4.renamed_orders_status', 'test_view4.renamed_orders_count', @@ -99,43 +99,43 @@ describe('Cube Folders', () => { (it) => it.config.name === 'test_view4' ); - expect(testView.config.folders.length).toBe(5); + expect(testView?.config.folders.length).toBe(5); - const folder1 = testView.config.folders.find( + const folder1 = testView?.config.folders.find( (it) => it.name === 'folder1' ); - expect(folder1.members).toEqual([ + expect(folder1?.members).toEqual([ 'test_view4.users_age', 'test_view4.users_state', 'test_view4.renamed_orders_status', ]); - const folder2 = testView.config.folders.find( + const folder2 = testView?.config.folders.find( (it) => it.name === 'folder2' ); - expect(folder2.members).toEqual( + expect(folder2?.members).toEqual( expect.arrayContaining(['test_view4.users_city', 'test_view4.users_renamed_in_view3_gender']) ); - const folder3 = testView.config.folders.find( + const folder3 = testView?.config.folders.find( (it) => it.name === 'folder3' ); - expect(folder3.members.length).toBe(1); - expect(folder3.members).toEqual([ + expect(folder3?.members.length).toBe(1); + expect(folder3?.members).toEqual([ 'test_view4.users_city', ]); - const folder4 = testView.config.folders.find( + const folder4 = testView?.config.folders.find( (it) => it.name === 'folder3/inner folder 4' ); - expect(folder4.members.length).toBe(1); - expect(folder4.members).toEqual(['test_view4.renamed_orders_status']); + expect(folder4?.members.length).toBe(1); + expect(folder4?.members).toEqual(['test_view4.renamed_orders_status']); - const folder5 = testView.config.folders.find( + const folder5 = testView?.config.folders.find( (it) => it.name === 'folder3/inner folder 5' ); - expect(folder5.members.length).toBe(9); - expect(folder5.members).toEqual([ + expect(folder5?.members.length).toBe(9); + expect(folder5?.members).toEqual([ 'test_view4.renamed_orders_count', 'test_view4.renamed_orders_id', 'test_view4.renamed_orders_number', @@ -153,33 +153,33 @@ describe('Cube Folders', () => { (it) => it.config.name === 'test_view4' ); - expect(testView.config.nestedFolders.length).toBe(3); + expect(testView?.config.nestedFolders.length).toBe(3); - const folder1 = testView.config.nestedFolders.find( + const folder1 = testView?.config.nestedFolders.find( (it) => it.name === 'folder1' ); - expect(folder1.members).toEqual([ + expect(folder1?.members).toEqual([ 'test_view4.users_age', 'test_view4.users_state', 'test_view4.renamed_orders_status', ]); - const folder2 = testView.config.nestedFolders.find( + const folder2 = testView?.config.nestedFolders.find( (it) => it.name === 'folder2' ); - expect(folder2.members).toEqual( + expect(folder2?.members).toEqual( expect.arrayContaining(['test_view4.users_city', 'test_view4.users_renamed_in_view3_gender']) ); - const folder3 = testView.config.nestedFolders.find( + const folder3 = testView?.config.nestedFolders.find( (it) => it.name === 'folder3' ); - expect(folder3.members.length).toBe(3); - expect(folder3.members[1]).toEqual( + expect(folder3?.members.length).toBe(3); + expect(folder3?.members[1]).toEqual( { name: 'inner folder 4', members: ['test_view4.renamed_orders_status'] } ); - expect(folder3.members[2].name).toEqual('inner folder 5'); - expect(folder3.members[2].members).toEqual([ + expect((folder3?.members[2] as any)?.name).toEqual('inner folder 5'); + expect((folder3?.members[2] as any)?.members).toEqual([ 'test_view4.renamed_orders_count', 'test_view4.renamed_orders_id', 'test_view4.renamed_orders_number', @@ -200,31 +200,31 @@ describe('Cube Folders', () => { (it) => it.config.name === 'test_view3' ); - expect(view2.config.folders.length).toBe(1); - expect(view3.config.folders.length).toBe(2); + expect(view2?.config.folders.length).toBe(1); + expect(view3?.config.folders.length).toBe(2); - const folder1 = view2.config.folders.find( + const folder1 = view2?.config.folders.find( (it) => it.name === 'folder1' ); - expect(folder1.members).toEqual([ + expect(folder1?.members).toEqual([ 'test_view2.users_age', 'test_view2.users_state', 'test_view2.renamed_orders_status', ]); - const folder1v3 = view3.config.folders.find( + const folder1v3 = view3?.config.folders.find( (it) => it.name === 'folder1' ); - expect(folder1v3.members).toEqual([ + expect(folder1v3?.members).toEqual([ 'test_view3.users_age', 'test_view3.users_state', 'test_view3.renamed_orders_status', ]); - const folder2 = view3.config.folders.find( + const folder2 = view3?.config.folders.find( (it) => it.name === 'folder2' ); - expect(folder2.members).toEqual( + expect(folder2?.members).toEqual( expect.arrayContaining(['test_view3.users_city', 'test_view3.users_renamed_in_view3_gender']) ); }); @@ -252,10 +252,10 @@ describe('Cube Folders', () => { (it) => it.config.name === 'test_view2' ); - expect(view.config.folders.length).toBe(1); + expect(view?.config.folders.length).toBe(1); - const folder1 = view.config.folders.find((it) => it.name === 'folder1'); - expect(folder1.members).toEqual([ + const folder1 = view?.config.folders.find((it) => it.name === 'folder1'); + expect(folder1?.members).toEqual([ 'test_view2.users_age', 'test_view2.users_state', 'test_view2.renamed_orders_status', diff --git a/packages/cubejs-schema-compiler/test/unit/hierarchies.test.ts b/packages/cubejs-schema-compiler/test/unit/hierarchies.test.ts index 0f2566ccfaa7e..faf3a0d686e53 100644 --- a/packages/cubejs-schema-compiler/test/unit/hierarchies.test.ts +++ b/packages/cubejs-schema-compiler/test/unit/hierarchies.test.ts @@ -17,8 +17,8 @@ describe('Cube hierarchies', () => { (it) => it.config.name === 'orders_users_view' ); - expect(ordersView.config.hierarchies.length).toBe(2); - expect(ordersView.config.hierarchies).toEqual([ + expect(ordersView?.config.hierarchies.length).toBe(2); + expect(ordersView?.config.hierarchies).toEqual([ { name: 'orders_users_view.orders_hierarchy', title: 'Hello Hierarchy', @@ -42,19 +42,19 @@ describe('Cube hierarchies', () => { const ordersIncludesExcludesView = metaTransformer.cubes.find( (it) => it.config.name === 'orders_includes_excludes_view' ); - expect(ordersIncludesExcludesView.config.hierarchies.length).toBe(1); + expect(ordersIncludesExcludesView?.config.hierarchies.length).toBe(1); const emptyView = metaTransformer.cubes.find( (it) => it.config.name === 'empty_view' ); - expect(emptyView.config.hierarchies.length).toBe(0); + expect(emptyView?.config.hierarchies.length).toBe(0); const allHierarchyView = metaTransformer.cubes.find( (it) => it.config.name === 'all_hierarchy_view' ); - expect(allHierarchyView.config.hierarchies.length).toBe(3); + expect(allHierarchyView?.config.hierarchies.length).toBe(3); - const prefixedHierarchy = allHierarchyView.config.hierarchies.find((it) => it.name === 'all_hierarchy_view.users_users_hierarchy'); + const prefixedHierarchy = allHierarchyView?.config.hierarchies.find((it) => it.name === 'all_hierarchy_view.users_users_hierarchy'); expect(prefixedHierarchy).toBeTruthy(); expect(prefixedHierarchy?.aliasMember).toEqual('users.users_hierarchy'); expect(prefixedHierarchy?.levels).toEqual(['all_hierarchy_view.users_age', 'all_hierarchy_view.users_city']); @@ -73,7 +73,7 @@ describe('Cube hierarchies', () => { (it) => it.config.name === 'only_hierarchy_included_view' ); - expect(view1.config.dimensions).toEqual( + expect(view1?.config.dimensions).toEqual( expect.arrayContaining([ expect.objectContaining({ name: 'only_hierarchy_included_view.status' }), expect.objectContaining({ name: 'only_hierarchy_included_view.number' }), @@ -85,8 +85,8 @@ describe('Cube hierarchies', () => { const view2 = metaTransformer.cubes.find( (it) => it.config.name === 'auto_include_view' ); - expect(view2.config.dimensions.length).toEqual(2); - expect(view2.config.dimensions).toEqual( + expect(view2?.config.dimensions.length).toEqual(2); + expect(view2?.config.dimensions).toEqual( expect.arrayContaining([ expect.objectContaining({ name: 'auto_include_view.status' }), expect.objectContaining({ name: 'auto_include_view.number' }), @@ -177,7 +177,7 @@ describe('Cube hierarchies', () => { (it) => it.config.name === 'orders' ); - expect(ordersCube.config.hierarchies).toEqual([ + expect(ordersCube?.config.hierarchies).toEqual([ { aliasMember: undefined, name: 'orders.hello', diff --git a/packages/cubejs-schema-compiler/test/unit/schema.test.ts b/packages/cubejs-schema-compiler/test/unit/schema.test.ts index 43dc8cd6839bc..0abb78cf03463 100644 --- a/packages/cubejs-schema-compiler/test/unit/schema.test.ts +++ b/packages/cubejs-schema-compiler/test/unit/schema.test.ts @@ -350,8 +350,8 @@ describe('Schema Testing', () => { expect(dimensions.length).toBeGreaterThan(0); expect(dimensions.every((dimension) => dimension.primaryKey)).toBeDefined(); expect(dimensions.every((dimension) => typeof dimension.primaryKey === 'boolean')).toBe(true); - expect(dimensions.find((dimension) => dimension.name === 'CubeA.id').primaryKey).toBe(true); - expect(dimensions.find((dimension) => dimension.name === 'CubeA.type').primaryKey).toBe(false); + expect(dimensions.find((dimension) => dimension.name === 'CubeA.id')?.primaryKey).toBe(true); + expect(dimensions.find((dimension) => dimension.name === 'CubeA.type')?.primaryKey).toBe(false); }); it('descriptions', async () => { @@ -369,15 +369,15 @@ describe('Schema Testing', () => { expect(dimensions).toBeDefined(); expect(dimensions.length).toBeGreaterThan(0); - expect(dimensions.find((dimension) => dimension.name === 'CubeA.id').description).toBe('id dimension from createCubeSchema'); + expect(dimensions.find((dimension) => dimension.name === 'CubeA.id')?.description).toBe('id dimension from createCubeSchema'); expect(measures).toBeDefined(); expect(measures.length).toBeGreaterThan(0); - expect(measures.find((measure) => measure.name === 'CubeA.count').description).toBe('count measure from createCubeSchema'); + expect(measures.find((measure) => measure.name === 'CubeA.count')?.description).toBe('count measure from createCubeSchema'); expect(segments).toBeDefined(); expect(segments.length).toBeGreaterThan(0); - expect(segments.find((segment) => segment.name === 'CubeA.sfUsers').description).toBe('SF users segment from createCubeSchema'); + expect(segments.find((segment) => segment.name === 'CubeA.sfUsers')?.description).toBe('SF users segment from createCubeSchema'); }); it('custom granularities in meta', async () => { @@ -393,27 +393,27 @@ describe('Schema Testing', () => { const dg = dimensions.find((dimension) => dimension.name === 'orders.createdAt'); expect(dg).toBeDefined(); - expect(dg.granularities).toBeDefined(); - expect(dg.granularities.length).toBeGreaterThan(0); + expect(dg?.granularities).toBeDefined(); + expect(dg?.granularities?.length).toBeGreaterThan(0); // Granularity defined with title - let gr = dg.granularities.find(g => g.name === 'half_year'); + let gr = dg?.granularities?.find(g => g.name === 'half_year'); expect(gr).toBeDefined(); - expect(gr.title).toBe('6 month intervals'); - expect(gr.interval).toBe('6 months'); + expect(gr?.title).toBe('6 month intervals'); + expect(gr?.interval).toBe('6 months'); - gr = dg.granularities.find(g => g.name === 'half_year_by_1st_april'); + gr = dg?.granularities?.find(g => g.name === 'half_year_by_1st_april'); expect(gr).toBeDefined(); - expect(gr.title).toBe('Half year from Apr to Oct'); - expect(gr.interval).toBe('6 months'); - expect(gr.offset).toBe('3 months'); + expect(gr?.title).toBe('Half year from Apr to Oct'); + expect(gr?.interval).toBe('6 months'); + expect(gr?.offset).toBe('3 months'); // // Granularity defined without title -> titlize() - gr = dg.granularities.find(g => g.name === 'half_year_by_1st_june'); + gr = dg?.granularities?.find(g => g.name === 'half_year_by_1st_june'); expect(gr).toBeDefined(); - expect(gr.title).toBe('Half Year By1 St June'); - expect(gr.interval).toBe('6 months'); - expect(gr.origin).toBe('2020-06-01 10:00:00'); + expect(gr?.title).toBe('Half Year By1 St June'); + expect(gr?.interval).toBe('6 months'); + expect(gr?.origin).toBe('2020-06-01 10:00:00'); }); describe('Joins', () => { @@ -557,12 +557,12 @@ describe('Schema Testing', () => { describe('Views', () => { it('extends custom granularities and timeshifts', async () => { - const { compiler, metaTransformer } = prepareJsCompiler([ + const { compiler, cubeEvaluator } = prepareJsCompiler([ createCubeSchemaWithCustomGranularitiesAndTimeShift('orders') ]); await compiler.compile(); - const { measures, dimensions } = metaTransformer.cubeEvaluator.evaluatedCubes.orders_view; + const { measures, dimensions } = cubeEvaluator.evaluatedCubes.orders_view; expect(dimensions.createdAt).toMatchSnapshot(); expect(measures.count_shifted_year).toMatchSnapshot(); }); @@ -572,10 +572,10 @@ describe('Schema Testing', () => { path.join(process.cwd(), '/test/unit/fixtures/folders.yml'), 'utf8' ); - const { compiler, metaTransformer } = prepareYamlCompiler(modelContent); + const { compiler, cubeEvaluator } = prepareYamlCompiler(modelContent); await compiler.compile(); - const testView3 = metaTransformer.cubeEvaluator.evaluatedCubes.test_view3; + const testView3 = cubeEvaluator.evaluatedCubes.test_view3; expect(testView3.dimensions).toMatchSnapshot(); expect(testView3.measures).toMatchSnapshot(); expect(testView3.measures).toMatchSnapshot(); diff --git a/packages/cubejs-schema-compiler/test/unit/switch-dimension.test.ts b/packages/cubejs-schema-compiler/test/unit/switch-dimension.test.ts index d95d7b222b56c..60c8baa0a1318 100644 --- a/packages/cubejs-schema-compiler/test/unit/switch-dimension.test.ts +++ b/packages/cubejs-schema-compiler/test/unit/switch-dimension.test.ts @@ -18,8 +18,8 @@ describe('Switch Dimension', () => { const statusDim = cube.config.dimensions.find((d) => d.name === 'orders.status'); const currencyDim = cube.config.dimensions.find((d) => d.name === 'orders.currency'); - expect(numberDim.type).toBe('number'); - expect(statusDim.type).toBe('string'); - expect(currencyDim.type).toBe('string'); + expect(numberDim?.type).toBe('number'); + expect(statusDim?.type).toBe('string'); + expect(currencyDim?.type).toBe('string'); }); }); diff --git a/packages/cubejs-schema-compiler/test/unit/yaml-schema.test.ts b/packages/cubejs-schema-compiler/test/unit/yaml-schema.test.ts index 6cfd7d055e227..5d8bbd504530d 100644 --- a/packages/cubejs-schema-compiler/test/unit/yaml-schema.test.ts +++ b/packages/cubejs-schema-compiler/test/unit/yaml-schema.test.ts @@ -386,15 +386,15 @@ describe('Yaml Schema Testing', () => { expect(dimensions).toBeDefined(); expect(dimensions.length).toBeGreaterThan(0); - expect(dimensions.find((dimension) => dimension.name === 'CubeA.id').description).toBe('id dimension from YAML test cube'); + expect(dimensions.find((dimension) => dimension.name === 'CubeA.id')?.description).toBe('id dimension from YAML test cube'); expect(measures).toBeDefined(); expect(measures.length).toBeGreaterThan(0); - expect(measures.find((measure) => measure.name === 'CubeA.count').description).toBe('count measure from YAML test cube'); + expect(measures.find((measure) => measure.name === 'CubeA.count')?.description).toBe('count measure from YAML test cube'); expect(segments).toBeDefined(); expect(segments.length).toBeGreaterThan(0); - expect(segments.find((segment) => segment.name === 'CubeA.sfUsers').description).toBe('SF users segment from createCubeSchema'); + expect(segments.find((segment) => segment.name === 'CubeA.sfUsers')?.description).toBe('SF users segment from createCubeSchema'); }); describe('Custom dimension granularities: ', () => {