@spooky-sync/query-builder 0.0.0-canary.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +413 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.d.ts +413 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +459 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +448 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +49 -0
- package/src/index.ts +65 -0
- package/src/query-builder.test.ts +470 -0
- package/src/query-builder.ts +933 -0
- package/src/repro_relationship.test.ts +59 -0
- package/src/table-schema.ts +268 -0
- package/src/types.ts +216 -0
- package/tsconfig.json +22 -0
- package/tsdown.config.ts +11 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { QueryBuilder, SchemaStructure } from './index';
|
|
3
|
+
|
|
4
|
+
describe('QueryBuilder Relationship Inference', () => {
|
|
5
|
+
const schema = {
|
|
6
|
+
tables: [
|
|
7
|
+
{
|
|
8
|
+
name: 'game_database',
|
|
9
|
+
columns: {
|
|
10
|
+
id: { type: 'string', recordId: true, optional: false },
|
|
11
|
+
games: { type: 'string', optional: true },
|
|
12
|
+
},
|
|
13
|
+
primaryKey: ['id'],
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: 'game',
|
|
17
|
+
columns: {
|
|
18
|
+
id: { type: 'string', recordId: true, optional: false },
|
|
19
|
+
database: { type: 'string', recordId: true, optional: false },
|
|
20
|
+
},
|
|
21
|
+
primaryKey: ['id'],
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
relationships: [
|
|
25
|
+
{
|
|
26
|
+
from: 'game_database',
|
|
27
|
+
field: 'games',
|
|
28
|
+
to: 'game',
|
|
29
|
+
cardinality: 'many',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
from: 'game',
|
|
33
|
+
field: 'database',
|
|
34
|
+
to: 'game_database',
|
|
35
|
+
cardinality: 'one',
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
backends: {},
|
|
39
|
+
} as const;
|
|
40
|
+
|
|
41
|
+
it('should correctly infer foreign key field for game_database -> games', () => {
|
|
42
|
+
const qb = new QueryBuilder(schema, 'game_database');
|
|
43
|
+
|
|
44
|
+
// We want to simulate: SELECT *, (SELECT * FROM game WHERE database=$parent.id) AS games FROM game_database
|
|
45
|
+
// This happens when we include 'games' in the related fields
|
|
46
|
+
|
|
47
|
+
// The QueryBuilder.related method adds to options.related
|
|
48
|
+
qb.related('games');
|
|
49
|
+
|
|
50
|
+
// To verify the generated subquery, we need to inspect the build output
|
|
51
|
+
// buildQueryFromOptions is internal, but we can check the result of build()
|
|
52
|
+
// or we can inspect the private options if we cast to any
|
|
53
|
+
|
|
54
|
+
const options = (qb as any).options;
|
|
55
|
+
expect(options.related).toHaveLength(1);
|
|
56
|
+
expect(options.related[0].alias).toBe('games');
|
|
57
|
+
expect(options.related[0].foreignKeyField).toBe('database');
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supported value types in the schema
|
|
3
|
+
*/
|
|
4
|
+
export type ValueType = 'string' | 'number' | 'boolean' | 'null' | 'json';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Column metadata defining the type and optionality of a field
|
|
8
|
+
*/
|
|
9
|
+
export interface ColumnSchema {
|
|
10
|
+
readonly type: ValueType;
|
|
11
|
+
readonly optional: boolean;
|
|
12
|
+
readonly dateTime?: boolean;
|
|
13
|
+
readonly recordId?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Table metadata containing columns and primary key information
|
|
18
|
+
*/
|
|
19
|
+
export interface TableSchemaMetadata {
|
|
20
|
+
readonly name: string;
|
|
21
|
+
readonly columns: {
|
|
22
|
+
readonly [columnName: string]: ColumnSchema;
|
|
23
|
+
};
|
|
24
|
+
readonly primaryKey: readonly string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Cardinality of a relationship: one-to-one or one-to-many
|
|
29
|
+
*/
|
|
30
|
+
export type Cardinality = 'one' | 'many';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Relationship metadata defining how tables relate to each other
|
|
34
|
+
*/
|
|
35
|
+
export interface RelationshipMetadata {
|
|
36
|
+
readonly model: string;
|
|
37
|
+
readonly table: string;
|
|
38
|
+
readonly cardinality: Cardinality;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Complete schema metadata structure
|
|
43
|
+
* Maps table names to their schemas and relationships
|
|
44
|
+
*/
|
|
45
|
+
export interface SchemaMetadataStructure {
|
|
46
|
+
readonly tables: {
|
|
47
|
+
readonly [tableName: string]: TableSchemaMetadata;
|
|
48
|
+
};
|
|
49
|
+
readonly relationships: {
|
|
50
|
+
readonly [tableName: string]: {
|
|
51
|
+
readonly [fieldName: string]: RelationshipMetadata;
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Type mapping from ValueType to TypeScript types
|
|
58
|
+
*/
|
|
59
|
+
export type TypeNameToTypeMap = {
|
|
60
|
+
string: string;
|
|
61
|
+
number: number;
|
|
62
|
+
boolean: boolean;
|
|
63
|
+
null: null;
|
|
64
|
+
json: unknown;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Convert a column type to its TypeScript type
|
|
69
|
+
*/
|
|
70
|
+
export type ColumnToTSType<T extends ColumnSchema> = T extends {
|
|
71
|
+
optional: true;
|
|
72
|
+
}
|
|
73
|
+
? TypeNameToTypeMap[T['type']] | null
|
|
74
|
+
: TypeNameToTypeMap[T['type']];
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Helper to extract relationship field names for a table
|
|
78
|
+
*/
|
|
79
|
+
export type RelationshipFieldNames<
|
|
80
|
+
Metadata extends SchemaMetadataStructure,
|
|
81
|
+
TableName extends keyof Metadata['relationships'] & string,
|
|
82
|
+
> = keyof Metadata['relationships'][TableName] & string;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Helper to get cardinality for a specific relationship
|
|
86
|
+
*/
|
|
87
|
+
export type GetRelationshipCardinality<
|
|
88
|
+
Metadata extends SchemaMetadataStructure,
|
|
89
|
+
TableName extends keyof Metadata['relationships'] & string,
|
|
90
|
+
FieldName extends keyof Metadata['relationships'][TableName] & string,
|
|
91
|
+
> = Metadata['relationships'][TableName][FieldName]['cardinality'];
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Helper to get related table name for a relationship
|
|
95
|
+
*/
|
|
96
|
+
export type GetRelatedTable<
|
|
97
|
+
Metadata extends SchemaMetadataStructure,
|
|
98
|
+
TableName extends keyof Metadata['relationships'] & string,
|
|
99
|
+
FieldName extends keyof Metadata['relationships'][TableName] & string,
|
|
100
|
+
> = Metadata['relationships'][TableName][FieldName]['table'];
|
|
101
|
+
|
|
102
|
+
// ============================================================================
|
|
103
|
+
// ACCESS SCHEMA HELPERS
|
|
104
|
+
// ============================================================================
|
|
105
|
+
|
|
106
|
+
export interface AccessDefinition {
|
|
107
|
+
readonly signIn: {
|
|
108
|
+
readonly params: Record<string, ColumnSchema>;
|
|
109
|
+
};
|
|
110
|
+
readonly signup: {
|
|
111
|
+
readonly params: Record<string, ColumnSchema>;
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ============================================================================
|
|
116
|
+
// ARRAY-BASED SCHEMA TYPE HELPERS
|
|
117
|
+
// ============================================================================
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Base schema structure for array-based schemas
|
|
121
|
+
*/
|
|
122
|
+
export interface SchemaStructure {
|
|
123
|
+
readonly tables: readonly {
|
|
124
|
+
readonly name: string;
|
|
125
|
+
readonly columns: Record<string, ColumnSchema>;
|
|
126
|
+
readonly primaryKey: readonly string[];
|
|
127
|
+
}[];
|
|
128
|
+
readonly relationships: readonly {
|
|
129
|
+
readonly from: string;
|
|
130
|
+
readonly field: string;
|
|
131
|
+
readonly to: string;
|
|
132
|
+
readonly cardinality: Cardinality;
|
|
133
|
+
}[];
|
|
134
|
+
readonly backends: Record<string, HTTPOutboxBackendDefinition>;
|
|
135
|
+
readonly access?: Record<string, AccessDefinition>;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface HTTPOutboxBackendDefinition {
|
|
139
|
+
readonly outboxTable: string;
|
|
140
|
+
readonly routes: Record<string, HTTPBackendRouteDefinition>;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface HTTPBackendRouteDefinition {
|
|
144
|
+
readonly args: Record<string, HTTPBackendRouteArgsDefinition>;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface HTTPBackendRouteArgsDefinition {
|
|
148
|
+
readonly type: ValueType;
|
|
149
|
+
readonly optional: boolean;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ============================================================================
|
|
153
|
+
// BACKEND / RUN TYPE HELPERS
|
|
154
|
+
// ============================================================================
|
|
155
|
+
|
|
156
|
+
/** Extract all backend names from the schema */
|
|
157
|
+
export type BackendNames<S extends SchemaStructure> = keyof S['backends'] & string;
|
|
158
|
+
|
|
159
|
+
/** Extract all route paths for a specific backend */
|
|
160
|
+
export type BackendRoutes<
|
|
161
|
+
S extends SchemaStructure,
|
|
162
|
+
B extends BackendNames<S>,
|
|
163
|
+
> = keyof S['backends'][B]['routes'] & string;
|
|
164
|
+
|
|
165
|
+
/** Build the typed payload for a specific route */
|
|
166
|
+
export type RoutePayload<
|
|
167
|
+
S extends SchemaStructure,
|
|
168
|
+
B extends BackendNames<S>,
|
|
169
|
+
R extends BackendRoutes<S, B>,
|
|
170
|
+
> = [keyof RouteArgsDef<S, B, R>] extends [never]
|
|
171
|
+
? Record<string, never>
|
|
172
|
+
: Prettify<
|
|
173
|
+
{ [K in RequiredRouteArgKeys<S, B, R>]: TypeNameToTypeMap[RouteArgsDef<S, B, R>[K]['type']] }
|
|
174
|
+
&
|
|
175
|
+
{ [K in OptionalRouteArgKeys<S, B, R>]?: TypeNameToTypeMap[RouteArgsDef<S, B, R>[K]['type']] }
|
|
176
|
+
>;
|
|
177
|
+
|
|
178
|
+
type RouteArgsDef<
|
|
179
|
+
S extends SchemaStructure,
|
|
180
|
+
B extends BackendNames<S>,
|
|
181
|
+
R extends BackendRoutes<S, B>,
|
|
182
|
+
> = S['backends'][B]['routes'][R]['args'];
|
|
183
|
+
|
|
184
|
+
type RequiredRouteArgKeys<
|
|
185
|
+
S extends SchemaStructure,
|
|
186
|
+
B extends BackendNames<S>,
|
|
187
|
+
R extends BackendRoutes<S, B>,
|
|
188
|
+
> = {
|
|
189
|
+
[K in keyof RouteArgsDef<S, B, R>]: RouteArgsDef<S, B, R>[K]['optional'] extends true ? never : K;
|
|
190
|
+
}[keyof RouteArgsDef<S, B, R>] & string;
|
|
191
|
+
|
|
192
|
+
type OptionalRouteArgKeys<
|
|
193
|
+
S extends SchemaStructure,
|
|
194
|
+
B extends BackendNames<S>,
|
|
195
|
+
R extends BackendRoutes<S, B>,
|
|
196
|
+
> = {
|
|
197
|
+
[K in keyof RouteArgsDef<S, B, R>]: RouteArgsDef<S, B, R>[K]['optional'] extends true ? K : never;
|
|
198
|
+
}[keyof RouteArgsDef<S, B, R>] & string;
|
|
199
|
+
|
|
200
|
+
type Prettify<T> = { [K in keyof T]: T[K] } & {};
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Extract a specific table by name from the schema tables array
|
|
204
|
+
*/
|
|
205
|
+
export type GetTable<S extends SchemaStructure, Name extends TableNames<S>> = Extract<
|
|
206
|
+
S['tables'][number],
|
|
207
|
+
{ name: Name }
|
|
208
|
+
>;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Extract all table names from the schema
|
|
212
|
+
*/
|
|
213
|
+
export type TableNames<S extends SchemaStructure> = S['tables'][number]['name'];
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Extract all field names from a type
|
|
217
|
+
*/
|
|
218
|
+
export type TableFieldNames<T extends { columns: Record<string, ColumnSchema> }> =
|
|
219
|
+
keyof T['columns'] & string;
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Convert table schema columns to a TypeScript model type
|
|
223
|
+
*/
|
|
224
|
+
export type TableModel<T extends { columns: Record<string, ColumnSchema> }> = {
|
|
225
|
+
[K in keyof T['columns']]: ColumnToTSType<T['columns'][K]>;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Extract all relationships for a specific table from relationships array
|
|
230
|
+
*/
|
|
231
|
+
export type TableRelationships<S extends SchemaStructure, TableName extends string> = Extract<
|
|
232
|
+
S['relationships'][number],
|
|
233
|
+
{ from: TableName }
|
|
234
|
+
>;
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Get relationship field names for a table
|
|
238
|
+
*/
|
|
239
|
+
export type RelationshipFields<
|
|
240
|
+
S extends SchemaStructure,
|
|
241
|
+
TableName extends string,
|
|
242
|
+
> = TableRelationships<S, TableName>['field'];
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Get specific relationship by table and field
|
|
246
|
+
*/
|
|
247
|
+
export type GetRelationship<
|
|
248
|
+
S extends SchemaStructure,
|
|
249
|
+
TableName extends string,
|
|
250
|
+
Field extends string,
|
|
251
|
+
> = Extract<Extract<S['relationships'][number], { from: TableName }>, { field: Field }>;
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Convert array-based schema to indexed format (for internal compatibility)
|
|
255
|
+
*/
|
|
256
|
+
export type SchemaToIndexed<S extends SchemaStructure> = {
|
|
257
|
+
tables: {
|
|
258
|
+
[K in S['tables'][number]['name']]: Extract<S['tables'][number], { name: K }>;
|
|
259
|
+
};
|
|
260
|
+
relationships: {
|
|
261
|
+
[K in S['tables'][number]['name']]: {
|
|
262
|
+
[R in Extract<S['relationships'][number], { from: K }>['field']]: Extract<
|
|
263
|
+
Extract<S['relationships'][number], { from: K }>,
|
|
264
|
+
{ field: R }
|
|
265
|
+
>;
|
|
266
|
+
};
|
|
267
|
+
};
|
|
268
|
+
};
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// Import new schema types
|
|
2
|
+
export type {
|
|
3
|
+
ValueType,
|
|
4
|
+
ColumnSchema,
|
|
5
|
+
TableSchemaMetadata,
|
|
6
|
+
Cardinality,
|
|
7
|
+
RelationshipMetadata,
|
|
8
|
+
SchemaMetadataStructure,
|
|
9
|
+
AccessDefinition,
|
|
10
|
+
} from './table-schema';
|
|
11
|
+
|
|
12
|
+
// Model types (backward compatibility)
|
|
13
|
+
export type GenericModel = Record<string, any>;
|
|
14
|
+
export type GenericSchema = Record<string, GenericModel>;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Helper to constrain related field names based on relationships metadata
|
|
18
|
+
*/
|
|
19
|
+
export type RelatedField<T extends string, R> = GetRelationshipFields<T, R> & string;
|
|
20
|
+
|
|
21
|
+
// Query interfaces
|
|
22
|
+
export interface QueryInfo {
|
|
23
|
+
query: string;
|
|
24
|
+
hash: number;
|
|
25
|
+
vars?: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface RelatedQuery {
|
|
29
|
+
/** The name of the related table to query */
|
|
30
|
+
relatedTable: string;
|
|
31
|
+
/** The alias for this subquery result (defaults to relatedTable name) */
|
|
32
|
+
alias?: string;
|
|
33
|
+
/** Optional query modifier for the subquery */
|
|
34
|
+
modifier?: SchemaAwareQueryModifier<SchemaStructure, string>;
|
|
35
|
+
/** The cardinality of the relationship */
|
|
36
|
+
cardinality: 'one' | 'many';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface QueryOptions<TModel extends GenericModel, IsOne extends boolean> {
|
|
40
|
+
select?: ((keyof TModel & string) | '*')[];
|
|
41
|
+
where?: Partial<TModel>;
|
|
42
|
+
limit?: number;
|
|
43
|
+
offset?: number;
|
|
44
|
+
orderBy?: Partial<Record<keyof TModel, 'asc' | 'desc'>>;
|
|
45
|
+
/** Related tables to include via subqueries */
|
|
46
|
+
related?: RelatedQuery[];
|
|
47
|
+
isOne?: IsOne;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface LiveQueryOptions<TModel extends GenericModel> extends Omit<
|
|
51
|
+
QueryOptions<TModel, boolean>,
|
|
52
|
+
'orderBy'
|
|
53
|
+
> {}
|
|
54
|
+
|
|
55
|
+
// Import schema types for schema-aware modifiers
|
|
56
|
+
import type {
|
|
57
|
+
SchemaStructure,
|
|
58
|
+
TableNames,
|
|
59
|
+
GetTable,
|
|
60
|
+
TableModel,
|
|
61
|
+
TableRelationships,
|
|
62
|
+
GetRelationship,
|
|
63
|
+
} from './table-schema';
|
|
64
|
+
|
|
65
|
+
// Query modifier type for related queries - now schema-aware!
|
|
66
|
+
export type QueryModifier<TModel extends GenericModel> = (
|
|
67
|
+
builder: QueryModifierBuilder<TModel>
|
|
68
|
+
) => QueryModifierBuilder<TModel>;
|
|
69
|
+
|
|
70
|
+
// Schema-aware query modifier that knows about relationships
|
|
71
|
+
export type SchemaAwareQueryModifier<
|
|
72
|
+
S extends SchemaStructure,
|
|
73
|
+
TableName extends TableNames<S>,
|
|
74
|
+
RelatedFields extends Record<string, any> = {},
|
|
75
|
+
> = (
|
|
76
|
+
builder: SchemaAwareQueryModifierBuilder<S, TableName, {}>
|
|
77
|
+
) => SchemaAwareQueryModifierBuilder<S, TableName, RelatedFields>;
|
|
78
|
+
|
|
79
|
+
// Simplified query builder interface for modifying subqueries
|
|
80
|
+
export interface QueryModifierBuilder<TModel extends GenericModel> {
|
|
81
|
+
where(conditions: Partial<TModel>): this;
|
|
82
|
+
select(...fields: ((keyof TModel & string) | '*')[]): this;
|
|
83
|
+
limit(count: number): this;
|
|
84
|
+
offset(count: number): this;
|
|
85
|
+
orderBy(field: keyof TModel & string, direction?: 'asc' | 'desc'): this;
|
|
86
|
+
related<Field extends string>(relatedField: Field, modifier?: QueryModifier<any>): this;
|
|
87
|
+
_getOptions(): QueryOptions<TModel, boolean>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Schema-aware query builder interface that understands relationships
|
|
91
|
+
export interface SchemaAwareQueryModifierBuilder<
|
|
92
|
+
S extends SchemaStructure,
|
|
93
|
+
TableName extends TableNames<S>,
|
|
94
|
+
RelatedFields extends Record<string, any> = {},
|
|
95
|
+
> {
|
|
96
|
+
where(conditions: Partial<TableModel<GetTable<S, TableName>>>): this;
|
|
97
|
+
select(...fields: ((keyof TableModel<GetTable<S, TableName>> & string) | '*')[]): this;
|
|
98
|
+
limit(count: number): this;
|
|
99
|
+
offset(count: number): this;
|
|
100
|
+
orderBy(
|
|
101
|
+
field: keyof TableModel<GetTable<S, TableName>> & string,
|
|
102
|
+
direction?: 'asc' | 'desc'
|
|
103
|
+
): this;
|
|
104
|
+
related<
|
|
105
|
+
Field extends TableRelationships<S, TableName>['field'],
|
|
106
|
+
Rel extends GetRelationship<S, TableName, Field>,
|
|
107
|
+
RelatedFields2 extends Record<string, any> = {},
|
|
108
|
+
>(
|
|
109
|
+
relatedField: Field,
|
|
110
|
+
modifier?: SchemaAwareQueryModifier<S, Rel['to'], RelatedFields2>
|
|
111
|
+
): SchemaAwareQueryModifierBuilder<
|
|
112
|
+
S,
|
|
113
|
+
TableName,
|
|
114
|
+
RelatedFields & {
|
|
115
|
+
[K in Field]: {
|
|
116
|
+
to: Rel['to'];
|
|
117
|
+
cardinality: Rel['cardinality'];
|
|
118
|
+
relatedFields: RelatedFields2;
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
>;
|
|
122
|
+
_getOptions(): QueryOptions<TableModel<GetTable<S, TableName>>, boolean>;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Extract fields from a model that are relationship fields (string or string[])
|
|
127
|
+
* Excludes common non-relationship fields like id, created_at, updated_at, etc.
|
|
128
|
+
*/
|
|
129
|
+
export type RelationshipFields<TModel extends GenericModel> = {
|
|
130
|
+
[K in keyof TModel]: K extends 'id' | 'created_at' | 'updated_at' | 'deleted_at'
|
|
131
|
+
? never
|
|
132
|
+
: TModel[K] extends string | string[] | null | undefined
|
|
133
|
+
? K
|
|
134
|
+
: never;
|
|
135
|
+
}[keyof TModel];
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Helper type to infer the related model type from a field name using Relationships metadata
|
|
139
|
+
* Simplified to directly access the nested structure
|
|
140
|
+
*/
|
|
141
|
+
export type InferRelatedModelFromMetadata<
|
|
142
|
+
Schema extends GenericSchema,
|
|
143
|
+
TableName extends string,
|
|
144
|
+
FieldName extends string,
|
|
145
|
+
Relationships,
|
|
146
|
+
> =
|
|
147
|
+
Relationships extends Record<string, Record<string, RelationshipDefinition>>
|
|
148
|
+
? TableName extends keyof Relationships
|
|
149
|
+
? FieldName extends keyof Relationships[TableName]
|
|
150
|
+
? Relationships[TableName][FieldName]['model']
|
|
151
|
+
: any
|
|
152
|
+
: any
|
|
153
|
+
: any;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get cardinality for a relationship field from metadata
|
|
157
|
+
* Simplified to directly access the nested structure
|
|
158
|
+
*/
|
|
159
|
+
export type GetCardinality<TableName extends string, FieldName extends string, Relationships> =
|
|
160
|
+
Relationships extends Record<string, Record<string, RelationshipDefinition>>
|
|
161
|
+
? TableName extends keyof Relationships
|
|
162
|
+
? FieldName extends keyof Relationships[TableName]
|
|
163
|
+
? Relationships[TableName][FieldName]['cardinality']
|
|
164
|
+
: 'many'
|
|
165
|
+
: 'many'
|
|
166
|
+
: 'many';
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Type that transforms a Model by replacing a field with its related records
|
|
170
|
+
* Uses Relationships metadata to determine cardinality and related table
|
|
171
|
+
*/
|
|
172
|
+
export type WithRelated<
|
|
173
|
+
Schema extends GenericSchema,
|
|
174
|
+
TModel extends Record<string, any>,
|
|
175
|
+
TableName extends string,
|
|
176
|
+
FieldName extends string,
|
|
177
|
+
Relationships,
|
|
178
|
+
> = FieldName extends keyof TModel
|
|
179
|
+
? Omit<TModel, FieldName> & {
|
|
180
|
+
[K in FieldName]: GetCardinality<TableName, FieldName, Relationships> extends 'one'
|
|
181
|
+
? InferRelatedModelFromMetadata<Schema, TableName, K, Relationships> | null
|
|
182
|
+
: InferRelatedModelFromMetadata<Schema, TableName, K, Relationships>[] | null;
|
|
183
|
+
}
|
|
184
|
+
: TModel;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Type to extract relationship fields from Relationships metadata
|
|
188
|
+
* Now simplified to just get the keys of the nested object
|
|
189
|
+
*/
|
|
190
|
+
export type GetRelationshipFields<TableName extends string, Relationships> =
|
|
191
|
+
Relationships extends Record<string, Record<string, any>>
|
|
192
|
+
? TableName extends keyof Relationships
|
|
193
|
+
? keyof Relationships[TableName] & string
|
|
194
|
+
: never
|
|
195
|
+
: never;
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Relationship metadata structure - now a nested object for better type safety
|
|
199
|
+
* Example:
|
|
200
|
+
* {
|
|
201
|
+
* thread: {
|
|
202
|
+
* author: { model: Schema["user"], table: "user", cardinality: "one" },
|
|
203
|
+
* comments: { model: Schema["comment"], table: "comment", cardinality: "many" }
|
|
204
|
+
* }
|
|
205
|
+
* }
|
|
206
|
+
*/
|
|
207
|
+
export interface RelationshipDefinition<Model = any> {
|
|
208
|
+
/** The related model type */
|
|
209
|
+
model: Model;
|
|
210
|
+
/** The related table name */
|
|
211
|
+
table: string;
|
|
212
|
+
/** Whether this is a 1:1 or 1:many relationship */
|
|
213
|
+
cardinality: 'one' | 'many';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export type RelationshipsMetadata = Record<string, Record<string, RelationshipDefinition>>;
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"lib": ["ES2020", "DOM"],
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"allowSyntheticDefaultImports": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"declaration": true,
|
|
15
|
+
"declarationMap": true,
|
|
16
|
+
"sourceMap": true,
|
|
17
|
+
"outDir": "./dist",
|
|
18
|
+
"rootDir": "./src"
|
|
19
|
+
},
|
|
20
|
+
"include": ["src/**/*"],
|
|
21
|
+
"exclude": ["node_modules", "dist"]
|
|
22
|
+
}
|