From 2579e24f2529cd667a3a053f450f5127497c1ce8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 14:10:05 +0000 Subject: [PATCH 1/3] Initial plan From c0935fb0382d9aaac6517a9818b3ae94bc6fc087 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 14:18:17 +0000 Subject: [PATCH 2/3] Add P0 Missing Protocols: Notification, Document, Change Management, External Lookup Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/spec/src/data/document.test.ts | 624 ++++++++++++++++ packages/spec/src/data/document.zod.ts | 370 ++++++++++ .../spec/src/data/external-lookup.test.ts | 664 ++++++++++++++++++ packages/spec/src/data/external-lookup.zod.ts | 255 +++++++ packages/spec/src/data/index.ts | 8 +- .../spec/src/system/change-management.test.ts | 656 +++++++++++++++++ .../spec/src/system/change-management.zod.ts | 372 ++++++++++ packages/spec/src/system/index.ts | 6 + packages/spec/src/system/notification.test.ts | 560 +++++++++++++++ packages/spec/src/system/notification.zod.ts | 379 ++++++++++ 10 files changed, 3893 insertions(+), 1 deletion(-) create mode 100644 packages/spec/src/data/document.test.ts create mode 100644 packages/spec/src/data/document.zod.ts create mode 100644 packages/spec/src/data/external-lookup.test.ts create mode 100644 packages/spec/src/data/external-lookup.zod.ts create mode 100644 packages/spec/src/system/change-management.test.ts create mode 100644 packages/spec/src/system/change-management.zod.ts create mode 100644 packages/spec/src/system/notification.test.ts create mode 100644 packages/spec/src/system/notification.zod.ts diff --git a/packages/spec/src/data/document.test.ts b/packages/spec/src/data/document.test.ts new file mode 100644 index 000000000..893bdd4c7 --- /dev/null +++ b/packages/spec/src/data/document.test.ts @@ -0,0 +1,624 @@ +import { describe, it, expect } from 'vitest'; +import { + DocumentVersionSchema, + DocumentTemplateSchema, + ESignatureConfigSchema, + DocumentSchema, + type Document, + type DocumentVersion, + type DocumentTemplate, + type ESignatureConfig, +} from './document.zod'; + +describe('DocumentVersionSchema', () => { + it('should validate complete document version', () => { + const validVersion: DocumentVersion = { + versionNumber: 2, + createdAt: 1704067200000, + createdBy: 'user_123', + size: 2048576, + checksum: 'a1b2c3d4e5f6', + downloadUrl: 'https://storage.example.com/docs/v2/file.pdf', + isLatest: true, + }; + + expect(() => DocumentVersionSchema.parse(validVersion)).not.toThrow(); + }); + + it('should accept minimal version', () => { + const minimalVersion = { + versionNumber: 1, + createdAt: Date.now(), + createdBy: 'user_456', + size: 1024, + checksum: 'checksum123', + downloadUrl: 'https://example.com/file.pdf', + }; + + expect(() => DocumentVersionSchema.parse(minimalVersion)).not.toThrow(); + }); + + it('should default isLatest to false', () => { + const version = { + versionNumber: 1, + createdAt: Date.now(), + createdBy: 'user_123', + size: 1024, + checksum: 'abc', + downloadUrl: 'https://example.com/file.pdf', + }; + + const parsed = DocumentVersionSchema.parse(version); + expect(parsed.isLatest).toBe(false); + }); + + it('should validate download URL', () => { + const invalidVersion = { + versionNumber: 1, + createdAt: Date.now(), + createdBy: 'user_123', + size: 1024, + checksum: 'abc', + downloadUrl: 'not-a-url', + }; + + expect(() => DocumentVersionSchema.parse(invalidVersion)).toThrow(); + }); +}); + +describe('DocumentTemplateSchema', () => { + it('should validate complete document template', () => { + const validTemplate: DocumentTemplate = { + id: 'contract-template', + name: 'Service Agreement', + description: 'Standard service agreement template', + fileUrl: 'https://example.com/templates/contract.docx', + fileType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + placeholders: [ + { + key: 'client_name', + label: 'Client Name', + type: 'text', + required: true, + }, + { + key: 'contract_date', + label: 'Contract Date', + type: 'date', + required: true, + }, + { + key: 'amount', + label: 'Contract Amount', + type: 'number', + required: false, + }, + ], + }; + + expect(() => DocumentTemplateSchema.parse(validTemplate)).not.toThrow(); + }); + + it('should accept minimal template', () => { + const minimalTemplate = { + id: 'simple-template', + name: 'Simple Template', + fileUrl: 'https://example.com/template.pdf', + fileType: 'application/pdf', + placeholders: [], + }; + + expect(() => DocumentTemplateSchema.parse(minimalTemplate)).not.toThrow(); + }); + + it('should default placeholder required to false', () => { + const template = { + id: 'template-1', + name: 'Template', + fileUrl: 'https://example.com/template.pdf', + fileType: 'application/pdf', + placeholders: [ + { + key: 'field1', + label: 'Field 1', + type: 'text' as const, + }, + ], + }; + + const parsed = DocumentTemplateSchema.parse(template); + expect(parsed.placeholders[0].required).toBe(false); + }); + + it('should accept all placeholder types', () => { + const types = ['text', 'number', 'date', 'image'] as const; + + types.forEach((type) => { + const template = { + id: `template-${type}`, + name: 'Template', + fileUrl: 'https://example.com/template.pdf', + fileType: 'application/pdf', + placeholders: [ + { + key: 'field', + label: 'Field', + type, + }, + ], + }; + + expect(() => DocumentTemplateSchema.parse(template)).not.toThrow(); + }); + }); + + it('should reject invalid placeholder type', () => { + const invalidTemplate = { + id: 'invalid-template', + name: 'Invalid', + fileUrl: 'https://example.com/template.pdf', + fileType: 'application/pdf', + placeholders: [ + { + key: 'field', + label: 'Field', + type: 'invalid', + }, + ], + }; + + expect(() => DocumentTemplateSchema.parse(invalidTemplate)).toThrow(); + }); +}); + +describe('ESignatureConfigSchema', () => { + it('should validate complete e-signature config', () => { + const validConfig: ESignatureConfig = { + provider: 'docusign', + enabled: true, + signers: [ + { + email: 'client@example.com', + name: 'John Doe', + role: 'Client', + order: 1, + }, + { + email: 'manager@example.com', + name: 'Jane Smith', + role: 'Manager', + order: 2, + }, + ], + expirationDays: 30, + reminderDays: 7, + }; + + expect(() => ESignatureConfigSchema.parse(validConfig)).not.toThrow(); + }); + + it('should accept minimal e-signature config', () => { + const minimalConfig = { + provider: 'hellosign', + signers: [ + { + email: 'signer@example.com', + name: 'Signer', + role: 'Signer', + order: 1, + }, + ], + }; + + expect(() => ESignatureConfigSchema.parse(minimalConfig)).not.toThrow(); + }); + + it('should default enabled to false', () => { + const config = { + provider: 'adobe-sign', + signers: [ + { + email: 'test@example.com', + name: 'Test', + role: 'Test', + order: 1, + }, + ], + }; + + const parsed = ESignatureConfigSchema.parse(config); + expect(parsed.enabled).toBe(false); + }); + + it('should default expirationDays to 30', () => { + const config = { + provider: 'custom', + signers: [ + { + email: 'test@example.com', + name: 'Test', + role: 'Test', + order: 1, + }, + ], + }; + + const parsed = ESignatureConfigSchema.parse(config); + expect(parsed.expirationDays).toBe(30); + }); + + it('should default reminderDays to 7', () => { + const config = { + provider: 'docusign', + signers: [ + { + email: 'test@example.com', + name: 'Test', + role: 'Test', + order: 1, + }, + ], + }; + + const parsed = ESignatureConfigSchema.parse(config); + expect(parsed.reminderDays).toBe(7); + }); + + it('should accept all provider types', () => { + const providers = ['docusign', 'adobe-sign', 'hellosign', 'custom'] as const; + + providers.forEach((provider) => { + const config = { + provider, + signers: [ + { + email: 'test@example.com', + name: 'Test', + role: 'Test', + order: 1, + }, + ], + }; + + expect(() => ESignatureConfigSchema.parse(config)).not.toThrow(); + }); + }); + + it('should validate signer email addresses', () => { + const invalidConfig = { + provider: 'docusign', + signers: [ + { + email: 'not-an-email', + name: 'Test', + role: 'Test', + order: 1, + }, + ], + }; + + expect(() => ESignatureConfigSchema.parse(invalidConfig)).toThrow(); + }); + + it('should accept multiple signers in order', () => { + const config = { + provider: 'docusign', + signers: [ + { + email: 'first@example.com', + name: 'First Signer', + role: 'Client', + order: 1, + }, + { + email: 'second@example.com', + name: 'Second Signer', + role: 'Vendor', + order: 2, + }, + { + email: 'third@example.com', + name: 'Third Signer', + role: 'Witness', + order: 3, + }, + ], + }; + + expect(() => ESignatureConfigSchema.parse(config)).not.toThrow(); + }); +}); + +describe('DocumentSchema', () => { + it('should validate complete document', () => { + const validDocument: Document = { + id: 'doc_123', + name: 'Service Agreement 2024', + description: 'Annual service agreement', + fileType: 'application/pdf', + fileSize: 1048576, + category: 'contracts', + tags: ['legal', '2024', 'services'], + versioning: { + enabled: true, + versions: [ + { + versionNumber: 1, + createdAt: 1704067200000, + createdBy: 'user_123', + size: 1048576, + checksum: 'abc123', + downloadUrl: 'https://example.com/docs/v1.pdf', + isLatest: true, + }, + ], + majorVersion: 1, + minorVersion: 0, + }, + access: { + isPublic: false, + sharedWith: ['user_456', 'team_789'], + expiresAt: 1735689600000, + }, + metadata: { + author: 'John Doe', + department: 'Legal', + }, + }; + + expect(() => DocumentSchema.parse(validDocument)).not.toThrow(); + }); + + it('should accept minimal document', () => { + const minimalDocument = { + id: 'doc_456', + name: 'Simple Document', + fileType: 'application/pdf', + fileSize: 1024, + }; + + expect(() => DocumentSchema.parse(minimalDocument)).not.toThrow(); + }); + + it('should validate document with template', () => { + const documentWithTemplate = { + id: 'doc_789', + name: 'Generated Contract', + fileType: 'application/pdf', + fileSize: 2048, + template: { + id: 'contract-template', + name: 'Contract Template', + fileUrl: 'https://example.com/template.docx', + fileType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + placeholders: [ + { + key: 'client_name', + label: 'Client Name', + type: 'text' as const, + required: true, + }, + ], + }, + }; + + expect(() => DocumentSchema.parse(documentWithTemplate)).not.toThrow(); + }); + + it('should validate document with e-signature', () => { + const documentWithSignature = { + id: 'doc_101', + name: 'Contract for Signature', + fileType: 'application/pdf', + fileSize: 1536, + eSignature: { + provider: 'docusign' as const, + enabled: true, + signers: [ + { + email: 'client@example.com', + name: 'Client', + role: 'Client', + order: 1, + }, + ], + expirationDays: 15, + reminderDays: 3, + }, + }; + + expect(() => DocumentSchema.parse(documentWithSignature)).not.toThrow(); + }); + + it('should validate versioning configuration', () => { + const documentWithVersions = { + id: 'doc_202', + name: 'Versioned Document', + fileType: 'application/pdf', + fileSize: 2048, + versioning: { + enabled: true, + versions: [ + { + versionNumber: 1, + createdAt: 1704000000000, + createdBy: 'user_123', + size: 1024, + checksum: 'v1-checksum', + downloadUrl: 'https://example.com/v1.pdf', + isLatest: false, + }, + { + versionNumber: 2, + createdAt: 1704067200000, + createdBy: 'user_456', + size: 2048, + checksum: 'v2-checksum', + downloadUrl: 'https://example.com/v2.pdf', + isLatest: true, + }, + ], + majorVersion: 2, + minorVersion: 0, + }, + }; + + expect(() => DocumentSchema.parse(documentWithVersions)).not.toThrow(); + }); + + it('should default access.isPublic to false', () => { + const document = { + id: 'doc_303', + name: 'Document', + fileType: 'application/pdf', + fileSize: 1024, + access: { + sharedWith: ['user_123'], + }, + }; + + const parsed = DocumentSchema.parse(document); + expect(parsed.access?.isPublic).toBe(false); + }); + + it('should validate document with tags and category', () => { + const document = { + id: 'doc_404', + name: 'Tagged Document', + fileType: 'application/pdf', + fileSize: 1024, + category: 'invoices', + tags: ['2024', 'Q1', 'paid'], + }; + + expect(() => DocumentSchema.parse(document)).not.toThrow(); + }); + + it('should validate document with custom metadata', () => { + const document = { + id: 'doc_505', + name: 'Document with Metadata', + fileType: 'application/pdf', + fileSize: 1024, + metadata: { + author: 'Jane Doe', + department: 'Finance', + projectCode: 'PROJ-2024-001', + customField: 'Custom Value', + }, + }; + + expect(() => DocumentSchema.parse(document)).not.toThrow(); + }); + + it('should validate complete document with all features', () => { + const completeDocument: Document = { + id: 'doc_complete', + name: 'Complete Document Example', + description: 'A fully-featured document with all options', + fileType: 'application/pdf', + fileSize: 5242880, + category: 'legal-contracts', + tags: ['important', 'signed', '2024', 'annual'], + versioning: { + enabled: true, + versions: [ + { + versionNumber: 1, + createdAt: 1704000000000, + createdBy: 'user_001', + size: 5000000, + checksum: 'checksum-v1', + downloadUrl: 'https://storage.example.com/docs/complete-v1.pdf', + isLatest: false, + }, + { + versionNumber: 2, + createdAt: 1704067200000, + createdBy: 'user_002', + size: 5242880, + checksum: 'checksum-v2', + downloadUrl: 'https://storage.example.com/docs/complete-v2.pdf', + isLatest: true, + }, + ], + majorVersion: 2, + minorVersion: 0, + }, + template: { + id: 'annual-contract-template', + name: 'Annual Contract Template', + description: 'Standard annual contract', + fileUrl: 'https://example.com/templates/annual-contract.docx', + fileType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + placeholders: [ + { + key: 'company_name', + label: 'Company Name', + type: 'text', + required: true, + }, + { + key: 'contract_value', + label: 'Contract Value', + type: 'number', + required: true, + }, + { + key: 'start_date', + label: 'Start Date', + type: 'date', + required: true, + }, + { + key: 'company_logo', + label: 'Company Logo', + type: 'image', + required: false, + }, + ], + }, + eSignature: { + provider: 'docusign', + enabled: true, + signers: [ + { + email: 'client@company.com', + name: 'John Client', + role: 'Client Representative', + order: 1, + }, + { + email: 'vendor@example.com', + name: 'Jane Vendor', + role: 'Vendor Representative', + order: 2, + }, + { + email: 'legal@example.com', + name: 'Legal Counsel', + role: 'Legal Reviewer', + order: 3, + }, + ], + expirationDays: 45, + reminderDays: 5, + }, + access: { + isPublic: false, + sharedWith: ['user_001', 'user_002', 'team_legal', 'team_finance'], + expiresAt: 1767225600000, // Future date + }, + metadata: { + author: 'Legal Department', + department: 'Legal', + projectCode: 'PROJ-2024-ANNUAL', + confidentialityLevel: 'High', + retentionYears: 7, + complianceStandards: ['SOX', 'GDPR'], + }, + }; + + expect(() => DocumentSchema.parse(completeDocument)).not.toThrow(); + }); +}); diff --git a/packages/spec/src/data/document.zod.ts b/packages/spec/src/data/document.zod.ts new file mode 100644 index 000000000..62381e880 --- /dev/null +++ b/packages/spec/src/data/document.zod.ts @@ -0,0 +1,370 @@ +import { z } from 'zod'; + +/** + * Document Version Schema + * + * Represents a single version of a document in a version-controlled system. + * Each version is immutable and maintains its own metadata and download URL. + * + * @example + * ```json + * { + * "versionNumber": 2, + * "createdAt": 1704067200000, + * "createdBy": "user_123", + * "size": 2048576, + * "checksum": "a1b2c3d4e5f6", + * "downloadUrl": "https://storage.example.com/docs/v2/file.pdf", + * "isLatest": true + * } + * ``` + */ +export const DocumentVersionSchema = z.object({ + /** + * Sequential version number (increments with each new version) + */ + versionNumber: z.number().describe('Version number'), + + /** + * Timestamp when this version was created (Unix milliseconds) + */ + createdAt: z.number().describe('Creation timestamp'), + + /** + * User ID who created this version + */ + createdBy: z.string().describe('Creator user ID'), + + /** + * File size in bytes + */ + size: z.number().describe('File size in bytes'), + + /** + * Checksum/hash of the file content (for integrity verification) + */ + checksum: z.string().describe('File checksum'), + + /** + * URL to download this specific version + */ + downloadUrl: z.string().url().describe('Download URL'), + + /** + * Whether this is the latest version + * @default false + */ + isLatest: z.boolean().optional().default(false).describe('Is latest version'), +}); + +/** + * Document Template Schema + * + * Defines a reusable document template with dynamic placeholders. + * Templates can be used to generate documents with variable content. + * + * @example + * ```json + * { + * "id": "contract-template", + * "name": "Service Agreement", + * "description": "Standard service agreement template", + * "fileUrl": "https://example.com/templates/contract.docx", + * "fileType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + * "placeholders": [ + * { + * "key": "client_name", + * "label": "Client Name", + * "type": "text", + * "required": true + * }, + * { + * "key": "contract_date", + * "label": "Contract Date", + * "type": "date", + * "required": true + * } + * ] + * } + * ``` + */ +export const DocumentTemplateSchema = z.object({ + /** + * Unique identifier for the template + */ + id: z.string().describe('Template ID'), + + /** + * Human-readable name of the template + */ + name: z.string().describe('Template name'), + + /** + * Optional description of the template's purpose + */ + description: z.string().optional().describe('Template description'), + + /** + * URL to the template file + */ + fileUrl: z.string().url().describe('Template file URL'), + + /** + * MIME type of the template file + */ + fileType: z.string().describe('File MIME type'), + + /** + * List of dynamic placeholders in the template + */ + placeholders: z.array(z.object({ + /** + * Placeholder identifier (used in template) + */ + key: z.string().describe('Placeholder key'), + + /** + * Human-readable label for the placeholder + */ + label: z.string().describe('Placeholder label'), + + /** + * Data type of the placeholder value + */ + type: z.enum(['text', 'number', 'date', 'image']).describe('Placeholder type'), + + /** + * Whether this placeholder must be filled + * @default false + */ + required: z.boolean().optional().default(false).describe('Is required'), + })).describe('Template placeholders'), +}); + +/** + * E-Signature Configuration Schema + * + * Configuration for electronic signature workflows. + * Supports integration with popular e-signature providers. + * + * @example + * ```json + * { + * "provider": "docusign", + * "enabled": true, + * "signers": [ + * { + * "email": "client@example.com", + * "name": "John Doe", + * "role": "Client", + * "order": 1 + * }, + * { + * "email": "manager@example.com", + * "name": "Jane Smith", + * "role": "Manager", + * "order": 2 + * } + * ], + * "expirationDays": 30, + * "reminderDays": 7 + * } + * ``` + */ +export const ESignatureConfigSchema = z.object({ + /** + * E-signature service provider + */ + provider: z.enum(['docusign', 'adobe-sign', 'hellosign', 'custom']).describe('E-signature provider'), + + /** + * Whether e-signature is enabled for this document + * @default false + */ + enabled: z.boolean().optional().default(false).describe('E-signature enabled'), + + /** + * List of signers in signing order + */ + signers: z.array(z.object({ + /** + * Signer's email address + */ + email: z.string().email().describe('Signer email'), + + /** + * Signer's full name + */ + name: z.string().describe('Signer name'), + + /** + * Signer's role in the document + */ + role: z.string().describe('Signer role'), + + /** + * Signing order (lower numbers sign first) + */ + order: z.number().describe('Signing order'), + })).describe('Document signers'), + + /** + * Days until signature request expires + * @default 30 + */ + expirationDays: z.number().optional().default(30).describe('Expiration days'), + + /** + * Days between reminder emails + * @default 7 + */ + reminderDays: z.number().optional().default(7).describe('Reminder interval days'), +}); + +/** + * Document Schema + * + * Comprehensive document management protocol supporting versioning, + * templates, e-signatures, and access control. + * + * @example + * ```json + * { + * "id": "doc_123", + * "name": "Service Agreement 2024", + * "description": "Annual service agreement", + * "fileType": "application/pdf", + * "fileSize": 1048576, + * "category": "contracts", + * "tags": ["legal", "2024", "services"], + * "versioning": { + * "enabled": true, + * "versions": [ + * { + * "versionNumber": 1, + * "createdAt": 1704067200000, + * "createdBy": "user_123", + * "size": 1048576, + * "checksum": "abc123", + * "downloadUrl": "https://example.com/docs/v1.pdf", + * "isLatest": true + * } + * ], + * "majorVersion": 1, + * "minorVersion": 0 + * }, + * "access": { + * "isPublic": false, + * "sharedWith": ["user_456", "team_789"], + * "expiresAt": 1735689600000 + * }, + * "metadata": { + * "author": "John Doe", + * "department": "Legal" + * } + * } + * ``` + */ +export const DocumentSchema = z.object({ + /** + * Unique document identifier + */ + id: z.string().describe('Document ID'), + + /** + * Document name + */ + name: z.string().describe('Document name'), + + /** + * Optional document description + */ + description: z.string().optional().describe('Document description'), + + /** + * MIME type of the document + */ + fileType: z.string().describe('File MIME type'), + + /** + * File size in bytes + */ + fileSize: z.number().describe('File size in bytes'), + + /** + * Document category for organization + */ + category: z.string().optional().describe('Document category'), + + /** + * Tags for searchability and organization + */ + tags: z.array(z.string()).optional().describe('Document tags'), + + /** + * Version control configuration + */ + versioning: z.object({ + /** + * Whether versioning is enabled + */ + enabled: z.boolean().describe('Versioning enabled'), + + /** + * List of all document versions + */ + versions: z.array(DocumentVersionSchema).describe('Version history'), + + /** + * Current major version number + */ + majorVersion: z.number().describe('Major version'), + + /** + * Current minor version number + */ + minorVersion: z.number().describe('Minor version'), + }).optional().describe('Version control'), + + /** + * Template configuration (if document is generated from template) + */ + template: DocumentTemplateSchema.optional().describe('Document template'), + + /** + * E-signature configuration + */ + eSignature: ESignatureConfigSchema.optional().describe('E-signature config'), + + /** + * Access control settings + */ + access: z.object({ + /** + * Whether document is publicly accessible + * @default false + */ + isPublic: z.boolean().optional().default(false).describe('Public access'), + + /** + * List of user/team IDs with access + */ + sharedWith: z.array(z.string()).optional().describe('Shared with'), + + /** + * Timestamp when access expires (Unix milliseconds) + */ + expiresAt: z.number().optional().describe('Access expiration'), + }).optional().describe('Access control'), + + /** + * Custom metadata fields + */ + metadata: z.record(z.any()).optional().describe('Custom metadata'), +}); + +// Type exports +export type Document = z.infer; +export type DocumentVersion = z.infer; +export type DocumentTemplate = z.infer; +export type ESignatureConfig = z.infer; diff --git a/packages/spec/src/data/external-lookup.test.ts b/packages/spec/src/data/external-lookup.test.ts new file mode 100644 index 000000000..dcac3d193 --- /dev/null +++ b/packages/spec/src/data/external-lookup.test.ts @@ -0,0 +1,664 @@ +import { describe, it, expect } from 'vitest'; +import { + ExternalDataSourceSchema, + FieldMappingSchema, + ExternalLookupSchema, + type ExternalLookup, + type ExternalDataSource, + type FieldMapping, +} from './external-lookup.zod'; + +describe('ExternalDataSourceSchema', () => { + it('should validate complete external data source', () => { + const validSource: ExternalDataSource = { + id: 'salesforce-accounts', + name: 'Salesforce Account Data', + type: 'rest-api', + endpoint: 'https://api.salesforce.com/services/data/v58.0', + authentication: { + type: 'oauth2', + config: { + clientId: 'client_123', + clientSecret: 'secret_456', + tokenUrl: 'https://login.salesforce.com/services/oauth2/token', + }, + }, + }; + + expect(() => ExternalDataSourceSchema.parse(validSource)).not.toThrow(); + }); + + it('should accept all data source types', () => { + const types = ['odata', 'rest-api', 'graphql', 'custom'] as const; + + types.forEach((type) => { + const source = { + id: `source-${type}`, + name: `${type} Source`, + type, + endpoint: 'https://api.example.com', + authentication: { + type: 'none' as const, + config: {}, + }, + }; + + expect(() => ExternalDataSourceSchema.parse(source)).not.toThrow(); + }); + }); + + it('should accept all authentication types', () => { + const authTypes = ['oauth2', 'api-key', 'basic', 'none'] as const; + + authTypes.forEach((authType) => { + const source = { + id: `auth-${authType}`, + name: 'Test Source', + type: 'rest-api' as const, + endpoint: 'https://api.example.com', + authentication: { + type: authType, + config: {}, + }, + }; + + expect(() => ExternalDataSourceSchema.parse(source)).not.toThrow(); + }); + }); + + it('should validate API key authentication', () => { + const source = { + id: 'api-key-source', + name: 'API Key Source', + type: 'rest-api', + endpoint: 'https://api.example.com', + authentication: { + type: 'api-key', + config: { + apiKey: 'sk-1234567890', + headerName: 'X-API-Key', + }, + }, + }; + + expect(() => ExternalDataSourceSchema.parse(source)).not.toThrow(); + }); + + it('should validate basic authentication', () => { + const source = { + id: 'basic-auth-source', + name: 'Basic Auth Source', + type: 'rest-api', + endpoint: 'https://api.example.com', + authentication: { + type: 'basic', + config: { + username: 'user', + password: 'pass', + }, + }, + }; + + expect(() => ExternalDataSourceSchema.parse(source)).not.toThrow(); + }); + + it('should reject invalid endpoint URL', () => { + const invalidSource = { + id: 'invalid-source', + name: 'Invalid Source', + type: 'rest-api', + endpoint: 'not-a-url', + authentication: { + type: 'none', + config: {}, + }, + }; + + expect(() => ExternalDataSourceSchema.parse(invalidSource)).toThrow(); + }); +}); + +describe('FieldMappingSchema', () => { + it('should validate complete field mapping', () => { + const validMapping: FieldMapping = { + externalField: 'AccountName', + localField: 'name', + type: 'text', + readonly: true, + }; + + expect(() => FieldMappingSchema.parse(validMapping)).not.toThrow(); + }); + + it('should accept minimal field mapping', () => { + const minimalMapping = { + externalField: 'ExternalField', + localField: 'local_field', + type: 'text', + }; + + expect(() => FieldMappingSchema.parse(minimalMapping)).not.toThrow(); + }); + + it('should default readonly to true', () => { + const mapping = { + externalField: 'Field1', + localField: 'field_1', + type: 'text', + }; + + const parsed = FieldMappingSchema.parse(mapping); + expect(parsed.readonly).toBe(true); + }); + + it('should accept writable field mapping', () => { + const writableMapping = { + externalField: 'Status', + localField: 'status', + type: 'text', + readonly: false, + }; + + expect(() => FieldMappingSchema.parse(writableMapping)).not.toThrow(); + }); + + it('should accept various field types', () => { + const types = ['text', 'number', 'boolean', 'date', 'datetime', 'lookup']; + + types.forEach((type) => { + const mapping = { + externalField: 'Field', + localField: 'field', + type, + }; + + expect(() => FieldMappingSchema.parse(mapping)).not.toThrow(); + }); + }); +}); + +describe('ExternalLookupSchema', () => { + it('should validate complete external lookup', () => { + const validLookup: ExternalLookup = { + fieldName: 'external_account', + dataSource: { + id: 'salesforce-api', + name: 'Salesforce', + type: 'rest-api', + endpoint: 'https://api.salesforce.com/services/data/v58.0', + authentication: { + type: 'oauth2', + config: { clientId: 'client_123' }, + }, + }, + query: { + endpoint: '/sobjects/Account', + method: 'GET', + parameters: { limit: 100 }, + }, + fieldMappings: [ + { + externalField: 'Name', + localField: 'account_name', + type: 'text', + readonly: true, + }, + { + externalField: 'Industry', + localField: 'industry', + type: 'text', + readonly: true, + }, + ], + caching: { + enabled: true, + ttl: 300, + strategy: 'ttl', + }, + fallback: { + enabled: true, + showError: true, + }, + rateLimit: { + requestsPerSecond: 10, + burstSize: 20, + }, + }; + + expect(() => ExternalLookupSchema.parse(validLookup)).not.toThrow(); + }); + + it('should accept minimal external lookup', () => { + const minimalLookup = { + fieldName: 'external_field', + dataSource: { + id: 'source-1', + name: 'Source', + type: 'rest-api', + endpoint: 'https://api.example.com', + authentication: { + type: 'none', + config: {}, + }, + }, + query: { + endpoint: '/data', + }, + fieldMappings: [ + { + externalField: 'Field1', + localField: 'field_1', + type: 'text', + }, + ], + }; + + expect(() => ExternalLookupSchema.parse(minimalLookup)).not.toThrow(); + }); + + it('should default query method to GET', () => { + const lookup = { + fieldName: 'test_field', + dataSource: { + id: 'source-1', + name: 'Source', + type: 'rest-api', + endpoint: 'https://api.example.com', + authentication: { + type: 'none', + config: {}, + }, + }, + query: { + endpoint: '/data', + }, + fieldMappings: [], + }; + + const parsed = ExternalLookupSchema.parse(lookup); + expect(parsed.query.method).toBe('GET'); + }); + + it('should accept POST query method', () => { + const lookup = { + fieldName: 'test_field', + dataSource: { + id: 'source-1', + name: 'Source', + type: 'rest-api', + endpoint: 'https://api.example.com', + authentication: { + type: 'none', + config: {}, + }, + }, + query: { + endpoint: '/search', + method: 'POST' as const, + parameters: { + query: 'search term', + }, + }, + fieldMappings: [], + }; + + expect(() => ExternalLookupSchema.parse(lookup)).not.toThrow(); + }); + + it('should default caching to enabled with 300s TTL', () => { + const lookup = { + fieldName: 'test_field', + dataSource: { + id: 'source-1', + name: 'Source', + type: 'rest-api', + endpoint: 'https://api.example.com', + authentication: { + type: 'none', + config: {}, + }, + }, + query: { + endpoint: '/data', + }, + fieldMappings: [], + caching: {}, + }; + + const parsed = ExternalLookupSchema.parse(lookup); + expect(parsed.caching?.enabled).toBe(true); + expect(parsed.caching?.ttl).toBe(300); + expect(parsed.caching?.strategy).toBe('ttl'); + }); + + it('should accept all cache strategies', () => { + const strategies = ['lru', 'lfu', 'ttl'] as const; + + strategies.forEach((strategy) => { + const lookup = { + fieldName: 'test_field', + dataSource: { + id: 'source-1', + name: 'Source', + type: 'rest-api', + endpoint: 'https://api.example.com', + authentication: { + type: 'none', + config: {}, + }, + }, + query: { + endpoint: '/data', + }, + fieldMappings: [], + caching: { + strategy, + }, + }; + + expect(() => ExternalLookupSchema.parse(lookup)).not.toThrow(); + }); + }); + + it('should validate custom cache TTL', () => { + const lookup = { + fieldName: 'test_field', + dataSource: { + id: 'source-1', + name: 'Source', + type: 'rest-api', + endpoint: 'https://api.example.com', + authentication: { + type: 'none', + config: {}, + }, + }, + query: { + endpoint: '/data', + }, + fieldMappings: [], + caching: { + enabled: true, + ttl: 600, + strategy: 'ttl' as const, + }, + }; + + const parsed = ExternalLookupSchema.parse(lookup); + expect(parsed.caching?.ttl).toBe(600); + }); + + it('should default fallback to enabled with showError true', () => { + const lookup = { + fieldName: 'test_field', + dataSource: { + id: 'source-1', + name: 'Source', + type: 'rest-api', + endpoint: 'https://api.example.com', + authentication: { + type: 'none', + config: {}, + }, + }, + query: { + endpoint: '/data', + }, + fieldMappings: [], + fallback: {}, + }; + + const parsed = ExternalLookupSchema.parse(lookup); + expect(parsed.fallback?.enabled).toBe(true); + expect(parsed.fallback?.showError).toBe(true); + }); + + it('should accept custom fallback value', () => { + const lookup = { + fieldName: 'test_field', + dataSource: { + id: 'source-1', + name: 'Source', + type: 'rest-api', + endpoint: 'https://api.example.com', + authentication: { + type: 'none', + config: {}, + }, + }, + query: { + endpoint: '/data', + }, + fieldMappings: [], + fallback: { + enabled: true, + defaultValue: 'N/A', + showError: false, + }, + }; + + const parsed = ExternalLookupSchema.parse(lookup); + expect(parsed.fallback?.defaultValue).toBe('N/A'); + expect(parsed.fallback?.showError).toBe(false); + }); + + it('should validate rate limiting', () => { + const lookup = { + fieldName: 'test_field', + dataSource: { + id: 'source-1', + name: 'Source', + type: 'rest-api', + endpoint: 'https://api.example.com', + authentication: { + type: 'none', + config: {}, + }, + }, + query: { + endpoint: '/data', + }, + fieldMappings: [], + rateLimit: { + requestsPerSecond: 5, + burstSize: 10, + }, + }; + + expect(() => ExternalLookupSchema.parse(lookup)).not.toThrow(); + }); + + it('should accept rate limit without burst size', () => { + const lookup = { + fieldName: 'test_field', + dataSource: { + id: 'source-1', + name: 'Source', + type: 'rest-api', + endpoint: 'https://api.example.com', + authentication: { + type: 'none', + config: {}, + }, + }, + query: { + endpoint: '/data', + }, + fieldMappings: [], + rateLimit: { + requestsPerSecond: 10, + }, + }; + + expect(() => ExternalLookupSchema.parse(lookup)).not.toThrow(); + }); + + it('should validate OData external lookup', () => { + const odataLookup = { + fieldName: 'odata_products', + dataSource: { + id: 'odata-service', + name: 'OData Product Service', + type: 'odata' as const, + endpoint: 'https://services.odata.org/V4/Northwind/Northwind.svc', + authentication: { + type: 'none' as const, + config: {}, + }, + }, + query: { + endpoint: '/Products', + method: 'GET' as const, + parameters: { + $filter: "ProductName eq 'Chai'", + $select: 'ProductID,ProductName,UnitPrice', + }, + }, + fieldMappings: [ + { + externalField: 'ProductID', + localField: 'product_id', + type: 'number', + readonly: true, + }, + { + externalField: 'ProductName', + localField: 'product_name', + type: 'text', + readonly: true, + }, + { + externalField: 'UnitPrice', + localField: 'unit_price', + type: 'currency', + readonly: true, + }, + ], + }; + + expect(() => ExternalLookupSchema.parse(odataLookup)).not.toThrow(); + }); + + it('should validate GraphQL external lookup', () => { + const graphqlLookup = { + fieldName: 'graphql_users', + dataSource: { + id: 'graphql-api', + name: 'GraphQL API', + type: 'graphql' as const, + endpoint: 'https://api.example.com/graphql', + authentication: { + type: 'api-key' as const, + config: { + apiKey: 'key_123', + headerName: 'Authorization', + }, + }, + }, + query: { + endpoint: '', + method: 'POST' as const, + parameters: { + query: '{ users { id name email } }', + }, + }, + fieldMappings: [ + { + externalField: 'id', + localField: 'user_id', + type: 'text', + readonly: true, + }, + { + externalField: 'name', + localField: 'user_name', + type: 'text', + readonly: true, + }, + { + externalField: 'email', + localField: 'user_email', + type: 'email', + readonly: true, + }, + ], + caching: { + enabled: true, + ttl: 180, + strategy: 'lru' as const, + }, + }; + + expect(() => ExternalLookupSchema.parse(graphqlLookup)).not.toThrow(); + }); + + it('should validate complete Salesforce-like external lookup', () => { + const salesforceLookup: ExternalLookup = { + fieldName: 'salesforce_contacts', + dataSource: { + id: 'salesforce-prod', + name: 'Salesforce Production', + type: 'rest-api', + endpoint: 'https://na1.salesforce.com/services/data/v58.0', + authentication: { + type: 'oauth2', + config: { + clientId: 'client_id', + clientSecret: 'client_secret', + tokenUrl: 'https://login.salesforce.com/services/oauth2/token', + scope: 'api', + }, + }, + }, + query: { + endpoint: '/query', + method: 'GET', + parameters: { + q: 'SELECT Id, Name, Email, Phone FROM Contact WHERE IsActive = true LIMIT 1000', + }, + }, + fieldMappings: [ + { + externalField: 'Id', + localField: 'salesforce_id', + type: 'text', + readonly: true, + }, + { + externalField: 'Name', + localField: 'contact_name', + type: 'text', + readonly: true, + }, + { + externalField: 'Email', + localField: 'email', + type: 'email', + readonly: true, + }, + { + externalField: 'Phone', + localField: 'phone', + type: 'phone', + readonly: true, + }, + ], + caching: { + enabled: true, + ttl: 600, + strategy: 'ttl', + }, + fallback: { + enabled: true, + defaultValue: null, + showError: true, + }, + rateLimit: { + requestsPerSecond: 5, + burstSize: 15, + }, + }; + + expect(() => ExternalLookupSchema.parse(salesforceLookup)).not.toThrow(); + }); +}); diff --git a/packages/spec/src/data/external-lookup.zod.ts b/packages/spec/src/data/external-lookup.zod.ts new file mode 100644 index 000000000..68557d3bf --- /dev/null +++ b/packages/spec/src/data/external-lookup.zod.ts @@ -0,0 +1,255 @@ +import { z } from 'zod'; + +/** + * External Data Source Schema + * + * Configuration for connecting to external data systems. + * Similar to Salesforce External Objects for real-time data integration. + * + * @example + * ```json + * { + * "id": "salesforce-accounts", + * "name": "Salesforce Account Data", + * "type": "rest-api", + * "endpoint": "https://api.salesforce.com/services/data/v58.0", + * "authentication": { + * "type": "oauth2", + * "config": { + * "clientId": "...", + * "clientSecret": "...", + * "tokenUrl": "https://login.salesforce.com/services/oauth2/token" + * } + * } + * } + * ``` + */ +export const ExternalDataSourceSchema = z.object({ + /** + * Unique identifier for the external data source + */ + id: z.string().describe('Data source ID'), + + /** + * Human-readable name of the data source + */ + name: z.string().describe('Data source name'), + + /** + * Protocol type for connecting to the data source + */ + type: z.enum(['odata', 'rest-api', 'graphql', 'custom']).describe('Protocol type'), + + /** + * Base URL endpoint for the external system + */ + endpoint: z.string().url().describe('API endpoint URL'), + + /** + * Authentication configuration + */ + authentication: z.object({ + /** + * Authentication method + */ + type: z.enum(['oauth2', 'api-key', 'basic', 'none']).describe('Auth type'), + + /** + * Authentication-specific configuration + * Structure varies based on auth type + */ + config: z.record(z.any()).describe('Auth configuration'), + }).describe('Authentication'), +}); + +/** + * Field Mapping Schema + * + * Maps external system fields to local object fields. + * Defines data type and read/write permissions. + * + * @example + * ```json + * { + * "externalField": "AccountName", + * "localField": "name", + * "type": "text", + * "readonly": true + * } + * ``` + */ +export const FieldMappingSchema = z.object({ + /** + * Field name in the external system + */ + externalField: z.string().describe('External field name'), + + /** + * Corresponding local field name (snake_case) + */ + localField: z.string().describe('Local field name'), + + /** + * Data type of the field + */ + type: z.string().describe('Field type'), + + /** + * Whether the field is read-only + * @default true + */ + readonly: z.boolean().optional().default(true).describe('Read-only field'), +}); + +/** + * External Lookup Schema + * + * Real-time data lookup protocol for external systems. + * Enables querying external data sources without replication. + * Inspired by Salesforce External Objects and OData protocols. + * + * @example + * ```json + * { + * "fieldName": "external_account", + * "dataSource": { + * "id": "salesforce-api", + * "name": "Salesforce", + * "type": "rest-api", + * "endpoint": "https://api.salesforce.com/services/data/v58.0", + * "authentication": { + * "type": "oauth2", + * "config": {"clientId": "..."} + * } + * }, + * "query": { + * "endpoint": "/sobjects/Account", + * "method": "GET", + * "parameters": {"limit": 100} + * }, + * "fieldMappings": [ + * { + * "externalField": "Name", + * "localField": "account_name", + * "type": "text", + * "readonly": true + * } + * ], + * "caching": { + * "enabled": true, + * "ttl": 300, + * "strategy": "ttl" + * }, + * "fallback": { + * "enabled": true, + * "showError": true + * }, + * "rateLimit": { + * "requestsPerSecond": 10, + * "burstSize": 20 + * } + * } + * ``` + */ +export const ExternalLookupSchema = z.object({ + /** + * Name of the field that uses external lookup + */ + fieldName: z.string().describe('Field name'), + + /** + * External data source configuration + */ + dataSource: ExternalDataSourceSchema.describe('External data source'), + + /** + * Query configuration for fetching external data + */ + query: z.object({ + /** + * API endpoint path (relative to base endpoint) + */ + endpoint: z.string().describe('Query endpoint path'), + + /** + * HTTP method for the query + * @default 'GET' + */ + method: z.enum(['GET', 'POST']).optional().default('GET').describe('HTTP method'), + + /** + * Query parameters or request body + */ + parameters: z.record(z.any()).optional().describe('Query parameters'), + }).describe('Query configuration'), + + /** + * Mapping between external and local fields + */ + fieldMappings: z.array(FieldMappingSchema).describe('Field mappings'), + + /** + * Cache configuration for external data + */ + caching: z.object({ + /** + * Whether caching is enabled + * @default true + */ + enabled: z.boolean().optional().default(true).describe('Cache enabled'), + + /** + * Time-to-live in seconds + * @default 300 + */ + ttl: z.number().optional().default(300).describe('Cache TTL (seconds)'), + + /** + * Cache eviction strategy + * @default 'ttl' + */ + strategy: z.enum(['lru', 'lfu', 'ttl']).optional().default('ttl').describe('Cache strategy'), + }).optional().describe('Caching configuration'), + + /** + * Fallback behavior when external system is unavailable + */ + fallback: z.object({ + /** + * Whether fallback is enabled + * @default true + */ + enabled: z.boolean().optional().default(true).describe('Fallback enabled'), + + /** + * Default value to use when external system fails + */ + defaultValue: z.any().optional().describe('Default fallback value'), + + /** + * Whether to show error message to user + * @default true + */ + showError: z.boolean().optional().default(true).describe('Show error to user'), + }).optional().describe('Fallback configuration'), + + /** + * Rate limiting to prevent overwhelming external system + */ + rateLimit: z.object({ + /** + * Maximum requests per second + */ + requestsPerSecond: z.number().describe('Requests per second limit'), + + /** + * Burst size for handling spikes + */ + burstSize: z.number().optional().describe('Burst size'), + }).optional().describe('Rate limiting'), +}); + +// Type exports +export type ExternalLookup = z.infer; +export type ExternalDataSource = z.infer; +export type FieldMapping = z.infer; diff --git a/packages/spec/src/data/index.ts b/packages/spec/src/data/index.ts index 25ef5bbb0..9c356d1fa 100644 --- a/packages/spec/src/data/index.ts +++ b/packages/spec/src/data/index.ts @@ -5,4 +5,10 @@ export * from './field.zod'; export * from './validation.zod'; export * from './hook.zod'; -export * from './dataset.zod'; \ No newline at end of file +export * from './dataset.zod'; + +// Document Management Protocol +export * from './document.zod'; + +// External Lookup Protocol +export * from './external-lookup.zod'; \ No newline at end of file diff --git a/packages/spec/src/system/change-management.test.ts b/packages/spec/src/system/change-management.test.ts new file mode 100644 index 000000000..0882e2a51 --- /dev/null +++ b/packages/spec/src/system/change-management.test.ts @@ -0,0 +1,656 @@ +import { describe, it, expect } from 'vitest'; +import { + ChangeTypeSchema, + ChangePrioritySchema, + ChangeStatusSchema, + ChangeImpactSchema, + RollbackPlanSchema, + ChangeRequestSchema, + type ChangeRequest, + type ChangeImpact, + type RollbackPlan, +} from './change-management.zod'; + +describe('ChangeTypeSchema', () => { + it('should accept all valid change types', () => { + const validTypes = ['standard', 'normal', 'emergency', 'major']; + + validTypes.forEach((type) => { + expect(() => ChangeTypeSchema.parse(type)).not.toThrow(); + }); + }); + + it('should reject invalid change type', () => { + expect(() => ChangeTypeSchema.parse('invalid')).toThrow(); + }); +}); + +describe('ChangePrioritySchema', () => { + it('should accept all valid priorities', () => { + const validPriorities = ['critical', 'high', 'medium', 'low']; + + validPriorities.forEach((priority) => { + expect(() => ChangePrioritySchema.parse(priority)).not.toThrow(); + }); + }); + + it('should reject invalid priority', () => { + expect(() => ChangePrioritySchema.parse('urgent')).toThrow(); + }); +}); + +describe('ChangeStatusSchema', () => { + it('should accept all valid statuses', () => { + const validStatuses = [ + 'draft', + 'submitted', + 'in-review', + 'approved', + 'scheduled', + 'in-progress', + 'completed', + 'failed', + 'rolled-back', + 'cancelled', + ]; + + validStatuses.forEach((status) => { + expect(() => ChangeStatusSchema.parse(status)).not.toThrow(); + }); + }); + + it('should reject invalid status', () => { + expect(() => ChangeStatusSchema.parse('pending')).toThrow(); + }); +}); + +describe('ChangeImpactSchema', () => { + it('should validate complete impact assessment', () => { + const validImpact: ChangeImpact = { + level: 'high', + affectedSystems: ['crm-api', 'customer-portal'], + affectedUsers: 5000, + downtime: { + required: true, + durationMinutes: 30, + }, + }; + + expect(() => ChangeImpactSchema.parse(validImpact)).not.toThrow(); + }); + + it('should accept minimal impact assessment', () => { + const minimalImpact = { + level: 'low', + affectedSystems: ['test-system'], + }; + + expect(() => ChangeImpactSchema.parse(minimalImpact)).not.toThrow(); + }); + + it('should accept all impact levels', () => { + const levels = ['low', 'medium', 'high', 'critical'] as const; + + levels.forEach((level) => { + const impact = { + level, + affectedSystems: ['system-1'], + }; + + expect(() => ChangeImpactSchema.parse(impact)).not.toThrow(); + }); + }); + + it('should validate downtime configuration', () => { + const impact = { + level: 'medium', + affectedSystems: ['api-gateway'], + downtime: { + required: false, + }, + }; + + expect(() => ChangeImpactSchema.parse(impact)).not.toThrow(); + }); + + it('should reject invalid impact level', () => { + const invalidImpact = { + level: 'severe', + affectedSystems: ['system'], + }; + + expect(() => ChangeImpactSchema.parse(invalidImpact)).toThrow(); + }); +}); + +describe('RollbackPlanSchema', () => { + it('should validate complete rollback plan', () => { + const validPlan: RollbackPlan = { + description: 'Revert database schema to previous version', + steps: [ + { + order: 1, + description: 'Stop application servers', + estimatedMinutes: 5, + }, + { + order: 2, + description: 'Restore database backup', + estimatedMinutes: 15, + }, + { + order: 3, + description: 'Restart application servers', + estimatedMinutes: 5, + }, + ], + testProcedure: 'Verify application login and basic functionality', + }; + + expect(() => RollbackPlanSchema.parse(validPlan)).not.toThrow(); + }); + + it('should accept rollback plan without test procedure', () => { + const planWithoutTest = { + description: 'Simple rollback', + steps: [ + { + order: 1, + description: 'Revert changes', + estimatedMinutes: 10, + }, + ], + }; + + expect(() => RollbackPlanSchema.parse(planWithoutTest)).not.toThrow(); + }); + + it('should validate multiple rollback steps', () => { + const plan = { + description: 'Multi-step rollback', + steps: [ + { + order: 1, + description: 'Step 1', + estimatedMinutes: 5, + }, + { + order: 2, + description: 'Step 2', + estimatedMinutes: 10, + }, + { + order: 3, + description: 'Step 3', + estimatedMinutes: 15, + }, + { + order: 4, + description: 'Step 4', + estimatedMinutes: 20, + }, + ], + }; + + expect(() => RollbackPlanSchema.parse(plan)).not.toThrow(); + }); +}); + +describe('ChangeRequestSchema', () => { + it('should validate complete change request', () => { + const validRequest: ChangeRequest = { + id: 'CHG-2024-001', + title: 'Upgrade CRM Database Schema', + description: 'Migrate customer database to new schema version 2.0', + type: 'normal', + priority: 'high', + status: 'approved', + requestedBy: 'user_123', + requestedAt: 1704067200000, + impact: { + level: 'high', + affectedSystems: ['crm-api', 'customer-portal'], + affectedUsers: 5000, + downtime: { + required: true, + durationMinutes: 30, + }, + }, + implementation: { + description: 'Execute database migration scripts', + steps: [ + { + order: 1, + description: 'Backup current database', + estimatedMinutes: 10, + }, + { + order: 2, + description: 'Run migration scripts', + estimatedMinutes: 15, + }, + ], + testing: 'Run integration test suite', + }, + rollbackPlan: { + description: 'Restore from backup', + steps: [ + { + order: 1, + description: 'Restore backup', + estimatedMinutes: 15, + }, + ], + }, + schedule: { + plannedStart: 1704153600000, + plannedEnd: 1704155400000, + }, + }; + + expect(() => ChangeRequestSchema.parse(validRequest)).not.toThrow(); + }); + + it('should accept minimal change request', () => { + const minimalRequest = { + id: 'CHG-2024-002', + title: 'Simple Change', + description: 'A simple change', + type: 'standard', + priority: 'low', + status: 'draft', + requestedBy: 'user_456', + requestedAt: Date.now(), + impact: { + level: 'low', + affectedSystems: ['test-system'], + }, + implementation: { + description: 'Make the change', + steps: [ + { + order: 1, + description: 'Execute change', + estimatedMinutes: 5, + }, + ], + }, + rollbackPlan: { + description: 'Undo the change', + steps: [ + { + order: 1, + description: 'Revert', + estimatedMinutes: 5, + }, + ], + }, + }; + + expect(() => ChangeRequestSchema.parse(minimalRequest)).not.toThrow(); + }); + + it('should validate standard change type', () => { + const standardChange = { + id: 'CHG-STD-001', + title: 'Standard Change', + description: 'Pre-approved standard change', + type: 'standard', + priority: 'low', + status: 'approved', + requestedBy: 'user_789', + requestedAt: Date.now(), + impact: { + level: 'low', + affectedSystems: ['component-a'], + }, + implementation: { + description: 'Standard procedure', + steps: [ + { + order: 1, + description: 'Execute', + estimatedMinutes: 10, + }, + ], + }, + rollbackPlan: { + description: 'Standard rollback', + steps: [ + { + order: 1, + description: 'Revert', + estimatedMinutes: 5, + }, + ], + }, + }; + + expect(() => ChangeRequestSchema.parse(standardChange)).not.toThrow(); + }); + + it('should validate emergency change type', () => { + const emergencyChange = { + id: 'CHG-EMG-001', + title: 'Emergency Security Patch', + description: 'Critical security vulnerability fix', + type: 'emergency', + priority: 'critical', + status: 'in-progress', + requestedBy: 'security_team', + requestedAt: Date.now(), + impact: { + level: 'critical', + affectedSystems: ['all-systems'], + affectedUsers: 50000, + downtime: { + required: true, + durationMinutes: 15, + }, + }, + implementation: { + description: 'Apply security patch', + steps: [ + { + order: 1, + description: 'Deploy patch', + estimatedMinutes: 10, + }, + ], + }, + rollbackPlan: { + description: 'Remove patch', + steps: [ + { + order: 1, + description: 'Rollback', + estimatedMinutes: 5, + }, + ], + }, + }; + + expect(() => ChangeRequestSchema.parse(emergencyChange)).not.toThrow(); + }); + + it('should validate major change requiring CAB approval', () => { + const majorChange = { + id: 'CHG-MAJ-001', + title: 'Major Infrastructure Upgrade', + description: 'Upgrade core infrastructure', + type: 'major', + priority: 'high', + status: 'in-review', + requestedBy: 'infrastructure_team', + requestedAt: Date.now(), + impact: { + level: 'critical', + affectedSystems: ['core-infrastructure', 'all-applications'], + affectedUsers: 100000, + downtime: { + required: true, + durationMinutes: 120, + }, + }, + implementation: { + description: 'Multi-phase infrastructure upgrade', + steps: [ + { + order: 1, + description: 'Phase 1: Database upgrade', + estimatedMinutes: 30, + }, + { + order: 2, + description: 'Phase 2: Application upgrade', + estimatedMinutes: 45, + }, + ], + testing: 'Comprehensive integration testing', + }, + rollbackPlan: { + description: 'Restore from snapshots', + steps: [ + { + order: 1, + description: 'Restore infrastructure snapshot', + estimatedMinutes: 60, + }, + ], + }, + approval: { + required: true, + approvers: [ + { + userId: 'cab_member_1', + approvedAt: 1704067200000, + comments: 'Approved with conditions', + }, + { + userId: 'cab_member_2', + }, + ], + }, + }; + + expect(() => ChangeRequestSchema.parse(majorChange)).not.toThrow(); + }); + + it('should validate schedule with actual times', () => { + const scheduledChange = { + id: 'CHG-2024-003', + title: 'Scheduled Maintenance', + description: 'Routine maintenance', + type: 'normal', + priority: 'medium', + status: 'completed', + requestedBy: 'ops_team', + requestedAt: Date.now(), + impact: { + level: 'medium', + affectedSystems: ['web-servers'], + }, + implementation: { + description: 'Update web servers', + steps: [ + { + order: 1, + description: 'Update', + estimatedMinutes: 20, + }, + ], + }, + rollbackPlan: { + description: 'Rollback update', + steps: [ + { + order: 1, + description: 'Revert', + estimatedMinutes: 10, + }, + ], + }, + schedule: { + plannedStart: 1704153600000, + plannedEnd: 1704155400000, + actualStart: 1704153650000, + actualEnd: 1704155350000, + }, + }; + + expect(() => ChangeRequestSchema.parse(scheduledChange)).not.toThrow(); + }); + + it('should validate attachments', () => { + const changeWithAttachments = { + id: 'CHG-2024-004', + title: 'Change with Documentation', + description: 'Well-documented change', + type: 'normal', + priority: 'medium', + status: 'submitted', + requestedBy: 'user_123', + requestedAt: Date.now(), + impact: { + level: 'medium', + affectedSystems: ['api'], + }, + implementation: { + description: 'API update', + steps: [ + { + order: 1, + description: 'Deploy', + estimatedMinutes: 15, + }, + ], + }, + rollbackPlan: { + description: 'Rollback', + steps: [ + { + order: 1, + description: 'Revert', + estimatedMinutes: 10, + }, + ], + }, + attachments: [ + { + name: 'implementation-plan.pdf', + url: 'https://example.com/docs/plan.pdf', + }, + { + name: 'architecture-diagram.png', + url: 'https://example.com/diagrams/arch.png', + }, + ], + }; + + expect(() => ChangeRequestSchema.parse(changeWithAttachments)).not.toThrow(); + }); + + it('should validate attachment URLs', () => { + const invalidChange = { + id: 'CHG-2024-005', + title: 'Invalid Attachment', + description: 'Change with invalid attachment URL', + type: 'normal', + priority: 'low', + status: 'draft', + requestedBy: 'user_123', + requestedAt: Date.now(), + impact: { + level: 'low', + affectedSystems: ['test'], + }, + implementation: { + description: 'Test', + steps: [ + { + order: 1, + description: 'Test', + estimatedMinutes: 5, + }, + ], + }, + rollbackPlan: { + description: 'Test', + steps: [ + { + order: 1, + description: 'Test', + estimatedMinutes: 5, + }, + ], + }, + attachments: [ + { + name: 'document.pdf', + url: 'not-a-valid-url', + }, + ], + }; + + expect(() => ChangeRequestSchema.parse(invalidChange)).toThrow(); + }); + + it('should validate failed change status', () => { + const failedChange = { + id: 'CHG-2024-006', + title: 'Failed Change', + description: 'Change that failed', + type: 'normal', + priority: 'high', + status: 'failed', + requestedBy: 'user_123', + requestedAt: Date.now(), + impact: { + level: 'high', + affectedSystems: ['database'], + }, + implementation: { + description: 'Database update', + steps: [ + { + order: 1, + description: 'Update schema', + estimatedMinutes: 20, + }, + ], + }, + rollbackPlan: { + description: 'Restore backup', + steps: [ + { + order: 1, + description: 'Restore', + estimatedMinutes: 15, + }, + ], + }, + }; + + expect(() => ChangeRequestSchema.parse(failedChange)).not.toThrow(); + }); + + it('should validate rolled-back change status', () => { + const rolledBackChange = { + id: 'CHG-2024-007', + title: 'Rolled Back Change', + description: 'Change that was rolled back', + type: 'normal', + priority: 'high', + status: 'rolled-back', + requestedBy: 'user_123', + requestedAt: Date.now(), + impact: { + level: 'high', + affectedSystems: ['application'], + }, + implementation: { + description: 'App update', + steps: [ + { + order: 1, + description: 'Deploy', + estimatedMinutes: 15, + }, + ], + }, + rollbackPlan: { + description: 'Revert deployment', + steps: [ + { + order: 1, + description: 'Rollback', + estimatedMinutes: 10, + }, + ], + testProcedure: 'Verify app functionality', + }, + }; + + expect(() => ChangeRequestSchema.parse(rolledBackChange)).not.toThrow(); + }); +}); diff --git a/packages/spec/src/system/change-management.zod.ts b/packages/spec/src/system/change-management.zod.ts new file mode 100644 index 000000000..08e641aaa --- /dev/null +++ b/packages/spec/src/system/change-management.zod.ts @@ -0,0 +1,372 @@ +import { z } from 'zod'; + +/** + * Change Type Enum + * + * Classification of change requests based on risk and approval requirements. + * Follows ITIL change management best practices. + */ +export const ChangeTypeSchema = z.enum([ + 'standard', // Pre-approved, low-risk changes + 'normal', // Requires standard approval process + 'emergency', // Fast-track approval for critical issues + 'major', // Requires CAB (Change Advisory Board) approval +]); + +/** + * Change Priority Enum + * + * Priority level for change request processing. + */ +export const ChangePrioritySchema = z.enum([ + 'critical', + 'high', + 'medium', + 'low', +]); + +/** + * Change Status Enum + * + * Current status of a change request in its lifecycle. + */ +export const ChangeStatusSchema = z.enum([ + 'draft', + 'submitted', + 'in-review', + 'approved', + 'scheduled', + 'in-progress', + 'completed', + 'failed', + 'rolled-back', + 'cancelled', +]); + +/** + * Change Impact Schema + * + * Assessment of the impact and scope of a change request. + * Used for risk evaluation and approval routing. + * + * @example + * ```json + * { + * "level": "high", + * "affectedSystems": ["crm-api", "customer-portal"], + * "affectedUsers": 5000, + * "downtime": { + * "required": true, + * "durationMinutes": 30 + * } + * } + * ``` + */ +export const ChangeImpactSchema = z.object({ + /** + * Overall impact level of the change + */ + level: z.enum(['low', 'medium', 'high', 'critical']).describe('Impact level'), + + /** + * List of systems affected by this change + */ + affectedSystems: z.array(z.string()).describe('Affected systems'), + + /** + * Estimated number of users affected + */ + affectedUsers: z.number().optional().describe('Affected user count'), + + /** + * Downtime requirements + */ + downtime: z.object({ + /** + * Whether downtime is required + */ + required: z.boolean().describe('Downtime required'), + + /** + * Duration of downtime in minutes + */ + durationMinutes: z.number().optional().describe('Downtime duration'), + }).optional().describe('Downtime information'), +}); + +/** + * Rollback Plan Schema + * + * Detailed procedure for reverting changes if implementation fails. + * Required for all non-standard changes. + * + * @example + * ```json + * { + * "description": "Revert database schema to previous version", + * "steps": [ + * { + * "order": 1, + * "description": "Stop application servers", + * "estimatedMinutes": 5 + * }, + * { + * "order": 2, + * "description": "Restore database backup", + * "estimatedMinutes": 15 + * } + * ], + * "testProcedure": "Verify application login and basic functionality" + * } + * ``` + */ +export const RollbackPlanSchema = z.object({ + /** + * High-level description of the rollback approach + */ + description: z.string().describe('Rollback description'), + + /** + * Sequential steps to execute rollback + */ + steps: z.array(z.object({ + /** + * Step execution order + */ + order: z.number().describe('Step order'), + + /** + * Detailed description of this step + */ + description: z.string().describe('Step description'), + + /** + * Estimated time to complete this step + */ + estimatedMinutes: z.number().describe('Estimated duration'), + })).describe('Rollback steps'), + + /** + * Testing procedure to verify successful rollback + */ + testProcedure: z.string().optional().describe('Test procedure'), +}); + +/** + * Change Request Schema + * + * Comprehensive change management protocol for IT governance. + * Supports change requests, deployment tracking, and ITIL compliance. + * + * @example + * ```json + * { + * "id": "CHG-2024-001", + * "title": "Upgrade CRM Database Schema", + * "description": "Migrate customer database to new schema version 2.0", + * "type": "normal", + * "priority": "high", + * "status": "approved", + * "requestedBy": "user_123", + * "requestedAt": 1704067200000, + * "impact": { + * "level": "high", + * "affectedSystems": ["crm-api", "customer-portal"], + * "affectedUsers": 5000, + * "downtime": { + * "required": true, + * "durationMinutes": 30 + * } + * }, + * "implementation": { + * "description": "Execute database migration scripts", + * "steps": [ + * { + * "order": 1, + * "description": "Backup current database", + * "estimatedMinutes": 10 + * } + * ], + * "testing": "Run integration test suite" + * }, + * "rollbackPlan": { + * "description": "Restore from backup", + * "steps": [ + * { + * "order": 1, + * "description": "Restore backup", + * "estimatedMinutes": 15 + * } + * ] + * }, + * "schedule": { + * "plannedStart": 1704153600000, + * "plannedEnd": 1704155400000 + * } + * } + * ``` + */ +export const ChangeRequestSchema = z.object({ + /** + * Unique change request identifier + */ + id: z.string().describe('Change request ID'), + + /** + * Short descriptive title of the change + */ + title: z.string().describe('Change title'), + + /** + * Detailed description of the change and its purpose + */ + description: z.string().describe('Change description'), + + /** + * Change classification type + */ + type: ChangeTypeSchema.describe('Change type'), + + /** + * Priority level for processing + */ + priority: ChangePrioritySchema.describe('Change priority'), + + /** + * Current status in the change lifecycle + */ + status: ChangeStatusSchema.describe('Change status'), + + /** + * User ID of the change requester + */ + requestedBy: z.string().describe('Requester user ID'), + + /** + * Timestamp when change was requested (Unix milliseconds) + */ + requestedAt: z.number().describe('Request timestamp'), + + /** + * Impact assessment of the change + */ + impact: ChangeImpactSchema.describe('Impact assessment'), + + /** + * Implementation plan and procedures + */ + implementation: z.object({ + /** + * High-level implementation description + */ + description: z.string().describe('Implementation description'), + + /** + * Sequential implementation steps + */ + steps: z.array(z.object({ + /** + * Step execution order + */ + order: z.number().describe('Step order'), + + /** + * Detailed description of this step + */ + description: z.string().describe('Step description'), + + /** + * Estimated time to complete this step + */ + estimatedMinutes: z.number().describe('Estimated duration'), + })).describe('Implementation steps'), + + /** + * Testing procedures to verify successful implementation + */ + testing: z.string().optional().describe('Testing procedure'), + }).describe('Implementation plan'), + + /** + * Rollback plan in case of failure + */ + rollbackPlan: RollbackPlanSchema.describe('Rollback plan'), + + /** + * Change schedule and timing + */ + schedule: z.object({ + /** + * Planned start time (Unix milliseconds) + */ + plannedStart: z.number().describe('Planned start time'), + + /** + * Planned end time (Unix milliseconds) + */ + plannedEnd: z.number().describe('Planned end time'), + + /** + * Actual start time (Unix milliseconds) + */ + actualStart: z.number().optional().describe('Actual start time'), + + /** + * Actual end time (Unix milliseconds) + */ + actualEnd: z.number().optional().describe('Actual end time'), + }).optional().describe('Schedule'), + + /** + * Approval workflow configuration + */ + approval: z.object({ + /** + * Whether approval is required for this change + */ + required: z.boolean().describe('Approval required'), + + /** + * List of approvers and their approval status + */ + approvers: z.array(z.object({ + /** + * Approver user ID + */ + userId: z.string().describe('Approver user ID'), + + /** + * Timestamp when approval was granted (Unix milliseconds) + */ + approvedAt: z.number().optional().describe('Approval timestamp'), + + /** + * Comments from the approver + */ + comments: z.string().optional().describe('Approver comments'), + })).describe('Approvers'), + }).optional().describe('Approval workflow'), + + /** + * Supporting documentation and files + */ + attachments: z.array(z.object({ + /** + * Attachment file name + */ + name: z.string().describe('Attachment name'), + + /** + * URL to download the attachment + */ + url: z.string().url().describe('Attachment URL'), + })).optional().describe('Attachments'), +}); + +// Type exports +export type ChangeRequest = z.infer; +export type ChangeType = z.infer; +export type ChangeStatus = z.infer; +export type ChangePriority = z.infer; +export type ChangeImpact = z.infer; +export type RollbackPlan = z.infer; diff --git a/packages/spec/src/system/index.ts b/packages/spec/src/system/index.ts index 118bc9b6a..789435139 100644 --- a/packages/spec/src/system/index.ts +++ b/packages/spec/src/system/index.ts @@ -56,6 +56,12 @@ export * from './encryption.zod'; export * from './compliance.zod'; export * from './masking.zod'; +// Notification Protocol +export * from './notification.zod'; + +// Change Management Protocol +export * from './change-management.zod'; + // Note: Auth, Identity, Policy, Role, Organization moved to @objectstack/spec/auth // Note: Territory moved to @objectstack/spec/permission // Note: Connector Protocol moved to @objectstack/spec/integration diff --git a/packages/spec/src/system/notification.test.ts b/packages/spec/src/system/notification.test.ts new file mode 100644 index 000000000..3f1abf63a --- /dev/null +++ b/packages/spec/src/system/notification.test.ts @@ -0,0 +1,560 @@ +import { describe, it, expect } from 'vitest'; +import { + EmailTemplateSchema, + SMSTemplateSchema, + PushNotificationSchema, + InAppNotificationSchema, + NotificationChannelSchema, + NotificationConfigSchema, + type NotificationConfig, + type EmailTemplate, + type SMSTemplate, +} from './notification.zod'; + +describe('EmailTemplateSchema', () => { + it('should validate complete email template', () => { + const validTemplate: EmailTemplate = { + id: 'welcome-email', + subject: 'Welcome to {{company_name}}', + body: '

Welcome {{user_name}}!

', + bodyType: 'html', + variables: ['company_name', 'user_name'], + attachments: [ + { + name: 'guide.pdf', + url: 'https://example.com/guide.pdf', + }, + ], + }; + + expect(() => EmailTemplateSchema.parse(validTemplate)).not.toThrow(); + }); + + it('should accept minimal email template', () => { + const minimalTemplate = { + id: 'simple-email', + subject: 'Test Email', + body: 'Simple text body', + }; + + expect(() => EmailTemplateSchema.parse(minimalTemplate)).not.toThrow(); + }); + + it('should default bodyType to html', () => { + const template = { + id: 'test', + subject: 'Test', + body: 'Body', + }; + + const parsed = EmailTemplateSchema.parse(template); + expect(parsed.bodyType).toBe('html'); + }); + + it('should accept text bodyType', () => { + const template = { + id: 'text-email', + subject: 'Plain Text', + body: 'Plain text body', + bodyType: 'text' as const, + }; + + expect(() => EmailTemplateSchema.parse(template)).not.toThrow(); + }); + + it('should accept markdown bodyType', () => { + const template = { + id: 'markdown-email', + subject: 'Markdown Email', + body: '# Header\n\nContent', + bodyType: 'markdown' as const, + }; + + expect(() => EmailTemplateSchema.parse(template)).not.toThrow(); + }); + + it('should validate attachment URLs', () => { + const invalidTemplate = { + id: 'email-1', + subject: 'Test', + body: 'Body', + attachments: [ + { + name: 'file.pdf', + url: 'not-a-url', + }, + ], + }; + + expect(() => EmailTemplateSchema.parse(invalidTemplate)).toThrow(); + }); +}); + +describe('SMSTemplateSchema', () => { + it('should validate complete SMS template', () => { + const validTemplate: SMSTemplate = { + id: 'verification-sms', + message: 'Your verification code is {{code}}', + maxLength: 160, + variables: ['code'], + }; + + expect(() => SMSTemplateSchema.parse(validTemplate)).not.toThrow(); + }); + + it('should accept minimal SMS template', () => { + const minimalTemplate = { + id: 'simple-sms', + message: 'Hello World', + }; + + expect(() => SMSTemplateSchema.parse(minimalTemplate)).not.toThrow(); + }); + + it('should default maxLength to 160', () => { + const template = { + id: 'sms-1', + message: 'Test message', + }; + + const parsed = SMSTemplateSchema.parse(template); + expect(parsed.maxLength).toBe(160); + }); + + it('should accept custom maxLength', () => { + const template = { + id: 'long-sms', + message: 'Long message', + maxLength: 320, + }; + + const parsed = SMSTemplateSchema.parse(template); + expect(parsed.maxLength).toBe(320); + }); +}); + +describe('PushNotificationSchema', () => { + it('should validate complete push notification', () => { + const validPush = { + title: 'New Message', + body: 'You have a new message from John', + icon: 'https://example.com/icon.png', + badge: 5, + data: { messageId: 'msg_123' }, + actions: [ + { action: 'view', title: 'View' }, + { action: 'dismiss', title: 'Dismiss' }, + ], + }; + + expect(() => PushNotificationSchema.parse(validPush)).not.toThrow(); + }); + + it('should accept minimal push notification', () => { + const minimalPush = { + title: 'Alert', + body: 'Something happened', + }; + + expect(() => PushNotificationSchema.parse(minimalPush)).not.toThrow(); + }); + + it('should validate icon URL', () => { + const invalidPush = { + title: 'Test', + body: 'Body', + icon: 'not-a-url', + }; + + expect(() => PushNotificationSchema.parse(invalidPush)).toThrow(); + }); + + it('should accept custom data payload', () => { + const push = { + title: 'Order Update', + body: 'Your order has shipped', + data: { + orderId: 'ord_123', + trackingNumber: 'TRK456', + status: 'shipped', + }, + }; + + expect(() => PushNotificationSchema.parse(push)).not.toThrow(); + }); +}); + +describe('InAppNotificationSchema', () => { + it('should validate complete in-app notification', () => { + const validNotification = { + title: 'System Update', + message: 'New features are now available', + type: 'info' as const, + actionUrl: '/updates', + dismissible: true, + expiresAt: 1704067200000, + }; + + expect(() => InAppNotificationSchema.parse(validNotification)).not.toThrow(); + }); + + it('should accept minimal in-app notification', () => { + const minimalNotification = { + title: 'Alert', + message: 'Important message', + type: 'warning' as const, + }; + + expect(() => InAppNotificationSchema.parse(minimalNotification)).not.toThrow(); + }); + + it('should default dismissible to true', () => { + const notification = { + title: 'Test', + message: 'Message', + type: 'info' as const, + }; + + const parsed = InAppNotificationSchema.parse(notification); + expect(parsed.dismissible).toBe(true); + }); + + it('should accept all notification types', () => { + const types = ['info', 'success', 'warning', 'error'] as const; + + types.forEach((type) => { + const notification = { + title: 'Test', + message: 'Message', + type, + }; + + expect(() => InAppNotificationSchema.parse(notification)).not.toThrow(); + }); + }); + + it('should reject invalid notification type', () => { + const invalidNotification = { + title: 'Test', + message: 'Message', + type: 'invalid', + }; + + expect(() => InAppNotificationSchema.parse(invalidNotification)).toThrow(); + }); +}); + +describe('NotificationChannelSchema', () => { + it('should accept all valid channels', () => { + const validChannels = [ + 'email', + 'sms', + 'push', + 'in-app', + 'slack', + 'teams', + 'webhook', + ]; + + validChannels.forEach((channel) => { + expect(() => NotificationChannelSchema.parse(channel)).not.toThrow(); + }); + }); + + it('should reject invalid channel', () => { + expect(() => NotificationChannelSchema.parse('invalid')).toThrow(); + }); +}); + +describe('NotificationConfigSchema', () => { + it('should validate email notification config', () => { + const validConfig: NotificationConfig = { + id: 'welcome-email', + name: 'Welcome Email', + channel: 'email', + template: { + id: 'tpl-001', + subject: 'Welcome to ObjectStack', + body: '

Welcome!

', + bodyType: 'html', + }, + recipients: { + to: ['user@example.com'], + }, + }; + + expect(() => NotificationConfigSchema.parse(validConfig)).not.toThrow(); + }); + + it('should validate SMS notification config', () => { + const validConfig = { + id: 'verification-sms', + name: 'Verification SMS', + channel: 'sms', + template: { + id: 'sms-001', + message: 'Your code is {{code}}', + }, + recipients: { + to: ['+1234567890'], + }, + }; + + expect(() => NotificationConfigSchema.parse(validConfig)).not.toThrow(); + }); + + it('should validate push notification config', () => { + const validConfig = { + id: 'push-alert', + name: 'Push Alert', + channel: 'push', + template: { + title: 'New Message', + body: 'You have a new message', + }, + recipients: { + to: ['device_token_123'], + }, + }; + + expect(() => NotificationConfigSchema.parse(validConfig)).not.toThrow(); + }); + + it('should validate in-app notification config', () => { + const validConfig = { + id: 'system-alert', + name: 'System Alert', + channel: 'in-app', + template: { + title: 'Update Available', + message: 'A new version is available', + type: 'info' as const, + }, + recipients: { + to: ['user_123'], + }, + }; + + expect(() => NotificationConfigSchema.parse(validConfig)).not.toThrow(); + }); + + it('should accept CC and BCC recipients', () => { + const config = { + id: 'email-with-cc', + name: 'Email with CC', + channel: 'email', + template: { + id: 'tpl-002', + subject: 'Test', + body: 'Body', + }, + recipients: { + to: ['user@example.com'], + cc: ['manager@example.com'], + bcc: ['archive@example.com'], + }, + }; + + expect(() => NotificationConfigSchema.parse(config)).not.toThrow(); + }); + + it('should validate immediate schedule', () => { + const config = { + id: 'immediate-notification', + name: 'Immediate', + channel: 'email', + template: { + id: 'tpl-003', + subject: 'Test', + body: 'Body', + }, + recipients: { + to: ['user@example.com'], + }, + schedule: { + type: 'immediate' as const, + }, + }; + + expect(() => NotificationConfigSchema.parse(config)).not.toThrow(); + }); + + it('should validate delayed schedule', () => { + const config = { + id: 'delayed-notification', + name: 'Delayed', + channel: 'email', + template: { + id: 'tpl-004', + subject: 'Test', + body: 'Body', + }, + recipients: { + to: ['user@example.com'], + }, + schedule: { + type: 'delayed' as const, + delay: 3600000, // 1 hour + }, + }; + + expect(() => NotificationConfigSchema.parse(config)).not.toThrow(); + }); + + it('should validate scheduled notification', () => { + const config = { + id: 'scheduled-notification', + name: 'Scheduled', + channel: 'email', + template: { + id: 'tpl-005', + subject: 'Test', + body: 'Body', + }, + recipients: { + to: ['user@example.com'], + }, + schedule: { + type: 'scheduled' as const, + scheduledAt: 1704067200000, + }, + }; + + expect(() => NotificationConfigSchema.parse(config)).not.toThrow(); + }); + + it('should validate retry policy with defaults', () => { + const config = { + id: 'notification-with-retry', + name: 'With Retry', + channel: 'email', + template: { + id: 'tpl-006', + subject: 'Test', + body: 'Body', + }, + recipients: { + to: ['user@example.com'], + }, + retryPolicy: { + backoffStrategy: 'exponential' as const, + }, + }; + + const parsed = NotificationConfigSchema.parse(config); + expect(parsed.retryPolicy?.enabled).toBe(true); + expect(parsed.retryPolicy?.maxRetries).toBe(3); + }); + + it('should validate retry policy backoff strategies', () => { + const strategies = ['exponential', 'linear', 'fixed'] as const; + + strategies.forEach((strategy) => { + const config = { + id: `retry-${strategy}`, + name: 'Test', + channel: 'email', + template: { + id: 'tpl-007', + subject: 'Test', + body: 'Body', + }, + recipients: { + to: ['user@example.com'], + }, + retryPolicy: { + backoffStrategy: strategy, + }, + }; + + expect(() => NotificationConfigSchema.parse(config)).not.toThrow(); + }); + }); + + it('should validate tracking configuration with defaults', () => { + const config = { + id: 'notification-with-tracking', + name: 'With Tracking', + channel: 'email', + template: { + id: 'tpl-008', + subject: 'Test', + body: 'Body', + }, + recipients: { + to: ['user@example.com'], + }, + tracking: {}, + }; + + const parsed = NotificationConfigSchema.parse(config); + expect(parsed.tracking?.trackOpens).toBe(false); + expect(parsed.tracking?.trackClicks).toBe(false); + expect(parsed.tracking?.trackDelivery).toBe(true); + }); + + it('should accept custom tracking configuration', () => { + const config = { + id: 'notification-custom-tracking', + name: 'Custom Tracking', + channel: 'email', + template: { + id: 'tpl-009', + subject: 'Test', + body: 'Body', + }, + recipients: { + to: ['user@example.com'], + }, + tracking: { + trackOpens: true, + trackClicks: true, + trackDelivery: true, + }, + }; + + expect(() => NotificationConfigSchema.parse(config)).not.toThrow(); + }); + + it('should validate complete notification config with all options', () => { + const completeConfig: NotificationConfig = { + id: 'complete-notification', + name: 'Complete Notification', + channel: 'email', + template: { + id: 'tpl-complete', + subject: 'Complete Email {{user_name}}', + body: '{{content}}', + bodyType: 'html', + variables: ['user_name', 'content'], + attachments: [ + { + name: 'report.pdf', + url: 'https://example.com/reports/report.pdf', + }, + ], + }, + recipients: { + to: ['user@example.com', 'admin@example.com'], + cc: ['manager@example.com'], + bcc: ['archive@example.com'], + }, + schedule: { + type: 'scheduled', + scheduledAt: 1704067200000, + }, + retryPolicy: { + enabled: true, + maxRetries: 5, + backoffStrategy: 'exponential', + }, + tracking: { + trackOpens: true, + trackClicks: true, + trackDelivery: true, + }, + }; + + expect(() => NotificationConfigSchema.parse(completeConfig)).not.toThrow(); + }); +}); diff --git a/packages/spec/src/system/notification.zod.ts b/packages/spec/src/system/notification.zod.ts new file mode 100644 index 000000000..e2ed60414 --- /dev/null +++ b/packages/spec/src/system/notification.zod.ts @@ -0,0 +1,379 @@ +import { z } from 'zod'; + +/** + * Email Template Schema + * + * Defines the structure and content of email notifications. + * Supports variables for personalization and file attachments. + * + * @example + * ```json + * { + * "id": "welcome-email", + * "subject": "Welcome to {{company_name}}", + * "body": "

Welcome {{user_name}}!

", + * "bodyType": "html", + * "variables": ["company_name", "user_name"], + * "attachments": [ + * { + * "name": "guide.pdf", + * "url": "https://example.com/guide.pdf" + * } + * ] + * } + * ``` + */ +export const EmailTemplateSchema = z.object({ + /** + * Unique identifier for the email template + */ + id: z.string().describe('Template identifier'), + + /** + * Email subject line (supports variable interpolation) + */ + subject: z.string().describe('Email subject'), + + /** + * Email body content + */ + body: z.string().describe('Email body content'), + + /** + * Content type of the email body + * @default 'html' + */ + bodyType: z.enum(['text', 'html', 'markdown']).optional().default('html').describe('Body content type'), + + /** + * List of template variables for dynamic content + */ + variables: z.array(z.string()).optional().describe('Template variables'), + + /** + * File attachments to include with the email + */ + attachments: z.array(z.object({ + name: z.string().describe('Attachment filename'), + url: z.string().url().describe('Attachment URL'), + })).optional().describe('Email attachments'), +}); + +/** + * SMS Template Schema + * + * Defines the structure of SMS text message notifications. + * Includes character limits and variable support. + * + * @example + * ```json + * { + * "id": "verification-sms", + * "message": "Your code is {{code}}", + * "maxLength": 160, + * "variables": ["code"] + * } + * ``` + */ +export const SMSTemplateSchema = z.object({ + /** + * Unique identifier for the SMS template + */ + id: z.string().describe('Template identifier'), + + /** + * SMS message content (supports variable interpolation) + */ + message: z.string().describe('SMS message content'), + + /** + * Maximum character length for the SMS + * @default 160 + */ + maxLength: z.number().optional().default(160).describe('Maximum message length'), + + /** + * List of template variables for dynamic content + */ + variables: z.array(z.string()).optional().describe('Template variables'), +}); + +/** + * Push Notification Schema + * + * Defines mobile and web push notification structure. + * Supports rich notifications with actions and badges. + * + * @example + * ```json + * { + * "title": "New Message", + * "body": "You have a new message from John", + * "icon": "https://example.com/icon.png", + * "badge": 5, + * "data": {"messageId": "msg_123"}, + * "actions": [ + * {"action": "view", "title": "View"}, + * {"action": "dismiss", "title": "Dismiss"} + * ] + * } + * ``` + */ +export const PushNotificationSchema = z.object({ + /** + * Notification title + */ + title: z.string().describe('Notification title'), + + /** + * Notification body text + */ + body: z.string().describe('Notification body'), + + /** + * Icon URL to display with notification + */ + icon: z.string().url().optional().describe('Notification icon URL'), + + /** + * Badge count to display on app icon + */ + badge: z.number().optional().describe('Badge count'), + + /** + * Custom data payload + */ + data: z.record(z.any()).optional().describe('Custom data'), + + /** + * Action buttons for the notification + */ + actions: z.array(z.object({ + action: z.string().describe('Action identifier'), + title: z.string().describe('Action button title'), + })).optional().describe('Notification actions'), +}); + +/** + * In-App Notification Schema + * + * Defines in-application notification banners and toasts. + * Includes severity levels and auto-dismiss settings. + * + * @example + * ```json + * { + * "title": "System Update", + * "message": "New features are now available", + * "type": "info", + * "actionUrl": "/updates", + * "dismissible": true, + * "expiresAt": 1704067200000 + * } + * ``` + */ +export const InAppNotificationSchema = z.object({ + /** + * Notification title + */ + title: z.string().describe('Notification title'), + + /** + * Notification message content + */ + message: z.string().describe('Notification message'), + + /** + * Notification severity type + */ + type: z.enum(['info', 'success', 'warning', 'error']).describe('Notification type'), + + /** + * Optional URL to navigate to when clicked + */ + actionUrl: z.string().optional().describe('Action URL'), + + /** + * Whether the notification can be dismissed by the user + * @default true + */ + dismissible: z.boolean().optional().default(true).describe('User dismissible'), + + /** + * Timestamp when notification expires (Unix milliseconds) + */ + expiresAt: z.number().optional().describe('Expiration timestamp'), +}); + +/** + * Notification Channel Enum + * + * Supported notification delivery channels. + */ +export const NotificationChannelSchema = z.enum([ + 'email', + 'sms', + 'push', + 'in-app', + 'slack', + 'teams', + 'webhook', +]); + +/** + * Notification Configuration Schema + * + * Unified notification management protocol supporting multiple channels. + * Includes scheduling, retry policies, and delivery tracking. + * + * @example + * ```json + * { + * "id": "welcome-notification", + * "name": "Welcome Email", + * "channel": "email", + * "template": { + * "id": "tpl-001", + * "subject": "Welcome!", + * "body": "

Welcome

", + * "bodyType": "html" + * }, + * "recipients": { + * "to": ["user@example.com"], + * "cc": ["admin@example.com"] + * }, + * "schedule": { + * "type": "immediate" + * }, + * "retryPolicy": { + * "enabled": true, + * "maxRetries": 3, + * "backoffStrategy": "exponential" + * }, + * "tracking": { + * "trackOpens": true, + * "trackClicks": true, + * "trackDelivery": true + * } + * } + * ``` + */ +export const NotificationConfigSchema = z.object({ + /** + * Unique identifier for this notification configuration + */ + id: z.string().describe('Notification ID'), + + /** + * Human-readable name for this notification + */ + name: z.string().describe('Notification name'), + + /** + * Delivery channel for the notification + */ + channel: NotificationChannelSchema.describe('Notification channel'), + + /** + * Notification template based on channel type + */ + template: z.union([ + EmailTemplateSchema, + SMSTemplateSchema, + PushNotificationSchema, + InAppNotificationSchema, + ]).describe('Notification template'), + + /** + * Recipient configuration + */ + recipients: z.object({ + /** + * Primary recipients + */ + to: z.array(z.string()).describe('Primary recipients'), + + /** + * CC recipients (email only) + */ + cc: z.array(z.string()).optional().describe('CC recipients'), + + /** + * BCC recipients (email only) + */ + bcc: z.array(z.string()).optional().describe('BCC recipients'), + }).describe('Recipients'), + + /** + * Scheduling configuration + */ + schedule: z.object({ + /** + * Scheduling type + */ + type: z.enum(['immediate', 'delayed', 'scheduled']).describe('Schedule type'), + + /** + * Delay in milliseconds (for delayed type) + */ + delay: z.number().optional().describe('Delay in milliseconds'), + + /** + * Scheduled send time (Unix timestamp in milliseconds) + */ + scheduledAt: z.number().optional().describe('Scheduled timestamp'), + }).optional().describe('Scheduling'), + + /** + * Retry policy for failed deliveries + */ + retryPolicy: z.object({ + /** + * Enable automatic retries + * @default true + */ + enabled: z.boolean().optional().default(true).describe('Enable retries'), + + /** + * Maximum number of retry attempts + * @default 3 + */ + maxRetries: z.number().optional().default(3).describe('Max retry attempts'), + + /** + * Backoff strategy for retries + */ + backoffStrategy: z.enum(['exponential', 'linear', 'fixed']).describe('Backoff strategy'), + }).optional().describe('Retry policy'), + + /** + * Delivery tracking configuration + */ + tracking: z.object({ + /** + * Track when emails are opened + * @default false + */ + trackOpens: z.boolean().optional().default(false).describe('Track opens'), + + /** + * Track when links are clicked + * @default false + */ + trackClicks: z.boolean().optional().default(false).describe('Track clicks'), + + /** + * Track delivery status + * @default true + */ + trackDelivery: z.boolean().optional().default(true).describe('Track delivery'), + }).optional().describe('Tracking configuration'), +}); + +// Type exports +export type NotificationConfig = z.infer; +export type NotificationChannel = z.infer; +export type EmailTemplate = z.infer; +export type SMSTemplate = z.infer; +export type PushNotification = z.infer; +export type InAppNotification = z.infer; From 7888821a7533dcf3724d8a8089ef6d98e20cf566 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 14:20:10 +0000 Subject: [PATCH 3/3] Add generated documentation and JSON schemas for new protocols Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- content/docs/references/data/connector.mdx | 34 ++ content/docs/references/data/document.mdx | 87 +++++ .../docs/references/data/external-lookup.mdx | 51 +++ content/docs/references/data/index.mdx | 2 + content/docs/references/data/meta.json | 2 + .../references/system/change-management.mdx | 108 ++++++ content/docs/references/system/index.mdx | 2 + content/docs/references/system/meta.json | 2 + .../docs/references/system/notification.mdx | 110 ++++++ packages/spec/json-schema/data/Document.json | 292 +++++++++++++++ .../json-schema/data/DocumentTemplate.json | 78 ++++ .../json-schema/data/DocumentVersion.json | 50 +++ .../json-schema/data/ESignatureConfig.json | 74 ++++ .../json-schema/data/ExternalDataSource.json | 68 ++++ .../spec/json-schema/data/ExternalLookup.json | 210 +++++++++++ .../spec/json-schema/data/FieldMapping.json | 34 ++ .../spec/json-schema/system/ChangeImpact.json | 55 +++ .../json-schema/system/ChangePriority.json | 15 + .../json-schema/system/ChangeRequest.json | 313 ++++++++++++++++ .../spec/json-schema/system/ChangeStatus.json | 21 ++ .../spec/json-schema/system/ChangeType.json | 15 + .../json-schema/system/EmailTemplate.json | 69 ++++ .../json-schema/system/InAppNotification.json | 48 +++ .../system/NotificationChannel.json | 18 + .../system/NotificationConfig.json | 343 ++++++++++++++++++ .../json-schema/system/PushNotification.json | 60 +++ .../spec/json-schema/system/RollbackPlan.json | 51 +++ .../spec/json-schema/system/SMSTemplate.json | 36 ++ 28 files changed, 2248 insertions(+) create mode 100644 content/docs/references/data/connector.mdx create mode 100644 content/docs/references/data/document.mdx create mode 100644 content/docs/references/data/external-lookup.mdx create mode 100644 content/docs/references/system/change-management.mdx create mode 100644 content/docs/references/system/notification.mdx create mode 100644 packages/spec/json-schema/data/Document.json create mode 100644 packages/spec/json-schema/data/DocumentTemplate.json create mode 100644 packages/spec/json-schema/data/DocumentVersion.json create mode 100644 packages/spec/json-schema/data/ESignatureConfig.json create mode 100644 packages/spec/json-schema/data/ExternalDataSource.json create mode 100644 packages/spec/json-schema/data/ExternalLookup.json create mode 100644 packages/spec/json-schema/data/FieldMapping.json create mode 100644 packages/spec/json-schema/system/ChangeImpact.json create mode 100644 packages/spec/json-schema/system/ChangePriority.json create mode 100644 packages/spec/json-schema/system/ChangeRequest.json create mode 100644 packages/spec/json-schema/system/ChangeStatus.json create mode 100644 packages/spec/json-schema/system/ChangeType.json create mode 100644 packages/spec/json-schema/system/EmailTemplate.json create mode 100644 packages/spec/json-schema/system/InAppNotification.json create mode 100644 packages/spec/json-schema/system/NotificationChannel.json create mode 100644 packages/spec/json-schema/system/NotificationConfig.json create mode 100644 packages/spec/json-schema/system/PushNotification.json create mode 100644 packages/spec/json-schema/system/RollbackPlan.json create mode 100644 packages/spec/json-schema/system/SMSTemplate.json diff --git a/content/docs/references/data/connector.mdx b/content/docs/references/data/connector.mdx new file mode 100644 index 000000000..d911637f9 --- /dev/null +++ b/content/docs/references/data/connector.mdx @@ -0,0 +1,34 @@ +--- +title: Connector +description: Connector protocol schemas +--- + +# Connector + + +**Source:** `packages/spec/src/data/connector.zod.ts` + + +## TypeScript Usage + +```typescript +import { FieldMappingSchema } from '@objectstack/spec/data'; +import type { FieldMapping } from '@objectstack/spec/data'; + +// Validate data +const result = FieldMappingSchema.parse(data); +``` + +--- + +## FieldMapping + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **externalField** | `string` | ✅ | External field name | +| **localField** | `string` | ✅ | Local field name | +| **type** | `string` | ✅ | Field type | +| **readonly** | `boolean` | optional | Read-only field | + diff --git a/content/docs/references/data/document.mdx b/content/docs/references/data/document.mdx new file mode 100644 index 000000000..75dba04f4 --- /dev/null +++ b/content/docs/references/data/document.mdx @@ -0,0 +1,87 @@ +--- +title: Document +description: Document protocol schemas +--- + +# Document + + +**Source:** `packages/spec/src/data/document.zod.ts` + + +## TypeScript Usage + +```typescript +import { DocumentSchema, DocumentTemplateSchema, DocumentVersionSchema, ESignatureConfigSchema } from '@objectstack/spec/data'; +import type { Document, DocumentTemplate, DocumentVersion, ESignatureConfig } from '@objectstack/spec/data'; + +// Validate data +const result = DocumentSchema.parse(data); +``` + +--- + +## Document + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Document ID | +| **name** | `string` | ✅ | Document name | +| **description** | `string` | optional | Document description | +| **fileType** | `string` | ✅ | File MIME type | +| **fileSize** | `number` | ✅ | File size in bytes | +| **category** | `string` | optional | Document category | +| **tags** | `string[]` | optional | Document tags | +| **versioning** | `object` | optional | Version control | +| **template** | `object` | optional | Document template | +| **eSignature** | `object` | optional | E-signature config | +| **access** | `object` | optional | Access control | +| **metadata** | `Record` | optional | Custom metadata | + +--- + +## DocumentTemplate + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Template ID | +| **name** | `string` | ✅ | Template name | +| **description** | `string` | optional | Template description | +| **fileUrl** | `string` | ✅ | Template file URL | +| **fileType** | `string` | ✅ | File MIME type | +| **placeholders** | `object[]` | ✅ | Template placeholders | + +--- + +## DocumentVersion + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **versionNumber** | `number` | ✅ | Version number | +| **createdAt** | `number` | ✅ | Creation timestamp | +| **createdBy** | `string` | ✅ | Creator user ID | +| **size** | `number` | ✅ | File size in bytes | +| **checksum** | `string` | ✅ | File checksum | +| **downloadUrl** | `string` | ✅ | Download URL | +| **isLatest** | `boolean` | optional | Is latest version | + +--- + +## ESignatureConfig + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **provider** | `Enum<'docusign' \| 'adobe-sign' \| 'hellosign' \| 'custom'>` | ✅ | E-signature provider | +| **enabled** | `boolean` | optional | E-signature enabled | +| **signers** | `object[]` | ✅ | Document signers | +| **expirationDays** | `number` | optional | Expiration days | +| **reminderDays** | `number` | optional | Reminder interval days | + diff --git a/content/docs/references/data/external-lookup.mdx b/content/docs/references/data/external-lookup.mdx new file mode 100644 index 000000000..eb65904dc --- /dev/null +++ b/content/docs/references/data/external-lookup.mdx @@ -0,0 +1,51 @@ +--- +title: External Lookup +description: External Lookup protocol schemas +--- + +# External Lookup + + +**Source:** `packages/spec/src/data/external-lookup.zod.ts` + + +## TypeScript Usage + +```typescript +import { ExternalDataSourceSchema, ExternalLookupSchema } from '@objectstack/spec/data'; +import type { ExternalDataSource, ExternalLookup } from '@objectstack/spec/data'; + +// Validate data +const result = ExternalDataSourceSchema.parse(data); +``` + +--- + +## ExternalDataSource + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Data source ID | +| **name** | `string` | ✅ | Data source name | +| **type** | `Enum<'odata' \| 'rest-api' \| 'graphql' \| 'custom'>` | ✅ | Protocol type | +| **endpoint** | `string` | ✅ | API endpoint URL | +| **authentication** | `object` | ✅ | Authentication | + +--- + +## ExternalLookup + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **fieldName** | `string` | ✅ | Field name | +| **dataSource** | `object` | ✅ | External data source | +| **query** | `object` | ✅ | Query configuration | +| **fieldMappings** | `object[]` | ✅ | Field mappings | +| **caching** | `object` | optional | Caching configuration | +| **fallback** | `object` | optional | Fallback configuration | +| **rateLimit** | `object` | optional | Rate limiting | + diff --git a/content/docs/references/data/index.mdx b/content/docs/references/data/index.mdx index a91672d32..bd834f232 100644 --- a/content/docs/references/data/index.mdx +++ b/content/docs/references/data/index.mdx @@ -9,6 +9,8 @@ This section contains all protocol schemas for the data layer of ObjectStack. + + diff --git a/content/docs/references/data/meta.json b/content/docs/references/data/meta.json index 6b613d272..661e79412 100644 --- a/content/docs/references/data/meta.json +++ b/content/docs/references/data/meta.json @@ -2,6 +2,8 @@ "title": "Data Protocol", "pages": [ "dataset", + "document", + "external-lookup", "field", "filter", "hook", diff --git a/content/docs/references/system/change-management.mdx b/content/docs/references/system/change-management.mdx new file mode 100644 index 000000000..aa19a4e25 --- /dev/null +++ b/content/docs/references/system/change-management.mdx @@ -0,0 +1,108 @@ +--- +title: Change Management +description: Change Management protocol schemas +--- + +# Change Management + + +**Source:** `packages/spec/src/system/change-management.zod.ts` + + +## TypeScript Usage + +```typescript +import { ChangeImpactSchema, ChangePrioritySchema, ChangeRequestSchema, ChangeStatusSchema, ChangeTypeSchema, RollbackPlanSchema } from '@objectstack/spec/system'; +import type { ChangeImpact, ChangePriority, ChangeRequest, ChangeStatus, ChangeType, RollbackPlan } from '@objectstack/spec/system'; + +// Validate data +const result = ChangeImpactSchema.parse(data); +``` + +--- + +## ChangeImpact + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **level** | `Enum<'low' \| 'medium' \| 'high' \| 'critical'>` | ✅ | Impact level | +| **affectedSystems** | `string[]` | ✅ | Affected systems | +| **affectedUsers** | `number` | optional | Affected user count | +| **downtime** | `object` | optional | Downtime information | + +--- + +## ChangePriority + +### Allowed Values + +* `critical` +* `high` +* `medium` +* `low` + +--- + +## ChangeRequest + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Change request ID | +| **title** | `string` | ✅ | Change title | +| **description** | `string` | ✅ | Change description | +| **type** | `Enum<'standard' \| 'normal' \| 'emergency' \| 'major'>` | ✅ | Change type | +| **priority** | `Enum<'critical' \| 'high' \| 'medium' \| 'low'>` | ✅ | Change priority | +| **status** | `Enum<'draft' \| 'submitted' \| 'in-review' \| 'approved' \| 'scheduled' \| 'in-progress' \| 'completed' \| 'failed' \| 'rolled-back' \| 'cancelled'>` | ✅ | Change status | +| **requestedBy** | `string` | ✅ | Requester user ID | +| **requestedAt** | `number` | ✅ | Request timestamp | +| **impact** | `object` | ✅ | Impact assessment | +| **implementation** | `object` | ✅ | Implementation plan | +| **rollbackPlan** | `object` | ✅ | Rollback plan | +| **schedule** | `object` | optional | Schedule | +| **approval** | `object` | optional | Approval workflow | +| **attachments** | `object[]` | optional | Attachments | + +--- + +## ChangeStatus + +### Allowed Values + +* `draft` +* `submitted` +* `in-review` +* `approved` +* `scheduled` +* `in-progress` +* `completed` +* `failed` +* `rolled-back` +* `cancelled` + +--- + +## ChangeType + +### Allowed Values + +* `standard` +* `normal` +* `emergency` +* `major` + +--- + +## RollbackPlan + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **description** | `string` | ✅ | Rollback description | +| **steps** | `object[]` | ✅ | Rollback steps | +| **testProcedure** | `string` | optional | Test procedure | + diff --git a/content/docs/references/system/index.mdx b/content/docs/references/system/index.mdx index 654db1db1..569cf0fdc 100644 --- a/content/docs/references/system/index.mdx +++ b/content/docs/references/system/index.mdx @@ -10,6 +10,7 @@ This section contains all protocol schemas for the system layer of ObjectStack. + @@ -28,6 +29,7 @@ This section contains all protocol schemas for the system layer of ObjectStack. + diff --git a/content/docs/references/system/meta.json b/content/docs/references/system/meta.json index 0321fd6b9..175581a12 100644 --- a/content/docs/references/system/meta.json +++ b/content/docs/references/system/meta.json @@ -3,6 +3,7 @@ "pages": [ "audit", "cache", + "change-management", "collaboration", "compliance", "context", @@ -21,6 +22,7 @@ "masking", "message-queue", "metrics", + "notification", "object-storage", "plugin", "plugin-capability", diff --git a/content/docs/references/system/notification.mdx b/content/docs/references/system/notification.mdx new file mode 100644 index 000000000..120e421cf --- /dev/null +++ b/content/docs/references/system/notification.mdx @@ -0,0 +1,110 @@ +--- +title: Notification +description: Notification protocol schemas +--- + +# Notification + + +**Source:** `packages/spec/src/system/notification.zod.ts` + + +## TypeScript Usage + +```typescript +import { EmailTemplateSchema, InAppNotificationSchema, NotificationChannelSchema, NotificationConfigSchema, PushNotificationSchema, SMSTemplateSchema } from '@objectstack/spec/system'; +import type { EmailTemplate, InAppNotification, NotificationChannel, NotificationConfig, PushNotification, SMSTemplate } from '@objectstack/spec/system'; + +// Validate data +const result = EmailTemplateSchema.parse(data); +``` + +--- + +## EmailTemplate + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Template identifier | +| **subject** | `string` | ✅ | Email subject | +| **body** | `string` | ✅ | Email body content | +| **bodyType** | `Enum<'text' \| 'html' \| 'markdown'>` | optional | Body content type | +| **variables** | `string[]` | optional | Template variables | +| **attachments** | `object[]` | optional | Email attachments | + +--- + +## InAppNotification + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **title** | `string` | ✅ | Notification title | +| **message** | `string` | ✅ | Notification message | +| **type** | `Enum<'info' \| 'success' \| 'warning' \| 'error'>` | ✅ | Notification type | +| **actionUrl** | `string` | optional | Action URL | +| **dismissible** | `boolean` | optional | User dismissible | +| **expiresAt** | `number` | optional | Expiration timestamp | + +--- + +## NotificationChannel + +### Allowed Values + +* `email` +* `sms` +* `push` +* `in-app` +* `slack` +* `teams` +* `webhook` + +--- + +## NotificationConfig + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Notification ID | +| **name** | `string` | ✅ | Notification name | +| **channel** | `Enum<'email' \| 'sms' \| 'push' \| 'in-app' \| 'slack' \| 'teams' \| 'webhook'>` | ✅ | Notification channel | +| **template** | `object \| object \| object \| object` | ✅ | Notification template | +| **recipients** | `object` | ✅ | Recipients | +| **schedule** | `object` | optional | Scheduling | +| **retryPolicy** | `object` | optional | Retry policy | +| **tracking** | `object` | optional | Tracking configuration | + +--- + +## PushNotification + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **title** | `string` | ✅ | Notification title | +| **body** | `string` | ✅ | Notification body | +| **icon** | `string` | optional | Notification icon URL | +| **badge** | `number` | optional | Badge count | +| **data** | `Record` | optional | Custom data | +| **actions** | `object[]` | optional | Notification actions | + +--- + +## SMSTemplate + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Template identifier | +| **message** | `string` | ✅ | SMS message content | +| **maxLength** | `number` | optional | Maximum message length | +| **variables** | `string[]` | optional | Template variables | + diff --git a/packages/spec/json-schema/data/Document.json b/packages/spec/json-schema/data/Document.json new file mode 100644 index 000000000..fe369c5af --- /dev/null +++ b/packages/spec/json-schema/data/Document.json @@ -0,0 +1,292 @@ +{ + "$ref": "#/definitions/Document", + "definitions": { + "Document": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Document ID" + }, + "name": { + "type": "string", + "description": "Document name" + }, + "description": { + "type": "string", + "description": "Document description" + }, + "fileType": { + "type": "string", + "description": "File MIME type" + }, + "fileSize": { + "type": "number", + "description": "File size in bytes" + }, + "category": { + "type": "string", + "description": "Document category" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Document tags" + }, + "versioning": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Versioning enabled" + }, + "versions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "versionNumber": { + "type": "number", + "description": "Version number" + }, + "createdAt": { + "type": "number", + "description": "Creation timestamp" + }, + "createdBy": { + "type": "string", + "description": "Creator user ID" + }, + "size": { + "type": "number", + "description": "File size in bytes" + }, + "checksum": { + "type": "string", + "description": "File checksum" + }, + "downloadUrl": { + "type": "string", + "format": "uri", + "description": "Download URL" + }, + "isLatest": { + "type": "boolean", + "default": false, + "description": "Is latest version" + } + }, + "required": [ + "versionNumber", + "createdAt", + "createdBy", + "size", + "checksum", + "downloadUrl" + ], + "additionalProperties": false + }, + "description": "Version history" + }, + "majorVersion": { + "type": "number", + "description": "Major version" + }, + "minorVersion": { + "type": "number", + "description": "Minor version" + } + }, + "required": [ + "enabled", + "versions", + "majorVersion", + "minorVersion" + ], + "additionalProperties": false, + "description": "Version control" + }, + "template": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Template ID" + }, + "name": { + "type": "string", + "description": "Template name" + }, + "description": { + "type": "string", + "description": "Template description" + }, + "fileUrl": { + "type": "string", + "format": "uri", + "description": "Template file URL" + }, + "fileType": { + "type": "string", + "description": "File MIME type" + }, + "placeholders": { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "Placeholder key" + }, + "label": { + "type": "string", + "description": "Placeholder label" + }, + "type": { + "type": "string", + "enum": [ + "text", + "number", + "date", + "image" + ], + "description": "Placeholder type" + }, + "required": { + "type": "boolean", + "default": false, + "description": "Is required" + } + }, + "required": [ + "key", + "label", + "type" + ], + "additionalProperties": false + }, + "description": "Template placeholders" + } + }, + "required": [ + "id", + "name", + "fileUrl", + "fileType", + "placeholders" + ], + "additionalProperties": false, + "description": "Document template" + }, + "eSignature": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "enum": [ + "docusign", + "adobe-sign", + "hellosign", + "custom" + ], + "description": "E-signature provider" + }, + "enabled": { + "type": "boolean", + "default": false, + "description": "E-signature enabled" + }, + "signers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "description": "Signer email" + }, + "name": { + "type": "string", + "description": "Signer name" + }, + "role": { + "type": "string", + "description": "Signer role" + }, + "order": { + "type": "number", + "description": "Signing order" + } + }, + "required": [ + "email", + "name", + "role", + "order" + ], + "additionalProperties": false + }, + "description": "Document signers" + }, + "expirationDays": { + "type": "number", + "default": 30, + "description": "Expiration days" + }, + "reminderDays": { + "type": "number", + "default": 7, + "description": "Reminder interval days" + } + }, + "required": [ + "provider", + "signers" + ], + "additionalProperties": false, + "description": "E-signature config" + }, + "access": { + "type": "object", + "properties": { + "isPublic": { + "type": "boolean", + "default": false, + "description": "Public access" + }, + "sharedWith": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Shared with" + }, + "expiresAt": { + "type": "number", + "description": "Access expiration" + } + }, + "additionalProperties": false, + "description": "Access control" + }, + "metadata": { + "type": "object", + "additionalProperties": {}, + "description": "Custom metadata" + } + }, + "required": [ + "id", + "name", + "fileType", + "fileSize" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/data/DocumentTemplate.json b/packages/spec/json-schema/data/DocumentTemplate.json new file mode 100644 index 000000000..2f391b111 --- /dev/null +++ b/packages/spec/json-schema/data/DocumentTemplate.json @@ -0,0 +1,78 @@ +{ + "$ref": "#/definitions/DocumentTemplate", + "definitions": { + "DocumentTemplate": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Template ID" + }, + "name": { + "type": "string", + "description": "Template name" + }, + "description": { + "type": "string", + "description": "Template description" + }, + "fileUrl": { + "type": "string", + "format": "uri", + "description": "Template file URL" + }, + "fileType": { + "type": "string", + "description": "File MIME type" + }, + "placeholders": { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "Placeholder key" + }, + "label": { + "type": "string", + "description": "Placeholder label" + }, + "type": { + "type": "string", + "enum": [ + "text", + "number", + "date", + "image" + ], + "description": "Placeholder type" + }, + "required": { + "type": "boolean", + "default": false, + "description": "Is required" + } + }, + "required": [ + "key", + "label", + "type" + ], + "additionalProperties": false + }, + "description": "Template placeholders" + } + }, + "required": [ + "id", + "name", + "fileUrl", + "fileType", + "placeholders" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/data/DocumentVersion.json b/packages/spec/json-schema/data/DocumentVersion.json new file mode 100644 index 000000000..0fe34ce72 --- /dev/null +++ b/packages/spec/json-schema/data/DocumentVersion.json @@ -0,0 +1,50 @@ +{ + "$ref": "#/definitions/DocumentVersion", + "definitions": { + "DocumentVersion": { + "type": "object", + "properties": { + "versionNumber": { + "type": "number", + "description": "Version number" + }, + "createdAt": { + "type": "number", + "description": "Creation timestamp" + }, + "createdBy": { + "type": "string", + "description": "Creator user ID" + }, + "size": { + "type": "number", + "description": "File size in bytes" + }, + "checksum": { + "type": "string", + "description": "File checksum" + }, + "downloadUrl": { + "type": "string", + "format": "uri", + "description": "Download URL" + }, + "isLatest": { + "type": "boolean", + "default": false, + "description": "Is latest version" + } + }, + "required": [ + "versionNumber", + "createdAt", + "createdBy", + "size", + "checksum", + "downloadUrl" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/data/ESignatureConfig.json b/packages/spec/json-schema/data/ESignatureConfig.json new file mode 100644 index 000000000..546b4ad41 --- /dev/null +++ b/packages/spec/json-schema/data/ESignatureConfig.json @@ -0,0 +1,74 @@ +{ + "$ref": "#/definitions/ESignatureConfig", + "definitions": { + "ESignatureConfig": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "enum": [ + "docusign", + "adobe-sign", + "hellosign", + "custom" + ], + "description": "E-signature provider" + }, + "enabled": { + "type": "boolean", + "default": false, + "description": "E-signature enabled" + }, + "signers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "description": "Signer email" + }, + "name": { + "type": "string", + "description": "Signer name" + }, + "role": { + "type": "string", + "description": "Signer role" + }, + "order": { + "type": "number", + "description": "Signing order" + } + }, + "required": [ + "email", + "name", + "role", + "order" + ], + "additionalProperties": false + }, + "description": "Document signers" + }, + "expirationDays": { + "type": "number", + "default": 30, + "description": "Expiration days" + }, + "reminderDays": { + "type": "number", + "default": 7, + "description": "Reminder interval days" + } + }, + "required": [ + "provider", + "signers" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/data/ExternalDataSource.json b/packages/spec/json-schema/data/ExternalDataSource.json new file mode 100644 index 000000000..c4eb9ea2c --- /dev/null +++ b/packages/spec/json-schema/data/ExternalDataSource.json @@ -0,0 +1,68 @@ +{ + "$ref": "#/definitions/ExternalDataSource", + "definitions": { + "ExternalDataSource": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Data source ID" + }, + "name": { + "type": "string", + "description": "Data source name" + }, + "type": { + "type": "string", + "enum": [ + "odata", + "rest-api", + "graphql", + "custom" + ], + "description": "Protocol type" + }, + "endpoint": { + "type": "string", + "format": "uri", + "description": "API endpoint URL" + }, + "authentication": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "oauth2", + "api-key", + "basic", + "none" + ], + "description": "Auth type" + }, + "config": { + "type": "object", + "additionalProperties": {}, + "description": "Auth configuration" + } + }, + "required": [ + "type", + "config" + ], + "additionalProperties": false, + "description": "Authentication" + } + }, + "required": [ + "id", + "name", + "type", + "endpoint", + "authentication" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/data/ExternalLookup.json b/packages/spec/json-schema/data/ExternalLookup.json new file mode 100644 index 000000000..ae7ea0626 --- /dev/null +++ b/packages/spec/json-schema/data/ExternalLookup.json @@ -0,0 +1,210 @@ +{ + "$ref": "#/definitions/ExternalLookup", + "definitions": { + "ExternalLookup": { + "type": "object", + "properties": { + "fieldName": { + "type": "string", + "description": "Field name" + }, + "dataSource": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Data source ID" + }, + "name": { + "type": "string", + "description": "Data source name" + }, + "type": { + "type": "string", + "enum": [ + "odata", + "rest-api", + "graphql", + "custom" + ], + "description": "Protocol type" + }, + "endpoint": { + "type": "string", + "format": "uri", + "description": "API endpoint URL" + }, + "authentication": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "oauth2", + "api-key", + "basic", + "none" + ], + "description": "Auth type" + }, + "config": { + "type": "object", + "additionalProperties": {}, + "description": "Auth configuration" + } + }, + "required": [ + "type", + "config" + ], + "additionalProperties": false, + "description": "Authentication" + } + }, + "required": [ + "id", + "name", + "type", + "endpoint", + "authentication" + ], + "additionalProperties": false, + "description": "External data source" + }, + "query": { + "type": "object", + "properties": { + "endpoint": { + "type": "string", + "description": "Query endpoint path" + }, + "method": { + "type": "string", + "enum": [ + "GET", + "POST" + ], + "default": "GET", + "description": "HTTP method" + }, + "parameters": { + "type": "object", + "additionalProperties": {}, + "description": "Query parameters" + } + }, + "required": [ + "endpoint" + ], + "additionalProperties": false, + "description": "Query configuration" + }, + "fieldMappings": { + "type": "array", + "items": { + "type": "object", + "properties": { + "externalField": { + "type": "string", + "description": "External field name" + }, + "localField": { + "type": "string", + "description": "Local field name" + }, + "type": { + "type": "string", + "description": "Field type" + }, + "readonly": { + "type": "boolean", + "default": true, + "description": "Read-only field" + } + }, + "required": [ + "externalField", + "localField", + "type" + ], + "additionalProperties": false + }, + "description": "Field mappings" + }, + "caching": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Cache enabled" + }, + "ttl": { + "type": "number", + "default": 300, + "description": "Cache TTL (seconds)" + }, + "strategy": { + "type": "string", + "enum": [ + "lru", + "lfu", + "ttl" + ], + "default": "ttl", + "description": "Cache strategy" + } + }, + "additionalProperties": false, + "description": "Caching configuration" + }, + "fallback": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Fallback enabled" + }, + "defaultValue": { + "description": "Default fallback value" + }, + "showError": { + "type": "boolean", + "default": true, + "description": "Show error to user" + } + }, + "additionalProperties": false, + "description": "Fallback configuration" + }, + "rateLimit": { + "type": "object", + "properties": { + "requestsPerSecond": { + "type": "number", + "description": "Requests per second limit" + }, + "burstSize": { + "type": "number", + "description": "Burst size" + } + }, + "required": [ + "requestsPerSecond" + ], + "additionalProperties": false, + "description": "Rate limiting" + } + }, + "required": [ + "fieldName", + "dataSource", + "query", + "fieldMappings" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/data/FieldMapping.json b/packages/spec/json-schema/data/FieldMapping.json new file mode 100644 index 000000000..4ec2dcf95 --- /dev/null +++ b/packages/spec/json-schema/data/FieldMapping.json @@ -0,0 +1,34 @@ +{ + "$ref": "#/definitions/FieldMapping", + "definitions": { + "FieldMapping": { + "type": "object", + "properties": { + "externalField": { + "type": "string", + "description": "External field name" + }, + "localField": { + "type": "string", + "description": "Local field name" + }, + "type": { + "type": "string", + "description": "Field type" + }, + "readonly": { + "type": "boolean", + "default": true, + "description": "Read-only field" + } + }, + "required": [ + "externalField", + "localField", + "type" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/system/ChangeImpact.json b/packages/spec/json-schema/system/ChangeImpact.json new file mode 100644 index 000000000..ead817cbe --- /dev/null +++ b/packages/spec/json-schema/system/ChangeImpact.json @@ -0,0 +1,55 @@ +{ + "$ref": "#/definitions/ChangeImpact", + "definitions": { + "ChangeImpact": { + "type": "object", + "properties": { + "level": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "critical" + ], + "description": "Impact level" + }, + "affectedSystems": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Affected systems" + }, + "affectedUsers": { + "type": "number", + "description": "Affected user count" + }, + "downtime": { + "type": "object", + "properties": { + "required": { + "type": "boolean", + "description": "Downtime required" + }, + "durationMinutes": { + "type": "number", + "description": "Downtime duration" + } + }, + "required": [ + "required" + ], + "additionalProperties": false, + "description": "Downtime information" + } + }, + "required": [ + "level", + "affectedSystems" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/system/ChangePriority.json b/packages/spec/json-schema/system/ChangePriority.json new file mode 100644 index 000000000..cdf051598 --- /dev/null +++ b/packages/spec/json-schema/system/ChangePriority.json @@ -0,0 +1,15 @@ +{ + "$ref": "#/definitions/ChangePriority", + "definitions": { + "ChangePriority": { + "type": "string", + "enum": [ + "critical", + "high", + "medium", + "low" + ] + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/system/ChangeRequest.json b/packages/spec/json-schema/system/ChangeRequest.json new file mode 100644 index 000000000..23bcfd1fc --- /dev/null +++ b/packages/spec/json-schema/system/ChangeRequest.json @@ -0,0 +1,313 @@ +{ + "$ref": "#/definitions/ChangeRequest", + "definitions": { + "ChangeRequest": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Change request ID" + }, + "title": { + "type": "string", + "description": "Change title" + }, + "description": { + "type": "string", + "description": "Change description" + }, + "type": { + "type": "string", + "enum": [ + "standard", + "normal", + "emergency", + "major" + ], + "description": "Change type" + }, + "priority": { + "type": "string", + "enum": [ + "critical", + "high", + "medium", + "low" + ], + "description": "Change priority" + }, + "status": { + "type": "string", + "enum": [ + "draft", + "submitted", + "in-review", + "approved", + "scheduled", + "in-progress", + "completed", + "failed", + "rolled-back", + "cancelled" + ], + "description": "Change status" + }, + "requestedBy": { + "type": "string", + "description": "Requester user ID" + }, + "requestedAt": { + "type": "number", + "description": "Request timestamp" + }, + "impact": { + "type": "object", + "properties": { + "level": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "critical" + ], + "description": "Impact level" + }, + "affectedSystems": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Affected systems" + }, + "affectedUsers": { + "type": "number", + "description": "Affected user count" + }, + "downtime": { + "type": "object", + "properties": { + "required": { + "type": "boolean", + "description": "Downtime required" + }, + "durationMinutes": { + "type": "number", + "description": "Downtime duration" + } + }, + "required": [ + "required" + ], + "additionalProperties": false, + "description": "Downtime information" + } + }, + "required": [ + "level", + "affectedSystems" + ], + "additionalProperties": false, + "description": "Impact assessment" + }, + "implementation": { + "type": "object", + "properties": { + "description": { + "type": "string", + "description": "Implementation description" + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "properties": { + "order": { + "type": "number", + "description": "Step order" + }, + "description": { + "type": "string", + "description": "Step description" + }, + "estimatedMinutes": { + "type": "number", + "description": "Estimated duration" + } + }, + "required": [ + "order", + "description", + "estimatedMinutes" + ], + "additionalProperties": false + }, + "description": "Implementation steps" + }, + "testing": { + "type": "string", + "description": "Testing procedure" + } + }, + "required": [ + "description", + "steps" + ], + "additionalProperties": false, + "description": "Implementation plan" + }, + "rollbackPlan": { + "type": "object", + "properties": { + "description": { + "type": "string", + "description": "Rollback description" + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "properties": { + "order": { + "type": "number", + "description": "Step order" + }, + "description": { + "type": "string", + "description": "Step description" + }, + "estimatedMinutes": { + "type": "number", + "description": "Estimated duration" + } + }, + "required": [ + "order", + "description", + "estimatedMinutes" + ], + "additionalProperties": false + }, + "description": "Rollback steps" + }, + "testProcedure": { + "type": "string", + "description": "Test procedure" + } + }, + "required": [ + "description", + "steps" + ], + "additionalProperties": false, + "description": "Rollback plan" + }, + "schedule": { + "type": "object", + "properties": { + "plannedStart": { + "type": "number", + "description": "Planned start time" + }, + "plannedEnd": { + "type": "number", + "description": "Planned end time" + }, + "actualStart": { + "type": "number", + "description": "Actual start time" + }, + "actualEnd": { + "type": "number", + "description": "Actual end time" + } + }, + "required": [ + "plannedStart", + "plannedEnd" + ], + "additionalProperties": false, + "description": "Schedule" + }, + "approval": { + "type": "object", + "properties": { + "required": { + "type": "boolean", + "description": "Approval required" + }, + "approvers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "userId": { + "type": "string", + "description": "Approver user ID" + }, + "approvedAt": { + "type": "number", + "description": "Approval timestamp" + }, + "comments": { + "type": "string", + "description": "Approver comments" + } + }, + "required": [ + "userId" + ], + "additionalProperties": false + }, + "description": "Approvers" + } + }, + "required": [ + "required", + "approvers" + ], + "additionalProperties": false, + "description": "Approval workflow" + }, + "attachments": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Attachment name" + }, + "url": { + "type": "string", + "format": "uri", + "description": "Attachment URL" + } + }, + "required": [ + "name", + "url" + ], + "additionalProperties": false + }, + "description": "Attachments" + } + }, + "required": [ + "id", + "title", + "description", + "type", + "priority", + "status", + "requestedBy", + "requestedAt", + "impact", + "implementation", + "rollbackPlan" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/system/ChangeStatus.json b/packages/spec/json-schema/system/ChangeStatus.json new file mode 100644 index 000000000..8fb05526f --- /dev/null +++ b/packages/spec/json-schema/system/ChangeStatus.json @@ -0,0 +1,21 @@ +{ + "$ref": "#/definitions/ChangeStatus", + "definitions": { + "ChangeStatus": { + "type": "string", + "enum": [ + "draft", + "submitted", + "in-review", + "approved", + "scheduled", + "in-progress", + "completed", + "failed", + "rolled-back", + "cancelled" + ] + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/system/ChangeType.json b/packages/spec/json-schema/system/ChangeType.json new file mode 100644 index 000000000..7d056daa8 --- /dev/null +++ b/packages/spec/json-schema/system/ChangeType.json @@ -0,0 +1,15 @@ +{ + "$ref": "#/definitions/ChangeType", + "definitions": { + "ChangeType": { + "type": "string", + "enum": [ + "standard", + "normal", + "emergency", + "major" + ] + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/system/EmailTemplate.json b/packages/spec/json-schema/system/EmailTemplate.json new file mode 100644 index 000000000..ca79574b7 --- /dev/null +++ b/packages/spec/json-schema/system/EmailTemplate.json @@ -0,0 +1,69 @@ +{ + "$ref": "#/definitions/EmailTemplate", + "definitions": { + "EmailTemplate": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Template identifier" + }, + "subject": { + "type": "string", + "description": "Email subject" + }, + "body": { + "type": "string", + "description": "Email body content" + }, + "bodyType": { + "type": "string", + "enum": [ + "text", + "html", + "markdown" + ], + "default": "html", + "description": "Body content type" + }, + "variables": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Template variables" + }, + "attachments": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Attachment filename" + }, + "url": { + "type": "string", + "format": "uri", + "description": "Attachment URL" + } + }, + "required": [ + "name", + "url" + ], + "additionalProperties": false + }, + "description": "Email attachments" + } + }, + "required": [ + "id", + "subject", + "body" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/system/InAppNotification.json b/packages/spec/json-schema/system/InAppNotification.json new file mode 100644 index 000000000..314dcd25c --- /dev/null +++ b/packages/spec/json-schema/system/InAppNotification.json @@ -0,0 +1,48 @@ +{ + "$ref": "#/definitions/InAppNotification", + "definitions": { + "InAppNotification": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Notification title" + }, + "message": { + "type": "string", + "description": "Notification message" + }, + "type": { + "type": "string", + "enum": [ + "info", + "success", + "warning", + "error" + ], + "description": "Notification type" + }, + "actionUrl": { + "type": "string", + "description": "Action URL" + }, + "dismissible": { + "type": "boolean", + "default": true, + "description": "User dismissible" + }, + "expiresAt": { + "type": "number", + "description": "Expiration timestamp" + } + }, + "required": [ + "title", + "message", + "type" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/system/NotificationChannel.json b/packages/spec/json-schema/system/NotificationChannel.json new file mode 100644 index 000000000..badc6d0ea --- /dev/null +++ b/packages/spec/json-schema/system/NotificationChannel.json @@ -0,0 +1,18 @@ +{ + "$ref": "#/definitions/NotificationChannel", + "definitions": { + "NotificationChannel": { + "type": "string", + "enum": [ + "email", + "sms", + "push", + "in-app", + "slack", + "teams", + "webhook" + ] + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/system/NotificationConfig.json b/packages/spec/json-schema/system/NotificationConfig.json new file mode 100644 index 000000000..2fe8a753a --- /dev/null +++ b/packages/spec/json-schema/system/NotificationConfig.json @@ -0,0 +1,343 @@ +{ + "$ref": "#/definitions/NotificationConfig", + "definitions": { + "NotificationConfig": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Notification ID" + }, + "name": { + "type": "string", + "description": "Notification name" + }, + "channel": { + "type": "string", + "enum": [ + "email", + "sms", + "push", + "in-app", + "slack", + "teams", + "webhook" + ], + "description": "Notification channel" + }, + "template": { + "anyOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Template identifier" + }, + "subject": { + "type": "string", + "description": "Email subject" + }, + "body": { + "type": "string", + "description": "Email body content" + }, + "bodyType": { + "type": "string", + "enum": [ + "text", + "html", + "markdown" + ], + "default": "html", + "description": "Body content type" + }, + "variables": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Template variables" + }, + "attachments": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Attachment filename" + }, + "url": { + "type": "string", + "format": "uri", + "description": "Attachment URL" + } + }, + "required": [ + "name", + "url" + ], + "additionalProperties": false + }, + "description": "Email attachments" + } + }, + "required": [ + "id", + "subject", + "body" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Template identifier" + }, + "message": { + "type": "string", + "description": "SMS message content" + }, + "maxLength": { + "type": "number", + "default": 160, + "description": "Maximum message length" + }, + "variables": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Template variables" + } + }, + "required": [ + "id", + "message" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Notification title" + }, + "body": { + "type": "string", + "description": "Notification body" + }, + "icon": { + "type": "string", + "format": "uri", + "description": "Notification icon URL" + }, + "badge": { + "type": "number", + "description": "Badge count" + }, + "data": { + "type": "object", + "additionalProperties": {}, + "description": "Custom data" + }, + "actions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "action": { + "type": "string", + "description": "Action identifier" + }, + "title": { + "type": "string", + "description": "Action button title" + } + }, + "required": [ + "action", + "title" + ], + "additionalProperties": false + }, + "description": "Notification actions" + } + }, + "required": [ + "title", + "body" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Notification title" + }, + "message": { + "type": "string", + "description": "Notification message" + }, + "type": { + "type": "string", + "enum": [ + "info", + "success", + "warning", + "error" + ], + "description": "Notification type" + }, + "actionUrl": { + "type": "string", + "description": "Action URL" + }, + "dismissible": { + "type": "boolean", + "default": true, + "description": "User dismissible" + }, + "expiresAt": { + "type": "number", + "description": "Expiration timestamp" + } + }, + "required": [ + "title", + "message", + "type" + ], + "additionalProperties": false + } + ], + "description": "Notification template" + }, + "recipients": { + "type": "object", + "properties": { + "to": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Primary recipients" + }, + "cc": { + "type": "array", + "items": { + "type": "string" + }, + "description": "CC recipients" + }, + "bcc": { + "type": "array", + "items": { + "type": "string" + }, + "description": "BCC recipients" + } + }, + "required": [ + "to" + ], + "additionalProperties": false, + "description": "Recipients" + }, + "schedule": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "immediate", + "delayed", + "scheduled" + ], + "description": "Schedule type" + }, + "delay": { + "type": "number", + "description": "Delay in milliseconds" + }, + "scheduledAt": { + "type": "number", + "description": "Scheduled timestamp" + } + }, + "required": [ + "type" + ], + "additionalProperties": false, + "description": "Scheduling" + }, + "retryPolicy": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable retries" + }, + "maxRetries": { + "type": "number", + "default": 3, + "description": "Max retry attempts" + }, + "backoffStrategy": { + "type": "string", + "enum": [ + "exponential", + "linear", + "fixed" + ], + "description": "Backoff strategy" + } + }, + "required": [ + "backoffStrategy" + ], + "additionalProperties": false, + "description": "Retry policy" + }, + "tracking": { + "type": "object", + "properties": { + "trackOpens": { + "type": "boolean", + "default": false, + "description": "Track opens" + }, + "trackClicks": { + "type": "boolean", + "default": false, + "description": "Track clicks" + }, + "trackDelivery": { + "type": "boolean", + "default": true, + "description": "Track delivery" + } + }, + "additionalProperties": false, + "description": "Tracking configuration" + } + }, + "required": [ + "id", + "name", + "channel", + "template", + "recipients" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/system/PushNotification.json b/packages/spec/json-schema/system/PushNotification.json new file mode 100644 index 000000000..d98ffcabe --- /dev/null +++ b/packages/spec/json-schema/system/PushNotification.json @@ -0,0 +1,60 @@ +{ + "$ref": "#/definitions/PushNotification", + "definitions": { + "PushNotification": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Notification title" + }, + "body": { + "type": "string", + "description": "Notification body" + }, + "icon": { + "type": "string", + "format": "uri", + "description": "Notification icon URL" + }, + "badge": { + "type": "number", + "description": "Badge count" + }, + "data": { + "type": "object", + "additionalProperties": {}, + "description": "Custom data" + }, + "actions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "action": { + "type": "string", + "description": "Action identifier" + }, + "title": { + "type": "string", + "description": "Action button title" + } + }, + "required": [ + "action", + "title" + ], + "additionalProperties": false + }, + "description": "Notification actions" + } + }, + "required": [ + "title", + "body" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/system/RollbackPlan.json b/packages/spec/json-schema/system/RollbackPlan.json new file mode 100644 index 000000000..a8f8d03d0 --- /dev/null +++ b/packages/spec/json-schema/system/RollbackPlan.json @@ -0,0 +1,51 @@ +{ + "$ref": "#/definitions/RollbackPlan", + "definitions": { + "RollbackPlan": { + "type": "object", + "properties": { + "description": { + "type": "string", + "description": "Rollback description" + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "properties": { + "order": { + "type": "number", + "description": "Step order" + }, + "description": { + "type": "string", + "description": "Step description" + }, + "estimatedMinutes": { + "type": "number", + "description": "Estimated duration" + } + }, + "required": [ + "order", + "description", + "estimatedMinutes" + ], + "additionalProperties": false + }, + "description": "Rollback steps" + }, + "testProcedure": { + "type": "string", + "description": "Test procedure" + } + }, + "required": [ + "description", + "steps" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/system/SMSTemplate.json b/packages/spec/json-schema/system/SMSTemplate.json new file mode 100644 index 000000000..a1a5ceb17 --- /dev/null +++ b/packages/spec/json-schema/system/SMSTemplate.json @@ -0,0 +1,36 @@ +{ + "$ref": "#/definitions/SMSTemplate", + "definitions": { + "SMSTemplate": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Template identifier" + }, + "message": { + "type": "string", + "description": "SMS message content" + }, + "maxLength": { + "type": "number", + "default": 160, + "description": "Maximum message length" + }, + "variables": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Template variables" + } + }, + "required": [ + "id", + "message" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file