Skip to content

Commit

Permalink
fix: better types for query builder schema
Browse files Browse the repository at this point in the history
  • Loading branch information
retro committed Mar 21, 2024
1 parent 96e7c0e commit ec57b76
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 53 deletions.
21 changes: 20 additions & 1 deletion src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1212,7 +1212,7 @@ await describe("semantic layer", async () => {
});
});

await describe("fill repository", async () => {
await describe("full repository", async () => {
const customersModel = semanticLayer
.model("customers")
.fromTable("Customer")
Expand Down Expand Up @@ -1658,5 +1658,24 @@ await describe("semantic layer", async () => {
},
]);
});

await it("should return same query after it's parsed by schema", async () => {
const query = {
dimensions: ["artists.name"],
metrics: ["tracks.sum_unit_price"],
filters: [
{
operator: "equals",
member: "genres.name",
value: ["Rock"],
},
],
order: { "artists.name": "asc" },
limit: 10,
};

const parsedQuery = queryBuilder.querySchema.parse(query);
assert.deepEqual(query, parsedQuery);
});
});
});
102 changes: 50 additions & 52 deletions src/lib/query-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from "./types.js";

import knex from "knex";
import { ZodSchema, z } from "zod";
import { z } from "zod";

import { Simplify } from "type-fest";
import { BaseDialect } from "./dialect/base.js";
Expand All @@ -29,69 +29,67 @@ function getMemberNamesSchema(memberPaths: string[]) {
const [first, ...rest] = memberPaths;
return z.array(z.enum([first, ...rest])).optional();
}
return z.array(z.never()).optional();
return z.array(z.string()).max(0).optional();
}

function buildQuerySchema(repository: AnyRepository) {
const dimensionPaths = repository.getDimensions().map((d) => d.getPath());
const metricPaths = repository.getMetrics().map((m) => m.getPath());
const memberPaths = [...dimensionPaths, ...metricPaths];

const registeredFilterFragmentBuildersSchemas = repository
.getFilterFragmentBuilderRegistry()
.getFilterFragmentBuilders()
.map((builder) => builder.fragmentBuilderSchema);

const filters: z.ZodType<AnyQueryFilter[]> = 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.string(), 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;
}

export class QueryBuilder<
D extends MemberNameToType,
M extends MemberNameToType,
F,
> {
public readonly querySchema: ZodSchema;
public readonly querySchema: ReturnType<typeof buildQuerySchema>;
constructor(
private readonly repository: AnyRepository,
private readonly Dialect: typeof BaseDialect,
private readonly client: knex.Knex,
) {
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<AnyQueryFilter[]> = 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;
this.querySchema = buildQuerySchema(repository);
}

unsafeBuildQuery(payload: unknown) {
Expand Down

0 comments on commit ec57b76

Please sign in to comment.