create-m5kdev 0.14.0 → 0.15.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-m5kdev",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "license": "GPL-3.0-only",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,113 @@
1
+ import { createBackendApp, type InferBackendAppRouter } from "@m5kdev/backend/app";
2
+ import { createBetterAuth } from "@m5kdev/backend/modules/auth/auth.lib";
3
+ import { createAuthBackendModule } from "@m5kdev/backend/modules/auth/auth.module";
4
+ import { createEmailBackendModule } from "@m5kdev/backend/modules/email/email.module";
5
+ import { createNotificationBackendModule } from "@m5kdev/backend/modules/notification/notification.module";
6
+ import { createWorkflowBackendModule } from "@m5kdev/backend/modules/workflow/workflow.module";
7
+ import { templates } from "{{PACKAGE_SCOPE}}/email";
8
+ import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";
9
+ import cors from "cors";
10
+ import express from "express";
11
+ import { postsModule } from "./modules/posts/posts.module";
12
+
13
+ const app = express();
14
+ const appUrl = process.env.VITE_APP_URL ?? "http://localhost:5173";
15
+ const serverUrl = process.env.VITE_SERVER_URL ?? "http://localhost:8080";
16
+
17
+ app.use(express.json());
18
+ app.use(
19
+ cors({
20
+ origin: [appUrl],
21
+ credentials: true,
22
+ methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
23
+ allowedHeaders: ["Content-Type", "Authorization"],
24
+ })
25
+ );
26
+
27
+ const databaseUrl = process.env.DATABASE_URL ?? "file:./local.db";
28
+ const syncUrl = process.env.TURSO_DATABASE_URL;
29
+ const authToken = process.env.TURSO_AUTH_TOKEN;
30
+ const redisUrl = process.env.REDIS_URL ?? "redis://127.0.0.1:6379";
31
+ const resendApiKey = process.env.RESEND_API_KEY;
32
+
33
+ const connection =
34
+ syncUrl && authToken
35
+ ? {
36
+ url: databaseUrl,
37
+ syncUrl,
38
+ authToken,
39
+ syncInterval: 60,
40
+ readYourWrites: true,
41
+ }
42
+ : {
43
+ url: databaseUrl,
44
+ };
45
+
46
+ export const backendApp = createBackendApp({
47
+ db: connection,
48
+ express: app,
49
+ app: {
50
+ name: "{{APP_NAME}}",
51
+ urls: {
52
+ web: appUrl,
53
+ api: serverUrl,
54
+ },
55
+ },
56
+ redis: {
57
+ url: redisUrl,
58
+ options: { maxRetriesPerRequest: null },
59
+ },
60
+ resend: resendApiKey ? { apiKey: resendApiKey } : undefined,
61
+ email: {
62
+ mode: resendApiKey ? "send" : "store",
63
+ from: "no-reply@local.m5kdev.test",
64
+ systemNotificationEmail: "ops@local.m5kdev.test",
65
+ outputDirectory: ".emails",
66
+ },
67
+ auth: {
68
+ factory({ db, services, appConfig }) {
69
+ return createBetterAuth({
70
+ orm: db.orm as never,
71
+ schema: db.schema as never,
72
+ services: {
73
+ email: services.email.email,
74
+ },
75
+ app: appConfig,
76
+ config: {
77
+ waitlist: false,
78
+ },
79
+ });
80
+ },
81
+ },
82
+ })
83
+ .use(
84
+ createEmailBackendModule({
85
+ templates: templates as never,
86
+ })
87
+ )
88
+ .use(
89
+ createAuthBackendModule({
90
+ emailModuleId: "email",
91
+ })
92
+ )
93
+ .use(
94
+ createWorkflowBackendModule({
95
+ queues: {
96
+ fast: { concurrency: 5 },
97
+ },
98
+ defaultQueue: "fast",
99
+ defaults: {
100
+ timeout: 60_000,
101
+ jobOptions: { removeOnComplete: { age: 3600 } },
102
+ },
103
+ })
104
+ )
105
+ .use(createNotificationBackendModule())
106
+ .use(postsModule);
107
+
108
+ export const builtBackendApp = backendApp.build();
109
+ export const appRouter = builtBackendApp.trpc.router;
110
+
111
+ export type AppRouter = InferBackendAppRouter<typeof backendApp>;
112
+ export type RouterInputs = inferRouterInputs<AppRouter>;
113
+ export type RouterOutputs = inferRouterOutputs<AppRouter>;
@@ -1,37 +1,7 @@
1
- import * as auth from "@m5kdev/backend/modules/auth/auth.db";
2
- import * as notification from "@m5kdev/backend/modules/notification/notification.db";
3
- import * as workflow from "@m5kdev/backend/modules/workflow/workflow.db";
4
- import { drizzle } from "drizzle-orm/libsql";
5
- import * as posts from "./modules/posts/posts.db";
6
-
7
- export const schema = {
8
- ...auth,
9
- ...workflow,
10
- ...notification,
11
- ...posts,
12
- };
13
-
14
- const databaseUrl = process.env.DATABASE_URL ?? "file:./local.db";
15
- const syncUrl = process.env.TURSO_DATABASE_URL;
16
- const authToken = process.env.TURSO_AUTH_TOKEN;
17
-
18
- const connection =
19
- syncUrl && authToken
20
- ? {
21
- url: databaseUrl,
22
- syncUrl,
23
- authToken,
24
- syncInterval: 60,
25
- readYourWrites: true,
26
- }
27
- : {
28
- url: databaseUrl,
29
- };
30
-
31
- export const orm = drizzle({
32
- connection,
33
- schema,
34
- });
35
-
36
- export type Orm = typeof orm;
37
- export type Schema = typeof schema;
1
+ import { builtBackendApp } from "./app";
2
+
3
+ export const orm = builtBackendApp.db.orm;
4
+ export const schema = builtBackendApp.db.schema;
5
+
6
+ export type Orm = typeof orm;
7
+ export type Schema = typeof schema;
@@ -1,58 +1,21 @@
1
- import { createAuthContext } from "@m5kdev/backend/utils/trpc";
2
- import * as trpcExpress from "@trpc/server/adapters/express";
3
- import { toNodeHandler } from "better-auth/node";
4
- import cors from "cors";
5
- import express from "express";
6
- import type { Server } from "node:http";
7
- import { auth } from "./lib/auth";
8
- import { notificationService } from "./service";
9
- import { appRouter } from "./trpc";
10
- import { workflowRegistry, workflowService } from "./workflow";
11
-
12
- workflowRegistry.registerService(notificationService);
13
-
14
- const app = express();
15
- const port = process.env.PORT ? Number.parseInt(process.env.PORT, 10) : 8080;
16
-
17
- let httpServer: Server | undefined;
18
-
19
- app.use(express.json());
20
- app.use(
21
- cors({
22
- origin: [process.env.VITE_APP_URL ?? "http://localhost:5173"],
23
- credentials: true,
24
- methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
25
- allowedHeaders: ["Content-Type", "Authorization"],
26
- }),
27
- );
28
-
29
- app.use(
30
- "/trpc",
31
- trpcExpress.createExpressMiddleware({
32
- router: appRouter,
33
- createContext: createAuthContext(auth as never),
34
- }),
35
- );
36
-
37
- app.all("/api/auth/*", toNodeHandler(auth));
38
-
39
- function logError(context: string, error: unknown): void {
40
- console.error(`[server] ${context}`, error);
41
- }
42
-
43
- async function shutdown(): Promise<void> {
44
- try {
45
- await workflowRegistry.stop();
46
- } catch (e) {
47
- logError("workflowRegistry.stop() failed", e);
48
- }
49
- try {
50
- await workflowService.close();
51
- } catch (e) {
52
- logError("workflowService.close() failed", e);
53
- }
54
- if (httpServer) {
55
- await new Promise<void>((resolve) => {
1
+ import type { Server } from "node:http";
2
+ import { builtBackendApp } from "./app";
3
+ const port = process.env.PORT ? Number.parseInt(process.env.PORT, 10) : 8080;
4
+
5
+ let httpServer: Server | undefined;
6
+
7
+ function logError(context: string, error: unknown): void {
8
+ console.error(`[server] ${context}`, error);
9
+ }
10
+
11
+ async function shutdown(): Promise<void> {
12
+ try {
13
+ await builtBackendApp.shutdown();
14
+ } catch (e) {
15
+ logError("builtBackendApp.shutdown() failed", e);
16
+ }
17
+ if (httpServer) {
18
+ await new Promise<void>((resolve) => {
56
19
  httpServer?.close((err) => {
57
20
  if (err) {
58
21
  logError("HTTP server close failed", err);
@@ -70,23 +33,23 @@ process.once("SIGINT", () => {
70
33
  process.once("SIGTERM", () => {
71
34
  void shutdown();
72
35
  });
73
-
74
- async function start(): Promise<void> {
75
- await workflowRegistry.start();
76
- await new Promise<void>((resolve, reject) => {
77
- httpServer = app.listen(port, () => {
78
- console.info(`Server running at ${process.env.VITE_SERVER_URL ?? `http://localhost:${port}`}`);
79
- resolve();
80
- });
36
+
37
+ async function start(): Promise<void> {
38
+ await builtBackendApp.start();
39
+ await new Promise<void>((resolve, reject) => {
40
+ httpServer = builtBackendApp.express.app.listen(port, () => {
41
+ console.info(`Server running at ${process.env.VITE_SERVER_URL ?? `http://localhost:${port}`}`);
42
+ resolve();
43
+ });
81
44
  httpServer.on("error", reject);
82
45
  });
83
46
  }
84
47
 
85
- void (async () => {
86
- try {
87
- await start();
88
- } catch (e) {
89
- logError("Fatal: workflowRegistry.start() or HTTP listen failed", e);
90
- process.exit(1);
91
- }
92
- })();
48
+ void (async () => {
49
+ try {
50
+ await start();
51
+ } catch (e) {
52
+ logError("Fatal: builtBackendApp.start() or HTTP listen failed", e);
53
+ process.exit(1);
54
+ }
55
+ })();
@@ -1,172 +1,6 @@
1
- import {
2
- createOrganizationAndTeam,
3
- getActiveOrganizationAndTeam,
4
- } from "@m5kdev/backend/modules/auth/auth.utils";
5
- import { betterAuth } from "better-auth";
6
- import { drizzleAdapter } from "better-auth/adapters/drizzle";
7
- import { admin, lastLoginMethod, organization } from "better-auth/plugins";
8
- import { orm, schema } from "../db";
9
- import { emailService } from "../service";
10
-
11
- export const auth = betterAuth({
12
- secret: process.env.BETTER_AUTH_SECRET,
13
- baseURL: process.env.VITE_SERVER_URL ?? "http://localhost:8080",
14
- session: {
15
- expiresIn: 60 * 60 * 24 * 7,
16
- updateAge: 60 * 60 * 24,
17
- cookieCache: {
18
- enabled: true,
19
- maxAge: 60 * 5,
20
- },
21
- additionalFields: {
22
- activeOrganizationRole: {
23
- type: "string",
24
- required: false,
25
- defaultValue: null,
26
- },
27
- activeTeamRole: {
28
- type: "string",
29
- required: false,
30
- defaultValue: null,
31
- },
32
- },
33
- },
34
- user: {
35
- additionalFields: {
36
- onboarding: {
37
- type: "number",
38
- required: false,
39
- defaultValue: null,
40
- },
41
- preferences: {
42
- type: "string",
43
- required: false,
44
- defaultValue: null,
45
- },
46
- flags: {
47
- type: "string",
48
- required: false,
49
- defaultValue: null,
50
- },
51
- stripeCustomerId: {
52
- type: "string",
53
- required: false,
54
- defaultValue: null,
55
- input: false,
56
- },
57
- paymentCustomerId: {
58
- type: "string",
59
- required: false,
60
- defaultValue: null,
61
- input: false,
62
- },
63
- paymentPlanTier: {
64
- type: "string",
65
- required: false,
66
- defaultValue: null,
67
- input: false,
68
- },
69
- paymentPlanExpiresAt: {
70
- type: "number",
71
- required: false,
72
- defaultValue: null,
73
- input: false,
74
- },
75
- },
76
- },
77
- database: drizzleAdapter(orm, {
78
- provider: "sqlite",
79
- schema,
80
- usePlural: true,
81
- }),
82
- emailAndPassword: {
83
- enabled: true,
84
- requireEmailVerification: false,
85
- sendResetPassword: async ({ user, url }) => {
86
- await emailService.sendResetPassword(user.email, url);
87
- },
88
- },
89
- emailVerification: {
90
- sendVerificationEmail: async ({ user, url }) => {
91
- await emailService.sendVerification(user.email, url);
92
- },
93
- },
94
- plugins: [
95
- admin(),
96
- lastLoginMethod(),
97
- organization({
98
- allowUserToCreateOrganization: false,
99
- teams: {
100
- enabled: true,
101
- allowRemovingAllTeams: false,
102
- },
103
- sendInvitationEmail: async (data) => {
104
- const invitationUrl = `${process.env.VITE_APP_URL ?? "http://localhost:5173"}/organization/accept-invitation?id=${data.id}`;
105
- await emailService.sendOrganizationInvite(
106
- data.email,
107
- data.organization.name,
108
- data.inviter.user.name || data.inviter.user.email,
109
- data.role,
110
- invitationUrl
111
- );
112
- },
113
- schema: {
114
- team: {
115
- modelName: "team",
116
- },
117
- teamMember: {
118
- modelName: "teamMember",
119
- additionalFields: {
120
- role: {
121
- type: "string",
122
- required: true,
123
- },
124
- },
125
- },
126
- member: {
127
- modelName: "member",
128
- },
129
- invitation: {
130
- modelName: "invitation",
131
- },
132
- organization: {
133
- modelName: "organization",
134
- },
135
- },
136
- }),
137
- ],
138
- trustedOrigins: [
139
- process.env.VITE_APP_URL ?? "http://localhost:5173",
140
- process.env.VITE_SERVER_URL ?? "http://localhost:8080",
141
- ],
142
- databaseHooks: {
143
- user: {
144
- create: {
145
- after: async (user) => {
146
- await createOrganizationAndTeam(orm, schema, user);
147
- },
148
- },
149
- },
150
- session: {
151
- create: {
152
- before: async (session) => {
153
- const { organizationId, teamId, organizationRole, teamRole } =
154
- await getActiveOrganizationAndTeam(orm, schema, session.userId);
155
-
156
- return {
157
- data: {
158
- ...session,
159
- activeOrganizationId: organizationId,
160
- activeTeamId: teamId,
161
- activeOrganizationRole: organizationRole,
162
- activeTeamRole: teamRole,
163
- },
164
- };
165
- },
166
- },
167
- },
168
- },
169
- });
170
-
171
- export type Session = typeof auth.$Infer.Session;
172
- export type User = Session["user"];
1
+ import { builtBackendApp } from "../app";
2
+
3
+ export const auth = builtBackendApp.auth!.instance;
4
+
5
+ export type Session = typeof auth.$Infer.Session;
6
+ export type User = Session["user"];
@@ -1,23 +1,45 @@
1
- import { 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, { onDelete: "set null" }),
8
- organizationId: text("organization_id").references(() => organizations.id, {
9
- onDelete: "set null",
10
- }),
11
- teamId: text("team_id").references(() => teams.id, { onDelete: "set null" }),
12
- title: text("title").notNull(),
13
- slug: text("slug").notNull().unique(),
14
- excerpt: text("excerpt"),
15
- content: text("content").notNull(),
16
- status: text("status").$type<"draft" | "published">().notNull().default("draft"),
17
- publishedAt: integer("published_at", { mode: "timestamp" }),
18
- createdAt: integer("created_at", { mode: "timestamp" })
19
- .notNull()
20
- .$default(() => new Date()),
21
- updatedAt: integer("updated_at", { mode: "timestamp" }),
22
- deletedAt: integer("deleted_at", { mode: "timestamp" }),
23
- });
1
+ import { type AnySQLiteColumn, integer, sqliteTable as table, text } from "drizzle-orm/sqlite-core";
2
+ import { organizations, teams, users } from "@m5kdev/backend/modules/auth/auth.db";
3
+ import { v4 as uuidv4 } from "uuid";
4
+
5
+ export function createPostsTables(
6
+ references: {
7
+ users: { id: AnySQLiteColumn };
8
+ organizations: { id: AnySQLiteColumn };
9
+ teams: { id: AnySQLiteColumn };
10
+ } = {
11
+ users,
12
+ organizations,
13
+ teams,
14
+ }
15
+ ) {
16
+ const posts = table("posts", {
17
+ id: text("id").primaryKey().$default(uuidv4),
18
+ authorUserId: text("author_user_id").references(() => references.users.id, {
19
+ onDelete: "set null",
20
+ }),
21
+ organizationId: text("organization_id").references(() => references.organizations.id, {
22
+ onDelete: "set null",
23
+ }),
24
+ teamId: text("team_id").references(() => references.teams.id, { onDelete: "set null" }),
25
+ title: text("title").notNull(),
26
+ slug: text("slug").notNull().unique(),
27
+ excerpt: text("excerpt"),
28
+ content: text("content").notNull(),
29
+ status: text("status").$type<"draft" | "published">().notNull().default("draft"),
30
+ publishedAt: integer("published_at", { mode: "timestamp" }),
31
+ createdAt: integer("created_at", { mode: "timestamp" })
32
+ .notNull()
33
+ .$default(() => new Date()),
34
+ updatedAt: integer("updated_at", { mode: "timestamp" }),
35
+ deletedAt: integer("deleted_at", { mode: "timestamp" }),
36
+ });
37
+
38
+ return {
39
+ posts,
40
+ };
41
+ }
42
+
43
+ const postTables = createPostsTables();
44
+
45
+ export const posts = postTables.posts;
@@ -0,0 +1,37 @@
1
+ import { defineBackendModule } from "@m5kdev/backend/app";
2
+ import { createPostsTRPC } from "./posts.trpc";
3
+ import { createPostsTables } from "./posts.db";
4
+ import { postsGrants } from "./posts.grants";
5
+ import { PostsRepository } from "./posts.repository";
6
+ import { PostsService } from "./posts.service";
7
+
8
+ export const postsModule = defineBackendModule({
9
+ id: "posts",
10
+ dependsOn: ["auth"],
11
+ db: ({ deps }) => {
12
+ const authTables = deps.auth.tables as any;
13
+ return {
14
+ tables: createPostsTables({
15
+ users: authTables.users,
16
+ organizations: authTables.organizations,
17
+ teams: authTables.teams,
18
+ }),
19
+ };
20
+ },
21
+ repositories: ({ db }) => {
22
+ const schema = db.schema as any;
23
+ return {
24
+ posts: new PostsRepository({
25
+ orm: db.orm as never,
26
+ schema: { posts: schema.posts } as never,
27
+ table: schema.posts,
28
+ }),
29
+ };
30
+ },
31
+ services: ({ repositories }) => ({
32
+ posts: new PostsService({ posts: repositories.posts }, {}, postsGrants),
33
+ }),
34
+ trpc: ({ trpc, services }) => ({
35
+ posts: createPostsTRPC(trpc, services.posts),
36
+ }),
37
+ });
@@ -1,13 +1,18 @@
1
- import type {
2
- PostSchema,
3
- PostsListInputSchema,
4
- PostsListOutputSchema,
5
- } from "{{PACKAGE_SCOPE}}/shared/modules/posts/posts.schema";
6
- import { BaseTableRepository } from "@m5kdev/backend/modules/base/base.repository";
7
- import type { ServerResultAsync } from "@m5kdev/backend/utils/types";
8
- import { and, asc, count, desc, eq, isNull, like, ne, or } from "drizzle-orm";
9
- import { err, ok } from "neverthrow";
10
- import type { Orm, Schema } from "../../db";
1
+ import type {
2
+ PostSchema,
3
+ PostsListInputSchema,
4
+ PostsListOutputSchema,
5
+ } from "{{PACKAGE_SCOPE}}/shared/modules/posts/posts.schema";
6
+ import { BaseTableRepository } from "@m5kdev/backend/modules/base/base.repository";
7
+ import type { ServerResultAsync } from "@m5kdev/backend/utils/types";
8
+ import { and, asc, count, desc, eq, isNull, like, ne, or } from "drizzle-orm";
9
+ import type { LibSQLDatabase } from "drizzle-orm/libsql";
10
+ import { err, ok } from "neverthrow";
11
+ import { posts } from "./posts.db";
12
+
13
+ const schema = { posts };
14
+ type Schema = typeof schema;
15
+ type Orm = LibSQLDatabase<Schema>;
11
16
 
12
17
  export class PostsRepository extends BaseTableRepository<
13
18
  Orm,
@@ -25,19 +30,11 @@ export class PostsRepository extends BaseTableRepository<
25
30
  conditions.push(eq(this.table.status, input.status));
26
31
  }
27
32
 
28
- if (search) {
29
- const escapedSearch = search.replace(/[%_]/g, '\\ if (search) {
30
- const pattern = `%${search}%`;
31
- const searchCondition = or(
32
- like(this.table.title, pattern),
33
- like(this.table.slug, pattern),
34
- like(this.table.excerpt, pattern),
35
- like(this.table.content, pattern)
36
- );
37
- ');
38
- const pattern = `%${escapedSearch}%`;
39
- const searchCondition = or(
40
- like(this.table.title, pattern),
33
+ if (search) {
34
+ const escapedSearch = search.replace(/[%_]/g, "\\$&");
35
+ const pattern = `%${escapedSearch}%`;
36
+ const searchCondition = or(
37
+ like(this.table.title, pattern),
41
38
  like(this.table.slug, pattern),
42
39
  like(this.table.excerpt, pattern),
43
40
  like(this.table.content, pattern)
@@ -111,5 +108,5 @@ export class PostsRepository extends BaseTableRepository<
111
108
  slug = `${candidate}-${suffix}`;
112
109
  suffix += 1;
113
110
  }
114
- }
115
- }
111
+ }
112
+ }
@@ -10,9 +10,9 @@ import type {
10
10
  PostUpdateInputSchema,
11
11
  PostUpdateOutputSchema,
12
12
  } from "{{PACKAGE_SCOPE}}/shared/modules/posts/posts.schema";
13
- import type { Context } from "@m5kdev/backend/modules/auth/auth.lib";
14
13
  import { BasePermissionService } from "@m5kdev/backend/modules/base/base.service";
15
14
  import type { ServerResultAsync } from "@m5kdev/backend/utils/types";
15
+ import type { Context } from "@m5kdev/backend/utils/trpc";
16
16
  import { err, ok } from "neverthrow";
17
17
  import type { PostsRepository } from "./posts.repository";
18
18
 
@@ -1,6 +1,6 @@
1
- import {
2
- postCreateInputSchema,
3
- postCreateOutputSchema,
1
+ import {
2
+ postCreateInputSchema,
3
+ postCreateOutputSchema,
4
4
  postPublishInputSchema,
5
5
  postPublishOutputSchema,
6
6
  postSoftDeleteInputSchema,
@@ -8,37 +8,41 @@ import {
8
8
  postsListInputSchema,
9
9
  postsListOutputSchema,
10
10
  postUpdateInputSchema,
11
- postUpdateOutputSchema,
12
- } from "{{PACKAGE_SCOPE}}/shared/modules/posts/posts.schema";
13
- import { handleTRPCResult } from "@m5kdev/backend/utils/trpc";
14
- import { postsService } from "../../service";
15
- import { procedure, router } from "../../utils/trpc";
16
-
17
- export const postsRouter = router({
18
- list: procedure
19
- .input(postsListInputSchema)
20
- .output(postsListOutputSchema)
21
- .query(async ({ ctx, input }) => handleTRPCResult(await postsService.list(input ?? {}, ctx))),
22
-
23
- create: procedure
24
- .input(postCreateInputSchema)
25
- .output(postCreateOutputSchema)
26
- .mutation(async ({ ctx, input }) => handleTRPCResult(await postsService.create(input, ctx))),
27
-
28
- update: procedure
29
- .input(postUpdateInputSchema)
30
- .output(postUpdateOutputSchema)
31
- .mutation(async ({ ctx, input }) => handleTRPCResult(await postsService.update(input, ctx))),
32
-
33
- publish: procedure
34
- .input(postPublishInputSchema)
35
- .output(postPublishOutputSchema)
36
- .mutation(async ({ ctx, input }) => handleTRPCResult(await postsService.publish(input, ctx))),
37
-
38
- softDelete: procedure
39
- .input(postSoftDeleteInputSchema)
40
- .output(postSoftDeleteOutputSchema)
41
- .mutation(async ({ ctx, input }) =>
42
- handleTRPCResult(await postsService.softDelete(input, ctx))
43
- ),
44
- });
11
+ postUpdateOutputSchema,
12
+ } from "{{PACKAGE_SCOPE}}/shared/modules/posts/posts.schema";
13
+ import { handleTRPCResult, type TRPCMethods } from "@m5kdev/backend/utils/trpc";
14
+ import type { PostsService } from "./posts.service";
15
+
16
+ export function createPostsTRPC(
17
+ { router, privateProcedure: procedure }: TRPCMethods,
18
+ postsService: PostsService
19
+ ) {
20
+ return router({
21
+ list: procedure
22
+ .input(postsListInputSchema)
23
+ .output(postsListOutputSchema)
24
+ .query(async ({ ctx, input }) => handleTRPCResult(await postsService.list(input ?? {}, ctx))),
25
+
26
+ create: procedure
27
+ .input(postCreateInputSchema)
28
+ .output(postCreateOutputSchema)
29
+ .mutation(async ({ ctx, input }) => handleTRPCResult(await postsService.create(input, ctx))),
30
+
31
+ update: procedure
32
+ .input(postUpdateInputSchema)
33
+ .output(postUpdateOutputSchema)
34
+ .mutation(async ({ ctx, input }) => handleTRPCResult(await postsService.update(input, ctx))),
35
+
36
+ publish: procedure
37
+ .input(postPublishInputSchema)
38
+ .output(postPublishOutputSchema)
39
+ .mutation(async ({ ctx, input }) => handleTRPCResult(await postsService.publish(input, ctx))),
40
+
41
+ softDelete: procedure
42
+ .input(postSoftDeleteInputSchema)
43
+ .output(postSoftDeleteOutputSchema)
44
+ .mutation(async ({ ctx, input }) =>
45
+ handleTRPCResult(await postsService.softDelete(input, ctx))
46
+ ),
47
+ });
48
+ }
@@ -1,10 +1,6 @@
1
- import { AuthRepository } from "@m5kdev/backend/modules/auth/auth.repository";
2
- import { NotificationRepository } from "@m5kdev/backend/modules/notification/notification.repository";
3
- import { WorkflowRepository } from "@m5kdev/backend/modules/workflow/workflow.repository";
4
- import { orm, schema } from "./db";
5
- import { PostsRepository } from "./modules/posts/posts.repository";
6
-
7
- export const authRepository = new AuthRepository({ orm, schema });
8
- export const workflowRepository = new WorkflowRepository({ orm, schema });
9
- export const notificationRepository = new NotificationRepository({ orm, schema });
10
- export const postsRepository = new PostsRepository({ orm, schema, table: schema.posts });
1
+ import { builtBackendApp } from "./app";
2
+
3
+ export const authRepository = builtBackendApp.modules.auth.repositories.auth;
4
+ export const workflowRepository = builtBackendApp.modules.workflow.repositories.workflow;
5
+ export const notificationRepository = builtBackendApp.modules.notification.repositories.notification;
6
+ export const postsRepository = builtBackendApp.modules.posts.repositories.posts;
@@ -1,19 +1,7 @@
1
- import { AuthService } from "@m5kdev/backend/modules/auth/auth.service";
2
- import { NotificationService } from "@m5kdev/backend/modules/notification/notification.service";
3
- import { LocalEmailService } from "./lib/localEmailService";
4
- import { postsGrants } from "./modules/posts/posts.grants";
5
- import { PostsService } from "./modules/posts/posts.service";
6
- import { authRepository, notificationRepository, postsRepository } from "./repository";
7
- import { workflowService } from "./workflow";
8
-
9
- export const emailService = new LocalEmailService({
10
- appName: "{{APP_NAME}}",
11
- appUrl: process.env.VITE_APP_URL ?? "http://localhost:5173",
12
- });
13
-
14
- export const authService = new AuthService({ auth: authRepository }, { email: emailService });
15
- export const postsService = new PostsService({ posts: postsRepository }, {}, postsGrants);
16
- export const notificationService = new NotificationService(
17
- { notification: notificationRepository },
18
- { workflow: workflowService },
19
- );
1
+ import { builtBackendApp, emailService } from "./app";
2
+
3
+ export { emailService };
4
+
5
+ export const authService = builtBackendApp.modules.auth.services.auth;
6
+ export const postsService = builtBackendApp.modules.posts.services.posts;
7
+ export const notificationService = builtBackendApp.modules.notification.services.notification;
@@ -1,16 +1,6 @@
1
- import { createNotificationTRPC } from "@m5kdev/backend/modules/notification/notification.trpc";
2
- import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";
3
- import { notificationService } from "./service";
4
- import { postsRouter as posts } from "./modules/posts/posts.trpc";
5
- import { router, trpcObject } from "./utils/trpc";
6
-
7
- const notification = createNotificationTRPC(trpcObject, notificationService);
8
-
9
- export const appRouter = router({
10
- posts,
11
- notification,
12
- });
13
-
14
- export type AppRouter = typeof appRouter;
15
- export type RouterInputs = inferRouterInputs<AppRouter>;
16
- export type RouterOutputs = inferRouterOutputs<AppRouter>;
1
+ export {
2
+ appRouter,
3
+ type AppRouter,
4
+ type RouterInputs,
5
+ type RouterOutputs,
6
+ } from "./app";
@@ -1,31 +1,8 @@
1
- import {
2
- type createAuthContext,
3
- verifyAdminProcedureContext,
4
- verifyProtectedProcedureContext,
5
- } from "@m5kdev/backend/utils/trpc";
6
- import { transformer } from "@m5kdev/commons/utils/trpc";
7
- import { initTRPC } from "@trpc/server";
8
-
9
- type Context = Awaited<ReturnType<ReturnType<typeof createAuthContext>>>;
10
-
11
- const t = initTRPC.context<Context>().create({ transformer });
12
-
13
- export const publicProcedure = t.procedure;
1
+ import { builtBackendApp } from "../app";
14
2
 
15
- export const procedure = t.procedure.use(({ ctx, next }) => {
16
- return next({ ctx: verifyProtectedProcedureContext(ctx) });
17
- });
3
+ export const router = builtBackendApp.trpc.methods.router;
4
+ export const publicProcedure = builtBackendApp.trpc.methods.publicProcedure;
5
+ export const procedure = builtBackendApp.trpc.methods.privateProcedure;
6
+ export const adminProcedure = builtBackendApp.trpc.methods.adminProcedure;
18
7
 
19
- export const adminProcedure = t.procedure.use(({ ctx, next }) => {
20
- return next({ ctx: verifyAdminProcedureContext(ctx) });
21
- });
22
-
23
- export const router = t.router;
24
- export const mergeRouters = t.mergeRouters;
25
-
26
- export const trpcObject = {
27
- router,
28
- privateProcedure: procedure,
29
- adminProcedure,
30
- publicProcedure,
31
- };
8
+ export const trpcObject = builtBackendApp.trpc.methods;
@@ -1,21 +1,4 @@
1
- import { WorkflowRegistry } from "@m5kdev/backend/modules/workflow/workflow.registry";
2
- import { WorkflowService } from "@m5kdev/backend/modules/workflow/workflow.service";
3
- import IORedis from "ioredis";
4
- import { workflowRepository } from "./repository";
5
-
6
- const redisUrl = process.env.REDIS_URL ?? "redis://127.0.0.1:6379";
7
- const connection = new IORedis(redisUrl, { maxRetriesPerRequest: null });
8
-
9
- export const workflowService = new WorkflowService(workflowRepository, {
10
- connection,
11
- queues: {
12
- fast: { concurrency: 5 },
13
- },
14
- defaultQueue: "fast",
15
- defaults: {
16
- timeout: 60_000,
17
- jobOptions: { removeOnComplete: { age: 3600 } },
18
- },
19
- });
20
-
21
- export const workflowRegistry = new WorkflowRegistry(workflowService);
1
+ import { builtBackendApp } from "./app";
2
+
3
+ export const workflowService = builtBackendApp.modules.workflow.services.workflow;
4
+ export const workflowRegistry = builtBackendApp.workflow!.registry;
@@ -1,58 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import { templates } from "{{PACKAGE_SCOPE}}/email";
4
- import { EmailService } from "@m5kdev/backend/modules/email/email.service";
5
- import { logger } from "@m5kdev/backend/utils/logger";
6
- import { ok } from "neverthrow";
7
-
8
- interface LocalEmailServiceProps {
9
- appName: string;
10
- appUrl: string;
11
- }
12
-
13
- export class LocalEmailService extends EmailService {
14
- constructor(props: LocalEmailServiceProps) {
15
- super({
16
- resendApiKey: "local",
17
- brand: {
18
- name: props.appName,
19
- logo: `${props.appUrl}/mark.svg`,
20
- tagline: `${props.appName} publishing workspace`,
21
- },
22
- noReplyFrom: "no-reply@local.m5kdev.test",
23
- systemNotificationEmail: "ops@local.m5kdev.test",
24
- templates: templates as never,
25
- });
26
- }
27
-
28
- override async sendTemplate(
29
- to: Parameters<EmailService["sendTemplate"]>[0],
30
- templateKey: Parameters<EmailService["sendTemplate"]>[1],
31
- templateProps: Parameters<EmailService["sendTemplate"]>[2],
32
- options?: Parameters<EmailService["sendTemplate"]>[3]
33
- ) {
34
- const template = this.templates[String(templateKey)];
35
- if (!template) {
36
- return this.error("NOT_FOUND", `Email template not found: ${String(templateKey)}`);
37
- }
38
-
39
- const outputDirectory = path.resolve(process.cwd(), ".emails");
40
- await fs.mkdir(outputDirectory, { recursive: true });
41
-
42
- const payload = {
43
- to,
44
- templateId: template.id,
45
- subject: options?.subject ?? template.subject ?? String(templateKey),
46
- previewText: options?.previewText ?? template.previewText ?? String(templateKey),
47
- props: templateProps,
48
- createdAt: new Date().toISOString(),
49
- };
50
-
51
- const filename = `${Date.now()}-${template.id}.json`;
52
- const outputPath = path.join(outputDirectory, filename);
53
- await fs.writeFile(outputPath, JSON.stringify(payload, null, 2), "utf8");
54
-
55
- logger.info(`Local email written to ${outputPath}`);
56
- return ok();
57
- }
58
- }