UNPKG

@kodeme-io/next-core-codegen

Version:

TypeScript code generation utilities for next-core framework with React Query hooks and API client generation

1,565 lines (1,558 loc) 107 kB
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { get: (a, b) => (typeof require !== "undefined" ? require : a)[b] }) : x)(function(x) { if (typeof require !== "undefined") return require.apply(this, arguments); throw Error('Dynamic require of "' + x + '" is not supported'); }); // src/introspectors/odoo-introspector.ts var OdooIntrospector = class { constructor(odoo, options = {}) { this.odoo = odoo; this.options = options; this.defaultAttributes = [ "string", "type", "required", "readonly", "relation", "selection", "help", "size", "digits", "store", "compute", "related", "company_dependent" ]; } /** * Introspect models using native fields_get() * This works with ANY Odoo model out of the box! */ async introspectModels(modelNames, options = {}) { const schemas = {}; const mergedOptions = { ...this.options, ...options }; console.log(`\u{1F50D} Introspecting ${modelNames.length} models...`); for (const modelName of modelNames) { try { console.log(` \u2022 ${modelName}`); const schema = await this.introspectModel(modelName, mergedOptions); schemas[modelName] = schema; } catch (error) { console.warn(` \u26A0\uFE0F Failed to introspect ${modelName}: ${error}`); } } console.log(`\u2705 Introspected ${Object.keys(schemas).length} models`); return schemas; } /** * Introspect a single model */ async introspectModel(modelName, options = {}) { const attributes = options.fieldAttributes || this.defaultAttributes; const fields = await this.odoo.callMethod( modelName, "fields_get", [], { attributes } ); const modelInfo = await this.getModelInfo(modelName); return { name: modelName, description: modelInfo?.description || this.formatModelName(modelName), fields: this.parseFields(fields, options) }; } /** * Get basic information about a model */ async getModelInfo(modelName) { try { const models = await this.odoo.searchRead( "ir.model", [["model", "=", modelName]], ["name", "info"], { limit: 1 } ); if (models.length > 0) { return { description: models[0].info }; } } catch (error) { } return null; } /** * Parse fields from Odoo fields_get() response */ parseFields(rawFields, options) { const fields = {}; for (const [fieldName, fieldDef] of Object.entries(rawFields)) { if (this.shouldSkipField(fieldName, fieldDef, options)) { continue; } fields[fieldName] = { type: fieldDef.type, string: fieldDef.string || fieldName, required: fieldDef.required || false, readonly: fieldDef.readonly || false, relation: fieldDef.relation, selection: fieldDef.selection, help: fieldDef.help, size: fieldDef.size, digits: fieldDef.digits, store: fieldDef.store, compute: fieldDef.compute, related: fieldDef.related, company_dependent: fieldDef.company_dependent }; } return fields; } /** * Determine if a field should be skipped based on options */ shouldSkipField(fieldName, fieldDef, options) { if (fieldName.startsWith("__")) { return true; } if (!options.includeInternal) { const internalFields = [ "create_uid", "write_uid", "create_date", "write_date", "display_name" ]; if (internalFields.includes(fieldName)) { return true; } if (/^(in_group_|sel_groups_)/.test(fieldName)) { return true; } } if (!options.includeComputed && fieldDef.compute) { return true; } if (!options.includeRelated && fieldDef.related) { return true; } return false; } /** * Discover all available models in the Odoo instance */ async discoverModels(options = {}) { try { const models = await this.discoverModelsFromIrModel(options); return models; } catch (error) { console.log("\u26A0\uFE0F Cannot access ir.model, using fallback method..."); return this.discoverModelsFromFallback(options); } } /** * Discover models using ir.model table */ async discoverModelsFromIrModel(options) { const domain = []; if (!options.includeTransient) { domain.push(["transient", "=", false]); } if (!options.includeAbstract) { domain.push(["abstract", "=", false]); } const models = await this.odoo.searchRead( "ir.model", domain, ["model"], { order: "model" } ); let modelNames = models.map((m) => m.model); if (options.excludePatterns) { modelNames = modelNames.filter((name) => { return !options.excludePatterns.some( (pattern) => this.matchesPattern(name, pattern) ); }); } console.log(`\u{1F4E6} Discovered ${modelNames.length} models`); return modelNames; } /** * Fallback model discovery using common Odoo models */ async discoverModelsFromFallback(options) { const commonModels = [ "res.partner", "res.users", "res.company", "product.product", "product.template", "product.category", "sale.order", "sale.order.line", "purchase.order", "purchase.order.line", "account.invoice", "account.invoice.line", "stock.picking", "stock.move", "mrp.production", "mrp.bom", "project.project", "project.task", "hr.employee", "crm.lead", "crm.team", "mail.thread", "ir.attachment", "ir.ui.view" ]; let availableModels = []; for (const modelName of commonModels) { try { const fieldInfo = await this.odoo.callMethod(modelName, "fields_get", [[]], {}); if (fieldInfo && typeof fieldInfo === "object") { availableModels.push(modelName); } } catch (error) { } } if (options.excludePatterns) { availableModels = availableModels.filter((name) => { return !options.excludePatterns.some( (pattern) => this.matchesPattern(name, pattern) ); }); } console.log(`\u{1F4E6} Found ${availableModels.length} accessible models (via fallback)`); return availableModels; } /** * Check if model name matches a pattern */ matchesPattern(name, pattern) { if (pattern.includes("*")) { const regex = new RegExp(pattern.replace(/\*/g, ".*")); return regex.test(name); } return name === pattern; } /** * Format model name to a readable description */ formatModelName(modelName) { return modelName.split(".").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" "); } /** * Get field statistics for a model */ async getFieldStatistics(modelName) { const schema = await this.introspectModel(modelName); const fields = Object.values(schema.fields); const stats = { totalFields: fields.length, requiredFields: fields.filter((f) => f.required).length, readonlyFields: fields.filter((f) => f.readonly).length, relationFields: fields.filter((f) => f.relation).length, computedFields: fields.filter((f) => f.compute).length, typeDistribution: {} }; for (const field of fields) { stats.typeDistribution[field.type] = (stats.typeDistribution[field.type] || 0) + 1; } return stats; } /** * Validate that generated types would match current Odoo schema */ async validateGeneratedTypes(modelNames, generatedTypesPath) { const issues = []; return { valid: issues.length === 0, issues }; } }; // src/generators/typescript-generator.ts var TypeScriptGenerator = class { constructor(options = {}) { this.options = options; this.defaultOptions = { includeComments: true, includeJSDoc: true, includeTimestamp: true, optionalFields: true, strictNulls: false, exportTypes: true, interfacePrefix: "", interfaceSuffix: "", enumForSelection: false }; } /** * Generate TypeScript code from model schemas */ generate(schemas) { const opts = { ...this.defaultOptions, ...this.options }; const lines = []; if (opts.includeComments) { lines.push("/**"); lines.push(" * Auto-generated TypeScript types from Odoo models"); if (opts.includeTimestamp) { lines.push(` * Generated: ${(/* @__PURE__ */ new Date()).toISOString()}`); } lines.push(" * "); lines.push(" * DO NOT EDIT MANUALLY!"); lines.push(" * Regenerate with: npx next-core-codegen types"); lines.push(" */"); lines.push(""); } if (opts.exportTypes) { lines.push("// Odoo-specific types"); lines.push("export type Many2one<T = any> = [number, string] | false"); lines.push("export type One2many = number[]"); lines.push("export type Many2many = number[]"); lines.push("export type Reference = string"); lines.push(""); } if (opts.enumForSelection) { lines.push(...this.generateSelectionEnums(schemas)); } for (const [modelName, schema] of Object.entries(schemas)) { lines.push(...this.generateInterface(modelName, schema, opts)); lines.push(""); } return lines.join("\n"); } /** * Generate TypeScript interface for a model */ generateInterface(modelName, schema, options) { const interfaceName = this.toInterfaceName(modelName, options); const lines = []; if (options.includeJSDoc) { lines.push("/**"); lines.push(` * ${schema.description}`); lines.push(` * Odoo Model: ${modelName}`); lines.push(" */"); } lines.push(`export interface ${interfaceName} {`); for (const [fieldName, field] of Object.entries(schema.fields)) { const fieldLines = this.generateField(fieldName, field, options); lines.push(...fieldLines.map((line) => " " + line)); } lines.push("}"); return lines; } /** * Generate TypeScript field definition */ generateField(fieldName, field, options) { const lines = []; if (options.includeJSDoc && (field.help || field.required)) { lines.push("/**"); if (field.help) { lines.push(` * ${field.help}`); } if (field.required) { lines.push(" * @required"); } lines.push(" */"); } const tsType = this.toTypeScriptType(field, options); const optional = options.optionalFields && !field.required ? "?" : ""; const nullish = options.strictNulls && !field.required ? " | null" : ""; lines.push(`${fieldName}${optional}: ${tsType}${nullish}`); return lines; } /** * Convert Odoo field type to TypeScript type */ toTypeScriptType(field, options) { const typeMap = { "char": "string", "text": "string", "html": "string", "integer": "number", "float": "number", "monetary": "number", "boolean": "boolean", "date": "string", "datetime": "string", "binary": "string", "selection": this.getSelectionType(field, options), "many2one": "Many2one", "one2many": "One2many", "many2many": "Many2many", "reference": "Reference" }; if (field.related) { return "string"; } if (field.compute && !field.store) { return "any"; } return typeMap[field.type] || "any"; } /** * Get TypeScript type for selection field */ getSelectionType(field, options) { if (!field.selection) { return "string"; } if (options.enumForSelection && field.selection.length > 0) { const enumName = this.toEnumName(field.string || "Selection"); return enumName; } const values = field.selection.map(([key]) => `'${key}'`); return values.join(" | "); } /** * Generate selection enums */ generateSelectionEnums(schemas) { const lines = ["// Selection field enums", ""]; const generatedEnums = /* @__PURE__ */ new Set(); for (const schema of Object.values(schemas)) { for (const [fieldName, field] of Object.entries(schema.fields)) { if (field.type === "selection" && field.selection) { const enumName = this.toEnumName(field.string || fieldName); if (!generatedEnums.has(enumName)) { generatedEnums.add(enumName); lines.push("/**"); lines.push(` * Selection options for ${field.string || fieldName}`); lines.push(" */"); lines.push(`export enum ${enumName} {`); for (const [key, label] of field.selection) { const enumKey = this.toEnumKey(key); lines.push(` /** ${label} */`); lines.push(` ${enumKey} = '${key}',`); } lines.push("}"); lines.push(""); } } } } return lines; } /** * Convert model name to interface name */ toInterfaceName(modelName, options) { const name = modelName.split(".").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(""); return `${options.interfacePrefix}${name}${options.interfaceSuffix}`; } /** * Convert selection string to enum name */ toEnumName(selectionName) { return selectionName.replace(/[^a-zA-Z0-9\s]/g, "").split(/\s+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(""); } /** * Convert selection key to enum key */ toEnumKey(key) { return key.toUpperCase().replace(/[^A-Z0-9_]/g, "_").replace(/^[0-9]/, "_$&"); } /** * Generate field statistics as comments */ generateStatistics(schemas) { const lines = [ "/**", " * Generation Statistics", " */" ]; let totalFields = 0; let totalModels = Object.keys(schemas).length; const typeCounts = {}; for (const schema of Object.values(schemas)) { totalFields += Object.keys(schema.fields).length; for (const field of Object.values(schema.fields)) { typeCounts[field.type] = (typeCounts[field.type] || 0) + 1; } } lines.push(`// Models: ${totalModels}`); lines.push(`// Total Fields: ${totalFields}`); lines.push("// Field Types:"); for (const [type, count] of Object.entries(typeCounts).sort()) { lines.push(`// ${type}: ${count}`); } return lines; } /** * Generate import statements for related models */ generateImports(schemas) { const imports = /* @__PURE__ */ new Set(); for (const schema of Object.values(schemas)) { for (const field of Object.values(schema.fields)) { if (field.relation) { const interfaceName = this.toInterfaceName(field.relation, this.defaultOptions); imports.add(interfaceName); } } } if (imports.size === 0) { return []; } const lines = [ "// Related model imports", ...Array.from(imports).map((name) => `import type { ${name} } from './${name.toLowerCase()}'`), "" ]; return lines; } }; // src/generators/hooks-generator.ts var HooksGenerator = class { constructor(options = {}) { this.options = options; this.defaultOptions = { includeComments: true, includeJSDoc: true, includeTimestamp: true, apiClientName: "OdooClient", queryClientImport: "@tanstack/react-query", includeOptimisticUpdates: true, includeInfiniteQueries: false, staleTime: 10 * 60 * 1e3, // 10 minutes cacheTime: 30 * 60 * 1e3 // 30 minutes }; } /** * Generate React Query hooks from model schemas */ generate(schemas) { const opts = { ...this.defaultOptions, ...this.options }; const lines = []; if (opts.includeComments) { lines.push("/**"); lines.push(" * Auto-generated React Query hooks from Odoo models"); if (opts.includeTimestamp) { lines.push(` * Generated: ${(/* @__PURE__ */ new Date()).toISOString()}`); } lines.push(" * "); lines.push(" * DO NOT EDIT MANUALLY!"); lines.push(" * Regenerate with: npx next-core-codegen hooks"); lines.push(" */"); lines.push(""); } lines.push(...this.generateImports(opts)); lines.push(""); for (const [modelName, schema] of Object.entries(schemas)) { lines.push(...this.generateModelHooks(modelName, schema, opts)); lines.push(""); } return lines.join("\n"); } /** * Generate imports section */ generateImports(options) { return [ `import { useQuery, useMutation, useQueryClient, type UseQueryOptions, type UseMutationOptions } from '${options.queryClientImport}'`, `import type { ${options.apiClientName} } from '@kodeme-io/next-core-odoo-api'`, "", "// Common types", "export interface ApiQueryOptions {", " domain?: any[]", " fields?: string[]", " limit?: number", " offset?: number", " order?: string", "}", "", "export interface SearchOptions extends ApiQueryOptions {", " context?: Record<string, any>", "}" ]; } /** * Generate all hooks for a single model */ generateModelHooks(modelName, schema, options) { const lines = []; const interfaceName = this.toInterfaceName(modelName); const modelKey = this.toModelKey(modelName); if (options.includeJSDoc) { lines.push("/**"); lines.push(` * Hooks for ${schema.description}`); lines.push(` * Odoo Model: ${modelName}`); lines.push(" */"); } lines.push(...this.generateQueryKeysFactory(modelName, modelKey, interfaceName)); lines.push(""); lines.push(...this.generateListHook(modelName, modelKey, interfaceName, options)); lines.push(""); lines.push(...this.generateDetailHook(modelName, modelKey, interfaceName, options)); lines.push(""); lines.push(...this.generateSearchHook(modelName, modelKey, interfaceName, options)); lines.push(""); lines.push(...this.generateCreateHook(modelName, modelKey, interfaceName, options)); lines.push(""); lines.push(...this.generateUpdateHook(modelName, modelKey, interfaceName, options)); lines.push(""); lines.push(...this.generateDeleteHook(modelName, modelKey, interfaceName, options)); lines.push(""); if (options.includeInfiniteQueries) { lines.push(...this.generateInfiniteQueryHook(modelName, modelKey, interfaceName, options)); lines.push(""); } return lines; } /** * Generate query keys factory for a model */ generateQueryKeysFactory(modelName, modelKey, interfaceName) { return [ "/**", ` * Query keys factory for ${modelName}`, " * Ensures type-safe query invalidation and cache management", " */", `export const ${modelKey}Keys = {`, ` all: ['${modelKey}'] as const,`, ` lists: () => [...${modelKey}Keys.all, 'list'] as const,`, ` list: (options?: ApiQueryOptions) => [...${modelKey}Keys.lists(), options || {}] as const,`, ` details: () => [...${modelKey}Keys.all, 'detail'] as const,`, ` detail: (id: number) => [...${modelKey}Keys.details(), id] as const,`, ` searches: () => [...${modelKey}Keys.all, 'search'] as const,`, ` search: (options?: SearchOptions) => [...${modelKey}Keys.searches(), options || {}] as const,`, "}" ]; } /** * Generate list query hook (e.g., usePartners) */ generateListHook(modelName, modelKey, interfaceName, options) { const hookName = `use${interfaceName}s`; return [ "/**", ` * Query hook to fetch list of ${modelName} records`, " * ", " * @example", " * ```tsx", ` * const { data, isLoading, error } = ${hookName}(api, {`, ` * domain: [['active', '=', true]],`, ` * limit: 10,`, ` * order: 'name ASC'`, " * })", " * ```", " */", `export function ${hookName}(`, ` api: ${options.apiClientName},`, ` queryOptions?: ApiQueryOptions,`, ` options?: Omit<UseQueryOptions<${interfaceName}[], Error>, 'queryKey' | 'queryFn'>`, `) {`, " return useQuery({", ` queryKey: ${modelKey}Keys.list(queryOptions),`, ` queryFn: async () => {`, ` const records = await api.searchRead(`, ` '${modelName}',`, ` queryOptions?.domain || [],`, ` queryOptions?.fields || [],`, ` {`, ` limit: queryOptions?.limit,`, ` offset: queryOptions?.offset,`, ` order: queryOptions?.order,`, ` }`, ` )`, ` return records as ${interfaceName}[]`, ` },`, ` staleTime: ${options.staleTime},`, ` ...options,`, " })", "}" ]; } /** * Generate detail query hook (e.g., usePartner) */ generateDetailHook(modelName, modelKey, interfaceName, options) { const hookName = `use${interfaceName}`; return [ "/**", ` * Query hook to fetch a single ${modelName} record by ID`, " * ", " * @example", " * ```tsx", ` * const { data, isLoading } = ${hookName}(api, 123)`, " * ```", " */", `export function ${hookName}(`, ` api: ${options.apiClientName},`, ` id: number,`, ` fields?: string[],`, ` options?: Omit<UseQueryOptions<${interfaceName} | null, Error>, 'queryKey' | 'queryFn'>`, `) {`, " return useQuery({", ` queryKey: ${modelKey}Keys.detail(id),`, ` queryFn: async () => {`, ` if (!id) return null`, ` const records = await api.searchRead(`, ` '${modelName}',`, ` [['id', '=', id]],`, ` fields || [],`, ` { limit: 1 }`, ` )`, ` return records.length > 0 ? (records[0] as ${interfaceName}) : null`, ` },`, ` staleTime: ${options.staleTime},`, ` enabled: !!id,`, ` ...options,`, " })", "}" ]; } /** * Generate search hook with custom domain */ generateSearchHook(modelName, modelKey, interfaceName, options) { const hookName = `useSearch${interfaceName}s`; return [ "/**", ` * Search hook with custom domain and context`, " * ", " * @example", " * ```tsx", ` * const { data } = ${hookName}(api, {`, ` * domain: [['email', 'ilike', '@example.com']],`, ` * context: { active_test: false }`, " * })", " * ```", " */", `export function ${hookName}(`, ` api: ${options.apiClientName},`, ` searchOptions?: SearchOptions,`, ` options?: Omit<UseQueryOptions<${interfaceName}[], Error>, 'queryKey' | 'queryFn'>`, `) {`, " return useQuery({", ` queryKey: ${modelKey}Keys.search(searchOptions),`, ` queryFn: async () => {`, ` const records = await api.searchRead(`, ` '${modelName}',`, ` searchOptions?.domain || [],`, ` searchOptions?.fields || [],`, ` {`, ` limit: searchOptions?.limit,`, ` offset: searchOptions?.offset,`, ` order: searchOptions?.order,`, ` context: searchOptions?.context,`, ` }`, ` )`, ` return records as ${interfaceName}[]`, ` },`, ` staleTime: ${options.staleTime},`, ` ...options,`, " })", "}" ]; } /** * Generate create mutation hook */ generateCreateHook(modelName, modelKey, interfaceName, options) { const hookName = `useCreate${interfaceName}`; return [ "/**", ` * Mutation hook to create a new ${modelName} record`, " * ", " * @example", " * ```tsx", ` * const { mutate, isPending } = ${hookName}(api)`, ` * mutate({ name: 'New Record', active: true })`, " * ```", " */", `export function ${hookName}(`, ` api: ${options.apiClientName},`, ` options?: Omit<UseMutationOptions<number, Error, Partial<${interfaceName}>>, 'mutationFn'>`, `) {`, " const queryClient = useQueryClient()", "", " return useMutation({", ` mutationFn: async (data: Partial<${interfaceName}>) => {`, ` const id = await api.create('${modelName}', data)`, ` return id`, ` },`, ` onSuccess: (newId, variables, context) => {`, ` // Invalidate all ${modelName} queries`, ` queryClient.invalidateQueries({ queryKey: ${modelKey}Keys.all })`, ` options?.onSuccess?.(newId, variables, context)`, ` },`, ` ...options,`, " })", "}" ]; } /** * Generate update mutation hook */ generateUpdateHook(modelName, modelKey, interfaceName, options) { const hookName = `useUpdate${interfaceName}`; const optimisticUpdate = options.includeOptimisticUpdates ? [ ` onMutate: async ({ id, data }) => {`, ` // Cancel outgoing queries`, ` await queryClient.cancelQueries({ queryKey: ${modelKey}Keys.detail(id) })`, ``, ` // Snapshot previous value`, ` const previous = queryClient.getQueryData(${modelKey}Keys.detail(id))`, ``, ` // Optimistically update`, ` if (previous) {`, ` queryClient.setQueryData(${modelKey}Keys.detail(id), {`, ` ...previous,`, ` ...data,`, ` })`, ` }`, ``, ` return { previous }`, ` },`, ` onError: (err, variables, context) => {`, ` // Rollback on error`, ` if (context?.previous) {`, ` queryClient.setQueryData(${modelKey}Keys.detail(variables.id), context.previous)`, ` }`, ` options?.onError?.(err, variables, context)`, ` },` ] : []; return [ "/**", ` * Mutation hook to update an existing ${modelName} record`, " * ", " * @example", " * ```tsx", ` * const { mutate } = ${hookName}(api)`, ` * mutate({ id: 123, data: { name: 'Updated Name' } })`, " * ```", " */", `export function ${hookName}(`, ` api: ${options.apiClientName},`, ` options?: Omit<UseMutationOptions<boolean, Error, { id: number, data: Partial<${interfaceName}> }>, 'mutationFn'>`, `) {`, " const queryClient = useQueryClient()", "", " return useMutation({", ` mutationFn: async ({ id, data }: { id: number, data: Partial<${interfaceName}> }) => {`, ` const success = await api.write('${modelName}', [id], data)`, ` return success`, ` },`, ...optimisticUpdate, ` onSuccess: (result, { id }, context) => {`, ` // Invalidate queries`, ` queryClient.invalidateQueries({ queryKey: ${modelKey}Keys.detail(id) })`, ` queryClient.invalidateQueries({ queryKey: ${modelKey}Keys.lists() })`, ` options?.onSuccess?.(result, { id, data: {} as any }, context)`, ` },`, ` ...options,`, " })", "}" ]; } /** * Generate delete mutation hook */ generateDeleteHook(modelName, modelKey, interfaceName, options) { const hookName = `useDelete${interfaceName}`; return [ "/**", ` * Mutation hook to delete a ${modelName} record`, " * ", " * @example", " * ```tsx", ` * const { mutate } = ${hookName}(api)`, ` * mutate(123)`, " * ```", " */", `export function ${hookName}(`, ` api: ${options.apiClientName},`, ` options?: Omit<UseMutationOptions<boolean, Error, number>, 'mutationFn'>`, `) {`, " const queryClient = useQueryClient()", "", " return useMutation({", ` mutationFn: async (id: number) => {`, ` const success = await api.unlink('${modelName}', [id])`, ` return success`, ` },`, ` onSuccess: (result, id, context) => {`, ` // Remove from cache`, ` queryClient.removeQueries({ queryKey: ${modelKey}Keys.detail(id) })`, ` queryClient.invalidateQueries({ queryKey: ${modelKey}Keys.lists() })`, ` options?.onSuccess?.(result, id, context)`, ` },`, ` ...options,`, " })", "}" ]; } /** * Generate infinite query hook for pagination */ generateInfiniteQueryHook(modelName, modelKey, interfaceName, options) { const hookName = `use${interfaceName}sInfinite`; return [ "/**", ` * Infinite query hook for paginated ${modelName} list`, " * ", " * @example", " * ```tsx", ` * const { data, fetchNextPage, hasNextPage } = ${hookName}(api, {`, ` * domain: [['active', '=', true]],`, ` * limit: 20`, " * })", " * ```", " */", `export function ${hookName}(`, ` api: ${options.apiClientName},`, ` queryOptions?: ApiQueryOptions & { limit?: number }`, `) {`, ` const limit = queryOptions?.limit || 20`, "", ` return useInfiniteQuery({`, ` queryKey: [...${modelKey}Keys.lists(), 'infinite', queryOptions],`, ` queryFn: async ({ pageParam = 0 }) => {`, ` const records = await api.searchRead(`, ` '${modelName}',`, ` queryOptions?.domain || [],`, ` queryOptions?.fields || [],`, ` {`, ` limit,`, ` offset: pageParam * limit,`, ` order: queryOptions?.order,`, ` }`, ` )`, ` return {`, ` records: records as ${interfaceName}[],`, ` nextPage: records.length === limit ? pageParam + 1 : undefined,`, ` }`, ` },`, ` getNextPageParam: (lastPage) => lastPage.nextPage,`, ` staleTime: ${options.staleTime},`, ` })`, "}" ]; } /** * Convert model name to interface name */ toInterfaceName(modelName) { return modelName.split(".").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(""); } /** * Convert model name to query key prefix */ toModelKey(modelName) { return modelName.replace(/\./g, ""); } }; // src/generators/api-client-generator.ts var ApiClientGenerator = class { constructor(options = {}) { this.options = options; this.defaultOptions = { includeComments: true, includeJSDoc: true, includeTimestamp: true, baseClientName: "OdooClient", className: "", includeAdvancedMethods: true, includeFieldHelpers: true }; } /** * Generate API client class from model schema */ generate(modelName, schema) { const opts = { ...this.defaultOptions, ...this.options }; const lines = []; const interfaceName = this.toInterfaceName(modelName); const className = opts.className || `${interfaceName}API`; if (opts.includeComments) { lines.push("/**"); lines.push(` * Auto-generated API client for ${modelName}`); if (opts.includeTimestamp) { lines.push(` * Generated: ${(/* @__PURE__ */ new Date()).toISOString()}`); } lines.push(" * "); lines.push(" * DO NOT EDIT MANUALLY!"); lines.push(" * Regenerate with: npx next-core-codegen api-client"); lines.push(" */"); lines.push(""); } lines.push(...this.generateImports(interfaceName, opts)); lines.push(""); lines.push(...this.generateClass(modelName, schema, className, interfaceName, opts)); return lines.join("\n"); } /** * Generate imports */ generateImports(interfaceName, options) { return [ `import type { ${options.baseClientName} } from '@kodeme-io/next-core-odoo-api'`, `import type { ${interfaceName} } from './types'`, "", "// Common query options", "export interface QueryOptions {", " domain?: any[]", " fields?: string[]", " limit?: number", " offset?: number", " order?: string", " context?: Record<string, any>", "}", "", "export interface CreateOptions {", " context?: Record<string, any>", "}", "", "export interface UpdateOptions {", " context?: Record<string, any>", "}", "", "export interface DeleteOptions {", " context?: Record<string, any>", "}" ]; } /** * Generate the API client class */ generateClass(modelName, schema, className, interfaceName, options) { const lines = []; if (options.includeJSDoc) { lines.push("/**"); lines.push(` * API client for ${schema.description}`); lines.push(` * Odoo Model: ${modelName}`); lines.push(" * "); lines.push(" * @example"); lines.push(" * ```typescript"); lines.push(` * const api = new ${className}(odooClient)`); lines.push(` * const records = await api.getAll()`); lines.push(` * const record = await api.getById(123)`); lines.push(` * const id = await api.create({ name: 'New Record' })`); lines.push(" * ```"); lines.push(" */"); } lines.push(`export class ${className} {`); lines.push(` private readonly modelName = '${modelName}'`); lines.push(""); lines.push(" /**"); lines.push(` * Creates a new ${className} instance`); lines.push(` * @param client - The Odoo client instance`); lines.push(" */"); lines.push(` constructor(private client: ${options.baseClientName}) {}`); lines.push(""); lines.push(...this.generateGetAllMethod(interfaceName).map((l) => " " + l)); lines.push(""); lines.push(...this.generateGetByIdMethod(interfaceName).map((l) => " " + l)); lines.push(""); lines.push(...this.generateSearchMethod(interfaceName).map((l) => " " + l)); lines.push(""); lines.push(...this.generateCountMethod().map((l) => " " + l)); lines.push(""); lines.push(...this.generateCreateMethod(interfaceName).map((l) => " " + l)); lines.push(""); lines.push(...this.generateCreateManyMethod(interfaceName).map((l) => " " + l)); lines.push(""); lines.push(...this.generateUpdateMethod(interfaceName).map((l) => " " + l)); lines.push(""); lines.push(...this.generateDeleteMethod().map((l) => " " + l)); lines.push(""); if (options.includeAdvancedMethods) { lines.push(...this.generateExistsMethod().map((l) => " " + l)); lines.push(""); lines.push(...this.generateFindOneMethod(interfaceName).map((l) => " " + l)); lines.push(""); lines.push(...this.generateUpsertMethod(interfaceName).map((l) => " " + l)); lines.push(""); lines.push(...this.generateBatchMethod(interfaceName).map((l) => " " + l)); lines.push(""); } if (options.includeFieldHelpers) { lines.push(...this.generateFieldHelpers(schema, interfaceName).map((l) => " " + l)); } lines.push("}"); return lines; } /** * Generate getAll method */ generateGetAllMethod(interfaceName) { return [ "/**", " * Get all records with optional filters", " * ", " * @example", " * ```typescript", " * const records = await api.getAll({", ` * domain: [['active', '=', true]],`, " * limit: 10,", " * order: 'name ASC'", " * })", " * ```", " */", `async getAll(options?: QueryOptions): Promise<${interfaceName}[]> {`, " const records = await this.client.searchRead(", " this.modelName,", " options?.domain || [],", " options?.fields || [],", " {", " limit: options?.limit,", " offset: options?.offset,", " order: options?.order,", " context: options?.context,", " }", " )", ` return records as ${interfaceName}[]`, "}" ]; } /** * Generate getById method */ generateGetByIdMethod(interfaceName) { return [ "/**", " * Get a single record by ID", " * ", " * @param id - Record ID", " * @param fields - Optional fields to fetch", " * @returns The record or null if not found", " */", `async getById(id: number, fields?: string[]): Promise<${interfaceName} | null> {`, " const records = await this.client.searchRead(", " this.modelName,", " [['id', '=', id]],", " fields || [],", " { limit: 1 }", " )", ` return records.length > 0 ? (records[0] as ${interfaceName}) : null`, "}" ]; } /** * Generate search method */ generateSearchMethod(interfaceName) { return [ "/**", " * Search records with custom domain", " * ", " * @example", " * ```typescript", " * const results = await api.search({", ` * domain: [['email', 'ilike', '@example.com'], ['active', '=', true]],`, " * limit: 50", " * })", " * ```", " */", `async search(options: QueryOptions): Promise<${interfaceName}[]> {`, " const records = await this.client.searchRead(", " this.modelName,", " options.domain || [],", " options.fields || [],", " {", " limit: options.limit,", " offset: options.offset,", " order: options.order,", " context: options.context,", " }", " )", ` return records as ${interfaceName}[]`, "}" ]; } /** * Generate count method */ generateCountMethod() { return [ "/**", " * Count records matching domain", " * ", " * @param domain - Search domain", " * @returns Number of matching records", " */", "async count(domain?: any[]): Promise<number> {", " return await this.client.searchCount(this.modelName, domain || [])", "}" ]; } /** * Generate create method */ generateCreateMethod(interfaceName) { return [ "/**", " * Create a new record", " * ", " * @param data - Record data", " * @param options - Create options", " * @returns Created record ID", " */", `async create(data: Partial<${interfaceName}>, options?: CreateOptions): Promise<number> {`, " return await this.client.create(this.modelName, data, options?.context)", "}" ]; } /** * Generate createMany method */ generateCreateManyMethod(interfaceName) { return [ "/**", " * Create multiple records", " * ", " * @param dataList - Array of record data", " * @param options - Create options", " * @returns Array of created record IDs", " */", `async createMany(dataList: Partial<${interfaceName}>[], options?: CreateOptions): Promise<number[]> {`, " const ids: number[] = []", " for (const data of dataList) {", " const id = await this.create(data, options)", " ids.push(id)", " }", " return ids", "}" ]; } /** * Generate update method */ generateUpdateMethod(interfaceName) { return [ "/**", " * Update record(s)", " * ", " * @param ids - Record ID(s) to update", " * @param data - Update data", " * @param options - Update options", " * @returns True if successful", " */", `async update(ids: number | number[], data: Partial<${interfaceName}>, options?: UpdateOptions): Promise<boolean> {`, " const idArray = Array.isArray(ids) ? ids : [ids]", " return await this.client.write(this.modelName, idArray, data, options?.context)", "}" ]; } /** * Generate delete method */ generateDeleteMethod() { return [ "/**", " * Delete record(s)", " * ", " * @param ids - Record ID(s) to delete", " * @param options - Delete options", " * @returns True if successful", " */", "async delete(ids: number | number[], options?: DeleteOptions): Promise<boolean> {", " const idArray = Array.isArray(ids) ? ids : [ids]", " return await this.client.unlink(this.modelName, idArray, options?.context)", "}" ]; } /** * Generate exists method */ generateExistsMethod() { return [ "/**", " * Check if a record exists", " * ", " * @param id - Record ID", " * @returns True if record exists", " */", "async exists(id: number): Promise<boolean> {", " const count = await this.count([['id', '=', id]])", " return count > 0", "}" ]; } /** * Generate findOne method */ generateFindOneMethod(interfaceName) { return [ "/**", " * Find first record matching domain", " * ", " * @param domain - Search domain", " * @param fields - Optional fields to fetch", " * @returns First matching record or null", " */", `async findOne(domain: any[], fields?: string[]): Promise<${interfaceName} | null> {`, " const records = await this.search({ domain, fields, limit: 1 })", " return records.length > 0 ? records[0] : null", "}" ]; } /** * Generate upsert method */ generateUpsertMethod(interfaceName) { return [ "/**", " * Update if exists, create if not", " * ", " * @param searchDomain - Domain to find existing record", " * @param data - Record data", " * @returns Record ID (existing or newly created)", " */", `async upsert(searchDomain: any[], data: Partial<${interfaceName}>): Promise<number> {`, " const existing = await this.findOne(searchDomain, ['id'])", " if (existing && existing.id) {", " await this.update(existing.id, data)", " return existing.id", " }", " return await this.create(data)", "}" ]; } /** * Generate batch operation method */ generateBatchMethod(interfaceName) { return [ "/**", " * Perform batch operations", " * ", " * @param operations - Array of operations", " * @returns Results of operations", " */", "async batch(operations: Array<{", " type: 'create' | 'update' | 'delete'", ` data?: Partial<${interfaceName}>`, " ids?: number[]", "}>): Promise<Array<number | boolean>> {", " const results: Array<number | boolean> = []", "", " for (const op of operations) {", " try {", " if (op.type === 'create' && op.data) {", " const id = await this.create(op.data)", " results.push(id)", " } else if (op.type === 'update' && op.ids && op.data) {", " const success = await this.update(op.ids, op.data)", " results.push(success)", " } else if (op.type === 'delete' && op.ids) {", " const success = await this.delete(op.ids)", " results.push(success)", " }", " } catch (error) {", " console.error(`Batch operation failed:`, error)", " results.push(false)", " }", " }", "", " return results", "}" ]; } /** * Generate field-specific helper methods */ generateFieldHelpers(schema, interfaceName) { const lines = []; const relationFields = Object.entries(schema.fields).filter(([, field]) => field.relation); const selectionFields = Object.entries(schema.fields).filter(([, field]) => field.type === "selection"); if (relationFields.length > 0 || selectionFields.length > 0) { lines.push("// Field-specific helper methods"); lines.push(""); } for (const [fieldName, field] of relationFields) { if (field.type === "many2one") { const methodName = `getBy${this.capitalize(fieldName)}`; lines.push("/**"); lines.push(` * Find records by ${field.string || fieldName}`); lines.push(" */"); lines.push(`async ${methodName}(${fieldName}Id: number, options?: QueryOptions): Promise<${interfaceName}[]> {`); lines.push(` return await this.search({`); lines.push(` ...options,`); lines.push(` domain: [...(options?.domain || []), ['${fieldName}', '=', ${fieldName}Id]],`); lines.push(` })`); lines.push("}"); lines.push(""); } } for (const [fieldName, field] of selectionFields) { if (field.selection && field.selection.length > 0) { const methodName = `getBy${this.capitalize(fieldName)}`; const valueType = field.selection.map(([key]) => `'${key}'`).join(" | "); lines.push("/**"); lines.push(` * Find records by ${field.string || fieldName}`); lines.push(" */"); lines.push(`async ${methodName}(${fieldName}: ${valueType}, options?: QueryOptions): Promise<${interfaceName}[]> {`); lines.push(` return await this.search({`); lines.push(` ...options,`); lines.push(` domain: [...(options?.domain || []), ['${fieldName}', '=', ${fieldName}]],`); lines.push(` })`); lines.push("}"); lines.push(""); } } return lines; } /** * Convert model name to interface name */ toInterfaceName(modelName) { return modelName.split(".").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(""); } /** * Capitalize first letter */ capitalize(str) { return str.charAt(0).toUpperCase() + str.slice(1); } }; // src/commands/generate-types.ts import { Command } from "commander"; // src/utils/config.ts import fs from "fs"; import path from "path"; function loadConfig(configPath) { const possiblePaths = configPath ? [configPath] : [ ".next-core.json", ".next-core.js", "next-core.config.json", "next-core.config.js" ]; for (const filePath of possiblePaths) { try { const fullPath = path.resolve(process.cwd(), filePath); if (fs.existsSync(fullPath)) { if (filePath.endsWith(".js")) { const configModule = __require(fullPath); const config2 = typeof configModule === "function" ? configModule() : configModule; return validateConfig(config2); } else { const content = fs.readFileSync(fullPath, "utf-8"); const config2 = JSON.parse(content); return validateConfig(config2); } } } catch (error) { throw new Error(`Failed to load config from ${filePath}: ${error}`); } } throw new Error( "No configuration file found. Create one of:\n \u2022 .next-core.json\n \u2022 .next-core.js\n \u2022 next-core.config.json\n \u2022 next-core.config.js" ); } function validateConfig(config2) { if (!config2 || typeof config2 !== "object") { throw new Error("Configuration must be an object"); } if (!config2.odoo || typeof config2.odoo !== "object") { throw new Error('Configuration must include an "odoo" object'); } const requiredOdooFields = ["url", "database", "username", "password"]; for (const field of requiredOdooFields) { if (!config2.odoo[field]) { throw new Error(`Odoo configuration missing required field: ${field}`); } } if (!config2.models || !Array.isArray(config2.models)) { config2.models = []; } if (!config2.output || typeof config2.output !== "string") { config2.output = "src/types/odoo.ts"; } if (!config2.exclude || !Array.isArray(config2.exclude)) { config2.exclude = []; } return config2; } function createDefaultConfig() { return { odoo: { url: "${ODOO_URL}", database: "${ODOO_DB}", username: "${ODOO_USERNAME}", password: "${ODOO_PASSWORD}" }, models: [ "res.partner", "res.users", "product.product", "product.category", "sale.order", "sale.order.line" ], output: "src/types/odoo.ts", exclude: [ "ir.*", "mail.*", "base.*" ] }; } async function saveConfig(config2, filePath) { const fullPath = path.resolve(process.cwd(), filePath); const dir = path.dirname(fullPath); await fs.promises.mkdir(dir, { recursive: true }); if (filePath.endsWith(".js")) { const content = `module.exports = ${JSON.stringify(config2, null, 2)}`; await fs.promises.writeFile(fullPath, content, "utf-8"); } else { const content = JSON.stringify(config2, null, 2); await fs.promises.writeFile(fullPath, content, "utf-8"); } } // src/utils/security.ts var SENSITIVE_FIELDS = [ "password", "password_hash", "api_key", "apikey", "secret", "token", "auth", "authorization", "credentials" ]; function isSensitiveField(fieldName) { const lowerField = fieldName.toLowerCase(); return SENSITIVE_FIELDS.some( (sensitive) => lowerField.includes(sensitive) || sensitive.includes(lowerField) ); } function redactValue(value, fieldName) { if (typeof value !== "string") { return value; } if (fieldName && isSensitiveField(fieldName)) { return maskSensitiveValue(value); } if (looksLikeSensitiveValue(value)) { return maskSensitiveValue(value); } return value; } function maskSensitiveValue(value) { if (!value || value.length === 0) { return "[EMPTY]"; } if (value === "${ODOO_PASSWORD}" || value.startsWith("${")) { return "[ENV_VAR]"; } if (value.length <= 4) { return value.split("").map(() => "*").join(""); } return `${value.substring(0, 2)}${"*".repeat(value.length - 4)}${value.substring(value.length - 2)}`; } function looksLikeSensitiveValue(value) { const patterns = [ /^[A-Za-z0-9+/]{40,}={0,