create-bw-app 0.24.8 → 0.25.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/README.md +3 -1
- package/package.json +1 -1
- package/src/bw.mjs +1 -1
- package/src/constants.mjs +14 -9
- package/src/generator.mjs +3 -6
- package/src/migrations.mjs +43 -5
- package/src/upgrade.mjs +54 -2
- package/template/base/app/(shell)/dashboard/dashboard-page-mount.tsx +7 -0
- package/template/base/app/(shell)/dashboard/page.tsx +1 -1
- package/template/base/app/(shell)/shell-layout-client.tsx +28 -7
- package/template/modules/crm/app/(shell)/crm/organizations/[id]/page.tsx +1 -1
- package/template/modules/crm/app/(shell)/crm/organizations/page.tsx +1 -3
- package/template/modules/crm/app/(shell)/crm/page.tsx +1 -5
- package/template/modules/crm/app/(shell)/crm/report/page.tsx +1 -5
- package/template/modules/projects/app/(shell)/account/page.tsx +5 -1
- package/template/modules/projects/app/(shell)/account/perfil/page.tsx +1 -0
- package/template/modules/projects/app/(shell)/projetos/projetos-server-mounts.tsx +3 -2
- package/template/modules/projects/app/api/account/projects/[id]/route.ts +2 -0
- package/template/modules/projects/app/api/account/projects/route.ts +2 -0
- package/template/modules/projects/app/api/projects/[id]/client-access/route.ts +2 -0
- package/template/modules/projects/app/api/projects/[id]/organizations/route.ts +2 -0
- package/template/modules/projects/app/api/projects/_handlers.ts +24 -0
- package/template/modules/projects/app/api/projects/setup-options/route.ts +2 -0
- package/template/supabase/modules/orgs/migrations/20260812120000_staff_only_organization_access_management.sql +7 -0
- package/template/supabase/modules/projects/migrations/20260810120000_project_client_access.sql +328 -0
- package/template/supabase/modules/projects/migrations/20260811120000_project_client_access_expand.sql +1061 -0
- package/template/supabase/modules/projects/migrations/20260811120500_project_member_sync_hardening.sql +156 -0
- package/template/supabase/modules/projects/migrations/20260811121000_project_client_access_enforcement.sql +651 -0
- package/template/supabase/modules/projects/migrations/20260811121500_project_client_organization_memberships.sql +29 -0
- package/template/supabase/modules/projects/migrations/20260811121700_project_client_meta_preview.sql +193 -0
- package/template/supabase/modules/projects/migrations/20260811122000_project_client_access_identity_cleanup.sql +201 -0
- package/template/supabase/modules/projects/migrations/20260811122500_remove_project_client_next_steps.sql +681 -0
package/template/supabase/modules/projects/migrations/20260811121700_project_client_meta_preview.sql
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
-- Add bounded client-visible meta previews before destructive cleanup. Keep the
|
|
2
|
+
-- legacy next-steps output during this compatibility phase so upgraded readers
|
|
3
|
+
-- can deploy while older consumers still understand the previous RPC shape.
|
|
4
|
+
|
|
5
|
+
DROP FUNCTION public.get_current_client_project(uuid);
|
|
6
|
+
DROP FUNCTION public.list_current_client_projects();
|
|
7
|
+
|
|
8
|
+
CREATE FUNCTION public.list_current_client_projects()
|
|
9
|
+
RETURNS TABLE (
|
|
10
|
+
project_id uuid,
|
|
11
|
+
name text,
|
|
12
|
+
reference text,
|
|
13
|
+
status text,
|
|
14
|
+
start_date date,
|
|
15
|
+
target_date date,
|
|
16
|
+
completed_at timestamptz,
|
|
17
|
+
archived_at timestamptz,
|
|
18
|
+
client_summary text,
|
|
19
|
+
client_scope text,
|
|
20
|
+
client_contact jsonb,
|
|
21
|
+
client_next_steps text,
|
|
22
|
+
meta_preview jsonb,
|
|
23
|
+
organizations jsonb,
|
|
24
|
+
progress_percent integer
|
|
25
|
+
)
|
|
26
|
+
LANGUAGE sql
|
|
27
|
+
STABLE
|
|
28
|
+
SECURITY DEFINER
|
|
29
|
+
SET search_path = public
|
|
30
|
+
AS $$
|
|
31
|
+
SELECT
|
|
32
|
+
project.id,
|
|
33
|
+
project.name,
|
|
34
|
+
project.code,
|
|
35
|
+
project.status,
|
|
36
|
+
project.start_date,
|
|
37
|
+
project.target_date,
|
|
38
|
+
project.completed_at,
|
|
39
|
+
project.archived_at,
|
|
40
|
+
project.client_summary,
|
|
41
|
+
project.client_scope,
|
|
42
|
+
CASE WHEN contact.id IS NULL THEN NULL ELSE jsonb_build_object(
|
|
43
|
+
'profile_id', contact.id,
|
|
44
|
+
'name', COALESCE(NULLIF(btrim(concat_ws(' ', contact.first_name, contact.last_name)), ''), contact.email),
|
|
45
|
+
'email', contact.email
|
|
46
|
+
) END,
|
|
47
|
+
project.client_next_steps,
|
|
48
|
+
COALESCE((
|
|
49
|
+
SELECT jsonb_agg(jsonb_build_object(
|
|
50
|
+
'id', current_meta.id,
|
|
51
|
+
'title', current_meta.title,
|
|
52
|
+
'status', current_meta.status,
|
|
53
|
+
'target_date', current_meta.target_date,
|
|
54
|
+
'completed_at', current_meta.completed_at,
|
|
55
|
+
'position', current_meta.position
|
|
56
|
+
) ORDER BY current_meta.position, current_meta.target_date, current_meta.id)
|
|
57
|
+
FROM (
|
|
58
|
+
SELECT milestone.*
|
|
59
|
+
FROM public.project_milestones milestone
|
|
60
|
+
WHERE milestone.project_id = project.id
|
|
61
|
+
AND milestone.visibility = 'client'
|
|
62
|
+
AND milestone.status IN ('in_progress', 'delayed')
|
|
63
|
+
ORDER BY milestone.position, milestone.target_date, milestone.id
|
|
64
|
+
LIMIT 3
|
|
65
|
+
) current_meta
|
|
66
|
+
), (
|
|
67
|
+
SELECT jsonb_agg(jsonb_build_object(
|
|
68
|
+
'id', pending_meta.id,
|
|
69
|
+
'title', pending_meta.title,
|
|
70
|
+
'status', pending_meta.status,
|
|
71
|
+
'target_date', pending_meta.target_date,
|
|
72
|
+
'completed_at', pending_meta.completed_at,
|
|
73
|
+
'position', pending_meta.position
|
|
74
|
+
) ORDER BY pending_meta.position, pending_meta.target_date, pending_meta.id)
|
|
75
|
+
FROM (
|
|
76
|
+
SELECT milestone.*
|
|
77
|
+
FROM public.project_milestones milestone
|
|
78
|
+
WHERE milestone.project_id = project.id
|
|
79
|
+
AND milestone.visibility = 'client'
|
|
80
|
+
AND milestone.status = 'pending'
|
|
81
|
+
ORDER BY milestone.position, milestone.target_date, milestone.id
|
|
82
|
+
LIMIT 1
|
|
83
|
+
) pending_meta
|
|
84
|
+
), '[]'::jsonb),
|
|
85
|
+
COALESCE((
|
|
86
|
+
SELECT jsonb_agg(jsonb_build_object('id', organization.id, 'name', organization.name) ORDER BY organization.name)
|
|
87
|
+
FROM public.project_client_organizations audience
|
|
88
|
+
JOIN public.organization_members org_member
|
|
89
|
+
ON org_member.organization_id = audience.organization_id
|
|
90
|
+
AND org_member.profile_id = public.current_profile_id()
|
|
91
|
+
JOIN public.organizations organization ON organization.id = audience.organization_id
|
|
92
|
+
WHERE audience.project_id = project.id
|
|
93
|
+
AND (
|
|
94
|
+
project.client_access_mode = 'all_org_clients'
|
|
95
|
+
OR EXISTS (
|
|
96
|
+
SELECT 1 FROM public.project_client_profiles grant_row
|
|
97
|
+
WHERE grant_row.project_id = project.id
|
|
98
|
+
AND grant_row.organization_id = audience.organization_id
|
|
99
|
+
AND grant_row.profile_id = public.current_profile_id()
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
), '[]'::jsonb),
|
|
103
|
+
CASE WHEN count(milestone.id) = 0 THEN 0
|
|
104
|
+
ELSE round(100.0 * count(milestone.id) FILTER (WHERE milestone.status = 'achieved') / count(milestone.id))::integer
|
|
105
|
+
END
|
|
106
|
+
FROM public.projects project
|
|
107
|
+
LEFT JOIN public.profiles contact ON contact.id = project.client_contact_profile_id
|
|
108
|
+
LEFT JOIN public.project_milestones milestone
|
|
109
|
+
ON milestone.project_id = project.id AND milestone.visibility = 'client'
|
|
110
|
+
WHERE public.can_current_client_view_project(project.id)
|
|
111
|
+
GROUP BY project.id, contact.id
|
|
112
|
+
ORDER BY project.updated_at DESC;
|
|
113
|
+
$$;
|
|
114
|
+
|
|
115
|
+
REVOKE ALL ON FUNCTION public.list_current_client_projects()
|
|
116
|
+
FROM PUBLIC, anon, service_role;
|
|
117
|
+
GRANT EXECUTE ON FUNCTION public.list_current_client_projects()
|
|
118
|
+
TO authenticated;
|
|
119
|
+
|
|
120
|
+
CREATE FUNCTION public.get_current_client_project(target_project_id uuid)
|
|
121
|
+
RETURNS TABLE (
|
|
122
|
+
project_id uuid,
|
|
123
|
+
name text,
|
|
124
|
+
reference text,
|
|
125
|
+
status text,
|
|
126
|
+
start_date date,
|
|
127
|
+
target_date date,
|
|
128
|
+
completed_at timestamptz,
|
|
129
|
+
archived_at timestamptz,
|
|
130
|
+
client_summary text,
|
|
131
|
+
client_scope text,
|
|
132
|
+
client_contact jsonb,
|
|
133
|
+
client_next_steps text,
|
|
134
|
+
meta_preview jsonb,
|
|
135
|
+
organizations jsonb,
|
|
136
|
+
progress_percent integer,
|
|
137
|
+
metas jsonb,
|
|
138
|
+
documents jsonb
|
|
139
|
+
)
|
|
140
|
+
LANGUAGE sql
|
|
141
|
+
STABLE
|
|
142
|
+
SECURITY DEFINER
|
|
143
|
+
SET search_path = public
|
|
144
|
+
AS $$
|
|
145
|
+
SELECT
|
|
146
|
+
list_row.project_id,
|
|
147
|
+
list_row.name,
|
|
148
|
+
list_row.reference,
|
|
149
|
+
list_row.status,
|
|
150
|
+
list_row.start_date,
|
|
151
|
+
list_row.target_date,
|
|
152
|
+
list_row.completed_at,
|
|
153
|
+
list_row.archived_at,
|
|
154
|
+
list_row.client_summary,
|
|
155
|
+
list_row.client_scope,
|
|
156
|
+
list_row.client_contact,
|
|
157
|
+
list_row.client_next_steps,
|
|
158
|
+
list_row.meta_preview,
|
|
159
|
+
list_row.organizations,
|
|
160
|
+
list_row.progress_percent,
|
|
161
|
+
COALESCE((
|
|
162
|
+
SELECT jsonb_agg(jsonb_build_object(
|
|
163
|
+
'id', milestone.id,
|
|
164
|
+
'title', milestone.title,
|
|
165
|
+
'status', milestone.status,
|
|
166
|
+
'target_date', milestone.target_date,
|
|
167
|
+
'completed_at', milestone.completed_at,
|
|
168
|
+
'position', milestone.position
|
|
169
|
+
) ORDER BY milestone.position, milestone.target_date, milestone.id)
|
|
170
|
+
FROM public.project_milestones milestone
|
|
171
|
+
WHERE milestone.project_id = target_project_id
|
|
172
|
+
AND milestone.visibility = 'client'
|
|
173
|
+
), '[]'::jsonb),
|
|
174
|
+
COALESCE((
|
|
175
|
+
SELECT jsonb_agg(jsonb_build_object(
|
|
176
|
+
'id', link.id,
|
|
177
|
+
'label', link.label,
|
|
178
|
+
'url', link.url,
|
|
179
|
+
'kind', link.kind,
|
|
180
|
+
'created_at', link.created_at
|
|
181
|
+
) ORDER BY link.created_at DESC, link.id)
|
|
182
|
+
FROM public.project_links link
|
|
183
|
+
WHERE link.project_id = target_project_id
|
|
184
|
+
AND link.visibility = 'client'
|
|
185
|
+
), '[]'::jsonb)
|
|
186
|
+
FROM public.list_current_client_projects() list_row
|
|
187
|
+
WHERE list_row.project_id = target_project_id;
|
|
188
|
+
$$;
|
|
189
|
+
|
|
190
|
+
REVOKE ALL ON FUNCTION public.get_current_client_project(uuid)
|
|
191
|
+
FROM PUBLIC, anon, service_role;
|
|
192
|
+
GRANT EXECUTE ON FUNCTION public.get_current_client_project(uuid)
|
|
193
|
+
TO authenticated;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
-- bw-migration-safety: destructive
|
|
2
|
+
-- Destructively normalize legacy project identities only after all readers and
|
|
3
|
+
-- writers use the explicit internal-team and client-audience model.
|
|
4
|
+
|
|
5
|
+
DELETE FROM public.project_members member
|
|
6
|
+
WHERE NOT EXISTS (
|
|
7
|
+
SELECT 1
|
|
8
|
+
FROM public.user_role_assignments role_assignment
|
|
9
|
+
WHERE role_assignment.profile_id = member.profile_id
|
|
10
|
+
AND role_assignment.role_code IN ('staff', 'admin')
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
-- The enforcement migration intentionally rejects unauthenticated identity
|
|
14
|
+
-- changes. Temporarily disable only that column guard for this reviewed,
|
|
15
|
+
-- destructive normalization; deferred consistency checks remain active.
|
|
16
|
+
ALTER TABLE public.projects DISABLE TRIGGER trg_protect_project_identity_fields;
|
|
17
|
+
|
|
18
|
+
UPDATE public.projects project
|
|
19
|
+
SET owner_profile_id = NULL
|
|
20
|
+
WHERE owner_profile_id IS NOT NULL
|
|
21
|
+
AND NOT EXISTS (
|
|
22
|
+
SELECT 1
|
|
23
|
+
FROM public.user_role_assignments role_assignment
|
|
24
|
+
WHERE role_assignment.profile_id = project.owner_profile_id
|
|
25
|
+
AND role_assignment.role_code IN ('staff', 'admin')
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
ALTER TABLE public.projects ENABLE TRIGGER trg_protect_project_identity_fields;
|
|
29
|
+
|
|
30
|
+
-- Existing owner_profile_id is canonical during cleanup. Keep exactly its
|
|
31
|
+
-- matching owner membership and demote stale owner rows.
|
|
32
|
+
UPDATE public.project_members member
|
|
33
|
+
SET role = 'contributor'
|
|
34
|
+
FROM public.projects project
|
|
35
|
+
WHERE project.id = member.project_id
|
|
36
|
+
AND member.role = 'owner'
|
|
37
|
+
AND member.profile_id IS DISTINCT FROM project.owner_profile_id;
|
|
38
|
+
|
|
39
|
+
DELETE FROM public.project_members member
|
|
40
|
+
USING public.projects project
|
|
41
|
+
WHERE project.id = member.project_id
|
|
42
|
+
AND project.owner_profile_id IS NULL
|
|
43
|
+
AND member.role = 'owner';
|
|
44
|
+
|
|
45
|
+
INSERT INTO public.project_members (project_id, profile_id, role)
|
|
46
|
+
SELECT project.id, project.owner_profile_id, 'owner'
|
|
47
|
+
FROM public.projects project
|
|
48
|
+
WHERE project.owner_profile_id IS NOT NULL
|
|
49
|
+
ON CONFLICT (project_id, profile_id)
|
|
50
|
+
DO UPDATE SET role = 'owner';
|
|
51
|
+
|
|
52
|
+
CREATE OR REPLACE FUNCTION public.enforce_internal_project_identity()
|
|
53
|
+
RETURNS trigger
|
|
54
|
+
LANGUAGE plpgsql
|
|
55
|
+
SECURITY DEFINER
|
|
56
|
+
SET search_path = public
|
|
57
|
+
AS $$
|
|
58
|
+
DECLARE
|
|
59
|
+
v_profile_id uuid;
|
|
60
|
+
BEGIN
|
|
61
|
+
-- Keep table-specific NEW fields in separate statements. A CASE expression
|
|
62
|
+
-- still asks PostgreSQL to resolve both record fields and fails on projects
|
|
63
|
+
-- because that row type has no profile_id column.
|
|
64
|
+
IF TG_TABLE_NAME = 'projects' THEN
|
|
65
|
+
v_profile_id := NEW.owner_profile_id;
|
|
66
|
+
ELSE
|
|
67
|
+
v_profile_id := NEW.profile_id;
|
|
68
|
+
END IF;
|
|
69
|
+
|
|
70
|
+
IF v_profile_id IS NOT NULL AND NOT EXISTS (
|
|
71
|
+
SELECT 1
|
|
72
|
+
FROM public.user_role_assignments role_assignment
|
|
73
|
+
WHERE role_assignment.profile_id = v_profile_id
|
|
74
|
+
AND role_assignment.role_code IN ('staff', 'admin')
|
|
75
|
+
) THEN
|
|
76
|
+
RAISE EXCEPTION 'Project owners and members must be internal staff.'
|
|
77
|
+
USING ERRCODE = '23514';
|
|
78
|
+
END IF;
|
|
79
|
+
RETURN NEW;
|
|
80
|
+
END;
|
|
81
|
+
$$;
|
|
82
|
+
|
|
83
|
+
DROP TRIGGER IF EXISTS trg_enforce_internal_project_owner ON public.projects;
|
|
84
|
+
CREATE TRIGGER trg_enforce_internal_project_owner
|
|
85
|
+
BEFORE INSERT OR UPDATE OF owner_profile_id ON public.projects
|
|
86
|
+
FOR EACH ROW
|
|
87
|
+
EXECUTE FUNCTION public.enforce_internal_project_identity();
|
|
88
|
+
|
|
89
|
+
DROP TRIGGER IF EXISTS trg_enforce_internal_project_member ON public.project_members;
|
|
90
|
+
CREATE TRIGGER trg_enforce_internal_project_member
|
|
91
|
+
BEFORE INSERT OR UPDATE OF profile_id, role ON public.project_members
|
|
92
|
+
FOR EACH ROW
|
|
93
|
+
EXECUTE FUNCTION public.enforce_internal_project_identity();
|
|
94
|
+
|
|
95
|
+
CREATE OR REPLACE FUNCTION public.validate_project_owner_consistency()
|
|
96
|
+
RETURNS trigger
|
|
97
|
+
LANGUAGE plpgsql
|
|
98
|
+
SECURITY DEFINER
|
|
99
|
+
SET search_path = public
|
|
100
|
+
AS $$
|
|
101
|
+
DECLARE
|
|
102
|
+
v_project_id uuid;
|
|
103
|
+
v_owner_profile_id uuid;
|
|
104
|
+
v_owner_count integer;
|
|
105
|
+
v_matching_owner_count integer;
|
|
106
|
+
BEGIN
|
|
107
|
+
IF TG_TABLE_NAME = 'projects' THEN
|
|
108
|
+
v_project_id := COALESCE(NEW.id, OLD.id);
|
|
109
|
+
ELSE
|
|
110
|
+
v_project_id := COALESCE(NEW.project_id, OLD.project_id);
|
|
111
|
+
END IF;
|
|
112
|
+
|
|
113
|
+
SELECT project.owner_profile_id
|
|
114
|
+
INTO v_owner_profile_id
|
|
115
|
+
FROM public.projects project
|
|
116
|
+
WHERE project.id = v_project_id;
|
|
117
|
+
IF NOT FOUND THEN
|
|
118
|
+
RETURN NULL;
|
|
119
|
+
END IF;
|
|
120
|
+
|
|
121
|
+
SELECT
|
|
122
|
+
count(*) FILTER (WHERE member.role = 'owner'),
|
|
123
|
+
count(*) FILTER (
|
|
124
|
+
WHERE member.role = 'owner'
|
|
125
|
+
AND member.profile_id = v_owner_profile_id
|
|
126
|
+
)
|
|
127
|
+
INTO v_owner_count, v_matching_owner_count
|
|
128
|
+
FROM public.project_members member
|
|
129
|
+
WHERE member.project_id = v_project_id;
|
|
130
|
+
|
|
131
|
+
IF (v_owner_profile_id IS NULL AND v_owner_count <> 0)
|
|
132
|
+
OR (v_owner_profile_id IS NOT NULL AND (v_owner_count <> 1 OR v_matching_owner_count <> 1)) THEN
|
|
133
|
+
RAISE EXCEPTION 'Project owner and owner membership must match exactly.'
|
|
134
|
+
USING ERRCODE = '23514';
|
|
135
|
+
END IF;
|
|
136
|
+
RETURN NULL;
|
|
137
|
+
END;
|
|
138
|
+
$$;
|
|
139
|
+
|
|
140
|
+
DROP TRIGGER IF EXISTS trg_validate_project_owner_from_project ON public.projects;
|
|
141
|
+
CREATE CONSTRAINT TRIGGER trg_validate_project_owner_from_project
|
|
142
|
+
AFTER INSERT OR UPDATE OF owner_profile_id ON public.projects
|
|
143
|
+
DEFERRABLE INITIALLY DEFERRED
|
|
144
|
+
FOR EACH ROW
|
|
145
|
+
EXECUTE FUNCTION public.validate_project_owner_consistency();
|
|
146
|
+
|
|
147
|
+
DROP TRIGGER IF EXISTS trg_validate_project_owner_from_member ON public.project_members;
|
|
148
|
+
CREATE CONSTRAINT TRIGGER trg_validate_project_owner_from_member
|
|
149
|
+
AFTER INSERT OR UPDATE OR DELETE ON public.project_members
|
|
150
|
+
DEFERRABLE INITIALLY DEFERRED
|
|
151
|
+
FOR EACH ROW
|
|
152
|
+
EXECUTE FUNCTION public.validate_project_owner_consistency();
|
|
153
|
+
|
|
154
|
+
CREATE OR REPLACE FUNCTION public.validate_project_primary_participation()
|
|
155
|
+
RETURNS trigger
|
|
156
|
+
LANGUAGE plpgsql
|
|
157
|
+
SECURITY DEFINER
|
|
158
|
+
SET search_path = public
|
|
159
|
+
AS $$
|
|
160
|
+
DECLARE
|
|
161
|
+
v_project_id uuid := COALESCE(NEW.project_id, OLD.project_id);
|
|
162
|
+
v_primary_organization_id uuid;
|
|
163
|
+
v_matching_primary_count integer;
|
|
164
|
+
v_other_primary_count integer;
|
|
165
|
+
BEGIN
|
|
166
|
+
SELECT project.organization_id
|
|
167
|
+
INTO v_primary_organization_id
|
|
168
|
+
FROM public.projects project
|
|
169
|
+
WHERE project.id = v_project_id;
|
|
170
|
+
IF NOT FOUND THEN
|
|
171
|
+
RETURN NULL;
|
|
172
|
+
END IF;
|
|
173
|
+
|
|
174
|
+
SELECT
|
|
175
|
+
count(*) FILTER (
|
|
176
|
+
WHERE participant.organization_id = v_primary_organization_id
|
|
177
|
+
AND participant.is_primary
|
|
178
|
+
),
|
|
179
|
+
count(*) FILTER (
|
|
180
|
+
WHERE participant.is_primary
|
|
181
|
+
AND participant.organization_id <> v_primary_organization_id
|
|
182
|
+
)
|
|
183
|
+
INTO v_matching_primary_count, v_other_primary_count
|
|
184
|
+
FROM public.project_organizations participant
|
|
185
|
+
WHERE participant.project_id = v_project_id;
|
|
186
|
+
|
|
187
|
+
IF v_matching_primary_count <> 1 OR v_other_primary_count <> 0 THEN
|
|
188
|
+
RAISE EXCEPTION 'The project primary organization must be its only primary participant.'
|
|
189
|
+
USING ERRCODE = '23514';
|
|
190
|
+
END IF;
|
|
191
|
+
RETURN NULL;
|
|
192
|
+
END;
|
|
193
|
+
$$;
|
|
194
|
+
|
|
195
|
+
DROP TRIGGER IF EXISTS trg_validate_project_primary_participation
|
|
196
|
+
ON public.project_organizations;
|
|
197
|
+
CREATE CONSTRAINT TRIGGER trg_validate_project_primary_participation
|
|
198
|
+
AFTER INSERT OR UPDATE OR DELETE ON public.project_organizations
|
|
199
|
+
DEFERRABLE INITIALLY DEFERRED
|
|
200
|
+
FOR EACH ROW
|
|
201
|
+
EXECUTE FUNCTION public.validate_project_primary_participation();
|