@nkzw/fate 0.0.1 → 0.0.3

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.
Files changed (3) hide show
  1. package/README.md +62 -12
  2. package/lib/server.mjs +30 -19
  3. package/package.json +1 -4
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
- ### Installation
41
+ ### Template
44
42
 
45
- **_fate_** requires React 19.2+.
43
+ Get started with [a ready-made template](https://github.com/nkzw-tech/fate-template) quickly:
46
44
 
47
- ```bash
48
- pnpm add react-fate @nkzw/fate
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
  [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?repo=nkzw-tech/fate)
58
108
 
59
- ### Core Concepts
109
+ ## Core Concepts
60
110
 
61
111
  **_fate_** has a minimal API surface and is aimed at reducing data fetching complexity.
62
112
 
63
- #### Thinking in Views
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 an object of a specific type, in this case a `Post`. It contains the unique ID for 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.
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 fiels or result in TypeScript errors. fate automatically deduplicates fields during runtime and ensures that each field is only fetched once.
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
 
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) node.resolvers.set(segment, field);
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
- collectViewPaths(nextPath, field, allowedPaths);
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) allowedPaths.add(nextPath);
346
- };
347
- const collectViewPaths = (basePath, view, allowedPaths) => {
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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nkzw/fate",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "fate is a modern data client for React.",
5
5
  "homepage": "https://github.com/nkzw-tech/fate",
6
6
  "repository": {
@@ -16,17 +16,14 @@
16
16
  "exports": {
17
17
  ".": {
18
18
  "types": "./lib/index.d.mts",
19
- "development": "./src/index.ts",
20
19
  "default": "./lib/index.mjs"
21
20
  },
22
21
  "./cli": {
23
22
  "types": "./lib/cli.d.mts",
24
- "development": "./src/cli.ts",
25
23
  "default": "./lib/cli.mjs"
26
24
  },
27
25
  "./server": {
28
26
  "types": "./lib/server.d.mts",
29
- "development": "./src/server.ts",
30
27
  "default": "./lib/server.mjs"
31
28
  }
32
29
  },