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 +1 -1
- package/templates/minimal-app/apps/server/src/app.ts.tpl +113 -0
- package/templates/minimal-app/apps/server/src/db.ts.tpl +7 -37
- package/templates/minimal-app/apps/server/src/index.ts.tpl +34 -71
- package/templates/minimal-app/apps/server/src/lib/auth.ts.tpl +6 -172
- package/templates/minimal-app/apps/server/src/modules/posts/posts.db.ts.tpl +45 -23
- package/templates/minimal-app/apps/server/src/modules/posts/posts.module.ts.tpl +37 -0
- package/templates/minimal-app/apps/server/src/modules/posts/posts.repository.ts.tpl +22 -25
- package/templates/minimal-app/apps/server/src/modules/posts/posts.service.ts.tpl +1 -1
- package/templates/minimal-app/apps/server/src/modules/posts/posts.trpc.ts.tpl +41 -37
- package/templates/minimal-app/apps/server/src/repository.ts.tpl +6 -10
- package/templates/minimal-app/apps/server/src/service.ts.tpl +7 -19
- package/templates/minimal-app/apps/server/src/trpc.ts.tpl +6 -16
- package/templates/minimal-app/apps/server/src/utils/trpc.ts.tpl +6 -29
- package/templates/minimal-app/apps/server/src/workflow.ts.tpl +4 -21
- package/templates/minimal-app/apps/server/src/lib/localEmailService.ts.tpl +0 -58
package/package.json
CHANGED
|
@@ -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
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export
|
|
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 {
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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 {
|
|
2
|
-
import {
|
|
3
|
-
import { v4 as uuidv4 } from "uuid";
|
|
4
|
-
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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 {
|
|
10
|
-
import
|
|
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,
|
|
30
|
-
const pattern = `%${
|
|
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 {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
})
|
|
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 {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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 {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
16
|
-
|
|
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
|
|
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 {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
}
|