diff --git a/.github/prompts/example-creator.prompt.md b/.github/prompts/example-creator.prompt.md index 66c9234f6..78bdfa5be 100644 --- a/.github/prompts/example-creator.prompt.md +++ b/.github/prompts/example-creator.prompt.md @@ -54,64 +54,51 @@ Create minimal examples for getting started. **Todo App Example:** ```typescript // examples/todo/src/objects/task.ts -import { ObjectSchema } from '@objectstack/spec'; +import { ObjectSchema, Field } from '@objectstack/spec/data'; -export const Task = ObjectSchema.parse({ +export const Task = ObjectSchema.create({ name: 'task', label: 'Task', icon: 'check-square', fields: { - title: { - name: 'title', + title: Field.text({ label: 'Title', - type: 'text', required: true, maxLength: 200, - }, + }), - description: { - name: 'description', + description: Field.textarea({ label: 'Description', - type: 'textarea', - }, + }), - status: { - name: 'status', + status: Field.select({ label: 'Status', - type: 'select', required: true, - defaultValue: 'todo', options: [ - { label: 'To Do', value: 'todo' }, + { label: 'To Do', value: 'todo', default: true }, { label: 'In Progress', value: 'in_progress' }, { label: 'Done', value: 'done' }, ], - }, + }), - priority: { - name: 'priority', + priority: Field.select({ label: 'Priority', - type: 'select', options: [ { label: 'Low', value: 'low' }, - { label: 'Medium', value: 'medium' }, + { label: 'Medium', value: 'medium', default: true }, { label: 'High', value: 'high' }, ], - }, + }), - due_date: { - name: 'due_date', + due_date: Field.date({ label: 'Due Date', - type: 'date', - }, + }), - completed_at: { - name: 'completed_at', + completed_at: Field.datetime({ label: 'Completed At', - type: 'datetime', readonly: true, - }, + }), }, enable: { @@ -127,62 +114,49 @@ Create examples demonstrating specific features. **Lookup Relationship Example:** ```typescript // examples/features/lookup-fields/src/objects/order.ts -export const Order = ObjectSchema.parse({ +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +export const Order = ObjectSchema.create({ name: 'order', label: 'Order', fields: { - order_number: { - name: 'order_number', - type: 'autonumber', + order_number: Field.autonumber({ label: 'Order Number', - }, + format: 'ORD-{0000}', + }), // Lookup to customer - customer_id: { - name: 'customer_id', + customer: Field.lookup('customer', { label: 'Customer', - type: 'lookup', - reference: 'customer', - referenceField: 'name', required: true, - }, + }), // Lookup to product - product_id: { - name: 'product_id', + product: Field.lookup('product', { label: 'Product', - type: 'lookup', - reference: 'product', - referenceField: 'name', required: true, - }, + }), // Formula field using lookup - unit_price: { - name: 'unit_price', + unit_price: Field.formula({ label: 'Unit Price', - type: 'formula', - expression: 'LOOKUP(product_id, "price")', + expression: 'LOOKUP(product, "price")', returnType: 'currency', - }, + }), - quantity: { - name: 'quantity', + quantity: Field.number({ label: 'Quantity', - type: 'number', required: true, min: 1, - }, + }), // Formula field calculating total - total: { - name: 'total', + total: Field.formula({ label: 'Total', - type: 'formula', expression: 'unit_price * quantity', returnType: 'currency', - }, + }), }, }); ``` @@ -190,35 +164,30 @@ export const Order = ObjectSchema.parse({ **Master-Detail Relationship Example:** ```typescript // examples/features/master-detail/src/objects/order-item.ts -export const OrderItem = ObjectSchema.parse({ +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +export const OrderItem = ObjectSchema.create({ name: 'order_item', label: 'Order Item', fields: { // Master-detail relationship (cascade delete) - order_id: { - name: 'order_id', + order: Field.masterDetail('order', { label: 'Order', - type: 'master_detail', - reference: 'order', cascade: 'delete', // Delete items when order is deleted required: true, - }, + }), - product_id: { - name: 'product_id', + product: Field.lookup('product', { label: 'Product', - type: 'lookup', - reference: 'product', required: true, - }, + }), - quantity: { - name: 'quantity', + quantity: Field.number({ label: 'Quantity', - type: 'number', required: true, - }, + min: 1, + }), }, }); ``` @@ -229,64 +198,62 @@ Create examples based on common use cases. **E-commerce Example:** ```typescript // examples/ecommerce/src/objects/product.ts -export const Product = ObjectSchema.parse({ +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +export const Product = ObjectSchema.create({ name: 'product', label: 'Product', + icon: 'package', fields: { - name: { - name: 'name', + name: Field.text({ label: 'Product Name', - type: 'text', required: true, - }, + maxLength: 255, + }), - sku: { - name: 'sku', + sku: Field.text({ label: 'SKU', - type: 'text', required: true, unique: true, - }, + }), - price: { - name: 'price', + price: Field.currency({ label: 'Price', - type: 'currency', required: true, - }, + scale: 2, + min: 0, + }), - stock_quantity: { - name: 'stock_quantity', + stock_quantity: Field.number({ label: 'Stock Quantity', - type: 'number', required: true, min: 0, - }, + }), - category: { - name: 'category', + category: Field.select({ label: 'Category', - type: 'select', options: [ { label: 'Electronics', value: 'electronics' }, { label: 'Clothing', value: 'clothing' }, { label: 'Books', value: 'books' }, ], - }, + }), - images: { - name: 'images', + images: Field.image({ label: 'Images', - type: 'image', multiple: true, - }, + }), - description: { - name: 'description', + description: Field.html({ label: 'Description', - type: 'html', - }, + }), + }, + + enable: { + apiEnabled: true, + searchable: true, + files: true, }, }); ``` @@ -294,79 +261,70 @@ export const Product = ObjectSchema.parse({ **HR Management Example:** ```typescript // examples/hr/src/objects/employee.ts -export const Employee = ObjectSchema.parse({ +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +export const Employee = ObjectSchema.create({ name: 'employee', label: 'Employee', + pluralLabel: 'Employees', + icon: 'user', fields: { - employee_id: { - name: 'employee_id', + employee_id: Field.autonumber({ label: 'Employee ID', - type: 'autonumber', - }, + format: 'EMP-{0000}', + }), - first_name: { - name: 'first_name', + first_name: Field.text({ label: 'First Name', - type: 'text', required: true, - }, + }), - last_name: { - name: 'last_name', + last_name: Field.text({ label: 'Last Name', - type: 'text', required: true, - }, + }), - email: { - name: 'email', + email: Field.email({ label: 'Work Email', - type: 'email', required: true, unique: true, - }, + }), - department_id: { - name: 'department_id', + department: Field.lookup('department', { label: 'Department', - type: 'lookup', - reference: 'department', - }, + }), - manager_id: { - name: 'manager_id', + manager: Field.lookup('employee', { label: 'Manager', - type: 'lookup', - reference: 'employee', - referenceField: 'full_name', - }, + description: 'Reports to', + }), - hire_date: { - name: 'hire_date', + hire_date: Field.date({ label: 'Hire Date', - type: 'date', required: true, - }, + }), - salary: { - name: 'salary', + salary: Field.currency({ label: 'Salary', - type: 'currency', - }, + scale: 2, + }), - status: { - name: 'status', + status: Field.select({ label: 'Status', - type: 'select', required: true, - defaultValue: 'active', options: [ - { label: 'Active', value: 'active' }, + { label: 'Active', value: 'active', default: true }, { label: 'On Leave', value: 'on_leave' }, { label: 'Terminated', value: 'terminated' }, ], - }, + }), + }, + + enable: { + trackHistory: true, + apiEnabled: true, + files: true, }, }); ``` @@ -377,48 +335,63 @@ Create examples showing advanced features. **Formula Field Example:** ```typescript // examples/advanced/formulas/src/objects/opportunity.ts -export const Opportunity = ObjectSchema.parse({ +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +export const Opportunity = ObjectSchema.create({ name: 'opportunity', label: 'Opportunity', + icon: 'dollar-sign', fields: { - // ... other fields + first_name: Field.text({ + label: 'First Name', + }), + + last_name: Field.text({ + label: 'Last Name', + }), + + account: Field.lookup('account', { + label: 'Account', + required: true, + }), + + amount: Field.currency({ + label: 'Amount', + scale: 2, + }), + + close_date: Field.date({ + label: 'Close Date', + }), // Simple formula - full_name: { - name: 'full_name', + full_name: Field.formula({ label: 'Full Name', - type: 'formula', expression: 'first_name + " " + last_name', returnType: 'text', - }, + }), // Formula with LOOKUP - account_industry: { - name: 'account_industry', + account_industry: Field.formula({ label: 'Account Industry', - type: 'formula', - expression: 'LOOKUP(account_id, "industry")', + expression: 'LOOKUP(account, "industry")', returnType: 'text', - }, + }), // Formula with conditional - risk_level: { - name: 'risk_level', + risk_level: Field.formula({ label: 'Risk Level', - type: 'formula', expression: 'IF(amount > 100000, "High", IF(amount > 50000, "Medium", "Low"))', returnType: 'text', - }, + }), // Formula with date calculation - days_to_close: { - name: 'days_to_close', + days_to_close: Field.formula({ label: 'Days to Close', - type: 'formula', expression: 'DATEDIFF(close_date, TODAY(), "days")', returnType: 'number', - }, + }), }, }); ``` @@ -426,36 +399,40 @@ export const Opportunity = ObjectSchema.parse({ **Rollup Summary Example:** ```typescript // examples/advanced/rollups/src/objects/account.ts -export const Account = ObjectSchema.parse({ +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +export const Account = ObjectSchema.create({ name: 'account', label: 'Account', + pluralLabel: 'Accounts', + icon: 'building', fields: { - // ... other fields + name: Field.text({ + label: 'Account Name', + required: true, + }), // Count related opportunities - opportunity_count: { - name: 'opportunity_count', + opportunity_count: Field.summary({ label: 'Number of Opportunities', - type: 'rollup_summary', - relatedObject: 'opportunity', - relatedField: 'account_id', - aggregateFunction: 'count', - }, + reference: 'opportunity', + summaryType: 'count', + }), // Sum related opportunities - total_opportunity_value: { - name: 'total_opportunity_value', + total_opportunity_value: Field.summary({ label: 'Total Opportunity Value', - type: 'rollup_summary', - relatedObject: 'opportunity', - relatedField: 'account_id', - fieldToAggregate: 'amount', - aggregateFunction: 'sum', - filters: { - stage: { $ne: 'lost' }, // Exclude lost opportunities - }, - }, + reference: 'opportunity', + summaryType: 'sum', + summaryField: 'amount', + referenceFilters: [['stage', '!=', 'lost']], // Exclude lost opportunities + }), + }, + + enable: { + trackHistory: true, + apiEnabled: true, }, }); ``` diff --git a/content/docs/introduction/architecture.mdx b/content/docs/introduction/architecture.mdx index 4a2690424..b9a4cc373 100644 --- a/content/docs/introduction/architecture.mdx +++ b/content/docs/introduction/architecture.mdx @@ -84,27 +84,37 @@ ObjectStack enforces **Separation of Concerns** through protocol boundaries: ```typescript // packages/crm/src/objects/customer.object.ts -import { Object, Field } from '@objectstack/spec'; +import { ObjectSchema, Field } from '@objectstack/spec/data'; -export const Customer = Object({ +export const Customer = ObjectSchema.create({ name: 'customer', label: 'Customer', + icon: 'building', + fields: { name: Field.text({ label: 'Company Name', required: true, maxLength: 120, }), + industry: Field.select({ label: 'Industry', - options: ['technology', 'finance', 'healthcare', 'retail'], + options: [ + { label: 'Technology', value: 'technology' }, + { label: 'Finance', value: 'finance' }, + { label: 'Healthcare', value: 'healthcare' }, + { label: 'Retail', value: 'retail' }, + ], }), + annual_revenue: Field.currency({ label: 'Annual Revenue', + scale: 2, }), - primary_contact: Field.lookup({ + + primary_contact: Field.lookup('contact', { label: 'Primary Contact', - object: 'contact', }), }, }); @@ -383,15 +393,37 @@ Here's how all three protocols collaborate for a **Kanban Board** feature: ### 1. ObjectQL: Define the Data ```typescript -export const Opportunity = Object({ +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +export const Opportunity = ObjectSchema.create({ name: 'opportunity', + label: 'Opportunity', + icon: 'target', + fields: { - title: Field.text({ required: true }), + title: Field.text({ + label: 'Title', + required: true, + }), + stage: Field.select({ - options: ['prospecting', 'qualification', 'proposal', 'closed_won'], + label: 'Stage', + options: [ + { label: 'Prospecting', value: 'prospecting', default: true }, + { label: 'Qualification', value: 'qualification' }, + { label: 'Proposal', value: 'proposal' }, + { label: 'Closed Won', value: 'closed_won' }, + ], + }), + + amount: Field.currency({ + label: 'Amount', + scale: 2, + }), + + customer: Field.lookup('customer', { + label: 'Customer', }), - amount: Field.currency(), - customer: Field.lookup({ object: 'customer' }), }, }); ``` diff --git a/content/docs/introduction/metadata-driven.mdx b/content/docs/introduction/metadata-driven.mdx index 1fce5a8b0..b3755f776 100644 --- a/content/docs/introduction/metadata-driven.mdx +++ b/content/docs/introduction/metadata-driven.mdx @@ -66,8 +66,13 @@ ObjectStack centralizes the "Intent" into a **single Protocol Definition**. The ```typescript // ONE definition (in objectstack.config.ts) -export const User = Object({ +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +export const User = ObjectSchema.create({ name: 'user', + label: 'User', + icon: 'user', + fields: { phone: Field.phone({ label: 'Phone Number', @@ -77,6 +82,8 @@ export const User = Object({ }); ``` +> **📘 Syntax Rules**: Always use `ObjectSchema.create()` with `Field.*` helpers for strict TypeScript validation and runtime checking. See [Object Definition Rules](#object-definition-rules) below. + From this single definition, ObjectStack automatically: ✅ Generates database schema @@ -188,14 +195,31 @@ React Flutter ```typescript // All you need: -export const Task = Object({ +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +export const Task = ObjectSchema.create({ name: 'task', + label: 'Task', + icon: 'check-square', + fields: { - title: Field.text({ required: true }), + title: Field.text({ + label: 'Title', + required: true, + }), + status: Field.select({ - options: ['todo', 'in_progress', 'done'], + label: 'Status', + options: [ + { label: 'To Do', value: 'todo', default: true }, + { label: 'In Progress', value: 'in_progress' }, + { label: 'Done', value: 'done' }, + ], + }), + + assignee: Field.lookup('user', { + label: 'Assignee', }), - assignee: Field.lookup({ object: 'user' }), }, }); @@ -246,6 +270,223 @@ You specify **exactly how** to draw each pixel. | **Flexibility** | Locked to tech stack | Technology agnostic | | **Boilerplate** | High (300+ lines) | Low (30 lines) | +## Object Definition Rules + +When defining objects and metadata in ObjectStack, follow these strict rules and principles: + +### 1. Always Use `ObjectSchema.create()` with `Field.*` Helpers + +**✅ Correct:** +```typescript +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +export const Account = ObjectSchema.create({ + name: 'account', + label: 'Account', + fields: { + name: Field.text({ required: true }), + industry: Field.select({ + options: [ + { label: 'Technology', value: 'technology' }, + { label: 'Finance', value: 'finance' }, + ], + }), + }, +}); +``` + +**❌ Deprecated:** +```typescript +// Old pattern - no runtime validation +const Account: ServiceObject = { + name: 'account', + fields: { + name: { type: 'text', required: true } + } +}; +``` + +**Why?** +- ✅ **Type Safety**: Compile-time type checking via `z.input` +- ✅ **Runtime Validation**: Zod validates structure at runtime +- ✅ **IDE Autocomplete**: Field helpers provide intelligent code completion +- ✅ **Error Prevention**: Catches typos and invalid configurations immediately + +### 2. Naming Conventions + +Follow these strict naming conventions for consistency: + +| Element | Convention | Examples | +|---------|-----------|----------| +| **Object Names** (machine names) | `snake_case` | `todo_task`, `project_milestone`, `user_profile` | +| **Field Names** (machine names) | `snake_case` | `first_name`, `annual_revenue`, `is_active` | +| **Constant Names** (exports) | `PascalCase` | `TodoTask`, `ProjectMilestone`, `UserProfile` | +| **Configuration Keys** (props) | `camelCase` | `maxLength`, `defaultValue`, `referenceFilters` | + +**Example:** +```typescript +// ✅ Correct naming +export const TodoTask = ObjectSchema.create({ + name: 'todo_task', // snake_case machine name + label: 'Todo Task', + + fields: { + due_date: Field.date({ // snake_case field name + label: 'Due Date', + defaultValue: null, // camelCase config key + }), + }, +}); +``` + +### 3. Select Field Options Must Use Label/Value Objects + +**✅ Correct:** +```typescript +status: Field.select({ + label: 'Status', + options: [ + { label: 'Open', value: 'open', default: true }, + { label: 'In Progress', value: 'in_progress' }, + { label: 'Closed', value: 'closed' }, + ], +}), +``` + +**❌ Incorrect:** +```typescript +status: Field.select({ + options: ['open', 'in_progress', 'closed'], // Wrong! +}), +``` + +**Why?** Option values are machine identifiers stored in the database and must be lowercase to avoid case-sensitivity issues in queries. + +### 4. Lookup Fields Must Specify Target Object + +**✅ Correct:** +```typescript +owner: Field.lookup('user', { + label: 'Owner', + required: true, +}), +``` + +**❌ Incorrect:** +```typescript +owner: Field.lookup({ + object: 'user', // Wrong property name +}), +``` + +### 5. Always Include Descriptive Labels + +**✅ Correct:** +```typescript +annual_revenue: Field.currency({ + label: 'Annual Revenue', + scale: 2, + min: 0, +}), +``` + +**❌ Avoid:** +```typescript +annual_revenue: Field.currency({ + // Missing label - field name will be used as fallback +}), +``` + +### 6. Use Enable Flags for Object Capabilities + +```typescript +export const Account = ObjectSchema.create({ + name: 'account', + label: 'Account', + + fields: { /* ... */ }, + + enable: { + trackHistory: true, // Enable field history tracking + searchable: true, // Include in global search + apiEnabled: true, // Expose via REST/GraphQL + files: true, // Enable file attachments + feeds: true, // Enable activity feed + activities: true, // Enable tasks and events + trash: true, // Enable soft delete + mru: true, // Track Most Recently Used + }, +}); +``` + +### Quick Reference + +```typescript +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +export const ExampleObject = ObjectSchema.create({ + name: 'example_object', // Required: snake_case + label: 'Example Object', // Required: Human-readable + pluralLabel: 'Example Objects', // Optional + icon: 'box', // Optional: Lucide icon name + description: 'Description text', // Optional + + fields: { + // Text field + text_field: Field.text({ + label: 'Text Field', + required: true, + maxLength: 255, + }), + + // Number field + number_field: Field.number({ + label: 'Number', + min: 0, + max: 100, + }), + + // Currency field + price: Field.currency({ + label: 'Price', + scale: 2, + min: 0, + }), + + // Select field + status: Field.select({ + label: 'Status', + options: [ + { label: 'Active', value: 'active', default: true }, + { label: 'Inactive', value: 'inactive' }, + ], + }), + + // Lookup field + owner: Field.lookup('user', { + label: 'Owner', + required: true, + }), + + // Boolean field + is_active: Field.boolean({ + label: 'Active', + defaultValue: true, + }), + + // Date field + due_date: Field.date({ + label: 'Due Date', + }), + }, + + enable: { + trackHistory: true, + apiEnabled: true, + }, +}); +``` + ## Next Steps - [The Stack](/docs/core-concepts/the-stack) - How the three protocols work together diff --git a/content/docs/objectos/i18n-standard.mdx b/content/docs/objectos/i18n-standard.mdx index 072f72057..3e3a48a0e 100644 --- a/content/docs/objectos/i18n-standard.mdx +++ b/content/docs/objectos/i18n-standard.mdx @@ -517,21 +517,23 @@ ObjectQL objects and fields can be **automatically translated**: ```typescript // Object definition -export default defineObject({ +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +export const Account = ObjectSchema.create({ name: 'account', label: 'account.label', // Translation key pluralLabel: 'account.pluralLabel', + icon: 'building', fields: { - name: { - type: 'text', + name: Field.text({ label: 'account.fields.name', // Translation key - }, - industry: { - type: 'select', + }), + + industry: Field.select({ label: 'account.fields.industry', - options: 'account.industries', // Translation key for array - }, + options: 'account.industries', // Translation key for options array + }), }, }); ``` diff --git a/content/docs/objectos/plugin-spec.mdx b/content/docs/objectos/plugin-spec.mdx index f3ecaaf2a..59c7e10f0 100644 --- a/content/docs/objectos/plugin-spec.mdx +++ b/content/docs/objectos/plugin-spec.mdx @@ -231,54 +231,43 @@ Define database objects using ObjectQL schema syntax: ```typescript // src/objects/account.object.ts -import { defineObject } from '@objectstack/core'; +import { ObjectSchema, Field } from '@objectstack/spec/data'; -export default defineObject({ +export const Account = ObjectSchema.create({ name: 'account', label: 'Account', pluralLabel: 'Accounts', + icon: 'building', fields: { - name: { - type: 'text', + name: Field.text({ label: 'Account Name', required: true, maxLength: 255, - }, + }), - industry: { - type: 'select', + industry: Field.select({ label: 'Industry', options: [ - { value: 'technology', label: 'Technology' }, - { value: 'finance', label: 'Finance' }, - { value: 'healthcare', label: 'Healthcare' }, + { label: 'Technology', value: 'technology' }, + { label: 'Finance', value: 'finance' }, + { label: 'Healthcare', value: 'healthcare' }, ], - }, + }), - annual_revenue: { - type: 'currency', + annual_revenue: Field.currency({ label: 'Annual Revenue', - precision: 2, - }, + scale: 2, + }), - primary_contact: { - type: 'lookup', + primary_contact: Field.lookup('contact', { label: 'Primary Contact', - reference: 'contact', - }, - - opportunities: { - type: 'reverse_lookup', - label: 'Opportunities', - reference: 'opportunity', - referenceField: 'account', - }, + }), }, enable: { trackHistory: true, - search: true, + searchable: true, apiEnabled: true, }, }); diff --git a/content/docs/objectql/schema.mdx b/content/docs/objectql/schema.mdx index 9036084dc..d4dc3bca4 100644 --- a/content/docs/objectql/schema.mdx +++ b/content/docs/objectql/schema.mdx @@ -43,14 +43,28 @@ label: Customer # JSON (Machine-generated) { "name": "customer", "label": "Customer" } +``` + +**TypeScript (Recommended for strict validation):** +```typescript +import { ObjectSchema, Field } from '@objectstack/spec/data'; -# TypeScript (Programmatic) -export default defineObject({ +export const Customer = ObjectSchema.create({ name: 'customer', - label: 'Customer' + label: 'Customer', + icon: 'building', + + fields: { + name: Field.text({ + label: 'Company Name', + required: true, + }), + }, }); ``` +> **📘 Best Practice**: Use `ObjectSchema.create()` with `Field.*` helpers in TypeScript for compile-time type checking and runtime validation. + ## Object Definition ### Minimal Example @@ -113,6 +127,66 @@ validations: message: "Budget must be positive" ``` +**TypeScript Complete Example:** + +```typescript +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +export const Project = ObjectSchema.create({ + name: 'project', + label: 'Project', + pluralLabel: 'Projects', + description: 'A business project or initiative', + icon: 'folder', + + fields: { + name: Field.text({ + label: 'Project Name', + required: true, + maxLength: 255, + searchable: true, + }), + + status: Field.select({ + label: 'Status', + options: [ + { label: 'Draft', value: 'draft', default: true }, + { label: 'Active', value: 'active' }, + { label: 'Completed', value: 'completed' }, + ], + }), + + budget: Field.currency({ + label: 'Budget', + scale: 2, + min: 0, + }), + + account: Field.lookup('account', { + label: 'Account', + required: true, + }), + }, + + enable: { + trackHistory: true, + searchable: true, + apiEnabled: true, + activities: true, + }, + + validations: [ + { + name: 'budget_positive', + type: 'script', + severity: 'error', + message: 'Budget must be positive', + condition: 'budget < 0', + }, + ], +}); +``` + ### Object Properties Reference | Property | Type | Required | Description | diff --git a/content/docs/references/api/contract.mdx b/content/docs/references/api/contract.mdx index d22aacade..1f72c687d 100644 --- a/content/docs/references/api/contract.mdx +++ b/content/docs/references/api/contract.mdx @@ -102,26 +102,6 @@ const result = ApiError.parse(data); ## ExportRequest -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **object** | `string` | ✅ | Object name (e.g. account) | -| **fields** | `string \| Object[]` | optional | Fields to retrieve | -| **where** | `any` | optional | Filtering criteria (WHERE) | -| **search** | `Object` | optional | Full-text search configuration ($search parameter) | -| **orderBy** | `Object[]` | optional | Sorting instructions (ORDER BY) | -| **limit** | `number` | optional | Max records to return (LIMIT) | -| **offset** | `number` | optional | Records to skip (OFFSET) | -| **cursor** | `Record` | optional | Cursor for keyset pagination | -| **joins** | `Object[]` | optional | Explicit Table Joins | -| **aggregations** | `Object[]` | optional | Aggregation functions | -| **groupBy** | `string[]` | optional | GROUP BY fields | -| **having** | `any` | optional | HAVING clause for aggregation filtering | -| **windowFunctions** | `Object[]` | optional | Window functions with OVER clause | -| **distinct** | `boolean` | optional | SELECT DISTINCT flag | -| **format** | `Enum<'csv' \| 'json' \| 'xlsx'>` | optional | | - --- diff --git a/content/docs/references/api/discovery.mdx b/content/docs/references/api/discovery.mdx index ab3b4cfdc..3c05f0c8f 100644 --- a/content/docs/references/api/discovery.mdx +++ b/content/docs/references/api/discovery.mdx @@ -47,6 +47,7 @@ const result = ApiCapabilities.parse(data); | :--- | :--- | :--- | :--- | | **data** | `string` | ✅ | e.g. /api/data | | **metadata** | `string` | ✅ | e.g. /api/meta | +| **ui** | `string` | optional | e.g. /api/ui | | **auth** | `string` | ✅ | e.g. /api/auth | | **automation** | `string` | optional | e.g. /api/automation | | **storage** | `string` | optional | e.g. /api/storage | diff --git a/content/docs/references/api/rest-server.mdx b/content/docs/references/api/rest-server.mdx index 79c65f009..4021a25da 100644 --- a/content/docs/references/api/rest-server.mdx +++ b/content/docs/references/api/rest-server.mdx @@ -156,6 +156,7 @@ const result = BatchEndpointsConfig.parse(data); | **apiPath** | `string` | optional | Full API path (defaults to `{basePath}`/`{version}`) | | **enableCrud** | `boolean` | optional | Enable automatic CRUD endpoint generation | | **enableMetadata** | `boolean` | optional | Enable metadata API endpoints | +| **enableUi** | `boolean` | optional | Enable UI API endpoints (Views, Menus, Layouts) | | **enableBatch** | `boolean` | optional | Enable batch operation endpoints | | **enableDiscovery** | `boolean` | optional | Enable API discovery endpoint | | **documentation** | `Object` | optional | OpenAPI/Swagger documentation config | diff --git a/content/docs/references/data/query.mdx b/content/docs/references/data/query.mdx index 2f649dbeb..8cc0e2927 100644 --- a/content/docs/references/data/query.mdx +++ b/content/docs/references/data/query.mdx @@ -152,6 +152,7 @@ Type: `string` | **orderBy** | `Object[]` | optional | Sorting instructions (ORDER BY) | | **limit** | `number` | optional | Max records to return (LIMIT) | | **offset** | `number` | optional | Records to skip (OFFSET) | +| **top** | `number` | optional | Alias for limit (OData compatibility) | | **cursor** | `Record` | optional | Cursor for keyset pagination | | **joins** | `Object[]` | optional | Explicit Table Joins | | **aggregations** | `Object[]` | optional | Aggregation functions | @@ -159,6 +160,7 @@ Type: `string` | **having** | `any` | optional | HAVING clause for aggregation filtering | | **windowFunctions** | `Object[]` | optional | Window functions with OVER clause | | **distinct** | `boolean` | optional | SELECT DISTINCT flag | +| **expand** | `Record` | optional | Recursive relation loading (nested queries) | --- diff --git a/content/docs/references/kernel/manifest.mdx b/content/docs/references/kernel/manifest.mdx index 501dab285..6d5fa1692 100644 --- a/content/docs/references/kernel/manifest.mdx +++ b/content/docs/references/kernel/manifest.mdx @@ -59,7 +59,7 @@ const result = Manifest.parse(data); | :--- | :--- | :--- | :--- | | **id** | `string` | ✅ | Unique package identifier (reverse domain style) | | **version** | `string` | ✅ | Package version (semantic versioning) | -| **type** | `Enum<'app' \| 'plugin' \| 'driver' \| 'module' \| 'objectql' \| 'gateway' \| 'adapter'>` | ✅ | Type of package | +| **type** | `Enum<'plugin' \| 'ui' \| 'driver' \| 'server' \| 'app' \| 'theme' \| 'agent' \| 'objectql' \| 'module' \| 'gateway' \| 'adapter'>` | ✅ | Type of package | | **name** | `string` | ✅ | Human-readable package name | | **description** | `string` | optional | Package description | | **permissions** | `string[]` | optional | Array of required permission strings | diff --git a/content/docs/references/kernel/plugin.mdx b/content/docs/references/kernel/plugin.mdx index 704fbef66..5b7a76d85 100644 --- a/content/docs/references/kernel/plugin.mdx +++ b/content/docs/references/kernel/plugin.mdx @@ -3,9 +3,9 @@ title: Plugin description: Plugin protocol schemas --- -Define an ObjectStack Plugin +Shared Plugin Types -Helper function for creating type-safe plugin definitions +These are the specialized plugin types common between Manifest (Package) and Plugin (Runtime). **Source:** `packages/spec/src/kernel/plugin.zod.ts` @@ -30,6 +30,10 @@ const result = Plugin.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **id** | `string` | optional | Unique Plugin ID (e.g. com.example.crm) | +| **type** | `Enum<'standard' \| 'ui' \| 'driver' \| 'server' \| 'app' \| 'theme' \| 'agent' \| 'objectql'>` | optional | Plugin Type categorization for runtime behavior | +| **staticPath** | `string` | optional | Absolute path to static assets (Required for type="ui-plugin") | +| **slug** | `string` | optional | URL path segment (Required for type="ui-plugin") | +| **default** | `boolean` | optional | Serve at root path (Only one "ui-plugin" can be default) | | **version** | `string` | optional | Semantic Version | | **description** | `string` | optional | | | **author** | `string` | optional | | diff --git a/content/prompts/platform/plugin.prompt.md b/content/prompts/platform/plugin.prompt.md index e1b9f5f4f..64bf06060 100644 --- a/content/prompts/platform/plugin.prompt.md +++ b/content/prompts/platform/plugin.prompt.md @@ -64,20 +64,48 @@ export default config; ### A. Define Objects (The Data Contract) **Reference:** `@objectstack/spec` -> `dist/data/object.zod.d.ts` -Use Zod schemas to define business entities. +Use `ObjectSchema.create()` with `Field.*` helpers for strict type checking and runtime validation. ```typescript // src/objects/todo.object.ts -import { ObjectSchema } from '@objectstack/spec/data'; +import { ObjectSchema, Field } from '@objectstack/spec/data'; -export const TodoObject = { +export const TodoTask = ObjectSchema.create({ name: 'todo_item', label: 'Todo Item', + icon: 'check-square', + fields: { - title: { type: 'text', required: true }, - is_completed: { type: 'boolean', defaultValue: false } - } -}; + title: Field.text({ + label: 'Title', + required: true, + maxLength: 255, + }), + + is_completed: Field.boolean({ + label: 'Completed', + defaultValue: false, + }), + + due_date: Field.date({ + label: 'Due Date', + }), + + priority: Field.select({ + label: 'Priority', + options: [ + { label: 'Low', value: 'low' }, + { label: 'Medium', value: 'medium', default: true }, + { label: 'High', value: 'high' }, + ], + }), + }, + + enable: { + apiEnabled: true, + trackHistory: true, + }, +}); ``` ### B. Implement Logic (The Runtime) diff --git a/content/prompts/plugin/app.prompt.md b/content/prompts/plugin/app.prompt.md index 502ee537a..e99a6b913 100644 --- a/content/prompts/plugin/app.prompt.md +++ b/content/prompts/plugin/app.prompt.md @@ -76,48 +76,69 @@ A "Complete" application must define metadata across these 5 layers: ## 3. Implementation Patterns ### A. Defining a Complex Object (Account) -// Definitions: dist/data/object.zod.d.ts + +Use `ObjectSchema.create()` with `Field.*` helpers for strict type checking and runtime validation. ```typescript -import { ObjectSchema } from '@objectstack/spec/data'; +import { ObjectSchema, Field } from '@objectstack/spec/data'; -export const AccountObject: ObjectSchema = { +export const Account = ObjectSchema.create({ name: 'account', label: 'Account', - enable: { - audit: true, // Track Field History - workflow: true, // Allow Process Builder - files: true // Attachments - }, + pluralLabel: 'Accounts', + icon: 'building', + fields: { - name: { type: 'text', required: true, searchable: true }, + // Text field with validation + name: Field.text({ + label: 'Account Name', + required: true, + searchable: true, + maxLength: 255, + }), - // Relationship - parent_id: { - type: 'lookup', - reference: 'account', - label: 'Parent Account' - }, + // Lookup relationship (Hierarchical) + parent_account: Field.lookup('account', { + label: 'Parent Account', + description: 'Parent company in hierarchy', + }), - // Status Logic - rating: { - type: 'select', - options: ['Hot', 'Warm', 'Cold'], - defaultValue: 'Warm' - }, + // Select field with options + rating: Field.select({ + label: 'Rating', + options: [ + { label: 'Hot', value: 'hot', color: '#FF0000' }, + { label: 'Warm', value: 'warm', color: '#FFA500', default: true }, + { label: 'Cold', value: 'cold', color: '#0000FF' }, + ], + }), - // Calculated - pipeline_value: { - type: 'rollup_summary', - reference: 'opportunity', - summaryType: 'sum', - summaryField: 'amount' - } - } -}; + // Currency field + annual_revenue: Field.currency({ + label: 'Annual Revenue', + scale: 2, + min: 0, + }), + + // Lookup to owner + owner: Field.lookup('user', { + label: 'Account Owner', + required: true, + }), + }, + + // Enable advanced features + enable: { + trackHistory: true, // Track field changes (audit) + apiEnabled: true, // Expose via REST/GraphQL + files: true, // Allow file attachments + feeds: true, // Enable activity feed + activities: true, // Enable tasks and events + trash: true, // Recycle bin support + }, +}); ``` -// Definitions: dist/ui/app.zod.d.ts ### B. Configuring the App & Navigation ```typescript diff --git a/content/prompts/plugin/metadata.prompt.md b/content/prompts/plugin/metadata.prompt.md index a6f4793b9..1f447c320 100644 --- a/content/prompts/plugin/metadata.prompt.md +++ b/content/prompts/plugin/metadata.prompt.md @@ -65,35 +65,74 @@ You must strictly adhere to the File Suffix Protocol. Every file type maps to a ## 2. Coding Standards -### **A. No "Magic Strings"** -* **Bad:** `type: 'text'` -* **Good:** Use strict literal types defined by the schema. If you are unsure, ask to check `@objectstack/spec` definitions. - -### **B. Constant Exports** -All metadata files must `export default` a strictly typed constant. +### **A. Strict TypeScript Validation** +Always use `ObjectSchema.create()` and `Field.*` helpers for strict type checking and runtime validation. ```typescript -// ✅ CORRECT -import type { ObjectSchema } from '@objectstack/spec/data'; +// ✅ CORRECT - Strict TypeScript validation with ObjectSchema.create() +import { ObjectSchema, Field } from '@objectstack/spec/data'; -const Issue: ObjectSchema = { +export const Issue = ObjectSchema.create({ name: 'issue', - // ... -}; -export default Issue; + label: 'Issue', + icon: 'alert-circle', + fields: { + title: Field.text({ + label: 'Title', + required: true, + maxLength: 255, + }), + description: Field.textarea({ + label: 'Description', + }), + status: Field.select({ + label: 'Status', + options: [ + { label: 'Open', value: 'open', default: true }, + { label: 'In Progress', value: 'in_progress' }, + { label: 'Closed', value: 'closed' }, + ], + }), + priority: Field.rating(5, { + label: 'Priority', + }), + assignee: Field.lookup('user', { + label: 'Assigned To', + }), + }, + enable: { + trackHistory: true, + apiEnabled: true, + }, +}); ``` ```typescript -// ❌ WRONG +// ❌ WRONG - No type checking export default { - name: 'issue' -} // Type is 'any', no validation! + name: 'issue', + fields: { + title: { type: 'text' } // No validation! + } +}; ``` -### **C. Naming Conventions** +```typescript +// ⚠️ DEPRECATED - Old pattern (type annotation only) +import type { ServiceObject } from '@objectstack/spec/data'; + +const Issue: ServiceObject = { + name: 'issue', + // No runtime validation, only compile-time checking +}; +export default Issue; +``` + +### **B. Naming Conventions** * **Filenames:** `snake_case` + `suffix.ts`. (e.g., `project_task.object.ts`) * **Metadata Keys:** `camelCase`. (e.g., `trackHistory`, `apiEnabled`) * **Machine Names:** `snake_case`. (e.g., `name: 'project_task'`) +* **Constant Names:** `PascalCase`. (e.g., `export const TodoTask = ObjectSchema.create({...})`) ## 3. Workflow Priorities @@ -103,7 +142,121 @@ export default { ## 4. Protocol Reference Snippets -### **A. View Definition (`*.view.ts`)** +### **A. Object Definition (`*.object.ts`)** + +**Best Practice:** Use `ObjectSchema.create()` with `Field.*` helpers for strict type checking and runtime validation. + +```typescript +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +export const Account = ObjectSchema.create({ + name: 'account', + label: 'Account', + pluralLabel: 'Accounts', + icon: 'building', + description: 'Companies and organizations doing business with us', + titleFormat: '{account_number} - {name}', + compactLayout: ['account_number', 'name', 'type', 'owner'], + + fields: { + // Field names must be snake_case (e.g., account_number, annual_revenue) + // AutoNumber field - Unique account identifier + account_number: Field.autonumber({ + label: 'Account Number', + format: 'ACC-{0000}', + }), + + // Text fields with validation + name: Field.text({ + label: 'Account Name', + required: true, + searchable: true, + maxLength: 255, + }), + + // Select field with options + type: Field.select({ + label: 'Account Type', + options: [ + { label: 'Prospect', value: 'prospect', color: '#FFA500', default: true }, + { label: 'Customer', value: 'customer', color: '#00AA00' }, + { label: 'Partner', value: 'partner', color: '#0000FF' }, + ] + }), + + // Number and currency fields + annual_revenue: Field.currency({ + label: 'Annual Revenue', + scale: 2, + min: 0, + }), + + number_of_employees: Field.number({ + label: 'Employees', + min: 0, + }), + + // Contact fields + phone: Field.text({ + label: 'Phone', + format: 'phone', + }), + + website: Field.url({ + label: 'Website', + }), + + // Relationship fields (Lookup) + owner: Field.lookup('user', { + label: 'Account Owner', + required: true, + }), + + parent_account: Field.lookup('account', { + label: 'Parent Account', + description: 'Parent company in hierarchy', + }), + + // Rich text + description: Field.markdown({ + label: 'Description', + }), + + // Boolean + is_active: Field.boolean({ + label: 'Active', + defaultValue: true, + }), + + // Date + last_activity_date: Field.date({ + label: 'Last Activity Date', + readonly: true, + }), + }, + + // Database indexes for performance + indexes: [ + { fields: ['name'], unique: false }, + { fields: ['owner'], unique: false }, + { fields: ['type', 'is_active'], unique: false }, + ], + + // Enable advanced features + enable: { + trackHistory: true, // Track field changes + searchable: true, // Include in global search + apiEnabled: true, // Expose via REST/GraphQL + files: true, // Allow file attachments + feeds: true, // Enable activity feed + activities: true, // Enable tasks and events + trash: true, // Recycle bin support + mru: true, // Track Most Recently Used + }, +}); +``` + +### **B. View Definition (`*.view.ts`)** **Key Pattern:** Decouple Interaction (Navigation/Actions) from Data Config. @@ -140,7 +293,7 @@ export const MyListView: View = { export default MyListView; ``` -### **B. Action Definition (`*.action.ts`)** +### **C. Action Definition (`*.action.ts`)** ```typescript import { Action } from '@objectstack/spec/ui'; diff --git a/content/prompts/zod-compliance.prompt.md b/content/prompts/zod-compliance.prompt.md index d315b996e..86e868631 100644 --- a/content/prompts/zod-compliance.prompt.md +++ b/content/prompts/zod-compliance.prompt.md @@ -52,27 +52,68 @@ When the user asks for a configuration, simulate this internal validation proces **Internal Zod Check:** * `ObjectSchema`: keys `name`, `fields` are required. `name` must be snake_case regex. * `FieldSchema`: - * `status`: type `select`, requires `options` array. + * `status`: type `select`, requires `options` array with value/label pairs. * `budget`: type `currency`, `scale` defaults to 2. **Generated Code:** ```typescript -import { ObjectSchema } from '@objectstack/spec/data'; +import { ObjectSchema, Field } from '@objectstack/spec/data'; -export const ProjectObject: ObjectSchema = { - name: 'project', // ✅ Validates regex /^[a-z_]+$/ +export const Project = ObjectSchema.create({ + name: 'project', // ✅ Validates regex /^[a-z_][a-z0-9_]*$/ (snake_case with numbers allowed after first char) label: 'Project', + icon: 'folder', + + fields: { + title: Field.text({ + label: 'Project Title', + required: true, + maxLength: 255, + }), + + status: Field.select({ + label: 'Status', + options: [ // ✅ Required for select fields + { label: 'New', value: 'new', default: true }, + { label: 'Active', value: 'active' }, + { label: 'Done', value: 'done' }, + ], + }), + + budget: Field.currency({ + label: 'Total Budget', + scale: 2, // ✅ Valid prop for currency + min: 0, + }), + + owner: Field.lookup('user', { + label: 'Project Owner', + required: true, + }), + }, + + enable: { + trackHistory: true, + apiEnabled: true, + files: true, + }, +}); +``` + +**❌ DEPRECATED Pattern (Type Annotation Only):** +```typescript +// This pattern is deprecated - use ObjectSchema.create() instead +import type { ServiceObject } from '@objectstack/spec/data'; + +export const ProjectObject: ServiceObject = { + name: 'project', fields: { status: { - type: 'select', // ✅ Validates Enum - options: ['New', 'Active', 'Done'], // ✅ Required for type='select' - label: 'Status' + type: 'select', + options: ['New', 'Active', 'Done'], // Wrong: should use {label, value} format }, budget: { type: 'currency', - scale: 2, // ✅ Valid prop for currency - precision: 18, - label: 'Total Budget' } } }; diff --git a/examples/app-react-crud/README.md b/examples/app-react-crud/README.md index 938915ec9..1f7fa2dac 100644 --- a/examples/app-react-crud/README.md +++ b/examples/app-react-crud/README.md @@ -36,19 +36,35 @@ Create an `objectstack.config.ts` to define your data models and application str ```typescript // objectstack.config.ts import { defineStack } from '@objectstack/spec'; +import { ObjectSchema, Field } from '@objectstack/spec/data'; -export const TaskObject = { +export const Task = ObjectSchema.create({ name: 'task', label: 'Task', + icon: 'check-square', + fields: { - subject: { type: 'text', required: true }, - priority: { type: 'number', defaultValue: 1 }, - isCompleted: { type: 'boolean', defaultValue: false } - } -}; + subject: Field.text({ + label: 'Subject', + required: true, + }), + + priority: Field.number({ + label: 'Priority', + defaultValue: 1, + min: 1, + max: 5, + }), + + is_completed: Field.boolean({ + label: 'Completed', + defaultValue: false, + }), + }, +}); export default defineStack({ - objects: [TaskObject] + objects: [Task] }); ``` diff --git a/packages/spec/README.md b/packages/spec/README.md index 084f9734b..e591d3b1f 100644 --- a/packages/spec/README.md +++ b/packages/spec/README.md @@ -12,9 +12,49 @@ The **Source of Truth** for the ObjectStack Protocol. Contains strictly typed Zo ## Usage +**Recommended: Use `ObjectSchema.create()` with `Field.*` helpers for strict TypeScript validation:** + +```typescript +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +// Create a validated object definition with type checking +export const Task = ObjectSchema.create({ + name: 'task', + label: 'Task', + icon: 'check-square', + + fields: { + title: Field.text({ + label: 'Title', + required: true, + maxLength: 200, + }), + + status: Field.select({ + label: 'Status', + options: [ + { label: 'To Do', value: 'todo', default: true }, + { label: 'In Progress', value: 'in_progress' }, + { label: 'Done', value: 'done' }, + ], + }), + }, + + enable: { + trackHistory: true, + apiEnabled: true, + }, +}); +``` + +**Alternative: Runtime validation of existing objects:** + ```typescript -import { ObjectSchema, ViewSchema } from '@objectstack/spec'; +import { ObjectSchema } from '@objectstack/spec/data'; // Validate a JSON object against the schema const result = ObjectSchema.parse(myObjectDefinition); +if (result.success) { + console.log('Valid object:', result.data); +} ```