@nkzw/fate 0.0.2 → 0.0.4
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/README.md +65 -13
- package/lib/cli.mjs +4 -3
- package/lib/index.d.mts +19 -19
- package/lib/server.d.mts +2 -2
- package/lib/server.mjs +30 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,8 +10,6 @@
|
|
|
10
10
|
|
|
11
11
|
**_fate_** is a modern data client for React and tRPC inspired by [Relay](https://relay.dev/) and [GraphQL](https://graphql.org/). It combines view composition, normalized caching, data masking, Async React features, and tRPC's type safety.
|
|
12
12
|
|
|
13
|
-
**_fate_** is designed to make data fetching and state management in React applications more composable, declarative, and predictable. The framework has a minimal API, no DSL, and no magic—_it's just JavaScript_.
|
|
14
|
-
|
|
15
13
|
### Features
|
|
16
14
|
|
|
17
15
|
- **View Composition:** Components declare their data requirements using co-located "views". Views are composed into a single request per screen, minimizing network requests and eliminating waterfalls.
|
|
@@ -26,7 +24,7 @@
|
|
|
26
24
|
|
|
27
25
|
**_fate_** is designed to make data fetching and state management in React applications more composable, declarative, and predictable. The framework has a minimal API, no DSL, and no magic—_it's just JavaScript_.
|
|
28
26
|
|
|
29
|
-
GraphQL and Relay introduced several novel ideas: fragments co‑located with components, a normalized cache keyed by global identifiers, and a compiler that hoists fragments into a single network request. These innovations made it possible to build large applications where data requirements are modular and self‑contained.
|
|
27
|
+
GraphQL and Relay introduced several novel ideas: fragments co‑located with components, [a normalized cache](https://relay.dev/docs/principles-and-architecture/thinking-in-graphql/#caching-a-graph) keyed by global identifiers, and a compiler that hoists fragments into a single network request. These innovations made it possible to build large applications where data requirements are modular and self‑contained.
|
|
30
28
|
|
|
31
29
|
Nakazawa Tech builds apps primarily with GraphQL and Relay. We advocate for these technologies in [talks](https://www.youtube.com/watch?v=rxPTEko8J7c&t=36s) and provide templates ([server](https://github.com/nkzw-tech/server-template), [client](https://github.com/nkzw-tech/web-app-template/tree/with-relay)) to help developers get started quickly.
|
|
32
30
|
|
|
@@ -40,14 +38,66 @@ _[Learn more](/docs/guide/getting-started.md) about fate's core concepts and fea
|
|
|
40
38
|
|
|
41
39
|
## Getting Started
|
|
42
40
|
|
|
43
|
-
###
|
|
41
|
+
### Template
|
|
44
42
|
|
|
45
|
-
|
|
43
|
+
Get started with [a ready-made template](https://github.com/nkzw-tech/fate-template) quickly:
|
|
46
44
|
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
::: code-group
|
|
46
|
+
|
|
47
|
+
```npm
|
|
48
|
+
npx giget@latest gh:nkzw-tech/fate-template
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```pnpm
|
|
52
|
+
pnpx giget@latest gh:nkzw-tech/fate-template
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```yarn
|
|
56
|
+
yarn dlx giget@latest gh:nkzw-tech/fate-template
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
:::
|
|
60
|
+
|
|
61
|
+
The `fate-template` comes with a simple tRPC backend and a React frontend using **_fate_**. It features modern tools to deliver an incredibly fast development experience. Follow its [README.md](https://github.com/nkzw-tech/fate-template#fate-quick-start-template) to get started.
|
|
62
|
+
|
|
63
|
+
### Manual Installation
|
|
64
|
+
|
|
65
|
+
**_fate_** requires React 19.2+. For your client you need to install `react-fate`:
|
|
66
|
+
|
|
67
|
+
::: code-group
|
|
68
|
+
|
|
69
|
+
```npm
|
|
70
|
+
npm add react-fate
|
|
49
71
|
```
|
|
50
72
|
|
|
73
|
+
```pnpm
|
|
74
|
+
pnpm add react-fate
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```yarn
|
|
78
|
+
yarn add react-fate
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
:::
|
|
82
|
+
|
|
83
|
+
And for your server, install the core `@nkzw/fate` package:
|
|
84
|
+
|
|
85
|
+
::: code-group
|
|
86
|
+
|
|
87
|
+
```npm
|
|
88
|
+
npm add @nkzw/fate
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
```pnpm
|
|
92
|
+
pnpm add @nkzw/fate
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```yarn
|
|
96
|
+
yarn add @nkzw/fate
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
:::
|
|
100
|
+
|
|
51
101
|
> [!WARNING]
|
|
52
102
|
>
|
|
53
103
|
> **_fate_** is currently in alpha and not production ready. If something doesn't work for you, please open a pull request.
|
|
@@ -56,11 +106,11 @@ If you'd like to try the example app in GitHub Codespaces, click the button belo
|
|
|
56
106
|
|
|
57
107
|
[](https://github.com/codespaces/new?repo=nkzw-tech/fate)
|
|
58
108
|
|
|
59
|
-
|
|
109
|
+
## Core Concepts
|
|
60
110
|
|
|
61
111
|
**_fate_** has a minimal API surface and is aimed at reducing data fetching complexity.
|
|
62
112
|
|
|
63
|
-
|
|
113
|
+
### Thinking in Views
|
|
64
114
|
|
|
65
115
|
In fate, each component declares the data it needs using views. Views are composed upward through the component tree until they reach a root, where the actual request is made. fate fetches all required data in a single request. React Suspense manages loading states, and any data-fetching errors naturally bubble up to React error boundaries. This eliminates the need for imperative loading logic or manual error handling.
|
|
66
116
|
|
|
@@ -122,7 +172,7 @@ export const PostCard = ({ post: postRef }: { post: ViewRef<'Post'> }) => {
|
|
|
122
172
|
};
|
|
123
173
|
```
|
|
124
174
|
|
|
125
|
-
A `ViewRef` is a reference to
|
|
175
|
+
A `ViewRef` is a reference to a concrete object of a specific type, for example a `Post` with id `7`. It contains the unique ID of the object, the type name (as `__typename`) and some fate-specific metadata. fate creates and manages these references for you, and you can pass them around your components as needed.
|
|
126
176
|
|
|
127
177
|
Components using `useView` listen to changes for all selected fields. When data changes, fate re-renders all of the fields that depend on that data. For example, if the `title` of the `Post` changes, the `PostCard` component re-renders with new data. However, if a different field such as `likes` that isn't selected in `PostView` changes, the `PostCard` component will not re-render.
|
|
128
178
|
|
|
@@ -304,7 +354,7 @@ export const PostView = view<Post>()({
|
|
|
304
354
|
});
|
|
305
355
|
```
|
|
306
356
|
|
|
307
|
-
Views are opaque objects. Even if you select the same field multiple times through different views, the composed object won't have conflicting
|
|
357
|
+
Views are opaque objects. Even if you select the same field multiple times through different views, the composed object won't have conflicting fields or result in TypeScript errors. fate automatically deduplicates fields during runtime and ensures that each field is only fetched once.
|
|
308
358
|
|
|
309
359
|
### `useView` and Suspense
|
|
310
360
|
|
|
@@ -691,7 +741,7 @@ useEffect(() => {
|
|
|
691
741
|
|
|
692
742
|
## Server Integration
|
|
693
743
|
|
|
694
|
-
Until now, we have focused on the client-side API of fate. You'll need a tRPC backend that follows some conventions so you can generate a typed client using fate's CLI.
|
|
744
|
+
Until now, we have focused on the client-side API of fate. You'll need a tRPC backend that follows some conventions so you can generate a typed client using fate's CLI. At the moment _fate_ is designed to work with tRPC and Prisma, but the framework is not coupled to any particular ORM or database, it's just what we are starting with.
|
|
695
745
|
|
|
696
746
|
### Conventions & Object Identity
|
|
697
747
|
|
|
@@ -992,4 +1042,6 @@ Probably. One day. _Maybe._
|
|
|
992
1042
|
## Acknowledgements
|
|
993
1043
|
|
|
994
1044
|
- [Relay](https://relay.dev/), [Isograph](https://isograph.dev/) & [GraphQL](https://graphql.org/) for inspiration
|
|
995
|
-
- [Ricky Hanlon](https://x.com/rickyfm) for guidance on Async React
|
|
1045
|
+
- [Ricky Hanlon](https://x.com/rickyfm) for guidance on Async React
|
|
1046
|
+
|
|
1047
|
+
**_fate_** was created by [@cnakazawa](https://x.com/cnakazawa) and is maintained by [Nakazawa Tech](https://nakazawa.tech/).
|
package/lib/cli.mjs
CHANGED
|
@@ -9,7 +9,7 @@ const isDataViewField = (field) => Boolean(field) && typeof field === "object" &
|
|
|
9
9
|
* Builds the schema object used by the CLI generator from your data views and
|
|
10
10
|
* list resolver configs.
|
|
11
11
|
*/
|
|
12
|
-
const
|
|
12
|
+
const createSchema = (dataViews, lists) => {
|
|
13
13
|
const canonicalViews = /* @__PURE__ */ new Map();
|
|
14
14
|
const entities = {};
|
|
15
15
|
const fateTypes = /* @__PURE__ */ new Map();
|
|
@@ -37,6 +37,7 @@ const createFateSchema = (dataViews, lists) => {
|
|
|
37
37
|
if (!typeName) throw new Error("Data view is missing a type name.");
|
|
38
38
|
if (!canonicalViews.has(typeName)) canonicalViews.set(typeName, view);
|
|
39
39
|
entities[typeName.toLowerCase()] = { type: typeName };
|
|
40
|
+
ensureType(view);
|
|
40
41
|
}
|
|
41
42
|
for (const [name, list] of Object.entries(lists)) {
|
|
42
43
|
const config = "fields" in list ? { view: list } : list;
|
|
@@ -88,8 +89,8 @@ const formatTypes = (types) => {
|
|
|
88
89
|
const indentBlock = (value, spaces) => value.split("\n").map((line) => line.length ? `${" ".repeat(spaces)}${line}` : line).join("\n");
|
|
89
90
|
const generate = async () => {
|
|
90
91
|
console.log(styleText("bold", `Generating fate client…\n`));
|
|
91
|
-
const
|
|
92
|
-
const { entities, types } =
|
|
92
|
+
const { appRouter, Lists, ...dataViews } = await import(moduleName);
|
|
93
|
+
const { entities, types } = createSchema(Object.values(dataViews), Lists);
|
|
93
94
|
const routerRecord = appRouter._def?.record ?? {};
|
|
94
95
|
const mutationEntries = [];
|
|
95
96
|
const byIdEntries = [];
|
package/lib/index.d.mts
CHANGED
|
@@ -26,8 +26,8 @@ declare const __FateMutationResultBrand: unique symbol;
|
|
|
26
26
|
type __ViewEntityAnchor<T$1 extends Entity> = {
|
|
27
27
|
readonly [__FateEntityBrand]?: T$1;
|
|
28
28
|
};
|
|
29
|
-
type __ViewSelectionAnchor<S
|
|
30
|
-
[__FateSelectionBrand]?: S
|
|
29
|
+
type __ViewSelectionAnchor<S> = {
|
|
30
|
+
[__FateSelectionBrand]?: S;
|
|
31
31
|
};
|
|
32
32
|
type __MutationEntityAnchor<T$1 extends Entity> = {
|
|
33
33
|
readonly [__FateMutationEntityBrand]?: T$1;
|
|
@@ -126,33 +126,33 @@ type SelectionViewSpread<T$1 extends Entity> = { readonly [K in ViewTag]?: Reado
|
|
|
126
126
|
/** Declarative selection of the fields a view needs from an entity. */
|
|
127
127
|
type Selection<T$1 extends Entity> = SelectionShape<T$1> & SelectionViewSpread<T$1>;
|
|
128
128
|
/** View payload stored on a view tag containing the raw selection used to mask data. */
|
|
129
|
-
type ViewPayload<T$1 extends Entity, S
|
|
130
|
-
select: S
|
|
129
|
+
type ViewPayload<T$1 extends Entity, S extends Selection<T$1> = Selection<T$1>> = Readonly<{
|
|
130
|
+
select: S;
|
|
131
131
|
[ViewKind]: true;
|
|
132
132
|
}>;
|
|
133
133
|
/** Definition of a view over an entity type, including the selection of fields. */
|
|
134
|
-
type View<T$1 extends Entity, S
|
|
135
|
-
[viewTag: ViewTag]: ViewPayload<T$1, S
|
|
136
|
-
}> & __ViewEntityAnchor<T$1> & __ViewSelectionAnchor<S
|
|
137
|
-
type HasViewTag<S
|
|
134
|
+
type View<T$1 extends Entity, S extends Selection<T$1> = Selection<T$1>> = Readonly<{
|
|
135
|
+
[viewTag: ViewTag]: ViewPayload<T$1, S>;
|
|
136
|
+
}> & __ViewEntityAnchor<T$1> & __ViewSelectionAnchor<S>;
|
|
137
|
+
type HasViewTag<S> = S extends { [K in ViewTag]?: infer P } ? P extends {
|
|
138
138
|
[ViewKind]: true;
|
|
139
139
|
} ? true : false : false;
|
|
140
140
|
/**
|
|
141
141
|
* Data returned from a resolved view with masking applied and view tags
|
|
142
142
|
* attached for downstream composition.
|
|
143
143
|
*/
|
|
144
|
-
type ViewData<T$1 extends Entity, S
|
|
144
|
+
type ViewData<T$1 extends Entity, S extends Selection<T$1>> = Readonly<(S extends Selection<T$1> ? Mask<T$1, S> : T$1) & {
|
|
145
145
|
[ViewsTag]: Set<string>;
|
|
146
146
|
}>;
|
|
147
147
|
/**
|
|
148
148
|
* Snapshot returned by the cache for a view, including the masked data and all
|
|
149
149
|
* referenced entity IDs.
|
|
150
150
|
*/
|
|
151
|
-
type ViewSnapshot<T$1 extends Entity, S
|
|
151
|
+
type ViewSnapshot<T$1 extends Entity, S extends Selection<T$1>> = Readonly<{
|
|
152
152
|
coverage: ReadonlyArray<readonly [id: EntityId, paths: ReadonlySet<string>]>;
|
|
153
|
-
data: ViewData<T$1, S
|
|
153
|
+
data: ViewData<T$1, S>;
|
|
154
154
|
}>;
|
|
155
|
-
type ConnectionMask<T$1 extends Entity, S
|
|
155
|
+
type ConnectionMask<T$1 extends Entity, S> = S extends {
|
|
156
156
|
items: infer ItemSelection;
|
|
157
157
|
pagination?: infer PaginationSelection;
|
|
158
158
|
} ? {
|
|
@@ -173,7 +173,7 @@ type EntityName<T$1> = T$1 extends {
|
|
|
173
173
|
__typename: infer N extends string;
|
|
174
174
|
} ? N : never;
|
|
175
175
|
/** Recursively applies a view selection to an entity to mask fields that aren't selected. */
|
|
176
|
-
type Mask<T$1, S
|
|
176
|
+
type Mask<T$1, S> = T$1 extends Array<infer U extends Entity> ? S extends true ? Array<U> : S extends ConnectionSelection<U> ? ConnectionMask<U, S> : HasViewTag<S> extends true ? Array<ViewRef<U['__typename']>> : Array<Mask<U, S>> : S extends true ? T$1 : S extends object ? HasViewTag<S> extends true ? ViewRef<EntityName<NonNullable<T$1>>> : { [K in keyof S as K extends 'args' ? never : K]: S[K] extends true ? NonNullable<T$1>[Extract<K, keyof T$1>] : Mask<NonNullable<T$1>[Extract<K, keyof T$1>], Extract<S[K], object>> } & (T$1 extends Entity ? Pick<NonNullable<T$1>, '__typename'> : Record<never, never>) : T$1;
|
|
177
177
|
/** Entity type captured from a view definition. */
|
|
178
178
|
type ViewEntity<V> = V extends View<infer T, any> ? T : never;
|
|
179
179
|
/** Name of the entity type captured from a view definition. */
|
|
@@ -345,15 +345,15 @@ type SelectionPlan = {
|
|
|
345
345
|
* Flattens a view into a `SelectionPlan`, expanding composed views and
|
|
346
346
|
* partitioning nested args so the client can fetch exactly what is declared.
|
|
347
347
|
*/
|
|
348
|
-
declare const getSelectionPlan: <T$1 extends Entity, S
|
|
348
|
+
declare const getSelectionPlan: <T$1 extends Entity, S extends Selection<T$1>, V extends View<T$1, S>>(viewComposition: V, ref: ViewRef<T$1["__typename"]> | null) => SelectionPlan;
|
|
349
349
|
//#endregion
|
|
350
350
|
//#region src/cache.d.ts
|
|
351
351
|
declare class ViewDataCache {
|
|
352
352
|
private cache;
|
|
353
353
|
private rootDependencies;
|
|
354
354
|
private dependencyIndex;
|
|
355
|
-
get<T$1 extends Entity, S
|
|
356
|
-
set<T$1 extends Entity, S
|
|
355
|
+
get<T$1 extends Entity, S extends Selection<T$1>, V extends View<T$1, S>>(entityId: EntityId, view: V, ref: ViewRef<T$1['__typename']>): FateThenable<ViewSnapshot<T$1, S>> | null;
|
|
356
|
+
set<T$1 extends Entity, S extends Selection<T$1>, V extends View<T$1, S>>(entityId: EntityId, view: V, ref: ViewRef<T$1['__typename']>, thenable: FateThenable<ViewSnapshot<T$1, S>>, dependencies: ReadonlySet<EntityId>): void;
|
|
357
357
|
invalidate(entityId: EntityId): void;
|
|
358
358
|
private invalidateDependents;
|
|
359
359
|
private delete;
|
|
@@ -528,7 +528,7 @@ declare class FateClient<Mutations extends Record<string, MutationDefinition<any
|
|
|
528
528
|
id: string | number;
|
|
529
529
|
[ViewsTag]: Set<string>;
|
|
530
530
|
}>;
|
|
531
|
-
readView<T$1 extends Entity, S
|
|
531
|
+
readView<T$1 extends Entity, S extends Selection<T$1>, V extends View<T$1, S>>(view: V, ref: ViewRef<T$1['__typename']>): FateThenable<ViewSnapshot<T$1, S>>;
|
|
532
532
|
private mergeListState;
|
|
533
533
|
registerOptimisticUpdate(entityId: EntityId | null, select: ReadonlySet<string>): number | null;
|
|
534
534
|
clearOptimisticUpdate(token: number | null): void;
|
|
@@ -570,7 +570,7 @@ declare function createClient<Mutations extends Record<string, MutationDefinitio
|
|
|
570
570
|
declare const toEntityId: (type: TypeName, rawId: string | number) => EntityId;
|
|
571
571
|
//#endregion
|
|
572
572
|
//#region src/view.d.ts
|
|
573
|
-
type SelectionValidation<T$1 extends Entity, S
|
|
573
|
+
type SelectionValidation<T$1 extends Entity, S extends Selection<T$1>> = Exclude<keyof Omit<S, typeof __FateEntityBrand | typeof __FateSelectionBrand>, keyof Selection<T$1>> extends never ? unknown : never;
|
|
574
574
|
/**
|
|
575
575
|
* Creates a reusable view for an object using the declared selection.
|
|
576
576
|
*
|
|
@@ -580,6 +580,6 @@ type SelectionValidation<T$1 extends Entity, S$1 extends Selection<T$1>> = Exclu
|
|
|
580
580
|
* title: true,
|
|
581
581
|
* });
|
|
582
582
|
*/
|
|
583
|
-
declare function view<T$1 extends Entity>(): <S
|
|
583
|
+
declare function view<T$1 extends Entity>(): <S extends Selection<T$1>>(select: S & SelectionValidation<T$1, S>) => View<T$1, S>;
|
|
584
584
|
//#endregion
|
|
585
585
|
export { type ConnectionMetadata, type ConnectionRef, ConnectionTag, type Entity, type EntityId, FateClient, type AnyRecord as FateRecord, type ListItem, type Mask, type MutationDefinition, type MutationEntity, type MutationIdentifier, type MutationInput, type MutationResult, type NodesItem, type Pagination, type Request, type RequestMode, type RequestOptions, type RequestResult, type Selection, type Snapshot, type Transport, type TypeConfig, type View, type ViewData, type ViewEntity, type ViewEntityName, type ViewRef, type ViewSelection, type ViewSnapshot, type ViewTag, createClient, createTRPCTransport, getSelectionPlan, isViewTag, mutation, toEntityId, view };
|
package/lib/server.d.mts
CHANGED
|
@@ -51,8 +51,8 @@ type DataViewConfig<Item$1 extends AnyRecord, Context> = Record<string, DataFiel
|
|
|
51
51
|
* title: true,
|
|
52
52
|
* });
|
|
53
53
|
*/
|
|
54
|
-
declare function dataView<Item$1 extends AnyRecord, Context = unknown>(typeName?: string): <Fields
|
|
55
|
-
readonly [dataViewFieldsKey]: Fields
|
|
54
|
+
declare function dataView<Item$1 extends AnyRecord, Context = unknown>(typeName?: string): <Fields extends DataViewConfig<Item$1, Context>>(fields: Fields) => DataView<Item$1, Context> & {
|
|
55
|
+
readonly [dataViewFieldsKey]: Fields;
|
|
56
56
|
};
|
|
57
57
|
/**
|
|
58
58
|
* Marks a data view as a list resolver so the server can respond with
|
package/lib/server.mjs
CHANGED
|
@@ -265,19 +265,28 @@ function resolver(config) {
|
|
|
265
265
|
}
|
|
266
266
|
const isResolverField = (field) => Boolean(field) && typeof field === "object" && "kind" in field && field.kind === "resolver";
|
|
267
267
|
const isDataViewField = (field) => Boolean(field) && typeof field === "object" && "fields" in field;
|
|
268
|
-
const filterToViewFields = (item, view) => {
|
|
268
|
+
const filterToViewFields = (item, view, selectedPaths, prefix = null) => {
|
|
269
269
|
if (!isRecord(item)) return item;
|
|
270
270
|
const filtered = {};
|
|
271
271
|
for (const [field, config] of Object.entries(view.fields)) {
|
|
272
|
+
const path = prefix ? `${prefix}.${field}` : field;
|
|
273
|
+
let hasSelection = selectedPaths.has(path);
|
|
274
|
+
if (!hasSelection) {
|
|
275
|
+
for (const selected of selectedPaths) if (selected.startsWith(`${path}.`)) {
|
|
276
|
+
hasSelection = true;
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (!hasSelection) continue;
|
|
272
281
|
if (!(field in item)) continue;
|
|
273
282
|
const value = item[field];
|
|
274
283
|
if (isDataViewField(config)) {
|
|
275
284
|
if (Array.isArray(value)) {
|
|
276
|
-
filtered[field] = value.map((entry) => isRecord(entry) ? filterToViewFields(entry, config) : entry);
|
|
285
|
+
filtered[field] = value.map((entry) => isRecord(entry) ? filterToViewFields(entry, config, selectedPaths, path) : entry);
|
|
277
286
|
continue;
|
|
278
287
|
}
|
|
279
288
|
if (isRecord(value)) {
|
|
280
|
-
filtered[field] = filterToViewFields(value, config);
|
|
289
|
+
filtered[field] = filterToViewFields(value, config, selectedPaths, path);
|
|
281
290
|
continue;
|
|
282
291
|
}
|
|
283
292
|
}
|
|
@@ -319,14 +328,17 @@ const createSelectedNode = (view, path) => ({
|
|
|
319
328
|
resolvers: /* @__PURE__ */ new Map(),
|
|
320
329
|
view
|
|
321
330
|
});
|
|
322
|
-
const assignPath = (node, segments, path, view, allowedPaths) => {
|
|
331
|
+
const assignPath = (node, segments, path, selectedPaths, view, allowedPaths) => {
|
|
323
332
|
if (segments.length === 0) return;
|
|
324
333
|
const [segment, ...rest] = segments;
|
|
325
334
|
const field = view.fields[segment];
|
|
326
335
|
if (!field) return;
|
|
327
336
|
const nextPath = path ? `${path}.${segment}` : segment;
|
|
328
337
|
if (isResolverField(field)) {
|
|
329
|
-
if (rest.length === 0)
|
|
338
|
+
if (rest.length === 0) {
|
|
339
|
+
node.resolvers.set(segment, field);
|
|
340
|
+
selectedPaths.add(nextPath);
|
|
341
|
+
}
|
|
330
342
|
return;
|
|
331
343
|
}
|
|
332
344
|
if (isDataViewField(field)) {
|
|
@@ -335,23 +347,20 @@ const assignPath = (node, segments, path, view, allowedPaths) => {
|
|
|
335
347
|
relationNode = createSelectedNode(field, nextPath);
|
|
336
348
|
node.relations.set(segment, relationNode);
|
|
337
349
|
}
|
|
350
|
+
if (field.fields.id === true) {
|
|
351
|
+
allowedPaths.add(`${nextPath}.id`);
|
|
352
|
+
selectedPaths.add(`${nextPath}.id`);
|
|
353
|
+
}
|
|
338
354
|
if (rest.length === 0) {
|
|
339
|
-
|
|
355
|
+
selectedPaths.add(nextPath);
|
|
340
356
|
return;
|
|
341
357
|
}
|
|
342
|
-
assignPath(relationNode, rest, nextPath, field, allowedPaths);
|
|
358
|
+
assignPath(relationNode, rest, nextPath, selectedPaths, field, allowedPaths);
|
|
343
359
|
return;
|
|
344
360
|
}
|
|
345
|
-
if (rest.length === 0)
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
for (const [field, child] of Object.entries(view.fields)) {
|
|
349
|
-
const nextPath = `${basePath}.${field}`;
|
|
350
|
-
if (child === true) {
|
|
351
|
-
allowedPaths.add(nextPath);
|
|
352
|
-
continue;
|
|
353
|
-
}
|
|
354
|
-
if (isDataViewField(child)) collectViewPaths(nextPath, child, allowedPaths);
|
|
361
|
+
if (rest.length === 0) {
|
|
362
|
+
allowedPaths.add(nextPath);
|
|
363
|
+
selectedPaths.add(nextPath);
|
|
355
364
|
}
|
|
356
365
|
};
|
|
357
366
|
const collectResolvers = (node, select, args$1, context) => {
|
|
@@ -409,10 +418,12 @@ const resolveNode = async (options) => {
|
|
|
409
418
|
*/
|
|
410
419
|
function createResolver({ args: args$1, ctx, select: initialSelect, view }) {
|
|
411
420
|
const allowedPaths = /* @__PURE__ */ new Set();
|
|
421
|
+
const selectedPaths = /* @__PURE__ */ new Set();
|
|
422
|
+
selectedPaths.add("id");
|
|
412
423
|
const root = createSelectedNode(view, null);
|
|
413
424
|
for (const path of initialSelect) {
|
|
414
425
|
if (!path) continue;
|
|
415
|
-
assignPath(root, path.split("."), null, view, allowedPaths);
|
|
426
|
+
assignPath(root, path.split("."), null, selectedPaths, view, allowedPaths);
|
|
416
427
|
}
|
|
417
428
|
const select = prismaSelect([...allowedPaths], args$1);
|
|
418
429
|
collectResolvers(root, select, args$1, ctx);
|
|
@@ -425,7 +436,7 @@ function createResolver({ args: args$1, ctx, select: initialSelect, view }) {
|
|
|
425
436
|
args: args$1,
|
|
426
437
|
context: ctx
|
|
427
438
|
}
|
|
428
|
-
}), root.view),
|
|
439
|
+
}), root.view, selectedPaths),
|
|
429
440
|
view: root.view
|
|
430
441
|
});
|
|
431
442
|
return {
|