kaneo (minimalist kanban) fork to experiment adding a tangled integration github.com/usekaneo/kaneo
at main 452 lines 12 kB view raw
1const HTTP_METHODS = [ 2 "get", 3 "put", 4 "post", 5 "delete", 6 "patch", 7 "head", 8 "options", 9 "trace", 10] as const; 11 12const wordCapitalize = (value: string): string => 13 value ? value.charAt(0).toUpperCase() + value.slice(1) : value; 14 15const toWords = (value: string): string[] => 16 value 17 .replace(/[{}]/g, "") 18 .split(/[^a-zA-Z0-9]+/) 19 .map((part) => part.trim().toLowerCase()) 20 .filter(Boolean); 21 22const toCamelCase = (parts: string[]): string => 23 parts 24 .map((part, index) => (index === 0 ? part : wordCapitalize(part))) 25 .join(""); 26 27const toTitleCase = (parts: string[]): string => 28 parts.map((part) => wordCapitalize(part)).join(" "); 29 30const splitCamelCase = (value: string): string[] => 31 value 32 .replace(/([a-z0-9])([A-Z])/g, "$1 $2") 33 .split(/[^a-zA-Z0-9]+/) 34 .map((part) => part.trim()) 35 .filter(Boolean); 36 37const summarizeAction = (action: string): string => { 38 if (action === "has") { 39 return "check"; 40 } 41 return action; 42}; 43 44export const normalizeApiServerUrl = (baseUrl: string): string => { 45 const trimmed = baseUrl.replace(/\/+$/, ""); 46 return trimmed.endsWith("/api") ? trimmed : `${trimmed}/api`; 47}; 48 49export const normalizeOrganizationAuthOperations = ( 50 authSpec: Record<string, unknown>, 51): Record<string, unknown> => { 52 const normalized = JSON.parse(JSON.stringify(authSpec)) as Record< 53 string, 54 unknown 55 >; 56 const paths = ((normalized as { paths?: unknown }).paths || {}) as Record< 57 string, 58 unknown 59 >; 60 const organizationPaths = Object.fromEntries( 61 Object.entries(paths) 62 .filter( 63 ([path]) => 64 path.startsWith("/organization") || 65 path.startsWith("/auth/organization"), 66 ) 67 .map(([path, pathItem]) => [ 68 path.startsWith("/auth/") ? path : `/auth${path}`, 69 pathItem, 70 ]), 71 ) as Record<string, unknown>; 72 73 for (const [path, pathItem] of Object.entries(organizationPaths)) { 74 if (!pathItem || typeof pathItem !== "object") { 75 continue; 76 } 77 78 const endpointWords = toWords( 79 path.replace(/^\/(?:auth\/)?organization\/?/, ""), 80 ); 81 const action = endpointWords[0] || "get"; 82 const rest = endpointWords.slice(1); 83 const opIdBaseParts = [action, "organization", ...rest]; 84 const summaryVerb = summarizeAction(action); 85 const summaryObjectParts = ["organization", ...rest]; 86 87 for (const method of HTTP_METHODS) { 88 const operation = (pathItem as Record<string, unknown>)[method] as 89 | Record<string, unknown> 90 | undefined; 91 if (!operation || typeof operation !== "object") { 92 continue; 93 } 94 95 operation.operationId = toCamelCase(opIdBaseParts); 96 operation.summary = `${wordCapitalize(summaryVerb)} ${toTitleCase( 97 summaryObjectParts, 98 )}`.trim(); 99 operation.tags = ["Organization Management"]; 100 } 101 } 102 103 const normalizedWithOnlyOrganizationPaths = { 104 ...normalized, 105 paths: organizationPaths, 106 tags: [ 107 { 108 name: "Organization Management", 109 }, 110 ], 111 } as Record<string, unknown>; 112 113 const refPattern = /^#\/components\/([^/]+)\/([^/]+)$/; 114 const refs = new Set<string>(); 115 const scanRefs = (value: unknown) => { 116 if (Array.isArray(value)) { 117 for (const entry of value) { 118 scanRefs(entry); 119 } 120 return; 121 } 122 if (!value || typeof value !== "object") { 123 return; 124 } 125 126 for (const [key, next] of Object.entries(value)) { 127 if (key === "$ref" && typeof next === "string") { 128 refs.add(next); 129 } else { 130 scanRefs(next); 131 } 132 } 133 }; 134 135 scanRefs( 136 ( 137 normalizedWithOnlyOrganizationPaths as { 138 paths?: unknown; 139 security?: unknown; 140 } 141 ).paths, 142 ); 143 scanRefs( 144 ( 145 normalizedWithOnlyOrganizationPaths as { 146 paths?: unknown; 147 security?: unknown; 148 } 149 ).security, 150 ); 151 152 const sourceComponents = ((normalized as { components?: unknown }) 153 .components || {}) as Record<string, unknown>; 154 const prunedComponents: Record<string, unknown> = {}; 155 156 if ( 157 sourceComponents.securitySchemes && 158 typeof sourceComponents.securitySchemes === "object" 159 ) { 160 prunedComponents.securitySchemes = sourceComponents.securitySchemes; 161 } 162 163 let changed = true; 164 while (changed) { 165 changed = false; 166 const pendingRefs = [...refs]; 167 for (const ref of pendingRefs) { 168 const match = refPattern.exec(ref); 169 if (!match) { 170 continue; 171 } 172 const section = match[1]; 173 const name = match[2]; 174 if (!section || !name) { 175 continue; 176 } 177 const sourceSection = sourceComponents[section] as 178 | Record<string, unknown> 179 | undefined; 180 if (!sourceSection || !(name in sourceSection)) { 181 continue; 182 } 183 184 if (!(section in prunedComponents)) { 185 prunedComponents[section] = {}; 186 } 187 const targetSection = prunedComponents[section] as Record< 188 string, 189 unknown 190 >; 191 if (name in targetSection) { 192 continue; 193 } 194 195 targetSection[name] = sourceSection[name]; 196 const before = refs.size; 197 scanRefs(sourceSection[name]); 198 if (refs.size > before) { 199 changed = true; 200 } 201 } 202 } 203 204 if (Object.keys(prunedComponents).length > 0) { 205 normalizedWithOnlyOrganizationPaths.components = prunedComponents; 206 } else { 207 delete normalizedWithOnlyOrganizationPaths.components; 208 } 209 210 return normalizedWithOnlyOrganizationPaths; 211}; 212 213export const mergeOpenApiSpecs = ( 214 honoSpec: Record<string, unknown>, 215 authSpec: Record<string, unknown>, 216) => { 217 const mergeRecord = (a: unknown, b: unknown): Record<string, unknown> => ({ 218 ...((a as Record<string, unknown>) || {}), 219 ...((b as Record<string, unknown>) || {}), 220 }); 221 222 const mergeArray = (a: unknown, b: unknown): unknown[] => [ 223 ...((a as unknown[]) || []), 224 ...((b as unknown[]) || []), 225 ]; 226 227 return { 228 ...honoSpec, 229 openapi: 230 (honoSpec as { openapi?: string }).openapi || 231 (authSpec as { openapi?: string }).openapi || 232 "3.0.3", 233 info: 234 (honoSpec as { info?: unknown }).info || 235 (authSpec as { info?: unknown }).info, 236 servers: 237 (honoSpec as { servers?: unknown[] }).servers || 238 (authSpec as { servers?: unknown[] }).servers, 239 security: 240 (honoSpec as { security?: unknown[] }).security || 241 (authSpec as { security?: unknown[] }).security, 242 paths: mergeRecord( 243 (honoSpec as { paths?: unknown }).paths, 244 (authSpec as { paths?: unknown }).paths, 245 ), 246 tags: mergeArray( 247 (honoSpec as { tags?: unknown[] }).tags, 248 (authSpec as { tags?: unknown[] }).tags, 249 ), 250 components: { 251 ...mergeRecord( 252 (honoSpec as { components?: unknown }).components, 253 (authSpec as { components?: unknown }).components, 254 ), 255 schemas: mergeRecord( 256 (honoSpec as { components?: { schemas?: unknown } }).components 257 ?.schemas, 258 (authSpec as { components?: { schemas?: unknown } }).components 259 ?.schemas, 260 ), 261 securitySchemes: mergeRecord( 262 (honoSpec as { components?: { securitySchemes?: unknown } }).components 263 ?.securitySchemes, 264 (authSpec as { components?: { securitySchemes?: unknown } }).components 265 ?.securitySchemes, 266 ), 267 }, 268 }; 269}; 270 271export const dedupeOperationIds = (spec: Record<string, unknown>) => { 272 const paths = ((spec as { paths?: unknown }).paths || {}) as Record< 273 string, 274 unknown 275 >; 276 const seen = new Set<string>(); 277 278 for (const [path, pathItem] of Object.entries(paths)) { 279 if (!pathItem || typeof pathItem !== "object") { 280 continue; 281 } 282 283 for (const method of HTTP_METHODS) { 284 const operation = (pathItem as Record<string, unknown>)[method] as 285 | Record<string, unknown> 286 | undefined; 287 288 if (!operation || typeof operation !== "object") { 289 continue; 290 } 291 292 const operationId = operation.operationId; 293 if (typeof operationId !== "string" || operationId.length === 0) { 294 continue; 295 } 296 297 if (!seen.has(operationId)) { 298 seen.add(operationId); 299 continue; 300 } 301 302 const pathSuffix = path 303 .replace(/\//g, "_") 304 .replace(/[{}]/g, "") 305 .replace(/_+/g, "_") 306 .replace(/^_+|_+$/g, ""); 307 const nextId = `${operationId}_${method}_${pathSuffix || "root"}`; 308 operation.operationId = nextId; 309 seen.add(nextId); 310 } 311 } 312 313 return spec; 314}; 315 316const isPlainObject = (value: unknown): value is Record<string, unknown> => 317 !!value && typeof value === "object" && !Array.isArray(value); 318 319const setObjectContents = ( 320 target: Record<string, unknown>, 321 source: Record<string, unknown>, 322) => { 323 for (const key of Object.keys(target)) { 324 delete target[key]; 325 } 326 Object.assign(target, source); 327}; 328 329export const normalizeNullableSchemasForOpenApi30 = ( 330 spec: Record<string, unknown>, 331) => { 332 const visit = (node: unknown): void => { 333 if (Array.isArray(node)) { 334 for (const item of node) { 335 visit(item); 336 } 337 return; 338 } 339 340 if (!isPlainObject(node)) { 341 return; 342 } 343 344 const typeValue = node.type; 345 if (Array.isArray(typeValue)) { 346 const nullRemoved = typeValue.filter((entry) => entry !== "null"); 347 const hadNull = nullRemoved.length !== typeValue.length; 348 349 if (hadNull && nullRemoved.length === 1) { 350 node.type = nullRemoved[0]; 351 node.nullable = true; 352 } 353 } 354 355 const anyOfValue = node.anyOf; 356 if (Array.isArray(anyOfValue) && anyOfValue.length >= 2) { 357 const nullSchema = anyOfValue.find( 358 (entry) => isPlainObject(entry) && entry.type === "null", 359 ); 360 const nonNullSchemas = anyOfValue.filter( 361 (entry) => !(isPlainObject(entry) && entry.type === "null"), 362 ); 363 364 if ( 365 nullSchema && 366 nonNullSchemas.length === 1 && 367 isPlainObject(nonNullSchemas[0]) 368 ) { 369 const { anyOf: _anyOf, ...rest } = node; 370 setObjectContents(node, { 371 ...rest, 372 ...(nonNullSchemas[0] as Record<string, unknown>), 373 nullable: true, 374 }); 375 } 376 } 377 378 for (const value of Object.values(node)) { 379 visit(value); 380 } 381 }; 382 383 visit(spec); 384 return spec; 385}; 386 387export const normalizeEmptyRequiredArrays = (spec: Record<string, unknown>) => { 388 const visit = (node: unknown): void => { 389 if (Array.isArray(node)) { 390 for (const item of node) { 391 visit(item); 392 } 393 return; 394 } 395 396 if (!isPlainObject(node)) { 397 return; 398 } 399 400 if (Array.isArray(node.required) && node.required.length === 0) { 401 delete node.required; 402 } 403 404 for (const value of Object.values(node)) { 405 visit(value); 406 } 407 }; 408 409 visit(spec); 410 return spec; 411}; 412 413export const ensureOperationSummaries = (spec: Record<string, unknown>) => { 414 const paths = ((spec as { paths?: unknown }).paths || {}) as Record< 415 string, 416 unknown 417 >; 418 419 for (const pathItem of Object.values(paths)) { 420 if (!pathItem || typeof pathItem !== "object") { 421 continue; 422 } 423 424 for (const method of HTTP_METHODS) { 425 const operation = (pathItem as Record<string, unknown>)[method] as 426 | Record<string, unknown> 427 | undefined; 428 if (!operation || typeof operation !== "object") { 429 continue; 430 } 431 432 const summary = operation.summary; 433 if (typeof summary === "string" && summary.trim().length > 0) { 434 continue; 435 } 436 437 const operationId = operation.operationId; 438 if (typeof operationId !== "string" || operationId.trim().length === 0) { 439 continue; 440 } 441 442 const words = splitCamelCase(operationId); 443 if (words.length === 0) { 444 continue; 445 } 446 447 operation.summary = words.map((word) => wordCapitalize(word)).join(" "); 448 } 449 } 450 451 return spec; 452};