-
Notifications
You must be signed in to change notification settings - Fork 20
/
Copy pathloadHints.ts
245 lines (226 loc) · 9.97 KB
/
loadHints.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
import { Entity } from "./Entity";
import { NormalizeHint } from "./normalizeHints";
import { getRelationFromMaybePolyKey } from "./reactiveHints";
import {
AsyncMethod,
AsyncProperty,
Collection,
LoadedCollection,
LoadedMethod,
LoadedProperty,
LoadedReadOnlyCollection,
LoadedReference,
OneToOneReference,
ReadOnlyCollection,
Reference,
Relation,
} from "./relations";
import { LoadedOneToOneReference } from "./relations/OneToOneReference";
import { OptsOf } from "./typeMap";
import { MaybePromise, NullOrDefinedOr, fail } from "./utils";
const deepLoad = Symbol();
type DeepLoadHint<T extends Entity> = NestedLoadHint<T> & { [deepLoad]: true };
// Use any to correctly handle subtype/base type load hints.
//
// If a load hint is typed as `LoadHint<SmallPublisher, "authors">`, but the property
// is actually `Publisher.authors`, if we do `P extends LoadedCollection<SmallPublisher, ...>`
// the `SmallPublisher !== Publisher`, so the `authors` property doesn't match, and then
// we drop the rest of the nested load hint.
//
// But if we check `P extends LoadedCollection<any, ...>` it works.
type MaybeBaseType = any;
/** Marks a given `T[K]` field as the loaded/synchronous version of the collection. */
export type MarkLoaded<T extends Entity, P, UH = {}> =
P extends OneToOneReference<MaybeBaseType, infer U>
? LoadedOneToOneReference<T, Loaded<U, UH>>
: P extends Reference<MaybeBaseType, infer U, infer N>
? LoadedReference<T, Loaded<U, UH>, N>
: P extends Collection<MaybeBaseType, infer U>
? LoadedCollection<T, Loaded<U, UH>>
: P extends ReadOnlyCollection<MaybeBaseType, infer U>
? LoadedReadOnlyCollection<T, Loaded<U, UH>>
: P extends AsyncProperty<MaybeBaseType, infer V>
? // prettier-ignore
[V] extends [(infer U extends Entity) | undefined]
? LoadedProperty<T, Loaded<U, UH> | Exclude<V, U>>
: V extends readonly (infer U extends Entity)[]
? LoadedProperty<T, Loaded<U, UH>[]>
: LoadedProperty<T, V>
: P extends AsyncMethod<T, infer A, infer V>
? LoadedMethod<T, A, V>
: unknown;
/** A version of MarkLoaded the uses `DeepLoadHint` for tests. */
type MarkDeepLoaded<T extends Entity, P> =
P extends OneToOneReference<MaybeBaseType, infer U>
? LoadedOneToOneReference<T, Loaded<U, DeepLoadHint<U>>>
: P extends Reference<MaybeBaseType, infer U, infer N>
? LoadedReference<T, Loaded<U, DeepLoadHint<U>>, N>
: P extends Collection<MaybeBaseType, infer U>
? LoadedCollection<T, Loaded<U, DeepLoadHint<U>>>
: P extends ReadOnlyCollection<MaybeBaseType, infer U>
? LoadedReadOnlyCollection<T, Loaded<U, DeepLoadHint<U>>>
: P extends AsyncProperty<MaybeBaseType, infer V>
? // prettier-ignore
[V] extends [(infer U extends Entity) | undefined]
? LoadedProperty<T, Loaded<U, DeepLoadHint<U>> | Exclude<V, U>>
: V extends readonly (infer U extends Entity)[]
? LoadedProperty<T, Loaded<U, DeepLoadHint<U>>[]>
: LoadedProperty<T, V>
: P extends AsyncMethod<T, infer A, infer V>
? LoadedMethod<T, A, V>
: unknown;
/**
* A helper type for `New` that marks every `Reference` and `LoadedCollection` in `T` as loaded.
*
* We also look in opts `O` for the "`U`" type, i.e. the next level up/down in the graph,
* because the call site's opts may be using an also-marked loaded parent/child as an opt,
* so this will infer the type of that parent/child and use that for the `U` type.
*
* This means things like `entity.parent.get.grandParent.get` will work on the resulting
* type.
*
* Note that this is also purposefully broken out of `New` because of some weirdness
* around type narrowing that wasn't working when inlined into `New`.
*/
type MaybeUseOptsType<T extends Entity, O, K extends keyof T & keyof O> =
O[K] extends NullOrDefinedOr<infer OK>
? OK extends Entity
? T[K] extends OneToOneReference<T, infer U>
? LoadedOneToOneReference<T, U>
: T[K] extends Reference<T, infer U, infer N>
? LoadedReference<T, OK, N>
: never
: OK extends Array<infer OU>
? OU extends Entity
? T[K] extends Collection<T, infer U>
? LoadedCollection<T, OU>
: never
: T[K]
: T[K]
: never;
/**
* Marks all references/collections of `T` as loaded, i.e. for newly instantiated entities where
* we know there are no already-existing rows with fk's to this new entity in the database.
*
* `O` is the generic from the call site so that if the caller passes `{ author: SomeLoadedAuthor }`,
* we'll prefer that type, as it might have more nested load hints that we can't otherwise assume.
*/
export type New<T extends Entity, O extends OptsOf<T> = OptsOf<T>> = T & {
// K will be `keyof T` and `keyof O` for codegen'd relations, but custom relations
// line `hasOneThrough` and `hasOneDerived` will not pass `keyof O` and so use the
// `: MarkLoaded`.
//
// Note that the safest thing is to probably make this `: unknown` instead so that
// custom relations are not marked loaded, b/c they will very likely require a `.load`
// to work. However, we have some tests that currently expect `author.image.get` to work
// on a new author, so keeping the `MarkLoaded` behavior for now.
[K in keyof T]: K extends keyof O ? MaybeUseOptsType<T, O, K> : MarkLoaded<T, T[K]>;
};
/**
* Marks all references/collections of `T` as deeply loaded, which is only useful for
* tests where we can have "the whole object graph" in-memory.
*/
export type DeepNew<T extends Entity> = Loaded<T, DeepLoadHint<T>>;
/** Detects whether an entity is newly created, and so we can treat all the relations as loaded. */
export function isNew<T extends Entity>(e: T): e is New<T> {
return e.isNewEntity;
}
/**
* All the loadable fields, i.e. relations or lazy-loaded/async properties, in an entity.
*
* We use a mapped type (instead of a code-generated type) so that we can pick up custom
* fields that have be added to the entity classes, i.e. `Author.numberOfBooks2` async
* properties.
*/
export type Loadable<T extends Entity> = {
-readonly [K in keyof T as LoadableValue<T[K]> extends never ? never : K]: LoadableValue<T[K]>;
};
/**
* Given an entity field/value, return the loadable entity.
*
* I.e. `Author.books` as a Reference or Collection, return `Book`.
*
* Note that we usually return entities, but for AsyncProperties it could be
* a calculated primitive value like number or string.
*/
export type LoadableValue<V> =
V extends Reference<any, infer U, any>
? U
: V extends Collection<any, infer U>
? U
: V extends ReadOnlyCollection<any, infer U>
? U
: V extends AsyncMethod<any, any, infer V>
? V
: V extends AsyncProperty<any, infer P>
? // If the AsyncProperty returns `Comment | undefined`, then we want to return `Comment`
// prettier-ignore
P extends (infer U extends Entity) | undefined ? U : P
: never;
/**
* A load hint of a single key, multiple keys, or nested keys and sub-hints.
*
* Load hints are different from reactive hints in that load hints include only references,
* collections, and async properties to preload (like `Book.author` or `Author.books`), and
* do not include any primitive fields (like `Author.firstName`).
*/
export type LoadHint<T extends Entity> =
| (keyof Loadable<T> & string)
| ReadonlyArray<keyof Loadable<T> & string>
| NestedLoadHint<T>;
export type NestedLoadHint<T extends Entity> = {
// Don't filter out entity-loadable keys, because we need to support `{ numberOfBooks2: {} }`
[K in keyof Loadable<T>]?: Loadable<T>[K] extends infer U extends Entity ? LoadHint<U> : {};
};
/** Given an entity `T` that is being populated with hints `H`, marks the `H` attributes as populated. */
export type Loaded<T extends Entity, H> = T & {
[K in keyof T & keyof NormalizeHint<H>]: H extends DeepLoadHint<T>
? MarkDeepLoaded<T, T[K]>
: MarkLoaded<T, T[K], NormalizeHint<H>[K]>;
};
/** Recursively checks if the relations from a load hint are loaded on an entity. */
export function isLoaded<T extends Entity, H extends LoadHint<T>>(entity: T, hint: H): entity is Loaded<T, H> {
if (typeof hint === "string") {
return (entity as any)[hint].isLoaded;
} else if (Array.isArray(hint)) {
return (hint as string[]).every((key) => (entity as any)[key].isLoaded);
} else if (typeof hint === "object") {
return Object.entries(hint as object).every(([key, nestedHint]) => {
const relation = getRelationFromMaybePolyKey(entity, key);
if (!relation || typeof relation.load !== "function") return true;
if (relation.isLoaded) {
const result = relation.get;
return Array.isArray(result)
? result.every((entity) => isLoaded(entity, nestedHint))
: result
? isLoaded(result, nestedHint)
: true;
} else {
return false;
}
});
} else {
throw new Error(`Unexpected hint ${hint}`);
}
}
export function maybePopulateThen<T extends Entity, H extends LoadHint<T>, R>(
entity: T,
hint: H,
fn: (loaded: Loaded<T, H>) => R,
): MaybePromise<R> {
return isLoaded(entity, hint) ? fn(entity) : (entity as any).populate(hint).then(fn);
}
export function assertLoaded<T extends Entity, H extends LoadHint<T>, L extends Loaded<T, H>>(
entity: T,
hint: H,
): asserts entity is L {
if (!isLoaded(entity, hint)) fail(`${entity.id} is not loaded for ${JSON.stringify(hint)}`);
}
export function ensureLoaded<T extends Entity, H extends LoadHint<T>, L extends Loaded<T, H>>(entity: T, hint: H): L {
assertLoaded<T, H, L>(entity, hint);
return entity;
}
/** From any `Relations` field in `T`, i.e. for loader hints. */
export type RelationsIn<T extends Entity> = SubType<T, Relation<any, any>>;
// https://medium.com/dailyjs/typescript-create-a-condition-based-subset-types-9d902cea5b8c
type SubType<T, C> = Pick<T, { [K in keyof T]: T[K] extends C ? K : never }[keyof T]>;