diff --git a/.changepacks/changepack_log_1eab7CjTJM0lglMgJPbc1.json b/.changepacks/changepack_log_1eab7CjTJM0lglMgJPbc1.json new file mode 100644 index 0000000..46db318 --- /dev/null +++ b/.changepacks/changepack_log_1eab7CjTJM0lglMgJPbc1.json @@ -0,0 +1 @@ +{"changes":{"packages/generator/package.json":"Patch","packages/core/package.json":"Patch"},"note":"Allow more type","date":"2026-01-19T12:09:16.172578500Z"} \ No newline at end of file diff --git a/packages/core/src/additional.ts b/packages/core/src/additional.ts index d835e75..9d9e093 100644 --- a/packages/core/src/additional.ts +++ b/packages/core/src/additional.ts @@ -21,7 +21,15 @@ export type DevupApiRequestInit = Omit & { params?: Record query?: | ConstructorParameters[0] - | Record + | Record< + string, + | string + | number + | boolean + | null + | undefined + | (number | string | boolean)[] + > middleware?: Middleware[] } diff --git a/packages/generator/src/__tests__/wrap-interface-key-guard.test.ts b/packages/generator/src/__tests__/wrap-interface-key-guard.test.ts index 6f7ad95..a822f1f 100644 --- a/packages/generator/src/__tests__/wrap-interface-key-guard.test.ts +++ b/packages/generator/src/__tests__/wrap-interface-key-guard.test.ts @@ -55,3 +55,15 @@ test.each([ ] as const)('wrapInterfaceKeyGuard wraps key with forbidden characters: %s -> %s', (key, expected) => { expect(wrapInterfaceKeyGuard(key)).toBe(expected) }) + +test.each([ + ['name?', 'name?'], // valid identifier with optional marker + ['email?', 'email?'], // valid identifier with optional marker + ['field_name?', 'field_name?'], // valid identifier with optional marker + ['field-name?', '[`field-name`]?'], // invalid identifier with optional marker + ['field name?', '[`field name`]?'], // invalid identifier with optional marker + ['/users?', '[`/users`]?'], // path with optional marker + ['123field?', '[`123field`]?'], // starts with number, with optional marker +] as const)('wrapInterfaceKeyGuard handles optional keys (ending with ?): %s -> %s', (key, expected) => { + expect(wrapInterfaceKeyGuard(key)).toBe(expected) +}) diff --git a/packages/generator/src/generate-crud-config.ts b/packages/generator/src/generate-crud-config.ts index 7962e70..2638dee 100644 --- a/packages/generator/src/generate-crud-config.ts +++ b/packages/generator/src/generate-crud-config.ts @@ -1,6 +1,7 @@ import type { OpenAPIV3_1 } from 'openapi-types' import type { CrudConfig, CrudField } from './crud-types' import { parseCrudConfigsFromMultiple } from './parse-crud-tags' +import { wrapInterfaceKeyGuard } from './wrap-interface-key-guard' /** * Convert string to PascalCase for component names @@ -286,7 +287,7 @@ export function generateCrudConfigTypes( lines.push("declare module '@devup-api/ui' {") lines.push(' interface DevupCrudApiNames {') for (const name of apiNames) { - lines.push(` ${name}: true`) + lines.push(` ${wrapInterfaceKeyGuard(name)}: true`) } lines.push(' }') lines.push('}') diff --git a/packages/generator/src/generate-schema.ts b/packages/generator/src/generate-schema.ts index 4a6d848..6b8907b 100644 --- a/packages/generator/src/generate-schema.ts +++ b/packages/generator/src/generate-schema.ts @@ -1,5 +1,6 @@ import type { OpenAPIV3_1 } from 'openapi-types' import type { ParameterDefinition } from './generate-interface' +import { wrapInterfaceKeyGuard } from './wrap-interface-key-guard' /** * Check if a schema is nullable (OpenAPI 3.0 or 3.1) @@ -298,14 +299,15 @@ function formatType(obj: Record, indent: number = 0): string { .map(([key, value]) => { // Handle string values (e.g., component references) if (typeof value === 'string') { - return `${nextIndentStr}${key}: ${value}` + return `${nextIndentStr}${wrapInterfaceKeyGuard(key)}: ${value}` } // Handle ParameterDefinition for params and query if (isParameterDefinition(value)) { const typeStr = formatTypeValue(value.type, nextIndent) const isOptional = value.required === false - const keyWithOptional = isOptional ? `${key}?` : key + const wrappedKey = wrapInterfaceKeyGuard(key) + const keyWithOptional = isOptional ? `${wrappedKey}?` : wrappedKey let description = '' if (value.description) { description += `${nextIndentStr}/**\n${nextIndentStr} * ${value.description}` @@ -325,7 +327,7 @@ function formatType(obj: Record, indent: number = 0): string { if (isTypeObject(value)) { const formattedValue = formatTypeValue(value.type, nextIndent) // Key already has '?' if it's optional (from getTypeFromSchema), keep it as is - return `${nextIndentStr}${key}: ${formattedValue}` + return `${nextIndentStr}${wrapInterfaceKeyGuard(key)}: ${formattedValue}` } // Check if value is an object (like params, query) with all optional properties @@ -337,7 +339,7 @@ function formatType(obj: Record, indent: number = 0): string { const optionalMarker = valueAllOptional ? '?' : '' const formattedValue = formatTypeValue(value, nextIndent) - return `${nextIndentStr}${key}${optionalMarker}: ${formattedValue}` + return `${nextIndentStr}${wrapInterfaceKeyGuard(key)}${optionalMarker}: ${formattedValue}` }) .join(';\n') diff --git a/packages/generator/src/wrap-interface-key-guard.ts b/packages/generator/src/wrap-interface-key-guard.ts index f418430..e537537 100644 --- a/packages/generator/src/wrap-interface-key-guard.ts +++ b/packages/generator/src/wrap-interface-key-guard.ts @@ -4,17 +4,23 @@ export function wrapInterfaceKeyGuard(key: string): string { return key } - // Check if key contains forbidden characters that require wrapping + // Check if key ends with '?' (optional marker in TypeScript) + // If so, process the base key and add '?' back at the end + const isOptional = key.endsWith('?') + const baseKey = isOptional ? key.slice(0, -1) : key + + // Check if base key contains forbidden characters that require wrapping // TypeScript identifier pattern: starts with letter/underscore/dollar, followed by letters/numbers/underscore/dollar - const isValidIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) + const isValidIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(baseKey) if ( !isValidIdentifier || - key.includes('"') || - key.includes("'") || - key.includes('`') + baseKey.includes('"') || + baseKey.includes("'") || + baseKey.includes('`') ) { - return `[\`${key}\`]` + const wrapped = `[\`${baseKey}\`]` + return isOptional ? `${wrapped}?` : wrapped } return key }