create-bw-app 0.23.2 → 0.24.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,7 +1,7 @@
1
1
  {
2
2
  "name": "create-bw-app",
3
3
  "private": false,
4
- "version": "0.23.2",
4
+ "version": "0.24.0",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "create-bw-app": "bin/create-bw-app.mjs",
package/src/constants.mjs CHANGED
@@ -173,16 +173,16 @@ export const PLATFORM_STARTER_FILES = [
173
173
  ];
174
174
 
175
175
  export const APP_DEPENDENCY_DEFAULTS = {
176
- "@brightweblabs/app-shell": "^0.10.1",
177
- "@brightweblabs/core-auth": "^0.9.3",
176
+ "@brightweblabs/app-shell": "^0.11.0",
177
+ "@brightweblabs/core-auth": "^0.10.0",
178
178
  "@brightweblabs/infra": "^0.7.0",
179
- "@brightweblabs/module-admin": "^0.8.2",
180
- "@brightweblabs/module-crm": "^0.13.1",
181
- "@brightweblabs/module-marketing": "^0.3.1",
182
- "@brightweblabs/module-orgs": "^0.3.18",
183
- "@brightweblabs/module-projects": "^0.12.3",
184
- "@brightweblabs/theme": "^0.7.1",
185
- "@brightweblabs/ui": "^1.4.2",
179
+ "@brightweblabs/module-admin": "^0.8.5",
180
+ "@brightweblabs/module-crm": "^0.14.0",
181
+ "@brightweblabs/module-marketing": "^0.4.0",
182
+ "@brightweblabs/module-orgs": "^0.4.0",
183
+ "@brightweblabs/module-projects": "^0.13.0",
184
+ "@brightweblabs/theme": "^0.7.2",
185
+ "@brightweblabs/ui": "^1.4.4",
186
186
  "geist": "1.7.2",
187
187
  "lucide-react": "^1.8.0",
188
188
  "next": "^16.0.0",
package/src/generator.mjs CHANGED
@@ -1206,6 +1206,7 @@ export function createOptionalModuleRouteFiles(selectedModules) {
1206
1206
  ], orgsEnabled),
1207
1207
  "app/api/organizations/[id]/route.ts": createOrganizationRoute([
1208
1208
  { method: "PATCH", handler: "handleOrganizationPatchRequest", context: true },
1209
+ { method: "DELETE", handler: "handleOrganizationDeleteRequest", context: true },
1209
1210
  ], orgsEnabled),
1210
1211
  "app/api/organizations/[id]/invitations/route.ts": createOrganizationRoute([
1211
1212
  { method: "GET", handler: "handleOrganizationInvitationsGetRequest", context: true },
package/src/upgrade.mjs CHANGED
@@ -59,9 +59,8 @@ export async function upgradeBrightwebApp(moduleKey, argvOptions = {}, runtimeOp
59
59
  }
60
60
  await applyMigrationWrites(migrationPlan.writes);
61
61
  appManifest.migrationCursor = migrationPlan.nextCursor;
62
- for (const update of plan.packageUpdates) {
63
- const key = Object.keys(catalog).find((candidate) => catalog[candidate].packageName === update.packageName);
64
- if (key && appManifest.modules[key]) appManifest.modules[key].version = cleanVersion(update.to) || appManifest.modules[key].version;
62
+ for (const [key, entry] of Object.entries(appManifest.modules)) {
63
+ if (catalog[key]?.packageRoot) entry.version = catalog[key].version;
65
64
  }
66
65
  for (const relativePath of plan.starterFilesToRefresh || []) {
67
66
  const targetPath = resolveSafeRelativePath(targetDir, relativePath, "Manifest scaffold file path");
@@ -1 +1 @@
1
- export { handleNotificationsGetRequest as GET, handleNotificationsPostRequest as POST } from "@brightweblabs/core-auth/notifications";
1
+ export { handleNotificationsDeleteRequest as DELETE, handleNotificationsGetRequest as GET, handleNotificationsPostRequest as POST } from "@brightweblabs/core-auth/notifications";
@@ -4,3 +4,8 @@ export async function PATCH(request: Request, context: { params: Promise<{ id: s
4
4
  const { handleOrganizationPatchRequest } = await import("@brightweblabs/module-orgs");
5
5
  return handleOrganizationPatchRequest(request, context);
6
6
  }
7
+
8
+ export async function DELETE(request: Request, context: { params: Promise<{ id: string }> }) {
9
+ const { handleOrganizationDeleteRequest } = await import("@brightweblabs/module-orgs");
10
+ return handleOrganizationDeleteRequest(request, context);
11
+ }
@@ -67,6 +67,9 @@ export function marketingTopicsOrderPost(request: Request) {
67
67
  export function marketingTopicPatch(request: Request, context: IdRouteContext) {
68
68
  return handlers.topicPatch(request, context);
69
69
  }
70
+ export function marketingTopicDelete(request: Request, context: IdRouteContext) {
71
+ return handlers.topicDelete(request, context);
72
+ }
70
73
  export function marketingWebhookPost(request: Request) {
71
74
  return handlers.webhookPost(request);
72
75
  }
@@ -100,6 +103,9 @@ export function marketingCampaignCancelPost(request: Request, context: IdRouteCo
100
103
  export function marketingCampaignRecipientsGet(request: Request, context: IdRouteContext) {
101
104
  return handlers.recipientsGet(request, context);
102
105
  }
106
+ export function marketingCampaignRecipientDelete(request: Request, context: { params: Promise<{ id: string; recipientId: string }> }) {
107
+ return handlers.recipientDelete(request, context);
108
+ }
103
109
  export function marketingCampaignRetryPost(request: Request, context: IdRouteContext) {
104
110
  return handlers.retryPost(request, context);
105
111
  }
@@ -0,0 +1,2 @@
1
+ export { marketingCampaignRecipientDelete as DELETE } from "../../../../_handlers";
2
+ export const dynamic = "force-dynamic";
@@ -1,2 +1,2 @@
1
- export { marketingTopicPatch as PATCH } from "../../_handlers";
1
+ export { marketingTopicDelete as DELETE, marketingTopicPatch as PATCH } from "../../_handlers";
2
2
  export const dynamic = "force-dynamic";
@@ -1,6 +1,7 @@
1
1
  import { requireServerPageAccess } from "@brightweblabs/core-auth/server";
2
2
  import {
3
3
  getProjectDashboard,
4
+ getProjectAccess,
4
5
  getProjectsPortfolioPageData,
5
6
  } from "@brightweblabs/module-projects";
6
7
  import {
@@ -37,18 +38,25 @@ export async function ProjectsServerMount() {
37
38
 
38
39
  async function getInitialProject(params: Promise<{ projectId: string }>) {
39
40
  const { projectId } = await params;
40
- const { supabase } = await requireServerPageAccess();
41
- return getProjectDashboard(supabase, projectId);
41
+ const { supabase, profileId, role } = await requireServerPageAccess();
42
+ const [initialData, access] = await Promise.all([
43
+ getProjectDashboard(supabase, projectId),
44
+ getProjectAccess(supabase, projectId, profileId, role),
45
+ ]);
46
+ return { initialData, access };
42
47
  }
43
48
 
44
49
  export async function ProjectDetailServerMount({ params }: { params: Promise<{ projectId: string }> }) {
45
- return <ProjectDetailPageLiveMount initialData={await getInitialProject(params)} navigation={navigation} />;
50
+ const { initialData, access } = await getInitialProject(params);
51
+ return <ProjectDetailPageLiveMount initialData={initialData} projectRole={access.projectRole} permissions={access.permissions} navigation={navigation} />;
46
52
  }
47
53
 
48
54
  export async function ProjectBoardServerMount({ params }: { params: Promise<{ projectId: string }> }) {
49
- return <ProjectBoardPageLiveMount initialData={await getInitialProject(params)} navigation={navigation} />;
55
+ const { initialData, access } = await getInitialProject(params);
56
+ return <ProjectBoardPageLiveMount initialData={initialData} projectRole={access.projectRole} permissions={access.permissions} navigation={navigation} />;
50
57
  }
51
58
 
52
59
  export async function ProjectTasksServerMount({ params }: { params: Promise<{ projectId: string }> }) {
53
- return <ProjectTasksPageLiveMount initialData={await getInitialProject(params)} navigation={navigation} />;
60
+ const { initialData, access } = await getInitialProject(params);
61
+ return <ProjectTasksPageLiveMount initialData={initialData} projectRole={access.projectRole} permissions={access.permissions} navigation={navigation} />;
54
62
  }
@@ -0,0 +1,78 @@
1
+ -- Per-profile notification dismissal without deleting shared audit events.
2
+
3
+ CREATE TABLE IF NOT EXISTS public.user_notification_dismissals (
4
+ profile_id uuid NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE,
5
+ activity_event_id uuid NOT NULL REFERENCES public.app_activity_events(id) ON DELETE CASCADE,
6
+ dismissed_at timestamptz NOT NULL DEFAULT now(),
7
+ PRIMARY KEY (profile_id, activity_event_id)
8
+ );
9
+
10
+ ALTER TABLE public.user_notification_dismissals ENABLE ROW LEVEL SECURITY;
11
+ REVOKE ALL ON TABLE public.user_notification_dismissals FROM PUBLIC, anon, authenticated;
12
+ GRANT ALL ON TABLE public.user_notification_dismissals TO service_role;
13
+
14
+ CREATE OR REPLACE FUNCTION public.current_notification_summary(p_profile_id uuid)
15
+ RETURNS TABLE(seen_at timestamptz, unread_count bigint)
16
+ LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public
17
+ AS $$
18
+ WITH notification_state AS (
19
+ SELECT state.alerts_seen_at FROM public.user_notification_state state
20
+ WHERE state.profile_id = p_profile_id LIMIT 1
21
+ )
22
+ SELECT (SELECT alerts_seen_at FROM notification_state), count(event.id)
23
+ FROM public.app_activity_events event
24
+ WHERE public.can_profile_view_app_activity_event(p_profile_id, event.domain, event.entity_table, event.entity_id, event.payload)
25
+ AND ((SELECT alerts_seen_at FROM notification_state) IS NULL OR event.created_at > (SELECT alerts_seen_at FROM notification_state))
26
+ AND (event.actor_profile_id IS NULL OR event.actor_profile_id <> p_profile_id)
27
+ AND NOT EXISTS (
28
+ SELECT 1 FROM public.user_notification_dismissals dismissal
29
+ WHERE dismissal.profile_id = p_profile_id AND dismissal.activity_event_id = event.id
30
+ )
31
+ $$;
32
+
33
+ DROP FUNCTION IF EXISTS public.current_notification_items(uuid, integer);
34
+ CREATE FUNCTION public.current_notification_items(p_profile_id uuid, p_limit integer DEFAULT 8)
35
+ RETURNS TABLE(id uuid, created_at timestamptz, domain text, event_type text, entity_table text, entity_id uuid, actor_profile_id uuid, summary text, payload jsonb)
36
+ LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public
37
+ AS $$
38
+ SELECT event.id, event.created_at, event.domain, event.event_type, event.entity_table,
39
+ event.entity_id, event.actor_profile_id, event.summary, COALESCE(event.payload, '{}'::jsonb)
40
+ FROM public.app_activity_events event
41
+ WHERE public.can_profile_view_app_activity_event(p_profile_id, event.domain, event.entity_table, event.entity_id, event.payload)
42
+ AND (event.actor_profile_id IS NULL OR event.actor_profile_id <> p_profile_id)
43
+ AND NOT EXISTS (
44
+ SELECT 1 FROM public.user_notification_dismissals dismissal
45
+ WHERE dismissal.profile_id = p_profile_id AND dismissal.activity_event_id = event.id
46
+ )
47
+ ORDER BY event.created_at DESC
48
+ LIMIT LEAST(GREATEST(COALESCE(p_limit, 8), 0), 50)
49
+ $$;
50
+
51
+ CREATE OR REPLACE FUNCTION public.dismiss_current_notifications(p_profile_id uuid, p_activity_event_id uuid DEFAULT NULL)
52
+ RETURNS integer
53
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = public
54
+ AS $$
55
+ DECLARE v_count integer;
56
+ BEGIN
57
+ IF p_activity_event_id IS NULL THEN
58
+ RETURN 0;
59
+ END IF;
60
+
61
+ INSERT INTO public.user_notification_dismissals(profile_id, activity_event_id)
62
+ SELECT p_profile_id, event.id
63
+ FROM public.app_activity_events event
64
+ WHERE event.id = p_activity_event_id
65
+ AND public.can_profile_view_app_activity_event(p_profile_id, event.domain, event.entity_table, event.entity_id, event.payload)
66
+ AND (event.actor_profile_id IS NULL OR event.actor_profile_id <> p_profile_id)
67
+ ON CONFLICT (profile_id, activity_event_id) DO NOTHING;
68
+ GET DIAGNOSTICS v_count = ROW_COUNT;
69
+ RETURN v_count;
70
+ END;
71
+ $$;
72
+
73
+ REVOKE ALL ON FUNCTION public.current_notification_summary(uuid) FROM PUBLIC, anon, authenticated;
74
+ REVOKE ALL ON FUNCTION public.current_notification_items(uuid, integer) FROM PUBLIC, anon, authenticated;
75
+ REVOKE ALL ON FUNCTION public.dismiss_current_notifications(uuid, uuid) FROM PUBLIC, anon, authenticated;
76
+ GRANT EXECUTE ON FUNCTION public.current_notification_summary(uuid) TO service_role;
77
+ GRANT EXECUTE ON FUNCTION public.current_notification_items(uuid, integer) TO service_role;
78
+ GRANT EXECUTE ON FUNCTION public.dismiss_current_notifications(uuid, uuid) TO service_role;
@@ -0,0 +1,107 @@
1
+ -- Serialize destructive campaign operations with delivery state transitions.
2
+
3
+ CREATE OR REPLACE FUNCTION public.maintain_marketing_campaign_recipient_count()
4
+ RETURNS trigger
5
+ LANGUAGE plpgsql
6
+ SECURITY DEFINER
7
+ SET search_path = public
8
+ AS $$
9
+ BEGIN
10
+ IF TG_OP = 'INSERT' THEN
11
+ UPDATE public.marketing_campaigns
12
+ SET total_recipients = total_recipients + 1
13
+ WHERE id = NEW.campaign_id;
14
+ RETURN NEW;
15
+ END IF;
16
+
17
+ UPDATE public.marketing_campaigns
18
+ SET total_recipients = GREATEST(0, total_recipients - 1)
19
+ WHERE id = OLD.campaign_id;
20
+ RETURN OLD;
21
+ END;
22
+ $$;
23
+
24
+ UPDATE public.marketing_campaigns campaign
25
+ SET total_recipients = (
26
+ SELECT count(*)::integer
27
+ FROM public.marketing_campaign_recipients recipient
28
+ WHERE recipient.campaign_id = campaign.id
29
+ );
30
+
31
+ DROP TRIGGER IF EXISTS trg_maintain_marketing_campaign_recipient_count
32
+ ON public.marketing_campaign_recipients;
33
+ CREATE TRIGGER trg_maintain_marketing_campaign_recipient_count
34
+ AFTER INSERT OR DELETE ON public.marketing_campaign_recipients
35
+ FOR EACH ROW
36
+ EXECUTE FUNCTION public.maintain_marketing_campaign_recipient_count();
37
+
38
+ CREATE OR REPLACE FUNCTION public.delete_marketing_campaign_safely(p_campaign_id uuid)
39
+ RETURNS text
40
+ LANGUAGE plpgsql
41
+ SECURITY INVOKER
42
+ SET search_path = public
43
+ AS $$
44
+ DECLARE
45
+ v_status text;
46
+ BEGIN
47
+ SELECT status INTO v_status
48
+ FROM public.marketing_campaigns
49
+ WHERE id = p_campaign_id
50
+ FOR UPDATE;
51
+
52
+ IF NOT FOUND THEN
53
+ RETURN 'not_found';
54
+ END IF;
55
+ IF v_status NOT IN ('draft', 'canceled') THEN
56
+ RETURN 'invalid_status';
57
+ END IF;
58
+
59
+ DELETE FROM public.marketing_campaigns WHERE id = p_campaign_id;
60
+ RETURN 'deleted';
61
+ END;
62
+ $$;
63
+
64
+ CREATE OR REPLACE FUNCTION public.delete_marketing_campaign_recipient_safely(
65
+ p_campaign_id uuid,
66
+ p_recipient_id uuid
67
+ )
68
+ RETURNS text
69
+ LANGUAGE plpgsql
70
+ SECURITY INVOKER
71
+ SET search_path = public
72
+ AS $$
73
+ DECLARE
74
+ v_campaign_status text;
75
+ v_deleted_id uuid;
76
+ BEGIN
77
+ SELECT status INTO v_campaign_status
78
+ FROM public.marketing_campaigns
79
+ WHERE id = p_campaign_id
80
+ FOR UPDATE;
81
+
82
+ IF NOT FOUND THEN
83
+ RETURN 'campaign_not_found';
84
+ END IF;
85
+ IF v_campaign_status NOT IN ('draft', 'scheduled', 'canceled') THEN
86
+ RETURN 'campaign_locked';
87
+ END IF;
88
+
89
+ DELETE FROM public.marketing_campaign_recipients
90
+ WHERE id = p_recipient_id
91
+ AND campaign_id = p_campaign_id
92
+ AND status IN ('queued', 'suppressed', 'skipped')
93
+ RETURNING id INTO v_deleted_id;
94
+
95
+ IF v_deleted_id IS NULL THEN
96
+ RETURN 'recipient_not_deletable';
97
+ END IF;
98
+
99
+ RETURN 'deleted';
100
+ END;
101
+ $$;
102
+
103
+ REVOKE ALL ON FUNCTION public.maintain_marketing_campaign_recipient_count() FROM PUBLIC, anon, authenticated;
104
+ REVOKE ALL ON FUNCTION public.delete_marketing_campaign_safely(uuid) FROM PUBLIC, anon;
105
+ REVOKE ALL ON FUNCTION public.delete_marketing_campaign_recipient_safely(uuid, uuid) FROM PUBLIC, anon;
106
+ GRANT EXECUTE ON FUNCTION public.delete_marketing_campaign_safely(uuid) TO authenticated, service_role;
107
+ GRANT EXECUTE ON FUNCTION public.delete_marketing_campaign_recipient_safely(uuid, uuid) TO authenticated, service_role;
@@ -0,0 +1,45 @@
1
+ -- Apply exact project membership and owner changes in one serialized transaction.
2
+
3
+ CREATE OR REPLACE FUNCTION public.sync_project_members_exact(
4
+ p_project_id uuid,
5
+ p_members jsonb,
6
+ p_owner_profile_id uuid DEFAULT NULL
7
+ )
8
+ RETURNS void
9
+ LANGUAGE plpgsql
10
+ SECURITY INVOKER
11
+ SET search_path = public
12
+ AS $$
13
+ BEGIN
14
+ IF jsonb_typeof(COALESCE(p_members, '[]'::jsonb)) <> 'array' THEN
15
+ RAISE EXCEPTION 'Project members must be a JSON array.' USING ERRCODE = '22023';
16
+ END IF;
17
+
18
+ PERFORM 1 FROM public.projects WHERE id = p_project_id FOR UPDATE;
19
+ IF NOT FOUND THEN
20
+ RAISE EXCEPTION 'Projeto não encontrado.' USING ERRCODE = 'P0002';
21
+ END IF;
22
+
23
+ INSERT INTO public.project_members(project_id, profile_id, role)
24
+ SELECT p_project_id, member.profile_id, member.role
25
+ FROM jsonb_to_recordset(COALESCE(p_members, '[]'::jsonb))
26
+ AS member(profile_id uuid, role text)
27
+ ON CONFLICT (project_id, profile_id) DO UPDATE SET role = EXCLUDED.role;
28
+
29
+ DELETE FROM public.project_members existing
30
+ WHERE existing.project_id = p_project_id
31
+ AND NOT EXISTS (
32
+ SELECT 1
33
+ FROM jsonb_to_recordset(COALESCE(p_members, '[]'::jsonb))
34
+ AS member(profile_id uuid, role text)
35
+ WHERE member.profile_id = existing.profile_id
36
+ );
37
+
38
+ UPDATE public.projects
39
+ SET owner_profile_id = p_owner_profile_id
40
+ WHERE id = p_project_id;
41
+ END;
42
+ $$;
43
+
44
+ REVOKE ALL ON FUNCTION public.sync_project_members_exact(uuid, jsonb, uuid) FROM PUBLIC, anon;
45
+ GRANT EXECUTE ON FUNCTION public.sync_project_members_exact(uuid, jsonb, uuid) TO authenticated, service_role;