From 52e1f754c13cefbb968ae5437256c077d792717e Mon Sep 17 00:00:00 2001 From: Mihael Konjevic Date: Wed, 20 Mar 2024 19:57:09 +0100 Subject: [PATCH] Expose query builder schema --- README.md | 10 +- package-lock.json | 12 +- package.json | 3 +- src/__tests__/index.test.ts | 564 +++++++++++++++++- src/lib/dialect/base.ts | 2 +- src/lib/model.ts | 6 + src/lib/query-builder.ts | 99 ++- src/lib/query-builder/build-query.ts | 6 +- src/lib/query-builder/filter-builder.ts | 27 +- .../filter-builder/filter-fragment-builder.ts | 2 +- src/lib/repository.ts | 32 +- src/lib/types.ts | 8 +- 12 files changed, 710 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index 918fd87..5242f07 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,8 @@ const repository = semanticLayer ({ sql, dimensions }) => sql`${dimensions.invoices.invoice_id} = ${dimensions.invoice_lines.invoice_id}` ); + +const queryBuilder = repository.build("postgresql"); ``` ### Data Querying @@ -126,7 +128,7 @@ Leverage the library's querying capabilities to fetch dimensions and metrics, ap ```typescript // Dimension and metric query -const query = repository.query({ +const query = queryBuilder.buildQuery({ dimensions: ["customers.customer_id"], metrics: ["invoices.total"], order: { "customers.customer_id": "asc" }, @@ -134,7 +136,7 @@ const query = repository.query({ }); // Metric query with filters -const query = repository.query({ +const query = queryBuilder.buildQuery({ metrics: ["invoices.total", "invoice_lines.quantity"], filters: [ { operator: "equals", member: "customers.customer_id", value: [1] }, @@ -142,7 +144,7 @@ const query = repository.query({ }); // Dimension query with filters -const query = repository.query({ +const query = queryBuilder.buildQuery({ dimensions: ["customers.first_name", "customers.last_name"], filters: [ { operator: "equals", member: "customers.customer_id", value: [1] }, @@ -150,7 +152,7 @@ const query = repository.query({ }); // Filtering and sorting -const query = repository.query({ +const query = queryBuilder.buildQuery({ dimensions: ["customers.first_name"], metrics: ["invoices.total"], filters: [{ operator: "gt", member: "invoices.total", value: [100] }], diff --git a/package-lock.json b/package-lock.json index 10f3549..076187f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,7 +40,8 @@ "pg": "^8.11.3", "rimraf": "^5.0.5", "semantic-release": "^23.0.4", - "typescript": "^5.4.2" + "typescript": "^5.4.2", + "zod-to-json-schema": "^3.22.4" }, "engines": { "node": "^21.2.0", @@ -11380,6 +11381,15 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zod-to-json-schema": { + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.22.4.tgz", + "integrity": "sha512-2Ed5dJ+n/O3cU383xSY28cuVi0BCQhF8nYqWU5paEpl7fVdqdAmiLdqLyfblbNdfOFwFfi/mqU4O1pwc60iBhQ==", + "dev": true, + "peerDependencies": { + "zod": "^3.22.4" + } } } } diff --git a/package.json b/package.json index bcc11d5..0b11bb5 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,8 @@ "pg": "^8.11.3", "rimraf": "^5.0.5", "semantic-release": "^23.0.4", - "typescript": "^5.4.2" + "typescript": "^5.4.2", + "zod-to-json-schema": "^3.22.4" }, "config": { "commitizen": { diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts index a2072fd..627a95f 100644 --- a/src/__tests__/index.test.ts +++ b/src/__tests__/index.test.ts @@ -1,16 +1,17 @@ -import * as C from "../index.js"; import * as assert from "node:assert/strict"; +import * as C from "../index.js"; +import { after, before, describe, it } from "node:test"; import { PostgreSqlContainer, StartedPostgreSqlContainer, } from "@testcontainers/postgresql"; -import { after, before, describe, it } from "node:test"; -import { InferSqlQueryResultType } from "../index.js"; import fs from "node:fs/promises"; import path from "node:path"; import pg from "pg"; +import { zodToJsonSchema } from "zod-to-json-schema"; +import { InferSqlQueryResultType } from "../index.js"; //import { format as sqlFormat } from "sql-formatter"; @@ -222,7 +223,7 @@ await describe("semantic layer", async () => { const queryBuilder = repository.build("postgresql"); await it("can query one dimension and one metric", async () => { - const query = queryBuilder.build({ + const query = queryBuilder.buildQuery({ dimensions: ["customers.customer_id"], metrics: ["invoices.total"], order: { "customers.customer_id": "asc" }, @@ -249,7 +250,7 @@ await describe("semantic layer", async () => { }); await it("can query one dimension and multiple metrics", async () => { - const query = queryBuilder.build({ + const query = queryBuilder.buildQuery({ dimensions: ["customers.customer_id"], metrics: ["invoices.total", "invoice_lines.total_unit_price"], order: { "customers.customer_id": "asc" }, @@ -316,7 +317,7 @@ await describe("semantic layer", async () => { }); await it("can query one dimension and metric and filter by a different metric", async () => { - const query = queryBuilder.build({ + const query = queryBuilder.buildQuery({ dimensions: ["customers.customer_id"], metrics: ["invoices.total"], order: { "customers.customer_id": "asc" }, @@ -350,7 +351,7 @@ await describe("semantic layer", async () => { }); await it("can query a metric and filter by a dimension", async () => { - const query = queryBuilder.build({ + const query = queryBuilder.buildQuery({ metrics: ["invoices.total"], filters: [ { operator: "equals", member: "customers.customer_id", value: [1] }, @@ -366,7 +367,7 @@ await describe("semantic layer", async () => { }); await it("can query multiple metrics and filter by a dimension", async () => { - const query = queryBuilder.build({ + const query = queryBuilder.buildQuery({ metrics: ["invoices.total", "invoice_lines.quantity"], filters: [ { operator: "equals", member: "customers.customer_id", value: [1] }, @@ -384,7 +385,7 @@ await describe("semantic layer", async () => { }); await it("can query dimensions only", async () => { - const query = queryBuilder.build({ + const query = queryBuilder.buildQuery({ dimensions: ["customers.customer_id", "albums.title"], filters: [ { operator: "equals", member: "customers.customer_id", value: [1] }, @@ -483,7 +484,7 @@ await describe("semantic layer", async () => { const queryBuilder = repository.build("postgresql"); await it("can query one dimension and multiple metrics", async () => { - const query = queryBuilder.build({ + const query = queryBuilder.buildQuery({ dimensions: ["customers.customer_id"], metrics: ["invoices.total"], order: { "customers.customer_id": "asc" }, @@ -539,4 +540,547 @@ await describe("semantic layer", async () => { ]); }); }); + + await describe("query schema", async () => { + await it("can parse a valid query", () => { + const customersModel = C.model("customers") + .fromSqlQuery('select * from "Customer"') + .withDimension("customer_id", { + type: "number", + primaryKey: true, + sql: ({ model, sql }) => sql`${model.column("CustomerId")}`, + }); + + const invoicesModel = C.model("invoices") + .fromSqlQuery('select * from "Invoice"') + .withDimension("invoice_id", { + type: "number", + primaryKey: true, + sql: ({ model }) => model.column("InvoiceId"), + }) + .withDimension("customer_id", { + type: "number", + sql: ({ model }) => model.column("CustomerId"), + }) + .withMetric("total", { + type: "string", + aggregateWith: "sum", + sql: ({ model }) => model.column("Total"), + }); + + const repository = C.repository() + .withModel(customersModel) + .withModel(invoicesModel) + .joinOneToMany( + "customers", + "invoices", + ({ sql, dimensions }) => + sql`${dimensions.customers.customer_id} = ${dimensions.invoices.customer_id}`, + ); + + const queryBuilder = repository.build("postgresql"); + + const query = { + dimensions: ["customers.customer_id"], + metrics: ["invoices.total"], + order: { "customers.customer_id": "asc" }, + filters: [ + { operator: "equals", member: "customers.customer_id", value: [1] }, + ], + limit: 10, + }; + + const parsed = queryBuilder.querySchema.safeParse(query); + assert.ok(parsed.success); + + const jsonSchema = zodToJsonSchema(queryBuilder.querySchema); + + assert.deepEqual(jsonSchema, { + type: "object", + properties: { + dimensions: { + type: "array", + items: { + type: "string", + enum: [ + "customers.customer_id", + "invoices.invoice_id", + "invoices.customer_id", + ], + }, + }, + metrics: { + type: "array", + items: { + type: "string", + enum: ["invoices.total"], + }, + }, + filters: { + type: "array", + items: { + anyOf: [ + { + type: "object", + properties: { + operator: { + type: "string", + const: "and", + }, + filters: { + $ref: "#/properties/filters", + }, + }, + required: ["operator", "filters"], + additionalProperties: false, + }, + { + type: "object", + properties: { + operator: { + type: "string", + const: "or", + }, + filters: { + $ref: "#/properties/filters", + }, + }, + required: ["operator", "filters"], + additionalProperties: false, + }, + { + type: "object", + properties: { + operator: { + type: "string", + const: "equals", + }, + member: { + type: "string", + }, + value: { + type: "array", + items: { + anyOf: [ + { + type: "string", + }, + { + type: "number", + }, + { + type: "integer", + format: "int64", + }, + { + type: "boolean", + }, + { + type: "string", + format: "date-time", + }, + ], + }, + }, + }, + required: ["operator", "member", "value"], + additionalProperties: false, + }, + { + type: "object", + properties: { + operator: { + type: "string", + const: "notEquals", + }, + member: { + type: "string", + }, + value: { + type: "array", + items: { + anyOf: [ + { + type: "string", + }, + { + type: "number", + }, + { + type: "integer", + format: "int64", + }, + { + type: "boolean", + }, + { + type: "string", + format: "date-time", + }, + ], + }, + }, + }, + required: ["operator", "member", "value"], + additionalProperties: false, + }, + { + type: "object", + properties: { + operator: { + type: "string", + const: "notSet", + }, + member: { + type: "string", + }, + }, + required: ["operator", "member"], + additionalProperties: false, + }, + { + type: "object", + properties: { + operator: { + type: "string", + const: "set", + }, + member: { + type: "string", + }, + }, + required: ["operator", "member"], + additionalProperties: false, + }, + { + type: "object", + properties: { + operator: { + type: "string", + const: "contains", + }, + member: { + type: "string", + }, + value: { + type: "array", + items: { + type: "string", + }, + }, + }, + required: ["operator", "member", "value"], + additionalProperties: false, + }, + { + type: "object", + properties: { + operator: { + type: "string", + const: "notContains", + }, + member: { + type: "string", + }, + value: { + type: "array", + items: { + type: "string", + }, + }, + }, + required: ["operator", "member", "value"], + additionalProperties: false, + }, + { + type: "object", + properties: { + operator: { + type: "string", + const: "startsWith", + }, + member: { + type: "string", + }, + value: { + type: "array", + items: { + type: "string", + }, + }, + }, + required: ["operator", "member", "value"], + additionalProperties: false, + }, + { + type: "object", + properties: { + operator: { + type: "string", + const: "notStartsWith", + }, + member: { + type: "string", + }, + value: { + type: "array", + items: { + type: "string", + }, + }, + }, + required: ["operator", "member", "value"], + additionalProperties: false, + }, + { + type: "object", + properties: { + operator: { + type: "string", + const: "endsWith", + }, + member: { + type: "string", + }, + value: { + type: "array", + items: { + type: "string", + }, + }, + }, + required: ["operator", "member", "value"], + additionalProperties: false, + }, + { + type: "object", + properties: { + operator: { + type: "string", + const: "notEndsWith", + }, + member: { + type: "string", + }, + value: { + type: "array", + items: { + type: "string", + }, + }, + }, + required: ["operator", "member", "value"], + additionalProperties: false, + }, + { + type: "object", + properties: { + operator: { + type: "string", + const: "gt", + }, + member: { + type: "string", + }, + value: { + type: "array", + items: { + type: "number", + }, + }, + }, + required: ["operator", "member", "value"], + additionalProperties: false, + }, + { + type: "object", + properties: { + operator: { + type: "string", + const: "gte", + }, + member: { + type: "string", + }, + value: { + type: "array", + items: { + type: "number", + }, + }, + }, + required: ["operator", "member", "value"], + additionalProperties: false, + }, + { + type: "object", + properties: { + operator: { + type: "string", + const: "lt", + }, + member: { + type: "string", + }, + value: { + type: "array", + items: { + type: "number", + }, + }, + }, + required: ["operator", "member", "value"], + additionalProperties: false, + }, + { + type: "object", + properties: { + operator: { + type: "string", + const: "lte", + }, + member: { + type: "string", + }, + value: { + type: "array", + items: { + type: "number", + }, + }, + }, + required: ["operator", "member", "value"], + additionalProperties: false, + }, + { + type: "object", + properties: { + operator: { + type: "string", + const: "inDateRange", + }, + member: { + type: "string", + }, + value: { + anyOf: [ + { + type: "string", + }, + { + type: "array", + minItems: 2, + maxItems: 2, + items: [ + { + anyOf: [ + { + type: "string", + }, + { + type: "string", + format: "date-time", + }, + ], + }, + { + anyOf: [ + { + type: "string", + }, + { + type: "string", + format: "date-time", + }, + ], + }, + ], + }, + ], + }, + }, + required: ["operator", "member", "value"], + additionalProperties: false, + }, + { + type: "object", + properties: { + operator: { + type: "string", + const: "notInDateRange", + }, + member: { + type: "string", + }, + value: { + $ref: "#/properties/filters/items/anyOf/16/properties/value", + }, + }, + required: ["operator", "member", "value"], + additionalProperties: false, + }, + { + type: "object", + properties: { + operator: { + type: "string", + const: "beforeDate", + }, + member: { + type: "string", + }, + value: { + anyOf: [ + { + type: "string", + }, + { + type: "string", + format: "date-time", + }, + ], + }, + }, + required: ["operator", "member", "value"], + additionalProperties: false, + }, + { + type: "object", + properties: { + operator: { + type: "string", + const: "afterDate", + }, + member: { + type: "string", + }, + value: { + $ref: "#/properties/filters/items/anyOf/18/properties/value", + }, + }, + required: ["operator", "member", "value"], + additionalProperties: false, + }, + ], + }, + }, + limit: { + type: "number", + }, + offset: { + type: "number", + }, + order: { + type: "object", + additionalProperties: { + type: "string", + enum: ["asc", "desc"], + }, + }, + }, + additionalProperties: false, + $schema: "http://json-schema.org/draft-07/schema#", + }); + }); + }); }); diff --git a/src/lib/dialect/base.ts b/src/lib/dialect/base.ts index e60675e..b532b39 100644 --- a/src/lib/dialect/base.ts +++ b/src/lib/dialect/base.ts @@ -1,5 +1,5 @@ -import { Granularity } from "../types.js"; import knex from "knex"; +import { Granularity } from "../types.js"; export class BaseDialect { constructor(private sqlQuery: knex.Knex.QueryBuilder) {} diff --git a/src/lib/model.ts b/src/lib/model.ts index a657813..fb0beaa 100644 --- a/src/lib/model.ts +++ b/src/lib/model.ts @@ -287,6 +287,12 @@ export class Model< } return member; } + getDimensions() { + return Object.values(this.dimensions); + } + getMetrics() { + return Object.values(this.metrics); + } getAs() { return this.config.type === "sqlQuery" ? this.config.alias diff --git a/src/lib/query-builder.ts b/src/lib/query-builder.ts index 27013fd..c85b55c 100644 --- a/src/lib/query-builder.ts +++ b/src/lib/query-builder.ts @@ -1,4 +1,6 @@ import { + AnyQuery, + AnyQueryFilter, MemberNameToType, Query, QueryMemberName, @@ -7,33 +9,92 @@ import { } from "./types.js"; import knex from "knex"; +import { ZodSchema, z } from "zod"; import { Simplify } from "type-fest"; -import type { AnyRepository } from "./repository.js"; import { BaseDialect } from "./dialect/base.js"; +import { buildQuery } from "./query-builder/build-query.js"; import { expandQueryToSegments } from "./query-builder/expand-query.js"; import { findOptimalJoinGraph } from "./query-builder/optimal-join-graph.js"; -import { buildQuery } from "./query-builder/build-query.js"; +import type { AnyRepository } from "./repository.js"; + +function isNonEmptyArray(arr: T[]): arr is [T, ...T[]] { + return arr.length > 0; +} + +function getMemberNamesSchema(memberPaths: string[]) { + if (isNonEmptyArray(memberPaths)) { + const [first, ...rest] = memberPaths; + return z.array(z.enum([first, ...rest])).optional(); + } + return z.array(z.never()).optional(); +} export class QueryBuilder< D extends MemberNameToType, M extends MemberNameToType, F, > { + public readonly querySchema: ZodSchema; constructor( private readonly repository: AnyRepository, private readonly Dialect: typeof BaseDialect, private readonly client: knex.Knex, - ) {} - - build( - query: Q & - Query< - string & keyof D, - string & keyof M, - F & { member: string & (keyof D | keyof M) } - >, ) { + this.querySchema = this.buildQuerySchema(); + } + + private buildQuerySchema() { + const dimensionPaths = this.repository + .getDimensions() + .map((d) => d.getPath()); + const metricPaths = this.repository.getMetrics().map((m) => m.getPath()); + const memberPaths = [...dimensionPaths, ...metricPaths]; + + const registeredFilterFragmentBuildersSchemas = this.repository + .getFilterFragmentBuilderRegistry() + .getFilterFragmentBuilders() + .map((builder) => builder.fragmentBuilderSchema); + + const filters: z.ZodType = z.array( + z.union([ + z.object({ + operator: z.literal("and"), + filters: z.lazy(() => filters), + }), + z.object({ + operator: z.literal("or"), + filters: z.lazy(() => filters), + }), + ...registeredFilterFragmentBuildersSchemas.map((schema) => + schema.refine((arg) => memberPaths.includes(arg.member), { + path: ["member"], + message: "Member not found", + }), + ), + ]), + ); + + const schema = z + .object({ + dimensions: getMemberNamesSchema(dimensionPaths), + metrics: getMemberNamesSchema(metricPaths), + filters: filters.optional(), + limit: z.number().optional(), + offset: z.number().optional(), + order: z.record(z.enum(["asc", "desc"])).optional(), + }) + .refine( + (arg) => (arg.dimensions?.length ?? 0) + (arg.metrics?.length ?? 0) > 0, + "At least one dimension or metric must be selected", + ); + + return schema; + } + + unsafeBuildQuery(payload: unknown) { + const query: AnyQuery = this.querySchema.parse(payload); + const { referencedModels, segments } = expandQueryToSegments( this.repository, query, @@ -56,6 +117,22 @@ export class QueryBuilder< const { sql, bindings } = sqlQuery.toSQL().toNative(); + return { + sql, + bindings: bindings as unknown[], + }; + } + + buildQuery( + query: Q & + Query< + string & keyof D, + string & keyof M, + F & { member: string & (keyof D | keyof M) } + >, + ) { + const { sql, bindings } = this.unsafeBuildQuery(query); + const result: SqlQueryResult< Simplify< QueryReturnType< diff --git a/src/lib/query-builder/build-query.ts b/src/lib/query-builder/build-query.ts index 55af3e6..ea94c18 100644 --- a/src/lib/query-builder/build-query.ts +++ b/src/lib/query-builder/build-query.ts @@ -2,11 +2,11 @@ import * as graphlib from "@dagrejs/graphlib"; import { AnyQuery, ModelQuery, QuerySegment } from "../types.js"; -import type { AnyRepository } from "../repository.js"; +import knex from "knex"; +import invariant from "tiny-invariant"; import { BaseDialect } from "../dialect/base.js"; import type { Join } from "../join.js"; -import invariant from "tiny-invariant"; -import knex from "knex"; +import type { AnyRepository } from "../repository.js"; interface ReferencedModels { all: string[]; diff --git a/src/lib/query-builder/filter-builder.ts b/src/lib/query-builder/filter-builder.ts index 5268d76..4d923d6 100644 --- a/src/lib/query-builder/filter-builder.ts +++ b/src/lib/query-builder/filter-builder.ts @@ -5,14 +5,18 @@ import { OrConnective, SqlWithBindings, } from "../types.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 { + inDateRange as filterInDateRange, + notInDateRange as filterNotInDateRange, +} from "./filter-builder/date-range-filter-builder.js"; +import { + AnyFilterFragmentBuilder, + GetFilterFragmentBuilderPayload, +} from "./filter-builder/filter-fragment-builder.js"; import { contains as filterContains, endsWith as filterEndsWith, @@ -21,20 +25,16 @@ import { notStartsWith as filterNotStartsWith, startsWith as filterStartsWith, } from "./filter-builder/ilike-filter-builder.js"; +import { + notSet as filterSet, + set as filterNotSet, +} from "./filter-builder/null-check-filter-builder.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 { - set as filterNotSet, - notSet as filterSet, -} from "./filter-builder/null-check-filter-builder.js"; import { BaseDialect } from "../dialect/base.js"; import type { Repository } from "../repository.js"; @@ -145,6 +145,9 @@ export class FilterFragmentBuilderRegistry { this.filterFragmentBuilders[builder.operator] = builder; return this; } + getFilterFragmentBuilders() { + return Object.values(this.filterFragmentBuilders); + } getFilterBuilder( repository: Repository, dialect: BaseDialect, diff --git a/src/lib/query-builder/filter-builder/filter-fragment-builder.ts b/src/lib/query-builder/filter-builder/filter-fragment-builder.ts index b51f7f9..507f577 100644 --- a/src/lib/query-builder/filter-builder/filter-fragment-builder.ts +++ b/src/lib/query-builder/filter-builder/filter-fragment-builder.ts @@ -1,7 +1,7 @@ import { ZodSchema, z } from "zod"; -import type { FilterBuilder } from "../filter-builder.js"; import { SqlWithBindings } from "../../types.js"; +import type { FilterBuilder } from "../filter-builder.js"; export class FilterFragmentBuilder< N extends string, diff --git a/src/lib/repository.ts b/src/lib/repository.ts index 868db4e..7f0c03d 100644 --- a/src/lib/repository.ts +++ b/src/lib/repository.ts @@ -1,10 +1,3 @@ -import { - AnyFilterFragmentBuilderRegistry, - GetFilterFragmentBuilderRegistryPayload, - defaultFilterFragmentBuilderRegistry, -} from "./query-builder/filter-builder.js"; -import { AnyModel, Model } from "./model.js"; -import { AvailableDialects, FilterType, MemberNameToType } from "./types.js"; import { JOIN_WEIGHTS, Join, @@ -14,12 +7,19 @@ import { JoinOnDef, REVERSED_JOIN, } from "./join.js"; +import { AnyModel, Model } from "./model.js"; +import { + AnyFilterFragmentBuilderRegistry, + GetFilterFragmentBuilderRegistryPayload, + defaultFilterFragmentBuilderRegistry, +} from "./query-builder/filter-builder.js"; +import { AvailableDialects, FilterType, MemberNameToType } from "./types.js"; -import { BaseDialect } from "./dialect/base.js"; -import { QueryBuilder } from "./query-builder.js"; import graphlib from "@dagrejs/graphlib"; -import invariant from "tiny-invariant"; import knex from "knex"; +import invariant from "tiny-invariant"; +import { BaseDialect } from "./dialect/base.js"; +import { QueryBuilder } from "./query-builder.js"; // biome-ignore lint/suspicious/noExplicitAny: Using any for inference export type ModelN = T extends Model ? N : never; @@ -98,6 +98,10 @@ export class Repository< >; } + public getFilterFragmentBuilderRegistry() { + return this.filterFragmentBuilderRegistry; + } + getFilterBuilder( repository: Repository, dialect: BaseDialect, @@ -234,6 +238,14 @@ export class Repository< throw new Error(`Member ${memberName} not found`); } + getDimensions() { + return Object.values(this.models).flatMap((m) => m.getDimensions()); + } + + getMetrics() { + return Object.values(this.models).flatMap((m) => m.getMetrics()); + } + getModel(modelName: string) { const model = this.models[modelName]; if (!model) { diff --git a/src/lib/types.ts b/src/lib/types.ts index 9544800..ba40d83 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -76,13 +76,7 @@ export type GranularityByDimensionType = typeof GranularityByDimensionType; export type Granularity = GranularityByDimensionType[keyof GranularityByDimensionType][number]; -export type MemberType = - | "string" - | "number" - | "date" - | "time" - | "datetime" - | "boolean"; +export type MemberType = "string" | "number" | "date" | "datetime" | "boolean"; export type MemberNameToType = { [k in never]: MemberType };