From ce30a87b694c751cfe3486a39ba28a967b0ca8f9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Jan 2026 14:45:02 +0000 Subject: [PATCH 1/8] feat: Add `base44 generate` command for TypeScript type generation Generates TypeScript types from local entity schemas: - Entity interfaces with system fields (id, created_date, updated_date) - CreateInput/UpdateInput types for CRUD operations - Filter types for query operations - Typed SDK client interface matching @base44/sdk API Usage: base44 generate [-o ] [--entities-only] --- src/cli/commands/generate/types.ts | 55 +++++++ src/cli/index.ts | 4 + src/core/generate/generator.ts | 234 +++++++++++++++++++++++++++++ src/core/generate/index.ts | 3 + src/core/generate/schema.ts | 37 +++++ src/core/generate/types.ts | 100 ++++++++++++ src/core/index.ts | 1 + 7 files changed, 434 insertions(+) create mode 100644 src/cli/commands/generate/types.ts create mode 100644 src/core/generate/generator.ts create mode 100644 src/core/generate/index.ts create mode 100644 src/core/generate/schema.ts create mode 100644 src/core/generate/types.ts diff --git a/src/cli/commands/generate/types.ts b/src/cli/commands/generate/types.ts new file mode 100644 index 00000000..44f3af61 --- /dev/null +++ b/src/cli/commands/generate/types.ts @@ -0,0 +1,55 @@ +import { Command } from "commander"; +import { log } from "@clack/prompts"; +import { generateTypes } from "@core/generate/index.js"; +import { runCommand, runTask } from "../../utils/index.js"; +import type { RunCommandResult } from "../../utils/runCommand.js"; +import { relative } from "node:path"; + +interface GenerateTypesOptions { + output?: string; + entitiesOnly?: boolean; +} + +async function generateTypesAction( + options: GenerateTypesOptions +): Promise { + const result = await runTask( + "Generating TypeScript types", + async () => { + return await generateTypes({ + output: options.output, + entitiesOnly: options.entitiesOnly, + }); + }, + { + successMessage: "Types generated successfully", + errorMessage: "Failed to generate types", + } + ); + + if (result.entityCount === 0) { + log.warn("No entities found in project"); + return { outroMessage: "No types generated" }; + } + + log.info(`Generated types for ${result.entityCount} entities`); + log.info(`Output directory: ${result.outputDir}`); + + const fileList = result.files + .map((f) => ` - ${relative(process.cwd(), f)}`) + .join("\n"); + log.info(`Files:\n${fileList}`); + + return { outroMessage: "Types generated successfully!" }; +} + +export const generateTypesCommand = new Command("generate") + .description("Generate TypeScript types from entity schemas") + .option("-o, --output ", "Output directory", "src/base44") + .option("--entities-only", "Only generate entity types, skip client types") + .action(async (options: GenerateTypesOptions) => { + await runCommand(() => generateTypesAction(options), { + requireAuth: false, + requireAppConfig: false, + }); + }); diff --git a/src/cli/index.ts b/src/cli/index.ts index d234fb33..a3739c87 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -6,6 +6,7 @@ import { whoamiCommand } from "./commands/auth/whoami.js"; import { logoutCommand } from "./commands/auth/logout.js"; import { entitiesPushCommand } from "./commands/entities/push.js"; import { functionsDeployCommand } from "./commands/functions/deploy.js"; +import { generateTypesCommand } from "./commands/generate/types.js"; import { createCommand } from "./commands/project/create.js"; import { dashboardCommand } from "./commands/project/dashboard.js"; import { deployCommand } from "./commands/project/deploy.js"; @@ -43,6 +44,9 @@ program.addCommand(entitiesPushCommand); // Register functions commands program.addCommand(functionsDeployCommand); +// Register generate commands +program.addCommand(generateTypesCommand); + // Register site commands program.addCommand(siteDeployCommand); diff --git a/src/core/generate/generator.ts b/src/core/generate/generator.ts new file mode 100644 index 00000000..6737a8a1 --- /dev/null +++ b/src/core/generate/generator.ts @@ -0,0 +1,234 @@ +import type { EntityDefinition, EntityField } from "./schema.js"; + +const HEADER = `// Auto-generated by Base44 CLI - DO NOT EDIT +// Regenerate with: base44 generate +`; + +/** + * Convert a JSON Schema type to TypeScript type + */ +function fieldToTypeScript(field: EntityField, required: boolean): string { + let tsType: string; + + // Handle enums + if (field.enum && field.enum.length > 0) { + tsType = field.enum.map((v) => `'${v}'`).join(" | "); + } else { + switch (field.type) { + case "string": + tsType = "string"; + break; + case "number": + tsType = "number"; + break; + case "boolean": + tsType = "boolean"; + break; + case "array": + if (field.items) { + const itemType = fieldToTypeScript(field.items, true); + tsType = `${itemType}[]`; + } else { + tsType = "unknown[]"; + } + break; + case "object": + if (field.properties) { + const props = Object.entries(field.properties) + .map(([name, prop]) => { + const isRequired = field.required?.includes(name) ?? false; + const propType = fieldToTypeScript(prop, isRequired); + return `${name}${isRequired ? "" : "?"}: ${propType}`; + }) + .join("; "); + tsType = `{ ${props} }`; + } else { + tsType = "Record"; + } + break; + default: + tsType = "unknown"; + } + } + + return tsType; +} + +/** + * Generate interface fields for an entity + */ +function generateInterfaceFields( + entity: EntityDefinition, + options: { allOptional?: boolean } = {} +): string { + if (!entity.properties) { + return ""; + } + + const requiredFields = new Set(entity.required ?? []); + + return Object.entries(entity.properties) + .map(([fieldName, field]) => { + const isRequired = options.allOptional + ? false + : requiredFields.has(fieldName); + const tsType = fieldToTypeScript(field, isRequired); + const optional = isRequired ? "" : "?"; + const description = field.description + ? ` /** ${field.description} */\n` + : ""; + return `${description} ${fieldName}${optional}: ${tsType};`; + }) + .join("\n"); +} + +/** + * Generate TypeScript interfaces for a single entity + */ +function generateEntityTypes(entity: EntityDefinition): string { + const name = entity.name; + const fields = generateInterfaceFields(entity); + const createFields = generateInterfaceFields(entity); + const updateFields = generateInterfaceFields(entity, { allOptional: true }); + const filterFields = generateInterfaceFields(entity, { allOptional: true }); + + return ` +/** ${name} entity */ +export interface ${name} extends BaseEntity { +${fields} +} + +/** Input for creating a ${name} */ +export interface ${name}CreateInput { +${createFields} +} + +/** Input for updating a ${name} (all fields optional) */ +export interface ${name}UpdateInput { +${updateFields} +} + +/** Filter query for ${name} */ +export interface ${name}Filter { +${filterFields} +} +`; +} + +/** + * Generate the entities.ts file content + */ +export function generateEntitiesFile(entities: EntityDefinition[]): string { + const baseEntity = ` +/** System fields present on all entities */ +export interface BaseEntity { + id: string; + created_date: string; + updated_date: string; +} +`; + + const entityTypes = entities.map(generateEntityTypes).join("\n"); + + // Generate union type of all entity names + const entityNames = entities.map((e) => `'${e.name}'`).join(" | "); + const entityNamesType = entities.length > 0 + ? `\n/** All entity names in this project */\nexport type EntityName = ${entityNames};\n` + : ""; + + return HEADER + baseEntity + entityTypes + entityNamesType; +} + +/** + * Generate the client.ts file content with typed SDK wrapper + */ +export function generateClientFile(entities: EntityDefinition[]): string { + if (entities.length === 0) { + return HEADER + "\n// No entities found\nexport {};\n"; + } + + // Generate imports + const imports = entities + .map((e) => `${e.name}, ${e.name}CreateInput, ${e.name}UpdateInput, ${e.name}Filter`) + .join(",\n "); + + // Generate entity handlers + const handlers = entities + .map( + (e) => + ` ${e.name}: EntityHandler<${e.name}, ${e.name}CreateInput, ${e.name}UpdateInput, ${e.name}Filter>;` + ) + .join("\n"); + + return `${HEADER} +import type { + ${imports} +} from './entities.js'; + +/** Response for list/filter operations */ +export interface ListResponse { + items: T[]; + total: number; +} + +/** Real-time subscription event */ +export interface RealtimeEvent { + type: 'create' | 'update' | 'delete'; + data: T; + id: string; + timestamp: string; +} + +/** Entity handler with CRUD operations */ +export interface EntityHandler { + /** Get all records with optional pagination */ + list(sort?: string, limit?: number, skip?: number, fields?: (keyof T)[]): Promise>; + /** Get records matching filter criteria */ + filter(query: TFilter, sort?: string, limit?: number, skip?: number, fields?: (keyof T)[]): Promise>; + /** Get a single record by ID */ + get(id: string): Promise; + /** Create a new record */ + create(data: TCreate): Promise; + /** Update an existing record */ + update(id: string, data: TUpdate): Promise; + /** Delete a record */ + delete(id: string): Promise; + /** Delete multiple records matching filter */ + deleteMany(query: TFilter): Promise; + /** Create multiple records at once */ + bulkCreate(data: TCreate[]): Promise; + /** Subscribe to real-time updates */ + subscribe(callback: (event: RealtimeEvent) => void): () => void; +} + +/** Typed entities interface for Base44 SDK */ +export interface TypedEntities { +${handlers} +} + +/** + * Typed Base44 client interface. + * Use with the Base44 SDK for type-safe entity access. + * + * @example + * import { createClient } from '@base44/sdk'; + * import type { TypedBase44Client } from './base44/client'; + * + * const base44 = createClient({ appId: 'your-app-id' }) as TypedBase44Client; + * const tasks = await base44.entities.Task.list(); + */ +export interface TypedBase44Client { + entities: TypedEntities; +} +`; +} + +/** + * Generate the index.ts barrel file + */ +export function generateIndexFile(): string { + return `${HEADER} +export * from './entities.js'; +export * from './client.js'; +`; +} diff --git a/src/core/generate/index.ts b/src/core/generate/index.ts new file mode 100644 index 00000000..21b4a404 --- /dev/null +++ b/src/core/generate/index.ts @@ -0,0 +1,3 @@ +export { generateTypes, type GenerateOptions, type GenerateResult } from "./types.js"; +export { generateEntitiesFile, generateClientFile, generateIndexFile } from "./generator.js"; +export { EntityDefinitionSchema, type EntityDefinition, type EntityField } from "./schema.js"; diff --git a/src/core/generate/schema.ts b/src/core/generate/schema.ts new file mode 100644 index 00000000..cfdea9bd --- /dev/null +++ b/src/core/generate/schema.ts @@ -0,0 +1,37 @@ +import { z } from "zod"; + +/** + * Entity field type definition (used for type generation, not strict validation) + */ +export interface EntityField { + type: "string" | "number" | "boolean" | "array" | "object"; + description?: string; + default?: unknown; + enum?: string[]; + items?: EntityField; + properties?: Record; + required?: string[]; + [key: string]: unknown; +} + +/** + * Entity definition type + */ +export interface EntityDefinition { + name: string; + type?: "object"; + properties?: Record; + required?: string[]; + [key: string]: unknown; +} + +/** + * Schema for a full entity definition + * Uses loose validation to allow JSON Schema flexibility + */ +export const EntityDefinitionSchema = z.object({ + name: z.string().min(1, "Entity name cannot be empty"), + type: z.literal("object").optional(), + properties: z.record(z.string(), z.unknown()).optional(), + required: z.array(z.string()).optional(), +}).passthrough(); diff --git a/src/core/generate/types.ts b/src/core/generate/types.ts new file mode 100644 index 00000000..c359367b --- /dev/null +++ b/src/core/generate/types.ts @@ -0,0 +1,100 @@ +import { join, dirname } from "node:path"; +import { mkdir, writeFile } from "node:fs/promises"; +import { readProjectConfig, findProjectRoot } from "../project/config.js"; +import { EntityDefinitionSchema, type EntityDefinition } from "./schema.js"; +import { + generateEntitiesFile, + generateClientFile, + generateIndexFile, +} from "./generator.js"; + +export interface GenerateOptions { + /** Output directory relative to project root (default: "src/base44") */ + output?: string; + /** Only generate entity types, skip client types */ + entitiesOnly?: boolean; +} + +export interface GenerateResult { + /** Number of entities processed */ + entityCount: number; + /** Files that were generated */ + files: string[]; + /** Output directory path */ + outputDir: string; +} + +/** + * Parse entities into EntityDefinition format for type generation + */ +function parseEntities(entities: Record[]): EntityDefinition[] { + return entities + .map((entity) => { + const result = EntityDefinitionSchema.safeParse(entity); + if (result.success) { + return result.data; + } + // Skip invalid entities but log warning with error details + const entityName = (entity as { name?: string }).name ?? "unknown"; + console.warn(`Skipping invalid entity "${entityName}": ${result.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join(', ')}`); + return null; + }) + .filter((e): e is EntityDefinition => e !== null); +} + +/** + * Generate TypeScript types from local entity schemas + */ +export async function generateTypes( + options: GenerateOptions = {} +): Promise { + const { output = "src/base44", entitiesOnly = false } = options; + + // Find project root + const projectRoot = await findProjectRoot(); + if (!projectRoot) { + throw new Error( + "Project root not found. Please run this command from within a Base44 project." + ); + } + + // Read project config and entities + const { entities } = await readProjectConfig(projectRoot.root); + + // Parse entities into our schema format + const parsedEntities = parseEntities(entities); + + // Determine output directory + const outputDir = join(projectRoot.root, output); + + // Ensure output directory exists + await mkdir(outputDir, { recursive: true }); + + const files: string[] = []; + + // Generate entities file + const entitiesContent = generateEntitiesFile(parsedEntities); + const entitiesPath = join(outputDir, "entities.ts"); + await writeFile(entitiesPath, entitiesContent, "utf-8"); + files.push(entitiesPath); + + // Generate client file (unless entities-only) + if (!entitiesOnly) { + const clientContent = generateClientFile(parsedEntities); + const clientPath = join(outputDir, "client.ts"); + await writeFile(clientPath, clientContent, "utf-8"); + files.push(clientPath); + + // Generate index file + const indexContent = generateIndexFile(); + const indexPath = join(outputDir, "index.ts"); + await writeFile(indexPath, indexContent, "utf-8"); + files.push(indexPath); + } + + return { + entityCount: parsedEntities.length, + files, + outputDir, + }; +} diff --git a/src/core/index.ts b/src/core/index.ts index 8f600d4c..da3356b6 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -3,6 +3,7 @@ export * from "./resources/index.js"; export * from "./project/index.js"; export * from "./clients/index.js"; export * from "./site/index.js"; +export * from "./generate/index.js"; export * from "./utils/index.js"; export * from "./errors.js"; export * from "./consts.js"; From fe13c5a546ceb796919df3d7db2ba86edeac0d9e Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Jan 2026 15:52:51 +0000 Subject: [PATCH 2/8] refactor: Rename command from `generate` to `types` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - base44 generate → base44 types - Update generated file header comments --- src/cli/commands/{generate/types.ts => types/generate.ts} | 2 +- src/cli/index.ts | 6 +++--- src/core/generate/generator.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) rename src/cli/commands/{generate/types.ts => types/generate.ts} (96%) diff --git a/src/cli/commands/generate/types.ts b/src/cli/commands/types/generate.ts similarity index 96% rename from src/cli/commands/generate/types.ts rename to src/cli/commands/types/generate.ts index 44f3af61..9c395a67 100644 --- a/src/cli/commands/generate/types.ts +++ b/src/cli/commands/types/generate.ts @@ -43,7 +43,7 @@ async function generateTypesAction( return { outroMessage: "Types generated successfully!" }; } -export const generateTypesCommand = new Command("generate") +export const typesGenerateCommand = new Command("types") .description("Generate TypeScript types from entity schemas") .option("-o, --output ", "Output directory", "src/base44") .option("--entities-only", "Only generate entity types, skip client types") diff --git a/src/cli/index.ts b/src/cli/index.ts index a3739c87..982aa454 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -6,7 +6,7 @@ import { whoamiCommand } from "./commands/auth/whoami.js"; import { logoutCommand } from "./commands/auth/logout.js"; import { entitiesPushCommand } from "./commands/entities/push.js"; import { functionsDeployCommand } from "./commands/functions/deploy.js"; -import { generateTypesCommand } from "./commands/generate/types.js"; +import { typesGenerateCommand } from "./commands/types/generate.js"; import { createCommand } from "./commands/project/create.js"; import { dashboardCommand } from "./commands/project/dashboard.js"; import { deployCommand } from "./commands/project/deploy.js"; @@ -44,8 +44,8 @@ program.addCommand(entitiesPushCommand); // Register functions commands program.addCommand(functionsDeployCommand); -// Register generate commands -program.addCommand(generateTypesCommand); +// Register types commands +program.addCommand(typesGenerateCommand); // Register site commands program.addCommand(siteDeployCommand); diff --git a/src/core/generate/generator.ts b/src/core/generate/generator.ts index 6737a8a1..87c2fe94 100644 --- a/src/core/generate/generator.ts +++ b/src/core/generate/generator.ts @@ -1,7 +1,7 @@ import type { EntityDefinition, EntityField } from "./schema.js"; const HEADER = `// Auto-generated by Base44 CLI - DO NOT EDIT -// Regenerate with: base44 generate +// Regenerate with: base44 types `; /** From 556e20bf4e6fa7981bd915bab9c086e7b3a67461 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Jan 2026 15:54:02 +0000 Subject: [PATCH 3/8] docs: Add documentation for `base44 types` command - Document the types command and its options - Add TypeScript type generation section with usage examples - Update project structure to show generated types directory --- README.md | 94 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 93 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 972009ad..55ebc242 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,93 @@ base44 deploy |---------|-------------| | `base44 site deploy` | Deploy built site files to Base44 hosting | +### Types + +| Command | Description | +|---------|-------------| +| `base44 types` | Generate TypeScript types from entity schemas | + +**Options:** +- `-o, --output ` - Output directory (default: `src/base44`) +- `--entities-only` - Only generate entity types, skip client types + +## TypeScript Type Generation + +Generate fully-typed interfaces from your entity schemas for type-safe SDK usage. + +### Usage + +```bash +# Generate types (outputs to src/base44/) +base44 types + +# Custom output directory +base44 types --output ./types +``` + +### Generated Files + +| File | Contents | +|------|----------| +| `entities.ts` | Entity interfaces, CreateInput, UpdateInput, Filter types | +| `client.ts` | Typed SDK client interface | +| `index.ts` | Barrel exports | + +### Setup with @base44/sdk + +1. Generate types: + ```bash + base44 types + ``` + +2. Add to `tsconfig.json`: + ```json + { + "include": ["src", "src/base44/entities.ts"] + } + ``` + +3. Use in your code: + ```typescript + import { createClient } from '@base44/sdk'; + import type { TypedBase44Client } from './base44/client'; + + const base44 = createClient({ appId: 'my-app' }) as TypedBase44Client; + + // Fully typed! + const { items: tasks } = await base44.entities.Task.list(); + await base44.entities.Task.create({ title: 'Buy milk' }); + ``` + +### Example + +Given an entity schema: +```jsonc +// base44/entities/task.jsonc +{ + "name": "Task", + "type": "object", + "properties": { + "title": { "type": "string", "description": "Task title" }, + "completed": { "type": "boolean", "default": false } + }, + "required": ["title"] +} +``` + +Generated types: +```typescript +export interface Task extends BaseEntity { + title: string; + completed?: boolean; +} + +export interface TaskCreateInput { + title: string; + completed?: boolean; +} +``` + ## Configuration ### Project Configuration @@ -115,7 +202,12 @@ my-project/ │ └── my-function/ │ ├── config.jsonc │ └── index.js -├── src/ # Your frontend code +├── src/ +│ ├── base44/ # Generated types (from `base44 types`) +│ │ ├── entities.ts +│ │ ├── client.ts +│ │ └── index.ts +│ └── ... # Your frontend code ├── dist/ # Built site files (for deployment) └── package.json ``` From f69d90f303cef903523c4d321e87e0c65993d45b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Jan 2026 19:33:55 +0000 Subject: [PATCH 4/8] refactor: Rename core/generate module to core/types Consistent naming with the `base44 types` command. --- src/cli/commands/types/generate.ts | 2 +- src/core/index.ts | 2 +- src/core/{generate => types}/generator.ts | 0 src/core/{generate => types}/index.ts | 0 src/core/{generate => types}/schema.ts | 0 src/core/{generate => types}/types.ts | 0 6 files changed, 2 insertions(+), 2 deletions(-) rename src/core/{generate => types}/generator.ts (100%) rename src/core/{generate => types}/index.ts (100%) rename src/core/{generate => types}/schema.ts (100%) rename src/core/{generate => types}/types.ts (100%) diff --git a/src/cli/commands/types/generate.ts b/src/cli/commands/types/generate.ts index 9c395a67..64df44ae 100644 --- a/src/cli/commands/types/generate.ts +++ b/src/cli/commands/types/generate.ts @@ -1,6 +1,6 @@ import { Command } from "commander"; import { log } from "@clack/prompts"; -import { generateTypes } from "@core/generate/index.js"; +import { generateTypes } from "@core/types/index.js"; import { runCommand, runTask } from "../../utils/index.js"; import type { RunCommandResult } from "../../utils/runCommand.js"; import { relative } from "node:path"; diff --git a/src/core/index.ts b/src/core/index.ts index da3356b6..1e5cf6eb 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -3,7 +3,7 @@ export * from "./resources/index.js"; export * from "./project/index.js"; export * from "./clients/index.js"; export * from "./site/index.js"; -export * from "./generate/index.js"; +export * from "./types/index.js"; export * from "./utils/index.js"; export * from "./errors.js"; export * from "./consts.js"; diff --git a/src/core/generate/generator.ts b/src/core/types/generator.ts similarity index 100% rename from src/core/generate/generator.ts rename to src/core/types/generator.ts diff --git a/src/core/generate/index.ts b/src/core/types/index.ts similarity index 100% rename from src/core/generate/index.ts rename to src/core/types/index.ts diff --git a/src/core/generate/schema.ts b/src/core/types/schema.ts similarity index 100% rename from src/core/generate/schema.ts rename to src/core/types/schema.ts diff --git a/src/core/generate/types.ts b/src/core/types/types.ts similarity index 100% rename from src/core/generate/types.ts rename to src/core/types/types.ts From 1c7f824b199f0992cfe5757fe6d70944a526a1bf Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Jan 2026 12:16:54 +0000 Subject: [PATCH 5/8] feat: Improve JSON Schema to TypeScript type mapping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add support for `integer` type (maps to `number`) - Add support for `null` type - Add support for union types: `["string", "null"]` → `string | null` - Support mixed enum values (string, number, boolean, null) - Add `format` field support in schema - Fix inline object generation for nested properties --- src/core/types/generator.ts | 135 ++++++++++++++++++++++++------------ src/core/types/schema.ts | 16 ++++- 2 files changed, 103 insertions(+), 48 deletions(-) diff --git a/src/core/types/generator.ts b/src/core/types/generator.ts index 87c2fe94..2cae4443 100644 --- a/src/core/types/generator.ts +++ b/src/core/types/generator.ts @@ -5,53 +5,98 @@ const HEADER = `// Auto-generated by Base44 CLI - DO NOT EDIT `; /** - * Convert a JSON Schema type to TypeScript type + * Convert a single JSON Schema type to TypeScript type */ -function fieldToTypeScript(field: EntityField, required: boolean): string { - let tsType: string; +function mapSingleType(type: string): string { + switch (type) { + case "string": + return "string"; + case "number": + case "integer": + return "number"; + case "boolean": + return "boolean"; + case "null": + return "null"; + case "object": + return "Record"; + case "array": + return "unknown[]"; + default: + return "unknown"; + } +} - // Handle enums +/** + * Convert a JSON Schema type to TypeScript type + */ +function fieldToTypeScript(field: EntityField): string { + // Handle enums first (takes precedence) if (field.enum && field.enum.length > 0) { - tsType = field.enum.map((v) => `'${v}'`).join(" | "); - } else { - switch (field.type) { - case "string": - tsType = "string"; - break; - case "number": - tsType = "number"; - break; - case "boolean": - tsType = "boolean"; - break; - case "array": - if (field.items) { - const itemType = fieldToTypeScript(field.items, true); - tsType = `${itemType}[]`; - } else { - tsType = "unknown[]"; - } - break; - case "object": - if (field.properties) { - const props = Object.entries(field.properties) - .map(([name, prop]) => { - const isRequired = field.required?.includes(name) ?? false; - const propType = fieldToTypeScript(prop, isRequired); - return `${name}${isRequired ? "" : "?"}: ${propType}`; - }) - .join("; "); - tsType = `{ ${props} }`; - } else { - tsType = "Record"; - } - break; - default: - tsType = "unknown"; - } + return field.enum.map((v) => + typeof v === "string" ? `'${v}'` : String(v) + ).join(" | "); + } + + // Handle union types: { "type": ["string", "null"] } + if (Array.isArray(field.type)) { + const types = field.type.map((t) => { + if (t === "array" && field.items) { + const itemType = fieldToTypeScript(field.items as EntityField); + return `${itemType}[]`; + } + if (t === "object" && field.properties) { + return generateInlineObject(field); + } + return mapSingleType(t); + }); + return types.join(" | "); } - return tsType; + // Handle single type + switch (field.type) { + case "string": + return "string"; + case "number": + case "integer": + return "number"; + case "boolean": + return "boolean"; + case "null": + return "null"; + case "array": + if (field.items) { + const itemType = fieldToTypeScript(field.items as EntityField); + return `${itemType}[]`; + } + return "unknown[]"; + case "object": + if (field.properties) { + return generateInlineObject(field); + } + return "Record"; + default: + return "unknown"; + } +} + +/** + * Generate an inline object type from field properties + */ +function generateInlineObject(field: EntityField): string { + if (!field.properties) { + return "Record"; + } + + const requiredFields = new Set(field.required ?? []); + const props = Object.entries(field.properties) + .map(([name, prop]) => { + const isRequired = requiredFields.has(name); + const propType = fieldToTypeScript(prop as EntityField); + return `${name}${isRequired ? "" : "?"}: ${propType}`; + }) + .join("; "); + return `{ ${props} }`; } /** @@ -72,10 +117,10 @@ function generateInterfaceFields( const isRequired = options.allOptional ? false : requiredFields.has(fieldName); - const tsType = fieldToTypeScript(field, isRequired); + const tsType = fieldToTypeScript(field as EntityField); const optional = isRequired ? "" : "?"; - const description = field.description - ? ` /** ${field.description} */\n` + const description = (field as EntityField).description + ? ` /** ${(field as EntityField).description} */\n` : ""; return `${description} ${fieldName}${optional}: ${tsType};`; }) diff --git a/src/core/types/schema.ts b/src/core/types/schema.ts index cfdea9bd..c7519b37 100644 --- a/src/core/types/schema.ts +++ b/src/core/types/schema.ts @@ -1,16 +1,26 @@ import { z } from "zod"; +/** JSON Schema primitive types */ +export type JsonSchemaType = "string" | "number" | "integer" | "boolean" | "array" | "object" | "null"; + /** - * Entity field type definition (used for type generation, not strict validation) + * Entity field type definition (JSON Schema compatible) + * Supports both single types and union types (e.g., ["string", "null"]) */ export interface EntityField { - type: "string" | "number" | "boolean" | "array" | "object"; + type: JsonSchemaType | JsonSchemaType[]; description?: string; default?: unknown; - enum?: string[]; + /** Enum values - can be strings, numbers, or mixed */ + enum?: (string | number | boolean | null)[]; + /** For array types - schema of array items */ items?: EntityField; + /** For object types - nested property schemas */ properties?: Record; + /** For object types - list of required property names */ required?: string[]; + /** JSON Schema format hint (date, date-time, email, uri, etc.) */ + format?: string; [key: string]: unknown; } From e42c9f10e092e2c7ff9e077f4c0ecdf0460724f5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Jan 2026 12:24:13 +0000 Subject: [PATCH 6/8] refactor: Use json-schema-to-typescript library for type generation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace in-house JSON Schema → TypeScript conversion with the battle-tested json-schema-to-typescript library. Benefits: - Full JSON Schema draft-07 support - Handles $ref, allOf, anyOf, oneOf - Proper JSDoc generation from descriptions - Better handling of complex nested types --- package-lock.json | 80 +++++++++++++-- package.json | 7 +- src/core/types/generator.ts | 199 ++++++++++++------------------------ src/core/types/types.ts | 2 +- 4 files changed, 140 insertions(+), 148 deletions(-) diff --git a/package-lock.json b/package-lock.json index a37a8861..260cf39a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "base44", "version": "0.0.15", "license": "ISC", + "dependencies": { + "json-schema-to-typescript": "^15.0.4" + }, "bin": { "base44": "dist/cli/index.js" }, @@ -46,6 +49,23 @@ "node": ">=20.19.0" } }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "11.9.3", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.9.3.tgz", + "integrity": "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==", + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + } + }, "node_modules/@babel/generator": { "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", @@ -918,6 +938,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "license": "MIT" + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", @@ -1676,7 +1702,6 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, "license": "MIT" }, "node_modules/@types/json5": { @@ -1690,7 +1715,6 @@ "version": "4.17.23", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.23.tgz", "integrity": "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==", - "dev": true, "license": "MIT" }, "node_modules/@types/lodash.kebabcase": { @@ -2135,7 +2159,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/array-buffer-byte-length": { @@ -3671,7 +3694,6 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -4428,7 +4450,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4474,7 +4495,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -4827,7 +4847,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -4856,6 +4875,29 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-to-typescript": { + "version": "15.0.4", + "resolved": "https://registry.npmjs.org/json-schema-to-typescript/-/json-schema-to-typescript-15.0.4.tgz", + "integrity": "sha512-Su9oK8DR4xCmDsLlyvadkXzX6+GGXJpbhwoLtOGArAG61dvbW4YQmSEno2y66ahpIdmLMg6YUf/QHLgiwvkrHQ==", + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^11.5.5", + "@types/json-schema": "^7.0.15", + "@types/lodash": "^4.17.7", + "is-glob": "^4.0.3", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "prettier": "^3.2.5", + "tinyglobby": "^0.2.9" + }, + "bin": { + "json2ts": "dist/src/cli.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -4936,6 +4978,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, "node_modules/lodash.kebabcase": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", @@ -5027,7 +5075,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5420,7 +5467,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -5501,6 +5547,21 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-ms": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", @@ -6325,7 +6386,6 @@ "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", diff --git a/package.json b/package.json index 73cd9ccf..7ad3a61f 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "json5": "^2.2.3", "ky": "^1.14.2", "lodash.kebabcase": "^4.1.1", + "open": "^11.0.0", "p-wait-for": "^6.0.0", "tar": "^7.5.4", "tsdown": "^0.12.4", @@ -60,10 +61,12 @@ "typescript": "^5.7.2", "typescript-eslint": "^8.52.0", "vitest": "^4.0.16", - "zod": "^4.3.5", - "open": "^11.0.0" + "zod": "^4.3.5" }, "engines": { "node": ">=20.19.0" + }, + "dependencies": { + "json-schema-to-typescript": "^15.0.4" } } diff --git a/src/core/types/generator.ts b/src/core/types/generator.ts index 2cae4443..c56bc6e6 100644 --- a/src/core/types/generator.ts +++ b/src/core/types/generator.ts @@ -1,169 +1,98 @@ -import type { EntityDefinition, EntityField } from "./schema.js"; +import { compile } from "json-schema-to-typescript"; +import type { EntityDefinition } from "./schema.js"; const HEADER = `// Auto-generated by Base44 CLI - DO NOT EDIT // Regenerate with: base44 types `; -/** - * Convert a single JSON Schema type to TypeScript type - */ -function mapSingleType(type: string): string { - switch (type) { - case "string": - return "string"; - case "number": - case "integer": - return "number"; - case "boolean": - return "boolean"; - case "null": - return "null"; - case "object": - return "Record"; - case "array": - return "unknown[]"; - default: - return "unknown"; - } -} +/** Options for json-schema-to-typescript */ +const COMPILE_OPTIONS = { + bannerComment: "", + additionalProperties: false, + strictIndexSignatures: true, + enableConstEnums: true, + declareExternallyReferenced: false, +}; /** - * Convert a JSON Schema type to TypeScript type + * Generate TypeScript interface from JSON Schema using json-schema-to-typescript */ -function fieldToTypeScript(field: EntityField): string { - // Handle enums first (takes precedence) - if (field.enum && field.enum.length > 0) { - return field.enum.map((v) => - typeof v === "string" ? `'${v}'` : String(v) - ).join(" | "); - } +async function generateEntityInterface(entity: EntityDefinition): Promise { + // Add title for the interface name + const schema = { + ...entity, + title: entity.name, + $schema: "http://json-schema.org/draft-07/schema#", + }; - // Handle union types: { "type": ["string", "null"] } - if (Array.isArray(field.type)) { - const types = field.type.map((t) => { - if (t === "array" && field.items) { - const itemType = fieldToTypeScript(field.items as EntityField); - return `${itemType}[]`; - } - if (t === "object" && field.properties) { - return generateInlineObject(field); - } - return mapSingleType(t); - }); - return types.join(" | "); - } + const result = await compile(schema, entity.name, COMPILE_OPTIONS); - // Handle single type - switch (field.type) { - case "string": - return "string"; - case "number": - case "integer": - return "number"; - case "boolean": - return "boolean"; - case "null": - return "null"; - case "array": - if (field.items) { - const itemType = fieldToTypeScript(field.items as EntityField); - return `${itemType}[]`; - } - return "unknown[]"; - case "object": - if (field.properties) { - return generateInlineObject(field); - } - return "Record"; - default: - return "unknown"; - } + // Modify to extend BaseEntity + return result.replace( + `export interface ${entity.name} {`, + `export interface ${entity.name} extends BaseEntity {` + ); } /** - * Generate an inline object type from field properties + * Generate CreateInput type (same as entity but without system fields) */ -function generateInlineObject(field: EntityField): string { - if (!field.properties) { - return "Record"; - } +async function generateCreateInput(entity: EntityDefinition): Promise { + const schema = { + ...entity, + title: `${entity.name}CreateInput`, + $schema: "http://json-schema.org/draft-07/schema#", + }; - const requiredFields = new Set(field.required ?? []); - const props = Object.entries(field.properties) - .map(([name, prop]) => { - const isRequired = requiredFields.has(name); - const propType = fieldToTypeScript(prop as EntityField); - return `${name}${isRequired ? "" : "?"}: ${propType}`; - }) - .join("; "); - return `{ ${props} }`; + return await compile(schema, `${entity.name}CreateInput`, COMPILE_OPTIONS); } /** - * Generate interface fields for an entity + * Generate UpdateInput type (all fields optional) */ -function generateInterfaceFields( - entity: EntityDefinition, - options: { allOptional?: boolean } = {} -): string { - if (!entity.properties) { - return ""; - } +async function generateUpdateInput(entity: EntityDefinition): Promise { + const schema = { + ...entity, + title: `${entity.name}UpdateInput`, + required: [], // All fields optional for updates + $schema: "http://json-schema.org/draft-07/schema#", + }; - const requiredFields = new Set(entity.required ?? []); - - return Object.entries(entity.properties) - .map(([fieldName, field]) => { - const isRequired = options.allOptional - ? false - : requiredFields.has(fieldName); - const tsType = fieldToTypeScript(field as EntityField); - const optional = isRequired ? "" : "?"; - const description = (field as EntityField).description - ? ` /** ${(field as EntityField).description} */\n` - : ""; - return `${description} ${fieldName}${optional}: ${tsType};`; - }) - .join("\n"); + return await compile(schema, `${entity.name}UpdateInput`, COMPILE_OPTIONS); } /** - * Generate TypeScript interfaces for a single entity + * Generate Filter type (all fields optional for querying) */ -function generateEntityTypes(entity: EntityDefinition): string { - const name = entity.name; - const fields = generateInterfaceFields(entity); - const createFields = generateInterfaceFields(entity); - const updateFields = generateInterfaceFields(entity, { allOptional: true }); - const filterFields = generateInterfaceFields(entity, { allOptional: true }); +async function generateFilterType(entity: EntityDefinition): Promise { + const schema = { + ...entity, + title: `${entity.name}Filter`, + required: [], // All fields optional for filters + $schema: "http://json-schema.org/draft-07/schema#", + }; - return ` -/** ${name} entity */ -export interface ${name} extends BaseEntity { -${fields} + return await compile(schema, `${entity.name}Filter`, COMPILE_OPTIONS); } -/** Input for creating a ${name} */ -export interface ${name}CreateInput { -${createFields} -} - -/** Input for updating a ${name} (all fields optional) */ -export interface ${name}UpdateInput { -${updateFields} -} +/** + * Generate all types for a single entity + */ +async function generateEntityTypes(entity: EntityDefinition): Promise { + const [mainInterface, createInput, updateInput, filterType] = await Promise.all([ + generateEntityInterface(entity), + generateCreateInput(entity), + generateUpdateInput(entity), + generateFilterType(entity), + ]); -/** Filter query for ${name} */ -export interface ${name}Filter { -${filterFields} -} -`; + return `${mainInterface}\n${createInput}\n${updateInput}\n${filterType}`; } /** * Generate the entities.ts file content */ -export function generateEntitiesFile(entities: EntityDefinition[]): string { +export async function generateEntitiesFile(entities: EntityDefinition[]): Promise { const baseEntity = ` /** System fields present on all entities */ export interface BaseEntity { @@ -173,7 +102,7 @@ export interface BaseEntity { } `; - const entityTypes = entities.map(generateEntityTypes).join("\n"); + const entityTypes = await Promise.all(entities.map(generateEntityTypes)); // Generate union type of all entity names const entityNames = entities.map((e) => `'${e.name}'`).join(" | "); @@ -181,7 +110,7 @@ export interface BaseEntity { ? `\n/** All entity names in this project */\nexport type EntityName = ${entityNames};\n` : ""; - return HEADER + baseEntity + entityTypes + entityNamesType; + return HEADER + baseEntity + entityTypes.join("\n") + entityNamesType; } /** diff --git a/src/core/types/types.ts b/src/core/types/types.ts index c359367b..a55733e8 100644 --- a/src/core/types/types.ts +++ b/src/core/types/types.ts @@ -73,7 +73,7 @@ export async function generateTypes( const files: string[] = []; // Generate entities file - const entitiesContent = generateEntitiesFile(parsedEntities); + const entitiesContent = await generateEntitiesFile(parsedEntities); const entitiesPath = join(outputDir, "entities.ts"); await writeFile(entitiesPath, entitiesContent, "utf-8"); files.push(entitiesPath); From c1be40cce90e33bc4c6d34e80b2441f439ba4cc9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Jan 2026 18:31:15 +0000 Subject: [PATCH 7/8] feat: Generate .d.ts files with module augmentation for Node.js and Deno - Output types.d.ts for Node.js (declare module '@base44/sdk') - Output types.deno.d.ts for Deno (declare module 'npm:@base44/sdk') - Default output directory changed to base44/ (alongside entity schemas) - User code remains unchanged - types are injected via module augmentation - Add --node-only flag to skip Deno types generation --- src/cli/commands/types/generate.ts | 10 +- src/core/types/generator.ts | 215 ++++++++--------------------- src/core/types/index.ts | 2 +- src/core/types/types.ts | 56 ++++---- 4 files changed, 89 insertions(+), 194 deletions(-) diff --git a/src/cli/commands/types/generate.ts b/src/cli/commands/types/generate.ts index 64df44ae..e1258e3a 100644 --- a/src/cli/commands/types/generate.ts +++ b/src/cli/commands/types/generate.ts @@ -7,7 +7,7 @@ import { relative } from "node:path"; interface GenerateTypesOptions { output?: string; - entitiesOnly?: boolean; + nodeOnly?: boolean; } async function generateTypesAction( @@ -18,7 +18,7 @@ async function generateTypesAction( async () => { return await generateTypes({ output: options.output, - entitiesOnly: options.entitiesOnly, + nodeOnly: options.nodeOnly, }); }, { @@ -44,9 +44,9 @@ async function generateTypesAction( } export const typesGenerateCommand = new Command("types") - .description("Generate TypeScript types from entity schemas") - .option("-o, --output ", "Output directory", "src/base44") - .option("--entities-only", "Only generate entity types, skip client types") + .description("Generate TypeScript declaration files from entity schemas") + .option("-o, --output ", "Output directory (default: base44)") + .option("--node-only", "Only generate Node.js types, skip Deno types") .action(async (options: GenerateTypesOptions) => { await runCommand(() => generateTypesAction(options), { requireAuth: false, diff --git a/src/core/types/generator.ts b/src/core/types/generator.ts index c56bc6e6..c125e732 100644 --- a/src/core/types/generator.ts +++ b/src/core/types/generator.ts @@ -1,8 +1,21 @@ import { compile } from "json-schema-to-typescript"; import type { EntityDefinition } from "./schema.js"; -const HEADER = `// Auto-generated by Base44 CLI - DO NOT EDIT +const HEADER_NODE = `// Auto-generated by Base44 CLI - DO NOT EDIT // Regenerate with: base44 types +// +// Setup: Add to tsconfig.json: +// { "include": ["src", "base44/types.d.ts"] } +`; + +const HEADER_DENO = `// Auto-generated by Base44 CLI - DO NOT EDIT +// Regenerate with: base44 types +// +// Setup: Add to deno.json: +// { "compilerOptions": { "types": ["./base44/types.deno.d.ts"] } } +// +// Or add to your entry file: +// /// `; /** Options for json-schema-to-typescript */ @@ -15,194 +28,78 @@ const COMPILE_OPTIONS = { }; /** - * Generate TypeScript interface from JSON Schema using json-schema-to-typescript + * Generate TypeScript interface from JSON Schema */ async function generateEntityInterface(entity: EntityDefinition): Promise { - // Add title for the interface name const schema = { ...entity, title: entity.name, $schema: "http://json-schema.org/draft-07/schema#", }; - const result = await compile(schema, entity.name, COMPILE_OPTIONS); - - // Modify to extend BaseEntity - return result.replace( - `export interface ${entity.name} {`, - `export interface ${entity.name} extends BaseEntity {` - ); -} - -/** - * Generate CreateInput type (same as entity but without system fields) - */ -async function generateCreateInput(entity: EntityDefinition): Promise { - const schema = { - ...entity, - title: `${entity.name}CreateInput`, - $schema: "http://json-schema.org/draft-07/schema#", - }; - - return await compile(schema, `${entity.name}CreateInput`, COMPILE_OPTIONS); + return await compile(schema, entity.name, COMPILE_OPTIONS); } /** - * Generate UpdateInput type (all fields optional) + * Generate all entity interfaces */ -async function generateUpdateInput(entity: EntityDefinition): Promise { - const schema = { - ...entity, - title: `${entity.name}UpdateInput`, - required: [], // All fields optional for updates - $schema: "http://json-schema.org/draft-07/schema#", - }; - - return await compile(schema, `${entity.name}UpdateInput`, COMPILE_OPTIONS); +async function generateEntityInterfaces(entities: EntityDefinition[]): Promise { + const interfaces = await Promise.all(entities.map(generateEntityInterface)); + return interfaces.join("\n"); } /** - * Generate Filter type (all fields optional for querying) + * Generate the module augmentation block */ -async function generateFilterType(entity: EntityDefinition): Promise { - const schema = { - ...entity, - title: `${entity.name}Filter`, - required: [], // All fields optional for filters - $schema: "http://json-schema.org/draft-07/schema#", - }; +function generateModuleAugmentation( + entities: EntityDefinition[], + moduleName: string +): string { + if (entities.length === 0) { + return ""; + } - return await compile(schema, `${entity.name}Filter`, COMPILE_OPTIONS); -} + const registryEntries = entities + .map((e) => ` ${e.name}: ${e.name};`) + .join("\n"); -/** - * Generate all types for a single entity - */ -async function generateEntityTypes(entity: EntityDefinition): Promise { - const [mainInterface, createInput, updateInput, filterType] = await Promise.all([ - generateEntityInterface(entity), - generateCreateInput(entity), - generateUpdateInput(entity), - generateFilterType(entity), - ]); - - return `${mainInterface}\n${createInput}\n${updateInput}\n${filterType}`; -} + return ` +// ─── SDK Type Augmentation ─────────────────────────────────────── -/** - * Generate the entities.ts file content - */ -export async function generateEntitiesFile(entities: EntityDefinition[]): Promise { - const baseEntity = ` -/** System fields present on all entities */ -export interface BaseEntity { - id: string; - created_date: string; - updated_date: string; +declare module '${moduleName}' { + interface EntityTypeRegistry { +${registryEntries} + } } `; - - const entityTypes = await Promise.all(entities.map(generateEntityTypes)); - - // Generate union type of all entity names - const entityNames = entities.map((e) => `'${e.name}'`).join(" | "); - const entityNamesType = entities.length > 0 - ? `\n/** All entity names in this project */\nexport type EntityName = ${entityNames};\n` - : ""; - - return HEADER + baseEntity + entityTypes.join("\n") + entityNamesType; } /** - * Generate the client.ts file content with typed SDK wrapper + * Generate types.d.ts for Node.js + * Uses: declare module '@base44/sdk' */ -export function generateClientFile(entities: EntityDefinition[]): string { - if (entities.length === 0) { - return HEADER + "\n// No entities found\nexport {};\n"; - } - - // Generate imports - const imports = entities - .map((e) => `${e.name}, ${e.name}CreateInput, ${e.name}UpdateInput, ${e.name}Filter`) - .join(",\n "); - - // Generate entity handlers - const handlers = entities - .map( - (e) => - ` ${e.name}: EntityHandler<${e.name}, ${e.name}CreateInput, ${e.name}UpdateInput, ${e.name}Filter>;` - ) - .join("\n"); +export async function generateNodeTypes(entities: EntityDefinition[]): Promise { + const entityInterfaces = await generateEntityInterfaces(entities); + const moduleAugmentation = generateModuleAugmentation(entities, "@base44/sdk"); - return `${HEADER} -import type { - ${imports} -} from './entities.js'; + return `${HEADER_NODE} +// ─── Entity Types ──────────────────────────────────────────────── -/** Response for list/filter operations */ -export interface ListResponse { - items: T[]; - total: number; -} - -/** Real-time subscription event */ -export interface RealtimeEvent { - type: 'create' | 'update' | 'delete'; - data: T; - id: string; - timestamp: string; -} - -/** Entity handler with CRUD operations */ -export interface EntityHandler { - /** Get all records with optional pagination */ - list(sort?: string, limit?: number, skip?: number, fields?: (keyof T)[]): Promise>; - /** Get records matching filter criteria */ - filter(query: TFilter, sort?: string, limit?: number, skip?: number, fields?: (keyof T)[]): Promise>; - /** Get a single record by ID */ - get(id: string): Promise; - /** Create a new record */ - create(data: TCreate): Promise; - /** Update an existing record */ - update(id: string, data: TUpdate): Promise; - /** Delete a record */ - delete(id: string): Promise; - /** Delete multiple records matching filter */ - deleteMany(query: TFilter): Promise; - /** Create multiple records at once */ - bulkCreate(data: TCreate[]): Promise; - /** Subscribe to real-time updates */ - subscribe(callback: (event: RealtimeEvent) => void): () => void; -} - -/** Typed entities interface for Base44 SDK */ -export interface TypedEntities { -${handlers} +${entityInterfaces} +${moduleAugmentation}`; } /** - * Typed Base44 client interface. - * Use with the Base44 SDK for type-safe entity access. - * - * @example - * import { createClient } from '@base44/sdk'; - * import type { TypedBase44Client } from './base44/client'; - * - * const base44 = createClient({ appId: 'your-app-id' }) as TypedBase44Client; - * const tasks = await base44.entities.Task.list(); + * Generate types.deno.d.ts for Deno + * Uses: declare module 'npm:@base44/sdk' */ -export interface TypedBase44Client { - entities: TypedEntities; -} -`; -} +export async function generateDenoTypes(entities: EntityDefinition[]): Promise { + const entityInterfaces = await generateEntityInterfaces(entities); + const moduleAugmentation = generateModuleAugmentation(entities, "npm:@base44/sdk"); -/** - * Generate the index.ts barrel file - */ -export function generateIndexFile(): string { - return `${HEADER} -export * from './entities.js'; -export * from './client.js'; -`; + return `${HEADER_DENO} +// ─── Entity Types ──────────────────────────────────────────────── + +${entityInterfaces} +${moduleAugmentation}`; } diff --git a/src/core/types/index.ts b/src/core/types/index.ts index 21b4a404..4484f023 100644 --- a/src/core/types/index.ts +++ b/src/core/types/index.ts @@ -1,3 +1,3 @@ export { generateTypes, type GenerateOptions, type GenerateResult } from "./types.js"; -export { generateEntitiesFile, generateClientFile, generateIndexFile } from "./generator.js"; +export { generateNodeTypes, generateDenoTypes } from "./generator.js"; export { EntityDefinitionSchema, type EntityDefinition, type EntityField } from "./schema.js"; diff --git a/src/core/types/types.ts b/src/core/types/types.ts index a55733e8..a7914c74 100644 --- a/src/core/types/types.ts +++ b/src/core/types/types.ts @@ -2,17 +2,13 @@ import { join, dirname } from "node:path"; import { mkdir, writeFile } from "node:fs/promises"; import { readProjectConfig, findProjectRoot } from "../project/config.js"; import { EntityDefinitionSchema, type EntityDefinition } from "./schema.js"; -import { - generateEntitiesFile, - generateClientFile, - generateIndexFile, -} from "./generator.js"; +import { generateNodeTypes, generateDenoTypes } from "./generator.js"; export interface GenerateOptions { - /** Output directory relative to project root (default: "src/base44") */ + /** Output directory relative to project root (default: "base44") */ output?: string; - /** Only generate entity types, skip client types */ - entitiesOnly?: boolean; + /** Skip generating Deno types */ + nodeOnly?: boolean; } export interface GenerateResult { @@ -36,19 +32,27 @@ function parseEntities(entities: Record[]): EntityDefinition[] } // Skip invalid entities but log warning with error details const entityName = (entity as { name?: string }).name ?? "unknown"; - console.warn(`Skipping invalid entity "${entityName}": ${result.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join(', ')}`); + console.warn( + `Skipping invalid entity "${entityName}": ${result.error.issues + .map((i) => `${i.path.join(".")}: ${i.message}`) + .join(", ")}` + ); return null; }) .filter((e): e is EntityDefinition => e !== null); } /** - * Generate TypeScript types from local entity schemas + * Generate TypeScript declaration files from local entity schemas. + * + * Generates: + * - types.d.ts - For Node.js (declare module '@base44/sdk') + * - types.deno.d.ts - For Deno (declare module 'npm:@base44/sdk') */ export async function generateTypes( options: GenerateOptions = {} ): Promise { - const { output = "src/base44", entitiesOnly = false } = options; + const { output = "base44", nodeOnly = false } = options; // Find project root const projectRoot = await findProjectRoot(); @@ -64,7 +68,7 @@ export async function generateTypes( // Parse entities into our schema format const parsedEntities = parseEntities(entities); - // Determine output directory + // Determine output directory (base44/ by default) const outputDir = join(projectRoot.root, output); // Ensure output directory exists @@ -72,24 +76,18 @@ export async function generateTypes( const files: string[] = []; - // Generate entities file - const entitiesContent = await generateEntitiesFile(parsedEntities); - const entitiesPath = join(outputDir, "entities.ts"); - await writeFile(entitiesPath, entitiesContent, "utf-8"); - files.push(entitiesPath); + // Generate Node.js types.d.ts + const nodeTypesContent = await generateNodeTypes(parsedEntities); + const nodeTypesPath = join(outputDir, "types.d.ts"); + await writeFile(nodeTypesPath, nodeTypesContent, "utf-8"); + files.push(nodeTypesPath); - // Generate client file (unless entities-only) - if (!entitiesOnly) { - const clientContent = generateClientFile(parsedEntities); - const clientPath = join(outputDir, "client.ts"); - await writeFile(clientPath, clientContent, "utf-8"); - files.push(clientPath); - - // Generate index file - const indexContent = generateIndexFile(); - const indexPath = join(outputDir, "index.ts"); - await writeFile(indexPath, indexContent, "utf-8"); - files.push(indexPath); + // Generate Deno types.deno.d.ts (unless node-only) + if (!nodeOnly) { + const denoTypesContent = await generateDenoTypes(parsedEntities); + const denoTypesPath = join(outputDir, "types.deno.d.ts"); + await writeFile(denoTypesPath, denoTypesContent, "utf-8"); + files.push(denoTypesPath); } return { From 2a72c0ce20288acee7f18dc6cb9d4d77002fdc32 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Feb 2026 14:47:38 +0000 Subject: [PATCH 8/8] docs: Add comprehensive documentation for `base44 types` command Document the full product specification including: - Command usage and options - Generated file structure for Node.js and Deno - Setup instructions for tsconfig.json and deno.json - User experience walkthrough with examples - JSON Schema to TypeScript type mapping reference - SDK requirements for module augmentation support - Recommended workflow for type generation --- docs/types-command.md | 307 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 307 insertions(+) create mode 100644 docs/types-command.md diff --git a/docs/types-command.md b/docs/types-command.md new file mode 100644 index 00000000..0d39e0ee --- /dev/null +++ b/docs/types-command.md @@ -0,0 +1,307 @@ +# `base44 types` Command + +Generate TypeScript declaration files from entity JSON schemas for type-safe SDK usage. + +## Overview + +The `base44 types` command reads your local entity schemas and generates `.d.ts` files that augment the `@base44/sdk` types. This enables full TypeScript support with **zero changes to your code**. + +## Command + +```bash +base44 types # Generate types for Node.js and Deno +base44 types --node-only # Skip Deno types +base44 types -o ./custom # Custom output directory +``` + +## Generated Files + +| File | Target | Module Augmentation | +|------|--------|---------------------| +| `base44/types.d.ts` | Node.js | `declare module '@base44/sdk'` | +| `base44/types.deno.d.ts` | Deno | `declare module 'npm:@base44/sdk'` | + +Both files should be **gitignored** (they are generated artifacts). + +## Setup + +### Node.js + +Add to `tsconfig.json`: +```json +{ + "include": ["src", "base44/types.d.ts"] +} +``` + +Add to `.gitignore`: +```gitignore +base44/types.d.ts +base44/types.deno.d.ts +``` + +### Deno + +Add to `deno.json`: +```json +{ + "compilerOptions": { + "types": ["./base44/types.deno.d.ts"] + } +} +``` + +Or add a reference in your entry file: +```typescript +/// +``` + +## User Experience + +### 1. Define Entities + +```jsonc +// base44/entities/task.jsonc +{ + "name": "Task", + "type": "object", + "properties": { + "title": { "type": "string", "description": "Task title" }, + "completed": { "type": "boolean", "default": false }, + "priority": { "type": "string", "enum": ["low", "medium", "high"] }, + "dueDate": { "type": ["string", "null"] }, + "tags": { "type": "array", "items": { "type": "string" } } + }, + "required": ["title"] +} +``` + +### 2. Generate Types + +```bash +base44 types +``` + +Output: +``` +┌ Base 44 +│ +◇ Types generated successfully +│ +● Generated types for 1 entities +● Output directory: /my-project/base44 +● Files: +│ - base44/types.d.ts +│ - base44/types.deno.d.ts +│ +└ Types generated successfully! +``` + +### 3. Use in Code (Unchanged) + +```typescript +import { createClient } from '@base44/sdk'; + +const base44 = createClient({ appId: 'my-app' }); + +// ✅ Fully typed - no casting, no generics, no type imports +const { items } = await base44.entities.Task.list(); + +// ✅ Autocomplete works +items.forEach(task => { + console.log(task.title); // string + console.log(task.completed); // boolean | undefined + console.log(task.priority); // "low" | "medium" | "high" | undefined +}); + +// ✅ Type-safe create +await base44.entities.Task.create({ + title: 'Buy groceries', + priority: 'high' +}); + +// ❌ Compile error: 'titel' does not exist +await base44.entities.Task.create({ titel: 'typo' }); + +// ❌ Compile error: 'urgent' is not assignable to 'low' | 'medium' | 'high' +await base44.entities.Task.create({ title: 'Test', priority: 'urgent' }); +``` + +## Example Generated Types + +### Input: `base44/entities/task.jsonc` + +```jsonc +{ + "name": "Task", + "type": "object", + "properties": { + "title": { "type": "string", "description": "Task title" }, + "completed": { "type": "boolean", "default": false }, + "priority": { "type": "string", "enum": ["low", "medium", "high"] }, + "dueDate": { "type": ["string", "null"] }, + "tags": { "type": "array", "items": { "type": "string" } } + }, + "required": ["title"] +} +``` + +### Output: `base44/types.d.ts` (Node.js) + +```typescript +// Auto-generated by Base44 CLI - DO NOT EDIT +// Regenerate with: base44 types +// +// Setup: Add to tsconfig.json: +// { "include": ["src", "base44/types.d.ts"] } + +// ─── Entity Types ──────────────────────────────────────────────── + +export interface Task { + /** + * Task title + */ + title: string; + completed?: boolean; + priority?: "low" | "medium" | "high"; + dueDate?: string | null; + tags?: string[]; +} + +// ─── SDK Type Augmentation ─────────────────────────────────────── + +declare module '@base44/sdk' { + interface EntityTypeRegistry { + Task: Task; + } +} +``` + +### Output: `base44/types.deno.d.ts` (Deno) + +```typescript +// Auto-generated by Base44 CLI - DO NOT EDIT +// Regenerate with: base44 types +// +// Setup: Add to deno.json: +// { "compilerOptions": { "types": ["./base44/types.deno.d.ts"] } } +// +// Or add to your entry file: +// /// + +// ─── Entity Types ──────────────────────────────────────────────── + +export interface Task { + /** + * Task title + */ + title: string; + completed?: boolean; + priority?: "low" | "medium" | "high"; + dueDate?: string | null; + tags?: string[]; +} + +// ─── SDK Type Augmentation ─────────────────────────────────────── + +declare module 'npm:@base44/sdk' { + interface EntityTypeRegistry { + Task: Task; + } +} +``` + +## How It Works + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 1. Entity Schemas │ +│ base44/entities/task.jsonc │ +│ base44/entities/product.jsonc │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ 2. CLI Command │ +│ $ base44 types │ +│ │ +│ Uses json-schema-to-typescript library │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ 3. Generated .d.ts Files │ +│ base44/types.d.ts (Node.js) │ +│ base44/types.deno.d.ts (Deno) │ +│ │ +│ Contains: │ +│ - Entity interfaces │ +│ - declare module '@base44/sdk' { ... } │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ 4. TypeScript Picks Up Types │ +│ Via tsconfig.json include or deno.json types │ +│ │ +│ base44.entities.Task → EntityHandler │ +└─────────────────────────────────────────────────────────────────┘ +``` + +## JSON Schema to TypeScript Mapping + +| JSON Schema | TypeScript | +|-------------|------------| +| `"type": "string"` | `string` | +| `"type": "number"` | `number` | +| `"type": "integer"` | `number` | +| `"type": "boolean"` | `boolean` | +| `"type": "null"` | `null` | +| `"type": "array", "items": {...}` | `T[]` | +| `"type": "object", "properties": {...}` | `{ ... }` | +| `"type": ["string", "null"]` | `string \| null` | +| `"enum": ["a", "b", "c"]` | `"a" \| "b" \| "c"` | + +## SDK Requirements + +For module augmentation to work, the SDK needs these additions: + +```typescript +/** + * Base fields present on all entities. + */ +export interface BaseEntity { + id: string; + created_date: string; + updated_date: string; +} + +/** + * Entity type registry - augmented by generated .d.ts files. + */ +export interface EntityTypeRegistry {} + +/** + * Check if registry has been augmented. + */ +type HasRegistry = keyof EntityTypeRegistry extends never ? false : true; + +/** + * Entities module - typed when registry is augmented, dynamic otherwise. + */ +export type EntitiesModule = HasRegistry extends true + ? { [K in keyof EntityTypeRegistry]: EntityHandler } + : { [entityName: string]: EntityHandler }; +``` + +These SDK changes are **backward compatible** - existing code continues to work, and types "light up" once users generate and include the `.d.ts` files. + +## Workflow + +| Step | Action | Frequency | +|------|--------|-----------| +| 1 | Define entities in `base44/entities/*.jsonc` | When schema changes | +| 2 | Run `base44 types` | After schema changes | +| 3 | Add `.d.ts` to tsconfig/deno.json | Once | +| 4 | Add `.d.ts` to .gitignore | Once | +| 5 | Write code with full type support | Always |