create-fate 1.0.3 → 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 (208) 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/pages/login.tsx +3 -3
  198. package/templates/fate/void/pnpm-workspace.yaml +12 -4
  199. package/templates/fate/void/src/ui/Button.tsx +42 -26
  200. package/templates/fate/void/src/ui/CategoryCard.tsx +3 -7
  201. package/templates/fate/void/src/ui/PostCard.tsx +38 -15
  202. package/templates/fate/void/vite.config.ts +15 -14
  203. package/templates/fate/drizzle/public/fate-logo-dark.svg +0 -4
  204. package/templates/fate/drizzle/public/fate-logo.svg +0 -4
  205. package/templates/fate/http/public/fate-logo-dark.svg +0 -4
  206. package/templates/fate/http/public/fate-logo.svg +0 -4
  207. package/templates/fate/prisma/public/fate-logo-dark.svg +0 -4
  208. package/templates/fate/prisma/public/fate-logo.svg +0 -4
@@ -0,0 +1,71 @@
1
+ import prisma from '../../prisma/prisma.tsx';
2
+ import builder, { DateTimeScalar } from '../builder.tsx';
3
+
4
+ const EventAttendee = builder.prismaNode('EventAttendee', {
5
+ fields: (t) => ({
6
+ notes: t.exposeString('notes'),
7
+ status: t.string({
8
+ nullable: false,
9
+ resolve: ({ status }) => status,
10
+ }),
11
+ user: t.relation('user', { nullable: false }),
12
+ }),
13
+ id: { field: 'id' },
14
+ });
15
+
16
+ const Event = builder.prismaNode('Event', {
17
+ fields: (t) => ({
18
+ attendees: t.relatedConnection('attendees', {
19
+ cursor: 'id',
20
+ nullable: false,
21
+ query: {
22
+ orderBy: [{ createdAt: 'asc' }, { id: 'asc' }],
23
+ },
24
+ }),
25
+ attendingCount: t.int({
26
+ nullable: false,
27
+ resolve: ({ id }) => prisma.eventAttendee.count({ where: { eventId: id, status: 'GOING' } }),
28
+ }),
29
+ capacity: t.exposeInt('capacity', { nullable: false }),
30
+ description: t.exposeString('description', { nullable: false }),
31
+ endAt: t.field({
32
+ nullable: false,
33
+ resolve: ({ endAt }) => endAt,
34
+ type: DateTimeScalar,
35
+ }),
36
+ host: t.relation('host', { nullable: false }),
37
+ livestreamUrl: t.exposeString('livestreamUrl'),
38
+ location: t.exposeString('location', { nullable: false }),
39
+ name: t.exposeString('name', { nullable: false }),
40
+ startAt: t.field({
41
+ nullable: false,
42
+ resolve: ({ startAt }) => startAt,
43
+ type: DateTimeScalar,
44
+ }),
45
+ topics: t.field({
46
+ nullable: false,
47
+ resolve: ({ topics }) => topics,
48
+ type: ['String'],
49
+ }),
50
+ type: t.string({
51
+ nullable: false,
52
+ resolve: ({ type }) => type,
53
+ }),
54
+ }),
55
+ id: { field: 'id' },
56
+ });
57
+
58
+ builder.queryFields((t) => ({
59
+ events: t.prismaConnection({
60
+ cursor: 'id',
61
+ resolve: (query) =>
62
+ prisma.event.findMany({
63
+ ...query,
64
+ orderBy: [{ startAt: 'asc' }, { id: 'asc' }],
65
+ }),
66
+ type: 'Event',
67
+ }),
68
+ }));
69
+
70
+ export { EventAttendee };
71
+ export default Event;
@@ -0,0 +1,45 @@
1
+ import prisma from '../../prisma/prisma.tsx';
2
+ import builder from '../builder.tsx';
3
+
4
+ const Post = builder.prismaNode('Post', {
5
+ fields: (t) => ({
6
+ author: t.relation('author', { nullable: false }),
7
+ category: t.relation('category'),
8
+ commentCount: t.int({
9
+ nullable: false,
10
+ resolve: ({ id }) => prisma.comment.count({ where: { postId: id } }),
11
+ }),
12
+ comments: t.relatedConnection('comments', {
13
+ cursor: 'id',
14
+ nullable: false,
15
+ query: {
16
+ orderBy: [{ createdAt: 'asc' }, { id: 'asc' }],
17
+ },
18
+ }),
19
+ content: t.exposeString('content', { nullable: false }),
20
+ likes: t.exposeInt('likes', { nullable: false }),
21
+ tags: t.relatedConnection('tags', {
22
+ cursor: 'id',
23
+ nullable: false,
24
+ query: {
25
+ orderBy: [{ name: 'asc' }, { id: 'asc' }],
26
+ },
27
+ }),
28
+ title: t.exposeString('title', { nullable: false }),
29
+ }),
30
+ id: { field: 'id' },
31
+ });
32
+
33
+ builder.queryFields((t) => ({
34
+ posts: t.prismaConnection({
35
+ cursor: 'id',
36
+ resolve: (query) =>
37
+ prisma.post.findMany({
38
+ ...query,
39
+ orderBy: [{ createdAt: 'desc' }, { id: 'desc' }],
40
+ }),
41
+ type: 'Post',
42
+ }),
43
+ }));
44
+
45
+ export default Post;
@@ -0,0 +1,11 @@
1
+ import builder from '../builder.tsx';
2
+
3
+ const Tag = builder.prismaNode('Tag', {
4
+ fields: (t) => ({
5
+ description: t.exposeString('description'),
6
+ name: t.exposeString('name', { nullable: false }),
7
+ }),
8
+ id: { field: 'id' },
9
+ });
10
+
11
+ export default Tag;
@@ -0,0 +1,28 @@
1
+ import prisma from '../../prisma/prisma.tsx';
2
+ import builder from '../builder.tsx';
3
+
4
+ const User = builder.prismaNode('User', {
5
+ fields: (t) => ({
6
+ email: t.exposeString('email', {
7
+ authScopes: (user) => ({ self: user.id }),
8
+ }),
9
+ name: t.exposeString('name', { nullable: false }),
10
+ username: t.exposeString('username'),
11
+ }),
12
+ id: { field: 'id' },
13
+ });
14
+
15
+ builder.queryFields((t) => ({
16
+ viewer: t.prismaField({
17
+ resolve: (query, _root, _args, { sessionUser }) =>
18
+ sessionUser
19
+ ? prisma.user.findUniqueOrThrow({
20
+ ...query,
21
+ where: { id: sessionUser.id },
22
+ })
23
+ : void query.include,
24
+ type: 'User',
25
+ }),
26
+ }));
27
+
28
+ export default User;
@@ -0,0 +1,16 @@
1
+ import { writeFileSync } from 'node:fs';
2
+ import './schemaImportMap.tsx';
3
+ import { join } from 'node:path';
4
+ import { printSchemaWithDirectives } from '@graphql-tools/utils';
5
+ import builder from './builder.tsx';
6
+
7
+ const schema = builder.toSchema();
8
+
9
+ if (process.env.NODE_ENV === 'development') {
10
+ writeFileSync(
11
+ join(process.cwd(), 'src/graphql/schema.graphql'),
12
+ printSchemaWithDirectives(schema),
13
+ 'utf8',
14
+ );
15
+ }
16
+ export default schema;
@@ -0,0 +1,193 @@
1
+ import { computed, count, dataView, field, list, type Entity } from '@nkzw/fate/server';
2
+ import type {
3
+ Category as PrismaCategory,
4
+ Comment as PrismaComment,
5
+ Event as PrismaEvent,
6
+ EventAttendee as PrismaEventAttendee,
7
+ Post as PrismaPost,
8
+ Tag as PrismaTag,
9
+ User as PrismaUser,
10
+ } from '../prisma/prisma-client/client.ts';
11
+ import type { AppContext } from './context.tsx';
12
+
13
+ export type CommentItem = PrismaComment & {
14
+ author?: PrismaUser | null;
15
+ post?: PrismaPost | null;
16
+ };
17
+
18
+ export type PostItem = PrismaPost & {
19
+ _count?: { comments: number };
20
+ author?: PrismaUser | null;
21
+ category?: PrismaCategory | null;
22
+ comments?: Array<CommentItem>;
23
+ tags?: Array<PrismaTag>;
24
+ };
25
+
26
+ export type CategoryItem = PrismaCategory & {
27
+ _count?: { posts: number };
28
+ posts?: Array<PostItem>;
29
+ };
30
+
31
+ type EventAttendeeItem = PrismaEventAttendee & {
32
+ user?: PrismaUser | null;
33
+ };
34
+
35
+ export type EventItem = PrismaEvent & {
36
+ _count?: { attendees: number };
37
+ attendees?: Array<EventAttendeeItem>;
38
+ host?: PrismaUser | null;
39
+ };
40
+
41
+ export const userDataView = dataView<PrismaUser>('User')({
42
+ email: computed<PrismaUser, string | null, AppContext>({
43
+ authorize: ({ id }, context) => context?.sessionUser?.id === id,
44
+ resolve: (_item, deps) => (deps.email as string | null) ?? null,
45
+ select: {
46
+ email: field('email'),
47
+ },
48
+ }),
49
+ id: true,
50
+ name: true,
51
+ username: true,
52
+ });
53
+
54
+ export const tagDataView = dataView<PrismaTag>('Tag')({
55
+ description: true,
56
+ id: true,
57
+ name: true,
58
+ });
59
+
60
+ export const categorySummaryDataView = dataView<PrismaCategory>('Category')({
61
+ id: true,
62
+ name: true,
63
+ });
64
+
65
+ const basePost = {
66
+ author: userDataView,
67
+ category: categorySummaryDataView,
68
+ commentCount: computed<PostItem, number>({
69
+ resolve: (_item, deps) => (deps.count as number) ?? 0,
70
+ select: {
71
+ count: count('comments'),
72
+ },
73
+ }),
74
+ content: true,
75
+ id: true,
76
+ likes: true,
77
+ title: true,
78
+ } as const;
79
+
80
+ export const postSummaryDataView = dataView<PostItem>('Post')({
81
+ ...basePost,
82
+ tags: list(tagDataView),
83
+ });
84
+
85
+ export const commentDataView = dataView<CommentItem>('Comment')({
86
+ author: userDataView,
87
+ content: true,
88
+ id: true,
89
+ post: postSummaryDataView,
90
+ });
91
+
92
+ export const postDataView = dataView<PostItem>('Post')({
93
+ ...basePost,
94
+ comments: list(commentDataView, { orderBy: { createdAt: 'asc', id: 'asc' } }),
95
+ tags: list(tagDataView),
96
+ });
97
+
98
+ export const categoryDataView = dataView<CategoryItem>('Category')({
99
+ description: true,
100
+ id: true,
101
+ name: true,
102
+ postCount: computed<CategoryItem, number>({
103
+ resolve: (_item, deps) => (deps.count as number) ?? 0,
104
+ select: {
105
+ count: count('posts'),
106
+ },
107
+ }),
108
+ posts: list(postDataView),
109
+ });
110
+
111
+ export const eventAttendeeDataView = dataView<EventAttendeeItem>('EventAttendee')({
112
+ id: true,
113
+ notes: true,
114
+ status: true,
115
+ user: userDataView,
116
+ });
117
+
118
+ export const eventDataView = dataView<EventItem>('Event')({
119
+ attendees: list(eventAttendeeDataView, { orderBy: { createdAt: 'asc', id: 'asc' } }),
120
+ attendingCount: computed<EventItem, number>({
121
+ resolve: (_item, deps) => (deps.count as number) ?? 0,
122
+ select: {
123
+ count: count('attendees', {
124
+ where: { status: 'GOING' },
125
+ }),
126
+ },
127
+ }),
128
+ capacity: true,
129
+ description: true,
130
+ endAt: true,
131
+ host: userDataView,
132
+ id: true,
133
+ livestreamUrl: true,
134
+ location: true,
135
+ name: true,
136
+ startAt: true,
137
+ topics: true,
138
+ type: true,
139
+ });
140
+
141
+ export type User = Entity<typeof userDataView, 'User'>;
142
+ export type Tag = Entity<typeof tagDataView, 'Tag'>;
143
+ export type Comment = Entity<
144
+ typeof commentDataView,
145
+ 'Comment',
146
+ {
147
+ author: User;
148
+ post: Post;
149
+ }
150
+ >;
151
+ export type EventAttendee = Entity<
152
+ typeof eventAttendeeDataView,
153
+ 'EventAttendee',
154
+ {
155
+ user: User;
156
+ }
157
+ >;
158
+ export type Post = Entity<
159
+ typeof postDataView,
160
+ 'Post',
161
+ {
162
+ author: User;
163
+ category: Category | null;
164
+ comments: Array<Comment>;
165
+ tags: Array<Tag>;
166
+ }
167
+ >;
168
+ export type Category = Entity<
169
+ typeof categoryDataView,
170
+ 'Category',
171
+ {
172
+ posts: Array<Post>;
173
+ }
174
+ >;
175
+ export type Event = Entity<
176
+ typeof eventDataView,
177
+ 'Event',
178
+ {
179
+ attendees: Array<EventAttendee>;
180
+ host: User;
181
+ }
182
+ >;
183
+
184
+ export const Root = {
185
+ categories: list(categoryDataView, { orderBy: { createdAt: 'asc', id: 'asc' } }),
186
+ commentSearch: {
187
+ procedure: 'search',
188
+ view: list(commentDataView, { orderBy: { createdAt: 'desc', id: 'desc' } }),
189
+ },
190
+ events: list(eventDataView, { orderBy: [{ startAt: 'asc' }, { id: 'asc' }] }),
191
+ posts: list(postDataView, { orderBy: { createdAt: 'desc', id: 'desc' } }),
192
+ viewer: userDataView,
193
+ };
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env NODE_ENV=development node_modules/.bin/nodemon -q -I --exec node --no-warnings --experimental-specifier-resolution=node --loader ts-node/esm --env-file .env
2
+ import { parseArgs, styleText } from 'node:util';
3
+ import { serve } from '@hono/node-server';
4
+ import parseInteger from '@nkzw/core/parseInteger.js';
5
+ import app from './app.tsx';
6
+
7
+ const name = 'Pothos GraphQL Server';
8
+
9
+ const {
10
+ values: { port: portArg },
11
+ } = parseArgs({
12
+ options: {
13
+ port: {
14
+ default: '9000',
15
+ short: 'p',
16
+ type: 'string',
17
+ },
18
+ },
19
+ });
20
+
21
+ const port = (portArg && parseInteger(portArg)) || 9000;
22
+
23
+ // See https://github.com/honojs/node-server/issues/233
24
+ process.on('uncaughtException', (error) => {
25
+ if (
26
+ !(error instanceof Error) ||
27
+ !error.message.includes('Invalid state: ReadableStream is already closed')
28
+ ) {
29
+ throw error;
30
+ }
31
+ });
32
+
33
+ serve({ fetch: app.fetch, port }, () =>
34
+ console.log(
35
+ `${styleText(['green', 'bold'], `${name}\n ➜`)} Server running on port ${styleText('bold', String(port))}.\n`,
36
+ ),
37
+ );
38
+
39
+ const setTitle = (title: string) => {
40
+ process.title = title;
41
+ if (process.stdout.isTTY) {
42
+ process.stdout.write(`${String.fromCharCode(27)}]0;🚀 ${title}${String.fromCharCode(7)}`);
43
+ }
44
+ };
45
+ setTimeout(() => setTitle(name), 0);
@@ -0,0 +1,31 @@
1
+ import { betterAuth } from 'better-auth';
2
+ import { prismaAdapter } from 'better-auth/adapters/prisma';
3
+ import { admin, username } from 'better-auth/plugins';
4
+ import prisma from '../prisma/prisma.tsx';
5
+ import { trustedOrigins } from './origins.ts';
6
+
7
+ export const auth = betterAuth({
8
+ advanced: {
9
+ database: {
10
+ generateId: false,
11
+ },
12
+ },
13
+ database: prismaAdapter(prisma, {
14
+ provider: 'postgresql',
15
+ }),
16
+ emailAndPassword: {
17
+ autoSignIn: true,
18
+ enabled: true,
19
+ maxPasswordLength: 128,
20
+ minPasswordLength: 8,
21
+ },
22
+ plugins: [admin(), username()],
23
+ session: {
24
+ cookieCache: {
25
+ enabled: true,
26
+ maxAge: 15 * 24 * 60 * 60,
27
+ },
28
+ },
29
+ telemetry: { enabled: false },
30
+ trustedOrigins,
31
+ });
@@ -0,0 +1,3 @@
1
+ import defineEnv from '@nkzw/define-env';
2
+
3
+ export default defineEnv(['CLIENT_DOMAIN', 'DATABASE_URL', 'BETTER_AUTH_SECRET']);
@@ -0,0 +1,18 @@
1
+ import env from './env.tsx';
2
+
3
+ const localDevOriginPattern = /^https?:\/\/(?:localhost|127\.0\.0\.1)(?::\d+)?$/;
4
+
5
+ const isDevelopment = process.env.NODE_ENV === 'development' || process.env.DEV;
6
+
7
+ export const clientOrigin = env('CLIENT_DOMAIN');
8
+
9
+ export const trustedOrigins = [
10
+ clientOrigin,
11
+ ...(isDevelopment ? ['http://localhost:*', 'http://127.0.0.1:*'] : []),
12
+ ];
13
+
14
+ export const resolveCorsOrigin = (origin: string) => {
15
+ if (origin === clientOrigin || (isDevelopment && localDevOriginPattern.test(origin))) {
16
+ return origin;
17
+ }
18
+ };
@@ -0,0 +1,9 @@
1
+ import { PrismaPg } from '@prisma/adapter-pg';
2
+ import env from '../lib/env.tsx';
3
+ import { PrismaClient } from './prisma-client/client.ts';
4
+
5
+ const adapter = new PrismaPg({
6
+ connectionString: env('DATABASE_URL'),
7
+ });
8
+
9
+ export default new PrismaClient({ adapter });
@@ -0,0 +1,181 @@
1
+ // This is your Prisma schema file,
2
+ // learn more about it in the docs: https://pris.ly/d/prisma-schema
3
+
4
+ generator client {
5
+ binaryTargets = ["native", "linux-musl"]
6
+ importFileExtension = "ts"
7
+ output = "./prisma-client"
8
+ provider = "prisma-client"
9
+ engineType = "client"
10
+ }
11
+
12
+ generator pothos {
13
+ provider = "prisma-pothos-types"
14
+ clientOutput = "./prisma-client"
15
+ output = "./pothos-types.ts"
16
+ }
17
+
18
+ generator json {
19
+ provider = "prisma-json-types-generator"
20
+ clientOutput = "./prisma-client"
21
+ }
22
+
23
+ datasource db {
24
+ provider = "postgresql"
25
+ }
26
+
27
+ model User {
28
+ id String @id @unique @default(uuid(7))
29
+ accounts Account[]
30
+ banExpires DateTime?
31
+ banned Boolean?
32
+ banReason String?
33
+ createdAt DateTime @default(now())
34
+ displayUsername String?
35
+ email String @unique
36
+ emailVerified Boolean
37
+ image String?
38
+ name String
39
+ password String?
40
+ role String @default("user")
41
+ sessions Session[]
42
+ posts Post[]
43
+ comments Comment[]
44
+ hostedEvents Event[] @relation("EventHost")
45
+ eventAttendance EventAttendee[]
46
+ updatedAt DateTime
47
+ username String? @unique
48
+
49
+ @@index([id(sort: Asc)])
50
+ @@map("user")
51
+ }
52
+
53
+ model Session {
54
+ id String @unique @default(uuid(7))
55
+ expiresAt DateTime
56
+ token String
57
+ createdAt DateTime
58
+ updatedAt DateTime
59
+ ipAddress String?
60
+ userAgent String?
61
+ userId String
62
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
63
+
64
+ @@unique([token])
65
+ @@map("session")
66
+ }
67
+
68
+ model Account {
69
+ id String @unique @default(uuid(7))
70
+ accountId String
71
+ providerId String
72
+ userId String
73
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
74
+ accessToken String?
75
+ refreshToken String?
76
+ idToken String?
77
+ accessTokenExpiresAt DateTime?
78
+ refreshTokenExpiresAt DateTime?
79
+ scope String?
80
+ password String?
81
+ createdAt DateTime
82
+ updatedAt DateTime
83
+
84
+ @@unique([providerId, accountId])
85
+ @@map("account")
86
+ }
87
+
88
+ model Post {
89
+ id String @id @default(uuid(7))
90
+ title String
91
+ content String
92
+ likes Int @default(0)
93
+ createdAt DateTime @default(now())
94
+ updatedAt DateTime @updatedAt
95
+ authorId String
96
+ author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
97
+ comments Comment[]
98
+ categoryId String?
99
+ category Category? @relation(fields: [categoryId], references: [id], onDelete: SetNull)
100
+ tags Tag[] @relation("PostTags")
101
+
102
+ @@index([authorId(sort: Asc)])
103
+ }
104
+
105
+ model Comment {
106
+ id String @id @default(uuid(7))
107
+ content String
108
+ createdAt DateTime @default(now())
109
+ postId String
110
+ authorId String?
111
+ post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
112
+ author User? @relation(fields: [authorId], references: [id], onDelete: SetNull)
113
+
114
+ @@index([authorId(sort: Asc)])
115
+ @@index([postId(sort: Asc)])
116
+ }
117
+
118
+ model Category {
119
+ id String @id @default(uuid(7))
120
+ name String @unique
121
+ description String?
122
+ createdAt DateTime @default(now())
123
+ posts Post[]
124
+ }
125
+
126
+ model Tag {
127
+ id String @id @default(uuid(7))
128
+ name String @unique
129
+ description String?
130
+ createdAt DateTime @default(now())
131
+ posts Post[] @relation("PostTags")
132
+ }
133
+
134
+ enum RSVPStatus {
135
+ INVITED
136
+ GOING
137
+ INTERESTED
138
+ DECLINED
139
+ }
140
+
141
+ enum EventType {
142
+ WORKSHOP
143
+ MEETUP
144
+ AMA
145
+ LAUNCH
146
+ COMMUNITY_CALL
147
+ }
148
+
149
+ model Event {
150
+ id String @id @default(uuid(7))
151
+ name String
152
+ description String
153
+ type EventType @default(MEETUP)
154
+ hostId String
155
+ host User @relation("EventHost", fields: [hostId], references: [id], onDelete: Cascade)
156
+ location String
157
+ startAt DateTime
158
+ endAt DateTime
159
+ capacity Int
160
+ topics String[] @default([])
161
+ livestreamUrl String?
162
+ attendees EventAttendee[]
163
+ createdAt DateTime @default(now())
164
+ updatedAt DateTime @updatedAt
165
+
166
+ @@index([hostId(sort: Asc)])
167
+ }
168
+
169
+ model EventAttendee {
170
+ id String @id @default(uuid(7))
171
+ eventId String
172
+ userId String
173
+ status RSVPStatus @default(INVITED)
174
+ notes String?
175
+ event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
176
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
177
+ createdAt DateTime @default(now())
178
+
179
+ @@unique([eventId, userId])
180
+ @@index([userId(sort: Asc)])
181
+ }