create-m5kdev 0.32.8 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/dist/src/catalog.cjs +76 -11
  2. package/dist/src/catalog.cjs.map +1 -1
  3. package/dist/src/catalog.d.cts +13 -1
  4. package/dist/src/create.cjs +6 -2
  5. package/dist/src/create.cjs.map +1 -1
  6. package/dist/src/doctor.cjs +13 -7
  7. package/dist/src/doctor.cjs.map +1 -1
  8. package/dist/src/fs.cjs +10 -5
  9. package/dist/src/fs.cjs.map +1 -1
  10. package/dist/src/prompts.cjs +32 -0
  11. package/dist/src/prompts.cjs.map +1 -1
  12. package/dist/src/prompts.d.cts +7 -1
  13. package/dist/src/template.cjs +23 -4
  14. package/dist/src/template.cjs.map +1 -1
  15. package/dist/src/template.d.cts +17 -3
  16. package/dist/src/types.d.cts +11 -4
  17. package/package.json +4 -1
  18. package/templates/minimal-app/.cursor/rules/module-dto-guide.mdc +1 -1
  19. package/templates/minimal-app/.cursor/rules/module-grants-guide.mdc +3 -3
  20. package/templates/minimal-app/.cursor/rules/module-module-guide.mdc +3 -2
  21. package/templates/minimal-app/.cursor/rules/module-service-guide.mdc +1 -1
  22. package/templates/minimal-app/.dockerignore.tpl +31 -0
  23. package/templates/minimal-app/.gitignore.tpl +1 -0
  24. package/templates/minimal-app/AGENTS.md.tpl +13 -10
  25. package/templates/minimal-app/CLAUDE.md.tpl +132 -131
  26. package/templates/minimal-app/README.md.tpl +50 -19
  27. package/templates/minimal-app/apps/e2e/package.json.tpl +23 -23
  28. package/templates/minimal-app/apps/e2e/playwright.config.ts.tpl +51 -39
  29. package/templates/minimal-app/apps/e2e/playwright.expo.config.ts.tpl +38 -38
  30. package/templates/minimal-app/apps/e2e/servers.ts.tpl +136 -112
  31. package/templates/minimal-app/apps/e2e/tests/conversation.spec.ts.tpl +22 -0
  32. package/templates/minimal-app/apps/e2e/tests/email.spec.ts.tpl +47 -47
  33. package/templates/minimal-app/apps/e2e/tests/expo-helpers.ts.tpl +73 -73
  34. package/templates/minimal-app/apps/e2e/tests/expo-standard.spec.ts.tpl +517 -517
  35. package/templates/minimal-app/apps/e2e/tests/expo-waitlist.spec.ts.tpl +94 -94
  36. package/templates/minimal-app/apps/e2e/tests/files.spec.ts.tpl +35 -0
  37. package/templates/minimal-app/apps/e2e/tests/helpers.ts.tpl +289 -289
  38. package/templates/minimal-app/apps/e2e/tests/standard.spec.ts.tpl +516 -516
  39. package/templates/minimal-app/apps/e2e/tests/waitlist.spec.ts.tpl +107 -107
  40. package/templates/minimal-app/apps/e2e/tests/workflow.spec.ts.tpl +26 -0
  41. package/templates/minimal-app/apps/e2e/tsconfig.json.tpl +10 -10
  42. package/templates/minimal-app/apps/email/package.json.tpl +34 -34
  43. package/templates/minimal-app/apps/email/src/emails/trialEndingEmail.tsx.tpl +40 -0
  44. package/templates/minimal-app/apps/email/src/index.ts.tpl +7 -0
  45. package/templates/minimal-app/apps/email/src/resources.ts.tpl +72 -65
  46. package/templates/minimal-app/apps/expo/app.json.tpl +36 -36
  47. package/templates/minimal-app/apps/expo/eslint.config.js.tpl +8 -8
  48. package/templates/minimal-app/apps/expo/metro.config.js.tpl +16 -16
  49. package/templates/minimal-app/apps/expo/package.json.tpl +59 -59
  50. package/templates/minimal-app/apps/expo/src/app/_layout.tsx.tpl +31 -31
  51. package/templates/minimal-app/apps/expo/src/app/claim-account.tsx.tpl +151 -151
  52. package/templates/minimal-app/apps/expo/src/app/forgot-password.tsx.tpl +53 -53
  53. package/templates/minimal-app/apps/expo/src/app/index.tsx.tpl +8 -8
  54. package/templates/minimal-app/apps/expo/src/app/login.tsx.tpl +71 -71
  55. package/templates/minimal-app/apps/expo/src/app/logout.tsx.tpl +19 -19
  56. package/templates/minimal-app/apps/expo/src/app/organization/accept-invitation.tsx.tpl +85 -85
  57. package/templates/minimal-app/apps/expo/src/app/posts.tsx.tpl +40 -40
  58. package/templates/minimal-app/apps/expo/src/app/reset-password.tsx.tpl +75 -75
  59. package/templates/minimal-app/apps/expo/src/app/signup.tsx.tpl +193 -193
  60. package/templates/minimal-app/apps/expo/src/components/AuthScaffold.tsx.tpl +34 -34
  61. package/templates/minimal-app/apps/expo/src/components/FormTextField.tsx.tpl +37 -37
  62. package/templates/minimal-app/apps/expo/src/components/LoadingScreen.tsx.tpl +10 -10
  63. package/templates/minimal-app/apps/expo/src/components/StatusText.tsx.tpl +22 -22
  64. package/templates/minimal-app/apps/expo/src/config.ts.tpl +9 -9
  65. package/templates/minimal-app/apps/expo/src/global.css.tpl +11 -11
  66. package/templates/minimal-app/apps/expo/src/lib/trpc.ts.tpl +20 -20
  67. package/templates/minimal-app/apps/expo/tsconfig.json.tpl +11 -11
  68. package/templates/minimal-app/apps/expo/uniwind-env.d.ts.tpl +1 -1
  69. package/templates/minimal-app/apps/landing/.env.production.example.tpl +7 -0
  70. package/templates/minimal-app/apps/landing/Dockerfile.tpl +39 -0
  71. package/templates/minimal-app/apps/landing/fly.toml.tpl +20 -0
  72. package/templates/minimal-app/apps/landing/index.html.tpl +35 -0
  73. package/templates/minimal-app/apps/landing/package.json.tpl +34 -0
  74. package/templates/minimal-app/apps/landing/scripts/fly-deploy.mjs.tpl +75 -0
  75. package/templates/minimal-app/apps/landing/scripts/fly-secrets.mjs.tpl +31 -0
  76. package/templates/minimal-app/apps/landing/src/App.tsx.tpl +12 -0
  77. package/templates/minimal-app/apps/landing/src/LandingPage.tsx.tpl +37 -0
  78. package/templates/minimal-app/apps/landing/src/index.css.tpl +24 -0
  79. package/templates/minimal-app/apps/landing/src/main.tsx.tpl +16 -0
  80. package/templates/minimal-app/apps/landing/src/vite-env.d.ts.tpl +11 -0
  81. package/templates/minimal-app/apps/landing/tsconfig.json.tpl +12 -0
  82. package/templates/minimal-app/apps/landing/vite.config.ts.tpl +25 -0
  83. package/templates/minimal-app/apps/server/AGENTS.md.tpl +54 -46
  84. package/templates/minimal-app/apps/server/{drizzle/seed.e2e.ts.tpl → db.e2e.ts.tpl} +57 -40
  85. package/templates/minimal-app/apps/server/{drizzle/seed.ts.tpl → db.ts.tpl} +27 -20
  86. package/templates/minimal-app/apps/server/drizzle/0002_concerned_proudstar.sql +3 -0
  87. package/templates/minimal-app/apps/server/drizzle/0003_silly_the_liberteens.sql +27 -0
  88. package/templates/minimal-app/apps/server/drizzle/meta/0000_snapshot.json.tpl +56 -166
  89. package/templates/minimal-app/apps/server/drizzle/meta/0001_snapshot.json.tpl +1796 -1910
  90. package/templates/minimal-app/apps/server/drizzle/meta/0002_snapshot.json.tpl +1818 -0
  91. package/templates/minimal-app/apps/server/drizzle/meta/0003_snapshot.json.tpl +2137 -0
  92. package/templates/minimal-app/apps/server/drizzle/meta/_journal.json.tpl +15 -1
  93. package/templates/minimal-app/apps/server/drizzle.config.ts.tpl +3 -33
  94. package/templates/minimal-app/apps/server/package.json.tpl +57 -56
  95. package/templates/minimal-app/apps/server/src/app.ts.tpl +149 -141
  96. package/templates/minimal-app/apps/server/src/index.ts.tpl +12 -65
  97. package/templates/minimal-app/apps/server/src/modules/conversation/conversation.mastra.ts.tpl +26 -0
  98. package/templates/minimal-app/apps/server/src/modules/demo-workflow/demo-workflow.module.ts.tpl +47 -0
  99. package/templates/minimal-app/apps/server/src/modules/demo-workflow/demo-workflow.service.ts.tpl +43 -0
  100. package/templates/minimal-app/apps/server/src/modules/demo-workflow/demo-workflow.trpc.ts.tpl +16 -0
  101. package/templates/minimal-app/apps/server/src/modules/posts/posts.db.ts.tpl +28 -28
  102. package/templates/minimal-app/apps/server/src/modules/posts/posts.grants.ts.tpl +28 -30
  103. package/templates/minimal-app/apps/server/src/modules/posts/posts.module.ts.tpl +54 -54
  104. package/templates/minimal-app/apps/server/src/modules/posts/posts.repository.ts.tpl +67 -67
  105. package/templates/minimal-app/apps/server/src/modules/posts/posts.service.ts.tpl +116 -115
  106. package/templates/minimal-app/apps/server/src/modules/posts/posts.trpc.ts.tpl +37 -37
  107. package/templates/minimal-app/apps/server/src/modules/test-harness/test-harness.module.ts.tpl +279 -279
  108. package/templates/minimal-app/apps/server/src/schema.ts.tpl +31 -20
  109. package/templates/minimal-app/apps/server/src/types.ts.tpl +12 -1
  110. package/templates/minimal-app/apps/server/tsconfig.json.tpl +1 -1
  111. package/templates/minimal-app/apps/server/tsup.config.ts.tpl +23 -9
  112. package/templates/minimal-app/apps/shared/.env.example.tpl +5 -0
  113. package/templates/minimal-app/apps/shared/.env.production.example.tpl +23 -0
  114. package/templates/minimal-app/apps/shared/.env.tpl +5 -0
  115. package/templates/minimal-app/apps/shared/Dockerfile.tpl +48 -0
  116. package/templates/minimal-app/apps/shared/fly.toml.tpl +24 -0
  117. package/templates/minimal-app/apps/shared/package.json.tpl +25 -24
  118. package/templates/minimal-app/apps/shared/scripts/fly-deploy.mjs.tpl +75 -0
  119. package/templates/minimal-app/apps/shared/scripts/fly-secrets.mjs.tpl +31 -0
  120. package/templates/minimal-app/apps/shared/src/modules/app/app.constants.ts.tpl +28 -28
  121. package/templates/minimal-app/apps/shared/src/modules/conversation/conversation.constants.ts.tpl +1 -0
  122. package/templates/minimal-app/apps/shared/src/modules/notification/notification.schema.ts.tpl +3 -3
  123. package/templates/minimal-app/apps/shared/src/modules/posts/posts.constants.ts.tpl +5 -5
  124. package/templates/minimal-app/apps/shared/src/modules/posts/posts.schema.ts.tpl +25 -25
  125. package/templates/minimal-app/apps/webapp/package.json.tpl +53 -53
  126. package/templates/minimal-app/apps/webapp/public/push-sw.js.tpl +34 -34
  127. package/templates/minimal-app/apps/webapp/src/App.tsx.tpl +46 -44
  128. package/templates/minimal-app/apps/webapp/src/Layout.tsx.tpl +96 -65
  129. package/templates/minimal-app/apps/webapp/src/Router.tsx.tpl +114 -93
  130. package/templates/minimal-app/apps/webapp/src/index.css.tpl +15 -15
  131. package/templates/minimal-app/apps/webapp/src/main.tsx.tpl +26 -26
  132. package/templates/minimal-app/apps/webapp/src/modules/conversation/ConversationRoute.tsx.tpl +36 -0
  133. package/templates/minimal-app/apps/webapp/src/modules/files/FilesRoute.tsx.tpl +88 -0
  134. package/templates/minimal-app/apps/webapp/src/modules/files/hooks/useFilesRoute.ts.tpl +50 -0
  135. package/templates/minimal-app/apps/webapp/src/modules/workflows/WorkflowsRoute.tsx.tpl +97 -0
  136. package/templates/minimal-app/apps/webapp/src/modules/workflows/hooks/useWorkflowsRoute.ts.tpl +64 -0
  137. package/templates/minimal-app/apps/webapp/src/utils/i18n.ts.tpl +13 -13
  138. package/templates/minimal-app/apps/webapp/src/utils/trpc.ts.tpl +10 -10
  139. package/templates/minimal-app/apps/webapp/src/vite-env.d.ts.tpl +1 -1
  140. package/templates/minimal-app/apps/webapp/translations/en/app.json.tpl +7 -7
  141. package/templates/minimal-app/apps/webapp/translations/en/{{APP_SLUG}}.json.tpl +193 -135
  142. package/templates/minimal-app/apps/webapp/tsconfig.json.tpl +17 -17
  143. package/templates/minimal-app/apps/webapp/vite.config.ts.tpl +31 -31
  144. package/templates/minimal-app/biome.json.tpl +17 -12
  145. package/templates/minimal-app/package.json.tpl +9 -5
  146. package/templates/minimal-app/pnpm-workspace.yaml.tpl +67 -58
  147. package/templates/minimal-app/template.manifest.json +92 -2
  148. package/templates/minimal-app/apps/server/drizzle/db.ts.tpl +0 -26
  149. package/templates/minimal-app/apps/server/drizzle/guard.ts.tpl +0 -36
  150. package/templates/minimal-app/apps/server/drizzle/reset.ts.tpl +0 -40
  151. package/templates/minimal-app/apps/server/drizzle/sync.ts.tpl +0 -16
  152. package/templates/minimal-app/apps/server/src/lib/auth.ts.tpl +0 -12
@@ -0,0 +1,43 @@
1
+ import type { ServerResultAsync } from "@m5kdev/backend/modules/base/base.dto";
2
+ import { BaseService } from "@m5kdev/backend/modules/base/base.service";
3
+ import type { WorkflowService } from "@m5kdev/backend/modules/workflow/workflow.service";
4
+ import type { FireAndForgetJobDefinition } from "@m5kdev/backend/modules/workflow/workflow.types";
5
+ import type { Context } from "@m5kdev/backend/utils/trpc";
6
+ import { workflowTriggerOutputSchema } from "@m5kdev/commons/modules/workflow/workflow.schema";
7
+ import { ok } from "neverthrow";
8
+
9
+ const DEMO_PING_JOB_NAME = "demo.ping";
10
+
11
+ interface DemoPingJobPayload {
12
+ readonly userId: string;
13
+ }
14
+
15
+ export class DemoWorkflowService extends BaseService<
16
+ Record<string, never>,
17
+ { workflow: WorkflowService },
18
+ Context
19
+ > {
20
+ readonly demoPingJob: FireAndForgetJobDefinition<DemoPingJobPayload>;
21
+
22
+ constructor(services: { workflow: WorkflowService }) {
23
+ super({}, services);
24
+
25
+ this.demoPingJob = this.service.workflow
26
+ .job<DemoPingJobPayload>({
27
+ name: DEMO_PING_JOB_NAME,
28
+ queue: "fast",
29
+ meta: (payload) => ({ userId: payload.userId }),
30
+ })
31
+ .handle(async () => {
32
+ // Completes immediately so the Starter webapp can observe a finished run.
33
+ });
34
+ }
35
+
36
+ readonly runDemo = this.procedure("runDemo")
37
+ .output(workflowTriggerOutputSchema)
38
+ .requireAuth()
39
+ .handle(async ({ ctx }): ServerResultAsync<{ jobId: string }> => {
40
+ const jobId = await this.demoPingJob.trigger({ userId: ctx.actor.userId });
41
+ return ok({ jobId });
42
+ });
43
+ }
@@ -0,0 +1,16 @@
1
+ import { handleTRPCResult, type TRPCMethods } from "@m5kdev/backend/utils/trpc";
2
+ import { workflowTriggerOutputSchema } from "@m5kdev/commons/modules/workflow/workflow.schema";
3
+ import type { DemoWorkflowService } from "./demo-workflow.service";
4
+
5
+ export function createDemoWorkflowTRPC(
6
+ { router, privateProcedure }: TRPCMethods,
7
+ demoWorkflowService: DemoWorkflowService
8
+ ) {
9
+ return router({
10
+ run: privateProcedure
11
+ .output(workflowTriggerOutputSchema)
12
+ .mutation(async ({ ctx }) =>
13
+ handleTRPCResult(await demoWorkflowService.runDemo(undefined, ctx))
14
+ ),
15
+ });
16
+ }
@@ -1,28 +1,28 @@
1
- import { members, organizations, teams, users } from "@m5kdev/backend/modules/auth/auth.db";
2
- import { integer, sqliteTable as table, text } from "drizzle-orm/sqlite-core";
3
- import { v4 as uuidv4 } from "uuid";
4
-
5
- export const posts = table("posts", {
6
- id: text("id").primaryKey().$default(uuidv4),
7
- authorUserId: text("author_user_id").references(() => users.id, {
8
- onDelete: "set null",
9
- }),
10
- memberId: text("member_id").references(() => members.id, {
11
- onDelete: "set null",
12
- }),
13
- organizationId: text("organization_id").references(() => organizations.id, {
14
- onDelete: "set null",
15
- }),
16
- teamId: text("team_id").references(() => teams.id, { onDelete: "set null" }),
17
- title: text("title").notNull(),
18
- slug: text("slug").notNull().unique(),
19
- excerpt: text("excerpt"),
20
- content: text("content").notNull(),
21
- status: text("status").$type<"draft" | "published">().notNull().default("draft"),
22
- publishedAt: integer("published_at", { mode: "timestamp" }),
23
- createdAt: integer("created_at", { mode: "timestamp" })
24
- .notNull()
25
- .$default(() => new Date()),
26
- updatedAt: integer("updated_at", { mode: "timestamp" }),
27
- deletedAt: integer("deleted_at", { mode: "timestamp" }),
28
- });
1
+ import { members, organizations, teams, users } from "@m5kdev/backend/modules/auth/auth.db";
2
+ import { integer, sqliteTable as table, text } from "drizzle-orm/sqlite-core";
3
+ import { v4 as uuidv4 } from "uuid";
4
+
5
+ export const posts = table("posts", {
6
+ id: text("id").primaryKey().$default(uuidv4),
7
+ authorUserId: text("author_user_id").references(() => users.id, {
8
+ onDelete: "set null",
9
+ }),
10
+ memberId: text("member_id").references(() => members.id, {
11
+ onDelete: "set null",
12
+ }),
13
+ organizationId: text("organization_id").references(() => organizations.id, {
14
+ onDelete: "set null",
15
+ }),
16
+ teamId: text("team_id").references(() => teams.id, { onDelete: "set null" }),
17
+ title: text("title").notNull(),
18
+ slug: text("slug").notNull().unique(),
19
+ excerpt: text("excerpt"),
20
+ content: text("content").notNull(),
21
+ status: text("status").$type<"draft" | "published">().notNull().default("draft"),
22
+ publishedAt: integer("published_at", { mode: "timestamp" }),
23
+ createdAt: integer("created_at", { mode: "timestamp" })
24
+ .notNull()
25
+ .$default(() => new Date()),
26
+ updatedAt: integer("updated_at", { mode: "timestamp" }),
27
+ deletedAt: integer("deleted_at", { mode: "timestamp" }),
28
+ });
@@ -1,30 +1,28 @@
1
- import { flattenNestedGrants } from "@m5kdev/backend/modules/base/base.grants";
2
-
3
- export const postsGrants = flattenNestedGrants({
4
- cash: {
5
- user: {
6
- admin: {
7
- read: "all",
8
- write: "all",
9
- delete: "all",
10
- publish: "all",
11
- },
12
- },
13
- organization: {
14
- owner: {
15
- read: "own",
16
- write: "own",
17
- delete: "own",
18
- publish: "own",
19
- },
20
- },
21
- team: {
22
- owner: {
23
- read: "own",
24
- write: "own",
25
- delete: "own",
26
- publish: "own",
27
- },
28
- },
29
- },
30
- });
1
+ import { flattenNestedGrants } from "@m5kdev/backend/modules/base/base.grants";
2
+
3
+ export const postsGrants = flattenNestedGrants({
4
+ cash: {
5
+ user: {
6
+ admin: {
7
+ read: "all",
8
+ write: "all",
9
+ delete: "all",
10
+ publish: "all",
11
+ },
12
+ },
13
+ organization: {
14
+ owner: {
15
+ read: "org",
16
+ write: "org",
17
+ delete: "org",
18
+ publish: "org",
19
+ },
20
+ member: {
21
+ read: "own",
22
+ write: "own",
23
+ delete: "own",
24
+ publish: "own",
25
+ },
26
+ },
27
+ },
28
+ });
@@ -1,54 +1,54 @@
1
- import { createBackendRouterMap } from "@m5kdev/backend/app";
2
- import {
3
- BaseModule,
4
- type ModuleRepositoriesContext,
5
- type ModuleServicesContext,
6
- type ModuleTRPCContext,
7
- } from "@m5kdev/backend/modules/base/base.module";
8
- import type * as postsTables from "./posts.db";
9
- import { postsGrants } from "./posts.grants";
10
- import { PostsRepository } from "./posts.repository";
11
- import { PostsService } from "./posts.service";
12
- import { createPostsTRPC } from "./posts.trpc";
13
-
14
- type PostsModuleTables = typeof postsTables;
15
- type PostsModuleRepositories = {
16
- posts: PostsRepository;
17
- };
18
- type PostsModuleServices = {
19
- posts: PostsService;
20
- };
21
- type PostsModuleRouters = {
22
- posts: ReturnType<typeof createPostsTRPC>;
23
- };
24
-
25
- export class PostsModule extends BaseModule<
26
- never,
27
- PostsModuleTables,
28
- PostsModuleRepositories,
29
- PostsModuleServices,
30
- PostsModuleRouters
31
- > {
32
- readonly id = "posts";
33
- override readonly dbDependsOn = ["auth"] as const;
34
-
35
- override repositories({ db }: ModuleRepositoriesContext<never, PostsModuleTables>) {
36
- return {
37
- posts: new PostsRepository({
38
- orm: db.orm,
39
- schema: db.schema,
40
- table: db.schema.posts,
41
- }),
42
- };
43
- }
44
-
45
- override services({ repositories }: ModuleServicesContext<never, PostsModuleRepositories>) {
46
- return {
47
- posts: new PostsService({ posts: repositories.posts }, {}, postsGrants),
48
- };
49
- }
50
-
51
- override trpc({ trpc, services }: ModuleTRPCContext<never, PostsModuleServices>) {
52
- return createBackendRouterMap("posts", createPostsTRPC(trpc, services.posts));
53
- }
54
- }
1
+ import { createBackendRouterMap } from "@m5kdev/backend/app";
2
+ import {
3
+ BaseModule,
4
+ type ModuleRepositoriesContext,
5
+ type ModuleServicesContext,
6
+ type ModuleTRPCContext,
7
+ } from "@m5kdev/backend/modules/base/base.module";
8
+ import type * as postsTables from "./posts.db";
9
+ import { postsGrants } from "./posts.grants";
10
+ import { PostsRepository } from "./posts.repository";
11
+ import { PostsService } from "./posts.service";
12
+ import { createPostsTRPC } from "./posts.trpc";
13
+
14
+ type PostsModuleTables = typeof postsTables;
15
+ type PostsModuleRepositories = {
16
+ posts: PostsRepository;
17
+ };
18
+ type PostsModuleServices = {
19
+ posts: PostsService;
20
+ };
21
+ type PostsModuleRouters = {
22
+ posts: ReturnType<typeof createPostsTRPC>;
23
+ };
24
+
25
+ export class PostsModule extends BaseModule<
26
+ never,
27
+ PostsModuleTables,
28
+ PostsModuleRepositories,
29
+ PostsModuleServices,
30
+ PostsModuleRouters
31
+ > {
32
+ readonly id = "posts";
33
+ override readonly dbDependsOn = ["auth"] as const;
34
+
35
+ override repositories({ db }: ModuleRepositoriesContext<never, PostsModuleTables>) {
36
+ return {
37
+ posts: new PostsRepository({
38
+ orm: db.orm,
39
+ schema: db.schema,
40
+ table: db.schema.posts,
41
+ }),
42
+ };
43
+ }
44
+
45
+ override services({ repositories }: ModuleServicesContext<never, PostsModuleRepositories>) {
46
+ return {
47
+ posts: new PostsService({ posts: repositories.posts }, {}, postsGrants),
48
+ };
49
+ }
50
+
51
+ override trpc({ trpc, services }: ModuleTRPCContext<never, PostsModuleServices>) {
52
+ return createBackendRouterMap("posts", createPostsTRPC(trpc, services.posts));
53
+ }
54
+ }
@@ -1,67 +1,67 @@
1
- import { BaseTableRepository } from "@m5kdev/backend/modules/base/base.repository";
2
-
3
- import { and, eq, isNull, ne } from "drizzle-orm";
4
- import type { LibSQLDatabase } from "drizzle-orm/libsql";
5
- import { err, ok } from "neverthrow";
6
- import { z } from "zod";
7
- import { posts } from "./posts.db";
8
- import { postSchemas } from "./posts.dto";
9
-
10
- const schema = { posts };
11
- type Schema = typeof schema;
12
- type Orm = LibSQLDatabase<Schema>;
13
-
14
- export class PostsRepository extends BaseTableRepository<
15
- Orm,
16
- Schema,
17
- Record<string, never>,
18
- Schema["posts"]
19
- > {
20
- findBySlug = this.query("findBySlug")
21
- .input(
22
- z.object({
23
- slug: z.string(),
24
- excludeId: z.string().optional(),
25
- })
26
- )
27
- .output(postSchemas.output.found)
28
- .handle(async ({ excludeId, slug }) => {
29
- const whereClause = excludeId
30
- ? and(eq(this.table.slug, slug), ne(this.table.id, excludeId), isNull(this.table.deletedAt))
31
- : and(eq(this.table.slug, slug), isNull(this.table.deletedAt));
32
-
33
- const result = await this.throwableQuery(() =>
34
- this.orm.select().from(this.table).where(whereClause).limit(1)
35
- );
36
- if (result.isErr()) return err(result.error);
37
- const [row] = result.value;
38
- return ok(row);
39
- });
40
-
41
- resolveUniqueSlug = this.query("resolveUniqueSlug")
42
- .input(
43
- z.object({
44
- candidate: z.string(),
45
- excludeId: z.string().optional(),
46
- })
47
- )
48
- .output(z.string())
49
- .handle(async ({ candidate, excludeId }) => {
50
- let slug = candidate;
51
- let suffix = 2;
52
-
53
- while (true) {
54
- const existing = await this.findBySlug({ slug, excludeId });
55
- if (existing.isErr()) {
56
- return err(existing.error);
57
- }
58
-
59
- if (!existing.value) {
60
- return ok(slug);
61
- }
62
-
63
- slug = `${candidate}-${suffix}`;
64
- suffix += 1;
65
- }
66
- });
67
- }
1
+ import { BaseTableRepository } from "@m5kdev/backend/modules/base/base.repository";
2
+
3
+ import { and, eq, isNull, ne } from "drizzle-orm";
4
+ import type { LibSQLDatabase } from "drizzle-orm/libsql";
5
+ import { err, ok } from "neverthrow";
6
+ import { z } from "zod";
7
+ import { posts } from "./posts.db";
8
+ import { postSchemas } from "./posts.dto";
9
+
10
+ const schema = { posts };
11
+ type Schema = typeof schema;
12
+ type Orm = LibSQLDatabase<Schema>;
13
+
14
+ export class PostsRepository extends BaseTableRepository<
15
+ Orm,
16
+ Schema,
17
+ Record<string, never>,
18
+ Schema["posts"]
19
+ > {
20
+ findBySlug = this.query("findBySlug")
21
+ .input(
22
+ z.object({
23
+ slug: z.string(),
24
+ excludeId: z.string().optional(),
25
+ })
26
+ )
27
+ .output(postSchemas.output.found)
28
+ .handle(async ({ excludeId, slug }) => {
29
+ const whereClause = excludeId
30
+ ? and(eq(this.table.slug, slug), ne(this.table.id, excludeId), isNull(this.table.deletedAt))
31
+ : and(eq(this.table.slug, slug), isNull(this.table.deletedAt));
32
+
33
+ const result = await this.throwableQuery(() =>
34
+ this.orm.select().from(this.table).where(whereClause).limit(1)
35
+ );
36
+ if (result.isErr()) return err(result.error);
37
+ const [row] = result.value;
38
+ return ok(row);
39
+ });
40
+
41
+ resolveUniqueSlug = this.query("resolveUniqueSlug")
42
+ .input(
43
+ z.object({
44
+ candidate: z.string(),
45
+ excludeId: z.string().optional(),
46
+ })
47
+ )
48
+ .output(z.string())
49
+ .handle(async ({ candidate, excludeId }) => {
50
+ let slug = candidate;
51
+ let suffix = 2;
52
+
53
+ while (true) {
54
+ const existing = await this.findBySlug({ slug, excludeId });
55
+ if (existing.isErr()) {
56
+ return err(existing.error);
57
+ }
58
+
59
+ if (!existing.value) {
60
+ return ok(slug);
61
+ }
62
+
63
+ slug = `${candidate}-${suffix}`;
64
+ suffix += 1;
65
+ }
66
+ });
67
+ }
@@ -1,115 +1,116 @@
1
- import { BasePermissionService } from "@m5kdev/backend/modules/base/base.service";
2
- import { serializeSpanValue, withSpan } from "@m5kdev/backend/utils/telemetry";
3
- import type { Context } from "@m5kdev/backend/utils/trpc";
4
- import { err, ok } from "neverthrow";
5
- import { postSchemas } from "./posts.dto";
6
- import type { PostsRepository } from "./posts.repository";
7
- import { createExcerpt, slugify } from "./posts.utils";
8
-
9
- export class PostsService extends BasePermissionService<
10
- { posts: PostsRepository },
11
- Record<string, never>,
12
- Context
13
- > {
14
- readonly list = this.procedure("list")
15
- .input(postSchemas.input.list)
16
- .output(postSchemas.output.list)
17
- .requireAuth()
18
- .handle(async ({ input }) => {
19
- return withSpan(
20
- {
21
- name: "posts.list.query",
22
- attributes: { input: serializeSpanValue(input) },
23
- },
24
- (span) => {
25
- span.addEvent("span started");
26
- return this.repository.posts.queryList(input, {
27
- globalSearchColumns: ["title", "excerpt", "content"],
28
- });
29
- }
30
- );
31
- });
32
-
33
- readonly create = this.procedure("create")
34
- .requireAuth()
35
- .input(postSchemas.input.create)
36
- .output(postSchemas.output.single)
37
- .access({
38
- action: "write",
39
- })
40
- .handle(async ({ input, ctx }) => {
41
- const uniqueSlug = await this.repository.posts.resolveUniqueSlug({
42
- candidate: slugify(input.slug ?? input.title),
43
- });
44
- if (uniqueSlug.isErr()) {
45
- return err(uniqueSlug.error);
46
- }
47
-
48
- return this.repository.posts.create({
49
- authorUserId: ctx.user.id,
50
- memberId: ctx.actor.memberId ?? null,
51
- organizationId: ctx.session.activeOrganizationId ?? null,
52
- teamId: ctx.session.activeTeamId ?? null,
53
- title: input.title.trim(),
54
- slug: uniqueSlug.value,
55
- excerpt: createExcerpt(input.excerpt, input.content),
56
- content: input.content.trim(),
57
- status: "draft",
58
- });
59
- });
60
-
61
- readonly update = this.procedure("update")
62
- .requireAuth()
63
- .input(postSchemas.input.update)
64
- .output(postSchemas.output.single)
65
- .loadResource("post", ({ input }) => this.repository.posts.findById(input.id))
66
- .access({
67
- action: "write",
68
- entityStep: "post",
69
- })
70
- .handle(async ({ input, state }) => {
71
- const content = input.content?.trim() ?? state.post.content;
72
-
73
- return this.repository.posts.update({
74
- id: input.id,
75
- title: input.title ? input.title.trim() : state.post.title,
76
- excerpt: createExcerpt(input.excerpt, content),
77
- content: content.trim(),
78
- });
79
- });
80
-
81
- readonly publish = this.procedure("publish")
82
- .input(postSchemas.input.publish)
83
- .output(postSchemas.output.single)
84
- .requireAuth()
85
- .loadResource("post", ({ input }) => this.repository.posts.findById(input.id))
86
- .access({
87
- action: "publish",
88
- entityStep: "post",
89
- })
90
- .handle(({ input, state }) => {
91
- return this.repository.posts.update({
92
- id: input.id,
93
- status: "published",
94
- publishedAt: state.post?.publishedAt ?? new Date(),
95
- });
96
- });
97
-
98
- readonly softDelete = this.procedure("softDelete")
99
- .input(postSchemas.input.delete)
100
- .output(postSchemas.output.uuid)
101
- .requireAuth()
102
- .loadResource("post", ({ input }) => this.repository.posts.findById(input.id))
103
- .access({
104
- action: "delete",
105
- entityStep: "post",
106
- })
107
- .handle(async ({ input }) => {
108
- const updated = await this.repository.posts.update({
109
- id: input.id,
110
- deletedAt: new Date(),
111
- });
112
- if (updated.isErr()) return err(updated.error);
113
- return ok({ id: updated.value.id });
114
- });
115
- }
1
+ import { BasePermissionService } from "@m5kdev/backend/modules/base/base.service";
2
+ import { serializeSpanValue, withSpan } from "@m5kdev/backend/utils/telemetry";
3
+ import type { Context } from "@m5kdev/backend/utils/trpc";
4
+ import { err, ok } from "neverthrow";
5
+ import { postSchemas } from "./posts.dto";
6
+ import type { PostsRepository } from "./posts.repository";
7
+ import { createExcerpt, slugify } from "./posts.utils";
8
+
9
+ export class PostsService extends BasePermissionService<
10
+ { posts: PostsRepository },
11
+ Record<string, never>,
12
+ Context
13
+ > {
14
+ readonly list = this.procedure("list")
15
+ .input(postSchemas.input.list)
16
+ .output(postSchemas.output.list)
17
+ .requireAuth("organization")
18
+ .addContextFilter(["organization"])
19
+ .handle(async ({ input }) => {
20
+ return withSpan(
21
+ {
22
+ name: "posts.list.query",
23
+ attributes: { input: serializeSpanValue(input) },
24
+ },
25
+ (span) => {
26
+ span.addEvent("span started");
27
+ return this.repository.posts.queryList(input, {
28
+ globalSearchColumns: ["title", "excerpt", "content"],
29
+ });
30
+ }
31
+ );
32
+ });
33
+
34
+ readonly create = this.procedure("create")
35
+ .requireAuth("organization")
36
+ .input(postSchemas.input.create)
37
+ .output(postSchemas.output.single)
38
+ .access({
39
+ action: "write",
40
+ })
41
+ .handle(async ({ input, ctx }) => {
42
+ const uniqueSlug = await this.repository.posts.resolveUniqueSlug({
43
+ candidate: slugify(input.slug ?? input.title),
44
+ });
45
+ if (uniqueSlug.isErr()) {
46
+ return err(uniqueSlug.error);
47
+ }
48
+
49
+ return this.repository.posts.create({
50
+ authorUserId: ctx.user.id,
51
+ memberId: ctx.actor.memberId ?? null,
52
+ organizationId: ctx.session.activeOrganizationId ?? null,
53
+ teamId: ctx.session.activeTeamId ?? null,
54
+ title: input.title.trim(),
55
+ slug: uniqueSlug.value,
56
+ excerpt: createExcerpt(input.excerpt, input.content),
57
+ content: input.content.trim(),
58
+ status: "draft",
59
+ });
60
+ });
61
+
62
+ readonly update = this.procedure("update")
63
+ .requireAuth("organization")
64
+ .input(postSchemas.input.update)
65
+ .output(postSchemas.output.single)
66
+ .loadResource("post", ({ input }) => this.repository.posts.findById(input.id))
67
+ .access({
68
+ action: "write",
69
+ entityStep: "post",
70
+ })
71
+ .handle(async ({ input, state }) => {
72
+ const content = input.content?.trim() ?? state.post.content;
73
+
74
+ return this.repository.posts.update({
75
+ id: input.id,
76
+ title: input.title ? input.title.trim() : state.post.title,
77
+ excerpt: createExcerpt(input.excerpt, content),
78
+ content: content.trim(),
79
+ });
80
+ });
81
+
82
+ readonly publish = this.procedure("publish")
83
+ .input(postSchemas.input.publish)
84
+ .output(postSchemas.output.single)
85
+ .requireAuth("organization")
86
+ .loadResource("post", ({ input }) => this.repository.posts.findById(input.id))
87
+ .access({
88
+ action: "publish",
89
+ entityStep: "post",
90
+ })
91
+ .handle(({ input, state }) => {
92
+ return this.repository.posts.update({
93
+ id: input.id,
94
+ status: "published",
95
+ publishedAt: state.post?.publishedAt ?? new Date(),
96
+ });
97
+ });
98
+
99
+ readonly softDelete = this.procedure("softDelete")
100
+ .input(postSchemas.input.delete)
101
+ .output(postSchemas.output.uuid)
102
+ .requireAuth("organization")
103
+ .loadResource("post", ({ input }) => this.repository.posts.findById(input.id))
104
+ .access({
105
+ action: "delete",
106
+ entityStep: "post",
107
+ })
108
+ .handle(async ({ input }) => {
109
+ const updated = await this.repository.posts.update({
110
+ id: input.id,
111
+ deletedAt: new Date(),
112
+ });
113
+ if (updated.isErr()) return err(updated.error);
114
+ return ok({ id: updated.value.id });
115
+ });
116
+ }