create-m5kdev 0.25.3 → 0.26.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 (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
@@ -1,193 +1,98 @@
1
- ---
2
- globs: apps/**/server/src/modules/**/*.repository.ts
3
- alwaysApply: false
4
- ---
5
-
6
- # Module Repository File Style Guide
7
-
8
- Conventions for repository classes in app modules.
9
-
10
- ## File Location
11
-
12
- ```
13
- apps/<app>/server/src/modules/<module>/<module>.repository.ts
14
- ```
15
-
16
- ## Conventions
17
-
18
- - Extend `BaseTableRepository<Orm, Schema, NestedRepos, Schema["table"]>` for table-bound modules.
19
- - Use `BaseRepository` for non-table or multi-table repositories.
20
- - Keep repositories data-focused: persistence, query composition, and lightweight mapping only.
21
- - Do not import service classes into repositories.
22
- - Use Drizzle operators (`and`, `eq`, `inArray`, `isNull`, `like`, etc.) and query helpers from base classes.
23
- - When running inside `this.orm.transaction`, use `tx` consistently; avoid mixing `this.orm` calls in the same transaction flow.
24
- - Leave orchestration, policy decisions, and side effects to services.
25
-
26
- ## Orm/Schema Type Pattern
27
-
28
- App repositories define their schema and ORM types inline from local table imports:
29
-
30
- ```typescript
31
- import type { LibSQLDatabase } from "drizzle-orm/libsql";
32
- import { posts } from "./posts.db";
33
-
34
- const schema = { posts };
35
- type Schema = typeof schema;
36
- type Orm = LibSQLDatabase<Schema>;
37
- ```
38
-
39
- This is passed to `BaseTableRepository` generics and provided by the module's `repositories()` hook via `db.orm` and `db.schema`.
40
-
41
- ## Built-in Base Methods
42
-
43
- `BaseTableRepository` provides typed CRUD helpers. Prefer them over hand-written queries when they fit:
44
-
45
- - `queryList(query, options)` — filtered/sorted/paginated lists.
46
- - `findById(id)` / `findManyById(ids)` — primary-key lookups.
47
- - `create(data)` / `createMany(data)` — inserts.
48
- - `update(data)` / `updateMany(data)` — updates.
49
- - `softDeleteById(id)` / `deleteById(id)` — deletes.
50
-
51
- Override only when custom joins, CTEs, or non-standard behavior is required.
52
-
53
- ## Error Handling
54
-
55
- - Use `throwableQuery` to wrap individual Drizzle queries. It converts thrown database errors into `ServerResultAsync`.
56
- - Do not wrap entire method bodies in `throwableAsync`. Keep `throwableQuery` scoped tightly around the database call so error-handling logic stays outside the wrapper.
57
- - Chain results with `isErr()` checks and return `err(result.error)` to propagate failures.
58
-
59
- ## Schema Type Imports
60
-
61
- Import schema types from the shared package for method signatures:
62
-
63
- ```typescript
64
- import type {
65
- PostSchema,
66
- PostsListInputSchema,
67
- PostsListOutputSchema,
68
- } from "@appname/shared/modules/posts/posts.schema";
69
- ```
70
-
71
- ## Example
72
-
73
- ```typescript
74
- import type {
75
- PostSchema,
76
- PostsListInputSchema,
77
- PostsListOutputSchema,
78
- } from "@appname/shared/modules/posts/posts.schema";
79
- import { BaseTableRepository } from "@m5kdev/backend/modules/base/base.repository";
80
- import type { ServerResultAsync } from "@m5kdev/backend/utils/types";
81
- import { and, asc, count, desc, eq, isNull, like, ne, or } from "drizzle-orm";
82
- import type { LibSQLDatabase } from "drizzle-orm/libsql";
83
- import { err, ok } from "neverthrow";
84
- import { posts } from "./posts.db";
85
-
86
- const schema = { posts };
87
- type Schema = typeof schema;
88
- type Orm = LibSQLDatabase<Schema>;
89
-
90
- export class PostsRepository extends BaseTableRepository<
91
- Orm,
92
- Schema,
93
- Record<string, never>,
94
- Schema["posts"]
95
- > {
96
- async list(input: PostsListInputSchema = {}): ServerResultAsync<PostsListOutputSchema> {
97
- const page = input.page ?? 1;
98
- const limit = input.limit ?? 6;
99
- const search = input.search?.trim();
100
- const conditions = [isNull(this.table.deletedAt)];
101
-
102
- if (input.status) {
103
- conditions.push(eq(this.table.status, input.status));
104
- }
105
-
106
- if (search) {
107
- const escapedSearch = search.replace(/[%_]/g, "\\$&");
108
- const pattern = `%${escapedSearch}%`;
109
- const searchCondition = or(
110
- like(this.table.title, pattern),
111
- like(this.table.slug, pattern),
112
- like(this.table.excerpt, pattern),
113
- like(this.table.content, pattern)
114
- );
115
- if (searchCondition) {
116
- conditions.push(searchCondition);
117
- }
118
- }
119
-
120
- const whereClause = and(...conditions);
121
- const ordering =
122
- input.sort === "title"
123
- ? input.order === "asc"
124
- ? asc(this.table.title)
125
- : desc(this.table.title)
126
- : desc(this.table.createdAt);
127
-
128
- const rowsQuery = this.orm
129
- .select()
130
- .from(this.table)
131
- .where(whereClause)
132
- .orderBy(ordering, desc(this.table.createdAt))
133
- .limit(limit)
134
- .offset((page - 1) * limit);
135
-
136
- const countQuery = this.orm.select({ count: count() }).from(this.table).where(whereClause);
137
-
138
- const result = await this.throwableQuery(() => Promise.all([rowsQuery, countQuery]));
139
- if (result.isErr()) return err(result.error);
140
- const [rows, [totalRow]] = result.value;
141
-
142
- return ok({
143
- rows: rows as PostSchema[],
144
- total: totalRow?.count ?? 0,
145
- });
146
- }
147
-
148
- async findBySlug(slug: string, excludeId?: string) {
149
- const whereClause = excludeId
150
- ? and(eq(this.table.slug, slug), ne(this.table.id, excludeId), isNull(this.table.deletedAt))
151
- : and(eq(this.table.slug, slug), isNull(this.table.deletedAt));
152
-
153
- const result = await this.throwableQuery(() =>
154
- this.orm.select().from(this.table).where(whereClause).limit(1)
155
- );
156
- if (result.isErr()) return err(result.error);
157
- const [row] = result.value;
158
- return ok(row);
159
- }
160
- }
161
- ```
162
-
163
- ## When to Use `async` Methods vs Query Builder
164
-
165
- Use regular `async` methods (as shown above) when:
166
-
167
- - The method requires an optional `tx` parameter for transaction support.
168
- - The method orchestrates multiple sequential queries with branching logic between them.
169
- - The method wraps a `this.orm.transaction()` call.
170
-
171
- Use `this.query()` builder for simpler single-query methods with schema-typed input/output:
172
-
173
- ```typescript
174
- findBySlug = this.query("findBySlug")
175
- .input(z.object({ slug: z.string() }))
176
- .output(postSchema.nullable())
177
- .handle(async ({ slug }) => {
178
- const result = await this.throwableQuery(() =>
179
- this.orm.select().from(this.table).where(eq(this.table.slug, slug)).limit(1)
180
- );
181
- if (result.isErr()) return err(result.error);
182
- const [row] = result.value;
183
- return row ?? null;
184
- });
185
- ```
186
-
187
- ## Best Practices
188
-
189
- 1. Prefer built-in `BaseTableRepository` methods (`findById`, `create`, `update`, `softDeleteById`) over hand-written queries for standard CRUD.
190
- 2. Use `throwableQuery` around each individual Drizzle query, not around the whole method body.
191
- 3. Filter soft-deleted rows with `isNull(this.table.deletedAt)` in custom queries.
192
- 4. Accept optional `tx?: Orm` parameter in methods that participate in transactions.
193
- 5. Return `ServerResultAsync` consistently from all repository methods.
1
+ ---
2
+ globs: apps/**/server/src/modules/**/*.repository.ts
3
+ alwaysApply: false
4
+ ---
5
+
6
+ # Module Repository File Style Guide
7
+
8
+ Conventions for repository classes in app modules. Repositories consume the
9
+ module DTO object (`postSchemas`) for query inputs/outputs — they do not
10
+ import hand-written types.
11
+
12
+ ## File Location
13
+
14
+ ```
15
+ apps/<app>/server/src/modules/<module>/<module>.repository.ts
16
+ ```
17
+
18
+ ## Conventions
19
+
20
+ - Extend `BaseTableRepository<Orm, Schema, NestedRepos, Schema["table"]>` for table-bound modules.
21
+ - Use `BaseRepository` for non-table or multi-table repositories.
22
+ - Keep repositories data-focused: persistence, query composition, and lightweight mapping only.
23
+ - Do not import service classes into repositories.
24
+ - Use Drizzle operators (`and`, `eq`, `inArray`, `isNull`, `like`, etc.) and query helpers from base classes.
25
+ - When running inside `this.orm.transaction`, use `tx` consistently; avoid mixing `this.orm` calls in the same transaction flow.
26
+ - Leave orchestration, policy decisions, and side effects to services.
27
+
28
+ ## Orm/Schema Type Pattern
29
+
30
+ ```typescript
31
+ import type { LibSQLDatabase } from "drizzle-orm/libsql";
32
+ import { posts } from "./posts.db";
33
+
34
+ const schema = { posts };
35
+ type Schema = typeof schema;
36
+ type Orm = LibSQLDatabase<Schema>;
37
+ ```
38
+
39
+ ## Built-in Base Methods — write nothing you get for free
40
+
41
+ `BaseTableRepository` provides typed CRUD. Prefer these over hand-written queries:
42
+
43
+ - `queryList(query, options)` — the generic list contract (`page`, `limit`,
44
+ `sort`, `order`, `q`, `filters`) translated to Drizzle. Pass
45
+ `globalSearchColumns` to enable `q`, `conditions` for extra scoping, and
46
+ `showDeleted` to include soft-deleted rows (excluded by default when the
47
+ table has `deletedAt`). Do NOT hand-write list/filter/count queries.
48
+ - `findById(id)` / `findManyById(ids)` — primary-key lookups.
49
+ - `create(data)` / `createMany(data)` — inserts.
50
+ - `update(data)` / `updateMany(data)` — updates.
51
+ - `softDeleteById(id)` / `deleteById(id)` — deletes.
52
+
53
+ ```typescript
54
+ // a full filtered/searched/paginated list — no custom SQL
55
+ this.repository.posts.queryList(input, {
56
+ globalSearchColumns: ["title", "excerpt", "content"],
57
+ });
58
+ ```
59
+
60
+ ## Custom queries: the `this.query()` builder
61
+
62
+ Single-purpose queries are declared with the builder, using DTO schemas for
63
+ outputs. **Outputs are validated at runtime** — a find-style query that can
64
+ return no row must use `output.found` (the optional row schema), or
65
+ validation will fail on the legitimate "not found" case.
66
+
67
+ ```typescript
68
+ import { z } from "zod";
69
+ import { postSchemas } from "./posts.dto";
70
+
71
+ findBySlug = this.query("findBySlug")
72
+ .input(z.object({ slug: z.string(), excludeId: z.string().optional() }))
73
+ .output(postSchemas.output.found) // NOT output.single — may return undefined
74
+ .handle(async ({ slug, excludeId }) => {
75
+ const whereClause = excludeId
76
+ ? and(eq(this.table.slug, slug), ne(this.table.id, excludeId), isNull(this.table.deletedAt))
77
+ : and(eq(this.table.slug, slug), isNull(this.table.deletedAt));
78
+
79
+ const result = await this.throwableQuery(() =>
80
+ this.orm.select().from(this.table).where(whereClause).limit(1)
81
+ );
82
+ if (result.isErr()) return err(result.error);
83
+ const [row] = result.value;
84
+ return ok(row);
85
+ });
86
+ ```
87
+
88
+ Use plain `async` methods instead of the builder only when the method needs an
89
+ optional `tx` parameter, wraps `this.orm.transaction()`, or orchestrates
90
+ multiple queries with branching between them.
91
+
92
+ ## Error Handling
93
+
94
+ - Use `throwableQuery` tightly around each Drizzle call — it converts thrown
95
+ database errors into `ServerResultAsync`. Do not wrap whole method bodies.
96
+ - Chain results with `isErr()` checks and `err(result.error)`.
97
+ - Filter soft-deleted rows with `isNull(this.table.deletedAt)` in custom
98
+ queries (`queryList` does this automatically).
@@ -1,173 +1,53 @@
1
- ---
2
- globs: apps/**/shared/src/modules/**/*.schema.ts
3
- alwaysApply: false
4
- ---
5
-
6
- # Module Schema File Style Guide
7
-
8
- Conventions for shared Zod schema files that define the contract between server and client.
9
-
10
- ## File Location
11
-
12
- ```
13
- apps/<app>/shared/src/modules/<module>/<module>.schema.ts
14
- ```
15
-
16
- Companion constants file:
17
-
18
- ```
19
- apps/<app>/shared/src/modules/<module>/<module>.constants.ts
20
- ```
21
-
22
- ## Core Philosophy
23
-
24
- - Define all Zod schemas and their inferred types in the shared package so both server and webapp can import them.
25
- - Import constants (enum values, page sizes) from the companion `<module>.constants.ts` file.
26
- - Extend `querySchema` from `@m5kdev/commons/modules/schemas/query.schema` for list inputs that need pagination, sorting, and filtering.
27
- - Export both the schema and its inferred `type` for every schema.
28
-
29
- ## Naming Conventions
30
-
31
- | Kind | Pattern | Example |
32
- |---|---|---|
33
- | Entity schema | `{entity}Schema` | `postSchema` |
34
- | Entity type | `{Entity}Schema` | `PostSchema` |
35
- | Enum schema | `{entity}{Field}Schema` | `postStatusSchema` |
36
- | List input | `{entities}ListInputSchema` | `postsListInputSchema` |
37
- | List output | `{entities}ListOutputSchema` | `postsListOutputSchema` |
38
- | Operation input | `{entity}{Action}InputSchema` | `postCreateInputSchema` |
39
- | Operation output | `{entity}{Action}OutputSchema` | `postCreateOutputSchema` |
40
-
41
- ## File Structure
42
-
43
- ```typescript
44
- import { querySchema } from "@m5kdev/commons/modules/schemas/query.schema";
45
- import { z } from "zod";
46
- import { STATUS_VALUES } from "./<module>.constants";
47
-
48
- // 1. Enum schemas (from constants)
49
- // 2. Main entity schema
50
- // 3. List input/output schemas
51
- // 4. Operation input/output schemas
52
- ```
53
-
54
- ## Section 1: Enum Schemas
55
-
56
- Derive Zod enums from constant tuples defined in the constants file:
57
-
58
- ```typescript
59
- export const postStatusSchema = z.enum(POST_STATUS_VALUES);
60
- export type PostStatusSchema = z.infer<typeof postStatusSchema>;
61
- ```
62
-
63
- ## Section 2: Main Entity Schema
64
-
65
- Define the full entity shape as a `z.object`. This is the canonical representation of the entity:
66
-
67
- ```typescript
68
- export const postSchema = z.object({
69
- id: z.string(),
70
- authorUserId: z.string().nullish(),
71
- organizationId: z.string().nullish(),
72
- teamId: z.string().nullish(),
73
- title: z.string(),
74
- slug: z.string(),
75
- excerpt: z.string().nullish(),
76
- content: z.string(),
77
- status: postStatusSchema,
78
- publishedAt: z.date().nullish(),
79
- createdAt: z.date(),
80
- updatedAt: z.date().nullish(),
81
- deletedAt: z.date().nullish(),
82
- });
83
- export type PostSchema = z.infer<typeof postSchema>;
84
- ```
85
-
86
- ## Section 3: List Input/Output Schemas
87
-
88
- Extend `querySchema` for list inputs with module-specific filters. Define list output as `{ rows, total }`:
89
-
90
- ```typescript
91
- export const postsListInputSchema = querySchema.extend({
92
- search: z.string().optional(),
93
- status: postStatusSchema.optional(),
94
- });
95
- export type PostsListInputSchema = z.infer<typeof postsListInputSchema>;
96
-
97
- export const postsListOutputSchema = z.object({
98
- rows: z.array(postSchema),
99
- total: z.number(),
100
- });
101
- export type PostsListOutputSchema = z.infer<typeof postsListOutputSchema>;
102
- ```
103
-
104
- ## Section 4: Operation Input/Output Schemas
105
-
106
- Define per-operation schemas. Output schemas can reuse the main entity schema or a subset:
107
-
108
- ```typescript
109
- export const postCreateInputSchema = z.object({
110
- title: z.string().min(1),
111
- slug: z.string().optional(),
112
- excerpt: z.string().optional(),
113
- content: z.string().min(1),
114
- });
115
- export type PostCreateInputSchema = z.infer<typeof postCreateInputSchema>;
116
-
117
- export const postCreateOutputSchema = postSchema;
118
- export type PostCreateOutputSchema = z.infer<typeof postCreateOutputSchema>;
119
- ```
120
-
121
- ## Patterns
122
-
123
- ### Reuse Entity Schema for Output
124
-
125
- When the operation returns the full entity, alias the main schema:
126
-
127
- ```typescript
128
- export const postUpdateOutputSchema = postSchema;
129
- ```
130
-
131
- ### Minimal Operation Schemas
132
-
133
- For operations that take only an ID and return a subset:
134
-
135
- ```typescript
136
- export const postSoftDeleteInputSchema = z.object({
137
- id: z.string(),
138
- });
139
-
140
- export const postSoftDeleteOutputSchema = z.object({
141
- id: z.string(),
142
- });
143
- ```
144
-
145
- ### Omitting Fields for Lighter Outputs
146
-
147
- Use `.omit()` or `.pick()` on the main schema to create lighter variants:
148
-
149
- ```typescript
150
- export const postSimpleSchema = postSchema.omit({
151
- content: true,
152
- deletedAt: true,
153
- });
154
- ```
155
-
156
- ## Constants File Conventions
157
-
158
- Keep enum values, filter presets, and page sizes in `<module>.constants.ts`:
159
-
160
- ```typescript
161
- export const POST_STATUS_VALUES = ["draft", "published"] as const;
162
- export const POST_FILTER_VALUES = ["all", ...POST_STATUS_VALUES] as const;
163
- export const POSTS_PAGE_SIZE = 6;
164
- ```
165
-
166
- ## Best Practices
167
-
168
- 1. Always export both the Zod schema and its inferred type.
169
- 2. Use `.nullish()` for optional nullable fields, `.optional()` for optional-only.
170
- 3. Extend `querySchema` for list inputs rather than redefining pagination fields.
171
- 4. Reuse the main entity schema as operation output when the full entity is returned.
172
- 5. Keep validation rules (`.min()`, `.max()`, `.regex()`) in input schemas.
173
- 6. Keep schemas flat and avoid deep nesting when possible.
1
+ ---
2
+ globs: apps/**/shared/src/modules/**/*.schema.ts
3
+ alwaysApply: false
4
+ ---
5
+
6
+ # Shared Schema Style Guide
7
+
8
+ Shared schemas have a **deliberately narrow role**. Do not mirror server types
9
+ here — types flow from the server:
10
+
11
+ - Row and list types are **inferred from the tRPC router**:
12
+ `inferRouterOutputs<AppRouter>` with `AppRouter` from
13
+ `{{PACKAGE_SCOPE}}/server/types`.
14
+ - The API surface (inputs/outputs) is defined by the server module's DTO
15
+ (`<module>.dto.ts`), derived from the Drizzle table.
16
+
17
+ ## When a schema belongs in shared
18
+
19
+ Put a Zod schema here only when at least one of these holds:
20
+
21
+ 1. **The frontend needs it at runtime** — e.g. validating a form beyond native
22
+ HTML validation.
23
+ 2. **Both sides genuinely share it** — then the server DTO must import it
24
+ (`postSchemas.input.create = postCreateInputSchema`), so the API contract
25
+ and the frontend validation are the same object and cannot drift.
26
+ 3. **It cannot be re-exported from the server** because the server version
27
+ depends on Drizzle (`drizzle-zod` schemas) and shipping that dependency to
28
+ the client is not acceptable.
29
+
30
+ Everything else — row schemas, list outputs, trivial `{ id }` inputs — must
31
+ NOT be duplicated in shared. Hand-written mirrors silently drift from the real
32
+ contract.
33
+
34
+ ```ts
35
+ // GOOD — form-validation schema, imported by both the DTO and the webapp form
36
+ export const postCreateInputSchema = z.object({
37
+ title: z.string().min(1),
38
+ slug: z.string().optional(),
39
+ excerpt: z.string().optional(),
40
+ content: z.string().min(1),
41
+ });
42
+ export type PostCreateInputSchema = z.infer<typeof postCreateInputSchema>;
43
+
44
+ // BAD — hand-mirrored row schema; the table already defines this shape
45
+ export const postSchema = z.object({ id: z.string(), title: z.string(), /* ... */ });
46
+ ```
47
+
48
+ ## Conventions
49
+
50
+ - File location: `apps/shared/src/modules/<module>/<module>.schema.ts`, with
51
+ enum values and constants in the companion `<module>.constants.ts`.
52
+ - Export both the schema and its inferred `type`.
53
+ - Build enums from constants: `z.enum(POST_STATUS_VALUES)`.