create-fate 1.0.4 → 1.3.0

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 (206) hide show
  1. package/README.md +3 -0
  2. package/bin/create-fate.mjs +594 -9
  3. package/package.json +2 -2
  4. package/templates/fate/README.md +3 -0
  5. package/templates/fate/_shared/vue-app/package.json +54 -0
  6. package/templates/fate/_shared/vue-app/pages/index.vue +57 -0
  7. package/templates/fate/_shared/vue-app/pages/layout.vue +32 -0
  8. package/templates/fate/_shared/vue-app/pages/post/[id].vue +24 -0
  9. package/templates/fate/_shared/vue-app/src/App.css +63 -0
  10. package/templates/fate/_shared/vue-app/src/fateViews.ts +14 -0
  11. package/templates/fate/_shared/vue-app/src/lib/cx.ts +6 -0
  12. package/templates/fate/_shared/vue-app/src/lib/env.ts +3 -0
  13. package/templates/fate/_shared/vue-app/src/ui/Button.vue +67 -0
  14. package/templates/fate/_shared/vue-app/src/ui/Card.vue +16 -0
  15. package/templates/fate/_shared/vue-app/src/ui/Error.vue +9 -0
  16. package/templates/fate/_shared/vue-app/src/ui/ErrorBoundary.vue +22 -0
  17. package/templates/fate/_shared/vue-app/src/ui/Header.vue +35 -0
  18. package/templates/fate/_shared/vue-app/src/ui/Link.vue +15 -0
  19. package/templates/fate/_shared/vue-app/src/ui/PostCard.vue +29 -0
  20. package/templates/fate/_shared/vue-app/src/ui/Section.vue +19 -0
  21. package/templates/fate/_shared/vue-app/src/ui/Thinking.vue +11 -0
  22. package/templates/fate/_shared/vue-app/src/ui/UserCard.vue +23 -0
  23. package/templates/fate/_shared/vue-app/src/vue-shims.d.ts +6 -0
  24. package/templates/fate/_shared/vue-app/tsconfig.json +22 -0
  25. package/templates/fate/_shared/vue-app/vite.config.ts +58 -0
  26. package/templates/fate/_shared/vue-app/void.json +5 -0
  27. package/templates/fate/cloudflare/.github/workflows/push.yml +27 -0
  28. package/templates/fate/cloudflare/.vite-hooks/pre-commit +1 -0
  29. package/templates/fate/cloudflare/.vscode/extensions.json +12 -0
  30. package/templates/fate/cloudflare/.vscode/settings.json +25 -0
  31. package/templates/fate/cloudflare/AGENTS.md +55 -0
  32. package/templates/fate/cloudflare/README.md +126 -0
  33. package/templates/fate/cloudflare/_gitignore +20 -0
  34. package/templates/fate/cloudflare/client/package.json +56 -0
  35. package/templates/fate/cloudflare/client/pages/index.tsx +96 -0
  36. package/templates/fate/cloudflare/client/pages/layout.tsx +82 -0
  37. package/templates/fate/cloudflare/client/pages/login.tsx +107 -0
  38. package/templates/fate/cloudflare/client/pages/post/[id].tsx +22 -0
  39. package/templates/fate/cloudflare/client/pages/search.tsx +99 -0
  40. package/templates/fate/cloudflare/client/public/icon.svg +1 -0
  41. package/templates/fate/cloudflare/client/src/App.css +153 -0
  42. package/templates/fate/cloudflare/client/src/lib/AvailableLanguages.tsx +5 -0
  43. package/templates/fate/cloudflare/client/src/lib/cx.tsx +6 -0
  44. package/templates/fate/cloudflare/client/src/lib/env.tsx +5 -0
  45. package/templates/fate/cloudflare/client/src/ui/Badge.tsx +31 -0
  46. package/templates/fate/cloudflare/client/src/ui/Button.tsx +108 -0
  47. package/templates/fate/cloudflare/client/src/ui/Card.tsx +32 -0
  48. package/templates/fate/cloudflare/client/src/ui/CommentCard.tsx +75 -0
  49. package/templates/fate/cloudflare/client/src/ui/Error.tsx +19 -0
  50. package/templates/fate/cloudflare/client/src/ui/H2.tsx +15 -0
  51. package/templates/fate/cloudflare/client/src/ui/H3.tsx +20 -0
  52. package/templates/fate/cloudflare/client/src/ui/Header.tsx +98 -0
  53. package/templates/fate/cloudflare/client/src/ui/Input.tsx +17 -0
  54. package/templates/fate/cloudflare/client/src/ui/Link.tsx +11 -0
  55. package/templates/fate/cloudflare/client/src/ui/LocaleSwitcher.tsx +52 -0
  56. package/templates/fate/cloudflare/client/src/ui/PostCard.tsx +303 -0
  57. package/templates/fate/cloudflare/client/src/ui/Section.tsx +23 -0
  58. package/templates/fate/cloudflare/client/src/ui/UserCard.tsx +131 -0
  59. package/templates/fate/cloudflare/client/src/user/AuthClient.tsx +6 -0
  60. package/templates/fate/cloudflare/client/translations/de_DE.json +689 -0
  61. package/templates/fate/cloudflare/client/translations/ja_JP.json +689 -0
  62. package/templates/fate/cloudflare/client/vite.config.ts +45 -0
  63. package/templates/fate/cloudflare/client/void.json +29 -0
  64. package/templates/fate/cloudflare/package.json +46 -0
  65. package/templates/fate/cloudflare/pnpm-workspace.yaml +39 -0
  66. package/templates/fate/cloudflare/server/.env.example +1 -0
  67. package/templates/fate/cloudflare/server/db/db.ts +30 -0
  68. package/templates/fate/cloudflare/server/db/migrations/20260508073357_initial_schema.sql +133 -0
  69. package/templates/fate/cloudflare/server/db/migrations/20260508120500_seed_cloudflare_demo.sql +170 -0
  70. package/templates/fate/cloudflare/server/db/migrations/meta/20260508073357_snapshot.json +868 -0
  71. package/templates/fate/cloudflare/server/db/migrations/meta/_journal.json +20 -0
  72. package/templates/fate/cloudflare/server/db/queries.ts +211 -0
  73. package/templates/fate/cloudflare/server/db/schema.ts +332 -0
  74. package/templates/fate/cloudflare/server/package.json +34 -0
  75. package/templates/fate/cloudflare/server/src/fate/commentPermissions.ts +17 -0
  76. package/templates/fate/cloudflare/server/src/fate/context.ts +15 -0
  77. package/templates/fate/cloudflare/server/src/fate/live.ts +7 -0
  78. package/templates/fate/cloudflare/server/src/fate/server.ts +411 -0
  79. package/templates/fate/cloudflare/server/src/fate/views.ts +164 -0
  80. package/templates/fate/cloudflare/server/src/index.ts +80 -0
  81. package/templates/fate/cloudflare/server/src/lib/auth.ts +66 -0
  82. package/templates/fate/cloudflare/server/src/lib/origins.ts +17 -0
  83. package/templates/fate/cloudflare/server/src/router.ts +2 -0
  84. package/templates/fate/cloudflare/server/src/user/SessionUser.tsx +17 -0
  85. package/templates/fate/cloudflare/server/tsconfig.json +10 -0
  86. package/templates/fate/cloudflare/server/wrangler.jsonc +29 -0
  87. package/templates/fate/cloudflare/tsconfig.json +33 -0
  88. package/templates/fate/cloudflare/vite.config.ts +72 -0
  89. package/templates/fate/drizzle/README.md +2 -8
  90. package/templates/fate/drizzle/client/pages/layout.tsx +2 -2
  91. package/templates/fate/drizzle/client/src/ui/PostCard.tsx +38 -15
  92. package/templates/fate/drizzle/pnpm-workspace.yaml +9 -3
  93. package/templates/fate/drizzle/server/vite.config.ts +1 -1
  94. package/templates/fate/graphql/.github/workflows/push.yml +27 -0
  95. package/templates/fate/graphql/.vite-hooks/pre-commit +1 -0
  96. package/templates/fate/graphql/.vscode/extensions.json +12 -0
  97. package/templates/fate/graphql/.vscode/settings.json +25 -0
  98. package/templates/fate/graphql/AGENTS.md +55 -0
  99. package/templates/fate/graphql/README.md +121 -0
  100. package/templates/fate/graphql/_gitignore +23 -0
  101. package/templates/fate/graphql/client/package.json +57 -0
  102. package/templates/fate/graphql/client/pages/index.tsx +96 -0
  103. package/templates/fate/graphql/client/pages/layout.tsx +83 -0
  104. package/templates/fate/graphql/client/pages/login.tsx +107 -0
  105. package/templates/fate/graphql/client/pages/post/[id].tsx +22 -0
  106. package/templates/fate/graphql/client/pages/search.tsx +99 -0
  107. package/templates/fate/graphql/client/public/icon.svg +1 -0
  108. package/templates/fate/graphql/client/src/App.css +153 -0
  109. package/templates/fate/graphql/client/src/lib/AvailableLanguages.tsx +5 -0
  110. package/templates/fate/graphql/client/src/lib/cx.tsx +6 -0
  111. package/templates/fate/graphql/client/src/lib/env.tsx +5 -0
  112. package/templates/fate/graphql/client/src/ui/Badge.tsx +31 -0
  113. package/templates/fate/graphql/client/src/ui/Button.tsx +108 -0
  114. package/templates/fate/graphql/client/src/ui/Card.tsx +32 -0
  115. package/templates/fate/graphql/client/src/ui/CommentCard.tsx +75 -0
  116. package/templates/fate/graphql/client/src/ui/Error.tsx +19 -0
  117. package/templates/fate/graphql/client/src/ui/H2.tsx +15 -0
  118. package/templates/fate/graphql/client/src/ui/H3.tsx +20 -0
  119. package/templates/fate/graphql/client/src/ui/Header.tsx +98 -0
  120. package/templates/fate/graphql/client/src/ui/Input.tsx +17 -0
  121. package/templates/fate/graphql/client/src/ui/Link.tsx +11 -0
  122. package/templates/fate/graphql/client/src/ui/LocaleSwitcher.tsx +52 -0
  123. package/templates/fate/graphql/client/src/ui/PostCard.tsx +303 -0
  124. package/templates/fate/graphql/client/src/ui/Section.tsx +23 -0
  125. package/templates/fate/graphql/client/src/ui/UserCard.tsx +131 -0
  126. package/templates/fate/graphql/client/src/user/AuthClient.tsx +6 -0
  127. package/templates/fate/graphql/client/translations/de_DE.json +689 -0
  128. package/templates/fate/graphql/client/translations/ja_JP.json +689 -0
  129. package/templates/fate/graphql/client/vite.config.ts +40 -0
  130. package/templates/fate/graphql/client/void.json +29 -0
  131. package/templates/fate/graphql/docker-compose.yml +11 -0
  132. package/templates/fate/graphql/package.json +47 -0
  133. package/templates/fate/graphql/pnpm-workspace.yaml +45 -0
  134. package/templates/fate/graphql/server/.env.example +5 -0
  135. package/templates/fate/graphql/server/README.md +27 -0
  136. package/templates/fate/graphql/server/package.json +64 -0
  137. package/templates/fate/graphql/server/prisma.config.ts +17 -0
  138. package/templates/fate/graphql/server/scripts/generate-graphql.tsx +40 -0
  139. package/templates/fate/graphql/server/src/__tests__/index.test.tsx +5 -0
  140. package/templates/fate/graphql/server/src/app.tsx +47 -0
  141. package/templates/fate/graphql/server/src/graphql/builder.tsx +121 -0
  142. package/templates/fate/graphql/server/src/graphql/context.tsx +22 -0
  143. package/templates/fate/graphql/server/src/graphql/fate.ts +85 -0
  144. package/templates/fate/graphql/server/src/graphql/lib/authDirectives.tsx +93 -0
  145. package/templates/fate/graphql/server/src/graphql/lib/decodeGlobalID.tsx +12 -0
  146. package/templates/fate/graphql/server/src/graphql/lib/decodeIDOrThrow.tsx +10 -0
  147. package/templates/fate/graphql/server/src/graphql/lib/encodeGlobalID.tsx +5 -0
  148. package/templates/fate/graphql/server/src/graphql/live.tsx +241 -0
  149. package/templates/fate/graphql/server/src/graphql/mutations.tsx +265 -0
  150. package/templates/fate/graphql/server/src/graphql/nodes/Category.tsx +35 -0
  151. package/templates/fate/graphql/server/src/graphql/nodes/Comment.tsx +46 -0
  152. package/templates/fate/graphql/server/src/graphql/nodes/Event.tsx +71 -0
  153. package/templates/fate/graphql/server/src/graphql/nodes/Post.tsx +45 -0
  154. package/templates/fate/graphql/server/src/graphql/nodes/Tag.tsx +11 -0
  155. package/templates/fate/graphql/server/src/graphql/nodes/User.tsx +28 -0
  156. package/templates/fate/graphql/server/src/graphql/schema.tsx +16 -0
  157. package/templates/fate/graphql/server/src/graphql/views.ts +193 -0
  158. package/templates/fate/graphql/server/src/index.tsx +45 -0
  159. package/templates/fate/graphql/server/src/lib/auth.tsx +31 -0
  160. package/templates/fate/graphql/server/src/lib/env.tsx +3 -0
  161. package/templates/fate/graphql/server/src/lib/origins.ts +18 -0
  162. package/templates/fate/graphql/server/src/prisma/prisma.tsx +9 -0
  163. package/templates/fate/graphql/server/src/prisma/schema.prisma +181 -0
  164. package/templates/fate/graphql/server/src/prisma/seed.tsx +161 -0
  165. package/templates/fate/graphql/server/src/user/SessionUser.tsx +17 -0
  166. package/templates/fate/graphql/server/src/user/isAdmin.tsx +3 -0
  167. package/templates/fate/graphql/server/tsconfig.json +33 -0
  168. package/templates/fate/graphql/server/vite.config.ts +55 -0
  169. package/templates/fate/graphql/tsconfig.json +33 -0
  170. package/templates/fate/graphql/vite.config.ts +80 -0
  171. package/templates/fate/graphql-client/.env.example +3 -0
  172. package/templates/fate/graphql-client/AGENTS.md +3 -0
  173. package/templates/fate/graphql-client/README.md +69 -0
  174. package/templates/fate/graphql-client/_gitignore +14 -0
  175. package/templates/fate/graphql-client/package.json +62 -0
  176. package/templates/fate/graphql-client/pages/index.tsx +69 -0
  177. package/templates/fate/graphql-client/pages/layout.tsx +41 -0
  178. package/templates/fate/graphql-client/pnpm-workspace.yaml +38 -0
  179. package/templates/fate/graphql-client/public/icon.svg +8 -0
  180. package/templates/fate/graphql-client/src/App.css +28 -0
  181. package/templates/fate/graphql-client/src/fate/graphql.ts +52 -0
  182. package/templates/fate/graphql-client/tsconfig.json +30 -0
  183. package/templates/fate/graphql-client/vite.config.ts +87 -0
  184. package/templates/fate/graphql-client/void.json +29 -0
  185. package/templates/fate/http/README.md +2 -8
  186. package/templates/fate/http/client/pages/layout.tsx +2 -2
  187. package/templates/fate/http/client/src/ui/PostCard.tsx +37 -15
  188. package/templates/fate/http/pnpm-workspace.yaml +9 -3
  189. package/templates/fate/http/server/vite.config.ts +1 -1
  190. package/templates/fate/prisma/README.md +2 -8
  191. package/templates/fate/prisma/client/pages/layout.tsx +2 -2
  192. package/templates/fate/prisma/client/src/ui/PostCard.tsx +37 -15
  193. package/templates/fate/prisma/pnpm-workspace.yaml +9 -3
  194. package/templates/fate/prisma/server/vite.config.ts +1 -1
  195. package/templates/fate/prisma/vite.config.ts +1 -1
  196. package/templates/fate/void/package.json +10 -2
  197. package/templates/fate/void/pnpm-workspace.yaml +12 -4
  198. package/templates/fate/void/src/ui/CategoryCard.tsx +3 -7
  199. package/templates/fate/void/src/ui/PostCard.tsx +38 -15
  200. package/templates/fate/void/vite.config.ts +5 -0
  201. package/templates/fate/drizzle/public/fate-logo-dark.svg +0 -4
  202. package/templates/fate/drizzle/public/fate-logo.svg +0 -4
  203. package/templates/fate/http/public/fate-logo-dark.svg +0 -4
  204. package/templates/fate/http/public/fate-logo.svg +0 -4
  205. package/templates/fate/prisma/public/fate-logo-dark.svg +0 -4
  206. package/templates/fate/prisma/public/fate-logo.svg +0 -4
@@ -0,0 +1,93 @@
1
+ import SchemaBuilder, {
2
+ BasePlugin,
3
+ PothosOutputFieldConfig,
4
+ PothosTypeConfig,
5
+ SchemaTypes,
6
+ } from '@pothos/core';
7
+ import { FieldAuthScopes, TypeAuthScopes } from '@pothos/plugin-scope-auth';
8
+
9
+ declare global {
10
+ export namespace PothosSchemaTypes {
11
+ export interface Plugins<Types extends SchemaTypes> {
12
+ authDirectives: AuthDirectivesPlugin<Types>;
13
+ }
14
+ }
15
+ }
16
+
17
+ export class AuthDirectivesPlugin<Types extends SchemaTypes> extends BasePlugin<Types> {
18
+ override onOutputFieldConfig(
19
+ fieldConfig: PothosOutputFieldConfig<Types>,
20
+ ): PothosOutputFieldConfig<Types> | null {
21
+ const { authScopes } = fieldConfig.pothosOptions;
22
+
23
+ if (!authScopes) {
24
+ return fieldConfig;
25
+ }
26
+
27
+ const scopes = this.#resolveScopes(authScopes);
28
+
29
+ return {
30
+ ...fieldConfig,
31
+ extensions: {
32
+ ...fieldConfig.extensions,
33
+ directives: {
34
+ ...(fieldConfig.extensions?.directives as Record<string, unknown>),
35
+ auth: scopes,
36
+ },
37
+ },
38
+ };
39
+ }
40
+
41
+ override onTypeConfig(typeConfig: PothosTypeConfig): PothosTypeConfig {
42
+ if (
43
+ typeConfig.kind === 'Enum' ||
44
+ typeConfig.kind === 'Scalar' ||
45
+ typeConfig.kind === 'InputObject' ||
46
+ typeConfig.kind === 'Union' ||
47
+ !typeConfig.pothosOptions.authScopes
48
+ ) {
49
+ return typeConfig;
50
+ }
51
+
52
+ const scopes = this.#resolveScopes(typeConfig.pothosOptions.authScopes);
53
+
54
+ return {
55
+ ...typeConfig,
56
+ extensions: {
57
+ ...typeConfig.extensions,
58
+ directives: {
59
+ ...(typeConfig.extensions?.directives as Record<string, unknown>),
60
+ auth: scopes,
61
+ },
62
+ },
63
+ };
64
+ }
65
+
66
+ #resolveScopes(
67
+ authScopes:
68
+ | FieldAuthScopes<Types, object, Record<string, unknown>>
69
+ | TypeAuthScopes<SchemaTypes, object>,
70
+ ) {
71
+ if (typeof authScopes !== 'function') {
72
+ return authScopes;
73
+ }
74
+
75
+ try {
76
+ const resolved = authScopes({}, {}, {}, {} as never);
77
+
78
+ if (typeof resolved !== 'object' || !resolved) {
79
+ return {};
80
+ }
81
+
82
+ return Object.fromEntries(
83
+ Object.entries(resolved).map(([key, value]) => [key, value === undefined ? true : value]),
84
+ );
85
+ } catch {
86
+ return {};
87
+ }
88
+ }
89
+ }
90
+
91
+ SchemaBuilder.registerPlugin('authDirectives', AuthDirectivesPlugin);
92
+
93
+ export default 'authDirectives';
@@ -0,0 +1,12 @@
1
+ export default function decodeGlobalID(globalID: string) {
2
+ const separator = globalID.indexOf('-');
3
+ if (separator <= 0 || separator >= globalID.length - 1) {
4
+ throw new Error(`decodeGlobalID: Invalid global ID '${globalID}'.`);
5
+ }
6
+ const typename = globalID.slice(0, separator);
7
+ const id = globalID.slice(separator + 1);
8
+ if (!id) {
9
+ throw new Error(`decodeGlobalID: Provided an empty id for '${typename}'.`);
10
+ }
11
+ return { id, typename };
12
+ }
@@ -0,0 +1,10 @@
1
+ import PrismaTypes from '../../prisma/pothos-types.ts';
2
+ import decodeGlobalID from './decodeGlobalID.tsx';
3
+
4
+ export default function decodeIDOrThrow(type: keyof PrismaTypes, globalID: string) {
5
+ const { id, typename } = decodeGlobalID(globalID);
6
+ if (typename !== type || !id) {
7
+ throw new Error(`Expected '${type}' but received '${typename}' with id '${id}'.`);
8
+ }
9
+ return id;
10
+ }
@@ -0,0 +1,5 @@
1
+ import PrismaTypes from '../../prisma/pothos-types.ts';
2
+
3
+ export default function encodeGlobalID(typename: keyof PrismaTypes, id: string | number | bigint) {
4
+ return `${typename}-${id}`;
5
+ }
@@ -0,0 +1,241 @@
1
+ import type { LiveConnectionSourceEvent, LiveSourceEvent } from '@nkzw/fate/server';
2
+ import { isRecord } from '@nkzw/fate/server';
3
+ import builder, { JSONScalar } from './builder.tsx';
4
+ import type { AppContext } from './context.tsx';
5
+ import { fate, live, viewsByType } from './fate.ts';
6
+ import decodeGlobalID from './lib/decodeGlobalID.tsx';
7
+
8
+ type ViewType = keyof typeof viewsByType;
9
+
10
+ type LiveNodePayload = {
11
+ data?: unknown;
12
+ delete?: boolean;
13
+ id?: string | number;
14
+ select?: Array<string>;
15
+ };
16
+
17
+ type LiveConnectionPayload = {
18
+ cursor?: string;
19
+ id?: string | number;
20
+ node?: unknown;
21
+ nodeType?: string;
22
+ targetCursor?: string;
23
+ type: LiveConnectionSourceEvent['type'];
24
+ };
25
+
26
+ const asRecord = (value: unknown): Record<string, unknown> | undefined =>
27
+ isRecord(value) ? value : undefined;
28
+
29
+ const decodeLiveId = (type: string, id: string) => {
30
+ const decoded = decodeGlobalID(id);
31
+ if (decoded.typename !== type) {
32
+ throw new Error(`Expected '${type}' but received '${decoded.typename}'.`);
33
+ }
34
+ return decoded.id;
35
+ };
36
+
37
+ const asViewType = (type: string): ViewType => {
38
+ if (!(type in viewsByType)) {
39
+ throw new Error(`Unknown Fate live type '${type}'.`);
40
+ }
41
+ return type as ViewType;
42
+ };
43
+
44
+ async function* entityEvents(type: string, id: string) {
45
+ for await (const [event] of live.subscribe(type, id)) {
46
+ yield event;
47
+ }
48
+ }
49
+
50
+ async function* connectionEvents(procedure: string, args: Record<string, unknown> | undefined) {
51
+ for await (const [event] of live.subscribeConnection({ args, procedure })) {
52
+ yield event;
53
+ }
54
+ }
55
+
56
+ const resolveNodePayload = async ({
57
+ args,
58
+ context,
59
+ event,
60
+ select,
61
+ type,
62
+ }: {
63
+ args?: Record<string, unknown>;
64
+ context: AppContext;
65
+ event: LiveSourceEvent;
66
+ select: Array<string>;
67
+ type: string;
68
+ }): Promise<LiveNodePayload> => {
69
+ if (event.type === 'delete') {
70
+ return {
71
+ delete: true,
72
+ id: event.id,
73
+ };
74
+ }
75
+
76
+ const viewType = asViewType(type);
77
+ const eventSelect = event.changed?.filter((path) => select.includes(path));
78
+ const liveSelect = eventSelect?.length ? eventSelect : select;
79
+ const data =
80
+ event.data ??
81
+ (await fate.resolveById({
82
+ ctx: context,
83
+ id: String(event.id),
84
+ input: {
85
+ args,
86
+ select: liveSelect,
87
+ },
88
+ view: viewsByType[viewType] as never,
89
+ }));
90
+
91
+ return {
92
+ data,
93
+ id: event.id,
94
+ select: liveSelect,
95
+ };
96
+ };
97
+
98
+ const resolveConnectionPayload = async ({
99
+ context,
100
+ event,
101
+ select,
102
+ selectionArgs,
103
+ type,
104
+ }: {
105
+ context: AppContext;
106
+ event: LiveConnectionSourceEvent;
107
+ select: Array<string>;
108
+ selectionArgs?: Record<string, unknown>;
109
+ type: string;
110
+ }): Promise<LiveConnectionPayload> => {
111
+ if (event.type === 'invalidate') {
112
+ return { type: event.type };
113
+ }
114
+
115
+ if (event.type === 'deleteEdge') {
116
+ return {
117
+ id: event.id,
118
+ nodeType: event.nodeType ?? type,
119
+ type: event.type,
120
+ };
121
+ }
122
+
123
+ const nodeType = asViewType(event.nodeType ?? type);
124
+ const node =
125
+ event.node ??
126
+ (event.id == null
127
+ ? null
128
+ : await fate.resolveById({
129
+ ctx: context,
130
+ id: String(event.id),
131
+ input: {
132
+ args: selectionArgs,
133
+ select,
134
+ },
135
+ view: viewsByType[nodeType] as never,
136
+ }));
137
+
138
+ return {
139
+ cursor: event.cursor,
140
+ id: event.id,
141
+ node,
142
+ nodeType,
143
+ targetCursor: event.targetCursor,
144
+ type: event.type,
145
+ };
146
+ };
147
+
148
+ const FateLiveNodeEvent = builder.objectRef<LiveNodePayload>('FateLiveNodeEvent').implement({
149
+ fields: (t) => ({
150
+ data: t.field({
151
+ nullable: true,
152
+ resolve: (event) => event.data,
153
+ type: JSONScalar,
154
+ }),
155
+ delete: t.boolean({
156
+ nullable: true,
157
+ resolve: (event) => event.delete,
158
+ }),
159
+ id: t.string({
160
+ nullable: true,
161
+ resolve: (event) => (event.id == null ? null : String(event.id)),
162
+ }),
163
+ select: t.field({
164
+ nullable: true,
165
+ resolve: (event) => event.select,
166
+ type: ['String'],
167
+ }),
168
+ }),
169
+ });
170
+
171
+ const FateLiveConnectionEvent = builder
172
+ .objectRef<LiveConnectionPayload>('FateLiveConnectionEvent')
173
+ .implement({
174
+ fields: (t) => ({
175
+ cursor: t.string({
176
+ nullable: true,
177
+ resolve: (event) => event.cursor,
178
+ }),
179
+ id: t.string({
180
+ nullable: true,
181
+ resolve: (event) => (event.id == null ? null : String(event.id)),
182
+ }),
183
+ node: t.field({
184
+ nullable: true,
185
+ resolve: (event) => event.node,
186
+ type: JSONScalar,
187
+ }),
188
+ nodeType: t.string({
189
+ nullable: true,
190
+ resolve: (event) => event.nodeType,
191
+ }),
192
+ targetCursor: t.string({
193
+ nullable: true,
194
+ resolve: (event) => event.targetCursor,
195
+ }),
196
+ type: t.string({
197
+ nullable: false,
198
+ resolve: (event) => event.type,
199
+ }),
200
+ }),
201
+ });
202
+
203
+ builder.subscriptionFields((t) => ({
204
+ fateLiveConnection: t.field({
205
+ args: {
206
+ args: t.arg({ type: JSONScalar }),
207
+ procedure: t.arg.string({ required: true }),
208
+ select: t.arg({ required: true, type: ['String'] }),
209
+ selectionArgs: t.arg({ type: JSONScalar }),
210
+ type: t.arg.string({ required: true }),
211
+ },
212
+ resolve: (event, { select, selectionArgs, type }, context) =>
213
+ resolveConnectionPayload({
214
+ context,
215
+ event: event as LiveConnectionSourceEvent,
216
+ select,
217
+ selectionArgs: asRecord(selectionArgs),
218
+ type,
219
+ }),
220
+ subscribe: (_root, { args, procedure }) => connectionEvents(procedure, asRecord(args)),
221
+ type: FateLiveConnectionEvent,
222
+ }),
223
+ fateLiveNode: t.field({
224
+ args: {
225
+ args: t.arg({ type: JSONScalar }),
226
+ id: t.arg.id({ required: true }),
227
+ select: t.arg({ required: true, type: ['String'] }),
228
+ type: t.arg.string({ required: true }),
229
+ },
230
+ resolve: (event, { args, select, type }, context) =>
231
+ resolveNodePayload({
232
+ args: asRecord(args),
233
+ context,
234
+ event: event as LiveSourceEvent,
235
+ select,
236
+ type,
237
+ }),
238
+ subscribe: (_root, { id, type }) => entityEvents(type, decodeLiveId(type, String(id))),
239
+ type: FateLiveNodeEvent,
240
+ }),
241
+ }));
@@ -0,0 +1,265 @@
1
+ import { auth } from '../lib/auth.tsx';
2
+ import prisma from '../prisma/prisma.tsx';
3
+ import builder, { JSONScalar } from './builder.tsx';
4
+ import { live } from './fate.ts';
5
+
6
+ const PostAddInput = builder.inputType('PostAddInput', {
7
+ fields: (t) => ({
8
+ args: t.field({ type: JSONScalar }),
9
+ content: t.string({ required: true }),
10
+ title: t.string({ required: true }),
11
+ }),
12
+ });
13
+
14
+ const PostLikeInput = builder.inputType('PostLikeInput', {
15
+ fields: (t) => ({
16
+ args: t.field({ type: JSONScalar }),
17
+ error: t.string(),
18
+ id: t.string({ required: true }),
19
+ slow: t.boolean(),
20
+ }),
21
+ });
22
+
23
+ const PostUnlikeInput = builder.inputType('PostUnlikeInput', {
24
+ fields: (t) => ({
25
+ args: t.field({ type: JSONScalar }),
26
+ id: t.string({ required: true }),
27
+ }),
28
+ });
29
+
30
+ const CommentAddInput = builder.inputType('CommentAddInput', {
31
+ fields: (t) => ({
32
+ args: t.field({ type: JSONScalar }),
33
+ content: t.string({ required: true }),
34
+ postId: t.string({ required: true }),
35
+ }),
36
+ });
37
+
38
+ const CommentDeleteInput = builder.inputType('CommentDeleteInput', {
39
+ fields: (t) => ({
40
+ args: t.field({ type: JSONScalar }),
41
+ id: t.string({ required: true }),
42
+ }),
43
+ });
44
+
45
+ const UserUpdateInput = builder.inputType('UserUpdateInput', {
46
+ fields: (t) => ({
47
+ args: t.field({ type: JSONScalar }),
48
+ name: t.string({ required: true }),
49
+ }),
50
+ });
51
+
52
+ builder.mutationField('postAdd', (t) =>
53
+ t.prismaField({
54
+ args: {
55
+ input: t.arg({ required: true, type: PostAddInput }),
56
+ },
57
+ resolve: async (query, _root, { input }, { sessionUser }) => {
58
+ if (!sessionUser) {
59
+ throw new Error('You must be logged in to add a post.');
60
+ }
61
+
62
+ const post = await prisma.post.create({
63
+ ...query,
64
+ data: {
65
+ authorId: sessionUser.id,
66
+ content: input.content,
67
+ title: input.title,
68
+ },
69
+ });
70
+
71
+ live.connection('posts').prependNode('Post', post.id);
72
+ return post;
73
+ },
74
+ type: 'Post',
75
+ }),
76
+ );
77
+
78
+ builder.mutationField('postLike', (t) =>
79
+ t.prismaField({
80
+ args: {
81
+ input: t.arg({ required: true, type: PostLikeInput }),
82
+ },
83
+ resolve: async (query, _root, { input }) => {
84
+ if (input.slow) {
85
+ await new Promise((resolve) => setTimeout(resolve, 2000));
86
+ }
87
+
88
+ if (input.error === 'boundary') {
89
+ throw new Error('Simulated error.');
90
+ }
91
+
92
+ if (input.error === 'callSite') {
93
+ await new Promise((resolve) => setTimeout(resolve, 200));
94
+ throw new Error('Gotta pay up.');
95
+ }
96
+
97
+ const existing = await prisma.post.findUnique({
98
+ where: { id: input.id },
99
+ });
100
+
101
+ if (!existing) {
102
+ throw new Error('Post not found.');
103
+ }
104
+
105
+ const post = await prisma.post.update({
106
+ ...query,
107
+ data: {
108
+ likes: {
109
+ increment: 1,
110
+ },
111
+ },
112
+ where: { id: input.id },
113
+ });
114
+
115
+ live.update('Post', input.id, { changed: ['likes'] });
116
+ return post;
117
+ },
118
+ type: 'Post',
119
+ }),
120
+ );
121
+
122
+ builder.mutationField('postUnlike', (t) =>
123
+ t.prismaField({
124
+ args: {
125
+ input: t.arg({ required: true, type: PostUnlikeInput }),
126
+ },
127
+ resolve: async (query, _root, { input }) => {
128
+ const post = await prisma.$transaction(async (tx) => {
129
+ const existing = await tx.post.findUnique({
130
+ select: {
131
+ likes: true,
132
+ },
133
+ where: {
134
+ id: input.id,
135
+ },
136
+ });
137
+
138
+ if (!existing) {
139
+ throw new Error('Post not found.');
140
+ }
141
+
142
+ if (existing.likes <= 0) {
143
+ return tx.post.findUniqueOrThrow({
144
+ ...query,
145
+ where: { id: input.id },
146
+ });
147
+ }
148
+
149
+ return tx.post.update({
150
+ ...query,
151
+ data: {
152
+ likes: {
153
+ decrement: 1,
154
+ },
155
+ },
156
+ where: { id: input.id },
157
+ });
158
+ });
159
+
160
+ live.update('Post', input.id, { changed: ['likes'] });
161
+ return post;
162
+ },
163
+ type: 'Post',
164
+ }),
165
+ );
166
+
167
+ builder.mutationField('commentAdd', (t) =>
168
+ t.prismaField({
169
+ args: {
170
+ input: t.arg({ required: true, type: CommentAddInput }),
171
+ },
172
+ resolve: async (query, _root, { input }, { sessionUser }) => {
173
+ if (!sessionUser) {
174
+ throw new Error('You must be logged in to add a comment.');
175
+ }
176
+
177
+ const post = await prisma.post.findUnique({
178
+ where: {
179
+ id: input.postId,
180
+ },
181
+ });
182
+
183
+ if (!post) {
184
+ throw new Error('Post not found.');
185
+ }
186
+
187
+ const comment = await prisma.comment.create({
188
+ ...query,
189
+ data: {
190
+ authorId: sessionUser.id,
191
+ content: input.content,
192
+ postId: input.postId,
193
+ },
194
+ });
195
+
196
+ live.connection('Post.comments', { id: input.postId }).appendNode('Comment', comment.id);
197
+ live.update('Post', input.postId, { changed: ['commentCount', 'comments'] });
198
+
199
+ return comment;
200
+ },
201
+ type: 'Comment',
202
+ }),
203
+ );
204
+
205
+ builder.mutationField('commentDelete', (t) =>
206
+ t.prismaField({
207
+ args: {
208
+ input: t.arg({ required: true, type: CommentDeleteInput }),
209
+ },
210
+ resolve: async (query, _root, { input }) => {
211
+ const comment = await prisma.comment.findUnique({
212
+ select: { authorId: true, postId: true },
213
+ where: { id: input.id },
214
+ });
215
+
216
+ if (!comment) {
217
+ throw new Error('Comment not found.');
218
+ }
219
+
220
+ const result = await prisma.comment.delete({
221
+ ...query,
222
+ where: { id: input.id },
223
+ });
224
+
225
+ live.connection('Post.comments', { id: comment.postId }).deleteEdge('Comment', input.id);
226
+ live.update('Post', comment.postId, { changed: ['commentCount', 'comments'] });
227
+
228
+ return result;
229
+ },
230
+ type: 'Comment',
231
+ }),
232
+ );
233
+
234
+ builder.mutationField('userUpdate', (t) =>
235
+ t.prismaField({
236
+ args: {
237
+ input: t.arg({ required: true, type: UserUpdateInput }),
238
+ },
239
+ resolve: async (query, _root, { input }, { headers, sessionUser }) => {
240
+ if (!sessionUser) {
241
+ throw new Error('You must be logged in to update your name.');
242
+ }
243
+
244
+ const name = input.name.trim();
245
+
246
+ if (name.length < 2 || name.length > 50) {
247
+ throw new Error('Name must be between 2 and 50 characters.');
248
+ }
249
+
250
+ await auth.api.updateUser({
251
+ body: { name },
252
+ headers,
253
+ });
254
+
255
+ const user = await prisma.user.findUniqueOrThrow({
256
+ ...query,
257
+ where: { id: sessionUser.id },
258
+ });
259
+
260
+ live.update('User', sessionUser.id, { changed: ['name'] });
261
+ return user;
262
+ },
263
+ type: 'User',
264
+ }),
265
+ );
@@ -0,0 +1,35 @@
1
+ import prisma from '../../prisma/prisma.tsx';
2
+ import builder from '../builder.tsx';
3
+
4
+ const Category = builder.prismaNode('Category', {
5
+ fields: (t) => ({
6
+ description: t.exposeString('description'),
7
+ name: t.exposeString('name', { nullable: false }),
8
+ postCount: t.int({
9
+ nullable: false,
10
+ resolve: ({ id }) => prisma.post.count({ where: { categoryId: id } }),
11
+ }),
12
+ posts: t.relatedConnection('posts', {
13
+ cursor: 'id',
14
+ nullable: false,
15
+ query: {
16
+ orderBy: [{ createdAt: 'desc' }, { id: 'desc' }],
17
+ },
18
+ }),
19
+ }),
20
+ id: { field: 'id' },
21
+ });
22
+
23
+ builder.queryFields((t) => ({
24
+ categories: t.prismaConnection({
25
+ cursor: 'id',
26
+ resolve: (query) =>
27
+ prisma.category.findMany({
28
+ ...query,
29
+ orderBy: [{ createdAt: 'asc' }, { id: 'asc' }],
30
+ }),
31
+ type: 'Category',
32
+ }),
33
+ }));
34
+
35
+ export default Category;
@@ -0,0 +1,46 @@
1
+ import prisma from '../../prisma/prisma.tsx';
2
+ import builder from '../builder.tsx';
3
+
4
+ const isDevelopment = process.env.NODE_ENV === 'development' || Boolean(process.env.DEV);
5
+
6
+ const Comment = builder.prismaNode('Comment', {
7
+ fields: (t) => ({
8
+ author: t.relation('author'),
9
+ content: t.exposeString('content', { nullable: false }),
10
+ post: t.relation('post', { nullable: false }),
11
+ }),
12
+ id: { field: 'id' },
13
+ });
14
+
15
+ builder.queryFields((t) => ({
16
+ commentSearch: t.prismaConnection({
17
+ args: {
18
+ query: t.arg.string({ required: true }),
19
+ },
20
+ cursor: 'id',
21
+ resolve: async (query, _, { query: searchQuery }) => {
22
+ const value = searchQuery.trim();
23
+ if (!value.length) {
24
+ return [];
25
+ }
26
+
27
+ if (isDevelopment && value.length > 1) {
28
+ await new Promise((resolve) => setTimeout(resolve, 500));
29
+ }
30
+
31
+ return prisma.comment.findMany({
32
+ ...query,
33
+ orderBy: [{ createdAt: 'desc' }, { id: 'desc' }],
34
+ where: {
35
+ content: {
36
+ contains: value,
37
+ mode: 'insensitive',
38
+ },
39
+ },
40
+ });
41
+ },
42
+ type: 'Comment',
43
+ }),
44
+ }));
45
+
46
+ export default Comment;