@brightweblabs/module-orgs 0.2.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/brightweb.module.json +47 -0
- package/package.json +27 -0
- package/src/data.ts +236 -0
- package/src/index.ts +17 -0
- package/src/registration.ts +8 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"contractVersion": 1,
|
|
3
|
+
"key": "orgs",
|
|
4
|
+
"title": "Organizations",
|
|
5
|
+
"description": "Organizations, membership, invitations, and shared organization access helpers.",
|
|
6
|
+
"requires": {
|
|
7
|
+
"core": ">=0.3",
|
|
8
|
+
"admin": ">=0.3"
|
|
9
|
+
},
|
|
10
|
+
"capabilities": {
|
|
11
|
+
"provides": [
|
|
12
|
+
{ "name": "orgs.organizations.read", "since": "0.1.0" },
|
|
13
|
+
{ "name": "orgs.organizations.write", "since": "0.1.0" },
|
|
14
|
+
{ "name": "orgs.membership.read", "since": "0.1.0" },
|
|
15
|
+
{ "name": "orgs.membership.admin", "since": "0.1.0" }
|
|
16
|
+
],
|
|
17
|
+
"consumes": []
|
|
18
|
+
},
|
|
19
|
+
"capabilityExports": {
|
|
20
|
+
"orgs.organizations.read": { "specifier": ".", "symbol": "listOrganizations" },
|
|
21
|
+
"orgs.organizations.write": { "specifier": ".", "symbol": "createOrganization" },
|
|
22
|
+
"orgs.membership.read": { "specifier": ".", "symbol": "listOrganizationMembers" },
|
|
23
|
+
"orgs.membership.admin": { "specifier": ".", "symbol": "setOrganizationMemberRole" }
|
|
24
|
+
},
|
|
25
|
+
"events": { "emits": [], "consumes": [] },
|
|
26
|
+
"permissions": {
|
|
27
|
+
"surfaces": "staff",
|
|
28
|
+
"grants": [
|
|
29
|
+
{ "name": "orgs.manage", "defaultRoles": ["staff", "admin"] }
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
"registration": "./registration",
|
|
33
|
+
"database": {
|
|
34
|
+
"namespace": ["organization"],
|
|
35
|
+
"ownedObjects": [
|
|
36
|
+
"organizations",
|
|
37
|
+
"organization_members",
|
|
38
|
+
"organization_invitations",
|
|
39
|
+
"is_org_admin",
|
|
40
|
+
"is_org_member",
|
|
41
|
+
"touch_organization_invitations_updated_at",
|
|
42
|
+
"trg_touch_organization_invitations_updated_at",
|
|
43
|
+
"normalize_organization_invitation_email",
|
|
44
|
+
"trg_normalize_organization_invitation_email"
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@brightweblabs/module-orgs",
|
|
3
|
+
"private": false,
|
|
4
|
+
"version": "0.2.0",
|
|
5
|
+
"main": "./src/index.ts",
|
|
6
|
+
"types": "./src/index.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"src",
|
|
9
|
+
"brightweb.module.json"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/BWLeoRibeiro/brightweb-platform.git"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"exports": {
|
|
20
|
+
".": "./src/index.ts",
|
|
21
|
+
"./registration": "./src/registration.ts"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@brightweblabs/app-shell": "workspace:*",
|
|
25
|
+
"@supabase/supabase-js": "^2.105.3"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/data.ts
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
+
|
|
3
|
+
export type OrganizationPrimaryContact = {
|
|
4
|
+
id: string;
|
|
5
|
+
first_name: string | null;
|
|
6
|
+
last_name: string | null;
|
|
7
|
+
email: string | null;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type Organization = {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
industry: string | null;
|
|
14
|
+
company_size: string | null;
|
|
15
|
+
budget_range: string | null;
|
|
16
|
+
website_url: string | null;
|
|
17
|
+
address: string | null;
|
|
18
|
+
taxIdentifierValue: string | null;
|
|
19
|
+
primary_contact_id: string | null;
|
|
20
|
+
primary_contact?: OrganizationPrimaryContact | null;
|
|
21
|
+
created_at: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type OrganizationsListParams = {
|
|
25
|
+
page?: number;
|
|
26
|
+
pageSize?: number;
|
|
27
|
+
search?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type OrganizationsListResult = {
|
|
31
|
+
items: Organization[];
|
|
32
|
+
page: number;
|
|
33
|
+
pageSize: number;
|
|
34
|
+
total: number;
|
|
35
|
+
totalPages: number;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type CreateOrganizationInput = {
|
|
39
|
+
name: string;
|
|
40
|
+
industry?: string | null;
|
|
41
|
+
companySize?: string | null;
|
|
42
|
+
budgetRange?: string | null;
|
|
43
|
+
websiteUrl?: string | null;
|
|
44
|
+
address?: string | null;
|
|
45
|
+
taxIdentifierValue?: string | null;
|
|
46
|
+
primaryContactId?: string | null;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type UpdateOrganizationInput = CreateOrganizationInput;
|
|
50
|
+
|
|
51
|
+
export type OrganizationMemberRole = "admin" | "member";
|
|
52
|
+
|
|
53
|
+
export type OrganizationMember = {
|
|
54
|
+
id: string;
|
|
55
|
+
organization_id: string;
|
|
56
|
+
profile_id: string;
|
|
57
|
+
role: OrganizationMemberRole;
|
|
58
|
+
joined_at: string;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
type RawOrganization = {
|
|
62
|
+
id: string;
|
|
63
|
+
name: string;
|
|
64
|
+
industry: string | null;
|
|
65
|
+
company_size: string | null;
|
|
66
|
+
budget_range: string | null;
|
|
67
|
+
website_url: string | null;
|
|
68
|
+
address: string | null;
|
|
69
|
+
tax_identifier_value?: string | null;
|
|
70
|
+
primary_contact_id: string | null;
|
|
71
|
+
primary_contact?: OrganizationPrimaryContact | OrganizationPrimaryContact[] | null;
|
|
72
|
+
created_at: string;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const ORGANIZATIONS_DEFAULT_PAGE_SIZE = 20;
|
|
76
|
+
export const ORGANIZATIONS_MAX_PAGE_SIZE = 100;
|
|
77
|
+
|
|
78
|
+
function normalizePage(page: number | undefined, fallback: number) {
|
|
79
|
+
return Number.isFinite(page) && (page ?? 0) > 0 ? Math.floor(page as number) : fallback;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizePageSize(pageSize: number | undefined, fallback: number, max: number) {
|
|
83
|
+
const normalized = Number.isFinite(pageSize) && (pageSize ?? 0) > 0 ? Math.floor(pageSize as number) : fallback;
|
|
84
|
+
return Math.min(normalized, max);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function normalizeOrganization(raw: RawOrganization): Organization {
|
|
88
|
+
const primaryContact = raw.primary_contact;
|
|
89
|
+
return {
|
|
90
|
+
id: raw.id,
|
|
91
|
+
name: raw.name,
|
|
92
|
+
industry: raw.industry,
|
|
93
|
+
company_size: raw.company_size,
|
|
94
|
+
budget_range: raw.budget_range,
|
|
95
|
+
website_url: raw.website_url,
|
|
96
|
+
address: raw.address,
|
|
97
|
+
taxIdentifierValue: typeof raw.tax_identifier_value === "string" ? raw.tax_identifier_value : null,
|
|
98
|
+
primary_contact_id: raw.primary_contact_id,
|
|
99
|
+
primary_contact: Array.isArray(primaryContact) ? primaryContact[0] ?? null : primaryContact ?? null,
|
|
100
|
+
created_at: raw.created_at,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function listOrganizations(
|
|
105
|
+
supabase: SupabaseClient,
|
|
106
|
+
params: OrganizationsListParams = {},
|
|
107
|
+
): Promise<OrganizationsListResult> {
|
|
108
|
+
const page = normalizePage(params.page, 1);
|
|
109
|
+
const pageSize = normalizePageSize(params.pageSize, ORGANIZATIONS_DEFAULT_PAGE_SIZE, ORGANIZATIONS_MAX_PAGE_SIZE);
|
|
110
|
+
const from = (page - 1) * pageSize;
|
|
111
|
+
const to = from + pageSize - 1;
|
|
112
|
+
const search = params.search?.trim() ?? "";
|
|
113
|
+
|
|
114
|
+
let query = supabase
|
|
115
|
+
.from("organizations")
|
|
116
|
+
.select(
|
|
117
|
+
"id, name, industry, company_size, budget_range, website_url, address, tax_identifier_value, primary_contact_id, created_at, primary_contact:profiles!organizations_primary_contact_id_fkey(id, first_name, last_name, email)",
|
|
118
|
+
{ count: "exact" },
|
|
119
|
+
)
|
|
120
|
+
.order("created_at", { ascending: false })
|
|
121
|
+
.range(from, to);
|
|
122
|
+
|
|
123
|
+
if (search) {
|
|
124
|
+
const safe = search.replace(/[%_,()\"]/g, "");
|
|
125
|
+
query = query.ilike("name", `%${safe}%`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const { data, error, count } = await query;
|
|
129
|
+
if (error) throw new Error(error.message);
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
items: ((data ?? []) as RawOrganization[]).map(normalizeOrganization),
|
|
133
|
+
page,
|
|
134
|
+
pageSize,
|
|
135
|
+
total: count ?? 0,
|
|
136
|
+
totalPages: Math.max(1, Math.ceil((count ?? 0) / pageSize)),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function createOrganization(
|
|
141
|
+
supabase: SupabaseClient,
|
|
142
|
+
input: CreateOrganizationInput,
|
|
143
|
+
): Promise<Organization> {
|
|
144
|
+
const name = input.name.trim();
|
|
145
|
+
if (!name) throw new Error("Organization name is required.");
|
|
146
|
+
|
|
147
|
+
const { data, error } = await supabase
|
|
148
|
+
.from("organizations")
|
|
149
|
+
.insert({
|
|
150
|
+
name,
|
|
151
|
+
industry: input.industry?.trim() || null,
|
|
152
|
+
company_size: input.companySize?.trim() || null,
|
|
153
|
+
budget_range: input.budgetRange?.trim() || null,
|
|
154
|
+
website_url: input.websiteUrl?.trim() || null,
|
|
155
|
+
address: input.address?.trim() || null,
|
|
156
|
+
tax_identifier_value: input.taxIdentifierValue?.trim() || null,
|
|
157
|
+
primary_contact_id: input.primaryContactId || null,
|
|
158
|
+
})
|
|
159
|
+
.select(
|
|
160
|
+
"id, name, industry, company_size, budget_range, website_url, address, tax_identifier_value, primary_contact_id, created_at, primary_contact:profiles!organizations_primary_contact_id_fkey(id, first_name, last_name, email)",
|
|
161
|
+
)
|
|
162
|
+
.single();
|
|
163
|
+
|
|
164
|
+
if (error) throw new Error(error.message);
|
|
165
|
+
return normalizeOrganization(data as RawOrganization);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function updateOrganization(
|
|
169
|
+
supabase: SupabaseClient,
|
|
170
|
+
organizationId: string,
|
|
171
|
+
input: UpdateOrganizationInput,
|
|
172
|
+
): Promise<Organization> {
|
|
173
|
+
const name = input.name.trim();
|
|
174
|
+
if (!name) throw new Error("Organization name is required.");
|
|
175
|
+
|
|
176
|
+
const { data, error } = await supabase
|
|
177
|
+
.from("organizations")
|
|
178
|
+
.update({
|
|
179
|
+
name,
|
|
180
|
+
industry: input.industry?.trim() || null,
|
|
181
|
+
company_size: input.companySize?.trim() || null,
|
|
182
|
+
budget_range: input.budgetRange?.trim() || null,
|
|
183
|
+
website_url: input.websiteUrl?.trim() || null,
|
|
184
|
+
address: input.address?.trim() || null,
|
|
185
|
+
tax_identifier_value: input.taxIdentifierValue?.trim() || null,
|
|
186
|
+
primary_contact_id: input.primaryContactId || null,
|
|
187
|
+
})
|
|
188
|
+
.eq("id", organizationId)
|
|
189
|
+
.select(
|
|
190
|
+
"id, name, industry, company_size, budget_range, website_url, address, tax_identifier_value, primary_contact_id, created_at, primary_contact:profiles!organizations_primary_contact_id_fkey(id, first_name, last_name, email)",
|
|
191
|
+
)
|
|
192
|
+
.single();
|
|
193
|
+
|
|
194
|
+
if (error) throw new Error(error.message);
|
|
195
|
+
return normalizeOrganization(data as RawOrganization);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function listOrganizationMembers(
|
|
199
|
+
supabase: SupabaseClient,
|
|
200
|
+
organizationId: string,
|
|
201
|
+
): Promise<OrganizationMember[]> {
|
|
202
|
+
const { data, error } = await supabase
|
|
203
|
+
.from("organization_members")
|
|
204
|
+
.select("id, organization_id, profile_id, role, joined_at")
|
|
205
|
+
.eq("organization_id", organizationId)
|
|
206
|
+
.order("joined_at", { ascending: true });
|
|
207
|
+
|
|
208
|
+
if (error) throw new Error(error.message);
|
|
209
|
+
return (data ?? []).filter(
|
|
210
|
+
(member): member is OrganizationMember => member.role === "admin" || member.role === "member",
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export async function setOrganizationMemberRole(
|
|
215
|
+
supabase: SupabaseClient,
|
|
216
|
+
organizationId: string,
|
|
217
|
+
profileId: string,
|
|
218
|
+
role: OrganizationMemberRole,
|
|
219
|
+
): Promise<OrganizationMember> {
|
|
220
|
+
const { data, error } = await supabase
|
|
221
|
+
.from("organization_members")
|
|
222
|
+
.upsert(
|
|
223
|
+
{
|
|
224
|
+
organization_id: organizationId,
|
|
225
|
+
profile_id: profileId,
|
|
226
|
+
role,
|
|
227
|
+
},
|
|
228
|
+
{ onConflict: "organization_id,profile_id" },
|
|
229
|
+
)
|
|
230
|
+
.select("id, organization_id, profile_id, role, joined_at")
|
|
231
|
+
.single();
|
|
232
|
+
|
|
233
|
+
if (error) throw new Error(error.message);
|
|
234
|
+
if (data.role !== "admin" && data.role !== "member") throw new Error("Invalid organization member role.");
|
|
235
|
+
return data as OrganizationMember;
|
|
236
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export {
|
|
2
|
+
ORGANIZATIONS_DEFAULT_PAGE_SIZE,
|
|
3
|
+
ORGANIZATIONS_MAX_PAGE_SIZE,
|
|
4
|
+
createOrganization,
|
|
5
|
+
listOrganizationMembers,
|
|
6
|
+
listOrganizations,
|
|
7
|
+
setOrganizationMemberRole,
|
|
8
|
+
updateOrganization,
|
|
9
|
+
type CreateOrganizationInput,
|
|
10
|
+
type Organization,
|
|
11
|
+
type OrganizationMember,
|
|
12
|
+
type OrganizationMemberRole,
|
|
13
|
+
type OrganizationPrimaryContact,
|
|
14
|
+
type OrganizationsListParams,
|
|
15
|
+
type OrganizationsListResult,
|
|
16
|
+
type UpdateOrganizationInput,
|
|
17
|
+
} from "./data";
|