Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changepacks/changepack_log_1eab7CjTJM0lglMgJPbc1.json
Original file line number Diff line number Diff line change
@@ -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"}
10 changes: 9 additions & 1 deletion packages/core/src/additional.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,15 @@ export type DevupApiRequestInit = Omit<RequestInit, 'body'> & {
params?: Record<string, string | number | boolean | null | undefined>
query?:
| ConstructorParameters<typeof URLSearchParams>[0]
| Record<string, string | number | (number | string)[]>
| Record<
string,
| string
| number
| boolean
| null
| undefined
| (number | string | boolean)[]
>
middleware?: Middleware[]
}

Expand Down
12 changes: 12 additions & 0 deletions packages/generator/src/__tests__/wrap-interface-key-guard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
3 changes: 2 additions & 1 deletion packages/generator/src/generate-crud-config.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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('}')
Expand Down
10 changes: 6 additions & 4 deletions packages/generator/src/generate-schema.ts
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -298,14 +299,15 @@ function formatType(obj: Record<string, unknown>, 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}`
Expand All @@ -325,7 +327,7 @@ function formatType(obj: Record<string, unknown>, 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
Expand All @@ -337,7 +339,7 @@ function formatType(obj: Record<string, unknown>, 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')

Expand Down
18 changes: 12 additions & 6 deletions packages/generator/src/wrap-interface-key-guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}