create-fate 1.0.4 → 1.3.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 (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 +41 -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,20 @@
1
+ {
2
+ "version": "7",
3
+ "dialect": "sqlite",
4
+ "entries": [
5
+ {
6
+ "idx": 0,
7
+ "version": "6",
8
+ "when": 1778225637900,
9
+ "tag": "20260508073357_initial_schema",
10
+ "breakpoints": true
11
+ },
12
+ {
13
+ "idx": 1,
14
+ "version": "6",
15
+ "when": 1778232300000,
16
+ "tag": "20260508120500_seed_cloudflare_demo",
17
+ "breakpoints": false
18
+ }
19
+ ]
20
+ }
@@ -0,0 +1,211 @@
1
+ import { and, db as runtimeDb, eq, gt, sql } from './db.ts';
2
+ import {
3
+ comment,
4
+ event,
5
+ eventAttendee,
6
+ post,
7
+ postToTag,
8
+ type CategoryRow,
9
+ type CommentRow,
10
+ type EventAttendeeRow,
11
+ type EventRow,
12
+ type PostRow,
13
+ type TagRow,
14
+ type UserRow,
15
+ } from './schema.ts';
16
+
17
+ type ItemRecord = Record<string, unknown>;
18
+ type InsertDatabase = Pick<typeof runtimeDb, 'insert'>;
19
+
20
+ export type CommentItem = CommentRow &
21
+ ItemRecord & {
22
+ author?: UserRow | null;
23
+ post?: PostItem | null;
24
+ };
25
+
26
+ export type PostItem = PostRow &
27
+ ItemRecord & {
28
+ author?: UserRow | null;
29
+ category?: CategoryRow | null;
30
+ comments?: Array<CommentItem>;
31
+ tags?: Array<TagRow>;
32
+ };
33
+
34
+ export type CategoryItem = CategoryRow &
35
+ ItemRecord & {
36
+ posts?: Array<PostItem>;
37
+ };
38
+
39
+ export type EventAttendeeItem = EventAttendeeRow &
40
+ ItemRecord & {
41
+ user?: UserRow | null;
42
+ };
43
+
44
+ export type EventItem = EventRow &
45
+ ItemRecord & {
46
+ attendees?: Array<EventAttendeeItem>;
47
+ host?: UserRow | null;
48
+ };
49
+
50
+ export const createPostRecord = async (
51
+ {
52
+ authorId,
53
+ categoryId,
54
+ content,
55
+ likes = 0,
56
+ tagIds = [],
57
+ title,
58
+ }: {
59
+ authorId: string;
60
+ categoryId?: string | null;
61
+ content: string;
62
+ likes?: number;
63
+ tagIds?: Array<string>;
64
+ title: string;
65
+ },
66
+ database: InsertDatabase = runtimeDb,
67
+ ) => {
68
+ const [created] = await database
69
+ .insert(post)
70
+ .values({
71
+ authorId,
72
+ categoryId,
73
+ content,
74
+ likes,
75
+ title,
76
+ updatedAt: new Date(),
77
+ })
78
+ .returning({ id: post.id });
79
+
80
+ if (!created) {
81
+ return null;
82
+ }
83
+
84
+ if (tagIds.length) {
85
+ await database.insert(postToTag).values(tagIds.map((tagId) => ({ postId: created.id, tagId })));
86
+ }
87
+
88
+ return created.id;
89
+ };
90
+
91
+ export const likePostRecord = async (id: string) => {
92
+ const [updated] = await runtimeDb
93
+ .update(post)
94
+ .set({
95
+ likes: sql`${post.likes} + 1`,
96
+ updatedAt: new Date(),
97
+ })
98
+ .where(eq(post.id, id))
99
+ .returning({ id: post.id });
100
+
101
+ return Boolean(updated);
102
+ };
103
+
104
+ export const unlikePostRecord = async (id: string) => {
105
+ const [updated] = await runtimeDb
106
+ .update(post)
107
+ .set({
108
+ likes: sql`${post.likes} - 1`,
109
+ updatedAt: new Date(),
110
+ })
111
+ .where(and(eq(post.id, id), gt(post.likes, 0)))
112
+ .returning({ id: post.id });
113
+
114
+ if (updated) {
115
+ return true;
116
+ }
117
+
118
+ const [existing] = await runtimeDb
119
+ .select({ id: post.id })
120
+ .from(post)
121
+ .where(eq(post.id, id))
122
+ .limit(1);
123
+
124
+ return Boolean(existing);
125
+ };
126
+
127
+ export const createCommentRecord = async ({
128
+ authorId,
129
+ content,
130
+ postId,
131
+ }: {
132
+ authorId: string;
133
+ content: string;
134
+ postId: string;
135
+ }) => {
136
+ const [created] = await runtimeDb
137
+ .insert(comment)
138
+ .values({
139
+ authorId,
140
+ content,
141
+ postId,
142
+ })
143
+ .returning({ id: comment.id });
144
+
145
+ return created?.id ?? null;
146
+ };
147
+
148
+ export const deleteCommentRecord = async (id: string) => {
149
+ await runtimeDb.delete(comment).where(eq(comment.id, id));
150
+ };
151
+
152
+ export const createEventRecord = async (
153
+ {
154
+ attendees,
155
+ capacity,
156
+ description,
157
+ endAt,
158
+ hostId,
159
+ livestreamUrl,
160
+ location,
161
+ name,
162
+ startAt,
163
+ topics,
164
+ type,
165
+ }: {
166
+ attendees: Array<{ notes: string | null; status: EventAttendeeRow['status']; userId: string }>;
167
+ capacity: number;
168
+ description: string;
169
+ endAt: Date;
170
+ hostId: string;
171
+ livestreamUrl: string | null;
172
+ location: string;
173
+ name: string;
174
+ startAt: Date;
175
+ topics: Array<string>;
176
+ type: EventRow['type'];
177
+ },
178
+ database: InsertDatabase = runtimeDb,
179
+ ) => {
180
+ const [created] = await database
181
+ .insert(event)
182
+ .values({
183
+ capacity,
184
+ description,
185
+ endAt,
186
+ hostId,
187
+ livestreamUrl,
188
+ location,
189
+ name,
190
+ startAt,
191
+ topics,
192
+ type,
193
+ updatedAt: new Date(),
194
+ })
195
+ .returning({ id: event.id });
196
+
197
+ if (!created) {
198
+ return null;
199
+ }
200
+
201
+ if (attendees.length) {
202
+ await database.insert(eventAttendee).values(
203
+ attendees.map((attendee) => ({
204
+ ...attendee,
205
+ eventId: created.id,
206
+ })),
207
+ );
208
+ }
209
+
210
+ return created.id;
211
+ };
@@ -0,0 +1,332 @@
1
+ import { relations } from 'drizzle-orm';
2
+ import {
3
+ index,
4
+ integer,
5
+ primaryKey,
6
+ sqliteTable,
7
+ text,
8
+ uniqueIndex,
9
+ } from 'drizzle-orm/sqlite-core';
10
+ import { sql } from './db.ts';
11
+
12
+ const idColumn = () =>
13
+ text()
14
+ .notNull()
15
+ .$defaultFn(() => crypto.randomUUID());
16
+
17
+ const timestampColumn = () =>
18
+ integer({ mode: 'timestamp' })
19
+ .notNull()
20
+ .default(sql`(unixepoch())`);
21
+ const nullableTimestampColumn = () => integer({ mode: 'timestamp' });
22
+
23
+ export const rsvpStatus = ['INVITED', 'GOING', 'INTERESTED', 'DECLINED'] as const;
24
+ export const eventType = ['WORKSHOP', 'MEETUP', 'AMA', 'LAUNCH', 'COMMUNITY_CALL'] as const;
25
+
26
+ export const user = sqliteTable(
27
+ 'user',
28
+ {
29
+ banExpires: nullableTimestampColumn(),
30
+ banned: integer({ mode: 'boolean' }),
31
+ banReason: text(),
32
+ createdAt: timestampColumn(),
33
+ displayUsername: text(),
34
+ email: text().notNull(),
35
+ emailVerified: integer({ mode: 'boolean' }).notNull().default(false),
36
+ id: idColumn().primaryKey(),
37
+ image: text(),
38
+ name: text().notNull(),
39
+ password: text(),
40
+ role: text().notNull().default('user'),
41
+ updatedAt: timestampColumn(),
42
+ username: text(),
43
+ },
44
+ (table) => [
45
+ uniqueIndex('user_email_key').on(table.email),
46
+ uniqueIndex('user_username_key').on(table.username),
47
+ index('user_id_idx').on(table.id),
48
+ ],
49
+ );
50
+
51
+ export const session = sqliteTable(
52
+ 'session',
53
+ {
54
+ createdAt: timestampColumn(),
55
+ expiresAt: integer({ mode: 'timestamp' }).notNull(),
56
+ id: idColumn().primaryKey(),
57
+ ipAddress: text(),
58
+ token: text().notNull(),
59
+ updatedAt: timestampColumn(),
60
+ userAgent: text(),
61
+ userId: text()
62
+ .notNull()
63
+ .references(() => user.id, { onDelete: 'cascade' }),
64
+ },
65
+ (table) => [uniqueIndex('session_token_key').on(table.token)],
66
+ );
67
+
68
+ export const account = sqliteTable(
69
+ 'account',
70
+ {
71
+ accessToken: text(),
72
+ accessTokenExpiresAt: nullableTimestampColumn(),
73
+ accountId: text().notNull(),
74
+ createdAt: timestampColumn(),
75
+ id: idColumn().primaryKey(),
76
+ idToken: text(),
77
+ password: text(),
78
+ providerId: text().notNull(),
79
+ refreshToken: text(),
80
+ refreshTokenExpiresAt: nullableTimestampColumn(),
81
+ scope: text(),
82
+ updatedAt: timestampColumn(),
83
+ userId: text()
84
+ .notNull()
85
+ .references(() => user.id, { onDelete: 'cascade' }),
86
+ },
87
+ (table) => [
88
+ uniqueIndex('account_providerId_accountId_key').on(table.providerId, table.accountId),
89
+ ],
90
+ );
91
+
92
+ export const category = sqliteTable(
93
+ 'Category',
94
+ {
95
+ createdAt: timestampColumn(),
96
+ description: text(),
97
+ id: idColumn().primaryKey(),
98
+ name: text().notNull(),
99
+ },
100
+ (table) => [uniqueIndex('Category_name_key').on(table.name)],
101
+ );
102
+
103
+ export const tag = sqliteTable(
104
+ 'Tag',
105
+ {
106
+ createdAt: timestampColumn(),
107
+ description: text(),
108
+ id: idColumn().primaryKey(),
109
+ name: text().notNull(),
110
+ },
111
+ (table) => [uniqueIndex('Tag_name_key').on(table.name)],
112
+ );
113
+
114
+ export const post = sqliteTable(
115
+ 'Post',
116
+ {
117
+ authorId: text()
118
+ .notNull()
119
+ .references(() => user.id, { onDelete: 'cascade' }),
120
+ categoryId: text().references(() => category.id, { onDelete: 'set null' }),
121
+ content: text().notNull(),
122
+ createdAt: timestampColumn(),
123
+ id: idColumn().primaryKey(),
124
+ likes: integer().notNull().default(0),
125
+ title: text().notNull(),
126
+ updatedAt: timestampColumn(),
127
+ },
128
+ (table) => [index('Post_authorId_idx').on(table.authorId)],
129
+ );
130
+
131
+ export const comment = sqliteTable(
132
+ 'Comment',
133
+ {
134
+ authorId: text().references(() => user.id, { onDelete: 'set null' }),
135
+ content: text().notNull(),
136
+ createdAt: timestampColumn(),
137
+ id: idColumn().primaryKey(),
138
+ postId: text()
139
+ .notNull()
140
+ .references(() => post.id, { onDelete: 'cascade' }),
141
+ },
142
+ (table) => [
143
+ index('Comment_authorId_idx').on(table.authorId),
144
+ index('Comment_postId_idx').on(table.postId),
145
+ ],
146
+ );
147
+
148
+ export const event = sqliteTable(
149
+ 'Event',
150
+ {
151
+ capacity: integer().notNull(),
152
+ createdAt: timestampColumn(),
153
+ description: text().notNull(),
154
+ endAt: integer({ mode: 'timestamp' }).notNull(),
155
+ hostId: text()
156
+ .notNull()
157
+ .references(() => user.id, { onDelete: 'cascade' }),
158
+ id: idColumn().primaryKey(),
159
+ livestreamUrl: text(),
160
+ location: text().notNull(),
161
+ name: text().notNull(),
162
+ startAt: integer({ mode: 'timestamp' }).notNull(),
163
+ topics: text({ mode: 'json' })
164
+ .$type<Array<string>>()
165
+ .notNull()
166
+ .default(sql`'[]'`),
167
+ type: text({ enum: eventType }).notNull().default('MEETUP'),
168
+ updatedAt: timestampColumn(),
169
+ },
170
+ (table) => [index('Event_hostId_idx').on(table.hostId)],
171
+ );
172
+
173
+ export const eventAttendee = sqliteTable(
174
+ 'EventAttendee',
175
+ {
176
+ createdAt: timestampColumn(),
177
+ eventId: text()
178
+ .notNull()
179
+ .references(() => event.id, { onDelete: 'cascade' }),
180
+ id: idColumn().primaryKey(),
181
+ notes: text(),
182
+ status: text({ enum: rsvpStatus }).notNull().default('INVITED'),
183
+ userId: text()
184
+ .notNull()
185
+ .references(() => user.id, { onDelete: 'cascade' }),
186
+ },
187
+ (table) => [
188
+ uniqueIndex('EventAttendee_eventId_userId_key').on(table.eventId, table.userId),
189
+ index('EventAttendee_userId_idx').on(table.userId),
190
+ ],
191
+ );
192
+
193
+ export const postToTag = sqliteTable(
194
+ '_PostTags',
195
+ {
196
+ postId: text('A')
197
+ .notNull()
198
+ .references(() => post.id, { onDelete: 'cascade' }),
199
+ tagId: text('B')
200
+ .notNull()
201
+ .references(() => tag.id, { onDelete: 'cascade' }),
202
+ },
203
+ (table) => [
204
+ primaryKey({ columns: [table.postId, table.tagId], name: '_PostTags_AB_pkey' }),
205
+ index('_PostTags_B_index').on(table.tagId),
206
+ ],
207
+ );
208
+
209
+ export const userRelations = relations(user, ({ many }) => ({
210
+ accounts: many(account),
211
+ comments: many(comment),
212
+ eventAttendance: many(eventAttendee),
213
+ hostedEvents: many(event, { relationName: 'EventHost' }),
214
+ posts: many(post),
215
+ sessions: many(session),
216
+ }));
217
+
218
+ export const sessionRelations = relations(session, ({ one }) => ({
219
+ user: one(user, {
220
+ fields: [session.userId],
221
+ references: [user.id],
222
+ }),
223
+ }));
224
+
225
+ export const accountRelations = relations(account, ({ one }) => ({
226
+ user: one(user, {
227
+ fields: [account.userId],
228
+ references: [user.id],
229
+ }),
230
+ }));
231
+
232
+ export const categoryRelations = relations(category, ({ many }) => ({
233
+ posts: many(post),
234
+ }));
235
+
236
+ export const tagRelations = relations(tag, ({ many }) => ({
237
+ postLinks: many(postToTag),
238
+ }));
239
+
240
+ export const postRelations = relations(post, ({ many, one }) => ({
241
+ author: one(user, {
242
+ fields: [post.authorId],
243
+ references: [user.id],
244
+ }),
245
+ category: one(category, {
246
+ fields: [post.categoryId],
247
+ references: [category.id],
248
+ }),
249
+ comments: many(comment),
250
+ postLinks: many(postToTag),
251
+ }));
252
+
253
+ export const commentRelations = relations(comment, ({ one }) => ({
254
+ author: one(user, {
255
+ fields: [comment.authorId],
256
+ references: [user.id],
257
+ }),
258
+ post: one(post, {
259
+ fields: [comment.postId],
260
+ references: [post.id],
261
+ }),
262
+ }));
263
+
264
+ export const eventRelations = relations(event, ({ many, one }) => ({
265
+ attendees: many(eventAttendee),
266
+ host: one(user, {
267
+ fields: [event.hostId],
268
+ references: [user.id],
269
+ relationName: 'EventHost',
270
+ }),
271
+ }));
272
+
273
+ export const eventAttendeeRelations = relations(eventAttendee, ({ one }) => ({
274
+ event: one(event, {
275
+ fields: [eventAttendee.eventId],
276
+ references: [event.id],
277
+ }),
278
+ user: one(user, {
279
+ fields: [eventAttendee.userId],
280
+ references: [user.id],
281
+ }),
282
+ }));
283
+
284
+ export const postToTagRelations = relations(postToTag, ({ one }) => ({
285
+ post: one(post, {
286
+ fields: [postToTag.postId],
287
+ references: [post.id],
288
+ }),
289
+ tag: one(tag, {
290
+ fields: [postToTag.tagId],
291
+ references: [tag.id],
292
+ }),
293
+ }));
294
+
295
+ export type UserRow = typeof user.$inferSelect;
296
+ export type SessionRow = typeof session.$inferSelect;
297
+ export type AccountRow = typeof account.$inferSelect;
298
+ export type CategoryRow = typeof category.$inferSelect;
299
+ export type TagRow = typeof tag.$inferSelect;
300
+ export type PostRow = typeof post.$inferSelect;
301
+ export type CommentRow = typeof comment.$inferSelect;
302
+ export type EventRow = typeof event.$inferSelect;
303
+ export type EventAttendeeRow = typeof eventAttendee.$inferSelect;
304
+ export type RSVPStatus = (typeof rsvpStatus)[number];
305
+ export type EventType = (typeof eventType)[number];
306
+
307
+ const schema = {
308
+ account,
309
+ accountRelations,
310
+ category,
311
+ categoryRelations,
312
+ comment,
313
+ commentRelations,
314
+ event,
315
+ eventAttendee,
316
+ eventAttendeeRelations,
317
+ eventRelations,
318
+ eventType,
319
+ post,
320
+ postRelations,
321
+ postToTag,
322
+ postToTagRelations,
323
+ rsvpStatus,
324
+ session,
325
+ sessionRelations,
326
+ tag,
327
+ tagRelations,
328
+ user,
329
+ userRelations,
330
+ } as const;
331
+
332
+ export default schema;
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@app/server",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "author": "Christoph Nakazawa <christoph.pojer@gmail.com>",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git://github.com/nkzw-tech/fate.git"
9
+ },
10
+ "type": "module",
11
+ "scripts": {
12
+ "build": "tsc --noEmit",
13
+ "db:migrate": "wrangler d1 migrations apply fate-cloudflare --local",
14
+ "db:migrate:remote": "wrangler d1 migrations apply fate-cloudflare --remote",
15
+ "deploy": "wrangler deploy",
16
+ "dev": "pnpm run db:migrate && wrangler dev",
17
+ "dev:setup": "pnpm run db:migrate",
18
+ "typecheck": "tsc --noEmit"
19
+ },
20
+ "dependencies": {
21
+ "@nkzw/core": "^1.3.1",
22
+ "@nkzw/fate": "latest",
23
+ "better-auth": "^1.6.13",
24
+ "cf-fate": "latest",
25
+ "drizzle-orm": "^0.45.2",
26
+ "zod": "^4.4.3"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^25.9.1",
30
+ "vite": "catalog:",
31
+ "vite-plus": "catalog:",
32
+ "wrangler": "^4.0.0"
33
+ }
34
+ }
@@ -0,0 +1,17 @@
1
+ import { FateRequestError } from '@nkzw/fate/server';
2
+
3
+ type CommentOwner = Readonly<{ authorId?: string | null }>;
4
+ type SessionUser = Readonly<{ id: string }> | null | undefined;
5
+
6
+ export const assertCanDeleteComment = (comment: CommentOwner, user: SessionUser) => {
7
+ if (!user) {
8
+ throw new FateRequestError('UNAUTHORIZED', 'You must be logged in.');
9
+ }
10
+
11
+ if (!comment.authorId || comment.authorId !== user.id) {
12
+ throw new FateRequestError('FORBIDDEN', 'You can only delete your own comments.');
13
+ }
14
+ };
15
+
16
+ export const getCommentDeleteFetchSelection = (select: ReadonlyArray<string>): Array<string> =>
17
+ select.filter((path) => path !== 'post' && !path.startsWith('post.'));
@@ -0,0 +1,15 @@
1
+ import { createAuth } from '../lib/auth.ts';
2
+ import { toSessionUser } from '../user/SessionUser.tsx';
3
+
4
+ export const createContext = async ({ request, secret }: { request: Request; secret?: string }) => {
5
+ const session = await createAuth(new URL(request.url).origin, secret).api.getSession({
6
+ headers: request.headers,
7
+ });
8
+
9
+ return {
10
+ headers: request.headers,
11
+ sessionUser: session?.user ? toSessionUser(session.user) : null,
12
+ };
13
+ };
14
+
15
+ export type AppContext = Awaited<ReturnType<typeof createContext>>;
@@ -0,0 +1,7 @@
1
+ import { defineCloudflareFateLiveStream } from 'cf-fate/server';
2
+
3
+ export const fateStream = defineCloudflareFateLiveStream({
4
+ allowAnonymousControl: true,
5
+ binding: 'FATE_LIVE',
6
+ id: 'fate',
7
+ });