Skip to content

Commit 98938bf

Browse files
authored
Merge pull request #684 from objectstack-ai/copilot/evaluate-map-syntax-support
2 parents 5a19f62 + b2c6dc2 commit 98938bf

8 files changed

Lines changed: 69 additions & 57 deletions

File tree

content/docs/getting-started/core-concepts.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,9 @@ export default defineStack({
3838
objects: [{
3939
name: 'user',
4040
label: 'User',
41-
fields: [
42-
{ name: 'phone', label: 'Phone Number', type: 'phone', required: true },
43-
],
41+
fields: {
42+
phone: { label: 'Phone Number', type: 'phone', required: true },
43+
},
4444
}],
4545
});
4646
```

content/docs/guides/common-patterns.mdx

Lines changed: 39 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,19 @@ export default defineStack({
2727
{
2828
name: 'project',
2929
label: 'Project',
30-
fields: [
31-
{ name: 'title', label: 'Title', type: 'text', required: true, maxLength: 200 },
32-
{ name: 'description', label: 'Description', type: 'textarea' },
33-
{ name: 'status', label: 'Status', type: 'select', options: [
30+
fields: {
31+
title: { label: 'Title', type: 'text', required: true, maxLength: 200 },
32+
description: { label: 'Description', type: 'textarea' },
33+
status: { label: 'Status', type: 'select', options: [
3434
{ label: 'Planning', value: 'planning', default: true },
3535
{ label: 'Active', value: 'active' },
3636
{ label: 'Complete', value: 'complete' },
3737
{ label: 'Archived', value: 'archived' }
3838
]},
39-
{ name: 'owner', label: 'Owner', type: 'lookup', reference: 'user' },
40-
{ name: 'due_date', label: 'Due Date', type: 'date' },
41-
{ name: 'budget', label: 'Budget', type: 'currency', currencyConfig: { precision: 2, defaultCurrency: 'USD' } }
42-
]
39+
owner: { label: 'Owner', type: 'lookup', reference: 'user' },
40+
due_date: { label: 'Due Date', type: 'date' },
41+
budget: { label: 'Budget', type: 'currency', currencyConfig: { precision: 2, defaultCurrency: 'USD' } },
42+
}
4343
}
4444
]
4545
});
@@ -85,27 +85,27 @@ Create a parent-child relationship where child records are cascaded on delete.
8585
{
8686
name: 'order',
8787
label: 'Order',
88-
fields: [
89-
{ name: 'order_number', label: 'Order #', type: 'autonumber', autonumberFormat: 'ORD-{0000}' },
90-
{ name: 'customer', label: 'Customer', type: 'lookup', reference: 'contact' },
91-
{ name: 'total', label: 'Total', type: 'summary', summaryOperations: ['sum'] },
92-
{ name: 'status', label: 'Status', type: 'select', options: [
88+
fields: {
89+
order_number: { label: 'Order #', type: 'autonumber', autonumberFormat: 'ORD-{0000}' },
90+
customer: { label: 'Customer', type: 'lookup', reference: 'contact' },
91+
total: { label: 'Total', type: 'summary', summaryOperations: ['sum'] },
92+
status: { label: 'Status', type: 'select', options: [
9393
{ label: 'Draft', value: 'draft', default: true },
9494
{ label: 'Submitted', value: 'submitted' },
9595
{ label: 'Fulfilled', value: 'fulfilled' }
96-
]}
97-
]
96+
]},
97+
}
9898
},
9999
{
100100
name: 'order_line',
101101
label: 'Order Line',
102-
fields: [
103-
{ name: 'order', label: 'Order', type: 'master_detail', reference: 'order' },
104-
{ name: 'product', label: 'Product', type: 'lookup', reference: 'product' },
105-
{ name: 'quantity', label: 'Qty', type: 'number', min: 1, required: true },
106-
{ name: 'unit_price', label: 'Unit Price', type: 'currency' },
107-
{ name: 'line_total', label: 'Line Total', type: 'formula', expression: 'quantity * unit_price' }
108-
]
102+
fields: {
103+
order: { label: 'Order', type: 'master_detail', reference: 'order' },
104+
product: { label: 'Product', type: 'lookup', reference: 'product' },
105+
quantity: { label: 'Qty', type: 'number', min: 1, required: true },
106+
unit_price: { label: 'Unit Price', type: 'currency' },
107+
line_total: { label: 'Line Total', type: 'formula', expression: 'quantity * unit_price' },
108+
}
109109
}
110110
]
111111
}
@@ -257,18 +257,18 @@ Define a multi-step approval process for records.
257257
objects: [{
258258
name: 'expense_report',
259259
label: 'Expense Report',
260-
fields: [
261-
{ name: 'title', label: 'Title', type: 'text', required: true },
262-
{ name: 'amount', label: 'Amount', type: 'currency' },
263-
{ name: 'status', label: 'Status', type: 'select', options: [
260+
fields: {
261+
title: { label: 'Title', type: 'text', required: true },
262+
amount: { label: 'Amount', type: 'currency' },
263+
status: { label: 'Status', type: 'select', options: [
264264
{ label: 'Draft', value: 'draft', default: true },
265265
{ label: 'Submitted', value: 'submitted' },
266266
{ label: 'Approved', value: 'approved' },
267267
{ label: 'Rejected', value: 'rejected' }
268268
]},
269-
{ name: 'submitted_by', label: 'Submitted By', type: 'lookup', reference: 'user' },
270-
{ name: 'approved_by', label: 'Approved By', type: 'lookup', reference: 'user' }
271-
],
269+
submitted_by: { label: 'Submitted By', type: 'lookup', reference: 'user' },
270+
approved_by: { label: 'Approved By', type: 'lookup', reference: 'user' },
271+
},
272272
enable: {
273273
trackHistory: true,
274274
apiEnabled: true
@@ -397,19 +397,19 @@ Restrict field visibility and editability based on user profiles.
397397
objects: [{
398398
name: 'employee',
399399
label: 'Employee',
400-
fields: [
401-
{ name: 'name', label: 'Name', type: 'text', required: true },
402-
{ name: 'email', label: 'Email', type: 'email', required: true },
403-
{ name: 'department', label: 'Department', type: 'lookup', reference: 'department' },
400+
fields: {
401+
name: { label: 'Name', type: 'text', required: true },
402+
email: { label: 'Email', type: 'email', required: true },
403+
department: { label: 'Department', type: 'lookup', reference: 'department' },
404404
// Sensitive fields with encryption
405-
{ name: 'salary', label: 'Salary', type: 'currency',
405+
salary: { label: 'Salary', type: 'currency',
406406
hidden: true, // Hidden from default views
407407
encryptionConfig: {
408408
algorithm: 'aes-256-gcm',
409409
keyRotation: true
410410
}
411411
},
412-
{ name: 'ssn', label: 'SSN', type: 'text',
412+
ssn: { label: 'SSN', type: 'text',
413413
hidden: true,
414414
maskingRule: {
415415
strategy: 'partial',
@@ -421,8 +421,8 @@ Restrict field visibility and editability based on user profiles.
421421
algorithm: 'aes-256-gcm',
422422
keyRotation: true
423423
}
424-
}
425-
]
424+
},
425+
}
426426
}]
427427
}
428428
```
@@ -441,9 +441,9 @@ import { defineStack } from '@objectstack/spec';
441441
export default defineStack({
442442
objects: [
443443
// Pattern 1: CRUD objects
444-
{ name: 'project', label: 'Project', fields: [/* ... */] },
444+
{ name: 'project', label: 'Project', fields: {/* ... */} },
445445
// Pattern 2: Master-detail
446-
{ name: 'task', label: 'Task', fields: [/* ... */] },
446+
{ name: 'task', label: 'Task', fields: {/* ... */} },
447447
],
448448
views: [
449449
// Pattern 3: List views

packages/cli/src/commands/compile.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import path from 'path';
55
import fs from 'fs';
66
import chalk from 'chalk';
77
import { ZodError } from 'zod';
8-
import { ObjectStackDefinitionSchema } from '@objectstack/spec';
8+
import { ObjectStackDefinitionSchema, normalizeStackInput } from '@objectstack/spec';
99
import { loadConfig } from '../utils/config.js';
1010
import {
1111
printHeader,
@@ -41,9 +41,10 @@ export const compileCommand = new Command('compile')
4141
printKV('Load time', `${duration}ms`);
4242
}
4343

44-
// 2. Validate against Protocol
44+
// 2. Normalize map-formatted stack definition and validate against Protocol
4545
if (!options.json) printStep('Validating protocol compliance...');
46-
const result = ObjectStackDefinitionSchema.safeParse(config);
46+
const normalized = normalizeStackInput(config as Record<string, unknown>);
47+
const result = ObjectStackDefinitionSchema.safeParse(normalized);
4748

4849
if (!result.success) {
4950
if (options.json) {

packages/cli/src/commands/doctor.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import chalk from 'chalk';
55
import { execSync } from 'child_process';
66
import fs from 'fs';
77
import path from 'path';
8+
import { normalizeStackInput } from '@objectstack/spec';
89
import { printHeader, printSuccess, printWarning, printError, printStep, printInfo } from '../utils/format.js';
910
import { loadConfig, configExists } from '../utils/config.js';
1011

@@ -476,7 +477,8 @@ export const doctorCommand = new Command('doctor')
476477
if (configExists()) {
477478
printStep('Loading configuration for analysis...');
478479
try {
479-
const { config } = await loadConfig();
480+
const { config: rawConfig } = await loadConfig();
481+
const config: any = normalizeStackInput(rawConfig as Record<string, unknown>);
480482

481483
// Circular dependency detection
482484
if (Array.isArray(config.objects) && config.objects.length > 0) {

packages/cli/src/commands/info.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { Command } from 'commander';
44
import chalk from 'chalk';
5+
import { normalizeStackInput } from '@objectstack/spec';
56
import { loadConfig } from '../utils/config.js';
67
import {
78
printHeader,
@@ -26,7 +27,8 @@ export const infoCommand = new Command('info')
2627
}
2728

2829
try {
29-
const { config, absolutePath, duration } = await loadConfig(configPath);
30+
const { config: rawConfig, absolutePath, duration } = await loadConfig(configPath);
31+
const config: any = normalizeStackInput(rawConfig as Record<string, unknown>);
3032
const stats = collectMetadataStats(config);
3133

3234
if (options.json) {

packages/cli/src/commands/lint.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { Command } from 'commander';
44
import chalk from 'chalk';
5+
import { normalizeStackInput } from '@objectstack/spec';
56
import { loadConfig } from '../utils/config.js';
67
import {
78
printHeader,
@@ -207,7 +208,8 @@ export const lintCommand = new Command('lint')
207208
printInfo(`Config: ${chalk.white(absolutePath)}`);
208209
}
209210

210-
const issues = lintConfig(config);
211+
const normalized = normalizeStackInput(config as Record<string, unknown>);
212+
const issues = lintConfig(normalized);
211213

212214
// ── JSON output ──
213215
if (options.json) {

packages/cli/src/commands/validate.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { Command } from 'commander';
44
import chalk from 'chalk';
55
import { ZodError } from 'zod';
6-
import { ObjectStackDefinitionSchema } from '@objectstack/spec';
6+
import { ObjectStackDefinitionSchema, normalizeStackInput } from '@objectstack/spec';
77
import { loadConfig } from '../utils/config.js';
88
import {
99
printHeader,
@@ -39,9 +39,10 @@ export const validateCommand = new Command('validate')
3939
printKV('Load time', `${duration}ms`);
4040
}
4141

42-
// 2. Validate against schema
42+
// 2. Normalize map-formatted stack definition and validate against schema
4343
if (!options.json) printStep('Validating against ObjectStack Protocol...');
44-
const result = ObjectStackDefinitionSchema.safeParse(config);
44+
const normalized = normalizeStackInput(config as Record<string, unknown>);
45+
const result = ObjectStackDefinitionSchema.safeParse(normalized);
4546

4647
if (!result.success) {
4748
if (options.json) {

packages/cli/src/utils/format.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -134,15 +134,19 @@ export interface MetadataStats {
134134
}
135135

136136
export function collectMetadataStats(config: any): MetadataStats {
137-
const count = (arr: any) => (Array.isArray(arr) ? arr.length : 0);
137+
const count = (val: any) => {
138+
if (Array.isArray(val)) return val.length;
139+
if (val && typeof val === 'object') return Object.keys(val).length;
140+
return 0;
141+
};
138142

139143
// Count total fields across all objects
140144
let fields = 0;
141-
if (Array.isArray(config.objects)) {
142-
for (const obj of config.objects) {
143-
if (obj.fields && typeof obj.fields === 'object') {
144-
fields += Object.keys(obj.fields).length;
145-
}
145+
const objects = Array.isArray(config.objects) ? config.objects :
146+
(config.objects && typeof config.objects === 'object' ? Object.values(config.objects) : []);
147+
for (const obj of objects as any[]) {
148+
if (obj.fields && typeof obj.fields === 'object') {
149+
fields += Object.keys(obj.fields).length;
146150
}
147151
}
148152

0 commit comments

Comments
 (0)