kaneo (minimalist kanban) fork to experiment adding a tangled integration github.com/usekaneo/kaneo
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at cd7cada2f86b4e866a15b4323bb8d6d7ab5bba8b 538 lines 14 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 markOptionalSchemaFieldsNullable = ( 414 spec: Record<string, unknown>, 415) => { 416 const schemas = ((spec as { components?: { schemas?: unknown } }).components 417 ?.schemas || {}) as Record<string, unknown>; 418 419 for (const schema of Object.values(schemas)) { 420 if (!isPlainObject(schema)) continue; 421 422 const properties = schema.properties as Record<string, unknown> | undefined; 423 if (!isPlainObject(properties)) continue; 424 425 const required = Array.isArray(schema.required) ? schema.required : []; 426 427 for (const [name, prop] of Object.entries(properties)) { 428 if (required.includes(name)) continue; 429 if (!isPlainObject(prop)) continue; 430 if (prop.nullable === true) continue; 431 if (typeof prop.type !== "string") continue; 432 433 prop.nullable = true; 434 } 435 } 436 437 return spec; 438}; 439 440export const normalizeEmptyAndEnumSchemas = (spec: Record<string, unknown>) => { 441 const visit = (node: unknown): void => { 442 if (Array.isArray(node)) { 443 for (const item of node) { 444 visit(item); 445 } 446 return; 447 } 448 449 if (!isPlainObject(node)) { 450 return; 451 } 452 453 // propertyNames is not valid in OpenAPI 3.0.x — remove it 454 if ("propertyNames" in node) { 455 delete node.propertyNames; 456 } 457 458 // Schema with enum but no type → add type: "string" 459 if (Array.isArray(node.enum) && !node.type && !node.$ref) { 460 node.type = "string"; 461 } 462 463 // $ref with siblings is invalid in 3.0.x → wrap in allOf 464 if (typeof node.$ref === "string" && Object.keys(node).length > 1) { 465 const ref = node.$ref as string; 466 delete node.$ref; 467 const rest = { ...node }; 468 for (const k of Object.keys(node)) { 469 delete node[k]; 470 } 471 Object.assign(node, { allOf: [{ $ref: ref }], ...rest }); 472 } 473 474 // For "properties" maps, check children for empty schemas (v.date() → {}) 475 if (isPlainObject(node.properties)) { 476 const props = node.properties as Record<string, unknown>; 477 for (const [name, schema] of Object.entries(props)) { 478 if (isPlainObject(schema) && Object.keys(schema).length === 0) { 479 props[name] = { type: "string", format: "date-time" }; 480 } 481 } 482 } 483 484 for (const [k, value] of Object.entries(node)) { 485 // Replace remaining empty schemas {} (e.g. v.any() or v.unknown()). 486 if (isPlainObject(value) && Object.keys(value).length === 0) { 487 // additionalProperties: {} → true (means "any additional properties") 488 node[k] = k === "additionalProperties" ? true : { type: "object" }; 489 continue; 490 } 491 visit(value); 492 } 493 }; 494 495 visit(spec); 496 return spec; 497}; 498 499export const ensureOperationSummaries = (spec: Record<string, unknown>) => { 500 const paths = ((spec as { paths?: unknown }).paths || {}) as Record< 501 string, 502 unknown 503 >; 504 505 for (const pathItem of Object.values(paths)) { 506 if (!pathItem || typeof pathItem !== "object") { 507 continue; 508 } 509 510 for (const method of HTTP_METHODS) { 511 const operation = (pathItem as Record<string, unknown>)[method] as 512 | Record<string, unknown> 513 | undefined; 514 if (!operation || typeof operation !== "object") { 515 continue; 516 } 517 518 const summary = operation.summary; 519 if (typeof summary === "string" && summary.trim().length > 0) { 520 continue; 521 } 522 523 const operationId = operation.operationId; 524 if (typeof operationId !== "string" || operationId.trim().length === 0) { 525 continue; 526 } 527 528 const words = splitCamelCase(operationId); 529 if (words.length === 0) { 530 continue; 531 } 532 533 operation.summary = words.map((word) => wordCapitalize(word)).join(" "); 534 } 535 } 536 537 return spec; 538};