I am currently working on developing an ORM for a graph database using TypeScript. Specifically, I am focusing on enhancing the "find" method to retrieve a list of a specific entity. The goal is to allow the function to accept a structure detailing the joins to be performed at the database level. Moreover, I aim to automatically type these additional fields for easy client access. While I have successfully implemented this feature with single-level nesting, my ultimate aim is to extend it to multiple levels.
Here is how I have achieved functionality for one nesting level:
interface IDocumentModel {
_id?: string;
}
type JoinParams<T extends Record<string, IDocumentModel>> = {
[K in keyof T]: {
model: DocumentModel<T[K]>;
};
};
type JoinResult<T, U> = (U & {
[K in keyof T]: T[K][];
})[];
class DocumentModel<T extends IDocumentModel> {
async find<X extends Record<string, IDocumentModel>>(
filter?: Partial<T>,
hydrate?: JoinParams<X>,
): Promise<JoinResult<X, T>> {
// TODO: implementation
}
}
const ParentModel = new DocumentModel<{ _id?: string, parentField: string }>();
const ChildModel = new DocumentModel<{ _id?: string, childField: string }>();
const results = await ParentModel.find(
{ _id: 'abc' },
{
children: {
model: ChildModel,
},
},
);
console.log(results[0].parentField);
console.log(results[0].children[0].childField);
Now, the challenge lies in extending this functionality to two levels or even beyond. Here is my progress on implementing two levels of nesting:
Below is my current approach to addressing this issue:
interface IDocumentModel {
_id?: string;
}
type JoinParams<
T extends
| Record<string, IDocumentModel>
| Record<string, Record<string, IDocumentModel>>,
> = {
[K in keyof T]: {
model: T extends Record<string, Record<string, IDocumentModel>>
? DocumentModel<T[K]['parent']>
: T extends Record<string, IDocumentModel>
? DocumentModel<T[K]>
: never;
hydrate?: T extends Record<string, Record<string, IDocumentModel>>
? JoinParams<Omit<T[K], 'parent'>>
: never;
};
};
type JoinResult<
T extends
| Record<string, IDocumentModel>
| Record<string, Record<string, IDocumentModel>>,
U,
> = (U & {
[K in keyof T]: T extends Record<string, Record<string, IDocumentModel>>
? JoinResult<Omit<T[K], 'parent'>, T[K]['parent']>
: T extends Record<string, IDocumentModel>
? T[K][]
: never;
})[];
class DocumentModel<T extends IDocumentModel> {
async find<X extends Record<string, Record<string, IDocumentModel>>>(
filter?: Partial<T>,
hydrate?: JoinParams<X>,
): Promise<JoinResult<X, T>> {
// TODO: implementation
}
}
const ParentModel = new DocumentModel<{ _id?: string, parentField: string }>();
const ChildModel = new DocumentModel<{ _id?: string, childField: string }>();
const GrandChildModel = new DocumentModel<{ _id?: string, grandChildField: string }>();
const results = await ParentModel.find(
{ _id: 'abc' },
{
children: {
model: ChildModel,
hydrate: {
grandchildren: {
model: GrandChildModel,
},
},
},
},
);
console.log(results[0].parentField);
console.log(results[0].children[0].childField);
console.log(results[0].children[0].grandchildren[0].grandChildField);
Upon testing, I noticed that autocomplete suggestions do not go beyond results[0].parentField
. This means that the IDE does not recognize results[0].children
as a valid field anymore.
I believe this provides sufficient information, but I am open to providing more clarity if needed.