diff --git a/packages/spec/src/api/contract.test.ts b/packages/spec/src/api/contract.test.ts index 29fcb7e8..7f82698c 100644 --- a/packages/spec/src/api/contract.test.ts +++ b/packages/spec/src/api/contract.test.ts @@ -233,12 +233,12 @@ describe('ExportRequestSchema', () => { const request = ExportRequestSchema.parse({ object: 'account', fields: ['name', 'email'], - filters: ['status', '=', 'active'], + where: { status: 'active' }, format: 'xlsx', }); expect(request.format).toBe('xlsx'); - expect(request.filters).toBeDefined(); + expect(request.where).toBeDefined(); }); }); diff --git a/packages/spec/src/api/endpoint.test.ts b/packages/spec/src/api/endpoint.test.ts index 895a1373..23cbcfbe 100644 --- a/packages/spec/src/api/endpoint.test.ts +++ b/packages/spec/src/api/endpoint.test.ts @@ -3,13 +3,13 @@ import { ApiEndpointSchema, RateLimitSchema, ApiMappingSchema, - HttpMethod, ApiEndpoint, -} from '../system/api.zod'; +} from './endpoint.zod'; +import { HttpMethod } from './router.zod'; describe('HttpMethod', () => { it('should accept valid HTTP methods', () => { - const validMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']; + const validMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']; validMethods.forEach(method => { expect(() => HttpMethod.parse(method)).not.toThrow(); @@ -17,8 +17,8 @@ describe('HttpMethod', () => { }); it('should reject invalid HTTP methods', () => { - expect(() => HttpMethod.parse('HEAD')).toThrow(); - expect(() => HttpMethod.parse('OPTIONS')).toThrow(); + expect(() => HttpMethod.parse('TRACE')).toThrow(); + expect(() => HttpMethod.parse('CONNECT')).toThrow(); expect(() => HttpMethod.parse('get')).toThrow(); }); }); diff --git a/packages/spec/src/data/object.test.ts b/packages/spec/src/data/object.test.ts index f200db15..80c31cec 100644 --- a/packages/spec/src/data/object.test.ts +++ b/packages/spec/src/data/object.test.ts @@ -9,8 +9,11 @@ describe('ObjectCapabilities', () => { expect(result.searchable).toBe(true); expect(result.apiEnabled).toBe(true); expect(result.files).toBe(false); - expect(result.feedEnabled).toBe(false); + expect(result.feeds).toBe(false); + expect(result.activities).toBe(false); expect(result.trash).toBe(true); + expect(result.mru).toBe(true); + expect(result.clone).toBe(true); }); it('should accept custom capability values', () => { @@ -19,8 +22,11 @@ describe('ObjectCapabilities', () => { searchable: false, apiEnabled: true, files: true, - feedEnabled: true, + feeds: true, + activities: false, trash: false, + mru: true, + clone: true, }; const result = ObjectCapabilities.parse(capabilities); diff --git a/packages/spec/src/data/query.test.ts b/packages/spec/src/data/query.test.ts index fe38c958..e67cc0bd 100644 --- a/packages/spec/src/data/query.test.ts +++ b/packages/spec/src/data/query.test.ts @@ -1,8 +1,6 @@ import { describe, it, expect } from 'vitest'; import { QuerySchema, - FilterOperator, - LogicOperator, AggregationFunction, JoinType, WindowFunction, @@ -12,35 +10,6 @@ import { type WindowFunctionNode, } from './query.zod'; -describe('FilterOperator', () => { - it('should accept valid filter operators', () => { - const validOperators = [ - '=', '!=', '<>', - '>', '>=', '<', '<=', - 'startswith', 'contains', 'notcontains', - 'between', 'in', 'notin', - 'is_null', 'is_not_null' - ]; - - validOperators.forEach(op => { - expect(() => FilterOperator.parse(op)).not.toThrow(); - }); - }); - - it('should reject invalid operators', () => { - expect(() => FilterOperator.parse('LIKE')).toThrow(); - expect(() => FilterOperator.parse('equals')).toThrow(); - }); -}); - -describe('LogicOperator', () => { - it('should accept valid logic operators', () => { - expect(() => LogicOperator.parse('and')).not.toThrow(); - expect(() => LogicOperator.parse('or')).not.toThrow(); - expect(() => LogicOperator.parse('not')).not.toThrow(); - }); -}); - describe('AggregationFunction', () => { it('should accept valid aggregation functions', () => { const validFunctions = [ @@ -111,7 +80,7 @@ describe('QuerySchema - Basic', () => { const query: QueryAST = { object: 'account', fields: ['name', 'email'], - sort: [ + orderBy: [ { field: 'name', order: 'asc' }, { field: 'created_at', order: 'desc' }, ], @@ -303,7 +272,7 @@ describe('QuerySchema - Aggregations', () => { { function: 'count', alias: 'order_count' }, ], groupBy: ['customer_id'], - having: ['order_count', '>', 5], + having: { order_count: { $gt: 5 } }, }; expect(() => QuerySchema.parse(query)).not.toThrow(); @@ -317,7 +286,7 @@ describe('QuerySchema - Aggregations', () => { { function: 'sum', field: 'amount', alias: 'total_amount' }, ], groupBy: ['customer_id'], - having: ['total_amount', '>', 1000], + having: { total_amount: { $gt: 1000 } }, }; expect(() => QuerySchema.parse(query)).not.toThrow(); @@ -331,7 +300,7 @@ describe('QuerySchema - Aggregations', () => { { function: 'avg', field: 'amount', alias: 'avg_amount' }, ], groupBy: ['customer_id'], - having: ['avg_amount', '>=', 500], + having: { avg_amount: { $gte: 500 } }, }; expect(() => QuerySchema.parse(query)).not.toThrow(); @@ -346,7 +315,7 @@ describe('QuerySchema - Aggregations', () => { { function: 'sum', field: 'amount', alias: 'total_amount' }, ], groupBy: ['customer_id'], - having: [['order_count', '>', 3], 'and', ['total_amount', '>', 1000]], + having: { $and: [{ order_count: { $gt: 3 } }, { total_amount: { $gt: 1000 } }] }, }; expect(() => QuerySchema.parse(query)).not.toThrow(); @@ -361,7 +330,7 @@ describe('QuerySchema - Aggregations', () => { { function: 'sum', field: 'amount', alias: 'total_amount' }, ], groupBy: ['customer_id'], - having: ['total_amount', '>', 5000], + having: { total_amount: { $gt: 5000 } }, }; expect(() => QuerySchema.parse(query)).not.toThrow(); @@ -379,7 +348,7 @@ describe('QuerySchema - Aggregations', () => { { function: 'sum', field: 'amount', alias: 'total_amount' }, ], groupBy: ['customer_id'], - sort: [{ field: 'total_amount', order: 'desc' }], + orderBy: [{ field: 'total_amount', order: 'desc' }], }; expect(() => QuerySchema.parse(query)).not.toThrow(); @@ -393,7 +362,7 @@ describe('QuerySchema - Aggregations', () => { { function: 'count', alias: 'order_count' }, ], groupBy: ['customer_id'], - sort: [{ field: 'order_count', order: 'desc' }], + orderBy: [{ field: 'order_count', order: 'desc' }], top: 10, skip: 0, }; @@ -455,8 +424,8 @@ describe('QuerySchema - Aggregations', () => { { function: 'max', field: 'created_at', alias: 'last_order_date' }, ], groupBy: ['customer_id'], - having: ['num_orders', '>', 1], - sort: [{ field: 'lifetime_value', order: 'desc' }], + having: { num_orders: { $gt: 1 } }, + orderBy: [{ field: 'lifetime_value', order: 'desc' }], }; expect(() => QuerySchema.parse(query)).not.toThrow(); @@ -472,7 +441,7 @@ describe('QuerySchema - Aggregations', () => { { function: 'sum', field: 'line_total', alias: 'total_revenue' }, ], groupBy: ['product_id'], - sort: [{ field: 'total_revenue', order: 'desc' }], + orderBy: [{ field: 'total_revenue', order: 'desc' }], top: 20, }; @@ -494,7 +463,7 @@ describe('QuerySchema - Joins', () => { type: 'inner', object: 'customer', alias: 'c', - on: ['order.customer_id', '=', 'c.id'], + on: { 'order.customer_id': { $eq: { $field: 'c.id' } } }, }, ], }; @@ -510,7 +479,7 @@ describe('QuerySchema - Joins', () => { { type: 'inner', object: 'customer', - on: ['order.customer_id', '=', 'customer.id'], + on: { 'order.customer_id': { $eq: { $field: 'customer.id' } } }, }, ], }; @@ -527,7 +496,12 @@ describe('QuerySchema - Joins', () => { type: 'inner', object: 'customer', alias: 'c', - on: [['order.customer_id', '=', 'c.id'], 'and', ['order.status', '=', 'active']], + on: { + $and: [ + { 'order.customer_id': { $eq: { $field: 'c.id' } } }, + { 'order.status': 'active' }, + ], + }, }, ], }; @@ -547,7 +521,7 @@ describe('QuerySchema - Joins', () => { { type: 'left', object: 'order', - on: ['customer.id', '=', 'order.customer_id'], + on: { 'customer.id': { $eq: { $field: 'order.customer_id' } } }, }, ], }; @@ -564,7 +538,7 @@ describe('QuerySchema - Joins', () => { type: 'left', object: 'order', alias: 'o', - on: ['customer.id', '=', 'o.customer_id'], + on: { 'customer.id': { $eq: { $field: 'o.customer_id' } } }, }, ], }; @@ -581,7 +555,7 @@ describe('QuerySchema - Joins', () => { type: 'left', object: 'order', alias: 'o', - on: ['customer.id', '=', 'o.customer_id'], + on: { 'customer.id': { $eq: { $field: 'o.customer_id' } } }, }, ], filters: ['o.id', 'is_null', null], @@ -603,7 +577,7 @@ describe('QuerySchema - Joins', () => { type: 'right', object: 'customer', alias: 'c', - on: ['order.customer_id', '=', 'c.id'], + on: { 'order.customer_id': { $eq: { $field: 'c.id' } } }, }, ], }; @@ -619,7 +593,7 @@ describe('QuerySchema - Joins', () => { { type: 'right', object: 'customer', - on: ['order.customer_id', '=', 'customer.id'], + on: { 'order.customer_id': { $eq: { $field: 'customer.id' } } }, }, ], }; @@ -640,7 +614,7 @@ describe('QuerySchema - Joins', () => { type: 'full', object: 'order', alias: 'o', - on: ['customer.id', '=', 'o.customer_id'], + on: { 'customer.id': { $eq: { $field: 'o.customer_id' } } }, }, ], }; @@ -657,7 +631,7 @@ describe('QuerySchema - Joins', () => { type: 'full', object: 'order', alias: 'o', - on: ['customer.id', '=', 'o.customer_id'], + on: { 'customer.id': { $eq: { $field: 'o.customer_id' } } }, }, ], filters: [['customer.id', 'is_null', null], 'or', ['o.id', 'is_null', null]], @@ -679,13 +653,13 @@ describe('QuerySchema - Joins', () => { type: 'inner', object: 'customer', alias: 'c', - on: ['order.customer_id', '=', 'c.id'], + on: { 'order.customer_id': { $eq: { $field: 'c.id' } } }, }, { type: 'inner', object: 'product', alias: 'p', - on: ['order.product_id', '=', 'p.id'], + on: { 'order.product_id': { $eq: { $field: 'p.id' } } }, }, ], }; @@ -702,19 +676,19 @@ describe('QuerySchema - Joins', () => { type: 'inner', object: 'customer', alias: 'c', - on: ['order.customer_id', '=', 'c.id'], + on: { 'order.customer_id': { $eq: { $field: 'c.id' } } }, }, { type: 'left', object: 'product', alias: 'p', - on: ['order.product_id', '=', 'p.id'], + on: { 'order.product_id': { $eq: { $field: 'p.id' } } }, }, { type: 'left', object: 'shipment', alias: 's', - on: ['order.id', '=', 's.order_id'], + on: { 'order.id': { $eq: { $field: 's.order_id' } } }, }, ], }; @@ -731,25 +705,25 @@ describe('QuerySchema - Joins', () => { type: 'inner', object: 'customer', alias: 'c', - on: ['order.customer_id', '=', 'c.id'], + on: { 'order.customer_id': { $eq: { $field: 'c.id' } } }, }, { type: 'inner', object: 'order_item', alias: 'oi', - on: ['order.id', '=', 'oi.order_id'], + on: { 'order.id': { $eq: { $field: 'oi.order_id' } } }, }, { type: 'inner', object: 'product', alias: 'p', - on: ['oi.product_id', '=', 'p.id'], + on: { 'oi.product_id': { $eq: { $field: 'p.id' } } }, }, { type: 'left', object: 'category', alias: 'cat', - on: ['p.category_id', '=', 'cat.id'], + on: { 'p.category_id': { $eq: { $field: 'cat.id' } } }, }, ], }; @@ -770,7 +744,7 @@ describe('QuerySchema - Joins', () => { type: 'left', object: 'employee', alias: 'manager', - on: ['employee.manager_id', '=', 'manager.id'], + on: { 'employee.manager_id': { $eq: { $field: 'manager.id' } } }, }, ], }; @@ -787,7 +761,7 @@ describe('QuerySchema - Joins', () => { type: 'left', object: 'category', alias: 'parent', - on: ['category.parent_id', '=', 'parent.id'], + on: { 'category.parent_id': { $eq: { $field: 'parent.id' } } }, }, ], }; @@ -809,7 +783,7 @@ describe('QuerySchema - Joins', () => { type: 'inner', object: 'customer', alias: 'c', - on: ['order.customer_id', '=', 'c.id'], + on: { 'order.customer_id': { $eq: { $field: 'c.id' } } }, }, ], }; @@ -826,11 +800,12 @@ describe('QuerySchema - Joins', () => { type: 'inner', object: 'customer', alias: 'c', - on: [ - ['order.customer_id', '=', 'c.id'], - 'and', - ['c.status', '=', 'active'], - ], + on: { + $and: [ + { 'order.customer_id': { $eq: { $field: 'c.id' } } }, + { 'c.status': 'active' }, + ], + }, }, ], }; @@ -851,7 +826,7 @@ describe('QuerySchema - Joins', () => { type: 'inner', object: 'customer', alias: 'c', - on: ['order.customer_id', '=', 'c.id'], + on: { 'order.customer_id': { $eq: { $field: 'c.id' } } }, }, ], aggregations: [ @@ -877,7 +852,7 @@ describe('QuerySchema - Joins', () => { type: 'inner', object: 'customer', alias: 'high_value_customers', - on: ['order.customer_id', '=', 'high_value_customers.id'], + on: { 'order.customer_id': { $eq: { $field: 'high_value_customers.id' } } }, subquery: { object: 'customer', fields: ['id'], @@ -899,7 +874,7 @@ describe('QuerySchema - Joins', () => { type: 'left', object: 'order', alias: 'order_summary', - on: ['customer.id', '=', 'order_summary.customer_id'], + on: { 'customer.id': { $eq: { $field: 'order_summary.customer_id' } } }, subquery: { object: 'order', fields: ['customer_id'], @@ -928,7 +903,7 @@ describe('QuerySchema - Joins', () => { { type: 'left', object: 'contact', - on: ['account.id', '=', 'contact.account_id'], + on: { 'account.id': { $eq: { $field: 'contact.account_id' } } }, }, ], }; @@ -945,19 +920,19 @@ describe('QuerySchema - Joins', () => { type: 'inner', object: 'customer', alias: 'c', - on: ['order.customer_id', '=', 'c.id'], + on: { 'order.customer_id': { $eq: { $field: 'c.id' } } }, }, { type: 'inner', object: 'order_item', alias: 'oi', - on: ['order.id', '=', 'oi.order_id'], + on: { 'order.id': { $eq: { $field: 'oi.order_id' } } }, }, { type: 'inner', object: 'product', alias: 'p', - on: ['oi.product_id', '=', 'p.id'], + on: { 'oi.product_id': { $eq: { $field: 'p.id' } } }, }, ], aggregations: [ @@ -979,7 +954,7 @@ describe('QuerySchema - Joins', () => { type: 'left', object: 'order', alias: 'o', - on: ['customer.id', '=', 'o.customer_id'], + on: { 'customer.id': { $eq: { $field: 'o.customer_id' } } }, }, ], aggregations: [ @@ -988,7 +963,7 @@ describe('QuerySchema - Joins', () => { { function: 'max', field: 'o.created_at', alias: 'last_order_date' }, ], groupBy: ['customer.id', 'customer.name', 'customer.email'], - sort: [{ field: 'lifetime_value', order: 'desc' }], + orderBy: [{ field: 'lifetime_value', order: 'desc' }], }; expect(() => QuerySchema.parse(query)).not.toThrow(); @@ -1565,7 +1540,7 @@ describe('QuerySchema - Complex Queries', () => { type: 'inner', object: 'customer', alias: 'c', - on: ['order.customer_id', '=', 'c.id'], + on: { 'order.customer_id': { $eq: { $field: 'c.id' } } }, }, ], aggregations: [ @@ -1573,8 +1548,8 @@ describe('QuerySchema - Complex Queries', () => { { function: 'count', alias: 'order_count' }, ], groupBy: ['customer_id'], - having: ['order_count', '>', 5], - sort: [{ field: 'total_amount', order: 'desc' }], + having: { order_count: { $gt: 5 } }, + orderBy: [{ field: 'total_amount', order: 'desc' }], top: 100, }; @@ -1591,7 +1566,7 @@ describe('QuerySchema - Complex Queries', () => { { type: 'inner', object: 'customer', - on: ['order.customer_id', '=', 'customer.id'], + on: { 'order.customer_id': { $eq: { $field: 'customer.id' } } }, }, ], aggregations: [ @@ -1608,8 +1583,8 @@ describe('QuerySchema - Complex Queries', () => { }, ], groupBy: ['customer_id'], - having: ['avg_amount', '>', 500], - sort: [{ field: 'avg_amount', order: 'desc' }], + having: { avg_amount: { $gt: 500 } }, + orderBy: [{ field: 'avg_amount', order: 'desc' }], top: 50, skip: 0, }; @@ -1653,7 +1628,7 @@ describe('QuerySchema - Edge Cases and Null Handling', () => { joins: [], windowFunctions: [], groupBy: [], - sort: [], + orderBy: [], }; expect(() => QuerySchema.parse(query)).not.toThrow(); @@ -1780,13 +1755,13 @@ describe('QuerySchema - Edge Cases and Null Handling', () => { { type: 'left', object: 'order', - on: ['customer.id', '=', 'order.customer_id'], + on: { 'customer.id': { $eq: { $field: 'order.customer_id' } } }, }, { type: 'inner', object: 'filtered_orders', alias: 'fo', - on: ['customer.id', '=', 'fo.customer_id'], + on: { 'customer.id': { $eq: { $field: 'fo.customer_id' } } }, subquery: { object: 'order', fields: ['customer_id', 'amount'], @@ -1829,7 +1804,7 @@ describe('QuerySchema - Edge Cases and Null Handling', () => { { type: 'invalid_join', object: 'customer', - on: ['order.customer_id', '=', 'customer.id'], + on: { 'order.customer_id': { $eq: { $field: 'customer.id' } } }, }, ], })).toThrow(); @@ -1851,7 +1826,7 @@ describe('QuerySchema - Edge Cases and Null Handling', () => { it('should reject invalid sort order', () => { expect(() => QuerySchema.parse({ object: 'account', - sort: [{ field: 'name', order: 'invalid' }], + orderBy: [{ field: 'name', order: 'invalid' }], })).toThrow(); }); }); @@ -1892,11 +1867,11 @@ describe('QuerySchema - Type Coercion Edge Cases', () => { it('should handle default sort order', () => { const query: QueryAST = { object: 'account', - sort: [{ field: 'name' }], // order defaults to 'asc' + orderBy: [{ field: 'name' }], // order defaults to 'asc' }; const result = QuerySchema.parse(query); - expect(result.sort?.[0].order).toBe('asc'); + expect(result.orderBy?.[0].order).toBe('asc'); }); it('should handle mixed field types', () => { @@ -1945,11 +1920,12 @@ describe('QuerySchema - Type Coercion Edge Cases', () => { { function: 'sum', field: 'amount', alias: 'total' }, ], groupBy: ['customer_id'], - having: [ - ['order_count', '>', 5], - 'and', - ['total', '>', 1000], - ], + having: { + $and: [ + { order_count: { $gt: 5 } }, + { total: { $gt: 1000 } }, + ], + }, }; expect(() => QuerySchema.parse(query)).not.toThrow(); diff --git a/packages/spec/src/data/validation.test.ts b/packages/spec/src/data/validation.test.ts index fd3b3682..128a8a1d 100644 --- a/packages/spec/src/data/validation.test.ts +++ b/packages/spec/src/data/validation.test.ts @@ -736,8 +736,7 @@ describe('ValidationRuleSchema (Discriminated Union)', () => { type: 'custom' as const, name: 'custom_business_rule', message: 'Custom validation failed', - field: 'business_field', - validatorFunction: 'validateBusinessRule', + handler: 'validateBusinessRule', }; expect(() => ValidationRuleSchema.parse(customValidation)).not.toThrow(); @@ -748,8 +747,7 @@ describe('ValidationRuleSchema (Discriminated Union)', () => { type: 'custom' as const, name: 'complex_validation', message: 'Validation failed', - field: 'data', - validatorFunction: 'complexValidator', + handler: 'complexValidator', params: { threshold: 100, mode: 'strict', @@ -764,7 +762,7 @@ describe('ValidationRuleSchema (Discriminated Union)', () => { type: 'custom' as const, name: 'record_level_check', message: 'Record validation failed', - validatorFunction: 'validateEntireRecord', + handler: 'validateEntireRecord', }; expect(() => ValidationRuleSchema.parse(validation)).not.toThrow(); @@ -1058,7 +1056,7 @@ describe('ValidationRuleSchema (Discriminated Union)', () => { type: 'custom', name: 'business_logic', message: 'Business logic validation failed', - validatorFunction: 'validateBusinessRules', + handler: 'validateBusinessRules', }, { type: 'conditional', @@ -1212,8 +1210,7 @@ describe('ValidationRuleSchema - Edge Cases and Null Handling', () => { type: 'custom' as const, name: 'custom_validation', message: 'Validation failed', - field: undefined, // Optional for record-level validation - validatorFunction: 'validateRecord', + handler: 'validateRecord', params: undefined, }; @@ -1225,7 +1222,7 @@ describe('ValidationRuleSchema - Edge Cases and Null Handling', () => { type: 'custom' as const, name: 'custom_validation', message: 'Validation failed', - validatorFunction: 'validateRecord', + handler: 'validateRecord', params: {}, }; @@ -1339,7 +1336,7 @@ describe('ValidationRuleSchema - Type Coercion Edge Cases', () => { type: 'custom' as const, name: 'custom_test', message: 'Test', - validatorFunction: 'validate', + handler: 'validate', params, }; diff --git a/packages/spec/src/driver/driver.test.ts b/packages/spec/src/driver/driver.test.ts index 0260fa78..b4e678af 100644 --- a/packages/spec/src/driver/driver.test.ts +++ b/packages/spec/src/driver/driver.test.ts @@ -77,9 +77,11 @@ describe('DriverInterfaceSchema', () => { checkHealth: async () => true, execute: async () => ({}), find: async () => [], + findStream: async function* () {}, findOne: async () => null, create: async () => ({}), update: async () => ({}), + upsert: async () => ({}), delete: async () => true, count: async () => 0, bulkCreate: async () => [], @@ -118,9 +120,11 @@ describe('DriverInterfaceSchema', () => { checkHealth: async () => true, execute: async () => ({}), find: async (object: string, query: any) => [], + findStream: async function* (object: string, query: any) {}, findOne: async (object: string, query: any) => null, create: async (object: string, data: any) => data, update: async (object: string, id: any, data: any) => data, + upsert: async (object: string, data: any) => data, delete: async (object: string, id: any) => true, count: async () => 0, bulkCreate: async (object: string, data: any[]) => data, @@ -222,9 +226,11 @@ describe('DriverInterfaceSchema', () => { checkHealth: async () => true, execute: async () => ({}), find: async () => [], + findStream: async function* () {}, findOne: async () => null, create: async () => ({}), update: async () => ({}), + upsert: async () => ({}), delete: async () => true, count: async () => 0, bulkCreate: async () => [], @@ -301,9 +307,11 @@ describe('DriverInterfaceSchema', () => { checkHealth: async () => true, execute: async () => ({}), find: async () => [], + findStream: async function* () {}, findOne: async () => null, create: async () => ({}), update: async () => ({}), + upsert: async () => ({}), delete: async () => true, count: async () => 0, bulkCreate: async () => [], @@ -363,9 +371,11 @@ describe('DriverInterfaceSchema', () => { checkHealth: async () => true, execute: async () => ({}), find: async () => [], + findStream: async function* () {}, findOne: async () => null, create: async () => ({}), update: async () => ({}), + upsert: async () => ({}), delete: async () => true, count: async () => 0, bulkCreate: async () => [], @@ -403,9 +413,11 @@ describe('DriverInterfaceSchema', () => { checkHealth: async () => true, execute: async () => ({}), find: async () => [], + findStream: async function* () {}, findOne: async () => null, create: async () => ({}), update: async () => ({}), + upsert: async () => ({}), delete: async () => true, count: async () => 0, bulkCreate: async () => [], @@ -445,9 +457,11 @@ describe('DriverInterfaceSchema', () => { checkHealth: async () => true, execute: async () => ({}), find: async (object, query) => [], + findStream: async function* (object, query) {}, findOne: async (object, query) => null, create: async (object, data) => data, update: async (object, id, data) => data, + upsert: async (object, data) => data, delete: async (object, id) => true, count: async () => 0, bulkCreate: async (object, data) => data, @@ -485,9 +499,11 @@ describe('DriverInterfaceSchema', () => { checkHealth: async () => true, execute: async () => ({}), find: async (object, query) => [], + findStream: async function* (object, query) {}, findOne: async (object, query) => null, create: async (object, data) => data, update: async (object, id, data) => data, + upsert: async (object, data) => data, delete: async (object, id) => true, count: async () => 0, bulkCreate: async (object, data) => data, @@ -525,9 +541,11 @@ describe('DriverInterfaceSchema', () => { checkHealth: async () => true, execute: async () => ({}), find: async (object, query) => [], + findStream: async function* (object, query) {}, findOne: async (object, query) => null, create: async (object, data) => data, update: async (object, id, data) => data, + upsert: async (object, data) => data, delete: async (object, id) => true, count: async () => 0, bulkCreate: async (object, data) => data, @@ -565,9 +583,11 @@ describe('DriverInterfaceSchema', () => { checkHealth: async () => true, execute: async () => ({}), find: async (object, query) => [], + findStream: async function* (object, query) {}, findOne: async (object, query) => null, create: async (object, data) => data, update: async (object, id, data) => data, + upsert: async (object, data) => data, delete: async (object, id) => true, count: async () => 0, bulkCreate: async (object, data) => data, @@ -605,9 +625,11 @@ describe('DriverInterfaceSchema', () => { checkHealth: async () => true, execute: async () => ({}), find: async (object, query) => [], + findStream: async function* (object, query) {}, findOne: async (object, query) => null, create: async (object, data) => data, update: async (object, id, data) => data, + upsert: async (object, data) => data, delete: async (object, id) => true, count: async () => 0, bulkCreate: async (object, data) => data, diff --git a/packages/spec/src/hub/tenant.zod.ts b/packages/spec/src/hub/tenant.zod.ts index fd411afa..a2df7498 100644 --- a/packages/spec/src/hub/tenant.zod.ts +++ b/packages/spec/src/hub/tenant.zod.ts @@ -48,9 +48,66 @@ export const TenantQuotaSchema = z.object({ export type TenantQuota = z.infer; -// Tenant Schema REMOVED. -// The concept of a "Tenant" is now an attribute of a "Space". -// See HubSpaceSchema in space.zod.ts which embeds TenantIsolationLevel and TenantQuotaSchema. +/** + * Tenant Schema + * + * @deprecated This schema is maintained for backward compatibility only. + * New implementations should use HubSpaceSchema which embeds tenant concepts. + * + * **Migration Guide:** + * ```typescript + * // Old approach (deprecated): + * const tenant: Tenant = { + * id: 'tenant_123', + * name: 'My Tenant', + * isolationLevel: 'shared_schema', + * quotas: { maxUsers: 100 } + * }; + * + * // New approach (recommended): + * const space: HubSpace = { + * id: '...uuid...', + * name: 'My Tenant', + * slug: 'my-tenant', + * ownerId: 'user_id', + * runtime: { + * isolation: 'shared_schema', + * quotas: { maxUsers: 100 } + * }, + * bom: { ... } + * }; + * ``` + * + * See HubSpaceSchema in space.zod.ts for the recommended approach. + */ +export const TenantSchema = z.object({ + /** + * Unique tenant identifier + */ + id: z.string().describe('Unique tenant identifier'), + + /** + * Tenant display name + */ + name: z.string().describe('Tenant display name'), + + /** + * Data isolation level + */ + isolationLevel: TenantIsolationLevel, + + /** + * Custom configuration values + */ + customizations: z.record(z.any()).optional().describe('Custom configuration values'), + + /** + * Resource quotas + */ + quotas: TenantQuotaSchema.optional(), +}); + +export type Tenant = z.infer; /** * Tenant Isolation Strategy Documentation diff --git a/packages/spec/src/kernel/plugin.test.ts b/packages/spec/src/kernel/plugin.test.ts index 821108fd..010972b2 100644 --- a/packages/spec/src/kernel/plugin.test.ts +++ b/packages/spec/src/kernel/plugin.test.ts @@ -42,6 +42,9 @@ describe('PluginContextSchema', () => { post: () => {}, use: () => {} } + }, + drivers: { + register: () => {} } }; @@ -81,6 +84,9 @@ describe('PluginContextSchema', () => { post: () => {}, use: () => {} } + }, + drivers: { + register: () => {} } }; @@ -130,6 +136,9 @@ describe('PluginContextSchema', () => { post: (path: string, handler: Function) => {}, use: (pathOrHandler: string | Function, handler?: Function) => {} } + }, + drivers: { + register: (driver: any) => {} } }; @@ -320,6 +329,9 @@ describe('Plugin Lifecycle Scenarios', () => { post: () => {}, use: () => {} } + }, + drivers: { + register: () => {} } } as any); } @@ -377,6 +389,9 @@ describe('Plugin Lifecycle Scenarios', () => { post: () => {}, use: () => {} } + }, + drivers: { + register: () => {} } } as any; @@ -444,6 +459,9 @@ describe('Plugin Lifecycle Scenarios', () => { post: () => {}, use: () => {} } + }, + drivers: { + register: () => {} } } as any, '1.0.0', @@ -503,6 +521,9 @@ describe('Plugin Lifecycle Scenarios', () => { post: () => {}, use: () => {} } + }, + drivers: { + register: () => {} } } as any); } diff --git a/packages/spec/src/permission/sharing.test.ts b/packages/spec/src/permission/sharing.test.ts index 5a038e35..72aed756 100644 --- a/packages/spec/src/permission/sharing.test.ts +++ b/packages/spec/src/permission/sharing.test.ts @@ -9,7 +9,7 @@ import { describe('SharingRuleType', () => { it('should accept valid sharing rule types', () => { - const validTypes = ['owner', 'criteria', 'manual', 'guest']; + const validTypes = ['owner', 'criteria']; validTypes.forEach(type => { expect(() => SharingRuleType.parse(type)).not.toThrow(); @@ -18,6 +18,8 @@ describe('SharingRuleType', () => { it('should reject invalid sharing rule types', () => { expect(() => SharingRuleType.parse('automatic')).toThrow(); + expect(() => SharingRuleType.parse('manual')).toThrow(); + expect(() => SharingRuleType.parse('guest')).toThrow(); expect(() => SharingRuleType.parse('public')).toThrow(); expect(() => SharingRuleType.parse('')).toThrow(); }); @@ -25,7 +27,7 @@ describe('SharingRuleType', () => { describe('SharingLevel', () => { it('should accept valid sharing levels', () => { - const validLevels = ['read', 'edit']; + const validLevels = ['read', 'edit', 'full']; validLevels.forEach(level => { expect(() => SharingLevel.parse(level)).not.toThrow(); @@ -35,7 +37,6 @@ describe('SharingLevel', () => { it('should reject invalid sharing levels', () => { expect(() => SharingLevel.parse('write')).toThrow(); expect(() => SharingLevel.parse('delete')).toThrow(); - expect(() => SharingLevel.parse('full')).toThrow(); }); }); @@ -60,7 +61,12 @@ describe('SharingRuleSchema', () => { const rule: SharingRule = { name: 'sales_team_access', object: 'opportunity', - sharedWith: 'group_sales_team', + type: 'criteria', + condition: "stage = 'Open'", + sharedWith: { + type: 'group', + value: 'group_sales_team', + }, }; expect(() => SharingRuleSchema.parse(rule)).not.toThrow(); @@ -70,19 +76,34 @@ describe('SharingRuleSchema', () => { expect(() => SharingRuleSchema.parse({ name: 'valid_rule_name', object: 'account', - sharedWith: 'group_id', + type: 'criteria', + condition: "status = 'Active'", + sharedWith: { + type: 'group', + value: 'group_id', + }, })).not.toThrow(); expect(() => SharingRuleSchema.parse({ name: 'InvalidRule', object: 'account', - sharedWith: 'group_id', + type: 'criteria', + condition: "status = 'Active'", + sharedWith: { + type: 'group', + value: 'group_id', + }, })).toThrow(); expect(() => SharingRuleSchema.parse({ name: 'invalid-rule', object: 'account', - sharedWith: 'group_id', + type: 'criteria', + condition: "status = 'Active'", + sharedWith: { + type: 'group', + value: 'group_id', + }, })).toThrow(); }); @@ -90,7 +111,12 @@ describe('SharingRuleSchema', () => { const rule = SharingRuleSchema.parse({ name: 'test_rule', object: 'account', - sharedWith: 'group_id', + type: 'criteria', + condition: "status = 'Active'", + sharedWith: { + type: 'group', + value: 'group_id', + }, }); expect(rule.active).toBe(true); @@ -105,38 +131,63 @@ describe('SharingRuleSchema', () => { active: true, object: 'opportunity', type: 'criteria', - criteria: "stage = 'Closed Won' AND amount > 100000", + condition: "stage = 'Closed Won' AND amount > 100000", accessLevel: 'edit', - sharedWith: 'group_executive_team', + sharedWith: { + type: 'group', + value: 'group_executive_team', + }, }); expect(rule.label).toBe('Full Sharing Rule'); - expect(rule.criteria).toContain('Closed Won'); + expect(rule.condition).toContain('Closed Won'); }); it('should accept different sharing rule types', () => { - const types: Array = ['owner', 'criteria', 'manual', 'guest']; + // Criteria-based rule + const criteriaRule = SharingRuleSchema.parse({ + name: 'test_criteria_rule', + object: 'account', + type: 'criteria', + condition: "status = 'Active'", + sharedWith: { + type: 'group', + value: 'group_id', + }, + }); + expect(criteriaRule.type).toBe('criteria'); - types.forEach(type => { - const rule = SharingRuleSchema.parse({ - name: 'test_rule', - object: 'account', - type, - sharedWith: 'group_id', - }); - expect(rule.type).toBe(type); + // Owner-based rule + const ownerRule = SharingRuleSchema.parse({ + name: 'test_owner_rule', + object: 'account', + type: 'owner', + ownedBy: { + type: 'role', + value: 'role_sales_rep', + }, + sharedWith: { + type: 'role', + value: 'role_sales_manager', + }, }); + expect(ownerRule.type).toBe('owner'); }); it('should accept different access levels', () => { - const levels: Array = ['read', 'edit']; + const levels: Array = ['read', 'edit', 'full']; levels.forEach(level => { const rule = SharingRuleSchema.parse({ name: 'test_rule', object: 'account', + type: 'criteria', + condition: "status = 'Active'", accessLevel: level, - sharedWith: 'group_id', + sharedWith: { + type: 'group', + value: 'group_id', + }, }); expect(rule.accessLevel).toBe(level); }); @@ -147,8 +198,15 @@ describe('SharingRuleSchema', () => { name: 'owner_hierarchy_rule', object: 'account', type: 'owner', + ownedBy: { + type: 'role', + value: 'role_sales_rep', + }, accessLevel: 'read', - sharedWith: 'role_sales_manager', + sharedWith: { + type: 'role', + value: 'role_sales_manager', + }, }); expect(rule.type).toBe('owner'); @@ -159,45 +217,61 @@ describe('SharingRuleSchema', () => { name: 'high_value_accounts', object: 'account', type: 'criteria', - criteria: "annual_revenue > 1000000 AND status = 'Active'", + condition: "annual_revenue > 1000000 AND status = 'Active'", accessLevel: 'read', - sharedWith: 'group_executive_team', + sharedWith: { + type: 'group', + value: 'group_executive_team', + }, }); expect(rule.type).toBe('criteria'); - expect(rule.criteria).toBeDefined(); + expect(rule.condition).toBeDefined(); }); - it('should accept manual sharing rule', () => { + it('should accept criteria sharing rule for manual approval workflows', () => { const rule = SharingRuleSchema.parse({ - name: 'manual_share', + name: 'manual_approval_share', object: 'opportunity', - type: 'manual', + type: 'criteria', + condition: "requires_approval = true", accessLevel: 'edit', - sharedWith: 'user_john_doe', + sharedWith: { + type: 'user', + value: 'user_john_doe', + }, }); - expect(rule.type).toBe('manual'); + expect(rule.type).toBe('criteria'); }); it('should accept guest sharing rule', () => { const rule = SharingRuleSchema.parse({ name: 'public_access', object: 'knowledge_article', - type: 'guest', + type: 'criteria', + condition: "is_published = true", accessLevel: 'read', - sharedWith: 'guest_users', + sharedWith: { + type: 'guest', + value: 'guest_users', + }, }); - expect(rule.type).toBe('guest'); + expect(rule.type).toBe('criteria'); }); it('should accept inactive sharing rule', () => { const rule = SharingRuleSchema.parse({ name: 'disabled_rule', object: 'account', + type: 'criteria', + condition: "status = 'Inactive'", active: false, - sharedWith: 'group_id', + sharedWith: { + type: 'group', + value: 'group_id', + }, }); expect(rule.active).toBe(false); @@ -209,12 +283,15 @@ describe('SharingRuleSchema', () => { label: 'West Coast Territory Access', object: 'account', type: 'criteria', - criteria: "billing_state IN ('CA', 'OR', 'WA')", + condition: "billing_state IN ('CA', 'OR', 'WA')", accessLevel: 'edit', - sharedWith: 'group_west_coast_sales', + sharedWith: { + type: 'group', + value: 'group_west_coast_sales', + }, }); - expect(rule.criteria).toContain('CA'); + expect(rule.condition).toContain('CA'); }); it('should handle department-based sharing', () => { @@ -222,9 +299,12 @@ describe('SharingRuleSchema', () => { name: 'finance_department_access', object: 'invoice', type: 'criteria', - criteria: "department = 'Finance'", + condition: "department = 'Finance'", accessLevel: 'edit', - sharedWith: 'group_finance_team', + sharedWith: { + type: 'group', + value: 'group_finance_team', + }, }); expect(rule.object).toBe('invoice'); @@ -235,9 +315,12 @@ describe('SharingRuleSchema', () => { name: 'readonly_access', object: 'contract', type: 'criteria', - criteria: "status = 'Executed'", + condition: "status = 'Executed'", accessLevel: 'read', - sharedWith: 'group_all_users', + sharedWith: { + type: 'group', + value: 'group_all_users', + }, }); expect(rule.accessLevel).toBe('read'); @@ -248,9 +331,12 @@ describe('SharingRuleSchema', () => { name: 'edit_access', object: 'opportunity', type: 'criteria', - criteria: "stage != 'Closed Won'", + condition: "stage != 'Closed Won'", accessLevel: 'edit', - sharedWith: 'group_sales_reps', + sharedWith: { + type: 'group', + value: 'group_sales_reps', + }, }); expect(rule.accessLevel).toBe('edit'); @@ -259,18 +345,30 @@ describe('SharingRuleSchema', () => { it('should reject sharing rule without required fields', () => { expect(() => SharingRuleSchema.parse({ object: 'account', - sharedWith: 'group_id', - })).toThrow(); + type: 'criteria', + condition: "status = 'Active'", + sharedWith: { + type: 'group', + value: 'group_id', + }, + })).toThrow(); // Missing name expect(() => SharingRuleSchema.parse({ name: 'test_rule', - sharedWith: 'group_id', - })).toThrow(); + type: 'criteria', + condition: "status = 'Active'", + sharedWith: { + type: 'group', + value: 'group_id', + }, + })).toThrow(); // Missing object expect(() => SharingRuleSchema.parse({ name: 'test_rule', object: 'account', - })).toThrow(); + type: 'criteria', + condition: "status = 'Active'", + })).toThrow(); // Missing sharedWith }); it('should reject invalid sharing rule type', () => { @@ -278,7 +376,11 @@ describe('SharingRuleSchema', () => { name: 'test_rule', object: 'account', type: 'invalid_type', - sharedWith: 'group_id', + condition: "status = 'Active'", + sharedWith: { + type: 'group', + value: 'group_id', + }, })).toThrow(); }); @@ -286,8 +388,13 @@ describe('SharingRuleSchema', () => { expect(() => SharingRuleSchema.parse({ name: 'test_rule', object: 'account', + type: 'criteria', + condition: "status = 'Active'", accessLevel: 'delete', - sharedWith: 'group_id', + sharedWith: { + type: 'group', + value: 'group_id', + }, })).toThrow(); }); });