diff --git a/packages/typegen/src/fmodata/generateODataTypes.ts b/packages/typegen/src/fmodata/generateODataTypes.ts index ad7dc9d..a3cb3bf 100644 --- a/packages/typegen/src/fmodata/generateODataTypes.ts +++ b/packages/typegen/src/fmodata/generateODataTypes.ts @@ -159,6 +159,7 @@ function generateTableOccurrence( existingFields?: ParsedTableOccurrence, alwaysOverrideFieldNames?: boolean, importAliases?: Map, // Map base name -> alias (e.g., "textField" -> "tf") + includeAllFieldsByDefault?: boolean, ): GeneratedTO { const fmtId = entityType["@TableID"]; const keyFields = entityType.$Key || []; @@ -232,6 +233,12 @@ function generateTableOccurrence( } } + // Determine includeAllFieldsByDefault: table-level override takes precedence, then top-level, default to true + const effectiveIncludeAllFieldsByDefault = + tableOverride?.includeAllFieldsByDefault ?? + includeAllFieldsByDefault ?? + true; + // Generate field builder definitions const fieldLines: string[] = []; const fieldEntries = Array.from(fields.entries()); @@ -250,6 +257,11 @@ function generateTableOccurrence( continue; } + // If includeAllFieldsByDefault is false, only include fields explicitly listed + if (!effectiveIncludeAllFieldsByDefault && !fieldOverride) { + continue; + } + validFieldEntries.push(entry); } @@ -933,6 +945,7 @@ export async function generateODataTypes( clearOldFiles = true, tables, alwaysOverrideFieldNames = true, + includeAllFieldsByDefault = true, } = config; const outputPath = path ?? "schema"; @@ -993,6 +1006,7 @@ export async function generateODataTypes( undefined, tableAlwaysOverrideFieldNames, undefined, + includeAllFieldsByDefault, ); generatedTOs.push({ ...generated, @@ -1052,6 +1066,7 @@ export async function generateODataTypes( existingFields, tableAlwaysOverrideFieldNames, existingFields.importAliases, + includeAllFieldsByDefault, ) : generated; diff --git a/packages/typegen/src/types.ts b/packages/typegen/src/types.ts index 81ab255..8b1cc39 100644 --- a/packages/typegen/src/types.ts +++ b/packages/typegen/src/types.ts @@ -128,6 +128,10 @@ const tableConfig = z.object({ description: "If undefined, the top-level setting will be used. If true, field names will always be updated to match metadata, even when matching by entity ID. If false, existing field names are preserved when matching by entity ID.", }), + includeAllFieldsByDefault: z.boolean().optional().meta({ + description: + "If true, all fields will be included by default. If false, only fields that are explicitly listed in the `fields` array will be included.", + }), }); const typegenConfigSingleBase = z.discriminatedUnion("type", [ @@ -182,6 +186,10 @@ const typegenConfigSingleBase = z.discriminatedUnion("type", [ description: "Required array of tables to generate. Only the tables specified here will be downloaded and generated. Each table can have field-level overrides for excluding fields, renaming variables, and overriding field types.", }), + includeAllFieldsByDefault: z.boolean().default(true).optional().meta({ + description: + "If true, all fields will be included by default. If false, only fields that are explicitly listed in the `fields` array will be included.", + }), }), ]); diff --git a/packages/typegen/web/src/App.tsx b/packages/typegen/web/src/App.tsx index bd4cd39..8724576 100644 --- a/packages/typegen/web/src/App.tsx +++ b/packages/typegen/web/src/App.tsx @@ -62,6 +62,7 @@ function createFmodataConfig(): SingleConfig { }, tables: [], alwaysOverrideFieldNames: true, + includeAllFieldsByDefault: true, }; } diff --git a/packages/typegen/web/src/components/ConfigEditor.tsx b/packages/typegen/web/src/components/ConfigEditor.tsx index c62e176..dd9a1f0 100644 --- a/packages/typegen/web/src/components/ConfigEditor.tsx +++ b/packages/typegen/web/src/components/ConfigEditor.tsx @@ -320,6 +320,18 @@ export function ConfigEditor({ index, onRemove }: ConfigEditorProps) { /> )} /> + ( + + )} + /> )} diff --git a/packages/typegen/web/src/components/MetadataFieldsDialog.tsx b/packages/typegen/web/src/components/MetadataFieldsDialog.tsx index cb2a69d..1983cc7 100644 --- a/packages/typegen/web/src/components/MetadataFieldsDialog.tsx +++ b/packages/typegen/web/src/components/MetadataFieldsDialog.tsx @@ -143,6 +143,12 @@ export function MetadataFieldsDialog({ name: `config.${configIndex}.tables` as const, }); + // Get the top-level includeAllFieldsByDefault value for display + const topLevelIncludeAllFieldsByDefault = useWatch({ + control, + name: `config.${configIndex}.includeAllFieldsByDefault` as const, + }); + // Use a ref to store the latest fieldsConfig to avoid unstable dependencies const fieldsConfigRef = useRef(EMPTY_FIELDS_CONFIG); @@ -219,6 +225,13 @@ export function MetadataFieldsDialog({ (t) => t?.tableName === tableName, ); + // Get effective includeAllFieldsByDefault value + const tableConfig = currentTables[tableIndex]; + const effectiveIncludeAllFieldsByDefault = + tableConfig?.includeAllFieldsByDefault ?? + topLevelIncludeAllFieldsByDefault ?? + true; + if (tableIndex < 0) { // Table doesn't exist in config yet if (exclude) { @@ -231,6 +244,13 @@ export function MetadataFieldsDialog({ ], { shouldDirty: true }, ); + } else if (!effectiveIncludeAllFieldsByDefault) { + // If includeAllFieldsByDefault is false, add field to array to include it + setValue( + `config.${configIndex}.tables` as any, + [...currentTables, { tableName, fields: [{ fieldName }] }], + { shouldDirty: true }, + ); } return; } @@ -266,41 +286,66 @@ export function MetadataFieldsDialog({ }); } } else { - // Remove exclude (or remove entire entry if no other config) - if (fieldIndex >= 0) { - const fieldConfig = currentFields[fieldIndex]!; - const { exclude: _, ...rest } = fieldConfig; + // Include the field + if (effectiveIncludeAllFieldsByDefault) { + // If includeAllFieldsByDefault is true, remove field from array (or just remove exclude property) + if (fieldIndex >= 0) { + const fieldConfig = currentFields[fieldIndex]!; + const { exclude: _, ...rest } = fieldConfig; - if (Object.keys(rest).length === 1 && rest.fieldName) { - // Only fieldName left, remove entire field entry - const newFields = currentFields.filter((_, i) => i !== fieldIndex); - const newTables = [...currentTables]; - - if ( - newFields.length === 0 && - Object.keys(newTables[tableIndex]!).length === 2 - ) { - // Only tableName and fields left, remove entire table entry - const filteredTables = currentTables.filter( - (_, i) => i !== tableIndex, - ); - setValue( - `config.${configIndex}.tables` as any, - filteredTables.length > 0 ? filteredTables : undefined, - { shouldDirty: true }, + if (Object.keys(rest).length === 1 && rest.fieldName) { + // Only fieldName left, remove entire field entry + const newFields = currentFields.filter( + (_, i) => i !== fieldIndex, ); + const newTables = [...currentTables]; + + const table = newTables[tableIndex]!; + const tableKeys = Object.keys(table); + const hasOnlyTableNameAndFields = + tableKeys.length === 2 && + tableKeys.includes("tableName") && + tableKeys.includes("fields"); + if (newFields.length === 0 && hasOnlyTableNameAndFields) { + // Only tableName and fields left, remove entire table entry + const filteredTables = currentTables.filter( + (_, i) => i !== tableIndex, + ); + setValue( + `config.${configIndex}.tables` as any, + filteredTables.length > 0 ? filteredTables : undefined, + { shouldDirty: true }, + ); + } else { + // Keep table but update fields + newTables[tableIndex] = { + ...newTables[tableIndex]!, + fields: newFields.length > 0 ? newFields : undefined, + }; + setValue(`config.${configIndex}.tables` as any, newTables, { + shouldDirty: true, + }); + } } else { - // Keep table but update fields + // Keep other properties + const newFields = [...currentFields]; + newFields[fieldIndex] = rest as any; + const newTables = [...currentTables]; newTables[tableIndex] = { ...newTables[tableIndex]!, - fields: newFields.length > 0 ? newFields : undefined, + fields: newFields, }; setValue(`config.${configIndex}.tables` as any, newTables, { shouldDirty: true, }); } - } else { - // Keep other properties + } + } else { + // If includeAllFieldsByDefault is false, add field to array to include it + if (fieldIndex >= 0) { + // Field exists, just remove exclude property + const fieldConfig = currentFields[fieldIndex]!; + const { exclude: _, ...rest } = fieldConfig; const newFields = [...currentFields]; newFields[fieldIndex] = rest as any; const newTables = [...currentTables]; @@ -311,11 +356,28 @@ export function MetadataFieldsDialog({ setValue(`config.${configIndex}.tables` as any, newTables, { shouldDirty: true, }); + } else { + // Add field to array + const newTables = [...currentTables]; + newTables[tableIndex] = { + ...newTables[tableIndex]!, + fields: [...currentFields, { fieldName }], + }; + setValue(`config.${configIndex}.tables` as any, newTables, { + shouldDirty: true, + }); } } } }, - [configType, configIndex, tableName, allTablesConfig, setValue], + [ + configType, + configIndex, + tableName, + allTablesConfig, + setValue, + topLevelIncludeAllFieldsByDefault, + ], ); // Get the field name for variableName - table should exist due to ensuredTableIndex above @@ -330,6 +392,10 @@ export function MetadataFieldsDialog({ const alwaysOverrideFieldNamesFieldName = `config.${configIndex}.tables.${currentTableIndex >= 0 ? currentTableIndex : 0}.alwaysOverrideFieldNames` as any; + // Get the field name for includeAllFieldsByDefault - table should exist due to ensuredTableIndex above + const includeAllFieldsByDefaultFieldName = + `config.${configIndex}.tables.${currentTableIndex >= 0 ? currentTableIndex : 0}.includeAllFieldsByDefault` as any; + // Helper to set field type override - use ref to avoid dependency on fieldsConfig const setFieldTypeOverride = useCallback( (fieldName: string, typeOverride: string | undefined) => { @@ -402,10 +468,13 @@ export function MetadataFieldsDialog({ const newFields = currentFields.filter((_, i) => i !== fieldIndex); const newTables = [...currentTables]; - if ( - newFields.length === 0 && - Object.keys(newTables[tableIndex]!).length === 2 - ) { + const table = newTables[tableIndex]!; + const tableKeys = Object.keys(table); + const hasOnlyTableNameAndFields = + tableKeys.length === 2 && + tableKeys.includes("tableName") && + tableKeys.includes("fields"); + if (newFields.length === 0 && hasOnlyTableNameAndFields) { // Only tableName and fields left, remove entire table entry const filteredTables = currentTables.filter( (_, i) => i !== tableIndex, @@ -444,6 +513,18 @@ export function MetadataFieldsDialog({ [configType, configIndex, tableName, allTablesConfig, setValue], ); + // Get the effective includeAllFieldsByDefault value (table-level override or top-level default) + const effectiveIncludeAllFieldsByDefault = useMemo(() => { + return ( + tableConfig?.includeAllFieldsByDefault ?? + topLevelIncludeAllFieldsByDefault ?? + true + ); + }, [ + tableConfig?.includeAllFieldsByDefault, + topLevelIncludeAllFieldsByDefault, + ]); + // Get fields for the selected table const fieldsData = useMemo(() => { if ( @@ -487,7 +568,22 @@ export function MetadataFieldsDialog({ const fieldConfig = Array.isArray(fieldsConfig) ? fieldsConfig.find((f) => f?.fieldName === fieldName) : undefined; - const isExcluded = fieldConfig?.exclude === true; + + // Determine if field is excluded: + // - If explicitly excluded (exclude === true), always exclude + // - If includeAllFieldsByDefault is false, exclude if field is not in fields array + // - Otherwise, include by default + let isExcluded: boolean; + if (fieldConfig?.exclude === true) { + isExcluded = true; + } else if (!effectiveIncludeAllFieldsByDefault) { + // If includeAllFieldsByDefault is false, only include fields explicitly in the array + isExcluded = !fieldConfig; + } else { + // Default behavior: include all unless explicitly excluded + isExcluded = false; + } + const typeOverride = fieldConfig?.typeOverride; const isPrimaryKey = keyFields.includes(fieldName); @@ -522,7 +618,22 @@ export function MetadataFieldsDialog({ const fieldConfig = Array.isArray(fieldsConfig) ? fieldsConfig.find((f) => f?.fieldName === fieldName) : undefined; - const isExcluded = fieldConfig?.exclude === true; + + // Determine if field is excluded: + // - If explicitly excluded (exclude === true), always exclude + // - If includeAllFieldsByDefault is false, exclude if field is not in fields array + // - Otherwise, include by default + let isExcluded: boolean; + if (fieldConfig?.exclude === true) { + isExcluded = true; + } else if (!effectiveIncludeAllFieldsByDefault) { + // If includeAllFieldsByDefault is false, only include fields explicitly in the array + isExcluded = !fieldConfig; + } else { + // Default behavior: include all unless explicitly excluded + isExcluded = false; + } + const typeOverride = fieldConfig?.typeOverride; const isPrimaryKey = keyFields.includes(fieldName); @@ -540,7 +651,12 @@ export function MetadataFieldsDialog({ } return fields; - }, [tableName, parsedMetadata, fieldsConfig]); + }, [ + tableName, + parsedMetadata, + fieldsConfig, + effectiveIncludeAllFieldsByDefault, + ]); // Check if all fields are included or excluded const allFieldsIncluded = useMemo(() => { @@ -560,57 +676,116 @@ export function MetadataFieldsDialog({ (t) => t?.tableName === tableName, ); - if (tableIndex < 0) { - // Table doesn't exist in config, nothing to do - return; - } + // Get effective includeAllFieldsByDefault value + const tableConfig = tableIndex >= 0 ? currentTables[tableIndex] : undefined; + const effectiveIncludeAllFieldsByDefault = + tableConfig?.includeAllFieldsByDefault ?? + topLevelIncludeAllFieldsByDefault ?? + true; - const currentFields = currentTables[tableIndex]?.fields ?? []; + const currentFields = + tableIndex >= 0 ? (currentTables[tableIndex]?.fields ?? []) : []; const allFieldNames = fieldsData.map((f) => f.fieldName); - // Remove exclude flags from all fields - const newFields = currentFields - .map((fieldConfig) => { - const fieldName = fieldConfig?.fieldName; - if (fieldName && allFieldNames.includes(fieldName)) { - const { exclude: _, ...rest } = fieldConfig; - // If only fieldName is left, don't include it - if (Object.keys(rest).length === 1 && rest.fieldName) { - return null; + let newFields: any[]; + let newTables: any[]; + + if (effectiveIncludeAllFieldsByDefault) { + // If includeAllFieldsByDefault is true, remove all field entries (or just remove exclude flags) + // since all fields are included by default + newFields = currentFields + .map((fieldConfig) => { + const fieldName = fieldConfig?.fieldName; + if (fieldName && allFieldNames.includes(fieldName)) { + const { exclude: _, ...rest } = fieldConfig; + // If only fieldName is left, don't include it + if (Object.keys(rest).length === 1 && rest.fieldName) { + return null; + } + return Object.keys(rest).length > 1 ? rest : null; } - return Object.keys(rest).length > 1 ? rest : null; + return fieldConfig; + }) + .filter((f) => f !== null) as any[]; + + newTables = [...currentTables]; + if (tableIndex < 0) { + // Table doesn't exist, but with includeAllFieldsByDefault=true, we don't need to add it + return; + } + + if (newFields.length === 0) { + // No fields left, remove fields array or entire table entry if only tableName and fields + const table = newTables[tableIndex]!; + const tableKeys = Object.keys(table); + const hasOnlyTableNameAndFields = + tableKeys.length === 2 && + tableKeys.includes("tableName") && + tableKeys.includes("fields"); + if (hasOnlyTableNameAndFields) { + const filteredTables = currentTables.filter( + (_, i) => i !== tableIndex, + ); + setValue( + `config.${configIndex}.tables` as any, + filteredTables.length > 0 ? filteredTables : undefined, + { shouldDirty: true }, + ); + } else { + newTables[tableIndex] = { + ...newTables[tableIndex]!, + fields: undefined, + }; + setValue(`config.${configIndex}.tables` as any, newTables, { + shouldDirty: true, + }); + } + } else { + newTables[tableIndex] = { + ...newTables[tableIndex]!, + fields: newFields, + }; + setValue(`config.${configIndex}.tables` as any, newTables, { + shouldDirty: true, + }); + } + } else { + // If includeAllFieldsByDefault is false, add all fields to the array (or ensure they're all there) + // Create a map of existing field configs + const fieldConfigMap = new Map( + currentFields.map((f) => [f?.fieldName, f]), + ); + + // Ensure all fields are in the array without exclude flags + newFields = allFieldNames.map((fieldName) => { + const existing = fieldConfigMap.get(fieldName); + if (existing) { + // Remove exclude flag if present + const { exclude: _, ...rest } = existing; + return rest; } - return fieldConfig; - }) - .filter((f) => f !== null) as any[]; - - const newTables = [...currentTables]; - if (newFields.length === 0) { - // No fields left, remove fields array or entire table entry if only tableName and fields - if (Object.keys(newTables[tableIndex]!).length === 2) { - const filteredTables = currentTables.filter((_, i) => i !== tableIndex); + // Add new field entry + return { fieldName }; + }); + + if (tableIndex < 0) { + // Table doesn't exist, add it with all fields setValue( `config.${configIndex}.tables` as any, - filteredTables.length > 0 ? filteredTables : undefined, + [...currentTables, { tableName, fields: newFields }], { shouldDirty: true }, ); } else { + // Update existing table + newTables = [...currentTables]; newTables[tableIndex] = { ...newTables[tableIndex]!, - fields: undefined, + fields: newFields, }; setValue(`config.${configIndex}.tables` as any, newTables, { shouldDirty: true, }); } - } else { - newTables[tableIndex] = { - ...newTables[tableIndex]!, - fields: newFields, - }; - setValue(`config.${configIndex}.tables` as any, newTables, { - shouldDirty: true, - }); } }, [ configType, @@ -619,6 +794,7 @@ export function MetadataFieldsDialog({ allTablesConfig, setValue, fieldsData, + topLevelIncludeAllFieldsByDefault, ]); // Helper to exclude all fields @@ -1082,6 +1258,71 @@ export function MetadataFieldsDialog({ ); }} /> + { + const isDefault = field.value === undefined; + const effectiveValue = + field.value ?? topLevelIncludeAllFieldsByDefault ?? true; + return ( + + + Include All Fields By Default{" "} + + + + + + + + ); + }} + /> diff --git a/packages/typegen/web/src/components/MetadataTablesEditor.tsx b/packages/typegen/web/src/components/MetadataTablesEditor.tsx index 4983145..7ff817d 100644 --- a/packages/typegen/web/src/components/MetadataTablesEditor.tsx +++ b/packages/typegen/web/src/components/MetadataTablesEditor.tsx @@ -63,11 +63,23 @@ function FieldCountCell({ name: `config.${configIndex}.tables` as const, }); + // Get the top-level includeAllFieldsByDefault value + const topLevelIncludeAllFieldsByDefault = useWatch({ + control, + name: `config.${configIndex}.includeAllFieldsByDefault` as const, + }); + const tableConfig = Array.isArray(allTablesConfig) ? allTablesConfig.find((t) => t?.tableName === tableName) : undefined; const fieldsConfig = tableConfig?.fields ?? []; + // Get the effective includeAllFieldsByDefault value (table-level override or top-level default) + const effectiveIncludeAllFieldsByDefault = + tableConfig?.includeAllFieldsByDefault ?? + topLevelIncludeAllFieldsByDefault ?? + true; + const fieldCount = useMemo(() => { if (!parsedMetadata?.entitySets || !parsedMetadata?.entityTypes) { return undefined; @@ -94,14 +106,17 @@ function FieldCountCell({ const includedFieldCount = useMemo(() => { if (fieldCount === undefined) return undefined; - // Count excluded fields - const excludedFields = fieldsConfig.filter( - (f) => f?.exclude === true, - ).length; - - // Total fields minus excluded fields - return fieldCount - excludedFields; - }, [fieldCount, fieldsConfig]); + if (effectiveIncludeAllFieldsByDefault) { + // If includeAllFieldsByDefault is true, count all fields minus explicitly excluded ones + const excludedFields = fieldsConfig.filter( + (f) => f?.exclude === true, + ).length; + return fieldCount - excludedFields; + } else { + // If includeAllFieldsByDefault is false, only count fields explicitly in the array that are not excluded + return fieldsConfig.filter((f) => f?.exclude !== true).length; + } + }, [fieldCount, fieldsConfig, effectiveIncludeAllFieldsByDefault]); if (isLoading) { return ;