create-m5kdev 0.34.0 → 0.36.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/.cursor/rules/frontend-data-guide.mdc +1 -0
- package/templates/minimal-app/.cursor/rules/server-event-emit.mdc +41 -0
- package/templates/minimal-app/AGENTS.md.tpl +1 -0
- package/templates/minimal-app/CLAUDE.md.tpl +1 -0
- package/templates/minimal-app/README.md.tpl +2 -1
- package/templates/minimal-app/apps/e2e/servers.ts.tpl +3 -0
- package/templates/minimal-app/apps/e2e/tests/expo-helpers.ts.tpl +2 -34
- package/templates/minimal-app/apps/e2e/tests/expo-standard.spec.ts.tpl +225 -5
- package/templates/minimal-app/apps/e2e/tests/helpers.ts.tpl +47 -1
- package/templates/minimal-app/apps/e2e/tests/standard.spec.ts.tpl +290 -8
- package/templates/minimal-app/apps/expo/app.json.tpl +1 -0
- package/templates/minimal-app/apps/expo/package.json.tpl +4 -0
- package/templates/minimal-app/apps/expo/src/app/_layout.tsx.tpl +4 -1
- package/templates/minimal-app/apps/expo/src/app/notifications.tsx.tpl +70 -0
- package/templates/minimal-app/apps/expo/src/app/organization/accept-invitation.tsx.tpl +8 -16
- package/templates/minimal-app/apps/expo/src/app/posts.tsx.tpl +4 -1
- package/templates/minimal-app/apps/expo/src/lib/trpc.ts.tpl +11 -2
- package/templates/minimal-app/apps/landing/vite.config.ts.tpl +2 -3
- package/templates/minimal-app/apps/server/AGENTS.md.tpl +1 -0
- package/templates/minimal-app/apps/server/db.e2e.ts.tpl +5 -40
- package/templates/minimal-app/apps/server/db.ts.tpl +5 -42
- package/templates/minimal-app/apps/server/drizzle/{0000_wealthy_absorbing_man.sql → 0000_whole_echo.sql} +102 -3
- package/templates/minimal-app/apps/server/drizzle/0001_yummy_spectrum.sql +26 -0
- package/templates/minimal-app/apps/server/drizzle/0002_crazy_lilith.sql +47 -0
- package/templates/minimal-app/apps/server/drizzle/0003_freezing_fantastic_four.sql +4 -0
- package/templates/minimal-app/apps/server/drizzle/0004_slow_ezekiel_stane.sql +99 -0
- package/templates/minimal-app/apps/server/drizzle/0005_wonderful_the_professor.sql +2 -0
- package/templates/minimal-app/apps/server/drizzle/meta/0000_snapshot.json.tpl +2625 -1780
- package/templates/minimal-app/apps/server/drizzle/meta/0001_snapshot.json.tpl +2671 -1796
- package/templates/minimal-app/apps/server/drizzle/meta/0002_snapshot.json.tpl +2716 -1818
- package/templates/minimal-app/apps/server/drizzle/meta/0003_snapshot.json.tpl +715 -164
- package/templates/minimal-app/apps/server/drizzle/meta/0004_snapshot.json.tpl +2498 -0
- package/templates/minimal-app/apps/server/drizzle/meta/0005_snapshot.json.tpl +2484 -0
- package/templates/minimal-app/apps/server/drizzle/meta/_journal.json.tpl +23 -9
- package/templates/minimal-app/apps/server/src/app.ts.tpl +4 -1
- package/templates/minimal-app/apps/server/src/modules/demo-workflow/demo-workflow.module.ts.tpl +7 -3
- package/templates/minimal-app/apps/server/src/modules/demo-workflow/demo-workflow.service.ts.tpl +12 -4
- package/templates/minimal-app/apps/server/src/modules/posts/posts.db.ts.tpl +2 -2
- package/templates/minimal-app/apps/server/src/modules/posts/posts.module.ts.tpl +7 -4
- package/templates/minimal-app/apps/server/src/modules/posts/posts.service.ts.tpl +2 -2
- package/templates/minimal-app/apps/server/src/modules/test-harness/test-harness.module.ts.tpl +13 -36
- package/templates/minimal-app/apps/server/src/schema.ts.tpl +3 -2
- package/templates/minimal-app/apps/shared/package.json.tpl +2 -0
- package/templates/minimal-app/apps/shared/src/modules/app/app.constants.ts.tpl +1 -1
- package/templates/minimal-app/apps/shared/src/modules/demo-workflow/demo-workflow.constants.ts.tpl +1 -0
- package/templates/minimal-app/apps/shared/src/modules/notification/notification.constants.ts.tpl +5 -0
- package/templates/minimal-app/apps/shared/src/modules/notification/notification.schema.ts.tpl +1 -1
- package/templates/minimal-app/apps/webapp/src/App.tsx.tpl +8 -5
- package/templates/minimal-app/apps/webapp/src/Layout.tsx.tpl +17 -1
- package/templates/minimal-app/apps/webapp/src/Router.tsx.tpl +10 -8
- package/templates/minimal-app/apps/webapp/src/StarterServerEventProvider.tsx.tpl +38 -0
- package/templates/minimal-app/apps/webapp/src/modules/notification/NotificationPreferencesRoute.tsx.tpl +29 -0
- package/templates/minimal-app/apps/webapp/src/modules/notification/StarterWebPush.tsx.tpl +44 -0
- package/templates/minimal-app/apps/webapp/translations/en/{{APP_SLUG}}.json.tpl +8 -0
- package/templates/minimal-app/package.json.tpl +5 -4
- package/templates/minimal-app/pnpm-workspace.yaml.tpl +12 -11
- package/templates/minimal-app/template.manifest.json +1 -0
- package/templates/minimal-app/apps/landing/scripts/fly-deploy.mjs.tpl +0 -75
- package/templates/minimal-app/apps/landing/scripts/fly-secrets.mjs.tpl +0 -31
- package/templates/minimal-app/apps/server/drizzle/0001_third_ink.sql +0 -1
- package/templates/minimal-app/apps/server/drizzle/0002_concerned_proudstar.sql +0 -3
- package/templates/minimal-app/apps/server/drizzle/0003_silly_the_liberteens.sql +0 -27
- package/templates/minimal-app/apps/shared/scripts/fly-deploy.mjs.tpl +0 -75
- package/templates/minimal-app/apps/shared/scripts/fly-secrets.mjs.tpl +0 -31
|
@@ -5,30 +5,44 @@
|
|
|
5
5
|
{
|
|
6
6
|
"idx": 0,
|
|
7
7
|
"version": "6",
|
|
8
|
-
"when":
|
|
9
|
-
"tag": "
|
|
8
|
+
"when": 1787759631723,
|
|
9
|
+
"tag": "0000_whole_echo",
|
|
10
10
|
"breakpoints": true
|
|
11
11
|
},
|
|
12
12
|
{
|
|
13
13
|
"idx": 1,
|
|
14
14
|
"version": "6",
|
|
15
|
-
"when":
|
|
16
|
-
"tag": "
|
|
15
|
+
"when": 1788197370969,
|
|
16
|
+
"tag": "0001_yummy_spectrum",
|
|
17
17
|
"breakpoints": true
|
|
18
18
|
},
|
|
19
19
|
{
|
|
20
20
|
"idx": 2,
|
|
21
21
|
"version": "6",
|
|
22
|
-
"when":
|
|
23
|
-
"tag": "
|
|
22
|
+
"when": 1788197859737,
|
|
23
|
+
"tag": "0002_crazy_lilith",
|
|
24
24
|
"breakpoints": true
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
"idx": 3,
|
|
28
28
|
"version": "6",
|
|
29
|
-
"when":
|
|
30
|
-
"tag": "
|
|
29
|
+
"when": 1788198203749,
|
|
30
|
+
"tag": "0003_freezing_fantastic_four",
|
|
31
|
+
"breakpoints": true
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"idx": 4,
|
|
35
|
+
"version": "6",
|
|
36
|
+
"when": 1788198833389,
|
|
37
|
+
"tag": "0004_slow_ezekiel_stane",
|
|
38
|
+
"breakpoints": true
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"idx": 5,
|
|
42
|
+
"version": "6",
|
|
43
|
+
"when": 1788253467922,
|
|
44
|
+
"tag": "0005_wonderful_the_professor",
|
|
31
45
|
"breakpoints": true
|
|
32
46
|
}
|
|
33
47
|
]
|
|
34
|
-
}
|
|
48
|
+
}
|
|
@@ -23,6 +23,9 @@ import {
|
|
|
23
23
|
APP_NAME,
|
|
24
24
|
APP_ROLES_CONFIG,
|
|
25
25
|
} from "{{PACKAGE_SCOPE}}/shared/modules/app/app.constants";
|
|
26
|
+
// m5k:notifications:start
|
|
27
|
+
import { NOTIFICATION_KINDS } from "{{PACKAGE_SCOPE}}/shared/modules/notification/notification.constants";
|
|
28
|
+
// m5k:notifications:end
|
|
26
29
|
// m5k:ai:start
|
|
27
30
|
import { mastra, openrouter } from "./modules/conversation/conversation.mastra";
|
|
28
31
|
// m5k:ai:end
|
|
@@ -138,7 +141,7 @@ export const builtBackendApp = createBackendApp(
|
|
|
138
141
|
new DemoWorkflowModule(),
|
|
139
142
|
// m5k:workflows:end
|
|
140
143
|
// m5k:notifications:start
|
|
141
|
-
new NotificationModule(),
|
|
144
|
+
new NotificationModule({ kinds: NOTIFICATION_KINDS }),
|
|
142
145
|
// m5k:notifications:end
|
|
143
146
|
new PostsModule(),
|
|
144
147
|
new EmailPreviewModule({ allowDelete: process.env.NODE_ENV !== "production" }),
|
package/templates/minimal-app/apps/server/src/modules/demo-workflow/demo-workflow.module.ts.tpl
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createBackendRouterMap } from "@m5kdev/backend/app";
|
|
2
|
+
import type { AuthModule } from "@m5kdev/backend/modules/auth/auth.module";
|
|
2
3
|
import {
|
|
3
4
|
BaseModule,
|
|
4
5
|
type ModuleServicesContext,
|
|
@@ -9,7 +10,7 @@ import type { WorkflowModule } from "@m5kdev/backend/modules/workflow/workflow.m
|
|
|
9
10
|
import { DemoWorkflowService } from "./demo-workflow.service";
|
|
10
11
|
import { createDemoWorkflowTRPC } from "./demo-workflow.trpc";
|
|
11
12
|
|
|
12
|
-
type DemoWorkflowModuleDeps = { workflow: WorkflowModule };
|
|
13
|
+
type DemoWorkflowModuleDeps = { workflow: WorkflowModule; auth: AuthModule };
|
|
13
14
|
type DemoWorkflowModuleServices = {
|
|
14
15
|
demoWorkflow: DemoWorkflowService;
|
|
15
16
|
};
|
|
@@ -25,13 +26,16 @@ export class DemoWorkflowModule extends BaseModule<
|
|
|
25
26
|
DemoWorkflowModuleRouters
|
|
26
27
|
> {
|
|
27
28
|
readonly id = "demo-workflow";
|
|
28
|
-
override readonly dependsOn = ["workflow"] as const;
|
|
29
|
+
override readonly dependsOn = ["workflow", "auth"] as const;
|
|
29
30
|
|
|
30
31
|
override services({
|
|
31
32
|
deps,
|
|
32
33
|
}: ModuleServicesContext<DemoWorkflowModuleDeps, Record<string, never>>) {
|
|
33
34
|
return {
|
|
34
|
-
demoWorkflow: new DemoWorkflowService({
|
|
35
|
+
demoWorkflow: new DemoWorkflowService({
|
|
36
|
+
workflow: deps.workflow.services.workflow,
|
|
37
|
+
auth: deps.auth.services.auth,
|
|
38
|
+
}),
|
|
35
39
|
};
|
|
36
40
|
}
|
|
37
41
|
|
package/templates/minimal-app/apps/server/src/modules/demo-workflow/demo-workflow.service.ts.tpl
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import type { AuthService } from "@m5kdev/backend/modules/auth/auth.service";
|
|
1
2
|
import type { ServerResultAsync } from "@m5kdev/backend/modules/base/base.dto";
|
|
2
3
|
import { BaseService } from "@m5kdev/backend/modules/base/base.service";
|
|
3
4
|
import type { WorkflowService } from "@m5kdev/backend/modules/workflow/workflow.service";
|
|
4
5
|
import type { FireAndForgetJobDefinition } from "@m5kdev/backend/modules/workflow/workflow.types";
|
|
5
6
|
import type { Context } from "@m5kdev/backend/utils/trpc";
|
|
6
7
|
import { workflowTriggerOutputSchema } from "@m5kdev/commons/modules/workflow/workflow.schema";
|
|
8
|
+
import { DEMO_WORKFLOW_SERVER_EVENT_RESOURCE } from "{{PACKAGE_SCOPE}}/shared/modules/demo-workflow/demo-workflow.constants";
|
|
7
9
|
import { ok } from "neverthrow";
|
|
8
10
|
|
|
9
11
|
const DEMO_PING_JOB_NAME = "demo.ping";
|
|
@@ -14,12 +16,12 @@ interface DemoPingJobPayload {
|
|
|
14
16
|
|
|
15
17
|
export class DemoWorkflowService extends BaseService<
|
|
16
18
|
Record<string, never>,
|
|
17
|
-
{ workflow: WorkflowService },
|
|
19
|
+
{ workflow: WorkflowService; auth: AuthService },
|
|
18
20
|
Context
|
|
19
21
|
> {
|
|
20
22
|
readonly demoPingJob: FireAndForgetJobDefinition<DemoPingJobPayload>;
|
|
21
23
|
|
|
22
|
-
constructor(services: { workflow: WorkflowService }) {
|
|
24
|
+
constructor(services: { workflow: WorkflowService; auth: AuthService }) {
|
|
23
25
|
super({}, services);
|
|
24
26
|
|
|
25
27
|
this.demoPingJob = this.service.workflow
|
|
@@ -28,8 +30,14 @@ export class DemoWorkflowService extends BaseService<
|
|
|
28
30
|
queue: "fast",
|
|
29
31
|
meta: (payload) => ({ userId: payload.userId }),
|
|
30
32
|
})
|
|
31
|
-
.handle(async () => {
|
|
32
|
-
|
|
33
|
+
.handle(async (payload) => {
|
|
34
|
+
this.service.auth.userEmit({
|
|
35
|
+
userId: payload.userId,
|
|
36
|
+
resource: DEMO_WORKFLOW_SERVER_EVENT_RESOURCE,
|
|
37
|
+
id: DEMO_PING_JOB_NAME,
|
|
38
|
+
change: "updated",
|
|
39
|
+
organizationId: null,
|
|
40
|
+
});
|
|
33
41
|
});
|
|
34
42
|
}
|
|
35
43
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { members, organizations,
|
|
1
|
+
import { members, organizations, users } from "@m5kdev/backend/modules/auth/auth.db";
|
|
2
2
|
import { integer, sqliteTable as table, text } from "drizzle-orm/sqlite-core";
|
|
3
3
|
import { v4 as uuidv4 } from "uuid";
|
|
4
4
|
|
|
@@ -13,7 +13,7 @@ export const posts = table("posts", {
|
|
|
13
13
|
organizationId: text("organization_id").references(() => organizations.id, {
|
|
14
14
|
onDelete: "set null",
|
|
15
15
|
}),
|
|
16
|
-
teamId: text("team_id")
|
|
16
|
+
teamId: text("team_id"),
|
|
17
17
|
title: text("title").notNull(),
|
|
18
18
|
slug: text("slug").notNull().unique(),
|
|
19
19
|
excerpt: text("excerpt"),
|
|
@@ -11,6 +11,7 @@ import { PostsRepository } from "./posts.repository";
|
|
|
11
11
|
import { PostsService } from "./posts.service";
|
|
12
12
|
import { createPostsTRPC } from "./posts.trpc";
|
|
13
13
|
|
|
14
|
+
type PostsModuleDeps = Record<string, never>;
|
|
14
15
|
type PostsModuleTables = typeof postsTables;
|
|
15
16
|
type PostsModuleRepositories = {
|
|
16
17
|
posts: PostsRepository;
|
|
@@ -23,7 +24,7 @@ type PostsModuleRouters = {
|
|
|
23
24
|
};
|
|
24
25
|
|
|
25
26
|
export class PostsModule extends BaseModule<
|
|
26
|
-
|
|
27
|
+
PostsModuleDeps,
|
|
27
28
|
PostsModuleTables,
|
|
28
29
|
PostsModuleRepositories,
|
|
29
30
|
PostsModuleServices,
|
|
@@ -32,7 +33,7 @@ export class PostsModule extends BaseModule<
|
|
|
32
33
|
readonly id = "posts";
|
|
33
34
|
override readonly dbDependsOn = ["auth"] as const;
|
|
34
35
|
|
|
35
|
-
override repositories({ db }: ModuleRepositoriesContext<
|
|
36
|
+
override repositories({ db }: ModuleRepositoriesContext<PostsModuleDeps, PostsModuleTables>) {
|
|
36
37
|
return {
|
|
37
38
|
posts: new PostsRepository({
|
|
38
39
|
orm: db.orm,
|
|
@@ -42,13 +43,15 @@ export class PostsModule extends BaseModule<
|
|
|
42
43
|
};
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
override services({
|
|
46
|
+
override services({
|
|
47
|
+
repositories,
|
|
48
|
+
}: ModuleServicesContext<PostsModuleDeps, PostsModuleRepositories>) {
|
|
46
49
|
return {
|
|
47
50
|
posts: new PostsService({ posts: repositories.posts }, {}, postsGrants),
|
|
48
51
|
};
|
|
49
52
|
}
|
|
50
53
|
|
|
51
|
-
override trpc({ trpc, services }: ModuleTRPCContext<
|
|
54
|
+
override trpc({ trpc, services }: ModuleTRPCContext<PostsModuleDeps, PostsModuleServices>) {
|
|
52
55
|
return createBackendRouterMap("posts", createPostsTRPC(trpc, services.posts));
|
|
53
56
|
}
|
|
54
57
|
}
|
|
@@ -50,7 +50,7 @@ export class PostsService extends BasePermissionService<
|
|
|
50
50
|
authorUserId: ctx.user.id,
|
|
51
51
|
memberId: ctx.actor.memberId ?? null,
|
|
52
52
|
organizationId: ctx.session.activeOrganizationId ?? null,
|
|
53
|
-
teamId:
|
|
53
|
+
teamId: null,
|
|
54
54
|
title: input.title.trim(),
|
|
55
55
|
slug: uniqueSlug.value,
|
|
56
56
|
excerpt: createExcerpt(input.excerpt, input.content),
|
|
@@ -88,7 +88,7 @@ export class PostsService extends BasePermissionService<
|
|
|
88
88
|
action: "publish",
|
|
89
89
|
entityStep: "post",
|
|
90
90
|
})
|
|
91
|
-
.handle(({ input, state }) => {
|
|
91
|
+
.handle(async ({ input, state }) => {
|
|
92
92
|
return this.repository.posts.update({
|
|
93
93
|
id: input.id,
|
|
94
94
|
status: "published",
|
package/templates/minimal-app/apps/server/src/modules/test-harness/test-harness.module.ts.tpl
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
type ModuleExpressContext,
|
|
4
4
|
type TableMap,
|
|
5
5
|
} from "@m5kdev/backend/modules/base/base.module";
|
|
6
|
-
import { and, desc, eq, isNotNull } from "drizzle-orm";
|
|
6
|
+
import { and, desc, eq, isNotNull, isNull } from "drizzle-orm";
|
|
7
7
|
import type { Request, Response } from "express";
|
|
8
8
|
import { v4 as uuidv4 } from "uuid";
|
|
9
9
|
|
|
@@ -77,13 +77,14 @@ export class TestHarnessModule extends BaseModule<
|
|
|
77
77
|
type: db.schema.organizations.type,
|
|
78
78
|
parentId: db.schema.organizations.parentId,
|
|
79
79
|
role: db.schema.members.role,
|
|
80
|
+
memberId: db.schema.members.id,
|
|
80
81
|
})
|
|
81
82
|
.from(db.schema.members)
|
|
82
83
|
.innerJoin(
|
|
83
84
|
db.schema.organizations,
|
|
84
85
|
eq(db.schema.members.organizationId, db.schema.organizations.id)
|
|
85
86
|
)
|
|
86
|
-
.where(eq(db.schema.members.userId, user.id));
|
|
87
|
+
.where(and(eq(db.schema.members.userId, user.id), isNull(db.schema.members.deletedAt)));
|
|
87
88
|
|
|
88
89
|
const [credentialAccount] = await db.orm
|
|
89
90
|
.select({ id: db.schema.accounts.id })
|
|
@@ -99,21 +100,16 @@ export class TestHarnessModule extends BaseModule<
|
|
|
99
100
|
|
|
100
101
|
const accountClaims = await db.orm
|
|
101
102
|
.select({
|
|
102
|
-
id: db.schema.
|
|
103
|
-
status: db.schema.
|
|
104
|
-
claimUserId: db.schema.
|
|
105
|
-
claimedEmail: db.schema.
|
|
106
|
-
claimedAt: db.schema.
|
|
107
|
-
expiresAt: db.schema.
|
|
103
|
+
id: db.schema.accountClaims.id,
|
|
104
|
+
status: db.schema.accountClaims.status,
|
|
105
|
+
claimUserId: db.schema.accountClaims.claimUserId,
|
|
106
|
+
claimedEmail: db.schema.accountClaims.claimedEmail,
|
|
107
|
+
claimedAt: db.schema.accountClaims.claimedAt,
|
|
108
|
+
expiresAt: db.schema.accountClaims.expiresAt,
|
|
108
109
|
})
|
|
109
|
-
.from(db.schema.
|
|
110
|
-
.where(
|
|
111
|
-
|
|
112
|
-
eq(db.schema.waitlist.type, "ACCOUNT_CLAIM"),
|
|
113
|
-
eq(db.schema.waitlist.claimUserId, user.id)
|
|
114
|
-
)
|
|
115
|
-
)
|
|
116
|
-
.orderBy(desc(db.schema.waitlist.createdAt));
|
|
110
|
+
.from(db.schema.accountClaims)
|
|
111
|
+
.where(eq(db.schema.accountClaims.claimUserId, user.id))
|
|
112
|
+
.orderBy(desc(db.schema.accountClaims.createdAt));
|
|
117
113
|
|
|
118
114
|
const [latestSession] = await db.orm
|
|
119
115
|
.select({
|
|
@@ -123,7 +119,6 @@ export class TestHarnessModule extends BaseModule<
|
|
|
123
119
|
activeOrganizationRole: db.schema.sessions.activeOrganizationRole,
|
|
124
120
|
activeOrganizationMemberId: db.schema.sessions.activeOrganizationMemberId,
|
|
125
121
|
activeOrganizationType: db.schema.sessions.activeOrganizationType,
|
|
126
|
-
activeTeamId: db.schema.sessions.activeTeamId,
|
|
127
122
|
impersonatedBy: db.schema.sessions.impersonatedBy,
|
|
128
123
|
createdAt: db.schema.sessions.createdAt,
|
|
129
124
|
})
|
|
@@ -172,7 +167,6 @@ export class TestHarnessModule extends BaseModule<
|
|
|
172
167
|
const now = new Date();
|
|
173
168
|
const userId = uuidv4();
|
|
174
169
|
const organizationId = uuidv4();
|
|
175
|
-
const teamId = uuidv4();
|
|
176
170
|
const claimId = createClaim ? uuidv4() : null;
|
|
177
171
|
|
|
178
172
|
await db.orm.transaction(async (tx) => {
|
|
@@ -202,26 +196,9 @@ export class TestHarnessModule extends BaseModule<
|
|
|
202
196
|
createdAt: now,
|
|
203
197
|
});
|
|
204
198
|
|
|
205
|
-
await tx.insert(db.schema.teams).values({
|
|
206
|
-
id: teamId,
|
|
207
|
-
name: "Editorial",
|
|
208
|
-
organizationId,
|
|
209
|
-
createdAt: now,
|
|
210
|
-
updatedAt: now,
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
await tx.insert(db.schema.teamMembers).values({
|
|
214
|
-
id: uuidv4(),
|
|
215
|
-
teamId,
|
|
216
|
-
userId,
|
|
217
|
-
role: "owner",
|
|
218
|
-
createdAt: now,
|
|
219
|
-
});
|
|
220
|
-
|
|
221
199
|
if (claimId) {
|
|
222
|
-
await tx.insert(db.schema.
|
|
200
|
+
await tx.insert(db.schema.accountClaims).values({
|
|
223
201
|
id: claimId,
|
|
224
|
-
type: "ACCOUNT_CLAIM",
|
|
225
202
|
status: "INVITED",
|
|
226
203
|
claimUserId: userId,
|
|
227
204
|
code: uuidv4(),
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
// biome-ignore-all assist/source/organizeImports: feature-gated table exports stay in marker blocks
|
|
2
2
|
export {
|
|
3
3
|
accountClaimMagicLinks,
|
|
4
|
+
accountClaims,
|
|
4
5
|
accounts,
|
|
5
6
|
apikeys,
|
|
6
7
|
invitations,
|
|
7
8
|
members,
|
|
8
9
|
organizations,
|
|
9
10
|
sessions,
|
|
10
|
-
teamMembers,
|
|
11
|
-
teams,
|
|
12
11
|
users,
|
|
13
12
|
verifications,
|
|
14
13
|
waitlist,
|
|
@@ -22,7 +21,9 @@ export { files } from "@m5kdev/backend/modules/file/file.db";
|
|
|
22
21
|
// m5k:notifications:start
|
|
23
22
|
export {
|
|
24
23
|
notificationDevices,
|
|
24
|
+
notificationPreferences,
|
|
25
25
|
notificationSendLogs,
|
|
26
|
+
notifications,
|
|
26
27
|
} from "@m5kdev/backend/modules/notification/notification.db";
|
|
27
28
|
// m5k:notifications:end
|
|
28
29
|
// m5k:workflows:start
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
"exports": {
|
|
21
21
|
"./modules/app/*": "./src/modules/app/*.ts",
|
|
22
22
|
"./modules/conversation/*": "./src/modules/conversation/*.ts",
|
|
23
|
+
"./modules/demo-workflow/*": "./src/modules/demo-workflow/*.ts",
|
|
24
|
+
"./modules/notification/*": "./src/modules/notification/*.ts",
|
|
23
25
|
"./modules/posts/*": "./src/modules/posts/*.ts"
|
|
24
26
|
}
|
|
25
27
|
}
|
|
@@ -18,7 +18,7 @@ export const APP_ROLES_CONFIG = defineAuthRoles({
|
|
|
18
18
|
organization: {
|
|
19
19
|
roles: ["member", "admin", "owner"],
|
|
20
20
|
managerRoles: ["admin", "owner"],
|
|
21
|
-
assignableRoles: ["member", "admin"
|
|
21
|
+
assignableRoles: ["member", "admin"],
|
|
22
22
|
defaultRole: "member",
|
|
23
23
|
},
|
|
24
24
|
team: {
|
package/templates/minimal-app/apps/shared/src/modules/demo-workflow/demo-workflow.constants.ts.tpl
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DEMO_WORKFLOW_SERVER_EVENT_RESOURCE = "workflow";
|
package/templates/minimal-app/apps/shared/src/modules/notification/notification.schema.ts.tpl
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
/** Re-export shared
|
|
1
|
+
/** Re-export shared Notification contracts from commons. */
|
|
2
2
|
export * from "@m5kdev/commons/modules/notification/notification.constants";
|
|
3
3
|
export * from "@m5kdev/commons/modules/notification/notification.schema";
|
|
@@ -14,6 +14,7 @@ import { NuqsAdapter } from "nuqs/adapters/react-router/v7";
|
|
|
14
14
|
import { BrowserRouter } from "react-router";
|
|
15
15
|
import { Toaster } from "sonner";
|
|
16
16
|
import { Router } from "./Router";
|
|
17
|
+
import { StarterServerEventProvider } from "./StarterServerEventProvider";
|
|
17
18
|
|
|
18
19
|
export function App() {
|
|
19
20
|
return (
|
|
@@ -31,11 +32,13 @@ export function App() {
|
|
|
31
32
|
<ThemeProvider defaultTheme="light" storageKey="m5kdev-theme">
|
|
32
33
|
<AuthProvider loader={<AppLoader />}>
|
|
33
34
|
<AppTrpcQueryProvider>
|
|
34
|
-
<
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
<StarterServerEventProvider>
|
|
36
|
+
<DialogProvider>
|
|
37
|
+
<Router />
|
|
38
|
+
</DialogProvider>
|
|
39
|
+
<Toaster richColors closeButton />
|
|
40
|
+
<Toast.Provider placement="bottom end" />
|
|
41
|
+
</StarterServerEventProvider>
|
|
39
42
|
</AppTrpcQueryProvider>
|
|
40
43
|
</AuthProvider>
|
|
41
44
|
</ThemeProvider>
|
|
@@ -7,8 +7,11 @@ import { AppSidebarHeader } from "@m5kdev/web-ui/modules/app/components/AppSideb
|
|
|
7
7
|
import { AppSidebarUser } from "@m5kdev/web-ui/modules/app/components/AppSidebarUser";
|
|
8
8
|
import { AuthOrganizationSelect } from "@m5kdev/web-ui/modules/auth/components/AuthOrganizationSelect";
|
|
9
9
|
import { AuthUtilityImpersonationBanner } from "@m5kdev/web-ui/modules/auth/components/AuthUtilityImpersonationBanner";
|
|
10
|
+
// m5k:notifications:start
|
|
11
|
+
import { NotificationSidebarInbox } from "@m5kdev/web-ui/modules/notification/components/NotificationSidebarInbox";
|
|
12
|
+
// m5k:notifications:end
|
|
10
13
|
import { APP_NAME } from "{{PACKAGE_SCOPE}}/shared/modules/app/app.constants";
|
|
11
|
-
import { FileIcon, UsersIcon } from "lucide-react";
|
|
14
|
+
import { BellIcon, FileIcon, UsersIcon } from "lucide-react";
|
|
12
15
|
// m5k:ai:start
|
|
13
16
|
import { SparklesIcon } from "lucide-react";
|
|
14
17
|
// m5k:ai:end
|
|
@@ -21,6 +24,9 @@ import { WorkflowIcon } from "lucide-react";
|
|
|
21
24
|
import { useState } from "react";
|
|
22
25
|
import { useTranslation } from "react-i18next";
|
|
23
26
|
import { useNavigate } from "react-router";
|
|
27
|
+
// m5k:notifications:start
|
|
28
|
+
import { StarterWebPush } from "./modules/notification/StarterWebPush";
|
|
29
|
+
// m5k:notifications:end
|
|
24
30
|
|
|
25
31
|
export function Layout() {
|
|
26
32
|
const { data: session } = useSession();
|
|
@@ -42,6 +48,9 @@ export function Layout() {
|
|
|
42
48
|
header: <AppSidebarHeader logo={{ src: "/logo.svg", alt: APP_NAME }} title={APP_NAME} />,
|
|
43
49
|
content: (
|
|
44
50
|
<>
|
|
51
|
+
{/* m5k:notifications:start */}
|
|
52
|
+
<NotificationSidebarInbox enablePush={<StarterWebPush />} />
|
|
53
|
+
{/* m5k:notifications:end */}
|
|
45
54
|
<div className="px-2 group-data-[collapsible=icon]:hidden">
|
|
46
55
|
<AuthOrganizationSelect />
|
|
47
56
|
</div>
|
|
@@ -73,6 +82,13 @@ export function Layout() {
|
|
|
73
82
|
link: "/workflows",
|
|
74
83
|
},
|
|
75
84
|
// m5k:workflows:end
|
|
85
|
+
// m5k:notifications:start
|
|
86
|
+
{
|
|
87
|
+
label: t("layout.navigation.notifications"),
|
|
88
|
+
icon: <BellIcon />,
|
|
89
|
+
link: "/notifications",
|
|
90
|
+
},
|
|
91
|
+
// m5k:notifications:end
|
|
76
92
|
{
|
|
77
93
|
label: t("layout.navigation.members"),
|
|
78
94
|
icon: <UsersIcon />,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// biome-ignore-all assist/source/organizeImports: feature-gated route imports stay in marker blocks
|
|
2
2
|
import { syncI18nLocale } from "@m5kdev/frontend/modules/app/utils/locale";
|
|
3
3
|
import { AuthAdminRouter } from "@m5kdev/web-ui/modules/auth/components/AuthAdminRouter";
|
|
4
|
-
import { AuthOrganizationAcceptInvitationRoute } from "@m5kdev/web-ui/modules/auth/components/AuthOrganizationAcceptInvitationRoute";
|
|
5
4
|
import { AuthOrganizationRouter } from "@m5kdev/web-ui/modules/auth/components/AuthOrganizationRouter";
|
|
6
5
|
import { AuthPublicRouter } from "@m5kdev/web-ui/modules/auth/components/AuthPublicRouter";
|
|
7
6
|
import { AuthUserRouter } from "@m5kdev/web-ui/modules/auth/components/AuthUserRouter";
|
|
@@ -20,6 +19,9 @@ import { FilesRoute } from "@/modules/files/FilesRoute";
|
|
|
20
19
|
// m5k:workflows:start
|
|
21
20
|
import { WorkflowsRoute } from "@/modules/workflows/WorkflowsRoute";
|
|
22
21
|
// m5k:workflows:end
|
|
22
|
+
// m5k:notifications:start
|
|
23
|
+
import { NotificationPreferencesRoute } from "@/modules/notification/NotificationPreferencesRoute";
|
|
24
|
+
// m5k:notifications:end
|
|
23
25
|
import { Layout } from "./Layout";
|
|
24
26
|
|
|
25
27
|
const preferenceSchema = z.object({
|
|
@@ -68,13 +70,6 @@ export function Router() {
|
|
|
68
70
|
onLocaleChange: syncI18nLocale,
|
|
69
71
|
})}
|
|
70
72
|
|
|
71
|
-
{/* public: a logged-out invitee must reach this route so it can send
|
|
72
|
-
them to signup — inside ProtectedRoutes they'd bounce to /login */}
|
|
73
|
-
<Route
|
|
74
|
-
path="/organization/accept-invitation"
|
|
75
|
-
element={<AuthOrganizationAcceptInvitationRoute />}
|
|
76
|
-
/>
|
|
77
|
-
|
|
78
73
|
<Route
|
|
79
74
|
element={
|
|
80
75
|
<ProtectedRoutes>
|
|
@@ -93,6 +88,13 @@ export function Router() {
|
|
|
93
88
|
// m5k:workflows:start
|
|
94
89
|
<Route key="workflows" path="workflows" element={<WorkflowsRoute />} />,
|
|
95
90
|
// m5k:workflows:end
|
|
91
|
+
// m5k:notifications:start
|
|
92
|
+
<Route
|
|
93
|
+
key="notifications"
|
|
94
|
+
path="notifications"
|
|
95
|
+
element={<NotificationPreferencesRoute />}
|
|
96
|
+
/>,
|
|
97
|
+
// m5k:notifications:end
|
|
96
98
|
]}
|
|
97
99
|
|
|
98
100
|
{AuthUserRouter({
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// biome-ignore-all assist/source/organizeImports: feature-gated Server event imports stay in marker blocks
|
|
2
|
+
import { ServerEventProvider } from "@m5kdev/frontend/modules/app/components/ServerEventProvider";
|
|
3
|
+
// m5k:workflows:start
|
|
4
|
+
import type { QueryClient } from "@tanstack/react-query";
|
|
5
|
+
import { DEMO_WORKFLOW_SERVER_EVENT_RESOURCE } from "{{PACKAGE_SCOPE}}/shared/modules/demo-workflow/demo-workflow.constants";
|
|
6
|
+
// m5k:workflows:end
|
|
7
|
+
import type { ReactNode } from "react";
|
|
8
|
+
// m5k:workflows:start
|
|
9
|
+
import { useTRPC } from "./utils/trpc";
|
|
10
|
+
// m5k:workflows:end
|
|
11
|
+
|
|
12
|
+
type Props = {
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** Kernel SSE for the authenticated session. Workflow handlers are feature-gated. */
|
|
17
|
+
export function StarterServerEventProvider({ children }: Props): ReactNode {
|
|
18
|
+
// m5k:workflows:start
|
|
19
|
+
const trpc = useTRPC();
|
|
20
|
+
const invalidateWorkflowList = (queryClient: QueryClient): void => {
|
|
21
|
+
void queryClient.invalidateQueries(trpc.workflow.list.queryFilter());
|
|
22
|
+
};
|
|
23
|
+
// m5k:workflows:end
|
|
24
|
+
return (
|
|
25
|
+
<ServerEventProvider
|
|
26
|
+
// m5k:workflows:start
|
|
27
|
+
handlers={{
|
|
28
|
+
[DEMO_WORKFLOW_SERVER_EVENT_RESOURCE]: (_event, queryClient) => {
|
|
29
|
+
invalidateWorkflowList(queryClient);
|
|
30
|
+
},
|
|
31
|
+
}}
|
|
32
|
+
onReconnect={invalidateWorkflowList}
|
|
33
|
+
// m5k:workflows:end
|
|
34
|
+
>
|
|
35
|
+
{children}
|
|
36
|
+
</ServerEventProvider>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Card } from "@heroui/react";
|
|
2
|
+
import { NotificationPreferences } from "@m5kdev/web-ui/modules/notification/components/NotificationPreferences";
|
|
3
|
+
import { NOTIFICATION_KINDS } from "{{PACKAGE_SCOPE}}/shared/modules/notification/notification.constants";
|
|
4
|
+
import { useTranslation } from "react-i18next";
|
|
5
|
+
import { StarterWebPush } from "./StarterWebPush";
|
|
6
|
+
|
|
7
|
+
export function NotificationPreferencesRoute() {
|
|
8
|
+
const { t } = useTranslation("{{APP_SLUG}}");
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<div className="grid gap-6 p-4" data-testid="notification-preferences-route">
|
|
12
|
+
<div className="justify-self-start">
|
|
13
|
+
<StarterWebPush />
|
|
14
|
+
</div>
|
|
15
|
+
<Card>
|
|
16
|
+
<Card.Header className="flex flex-col gap-1">
|
|
17
|
+
<p className="text-xs font-medium uppercase tracking-wide text-muted">
|
|
18
|
+
{t("notifications.hero.eyebrow")}
|
|
19
|
+
</p>
|
|
20
|
+
<Card.Title>{t("notifications.hero.title")}</Card.Title>
|
|
21
|
+
<Card.Description>{t("notifications.hero.body")}</Card.Description>
|
|
22
|
+
</Card.Header>
|
|
23
|
+
<Card.Content>
|
|
24
|
+
<NotificationPreferences kinds={NOTIFICATION_KINDS} offerEmail />
|
|
25
|
+
</Card.Content>
|
|
26
|
+
</Card>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Button } from "@heroui/react";
|
|
2
|
+
import { useSession } from "@m5kdev/frontend/modules/auth/hooks/useSession";
|
|
3
|
+
import { useWebPush } from "@m5kdev/web-ui/hooks/useWebPush";
|
|
4
|
+
import { BellRingIcon } from "lucide-react";
|
|
5
|
+
import type { ReactNode } from "react";
|
|
6
|
+
import { useTranslation } from "react-i18next";
|
|
7
|
+
import { useTRPC } from "../../utils/trpc";
|
|
8
|
+
|
|
9
|
+
export function StarterWebPush(): ReactNode {
|
|
10
|
+
const { t } = useTranslation("{{APP_SLUG}}");
|
|
11
|
+
const { data: session } = useSession();
|
|
12
|
+
const trpc = useTRPC();
|
|
13
|
+
const webPush = useWebPush({
|
|
14
|
+
enabled: Boolean(session?.user?.id),
|
|
15
|
+
messages: {
|
|
16
|
+
unsupported: t("layout.push.unsupported"),
|
|
17
|
+
denied: t("layout.push.denied"),
|
|
18
|
+
noVapid: t("layout.push.noVapid"),
|
|
19
|
+
badSubscription: t("layout.push.badSubscription"),
|
|
20
|
+
failed: t("layout.push.failed"),
|
|
21
|
+
enabled: t("layout.push.enabled"),
|
|
22
|
+
},
|
|
23
|
+
vapidPublicKeyQuery: trpc.notification.vapidPublicKey.queryOptions(),
|
|
24
|
+
registerDeviceMutation: trpc.notification.registerDevice.mutationOptions(),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (!session?.user?.id || webPush.flowStatus === "done") return null;
|
|
28
|
+
|
|
29
|
+
const label = webPush.permission === "denied" ? t("layout.push.denied") : t("layout.push.cta");
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<Button
|
|
33
|
+
size="sm"
|
|
34
|
+
variant="secondary"
|
|
35
|
+
isDisabled={!webPush.canSubscribe || webPush.isWorking}
|
|
36
|
+
onPress={() => {
|
|
37
|
+
void webPush.subscribe();
|
|
38
|
+
}}
|
|
39
|
+
>
|
|
40
|
+
<BellRingIcon className="size-4" />
|
|
41
|
+
{label}
|
|
42
|
+
</Button>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"organization": "Organization",
|
|
20
20
|
"account": "Account",
|
|
21
21
|
"members": "Members",
|
|
22
|
+
"notifications": "Notifications",
|
|
22
23
|
"childOrgs": "Child orgs",
|
|
23
24
|
"orgPreferences": "Org preferences",
|
|
24
25
|
"profile": "Profile",
|
|
@@ -184,6 +185,13 @@
|
|
|
184
185
|
"failed": "Upload failed."
|
|
185
186
|
}
|
|
186
187
|
},
|
|
188
|
+
"notifications": {
|
|
189
|
+
"hero": {
|
|
190
|
+
"eyebrow": "This membership",
|
|
191
|
+
"title": "Notification preferences",
|
|
192
|
+
"body": "Mute channels for this organization. Other organizations keep their own settings."
|
|
193
|
+
}
|
|
194
|
+
},
|
|
187
195
|
"auth": {
|
|
188
196
|
"header": {
|
|
189
197
|
"eyebrow": "Framework Starter",
|