create-m5kdev 0.5.0 → 0.7.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.5.0",
3
+ "version": "0.7.0",
4
4
  "license": "GPL-3.0-only",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,40 @@
1
+ import type { ResourceGrant } from "@m5kdev/backend/modules/base/base.grants";
2
+
3
+ export const postsGrants: ResourceGrant[] = [
4
+ {
5
+ action: "write",
6
+ level: "team",
7
+ role: "owner",
8
+ access: "own",
9
+ },
10
+ {
11
+ action: "publish",
12
+ level: "team",
13
+ role: "owner",
14
+ access: "own",
15
+ },
16
+ {
17
+ action: "delete",
18
+ level: "team",
19
+ role: "owner",
20
+ access: "own",
21
+ },
22
+ {
23
+ action: "write",
24
+ level: "organization",
25
+ role: "owner",
26
+ access: "own",
27
+ },
28
+ {
29
+ action: "publish",
30
+ level: "organization",
31
+ role: "owner",
32
+ access: "own",
33
+ },
34
+ {
35
+ action: "delete",
36
+ level: "organization",
37
+ role: "owner",
38
+ access: "own",
39
+ },
40
+ ];
@@ -1,135 +1,155 @@
1
- import type {
2
- PostCreateInputSchema,
3
- PostCreateOutputSchema,
1
+ import type {
2
+ PostCreateInputSchema,
3
+ PostCreateOutputSchema,
4
4
  PostPublishInputSchema,
5
5
  PostPublishOutputSchema,
6
6
  PostSoftDeleteInputSchema,
7
7
  PostSoftDeleteOutputSchema,
8
8
  PostsListInputSchema,
9
9
  PostsListOutputSchema,
10
- PostUpdateInputSchema,
11
- PostUpdateOutputSchema,
12
- } from "{{PACKAGE_SCOPE}}/shared/modules/posts/posts.schema";
13
- import { BaseService } from "@m5kdev/backend/modules/base/base.service";
14
- import type { ServerResultAsync } from "@m5kdev/backend/utils/types";
15
- import { err, ok } from "neverthrow";
16
- import type { PostsRepository } from "./posts.repository";
17
-
18
- type RequestContext = {
19
- session: {
20
- activeOrganizationId?: string | null;
21
- activeTeamId?: string | null;
22
- };
23
- user: {
24
- id: string;
25
- };
26
- };
27
-
28
- export class PostsService extends BaseService<{ posts: PostsRepository }, Record<string, never>> {
29
- async list(
30
- input: PostsListInputSchema,
31
- _ctx: RequestContext
32
- ): ServerResultAsync<PostsListOutputSchema> {
33
- return this.repository.posts.list(input);
34
- }
35
-
36
- async create(
37
- input: PostCreateInputSchema,
38
- ctx: RequestContext
39
- ): ServerResultAsync<PostCreateOutputSchema> {
40
- const uniqueSlug = await this.repository.posts.resolveUniqueSlug(
41
- this.slugify(input.slug ?? input.title)
42
- );
43
- if (uniqueSlug.isErr()) {
44
- return err(uniqueSlug.error);
45
- }
46
-
47
- return this.repository.posts.create({
48
- authorUserId: ctx.user.id,
49
- organizationId: ctx.session.activeOrganizationId ?? null,
50
- teamId: ctx.session.activeTeamId ?? null,
51
- title: input.title.trim(),
52
- slug: uniqueSlug.value,
53
- excerpt: this.createExcerpt(input.excerpt, input.content),
54
- content: input.content.trim(),
55
- status: "draft",
56
- }) as ServerResultAsync<PostCreateOutputSchema>;
57
- }
58
-
59
- async update(
60
- input: PostUpdateInputSchema,
61
- _ctx: RequestContext
62
- ): ServerResultAsync<PostUpdateOutputSchema> {
63
- const current = await this.repository.posts.findById(input.id);
64
- if (current.isErr()) {
65
- return err(current.error);
66
- }
67
- if (!current.value || current.value.deletedAt) {
68
- return this.error("NOT_FOUND", "Post not found");
69
- }
70
-
71
- const uniqueSlug = await this.repository.posts.resolveUniqueSlug(
72
- this.slugify(input.slug ?? input.title),
73
- input.id
74
- );
75
- if (uniqueSlug.isErr()) {
76
- return err(uniqueSlug.error);
77
- }
78
-
79
- return this.repository.posts.update({
80
- id: input.id,
81
- title: input.title.trim(),
82
- slug: uniqueSlug.value,
83
- excerpt: this.createExcerpt(input.excerpt, input.content),
84
- content: input.content.trim(),
85
- }) as ServerResultAsync<PostUpdateOutputSchema>;
86
- }
87
-
88
- async publish(
89
- input: PostPublishInputSchema,
90
- _ctx: RequestContext
91
- ): ServerResultAsync<PostPublishOutputSchema> {
92
- const current = await this.repository.posts.findById(input.id);
93
- if (current.isErr()) {
94
- return err(current.error);
95
- }
96
- if (!current.value || current.value.deletedAt) {
97
- return this.error("NOT_FOUND", "Post not found");
98
- }
99
-
100
- return this.repository.posts.update({
101
- id: input.id,
102
- status: "published",
103
- publishedAt: current.value.publishedAt ?? new Date(),
104
- }) as ServerResultAsync<PostPublishOutputSchema>;
105
- }
106
-
107
- async softDelete(
108
- input: PostSoftDeleteInputSchema,
109
- _ctx: RequestContext
110
- ): ServerResultAsync<PostSoftDeleteOutputSchema> {
111
- const current = await this.repository.posts.findById(input.id);
112
- if (current.isErr()) {
113
- return err(current.error);
114
- }
115
- if (!current.value || current.value.deletedAt) {
116
- return this.error("NOT_FOUND", "Post not found");
117
- }
118
-
119
- const updated = await this.repository.posts.update({
120
- id: input.id,
121
- deletedAt: new Date(),
122
- });
123
-
124
- if (updated.isErr()) {
125
- return err(updated.error);
126
- }
127
-
128
- return ok({ id: updated.value.id });
129
- }
130
-
131
- private slugify(value: string): string {
132
- const slug = value
10
+ PostUpdateInputSchema,
11
+ PostUpdateOutputSchema,
12
+ } from "{{PACKAGE_SCOPE}}/shared/modules/posts/posts.schema";
13
+ import type { Context } from "@m5kdev/backend/modules/auth/auth.lib";
14
+ import { BasePermissionService } from "@m5kdev/backend/modules/base/base.service";
15
+ import type { ServerResultAsync } from "@m5kdev/backend/utils/types";
16
+ import { err, ok } from "neverthrow";
17
+ import type { PostsRepository } from "./posts.repository";
18
+
19
+ type RequestContext = Context;
20
+
21
+ export class PostsService extends BasePermissionService<
22
+ { posts: PostsRepository },
23
+ Record<string, never>,
24
+ RequestContext
25
+ > {
26
+ readonly list = this.procedure<PostsListInputSchema>("list")
27
+ .requireAuth()
28
+ .handle(({ input }): ServerResultAsync<PostsListOutputSchema> => {
29
+ return this.repository.posts.list(input);
30
+ });
31
+
32
+ readonly create = this.procedure<PostCreateInputSchema>("create")
33
+ .requireAuth()
34
+ .access({
35
+ action: "write",
36
+ entities: ({ ctx }) => ({
37
+ organizationId: ctx.session.activeOrganizationId ?? null,
38
+ teamId: ctx.session.activeTeamId ?? null,
39
+ }),
40
+ })
41
+ .handle(async ({ input, ctx }): ServerResultAsync<PostCreateOutputSchema> => {
42
+ const uniqueSlug = await this.repository.posts.resolveUniqueSlug(
43
+ this.slugify(input.slug ?? input.title)
44
+ );
45
+ if (uniqueSlug.isErr()) {
46
+ return err(uniqueSlug.error);
47
+ }
48
+
49
+ return this.repository.posts.create({
50
+ authorUserId: ctx.user.id,
51
+ organizationId: ctx.session.activeOrganizationId ?? null,
52
+ teamId: ctx.session.activeTeamId ?? null,
53
+ title: input.title.trim(),
54
+ slug: uniqueSlug.value,
55
+ excerpt: this.createExcerpt(input.excerpt, input.content),
56
+ content: input.content.trim(),
57
+ status: "draft",
58
+ }) as ServerResultAsync<PostCreateOutputSchema>;
59
+ });
60
+
61
+ readonly update = this.procedure<PostUpdateInputSchema>("update")
62
+ .requireAuth()
63
+ .use("post", async ({ input }) => {
64
+ const current = await this.repository.posts.findById(input.id);
65
+ if (current.isErr()) {
66
+ return err(current.error);
67
+ }
68
+ if (!current.value || current.value.deletedAt) {
69
+ return this.error("NOT_FOUND", "Post not found");
70
+ }
71
+
72
+ return current.value;
73
+ })
74
+ .access({
75
+ action: "write",
76
+ entityStep: "post",
77
+ })
78
+ .handle(async ({ input }): ServerResultAsync<PostUpdateOutputSchema> => {
79
+ const uniqueSlug = await this.repository.posts.resolveUniqueSlug(
80
+ this.slugify(input.slug ?? input.title),
81
+ input.id
82
+ );
83
+ if (uniqueSlug.isErr()) {
84
+ return err(uniqueSlug.error);
85
+ }
86
+
87
+ return this.repository.posts.update({
88
+ id: input.id,
89
+ title: input.title.trim(),
90
+ slug: uniqueSlug.value,
91
+ excerpt: this.createExcerpt(input.excerpt, input.content),
92
+ content: input.content.trim(),
93
+ }) as ServerResultAsync<PostUpdateOutputSchema>;
94
+ });
95
+
96
+ readonly publish = this.procedure<PostPublishInputSchema>("publish")
97
+ .requireAuth()
98
+ .use("post", async ({ input }) => {
99
+ const current = await this.repository.posts.findById(input.id);
100
+ if (current.isErr()) {
101
+ return err(current.error);
102
+ }
103
+ if (!current.value || current.value.deletedAt) {
104
+ return this.error("NOT_FOUND", "Post not found");
105
+ }
106
+
107
+ return current.value;
108
+ })
109
+ .access({
110
+ action: "publish",
111
+ entityStep: "post",
112
+ })
113
+ .handle(({ input, state }): ServerResultAsync<PostPublishOutputSchema> => {
114
+ return this.repository.posts.update({
115
+ id: input.id,
116
+ status: "published",
117
+ publishedAt: state.post.publishedAt ?? new Date(),
118
+ }) as ServerResultAsync<PostPublishOutputSchema>;
119
+ });
120
+
121
+ readonly softDelete = this.procedure<PostSoftDeleteInputSchema>("softDelete")
122
+ .requireAuth()
123
+ .use("post", async ({ input }) => {
124
+ const current = await this.repository.posts.findById(input.id);
125
+ if (current.isErr()) {
126
+ return err(current.error);
127
+ }
128
+ if (!current.value || current.value.deletedAt) {
129
+ return this.error("NOT_FOUND", "Post not found");
130
+ }
131
+
132
+ return current.value;
133
+ })
134
+ .access({
135
+ action: "delete",
136
+ entityStep: "post",
137
+ })
138
+ .handle(async ({ input }): ServerResultAsync<PostSoftDeleteOutputSchema> => {
139
+ const updated = await this.repository.posts.update({
140
+ id: input.id,
141
+ deletedAt: new Date(),
142
+ });
143
+
144
+ if (updated.isErr()) {
145
+ return err(updated.error);
146
+ }
147
+
148
+ return ok({ id: updated.value.id });
149
+ });
150
+
151
+ private slugify(value: string): string {
152
+ const slug = value
133
153
  .trim()
134
154
  .toLowerCase()
135
155
  .replace(/[^a-z0-9]+/g, "-")
@@ -1,12 +1,13 @@
1
- import { AuthService } from "@m5kdev/backend/modules/auth/auth.service";
2
- import { LocalEmailService } from "./lib/localEmailService";
3
- import { PostsService } from "./modules/posts/posts.service";
4
- import { authRepository, postsRepository } from "./repository";
1
+ import { AuthService } from "@m5kdev/backend/modules/auth/auth.service";
2
+ import { LocalEmailService } from "./lib/localEmailService";
3
+ import { postsGrants } from "./modules/posts/posts.grants";
4
+ import { PostsService } from "./modules/posts/posts.service";
5
+ import { authRepository, postsRepository } from "./repository";
5
6
 
6
7
  export const emailService = new LocalEmailService({
7
8
  appName: "{{APP_NAME}}",
8
9
  appUrl: process.env.VITE_APP_URL ?? "http://localhost:5173",
9
10
  });
10
-
11
- export const authService = new AuthService({ auth: authRepository }, { email: emailService });
12
- export const postsService = new PostsService({ posts: postsRepository }, {});
11
+
12
+ export const authService = new AuthService({ auth: authRepository }, { email: emailService });
13
+ export const postsService = new PostsService({ posts: postsRepository }, {}, postsGrants);
@@ -1,7 +1,8 @@
1
- import {
2
- type createAuthContext,
3
- verifyProtectedProcedureContext,
4
- } from "@m5kdev/backend/utils/trpc";
1
+ import {
2
+ type createAuthContext,
3
+ verifyAdminProcedureContext,
4
+ verifyProtectedProcedureContext,
5
+ } from "@m5kdev/backend/utils/trpc";
5
6
  import { transformer } from "@m5kdev/commons/utils/trpc";
6
7
  import { initTRPC } from "@trpc/server";
7
8
 
@@ -9,19 +10,22 @@ type Context = Awaited<ReturnType<ReturnType<typeof createAuthContext>>>;
9
10
 
10
11
  const t = initTRPC.context<Context>().create({ transformer });
11
12
 
12
- export const publicProcedure = t.procedure;
13
-
14
- export const procedure = t.procedure.use(({ ctx, next }) => {
15
- verifyProtectedProcedureContext(ctx);
16
- return next({ ctx });
17
- });
18
-
19
- export const router = t.router;
20
- export const mergeRouters = t.mergeRouters;
21
-
22
- export const trpcObject = {
23
- router,
24
- privateProcedure: procedure,
25
- adminProcedure: procedure,
26
- publicProcedure,
27
- };
13
+ export const publicProcedure = t.procedure;
14
+
15
+ export const procedure = t.procedure.use(({ ctx, next }) => {
16
+ return next({ ctx: verifyProtectedProcedureContext(ctx) });
17
+ });
18
+
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
+ };