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.
- package/dist/src/create.cjs +18 -1
- package/dist/src/create.cjs.map +1 -1
- package/dist/src/fs.cjs +65 -5
- package/dist/src/fs.cjs.map +1 -1
- package/dist/src/fs.d.cts +13 -2
- package/dist/src/index.cjs +0 -0
- package/dist/src/paths.cjs +2 -2
- package/dist/src/prompts.cjs +22 -0
- package/dist/src/prompts.cjs.map +1 -1
- package/dist/src/runCli.cjs +13 -2
- package/dist/src/runCli.cjs.map +1 -1
- package/dist/src/types.d.cts +9 -1
- package/package.json +8 -6
- package/templates/minimal-app/.cursor/rules/frontend-data-guide.mdc +46 -3
- package/templates/minimal-app/.cursor/rules/module-db-guide.mdc +15 -1
- package/templates/minimal-app/.cursor/rules/module-dto-guide.mdc +60 -0
- package/templates/minimal-app/.cursor/rules/module-repository-guide.mdc +98 -193
- package/templates/minimal-app/.cursor/rules/module-schema-guide.mdc +53 -173
- package/templates/minimal-app/.cursor/rules/module-service-guide.mdc +133 -240
- package/templates/minimal-app/.cursor/rules/module-trpc-guide.mdc +71 -113
- package/templates/minimal-app/README.md.tpl +3 -3
- package/templates/minimal-app/apps/.gitignore.tpl +13 -0
- package/templates/minimal-app/apps/e2e/package.json.tpl +23 -0
- package/templates/minimal-app/apps/e2e/playwright.config.ts.tpl +39 -0
- package/templates/minimal-app/apps/e2e/playwright.expo.config.ts.tpl +38 -0
- package/templates/minimal-app/apps/e2e/servers.ts.tpl +100 -0
- package/templates/minimal-app/apps/e2e/tests/email.spec.ts.tpl +47 -0
- package/templates/minimal-app/apps/e2e/tests/expo-helpers.ts.tpl +73 -0
- package/templates/minimal-app/apps/e2e/tests/expo-standard.spec.ts.tpl +517 -0
- package/templates/minimal-app/apps/e2e/tests/expo-waitlist.spec.ts.tpl +94 -0
- package/templates/minimal-app/apps/e2e/tests/helpers.ts.tpl +289 -0
- package/templates/minimal-app/apps/e2e/tests/standard.spec.ts.tpl +516 -0
- package/templates/minimal-app/apps/e2e/tests/waitlist.spec.ts.tpl +107 -0
- package/templates/minimal-app/apps/e2e/tsconfig.json.tpl +10 -0
- package/templates/minimal-app/apps/email/package.json.tpl +34 -34
- package/templates/minimal-app/apps/email/src/emails/accountDeletionEmail.tsx.tpl +35 -34
- package/templates/minimal-app/apps/email/src/emails/organizationInviteEmail.tsx.tpl +34 -33
- package/templates/minimal-app/apps/email/src/emails/passwordResetEmail.tsx.tpl +35 -34
- package/templates/minimal-app/apps/email/src/emails/verificationEmail.tsx.tpl +37 -36
- package/templates/minimal-app/apps/email/src/emails/waitlistEmails.tsx.tpl +87 -0
- package/templates/minimal-app/apps/email/src/index.ts.tpl +72 -32
- package/templates/minimal-app/apps/email/src/resources.ts.tpl +65 -36
- package/templates/minimal-app/apps/email/tsconfig.json.tpl +16 -11
- package/templates/minimal-app/apps/expo/.gitignore.tpl +44 -0
- package/templates/minimal-app/apps/expo/.prettierignore +13 -0
- package/templates/minimal-app/apps/expo/.prettierrc.json.tpl +9 -0
- package/templates/minimal-app/apps/expo/CHANGELOG.md.tpl +156 -0
- package/templates/minimal-app/apps/expo/README.md.tpl +44 -0
- package/templates/minimal-app/apps/expo/app.json.tpl +36 -0
- package/templates/minimal-app/apps/expo/assets/images/adaptive-icon.png +0 -0
- package/templates/minimal-app/apps/expo/assets/images/favicon.png +0 -0
- package/templates/minimal-app/apps/expo/assets/images/icon.png +0 -0
- package/templates/minimal-app/apps/expo/assets/images/splash-icon.png +0 -0
- package/templates/minimal-app/apps/expo/eslint.config.js.tpl +8 -0
- package/templates/minimal-app/apps/expo/expo-env.d.ts.tpl +3 -0
- package/templates/minimal-app/apps/expo/metro.config.js.tpl +16 -0
- package/templates/minimal-app/apps/expo/package.json.tpl +59 -0
- package/templates/minimal-app/apps/expo/src/app/_layout.tsx.tpl +31 -0
- package/templates/minimal-app/apps/expo/src/app/claim-account.tsx.tpl +151 -0
- package/templates/minimal-app/apps/expo/src/app/forgot-password.tsx.tpl +53 -0
- package/templates/minimal-app/apps/expo/src/app/index.tsx.tpl +8 -0
- package/templates/minimal-app/apps/expo/src/app/login.tsx.tpl +71 -0
- package/templates/minimal-app/apps/expo/src/app/logout.tsx.tpl +19 -0
- package/templates/minimal-app/apps/expo/src/app/organization/accept-invitation.tsx.tpl +85 -0
- package/templates/minimal-app/apps/expo/src/app/posts.tsx.tpl +40 -0
- package/templates/minimal-app/apps/expo/src/app/reset-password.tsx.tpl +75 -0
- package/templates/minimal-app/apps/expo/src/app/signup.tsx.tpl +193 -0
- package/templates/minimal-app/apps/expo/src/components/AuthScaffold.tsx.tpl +34 -0
- package/templates/minimal-app/apps/expo/src/components/FormTextField.tsx.tpl +37 -0
- package/templates/minimal-app/apps/expo/src/components/LoadingScreen.tsx.tpl +10 -0
- package/templates/minimal-app/apps/expo/src/components/StatusText.tsx.tpl +22 -0
- package/templates/minimal-app/apps/expo/src/config.ts.tpl +9 -0
- package/templates/minimal-app/apps/expo/src/global.css.tpl +11 -0
- package/templates/minimal-app/apps/expo/src/lib/trpc.ts.tpl +20 -0
- package/templates/minimal-app/apps/expo/tsconfig.json.tpl +11 -0
- package/templates/minimal-app/apps/expo/uniwind-env.d.ts.tpl +1 -0
- package/templates/minimal-app/apps/expo/uniwind-types.d.ts.tpl +10 -0
- package/templates/minimal-app/apps/server/AGENTS.md.tpl +39 -39
- package/templates/minimal-app/apps/server/drizzle/0000_wealthy_absorbing_man.sql +268 -0
- package/templates/minimal-app/apps/server/drizzle/db.ts.tpl +26 -0
- package/templates/minimal-app/apps/server/drizzle/meta/0000_snapshot.json.tpl +1890 -0
- package/templates/minimal-app/apps/server/drizzle/meta/_journal.json.tpl +13 -0
- package/templates/minimal-app/apps/server/drizzle/reset.ts.tpl +33 -0
- package/templates/minimal-app/apps/server/drizzle/seed.e2e.ts.tpl +244 -0
- package/templates/minimal-app/apps/server/drizzle/seed.ts.tpl +181 -180
- package/templates/minimal-app/apps/server/drizzle/sync.ts.tpl +14 -21
- package/templates/minimal-app/apps/server/drizzle.config.ts.tpl +33 -35
- package/templates/minimal-app/apps/server/package.json.tpl +56 -52
- package/templates/minimal-app/apps/server/src/app.ts.tpl +142 -126
- package/templates/minimal-app/apps/server/src/index.ts.tpl +58 -58
- package/templates/minimal-app/apps/server/src/lib/auth.ts.tpl +12 -12
- package/templates/minimal-app/apps/server/src/modules/posts/posts.db.ts.tpl +25 -25
- package/templates/minimal-app/apps/server/src/modules/posts/posts.dto.ts.tpl +23 -0
- package/templates/minimal-app/apps/server/src/modules/posts/posts.grants.ts.tpl +30 -40
- package/templates/minimal-app/apps/server/src/modules/posts/posts.module.ts.tpl +54 -54
- package/templates/minimal-app/apps/server/src/modules/posts/posts.repository.ts.tpl +67 -112
- package/templates/minimal-app/apps/server/src/modules/posts/posts.service.ts.tpl +104 -170
- package/templates/minimal-app/apps/server/src/modules/posts/posts.trpc.ts.tpl +37 -48
- package/templates/minimal-app/apps/server/src/modules/posts/posts.utils.ts.tpl +19 -0
- package/templates/minimal-app/apps/server/src/modules/test-harness/test-harness.module.ts.tpl +279 -0
- package/templates/minimal-app/apps/server/src/schema.ts.tpl +20 -0
- package/templates/minimal-app/apps/server/src/types.ts.tpl +1 -1
- package/templates/minimal-app/apps/server/tsconfig.json.tpl +20 -14
- package/templates/minimal-app/apps/server/tsup.config.ts.tpl +9 -9
- package/templates/minimal-app/apps/shared/package.json.tpl +24 -24
- package/templates/minimal-app/apps/shared/src/modules/app/app.constants.ts.tpl +28 -2
- package/templates/minimal-app/apps/shared/src/modules/notification/notification.schema.ts.tpl +3 -3
- package/templates/minimal-app/apps/shared/src/modules/posts/posts.constants.ts.tpl +5 -5
- package/templates/minimal-app/apps/shared/src/modules/posts/posts.schema.ts.tpl +25 -76
- package/templates/minimal-app/apps/shared/tsconfig.json.tpl +16 -12
- package/templates/minimal-app/apps/webapp/index.html.tpl +22 -22
- package/templates/minimal-app/apps/webapp/package.json.tpl +53 -52
- package/templates/minimal-app/apps/webapp/public/{push-sw.js → push-sw.js.tpl} +34 -34
- package/templates/minimal-app/apps/webapp/src/App.tsx.tpl +44 -13
- package/templates/minimal-app/apps/webapp/src/Layout.tsx.tpl +18 -12
- package/templates/minimal-app/apps/webapp/src/Router.tsx.tpl +33 -40
- package/templates/minimal-app/apps/webapp/src/index.css.tpl +15 -75
- package/templates/minimal-app/apps/webapp/src/main.tsx.tpl +26 -26
- package/templates/minimal-app/apps/webapp/src/modules/posts/PostsRoute.tsx.tpl +278 -307
- package/templates/minimal-app/apps/webapp/src/modules/posts/components/PostCard.tsx.tpl +96 -138
- package/templates/minimal-app/apps/webapp/src/modules/posts/components/PostEditorModal.tsx.tpl +118 -124
- package/templates/minimal-app/apps/webapp/src/modules/posts/hooks/usePostActions.ts.tpl +108 -116
- package/templates/minimal-app/apps/webapp/src/modules/posts/hooks/usePostsList.ts.tpl +15 -9
- package/templates/minimal-app/apps/webapp/src/modules/posts/hooks/usePostsRoute.ts.tpl +13 -13
- package/templates/minimal-app/apps/webapp/src/modules/posts/posts.utils.ts.tpl +16 -16
- package/templates/minimal-app/apps/webapp/src/utils/i18n.ts.tpl +13 -13
- package/templates/minimal-app/apps/webapp/src/utils/trpc.ts.tpl +10 -6
- package/templates/minimal-app/apps/webapp/src/vite-env.d.ts.tpl +1 -1
- package/templates/minimal-app/apps/webapp/translations/{app.json.tpl → en/app.json.tpl} +7 -7
- package/templates/minimal-app/apps/webapp/translations/en/{blog-app.json.tpl → {{APP_SLUG}}.json.tpl} +135 -135
- package/templates/minimal-app/apps/webapp/tsconfig.json.tpl +17 -17
- package/templates/minimal-app/apps/webapp/vite.config.ts.tpl +31 -31
- package/templates/minimal-app/template.manifest.json +21 -0
- package/templates/minimal-app/apps/server/drizzle/generate-schema.ts.tpl +0 -31
- package/templates/minimal-app/apps/server/src/db.ts.tpl +0 -7
- package/templates/minimal-app/apps/server/src/generated/schema.ts.tpl +0 -13
- package/templates/minimal-app/apps/server/src/utils/trpc.ts.tpl +0 -8
- package/templates/minimal-app/apps/shared/src/modules/app/locale.constants.ts.tpl +0 -7
- package/templates/minimal-app/apps/shared/src/modules/app/roles.constants.ts.tpl +0 -17
- package/templates/minimal-app/apps/webapp/src/Providers.tsx.tpl +0 -36
- package/templates/minimal-app/apps/webapp/src/modules/notification/PushNotificationsPanel.tsx.tpl +0 -89
- package/templates/minimal-app/apps/webapp/src/navigation/navigation.config.tsx.tpl +0 -85
- /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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
`
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
- `
|
|
49
|
-
- `
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
} from "
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
#
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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)`.
|