Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This pull request adds simplified WebSocket collaboration schemas and row-level security (RLS) capabilities to the ObjectStack specification. The changes introduce lightweight alternatives to existing comprehensive protocols for common use cases, enabling easier integration for basic real-time collaboration and tenant isolation scenarios.
Changes:
- Adds simplified WebSocket event schemas (WebSocketEventSchema, SimplePresenceStateSchema, SimpleCursorPositionSchema, WebSocketServerConfigSchema) with Unix timestamp support for performance
- Extends PermissionSetSchema with embedded RLS rules using structured filter objects instead of SQL expressions
- Generates corresponding JSON schemas and documentation for IDE/tooling integration
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/spec/src/api/websocket.zod.ts | Adds simplified WebSocket collaboration API with basic event protocol, presence tracking, cursor sharing, and server configuration schemas |
| packages/spec/src/permission/permission.zod.ts | Extends permission protocol with simplified RLS rules (RLSRuleSchema) and context variables for row-level filtering |
| packages/spec/json-schema/permission/RLSRule.json | Generated JSON schema for RLS rule validation |
| packages/spec/json-schema/permission/PermissionSet.json | Updated JSON schema including new rls and contextVariables fields |
| packages/spec/json-schema/api/WebSocketServerConfig.json | Generated JSON schema for WebSocket server configuration |
| packages/spec/json-schema/api/WebSocketEvent.json | Generated JSON schema for simplified WebSocket events |
| packages/spec/json-schema/api/SimplePresenceState.json | Generated JSON schema for simplified presence tracking |
| packages/spec/json-schema/api/SimpleCursorPosition.json | Generated JSON schema for simplified cursor positions |
| content/docs/references/permission/permission.mdx | Updated documentation with RLSRule schema reference and property tables |
| content/docs/references/api/websocket.mdx | Updated documentation with new simplified WebSocket schema references |
| 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'), |
There was a problem hiding this comment.
The RLS filter implementation lacks security documentation about preventing injection attacks. The comprehensive RLS protocol (../permission/rls.zod.ts, lines 267-275) includes important security notes about using parameterized queries to prevent SQL injection when evaluating RLS conditions. Consider adding similar security guidance in the documentation for the simplified RLS filter, explaining how filter values and context variables should be safely evaluated at runtime.
| operation: z.enum(['read', 'create', 'update', 'delete']) | ||
| .describe('Database operation this rule applies to'), |
There was a problem hiding this comment.
The operation enum values in the simplified RLS schema differ from the comprehensive RLS protocol. The comprehensive protocol uses database operations (select, insert, update, delete, all) while the simplified version uses CRUD operations (read, create, update, delete). This inconsistency could cause confusion and integration issues.
Consider either:
- Aligning with the comprehensive protocol by using
selectandinsertinstead ofreadandcreate, OR - Adding explicit documentation explaining why different operation names are used and how they map between the two protocols
| 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<typeof WebSocketEventSchema>; | ||
|
|
||
| /** | ||
| * 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<typeof SimplePresenceStateSchema>; | ||
|
|
||
| /** | ||
| * 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<typeof SimpleCursorPositionSchema>; | ||
|
|
||
| /** | ||
| * 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<typeof WebSocketServerConfigSchema>; |
There was a problem hiding this comment.
The newly introduced WebSocketEventSchema, SimplePresenceStateSchema, SimpleCursorPositionSchema, and WebSocketServerConfigSchema lack test coverage. The codebase follows a pattern of comprehensive testing for all schemas (e.g., WebSocketConfigSchema has tests in websocket.test.ts). Add test cases to verify schema validation, defaults, and edge cases for these new schemas.
| 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<typeof RLSRuleSchema>; |
There was a problem hiding this comment.
The newly introduced RLSRuleSchema lacks test coverage. The codebase follows a pattern of comprehensive testing for all schemas (e.g., PermissionSetSchema has extensive tests in permission.test.ts). Add test cases to validate the RLS rule schema, including filter operators, context variable references, and edge cases such as invalid snake_case names or unsupported operators.
| name: z.string() | ||
| .regex(/^[a-z_][a-z0-9_]*$/) |
There was a problem hiding this comment.
The RLSRuleSchema duplicates the snake_case validation pattern instead of using the existing SnakeCaseIdentifierSchema imported at line 2. For consistency with the codebase (e.g., PermissionSetSchema at line 129), use SnakeCaseIdentifierSchema for the name field instead of the inline regex pattern.
| name: z.string() | |
| .regex(/^[a-z_][a-z0-9_]*$/) | |
| name: SnakeCaseIdentifierSchema |
| name: z.string() | ||
| .regex(/^[a-z_][a-z0-9_]*$/) | ||
| .describe('Rule unique identifier (snake_case)'), | ||
| objectName: z.string().describe('Target object name'), |
There was a problem hiding this comment.
The simplified RLS schema uses objectName while the comprehensive RLS protocol (../permission/rls.zod.ts, line 240) uses object for the same field. This naming inconsistency between the two protocols could confuse users when migrating from the simplified to the comprehensive version. Consider using object to maintain consistency with the comprehensive protocol.
| z.object({ contextVariable: z.string() }) | ||
| .describe('Reference to context variable (e.g., { contextVariable: "current_user.tenant_id" })'), |
There was a problem hiding this comment.
The contextVariable field accepts any string without validation. Consider adding pattern validation to ensure the context variable follows the expected format (e.g., 'current_user.tenant_id', 'current_user.department'). This would provide better developer experience through early validation and prevent runtime errors.
| z.object({ contextVariable: z.string() }) | |
| .describe('Reference to context variable (e.g., { contextVariable: "current_user.tenant_id" })'), | |
| z.object({ | |
| contextVariable: z.string() | |
| .regex(/^current_user\.[a-z_][a-z0-9_]*$/) | |
| .describe('Reference to context variable (current_user.<snake_case_field>, e.g., "current_user.tenant_id")'), | |
| }), |
| '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")'), |
There was a problem hiding this comment.
The channel field accepts any string without validation. Consider adding pattern validation or documentation about expected channel identifier formats (e.g., 'record.{object}.{id}', 'user.{id}'). The examples in the documentation show specific patterns like 'record.account.123' and 'user.456', but these patterns aren't enforced by the schema.
Adds simplified WebSocket collaboration APIs and row-level security capabilities to the permission system. Complements existing comprehensive protocols with lightweight alternatives for common use cases.
Changes
WebSocket Protocol (
api/websocket.zod.ts)Added simplified schemas alongside existing comprehensive protocol:
WebSocketEventSchema- Basic event protocol (subscribe, data-change, presence-update, cursor-update)SimplePresenceStateSchema- User presence trackingSimpleCursorPositionSchema- Collaborative cursor positionWebSocketServerConfigSchema- Server-side feature togglesPermission Protocol (
permission/permission.zod.ts)Extended
PermissionSetSchemawith row-level security:RLSRuleSchema- Field-level access filters with context variable supportrlsfield - Array of RLS rules per permission setcontextVariablesfield - Runtime evaluation contextImplementation Notes
Original prompt
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.