-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathbase.ts
399 lines (344 loc) · 12 KB
/
base.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
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
import * as clause from "./clause";
import { ObjectLoaderFactory } from "./loaders";
import { OrderBy } from "./query_impl";
// Loader is the primitive data fetching abstraction in the framework
// implementation details up to each instance
// A DataLoader could be used internally or not.
// For Loaders that use DataLoader, there's batching done to fetch multiple pieces
// of information.
// Using Loader and LoaderFactory allows us to use the same instance of Loader across a
// request and potentially batch data as needed when possible
export interface Loader<K, V> {
context?: Context;
load(key: K): Promise<V>;
// TODO we need a loadMany() API similar to DataLoaer
// what's the plural api to be?
loadMany?(keys: K[]): Promise<(V | null)[]>;
clearAll(): any;
}
export interface LoaderWithLoadMany<T, V> extends Loader<T, V> {
loadMany(keys: T[]): Promise<V[]>;
}
// A LoaderFactory is used to create a Loader
// We cache data on a per-request basis therefore for each new request, createLoader
// is called to get a new instance of Loader which will then be used to load data as needed
export interface LoaderFactory<K, V> {
name: string; // used to have a per-request cache of each loader type
// factory method.
// when context is passed, there's potentially opportunities to batch data in the same
// request
// when no context is passed, no batching possible (except with explicit call to loadMany API)
createLoader(context?: Context): Loader<K, V>;
}
interface LoaderFactoryWithLoaderMany<T, V> extends LoaderFactory<T, V> {
createLoader(context?: Context): LoaderWithLoadMany<T, V>;
}
// better name for this?
// this is used by EntQuery and then smart decision is made of what to do
export interface ConfigurableLoaderFactory<T, V> extends LoaderFactory<T, V> {
createConfigurableLoader(
options: EdgeQueryableDataOptions,
context?: Context,
): Loader<T, V>;
}
export type EdgeQueryableDataOptions = Partial<
Pick<
QueryableDataOptions,
"limit" | "orderby" | "clause" | "disableTransformations"
>
>;
export type EdgeQueryableDataOptionsConfigureLoader = Pick<
EdgeQueryableDataOptions,
"disableTransformations"
>;
// PrimableLoader allows us to prime data in the cache that's retrieved from
// other sources
export interface PrimableLoader<K, V> extends Loader<K, V> {
prime(d: V): void;
// prime this loader and any other loader it's aware of
primeAll?(d: V): void;
}
export type QueryOptions = Required<
Pick<LoadRowsOptions, "clause" | "fields" | "tableName">
> &
Pick<LoadRowsOptions, "orderby" | "join">;
interface cache {
getLoader<K, V>(name: string, create: () => Loader<K, V>): Loader<K, V>;
getLoaderWithLoadMany<K, V>(
name: string,
create: () => LoaderWithLoadMany<K, V>,
): LoaderWithLoadMany<K, V>;
getCachedRows(options: QueryOptions): Data[] | null;
getCachedRow(options: QueryOptions): Data | null;
primeCache(options: QueryOptions, rows: Data[]): void;
primeCache(options: QueryOptions, rows: Data): void;
clearCache(): void;
reset(): void;
}
export interface Context<TViewer extends Viewer = Viewer> {
// TODO https://github.com/lolopinto/ent/pull/1658
// if we ever make Context required, as part of that breaking change add reset()
// method so that we can reset the context for long-running "requests" like websockets
// and that'll be the official API/way of doing this
// TODO https://github.com/lolopinto/ent/issues/1576
getViewer(): TViewer;
// optional per (request)contet
// absence means we are not doing any caching
// presence means we have loader, ent cache etc
// TODO expose this? use
cache?: cache;
}
export interface Viewer<
TEnt extends any = Ent<any> | null,
TID extends any = ID | null,
> {
viewerID: TID;
viewer: () => Promise<TEnt>;
instanceKey: () => string;
// isOmniscient?(): boolean; // optional function to indicate a viewer that can see anything e.g. admin
// TODO determine if we want this here.
// just helpful to have it here
// not providing a default AllowIfOmniRule
// where should dataloaders be put?
// I want dataloaders to be created on demand as needed
// so it seems having it in Context (per-request info makes sense)
// so does that mean we should pass Context all the way down and not Viewer?
context?: Context<any>;
}
export interface Ent<TViewer extends Viewer = Viewer> {
id: ID;
viewer: TViewer;
getPrivacyPolicy(): PrivacyPolicy<this, TViewer>;
nodeType: string;
// used to set raw data that's then used by ent internals
// shouldn't be used...
__setRawDBData<T extends Data = Data>(data: T);
}
export declare type Data = {
[key: string]: any;
};
export interface EntConstructor<
TEnt extends Ent,
TViewer extends Viewer = Viewer,
> {
new (viewer: TViewer, data: Data): TEnt;
}
export type ID = string | number;
export interface DataOptions {
// TODO pool or client later since we should get it from there
// TODO this can be passed in for scenarios where we are not using default configuration
// clientConfig?: ClientConfig;
tableName: string;
// rename table as alias
alias?: string;
// TODO remove this from here since we're changing how this works....
context?: Context;
}
export interface SelectBaseDataOptions extends DataOptions {
// list of fields to read
fields: (
| string
| {
alias: string;
column: string;
}
)[];
// use this alias to alias the fields instead of the table name or table alias
// takes precedence over tableName and alias
fieldsAlias?: string;
// don't use either alias for this query.
// possible reason in when doing aggregate queries and may have already aliased what we're querying
disableFieldsAlias?: boolean;
// don't use the alias for the order by clause
// this is useful when doing a join and the order by clause is not on the main table
disableDefaultOrderByAlias?: boolean;
}
export interface SelectDataOptions extends SelectBaseDataOptions {
// primary key we're selecting from most often 'id'
key: string;
// if postgres and using an integer primary key, we need to pass this so that when we do an In query,
// we can cast accurately
// TODO https://github.com/lolopinto/ent/issues/1431
keyType?: string; // 'uuid' | 'integer' etc...
// if exists, we and with the primary key query
clause?: clause.Clause | (() => clause.Clause | undefined);
}
export interface QueryableDataOptions
extends SelectBaseDataOptions,
QueryDataOptions {}
// for now, no complicated joins. just simple joins
interface JoinOptions<T2 extends Data = Data, K2 = keyof T2> {
tableName: string;
alias?: string;
clause: clause.Clause<T2, K2>;
type?: "inner" | "outer" | "left" | "right";
}
export interface QueryDataOptions<T extends Data = Data, K = keyof T> {
distinct?: boolean;
// To alias the main table's query when doing joins.
alias?: string;
clause: clause.Clause<T, K>;
orderby?: OrderBy; // this technically doesn't make sense when querying just one row but whatevs
groupby?: K;
limit?: number;
disableTransformations?: boolean;
join?: JoinOptions[];
}
// For loading data from database
export interface LoadRowOptions extends QueryableDataOptions {}
export interface LoadRowsOptions extends QueryableDataOptions {}
interface OnConflictOptions {
// TODO should these change to fields instead of columns?
onConflictCols: string[];
// onConflictConstraint doesn't work with do nothing since ent always reloads the
// row after insert and if there's no conflict columns provided, we have no way of querying
// the db for the original/conflicting row
onConflictConstraint?: string;
// update values based on fields
// if not provided, we do nothing
updateCols?: string[];
}
export interface CreateRowOptions extends DataOptions {
// fields to be edited
fields: Data;
fieldsToLog?: Data;
onConflict?: OnConflictOptions;
}
export interface EditRowOptions extends Omit<CreateRowOptions, "onConflict"> {
whereClause: clause.Clause;
// if a column exists in here as opposed to in fields, we use the expression given
// instead of the value
expressions?: Map<string, clause.Clause>;
}
interface LoadableEntOptions<
TEnt extends Ent,
TViewer extends Viewer = Viewer,
TData extends Data = Data,
> {
loaderFactory: ObjectLoaderFactory<TData>;
ent: EntConstructor<TEnt, TViewer>;
}
export interface LoaderFactoryWithOptions<T extends Data = Data>
extends LoaderFactoryWithLoaderMany<any, T | null> {
options?: SelectDataOptions;
}
// information needed to load an ent from the databse
export interface LoadEntOptions<
TEnt extends Ent,
TViewer extends Viewer = Viewer,
TData extends Data = Data,
> extends LoadableEntOptions<TEnt, TViewer, TData>,
// extending DataOptions and fields is to make APIs like loadEntsFromClause work until we come up with a cleaner API
SelectBaseDataOptions {
// if passed in, it means there's field privacy on the ents *and* we want to apply it at ent load
// if there's field privacy on the ent and not passed in, it'll be applied on demand when we try and load the ent
fieldPrivacy?: Map<string, PrivacyPolicy>;
}
export interface SelectCustomDataOptions<T extends Data = Data>
extends SelectBaseDataOptions {
// main loader factory for the ent, passed in for priming the data so subsequent fetches of this id don't reload
loaderFactory: ObjectLoaderFactory<T>;
// should we prime the ent after loading. uses loaderFactory above
// only pass prime if the fields is equivalent to the ids of the other loader factory
// it doesn't check...
prime?: boolean;
}
export interface LoadCustomEntOptions<
TEnt extends Ent,
TViewer extends Viewer = Viewer,
TData extends Data = Data,
>
// extending DataOptions and fields is to make APIs like loadEntsFromClause work until we come up with a cleaner API
extends SelectCustomDataOptions<TData> {
ent: EntConstructor<TEnt, TViewer>;
fieldPrivacy?: Map<string, PrivacyPolicy>;
}
export interface LoaderInfo<T = Data> {
tableName: string;
fields: string[];
nodeType: string;
loaderFactory: LoaderFactory<ID, T | null>;
}
// information needed to edit an ent
export interface EditEntOptions<T extends Ent>
extends LoadableEntOptions<T>,
EditRowOptions {}
// Privacy
enum privacyResult {
// using http status codes similar to golang for the lols
Allow = 200,
Deny = 401,
Skip = 307,
}
export interface PrivacyResult {
result: privacyResult;
error?: PrivacyError;
getError?(
policy: PrivacyPolicy,
rule: PrivacyPolicyRule,
ent?: Ent,
): PrivacyError;
}
export interface PrivacyError extends Error {
privacyPolicy: PrivacyPolicy<Ent>;
ent?: Ent;
}
const allow: PrivacyResult = {
result: privacyResult.Allow,
};
export function Allow(): PrivacyResult {
return allow;
}
const skip: PrivacyResult = {
result: privacyResult.Skip,
};
export function Skip(): PrivacyResult {
return skip;
}
const deny: PrivacyResult = {
result: privacyResult.Deny,
};
export function Deny(): PrivacyResult {
return deny;
}
class DenyWithReasonError extends Error implements PrivacyError {
privacyPolicy: PrivacyPolicy;
privacyRule: PrivacyPolicyRule;
ent?: Ent;
constructor(
privacyPolicy: PrivacyPolicy,
rule: PrivacyPolicyRule,
msg: string,
ent?: Ent,
) {
super(msg);
this.privacyPolicy = privacyPolicy;
this.privacyRule = rule;
this.ent = ent;
}
}
export function DenyWithReason(e: PrivacyError | string): PrivacyResult {
if (typeof e === "string") {
return {
result: privacyResult.Deny,
getError(policy: PrivacyPolicy, rule: PrivacyPolicyRule, ent?: Ent) {
return new DenyWithReasonError(policy, rule, e, ent);
},
};
}
return {
result: privacyResult.Deny,
error: e,
};
}
export interface PrivacyPolicyRule<TEnt extends Ent = Ent, TViewer = Viewer> {
apply(v: TViewer, ent?: TEnt): Promise<PrivacyResult>;
}
export interface PrivacyPolicy<TEnt extends Ent = Ent, TViewer = Viewer> {
rules: PrivacyPolicyRule<TEnt, TViewer>[];
}
export enum WriteOperation {
Insert = "insert",
Edit = "edit",
Delete = "delete",
}