Skip to content

Commit 1e9e213

Browse files
committed
refactor: on repository build validate that repository models are properly connected
1 parent 07f3224 commit 1e9e213

File tree

2 files changed

+148
-0
lines changed

2 files changed

+148
-0
lines changed
+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import * as semanticLayer from "../index.js";
2+
3+
import { assert, it } from "vitest";
4+
5+
const customersModel = semanticLayer
6+
.model()
7+
.withName("customers")
8+
.fromTable("Customer")
9+
.withDimension("customer_id", {
10+
type: "number",
11+
primaryKey: true,
12+
sql: ({ model, sql }) => sql`${model.column("CustomerId")}`,
13+
})
14+
.withDimension("first_name", {
15+
type: "string",
16+
sql: ({ model }) => model.column("FirstName"),
17+
});
18+
19+
const invoicesModel = semanticLayer
20+
.model()
21+
.withName("invoices")
22+
.fromTable("Invoice")
23+
.withDimension("invoice_id", {
24+
type: "number",
25+
primaryKey: true,
26+
sql: ({ model }) => model.column("InvoiceId"),
27+
})
28+
.withDimension("customer_id", {
29+
type: "number",
30+
sql: ({ model }) => model.column("CustomerId"),
31+
});
32+
33+
const invoiceLinesModel = semanticLayer
34+
.model()
35+
.withName("invoice_lines")
36+
.fromTable("InvoiceLine")
37+
.withDimension("invoice_line_id", {
38+
type: "number",
39+
primaryKey: true,
40+
sql: ({ model }) => model.column("InvoiceLineId"),
41+
})
42+
.withDimension("invoice_id", {
43+
type: "number",
44+
sql: ({ model }) => model.column("InvoiceId"),
45+
})
46+
.withDimension("track_id", {
47+
type: "number",
48+
sql: ({ model }) => model.column("TrackId"),
49+
});
50+
51+
const tracksModel = semanticLayer
52+
.model()
53+
.withName("tracks")
54+
.fromTable("Track")
55+
.withDimension("track_id", {
56+
type: "number",
57+
primaryKey: true,
58+
sql: ({ model }) => model.column("TrackId"),
59+
});
60+
61+
it("will correctly check if all models are connected when no joins exists", () => {
62+
const repository = semanticLayer
63+
.repository()
64+
.withModel(customersModel)
65+
.withModel(invoicesModel)
66+
.withModel(invoiceLinesModel)
67+
.withModel(tracksModel);
68+
69+
assert.throws(() => {
70+
repository.build("postgresql");
71+
}, "All models in a repository must be connected.");
72+
});
73+
74+
it("will correctly check if all models are connected when only some models are connected (1)", () => {
75+
const repository = semanticLayer
76+
.repository()
77+
.withModel(customersModel)
78+
.withModel(invoicesModel)
79+
.withModel(invoiceLinesModel)
80+
.withModel(tracksModel)
81+
.joinOneToMany(
82+
"customers",
83+
"invoices",
84+
({ sql, models }) =>
85+
sql`${models.customers.dimension(
86+
"customer_id",
87+
)} = ${models.invoices.dimension("customer_id")}`,
88+
);
89+
90+
assert.throws(() => {
91+
repository.build("postgresql");
92+
}, "All models in a repository must be connected.");
93+
});
94+
95+
it("will correctly check if all models are connected when only some models are connected (2)", () => {
96+
const repository = semanticLayer
97+
.repository()
98+
.withModel(customersModel)
99+
.withModel(invoicesModel)
100+
.withModel(invoiceLinesModel)
101+
.withModel(tracksModel)
102+
.joinOneToMany(
103+
"customers",
104+
"invoices",
105+
({ sql, models }) =>
106+
sql`${models.customers.dimension(
107+
"customer_id",
108+
)} = ${models.invoices.dimension("customer_id")}`,
109+
)
110+
.joinManyToOne(
111+
"invoice_lines",
112+
"tracks",
113+
({ sql, models }) =>
114+
sql`${models.invoice_lines.dimension(
115+
"track_id",
116+
)} = ${models.tracks.dimension("track_id")}`,
117+
);
118+
119+
assert.throws(() => {
120+
repository.build("postgresql");
121+
}, "All models in a repository must be connected.");
122+
});

src/lib/repository.ts

+26
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,22 @@ export type ModelWithMatchingContext<C, T extends AnyModel> = [C] extends [
5555

5656
export type AnyRepository = Repository<any, any, any, any, any, any>;
5757

58+
function isRepositoryProperlyConnected(
59+
modelNames: string[],
60+
components: string[][],
61+
) {
62+
if (modelNames.length === 1) {
63+
return true;
64+
}
65+
66+
if (components.length === 1) {
67+
const firstComponent = components[0]!;
68+
return firstComponent.length === modelNames.length;
69+
}
70+
71+
return false;
72+
}
73+
5874
export class Repository<
5975
TContext,
6076
TModelNames extends string = never,
@@ -456,6 +472,16 @@ export class Repository<
456472
build<N extends AvailableDialectsNames, P = DialectParamsReturnType<N>>(
457473
dialectName: N,
458474
) {
475+
const repositoryGraphComponents = graphlib.alg.components(this.graph);
476+
477+
invariant(
478+
isRepositoryProperlyConnected(
479+
Object.keys(this.models),
480+
repositoryGraphComponents,
481+
),
482+
"All models in a repository must be connected.",
483+
);
484+
459485
const dialect = AvailableDialects[dialectName];
460486
return new QueryBuilder<
461487
TContext,

0 commit comments

Comments
 (0)