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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions content/docs/getting-started/core-concepts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
},
}],
});
```
Expand Down
78 changes: 39 additions & 39 deletions content/docs/guides/common-patterns.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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' } },
}
}
]
});
Expand Down Expand Up @@ -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' },
}
}
]
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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',
Expand All @@ -421,8 +421,8 @@ Restrict field visibility and editability based on user profiles.
algorithm: 'aes-256-gcm',
keyRotation: true
}
}
]
},
}
}]
}
```
Expand All @@ -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
Expand Down
7 changes: 4 additions & 3 deletions packages/cli/src/commands/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<string, unknown>);
const result = ObjectStackDefinitionSchema.safeParse(normalized);

if (!result.success) {
if (options.json) {
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/commands/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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<string, unknown>);

// Circular dependency detection
if (Array.isArray(config.objects) && config.objects.length > 0) {
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/commands/info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<string, unknown>);
const stats = collectMetadataStats(config);

if (options.json) {
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/commands/lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<string, unknown>);
const issues = lintConfig(normalized);

// ── JSON output ──
if (options.json) {
Expand Down
7 changes: 4 additions & 3 deletions packages/cli/src/commands/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<string, unknown>);
const result = ObjectStackDefinitionSchema.safeParse(normalized);

if (!result.success) {
if (options.json) {
Expand Down
16 changes: 10 additions & 6 deletions packages/cli/src/utils/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand Down