create-m5kdev 0.25.3 → 0.26.2

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 (143) hide show
  1. package/dist/src/create.cjs +18 -1
  2. package/dist/src/create.cjs.map +1 -1
  3. package/dist/src/fs.cjs +65 -5
  4. package/dist/src/fs.cjs.map +1 -1
  5. package/dist/src/fs.d.cts +13 -2
  6. package/dist/src/index.cjs +0 -0
  7. package/dist/src/paths.cjs +2 -2
  8. package/dist/src/prompts.cjs +22 -0
  9. package/dist/src/prompts.cjs.map +1 -1
  10. package/dist/src/runCli.cjs +13 -2
  11. package/dist/src/runCli.cjs.map +1 -1
  12. package/dist/src/types.d.cts +9 -1
  13. package/package.json +8 -6
  14. package/templates/minimal-app/.cursor/rules/frontend-data-guide.mdc +46 -3
  15. package/templates/minimal-app/.cursor/rules/module-db-guide.mdc +15 -1
  16. package/templates/minimal-app/.cursor/rules/module-dto-guide.mdc +60 -0
  17. package/templates/minimal-app/.cursor/rules/module-repository-guide.mdc +98 -193
  18. package/templates/minimal-app/.cursor/rules/module-schema-guide.mdc +53 -173
  19. package/templates/minimal-app/.cursor/rules/module-service-guide.mdc +133 -240
  20. package/templates/minimal-app/.cursor/rules/module-trpc-guide.mdc +71 -113
  21. package/templates/minimal-app/README.md.tpl +3 -3
  22. package/templates/minimal-app/apps/.gitignore.tpl +13 -0
  23. package/templates/minimal-app/apps/e2e/package.json.tpl +23 -0
  24. package/templates/minimal-app/apps/e2e/playwright.config.ts.tpl +39 -0
  25. package/templates/minimal-app/apps/e2e/playwright.expo.config.ts.tpl +38 -0
  26. package/templates/minimal-app/apps/e2e/servers.ts.tpl +100 -0
  27. package/templates/minimal-app/apps/e2e/tests/email.spec.ts.tpl +47 -0
  28. package/templates/minimal-app/apps/e2e/tests/expo-helpers.ts.tpl +73 -0
  29. package/templates/minimal-app/apps/e2e/tests/expo-standard.spec.ts.tpl +517 -0
  30. package/templates/minimal-app/apps/e2e/tests/expo-waitlist.spec.ts.tpl +94 -0
  31. package/templates/minimal-app/apps/e2e/tests/helpers.ts.tpl +289 -0
  32. package/templates/minimal-app/apps/e2e/tests/standard.spec.ts.tpl +516 -0
  33. package/templates/minimal-app/apps/e2e/tests/waitlist.spec.ts.tpl +107 -0
  34. package/templates/minimal-app/apps/e2e/tsconfig.json.tpl +10 -0
  35. package/templates/minimal-app/apps/email/package.json.tpl +34 -34
  36. package/templates/minimal-app/apps/email/src/emails/accountDeletionEmail.tsx.tpl +35 -34
  37. package/templates/minimal-app/apps/email/src/emails/organizationInviteEmail.tsx.tpl +34 -33
  38. package/templates/minimal-app/apps/email/src/emails/passwordResetEmail.tsx.tpl +35 -34
  39. package/templates/minimal-app/apps/email/src/emails/verificationEmail.tsx.tpl +37 -36
  40. package/templates/minimal-app/apps/email/src/emails/waitlistEmails.tsx.tpl +87 -0
  41. package/templates/minimal-app/apps/email/src/index.ts.tpl +72 -32
  42. package/templates/minimal-app/apps/email/src/resources.ts.tpl +65 -36
  43. package/templates/minimal-app/apps/email/tsconfig.json.tpl +16 -11
  44. package/templates/minimal-app/apps/expo/.gitignore.tpl +44 -0
  45. package/templates/minimal-app/apps/expo/.prettierignore +13 -0
  46. package/templates/minimal-app/apps/expo/.prettierrc.json.tpl +9 -0
  47. package/templates/minimal-app/apps/expo/CHANGELOG.md.tpl +156 -0
  48. package/templates/minimal-app/apps/expo/README.md.tpl +44 -0
  49. package/templates/minimal-app/apps/expo/app.json.tpl +36 -0
  50. package/templates/minimal-app/apps/expo/assets/images/adaptive-icon.png +0 -0
  51. package/templates/minimal-app/apps/expo/assets/images/favicon.png +0 -0
  52. package/templates/minimal-app/apps/expo/assets/images/icon.png +0 -0
  53. package/templates/minimal-app/apps/expo/assets/images/splash-icon.png +0 -0
  54. package/templates/minimal-app/apps/expo/eslint.config.js.tpl +8 -0
  55. package/templates/minimal-app/apps/expo/expo-env.d.ts.tpl +3 -0
  56. package/templates/minimal-app/apps/expo/metro.config.js.tpl +16 -0
  57. package/templates/minimal-app/apps/expo/package.json.tpl +59 -0
  58. package/templates/minimal-app/apps/expo/src/app/_layout.tsx.tpl +31 -0
  59. package/templates/minimal-app/apps/expo/src/app/claim-account.tsx.tpl +151 -0
  60. package/templates/minimal-app/apps/expo/src/app/forgot-password.tsx.tpl +53 -0
  61. package/templates/minimal-app/apps/expo/src/app/index.tsx.tpl +8 -0
  62. package/templates/minimal-app/apps/expo/src/app/login.tsx.tpl +71 -0
  63. package/templates/minimal-app/apps/expo/src/app/logout.tsx.tpl +19 -0
  64. package/templates/minimal-app/apps/expo/src/app/organization/accept-invitation.tsx.tpl +85 -0
  65. package/templates/minimal-app/apps/expo/src/app/posts.tsx.tpl +40 -0
  66. package/templates/minimal-app/apps/expo/src/app/reset-password.tsx.tpl +75 -0
  67. package/templates/minimal-app/apps/expo/src/app/signup.tsx.tpl +193 -0
  68. package/templates/minimal-app/apps/expo/src/components/AuthScaffold.tsx.tpl +34 -0
  69. package/templates/minimal-app/apps/expo/src/components/FormTextField.tsx.tpl +37 -0
  70. package/templates/minimal-app/apps/expo/src/components/LoadingScreen.tsx.tpl +10 -0
  71. package/templates/minimal-app/apps/expo/src/components/StatusText.tsx.tpl +22 -0
  72. package/templates/minimal-app/apps/expo/src/config.ts.tpl +9 -0
  73. package/templates/minimal-app/apps/expo/src/global.css.tpl +11 -0
  74. package/templates/minimal-app/apps/expo/src/lib/trpc.ts.tpl +20 -0
  75. package/templates/minimal-app/apps/expo/tsconfig.json.tpl +11 -0
  76. package/templates/minimal-app/apps/expo/uniwind-env.d.ts.tpl +1 -0
  77. package/templates/minimal-app/apps/expo/uniwind-types.d.ts.tpl +10 -0
  78. package/templates/minimal-app/apps/server/AGENTS.md.tpl +39 -39
  79. package/templates/minimal-app/apps/server/drizzle/0000_wealthy_absorbing_man.sql +268 -0
  80. package/templates/minimal-app/apps/server/drizzle/db.ts.tpl +26 -0
  81. package/templates/minimal-app/apps/server/drizzle/meta/0000_snapshot.json.tpl +1890 -0
  82. package/templates/minimal-app/apps/server/drizzle/meta/_journal.json.tpl +13 -0
  83. package/templates/minimal-app/apps/server/drizzle/reset.ts.tpl +33 -0
  84. package/templates/minimal-app/apps/server/drizzle/seed.e2e.ts.tpl +244 -0
  85. package/templates/minimal-app/apps/server/drizzle/seed.ts.tpl +181 -180
  86. package/templates/minimal-app/apps/server/drizzle/sync.ts.tpl +14 -21
  87. package/templates/minimal-app/apps/server/drizzle.config.ts.tpl +33 -35
  88. package/templates/minimal-app/apps/server/package.json.tpl +56 -52
  89. package/templates/minimal-app/apps/server/src/app.ts.tpl +142 -126
  90. package/templates/minimal-app/apps/server/src/index.ts.tpl +58 -58
  91. package/templates/minimal-app/apps/server/src/lib/auth.ts.tpl +12 -12
  92. package/templates/minimal-app/apps/server/src/modules/posts/posts.db.ts.tpl +25 -25
  93. package/templates/minimal-app/apps/server/src/modules/posts/posts.dto.ts.tpl +23 -0
  94. package/templates/minimal-app/apps/server/src/modules/posts/posts.grants.ts.tpl +30 -40
  95. package/templates/minimal-app/apps/server/src/modules/posts/posts.module.ts.tpl +54 -54
  96. package/templates/minimal-app/apps/server/src/modules/posts/posts.repository.ts.tpl +67 -112
  97. package/templates/minimal-app/apps/server/src/modules/posts/posts.service.ts.tpl +104 -170
  98. package/templates/minimal-app/apps/server/src/modules/posts/posts.trpc.ts.tpl +37 -48
  99. package/templates/minimal-app/apps/server/src/modules/posts/posts.utils.ts.tpl +19 -0
  100. package/templates/minimal-app/apps/server/src/modules/test-harness/test-harness.module.ts.tpl +279 -0
  101. package/templates/minimal-app/apps/server/src/schema.ts.tpl +20 -0
  102. package/templates/minimal-app/apps/server/src/types.ts.tpl +1 -1
  103. package/templates/minimal-app/apps/server/tsconfig.json.tpl +20 -14
  104. package/templates/minimal-app/apps/server/tsup.config.ts.tpl +9 -9
  105. package/templates/minimal-app/apps/shared/package.json.tpl +24 -24
  106. package/templates/minimal-app/apps/shared/src/modules/app/app.constants.ts.tpl +28 -2
  107. package/templates/minimal-app/apps/shared/src/modules/notification/notification.schema.ts.tpl +3 -3
  108. package/templates/minimal-app/apps/shared/src/modules/posts/posts.constants.ts.tpl +5 -5
  109. package/templates/minimal-app/apps/shared/src/modules/posts/posts.schema.ts.tpl +25 -76
  110. package/templates/minimal-app/apps/shared/tsconfig.json.tpl +16 -12
  111. package/templates/minimal-app/apps/webapp/index.html.tpl +22 -22
  112. package/templates/minimal-app/apps/webapp/package.json.tpl +53 -52
  113. package/templates/minimal-app/apps/webapp/public/{push-sw.js → push-sw.js.tpl} +34 -34
  114. package/templates/minimal-app/apps/webapp/src/App.tsx.tpl +44 -13
  115. package/templates/minimal-app/apps/webapp/src/Layout.tsx.tpl +18 -12
  116. package/templates/minimal-app/apps/webapp/src/Router.tsx.tpl +33 -40
  117. package/templates/minimal-app/apps/webapp/src/index.css.tpl +15 -75
  118. package/templates/minimal-app/apps/webapp/src/main.tsx.tpl +26 -26
  119. package/templates/minimal-app/apps/webapp/src/modules/posts/PostsRoute.tsx.tpl +278 -307
  120. package/templates/minimal-app/apps/webapp/src/modules/posts/components/PostCard.tsx.tpl +96 -138
  121. package/templates/minimal-app/apps/webapp/src/modules/posts/components/PostEditorModal.tsx.tpl +118 -124
  122. package/templates/minimal-app/apps/webapp/src/modules/posts/hooks/usePostActions.ts.tpl +108 -116
  123. package/templates/minimal-app/apps/webapp/src/modules/posts/hooks/usePostsList.ts.tpl +15 -9
  124. package/templates/minimal-app/apps/webapp/src/modules/posts/hooks/usePostsRoute.ts.tpl +13 -13
  125. package/templates/minimal-app/apps/webapp/src/modules/posts/posts.utils.ts.tpl +16 -16
  126. package/templates/minimal-app/apps/webapp/src/utils/i18n.ts.tpl +13 -13
  127. package/templates/minimal-app/apps/webapp/src/utils/trpc.ts.tpl +10 -6
  128. package/templates/minimal-app/apps/webapp/src/vite-env.d.ts.tpl +1 -1
  129. package/templates/minimal-app/apps/webapp/translations/{app.json.tpl → en/app.json.tpl} +7 -7
  130. package/templates/minimal-app/apps/webapp/translations/en/{blog-app.json.tpl → {{APP_SLUG}}.json.tpl} +135 -135
  131. package/templates/minimal-app/apps/webapp/tsconfig.json.tpl +17 -17
  132. package/templates/minimal-app/apps/webapp/vite.config.ts.tpl +31 -31
  133. package/templates/minimal-app/template.manifest.json +21 -0
  134. package/templates/minimal-app/apps/server/drizzle/generate-schema.ts.tpl +0 -31
  135. package/templates/minimal-app/apps/server/src/db.ts.tpl +0 -7
  136. package/templates/minimal-app/apps/server/src/generated/schema.ts.tpl +0 -13
  137. package/templates/minimal-app/apps/server/src/utils/trpc.ts.tpl +0 -8
  138. package/templates/minimal-app/apps/shared/src/modules/app/locale.constants.ts.tpl +0 -7
  139. package/templates/minimal-app/apps/shared/src/modules/app/roles.constants.ts.tpl +0 -17
  140. package/templates/minimal-app/apps/webapp/src/Providers.tsx.tpl +0 -36
  141. package/templates/minimal-app/apps/webapp/src/modules/notification/PushNotificationsPanel.tsx.tpl +0 -89
  142. package/templates/minimal-app/apps/webapp/src/navigation/navigation.config.tsx.tpl +0 -85
  143. /package/templates/minimal-app/apps/webapp/public/{logo.svg → logo.svg.tpl} +0 -0
@@ -1,25 +1,25 @@
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, {
8
- onDelete: "set null",
9
- }),
10
- organizationId: text("organization_id").references(() => organizations.id, {
11
- onDelete: "set null",
12
- }),
13
- teamId: text("team_id").references(() => teams.id, { onDelete: "set null" }),
14
- title: text("title").notNull(),
15
- slug: text("slug").notNull().unique(),
16
- excerpt: text("excerpt"),
17
- content: text("content").notNull(),
18
- status: text("status").$type<"draft" | "published">().notNull().default("draft"),
19
- publishedAt: integer("published_at", { mode: "timestamp" }),
20
- createdAt: integer("created_at", { mode: "timestamp" })
21
- .notNull()
22
- .$default(() => new Date()),
23
- updatedAt: integer("updated_at", { mode: "timestamp" }),
24
- deletedAt: integer("deleted_at", { mode: "timestamp" }),
25
- });
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, {
8
+ onDelete: "set null",
9
+ }),
10
+ organizationId: text("organization_id").references(() => organizations.id, {
11
+ onDelete: "set null",
12
+ }),
13
+ teamId: text("team_id").references(() => teams.id, { onDelete: "set null" }),
14
+ title: text("title").notNull(),
15
+ slug: text("slug").notNull().unique(),
16
+ excerpt: text("excerpt"),
17
+ content: text("content").notNull(),
18
+ status: text("status").$type<"draft" | "published">().notNull().default("draft"),
19
+ publishedAt: integer("published_at", { mode: "timestamp" }),
20
+ createdAt: integer("created_at", { mode: "timestamp" })
21
+ .notNull()
22
+ .$default(() => new Date()),
23
+ updatedAt: integer("updated_at", { mode: "timestamp" }),
24
+ deletedAt: integer("deleted_at", { mode: "timestamp" }),
25
+ });
@@ -0,0 +1,23 @@
1
+ import { createZodSchemas } from "@m5kdev/backend/modules/base/base.dto";
2
+ import {
3
+ postCreateInputSchema,
4
+ postUpdateInputSchema,
5
+ } from "{{PACKAGE_SCOPE}}/shared/modules/posts/posts.schema";
6
+ import { z } from "zod";
7
+ import { posts } from "./posts.db";
8
+
9
+ const { output, input } = createZodSchemas(posts);
10
+
11
+ export const postSchemas = {
12
+ output,
13
+ input: {
14
+ ...input,
15
+ // create/update come from shared: the same schemas validate the webapp form,
16
+ // so the API contract and the frontend validation cannot drift apart
17
+ create: postCreateInputSchema,
18
+ update: postUpdateInputSchema,
19
+ publish: z.object({
20
+ id: z.string(),
21
+ }),
22
+ },
23
+ };
@@ -1,40 +1,30 @@
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
+ import { flattenNestedGrants } from "@m5kdev/backend/modules/base/base.grants";
2
+
3
+ export const postsGrants = flattenNestedGrants({
4
+ cash: {
5
+ user: {
6
+ admin: {
7
+ read: "all",
8
+ write: "all",
9
+ delete: "all",
10
+ publish: "all",
11
+ },
12
+ },
13
+ organization: {
14
+ owner: {
15
+ read: "own",
16
+ write: "own",
17
+ delete: "own",
18
+ publish: "own",
19
+ },
20
+ },
21
+ team: {
22
+ owner: {
23
+ read: "own",
24
+ write: "own",
25
+ delete: "own",
26
+ publish: "own",
27
+ },
28
+ },
29
+ },
30
+ });
@@ -1,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,112 +1,67 @@
1
- import { BaseTableRepository } from "@m5kdev/backend/modules/base/base.repository";
2
- import type { ServerResultAsync } from "@m5kdev/backend/utils/types";
3
- import type {
4
- PostSchema,
5
- PostsListInputSchema,
6
- PostsListOutputSchema,
7
- } from "{{PACKAGE_SCOPE}}/shared/modules/posts/posts.schema";
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>;
16
-
17
- export class PostsRepository extends BaseTableRepository<
18
- Orm,
19
- Schema,
20
- Record<string, never>,
21
- Schema["posts"]
22
- > {
23
- async list(input: PostsListInputSchema = {}): ServerResultAsync<PostsListOutputSchema> {
24
- const page = input.page ?? 1;
25
- const limit = input.limit ?? 6;
26
- const search = input.search?.trim();
27
- const conditions = [isNull(this.table.deletedAt)];
28
-
29
- if (input.status) {
30
- conditions.push(eq(this.table.status, input.status));
31
- }
32
-
33
- if (search) {
34
- const escapedSearch = search.replace(/[%_]/g, "\\$&");
35
- const pattern = `%${escapedSearch}%`;
36
- const searchCondition = or(
37
- like(this.table.title, pattern),
38
- like(this.table.slug, pattern),
39
- like(this.table.excerpt, pattern),
40
- like(this.table.content, pattern)
41
- );
42
- if (searchCondition) {
43
- conditions.push(searchCondition);
44
- }
45
- }
46
-
47
- const whereClause = and(...conditions);
48
- const ordering =
49
- input.sort === "title"
50
- ? input.order === "asc"
51
- ? asc(this.table.title)
52
- : desc(this.table.title)
53
- : input.sort === "publishedAt"
54
- ? input.order === "asc"
55
- ? asc(this.table.publishedAt)
56
- : desc(this.table.publishedAt)
57
- : input.order === "asc"
58
- ? asc(this.table.updatedAt)
59
- : desc(this.table.updatedAt);
60
-
61
- const rowsQuery = this.orm
62
- .select()
63
- .from(this.table)
64
- .where(whereClause)
65
- .orderBy(ordering, desc(this.table.createdAt))
66
- .limit(limit)
67
- .offset((page - 1) * limit);
68
-
69
- const countQuery = this.orm.select({ count: count() }).from(this.table).where(whereClause);
70
-
71
- const result = await this.throwableQuery(() => Promise.all([rowsQuery, countQuery]));
72
- if (result.isErr()) return err(result.error);
73
- const [rows, [totalRow]] = result.value;
74
-
75
- return ok({
76
- rows: rows as PostSchema[],
77
- total: totalRow?.count ?? 0,
78
- });
79
- }
80
-
81
- async findBySlug(slug: string, excludeId?: string) {
82
- const whereClause = excludeId
83
- ? and(eq(this.table.slug, slug), ne(this.table.id, excludeId), isNull(this.table.deletedAt))
84
- : and(eq(this.table.slug, slug), isNull(this.table.deletedAt));
85
-
86
- const result = await this.throwableQuery(() =>
87
- this.orm.select().from(this.table).where(whereClause).limit(1)
88
- );
89
- if (result.isErr()) return err(result.error);
90
- const [row] = result.value;
91
- return ok(row);
92
- }
93
-
94
- async resolveUniqueSlug(candidate: string, excludeId?: string) {
95
- let slug = candidate;
96
- let suffix = 2;
97
-
98
- while (true) {
99
- const existing = await this.findBySlug(slug, excludeId);
100
- if (existing.isErr()) {
101
- return existing;
102
- }
103
-
104
- if (!existing.value) {
105
- return ok(slug);
106
- }
107
-
108
- slug = `${candidate}-${suffix}`;
109
- suffix += 1;
110
- }
111
- }
112
- }
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
+ }