@lcas58/esmi-api-types 1.0.5 → 1.0.7
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/dist/src/app.d.ts +1158 -0
- package/dist/src/app.js +22 -0
- package/dist/src/routes/events/events.handlers.d.ts +20 -0
- package/dist/src/routes/events/events.handlers.js +177 -0
- package/dist/src/routes/events/events.index.d.ts +501 -0
- package/dist/src/routes/events/events.index.js +9 -0
- package/dist/src/routes/events/events.routes.d.ts +1375 -0
- package/dist/src/routes/events/events.routes.js +72 -0
- package/dist/src/routes/events/schemas/event.schemas.d.ts +896 -0
- package/dist/src/routes/events/schemas/event.schemas.js +49 -0
- package/dist/src/routes/events/schemas/index.d.ts +1 -0
- package/dist/src/routes/events/schemas/index.js +1 -0
- package/dist/src/routes/index.route.d.ts +13 -0
- package/dist/src/routes/index.route.js +19 -0
- package/dist/src/routes/leagues/leagues.handlers.d.ts +3 -0
- package/dist/src/routes/leagues/leagues.handlers.js +49 -0
- package/dist/src/routes/leagues/leagues.index.d.ts +53 -0
- package/dist/src/routes/leagues/leagues.index.js +6 -0
- package/dist/src/routes/leagues/leagues.routes.d.ts +137 -0
- package/dist/src/routes/leagues/leagues.routes.js +47 -0
- package/dist/src/routes/marketing/marketing.handlers.d.ts +4 -0
- package/dist/src/routes/marketing/marketing.handlers.js +20 -0
- package/dist/src/routes/marketing/marketing.index.d.ts +58 -0
- package/dist/src/routes/marketing/marketing.index.js +7 -0
- package/dist/src/routes/marketing/marketing.routes.d.ts +154 -0
- package/dist/src/routes/marketing/marketing.routes.js +27 -0
- package/dist/src/routes/organizations/organizations.handlers.d.ts +74 -0
- package/dist/src/routes/organizations/organizations.handlers.js +485 -0
- package/dist/src/routes/organizations/organizations.index.d.ts +517 -0
- package/dist/src/routes/organizations/organizations.index.js +12 -0
- package/dist/src/routes/organizations/organizations.routes.d.ts +1236 -0
- package/dist/src/routes/organizations/organizations.routes.js +137 -0
- package/dist/src/routes/organizations/tasks.test.d.ts +0 -0
- package/dist/src/routes/organizations/tasks.test.js +181 -0
- package/dist/src/routes/tags/tags.handlers.d.ts +3 -0
- package/dist/src/routes/tags/tags.handlers.js +15 -0
- package/dist/src/routes/tags/tags.index.d.ts +24 -0
- package/dist/src/routes/tags/tags.index.js +6 -0
- package/dist/src/routes/tags/tags.routes.d.ts +68 -0
- package/dist/src/routes/tags/tags.routes.js +25 -0
- package/dist/src/shared/client-types.d.ts +8 -3
- package/dist/src/shared/client-types.js +1 -1
- package/package.json +4 -2
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { AppRouteHandler } from "../../lib/types.js";
|
|
2
|
+
import type { CheckOrganizationNameRoute, CheckOwnershipRoute, CreateRoute, GetOneRoute, ListRoute, PatchRoute, RemoveRoute } from "./organizations.routes.js";
|
|
3
|
+
/**
|
|
4
|
+
* Retrieves a single organization by ID
|
|
5
|
+
*
|
|
6
|
+
* @description Fetches an organization with all related data (tags, websites, social media).
|
|
7
|
+
* Only the organization owner can access the full details.
|
|
8
|
+
*
|
|
9
|
+
* @param c - Hono context containing request data and environment
|
|
10
|
+
* @returns Organization data with related entities or error response
|
|
11
|
+
*/
|
|
12
|
+
export declare const getOne: AppRouteHandler<GetOneRoute>;
|
|
13
|
+
/**
|
|
14
|
+
* Lists organizations with optional filtering
|
|
15
|
+
*
|
|
16
|
+
* @description Retrieves a list of organizations with support for filtering by:
|
|
17
|
+
* - ownerId: Filter by organization owner
|
|
18
|
+
* - city, state, country: Filter by location (case-insensitive partial match)
|
|
19
|
+
*
|
|
20
|
+
* @param c - Hono context containing query parameters
|
|
21
|
+
* @returns Array of organizations with related data
|
|
22
|
+
*/
|
|
23
|
+
export declare const list: AppRouteHandler<ListRoute>;
|
|
24
|
+
/**
|
|
25
|
+
* Creates a new organization with related data
|
|
26
|
+
*
|
|
27
|
+
* @description Creates an organization along with optional tags, websites, and social media.
|
|
28
|
+
* Uses a database transaction to ensure data consistency.
|
|
29
|
+
*
|
|
30
|
+
* @param c - Hono context containing organization data and authenticated user
|
|
31
|
+
* @returns Created organization data or error response
|
|
32
|
+
*/
|
|
33
|
+
export declare const create: AppRouteHandler<CreateRoute>;
|
|
34
|
+
/**
|
|
35
|
+
* Updates an existing organization and its related data
|
|
36
|
+
*
|
|
37
|
+
* @description Performs partial updates to an organization including related entities.
|
|
38
|
+
* Uses replace strategy for tags, websites, and social media (delete all, insert new).
|
|
39
|
+
*
|
|
40
|
+
* @param c - Hono context containing organization ID and update data
|
|
41
|
+
* @returns Updated organization data or error response
|
|
42
|
+
*/
|
|
43
|
+
export declare const patch: AppRouteHandler<PatchRoute>;
|
|
44
|
+
/**
|
|
45
|
+
* Deletes an organization and all related data
|
|
46
|
+
*
|
|
47
|
+
* @description Removes an organization from the database. Due to CASCADE constraints,
|
|
48
|
+
* all related data (websites, social media, tags) will be automatically deleted.
|
|
49
|
+
*
|
|
50
|
+
* @param c - Hono context containing organization ID
|
|
51
|
+
* @returns 204 No Content on success or 404 Not Found if organization doesn't exist
|
|
52
|
+
*/
|
|
53
|
+
export declare const remove: AppRouteHandler<RemoveRoute>;
|
|
54
|
+
/**
|
|
55
|
+
* Checks if the authenticated user owns the specified organization
|
|
56
|
+
*
|
|
57
|
+
* @description Verifies ownership without returning organization data.
|
|
58
|
+
* Used for authorization checks in the frontend.
|
|
59
|
+
*
|
|
60
|
+
* @param c - Hono context containing organization ID and authenticated user
|
|
61
|
+
* @returns Ownership status (true/false) or 404 if organization doesn't exist
|
|
62
|
+
*/
|
|
63
|
+
export declare const checkOwnership: AppRouteHandler<CheckOwnershipRoute>;
|
|
64
|
+
/**
|
|
65
|
+
* Checks if an organization name is available
|
|
66
|
+
*
|
|
67
|
+
* @description Verifies if the provided name is unique in the database.
|
|
68
|
+
* When taken, returns similar available slugs using pg_trgm similarity.
|
|
69
|
+
* Used for name validation in the frontend.
|
|
70
|
+
*
|
|
71
|
+
* @param c - Hono context containing organization name
|
|
72
|
+
* @returns Availability status and suggestions if taken
|
|
73
|
+
*/
|
|
74
|
+
export declare const checkOrganizationName: AppRouteHandler<CheckOrganizationNameRoute>;
|
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
import { and, eq, ilike, sql } from "drizzle-orm";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import * as HttpStatusCodes from "stoker/http-status-codes";
|
|
4
|
+
import * as HttpStatusPhrases from "stoker/http-status-phrases";
|
|
5
|
+
import { closePool, createDb, createPool } from "../../db/index.js";
|
|
6
|
+
import { generateSocialMediaUrl, organizationSocialMedia, organizationTags, organizationWebsites } from "../../db/schema/index.js";
|
|
7
|
+
import organization from "../../db/schema/organization.js";
|
|
8
|
+
import { processAvatarUpdate } from "../../lib/avatar-helpers.js";
|
|
9
|
+
import { ZOD_ERROR_CODES, ZOD_ERROR_MESSAGES } from "../../lib/constants.js";
|
|
10
|
+
import { slugify } from "../../lib/helpers.js";
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// HELPER FUNCTIONS
|
|
13
|
+
// =============================================================================
|
|
14
|
+
/**
|
|
15
|
+
* Transforms raw organization data from database to API response format
|
|
16
|
+
*
|
|
17
|
+
* @param organizationData - Raw organization data from database query
|
|
18
|
+
* @returns Transformed organization data for API response
|
|
19
|
+
*/
|
|
20
|
+
function transformOrganizationData(organizationData) {
|
|
21
|
+
return {
|
|
22
|
+
...organizationData,
|
|
23
|
+
tags: organizationData.tags.map((t) => ({
|
|
24
|
+
id: t.tagId,
|
|
25
|
+
name: t.tag.name,
|
|
26
|
+
})),
|
|
27
|
+
websites: organizationData.websites.map((w) => ({
|
|
28
|
+
id: w.id,
|
|
29
|
+
url: w.url,
|
|
30
|
+
label: w.label || "",
|
|
31
|
+
})),
|
|
32
|
+
socialMedia: organizationData.socialMedia.map((s) => ({
|
|
33
|
+
id: s.id,
|
|
34
|
+
platform: s.platform,
|
|
35
|
+
url: s.url || "",
|
|
36
|
+
handle: s.handle,
|
|
37
|
+
})),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Builds location-based filters for organization queries
|
|
42
|
+
*
|
|
43
|
+
* @param city - Optional city filter (case-insensitive partial match)
|
|
44
|
+
* @param state - Optional state filter (case-insensitive partial match)
|
|
45
|
+
* @param country - Optional country filter (case-insensitive partial match)
|
|
46
|
+
* @returns Array of SQL filter conditions
|
|
47
|
+
*/
|
|
48
|
+
function buildLocationFilters(city, state, country) {
|
|
49
|
+
const filters = [];
|
|
50
|
+
const trimmedCity = city?.trim();
|
|
51
|
+
const trimmedState = state?.trim();
|
|
52
|
+
const trimmedCountry = country?.trim();
|
|
53
|
+
if (trimmedCity)
|
|
54
|
+
filters.push(ilike(organization.city, trimmedCity));
|
|
55
|
+
if (trimmedState)
|
|
56
|
+
filters.push(ilike(organization.state, trimmedState));
|
|
57
|
+
if (trimmedCountry)
|
|
58
|
+
filters.push(ilike(organization.country, trimmedCountry));
|
|
59
|
+
return filters;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Creates a where clause from an array of SQL conditions
|
|
63
|
+
*
|
|
64
|
+
* @param filters - Array of SQL filter conditions
|
|
65
|
+
* @returns Combined where clause or undefined if no filters
|
|
66
|
+
*/
|
|
67
|
+
function createWhereClause(filters) {
|
|
68
|
+
if (filters.length === 0)
|
|
69
|
+
return undefined;
|
|
70
|
+
if (filters.length === 1)
|
|
71
|
+
return filters[0];
|
|
72
|
+
return and(...filters);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Validates and trims location fields from update data
|
|
76
|
+
*
|
|
77
|
+
* @param updateData - Object containing update fields
|
|
78
|
+
* @returns Processed update data with trimmed location fields
|
|
79
|
+
*/
|
|
80
|
+
function processLocationUpdates(updateData) {
|
|
81
|
+
if (typeof updateData.city === "string")
|
|
82
|
+
updateData.city = updateData.city.trim();
|
|
83
|
+
if (typeof updateData.state === "string")
|
|
84
|
+
updateData.state = updateData.state.trim() || null;
|
|
85
|
+
if (typeof updateData.shortStateCd === "string")
|
|
86
|
+
updateData.shortStateCd = updateData.shortStateCd.trim() || null;
|
|
87
|
+
if (typeof updateData.country === "string")
|
|
88
|
+
updateData.country = updateData.country.trim();
|
|
89
|
+
return updateData;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Creates organization tags in a database transaction
|
|
93
|
+
*
|
|
94
|
+
* @param tx - Database transaction instance
|
|
95
|
+
* @param organizationId - ID of the organization
|
|
96
|
+
* @param tags - Array of tag data to create
|
|
97
|
+
*/
|
|
98
|
+
async function createOrganizationTags(tx, organizationId, tags) {
|
|
99
|
+
if (tags && tags.length > 0) {
|
|
100
|
+
await tx.insert(organizationTags).values(tags.map(tag => ({
|
|
101
|
+
organizationId,
|
|
102
|
+
tagId: tag.tagId,
|
|
103
|
+
})));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Creates organization websites in a database transaction
|
|
108
|
+
*
|
|
109
|
+
* @param tx - Database transaction instance
|
|
110
|
+
* @param organizationId - ID of the organization
|
|
111
|
+
* @param websites - Array of website data to create
|
|
112
|
+
*/
|
|
113
|
+
async function createOrganizationWebsites(tx, organizationId, websites) {
|
|
114
|
+
if (websites && websites.length > 0) {
|
|
115
|
+
await tx.insert(organizationWebsites).values(websites.map(website => ({
|
|
116
|
+
...website,
|
|
117
|
+
organizationId,
|
|
118
|
+
})));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Creates organization social media in a database transaction
|
|
123
|
+
*
|
|
124
|
+
* @param tx - Database transaction instance
|
|
125
|
+
* @param organizationId - ID of the organization
|
|
126
|
+
* @param socialMedia - Array of social media data to create
|
|
127
|
+
*/
|
|
128
|
+
async function createOrganizationSocialMedia(tx, organizationId, socialMedia) {
|
|
129
|
+
if (socialMedia && socialMedia.length > 0) {
|
|
130
|
+
await tx.insert(organizationSocialMedia).values(socialMedia.map(social => ({
|
|
131
|
+
...social,
|
|
132
|
+
organizationId,
|
|
133
|
+
// Generate URL dynamically if not provided
|
|
134
|
+
url: social.url || generateSocialMediaUrl(social.platform, social.handle),
|
|
135
|
+
})));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// =============================================================================
|
|
139
|
+
// ORGANIZATION HANDLERS
|
|
140
|
+
// =============================================================================
|
|
141
|
+
/**
|
|
142
|
+
* Retrieves a single organization by ID
|
|
143
|
+
*
|
|
144
|
+
* @description Fetches an organization with all related data (tags, websites, social media).
|
|
145
|
+
* Only the organization owner can access the full details.
|
|
146
|
+
*
|
|
147
|
+
* @param c - Hono context containing request data and environment
|
|
148
|
+
* @returns Organization data with related entities or error response
|
|
149
|
+
*/
|
|
150
|
+
export const getOne = async (c) => {
|
|
151
|
+
const { db } = createDb(c.env);
|
|
152
|
+
const { id } = c.req.valid("param");
|
|
153
|
+
const authUser = c.get("user");
|
|
154
|
+
const organizationData = await db.query.organization.findFirst({
|
|
155
|
+
where: eq(organization.id, id),
|
|
156
|
+
with: {
|
|
157
|
+
tags: {
|
|
158
|
+
with: {
|
|
159
|
+
tag: true,
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
websites: true,
|
|
163
|
+
socialMedia: true,
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
if (!organizationData) {
|
|
167
|
+
return c.json({
|
|
168
|
+
message: HttpStatusPhrases.NOT_FOUND,
|
|
169
|
+
}, HttpStatusCodes.NOT_FOUND);
|
|
170
|
+
}
|
|
171
|
+
// Check if the user owns this organization
|
|
172
|
+
if (organizationData.ownerId !== authUser.id) {
|
|
173
|
+
return c.json({
|
|
174
|
+
message: "Unauthorized",
|
|
175
|
+
}, HttpStatusCodes.UNAUTHORIZED);
|
|
176
|
+
}
|
|
177
|
+
const transformedData = transformOrganizationData(organizationData);
|
|
178
|
+
return c.json(transformedData, HttpStatusCodes.OK);
|
|
179
|
+
};
|
|
180
|
+
/**
|
|
181
|
+
* Lists organizations with optional filtering
|
|
182
|
+
*
|
|
183
|
+
* @description Retrieves a list of organizations with support for filtering by:
|
|
184
|
+
* - ownerId: Filter by organization owner
|
|
185
|
+
* - city, state, country: Filter by location (case-insensitive partial match)
|
|
186
|
+
*
|
|
187
|
+
* @param c - Hono context containing query parameters
|
|
188
|
+
* @returns Array of organizations with related data
|
|
189
|
+
*/
|
|
190
|
+
export const list = async (c) => {
|
|
191
|
+
const { db } = createDb(c.env);
|
|
192
|
+
const { ownerId, city, state, country } = c.req.valid("query");
|
|
193
|
+
const filters = [];
|
|
194
|
+
// Add owner filter if specified
|
|
195
|
+
if (ownerId)
|
|
196
|
+
filters.push(eq(organization.ownerId, ownerId));
|
|
197
|
+
// Add location filters
|
|
198
|
+
const locationFilters = buildLocationFilters(city, state, country);
|
|
199
|
+
filters.push(...locationFilters);
|
|
200
|
+
const whereClause = createWhereClause(filters);
|
|
201
|
+
const organizations = await db.query.organization.findMany({
|
|
202
|
+
where: whereClause,
|
|
203
|
+
with: {
|
|
204
|
+
tags: {
|
|
205
|
+
with: {
|
|
206
|
+
tag: true,
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
websites: true,
|
|
210
|
+
socialMedia: true,
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
return c.json(organizations, HttpStatusCodes.OK);
|
|
214
|
+
};
|
|
215
|
+
/**
|
|
216
|
+
* Creates a new organization with related data
|
|
217
|
+
*
|
|
218
|
+
* @description Creates an organization along with optional tags, websites, and social media.
|
|
219
|
+
* Uses a database transaction to ensure data consistency.
|
|
220
|
+
*
|
|
221
|
+
* @param c - Hono context containing organization data and authenticated user
|
|
222
|
+
* @returns Created organization data or error response
|
|
223
|
+
*/
|
|
224
|
+
export const create = async (c) => {
|
|
225
|
+
const { pool, dbPool } = createPool(c.env);
|
|
226
|
+
const data = c.req.valid("json");
|
|
227
|
+
const user = c.get("user");
|
|
228
|
+
// Process avatar if provided
|
|
229
|
+
let processedAvatar;
|
|
230
|
+
if (data.avatar) {
|
|
231
|
+
// We'll generate a temporary ID for the organization to use in avatar processing
|
|
232
|
+
const tempOrgId = randomUUID();
|
|
233
|
+
const avatarResult = await processAvatarUpdate(data.avatar, tempOrgId, undefined, c.env);
|
|
234
|
+
if (!avatarResult.success) {
|
|
235
|
+
return c.json({
|
|
236
|
+
success: false,
|
|
237
|
+
error: {
|
|
238
|
+
issues: [
|
|
239
|
+
{
|
|
240
|
+
code: "invalid_type",
|
|
241
|
+
path: ["avatar"],
|
|
242
|
+
message: avatarResult.error || "Invalid avatar",
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
name: "ZodError",
|
|
246
|
+
},
|
|
247
|
+
}, HttpStatusCodes.UNPROCESSABLE_ENTITY);
|
|
248
|
+
}
|
|
249
|
+
processedAvatar = avatarResult.avatarValue;
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
// Use transaction to ensure all related data is created together
|
|
253
|
+
const result = await dbPool.transaction(async (tx) => {
|
|
254
|
+
// Create organization with trimmed location data
|
|
255
|
+
const [insertedOrg] = await tx.insert(organization).values({
|
|
256
|
+
name: data.name.trim(),
|
|
257
|
+
description: data.description.trim(),
|
|
258
|
+
ownerId: user.id,
|
|
259
|
+
avatar: processedAvatar,
|
|
260
|
+
slug: slugify(data.name.trim()),
|
|
261
|
+
city: data.city?.trim(),
|
|
262
|
+
state: data.state?.trim(),
|
|
263
|
+
stateCd: data.stateCd?.trim(),
|
|
264
|
+
country: data.country?.trim(),
|
|
265
|
+
}).returning();
|
|
266
|
+
// Create related data using helper functions
|
|
267
|
+
await createOrganizationTags(tx, insertedOrg.id, data.tags);
|
|
268
|
+
await createOrganizationWebsites(tx, insertedOrg.id, data.websites);
|
|
269
|
+
await createOrganizationSocialMedia(tx, insertedOrg.id, data.socialMedia);
|
|
270
|
+
return insertedOrg;
|
|
271
|
+
});
|
|
272
|
+
return c.json(result, HttpStatusCodes.CREATED);
|
|
273
|
+
}
|
|
274
|
+
finally {
|
|
275
|
+
await closePool(pool);
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
/**
|
|
279
|
+
* Updates an existing organization and its related data
|
|
280
|
+
*
|
|
281
|
+
* @description Performs partial updates to an organization including related entities.
|
|
282
|
+
* Uses replace strategy for tags, websites, and social media (delete all, insert new).
|
|
283
|
+
*
|
|
284
|
+
* @param c - Hono context containing organization ID and update data
|
|
285
|
+
* @returns Updated organization data or error response
|
|
286
|
+
*/
|
|
287
|
+
export const patch = async (c) => {
|
|
288
|
+
const { db } = createDb(c.env);
|
|
289
|
+
const { id } = c.req.valid("param");
|
|
290
|
+
const updates = c.req.valid("json");
|
|
291
|
+
// Validate that updates are provided
|
|
292
|
+
if (Object.keys(updates).length === 0) {
|
|
293
|
+
return c.json({
|
|
294
|
+
success: false,
|
|
295
|
+
error: {
|
|
296
|
+
issues: [
|
|
297
|
+
{
|
|
298
|
+
code: ZOD_ERROR_CODES.INVALID_UPDATES,
|
|
299
|
+
path: [],
|
|
300
|
+
message: ZOD_ERROR_MESSAGES.NO_UPDATES,
|
|
301
|
+
},
|
|
302
|
+
],
|
|
303
|
+
name: "ZodError",
|
|
304
|
+
},
|
|
305
|
+
}, HttpStatusCodes.UNPROCESSABLE_ENTITY);
|
|
306
|
+
}
|
|
307
|
+
const { tags, websites, socialMedia, avatar, ...rest } = updates;
|
|
308
|
+
// If avatar is being updated, fetch current org to get old avatar URL
|
|
309
|
+
let processedAvatar;
|
|
310
|
+
if (avatar !== undefined) {
|
|
311
|
+
const currentOrg = await db.query.organization.findFirst({
|
|
312
|
+
where: eq(organization.id, id),
|
|
313
|
+
columns: {
|
|
314
|
+
avatar: true,
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
if (!currentOrg) {
|
|
318
|
+
return c.json({
|
|
319
|
+
message: HttpStatusPhrases.NOT_FOUND,
|
|
320
|
+
}, HttpStatusCodes.NOT_FOUND);
|
|
321
|
+
}
|
|
322
|
+
// Handle avatar removal (null means remove)
|
|
323
|
+
if (avatar === null) {
|
|
324
|
+
// Delete old R2 image if exists
|
|
325
|
+
if (currentOrg.avatar) {
|
|
326
|
+
const { deleteFromR2 } = await import("../../lib/avatar-helpers.js");
|
|
327
|
+
await deleteFromR2(currentOrg.avatar, c.env);
|
|
328
|
+
}
|
|
329
|
+
processedAvatar = null;
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
// Process avatar upload/update
|
|
333
|
+
const avatarResult = await processAvatarUpdate(avatar, id, currentOrg.avatar ?? undefined, c.env);
|
|
334
|
+
if (!avatarResult.success) {
|
|
335
|
+
return c.json({
|
|
336
|
+
success: false,
|
|
337
|
+
error: {
|
|
338
|
+
issues: [
|
|
339
|
+
{
|
|
340
|
+
code: "invalid_type",
|
|
341
|
+
path: ["avatar"],
|
|
342
|
+
message: avatarResult.error || "Invalid avatar",
|
|
343
|
+
},
|
|
344
|
+
],
|
|
345
|
+
name: "ZodError",
|
|
346
|
+
},
|
|
347
|
+
}, HttpStatusCodes.UNPROCESSABLE_ENTITY);
|
|
348
|
+
}
|
|
349
|
+
processedAvatar = avatarResult.avatarValue;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
const updateData = processLocationUpdates({
|
|
353
|
+
...rest,
|
|
354
|
+
slug: slugify(rest.name?.trim() || ""),
|
|
355
|
+
...(processedAvatar !== undefined && { avatar: processedAvatar }),
|
|
356
|
+
});
|
|
357
|
+
const [updatedOrganization] = await db.update(organization)
|
|
358
|
+
.set(updateData)
|
|
359
|
+
.where(eq(organization.id, id))
|
|
360
|
+
.returning();
|
|
361
|
+
if (!updatedOrganization) {
|
|
362
|
+
return c.json({
|
|
363
|
+
message: HttpStatusPhrases.NOT_FOUND,
|
|
364
|
+
}, HttpStatusCodes.NOT_FOUND);
|
|
365
|
+
}
|
|
366
|
+
// Handle related data updates using replace strategy
|
|
367
|
+
if (tags !== undefined) {
|
|
368
|
+
await db.delete(organizationTags)
|
|
369
|
+
.where(eq(organizationTags.organizationId, id));
|
|
370
|
+
await createOrganizationTags(db, id, tags);
|
|
371
|
+
}
|
|
372
|
+
if (websites !== undefined) {
|
|
373
|
+
await db.delete(organizationWebsites)
|
|
374
|
+
.where(eq(organizationWebsites.organizationId, id));
|
|
375
|
+
await createOrganizationWebsites(db, id, websites);
|
|
376
|
+
}
|
|
377
|
+
if (socialMedia !== undefined) {
|
|
378
|
+
await db.delete(organizationSocialMedia)
|
|
379
|
+
.where(eq(organizationSocialMedia.organizationId, id));
|
|
380
|
+
await createOrganizationSocialMedia(db, id, socialMedia);
|
|
381
|
+
}
|
|
382
|
+
return c.json(updatedOrganization, HttpStatusCodes.OK);
|
|
383
|
+
};
|
|
384
|
+
/**
|
|
385
|
+
* Deletes an organization and all related data
|
|
386
|
+
*
|
|
387
|
+
* @description Removes an organization from the database. Due to CASCADE constraints,
|
|
388
|
+
* all related data (websites, social media, tags) will be automatically deleted.
|
|
389
|
+
*
|
|
390
|
+
* @param c - Hono context containing organization ID
|
|
391
|
+
* @returns 204 No Content on success or 404 Not Found if organization doesn't exist
|
|
392
|
+
*/
|
|
393
|
+
export const remove = async (c) => {
|
|
394
|
+
const { db } = createDb(c.env);
|
|
395
|
+
const { id } = c.req.valid("param");
|
|
396
|
+
const result = await db.delete(organization).where(eq(organization.id, id));
|
|
397
|
+
if (result.rowCount === 0) {
|
|
398
|
+
return c.json({
|
|
399
|
+
message: HttpStatusPhrases.NOT_FOUND,
|
|
400
|
+
}, HttpStatusCodes.NOT_FOUND);
|
|
401
|
+
}
|
|
402
|
+
return c.body(null, HttpStatusCodes.NO_CONTENT);
|
|
403
|
+
};
|
|
404
|
+
/**
|
|
405
|
+
* Checks if the authenticated user owns the specified organization
|
|
406
|
+
*
|
|
407
|
+
* @description Verifies ownership without returning organization data.
|
|
408
|
+
* Used for authorization checks in the frontend.
|
|
409
|
+
*
|
|
410
|
+
* @param c - Hono context containing organization ID and authenticated user
|
|
411
|
+
* @returns Ownership status (true/false) or 404 if organization doesn't exist
|
|
412
|
+
*/
|
|
413
|
+
export const checkOwnership = async (c) => {
|
|
414
|
+
const { db } = createDb(c.env);
|
|
415
|
+
const { id } = c.req.valid("param");
|
|
416
|
+
const authUser = c.get("user");
|
|
417
|
+
const organizationData = await db.query.organization.findFirst({
|
|
418
|
+
where: eq(organization.id, id),
|
|
419
|
+
columns: {
|
|
420
|
+
id: true,
|
|
421
|
+
ownerId: true,
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
if (!organizationData) {
|
|
425
|
+
return c.json({
|
|
426
|
+
message: HttpStatusPhrases.NOT_FOUND,
|
|
427
|
+
}, HttpStatusCodes.NOT_FOUND);
|
|
428
|
+
}
|
|
429
|
+
const isOwner = organizationData.ownerId === authUser.id;
|
|
430
|
+
return c.json({
|
|
431
|
+
isOwner,
|
|
432
|
+
}, HttpStatusCodes.OK);
|
|
433
|
+
};
|
|
434
|
+
/**
|
|
435
|
+
* Checks if an organization name is available
|
|
436
|
+
*
|
|
437
|
+
* @description Verifies if the provided name is unique in the database.
|
|
438
|
+
* When taken, returns similar available slugs using pg_trgm similarity.
|
|
439
|
+
* Used for name validation in the frontend.
|
|
440
|
+
*
|
|
441
|
+
* @param c - Hono context containing organization name
|
|
442
|
+
* @returns Availability status and suggestions if taken
|
|
443
|
+
*/
|
|
444
|
+
// Todo: Add rate limit to this route
|
|
445
|
+
export const checkOrganizationName = async (c) => {
|
|
446
|
+
const { db } = createDb(c.env);
|
|
447
|
+
const { name } = c.req.valid("query");
|
|
448
|
+
if (!name?.trim()) {
|
|
449
|
+
return c.json({ message: "Name is required" }, HttpStatusCodes.UNPROCESSABLE_ENTITY);
|
|
450
|
+
}
|
|
451
|
+
const slug = slugify(name.trim());
|
|
452
|
+
// Check if slug exists using case-insensitive comparison
|
|
453
|
+
const exists = await db
|
|
454
|
+
.select({
|
|
455
|
+
id: organization.id,
|
|
456
|
+
slug: organization.slug,
|
|
457
|
+
})
|
|
458
|
+
.from(organization)
|
|
459
|
+
.where(sql `LOWER(TRIM(${organization.slug})) = ${slug.toLowerCase()}`)
|
|
460
|
+
.limit(1);
|
|
461
|
+
if (exists.length === 0) {
|
|
462
|
+
return c.json({ exists: false }, HttpStatusCodes.OK);
|
|
463
|
+
}
|
|
464
|
+
// Quick predefined suggestions
|
|
465
|
+
const candidates = [
|
|
466
|
+
`${slug}-org`,
|
|
467
|
+
`${slug}-team`,
|
|
468
|
+
`${slug}-club`,
|
|
469
|
+
`${slug}-league`,
|
|
470
|
+
`${slug}-hq`,
|
|
471
|
+
];
|
|
472
|
+
// Get first 3 that don't exist
|
|
473
|
+
const available = await db.execute(sql `
|
|
474
|
+
SELECT s.candidate_slug AS slug
|
|
475
|
+
FROM unnest(ARRAY[${sql.join(candidates.map(c => sql `${c}`), sql `, `)}]) AS s(candidate_slug)
|
|
476
|
+
WHERE NOT EXISTS (
|
|
477
|
+
SELECT 1 FROM organization o WHERE o.slug = s.candidate_slug
|
|
478
|
+
)
|
|
479
|
+
LIMIT 3
|
|
480
|
+
`);
|
|
481
|
+
return c.json({
|
|
482
|
+
exists: true,
|
|
483
|
+
suggestions: available.rows.map(r => r.slug),
|
|
484
|
+
}, HttpStatusCodes.OK);
|
|
485
|
+
};
|