diff --git a/content/docs/getting-started/core-concepts.mdx b/content/docs/getting-started/core-concepts.mdx index 11830611b1..5240778b82 100644 --- a/content/docs/getting-started/core-concepts.mdx +++ b/content/docs/getting-started/core-concepts.mdx @@ -38,9 +38,9 @@ export default defineStack({ objects: [{ name: 'user', label: 'User', - fields: [ - { name: 'phone', label: 'Phone Number', type: 'phone', required: true }, - ], + fields: { + phone: { label: 'Phone Number', type: 'phone', required: true }, + }, }], }); ``` diff --git a/content/docs/guides/common-patterns.mdx b/content/docs/guides/common-patterns.mdx index 40d4082fd4..1699345796 100644 --- a/content/docs/guides/common-patterns.mdx +++ b/content/docs/guides/common-patterns.mdx @@ -27,19 +27,19 @@ export default defineStack({ { name: 'project', label: 'Project', - fields: [ - { name: 'title', label: 'Title', type: 'text', required: true, maxLength: 200 }, - { name: 'description', label: 'Description', type: 'textarea' }, - { name: 'status', label: 'Status', type: 'select', options: [ + fields: { + title: { label: 'Title', type: 'text', required: true, maxLength: 200 }, + description: { label: 'Description', type: 'textarea' }, + status: { label: 'Status', type: 'select', options: [ { label: 'Planning', value: 'planning', default: true }, { label: 'Active', value: 'active' }, { label: 'Complete', value: 'complete' }, { label: 'Archived', value: 'archived' } ]}, - { name: 'owner', label: 'Owner', type: 'lookup', reference: 'user' }, - { name: 'due_date', label: 'Due Date', type: 'date' }, - { name: 'budget', label: 'Budget', type: 'currency', currencyConfig: { precision: 2, defaultCurrency: 'USD' } } - ] + owner: { label: 'Owner', type: 'lookup', reference: 'user' }, + due_date: { label: 'Due Date', type: 'date' }, + budget: { label: 'Budget', type: 'currency', currencyConfig: { precision: 2, defaultCurrency: 'USD' } }, + } } ] }); @@ -85,27 +85,27 @@ Create a parent-child relationship where child records are cascaded on delete. { name: 'order', label: 'Order', - fields: [ - { name: 'order_number', label: 'Order #', type: 'autonumber', autonumberFormat: 'ORD-{0000}' }, - { name: 'customer', label: 'Customer', type: 'lookup', reference: 'contact' }, - { name: 'total', label: 'Total', type: 'summary', summaryOperations: ['sum'] }, - { name: 'status', label: 'Status', type: 'select', options: [ + fields: { + order_number: { label: 'Order #', type: 'autonumber', autonumberFormat: 'ORD-{0000}' }, + customer: { label: 'Customer', type: 'lookup', reference: 'contact' }, + total: { label: 'Total', type: 'summary', summaryOperations: ['sum'] }, + status: { label: 'Status', type: 'select', options: [ { label: 'Draft', value: 'draft', default: true }, { label: 'Submitted', value: 'submitted' }, { label: 'Fulfilled', value: 'fulfilled' } - ]} - ] + ]}, + } }, { name: 'order_line', label: 'Order Line', - fields: [ - { name: 'order', label: 'Order', type: 'master_detail', reference: 'order' }, - { name: 'product', label: 'Product', type: 'lookup', reference: 'product' }, - { name: 'quantity', label: 'Qty', type: 'number', min: 1, required: true }, - { name: 'unit_price', label: 'Unit Price', type: 'currency' }, - { name: 'line_total', label: 'Line Total', type: 'formula', expression: 'quantity * unit_price' } - ] + fields: { + order: { label: 'Order', type: 'master_detail', reference: 'order' }, + product: { label: 'Product', type: 'lookup', reference: 'product' }, + quantity: { label: 'Qty', type: 'number', min: 1, required: true }, + unit_price: { label: 'Unit Price', type: 'currency' }, + line_total: { label: 'Line Total', type: 'formula', expression: 'quantity * unit_price' }, + } } ] } @@ -257,18 +257,18 @@ Define a multi-step approval process for records. objects: [{ name: 'expense_report', label: 'Expense Report', - fields: [ - { name: 'title', label: 'Title', type: 'text', required: true }, - { name: 'amount', label: 'Amount', type: 'currency' }, - { name: 'status', label: 'Status', type: 'select', options: [ + fields: { + title: { label: 'Title', type: 'text', required: true }, + amount: { label: 'Amount', type: 'currency' }, + status: { label: 'Status', type: 'select', options: [ { label: 'Draft', value: 'draft', default: true }, { label: 'Submitted', value: 'submitted' }, { label: 'Approved', value: 'approved' }, { label: 'Rejected', value: 'rejected' } ]}, - { name: 'submitted_by', label: 'Submitted By', type: 'lookup', reference: 'user' }, - { name: 'approved_by', label: 'Approved By', type: 'lookup', reference: 'user' } - ], + submitted_by: { label: 'Submitted By', type: 'lookup', reference: 'user' }, + approved_by: { label: 'Approved By', type: 'lookup', reference: 'user' }, + }, enable: { trackHistory: true, apiEnabled: true @@ -397,19 +397,19 @@ Restrict field visibility and editability based on user profiles. objects: [{ name: 'employee', label: 'Employee', - fields: [ - { name: 'name', label: 'Name', type: 'text', required: true }, - { name: 'email', label: 'Email', type: 'email', required: true }, - { name: 'department', label: 'Department', type: 'lookup', reference: 'department' }, + fields: { + name: { label: 'Name', type: 'text', required: true }, + email: { label: 'Email', type: 'email', required: true }, + department: { label: 'Department', type: 'lookup', reference: 'department' }, // Sensitive fields with encryption - { name: 'salary', label: 'Salary', type: 'currency', + salary: { label: 'Salary', type: 'currency', hidden: true, // Hidden from default views encryptionConfig: { algorithm: 'aes-256-gcm', keyRotation: true } }, - { name: 'ssn', label: 'SSN', type: 'text', + ssn: { label: 'SSN', type: 'text', hidden: true, maskingRule: { strategy: 'partial', @@ -421,8 +421,8 @@ Restrict field visibility and editability based on user profiles. algorithm: 'aes-256-gcm', keyRotation: true } - } - ] + }, + } }] } ``` @@ -441,9 +441,9 @@ import { defineStack } from '@objectstack/spec'; export default defineStack({ objects: [ // Pattern 1: CRUD objects - { name: 'project', label: 'Project', fields: [/* ... */] }, + { name: 'project', label: 'Project', fields: {/* ... */} }, // Pattern 2: Master-detail - { name: 'task', label: 'Task', fields: [/* ... */] }, + { name: 'task', label: 'Task', fields: {/* ... */} }, ], views: [ // Pattern 3: List views diff --git a/packages/cli/src/commands/compile.ts b/packages/cli/src/commands/compile.ts index b6a1fa8418..2a5dffb2e2 100644 --- a/packages/cli/src/commands/compile.ts +++ b/packages/cli/src/commands/compile.ts @@ -5,7 +5,7 @@ import path from 'path'; import fs from 'fs'; import chalk from 'chalk'; import { ZodError } from 'zod'; -import { ObjectStackDefinitionSchema } from '@objectstack/spec'; +import { ObjectStackDefinitionSchema, normalizeStackInput } from '@objectstack/spec'; import { loadConfig } from '../utils/config.js'; import { printHeader, @@ -41,9 +41,10 @@ export const compileCommand = new Command('compile') printKV('Load time', `${duration}ms`); } - // 2. Validate against Protocol + // 2. Normalize map-formatted stack definition and validate against Protocol if (!options.json) printStep('Validating protocol compliance...'); - const result = ObjectStackDefinitionSchema.safeParse(config); + const normalized = normalizeStackInput(config as Record); + const result = ObjectStackDefinitionSchema.safeParse(normalized); if (!result.success) { if (options.json) { diff --git a/packages/cli/src/commands/doctor.ts b/packages/cli/src/commands/doctor.ts index c658ed423b..2e473bedcb 100644 --- a/packages/cli/src/commands/doctor.ts +++ b/packages/cli/src/commands/doctor.ts @@ -5,6 +5,7 @@ import chalk from 'chalk'; import { execSync } from 'child_process'; import fs from 'fs'; import path from 'path'; +import { normalizeStackInput } from '@objectstack/spec'; import { printHeader, printSuccess, printWarning, printError, printStep, printInfo } from '../utils/format.js'; import { loadConfig, configExists } from '../utils/config.js'; @@ -476,7 +477,8 @@ export const doctorCommand = new Command('doctor') if (configExists()) { printStep('Loading configuration for analysis...'); try { - const { config } = await loadConfig(); + const { config: rawConfig } = await loadConfig(); + const config: any = normalizeStackInput(rawConfig as Record); // Circular dependency detection if (Array.isArray(config.objects) && config.objects.length > 0) { diff --git a/packages/cli/src/commands/info.ts b/packages/cli/src/commands/info.ts index f08c56f03d..2dd37d7a61 100644 --- a/packages/cli/src/commands/info.ts +++ b/packages/cli/src/commands/info.ts @@ -2,6 +2,7 @@ import { Command } from 'commander'; import chalk from 'chalk'; +import { normalizeStackInput } from '@objectstack/spec'; import { loadConfig } from '../utils/config.js'; import { printHeader, @@ -26,7 +27,8 @@ export const infoCommand = new Command('info') } try { - const { config, absolutePath, duration } = await loadConfig(configPath); + const { config: rawConfig, absolutePath, duration } = await loadConfig(configPath); + const config: any = normalizeStackInput(rawConfig as Record); const stats = collectMetadataStats(config); if (options.json) { diff --git a/packages/cli/src/commands/lint.ts b/packages/cli/src/commands/lint.ts index c4801c568a..a03c586e98 100644 --- a/packages/cli/src/commands/lint.ts +++ b/packages/cli/src/commands/lint.ts @@ -2,6 +2,7 @@ import { Command } from 'commander'; import chalk from 'chalk'; +import { normalizeStackInput } from '@objectstack/spec'; import { loadConfig } from '../utils/config.js'; import { printHeader, @@ -207,7 +208,8 @@ export const lintCommand = new Command('lint') printInfo(`Config: ${chalk.white(absolutePath)}`); } - const issues = lintConfig(config); + const normalized = normalizeStackInput(config as Record); + const issues = lintConfig(normalized); // ── JSON output ── if (options.json) { diff --git a/packages/cli/src/commands/validate.ts b/packages/cli/src/commands/validate.ts index 2bc8b7fa8c..c1872594f9 100644 --- a/packages/cli/src/commands/validate.ts +++ b/packages/cli/src/commands/validate.ts @@ -3,7 +3,7 @@ import { Command } from 'commander'; import chalk from 'chalk'; import { ZodError } from 'zod'; -import { ObjectStackDefinitionSchema } from '@objectstack/spec'; +import { ObjectStackDefinitionSchema, normalizeStackInput } from '@objectstack/spec'; import { loadConfig } from '../utils/config.js'; import { printHeader, @@ -39,9 +39,10 @@ export const validateCommand = new Command('validate') printKV('Load time', `${duration}ms`); } - // 2. Validate against schema + // 2. Normalize map-formatted stack definition and validate against schema if (!options.json) printStep('Validating against ObjectStack Protocol...'); - const result = ObjectStackDefinitionSchema.safeParse(config); + const normalized = normalizeStackInput(config as Record); + const result = ObjectStackDefinitionSchema.safeParse(normalized); if (!result.success) { if (options.json) { diff --git a/packages/cli/src/utils/format.ts b/packages/cli/src/utils/format.ts index b317e50dcb..db9cba4ff1 100644 --- a/packages/cli/src/utils/format.ts +++ b/packages/cli/src/utils/format.ts @@ -134,15 +134,19 @@ export interface MetadataStats { } export function collectMetadataStats(config: any): MetadataStats { - const count = (arr: any) => (Array.isArray(arr) ? arr.length : 0); + const count = (val: any) => { + if (Array.isArray(val)) return val.length; + if (val && typeof val === 'object') return Object.keys(val).length; + return 0; + }; // Count total fields across all objects let fields = 0; - if (Array.isArray(config.objects)) { - for (const obj of config.objects) { - if (obj.fields && typeof obj.fields === 'object') { - fields += Object.keys(obj.fields).length; - } + const objects = Array.isArray(config.objects) ? config.objects : + (config.objects && typeof config.objects === 'object' ? Object.values(config.objects) : []); + for (const obj of objects as any[]) { + if (obj.fields && typeof obj.fields === 'object') { + fields += Object.keys(obj.fields).length; } }