create-fate 1.5.2 → 1.6.1

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 (142) hide show
  1. package/README.md +3 -10
  2. package/bin/create-fate.mjs +16 -52
  3. package/package.json +1 -1
  4. package/templates/fate/README.md +1 -2
  5. package/templates/fate/_shared/vue-app/package.json +4 -3
  6. package/templates/fate/_shared/vue-app/vite.config.ts +18 -11
  7. package/templates/fate/drizzle/README.md +1 -1
  8. package/templates/fate/drizzle/client/package.json +6 -6
  9. package/templates/fate/graphql/README.md +1 -1
  10. package/templates/fate/graphql/client/package.json +6 -6
  11. package/templates/fate/graphql/server/.gitignore +5 -0
  12. package/templates/fate/graphql/server/node_modules/.bin/esbuild +22 -0
  13. package/templates/fate/graphql/server/node_modules/.bin/eslint +22 -0
  14. package/templates/fate/graphql/server/node_modules/.bin/fate +22 -0
  15. package/templates/fate/graphql/server/node_modules/.bin/intent +22 -0
  16. package/templates/fate/graphql/server/node_modules/.bin/jiti +22 -0
  17. package/templates/fate/graphql/server/node_modules/.bin/nft +22 -0
  18. package/templates/fate/graphql/server/node_modules/.bin/nodemon +22 -0
  19. package/templates/fate/graphql/server/node_modules/.bin/oxfmt +22 -0
  20. package/templates/fate/graphql/server/node_modules/.bin/oxlint +22 -0
  21. package/templates/fate/graphql/server/node_modules/.bin/prisma +22 -0
  22. package/templates/fate/graphql/server/node_modules/.bin/prisma-json-types-generator +22 -0
  23. package/templates/fate/graphql/server/node_modules/.bin/prisma-pothos-types +22 -0
  24. package/templates/fate/graphql/server/node_modules/.bin/tsc +22 -0
  25. package/templates/fate/graphql/server/node_modules/.bin/tsgolint +22 -0
  26. package/templates/fate/graphql/server/node_modules/.bin/tsx +22 -0
  27. package/templates/fate/graphql/server/node_modules/.bin/vitest +22 -0
  28. package/templates/fate/graphql/server/node_modules/.bin/vp +22 -0
  29. package/templates/fate/graphql/server/node_modules/.bin/vpr +22 -0
  30. package/templates/fate/graphql/server/node_modules/.bin/yaml +22 -0
  31. package/templates/fate/graphql-client/README.md +12 -2
  32. package/templates/fate/graphql-client/env.ts +6 -0
  33. package/templates/fate/graphql-client/package.json +6 -6
  34. package/templates/fate/http/README.md +1 -1
  35. package/templates/fate/http/client/package.json +6 -6
  36. package/templates/fate/prisma/README.md +1 -1
  37. package/templates/fate/prisma/client/package.json +6 -6
  38. package/templates/fate/void/README.md +13 -1
  39. package/templates/fate/void/db/migrations/20260918045815_legal_dragon_lord.sql +15 -0
  40. package/templates/fate/{cloudflare/server/db/migrations/meta/20260508073357_snapshot.json → void/db/migrations/meta/20260918045815_snapshot.json} +174 -124
  41. package/templates/fate/void/db/migrations/meta/_journal.json +7 -0
  42. package/templates/fate/void/package.json +6 -7
  43. package/templates/fate/cloudflare/.github/workflows/push.yml +0 -27
  44. package/templates/fate/cloudflare/.vite-hooks/pre-commit +0 -1
  45. package/templates/fate/cloudflare/.vscode/extensions.json +0 -12
  46. package/templates/fate/cloudflare/.vscode/settings.json +0 -25
  47. package/templates/fate/cloudflare/AGENTS.md +0 -55
  48. package/templates/fate/cloudflare/README.md +0 -137
  49. package/templates/fate/cloudflare/_gitignore +0 -20
  50. package/templates/fate/cloudflare/client/node_modules/.bin/browserslist +0 -22
  51. package/templates/fate/cloudflare/client/node_modules/.bin/cf-wrangler +0 -22
  52. package/templates/fate/cloudflare/client/node_modules/.bin/drizzle-kit +0 -22
  53. package/templates/fate/cloudflare/client/node_modules/.bin/esbuild +0 -22
  54. package/templates/fate/cloudflare/client/node_modules/.bin/fate +0 -22
  55. package/templates/fate/cloudflare/client/node_modules/.bin/fbtee +0 -22
  56. package/templates/fate/cloudflare/client/node_modules/.bin/intent +0 -22
  57. package/templates/fate/cloudflare/client/node_modules/.bin/jiti +0 -22
  58. package/templates/fate/cloudflare/client/node_modules/.bin/oxfmt +0 -22
  59. package/templates/fate/cloudflare/client/node_modules/.bin/oxlint +0 -22
  60. package/templates/fate/cloudflare/client/node_modules/.bin/rolldown +0 -22
  61. package/templates/fate/cloudflare/client/node_modules/.bin/tsc +0 -22
  62. package/templates/fate/cloudflare/client/node_modules/.bin/tsx +0 -22
  63. package/templates/fate/cloudflare/client/node_modules/.bin/vitest +0 -22
  64. package/templates/fate/cloudflare/client/node_modules/.bin/void +0 -22
  65. package/templates/fate/cloudflare/client/node_modules/.bin/vp +0 -22
  66. package/templates/fate/cloudflare/client/node_modules/.bin/vpr +0 -22
  67. package/templates/fate/cloudflare/client/node_modules/.bin/workerd +0 -22
  68. package/templates/fate/cloudflare/client/node_modules/.bin/wrangler +0 -22
  69. package/templates/fate/cloudflare/client/node_modules/.bin/wrangler2 +0 -22
  70. package/templates/fate/cloudflare/client/package.json +0 -62
  71. package/templates/fate/cloudflare/client/pages/index.tsx +0 -96
  72. package/templates/fate/cloudflare/client/pages/layout.tsx +0 -68
  73. package/templates/fate/cloudflare/client/pages/login.tsx +0 -107
  74. package/templates/fate/cloudflare/client/pages/post/[id].tsx +0 -22
  75. package/templates/fate/cloudflare/client/pages/search.tsx +0 -99
  76. package/templates/fate/cloudflare/client/public/icon.svg +0 -1
  77. package/templates/fate/cloudflare/client/src/App.css +0 -153
  78. package/templates/fate/cloudflare/client/src/lib/AvailableLanguages.tsx +0 -5
  79. package/templates/fate/cloudflare/client/src/lib/LocaleContext.tsx +0 -61
  80. package/templates/fate/cloudflare/client/src/lib/cx.tsx +0 -6
  81. package/templates/fate/cloudflare/client/src/lib/env.tsx +0 -5
  82. package/templates/fate/cloudflare/client/src/ui/Badge.tsx +0 -31
  83. package/templates/fate/cloudflare/client/src/ui/Button.tsx +0 -108
  84. package/templates/fate/cloudflare/client/src/ui/Card.tsx +0 -32
  85. package/templates/fate/cloudflare/client/src/ui/CommentCard.tsx +0 -75
  86. package/templates/fate/cloudflare/client/src/ui/Error.tsx +0 -19
  87. package/templates/fate/cloudflare/client/src/ui/H2.tsx +0 -15
  88. package/templates/fate/cloudflare/client/src/ui/H3.tsx +0 -20
  89. package/templates/fate/cloudflare/client/src/ui/Header.tsx +0 -98
  90. package/templates/fate/cloudflare/client/src/ui/Input.tsx +0 -17
  91. package/templates/fate/cloudflare/client/src/ui/Link.tsx +0 -11
  92. package/templates/fate/cloudflare/client/src/ui/LocaleSwitcher.tsx +0 -34
  93. package/templates/fate/cloudflare/client/src/ui/PostCard.tsx +0 -303
  94. package/templates/fate/cloudflare/client/src/ui/Section.tsx +0 -23
  95. package/templates/fate/cloudflare/client/src/ui/UserCard.tsx +0 -131
  96. package/templates/fate/cloudflare/client/src/user/AuthClient.tsx +0 -6
  97. package/templates/fate/cloudflare/client/start.mjs +0 -11
  98. package/templates/fate/cloudflare/client/translations/de-DE.json +0 -632
  99. package/templates/fate/cloudflare/client/translations/ja-JP.json +0 -632
  100. package/templates/fate/cloudflare/client/vite.config.ts +0 -41
  101. package/templates/fate/cloudflare/client/void.json +0 -29
  102. package/templates/fate/cloudflare/package.json +0 -42
  103. package/templates/fate/cloudflare/pnpm-workspace.yaml +0 -41
  104. package/templates/fate/cloudflare/server/.env.example +0 -1
  105. package/templates/fate/cloudflare/server/db/db.ts +0 -30
  106. package/templates/fate/cloudflare/server/db/migrations/20260508073357_initial_schema.sql +0 -134
  107. package/templates/fate/cloudflare/server/db/migrations/20260508120500_seed_cloudflare_demo.sql +0 -170
  108. package/templates/fate/cloudflare/server/db/migrations/meta/_journal.json +0 -20
  109. package/templates/fate/cloudflare/server/db/queries.ts +0 -211
  110. package/templates/fate/cloudflare/server/db/schema.ts +0 -331
  111. package/templates/fate/cloudflare/server/node_modules/.bin/cf-wrangler +0 -22
  112. package/templates/fate/cloudflare/server/node_modules/.bin/drizzle-kit +0 -22
  113. package/templates/fate/cloudflare/server/node_modules/.bin/esbuild +0 -22
  114. package/templates/fate/cloudflare/server/node_modules/.bin/fate +0 -22
  115. package/templates/fate/cloudflare/server/node_modules/.bin/intent +0 -22
  116. package/templates/fate/cloudflare/server/node_modules/.bin/jiti +0 -22
  117. package/templates/fate/cloudflare/server/node_modules/.bin/oxfmt +0 -22
  118. package/templates/fate/cloudflare/server/node_modules/.bin/oxlint +0 -22
  119. package/templates/fate/cloudflare/server/node_modules/.bin/tsc +0 -22
  120. package/templates/fate/cloudflare/server/node_modules/.bin/tsx +0 -22
  121. package/templates/fate/cloudflare/server/node_modules/.bin/vitest +0 -22
  122. package/templates/fate/cloudflare/server/node_modules/.bin/vp +0 -22
  123. package/templates/fate/cloudflare/server/node_modules/.bin/vpr +0 -22
  124. package/templates/fate/cloudflare/server/node_modules/.bin/workerd +0 -22
  125. package/templates/fate/cloudflare/server/node_modules/.bin/wrangler +0 -22
  126. package/templates/fate/cloudflare/server/node_modules/.bin/wrangler2 +0 -22
  127. package/templates/fate/cloudflare/server/package.json +0 -34
  128. package/templates/fate/cloudflare/server/src/fate/commentPermissions.ts +0 -17
  129. package/templates/fate/cloudflare/server/src/fate/context.ts +0 -15
  130. package/templates/fate/cloudflare/server/src/fate/live.ts +0 -7
  131. package/templates/fate/cloudflare/server/src/fate/server.ts +0 -414
  132. package/templates/fate/cloudflare/server/src/fate/views.ts +0 -164
  133. package/templates/fate/cloudflare/server/src/index.ts +0 -80
  134. package/templates/fate/cloudflare/server/src/lib/auth.ts +0 -64
  135. package/templates/fate/cloudflare/server/src/lib/origins.ts +0 -17
  136. package/templates/fate/cloudflare/server/src/router.ts +0 -2
  137. package/templates/fate/cloudflare/server/src/user/SessionUser.tsx +0 -17
  138. package/templates/fate/cloudflare/server/tsconfig.json +0 -10
  139. package/templates/fate/cloudflare/server/wrangler.jsonc +0 -29
  140. package/templates/fate/cloudflare/tsconfig.json +0 -24
  141. package/templates/fate/cloudflare/vite.config.ts +0 -75
  142. package/templates/fate/graphql-client/.env.example +0 -3
@@ -1,414 +0,0 @@
1
- import {
2
- createFateServer,
3
- createSourcePlan,
4
- FateRequestError,
5
- getNestedSelection,
6
- getScopedArgs,
7
- hasNestedSelection,
8
- } from '@nkzw/fate/server';
9
- import { createDrizzleSourceAdapter, type DrizzleQueryExtra } from '@nkzw/fate/server/drizzle';
10
- import { createCloudflareFateLive } from 'cf-fate/server';
11
- import { z } from 'zod';
12
- import { db, eq, like } from '../../db/db.ts';
13
- import {
14
- type CommentItem,
15
- type PostItem,
16
- createCommentRecord,
17
- createPostRecord,
18
- deleteCommentRecord,
19
- likePostRecord,
20
- unlikePostRecord,
21
- } from '../../db/queries.ts';
22
- import schema, { comment, user as userTable } from '../../db/schema.ts';
23
- import { assertCanDeleteComment, getCommentDeleteFetchSelection } from './commentPermissions.ts';
24
- import { createContext, type AppContext } from './context.ts';
25
- import { commentDataView, postDataView, Root, userDataView, type Post } from './views.ts';
26
-
27
- type ResolverOptions<Input> = {
28
- ctx: AppContext;
29
- input: Input;
30
- select: Array<string>;
31
- };
32
-
33
- type ListResolverOptions = ResolverOptions<{
34
- args?: Record<string, unknown>;
35
- }>;
36
-
37
- const source = createDrizzleSourceAdapter<AppContext>({
38
- db,
39
- schema,
40
- views: Root,
41
- });
42
- const isDevelopment = Boolean(
43
- (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env?.DEV,
44
- );
45
- export const fateLive = createCloudflareFateLive();
46
- export const { live } = fateLive;
47
-
48
- type CloudflareAdapterContext = {
49
- env?: {
50
- BETTER_AUTH_SECRET?: string;
51
- };
52
- };
53
-
54
- const requireUser = (ctx: AppContext) => {
55
- if (!ctx.sessionUser) {
56
- throw new FateRequestError('UNAUTHORIZED', 'You must be logged in.');
57
- }
58
-
59
- return ctx.sessionUser;
60
- };
61
-
62
- export const fateServer = createFateServer({
63
- context: ({ adapterContext, request }) =>
64
- createContext({
65
- request,
66
- secret: (adapterContext as CloudflareAdapterContext | undefined)?.env?.BETTER_AUTH_SECRET,
67
- }),
68
- lists: {
69
- commentSearch: {
70
- resolve: async ({ ctx, input, select }: ListResolverOptions) => {
71
- const query = input.args?.query;
72
- if (typeof query !== 'string' || !query.trim().length) {
73
- return {
74
- items: [],
75
- pagination: { hasNext: false, hasPrevious: false },
76
- };
77
- }
78
-
79
- if (isDevelopment && query.trim().length > 1) {
80
- await new Promise((resolve) => setTimeout(resolve, 500));
81
- }
82
-
83
- const first = typeof input.args?.first === 'number' ? input.args.first : 20;
84
- const cursor = typeof input.args?.after === 'string' ? input.args.after : undefined;
85
- const nodes = await source.resolveConnection({
86
- ctx,
87
- cursor,
88
- direction: 'forward',
89
- extra: {
90
- where: like(
91
- comment.content,
92
- `%${query.trim()}%`,
93
- ) as unknown as DrizzleQueryExtra['where'],
94
- },
95
- input: { args: input.args, select },
96
- skip: cursor ? 1 : undefined,
97
- take: first + 1,
98
- view: commentDataView,
99
- });
100
- const hasNext = nodes.length > first;
101
- const items = nodes.slice(0, first).map((node) => ({
102
- cursor: String(node.id),
103
- node,
104
- }));
105
-
106
- return {
107
- items,
108
- pagination: {
109
- hasNext,
110
- hasPrevious: Boolean(cursor),
111
- nextCursor: items.at(-1)?.cursor,
112
- previousCursor: cursor ? items[0]?.cursor : undefined,
113
- },
114
- };
115
- },
116
- type: 'Comment',
117
- },
118
- },
119
- live,
120
- mutations: {
121
- 'comment.add': {
122
- input: z.object({
123
- content: z.string().min(1, 'Content is required'),
124
- postId: z.string().min(1, 'Post id is required'),
125
- }),
126
- resolve: async ({
127
- ctx,
128
- input,
129
- select,
130
- }: ResolverOptions<{ content: string; postId: string }>) => {
131
- const user = requireUser(ctx);
132
- const post = await source.resolveById({
133
- ctx,
134
- id: input.postId,
135
- input: { select: ['id'] },
136
- view: postDataView,
137
- });
138
-
139
- if (!post) {
140
- throw new Error('Post not found.');
141
- }
142
-
143
- const commentId = await createCommentRecord({
144
- authorId: user.id,
145
- content: input.content,
146
- postId: input.postId,
147
- });
148
-
149
- if (!commentId) {
150
- throw new Error('Failed to create comment.');
151
- }
152
-
153
- const created = (await source.resolveById({
154
- ctx,
155
- id: commentId,
156
- input: {
157
- select: select.filter((field) => field !== 'post' && !field.startsWith('post.')),
158
- },
159
- view: commentDataView,
160
- })) as (CommentItem & { post?: PostItem | null }) | null;
161
-
162
- if (!created) {
163
- throw new Error('Comment not found.');
164
- }
165
-
166
- const liveComment = await source.resolveById({
167
- ctx,
168
- id: commentId,
169
- input: {
170
- select: ['author.id', 'author.name', 'author.username', 'content', 'id'],
171
- },
172
- view: commentDataView,
173
- });
174
-
175
- if (hasNestedSelection(select, 'post')) {
176
- created.post = (await source.resolveById<PostItem>({
177
- ctx,
178
- id: input.postId,
179
- input: {
180
- args: getScopedArgs(undefined, 'post'),
181
- select: getNestedSelection(select, 'post'),
182
- },
183
- view: postDataView,
184
- })) as PostItem | null;
185
- }
186
-
187
- live
188
- .connection('Post.comments', { id: input.postId })
189
- .appendNode('Comment', commentId, { node: liveComment ?? created });
190
- live.update('Post', input.postId, { changed: ['commentCount', 'comments'] });
191
-
192
- return created as CommentItem & { post?: { commentCount: number } };
193
- },
194
- type: 'Comment',
195
- },
196
- 'comment.delete': {
197
- input: z.object({
198
- id: z.string().min(1, 'Comment id is required'),
199
- }),
200
- resolve: async ({ ctx, input, select }: ResolverOptions<{ id: string }>) => {
201
- const plan = createSourcePlan({
202
- ctx,
203
- select,
204
- source: source.getSource(commentDataView),
205
- });
206
- const commentSelection = getCommentDeleteFetchSelection(select);
207
-
208
- const existing = await source.fetchById<CommentItem>({
209
- ctx,
210
- extra: { extraFields: ['authorId', 'postId'] },
211
- id: input.id,
212
- plan: createSourcePlan({
213
- ctx,
214
- select: commentSelection,
215
- source: source.getSource(commentDataView),
216
- }),
217
- });
218
-
219
- if (!existing) {
220
- throw new FateRequestError('NOT_FOUND', 'Comment not found.');
221
- }
222
-
223
- assertCanDeleteComment(existing, ctx.sessionUser);
224
-
225
- await deleteCommentRecord(input.id);
226
-
227
- if (existing.postId && hasNestedSelection(select, 'post')) {
228
- const post = await source.fetchById<PostItem>({
229
- ctx,
230
- id: existing.postId,
231
- plan: createSourcePlan({
232
- args: getScopedArgs(undefined, 'post'),
233
- ctx,
234
- select: getNestedSelection(select, 'post'),
235
- source: source.getSource(postDataView),
236
- }),
237
- });
238
-
239
- existing.post = post;
240
- }
241
-
242
- const resolved = (await plan.resolve(existing)) as CommentItem & {
243
- post?: { commentCount: number };
244
- };
245
-
246
- if (existing.postId) {
247
- live.connection('Post.comments', { id: existing.postId }).deleteEdge('Comment', input.id);
248
- live.update('Post', existing.postId, { changed: ['commentCount', 'comments'] });
249
- }
250
-
251
- return resolved;
252
- },
253
- type: 'Comment',
254
- },
255
- 'post.add': {
256
- input: z.object({
257
- content: z.string().min(1, 'Content is required'),
258
- title: z.string().min(1, 'Title is required'),
259
- }),
260
- resolve: async ({
261
- ctx,
262
- input,
263
- select,
264
- }: ResolverOptions<{ content: string; title: string }>) => {
265
- const user = requireUser(ctx);
266
- const postId = await createPostRecord({
267
- authorId: user.id,
268
- content: input.content,
269
- title: input.title,
270
- });
271
-
272
- if (!postId) {
273
- throw new Error('Failed to create post.');
274
- }
275
-
276
- const post = await source.resolveById({
277
- ctx,
278
- id: postId,
279
- input: { select },
280
- view: postDataView,
281
- });
282
-
283
- if (!post) {
284
- throw new Error('Post not found.');
285
- }
286
-
287
- live.connection('posts').prependNode('Post', postId);
288
-
289
- return post as Post;
290
- },
291
- type: 'Post',
292
- },
293
- 'post.like': {
294
- input: z.object({
295
- error: z.enum(['boundary', 'callSite']).optional(),
296
- id: z.string().min(1, 'Post id is required.'),
297
- slow: z.boolean().optional(),
298
- }),
299
- resolve: async ({
300
- ctx,
301
- input,
302
- select,
303
- }: ResolverOptions<{ error?: 'boundary' | 'callSite'; id: string; slow?: boolean }>) => {
304
- if (input.slow) {
305
- await new Promise((resolve) => setTimeout(resolve, 2000));
306
- }
307
-
308
- if (input.error === 'boundary') {
309
- throw new Error('Simulated error.');
310
- } else if (input.error === 'callSite') {
311
- await new Promise((resolve) => setTimeout(resolve, 200));
312
- throw new Error('Gotta pay up.');
313
- }
314
-
315
- const updated = await likePostRecord(input.id);
316
- if (!updated) {
317
- throw new Error('Post not found.');
318
- }
319
-
320
- const post = await source.resolveById({
321
- ctx,
322
- id: input.id,
323
- input: { select },
324
- view: postDataView,
325
- });
326
-
327
- if (!post) {
328
- throw new Error('Post not found.');
329
- }
330
-
331
- live.update('Post', input.id, { changed: ['likes'], data: post });
332
-
333
- return post as Post;
334
- },
335
- type: 'Post',
336
- },
337
- 'post.unlike': {
338
- input: z.object({
339
- id: z.string().min(1, 'Post id is required.'),
340
- }),
341
- resolve: async ({ ctx, input, select }: ResolverOptions<{ id: string }>) => {
342
- const updated = await unlikePostRecord(input.id);
343
- if (!updated) {
344
- throw new Error('Post not found.');
345
- }
346
-
347
- const post = await source.resolveById({
348
- ctx,
349
- id: input.id,
350
- input: { select },
351
- view: postDataView,
352
- });
353
-
354
- if (!post) {
355
- throw new Error('Post not found.');
356
- }
357
-
358
- live.update('Post', input.id, { changed: ['likes'], data: post });
359
-
360
- return post as Post;
361
- },
362
- type: 'Post',
363
- },
364
- 'user.update': {
365
- input: z.object({
366
- name: z
367
- .string()
368
- .trim()
369
- .min(2, 'Name must be at least 2 characters.')
370
- .max(50, 'Name must be at most 32 characters.'),
371
- }),
372
- resolve: async ({ ctx, input, select }: ResolverOptions<{ name: string }>) => {
373
- const user = requireUser(ctx);
374
-
375
- await db
376
- .update(userTable)
377
- .set({ name: input.name, updatedAt: new Date() })
378
- .where(eq(userTable.id, user.id));
379
-
380
- const updated = await source.resolveById({
381
- ctx,
382
- id: user.id,
383
- input: { select },
384
- view: userDataView,
385
- });
386
-
387
- if (!updated) {
388
- throw new Error('User not found.');
389
- }
390
-
391
- return updated;
392
- },
393
- type: 'User',
394
- },
395
- },
396
- queries: {
397
- viewer: {
398
- resolve: ({ ctx, select }: Omit<ResolverOptions<unknown>, 'input'>) =>
399
- ctx.sessionUser
400
- ? source.resolveById({
401
- ctx,
402
- id: ctx.sessionUser.id,
403
- input: { select },
404
- view: Root.viewer,
405
- })
406
- : null,
407
- type: 'User',
408
- },
409
- },
410
- roots: Root,
411
- sources: source,
412
- });
413
-
414
- export * from './views.ts';
@@ -1,164 +0,0 @@
1
- import { computed, count, dataView, field, list, type Entity } from '@nkzw/fate/server';
2
- import type {
3
- CategoryItem,
4
- CommentItem,
5
- EventAttendeeItem,
6
- EventItem,
7
- PostItem,
8
- } from '../../db/queries.ts';
9
- import type { CategoryRow, TagRow, UserRow } from '../../db/schema.ts';
10
- import type { AppContext } from './context.ts';
11
-
12
- export const userDataView = dataView<UserRow>('User')({
13
- email: computed<UserRow, string | null, AppContext>({
14
- authorize: ({ id }, context) => context?.sessionUser?.id === id,
15
- resolve: (_item, deps) => (deps.email as string | null) ?? null,
16
- select: {
17
- email: field('email'),
18
- },
19
- }),
20
- id: true,
21
- name: true,
22
- username: true,
23
- });
24
-
25
- export const tagDataView = dataView<TagRow>('Tag')({
26
- description: true,
27
- id: true,
28
- name: true,
29
- });
30
-
31
- export const categorySummaryDataView = dataView<CategoryRow>('Category')({
32
- id: true,
33
- name: true,
34
- });
35
-
36
- const basePost = {
37
- author: userDataView,
38
- category: categorySummaryDataView,
39
- commentCount: computed<PostItem, number>({
40
- resolve: (_item, deps) => (deps.count as number) ?? 0,
41
- select: {
42
- count: count('comments'),
43
- },
44
- }),
45
- content: true,
46
- id: true,
47
- likes: true,
48
- title: true,
49
- } as const;
50
-
51
- export const postSummaryDataView = dataView<PostItem>('Post')({
52
- ...basePost,
53
- tags: list(tagDataView),
54
- });
55
-
56
- export const commentDataView = dataView<CommentItem>('Comment')({
57
- author: userDataView,
58
- content: true,
59
- id: true,
60
- post: postSummaryDataView,
61
- });
62
-
63
- export const postDataView = dataView<PostItem>('Post')({
64
- ...basePost,
65
- comments: list(commentDataView, { orderBy: { createdAt: 'asc', id: 'asc' } }),
66
- tags: list(tagDataView),
67
- });
68
-
69
- export const categoryDataView = dataView<CategoryItem>('Category')({
70
- description: true,
71
- id: true,
72
- name: true,
73
- postCount: computed<CategoryItem, number>({
74
- resolve: (_item, deps) => (deps.count as number) ?? 0,
75
- select: {
76
- count: count('posts'),
77
- },
78
- }),
79
- posts: list(postDataView),
80
- });
81
-
82
- export const eventAttendeeDataView = dataView<EventAttendeeItem>('EventAttendee')({
83
- id: true,
84
- notes: true,
85
- status: true,
86
- user: userDataView,
87
- });
88
-
89
- export const eventDataView = dataView<EventItem>('Event')({
90
- attendees: list(eventAttendeeDataView, { orderBy: { createdAt: 'asc', id: 'asc' } }),
91
- attendingCount: computed<EventItem, number>({
92
- resolve: (_item, deps) => (deps.count as number) ?? 0,
93
- select: {
94
- count: count('attendees', {
95
- where: { status: 'GOING' },
96
- }),
97
- },
98
- }),
99
- capacity: true,
100
- description: true,
101
- endAt: true,
102
- host: userDataView,
103
- id: true,
104
- livestreamUrl: true,
105
- location: true,
106
- name: true,
107
- startAt: true,
108
- topics: true,
109
- type: true,
110
- });
111
-
112
- export type User = Entity<typeof userDataView, 'User'>;
113
- export type Tag = Entity<typeof tagDataView, 'Tag'>;
114
- export type Comment = Entity<
115
- typeof commentDataView,
116
- 'Comment',
117
- {
118
- author: User;
119
- post: Post;
120
- }
121
- >;
122
- export type EventAttendee = Entity<
123
- typeof eventAttendeeDataView,
124
- 'EventAttendee',
125
- {
126
- user: User;
127
- }
128
- >;
129
- export type Post = Entity<
130
- typeof postDataView,
131
- 'Post',
132
- {
133
- author: User;
134
- category: Category | null;
135
- comments: Array<Comment>;
136
- tags: Array<Tag>;
137
- }
138
- >;
139
- export type Category = Entity<
140
- typeof categoryDataView,
141
- 'Category',
142
- {
143
- posts: Array<Post>;
144
- }
145
- >;
146
- export type Event = Entity<
147
- typeof eventDataView,
148
- 'Event',
149
- {
150
- attendees: Array<EventAttendee>;
151
- host: User;
152
- }
153
- >;
154
-
155
- export const Root = {
156
- categories: list(categoryDataView, { orderBy: { createdAt: 'asc', id: 'asc' } }),
157
- commentSearch: {
158
- procedure: 'search',
159
- view: list(commentDataView, { orderBy: { createdAt: 'desc', id: 'desc' } }),
160
- },
161
- events: list(eventDataView, { orderBy: [{ startAt: 'asc' }, { id: 'asc' }] }),
162
- posts: list(postDataView, { orderBy: { createdAt: 'desc', id: 'desc' } }),
163
- viewer: userDataView,
164
- };
@@ -1,80 +0,0 @@
1
- import {
2
- createCloudflareFateLiveDurableObject,
3
- defineCloudflareFateLiveRoute,
4
- defineCloudflareFateRoute,
5
- } from 'cf-fate/server';
6
- import { withDatabase, type D1Binding } from '../db/db.ts';
7
- import { fateStream } from './fate/live.ts';
8
- import { fateLive, fateServer } from './fate/server.ts';
9
- import { createAuth } from './lib/auth.ts';
10
- import { clientOrigin, resolveCorsOrigin } from './lib/origins.ts';
11
-
12
- type ExecutionContext = {
13
- waitUntil(promise: Promise<unknown>): void;
14
- };
15
-
16
- type Env = Record<string, unknown> & {
17
- BETTER_AUTH_SECRET?: string;
18
- DB: D1Binding;
19
- FATE_LIVE: unknown;
20
- };
21
-
22
- const fateRoute = defineCloudflareFateRoute(fateServer, fateLive, { stream: fateStream });
23
- const fateLiveRoute = defineCloudflareFateLiveRoute(fateStream);
24
-
25
- export const FateLiveDurableObject = createCloudflareFateLiveDurableObject({
26
- binding: 'FATE_LIVE',
27
- });
28
-
29
- const corsHeaders = (request: Request): Headers => {
30
- const origin = resolveCorsOrigin(request.headers.get('origin'));
31
- const headers = new Headers();
32
- if (origin) {
33
- headers.set('access-control-allow-origin', origin);
34
- headers.set('access-control-allow-credentials', 'true');
35
- headers.set('vary', 'Origin');
36
- }
37
- return headers;
38
- };
39
-
40
- const withCors = (request: Request, response: Response): Response => {
41
- const headers = new Headers(response.headers);
42
- corsHeaders(request).forEach((value, key) => headers.set(key, value));
43
- return new Response(response.body, {
44
- headers,
45
- status: response.status,
46
- statusText: response.statusText,
47
- });
48
- };
49
-
50
- const optionsResponse = (request: Request) => {
51
- const headers = corsHeaders(request);
52
- headers.set('access-control-allow-headers', 'content-type, authorization');
53
- headers.set('access-control-allow-methods', 'GET, POST, OPTIONS');
54
- headers.set('access-control-max-age', '86400');
55
- return new Response(null, { headers, status: 204 });
56
- };
57
-
58
- export default {
59
- fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
60
- return withDatabase(env.DB, async () => {
61
- if (request.method === 'OPTIONS') {
62
- return optionsResponse(request);
63
- }
64
-
65
- const url = new URL(request.url);
66
- let response: Response;
67
- if (url.pathname === '/fate' || url.pathname.startsWith('/fate/')) {
68
- response = await fateRoute.fetch(request, env, ctx);
69
- } else if (url.pathname === '/fate-live' || url.pathname.startsWith('/fate-live/')) {
70
- response = await fateLiveRoute.fetch(request, env, ctx);
71
- } else if (url.pathname.startsWith('/api/auth/')) {
72
- response = await createAuth(url.origin, env.BETTER_AUTH_SECRET).handler(request);
73
- } else {
74
- response = Response.redirect(clientOrigin, 302);
75
- }
76
-
77
- return withCors(request, response);
78
- });
79
- },
80
- };