create-m5kdev 0.41.0 → 0.41.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 (61) hide show
  1. package/package.json +1 -1
  2. package/templates/minimal-app/.cursor/rules/frontend-component-guide.mdc +95 -95
  3. package/templates/minimal-app/.cursor/rules/frontend-data-guide.mdc +103 -103
  4. package/templates/minimal-app/.cursor/rules/frontend-form-guide.mdc +80 -80
  5. package/templates/minimal-app/.cursor/rules/frontend-hook-guide.mdc +79 -79
  6. package/templates/minimal-app/.cursor/rules/frontend-i18n-guide.mdc +45 -45
  7. package/templates/minimal-app/.cursor/rules/module-dto-guide.mdc +60 -60
  8. package/templates/minimal-app/.cursor/rules/module-prompt-guide.mdc +214 -214
  9. package/templates/minimal-app/.cursor/rules/module-repository-guide.mdc +167 -167
  10. package/templates/minimal-app/.cursor/rules/module-schema-guide.mdc +53 -53
  11. package/templates/minimal-app/.cursor/rules/module-service-guide.mdc +367 -367
  12. package/templates/minimal-app/.cursor/rules/module-trpc-guide.mdc +71 -71
  13. package/templates/minimal-app/.github/workflows/check.yml.tpl +21 -0
  14. package/templates/minimal-app/AGENTS.md.tpl +22 -31
  15. package/templates/minimal-app/CLAUDE.md.tpl +15 -1
  16. package/templates/minimal-app/GLOSSARY.md.tpl +181 -0
  17. package/templates/minimal-app/README.md.tpl +2 -0
  18. package/templates/minimal-app/apps/.gitignore.tpl +13 -13
  19. package/templates/minimal-app/apps/e2e/package.json.tpl +23 -23
  20. package/templates/minimal-app/apps/email/package.json.tpl +34 -34
  21. package/templates/minimal-app/apps/expo/.gitignore.tpl +44 -44
  22. package/templates/minimal-app/apps/expo/.prettierignore +13 -13
  23. package/templates/minimal-app/apps/expo/.prettierrc.json.tpl +9 -9
  24. package/templates/minimal-app/apps/expo/CHANGELOG.md.tpl +156 -156
  25. package/templates/minimal-app/apps/expo/README.md.tpl +44 -44
  26. package/templates/minimal-app/apps/expo/expo-env.d.ts.tpl +3 -0
  27. package/templates/minimal-app/apps/expo/package.json.tpl +63 -63
  28. package/templates/minimal-app/apps/expo/uniwind-types.d.ts.tpl +10 -0
  29. package/templates/minimal-app/apps/landing/package.json.tpl +34 -34
  30. package/templates/minimal-app/apps/server/drizzle/0000_whole_echo.sql +366 -366
  31. package/templates/minimal-app/apps/server/drizzle/0002_crazy_lilith.sql +46 -46
  32. package/templates/minimal-app/apps/server/drizzle/0003_freezing_fantastic_four.sql +3 -3
  33. package/templates/minimal-app/apps/server/drizzle/0004_slow_ezekiel_stane.sql +98 -98
  34. package/templates/minimal-app/apps/server/drizzle/0005_wonderful_the_professor.sql +1 -1
  35. package/templates/minimal-app/apps/server/drizzle/0006_lazy_blue_marvel.sql +158 -158
  36. package/templates/minimal-app/apps/server/drizzle/0007_abnormal_ultimo.sql +1 -1
  37. package/templates/minimal-app/apps/server/drizzle/0008_steady_nekra.sql +86 -86
  38. package/templates/minimal-app/apps/server/drizzle/meta/0000_snapshot.json.tpl +2624 -2624
  39. package/templates/minimal-app/apps/server/drizzle/meta/0001_snapshot.json.tpl +2670 -2670
  40. package/templates/minimal-app/apps/server/drizzle/meta/0002_snapshot.json.tpl +2715 -2715
  41. package/templates/minimal-app/apps/server/drizzle/meta/0003_snapshot.json.tpl +2687 -2687
  42. package/templates/minimal-app/apps/server/drizzle/meta/0004_snapshot.json.tpl +2497 -2497
  43. package/templates/minimal-app/apps/server/drizzle/meta/0005_snapshot.json.tpl +2483 -2483
  44. package/templates/minimal-app/apps/server/drizzle/meta/0006_snapshot.json.tpl +3602 -3602
  45. package/templates/minimal-app/apps/server/drizzle/meta/0007_snapshot.json.tpl +3619 -3619
  46. package/templates/minimal-app/apps/server/drizzle/meta/0008_snapshot.json.tpl +3619 -3619
  47. package/templates/minimal-app/apps/server/drizzle/meta/_journal.json.tpl +76 -76
  48. package/templates/minimal-app/apps/server/package.json.tpl +57 -57
  49. package/templates/minimal-app/apps/server/src/app.ts.tpl +1 -0
  50. package/templates/minimal-app/apps/shared/package.json.tpl +27 -27
  51. package/templates/minimal-app/apps/webapp/AGENTS.md.tpl +24 -24
  52. package/templates/minimal-app/apps/webapp/index.html.tpl +22 -22
  53. package/templates/minimal-app/apps/webapp/package.json.tpl +53 -53
  54. package/templates/minimal-app/apps/webapp/public/logo.svg.tpl +6 -6
  55. package/templates/minimal-app/biome.json.tpl +1 -0
  56. package/templates/minimal-app/docs/adr/.gitkeep +0 -0
  57. package/templates/minimal-app/docs/agents/domain.md.tpl +38 -0
  58. package/templates/minimal-app/docs/agents/issue-tracker.md.tpl +45 -0
  59. package/templates/minimal-app/docs/agents/triage-labels.md.tpl +15 -0
  60. package/templates/minimal-app/pnpm-workspace.yaml.tpl +12 -12
  61. package/templates/minimal-app/template.manifest.json +12 -0
@@ -1,167 +1,167 @@
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
- ## Prefer Built-in Base Methods (Strong Default)
40
-
41
- `BaseTableRepository` provides typed CRUD helpers. **Always use these before
42
- adding custom repository methods or raw Drizzle.** Services should call these
43
- helpers directly in most cases.
44
-
45
- - `queryList(query, options)` — filtered/sorted/paginated lists (`page`,
46
- `limit`, `sort`, `order`, `q`, `filters`). Pass `globalSearchColumns` for
47
- `q`, `conditions` for extra scoping, `showDeleted` to include soft-deleted
48
- rows (excluded by default when the table has `deletedAt`).
49
- - `queryFind(query, options)` — filtered/sorted single-row lookup (no
50
- pagination/count). Same filter/`conditions` contract as `queryList`.
51
- - `findById(id)` / `findManyById(ids)` — primary-key lookups.
52
- - `create(data)` / `createMany(data)` — inserts.
53
- - `update(data)` / `updateMany(data)` — updates.
54
- - `softDeleteById(id)` / `deleteById(id)` — deletes.
55
-
56
- ```typescript
57
- // Preferred from the service — no custom repository list method
58
- this.repository.posts.queryList(input, {
59
- globalSearchColumns: ["title", "excerpt", "content"],
60
- });
61
- ```
62
-
63
- ### Usually Do Not Add Custom List / Getter / Create / Update Methods
64
-
65
- - **Lists and getters**: `queryList` / `queryFind` already expose read APIs
66
- via `filters` (and optional `conditions`). Prefer calling them from the
67
- service with the needed filters (or `.addFilters` on the procedure). Do not
68
- add thin `listPublished`, `findBySlug`, `getMain`, etc. unless the same
69
- filter set is reused heavily *and* belongs in the repository boundary.
70
- - **Creates**: `create` is permissive — stamp defaults (`status`, `memberId`,
71
- …) in the **service** and pass them to `create`. Do not add a custom
72
- repository `create*` just to apply defaults.
73
- - **Updates**: `update` accepts partial payloads — omit fields you do not
74
- want to change. Do not add a custom repository `update*` just to restrict
75
- columns or merge patches.
76
- - Write raw `this.orm.select()...` or a custom method only for joins, CTEs,
77
- multi-table writes, transactions, or predicates that cannot be expressed
78
- with filters / `TableConditionBuilder`.
79
-
80
- Filter shape: `{ columnId, type, method, value }` (`type`: `string` |
81
- `number` | `date` | `boolean` | `enum` | `jsonArray`; common `method`s:
82
- `equals`, `contains`, `oneOf`, `is_null`, …). Soft-delete exclusion is
83
- applied automatically when the table has `deletedAt` (unless
84
- `showDeleted: true`).
85
-
86
- ## Custom queries: the `this.query()` builder
87
-
88
- Use the builder only when a shared lookup truly belongs in the repository.
89
- Wire DTO schemas for inputs/outputs. **Outputs are validated at runtime** —
90
- a find-style query that can return no row must use `output.found` (the
91
- optional row schema), or validation will fail on the legitimate "not found"
92
- case.
93
-
94
- Wrap `queryFind` / `queryList` with filters — never reimplement
95
- select/where/limit:
96
-
97
- ```typescript
98
- import { z } from "zod";
99
- import { postSchemas } from "./posts.dto";
100
-
101
- // Shared domain lookup used from multiple services/jobs — still no hand-written SQL
102
- findBySlug = this.query("findBySlug")
103
- .input(z.object({ slug: z.string() }))
104
- .output(postSchemas.output.found) // NOT output.single — may return undefined
105
- .handle(({ slug }) =>
106
- this.queryFind({
107
- filters: [
108
- { columnId: "slug", type: "string", method: "equals", value: slug },
109
- ],
110
- })
111
- );
112
- ```
113
-
114
- ### Extend With `conditions` When Filters Alone Are Not Enough
115
-
116
- Pass a `TableConditionBuilder` via `options.conditions` for extra predicates
117
- (optional excludes, `ne`, `inArray`, …) while still reusing soft-delete and
118
- column picking from `queryFind` / `queryList`:
119
-
120
- ```typescript
121
- import { ne } from "drizzle-orm";
122
- import { z } from "zod";
123
- import { postSchemas } from "./posts.dto";
124
-
125
- // Prefer calling queryFind with conditions from the service when one-off;
126
- // repository wrapper only if shared across callers.
127
- findBySlug = this.query("findBySlug")
128
- .input(
129
- z.object({
130
- slug: z.string(),
131
- excludeId: z.string().optional(),
132
- })
133
- )
134
- .output(postSchemas.output.found)
135
- .handle(({ slug, excludeId }) => {
136
- const conditions = this.getConditionBuilder(this.table);
137
- if (excludeId) {
138
- conditions.push(ne(this.table.id, excludeId));
139
- }
140
-
141
- return this.queryFind(
142
- {
143
- filters: [
144
- { columnId: "slug", type: "string", method: "equals", value: slug },
145
- ],
146
- },
147
- { conditions }
148
- );
149
- });
150
- ```
151
-
152
- Use plain `async` methods instead of the builder only when the method needs an
153
- optional `tx` parameter, wraps `this.orm.transaction()`, or orchestrates
154
- multiple queries with branching between them.
155
-
156
- ## Error Handling
157
-
158
- - Use `throwableQuery` tightly around each **raw** Drizzle call — it converts
159
- thrown database errors into `ServerResultAsync`. Do not wrap whole method
160
- bodies.
161
- - Built-in helpers (`queryFind`, `queryList`, `findById`, `create`, …)
162
- already use `throwableQuery` internally — do not wrap their return values
163
- again.
164
- - Chain results with `isErr()` checks and `err(result.error)`.
165
- - Soft-deleted rows are excluded automatically by `queryList` / `queryFind`
166
- when the table has `deletedAt` (unless `showDeleted: true`). Only add
167
- `isNull(this.table.deletedAt)` in raw custom queries.
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
+ ## Prefer Built-in Base Methods (Strong Default)
40
+
41
+ `BaseTableRepository` provides typed CRUD helpers. **Always use these before
42
+ adding custom repository methods or raw Drizzle.** Services should call these
43
+ helpers directly in most cases.
44
+
45
+ - `queryList(query, options)` — filtered/sorted/paginated lists (`page`,
46
+ `limit`, `sort`, `order`, `q`, `filters`). Pass `globalSearchColumns` for
47
+ `q`, `conditions` for extra scoping, `showDeleted` to include soft-deleted
48
+ rows (excluded by default when the table has `deletedAt`).
49
+ - `queryFind(query, options)` — filtered/sorted single-row lookup (no
50
+ pagination/count). Same filter/`conditions` contract as `queryList`.
51
+ - `findById(id)` / `findManyById(ids)` — primary-key lookups.
52
+ - `create(data)` / `createMany(data)` — inserts.
53
+ - `update(data)` / `updateMany(data)` — updates.
54
+ - `softDeleteById(id)` / `deleteById(id)` — deletes.
55
+
56
+ ```typescript
57
+ // Preferred from the service — no custom repository list method
58
+ this.repository.posts.queryList(input, {
59
+ globalSearchColumns: ["title", "excerpt", "content"],
60
+ });
61
+ ```
62
+
63
+ ### Usually Do Not Add Custom List / Getter / Create / Update Methods
64
+
65
+ - **Lists and getters**: `queryList` / `queryFind` already expose read APIs
66
+ via `filters` (and optional `conditions`). Prefer calling them from the
67
+ service with the needed filters (or `.addFilters` on the procedure). Do not
68
+ add thin `listPublished`, `findBySlug`, `getMain`, etc. unless the same
69
+ filter set is reused heavily *and* belongs in the repository boundary.
70
+ - **Creates**: `create` is permissive — stamp defaults (`status`, `memberId`,
71
+ …) in the **service** and pass them to `create`. Do not add a custom
72
+ repository `create*` just to apply defaults.
73
+ - **Updates**: `update` accepts partial payloads — omit fields you do not
74
+ want to change. Do not add a custom repository `update*` just to restrict
75
+ columns or merge patches.
76
+ - Write raw `this.orm.select()...` or a custom method only for joins, CTEs,
77
+ multi-table writes, transactions, or predicates that cannot be expressed
78
+ with filters / `TableConditionBuilder`.
79
+
80
+ Filter shape: `{ columnId, type, method, value }` (`type`: `string` |
81
+ `number` | `date` | `boolean` | `enum` | `jsonArray`; common `method`s:
82
+ `equals`, `contains`, `oneOf`, `is_null`, …). Soft-delete exclusion is
83
+ applied automatically when the table has `deletedAt` (unless
84
+ `showDeleted: true`).
85
+
86
+ ## Custom queries: the `this.query()` builder
87
+
88
+ Use the builder only when a shared lookup truly belongs in the repository.
89
+ Wire DTO schemas for inputs/outputs. **Outputs are validated at runtime** —
90
+ a find-style query that can return no row must use `output.found` (the
91
+ optional row schema), or validation will fail on the legitimate "not found"
92
+ case.
93
+
94
+ Wrap `queryFind` / `queryList` with filters — never reimplement
95
+ select/where/limit:
96
+
97
+ ```typescript
98
+ import { z } from "zod";
99
+ import { postSchemas } from "./posts.dto";
100
+
101
+ // Shared domain lookup used from multiple services/jobs — still no hand-written SQL
102
+ findBySlug = this.query("findBySlug")
103
+ .input(z.object({ slug: z.string() }))
104
+ .output(postSchemas.output.found) // NOT output.single — may return undefined
105
+ .handle(({ slug }) =>
106
+ this.queryFind({
107
+ filters: [
108
+ { columnId: "slug", type: "string", method: "equals", value: slug },
109
+ ],
110
+ })
111
+ );
112
+ ```
113
+
114
+ ### Extend With `conditions` When Filters Alone Are Not Enough
115
+
116
+ Pass a `TableConditionBuilder` via `options.conditions` for extra predicates
117
+ (optional excludes, `ne`, `inArray`, …) while still reusing soft-delete and
118
+ column picking from `queryFind` / `queryList`:
119
+
120
+ ```typescript
121
+ import { ne } from "drizzle-orm";
122
+ import { z } from "zod";
123
+ import { postSchemas } from "./posts.dto";
124
+
125
+ // Prefer calling queryFind with conditions from the service when one-off;
126
+ // repository wrapper only if shared across callers.
127
+ findBySlug = this.query("findBySlug")
128
+ .input(
129
+ z.object({
130
+ slug: z.string(),
131
+ excludeId: z.string().optional(),
132
+ })
133
+ )
134
+ .output(postSchemas.output.found)
135
+ .handle(({ slug, excludeId }) => {
136
+ const conditions = this.getConditionBuilder(this.table);
137
+ if (excludeId) {
138
+ conditions.push(ne(this.table.id, excludeId));
139
+ }
140
+
141
+ return this.queryFind(
142
+ {
143
+ filters: [
144
+ { columnId: "slug", type: "string", method: "equals", value: slug },
145
+ ],
146
+ },
147
+ { conditions }
148
+ );
149
+ });
150
+ ```
151
+
152
+ Use plain `async` methods instead of the builder only when the method needs an
153
+ optional `tx` parameter, wraps `this.orm.transaction()`, or orchestrates
154
+ multiple queries with branching between them.
155
+
156
+ ## Error Handling
157
+
158
+ - Use `throwableQuery` tightly around each **raw** Drizzle call — it converts
159
+ thrown database errors into `ServerResultAsync`. Do not wrap whole method
160
+ bodies.
161
+ - Built-in helpers (`queryFind`, `queryList`, `findById`, `create`, …)
162
+ already use `throwableQuery` internally — do not wrap their return values
163
+ again.
164
+ - Chain results with `isErr()` checks and `err(result.error)`.
165
+ - Soft-deleted rows are excluded automatically by `queryList` / `queryFind`
166
+ when the table has `deletedAt` (unless `showDeleted: true`). Only add
167
+ `isNull(this.table.deletedAt)` in raw custom queries.
@@ -1,53 +1,53 @@
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)`.
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)`.