diff --git a/content/docs/references/api/websocket.mdx b/content/docs/references/api/websocket.mdx index 539c1ae54..c3ff861d5 100644 --- a/content/docs/references/api/websocket.mdx +++ b/content/docs/references/api/websocket.mdx @@ -12,8 +12,8 @@ description: Websocket protocol schemas ## TypeScript Usage ```typescript -import { AckMessageSchema, CursorMessageSchema, CursorPositionSchema, DocumentStateSchema, EditMessageSchema, EditOperationSchema, EditOperationTypeSchema, ErrorMessageSchema, EventFilterSchema, EventFilterConditionSchema, EventMessageSchema, EventPatternSchema, EventSubscriptionSchema, FilterOperatorSchema, PingMessageSchema, PongMessageSchema, PresenceMessageSchema, PresenceStateSchema, PresenceUpdateSchema, SubscribeMessageSchema, UnsubscribeMessageSchema, UnsubscribeRequestSchema, WebSocketConfigSchema, WebSocketMessageSchema, WebSocketMessageTypeSchema, WebSocketPresenceStatusSchema } from '@objectstack/spec/api'; -import type { AckMessage, CursorMessage, CursorPosition, DocumentState, EditMessage, EditOperation, EditOperationType, ErrorMessage, EventFilter, EventFilterCondition, EventMessage, EventPattern, EventSubscription, FilterOperator, PingMessage, PongMessage, PresenceMessage, PresenceState, PresenceUpdate, SubscribeMessage, UnsubscribeMessage, UnsubscribeRequest, WebSocketConfig, WebSocketMessage, WebSocketMessageType, WebSocketPresenceStatus } from '@objectstack/spec/api'; +import { AckMessageSchema, CursorMessageSchema, CursorPositionSchema, DocumentStateSchema, EditMessageSchema, EditOperationSchema, EditOperationTypeSchema, ErrorMessageSchema, EventFilterSchema, EventFilterConditionSchema, EventMessageSchema, EventPatternSchema, EventSubscriptionSchema, FilterOperatorSchema, PingMessageSchema, PongMessageSchema, PresenceMessageSchema, PresenceStateSchema, PresenceUpdateSchema, SimpleCursorPositionSchema, SimplePresenceStateSchema, SubscribeMessageSchema, UnsubscribeMessageSchema, UnsubscribeRequestSchema, WebSocketConfigSchema, WebSocketEventSchema, WebSocketMessageSchema, WebSocketMessageTypeSchema, WebSocketPresenceStatusSchema, WebSocketServerConfigSchema } from '@objectstack/spec/api'; +import type { AckMessage, CursorMessage, CursorPosition, DocumentState, EditMessage, EditOperation, EditOperationType, ErrorMessage, EventFilter, EventFilterCondition, EventMessage, EventPattern, EventSubscription, FilterOperator, PingMessage, PongMessage, PresenceMessage, PresenceState, PresenceUpdate, SimpleCursorPosition, SimplePresenceState, SubscribeMessage, UnsubscribeMessage, UnsubscribeRequest, WebSocketConfig, WebSocketEvent, WebSocketMessage, WebSocketMessageType, WebSocketPresenceStatus, WebSocketServerConfig } from '@objectstack/spec/api'; // Validate data const result = AckMessageSchema.parse(data); @@ -289,6 +289,34 @@ Event pattern (supports wildcards like "record.*" or "*.created") --- +## SimpleCursorPosition + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **userId** | `string` | ✅ | User identifier | +| **recordId** | `string` | ✅ | Record identifier being edited | +| **fieldName** | `string` | ✅ | Field name being edited | +| **position** | `number` | ✅ | Cursor position (character offset from start) | +| **selection** | `object` | optional | Text selection range (if text is selected) | + +--- + +## SimplePresenceState + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **userId** | `string` | ✅ | User identifier | +| **userName** | `string` | ✅ | User display name | +| **status** | `Enum<'online' \| 'away' \| 'offline'>` | ✅ | User presence status | +| **lastSeen** | `number` | ✅ | Unix timestamp of last activity in milliseconds | +| **metadata** | `Record` | optional | Additional presence metadata (e.g., current page, custom status) | + +--- + ## SubscribeMessage ### Properties @@ -342,6 +370,19 @@ Event pattern (supports wildcards like "record.*" or "*.created") --- +## WebSocketEvent + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'subscribe' \| 'unsubscribe' \| 'data-change' \| 'presence-update' \| 'cursor-update' \| 'error'>` | ✅ | Event type | +| **channel** | `string` | ✅ | Channel identifier (e.g., "record.account.123", "user.456") | +| **payload** | `any` | optional | Event payload data | +| **timestamp** | `number` | ✅ | Unix timestamp in milliseconds | + +--- + ## WebSocketMessage --- @@ -372,3 +413,18 @@ Event pattern (supports wildcards like "record.*" or "*.created") * `busy` * `offline` +--- + +## WebSocketServerConfig + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional | Enable WebSocket server | +| **path** | `string` | optional | WebSocket endpoint path | +| **heartbeatInterval** | `number` | optional | Heartbeat interval in milliseconds | +| **reconnectAttempts** | `number` | optional | Maximum reconnection attempts for clients | +| **presence** | `boolean` | optional | Enable presence tracking | +| **cursorSharing** | `boolean` | optional | Enable collaborative cursor sharing | + diff --git a/content/docs/references/permission/permission.mdx b/content/docs/references/permission/permission.mdx index f60179db2..d1e32c936 100644 --- a/content/docs/references/permission/permission.mdx +++ b/content/docs/references/permission/permission.mdx @@ -12,8 +12,8 @@ description: Permission protocol schemas ## TypeScript Usage ```typescript -import { FieldPermissionSchema, ObjectPermissionSchema, PermissionSetSchema } from '@objectstack/spec/permission'; -import type { FieldPermission, ObjectPermission, PermissionSet } from '@objectstack/spec/permission'; +import { FieldPermissionSchema, ObjectPermissionSchema, PermissionSetSchema, RLSRuleSchema } from '@objectstack/spec/permission'; +import type { FieldPermission, ObjectPermission, PermissionSet, RLSRule } from '@objectstack/spec/permission'; // Validate data const result = FieldPermissionSchema.parse(data); @@ -62,4 +62,21 @@ const result = FieldPermissionSchema.parse(data); | **objects** | `Record` | ✅ | Entity permissions | | **fields** | `Record` | optional | Field level security | | **systemPermissions** | `string[]` | optional | System level capabilities | +| **rls** | `object[]` | optional | Row-level security rules | +| **contextVariables** | `Record` | optional | Context variables for RLS evaluation | + +--- + +## RLSRule + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Rule unique identifier (snake_case) | +| **objectName** | `string` | ✅ | Target object name | +| **operation** | `Enum<'read' \| 'create' \| 'update' \| 'delete'>` | ✅ | Database operation this rule applies to | +| **filter** | `object` | ✅ | Filter condition for row-level access | +| **enabled** | `boolean` | optional | Whether this rule is active | +| **priority** | `number` | optional | Rule evaluation priority (higher = evaluated first) | diff --git a/packages/spec/json-schema/api/SimpleCursorPosition.json b/packages/spec/json-schema/api/SimpleCursorPosition.json new file mode 100644 index 000000000..95ed09641 --- /dev/null +++ b/packages/spec/json-schema/api/SimpleCursorPosition.json @@ -0,0 +1,53 @@ +{ + "$ref": "#/definitions/SimpleCursorPosition", + "definitions": { + "SimpleCursorPosition": { + "type": "object", + "properties": { + "userId": { + "type": "string", + "description": "User identifier" + }, + "recordId": { + "type": "string", + "description": "Record identifier being edited" + }, + "fieldName": { + "type": "string", + "description": "Field name being edited" + }, + "position": { + "type": "number", + "description": "Cursor position (character offset from start)" + }, + "selection": { + "type": "object", + "properties": { + "start": { + "type": "number", + "description": "Selection start position" + }, + "end": { + "type": "number", + "description": "Selection end position" + } + }, + "required": [ + "start", + "end" + ], + "additionalProperties": false, + "description": "Text selection range (if text is selected)" + } + }, + "required": [ + "userId", + "recordId", + "fieldName", + "position" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/api/SimplePresenceState.json b/packages/spec/json-schema/api/SimplePresenceState.json new file mode 100644 index 000000000..ddb406f6e --- /dev/null +++ b/packages/spec/json-schema/api/SimplePresenceState.json @@ -0,0 +1,44 @@ +{ + "$ref": "#/definitions/SimplePresenceState", + "definitions": { + "SimplePresenceState": { + "type": "object", + "properties": { + "userId": { + "type": "string", + "description": "User identifier" + }, + "userName": { + "type": "string", + "description": "User display name" + }, + "status": { + "type": "string", + "enum": [ + "online", + "away", + "offline" + ], + "description": "User presence status" + }, + "lastSeen": { + "type": "number", + "description": "Unix timestamp of last activity in milliseconds" + }, + "metadata": { + "type": "object", + "additionalProperties": {}, + "description": "Additional presence metadata (e.g., current page, custom status)" + } + }, + "required": [ + "userId", + "userName", + "status", + "lastSeen" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/api/WebSocketEvent.json b/packages/spec/json-schema/api/WebSocketEvent.json new file mode 100644 index 000000000..1fd12a244 --- /dev/null +++ b/packages/spec/json-schema/api/WebSocketEvent.json @@ -0,0 +1,40 @@ +{ + "$ref": "#/definitions/WebSocketEvent", + "definitions": { + "WebSocketEvent": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "subscribe", + "unsubscribe", + "data-change", + "presence-update", + "cursor-update", + "error" + ], + "description": "Event type" + }, + "channel": { + "type": "string", + "description": "Channel identifier (e.g., \"record.account.123\", \"user.456\")" + }, + "payload": { + "description": "Event payload data" + }, + "timestamp": { + "type": "number", + "description": "Unix timestamp in milliseconds" + } + }, + "required": [ + "type", + "channel", + "timestamp" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/api/WebSocketServerConfig.json b/packages/spec/json-schema/api/WebSocketServerConfig.json new file mode 100644 index 000000000..8e99ad524 --- /dev/null +++ b/packages/spec/json-schema/api/WebSocketServerConfig.json @@ -0,0 +1,42 @@ +{ + "$ref": "#/definitions/WebSocketServerConfig", + "definitions": { + "WebSocketServerConfig": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable WebSocket server" + }, + "path": { + "type": "string", + "default": "/ws", + "description": "WebSocket endpoint path" + }, + "heartbeatInterval": { + "type": "number", + "default": 30000, + "description": "Heartbeat interval in milliseconds" + }, + "reconnectAttempts": { + "type": "number", + "default": 5, + "description": "Maximum reconnection attempts for clients" + }, + "presence": { + "type": "boolean", + "default": false, + "description": "Enable presence tracking" + }, + "cursorSharing": { + "type": "boolean", + "default": false, + "description": "Enable collaborative cursor sharing" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/permission/PermissionSet.json b/packages/spec/json-schema/permission/PermissionSet.json index 481cdb833..cc55ea969 100644 --- a/packages/spec/json-schema/permission/PermissionSet.json +++ b/packages/spec/json-schema/permission/PermissionSet.json @@ -100,6 +100,116 @@ "type": "string" }, "description": "System level capabilities" + }, + "rls": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z_][a-z0-9_]*$", + "description": "Rule unique identifier (snake_case)" + }, + "objectName": { + "type": "string", + "description": "Target object name" + }, + "operation": { + "type": "string", + "enum": [ + "read", + "create", + "update", + "delete" + ], + "description": "Database operation this rule applies to" + }, + "filter": { + "type": "object", + "properties": { + "field": { + "type": "string", + "description": "Field name to filter on" + }, + "operator": { + "type": "string", + "enum": [ + "eq", + "ne", + "in", + "nin", + "gt", + "gte", + "lt", + "lte" + ], + "description": "Filter operator" + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "array" + }, + { + "type": "object", + "properties": { + "contextVariable": { + "type": "string" + } + }, + "required": [ + "contextVariable" + ], + "additionalProperties": false, + "description": "Reference to context variable (e.g., { contextVariable: \"current_user.tenant_id\" })" + } + ], + "description": "Filter value or context variable reference" + } + }, + "required": [ + "field", + "operator", + "value" + ], + "additionalProperties": false, + "description": "Filter condition for row-level access" + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Whether this rule is active" + }, + "priority": { + "type": "number", + "default": 0, + "description": "Rule evaluation priority (higher = evaluated first)" + } + }, + "required": [ + "name", + "objectName", + "operation", + "filter" + ], + "additionalProperties": false + }, + "description": "Row-level security rules" + }, + "contextVariables": { + "type": "object", + "additionalProperties": {}, + "description": "Context variables for RLS evaluation" } }, "required": [ diff --git a/packages/spec/json-schema/permission/RLSRule.json b/packages/spec/json-schema/permission/RLSRule.json new file mode 100644 index 000000000..473e5aa82 --- /dev/null +++ b/packages/spec/json-schema/permission/RLSRule.json @@ -0,0 +1,107 @@ +{ + "$ref": "#/definitions/RLSRule", + "definitions": { + "RLSRule": { + "type": "object", + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z_][a-z0-9_]*$", + "description": "Rule unique identifier (snake_case)" + }, + "objectName": { + "type": "string", + "description": "Target object name" + }, + "operation": { + "type": "string", + "enum": [ + "read", + "create", + "update", + "delete" + ], + "description": "Database operation this rule applies to" + }, + "filter": { + "type": "object", + "properties": { + "field": { + "type": "string", + "description": "Field name to filter on" + }, + "operator": { + "type": "string", + "enum": [ + "eq", + "ne", + "in", + "nin", + "gt", + "gte", + "lt", + "lte" + ], + "description": "Filter operator" + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "array" + }, + { + "type": "object", + "properties": { + "contextVariable": { + "type": "string" + } + }, + "required": [ + "contextVariable" + ], + "additionalProperties": false, + "description": "Reference to context variable (e.g., { contextVariable: \"current_user.tenant_id\" })" + } + ], + "description": "Filter value or context variable reference" + } + }, + "required": [ + "field", + "operator", + "value" + ], + "additionalProperties": false, + "description": "Filter condition for row-level access" + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Whether this rule is active" + }, + "priority": { + "type": "number", + "default": 0, + "description": "Rule evaluation priority (higher = evaluated first)" + } + }, + "required": [ + "name", + "objectName", + "operation", + "filter" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/src/api/websocket.zod.ts b/packages/spec/src/api/websocket.zod.ts index f0e46a005..f6e954e29 100644 --- a/packages/spec/src/api/websocket.zod.ts +++ b/packages/spec/src/api/websocket.zod.ts @@ -431,3 +431,141 @@ export const WebSocketConfigSchema = z.object({ }); export type WebSocketConfig = z.infer; + +// ========================================== +// Simplified Collaboration API +// ========================================== + +/** + * Simplified WebSocket Event Schema + * + * A simplified event schema for basic WebSocket communication. + * Complements the comprehensive WebSocketMessageSchema above for simpler use cases. + * + * @example Subscribe to channel + * ```typescript + * { + * type: 'subscribe', + * channel: 'record.account.123', + * payload: { events: ['created', 'updated'] }, + * timestamp: Date.now() + * } + * ``` + * + * @example Data change notification + * ```typescript + * { + * type: 'data-change', + * channel: 'record.account.123', + * payload: { id: '123', action: 'updated', data: {...} }, + * timestamp: Date.now() + * } + * ``` + */ +export const WebSocketEventSchema = z.object({ + type: z.enum([ + 'subscribe', // Client subscribes to channel + 'unsubscribe', // Client unsubscribes from channel + 'data-change', // Data modification event + 'presence-update', // User presence change + 'cursor-update', // Cursor position change (collaborative editing) + 'error', // Error message + ]).describe('Event type'), + channel: z.string().describe('Channel identifier (e.g., "record.account.123", "user.456")'), + payload: z.any().describe('Event payload data'), + timestamp: z.number().describe('Unix timestamp in milliseconds'), +}); + +export type WebSocketEvent = z.infer; + +/** + * Simplified Presence State Schema + * + * A simplified presence schema for basic user presence tracking. + * Complements the comprehensive PresenceStateSchema for simpler integrations. + * + * Use this for basic presence features. For advanced features like device tracking, + * custom status, and session management, use the comprehensive PresenceStateSchema above. + * + * @example User online + * ```typescript + * { + * userId: 'user123', + * userName: 'John Doe', + * status: 'online', + * lastSeen: Date.now(), + * metadata: { currentPage: '/dashboard' } + * } + * ``` + */ +export const SimplePresenceStateSchema = z.object({ + userId: z.string().describe('User identifier'), + userName: z.string().describe('User display name'), + status: z.enum(['online', 'away', 'offline']).describe('User presence status'), + lastSeen: z.number().describe('Unix timestamp of last activity in milliseconds'), + metadata: z.record(z.any()).optional().describe('Additional presence metadata (e.g., current page, custom status)'), +}); + +export type SimplePresenceState = z.infer; + +/** + * Simplified Cursor Position Schema + * + * A simplified cursor position schema for basic collaborative editing. + * Complements the comprehensive CursorPositionSchema for simpler use cases. + * + * Use this for basic cursor sharing. For advanced features like selections, + * color coding, and document versioning, use the comprehensive CursorPositionSchema above. + * + * @example Cursor in text field + * ```typescript + * { + * userId: 'user123', + * recordId: 'account_456', + * fieldName: 'description', + * position: 42, + * selection: { start: 42, end: 57 } + * } + * ``` + */ +export const SimpleCursorPositionSchema = z.object({ + userId: z.string().describe('User identifier'), + recordId: z.string().describe('Record identifier being edited'), + fieldName: z.string().describe('Field name being edited'), + position: z.number().describe('Cursor position (character offset from start)'), + selection: z.object({ + start: z.number().describe('Selection start position'), + end: z.number().describe('Selection end position'), + }).optional().describe('Text selection range (if text is selected)'), +}); + +export type SimpleCursorPosition = z.infer; + +/** + * WebSocket Server Configuration Schema + * + * Server-side configuration for WebSocket services. + * Controls features like presence tracking, cursor sharing, and connection management. + * + * @example Production configuration + * ```typescript + * { + * enabled: true, + * path: '/ws', + * heartbeatInterval: 30000, + * reconnectAttempts: 5, + * presence: true, + * cursorSharing: true + * } + * ``` + */ +export const WebSocketServerConfigSchema = z.object({ + enabled: z.boolean().default(false).describe('Enable WebSocket server'), + path: z.string().default('/ws').describe('WebSocket endpoint path'), + heartbeatInterval: z.number().default(30000).describe('Heartbeat interval in milliseconds'), + reconnectAttempts: z.number().default(5).describe('Maximum reconnection attempts for clients'), + presence: z.boolean().default(false).describe('Enable presence tracking'), + cursorSharing: z.boolean().default(false).describe('Enable collaborative cursor sharing'), +}); + +export type WebSocketServerConfig = z.infer; diff --git a/packages/spec/src/permission/permission.zod.ts b/packages/spec/src/permission/permission.zod.ts index 3a414a493..35517a423 100644 --- a/packages/spec/src/permission/permission.zod.ts +++ b/packages/spec/src/permission/permission.zod.ts @@ -1,6 +1,57 @@ import { z } from 'zod'; import { SnakeCaseIdentifierSchema } from '../shared/identifiers.zod'; +/** + * Row-Level Security Rule Schema (Simplified) + * + * Simplified RLS rule definition that can be embedded in permission sets. + * For comprehensive RLS features, see ../permission/rls.zod.ts + * + * This schema allows permission sets to include basic row-level filters + * that restrict data access based on user context. + * + * @example Tenant isolation rule + * ```typescript + * { + * name: 'tenant_isolation', + * objectName: 'account', + * operation: 'read', + * filter: { + * field: 'tenant_id', + * operator: 'eq', + * value: { contextVariable: 'current_user.tenant_id' } + * }, + * enabled: true, + * priority: 0 + * } + * ``` + */ +export const RLSRuleSchema = z.object({ + name: z.string() + .regex(/^[a-z_][a-z0-9_]*$/) + .describe('Rule unique identifier (snake_case)'), + objectName: z.string().describe('Target object name'), + operation: z.enum(['read', 'create', 'update', 'delete']) + .describe('Database operation this rule applies to'), + filter: z.object({ + field: z.string().describe('Field name to filter on'), + operator: z.enum(['eq', 'ne', 'in', 'nin', 'gt', 'gte', 'lt', 'lte']) + .describe('Filter operator'), + value: z.union([ + z.string(), + z.number(), + z.boolean(), + z.array(z.any()), + z.object({ contextVariable: z.string() }) + .describe('Reference to context variable (e.g., { contextVariable: "current_user.tenant_id" })'), + ]).describe('Filter value or context variable reference'), + }).describe('Filter condition for row-level access'), + enabled: z.boolean().default(true).describe('Whether this rule is active'), + priority: z.number().default(0).describe('Rule evaluation priority (higher = evaluated first)'), +}); + +export type RLSRule = z.infer; + /** * Entity (Object) Level Permissions * Defines CRUD + VAMA (View All / Modify All) + Lifecycle access. @@ -91,6 +142,54 @@ export const PermissionSetSchema = z.object({ /** System permissions (e.g., "manage_users") */ systemPermissions: z.array(z.string()).optional().describe('System level capabilities'), + + /** + * Row-Level Security Rules + * + * Simplified RLS rules that filter records based on user context. + * These rules are applied in addition to object-level permissions. + * + * For comprehensive RLS features, use the dedicated RLS protocol in ../permission/rls.zod.ts + * + * @example Multi-tenant isolation + * ```typescript + * rls: [{ + * name: 'tenant_filter', + * objectName: 'account', + * operation: 'read', + * filter: { + * field: 'tenant_id', + * operator: 'eq', + * value: { contextVariable: 'current_user.tenant_id' } + * } + * }] + * ``` + */ + rls: z.array(RLSRuleSchema).optional().describe('Row-level security rules'), + + /** + * Context-Based Access Control Variables + * + * Custom context variables that can be referenced in RLS rules. + * These variables are evaluated at runtime based on the user's session. + * + * Common context variables: + * - `current_user.id` - Current user ID + * - `current_user.tenant_id` - User's tenant/organization ID + * - `current_user.department` - User's department + * - `current_user.role` - User's role + * - `current_user.region` - User's geographic region + * + * @example Custom context + * ```typescript + * contextVariables: { + * allowed_regions: ['US', 'EU'], + * access_level: 2, + * custom_attribute: 'value' + * } + * ``` + */ + contextVariables: z.record(z.any()).optional().describe('Context variables for RLS evaluation'), }); export type PermissionSet = z.infer;