create-m5kdev 0.25.3 → 0.26.2

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 (143) hide show
  1. package/dist/src/create.cjs +18 -1
  2. package/dist/src/create.cjs.map +1 -1
  3. package/dist/src/fs.cjs +65 -5
  4. package/dist/src/fs.cjs.map +1 -1
  5. package/dist/src/fs.d.cts +13 -2
  6. package/dist/src/index.cjs +0 -0
  7. package/dist/src/paths.cjs +2 -2
  8. package/dist/src/prompts.cjs +22 -0
  9. package/dist/src/prompts.cjs.map +1 -1
  10. package/dist/src/runCli.cjs +13 -2
  11. package/dist/src/runCli.cjs.map +1 -1
  12. package/dist/src/types.d.cts +9 -1
  13. package/package.json +8 -6
  14. package/templates/minimal-app/.cursor/rules/frontend-data-guide.mdc +46 -3
  15. package/templates/minimal-app/.cursor/rules/module-db-guide.mdc +15 -1
  16. package/templates/minimal-app/.cursor/rules/module-dto-guide.mdc +60 -0
  17. package/templates/minimal-app/.cursor/rules/module-repository-guide.mdc +98 -193
  18. package/templates/minimal-app/.cursor/rules/module-schema-guide.mdc +53 -173
  19. package/templates/minimal-app/.cursor/rules/module-service-guide.mdc +133 -240
  20. package/templates/minimal-app/.cursor/rules/module-trpc-guide.mdc +71 -113
  21. package/templates/minimal-app/README.md.tpl +3 -3
  22. package/templates/minimal-app/apps/.gitignore.tpl +13 -0
  23. package/templates/minimal-app/apps/e2e/package.json.tpl +23 -0
  24. package/templates/minimal-app/apps/e2e/playwright.config.ts.tpl +39 -0
  25. package/templates/minimal-app/apps/e2e/playwright.expo.config.ts.tpl +38 -0
  26. package/templates/minimal-app/apps/e2e/servers.ts.tpl +100 -0
  27. package/templates/minimal-app/apps/e2e/tests/email.spec.ts.tpl +47 -0
  28. package/templates/minimal-app/apps/e2e/tests/expo-helpers.ts.tpl +73 -0
  29. package/templates/minimal-app/apps/e2e/tests/expo-standard.spec.ts.tpl +517 -0
  30. package/templates/minimal-app/apps/e2e/tests/expo-waitlist.spec.ts.tpl +94 -0
  31. package/templates/minimal-app/apps/e2e/tests/helpers.ts.tpl +289 -0
  32. package/templates/minimal-app/apps/e2e/tests/standard.spec.ts.tpl +516 -0
  33. package/templates/minimal-app/apps/e2e/tests/waitlist.spec.ts.tpl +107 -0
  34. package/templates/minimal-app/apps/e2e/tsconfig.json.tpl +10 -0
  35. package/templates/minimal-app/apps/email/package.json.tpl +34 -34
  36. package/templates/minimal-app/apps/email/src/emails/accountDeletionEmail.tsx.tpl +35 -34
  37. package/templates/minimal-app/apps/email/src/emails/organizationInviteEmail.tsx.tpl +34 -33
  38. package/templates/minimal-app/apps/email/src/emails/passwordResetEmail.tsx.tpl +35 -34
  39. package/templates/minimal-app/apps/email/src/emails/verificationEmail.tsx.tpl +37 -36
  40. package/templates/minimal-app/apps/email/src/emails/waitlistEmails.tsx.tpl +87 -0
  41. package/templates/minimal-app/apps/email/src/index.ts.tpl +72 -32
  42. package/templates/minimal-app/apps/email/src/resources.ts.tpl +65 -36
  43. package/templates/minimal-app/apps/email/tsconfig.json.tpl +16 -11
  44. package/templates/minimal-app/apps/expo/.gitignore.tpl +44 -0
  45. package/templates/minimal-app/apps/expo/.prettierignore +13 -0
  46. package/templates/minimal-app/apps/expo/.prettierrc.json.tpl +9 -0
  47. package/templates/minimal-app/apps/expo/CHANGELOG.md.tpl +156 -0
  48. package/templates/minimal-app/apps/expo/README.md.tpl +44 -0
  49. package/templates/minimal-app/apps/expo/app.json.tpl +36 -0
  50. package/templates/minimal-app/apps/expo/assets/images/adaptive-icon.png +0 -0
  51. package/templates/minimal-app/apps/expo/assets/images/favicon.png +0 -0
  52. package/templates/minimal-app/apps/expo/assets/images/icon.png +0 -0
  53. package/templates/minimal-app/apps/expo/assets/images/splash-icon.png +0 -0
  54. package/templates/minimal-app/apps/expo/eslint.config.js.tpl +8 -0
  55. package/templates/minimal-app/apps/expo/expo-env.d.ts.tpl +3 -0
  56. package/templates/minimal-app/apps/expo/metro.config.js.tpl +16 -0
  57. package/templates/minimal-app/apps/expo/package.json.tpl +59 -0
  58. package/templates/minimal-app/apps/expo/src/app/_layout.tsx.tpl +31 -0
  59. package/templates/minimal-app/apps/expo/src/app/claim-account.tsx.tpl +151 -0
  60. package/templates/minimal-app/apps/expo/src/app/forgot-password.tsx.tpl +53 -0
  61. package/templates/minimal-app/apps/expo/src/app/index.tsx.tpl +8 -0
  62. package/templates/minimal-app/apps/expo/src/app/login.tsx.tpl +71 -0
  63. package/templates/minimal-app/apps/expo/src/app/logout.tsx.tpl +19 -0
  64. package/templates/minimal-app/apps/expo/src/app/organization/accept-invitation.tsx.tpl +85 -0
  65. package/templates/minimal-app/apps/expo/src/app/posts.tsx.tpl +40 -0
  66. package/templates/minimal-app/apps/expo/src/app/reset-password.tsx.tpl +75 -0
  67. package/templates/minimal-app/apps/expo/src/app/signup.tsx.tpl +193 -0
  68. package/templates/minimal-app/apps/expo/src/components/AuthScaffold.tsx.tpl +34 -0
  69. package/templates/minimal-app/apps/expo/src/components/FormTextField.tsx.tpl +37 -0
  70. package/templates/minimal-app/apps/expo/src/components/LoadingScreen.tsx.tpl +10 -0
  71. package/templates/minimal-app/apps/expo/src/components/StatusText.tsx.tpl +22 -0
  72. package/templates/minimal-app/apps/expo/src/config.ts.tpl +9 -0
  73. package/templates/minimal-app/apps/expo/src/global.css.tpl +11 -0
  74. package/templates/minimal-app/apps/expo/src/lib/trpc.ts.tpl +20 -0
  75. package/templates/minimal-app/apps/expo/tsconfig.json.tpl +11 -0
  76. package/templates/minimal-app/apps/expo/uniwind-env.d.ts.tpl +1 -0
  77. package/templates/minimal-app/apps/expo/uniwind-types.d.ts.tpl +10 -0
  78. package/templates/minimal-app/apps/server/AGENTS.md.tpl +39 -39
  79. package/templates/minimal-app/apps/server/drizzle/0000_wealthy_absorbing_man.sql +268 -0
  80. package/templates/minimal-app/apps/server/drizzle/db.ts.tpl +26 -0
  81. package/templates/minimal-app/apps/server/drizzle/meta/0000_snapshot.json.tpl +1890 -0
  82. package/templates/minimal-app/apps/server/drizzle/meta/_journal.json.tpl +13 -0
  83. package/templates/minimal-app/apps/server/drizzle/reset.ts.tpl +33 -0
  84. package/templates/minimal-app/apps/server/drizzle/seed.e2e.ts.tpl +244 -0
  85. package/templates/minimal-app/apps/server/drizzle/seed.ts.tpl +181 -180
  86. package/templates/minimal-app/apps/server/drizzle/sync.ts.tpl +14 -21
  87. package/templates/minimal-app/apps/server/drizzle.config.ts.tpl +33 -35
  88. package/templates/minimal-app/apps/server/package.json.tpl +56 -52
  89. package/templates/minimal-app/apps/server/src/app.ts.tpl +142 -126
  90. package/templates/minimal-app/apps/server/src/index.ts.tpl +58 -58
  91. package/templates/minimal-app/apps/server/src/lib/auth.ts.tpl +12 -12
  92. package/templates/minimal-app/apps/server/src/modules/posts/posts.db.ts.tpl +25 -25
  93. package/templates/minimal-app/apps/server/src/modules/posts/posts.dto.ts.tpl +23 -0
  94. package/templates/minimal-app/apps/server/src/modules/posts/posts.grants.ts.tpl +30 -40
  95. package/templates/minimal-app/apps/server/src/modules/posts/posts.module.ts.tpl +54 -54
  96. package/templates/minimal-app/apps/server/src/modules/posts/posts.repository.ts.tpl +67 -112
  97. package/templates/minimal-app/apps/server/src/modules/posts/posts.service.ts.tpl +104 -170
  98. package/templates/minimal-app/apps/server/src/modules/posts/posts.trpc.ts.tpl +37 -48
  99. package/templates/minimal-app/apps/server/src/modules/posts/posts.utils.ts.tpl +19 -0
  100. package/templates/minimal-app/apps/server/src/modules/test-harness/test-harness.module.ts.tpl +279 -0
  101. package/templates/minimal-app/apps/server/src/schema.ts.tpl +20 -0
  102. package/templates/minimal-app/apps/server/src/types.ts.tpl +1 -1
  103. package/templates/minimal-app/apps/server/tsconfig.json.tpl +20 -14
  104. package/templates/minimal-app/apps/server/tsup.config.ts.tpl +9 -9
  105. package/templates/minimal-app/apps/shared/package.json.tpl +24 -24
  106. package/templates/minimal-app/apps/shared/src/modules/app/app.constants.ts.tpl +28 -2
  107. package/templates/minimal-app/apps/shared/src/modules/notification/notification.schema.ts.tpl +3 -3
  108. package/templates/minimal-app/apps/shared/src/modules/posts/posts.constants.ts.tpl +5 -5
  109. package/templates/minimal-app/apps/shared/src/modules/posts/posts.schema.ts.tpl +25 -76
  110. package/templates/minimal-app/apps/shared/tsconfig.json.tpl +16 -12
  111. package/templates/minimal-app/apps/webapp/index.html.tpl +22 -22
  112. package/templates/minimal-app/apps/webapp/package.json.tpl +53 -52
  113. package/templates/minimal-app/apps/webapp/public/{push-sw.js → push-sw.js.tpl} +34 -34
  114. package/templates/minimal-app/apps/webapp/src/App.tsx.tpl +44 -13
  115. package/templates/minimal-app/apps/webapp/src/Layout.tsx.tpl +18 -12
  116. package/templates/minimal-app/apps/webapp/src/Router.tsx.tpl +33 -40
  117. package/templates/minimal-app/apps/webapp/src/index.css.tpl +15 -75
  118. package/templates/minimal-app/apps/webapp/src/main.tsx.tpl +26 -26
  119. package/templates/minimal-app/apps/webapp/src/modules/posts/PostsRoute.tsx.tpl +278 -307
  120. package/templates/minimal-app/apps/webapp/src/modules/posts/components/PostCard.tsx.tpl +96 -138
  121. package/templates/minimal-app/apps/webapp/src/modules/posts/components/PostEditorModal.tsx.tpl +118 -124
  122. package/templates/minimal-app/apps/webapp/src/modules/posts/hooks/usePostActions.ts.tpl +108 -116
  123. package/templates/minimal-app/apps/webapp/src/modules/posts/hooks/usePostsList.ts.tpl +15 -9
  124. package/templates/minimal-app/apps/webapp/src/modules/posts/hooks/usePostsRoute.ts.tpl +13 -13
  125. package/templates/minimal-app/apps/webapp/src/modules/posts/posts.utils.ts.tpl +16 -16
  126. package/templates/minimal-app/apps/webapp/src/utils/i18n.ts.tpl +13 -13
  127. package/templates/minimal-app/apps/webapp/src/utils/trpc.ts.tpl +10 -6
  128. package/templates/minimal-app/apps/webapp/src/vite-env.d.ts.tpl +1 -1
  129. package/templates/minimal-app/apps/webapp/translations/{app.json.tpl → en/app.json.tpl} +7 -7
  130. package/templates/minimal-app/apps/webapp/translations/en/{blog-app.json.tpl → {{APP_SLUG}}.json.tpl} +135 -135
  131. package/templates/minimal-app/apps/webapp/tsconfig.json.tpl +17 -17
  132. package/templates/minimal-app/apps/webapp/vite.config.ts.tpl +31 -31
  133. package/templates/minimal-app/template.manifest.json +21 -0
  134. package/templates/minimal-app/apps/server/drizzle/generate-schema.ts.tpl +0 -31
  135. package/templates/minimal-app/apps/server/src/db.ts.tpl +0 -7
  136. package/templates/minimal-app/apps/server/src/generated/schema.ts.tpl +0 -13
  137. package/templates/minimal-app/apps/server/src/utils/trpc.ts.tpl +0 -8
  138. package/templates/minimal-app/apps/shared/src/modules/app/locale.constants.ts.tpl +0 -7
  139. package/templates/minimal-app/apps/shared/src/modules/app/roles.constants.ts.tpl +0 -17
  140. package/templates/minimal-app/apps/webapp/src/Providers.tsx.tpl +0 -36
  141. package/templates/minimal-app/apps/webapp/src/modules/notification/PushNotificationsPanel.tsx.tpl +0 -89
  142. package/templates/minimal-app/apps/webapp/src/navigation/navigation.config.tsx.tpl +0 -85
  143. /package/templates/minimal-app/apps/webapp/public/{logo.svg → logo.svg.tpl} +0 -0
@@ -0,0 +1,13 @@
1
+ {
2
+ "version": "7",
3
+ "dialect": "sqlite",
4
+ "entries": [
5
+ {
6
+ "idx": 0,
7
+ "version": "6",
8
+ "when": 1783931793066,
9
+ "tag": "0000_wealthy_absorbing_man",
10
+ "breakpoints": true
11
+ }
12
+ ]
13
+ }
@@ -0,0 +1,33 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ /** Deletes the local SQLite database (and stored-email output) for a clean slate. */
5
+ function sqlitePathFromDatabaseUrl(url: string) {
6
+ if (!url.startsWith("file:")) return null;
7
+ return path.resolve(process.cwd(), url.slice("file:".length));
8
+ }
9
+
10
+ async function removeIfExists(target: string) {
11
+ await fs.rm(target, { recursive: true, force: true });
12
+ }
13
+
14
+ async function reset() {
15
+ const dbPath = sqlitePathFromDatabaseUrl(process.env.DATABASE_URL ?? "");
16
+ if (dbPath) {
17
+ await removeIfExists(dbPath);
18
+ await removeIfExists(`${dbPath}-shm`);
19
+ await removeIfExists(`${dbPath}-wal`);
20
+ await fs.mkdir(path.dirname(dbPath), { recursive: true });
21
+ }
22
+
23
+ const emailDirectory = process.env.EMAIL_OUTPUT_DIRECTORY
24
+ ? path.resolve(process.cwd(), process.env.EMAIL_OUTPUT_DIRECTORY)
25
+ : null;
26
+
27
+ if (emailDirectory) {
28
+ await removeIfExists(emailDirectory);
29
+ await fs.mkdir(emailDirectory, { recursive: true });
30
+ }
31
+ }
32
+
33
+ void reset();
@@ -0,0 +1,244 @@
1
+ /**
2
+ * E2E fixture seed — used only by the e2e package's prepare scripts, never by
3
+ * generated apps (the CLI excludes this file). The user-facing demo seed lives
4
+ * in ./seed.ts.
5
+ */
6
+ import { hashPassword } from "better-auth/crypto";
7
+ import { eq } from "drizzle-orm";
8
+ import { v4 as uuidv4 } from "uuid";
9
+ import { orm, schema } from "./db";
10
+
11
+ const profile = process.env.AUTH_E2E_PROFILE ?? "standard";
12
+ const ADMIN_EMAIL = `admin.${profile}@auth-e2e.local`;
13
+ const ADMIN_PASSWORD = "password1234";
14
+
15
+ function slugify(value: string): string {
16
+ return value
17
+ .trim()
18
+ .toLowerCase()
19
+ .replace(/[^a-z0-9]+/g, "-")
20
+ .replace(/^-+|-+$/g, "");
21
+ }
22
+
23
+ async function ensureUser({
24
+ id,
25
+ name,
26
+ email,
27
+ password,
28
+ role = "user",
29
+ emailVerified = true,
30
+ }: {
31
+ id: string;
32
+ name: string;
33
+ email: string;
34
+ password?: string;
35
+ role?: string;
36
+ emailVerified?: boolean;
37
+ }) {
38
+ const [existingUser] = await orm
39
+ .select()
40
+ .from(schema.users)
41
+ .where(eq(schema.users.email, email))
42
+ .limit(1);
43
+ if (existingUser) return existingUser;
44
+
45
+ const [user] = await orm
46
+ .insert(schema.users)
47
+ .values({
48
+ id,
49
+ name,
50
+ email,
51
+ emailVerified,
52
+ role,
53
+ createdAt: new Date(),
54
+ updatedAt: new Date(),
55
+ })
56
+ .returning();
57
+
58
+ if (!user) throw new Error(`Failed to seed user ${email}`);
59
+
60
+ if (password) {
61
+ await orm.insert(schema.accounts).values({
62
+ id: uuidv4(),
63
+ accountId: user.id,
64
+ providerId: "credential",
65
+ userId: user.id,
66
+ password: await hashPassword(password),
67
+ createdAt: new Date(),
68
+ updatedAt: new Date(),
69
+ });
70
+ }
71
+
72
+ return user;
73
+ }
74
+
75
+ async function ensureOrganization(userId: string) {
76
+ const organizationId = `auth-e2e-enterprise-${profile}`;
77
+ const teamId = `auth-e2e-team-${profile}`;
78
+
79
+ const [existingOrganization] = await orm
80
+ .select()
81
+ .from(schema.organizations)
82
+ .where(eq(schema.organizations.id, organizationId))
83
+ .limit(1);
84
+
85
+ if (!existingOrganization) {
86
+ await orm.insert(schema.organizations).values({
87
+ id: organizationId,
88
+ name: `Auth E2E Enterprise ${profile}`,
89
+ slug: `auth-e2e-enterprise-${profile}`,
90
+ type: "enterprise",
91
+ createdAt: new Date(),
92
+ });
93
+ }
94
+
95
+ const [existingMember] = await orm
96
+ .select()
97
+ .from(schema.members)
98
+ .where(eq(schema.members.userId, userId))
99
+ .limit(1);
100
+
101
+ if (!existingMember) {
102
+ await orm.insert(schema.members).values({
103
+ id: uuidv4(),
104
+ organizationId,
105
+ userId,
106
+ role: "owner",
107
+ createdAt: new Date(),
108
+ });
109
+ }
110
+
111
+ const [existingTeam] = await orm
112
+ .select()
113
+ .from(schema.teams)
114
+ .where(eq(schema.teams.id, teamId))
115
+ .limit(1);
116
+
117
+ if (!existingTeam) {
118
+ await orm.insert(schema.teams).values({
119
+ id: teamId,
120
+ name: "Editorial",
121
+ organizationId,
122
+ createdAt: new Date(),
123
+ updatedAt: new Date(),
124
+ });
125
+ }
126
+
127
+ const [existingTeamMember] = await orm
128
+ .select()
129
+ .from(schema.teamMembers)
130
+ .where(eq(schema.teamMembers.userId, userId))
131
+ .limit(1);
132
+
133
+ if (!existingTeamMember) {
134
+ await orm.insert(schema.teamMembers).values({
135
+ id: uuidv4(),
136
+ teamId,
137
+ userId,
138
+ role: "owner",
139
+ createdAt: new Date(),
140
+ });
141
+ }
142
+
143
+ return { organizationId, teamId };
144
+ }
145
+
146
+ async function seedPosts(userId: string, organizationId: string) {
147
+ const existingPosts = await orm.select().from(schema.posts).limit(1);
148
+ if (existingPosts.length > 0) return;
149
+
150
+ const posts = [
151
+ {
152
+ title: "A small blog that protects auth changes",
153
+ excerpt: "The first fixture post explains why the E2E app exists.",
154
+ content:
155
+ "This post is intentionally simple. It gives Playwright a protected application surface after authentication succeeds.",
156
+ },
157
+ {
158
+ title: "Organization invites should never be guessed",
159
+ excerpt: "Invite links are generated by Better Auth and captured by stored email previews.",
160
+ content:
161
+ "The E2E suite verifies that organization invitation emails contain the correct app URL and can be accepted by a new user.",
162
+ },
163
+ ];
164
+
165
+ await orm.insert(schema.posts).values(
166
+ posts.map((post, index) => ({
167
+ id: uuidv4(),
168
+ ...post,
169
+ slug: `${slugify(post.title)}-${profile}-${index}`,
170
+ authorUserId: userId,
171
+ organizationId,
172
+ createdAt: new Date(Date.now() - index * 60_000),
173
+ }))
174
+ );
175
+ }
176
+
177
+ async function seedExpiredWaitlistCode() {
178
+ const [existing] = await orm
179
+ .select()
180
+ .from(schema.waitlist)
181
+ .where(eq(schema.waitlist.code, "expired-waitlist-code"))
182
+ .limit(1);
183
+ if (existing) return;
184
+
185
+ await orm.insert(schema.waitlist).values({
186
+ id: uuidv4(),
187
+ email: `expired.${profile}@auth-e2e.local`,
188
+ name: "Expired Invite",
189
+ type: "WAITLIST",
190
+ status: "INVITED",
191
+ code: "expired-waitlist-code",
192
+ expiresAt: new Date(Date.now() - 60_000),
193
+ createdAt: new Date(),
194
+ updatedAt: new Date(),
195
+ });
196
+ }
197
+
198
+ async function seedProvisionedClaimUser() {
199
+ const user = await ensureUser({
200
+ id: `auth-e2e-claim-user-${profile}`,
201
+ name: "Provisioned Claim User",
202
+ email: `claim.${profile}@provisioned.auth-e2e.local`,
203
+ role: "user",
204
+ emailVerified: false,
205
+ });
206
+
207
+ const [existingClaim] = await orm
208
+ .select()
209
+ .from(schema.waitlist)
210
+ .where(eq(schema.waitlist.claimUserId, user.id))
211
+ .limit(1);
212
+ if (existingClaim) return user;
213
+
214
+ await orm.insert(schema.waitlist).values({
215
+ id: `auth-e2e-claim-${profile}`,
216
+ type: "ACCOUNT_CLAIM",
217
+ status: "INVITED",
218
+ claimUserId: user.id,
219
+ code: uuidv4(),
220
+ expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 14),
221
+ createdAt: new Date(),
222
+ updatedAt: new Date(),
223
+ });
224
+
225
+ return user;
226
+ }
227
+
228
+ async function seed() {
229
+ const admin = await ensureUser({
230
+ id: `auth-e2e-admin-${profile}`,
231
+ name: "Auth E2E Admin",
232
+ email: ADMIN_EMAIL,
233
+ password: ADMIN_PASSWORD,
234
+ role: "admin",
235
+ });
236
+ const { organizationId } = await ensureOrganization(admin.id);
237
+ await seedPosts(admin.id, organizationId);
238
+ await seedExpiredWaitlistCode();
239
+ await seedProvisionedClaimUser();
240
+
241
+ console.info(`Seeded ${profile}. Admin login: ${ADMIN_EMAIL} / ${ADMIN_PASSWORD}`);
242
+ }
243
+
244
+ void seed();
@@ -1,180 +1,181 @@
1
- import { hashPassword } from "better-auth/crypto";
2
- import { eq } from "drizzle-orm";
3
- import { v4 as uuidv4 } from "uuid";
4
- import { orm, schema } from "../src/db";
5
- import { syncDatabase } from "./sync";
6
-
7
- const DEMO_EMAIL = "admin@{{APP_SLUG}}.local";
8
- const DEMO_PASSWORD = "password1234";
9
- const ORGANIZATION_ID = "{{APP_SLUG}}-org";
10
- const TEAM_ID = "{{APP_SLUG}}-team";
11
-
12
- async function ensureDemoUser() {
13
- const [existingUser] = await orm
14
- .select()
15
- .from(schema.users)
16
- .where(eq(schema.users.email, DEMO_EMAIL))
17
- .limit(1);
18
-
19
- if (existingUser) {
20
- return existingUser;
21
- }
22
-
23
- const userId = uuidv4();
24
- const [user] = await orm
25
- .insert(schema.users)
26
- .values({
27
- id: userId,
28
- name: "Demo Editor",
29
- email: DEMO_EMAIL,
30
- emailVerified: true,
31
- role: "admin",
32
- createdAt: new Date(),
33
- updatedAt: new Date(),
34
- })
35
- .returning();
36
-
37
- if (!user) {
38
- throw new Error("Failed to create demo user.");
39
- }
40
-
41
- await orm.insert(schema.accounts).values({
42
- id: uuidv4(),
43
- accountId: user.id,
44
- providerId: "credential",
45
- userId: user.id,
46
- password: await hashPassword(DEMO_PASSWORD),
47
- createdAt: new Date(),
48
- updatedAt: new Date(),
49
- });
50
-
51
- return user;
52
- }
53
-
54
- async function ensureOrganization(userId: string) {
55
- const [existingOrganization] = await orm
56
- .select()
57
- .from(schema.organizations)
58
- .where(eq(schema.organizations.id, ORGANIZATION_ID))
59
- .limit(1);
60
-
61
- if (!existingOrganization) {
62
- await orm.insert(schema.organizations).values({
63
- id: ORGANIZATION_ID,
64
- name: "{{APP_NAME}}",
65
- slug: "{{APP_SLUG}}",
66
- type: "enterprise",
67
- createdAt: new Date(),
68
- });
69
- }
70
-
71
- const [existingMember] = await orm
72
- .select()
73
- .from(schema.members)
74
- .where(eq(schema.members.userId, userId))
75
- .limit(1);
76
-
77
- if (!existingMember) {
78
- await orm.insert(schema.members).values({
79
- id: uuidv4(),
80
- organizationId: ORGANIZATION_ID,
81
- userId,
82
- role: "owner",
83
- createdAt: new Date(),
84
- });
85
- }
86
-
87
- const [existingTeam] = await orm
88
- .select()
89
- .from(schema.teams)
90
- .where(eq(schema.teams.id, TEAM_ID))
91
- .limit(1);
92
-
93
- if (!existingTeam) {
94
- await orm.insert(schema.teams).values({
95
- id: TEAM_ID,
96
- name: "Editorial",
97
- organizationId: ORGANIZATION_ID,
98
- createdAt: new Date(),
99
- updatedAt: new Date(),
100
- });
101
- }
102
-
103
- const [existingTeamMember] = await orm
104
- .select()
105
- .from(schema.teamMembers)
106
- .where(eq(schema.teamMembers.userId, userId))
107
- .limit(1);
108
-
109
- if (!existingTeamMember) {
110
- await orm.insert(schema.teamMembers).values({
111
- id: uuidv4(),
112
- teamId: TEAM_ID,
113
- userId,
114
- role: "owner",
115
- createdAt: new Date(),
116
- });
117
- }
118
-
119
- return { organizationId: ORGANIZATION_ID, teamId: TEAM_ID };
120
- }
121
-
122
- async function seedPosts(userId: string, organizationId: string | null, teamId: string | null) {
123
- const existingPosts = await orm.select().from(schema.posts).limit(1);
124
- if (existingPosts.length > 0) {
125
- return;
126
- }
127
-
128
- await orm.insert(schema.posts).values([
129
- {
130
- authorUserId: userId,
131
- organizationId,
132
- teamId,
133
- title: "An editorial workflow you can ship in an afternoon",
134
- slug: "editorial-workflow-in-an-afternoon",
135
- excerpt: "A first draft on how this minimal starter is wired together.",
136
- content:
137
- "This starter keeps the moving parts visible. Shared contracts live in the shared app, business rules stay in services, and the web layer only does composition and UI work.",
138
- status: "published",
139
- publishedAt: new Date(),
140
- createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5),
141
- },
142
- {
143
- authorUserId: userId,
144
- organizationId,
145
- teamId,
146
- title: "Three habits that keep CRUD apps from drifting into chaos",
147
- slug: "three-habits-that-keep-crud{{PACKAGE_SCOPE}}s-focused",
148
- excerpt: "A draft about explicit composition, typed contracts, and URL state.",
149
- content:
150
- "Keep your composition explicit, keep your schemas honest, and keep the URL involved in user intent. Those three choices do most of the architectural work in a small product.",
151
- status: "draft",
152
- createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 2),
153
- },
154
- {
155
- authorUserId: userId,
156
- organizationId,
157
- teamId,
158
- title: "What a minimal platform starter should still refuse to compromise on",
159
- slug: "minimal-platform-starter-non-negotiables",
160
- excerpt: "A published note on keeping the foundation opinionated without being heavy.",
161
- content:
162
- "Even a minimal starter should include auth, structured modules, and a coherent app shell. Cutting those corners only moves the complexity into the first week of work.",
163
- status: "published",
164
- publishedAt: new Date(Date.now() - 1000 * 60 * 60 * 16),
165
- createdAt: new Date(Date.now() - 1000 * 60 * 60 * 18),
166
- },
167
- ]);
168
- }
169
-
170
- async function seed() {
171
- await syncDatabase();
172
- const user = await ensureDemoUser();
173
- const { organizationId, teamId } = await ensureOrganization(user.id);
174
-
175
- await seedPosts(user.id, organizationId, teamId);
176
-
177
- console.info(`Seed completed. Demo login: ${DEMO_EMAIL} / ${DEMO_PASSWORD}`);
178
- }
179
-
180
- void seed();
1
+ import { hashPassword } from "better-auth/crypto";
2
+ import { eq } from "drizzle-orm";
3
+ import { v4 as uuidv4 } from "uuid";
4
+ import { isRemote, orm, schema } from "./db";
5
+
6
+ const DEMO_EMAIL = "admin{{PACKAGE_SCOPE}}.local";
7
+ const DEMO_PASSWORD = "password1234";
8
+ const ORGANIZATION_ID = "{{APP_SLUG}}-org";
9
+ const TEAM_ID = "{{APP_SLUG}}-team";
10
+
11
+ async function ensureDemoUser() {
12
+ const [existingUser] = await orm
13
+ .select()
14
+ .from(schema.users)
15
+ .where(eq(schema.users.email, DEMO_EMAIL))
16
+ .limit(1);
17
+
18
+ if (existingUser) {
19
+ return existingUser;
20
+ }
21
+
22
+ const userId = uuidv4();
23
+ const [user] = await orm
24
+ .insert(schema.users)
25
+ .values({
26
+ id: userId,
27
+ name: "Demo Editor",
28
+ email: DEMO_EMAIL,
29
+ emailVerified: true,
30
+ role: "admin",
31
+ createdAt: new Date(),
32
+ updatedAt: new Date(),
33
+ })
34
+ .returning();
35
+
36
+ if (!user) {
37
+ throw new Error("Failed to create demo user.");
38
+ }
39
+
40
+ await orm.insert(schema.accounts).values({
41
+ id: uuidv4(),
42
+ accountId: user.id,
43
+ providerId: "credential",
44
+ userId: user.id,
45
+ password: await hashPassword(DEMO_PASSWORD),
46
+ createdAt: new Date(),
47
+ updatedAt: new Date(),
48
+ });
49
+
50
+ return user;
51
+ }
52
+
53
+ async function ensureOrganization(userId: string) {
54
+ const [existingOrganization] = await orm
55
+ .select()
56
+ .from(schema.organizations)
57
+ .where(eq(schema.organizations.id, ORGANIZATION_ID))
58
+ .limit(1);
59
+
60
+ if (!existingOrganization) {
61
+ await orm.insert(schema.organizations).values({
62
+ id: ORGANIZATION_ID,
63
+ name: "{{APP_NAME}}",
64
+ slug: "{{APP_SLUG}}",
65
+ type: "enterprise",
66
+ createdAt: new Date(),
67
+ });
68
+ }
69
+
70
+ const [existingMember] = await orm
71
+ .select()
72
+ .from(schema.members)
73
+ .where(eq(schema.members.userId, userId))
74
+ .limit(1);
75
+
76
+ if (!existingMember) {
77
+ await orm.insert(schema.members).values({
78
+ id: uuidv4(),
79
+ organizationId: ORGANIZATION_ID,
80
+ userId,
81
+ role: "owner",
82
+ createdAt: new Date(),
83
+ });
84
+ }
85
+
86
+ const [existingTeam] = await orm
87
+ .select()
88
+ .from(schema.teams)
89
+ .where(eq(schema.teams.id, TEAM_ID))
90
+ .limit(1);
91
+
92
+ if (!existingTeam) {
93
+ await orm.insert(schema.teams).values({
94
+ id: TEAM_ID,
95
+ name: "Editorial",
96
+ organizationId: ORGANIZATION_ID,
97
+ createdAt: new Date(),
98
+ updatedAt: new Date(),
99
+ });
100
+ }
101
+
102
+ const [existingTeamMember] = await orm
103
+ .select()
104
+ .from(schema.teamMembers)
105
+ .where(eq(schema.teamMembers.userId, userId))
106
+ .limit(1);
107
+
108
+ if (!existingTeamMember) {
109
+ await orm.insert(schema.teamMembers).values({
110
+ id: uuidv4(),
111
+ teamId: TEAM_ID,
112
+ userId,
113
+ role: "owner",
114
+ createdAt: new Date(),
115
+ });
116
+ }
117
+
118
+ return { organizationId: ORGANIZATION_ID, teamId: TEAM_ID };
119
+ }
120
+
121
+ async function seedPosts(userId: string, organizationId: string | null, teamId: string | null) {
122
+ const existingPosts = await orm.select().from(schema.posts).limit(1);
123
+ if (existingPosts.length > 0) {
124
+ return;
125
+ }
126
+
127
+ await orm.insert(schema.posts).values([
128
+ {
129
+ authorUserId: userId,
130
+ organizationId,
131
+ teamId,
132
+ title: "An editorial workflow you can ship in an afternoon",
133
+ slug: "editorial-workflow-in-an-afternoon",
134
+ excerpt: "A first draft on how this minimal starter is wired together.",
135
+ content:
136
+ "This starter keeps the moving parts visible. Shared contracts live in the shared app, business rules stay in services, and the web layer only does composition and UI work.",
137
+ status: "published",
138
+ publishedAt: new Date(),
139
+ createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5),
140
+ },
141
+ {
142
+ authorUserId: userId,
143
+ organizationId,
144
+ teamId,
145
+ title: "Three habits that keep CRUD apps from drifting into chaos",
146
+ slug: "three-habits-that-keep-crud{{PACKAGE_SCOPE}}s-focused",
147
+ excerpt: "A draft about explicit composition, typed contracts, and URL state.",
148
+ content:
149
+ "Keep your composition explicit, keep your schemas honest, and keep the URL involved in user intent. Those three choices do most of the architectural work in a small product.",
150
+ status: "draft",
151
+ createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 2),
152
+ },
153
+ {
154
+ authorUserId: userId,
155
+ organizationId,
156
+ teamId,
157
+ title: "What a minimal platform starter should still refuse to compromise on",
158
+ slug: "minimal-platform-starter-non-negotiables",
159
+ excerpt: "A published note on keeping the foundation opinionated without being heavy.",
160
+ content:
161
+ "Even a minimal starter should include auth, structured modules, and a coherent app shell. Cutting those corners only moves the complexity into the first week of work.",
162
+ status: "published",
163
+ publishedAt: new Date(Date.now() - 1000 * 60 * 60 * 16),
164
+ createdAt: new Date(Date.now() - 1000 * 60 * 60 * 18),
165
+ },
166
+ ]);
167
+ }
168
+
169
+ async function seed() {
170
+ if (isRemote) {
171
+ await orm.$client.sync();
172
+ }
173
+ const user = await ensureDemoUser();
174
+ const { organizationId, teamId } = await ensureOrganization(user.id);
175
+
176
+ await seedPosts(user.id, organizationId, teamId);
177
+
178
+ console.info(`Seed completed. Demo login: ${DEMO_EMAIL} / ${DEMO_PASSWORD}`);
179
+ }
180
+
181
+ void seed();