@isograph/react 0.0.0-main-4ef7c123

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/src/index.tsx ADDED
@@ -0,0 +1,647 @@
1
+ import {
2
+ DataId,
3
+ StoreRecord,
4
+ DataTypeValue,
5
+ Link,
6
+ ROOT_ID,
7
+ getOrCreateCacheForArtifact,
8
+ onNextChange,
9
+ store,
10
+ getParentRecordKey,
11
+ } from "./cache";
12
+ import { useLazyDisposableState } from "@isograph/react-disposable-state";
13
+ import { type PromiseWrapper } from "./PromiseWrapper";
14
+ import React from "react";
15
+
16
+ export {
17
+ setNetwork,
18
+ makeNetworkRequest,
19
+ subscribe,
20
+ DataId,
21
+ Link,
22
+ StoreRecord,
23
+ } from "./cache";
24
+
25
+ // This type should be treated as an opaque type.
26
+ export type IsographFetchableResolver<
27
+ TReadFromStore extends Object,
28
+ TResolverProps,
29
+ TResolverResult
30
+ > = {
31
+ kind: "FetchableResolver";
32
+ queryText: string;
33
+ normalizationAst: NormalizationAst;
34
+ readerArtifact: ReaderArtifact<
35
+ TReadFromStore,
36
+ TResolverProps,
37
+ TResolverResult
38
+ >;
39
+ nestedRefetchQueries: RefetchQueryArtifactWrapper[];
40
+ };
41
+
42
+ export type ReaderArtifact<
43
+ TReadFromStore extends Object,
44
+ TResolverProps,
45
+ TResolverResult
46
+ > = {
47
+ kind: "ReaderArtifact";
48
+ readerAst: ReaderAst<TReadFromStore>;
49
+ resolver: (data: TResolverProps) => TResolverResult;
50
+ variant: ReaderResolverVariant;
51
+ };
52
+
53
+ export type ReaderAstNode =
54
+ | ReaderScalarField
55
+ | ReaderLinkedField
56
+ | ReaderResolverField
57
+ | ReaderRefetchField
58
+ | ReaderMutationField;
59
+
60
+ // @ts-ignore
61
+ export type ReaderAst<TReadFromStore> = ReaderAstNode[];
62
+
63
+ export type ReaderScalarField = {
64
+ kind: "Scalar";
65
+ fieldName: string;
66
+ alias: string | null;
67
+ arguments: Arguments | null;
68
+ };
69
+ export type ReaderLinkedField = {
70
+ kind: "Linked";
71
+ fieldName: string;
72
+ alias: string | null;
73
+ selections: ReaderAst<unknown>;
74
+ arguments: Arguments | null;
75
+ };
76
+
77
+ export type ReaderResolverVariant =
78
+ | { kind: "Eager" }
79
+ // componentName is the component's cacheKey for getRefReaderByName
80
+ // and is the type + field concatenated
81
+ | { kind: "Component"; componentName: string };
82
+
83
+ export type ReaderResolverField = {
84
+ kind: "Resolver";
85
+ alias: string;
86
+ readerArtifact: ReaderArtifact<any, any, any>;
87
+ arguments: Arguments | null;
88
+ usedRefetchQueries: number[];
89
+ };
90
+
91
+ export type ReaderRefetchField = {
92
+ kind: "RefetchField";
93
+ alias: string;
94
+ // TODO this bad modeling. A refetch field cannot have variant: "Component" (I think)
95
+ readerArtifact: ReaderArtifact<any, any, any>;
96
+ refetchQuery: number;
97
+ };
98
+
99
+ export type ReaderMutationField = {
100
+ kind: "MutationField";
101
+ alias: string;
102
+ // TODO this bad modeling. A mutation field cannot have variant: "Component" (I think)
103
+ readerArtifact: ReaderArtifact<any, any, any>;
104
+ refetchQuery: number;
105
+ allowedVariables: string[];
106
+ };
107
+
108
+ export type NormalizationAstNode =
109
+ | NormalizationScalarField
110
+ | NormalizationLinkedField;
111
+ // @ts-ignore
112
+ export type NormalizationAst = NormalizationAstNode[];
113
+
114
+ export type NormalizationScalarField = {
115
+ kind: "Scalar";
116
+ fieldName: string;
117
+ arguments: Arguments | null;
118
+ };
119
+
120
+ export type NormalizationLinkedField = {
121
+ kind: "Linked";
122
+ fieldName: string;
123
+ arguments: Arguments | null;
124
+ selections: NormalizationAst;
125
+ };
126
+
127
+ // This is more like an entrypoint, but one specifically for a refetch query/mutation
128
+ export type RefetchQueryArtifact = {
129
+ kind: "RefetchQuery";
130
+ queryText: string;
131
+ normalizationAst: NormalizationAst;
132
+ };
133
+
134
+ // TODO rename
135
+ export type RefetchQueryArtifactWrapper = {
136
+ artifact: RefetchQueryArtifact;
137
+ allowedVariables: string[];
138
+ };
139
+
140
+ export type Arguments = Argument[];
141
+ export type Argument = {
142
+ argumentName: string;
143
+ variableName: string;
144
+ };
145
+
146
+ export type FragmentReference<
147
+ TReadFromStore extends Object,
148
+ TResolverProps,
149
+ TResolverResult
150
+ > = {
151
+ kind: "FragmentReference";
152
+ readerArtifact: ReaderArtifact<
153
+ TReadFromStore,
154
+ TResolverProps,
155
+ TResolverResult
156
+ >;
157
+ root: DataId;
158
+ variables: { [index: string]: string } | null;
159
+ // TODO: We should instead have ReaderAst<TResolverProps>
160
+ nestedRefetchQueries: RefetchQueryArtifactWrapper[];
161
+ };
162
+
163
+ export function isoFetch<T extends IsographFetchableResolver<any, any, any>>(
164
+ _text: TemplateStringsArray
165
+ ): T {
166
+ return void 0 as any;
167
+ }
168
+
169
+ export function iso<TResolverParameter, TResolverReturn = TResolverParameter>(
170
+ _queryText: TemplateStringsArray
171
+ ): (
172
+ x: ((param: TResolverParameter) => TResolverReturn) | void
173
+ ) => (param: TResolverParameter) => TResolverReturn {
174
+ // The name `identity` here is a bit of a double entendre.
175
+ // First, it is the identity function, constrained to operate
176
+ // on a very specific type. Thus, the value of b Declare`...`(
177
+ // someFunction) is someFunction. But furthermore, if one
178
+ // write b Declare`...` and passes no function, the resolver itself
179
+ // is the identity function. At that point, the types
180
+ // TResolverParameter and TResolverReturn must be identical.
181
+
182
+ return function identity(
183
+ x: (param: TResolverParameter) => TResolverReturn
184
+ ): (param: TResolverParameter) => TResolverReturn {
185
+ return x;
186
+ };
187
+ }
188
+
189
+ export function useLazyReference<
190
+ TReadFromStore extends Object,
191
+ TResolverProps,
192
+ TResolverResult
193
+ >(
194
+ fetchableResolverArtifact: IsographFetchableResolver<
195
+ TReadFromStore,
196
+ TResolverProps,
197
+ TResolverResult
198
+ >,
199
+ variables: object
200
+ ): {
201
+ queryReference: FragmentReference<
202
+ TReadFromStore,
203
+ TResolverProps,
204
+ TResolverResult
205
+ >;
206
+ } {
207
+ // Typechecking fails here... TODO investigate
208
+ const cache = getOrCreateCacheForArtifact(
209
+ fetchableResolverArtifact,
210
+ variables
211
+ );
212
+
213
+ // TODO add comment explaining why we never use this value
214
+ // @ts-ignore
215
+ const data =
216
+ // @ts-ignore
217
+ useLazyDisposableState<PromiseWrapper<TResolverResult>>(cache).state;
218
+
219
+ return {
220
+ queryReference: {
221
+ kind: "FragmentReference",
222
+ readerArtifact: fetchableResolverArtifact.readerArtifact,
223
+ root: ROOT_ID,
224
+ variables,
225
+ nestedRefetchQueries: fetchableResolverArtifact.nestedRefetchQueries,
226
+ },
227
+ };
228
+ }
229
+
230
+ export function read<
231
+ TReadFromStore extends Object,
232
+ TResolverProps,
233
+ TResolverResult
234
+ >(
235
+ fragmentReference: FragmentReference<
236
+ TReadFromStore,
237
+ TResolverProps,
238
+ TResolverResult
239
+ >
240
+ ): TResolverResult {
241
+ const variant = fragmentReference.readerArtifact.variant;
242
+ if (variant.kind === "Eager") {
243
+ const data = readData(
244
+ fragmentReference.readerArtifact.readerAst,
245
+ fragmentReference.root,
246
+ fragmentReference.variables ?? {},
247
+ fragmentReference.nestedRefetchQueries
248
+ );
249
+ if (data.kind === "MissingData") {
250
+ throw onNextChange();
251
+ } else {
252
+ return fragmentReference.readerArtifact.resolver(data.data);
253
+ }
254
+ } else if (variant.kind === "Component") {
255
+ return (additionalRuntimeProps: any) => {
256
+ // TODO also incorporate the typename
257
+ const RefReaderForName = getRefReaderForName(variant.componentName);
258
+ // TODO do not create a new reference on every render?
259
+ return (
260
+ <RefReaderForName
261
+ reference={{
262
+ kind: "FragmentReference",
263
+ readerArtifact: fragmentReference.readerArtifact,
264
+ root: fragmentReference.root,
265
+ variables: fragmentReference.variables,
266
+ nestedRefetchQueries: fragmentReference.nestedRefetchQueries,
267
+ }}
268
+ additionalRuntimeProps={additionalRuntimeProps}
269
+ />
270
+ );
271
+ };
272
+ }
273
+ // Why can't Typescript realize that this is unreachable??
274
+ throw new Error("This is unreachable");
275
+ }
276
+
277
+ export function readButDoNotEvaluate<TReadFromStore extends Object>(
278
+ reference: FragmentReference<TReadFromStore, unknown, unknown>
279
+ ): TReadFromStore {
280
+ const response = readData(
281
+ reference.readerArtifact.readerAst,
282
+ reference.root,
283
+ reference.variables ?? {},
284
+ reference.nestedRefetchQueries
285
+ );
286
+ console.log("done reading but not evaluating", { response });
287
+ if (response.kind === "MissingData") {
288
+ throw onNextChange();
289
+ } else {
290
+ return response.data;
291
+ }
292
+ }
293
+
294
+ type ReadDataResult<TReadFromStore> =
295
+ | {
296
+ kind: "Success";
297
+ data: TReadFromStore;
298
+ }
299
+ | {
300
+ kind: "MissingData";
301
+ reason: string;
302
+ nestedReason?: ReadDataResult<unknown>;
303
+ };
304
+
305
+ function readData<TReadFromStore>(
306
+ ast: ReaderAst<TReadFromStore>,
307
+ root: DataId,
308
+ variables: { [index: string]: string },
309
+ nestedRefetchQueries: RefetchQueryArtifactWrapper[]
310
+ ): ReadDataResult<TReadFromStore> {
311
+ let storeRecord = store[root];
312
+ if (storeRecord === undefined) {
313
+ return { kind: "MissingData", reason: "No record for root " + root };
314
+ }
315
+
316
+ if (storeRecord === null) {
317
+ return { kind: "Success", data: null as any };
318
+ }
319
+
320
+ let target: { [index: string]: any } = {};
321
+
322
+ for (const field of ast) {
323
+ switch (field.kind) {
324
+ case "Scalar": {
325
+ const storeRecordName = getParentRecordKey(field, variables);
326
+ const value = storeRecord[storeRecordName];
327
+ // TODO consider making scalars into discriminated unions. This probably has
328
+ // to happen for when we handle errors.
329
+ if (value === undefined) {
330
+ return {
331
+ kind: "MissingData",
332
+ reason: "No value for " + storeRecordName + " on root " + root,
333
+ };
334
+ }
335
+ target[field.alias ?? field.fieldName] = value;
336
+ break;
337
+ }
338
+ case "Linked": {
339
+ const storeRecordName = getParentRecordKey(field, variables);
340
+ const value = storeRecord[storeRecordName];
341
+ if (Array.isArray(value)) {
342
+ const results = [];
343
+ for (const item of value) {
344
+ const link = assertLink(item);
345
+ if (link === undefined) {
346
+ return {
347
+ kind: "MissingData",
348
+ reason:
349
+ "No link for " +
350
+ storeRecordName +
351
+ " on root " +
352
+ root +
353
+ ". Link is " +
354
+ JSON.stringify(item),
355
+ };
356
+ } else if (link === null) {
357
+ results.push(null);
358
+ continue;
359
+ }
360
+ const result = readData(
361
+ field.selections,
362
+ link.__link,
363
+ variables,
364
+ nestedRefetchQueries
365
+ );
366
+ if (result.kind === "MissingData") {
367
+ return {
368
+ kind: "MissingData",
369
+ reason:
370
+ "Missing data for " +
371
+ storeRecordName +
372
+ " on root " +
373
+ root +
374
+ ". Link is " +
375
+ JSON.stringify(item),
376
+ nestedReason: result,
377
+ };
378
+ }
379
+ results.push(result.data);
380
+ }
381
+ target[field.alias ?? field.fieldName] = results;
382
+ break;
383
+ }
384
+ let link = assertLink(value);
385
+ if (link === undefined) {
386
+ // TODO make this configurable, and also generated and derived from the schema
387
+ const altLink = missingFieldHandler(
388
+ storeRecord,
389
+ root,
390
+ field.fieldName,
391
+ field.arguments,
392
+ variables
393
+ );
394
+ if (altLink === undefined) {
395
+ return {
396
+ kind: "MissingData",
397
+ reason:
398
+ "No link for " +
399
+ storeRecordName +
400
+ " on root " +
401
+ root +
402
+ ". Link is " +
403
+ JSON.stringify(value),
404
+ };
405
+ } else {
406
+ link = altLink;
407
+ }
408
+ } else if (link === null) {
409
+ target[field.alias ?? field.fieldName] = null;
410
+ break;
411
+ }
412
+ const targetId = link.__link;
413
+ const data = readData(
414
+ field.selections,
415
+ targetId,
416
+ variables,
417
+ nestedRefetchQueries
418
+ );
419
+ if (data.kind === "MissingData") {
420
+ return {
421
+ kind: "MissingData",
422
+ reason: "Missing data for " + storeRecordName + " on root " + root,
423
+ nestedReason: data,
424
+ };
425
+ }
426
+ target[field.alias ?? field.fieldName] = data.data;
427
+ break;
428
+ }
429
+ case "RefetchField": {
430
+ const data = readData(
431
+ field.readerArtifact.readerAst,
432
+ root,
433
+ variables,
434
+ // Refetch fields just read the id, and don't need refetch query artifacts
435
+ []
436
+ );
437
+ console.log("refetch field data", data, field);
438
+ if (data.kind === "MissingData") {
439
+ return {
440
+ kind: "MissingData",
441
+ reason: "Missing data for " + field.alias + " on root " + root,
442
+ nestedReason: data,
443
+ };
444
+ } else {
445
+ const refetchQueryIndex = field.refetchQuery;
446
+ if (refetchQueryIndex == null) {
447
+ throw new Error("refetchQuery is null in RefetchField");
448
+ }
449
+ const refetchQuery = nestedRefetchQueries[refetchQueryIndex];
450
+ const refetchQueryArtifact = refetchQuery.artifact;
451
+ const allowedVariables = refetchQuery.allowedVariables;
452
+
453
+ target[field.alias] = field.readerArtifact.resolver(
454
+ refetchQueryArtifact,
455
+ {
456
+ ...data.data,
457
+ // TODO continue from here
458
+ // variables need to be filtered for what we need just for the refetch query
459
+ ...filterVariables(variables, allowedVariables),
460
+ }
461
+ );
462
+ }
463
+ break;
464
+ }
465
+ case "MutationField": {
466
+ const data = readData(
467
+ field.readerArtifact.readerAst,
468
+ root,
469
+ variables,
470
+ // Refetch fields just read the id, and don't need refetch query artifacts
471
+ []
472
+ );
473
+ console.log("refetch field data", data, field);
474
+ if (data.kind === "MissingData") {
475
+ return {
476
+ kind: "MissingData",
477
+ reason: "Missing data for " + field.alias + " on root " + root,
478
+ nestedReason: data,
479
+ };
480
+ } else {
481
+ const refetchQueryIndex = field.refetchQuery;
482
+ if (refetchQueryIndex == null) {
483
+ throw new Error("refetchQuery is null in MutationField");
484
+ }
485
+ const refetchQuery = nestedRefetchQueries[refetchQueryIndex];
486
+ const refetchQueryArtifact = refetchQuery.artifact;
487
+ const allowedVariables = refetchQuery.allowedVariables;
488
+
489
+ target[field.alias] = field.readerArtifact.resolver(
490
+ refetchQueryArtifact,
491
+ data.data,
492
+ filterVariables(variables, allowedVariables)
493
+ );
494
+ }
495
+ break;
496
+ }
497
+ case "Resolver": {
498
+ const usedRefetchQueries = field.usedRefetchQueries;
499
+ const resolverRefetchQueries = usedRefetchQueries.map(
500
+ (index) => nestedRefetchQueries[index]
501
+ );
502
+
503
+ const variant = field.readerArtifact.variant;
504
+ if (variant.kind === "Eager") {
505
+ const data = readData(
506
+ field.readerArtifact.readerAst,
507
+ root,
508
+ variables,
509
+ resolverRefetchQueries
510
+ );
511
+ if (data.kind === "MissingData") {
512
+ return {
513
+ kind: "MissingData",
514
+ reason: "Missing data for " + field.alias + " on root " + root,
515
+ nestedReason: data,
516
+ };
517
+ } else {
518
+ target[field.alias] = field.readerArtifact.resolver(data.data);
519
+ }
520
+ } else if (variant.kind === "Component") {
521
+ target[field.alias] = (additionalRuntimeProps: any) => {
522
+ // TODO also incorporate the typename
523
+ const RefReaderForName = getRefReaderForName(variant.componentName);
524
+ // TODO do not create a new reference on every render?
525
+ return (
526
+ <RefReaderForName
527
+ reference={{
528
+ kind: "FragmentReference",
529
+ readerArtifact: field.readerArtifact,
530
+ root,
531
+ variables,
532
+ nestedRefetchQueries: resolverRefetchQueries,
533
+ }}
534
+ additionalRuntimeProps={additionalRuntimeProps}
535
+ />
536
+ );
537
+ };
538
+ }
539
+ break;
540
+ }
541
+ }
542
+ }
543
+ return { kind: "Success", data: target as any };
544
+ }
545
+
546
+ let customMissingFieldHandler: typeof defaultMissingFieldHandler | null = null;
547
+
548
+ function missingFieldHandler(
549
+ storeRecord: StoreRecord,
550
+ root: DataId,
551
+ fieldName: string,
552
+ arguments_: { [index: string]: any } | null,
553
+ variables: { [index: string]: any } | null
554
+ ): Link | undefined {
555
+ if (customMissingFieldHandler != null) {
556
+ return customMissingFieldHandler(
557
+ storeRecord,
558
+ root,
559
+ fieldName,
560
+ arguments_,
561
+ variables
562
+ );
563
+ } else {
564
+ return defaultMissingFieldHandler(
565
+ storeRecord,
566
+ root,
567
+ fieldName,
568
+ arguments_,
569
+ variables
570
+ );
571
+ }
572
+ }
573
+
574
+ export function defaultMissingFieldHandler(
575
+ storeRecord: StoreRecord,
576
+ root: DataId,
577
+ fieldName: string,
578
+ arguments_: { [index: string]: any } | null,
579
+ variables: { [index: string]: any } | null
580
+ ): Link | undefined {
581
+ if (fieldName === "node" || fieldName === "user") {
582
+ const variable = arguments_?.["id"];
583
+ const value = variables?.[variable];
584
+
585
+ // TODO can we handle explicit nulls here too? Probably, after wrapping in objects
586
+ if (value != null) {
587
+ return { __link: value };
588
+ }
589
+ }
590
+ }
591
+
592
+ export function setMissingFieldHandler(
593
+ handler: typeof defaultMissingFieldHandler
594
+ ) {
595
+ customMissingFieldHandler = handler;
596
+ }
597
+
598
+ function assertLink(link: DataTypeValue): Link | undefined | null {
599
+ if (Array.isArray(link)) {
600
+ throw new Error("Unexpected array");
601
+ }
602
+ if (typeof link === "object") {
603
+ return link;
604
+ }
605
+ if (link === undefined) {
606
+ return undefined;
607
+ }
608
+ throw new Error("Invalid link");
609
+ }
610
+
611
+ const refReaders: { [index: string]: any } = {};
612
+ export function getRefReaderForName(name: string) {
613
+ if (refReaders[name] == null) {
614
+ function Component({
615
+ reference,
616
+ additionalRuntimeProps,
617
+ }: {
618
+ reference: FragmentReference<any, any, any>;
619
+ additionalRuntimeProps: any;
620
+ }) {
621
+ const data = readButDoNotEvaluate(reference);
622
+
623
+ return reference.readerArtifact.resolver({
624
+ data,
625
+ ...additionalRuntimeProps,
626
+ });
627
+ }
628
+ Component.displayName = `${name} @component`;
629
+ refReaders[name] = Component;
630
+ }
631
+ return refReaders[name];
632
+ }
633
+
634
+ export type IsographComponentProps<TDataType, TOtherProps = Object> = {
635
+ data: TDataType;
636
+ } & TOtherProps;
637
+
638
+ function filterVariables(
639
+ variables: { [index: string]: string },
640
+ allowedVariables: string[]
641
+ ): { [index: string]: string } {
642
+ const result: { [index: string]: string } = {};
643
+ for (const key of allowedVariables) {
644
+ result[key] = variables[key];
645
+ }
646
+ return result;
647
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "../../tsconfig.build.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist/",
5
+ "rootDir": "./src/",
6
+ "declaration": true,
7
+ "jsx": "react",
8
+ "lib": ["es2017", "DOM"]
9
+ },
10
+ "include": ["./**/*.ts", "./**/*.tsx"]
11
+ }