@object-ui/core 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,280 @@
1
+ /**
2
+ * @object-ui/core - Schema Validation
3
+ *
4
+ * Runtime validation utilities for Object UI schemas.
5
+ * These utilities help ensure schemas are valid before rendering.
6
+ *
7
+ * @module validation
8
+ * @packageDocumentation
9
+ */
10
+ /**
11
+ * Validation rules for base schema
12
+ */
13
+ const BASE_SCHEMA_RULES = {
14
+ type: {
15
+ required: true,
16
+ validate: (value) => typeof value === 'string' && value.length > 0,
17
+ message: 'type must be a non-empty string'
18
+ },
19
+ id: {
20
+ required: false,
21
+ validate: (value) => typeof value === 'string',
22
+ message: 'id must be a string'
23
+ },
24
+ className: {
25
+ required: false,
26
+ validate: (value) => typeof value === 'string',
27
+ message: 'className must be a string'
28
+ },
29
+ visible: {
30
+ required: false,
31
+ validate: (value) => typeof value === 'boolean',
32
+ message: 'visible must be a boolean'
33
+ },
34
+ disabled: {
35
+ required: false,
36
+ validate: (value) => typeof value === 'boolean',
37
+ message: 'disabled must be a boolean'
38
+ }
39
+ };
40
+ /**
41
+ * Validate a schema against base rules
42
+ */
43
+ function validateBaseSchema(schema, path = 'schema') {
44
+ const errors = [];
45
+ if (!schema || typeof schema !== 'object') {
46
+ errors.push({
47
+ path,
48
+ message: 'Schema must be an object',
49
+ type: 'error',
50
+ code: 'INVALID_SCHEMA'
51
+ });
52
+ return errors;
53
+ }
54
+ // Validate required and optional properties
55
+ Object.entries(BASE_SCHEMA_RULES).forEach(([key, rule]) => {
56
+ const value = schema[key];
57
+ if (rule.required && value === undefined) {
58
+ errors.push({
59
+ path: `${path}.${key}`,
60
+ message: `${key} is required`,
61
+ type: 'error',
62
+ code: 'MISSING_REQUIRED'
63
+ });
64
+ }
65
+ if (value !== undefined && !rule.validate(value)) {
66
+ errors.push({
67
+ path: `${path}.${key}`,
68
+ message: rule.message,
69
+ type: 'error',
70
+ code: 'INVALID_TYPE'
71
+ });
72
+ }
73
+ });
74
+ return errors;
75
+ }
76
+ /**
77
+ * Validate CRUD schema specific properties
78
+ */
79
+ function validateCRUDSchema(schema, path = 'schema') {
80
+ const errors = [];
81
+ if (schema.type === 'crud') {
82
+ // Check required properties for CRUD
83
+ if (!schema.columns || !Array.isArray(schema.columns)) {
84
+ errors.push({
85
+ path: `${path}.columns`,
86
+ message: 'CRUD schema requires columns array',
87
+ type: 'error',
88
+ code: 'MISSING_COLUMNS'
89
+ });
90
+ }
91
+ if (!schema.api && !schema.dataSource) {
92
+ errors.push({
93
+ path: `${path}.api`,
94
+ message: 'CRUD schema requires api or dataSource',
95
+ type: 'warning',
96
+ code: 'MISSING_DATA_SOURCE'
97
+ });
98
+ }
99
+ // Validate columns
100
+ if (schema.columns && Array.isArray(schema.columns)) {
101
+ schema.columns.forEach((column, index) => {
102
+ if (!column.name) {
103
+ errors.push({
104
+ path: `${path}.columns[${index}]`,
105
+ message: 'Column requires name property',
106
+ type: 'error',
107
+ code: 'MISSING_COLUMN_NAME'
108
+ });
109
+ }
110
+ });
111
+ }
112
+ // Validate fields if present
113
+ if (schema.fields && Array.isArray(schema.fields)) {
114
+ schema.fields.forEach((field, index) => {
115
+ if (!field.name) {
116
+ errors.push({
117
+ path: `${path}.fields[${index}]`,
118
+ message: 'Field requires name property',
119
+ type: 'error',
120
+ code: 'MISSING_FIELD_NAME'
121
+ });
122
+ }
123
+ });
124
+ }
125
+ }
126
+ return errors;
127
+ }
128
+ /**
129
+ * Validate form schema specific properties
130
+ */
131
+ function validateFormSchema(schema, path = 'schema') {
132
+ const errors = [];
133
+ if (schema.type === 'form') {
134
+ if (schema.fields && Array.isArray(schema.fields)) {
135
+ schema.fields.forEach((field, index) => {
136
+ if (!field.name) {
137
+ errors.push({
138
+ path: `${path}.fields[${index}]`,
139
+ message: 'Form field requires name property',
140
+ type: 'error',
141
+ code: 'MISSING_FIELD_NAME'
142
+ });
143
+ }
144
+ // Check for duplicate field names
145
+ const duplicates = schema.fields.filter((f) => f.name === field.name);
146
+ if (duplicates.length > 1) {
147
+ errors.push({
148
+ path: `${path}.fields[${index}]`,
149
+ message: `Duplicate field name: ${field.name}`,
150
+ type: 'warning',
151
+ code: 'DUPLICATE_FIELD_NAME'
152
+ });
153
+ }
154
+ });
155
+ }
156
+ }
157
+ return errors;
158
+ }
159
+ /**
160
+ * Validate child schemas recursively
161
+ */
162
+ function validateChildren(schema, path = 'schema') {
163
+ const errors = [];
164
+ const children = schema.children || schema.body;
165
+ if (children) {
166
+ if (Array.isArray(children)) {
167
+ children.forEach((child, index) => {
168
+ if (typeof child === 'object' && child !== null) {
169
+ const childResult = validateSchema(child, `${path}.children[${index}]`);
170
+ errors.push(...childResult.errors, ...childResult.warnings);
171
+ }
172
+ });
173
+ }
174
+ else if (typeof children === 'object' && children !== null) {
175
+ const childResult = validateSchema(children, `${path}.children`);
176
+ errors.push(...childResult.errors, ...childResult.warnings);
177
+ }
178
+ }
179
+ return errors;
180
+ }
181
+ /**
182
+ * Validate a complete schema
183
+ *
184
+ * @param schema - The schema to validate
185
+ * @param options - Validation options
186
+ * @returns Validation result with errors and warnings
187
+ *
188
+ * @example
189
+ * ```typescript
190
+ * const result = validateSchema({
191
+ * type: 'form',
192
+ * fields: [
193
+ * { name: 'email', type: 'input' }
194
+ * ]
195
+ * });
196
+ *
197
+ * if (!result.valid) {
198
+ * console.error('Validation errors:', result.errors);
199
+ * }
200
+ * ```
201
+ */
202
+ export function validateSchema(schema, path = 'schema') {
203
+ const allErrors = [];
204
+ // Validate base schema
205
+ allErrors.push(...validateBaseSchema(schema, path));
206
+ // Validate type-specific schemas
207
+ allErrors.push(...validateCRUDSchema(schema, path));
208
+ allErrors.push(...validateFormSchema(schema, path));
209
+ // Validate children recursively
210
+ allErrors.push(...validateChildren(schema, path));
211
+ const errors = allErrors.filter(e => e.type === 'error');
212
+ const warnings = allErrors.filter(e => e.type === 'warning');
213
+ return {
214
+ valid: errors.length === 0,
215
+ errors,
216
+ warnings
217
+ };
218
+ }
219
+ /**
220
+ * Assert that a schema is valid, throwing an error if not
221
+ *
222
+ * @param schema - The schema to validate
223
+ * @throws Error if schema is invalid
224
+ *
225
+ * @example
226
+ * ```typescript
227
+ * try {
228
+ * assertValidSchema(schema);
229
+ * // Schema is valid, continue rendering
230
+ * } catch (error) {
231
+ * console.error('Invalid schema:', error.message);
232
+ * }
233
+ * ```
234
+ */
235
+ export function assertValidSchema(schema) {
236
+ const result = validateSchema(schema);
237
+ if (!result.valid) {
238
+ const errorMessages = result.errors.map(e => `${e.path}: ${e.message}`).join('\n');
239
+ throw new Error(`Schema validation failed:\n${errorMessages}`);
240
+ }
241
+ }
242
+ /**
243
+ * Check if a value is a valid schema
244
+ *
245
+ * @param value - The value to check
246
+ * @returns True if the value is a valid schema
247
+ *
248
+ * @example
249
+ * ```typescript
250
+ * if (isValidSchema(data)) {
251
+ * renderSchema(data);
252
+ * }
253
+ * ```
254
+ */
255
+ export function isValidSchema(value) {
256
+ const result = validateSchema(value);
257
+ return result.valid;
258
+ }
259
+ /**
260
+ * Get a human-readable error summary
261
+ *
262
+ * @param result - The validation result
263
+ * @returns Formatted error summary
264
+ */
265
+ export function formatValidationErrors(result) {
266
+ const parts = [];
267
+ if (result.errors.length > 0) {
268
+ parts.push('Errors:');
269
+ result.errors.forEach(error => {
270
+ parts.push(` - ${error.path}: ${error.message}`);
271
+ });
272
+ }
273
+ if (result.warnings.length > 0) {
274
+ parts.push('Warnings:');
275
+ result.warnings.forEach(warning => {
276
+ parts.push(` - ${warning.path}: ${warning.message}`);
277
+ });
278
+ }
279
+ return parts.join('\n');
280
+ }
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@object-ui/core",
3
+ "version": "0.3.0",
4
+ "license": "MIT",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "dependencies": {
8
+ "lodash": "^4.17.21",
9
+ "zod": "^3.22.4",
10
+ "@object-ui/types": "0.3.0"
11
+ },
12
+ "devDependencies": {
13
+ "typescript": "^5.9.3",
14
+ "vitest": "^4.0.17"
15
+ },
16
+ "scripts": {
17
+ "build": "tsc",
18
+ "test": "vitest run",
19
+ "type-check": "tsc --noEmit",
20
+ "lint": "eslint ."
21
+ }
22
+ }
@@ -0,0 +1,287 @@
1
+ /**
2
+ * @object-ui/core - Schema Builder
3
+ *
4
+ * Fluent API for building schemas programmatically.
5
+ * Provides type-safe builder functions for common schema patterns.
6
+ *
7
+ * @module builder
8
+ * @packageDocumentation
9
+ */
10
+ import type { BaseSchema, FormSchema, FormField, CRUDSchema, TableColumn, ActionSchema, ButtonSchema, InputSchema, CardSchema, GridSchema, FlexSchema } from '@object-ui/types';
11
+ /**
12
+ * Base builder class
13
+ */
14
+ declare class SchemaBuilder<T extends BaseSchema> {
15
+ protected schema: any;
16
+ constructor(type: string);
17
+ /**
18
+ * Set the ID
19
+ */
20
+ id(id: string): this;
21
+ /**
22
+ * Set the className
23
+ */
24
+ className(className: string): this;
25
+ /**
26
+ * Set visibility
27
+ */
28
+ visible(visible: boolean): this;
29
+ /**
30
+ * Set conditional visibility
31
+ */
32
+ visibleOn(expression: string): this;
33
+ /**
34
+ * Set disabled state
35
+ */
36
+ disabled(disabled: boolean): this;
37
+ /**
38
+ * Set test ID
39
+ */
40
+ testId(testId: string): this;
41
+ /**
42
+ * Build the final schema
43
+ */
44
+ build(): T;
45
+ }
46
+ /**
47
+ * Form builder
48
+ */
49
+ export declare class FormBuilder extends SchemaBuilder<FormSchema> {
50
+ constructor();
51
+ /**
52
+ * Add a field to the form
53
+ */
54
+ field(field: FormField): this;
55
+ /**
56
+ * Add multiple fields
57
+ */
58
+ fields(fields: FormField[]): this;
59
+ /**
60
+ * Set default values
61
+ */
62
+ defaultValues(values: Record<string, any>): this;
63
+ /**
64
+ * Set submit label
65
+ */
66
+ submitLabel(label: string): this;
67
+ /**
68
+ * Set form layout
69
+ */
70
+ layout(layout: 'vertical' | 'horizontal'): this;
71
+ /**
72
+ * Set number of columns
73
+ */
74
+ columns(columns: number): this;
75
+ /**
76
+ * Set submit handler
77
+ */
78
+ onSubmit(handler: (data: Record<string, any>) => void | Promise<void>): this;
79
+ }
80
+ /**
81
+ * CRUD builder
82
+ */
83
+ export declare class CRUDBuilder extends SchemaBuilder<CRUDSchema> {
84
+ constructor();
85
+ /**
86
+ * Set resource name
87
+ */
88
+ resource(resource: string): this;
89
+ /**
90
+ * Set API endpoint
91
+ */
92
+ api(api: string): this;
93
+ /**
94
+ * Set title
95
+ */
96
+ title(title: string): this;
97
+ /**
98
+ * Set description
99
+ */
100
+ description(description: string): this;
101
+ /**
102
+ * Add a column
103
+ */
104
+ column(column: TableColumn): this;
105
+ /**
106
+ * Set all columns
107
+ */
108
+ columns(columns: TableColumn[]): this;
109
+ /**
110
+ * Set form fields
111
+ */
112
+ fields(fields: FormField[]): this;
113
+ /**
114
+ * Enable create operation
115
+ */
116
+ enableCreate(label?: string): this;
117
+ /**
118
+ * Enable update operation
119
+ */
120
+ enableUpdate(label?: string): this;
121
+ /**
122
+ * Enable delete operation
123
+ */
124
+ enableDelete(label?: string, confirmText?: string): this;
125
+ /**
126
+ * Set pagination
127
+ */
128
+ pagination(pageSize?: number): this;
129
+ /**
130
+ * Enable row selection
131
+ */
132
+ selectable(mode?: 'single' | 'multiple'): this;
133
+ /**
134
+ * Add a batch action
135
+ */
136
+ batchAction(action: ActionSchema): this;
137
+ /**
138
+ * Add a row action
139
+ */
140
+ rowAction(action: ActionSchema): this;
141
+ }
142
+ /**
143
+ * Button builder
144
+ */
145
+ export declare class ButtonBuilder extends SchemaBuilder<ButtonSchema> {
146
+ constructor();
147
+ /**
148
+ * Set button label
149
+ */
150
+ label(label: string): this;
151
+ /**
152
+ * Set button variant
153
+ */
154
+ variant(variant: 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link'): this;
155
+ /**
156
+ * Set button size
157
+ */
158
+ size(size: 'default' | 'sm' | 'lg' | 'icon'): this;
159
+ /**
160
+ * Set button icon
161
+ */
162
+ icon(icon: string): this;
163
+ /**
164
+ * Set click handler
165
+ */
166
+ onClick(handler: () => void | Promise<void>): this;
167
+ /**
168
+ * Set loading state
169
+ */
170
+ loading(loading: boolean): this;
171
+ }
172
+ /**
173
+ * Input builder
174
+ */
175
+ export declare class InputBuilder extends SchemaBuilder<InputSchema> {
176
+ constructor();
177
+ /**
178
+ * Set field name
179
+ */
180
+ name(name: string): this;
181
+ /**
182
+ * Set label
183
+ */
184
+ label(label: string): this;
185
+ /**
186
+ * Set placeholder
187
+ */
188
+ placeholder(placeholder: string): this;
189
+ /**
190
+ * Set input type
191
+ */
192
+ inputType(type: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url'): this;
193
+ /**
194
+ * Mark as required
195
+ */
196
+ required(required?: boolean): this;
197
+ /**
198
+ * Set default value
199
+ */
200
+ defaultValue(value: string | number): this;
201
+ }
202
+ /**
203
+ * Card builder
204
+ */
205
+ export declare class CardBuilder extends SchemaBuilder<CardSchema> {
206
+ constructor();
207
+ /**
208
+ * Set card title
209
+ */
210
+ title(title: string): this;
211
+ /**
212
+ * Set card description
213
+ */
214
+ description(description: string): this;
215
+ /**
216
+ * Set card content
217
+ */
218
+ content(content: BaseSchema | BaseSchema[]): this;
219
+ /**
220
+ * Set card variant
221
+ */
222
+ variant(variant: 'default' | 'outline' | 'ghost'): this;
223
+ /**
224
+ * Make card hoverable
225
+ */
226
+ hoverable(hoverable?: boolean): this;
227
+ }
228
+ /**
229
+ * Grid builder
230
+ */
231
+ export declare class GridBuilder extends SchemaBuilder<GridSchema> {
232
+ constructor();
233
+ /**
234
+ * Set number of columns
235
+ */
236
+ columns(columns: number): this;
237
+ /**
238
+ * Set gap
239
+ */
240
+ gap(gap: number): this;
241
+ /**
242
+ * Add a child
243
+ */
244
+ child(child: BaseSchema): this;
245
+ /**
246
+ * Set all children
247
+ */
248
+ children(children: BaseSchema[]): this;
249
+ }
250
+ /**
251
+ * Flex builder
252
+ */
253
+ export declare class FlexBuilder extends SchemaBuilder<FlexSchema> {
254
+ constructor();
255
+ /**
256
+ * Set flex direction
257
+ */
258
+ direction(direction: 'row' | 'col' | 'row-reverse' | 'col-reverse'): this;
259
+ /**
260
+ * Set justify content
261
+ */
262
+ justify(justify: 'start' | 'end' | 'center' | 'between' | 'around' | 'evenly'): this;
263
+ /**
264
+ * Set align items
265
+ */
266
+ align(align: 'start' | 'end' | 'center' | 'baseline' | 'stretch'): this;
267
+ /**
268
+ * Set gap
269
+ */
270
+ gap(gap: number): this;
271
+ /**
272
+ * Add a child
273
+ */
274
+ child(child: BaseSchema): this;
275
+ /**
276
+ * Set all children
277
+ */
278
+ children(children: BaseSchema[]): this;
279
+ }
280
+ export declare const form: () => FormBuilder;
281
+ export declare const crud: () => CRUDBuilder;
282
+ export declare const button: () => ButtonBuilder;
283
+ export declare const input: () => InputBuilder;
284
+ export declare const card: () => CardBuilder;
285
+ export declare const grid: () => GridBuilder;
286
+ export declare const flex: () => FlexBuilder;
287
+ export {};