Skip to content

Commit 77e30dd

Browse files
oskardudyczclaude
andcommitted
feat: add database-level error collection to relationship validation
Completed hierarchical error context by implementing database-level error collection that aggregates schema errors as an array. Changes: - Modified ValidateDatabaseSchemas to use MapRecordCollectErrors instead of ExtractTypeValidationErrors - Database-level validation now collects errors from all schemas as an array - Removed unused ExtractTypeValidationErrors import - Added comprehensive tests in validateDatabaseSchemas.type.spec.ts covering: * Success case when all schemas are valid * Single invalid schema with full context chain (schema → table → relationship) * Multiple invalid schemas with errors collected as separate array items * Empty database success case The complete error hierarchy is now: - Database level: Array of schema errors - Schema level: { schema, errors: [...table errors] } - Table level: { table, errors: [...relationship errors] } - Relationship level: { relationship, errors: [...validation errors] } All validation errors maintain proper TypeValidationResult structure throughout the chain, enabling precise error reporting with full context at every level. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 673f0f2 commit 77e30dd

File tree

2 files changed

+230
-11
lines changed

2 files changed

+230
-11
lines changed

src/packages/dumbo/src/core/schema/components/relationships/relationshipValidation.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import type {
1111
AND,
1212
AnyTypeValidationError,
1313
AnyTypeValidationFailed,
14-
ExtractTypeValidationErrors,
1514
FailOnFirstTypeValidationError,
1615
FilterNotExistingInUnion,
1716
HaveTuplesTheSameLength,
@@ -479,16 +478,20 @@ export type ValidateDatabaseSchema<
479478
? ValidateSchemaTables<Tables, SchemaName, Schema, Schemas>
480479
: TypeValidationSuccess;
481480

482-
export type ValidateDatabaseSchemas<Schemas extends DatabaseSchemas> = {
483-
[SchemaName in keyof Schemas]: ValidateDatabaseSchema<
484-
Schemas[SchemaName],
485-
Schemas
486-
>;
487-
}[keyof Schemas] extends infer Results
488-
? ExtractTypeValidationErrors<Results> extends never
489-
? TypeValidationSuccess
490-
: TypeValidationError<ExtractTypeValidationErrors<Results>>
491-
: TypeValidationSuccess;
481+
export type ValidateDatabaseSchemas<Schemas extends DatabaseSchemas> =
482+
MapRecordCollectErrors<
483+
Schemas,
484+
{
485+
[SchemaName in keyof Schemas]: ValidateDatabaseSchema<
486+
Schemas[SchemaName],
487+
Schemas
488+
>;
489+
}
490+
> extends infer Results
491+
? AnyTypeValidationFailed<Results> extends true
492+
? TypeValidationError<Results>
493+
: TypeValidationSuccess
494+
: TypeValidationSuccess;
492495

493496
// TODO: Use in DatabaseSchema schema component validation
494497
// export type ValidatedSchemaComponent<
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import { describe, it } from 'node:test';
2+
import { SQL } from '../../../sql';
3+
import type { Equals, Expect, IsError } from '../../../testing';
4+
import type { TypeValidationResult } from '../../../typing';
5+
import { dumboSchema } from '../../dumboSchema';
6+
import { relationship } from './relationshipTypes';
7+
import type { ValidateDatabaseSchemas } from './relationshipValidation';
8+
9+
const { schema, table, column } = dumboSchema;
10+
const { Integer, BigInteger } = SQL.column.type;
11+
12+
void describe('ValidateDatabaseSchemas', () => {
13+
const usersTable = table('users', {
14+
columns: {
15+
id: column('id', Integer),
16+
},
17+
});
18+
19+
const postsTable = table('posts', {
20+
columns: {
21+
post_id: column('post_id', Integer),
22+
user_id: column('user_id', Integer),
23+
},
24+
relationships: {
25+
user: relationship(['user_id'], ['public.users.id'], 'one-to-one'),
26+
},
27+
});
28+
29+
void it('returns success when all schemas are valid', () => {
30+
const publicSchema = schema('public', {
31+
users: usersTable,
32+
posts: postsTable,
33+
});
34+
35+
type ValidSchemas = {
36+
public: typeof publicSchema;
37+
};
38+
39+
type Result = ValidateDatabaseSchemas<ValidSchemas>;
40+
41+
type _Then = Expect<Equals<Result, TypeValidationResult<true, undefined>>>;
42+
});
43+
44+
void it('collects errors from a single invalid schema', () => {
45+
const invalidTable = table('invalid', {
46+
columns: {
47+
col1: column('col1', Integer),
48+
col2: column('col2', Integer),
49+
},
50+
relationships: {
51+
bad_rel: relationship(
52+
['col1', 'col2'],
53+
['public.users.id'],
54+
'one-to-one',
55+
),
56+
},
57+
});
58+
59+
const publicSchema = schema('public', {
60+
users: usersTable,
61+
invalid: invalidTable,
62+
});
63+
64+
type TestSchemas = {
65+
public: typeof publicSchema;
66+
};
67+
68+
type Result = ValidateDatabaseSchemas<TestSchemas>;
69+
70+
type Expected = TypeValidationResult<
71+
false,
72+
[
73+
TypeValidationResult<
74+
false,
75+
{
76+
schema: 'public';
77+
errors: [
78+
TypeValidationResult<
79+
false,
80+
{
81+
table: 'invalid';
82+
errors: [
83+
TypeValidationResult<
84+
false,
85+
{
86+
relationship: 'bad_rel';
87+
errors: [
88+
{
89+
errorCode: 'reference_length_mismatch';
90+
columns: readonly ['col1', 'col2'];
91+
references: readonly ['public.users.id'];
92+
},
93+
];
94+
}
95+
>,
96+
];
97+
}
98+
>,
99+
];
100+
}
101+
>,
102+
]
103+
>;
104+
105+
type _Then = [Expect<IsError<Result>>, Expect<Equals<Result, Expected>>];
106+
});
107+
108+
void it('collects errors from multiple invalid schemas', () => {
109+
const invalidTable1 = table('invalid1', {
110+
columns: {
111+
col1: column('col1', Integer),
112+
},
113+
relationships: {
114+
bad_rel: relationship(['col1'], ['nonexistent.table.id'], 'one-to-one'),
115+
},
116+
});
117+
118+
const schema1 = schema('schema1', {
119+
invalid1: invalidTable1,
120+
});
121+
122+
const invalidTable2 = table('invalid2', {
123+
columns: {
124+
col2: column('col2', BigInteger),
125+
},
126+
relationships: {
127+
user: relationship(['col2'], ['schema1.invalid1.col1'], 'one-to-one'),
128+
},
129+
});
130+
131+
const schema2 = schema('schema2', {
132+
invalid2: invalidTable2,
133+
});
134+
135+
type TestSchemas = {
136+
schema1: typeof schema1;
137+
schema2: typeof schema2;
138+
};
139+
140+
type Result = ValidateDatabaseSchemas<TestSchemas>;
141+
142+
type Expected = TypeValidationResult<
143+
false,
144+
[
145+
TypeValidationResult<
146+
false,
147+
{
148+
schema: 'schema1';
149+
errors: [
150+
TypeValidationResult<
151+
false,
152+
{
153+
table: 'invalid1';
154+
errors: [
155+
TypeValidationResult<
156+
false,
157+
{
158+
relationship: 'bad_rel';
159+
errors: [
160+
{
161+
errorCode: 'missing_schema';
162+
reference: 'nonexistent.table.id';
163+
},
164+
];
165+
}
166+
>,
167+
];
168+
}
169+
>,
170+
];
171+
}
172+
>,
173+
TypeValidationResult<
174+
false,
175+
{
176+
schema: 'schema2';
177+
errors: [
178+
TypeValidationResult<
179+
false,
180+
{
181+
table: 'invalid2';
182+
errors: [
183+
TypeValidationResult<
184+
false,
185+
{
186+
relationship: 'user';
187+
errors: [
188+
{
189+
errorCode: 'type_mismatch';
190+
reference: 'schema1.invalid1.col1';
191+
referenceType: 'INTEGER';
192+
columnTypeName: 'BIGINT';
193+
},
194+
];
195+
}
196+
>,
197+
];
198+
}
199+
>,
200+
];
201+
}
202+
>,
203+
]
204+
>;
205+
206+
type _Then = [Expect<IsError<Result>>, Expect<Equals<Result, Expected>>];
207+
});
208+
209+
void it('returns success when database has no schemas', () => {
210+
type EmptySchemas = {};
211+
212+
type Result = ValidateDatabaseSchemas<EmptySchemas>;
213+
214+
type _Then = Expect<Equals<Result, TypeValidationResult<true, undefined>>>;
215+
});
216+
});

0 commit comments

Comments
 (0)