mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(docs): preserve items response fields (#6587)
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { parseConstProperties, parsePropertiesContent } from './generate-docs'
|
||||
|
||||
describe('documentation output property parsing', () => {
|
||||
it('keeps a response field named items inside an array element', () => {
|
||||
const properties = parsePropertiesContent(`
|
||||
vaults: {
|
||||
type: 'array',
|
||||
description: 'List of accessible vaults',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Vault ID' },
|
||||
items: { type: 'number', description: 'Number of items in the vault' },
|
||||
},
|
||||
},
|
||||
},
|
||||
`)
|
||||
|
||||
expect(Object.keys(properties)).toEqual(['vaults'])
|
||||
expect(properties.vaults.items.properties.items).toEqual({
|
||||
type: 'number',
|
||||
description: 'Number of items in the vault',
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps a response field named items that directly references a constant', () => {
|
||||
const properties = parsePropertiesContent('items: ATTENDEES_OUTPUT,', 'calcom')
|
||||
|
||||
expect(properties.items).toMatchObject({
|
||||
type: 'array',
|
||||
description: 'List of attendees',
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps a response field named items that references a constant property', () => {
|
||||
const properties = parsePropertiesContent('items: EVENT_TYPE_OUTPUT_PROPERTIES.id,', 'calcom')
|
||||
|
||||
expect(properties.items).toEqual({
|
||||
type: 'number',
|
||||
description: 'Event type ID',
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps items fields in constant-defined property maps', () => {
|
||||
const typesContent = `
|
||||
export const RECORD_OUTPUT_PROPERTIES = {
|
||||
id: { type: 'string', description: 'Record ID' },
|
||||
}
|
||||
`
|
||||
const properties = parseConstProperties(
|
||||
`
|
||||
items: {
|
||||
type: 'object',
|
||||
description: 'Result page',
|
||||
properties: {
|
||||
object: { type: 'string', description: 'Page type' },
|
||||
data: {
|
||||
type: 'array',
|
||||
description: 'Result records',
|
||||
items: { type: 'object', properties: RECORD_OUTPUT_PROPERTIES },
|
||||
},
|
||||
hasMore: { type: 'boolean', description: 'Whether more results exist' },
|
||||
},
|
||||
},
|
||||
`,
|
||||
'test',
|
||||
typesContent,
|
||||
0
|
||||
)
|
||||
|
||||
expect(Object.keys(properties)).toEqual(['items'])
|
||||
expect(Object.keys(properties.items.properties)).toEqual(['object', 'data', 'hasMore'])
|
||||
expect(properties.items.properties.data.items.properties.id).toEqual({
|
||||
type: 'string',
|
||||
description: 'Record ID',
|
||||
})
|
||||
})
|
||||
})
|
||||
+92
-39
@@ -7,8 +7,6 @@ import { glob } from 'glob'
|
||||
import type { BlockCategory } from '../apps/sim/blocks/types'
|
||||
import { IntegrationType } from '../apps/sim/blocks/types'
|
||||
|
||||
console.log('Starting documentation generator...')
|
||||
|
||||
/**
|
||||
* Cache for resolved const definitions from types files.
|
||||
* Key: "toolPrefix:constName" (e.g., "calcom:SCHEDULE_DATA_OUTPUT_PROPERTIES")
|
||||
@@ -1671,7 +1669,7 @@ function resolveConstReference(
|
||||
/**
|
||||
* Parse properties from a const definition, resolving nested const references.
|
||||
*/
|
||||
function parseConstProperties(
|
||||
export function parseConstProperties(
|
||||
content: string,
|
||||
toolPrefix: string,
|
||||
typesContent: string,
|
||||
@@ -1705,10 +1703,6 @@ function parseConstProperties(
|
||||
const propName = match[1]
|
||||
const constRef = match[2]
|
||||
|
||||
if (propName === 'items') {
|
||||
continue
|
||||
}
|
||||
|
||||
const beforeMatch = content.substring(0, match.index)
|
||||
const openBraces = (beforeMatch.match(/\{/g) || []).length
|
||||
const closeBraces = (beforeMatch.match(/\}/g) || []).length
|
||||
@@ -1725,7 +1719,13 @@ function parseConstProperties(
|
||||
const propContent = content.substring(startPos + 1, endPos - 1).trim()
|
||||
// If it starts with 'type:', it's an output field definition - process it
|
||||
if (propContent.match(/^\s*type\s*:/)) {
|
||||
const parsedProp = parseConstFieldContent(propContent, toolPrefix, typesContent, depth)
|
||||
const parsedProp = parseConstFieldContent(
|
||||
propContent,
|
||||
toolPrefix,
|
||||
typesContent,
|
||||
depth,
|
||||
propName
|
||||
)
|
||||
if (parsedProp) {
|
||||
properties[propName] = parsedProp
|
||||
}
|
||||
@@ -1747,7 +1747,13 @@ function parseConstProperties(
|
||||
|
||||
if (endPos !== -1) {
|
||||
const propContent = content.substring(startPos + 1, endPos - 1).trim()
|
||||
const parsedProp = parseConstFieldContent(propContent, toolPrefix, typesContent, depth)
|
||||
const parsedProp = parseConstFieldContent(
|
||||
propContent,
|
||||
toolPrefix,
|
||||
typesContent,
|
||||
depth,
|
||||
propName
|
||||
)
|
||||
if (parsedProp) {
|
||||
properties[propName] = parsedProp
|
||||
}
|
||||
@@ -1830,7 +1836,8 @@ function parseConstFieldContent(
|
||||
fieldContent: string,
|
||||
toolPrefix: string,
|
||||
typesContent: string,
|
||||
depth: number
|
||||
depth: number,
|
||||
propertyName?: string
|
||||
): any {
|
||||
const typeMatch = fieldContent.match(/type\s*:\s*['"]([^'"]+)['"]/)
|
||||
const description = extractDescription(fieldContent)
|
||||
@@ -1845,7 +1852,7 @@ function parseConstFieldContent(
|
||||
}
|
||||
|
||||
if (fieldType === 'object' || fieldType === 'json') {
|
||||
const propsConstMatch = fieldContent.match(/properties\s*:\s*([A-Z][A-Z_0-9]+)/)
|
||||
const propsConstMatch = matchSchemaKeyword(fieldContent, propertyName, PROPERTIES_CONST_PATTERN)
|
||||
if (propsConstMatch) {
|
||||
const resolvedProps = resolveConstFromTypesContent(
|
||||
propsConstMatch[1],
|
||||
@@ -1857,7 +1864,11 @@ function parseConstFieldContent(
|
||||
result.properties = resolvedProps
|
||||
}
|
||||
} else {
|
||||
const propertiesStart = fieldContent.search(/properties\s*:\s*\{/)
|
||||
const propertiesStart = findSchemaKeyword(
|
||||
fieldContent,
|
||||
propertyName,
|
||||
PROPERTIES_INLINE_PATTERN
|
||||
)
|
||||
if (propertiesStart !== -1) {
|
||||
const braceStart = fieldContent.indexOf('{', propertiesStart)
|
||||
const braceEnd = findMatchingClose(fieldContent, braceStart)
|
||||
@@ -1875,7 +1886,7 @@ function parseConstFieldContent(
|
||||
}
|
||||
}
|
||||
|
||||
const itemsConstMatch = fieldContent.match(/items\s*:\s*([A-Z][A-Z_0-9]+)/)
|
||||
const itemsConstMatch = matchSchemaKeyword(fieldContent, propertyName, ITEMS_CONST_PATTERN)
|
||||
if (itemsConstMatch) {
|
||||
const resolvedItems = resolveConstFromTypesContent(
|
||||
itemsConstMatch[1],
|
||||
@@ -1887,7 +1898,7 @@ function parseConstFieldContent(
|
||||
result.items = resolvedItems
|
||||
}
|
||||
} else {
|
||||
const itemsStart = fieldContent.search(/items\s*:\s*\{/)
|
||||
const itemsStart = findSchemaKeyword(fieldContent, propertyName, ITEMS_INLINE_PATTERN)
|
||||
if (itemsStart !== -1) {
|
||||
const braceStart = fieldContent.indexOf('{', itemsStart)
|
||||
const braceEnd = findMatchingClose(fieldContent, braceStart)
|
||||
@@ -2438,7 +2449,44 @@ function isAtDepthZero(content: string, matchIndex: number): boolean {
|
||||
return depth === 0
|
||||
}
|
||||
|
||||
function parseFieldContent(fieldContent: string, toolPrefix?: string): any {
|
||||
function findTopLevelMatch(content: string, pattern: RegExp): RegExpExecArray | null {
|
||||
const flags = pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`
|
||||
const regex = new RegExp(pattern.source, flags)
|
||||
let match: RegExpExecArray | null
|
||||
|
||||
while ((match = regex.exec(content)) !== null) {
|
||||
if (isAtDepthZero(content, match.index)) {
|
||||
return match
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const PROPERTIES_CONST_PATTERN = /properties\s*:\s*([A-Z][A-Z_0-9]+)/
|
||||
const PROPERTIES_INLINE_PATTERN = /properties\s*:\s*{/
|
||||
const ITEMS_CONST_PATTERN = /items\s*:\s*([A-Z][A-Z_0-9]+)/
|
||||
const ITEMS_INLINE_PATTERN = /items\s*:\s*{/
|
||||
|
||||
function matchSchemaKeyword(
|
||||
content: string,
|
||||
propertyName: string | undefined,
|
||||
pattern: RegExp
|
||||
): RegExpExecArray | null {
|
||||
return propertyName === 'items' ? findTopLevelMatch(content, pattern) : content.match(pattern)
|
||||
}
|
||||
|
||||
function findSchemaKeyword(
|
||||
content: string,
|
||||
propertyName: string | undefined,
|
||||
pattern: RegExp
|
||||
): number {
|
||||
return propertyName === 'items'
|
||||
? (findTopLevelMatch(content, pattern)?.index ?? -1)
|
||||
: content.search(pattern)
|
||||
}
|
||||
|
||||
function parseFieldContent(fieldContent: string, toolPrefix?: string, propertyName?: string): any {
|
||||
// Only match `type:` that is at the top level of fieldContent (depth 0).
|
||||
// Child objects like `title: { type: 'string', ... }` also contain `type:` but at depth 1.
|
||||
const typeRegex = /type\s*:\s*['"]([^'"]+)['"]/g
|
||||
@@ -2492,15 +2540,18 @@ function parseFieldContent(fieldContent: string, toolPrefix?: string): any {
|
||||
|
||||
if (fieldType === 'object' || fieldType === 'json') {
|
||||
// Check for const reference first (e.g., properties: SCHEDULE_DATA_OUTPUT_PROPERTIES)
|
||||
const propsConstMatch = fieldContent.match(/properties\s*:\s*([A-Z][A-Z_0-9]+)/)
|
||||
const propsConstMatch = matchSchemaKeyword(fieldContent, propertyName, PROPERTIES_CONST_PATTERN)
|
||||
if (propsConstMatch && toolPrefix) {
|
||||
const resolvedProps = resolveConstReference(propsConstMatch[1], toolPrefix)
|
||||
if (resolvedProps) {
|
||||
result.properties = resolvedProps
|
||||
}
|
||||
} else {
|
||||
const propertiesRegex = /properties\s*:\s*{/
|
||||
const propertiesStart = fieldContent.search(propertiesRegex)
|
||||
const propertiesStart = findSchemaKeyword(
|
||||
fieldContent,
|
||||
propertyName,
|
||||
PROPERTIES_INLINE_PATTERN
|
||||
)
|
||||
|
||||
if (propertiesStart !== -1) {
|
||||
const braceStart = fieldContent.indexOf('{', propertiesStart)
|
||||
@@ -2515,15 +2566,14 @@ function parseFieldContent(fieldContent: string, toolPrefix?: string): any {
|
||||
}
|
||||
|
||||
// Check for items const reference (e.g., items: ATTENDEES_OUTPUT)
|
||||
const itemsConstMatch = fieldContent.match(/items\s*:\s*([A-Z][A-Z_0-9]+)/)
|
||||
const itemsConstMatch = matchSchemaKeyword(fieldContent, propertyName, ITEMS_CONST_PATTERN)
|
||||
if (itemsConstMatch && toolPrefix) {
|
||||
const resolvedItems = resolveConstReference(itemsConstMatch[1], toolPrefix)
|
||||
if (resolvedItems) {
|
||||
result.items = resolvedItems
|
||||
}
|
||||
} else {
|
||||
const itemsRegex = /items\s*:\s*{/
|
||||
const itemsStart = fieldContent.search(itemsRegex)
|
||||
const itemsStart = findSchemaKeyword(fieldContent, propertyName, ITEMS_INLINE_PATTERN)
|
||||
|
||||
if (itemsStart !== -1) {
|
||||
const braceStart = fieldContent.indexOf('{', itemsStart)
|
||||
@@ -2586,7 +2636,7 @@ function parseFieldContent(fieldContent: string, toolPrefix?: string): any {
|
||||
return result
|
||||
}
|
||||
|
||||
function parsePropertiesContent(
|
||||
export function parsePropertiesContent(
|
||||
propertiesContent: string,
|
||||
toolPrefix?: string
|
||||
): Record<string, any> {
|
||||
@@ -2602,7 +2652,7 @@ function parsePropertiesContent(
|
||||
const propName = constMatch[1]
|
||||
const constName = constMatch[2]
|
||||
|
||||
if (propName === 'items' || propName === 'properties' || propName === 'type') {
|
||||
if (propName === 'properties' || propName === 'type') {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -2627,7 +2677,7 @@ function parsePropertiesContent(
|
||||
const constName = propAccessMatch[2]
|
||||
const accessedProp = propAccessMatch[3]
|
||||
|
||||
if (propName === 'items' || propName === 'properties' || propName === 'type') {
|
||||
if (propName === 'properties' || propName === 'type') {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -2675,7 +2725,7 @@ function parsePropertiesContent(
|
||||
while ((match = propStartRegex.exec(propertiesContent)) !== null) {
|
||||
const propName = match[1]
|
||||
|
||||
if (propName === 'items' || propName === 'properties') {
|
||||
if (propName === 'properties') {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -2719,7 +2769,7 @@ function parsePropertiesContent(
|
||||
}
|
||||
|
||||
propPositions.forEach((prop) => {
|
||||
const parsedProp = parseFieldContent(prop.content, toolPrefix)
|
||||
const parsedProp = parseFieldContent(prop.content, toolPrefix, prop.name)
|
||||
if (parsedProp) {
|
||||
properties[prop.name] = parsedProp
|
||||
}
|
||||
@@ -3855,17 +3905,20 @@ function updateMetaJson() {
|
||||
console.log(`Updated meta.json with ${items.length} entries`)
|
||||
}
|
||||
|
||||
generateAllBlockDocs()
|
||||
.then((success) => {
|
||||
if (success) {
|
||||
console.log('Documentation generation completed successfully')
|
||||
process.exit(0)
|
||||
} else {
|
||||
console.error('Documentation generation failed')
|
||||
if (import.meta.main) {
|
||||
console.log('Starting documentation generator...')
|
||||
generateAllBlockDocs()
|
||||
.then((success) => {
|
||||
if (success) {
|
||||
console.log('Documentation generation completed successfully')
|
||||
process.exit(0)
|
||||
} else {
|
||||
console.error('Documentation generation failed')
|
||||
process.exit(1)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Fatal error:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Fatal error:', error)
|
||||
process.exit(1)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user