Skip to content
Draft
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
4 changes: 3 additions & 1 deletion packages/foundation/core/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export function convertIntrospectedSchemaToObjects(
if (foreignKey) {
// This is a lookup field
fieldConfig = {
name: column.name,
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name property is now explicitly set in both lookup and regular field configurations. This is redundant since the comment on line 114 in the original code states 'If defined within an object map, this is often automatically populated from the key.' Consider documenting why explicit name assignment is now required, or if this is needed for protocol compliance.

Copilot uses AI. Check for mistakes.
type: 'lookup',
reference_to: foreignKey.referencedTable,
label: toTitleCase(column.name),
Expand All @@ -121,6 +122,7 @@ export function convertIntrospectedSchemaToObjects(
const fieldType = mapDatabaseTypeToFieldType(column.type);

fieldConfig = {
name: column.name,
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name property is now explicitly set in both lookup and regular field configurations. This is redundant since the comment on line 114 in the original code states 'If defined within an object map, this is often automatically populated from the key.' Consider documenting why explicit name assignment is now required, or if this is needed for protocol compliance.

Copilot uses AI. Check for mistakes.
type: fieldType,
label: toTitleCase(column.name),
required: !column.nullable
Expand All @@ -133,7 +135,7 @@ export function convertIntrospectedSchemaToObjects(

// Add max length for text fields
if (column.maxLength && (fieldType === 'text' || fieldType === 'textarea')) {
fieldConfig.max_length = column.maxLength;
fieldConfig.maxLength = column.maxLength;
}

// Add default value
Expand Down
2 changes: 1 addition & 1 deletion packages/foundation/core/test/util.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ describe('Utility Functions', () => {
const objects = convertIntrospectedSchemaToObjects(schema);
const fields = objects[0].fields!;

expect(fields.short_text.max_length).toBe(100);
expect(fields.short_text.maxLength).toBe(100);
});

it('should add default value when present', () => {
Expand Down
5 changes: 4 additions & 1 deletion packages/foundation/types/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,13 @@
"schemas"
],
"scripts": {
"build": "tsc && npm run generate:schemas",
"build": "tsc",
"generate:schemas": "node scripts/generate-schemas.js",
"test": "jest --passWithNoTests"
},
"dependencies": {
"@objectstack/spec": "^0.1.1"
},
"devDependencies": {
"ts-json-schema-generator": "^2.4.0"
}
Expand Down
24 changes: 24 additions & 0 deletions packages/foundation/types/src/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,21 @@
* LICENSE file in the root directory of this source tree.
*/

// Import and re-export types from the Protocol Constitution (@objectstack/spec)
import type { Action } from '@objectstack/spec';
import { FieldConfig } from "./field";
import { HookAPI } from "./hook"; // Reuse the restricted API interface

/**
* Re-export Protocol Types from the Constitution
*/
export type { Action as SpecAction };

/**
* RUNTIME-SPECIFIC TYPES
* The following types extend the Protocol Action definition with runtime execution capabilities
*/

/**
* Defines the scope of the action.
* - `record`: Acts on a specific record instance (e.g. "Approve Order").
Expand All @@ -24,6 +36,8 @@ export type ActionInputDefinition = Record<string, FieldConfig>;

/**
* Context passed to the action handler execution.
*
* RUNTIME TYPE: Used during action execution.
*/
export interface ActionContext<BaseT = any, InputT = any> {
/** The object this action belongs to. */
Expand Down Expand Up @@ -58,11 +72,19 @@ export interface ActionContext<BaseT = any, InputT = any> {
}

/**
* Runtime Action Configuration
*
* The configuration of an Action visible to the Metadata engine (YAML/JSON side).
* Compatible with Protocol Action but adds runtime-specific options.
*/
export interface ActionConfig {
/** Display label */
label?: string;

/** Description */
description?: string;

/** Icon name */
icon?: string;

/**
Expand Down Expand Up @@ -93,6 +115,8 @@ export interface ActionConfig {

/**
* The full implementation definition (Code side).
*
* RUNTIME TYPE: Includes the handler function for execution.
*/
export interface ActionDefinition<BaseT = any, InputT = any, ReturnT = any> extends ActionConfig {
/**
Expand Down
197 changes: 104 additions & 93 deletions packages/foundation/types/src/field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,26 @@
* LICENSE file in the root directory of this source tree.
*/

import { FieldValidation, ValidationAiContext } from './validation';
// Import types from the Protocol Constitution (@objectstack/spec)
import type { FieldType as ProtocolFieldType, Field, SelectOption as SpecSelectOption } from '@objectstack/spec';

/**
* Re-export Protocol Types from the Constitution
* These are the wire-protocol standard types defined in @objectstack/spec
*/
export type { Field as SpecField, SpecSelectOption, ProtocolFieldType };

/**
* RUNTIME-SPECIFIC TYPES
* The following types extend or complement the Protocol Constitution
* with runtime-specific properties that don't belong in the wire protocol.
*/

/**
* Attachment field data structure for file and image types.
* Stores metadata about uploaded files, with actual file content stored separately.
*
* This is a RUNTIME type - not part of the wire protocol.
*/
export interface AttachmentData {
/** Unique identifier for this file */
Expand Down Expand Up @@ -41,6 +56,8 @@ export interface AttachmentData {
/**
* Image-specific attachment data with additional metadata.
* Extends AttachmentData with image-specific properties.
*
* This is a RUNTIME type - not part of the wire protocol.
*/
export interface ImageAttachmentData extends AttachmentData {
/** Image width in pixels */
Expand All @@ -61,111 +78,89 @@ export interface ImageAttachmentData extends AttachmentData {
}

/**
* Represents the supported field data types in the ObjectQL schema.
* These types determine how data is stored, validated, and rendered.
* Runtime Field Type
*
* - `text`: Simple string.
* - `textarea`: Long string.
* - `select`: Choice from a list.
* - `lookup`: Relationship to another object.
* - `file`: File attachment. Value stored as AttachmentData (single) or AttachmentData[] (multiple).
* - `image`: Image attachment. Value stored as ImageAttachmentData (single) or ImageAttachmentData[] (multiple).
* Extends the Protocol FieldType with runtime-specific types.
* The Protocol Constitution defines the core field types.
* We add runtime-specific types like 'vector', 'grid', 'location', 'object' here.
*/
export type FieldType =
| 'text'
| 'textarea'
| 'markdown'
| 'html'
| 'select'
| 'date'
| 'datetime'
| 'time'
| 'number'
| 'currency'
| 'percent'
| 'boolean'
| 'email'
| 'phone'
| 'url'
| 'image'
| 'file'
| 'location'
| 'lookup'
| 'master_detail'
| 'password'
| 'formula'
| 'summary'
| 'auto_number'
| 'object'
| 'vector'
| 'grid';
| ProtocolFieldType
| 'location' // Runtime: Geographic location
| 'object' // Runtime: Nested object/JSON
| 'vector' // Runtime: Vector embeddings for AI
| 'grid'; // Runtime: Inline grid/table

/**
* Defines a single option for select/multiselect fields.
* Runtime Field Option
*
* Extends the Protocol SelectOption to allow number values (for backwards compatibility).
*/
export interface FieldOption {
/** The display label for the option. */
label: string;
/** The actual value stored in the database. */
value: string | number;
/** Optional color for visual representation */
color?: string;
/** Whether this is the default option */
default?: boolean;
}

/**
* Configuration for a single field on an object.
* This defines the schema, validation rules, and UI hints for the attribute.
* Runtime Field Configuration
*
* Extends the Protocol Field definition with runtime-specific properties.
* The Protocol Constitution (SpecField) defines the core field schema.
* This adds runtime conveniences and extensions.
*
* We make certain spec fields optional since Zod applies defaults at parse time.
*/
export interface FieldConfig {
/**
* The unique API name of the field.
* If defined within an object map, this is often automatically populated from the key.
*/
name?: string;

/** The human-readable label used in UIs. */
label?: string;

/** Description of the field for documentation or tooltip. */
description?: string;

/** The data type of the field. */
export interface FieldConfig extends Omit<Field, 'type' | 'options' | 'required' | 'multiple' | 'unique' | 'deleteBehavior' | 'hidden' | 'readonly' | 'encryption' | 'index' | 'externalId'> {
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Omit list contains 11 properties being excluded from the base Field type. This creates a tight coupling to the protocol definition and makes the interface fragile - any changes to the protocol's Field interface could silently break this contract. Consider using a Pick approach to explicitly declare which protocol properties are inherited, or document why each property needs to be re-declared.

Copilot uses AI. Check for mistakes.
/** The data type of the field (extended with runtime types) */
type: FieldType;

/** Options for select fields (extended to allow number values) */
options?: FieldOption[];

/** Whether the field is mandatory. Defaults to false. */
required?: boolean;


/** Whether the field allows multiple values. */
multiple?: boolean;

/** Whether the field is unique in the table. */
unique?: boolean;

/** Whether to create a database index for this field. */
index?: boolean;

/** Whether the field is read-only in UI. */
readonly?: boolean;


/** Delete behavior for relationships */
deleteBehavior?: 'set_null' | 'cascade' | 'restrict';

/** Whether the field is hidden from default UI/API response. */
hidden?: boolean;

/** The default value if not provided during creation. */
defaultValue?: any;

/** Tooltip or help text for the user. */
help_text?: string;
/** Whether the field is read-only in UI. */
readonly?: boolean;

/**
* Whether the field allows multiple values.
* Supported by 'select', 'lookup', 'file', 'image'.
*/
multiple?: boolean;
/** Whether the field is encrypted */
encryption?: boolean;

/**
* Options for select fields.
* List of available choices for select/multiselect fields.
/** Whether to create a database index for this field. */
index?: boolean;

/** Whether this is an external ID field */
externalId?: boolean;

/**
* RUNTIME EXTENSIONS BELOW
* These properties are NOT in the wire protocol but are useful for the runtime.
*/
options?: FieldOption[];

/** Tooltip or help text for the user. */
help_text?: string;

/**
* Reference to another object for lookup/master_detail fields.
* Specifies the target object name for relationship fields.
* @deprecated Use 'reference' from SpecField instead
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This property is marked as deprecated in favor of the protocol's reference field, but there's no migration path or guidance provided. Consider adding a comment explaining how to migrate from reference_to to reference, or implement a getter/setter that maps between the two to ease the transition.

Suggested change
* @deprecated Use 'reference' from SpecField instead
*
* @deprecated Legacy alias for the protocol-level {@link SpecField.reference} field.
*
* Migration guidance:
* - New schemas MUST use the `reference` field defined on SpecField (wire protocol) instead of `reference_to`.
* - During migration, engines and tooling SHOULD:
* - Treat `reference_to` as a fallback alias for `reference` (i.e., if `reference` is undefined and `reference_to`
* is defined, behave as though `reference` were set to the same value).
* - Prefer the protocol `reference` value when both are present.
*
* This property is retained only for backward compatibility with older *.object.yml definitions and may be removed
* in a future major version once all callers have migrated to `reference`.

Copilot uses AI. Check for mistakes.
*/
reference_to?: string;

Expand Down Expand Up @@ -206,37 +201,52 @@ export interface FieldConfig {
*/
min_height?: number;

// Validation properties
/** Minimum for number/currency/percent. */
min?: number;
/** Maximum for number/currency/percent. */
max?: number;
/** Minimum length for text based fields. */
min_length?: number;
/** Maximum length for text based fields. */
max_length?: number;
/** Regular expression pattern for validation. */
/**
* Regular expression pattern for validation.
* @deprecated Use validation.pattern instead
*/
regex?: string;
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This property is marked as deprecated in favor of validation.pattern, but it's also included again inside the validation object (line 230 has regex as an alias for pattern). This creates ambiguity about which property should be used. Consider removing the deprecated top-level property entirely or providing a clear migration timeline.

Copilot uses AI. Check for mistakes.

/**
* Field validation configuration.
* Defines validation rules applied at the field level.
*/
validation?: FieldValidation;
validation?: {
/** Format validation (email, url, etc.) */
format?: 'email' | 'url' | 'phone' | 'date' | 'datetime';
/** Allowed protocols for URL validation */
protocols?: string[];
/** Minimum value for numbers */
min?: number;
/** Maximum value for numbers */
max?: number;
/** Minimum length for strings */
min_length?: number;
/** Maximum length for strings */
max_length?: number;
/** Regular expression pattern for validation */
pattern?: string;
/** Regex pattern (alias for pattern) */
regex?: string;
/** Custom validation message */
message?: string;
};

/**
* AI context for the field.
* Provides semantic information for AI tools.
*/
ai_context?: ValidationAiContext;
ai_context?: {
intent?: string;
validation_strategy?: string;
[key: string]: unknown;
};

// Vector properties
// Vector properties (runtime-specific)
/** Dimension of the vector for 'vector' type fields. */
dimension?: number;

// Formula properties
/** Formula expression (for 'formula' type fields). */
formula?: string;
// Formula properties (extended from protocol)
/** Expected return data type for formula fields. */
data_type?: 'number' | 'text' | 'date' | 'datetime' | 'boolean' | 'currency' | 'percent';
/** Display format for formula results (e.g., "0.00", "YYYY-MM-DD"). */
Expand All @@ -248,12 +258,13 @@ export interface FieldConfig {
/** Default value for null/undefined referenced fields in formulas. */
treat_blank_as?: string | number | boolean | Date | null;

// Summary properties
// Summary properties (extended from protocol)
/** Object to summarize. */
summary_object?: string;
/** Field on the summary object. */
summary_field?: string;
/** Type of summary (count, sum, min, max, avg). */
summary_type?: string;
/** Filters for summary */
filters?: unknown[];
}
Loading
Loading