From 56da423c28086bd164cef57fcbb8b9dc39c46fc3 Mon Sep 17 00:00:00 2001 From: Mihael Konjevic Date: Tue, 6 Aug 2024 23:57:20 +0200 Subject: [PATCH] Wip granularities rewrite --- src/__tests__/granularity.test.ts | 11 +- src/lib/custom-granularity.ts | 162 ++++++++++++++++++++++++ src/lib/model.ts | 92 +++++++++----- src/lib/query-builder.ts | 46 +++++++ src/lib/query-builder/build-query.ts | 40 +++--- src/lib/query-builder/filter-builder.ts | 36 +++--- src/lib/repository.ts | 118 ++++++++--------- src/lib/types.ts | 57 +++++---- 8 files changed, 405 insertions(+), 157 deletions(-) create mode 100644 src/lib/custom-granularity.ts diff --git a/src/__tests__/granularity.test.ts b/src/__tests__/granularity.test.ts index c7d46d6..32316bf 100644 --- a/src/__tests__/granularity.test.ts +++ b/src/__tests__/granularity.test.ts @@ -297,12 +297,11 @@ const playlistModel = semanticLayer type: "string", sql: ({ model }) => model.column("Name"), }) - .withGranularity("name", [ - { - key: "name", - elements: ["name", "playlist_id"], - display: "name", - }, + .withCategoricalGranularity("name", ({ element }) => [ + element("name") + .withDimensions("playlist_id", "name") + .withKey("playlist_id") + .withFormat(({ dimension }) => dimension("name")), ]); const playlistTrackModel = semanticLayer diff --git a/src/lib/custom-granularity.ts b/src/lib/custom-granularity.ts new file mode 100644 index 0000000..1ec2802 --- /dev/null +++ b/src/lib/custom-granularity.ts @@ -0,0 +1,162 @@ +import { AnyModel } from "./model.js"; +import { AnyRepository } from "./repository.js"; +import invariant from "tiny-invariant"; + +export interface CustomGranularityElementConfig { + name: string; + dimensions: string[]; + keyDimensions: string[]; + formatDimensions: string[]; + formatter: (row: Record) => string; +} + +export type AnyCustomGranularityElement = CustomGranularityElement; + +export abstract class CustomGranularityElementFormatter { + abstract getFormatter( + parent: AnyModel | AnyRepository, + ): (row: Record) => string; + abstract getReferencedDimensionNames(): string[]; +} + +export class CustomGranularityElementDimensionRef extends CustomGranularityElementFormatter { + constructor(public readonly dimensionName: string) { + super(); + } + getFormatter(parent: AnyModel | AnyRepository) { + const dimension = parent.getDimension(this.dimensionName); + return (row: Record) => { + const value = row[dimension.getAlias()]; + return dimension.unsafeFormatValue(value); + }; + } + getReferencedDimensionNames() { + return [this.dimensionName]; + } +} +export class CustomGranularityElementTemplateWithDimensionRefs extends CustomGranularityElementFormatter { + constructor( + public readonly strings: string[], + public readonly values: unknown[], + ) { + super(); + } + getReferencedDimensionNames() { + const dimensions: string[] = []; + for (const value of this.values) { + if (value instanceof CustomGranularityElementDimensionRef) { + dimensions.push(value.dimensionName); + } + } + return dimensions; + } + getFormatter(parent: AnyModel | AnyRepository) { + return (row: Record) => { + const result = []; + for (let i = 0; i < this.strings.length; i++) { + result.push(this.strings[i]!); + const nextValue = this.values[i]; + if (nextValue) { + if (nextValue instanceof CustomGranularityElementDimensionRef) { + const dimension = parent.getDimension(nextValue.dimensionName); + const value = row[dimension.getAlias()]; + result.push(dimension.unsafeFormatValue(value)); + } else { + result.push(nextValue); + } + } + } + return result.join(""); + }; + } +} + +export class CustomGranularityElement { + private readonly dimensionRefs: Record< + string, + CustomGranularityElementDimensionRef + >; + private keys: string[] | null = null; + private formatter: CustomGranularityElementFormatter | null = null; + constructor( + public readonly name: string, + private readonly dimensionNames: string[], + ) { + this.dimensionRefs = dimensionNames.reduce< + Record + >((acc, dimensionName) => { + acc[dimensionName] = new CustomGranularityElementDimensionRef( + dimensionName, + ); + return acc; + }, {}); + } + withKey(...keys: K[]) { + this.keys = keys; + return this; + } + withFormat( + formatter: (props: { + dimension: (name: D) => CustomGranularityElementDimensionRef; + template: ( + strings: TemplateStringsArray, + ...values: unknown[] + ) => CustomGranularityElementTemplateWithDimensionRefs; + }) => + | CustomGranularityElementDimensionRef + | CustomGranularityElementTemplateWithDimensionRefs, + ) { + this.formatter = formatter({ + dimension: (name: D) => { + const dimensionRef = this.dimensionRefs[name]; + invariant(dimensionRef, `Dimension ${name} not found`); + return dimensionRef; + }, + template: (strings, ...values) => { + return new CustomGranularityElementTemplateWithDimensionRefs( + [...strings], + values, + ); + }, + }); + return this; + } + gerDefaultFormatter(parent: AnyModel | AnyRepository) { + return (row: Record) => + this.dimensionNames + .map((dimensionName) => + parent + .getDimension(dimensionName) + .unsafeFormatValue(row[dimensionName]), + ) + .join(", "); + } + getConfig(parent: AnyModel | AnyRepository): CustomGranularityElementConfig { + const dimensionNames = this.dimensionNames.map((dimensionName) => + parent.getDimension(dimensionName).getPath(), + ); + return { + name: this.name, + dimensions: dimensionNames, + keyDimensions: + this.keys?.map((dimensionNames) => + parent.getDimension(dimensionNames).getPath(), + ) ?? dimensionNames, + formatDimensions: + this.formatter?.getReferencedDimensionNames() ?? dimensionNames, + formatter: + this.formatter?.getFormatter(parent) ?? + this.gerDefaultFormatter(parent), + }; + } +} + +export class CustomGranularityElementInit { + constructor( + public readonly parent: AnyModel | AnyRepository, + public readonly name: string, + ) {} + withDimensions(...dimensionNames: GD[]) { + return new CustomGranularityElement(this.name, dimensionNames); + } +} diff --git a/src/lib/model.ts b/src/lib/model.ts index 9ba4c0e..10378f2 100644 --- a/src/lib/model.ts +++ b/src/lib/model.ts @@ -1,7 +1,8 @@ -import { Get, Simplify } from "type-fest"; import { - CustomGranularity, - CustomGranularityElements, + AnyCustomGranularityElement, + CustomGranularityElementInit, +} from "./custom-granularity.js"; +import { DimensionWithTemporalGranularity, GranularityType, MemberFormat, @@ -12,9 +13,10 @@ import { TemporalGranularityIndex, makeTemporalGranularityElementsForDimension, } from "./types.js"; +import { Get, Simplify } from "type-fest"; -import invariant from "tiny-invariant"; import { AnyBaseDialect } from "./dialect/base.js"; +import invariant from "tiny-invariant"; export type NextColumnRefOrDimensionRefAlias = () => string; @@ -253,10 +255,11 @@ export abstract class Member { abstract isMetric(): this is Metric; abstract isDimension(): this is Dimension; - getAlias(dialect: AnyBaseDialect) { - return dialect.asIdentifier( - `${this.model.name}___${this.name.replaceAll(".", "___")}`, - ); + getQuotedAlias(dialect: AnyBaseDialect) { + return dialect.asIdentifier(this.getAlias()); + } + getAlias() { + return `${this.model.name}.${this.name}`; } getPath() { return `${this.model.name}.${this.name}`; @@ -294,6 +297,19 @@ export abstract class Member { getFormat() { return this.props.format; } + unsafeFormatValue(value: unknown) { + const format = this.getFormat(); + if (typeof format === "function") { + return (format as (value: unknown) => string)(value); + } + if (format === "currency") { + return `$${value}`; + } + if (format === "percentage") { + return `${value}%`; + } + return String(value); + } abstract clone(model: AnyModel): Member; } @@ -417,7 +433,14 @@ export class Model< > { public readonly dimensions: Record = {}; public readonly metrics: Record = {}; - public readonly granularities: CustomGranularity[] = []; + public readonly categoricalGranularities: { + name: string; + elements: AnyCustomGranularityElement[]; + }[] = []; + public readonly temporalGranularities: { + name: string; + elements: AnyCustomGranularityElement[]; + }[] = []; public readonly granularitiesNames: Set = new Set(); constructor( @@ -461,6 +484,7 @@ export class Model< ...dimensionWithoutFormat, type: TemporalGranularityIndex[g].type, description: TemporalGranularityIndex[g].description, + format: (value: unknown) => `${value}`, }, g, ); @@ -469,7 +493,6 @@ export class Model< name, makeTemporalGranularityElementsForDimension(name, dimension.type), "temporal", - "bottom", ); } return this; @@ -488,36 +511,46 @@ export class Model< } unsafeWithGranularity( granularityName: string, - elements: CustomGranularityElements, + elements: AnyCustomGranularityElement[], type: GranularityType, - position: "top" | "bottom" = "bottom", ) { invariant( this.granularitiesNames.has(granularityName) === false, `Granularity ${granularityName} already exists`, ); this.granularitiesNames.add(granularityName); - if (position === "top") { - this.granularities.unshift({ - name: granularityName, - type, - elements, - }); - } else { - this.granularities.push({ - name: granularityName, - type, - elements, - }); + if (type === "categorical") { + this.categoricalGranularities.push({ name: granularityName, elements }); + } else if (type === "temporal") { + this.temporalGranularities.push({ name: granularityName, elements }); } return this; } - withGranularity( + withCategoricalGranularity( granularityName: Exclude, - elements: CustomGranularityElements>, - type: GranularityType = "custom", + builder: (args: { + element: ( + name: string, + ) => CustomGranularityElementInit>; + }) => AnyCustomGranularityElement[], ): Model { - return this.unsafeWithGranularity(granularityName, elements, type, "top"); + const elements = builder({ + element: (name) => new CustomGranularityElementInit(this, name), + }); + return this.unsafeWithGranularity(granularityName, elements, "categorical"); + } + withTemporalGranularity( + granularityName: Exclude, + builder: (args: { + element: ( + name: string, + ) => CustomGranularityElementInit>; + }) => AnyCustomGranularityElement[], + ): Model { + const elements = builder({ + element: (name) => new CustomGranularityElementInit(this, name), + }); + return this.unsafeWithGranularity(granularityName, elements, "temporal"); } getMetric(name: string & keyof M) { const metric = this.metrics[name]; @@ -593,7 +626,8 @@ export class Model< for (const [key, value] of Object.entries(this.metrics)) { newModel.metrics[key] = value.clone(newModel); } - newModel.granularities.push(...this.granularities); + newModel.temporalGranularities.push(...this.temporalGranularities); + newModel.categoricalGranularities.push(...this.categoricalGranularities); for (const granularityName of this.granularitiesNames) { newModel.granularitiesNames.add(granularityName); } diff --git a/src/lib/query-builder.ts b/src/lib/query-builder.ts index 9b6edb6..ff53a66 100644 --- a/src/lib/query-builder.ts +++ b/src/lib/query-builder.ts @@ -2,6 +2,7 @@ import { AnyInputQuery, AnyMemberFormat, FilterType, + GranularityConfig, InputQuery, IntrospectionResult, MemberNameToType, @@ -56,11 +57,56 @@ export class QueryBuilder< P, > { public readonly querySchema: QuerySchema; + public readonly granularityConfigs: GranularityConfig[]; constructor( public readonly repository: AnyRepository, public readonly dialect: AnyBaseDialect, ) { this.querySchema = buildQuerySchema(this); + this.granularityConfigs = this.getGranularityConfigs(repository); + } + + private getGranularityConfigs(repository: AnyRepository) { + const granularityConfigs: GranularityConfig[] = []; + for (const categoricalGranularity of repository.categoricalGranularities) { + granularityConfigs.push({ + name: categoricalGranularity.name, + type: "categorical", + elements: categoricalGranularity.elements.map((element) => + element.getConfig(repository), + ), + }); + } + for (const model of repository.getModels()) { + for (const granularity of model.categoricalGranularities) { + granularityConfigs.push({ + name: granularity.name, + type: "categorical", + elements: granularity.elements.map((element) => + element.getConfig(repository), + ), + }); + } + for (const granularity of model.temporalGranularities) { + granularityConfigs.push({ + name: granularity.name, + type: "temporal", + elements: granularity.elements.map((element) => + element.getConfig(repository), + ), + }); + } + } + for (const temporalGranularity of repository.temporalGranularities) { + granularityConfigs.push({ + name: temporalGranularity.name, + type: "temporal", + elements: temporalGranularity.elements.map((element) => + element.getConfig(repository), + ), + }); + } + return granularityConfigs; } unsafeBuildGenericQueryWithoutSchemaParse( diff --git a/src/lib/query-builder/build-query.ts b/src/lib/query-builder/build-query.ts index 7784fae..2c45a71 100644 --- a/src/lib/query-builder/build-query.ts +++ b/src/lib/query-builder/build-query.ts @@ -2,12 +2,12 @@ import * as graphlib from "@dagrejs/graphlib"; import { ModelQuery, Order, Query, QuerySegment } from "../types.js"; -import invariant from "tiny-invariant"; import { AnyBaseDialect } from "../dialect/base.js"; import type { AnyJoin } from "../join.js"; import { AnyModel } from "../model.js"; import { AnyQueryBuilder } from "../query-builder.js"; import type { AnyRepository } from "../repository.js"; +import invariant from "tiny-invariant"; interface ReferencedModels { all: string[]; @@ -148,7 +148,7 @@ function buildQuerySegmentJoinQuery( sqlQuery.select( queryBuilder.dialect.fragment( - `${sql} as ${dimension.getAlias(queryBuilder.dialect)}`, + `${sql} as ${dimension.getQuotedAlias(queryBuilder.dialect)}`, bindings, ), ); @@ -214,17 +214,17 @@ function buildQuerySegment( const dimension = queryBuilder.repository.getDimension(dimensionName); sqlQuery.select( queryBuilder.dialect.fragment( - `${queryBuilder.dialect.asIdentifier(alias)}.${dimension.getAlias( + `${queryBuilder.dialect.asIdentifier(alias)}.${dimension.getQuotedAlias( queryBuilder.dialect, - )} as ${dimension.getAlias(queryBuilder.dialect)}`, + )} as ${dimension.getQuotedAlias(queryBuilder.dialect)}`, ), ); if (hasMetrics) { sqlQuery.groupBy( queryBuilder.dialect.fragment( - `${queryBuilder.dialect.asIdentifier(alias)}.${dimension.getAlias( - queryBuilder.dialect, - )}`, + `${queryBuilder.dialect.asIdentifier( + alias, + )}.${dimension.getQuotedAlias(queryBuilder.dialect)}`, ), ); } @@ -236,7 +236,7 @@ function buildQuerySegment( sqlQuery.select( queryBuilder.dialect.fragment( - `${sql} as ${metric.getAlias(queryBuilder.dialect)}`, + `${sql} as ${metric.getQuotedAlias(queryBuilder.dialect)}`, bindings, ), ); @@ -284,9 +284,11 @@ export function buildQuery( rootSqlQuery.select( queryBuilder.dialect.fragment( - `${queryBuilder.dialect.asIdentifier(rootAlias)}.${dimension.getAlias( + `${queryBuilder.dialect.asIdentifier( + rootAlias, + )}.${dimension.getQuotedAlias( queryBuilder.dialect, - )} as ${dimension.getAlias(queryBuilder.dialect)}`, + )} as ${dimension.getQuotedAlias(queryBuilder.dialect)}`, ), ); } @@ -297,9 +299,11 @@ export function buildQuery( rootSqlQuery.select( queryBuilder.dialect.fragment( - `${queryBuilder.dialect.asIdentifier(rootAlias)}.${metric.getAlias( + `${queryBuilder.dialect.asIdentifier( + rootAlias, + )}.${metric.getQuotedAlias( queryBuilder.dialect, - )} as ${metric.getAlias(queryBuilder.dialect)}`, + )} as ${metric.getQuotedAlias(queryBuilder.dialect)}`, ), ); } @@ -313,11 +317,11 @@ export function buildQuery( .map((dimension) => { return `${queryBuilder.dialect.asIdentifier( rootAlias, - )}.${dimension.getAlias( + )}.${dimension.getQuotedAlias( queryBuilder.dialect, )} = ${queryBuilder.dialect.asIdentifier( alias, - )}.${dimension.getAlias(queryBuilder.dialect)}`; + )}.${dimension.getQuotedAlias(queryBuilder.dialect)}`; }) .join(" and ") : "1 = 1"; @@ -332,9 +336,11 @@ export function buildQuery( const metric = queryBuilder.repository.getMetric(metricName); rootSqlQuery.select( queryBuilder.dialect.fragment( - `${queryBuilder.dialect.asIdentifier(alias)}.${metric.getAlias( + `${queryBuilder.dialect.asIdentifier( + alias, + )}.${metric.getQuotedAlias( queryBuilder.dialect, - )} as ${metric.getAlias(queryBuilder.dialect)}`, + )} as ${metric.getQuotedAlias(queryBuilder.dialect)}`, ), ); } @@ -366,7 +372,7 @@ export function buildQuery( ).map(({ member, direction }) => { const memberSql = queryBuilder.repository .getMember(member) - .getAlias(queryBuilder.dialect); + .getQuotedAlias(queryBuilder.dialect); return `${memberSql} ${direction}`; }); diff --git a/src/lib/query-builder/filter-builder.ts b/src/lib/query-builder/filter-builder.ts index afa737d..fc84629 100644 --- a/src/lib/query-builder/filter-builder.ts +++ b/src/lib/query-builder/filter-builder.ts @@ -5,19 +5,14 @@ import { OrConnective, SqlWithBindings, } from "../types.js"; -import { - afterDate as filterAfterDate, - beforeDate as filterBeforeDate, -} from "./filter-builder/date-filter-builder.js"; -import { - inDateRange as filterInDateRange, - notInDateRange as filterNotInDateRange, -} from "./filter-builder/date-range-filter-builder.js"; -import { equals as filterEquals, filterIn } from "./filter-builder/equals.js"; import { AnyFilterFragmentBuilder, GetFilterFragmentBuilderPayload, } from "./filter-builder/filter-fragment-builder.js"; +import { + afterDate as filterAfterDate, + beforeDate as filterBeforeDate, +} from "./filter-builder/date-filter-builder.js"; import { contains as filterContains, endsWith as filterEndsWith, @@ -26,24 +21,29 @@ import { notStartsWith as filterNotStartsWith, startsWith as filterStartsWith, } from "./filter-builder/ilike-filter-builder.js"; -import { - notEquals as filterNotEquals, - notIn as filterNotIn, -} from "./filter-builder/not-equals.js"; -import { - notSet as filterSet, - set as filterNotSet, -} from "./filter-builder/null-check-filter-builder.js"; +import { equals as filterEquals, filterIn } from "./filter-builder/equals.js"; import { gt as filterGt, gte as filterGte, lt as filterLt, lte as filterLte, } from "./filter-builder/number-comparison-filter-builder.js"; +import { + inDateRange as filterInDateRange, + notInDateRange as filterNotInDateRange, +} from "./filter-builder/date-range-filter-builder.js"; import { inQuery as filterInQuery, notInQuery as filterNotInQuery, } from "./filter-builder/query-filter-builder.js"; +import { + notEquals as filterNotEquals, + notIn as filterNotIn, +} from "./filter-builder/not-equals.js"; +import { + set as filterNotSet, + notSet as filterSet, +} from "./filter-builder/null-check-filter-builder.js"; import { AnyQueryBuilder } from "../query-builder.js"; import { sqlAsSqlWithBindings } from "./util.js"; @@ -74,7 +74,7 @@ export class FilterBuilder { } if (this.filterType === "metric" && member.isMetric()) { const prefix = this.metricPrefixes?.[member.model.name]; - const sql = member.getAlias(this.queryBuilder.dialect); + const sql = member.getQuotedAlias(this.queryBuilder.dialect); return sqlAsSqlWithBindings( prefix ? `${this.queryBuilder.dialect.asIdentifier(prefix)}.${sql}` diff --git a/src/lib/repository.ts b/src/lib/repository.ts index 38506a1..641f2d4 100644 --- a/src/lib/repository.ts +++ b/src/lib/repository.ts @@ -1,8 +1,12 @@ import { - AvailableDialects, - AvailableDialectsNames, - DialectParamsReturnType, -} from "./dialect.js"; + AnyCustomGranularityElement, + CustomGranularityElementInit, +} from "./custom-granularity.js"; +import { + AnyFilterFragmentBuilderRegistry, + GetFilterFragmentBuilderRegistryPayload, + defaultFilterFragmentBuilderRegistry, +} from "./query-builder/filter-builder.js"; import { AnyJoin, JOIN_WEIGHTS, @@ -14,22 +18,17 @@ import { makeModelJoinPayload, } from "./join.js"; import { AnyModel, Model } from "./model.js"; -import type { Dimension, Metric } from "./model.js"; -import { - AnyFilterFragmentBuilderRegistry, - GetFilterFragmentBuilderRegistryPayload, - defaultFilterFragmentBuilderRegistry, -} from "./query-builder/filter-builder.js"; import { - CustomGranularity, - CustomGranularityElements, - GranularityType, - MemberNameToType, -} from "./types.js"; + AvailableDialects, + AvailableDialectsNames, + DialectParamsReturnType, +} from "./dialect.js"; +import type { Dimension, Metric } from "./model.js"; +import { GranularityType, MemberNameToType } from "./types.js"; +import { QueryBuilder } from "./query-builder.js"; import graphlib from "@dagrejs/graphlib"; import invariant from "tiny-invariant"; -import { QueryBuilder } from "./query-builder.js"; export type ModelC = T extends Model ? C @@ -80,8 +79,15 @@ export class Repository< > = {} as Record; readonly metricsIndex: Record = {} as Record; - readonly granularities: CustomGranularity[] = []; - readonly granularitiesNames: Set = new Set(); + public readonly categoricalGranularities: { + name: string; + elements: AnyCustomGranularityElement[]; + }[] = []; + public readonly temporalGranularities: { + name: string; + elements: AnyCustomGranularityElement[]; + }[] = []; + public readonly granularitiesNames: Set = new Set(); withModel(model: ModelWithMatchingContext) { this.models[model.name] = model; @@ -97,32 +103,6 @@ export class Repository< metric, }; } - for (const granularity of Object.values(model.granularities)) { - this.unsafeWithGranularity( - `${model.name}.${granularity.name}`, - granularity.elements.map((element) => { - if (typeof element === "string") { - return `${model.name}.${element}`; - } - const { key, elements, display } = element; - const namespacedDisplay = - display === undefined - ? undefined - : typeof display === "string" - ? `${model.name}.${display}` - : display.map((element) => `${model.name}.${element}`); - const namespacedElements = elements.map( - (element) => `${model.name}.${element}`, - ); - return { - key, - elements: namespacedElements, - display: namespacedDisplay, - }; - }), - granularity.type ?? "custom", - ); - } return this as unknown as Repository< C, @@ -136,37 +116,46 @@ export class Repository< unsafeWithGranularity( granularityName: string, - elements: CustomGranularityElements, + elements: AnyCustomGranularityElement[], type: GranularityType, - position: "top" | "bottom" = "bottom", ) { invariant( this.granularitiesNames.has(granularityName) === false, `Granularity ${granularityName} already exists`, ); this.granularitiesNames.add(granularityName); - if (position === "top") { - this.granularities.unshift({ - name: granularityName, - type, - elements, - }); - } else { - this.granularities.push({ - name: granularityName, - type, - elements, - }); + if (type === "categorical") { + this.categoricalGranularities.push({ name: granularityName, elements }); + } else if (type === "temporal") { + this.temporalGranularities.push({ name: granularityName, elements }); } return this; } - - withGranularity( + withCategoricalGranularity( granularityName: Exclude, - elements: CustomGranularityElements>, - type: GranularityType = "custom", + builder: (args: { + element: ( + name: string, + ) => CustomGranularityElementInit>; + }) => AnyCustomGranularityElement[], ): Repository { - return this.unsafeWithGranularity(granularityName, elements, type, "top"); + const elements = builder({ + element: (name) => new CustomGranularityElementInit(this, name), + }); + return this.unsafeWithGranularity(granularityName, elements, "categorical"); + } + withTemporalGranularity( + granularityName: Exclude, + builder: (args: { + element: ( + name: string, + ) => CustomGranularityElementInit>; + }) => AnyCustomGranularityElement[], + ): Repository { + const elements = builder({ + element: (name) => new CustomGranularityElementInit(this, name), + }); + return this.unsafeWithGranularity(granularityName, elements, "temporal"); } withFilterFragmentBuilderRegistry( @@ -322,6 +311,9 @@ export class Repository< } return model; } + getModels() { + return Object.values(this.models); + } getModelJoins(modelName: string) { return Object.values(this.joins[modelName] ?? {}); diff --git a/src/lib/types.ts b/src/lib/types.ts index aa48aae..73c88f3 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1,5 +1,9 @@ import { Replace, Simplify } from "type-fest"; import { exhaustiveCheck } from "./util.js"; +import { + CustomGranularityElement, + CustomGranularityElementConfig, +} from "./custom-granularity.js"; export interface AndConnective { operator: "and"; @@ -191,25 +195,37 @@ export function makeTemporalGranularityElementsForDimension( switch (dimensionType) { case "time": return [ - ...TemporalGranularityByDimensionType.time.map( - (granularity) => `${dimensionName}.${granularity}`, - ), - dimensionName, + ...TemporalGranularityByDimensionType.time.map((granularity) => { + const granularityDimensionName = `${dimensionName}.${granularity}`; + return new CustomGranularityElement(granularityDimensionName, [ + granularityDimensionName, + ]); + }), + new CustomGranularityElement(dimensionName, [dimensionName]), ]; - case "date": + + case "date": { return [ - ...TemporalGranularityByDimensionType.date.map( - (granularity) => `${dimensionName}.${granularity}`, - ), - dimensionName, + ...TemporalGranularityByDimensionType.date.map((granularity) => { + const granularityDimensionName = `${dimensionName}.${granularity}`; + return new CustomGranularityElement(granularityDimensionName, [ + granularityDimensionName, + ]); + }), + new CustomGranularityElement(dimensionName, [dimensionName]), ]; - case "datetime": + } + case "datetime": { return [ - ...TemporalGranularityByDimensionType.datetime.map( - (granularity) => `${dimensionName}.${granularity}`, - ), - dimensionName, + ...TemporalGranularityByDimensionType.datetime.map((granularity) => { + const granularityDimensionName = `${dimensionName}.${granularity}`; + return new CustomGranularityElement(granularityDimensionName, [ + granularityDimensionName, + ]); + }), + new CustomGranularityElement(dimensionName, [dimensionName]), ]; + } default: exhaustiveCheck( dimensionType, @@ -358,16 +374,9 @@ export type InputQueryMN = Q extends InputQuery export type AnyInputQuery = InputQuery; -export type CustomGranularityElement = - | MN - | { key: string; elements: MN[]; display?: MN | MN[] }; -export type CustomGranularityElements = - CustomGranularityElement[]; - -export type GranularityType = "custom" | "temporal"; - -export interface CustomGranularity { +export type GranularityType = "categorical" | "temporal"; +export interface GranularityConfig { name: string; type: GranularityType; - elements: CustomGranularityElements; + elements: CustomGranularityElementConfig[]; }