create-m5kdev 0.33.0 → 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 (147) hide show
  1. package/dist/src/catalog.cjs +21 -1
  2. package/dist/src/catalog.cjs.map +1 -1
  3. package/dist/src/catalog.d.cts +3 -1
  4. package/dist/src/create.cjs +6 -2
  5. package/dist/src/create.cjs.map +1 -1
  6. package/dist/src/doctor.cjs.map +1 -1
  7. package/dist/src/fs.cjs +10 -5
  8. package/dist/src/fs.cjs.map +1 -1
  9. package/dist/src/prompts.cjs +32 -0
  10. package/dist/src/prompts.cjs.map +1 -1
  11. package/dist/src/prompts.d.cts +7 -1
  12. package/dist/src/template.cjs +23 -4
  13. package/dist/src/template.cjs.map +1 -1
  14. package/dist/src/template.d.cts +17 -3
  15. package/dist/src/types.d.cts +11 -4
  16. package/package.json +4 -1
  17. package/templates/minimal-app/.cursor/rules/module-dto-guide.mdc +1 -1
  18. package/templates/minimal-app/.cursor/rules/module-grants-guide.mdc +3 -3
  19. package/templates/minimal-app/.cursor/rules/module-module-guide.mdc +3 -2
  20. package/templates/minimal-app/.cursor/rules/module-service-guide.mdc +1 -1
  21. package/templates/minimal-app/.dockerignore.tpl +31 -0
  22. package/templates/minimal-app/.gitignore.tpl +1 -0
  23. package/templates/minimal-app/AGENTS.md.tpl +13 -10
  24. package/templates/minimal-app/CLAUDE.md.tpl +132 -131
  25. package/templates/minimal-app/README.md.tpl +50 -19
  26. package/templates/minimal-app/apps/e2e/package.json.tpl +23 -23
  27. package/templates/minimal-app/apps/e2e/playwright.config.ts.tpl +51 -39
  28. package/templates/minimal-app/apps/e2e/playwright.expo.config.ts.tpl +38 -38
  29. package/templates/minimal-app/apps/e2e/servers.ts.tpl +136 -112
  30. package/templates/minimal-app/apps/e2e/tests/conversation.spec.ts.tpl +22 -0
  31. package/templates/minimal-app/apps/e2e/tests/email.spec.ts.tpl +47 -47
  32. package/templates/minimal-app/apps/e2e/tests/expo-helpers.ts.tpl +73 -73
  33. package/templates/minimal-app/apps/e2e/tests/expo-standard.spec.ts.tpl +517 -517
  34. package/templates/minimal-app/apps/e2e/tests/expo-waitlist.spec.ts.tpl +94 -94
  35. package/templates/minimal-app/apps/e2e/tests/files.spec.ts.tpl +35 -0
  36. package/templates/minimal-app/apps/e2e/tests/helpers.ts.tpl +289 -289
  37. package/templates/minimal-app/apps/e2e/tests/standard.spec.ts.tpl +516 -516
  38. package/templates/minimal-app/apps/e2e/tests/waitlist.spec.ts.tpl +107 -107
  39. package/templates/minimal-app/apps/e2e/tests/workflow.spec.ts.tpl +26 -0
  40. package/templates/minimal-app/apps/e2e/tsconfig.json.tpl +10 -10
  41. package/templates/minimal-app/apps/email/package.json.tpl +34 -34
  42. package/templates/minimal-app/apps/email/src/emails/trialEndingEmail.tsx.tpl +40 -0
  43. package/templates/minimal-app/apps/email/src/index.ts.tpl +7 -0
  44. package/templates/minimal-app/apps/email/src/resources.ts.tpl +72 -65
  45. package/templates/minimal-app/apps/expo/app.json.tpl +36 -36
  46. package/templates/minimal-app/apps/expo/eslint.config.js.tpl +8 -8
  47. package/templates/minimal-app/apps/expo/metro.config.js.tpl +16 -16
  48. package/templates/minimal-app/apps/expo/src/app/_layout.tsx.tpl +31 -31
  49. package/templates/minimal-app/apps/expo/src/app/claim-account.tsx.tpl +151 -151
  50. package/templates/minimal-app/apps/expo/src/app/forgot-password.tsx.tpl +53 -53
  51. package/templates/minimal-app/apps/expo/src/app/index.tsx.tpl +8 -8
  52. package/templates/minimal-app/apps/expo/src/app/login.tsx.tpl +71 -71
  53. package/templates/minimal-app/apps/expo/src/app/logout.tsx.tpl +19 -19
  54. package/templates/minimal-app/apps/expo/src/app/organization/accept-invitation.tsx.tpl +85 -85
  55. package/templates/minimal-app/apps/expo/src/app/posts.tsx.tpl +40 -40
  56. package/templates/minimal-app/apps/expo/src/app/reset-password.tsx.tpl +75 -75
  57. package/templates/minimal-app/apps/expo/src/app/signup.tsx.tpl +193 -193
  58. package/templates/minimal-app/apps/expo/src/components/AuthScaffold.tsx.tpl +34 -34
  59. package/templates/minimal-app/apps/expo/src/components/FormTextField.tsx.tpl +37 -37
  60. package/templates/minimal-app/apps/expo/src/components/LoadingScreen.tsx.tpl +10 -10
  61. package/templates/minimal-app/apps/expo/src/components/StatusText.tsx.tpl +22 -22
  62. package/templates/minimal-app/apps/expo/src/config.ts.tpl +9 -9
  63. package/templates/minimal-app/apps/expo/src/global.css.tpl +11 -11
  64. package/templates/minimal-app/apps/expo/src/lib/trpc.ts.tpl +20 -20
  65. package/templates/minimal-app/apps/expo/tsconfig.json.tpl +11 -11
  66. package/templates/minimal-app/apps/expo/uniwind-env.d.ts.tpl +1 -1
  67. package/templates/minimal-app/apps/landing/.env.production.example.tpl +7 -0
  68. package/templates/minimal-app/apps/landing/Dockerfile.tpl +39 -0
  69. package/templates/minimal-app/apps/landing/fly.toml.tpl +20 -0
  70. package/templates/minimal-app/apps/landing/index.html.tpl +35 -0
  71. package/templates/minimal-app/apps/landing/package.json.tpl +34 -0
  72. package/templates/minimal-app/apps/landing/scripts/fly-deploy.mjs.tpl +75 -0
  73. package/templates/minimal-app/apps/landing/scripts/fly-secrets.mjs.tpl +31 -0
  74. package/templates/minimal-app/apps/landing/src/App.tsx.tpl +12 -0
  75. package/templates/minimal-app/apps/landing/src/LandingPage.tsx.tpl +37 -0
  76. package/templates/minimal-app/apps/landing/src/index.css.tpl +24 -0
  77. package/templates/minimal-app/apps/landing/src/main.tsx.tpl +16 -0
  78. package/templates/minimal-app/apps/landing/src/vite-env.d.ts.tpl +11 -0
  79. package/templates/minimal-app/apps/landing/tsconfig.json.tpl +12 -0
  80. package/templates/minimal-app/apps/landing/vite.config.ts.tpl +25 -0
  81. package/templates/minimal-app/apps/server/AGENTS.md.tpl +54 -46
  82. package/templates/minimal-app/apps/server/{drizzle/seed.e2e.ts.tpl → db.e2e.ts.tpl} +57 -40
  83. package/templates/minimal-app/apps/server/{drizzle/seed.ts.tpl → db.ts.tpl} +27 -20
  84. package/templates/minimal-app/apps/server/drizzle/0003_silly_the_liberteens.sql +27 -0
  85. package/templates/minimal-app/apps/server/drizzle/meta/0000_snapshot.json.tpl +56 -166
  86. package/templates/minimal-app/apps/server/drizzle/meta/0001_snapshot.json.tpl +1796 -1910
  87. package/templates/minimal-app/apps/server/drizzle/meta/0002_snapshot.json.tpl +1818 -1932
  88. package/templates/minimal-app/apps/server/drizzle/meta/0003_snapshot.json.tpl +2137 -0
  89. package/templates/minimal-app/apps/server/drizzle/meta/_journal.json.tpl +8 -1
  90. package/templates/minimal-app/apps/server/drizzle.config.ts.tpl +3 -33
  91. package/templates/minimal-app/apps/server/package.json.tpl +5 -3
  92. package/templates/minimal-app/apps/server/src/app.ts.tpl +36 -0
  93. package/templates/minimal-app/apps/server/src/modules/conversation/conversation.mastra.ts.tpl +26 -0
  94. package/templates/minimal-app/apps/server/src/modules/demo-workflow/demo-workflow.module.ts.tpl +47 -0
  95. package/templates/minimal-app/apps/server/src/modules/demo-workflow/demo-workflow.service.ts.tpl +43 -0
  96. package/templates/minimal-app/apps/server/src/modules/demo-workflow/demo-workflow.trpc.ts.tpl +16 -0
  97. package/templates/minimal-app/apps/server/src/modules/posts/posts.db.ts.tpl +28 -28
  98. package/templates/minimal-app/apps/server/src/modules/posts/posts.grants.ts.tpl +28 -28
  99. package/templates/minimal-app/apps/server/src/modules/posts/posts.module.ts.tpl +54 -54
  100. package/templates/minimal-app/apps/server/src/modules/posts/posts.repository.ts.tpl +67 -67
  101. package/templates/minimal-app/apps/server/src/modules/posts/posts.service.ts.tpl +116 -116
  102. package/templates/minimal-app/apps/server/src/modules/posts/posts.trpc.ts.tpl +37 -37
  103. package/templates/minimal-app/apps/server/src/modules/test-harness/test-harness.module.ts.tpl +279 -279
  104. package/templates/minimal-app/apps/server/src/schema.ts.tpl +31 -20
  105. package/templates/minimal-app/apps/server/src/types.ts.tpl +12 -12
  106. package/templates/minimal-app/apps/server/tsconfig.json.tpl +1 -1
  107. package/templates/minimal-app/apps/server/tsup.config.ts.tpl +23 -9
  108. package/templates/minimal-app/apps/shared/.env.example.tpl +5 -0
  109. package/templates/minimal-app/apps/shared/.env.production.example.tpl +23 -0
  110. package/templates/minimal-app/apps/shared/.env.tpl +5 -0
  111. package/templates/minimal-app/apps/shared/Dockerfile.tpl +48 -0
  112. package/templates/minimal-app/apps/shared/fly.toml.tpl +24 -0
  113. package/templates/minimal-app/apps/shared/package.json.tpl +25 -24
  114. package/templates/minimal-app/apps/shared/scripts/fly-deploy.mjs.tpl +75 -0
  115. package/templates/minimal-app/apps/shared/scripts/fly-secrets.mjs.tpl +31 -0
  116. package/templates/minimal-app/apps/shared/src/modules/app/app.constants.ts.tpl +28 -28
  117. package/templates/minimal-app/apps/shared/src/modules/conversation/conversation.constants.ts.tpl +1 -0
  118. package/templates/minimal-app/apps/shared/src/modules/notification/notification.schema.ts.tpl +3 -3
  119. package/templates/minimal-app/apps/shared/src/modules/posts/posts.constants.ts.tpl +5 -5
  120. package/templates/minimal-app/apps/shared/src/modules/posts/posts.schema.ts.tpl +25 -25
  121. package/templates/minimal-app/apps/webapp/package.json.tpl +53 -53
  122. package/templates/minimal-app/apps/webapp/public/push-sw.js.tpl +34 -34
  123. package/templates/minimal-app/apps/webapp/src/App.tsx.tpl +46 -44
  124. package/templates/minimal-app/apps/webapp/src/Layout.tsx.tpl +96 -65
  125. package/templates/minimal-app/apps/webapp/src/Router.tsx.tpl +114 -93
  126. package/templates/minimal-app/apps/webapp/src/index.css.tpl +15 -15
  127. package/templates/minimal-app/apps/webapp/src/main.tsx.tpl +26 -26
  128. package/templates/minimal-app/apps/webapp/src/modules/conversation/ConversationRoute.tsx.tpl +36 -0
  129. package/templates/minimal-app/apps/webapp/src/modules/files/FilesRoute.tsx.tpl +88 -0
  130. package/templates/minimal-app/apps/webapp/src/modules/files/hooks/useFilesRoute.ts.tpl +50 -0
  131. package/templates/minimal-app/apps/webapp/src/modules/workflows/WorkflowsRoute.tsx.tpl +97 -0
  132. package/templates/minimal-app/apps/webapp/src/modules/workflows/hooks/useWorkflowsRoute.ts.tpl +64 -0
  133. package/templates/minimal-app/apps/webapp/src/utils/i18n.ts.tpl +13 -13
  134. package/templates/minimal-app/apps/webapp/src/utils/trpc.ts.tpl +10 -10
  135. package/templates/minimal-app/apps/webapp/src/vite-env.d.ts.tpl +1 -1
  136. package/templates/minimal-app/apps/webapp/translations/en/app.json.tpl +7 -7
  137. package/templates/minimal-app/apps/webapp/translations/en/{{APP_SLUG}}.json.tpl +193 -135
  138. package/templates/minimal-app/apps/webapp/tsconfig.json.tpl +17 -17
  139. package/templates/minimal-app/apps/webapp/vite.config.ts.tpl +31 -31
  140. package/templates/minimal-app/biome.json.tpl +17 -12
  141. package/templates/minimal-app/package.json.tpl +6 -2
  142. package/templates/minimal-app/pnpm-workspace.yaml.tpl +14 -7
  143. package/templates/minimal-app/template.manifest.json +92 -2
  144. package/templates/minimal-app/apps/server/drizzle/db.ts.tpl +0 -26
  145. package/templates/minimal-app/apps/server/drizzle/guard.ts.tpl +0 -36
  146. package/templates/minimal-app/apps/server/drizzle/reset.ts.tpl +0 -40
  147. package/templates/minimal-app/apps/server/drizzle/sync.ts.tpl +0 -16
@@ -22,6 +22,13 @@
22
22
  "when": 1786951360347,
23
23
  "tag": "0002_concerned_proudstar",
24
24
  "breakpoints": true
25
+ },
26
+ {
27
+ "idx": 3,
28
+ "version": "6",
29
+ "when": 1787145865079,
30
+ "tag": "0003_silly_the_liberteens",
31
+ "breakpoints": true
25
32
  }
26
33
  ]
27
- }
34
+ }
@@ -1,33 +1,3 @@
1
- import * as dotenv from "dotenv";
2
- import { defineConfig } from "drizzle-kit";
3
-
4
- dotenv.config({ path: process.env.DRIZZLE_ENV_PATH || "../shared/.env" });
5
-
6
- const url = process.env.TURSO_DATABASE_URL || process.env.DATABASE_URL;
7
- const authToken = process.env.TURSO_AUTH_TOKEN;
8
- const schema = process.env.DRIZZLE_SCHEMA_PATH || "./src/schema.ts";
9
-
10
- if (!url) {
11
- throw new Error("DATABASE_URL or TURSO_DATABASE_URL must be set");
12
- }
13
-
14
- export default defineConfig(
15
- url && authToken
16
- ? {
17
- dialect: "turso",
18
- schema,
19
- out: "./drizzle",
20
- dbCredentials: {
21
- url,
22
- authToken,
23
- },
24
- }
25
- : {
26
- dialect: "sqlite",
27
- schema,
28
- out: "./drizzle",
29
- dbCredentials: {
30
- url,
31
- },
32
- }
33
- );
1
+ import { defineDrizzleKitConfig } from "@m5kdev/backend/db";
2
+
3
+ export default defineDrizzleKitConfig({ schema: "./src/schema.ts" });
@@ -13,9 +13,9 @@
13
13
  "drizzle:generate": "drizzle-kit generate",
14
14
  "drizzle:migrate": "drizzle-kit migrate",
15
15
  "drizzle:studio": "drizzle-kit studio",
16
- "drizzle:sync": "tsx --env-file=../shared/.env drizzle/sync.ts",
17
- "drizzle:seed": "tsx --env-file=../shared/.env drizzle/seed.ts",
18
- "drizzle:reset": "tsx --env-file=../shared/.env drizzle/reset.ts"
16
+ "drizzle:sync": "tsx --env-file=../shared/.env db.ts sync",
17
+ "drizzle:seed": "tsx --env-file=../shared/.env db.ts seed",
18
+ "drizzle:reset": "tsx --env-file=../shared/.env db.ts reset"
19
19
  },
20
20
  "dependencies": {
21
21
  "{{PACKAGE_SCOPE}}/email": "workspace:*",
@@ -23,6 +23,8 @@
23
23
  "@libsql/client": "catalog:m5kdev",
24
24
  "@m5kdev/backend": "catalog:m5kdev",
25
25
  "@m5kdev/commons": "catalog:m5kdev",
26
+ "@mastra/core": "catalog:m5kdev",
27
+ "@openrouter/ai-sdk-provider": "catalog:m5kdev",
26
28
  "@opentelemetry/api": "catalog:m5kdev",
27
29
  "@trpc/server": "catalog:m5kdev",
28
30
  "better-auth": "catalog:m5kdev",
@@ -1,10 +1,21 @@
1
1
  import { createBackendApp } from "@m5kdev/backend/app";
2
2
  import { shutdownTelemetry } from "@m5kdev/backend/lib/otel";
3
+ // m5k:ai:start
4
+ import { AIModule } from "@m5kdev/backend/modules/ai/ai.module";
5
+ // m5k:ai:end
3
6
  import { createBetterAuth } from "@m5kdev/backend/modules/auth/auth.lib";
4
7
  import { AuthModule } from "@m5kdev/backend/modules/auth/auth.module";
5
8
  import { EmailModule } from "@m5kdev/backend/modules/email/email.module";
6
9
  import { EmailPreviewModule } from "@m5kdev/backend/modules/email/email.preview.module";
10
+ // m5k:files:start
11
+ import { FileModule } from "@m5kdev/backend/modules/file/file.module";
12
+ // m5k:files:end
13
+ // m5k:notifications:start
14
+ import { NotificationModule } from "@m5kdev/backend/modules/notification/notification.module";
15
+ // m5k:notifications:end
16
+ // m5k:workflows:start
7
17
  import { WorkflowModule } from "@m5kdev/backend/modules/workflow/workflow.module";
18
+ // m5k:workflows:end
8
19
  import { templates } from "{{PACKAGE_SCOPE}}/email";
9
20
  import { emailResources } from "{{PACKAGE_SCOPE}}/email/resources";
10
21
  import {
@@ -12,6 +23,12 @@ import {
12
23
  APP_NAME,
13
24
  APP_ROLES_CONFIG,
14
25
  } from "{{PACKAGE_SCOPE}}/shared/modules/app/app.constants";
26
+ // m5k:ai:start
27
+ import { mastra, openrouter } from "./modules/conversation/conversation.mastra";
28
+ // m5k:ai:end
29
+ // m5k:workflows:start
30
+ import { DemoWorkflowModule } from "./modules/demo-workflow/demo-workflow.module";
31
+ // m5k:workflows:end
15
32
  import { PostsModule } from "./modules/posts/posts.module";
16
33
  // m5k:test-harness:start
17
34
  import { TestHarnessModule } from "./modules/test-harness/test-harness.module";
@@ -23,7 +40,9 @@ const serverUrl = process.env.VITE_SERVER_URL ?? "http://localhost:8080";
23
40
  const databaseUrl = process.env.DATABASE_URL ?? "file:./local.db";
24
41
  const syncUrl = process.env.TURSO_DATABASE_URL;
25
42
  const authToken = process.env.TURSO_AUTH_TOKEN;
43
+ // m5k:workflows:start
26
44
  const redisUrl = process.env.REDIS_URL ?? "redis://127.0.0.1:6379";
45
+ // m5k:workflows:end
27
46
  const resendApiKey = process.env.RESEND_API_KEY;
28
47
  const enableWaitlist = process.env.VITE_ENABLE_WAITLIST === "true";
29
48
 
@@ -57,10 +76,13 @@ export const builtBackendApp = createBackendApp(
57
76
  i18n: {
58
77
  resources: emailResources,
59
78
  },
79
+ spa: { root: "./client" },
80
+ // m5k:workflows:start
60
81
  redis: {
61
82
  url: redisUrl,
62
83
  options: { maxRetriesPerRequest: null },
63
84
  },
85
+ // m5k:workflows:end
64
86
  resend: resendApiKey ? { apiKey: resendApiKey } : undefined,
65
87
  email: {
66
88
  mode: resendApiKey ? "send" : "store",
@@ -93,6 +115,15 @@ export const builtBackendApp = createBackendApp(
93
115
  [
94
116
  new EmailModule(templates as never),
95
117
  new AuthModule(),
118
+ // m5k:ai:start
119
+ new AIModule({
120
+ libs: { mastra, openrouter },
121
+ }),
122
+ // m5k:ai:end
123
+ // m5k:files:start
124
+ new FileModule(),
125
+ // m5k:files:end
126
+ // m5k:workflows:start
96
127
  new WorkflowModule({
97
128
  queues: {
98
129
  fast: { concurrency: 5 },
@@ -104,6 +135,11 @@ export const builtBackendApp = createBackendApp(
104
135
  },
105
136
  reconcile: { enabled: true },
106
137
  }),
138
+ new DemoWorkflowModule(),
139
+ // m5k:workflows:end
140
+ // m5k:notifications:start
141
+ new NotificationModule(),
142
+ // m5k:notifications:end
107
143
  new PostsModule(),
108
144
  new EmailPreviewModule({ allowDelete: process.env.NODE_ENV !== "production" }),
109
145
  // m5k:test-harness:start
@@ -0,0 +1,26 @@
1
+ import { Mastra } from "@mastra/core";
2
+ import { Agent } from "@mastra/core/agent";
3
+ import { createOpenRouter } from "@openrouter/ai-sdk-provider";
4
+ import { STARTER_ASSISTANT_AGENT_ID } from "{{PACKAGE_SCOPE}}/shared/modules/conversation/conversation.constants";
5
+
6
+ /** Cheap OpenRouter chat model for the starter Conversation example. */
7
+ const STARTER_CHAT_MODEL = "google/gemini-2.5-flash";
8
+
9
+ export const openrouter = createOpenRouter({
10
+ apiKey: process.env.OPENROUTER_API_KEY ?? "",
11
+ });
12
+
13
+ const assistant = new Agent({
14
+ id: STARTER_ASSISTANT_AGENT_ID,
15
+ name: "Assistant",
16
+ instructions: "You are a helpful assistant for this starter app. Keep answers concise.",
17
+ model: openrouter.chat(STARTER_CHAT_MODEL, {
18
+ usage: { include: true },
19
+ }),
20
+ });
21
+
22
+ export const mastra = new Mastra({
23
+ agents: {
24
+ [STARTER_ASSISTANT_AGENT_ID]: assistant,
25
+ },
26
+ });
@@ -0,0 +1,47 @@
1
+ import { createBackendRouterMap } from "@m5kdev/backend/app";
2
+ import {
3
+ BaseModule,
4
+ type ModuleServicesContext,
5
+ type ModuleTRPCContext,
6
+ type TableMap,
7
+ } from "@m5kdev/backend/modules/base/base.module";
8
+ import type { WorkflowModule } from "@m5kdev/backend/modules/workflow/workflow.module";
9
+ import { DemoWorkflowService } from "./demo-workflow.service";
10
+ import { createDemoWorkflowTRPC } from "./demo-workflow.trpc";
11
+
12
+ type DemoWorkflowModuleDeps = { workflow: WorkflowModule };
13
+ type DemoWorkflowModuleServices = {
14
+ demoWorkflow: DemoWorkflowService;
15
+ };
16
+ type DemoWorkflowModuleRouters = {
17
+ demoWorkflow: ReturnType<typeof createDemoWorkflowTRPC>;
18
+ };
19
+
20
+ export class DemoWorkflowModule extends BaseModule<
21
+ DemoWorkflowModuleDeps,
22
+ TableMap,
23
+ Record<string, never>,
24
+ DemoWorkflowModuleServices,
25
+ DemoWorkflowModuleRouters
26
+ > {
27
+ readonly id = "demo-workflow";
28
+ override readonly dependsOn = ["workflow"] as const;
29
+
30
+ override services({
31
+ deps,
32
+ }: ModuleServicesContext<DemoWorkflowModuleDeps, Record<string, never>>) {
33
+ return {
34
+ demoWorkflow: new DemoWorkflowService({ workflow: deps.workflow.services.workflow }),
35
+ };
36
+ }
37
+
38
+ override trpc({
39
+ trpc,
40
+ services,
41
+ }: ModuleTRPCContext<DemoWorkflowModuleDeps, DemoWorkflowModuleServices>) {
42
+ return createBackendRouterMap(
43
+ "demoWorkflow",
44
+ createDemoWorkflowTRPC(trpc, services.demoWorkflow)
45
+ );
46
+ }
47
+ }
@@ -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,28 +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: "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
+ 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
+ }