From ff1348fb87237ffc853430b8cd8ad1ad92a31bd1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 11:45:58 +0000 Subject: [PATCH 1/3] Initial plan From 34375ec233e2041225ecf690ca7b9b1366495e9a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 11:53:35 +0000 Subject: [PATCH 2/3] Add object storage protocol and file attachment configuration - Created object-storage.zod.ts with comprehensive schemas - Added file attachment configuration to field.zod.ts - Added comprehensive tests for all new schemas - Generated documentation automatically Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- content/docs/references/data/field.mdx | 38 +- content/docs/references/integration/misc.mdx | 17 +- .../references/integration/object-storage.mdx | 34 + content/docs/references/system/index.mdx | 1 + content/docs/references/system/meta.json | 1 + .../docs/references/system/object-storage.mdx | 240 ++++++ packages/spec/json-schema/data/Field.json | 214 ++++++ .../data/FileAttachmentConfig.json | 219 ++++++ packages/spec/json-schema/data/Object.json | 214 ++++++ .../system/AccessControlConfig.json | 106 +++ .../spec/json-schema/system/BucketConfig.json | 332 +++++++++ .../json-schema/system/LifecycleAction.json | 15 + .../system/LifecyclePolicyConfig.json | 84 +++ .../system/LifecyclePolicyRule.json | 68 ++ .../system/MultipartUploadConfig.json | 49 ++ .../json-schema/system/ObjectMetadata.json | 90 +++ .../system/ObjectStorageConfig.json | 444 +++++++++++ .../system/PresignedUrlConfig.json | 49 ++ .../spec/json-schema/system/StorageAcl.json | 18 + .../spec/json-schema/system/StorageClass.json | 17 + .../json-schema/system/StorageConnection.json | 62 ++ .../json-schema/system/StorageProvider.json | 21 + .../spec/json-schema/ui/FieldWidgetProps.json | 214 ++++++ packages/spec/src/data/field.test.ts | 319 +++++++- packages/spec/src/data/field.zod.ts | 92 +++ packages/spec/src/system/index.ts | 3 + .../spec/src/system/object-storage.test.ts | 704 ++++++++++++++++++ .../spec/src/system/object-storage.zod.ts | 583 +++++++++++++++ 28 files changed, 4230 insertions(+), 18 deletions(-) create mode 100644 content/docs/references/integration/object-storage.mdx create mode 100644 content/docs/references/system/object-storage.mdx create mode 100644 packages/spec/json-schema/data/FileAttachmentConfig.json create mode 100644 packages/spec/json-schema/system/AccessControlConfig.json create mode 100644 packages/spec/json-schema/system/BucketConfig.json create mode 100644 packages/spec/json-schema/system/LifecycleAction.json create mode 100644 packages/spec/json-schema/system/LifecyclePolicyConfig.json create mode 100644 packages/spec/json-schema/system/LifecyclePolicyRule.json create mode 100644 packages/spec/json-schema/system/MultipartUploadConfig.json create mode 100644 packages/spec/json-schema/system/ObjectMetadata.json create mode 100644 packages/spec/json-schema/system/ObjectStorageConfig.json create mode 100644 packages/spec/json-schema/system/PresignedUrlConfig.json create mode 100644 packages/spec/json-schema/system/StorageAcl.json create mode 100644 packages/spec/json-schema/system/StorageClass.json create mode 100644 packages/spec/json-schema/system/StorageConnection.json create mode 100644 packages/spec/json-schema/system/StorageProvider.json create mode 100644 packages/spec/src/system/object-storage.test.ts create mode 100644 packages/spec/src/system/object-storage.zod.ts diff --git a/content/docs/references/data/field.mdx b/content/docs/references/data/field.mdx index 1a9c5ae5c..43975793d 100644 --- a/content/docs/references/data/field.mdx +++ b/content/docs/references/data/field.mdx @@ -12,8 +12,8 @@ description: Field protocol schemas ## TypeScript Usage ```typescript -import { AddressSchema, CurrencyConfigSchema, CurrencyValueSchema, FieldSchema, FieldTypeSchema, LocationCoordinatesSchema, SelectOptionSchema, VectorConfigSchema } from '@objectstack/spec/data'; -import type { Address, CurrencyConfig, CurrencyValue, Field, FieldType, LocationCoordinates, SelectOption, VectorConfig } from '@objectstack/spec/data'; +import { AddressSchema, CurrencyConfigSchema, CurrencyValueSchema, FieldSchema, FieldTypeSchema, FileAttachmentConfigSchema, LocationCoordinatesSchema, SelectOptionSchema, VectorConfigSchema } from '@objectstack/spec/data'; +import type { Address, CurrencyConfig, CurrencyValue, Field, FieldType, FileAttachmentConfig, LocationCoordinates, SelectOption, VectorConfig } from '@objectstack/spec/data'; // Validate data const result = AddressSchema.parse(data); @@ -110,6 +110,7 @@ const result = AddressSchema.parse(data); | **allowScanning** | `boolean` | optional | Enable camera scanning for barcode/QR code input | | **currencyConfig** | `object` | optional | Configuration for currency field type | | **vectorConfig** | `object` | optional | Configuration for vector field type (AI/ML embeddings) | +| **fileAttachmentConfig** | `object` | optional | Configuration for file and attachment field types | | **hidden** | `boolean` | optional | Hidden from default UI | | **readonly** | `boolean` | optional | Read-only in UI | | **encryption** | `boolean` | optional | Encrypt at rest | @@ -169,6 +170,39 @@ const result = AddressSchema.parse(data); --- +## FileAttachmentConfig + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **minSize** | `number` | optional | Minimum file size in bytes | +| **maxSize** | `number` | optional | Maximum file size in bytes (e.g., 10485760 = 10MB) | +| **allowedTypes** | `string[]` | optional | Allowed file extensions (e.g., [".pdf", ".docx", ".jpg"]) | +| **blockedTypes** | `string[]` | optional | Blocked file extensions (e.g., [".exe", ".bat", ".sh"]) | +| **allowedMimeTypes** | `string[]` | optional | Allowed MIME types (e.g., ["image/jpeg", "application/pdf"]) | +| **blockedMimeTypes** | `string[]` | optional | Blocked MIME types | +| **virusScan** | `boolean` | optional | Enable virus scanning for uploaded files | +| **virusScanProvider** | `Enum<'clamav' \| 'virustotal' \| 'metadefender' \| 'custom'>` | optional | Virus scanning service provider | +| **virusScanOnUpload** | `boolean` | optional | Scan files immediately on upload | +| **quarantineOnThreat** | `boolean` | optional | Quarantine files if threat detected | +| **storageProvider** | `string` | optional | Object storage provider name (references ObjectStorageConfig) | +| **storageBucket** | `string` | optional | Target bucket name | +| **storagePrefix** | `string` | optional | Storage path prefix (e.g., "uploads/documents/") | +| **imageValidation** | `object` | optional | Image-specific validation rules | +| **allowMultiple** | `boolean` | optional | Allow multiple file uploads (overrides field.multiple) | +| **allowReplace** | `boolean` | optional | Allow replacing existing files | +| **allowDelete** | `boolean` | optional | Allow deleting uploaded files | +| **requireUpload** | `boolean` | optional | Require at least one file when field is required | +| **extractMetadata** | `boolean` | optional | Extract file metadata (name, size, type, etc.) | +| **extractText** | `boolean` | optional | Extract text content from documents (OCR/parsing) | +| **versioningEnabled** | `boolean` | optional | Keep previous versions of replaced files | +| **maxVersions** | `number` | optional | Maximum number of versions to retain | +| **publicRead** | `boolean` | optional | Allow public read access to uploaded files | +| **presignedUrlExpiry** | `number` | optional | Presigned URL expiration in seconds (default: 1 hour) | + +--- + ## LocationCoordinates ### Properties diff --git a/content/docs/references/integration/misc.mdx b/content/docs/references/integration/misc.mdx index a002a9d45..a11f69cce 100644 --- a/content/docs/references/integration/misc.mdx +++ b/content/docs/references/integration/misc.mdx @@ -12,8 +12,8 @@ description: Misc protocol schemas ## TypeScript Usage ```typescript -import { AckModeSchema, ApiVersionConfigSchema, CdcConfigSchema, ConsumerConfigSchema, DatabaseConnectorSchema, DatabasePoolConfigSchema, DatabaseProviderSchema, DatabaseTableSchema, DeliveryGuaranteeSchema, DlqConfigSchema, FileAccessPatternSchema, FileFilterConfigSchema, FileMetadataConfigSchema, FileStorageConnectorSchema, FileStorageProviderSchema, FileVersioningConfigSchema, MessageFormatSchema, MessageQueueConnectorSchema, MessageQueueProviderSchema, MultipartUploadConfigSchema, ProducerConfigSchema, SaasConnectorSchema, SaasObjectTypeSchema, SaasProviderSchema, SslConfigSchema, StorageBucketSchema, TopicQueueSchema } from '@objectstack/spec/integration'; -import type { AckMode, ApiVersionConfig, CdcConfig, ConsumerConfig, DatabaseConnector, DatabasePoolConfig, DatabaseProvider, DatabaseTable, DeliveryGuarantee, DlqConfig, FileAccessPattern, FileFilterConfig, FileMetadataConfig, FileStorageConnector, FileStorageProvider, FileVersioningConfig, MessageFormat, MessageQueueConnector, MessageQueueProvider, MultipartUploadConfig, ProducerConfig, SaasConnector, SaasObjectType, SaasProvider, SslConfig, StorageBucket, TopicQueue } from '@objectstack/spec/integration'; +import { AckModeSchema, ApiVersionConfigSchema, CdcConfigSchema, ConsumerConfigSchema, DatabaseConnectorSchema, DatabasePoolConfigSchema, DatabaseProviderSchema, DatabaseTableSchema, DeliveryGuaranteeSchema, DlqConfigSchema, FileAccessPatternSchema, FileFilterConfigSchema, FileMetadataConfigSchema, FileStorageConnectorSchema, FileStorageProviderSchema, FileVersioningConfigSchema, MessageFormatSchema, MessageQueueConnectorSchema, MessageQueueProviderSchema, ProducerConfigSchema, SaasConnectorSchema, SaasObjectTypeSchema, SaasProviderSchema, SslConfigSchema, StorageBucketSchema, TopicQueueSchema } from '@objectstack/spec/integration'; +import type { AckMode, ApiVersionConfig, CdcConfig, ConsumerConfig, DatabaseConnector, DatabasePoolConfig, DatabaseProvider, DatabaseTable, DeliveryGuarantee, DlqConfig, FileAccessPattern, FileFilterConfig, FileMetadataConfig, FileStorageConnector, FileStorageProvider, FileVersioningConfig, MessageFormat, MessageQueueConnector, MessageQueueProvider, ProducerConfig, SaasConnector, SaasObjectType, SaasProvider, SslConfig, StorageBucket, TopicQueue } from '@objectstack/spec/integration'; // Validate data const result = AckModeSchema.parse(data); @@ -374,19 +374,6 @@ Message queue provider type --- -## MultipartUploadConfig - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **enabled** | `boolean` | optional | Enable multipart uploads | -| **partSize** | `number` | optional | Part size in bytes (min 5MB) | -| **maxConcurrentParts** | `number` | optional | Maximum concurrent part uploads | -| **threshold** | `number` | optional | File size threshold for multipart upload in bytes | - ---- - ## ProducerConfig ### Properties diff --git a/content/docs/references/integration/object-storage.mdx b/content/docs/references/integration/object-storage.mdx new file mode 100644 index 000000000..a4e27d250 --- /dev/null +++ b/content/docs/references/integration/object-storage.mdx @@ -0,0 +1,34 @@ +--- +title: Object Storage +description: Object Storage protocol schemas +--- + +# Object Storage + + +**Source:** `packages/spec/src/integration/object-storage.zod.ts` + + +## TypeScript Usage + +```typescript +import { MultipartUploadConfigSchema } from '@objectstack/spec/integration'; +import type { MultipartUploadConfig } from '@objectstack/spec/integration'; + +// Validate data +const result = MultipartUploadConfigSchema.parse(data); +``` + +--- + +## MultipartUploadConfig + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional | Enable multipart uploads | +| **partSize** | `number` | optional | Part size in bytes (min 5MB) | +| **maxConcurrentParts** | `number` | optional | Maximum concurrent part uploads | +| **threshold** | `number` | optional | File size threshold for multipart upload in bytes | + diff --git a/content/docs/references/system/index.mdx b/content/docs/references/system/index.mdx index 7d17cb63c..a0bbcbf76 100644 --- a/content/docs/references/system/index.mdx +++ b/content/docs/references/system/index.mdx @@ -21,6 +21,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 64790a4f5..83f0400ce 100644 --- a/content/docs/references/system/meta.json +++ b/content/docs/references/system/meta.json @@ -14,6 +14,7 @@ "logging", "manifest", "metrics", + "object-storage", "plugin", "plugin-capability", "scoped-storage", diff --git a/content/docs/references/system/object-storage.mdx b/content/docs/references/system/object-storage.mdx new file mode 100644 index 000000000..5857b4351 --- /dev/null +++ b/content/docs/references/system/object-storage.mdx @@ -0,0 +1,240 @@ +--- +title: Object Storage +description: Object Storage protocol schemas +--- + +# Object Storage + + +**Source:** `packages/spec/src/system/object-storage.zod.ts` + + +## TypeScript Usage + +```typescript +import { AccessControlConfigSchema, BucketConfigSchema, LifecycleActionSchema, LifecyclePolicyConfigSchema, LifecyclePolicyRuleSchema, MultipartUploadConfigSchema, ObjectMetadataSchema, ObjectStorageConfigSchema, PresignedUrlConfigSchema, StorageAclSchema, StorageClassSchema, StorageConnectionSchema, StorageProviderSchema } from '@objectstack/spec/system'; +import type { AccessControlConfig, BucketConfig, LifecycleAction, LifecyclePolicyConfig, LifecyclePolicyRule, MultipartUploadConfig, ObjectMetadata, ObjectStorageConfig, PresignedUrlConfig, StorageAcl, StorageClass, StorageConnection, StorageProvider } from '@objectstack/spec/system'; + +// Validate data +const result = AccessControlConfigSchema.parse(data); +``` + +--- + +## AccessControlConfig + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **acl** | `Enum<'private' \| 'public_read' \| 'public_read_write' \| 'authenticated_read' \| 'bucket_owner_read' \| 'bucket_owner_full_control'>` | optional | Default access control level | +| **allowedOrigins** | `string[]` | optional | CORS allowed origins | +| **allowedMethods** | `Enum<'GET' \| 'PUT' \| 'POST' \| 'DELETE' \| 'HEAD'>[]` | optional | CORS allowed HTTP methods | +| **allowedHeaders** | `string[]` | optional | CORS allowed headers | +| **exposeHeaders** | `string[]` | optional | CORS exposed headers | +| **maxAge** | `number` | optional | CORS preflight cache duration in seconds | +| **corsEnabled** | `boolean` | optional | Enable CORS configuration | +| **publicAccess** | `object` | optional | Public access control | +| **ipWhitelist** | `string[]` | optional | Allowed IP addresses/CIDR blocks | +| **ipBlacklist** | `string[]` | optional | Blocked IP addresses/CIDR blocks | + +--- + +## BucketConfig + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Bucket identifier in ObjectStack (snake_case) | +| **label** | `string` | ✅ | Display label | +| **bucketName** | `string` | ✅ | Actual bucket/container name in storage provider | +| **region** | `string` | optional | Storage region (e.g., us-east-1, westus) | +| **provider** | `Enum<'s3' \| 'azure_blob' \| 'gcs' \| 'minio' \| 'r2' \| 'spaces' \| 'wasabi' \| 'backblaze' \| 'local'>` | ✅ | Storage provider | +| **endpoint** | `string` | optional | Custom endpoint URL (for S3-compatible providers) | +| **pathStyle** | `boolean` | optional | Use path-style URLs (for S3-compatible providers) | +| **versioning** | `boolean` | optional | Enable object versioning | +| **encryption** | `object` | optional | Server-side encryption configuration | +| **accessControl** | `object` | optional | Access control configuration | +| **lifecyclePolicy** | `object` | optional | Lifecycle policy configuration | +| **multipartConfig** | `object` | optional | Multipart upload configuration | +| **tags** | `Record` | optional | Bucket tags for organization | +| **description** | `string` | optional | Bucket description | +| **enabled** | `boolean` | optional | Enable this bucket | + +--- + +## LifecycleAction + +Lifecycle policy action type + +### Allowed Values + +* `transition` +* `delete` +* `abort` + +--- + +## LifecyclePolicyConfig + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional | Enable lifecycle policies | +| **rules** | `object[]` | optional | Lifecycle rules | + +--- + +## LifecyclePolicyRule + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Rule identifier | +| **enabled** | `boolean` | optional | Enable this rule | +| **action** | `Enum<'transition' \| 'delete' \| 'abort'>` | ✅ | Action to perform | +| **prefix** | `string` | optional | Object key prefix filter (e.g., "uploads/") | +| **tags** | `Record` | optional | Object tag filters | +| **daysAfterCreation** | `number` | optional | Days after object creation | +| **daysAfterModification** | `number` | optional | Days after last modification | +| **targetStorageClass** | `Enum<'standard' \| 'intelligent' \| 'infrequent_access' \| 'glacier' \| 'deep_archive'>` | optional | Target storage class for transition action | + +--- + +## MultipartUploadConfig + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional | Enable multipart uploads | +| **partSize** | `number` | optional | Part size in bytes (min 5MB, max 5GB) | +| **maxParts** | `number` | optional | Maximum number of parts (max 10,000) | +| **threshold** | `number` | optional | File size threshold to trigger multipart upload (bytes) | +| **maxConcurrent** | `number` | optional | Maximum concurrent part uploads | +| **abortIncompleteAfterDays** | `number` | optional | Auto-abort incomplete uploads after N days | + +--- + +## ObjectMetadata + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **contentType** | `string` | ✅ | MIME type (e.g., image/jpeg, application/pdf) | +| **contentLength** | `number` | ✅ | File size in bytes | +| **contentEncoding** | `string` | optional | Content encoding (e.g., gzip) | +| **contentDisposition** | `string` | optional | Content disposition header | +| **contentLanguage** | `string` | optional | Content language | +| **cacheControl** | `string` | optional | Cache control directives | +| **etag** | `string` | optional | Entity tag for versioning/caching | +| **lastModified** | `string` | optional | Last modification timestamp | +| **versionId** | `string` | optional | Object version identifier | +| **storageClass** | `Enum<'standard' \| 'intelligent' \| 'infrequent_access' \| 'glacier' \| 'deep_archive'>` | optional | Storage class/tier | +| **encryption** | `object` | optional | Server-side encryption configuration | +| **custom** | `Record` | optional | Custom user-defined metadata | + +--- + +## ObjectStorageConfig + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Storage configuration identifier | +| **label** | `string` | ✅ | Display label | +| **provider** | `Enum<'s3' \| 'azure_blob' \| 'gcs' \| 'minio' \| 'r2' \| 'spaces' \| 'wasabi' \| 'backblaze' \| 'local'>` | ✅ | Primary storage provider | +| **connection** | `object` | ✅ | Connection credentials | +| **buckets** | `object[]` | optional | Configured buckets | +| **defaultBucket** | `string` | optional | Default bucket name for operations | +| **enabled** | `boolean` | optional | Enable this storage configuration | +| **description** | `string` | optional | Configuration description | + +--- + +## PresignedUrlConfig + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **operation** | `Enum<'get' \| 'put' \| 'delete' \| 'head'>` | ✅ | Allowed operation | +| **expiresIn** | `number` | ✅ | Expiration time in seconds (max 7 days) | +| **contentType** | `string` | optional | Required content type for PUT operations | +| **maxSize** | `number` | optional | Maximum file size in bytes for PUT operations | +| **responseContentType** | `string` | optional | Override content-type for GET operations | +| **responseContentDisposition** | `string` | optional | Override content-disposition for GET operations | + +--- + +## StorageAcl + +Storage access control level + +### Allowed Values + +* `private` +* `public_read` +* `public_read_write` +* `authenticated_read` +* `bucket_owner_read` +* `bucket_owner_full_control` + +--- + +## StorageClass + +Storage class/tier for cost optimization + +### Allowed Values + +* `standard` +* `intelligent` +* `infrequent_access` +* `glacier` +* `deep_archive` + +--- + +## StorageConnection + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **accessKeyId** | `string` | optional | AWS access key ID or MinIO access key | +| **secretAccessKey** | `string` | optional | AWS secret access key or MinIO secret key | +| **sessionToken** | `string` | optional | AWS session token for temporary credentials | +| **accountName** | `string` | optional | Azure storage account name | +| **accountKey** | `string` | optional | Azure storage account key | +| **sasToken** | `string` | optional | Azure SAS token | +| **projectId** | `string` | optional | GCP project ID | +| **credentials** | `string` | optional | GCP service account credentials JSON | +| **endpoint** | `string` | optional | Custom endpoint URL | +| **region** | `string` | optional | Default region | +| **useSSL** | `boolean` | optional | Use SSL/TLS for connections | +| **timeout** | `number` | optional | Connection timeout in milliseconds | + +--- + +## StorageProvider + +Storage provider type + +### Allowed Values + +* `s3` +* `azure_blob` +* `gcs` +* `minio` +* `r2` +* `spaces` +* `wasabi` +* `backblaze` +* `local` + diff --git a/packages/spec/json-schema/data/Field.json b/packages/spec/json-schema/data/Field.json index 18a074f82..fbb00ca3a 100644 --- a/packages/spec/json-schema/data/Field.json +++ b/packages/spec/json-schema/data/Field.json @@ -391,6 +391,220 @@ "additionalProperties": false, "description": "Configuration for vector field type (AI/ML embeddings)" }, + "fileAttachmentConfig": { + "type": "object", + "properties": { + "minSize": { + "type": "number", + "minimum": 0, + "description": "Minimum file size in bytes" + }, + "maxSize": { + "type": "number", + "minimum": 1, + "description": "Maximum file size in bytes (e.g., 10485760 = 10MB)" + }, + "allowedTypes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Allowed file extensions (e.g., [\".pdf\", \".docx\", \".jpg\"])" + }, + "blockedTypes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Blocked file extensions (e.g., [\".exe\", \".bat\", \".sh\"])" + }, + "allowedMimeTypes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Allowed MIME types (e.g., [\"image/jpeg\", \"application/pdf\"])" + }, + "blockedMimeTypes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Blocked MIME types" + }, + "virusScan": { + "type": "boolean", + "default": false, + "description": "Enable virus scanning for uploaded files" + }, + "virusScanProvider": { + "type": "string", + "enum": [ + "clamav", + "virustotal", + "metadefender", + "custom" + ], + "description": "Virus scanning service provider" + }, + "virusScanOnUpload": { + "type": "boolean", + "default": true, + "description": "Scan files immediately on upload" + }, + "quarantineOnThreat": { + "type": "boolean", + "default": true, + "description": "Quarantine files if threat detected" + }, + "storageProvider": { + "type": "string", + "description": "Object storage provider name (references ObjectStorageConfig)" + }, + "storageBucket": { + "type": "string", + "description": "Target bucket name" + }, + "storagePrefix": { + "type": "string", + "description": "Storage path prefix (e.g., \"uploads/documents/\")" + }, + "imageValidation": { + "type": "object", + "properties": { + "minWidth": { + "type": "number", + "minimum": 1, + "description": "Minimum image width in pixels" + }, + "maxWidth": { + "type": "number", + "minimum": 1, + "description": "Maximum image width in pixels" + }, + "minHeight": { + "type": "number", + "minimum": 1, + "description": "Minimum image height in pixels" + }, + "maxHeight": { + "type": "number", + "minimum": 1, + "description": "Maximum image height in pixels" + }, + "aspectRatio": { + "type": "string", + "description": "Required aspect ratio (e.g., \"16:9\", \"1:1\")" + }, + "generateThumbnails": { + "type": "boolean", + "default": false, + "description": "Auto-generate thumbnails" + }, + "thumbnailSizes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Thumbnail variant name (e.g., \"small\", \"medium\", \"large\")" + }, + "width": { + "type": "number", + "minimum": 1, + "description": "Thumbnail width in pixels" + }, + "height": { + "type": "number", + "minimum": 1, + "description": "Thumbnail height in pixels" + }, + "crop": { + "type": "boolean", + "default": false, + "description": "Crop to exact dimensions" + } + }, + "required": [ + "name", + "width", + "height" + ], + "additionalProperties": false + }, + "description": "Thumbnail size configurations" + }, + "preserveMetadata": { + "type": "boolean", + "default": false, + "description": "Preserve EXIF metadata" + }, + "autoRotate": { + "type": "boolean", + "default": true, + "description": "Auto-rotate based on EXIF orientation" + } + }, + "additionalProperties": false, + "description": "Image-specific validation rules" + }, + "allowMultiple": { + "type": "boolean", + "default": false, + "description": "Allow multiple file uploads (overrides field.multiple)" + }, + "allowReplace": { + "type": "boolean", + "default": true, + "description": "Allow replacing existing files" + }, + "allowDelete": { + "type": "boolean", + "default": true, + "description": "Allow deleting uploaded files" + }, + "requireUpload": { + "type": "boolean", + "default": false, + "description": "Require at least one file when field is required" + }, + "extractMetadata": { + "type": "boolean", + "default": true, + "description": "Extract file metadata (name, size, type, etc.)" + }, + "extractText": { + "type": "boolean", + "default": false, + "description": "Extract text content from documents (OCR/parsing)" + }, + "versioningEnabled": { + "type": "boolean", + "default": false, + "description": "Keep previous versions of replaced files" + }, + "maxVersions": { + "type": "number", + "minimum": 1, + "description": "Maximum number of versions to retain" + }, + "publicRead": { + "type": "boolean", + "default": false, + "description": "Allow public read access to uploaded files" + }, + "presignedUrlExpiry": { + "type": "number", + "minimum": 60, + "maximum": 604800, + "default": 3600, + "description": "Presigned URL expiration in seconds (default: 1 hour)" + } + }, + "additionalProperties": false, + "description": "Configuration for file and attachment field types" + }, "hidden": { "type": "boolean", "default": false, diff --git a/packages/spec/json-schema/data/FileAttachmentConfig.json b/packages/spec/json-schema/data/FileAttachmentConfig.json new file mode 100644 index 000000000..8f4eb33be --- /dev/null +++ b/packages/spec/json-schema/data/FileAttachmentConfig.json @@ -0,0 +1,219 @@ +{ + "$ref": "#/definitions/FileAttachmentConfig", + "definitions": { + "FileAttachmentConfig": { + "type": "object", + "properties": { + "minSize": { + "type": "number", + "minimum": 0, + "description": "Minimum file size in bytes" + }, + "maxSize": { + "type": "number", + "minimum": 1, + "description": "Maximum file size in bytes (e.g., 10485760 = 10MB)" + }, + "allowedTypes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Allowed file extensions (e.g., [\".pdf\", \".docx\", \".jpg\"])" + }, + "blockedTypes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Blocked file extensions (e.g., [\".exe\", \".bat\", \".sh\"])" + }, + "allowedMimeTypes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Allowed MIME types (e.g., [\"image/jpeg\", \"application/pdf\"])" + }, + "blockedMimeTypes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Blocked MIME types" + }, + "virusScan": { + "type": "boolean", + "default": false, + "description": "Enable virus scanning for uploaded files" + }, + "virusScanProvider": { + "type": "string", + "enum": [ + "clamav", + "virustotal", + "metadefender", + "custom" + ], + "description": "Virus scanning service provider" + }, + "virusScanOnUpload": { + "type": "boolean", + "default": true, + "description": "Scan files immediately on upload" + }, + "quarantineOnThreat": { + "type": "boolean", + "default": true, + "description": "Quarantine files if threat detected" + }, + "storageProvider": { + "type": "string", + "description": "Object storage provider name (references ObjectStorageConfig)" + }, + "storageBucket": { + "type": "string", + "description": "Target bucket name" + }, + "storagePrefix": { + "type": "string", + "description": "Storage path prefix (e.g., \"uploads/documents/\")" + }, + "imageValidation": { + "type": "object", + "properties": { + "minWidth": { + "type": "number", + "minimum": 1, + "description": "Minimum image width in pixels" + }, + "maxWidth": { + "type": "number", + "minimum": 1, + "description": "Maximum image width in pixels" + }, + "minHeight": { + "type": "number", + "minimum": 1, + "description": "Minimum image height in pixels" + }, + "maxHeight": { + "type": "number", + "minimum": 1, + "description": "Maximum image height in pixels" + }, + "aspectRatio": { + "type": "string", + "description": "Required aspect ratio (e.g., \"16:9\", \"1:1\")" + }, + "generateThumbnails": { + "type": "boolean", + "default": false, + "description": "Auto-generate thumbnails" + }, + "thumbnailSizes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Thumbnail variant name (e.g., \"small\", \"medium\", \"large\")" + }, + "width": { + "type": "number", + "minimum": 1, + "description": "Thumbnail width in pixels" + }, + "height": { + "type": "number", + "minimum": 1, + "description": "Thumbnail height in pixels" + }, + "crop": { + "type": "boolean", + "default": false, + "description": "Crop to exact dimensions" + } + }, + "required": [ + "name", + "width", + "height" + ], + "additionalProperties": false + }, + "description": "Thumbnail size configurations" + }, + "preserveMetadata": { + "type": "boolean", + "default": false, + "description": "Preserve EXIF metadata" + }, + "autoRotate": { + "type": "boolean", + "default": true, + "description": "Auto-rotate based on EXIF orientation" + } + }, + "additionalProperties": false, + "description": "Image-specific validation rules" + }, + "allowMultiple": { + "type": "boolean", + "default": false, + "description": "Allow multiple file uploads (overrides field.multiple)" + }, + "allowReplace": { + "type": "boolean", + "default": true, + "description": "Allow replacing existing files" + }, + "allowDelete": { + "type": "boolean", + "default": true, + "description": "Allow deleting uploaded files" + }, + "requireUpload": { + "type": "boolean", + "default": false, + "description": "Require at least one file when field is required" + }, + "extractMetadata": { + "type": "boolean", + "default": true, + "description": "Extract file metadata (name, size, type, etc.)" + }, + "extractText": { + "type": "boolean", + "default": false, + "description": "Extract text content from documents (OCR/parsing)" + }, + "versioningEnabled": { + "type": "boolean", + "default": false, + "description": "Keep previous versions of replaced files" + }, + "maxVersions": { + "type": "number", + "minimum": 1, + "description": "Maximum number of versions to retain" + }, + "publicRead": { + "type": "boolean", + "default": false, + "description": "Allow public read access to uploaded files" + }, + "presignedUrlExpiry": { + "type": "number", + "minimum": 60, + "maximum": 604800, + "default": 3600, + "description": "Presigned URL expiration in seconds (default: 1 hour)" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/data/Object.json b/packages/spec/json-schema/data/Object.json index 155ba679c..fbc645dd2 100644 --- a/packages/spec/json-schema/data/Object.json +++ b/packages/spec/json-schema/data/Object.json @@ -448,6 +448,220 @@ "additionalProperties": false, "description": "Configuration for vector field type (AI/ML embeddings)" }, + "fileAttachmentConfig": { + "type": "object", + "properties": { + "minSize": { + "type": "number", + "minimum": 0, + "description": "Minimum file size in bytes" + }, + "maxSize": { + "type": "number", + "minimum": 1, + "description": "Maximum file size in bytes (e.g., 10485760 = 10MB)" + }, + "allowedTypes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Allowed file extensions (e.g., [\".pdf\", \".docx\", \".jpg\"])" + }, + "blockedTypes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Blocked file extensions (e.g., [\".exe\", \".bat\", \".sh\"])" + }, + "allowedMimeTypes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Allowed MIME types (e.g., [\"image/jpeg\", \"application/pdf\"])" + }, + "blockedMimeTypes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Blocked MIME types" + }, + "virusScan": { + "type": "boolean", + "default": false, + "description": "Enable virus scanning for uploaded files" + }, + "virusScanProvider": { + "type": "string", + "enum": [ + "clamav", + "virustotal", + "metadefender", + "custom" + ], + "description": "Virus scanning service provider" + }, + "virusScanOnUpload": { + "type": "boolean", + "default": true, + "description": "Scan files immediately on upload" + }, + "quarantineOnThreat": { + "type": "boolean", + "default": true, + "description": "Quarantine files if threat detected" + }, + "storageProvider": { + "type": "string", + "description": "Object storage provider name (references ObjectStorageConfig)" + }, + "storageBucket": { + "type": "string", + "description": "Target bucket name" + }, + "storagePrefix": { + "type": "string", + "description": "Storage path prefix (e.g., \"uploads/documents/\")" + }, + "imageValidation": { + "type": "object", + "properties": { + "minWidth": { + "type": "number", + "minimum": 1, + "description": "Minimum image width in pixels" + }, + "maxWidth": { + "type": "number", + "minimum": 1, + "description": "Maximum image width in pixels" + }, + "minHeight": { + "type": "number", + "minimum": 1, + "description": "Minimum image height in pixels" + }, + "maxHeight": { + "type": "number", + "minimum": 1, + "description": "Maximum image height in pixels" + }, + "aspectRatio": { + "type": "string", + "description": "Required aspect ratio (e.g., \"16:9\", \"1:1\")" + }, + "generateThumbnails": { + "type": "boolean", + "default": false, + "description": "Auto-generate thumbnails" + }, + "thumbnailSizes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Thumbnail variant name (e.g., \"small\", \"medium\", \"large\")" + }, + "width": { + "type": "number", + "minimum": 1, + "description": "Thumbnail width in pixels" + }, + "height": { + "type": "number", + "minimum": 1, + "description": "Thumbnail height in pixels" + }, + "crop": { + "type": "boolean", + "default": false, + "description": "Crop to exact dimensions" + } + }, + "required": [ + "name", + "width", + "height" + ], + "additionalProperties": false + }, + "description": "Thumbnail size configurations" + }, + "preserveMetadata": { + "type": "boolean", + "default": false, + "description": "Preserve EXIF metadata" + }, + "autoRotate": { + "type": "boolean", + "default": true, + "description": "Auto-rotate based on EXIF orientation" + } + }, + "additionalProperties": false, + "description": "Image-specific validation rules" + }, + "allowMultiple": { + "type": "boolean", + "default": false, + "description": "Allow multiple file uploads (overrides field.multiple)" + }, + "allowReplace": { + "type": "boolean", + "default": true, + "description": "Allow replacing existing files" + }, + "allowDelete": { + "type": "boolean", + "default": true, + "description": "Allow deleting uploaded files" + }, + "requireUpload": { + "type": "boolean", + "default": false, + "description": "Require at least one file when field is required" + }, + "extractMetadata": { + "type": "boolean", + "default": true, + "description": "Extract file metadata (name, size, type, etc.)" + }, + "extractText": { + "type": "boolean", + "default": false, + "description": "Extract text content from documents (OCR/parsing)" + }, + "versioningEnabled": { + "type": "boolean", + "default": false, + "description": "Keep previous versions of replaced files" + }, + "maxVersions": { + "type": "number", + "minimum": 1, + "description": "Maximum number of versions to retain" + }, + "publicRead": { + "type": "boolean", + "default": false, + "description": "Allow public read access to uploaded files" + }, + "presignedUrlExpiry": { + "type": "number", + "minimum": 60, + "maximum": 604800, + "default": 3600, + "description": "Presigned URL expiration in seconds (default: 1 hour)" + } + }, + "additionalProperties": false, + "description": "Configuration for file and attachment field types" + }, "hidden": { "type": "boolean", "default": false, diff --git a/packages/spec/json-schema/system/AccessControlConfig.json b/packages/spec/json-schema/system/AccessControlConfig.json new file mode 100644 index 000000000..e95212181 --- /dev/null +++ b/packages/spec/json-schema/system/AccessControlConfig.json @@ -0,0 +1,106 @@ +{ + "$ref": "#/definitions/AccessControlConfig", + "definitions": { + "AccessControlConfig": { + "type": "object", + "properties": { + "acl": { + "type": "string", + "enum": [ + "private", + "public_read", + "public_read_write", + "authenticated_read", + "bucket_owner_read", + "bucket_owner_full_control" + ], + "description": "Default access control level", + "default": "private" + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string" + }, + "description": "CORS allowed origins" + }, + "allowedMethods": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "GET", + "PUT", + "POST", + "DELETE", + "HEAD" + ] + }, + "description": "CORS allowed HTTP methods" + }, + "allowedHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "description": "CORS allowed headers" + }, + "exposeHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "description": "CORS exposed headers" + }, + "maxAge": { + "type": "number", + "minimum": 0, + "description": "CORS preflight cache duration in seconds" + }, + "corsEnabled": { + "type": "boolean", + "default": false, + "description": "Enable CORS configuration" + }, + "publicAccess": { + "type": "object", + "properties": { + "allowPublicRead": { + "type": "boolean", + "default": false, + "description": "Allow public read access" + }, + "allowPublicWrite": { + "type": "boolean", + "default": false, + "description": "Allow public write access" + }, + "allowPublicList": { + "type": "boolean", + "default": false, + "description": "Allow public bucket listing" + } + }, + "additionalProperties": false, + "description": "Public access control" + }, + "ipWhitelist": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Allowed IP addresses/CIDR blocks" + }, + "ipBlacklist": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Blocked IP addresses/CIDR blocks" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/system/BucketConfig.json b/packages/spec/json-schema/system/BucketConfig.json new file mode 100644 index 000000000..377f88cde --- /dev/null +++ b/packages/spec/json-schema/system/BucketConfig.json @@ -0,0 +1,332 @@ +{ + "$ref": "#/definitions/BucketConfig", + "definitions": { + "BucketConfig": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 2, + "pattern": "^[a-z][a-z0-9_.]*$", + "description": "Bucket identifier in ObjectStack (snake_case)" + }, + "label": { + "type": "string", + "description": "Display label" + }, + "bucketName": { + "type": "string", + "description": "Actual bucket/container name in storage provider" + }, + "region": { + "type": "string", + "description": "Storage region (e.g., us-east-1, westus)" + }, + "provider": { + "type": "string", + "enum": [ + "s3", + "azure_blob", + "gcs", + "minio", + "r2", + "spaces", + "wasabi", + "backblaze", + "local" + ], + "description": "Storage provider" + }, + "endpoint": { + "type": "string", + "description": "Custom endpoint URL (for S3-compatible providers)" + }, + "pathStyle": { + "type": "boolean", + "default": false, + "description": "Use path-style URLs (for S3-compatible providers)" + }, + "versioning": { + "type": "boolean", + "default": false, + "description": "Enable object versioning" + }, + "encryption": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable server-side encryption" + }, + "algorithm": { + "type": "string", + "enum": [ + "AES256", + "aws:kms", + "azure:kms", + "gcp:kms" + ], + "default": "AES256", + "description": "Encryption algorithm" + }, + "kmsKeyId": { + "type": "string", + "description": "KMS key ID for managed encryption" + } + }, + "additionalProperties": false, + "description": "Server-side encryption configuration" + }, + "accessControl": { + "type": "object", + "properties": { + "acl": { + "type": "string", + "enum": [ + "private", + "public_read", + "public_read_write", + "authenticated_read", + "bucket_owner_read", + "bucket_owner_full_control" + ], + "description": "Default access control level", + "default": "private" + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string" + }, + "description": "CORS allowed origins" + }, + "allowedMethods": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "GET", + "PUT", + "POST", + "DELETE", + "HEAD" + ] + }, + "description": "CORS allowed HTTP methods" + }, + "allowedHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "description": "CORS allowed headers" + }, + "exposeHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "description": "CORS exposed headers" + }, + "maxAge": { + "type": "number", + "minimum": 0, + "description": "CORS preflight cache duration in seconds" + }, + "corsEnabled": { + "type": "boolean", + "default": false, + "description": "Enable CORS configuration" + }, + "publicAccess": { + "type": "object", + "properties": { + "allowPublicRead": { + "type": "boolean", + "default": false, + "description": "Allow public read access" + }, + "allowPublicWrite": { + "type": "boolean", + "default": false, + "description": "Allow public write access" + }, + "allowPublicList": { + "type": "boolean", + "default": false, + "description": "Allow public bucket listing" + } + }, + "additionalProperties": false, + "description": "Public access control" + }, + "ipWhitelist": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Allowed IP addresses/CIDR blocks" + }, + "ipBlacklist": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Blocked IP addresses/CIDR blocks" + } + }, + "additionalProperties": false, + "description": "Access control configuration" + }, + "lifecyclePolicy": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable lifecycle policies" + }, + "rules": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 2, + "pattern": "^[a-z][a-z0-9_.]*$", + "description": "Rule identifier" + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable this rule" + }, + "action": { + "type": "string", + "enum": [ + "transition", + "delete", + "abort" + ], + "description": "Action to perform" + }, + "prefix": { + "type": "string", + "description": "Object key prefix filter (e.g., \"uploads/\")" + }, + "tags": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Object tag filters" + }, + "daysAfterCreation": { + "type": "number", + "minimum": 0, + "description": "Days after object creation" + }, + "daysAfterModification": { + "type": "number", + "minimum": 0, + "description": "Days after last modification" + }, + "targetStorageClass": { + "type": "string", + "enum": [ + "standard", + "intelligent", + "infrequent_access", + "glacier", + "deep_archive" + ], + "description": "Target storage class for transition action" + } + }, + "required": [ + "id", + "action" + ], + "additionalProperties": false + }, + "default": [], + "description": "Lifecycle rules" + } + }, + "additionalProperties": false, + "description": "Lifecycle policy configuration" + }, + "multipartConfig": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable multipart uploads" + }, + "partSize": { + "type": "number", + "minimum": 5242880, + "maximum": 5368709120, + "default": 10485760, + "description": "Part size in bytes (min 5MB, max 5GB)" + }, + "maxParts": { + "type": "number", + "minimum": 1, + "maximum": 10000, + "default": 10000, + "description": "Maximum number of parts (max 10,000)" + }, + "threshold": { + "type": "number", + "minimum": 0, + "default": 104857600, + "description": "File size threshold to trigger multipart upload (bytes)" + }, + "maxConcurrent": { + "type": "number", + "minimum": 1, + "maximum": 100, + "default": 4, + "description": "Maximum concurrent part uploads" + }, + "abortIncompleteAfterDays": { + "type": "number", + "minimum": 1, + "description": "Auto-abort incomplete uploads after N days" + } + }, + "additionalProperties": false, + "description": "Multipart upload configuration" + }, + "tags": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Bucket tags for organization" + }, + "description": { + "type": "string", + "description": "Bucket description" + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable this bucket" + } + }, + "required": [ + "name", + "label", + "bucketName", + "provider" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/system/LifecycleAction.json b/packages/spec/json-schema/system/LifecycleAction.json new file mode 100644 index 000000000..4d503f49b --- /dev/null +++ b/packages/spec/json-schema/system/LifecycleAction.json @@ -0,0 +1,15 @@ +{ + "$ref": "#/definitions/LifecycleAction", + "definitions": { + "LifecycleAction": { + "type": "string", + "enum": [ + "transition", + "delete", + "abort" + ], + "description": "Lifecycle policy action type" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/system/LifecyclePolicyConfig.json b/packages/spec/json-schema/system/LifecyclePolicyConfig.json new file mode 100644 index 000000000..69ca2bc6c --- /dev/null +++ b/packages/spec/json-schema/system/LifecyclePolicyConfig.json @@ -0,0 +1,84 @@ +{ + "$ref": "#/definitions/LifecyclePolicyConfig", + "definitions": { + "LifecyclePolicyConfig": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable lifecycle policies" + }, + "rules": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 2, + "pattern": "^[a-z][a-z0-9_.]*$", + "description": "Rule identifier" + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable this rule" + }, + "action": { + "type": "string", + "enum": [ + "transition", + "delete", + "abort" + ], + "description": "Action to perform" + }, + "prefix": { + "type": "string", + "description": "Object key prefix filter (e.g., \"uploads/\")" + }, + "tags": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Object tag filters" + }, + "daysAfterCreation": { + "type": "number", + "minimum": 0, + "description": "Days after object creation" + }, + "daysAfterModification": { + "type": "number", + "minimum": 0, + "description": "Days after last modification" + }, + "targetStorageClass": { + "type": "string", + "enum": [ + "standard", + "intelligent", + "infrequent_access", + "glacier", + "deep_archive" + ], + "description": "Target storage class for transition action" + } + }, + "required": [ + "id", + "action" + ], + "additionalProperties": false + }, + "default": [], + "description": "Lifecycle rules" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/system/LifecyclePolicyRule.json b/packages/spec/json-schema/system/LifecyclePolicyRule.json new file mode 100644 index 000000000..86ee8a8f2 --- /dev/null +++ b/packages/spec/json-schema/system/LifecyclePolicyRule.json @@ -0,0 +1,68 @@ +{ + "$ref": "#/definitions/LifecyclePolicyRule", + "definitions": { + "LifecyclePolicyRule": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 2, + "pattern": "^[a-z][a-z0-9_.]*$", + "description": "Rule identifier" + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable this rule" + }, + "action": { + "type": "string", + "enum": [ + "transition", + "delete", + "abort" + ], + "description": "Action to perform" + }, + "prefix": { + "type": "string", + "description": "Object key prefix filter (e.g., \"uploads/\")" + }, + "tags": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Object tag filters" + }, + "daysAfterCreation": { + "type": "number", + "minimum": 0, + "description": "Days after object creation" + }, + "daysAfterModification": { + "type": "number", + "minimum": 0, + "description": "Days after last modification" + }, + "targetStorageClass": { + "type": "string", + "enum": [ + "standard", + "intelligent", + "infrequent_access", + "glacier", + "deep_archive" + ], + "description": "Target storage class for transition action" + } + }, + "required": [ + "id", + "action" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/system/MultipartUploadConfig.json b/packages/spec/json-schema/system/MultipartUploadConfig.json new file mode 100644 index 000000000..7389c9fd7 --- /dev/null +++ b/packages/spec/json-schema/system/MultipartUploadConfig.json @@ -0,0 +1,49 @@ +{ + "$ref": "#/definitions/MultipartUploadConfig", + "definitions": { + "MultipartUploadConfig": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable multipart uploads" + }, + "partSize": { + "type": "number", + "minimum": 5242880, + "maximum": 5368709120, + "default": 10485760, + "description": "Part size in bytes (min 5MB, max 5GB)" + }, + "maxParts": { + "type": "number", + "minimum": 1, + "maximum": 10000, + "default": 10000, + "description": "Maximum number of parts (max 10,000)" + }, + "threshold": { + "type": "number", + "minimum": 0, + "default": 104857600, + "description": "File size threshold to trigger multipart upload (bytes)" + }, + "maxConcurrent": { + "type": "number", + "minimum": 1, + "maximum": 100, + "default": 4, + "description": "Maximum concurrent part uploads" + }, + "abortIncompleteAfterDays": { + "type": "number", + "minimum": 1, + "description": "Auto-abort incomplete uploads after N days" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/system/ObjectMetadata.json b/packages/spec/json-schema/system/ObjectMetadata.json new file mode 100644 index 000000000..891db14a4 --- /dev/null +++ b/packages/spec/json-schema/system/ObjectMetadata.json @@ -0,0 +1,90 @@ +{ + "$ref": "#/definitions/ObjectMetadata", + "definitions": { + "ObjectMetadata": { + "type": "object", + "properties": { + "contentType": { + "type": "string", + "description": "MIME type (e.g., image/jpeg, application/pdf)" + }, + "contentLength": { + "type": "number", + "minimum": 0, + "description": "File size in bytes" + }, + "contentEncoding": { + "type": "string", + "description": "Content encoding (e.g., gzip)" + }, + "contentDisposition": { + "type": "string", + "description": "Content disposition header" + }, + "contentLanguage": { + "type": "string", + "description": "Content language" + }, + "cacheControl": { + "type": "string", + "description": "Cache control directives" + }, + "etag": { + "type": "string", + "description": "Entity tag for versioning/caching" + }, + "lastModified": { + "type": "string", + "format": "date-time", + "description": "Last modification timestamp" + }, + "versionId": { + "type": "string", + "description": "Object version identifier" + }, + "storageClass": { + "type": "string", + "enum": [ + "standard", + "intelligent", + "infrequent_access", + "glacier", + "deep_archive" + ], + "description": "Storage class/tier" + }, + "encryption": { + "type": "object", + "properties": { + "algorithm": { + "type": "string", + "description": "Encryption algorithm (e.g., AES256, aws:kms)" + }, + "keyId": { + "type": "string", + "description": "KMS key ID if using managed encryption" + } + }, + "required": [ + "algorithm" + ], + "additionalProperties": false, + "description": "Server-side encryption configuration" + }, + "custom": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Custom user-defined metadata" + } + }, + "required": [ + "contentType", + "contentLength" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/system/ObjectStorageConfig.json b/packages/spec/json-schema/system/ObjectStorageConfig.json new file mode 100644 index 000000000..3b04da9c1 --- /dev/null +++ b/packages/spec/json-schema/system/ObjectStorageConfig.json @@ -0,0 +1,444 @@ +{ + "$ref": "#/definitions/ObjectStorageConfig", + "definitions": { + "ObjectStorageConfig": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 2, + "pattern": "^[a-z][a-z0-9_.]*$", + "description": "Storage configuration identifier" + }, + "label": { + "type": "string", + "description": "Display label" + }, + "provider": { + "type": "string", + "enum": [ + "s3", + "azure_blob", + "gcs", + "minio", + "r2", + "spaces", + "wasabi", + "backblaze", + "local" + ], + "description": "Primary storage provider" + }, + "connection": { + "type": "object", + "properties": { + "accessKeyId": { + "type": "string", + "description": "AWS access key ID or MinIO access key" + }, + "secretAccessKey": { + "type": "string", + "description": "AWS secret access key or MinIO secret key" + }, + "sessionToken": { + "type": "string", + "description": "AWS session token for temporary credentials" + }, + "accountName": { + "type": "string", + "description": "Azure storage account name" + }, + "accountKey": { + "type": "string", + "description": "Azure storage account key" + }, + "sasToken": { + "type": "string", + "description": "Azure SAS token" + }, + "projectId": { + "type": "string", + "description": "GCP project ID" + }, + "credentials": { + "type": "string", + "description": "GCP service account credentials JSON" + }, + "endpoint": { + "type": "string", + "description": "Custom endpoint URL" + }, + "region": { + "type": "string", + "description": "Default region" + }, + "useSSL": { + "type": "boolean", + "default": true, + "description": "Use SSL/TLS for connections" + }, + "timeout": { + "type": "number", + "minimum": 0, + "description": "Connection timeout in milliseconds" + } + }, + "additionalProperties": false, + "description": "Connection credentials" + }, + "buckets": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 2, + "pattern": "^[a-z][a-z0-9_.]*$", + "description": "Bucket identifier in ObjectStack (snake_case)" + }, + "label": { + "type": "string", + "description": "Display label" + }, + "bucketName": { + "type": "string", + "description": "Actual bucket/container name in storage provider" + }, + "region": { + "type": "string", + "description": "Storage region (e.g., us-east-1, westus)" + }, + "provider": { + "type": "string", + "enum": [ + "s3", + "azure_blob", + "gcs", + "minio", + "r2", + "spaces", + "wasabi", + "backblaze", + "local" + ], + "description": "Storage provider" + }, + "endpoint": { + "type": "string", + "description": "Custom endpoint URL (for S3-compatible providers)" + }, + "pathStyle": { + "type": "boolean", + "default": false, + "description": "Use path-style URLs (for S3-compatible providers)" + }, + "versioning": { + "type": "boolean", + "default": false, + "description": "Enable object versioning" + }, + "encryption": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable server-side encryption" + }, + "algorithm": { + "type": "string", + "enum": [ + "AES256", + "aws:kms", + "azure:kms", + "gcp:kms" + ], + "default": "AES256", + "description": "Encryption algorithm" + }, + "kmsKeyId": { + "type": "string", + "description": "KMS key ID for managed encryption" + } + }, + "additionalProperties": false, + "description": "Server-side encryption configuration" + }, + "accessControl": { + "type": "object", + "properties": { + "acl": { + "type": "string", + "enum": [ + "private", + "public_read", + "public_read_write", + "authenticated_read", + "bucket_owner_read", + "bucket_owner_full_control" + ], + "description": "Default access control level", + "default": "private" + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string" + }, + "description": "CORS allowed origins" + }, + "allowedMethods": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "GET", + "PUT", + "POST", + "DELETE", + "HEAD" + ] + }, + "description": "CORS allowed HTTP methods" + }, + "allowedHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "description": "CORS allowed headers" + }, + "exposeHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "description": "CORS exposed headers" + }, + "maxAge": { + "type": "number", + "minimum": 0, + "description": "CORS preflight cache duration in seconds" + }, + "corsEnabled": { + "type": "boolean", + "default": false, + "description": "Enable CORS configuration" + }, + "publicAccess": { + "type": "object", + "properties": { + "allowPublicRead": { + "type": "boolean", + "default": false, + "description": "Allow public read access" + }, + "allowPublicWrite": { + "type": "boolean", + "default": false, + "description": "Allow public write access" + }, + "allowPublicList": { + "type": "boolean", + "default": false, + "description": "Allow public bucket listing" + } + }, + "additionalProperties": false, + "description": "Public access control" + }, + "ipWhitelist": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Allowed IP addresses/CIDR blocks" + }, + "ipBlacklist": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Blocked IP addresses/CIDR blocks" + } + }, + "additionalProperties": false, + "description": "Access control configuration" + }, + "lifecyclePolicy": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable lifecycle policies" + }, + "rules": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 2, + "pattern": "^[a-z][a-z0-9_.]*$", + "description": "Rule identifier" + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable this rule" + }, + "action": { + "type": "string", + "enum": [ + "transition", + "delete", + "abort" + ], + "description": "Action to perform" + }, + "prefix": { + "type": "string", + "description": "Object key prefix filter (e.g., \"uploads/\")" + }, + "tags": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Object tag filters" + }, + "daysAfterCreation": { + "type": "number", + "minimum": 0, + "description": "Days after object creation" + }, + "daysAfterModification": { + "type": "number", + "minimum": 0, + "description": "Days after last modification" + }, + "targetStorageClass": { + "type": "string", + "enum": [ + "standard", + "intelligent", + "infrequent_access", + "glacier", + "deep_archive" + ], + "description": "Target storage class for transition action" + } + }, + "required": [ + "id", + "action" + ], + "additionalProperties": false + }, + "default": [], + "description": "Lifecycle rules" + } + }, + "additionalProperties": false, + "description": "Lifecycle policy configuration" + }, + "multipartConfig": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable multipart uploads" + }, + "partSize": { + "type": "number", + "minimum": 5242880, + "maximum": 5368709120, + "default": 10485760, + "description": "Part size in bytes (min 5MB, max 5GB)" + }, + "maxParts": { + "type": "number", + "minimum": 1, + "maximum": 10000, + "default": 10000, + "description": "Maximum number of parts (max 10,000)" + }, + "threshold": { + "type": "number", + "minimum": 0, + "default": 104857600, + "description": "File size threshold to trigger multipart upload (bytes)" + }, + "maxConcurrent": { + "type": "number", + "minimum": 1, + "maximum": 100, + "default": 4, + "description": "Maximum concurrent part uploads" + }, + "abortIncompleteAfterDays": { + "type": "number", + "minimum": 1, + "description": "Auto-abort incomplete uploads after N days" + } + }, + "additionalProperties": false, + "description": "Multipart upload configuration" + }, + "tags": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Bucket tags for organization" + }, + "description": { + "type": "string", + "description": "Bucket description" + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable this bucket" + } + }, + "required": [ + "name", + "label", + "bucketName", + "provider" + ], + "additionalProperties": false + }, + "default": [], + "description": "Configured buckets" + }, + "defaultBucket": { + "type": "string", + "description": "Default bucket name for operations" + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable this storage configuration" + }, + "description": { + "type": "string", + "description": "Configuration description" + } + }, + "required": [ + "name", + "label", + "provider", + "connection" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/system/PresignedUrlConfig.json b/packages/spec/json-schema/system/PresignedUrlConfig.json new file mode 100644 index 000000000..09e6afd58 --- /dev/null +++ b/packages/spec/json-schema/system/PresignedUrlConfig.json @@ -0,0 +1,49 @@ +{ + "$ref": "#/definitions/PresignedUrlConfig", + "definitions": { + "PresignedUrlConfig": { + "type": "object", + "properties": { + "operation": { + "type": "string", + "enum": [ + "get", + "put", + "delete", + "head" + ], + "description": "Allowed operation" + }, + "expiresIn": { + "type": "number", + "minimum": 1, + "maximum": 604800, + "description": "Expiration time in seconds (max 7 days)" + }, + "contentType": { + "type": "string", + "description": "Required content type for PUT operations" + }, + "maxSize": { + "type": "number", + "minimum": 0, + "description": "Maximum file size in bytes for PUT operations" + }, + "responseContentType": { + "type": "string", + "description": "Override content-type for GET operations" + }, + "responseContentDisposition": { + "type": "string", + "description": "Override content-disposition for GET operations" + } + }, + "required": [ + "operation", + "expiresIn" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/system/StorageAcl.json b/packages/spec/json-schema/system/StorageAcl.json new file mode 100644 index 000000000..713497519 --- /dev/null +++ b/packages/spec/json-schema/system/StorageAcl.json @@ -0,0 +1,18 @@ +{ + "$ref": "#/definitions/StorageAcl", + "definitions": { + "StorageAcl": { + "type": "string", + "enum": [ + "private", + "public_read", + "public_read_write", + "authenticated_read", + "bucket_owner_read", + "bucket_owner_full_control" + ], + "description": "Storage access control level" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/system/StorageClass.json b/packages/spec/json-schema/system/StorageClass.json new file mode 100644 index 000000000..158e2215c --- /dev/null +++ b/packages/spec/json-schema/system/StorageClass.json @@ -0,0 +1,17 @@ +{ + "$ref": "#/definitions/StorageClass", + "definitions": { + "StorageClass": { + "type": "string", + "enum": [ + "standard", + "intelligent", + "infrequent_access", + "glacier", + "deep_archive" + ], + "description": "Storage class/tier for cost optimization" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/system/StorageConnection.json b/packages/spec/json-schema/system/StorageConnection.json new file mode 100644 index 000000000..f69626b4e --- /dev/null +++ b/packages/spec/json-schema/system/StorageConnection.json @@ -0,0 +1,62 @@ +{ + "$ref": "#/definitions/StorageConnection", + "definitions": { + "StorageConnection": { + "type": "object", + "properties": { + "accessKeyId": { + "type": "string", + "description": "AWS access key ID or MinIO access key" + }, + "secretAccessKey": { + "type": "string", + "description": "AWS secret access key or MinIO secret key" + }, + "sessionToken": { + "type": "string", + "description": "AWS session token for temporary credentials" + }, + "accountName": { + "type": "string", + "description": "Azure storage account name" + }, + "accountKey": { + "type": "string", + "description": "Azure storage account key" + }, + "sasToken": { + "type": "string", + "description": "Azure SAS token" + }, + "projectId": { + "type": "string", + "description": "GCP project ID" + }, + "credentials": { + "type": "string", + "description": "GCP service account credentials JSON" + }, + "endpoint": { + "type": "string", + "description": "Custom endpoint URL" + }, + "region": { + "type": "string", + "description": "Default region" + }, + "useSSL": { + "type": "boolean", + "default": true, + "description": "Use SSL/TLS for connections" + }, + "timeout": { + "type": "number", + "minimum": 0, + "description": "Connection timeout in milliseconds" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/system/StorageProvider.json b/packages/spec/json-schema/system/StorageProvider.json new file mode 100644 index 000000000..c2984bb7d --- /dev/null +++ b/packages/spec/json-schema/system/StorageProvider.json @@ -0,0 +1,21 @@ +{ + "$ref": "#/definitions/StorageProvider", + "definitions": { + "StorageProvider": { + "type": "string", + "enum": [ + "s3", + "azure_blob", + "gcs", + "minio", + "r2", + "spaces", + "wasabi", + "backblaze", + "local" + ], + "description": "Storage provider type" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/ui/FieldWidgetProps.json b/packages/spec/json-schema/ui/FieldWidgetProps.json index 097b5db93..b9862fddb 100644 --- a/packages/spec/json-schema/ui/FieldWidgetProps.json +++ b/packages/spec/json-schema/ui/FieldWidgetProps.json @@ -411,6 +411,220 @@ "additionalProperties": false, "description": "Configuration for vector field type (AI/ML embeddings)" }, + "fileAttachmentConfig": { + "type": "object", + "properties": { + "minSize": { + "type": "number", + "minimum": 0, + "description": "Minimum file size in bytes" + }, + "maxSize": { + "type": "number", + "minimum": 1, + "description": "Maximum file size in bytes (e.g., 10485760 = 10MB)" + }, + "allowedTypes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Allowed file extensions (e.g., [\".pdf\", \".docx\", \".jpg\"])" + }, + "blockedTypes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Blocked file extensions (e.g., [\".exe\", \".bat\", \".sh\"])" + }, + "allowedMimeTypes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Allowed MIME types (e.g., [\"image/jpeg\", \"application/pdf\"])" + }, + "blockedMimeTypes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Blocked MIME types" + }, + "virusScan": { + "type": "boolean", + "default": false, + "description": "Enable virus scanning for uploaded files" + }, + "virusScanProvider": { + "type": "string", + "enum": [ + "clamav", + "virustotal", + "metadefender", + "custom" + ], + "description": "Virus scanning service provider" + }, + "virusScanOnUpload": { + "type": "boolean", + "default": true, + "description": "Scan files immediately on upload" + }, + "quarantineOnThreat": { + "type": "boolean", + "default": true, + "description": "Quarantine files if threat detected" + }, + "storageProvider": { + "type": "string", + "description": "Object storage provider name (references ObjectStorageConfig)" + }, + "storageBucket": { + "type": "string", + "description": "Target bucket name" + }, + "storagePrefix": { + "type": "string", + "description": "Storage path prefix (e.g., \"uploads/documents/\")" + }, + "imageValidation": { + "type": "object", + "properties": { + "minWidth": { + "type": "number", + "minimum": 1, + "description": "Minimum image width in pixels" + }, + "maxWidth": { + "type": "number", + "minimum": 1, + "description": "Maximum image width in pixels" + }, + "minHeight": { + "type": "number", + "minimum": 1, + "description": "Minimum image height in pixels" + }, + "maxHeight": { + "type": "number", + "minimum": 1, + "description": "Maximum image height in pixels" + }, + "aspectRatio": { + "type": "string", + "description": "Required aspect ratio (e.g., \"16:9\", \"1:1\")" + }, + "generateThumbnails": { + "type": "boolean", + "default": false, + "description": "Auto-generate thumbnails" + }, + "thumbnailSizes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Thumbnail variant name (e.g., \"small\", \"medium\", \"large\")" + }, + "width": { + "type": "number", + "minimum": 1, + "description": "Thumbnail width in pixels" + }, + "height": { + "type": "number", + "minimum": 1, + "description": "Thumbnail height in pixels" + }, + "crop": { + "type": "boolean", + "default": false, + "description": "Crop to exact dimensions" + } + }, + "required": [ + "name", + "width", + "height" + ], + "additionalProperties": false + }, + "description": "Thumbnail size configurations" + }, + "preserveMetadata": { + "type": "boolean", + "default": false, + "description": "Preserve EXIF metadata" + }, + "autoRotate": { + "type": "boolean", + "default": true, + "description": "Auto-rotate based on EXIF orientation" + } + }, + "additionalProperties": false, + "description": "Image-specific validation rules" + }, + "allowMultiple": { + "type": "boolean", + "default": false, + "description": "Allow multiple file uploads (overrides field.multiple)" + }, + "allowReplace": { + "type": "boolean", + "default": true, + "description": "Allow replacing existing files" + }, + "allowDelete": { + "type": "boolean", + "default": true, + "description": "Allow deleting uploaded files" + }, + "requireUpload": { + "type": "boolean", + "default": false, + "description": "Require at least one file when field is required" + }, + "extractMetadata": { + "type": "boolean", + "default": true, + "description": "Extract file metadata (name, size, type, etc.)" + }, + "extractText": { + "type": "boolean", + "default": false, + "description": "Extract text content from documents (OCR/parsing)" + }, + "versioningEnabled": { + "type": "boolean", + "default": false, + "description": "Keep previous versions of replaced files" + }, + "maxVersions": { + "type": "number", + "minimum": 1, + "description": "Maximum number of versions to retain" + }, + "publicRead": { + "type": "boolean", + "default": false, + "description": "Allow public read access to uploaded files" + }, + "presignedUrlExpiry": { + "type": "number", + "minimum": 60, + "maximum": 604800, + "default": 3600, + "description": "Presigned URL expiration in seconds (default: 1 hour)" + } + }, + "additionalProperties": false, + "description": "Configuration for file and attachment field types" + }, "hidden": { "type": "boolean", "default": false, diff --git a/packages/spec/src/data/field.test.ts b/packages/spec/src/data/field.test.ts index 0f93b6200..69358f915 100644 --- a/packages/spec/src/data/field.test.ts +++ b/packages/spec/src/data/field.test.ts @@ -6,11 +6,13 @@ import { CurrencyConfigSchema, CurrencyValueSchema, VectorConfigSchema, + FileAttachmentConfigSchema, Field, type SelectOption, type CurrencyConfig, type CurrencyValue, - type VectorConfig + type VectorConfig, + type FileAttachmentConfig, } from './field.zod'; describe('FieldType', () => { @@ -1015,4 +1017,319 @@ describe('Field Factory Helpers', () => { expect(ragField.vectorConfig?.indexType).toBe('hnsw'); }); }); + + describe('FileAttachmentConfigSchema', () => { + it('should accept minimal config', () => { + const config = FileAttachmentConfigSchema.parse({}); + + expect(config.virusScan).toBe(false); + expect(config.virusScanOnUpload).toBe(true); + expect(config.quarantineOnThreat).toBe(true); + expect(config.allowMultiple).toBe(false); + expect(config.allowReplace).toBe(true); + expect(config.allowDelete).toBe(true); + expect(config.requireUpload).toBe(false); + expect(config.extractMetadata).toBe(true); + expect(config.extractText).toBe(false); + expect(config.versioningEnabled).toBe(false); + expect(config.publicRead).toBe(false); + expect(config.presignedUrlExpiry).toBe(3600); + }); + + it('should accept file size limits', () => { + const config = FileAttachmentConfigSchema.parse({ + minSize: 1024, + maxSize: 10485760, // 10MB + }); + + expect(config.minSize).toBe(1024); + expect(config.maxSize).toBe(10485760); + }); + + it('should accept allowed file types', () => { + const config = FileAttachmentConfigSchema.parse({ + allowedTypes: ['.pdf', '.docx', '.xlsx'], + allowedMimeTypes: ['application/pdf', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'], + }); + + expect(config.allowedTypes).toHaveLength(3); + expect(config.allowedMimeTypes).toHaveLength(2); + }); + + it('should accept blocked file types', () => { + const config = FileAttachmentConfigSchema.parse({ + blockedTypes: ['.exe', '.bat', '.sh'], + blockedMimeTypes: ['application/x-executable'], + }); + + expect(config.blockedTypes).toHaveLength(3); + expect(config.blockedMimeTypes).toHaveLength(1); + }); + + it('should accept virus scanning configuration', () => { + const config = FileAttachmentConfigSchema.parse({ + virusScan: true, + virusScanProvider: 'clamav', + virusScanOnUpload: true, + quarantineOnThreat: true, + }); + + expect(config.virusScan).toBe(true); + expect(config.virusScanProvider).toBe('clamav'); + expect(config.virusScanOnUpload).toBe(true); + expect(config.quarantineOnThreat).toBe(true); + }); + + it('should accept all virus scan providers', () => { + const providers = ['clamav', 'virustotal', 'metadefender', 'custom'] as const; + + providers.forEach(provider => { + const config = { + virusScan: true, + virusScanProvider: provider, + }; + + expect(() => FileAttachmentConfigSchema.parse(config)).not.toThrow(); + }); + }); + + it('should accept storage configuration', () => { + const config = FileAttachmentConfigSchema.parse({ + storageProvider: 'aws_s3_storage', + storageBucket: 'user_uploads', + storagePrefix: 'documents/', + }); + + expect(config.storageProvider).toBe('aws_s3_storage'); + expect(config.storageBucket).toBe('user_uploads'); + expect(config.storagePrefix).toBe('documents/'); + }); + + it('should accept image validation config', () => { + const config = FileAttachmentConfigSchema.parse({ + imageValidation: { + minWidth: 100, + maxWidth: 4096, + minHeight: 100, + maxHeight: 4096, + aspectRatio: '16:9', + generateThumbnails: true, + thumbnailSizes: [ + { name: 'small', width: 150, height: 150, crop: true }, + { name: 'medium', width: 300, height: 300, crop: true }, + { name: 'large', width: 600, height: 600, crop: false }, + ], + preserveMetadata: false, + autoRotate: true, + }, + }); + + expect(config.imageValidation?.minWidth).toBe(100); + expect(config.imageValidation?.maxWidth).toBe(4096); + expect(config.imageValidation?.generateThumbnails).toBe(true); + expect(config.imageValidation?.thumbnailSizes).toHaveLength(3); + }); + + it('should accept upload behavior config', () => { + const config = FileAttachmentConfigSchema.parse({ + allowMultiple: true, + allowReplace: false, + allowDelete: false, + requireUpload: true, + }); + + expect(config.allowMultiple).toBe(true); + expect(config.allowReplace).toBe(false); + expect(config.allowDelete).toBe(false); + expect(config.requireUpload).toBe(true); + }); + + it('should accept metadata extraction config', () => { + const config = FileAttachmentConfigSchema.parse({ + extractMetadata: true, + extractText: true, + }); + + expect(config.extractMetadata).toBe(true); + expect(config.extractText).toBe(true); + }); + + it('should accept versioning config', () => { + const config = FileAttachmentConfigSchema.parse({ + versioningEnabled: true, + maxVersions: 10, + }); + + expect(config.versioningEnabled).toBe(true); + expect(config.maxVersions).toBe(10); + }); + + it('should accept access control config', () => { + const config = FileAttachmentConfigSchema.parse({ + publicRead: true, + presignedUrlExpiry: 1800, + }); + + expect(config.publicRead).toBe(true); + expect(config.presignedUrlExpiry).toBe(1800); + }); + + it('should enforce presigned URL expiry limits', () => { + // Min 60 seconds + expect(() => FileAttachmentConfigSchema.parse({ + presignedUrlExpiry: 59, + })).toThrow(); + + expect(() => FileAttachmentConfigSchema.parse({ + presignedUrlExpiry: 60, + })).not.toThrow(); + + // Max 7 days + expect(() => FileAttachmentConfigSchema.parse({ + presignedUrlExpiry: 604800, + })).not.toThrow(); + + expect(() => FileAttachmentConfigSchema.parse({ + presignedUrlExpiry: 604801, + })).toThrow(); + }); + + it('should validate file field with attachment config', () => { + const field = { + name: 'resume', + label: 'Resume', + type: 'file' as const, + required: true, + multiple: false, + fileAttachmentConfig: { + maxSize: 5242880, // 5MB + allowedTypes: ['.pdf', '.docx'], + virusScan: true, + virusScanProvider: 'clamav' as const, + storageProvider: 'main_storage', + storageBucket: 'user_uploads', + storagePrefix: 'resumes/', + }, + }; + + const result = FieldSchema.safeParse(field); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.fileAttachmentConfig?.maxSize).toBe(5242880); + expect(result.data.fileAttachmentConfig?.allowedTypes).toHaveLength(2); + expect(result.data.fileAttachmentConfig?.virusScan).toBe(true); + } + }); + + it('should validate image field with attachment config', () => { + const field = { + name: 'profile_picture', + label: 'Profile Picture', + type: 'image' as const, + required: false, + fileAttachmentConfig: { + maxSize: 2097152, // 2MB + allowedTypes: ['.jpg', '.jpeg', '.png', '.webp'], + imageValidation: { + minWidth: 200, + maxWidth: 2048, + minHeight: 200, + maxHeight: 2048, + aspectRatio: '1:1', + generateThumbnails: true, + thumbnailSizes: [ + { name: 'thumb', width: 100, height: 100, crop: true }, + { name: 'small', width: 200, height: 200, crop: true }, + ], + autoRotate: true, + }, + storageProvider: 'main_storage', + storageBucket: 'user_uploads', + storagePrefix: 'avatars/', + }, + }; + + const result = FieldSchema.safeParse(field); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.fileAttachmentConfig?.imageValidation?.aspectRatio).toBe('1:1'); + expect(result.data.fileAttachmentConfig?.imageValidation?.thumbnailSizes).toHaveLength(2); + } + }); + + it('should work with comprehensive file upload configuration', () => { + const field = { + name: 'contract_document', + label: 'Contract Document', + type: 'file' as const, + description: 'Upload signed contract (PDF only)', + required: true, + multiple: false, + fileAttachmentConfig: { + minSize: 1024, // 1KB + maxSize: 52428800, // 50MB + allowedTypes: ['.pdf'], + allowedMimeTypes: ['application/pdf'], + virusScan: true, + virusScanProvider: 'virustotal' as const, + virusScanOnUpload: true, + quarantineOnThreat: true, + storageProvider: 'secure_storage', + storageBucket: 'legal_documents', + storagePrefix: 'contracts/', + allowMultiple: false, + allowReplace: true, + allowDelete: false, + requireUpload: true, + extractMetadata: true, + extractText: true, + versioningEnabled: true, + maxVersions: 5, + publicRead: false, + presignedUrlExpiry: 3600, + }, + }; + + const result = FieldSchema.safeParse(field); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.type).toBe('file'); + expect(result.data.fileAttachmentConfig?.virusScan).toBe(true); + expect(result.data.fileAttachmentConfig?.versioningEnabled).toBe(true); + expect(result.data.fileAttachmentConfig?.extractText).toBe(true); + expect(result.data.fileAttachmentConfig?.allowDelete).toBe(false); + } + }); + + it('should reject negative file sizes', () => { + expect(() => FileAttachmentConfigSchema.parse({ + minSize: -1, + })).toThrow(); + + expect(() => FileAttachmentConfigSchema.parse({ + maxSize: 0, + })).toThrow(); + }); + + it('should reject invalid image dimensions', () => { + expect(() => FileAttachmentConfigSchema.parse({ + imageValidation: { + minWidth: 0, + }, + })).toThrow(); + + expect(() => FileAttachmentConfigSchema.parse({ + imageValidation: { + maxHeight: -100, + }, + })).toThrow(); + }); + + it('should reject invalid versioning config', () => { + expect(() => FileAttachmentConfigSchema.parse({ + versioningEnabled: true, + maxVersions: 0, + })).toThrow(); + }); + }); }); diff --git a/packages/spec/src/data/field.zod.ts b/packages/spec/src/data/field.zod.ts index 443aa5f5f..d7e9825b0 100644 --- a/packages/spec/src/data/field.zod.ts +++ b/packages/spec/src/data/field.zod.ts @@ -155,6 +155,94 @@ export const VectorConfigSchema = z.object({ indexType: z.enum(['hnsw', 'ivfflat', 'flat']).optional().describe('Vector index algorithm (HNSW for high accuracy, IVFFlat for large datasets)'), }); +/** + * File Attachment Configuration Schema + * Configuration for file and attachment field types + * + * Provides comprehensive file upload capabilities with: + * - File type restrictions (whitelist/blacklist) + * - File size limits (min/max) + * - Virus scanning integration + * - Storage provider integration + * - Image-specific features (dimensions, thumbnails) + * + * @example Basic file upload with size limit + * { + * maxSize: 10485760, // 10MB + * allowedTypes: ['.pdf', '.docx', '.xlsx'], + * virusScan: true + * } + * + * @example Image upload with validation + * { + * maxSize: 5242880, // 5MB + * allowedTypes: ['.jpg', '.jpeg', '.png', '.webp'], + * imageValidation: { + * maxWidth: 4096, + * maxHeight: 4096, + * generateThumbnails: true + * } + * } + */ +export const FileAttachmentConfigSchema = z.object({ + /** File Size Limits */ + minSize: z.number().min(0).optional().describe('Minimum file size in bytes'), + maxSize: z.number().min(1).optional().describe('Maximum file size in bytes (e.g., 10485760 = 10MB)'), + + /** File Type Restrictions */ + allowedTypes: z.array(z.string()).optional().describe('Allowed file extensions (e.g., [".pdf", ".docx", ".jpg"])'), + blockedTypes: z.array(z.string()).optional().describe('Blocked file extensions (e.g., [".exe", ".bat", ".sh"])'), + allowedMimeTypes: z.array(z.string()).optional().describe('Allowed MIME types (e.g., ["image/jpeg", "application/pdf"])'), + blockedMimeTypes: z.array(z.string()).optional().describe('Blocked MIME types'), + + /** Virus Scanning */ + virusScan: z.boolean().default(false).describe('Enable virus scanning for uploaded files'), + virusScanProvider: z.enum(['clamav', 'virustotal', 'metadefender', 'custom']).optional().describe('Virus scanning service provider'), + virusScanOnUpload: z.boolean().default(true).describe('Scan files immediately on upload'), + quarantineOnThreat: z.boolean().default(true).describe('Quarantine files if threat detected'), + + /** Storage Configuration */ + storageProvider: z.string().optional().describe('Object storage provider name (references ObjectStorageConfig)'), + storageBucket: z.string().optional().describe('Target bucket name'), + storagePrefix: z.string().optional().describe('Storage path prefix (e.g., "uploads/documents/")'), + + /** Image-Specific Validation */ + imageValidation: z.object({ + minWidth: z.number().min(1).optional().describe('Minimum image width in pixels'), + maxWidth: z.number().min(1).optional().describe('Maximum image width in pixels'), + minHeight: z.number().min(1).optional().describe('Minimum image height in pixels'), + maxHeight: z.number().min(1).optional().describe('Maximum image height in pixels'), + aspectRatio: z.string().optional().describe('Required aspect ratio (e.g., "16:9", "1:1")'), + generateThumbnails: z.boolean().default(false).describe('Auto-generate thumbnails'), + thumbnailSizes: z.array(z.object({ + name: z.string().describe('Thumbnail variant name (e.g., "small", "medium", "large")'), + width: z.number().min(1).describe('Thumbnail width in pixels'), + height: z.number().min(1).describe('Thumbnail height in pixels'), + crop: z.boolean().default(false).describe('Crop to exact dimensions'), + })).optional().describe('Thumbnail size configurations'), + preserveMetadata: z.boolean().default(false).describe('Preserve EXIF metadata'), + autoRotate: z.boolean().default(true).describe('Auto-rotate based on EXIF orientation'), + }).optional().describe('Image-specific validation rules'), + + /** Upload Behavior */ + allowMultiple: z.boolean().default(false).describe('Allow multiple file uploads (overrides field.multiple)'), + allowReplace: z.boolean().default(true).describe('Allow replacing existing files'), + allowDelete: z.boolean().default(true).describe('Allow deleting uploaded files'), + requireUpload: z.boolean().default(false).describe('Require at least one file when field is required'), + + /** Metadata Extraction */ + extractMetadata: z.boolean().default(true).describe('Extract file metadata (name, size, type, etc.)'), + extractText: z.boolean().default(false).describe('Extract text content from documents (OCR/parsing)'), + + /** Versioning */ + versioningEnabled: z.boolean().default(false).describe('Keep previous versions of replaced files'), + maxVersions: z.number().min(1).optional().describe('Maximum number of versions to retain'), + + /** Access Control */ + publicRead: z.boolean().default(false).describe('Allow public read access to uploaded files'), + presignedUrlExpiry: z.number().min(60).max(604800).default(3600).describe('Presigned URL expiration in seconds (default: 1 hour)'), +}); + /** * Field Schema - Best Practice Enterprise Pattern */ @@ -242,6 +330,9 @@ export const FieldSchema = z.object({ // Vector field config vectorConfig: VectorConfigSchema.optional().describe('Configuration for vector field type (AI/ML embeddings)'), + // File attachment field config + fileAttachmentConfig: FileAttachmentConfigSchema.optional().describe('Configuration for file and attachment field types'), + /** Security & Visibility */ hidden: z.boolean().default(false).describe('Hidden from default UI'), readonly: z.boolean().default(false).describe('Read-only in UI'), @@ -259,6 +350,7 @@ export type Address = z.infer; export type CurrencyConfig = z.infer; export type CurrencyValue = z.infer; export type VectorConfig = z.infer; +export type FileAttachmentConfig = z.infer; /** * Field Factory Helper diff --git a/packages/spec/src/system/index.ts b/packages/spec/src/system/index.ts index d7e866291..762ab187b 100644 --- a/packages/spec/src/system/index.ts +++ b/packages/spec/src/system/index.ts @@ -37,6 +37,9 @@ export * from './driver/postgres.zod'; // Data Engine Protocol export * from './data-engine.zod'; +// Object Storage Protocol +export * from './object-storage.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/object-storage.test.ts b/packages/spec/src/system/object-storage.test.ts new file mode 100644 index 000000000..b06d9a14c --- /dev/null +++ b/packages/spec/src/system/object-storage.test.ts @@ -0,0 +1,704 @@ +import { describe, it, expect } from 'vitest'; +import { + StorageProviderSchema, + StorageAclSchema, + StorageClassSchema, + LifecycleActionSchema, + ObjectMetadataSchema, + PresignedUrlConfigSchema, + MultipartUploadConfigSchema, + AccessControlConfigSchema, + LifecyclePolicyRuleSchema, + LifecyclePolicyConfigSchema, + BucketConfigSchema, + StorageConnectionSchema, + ObjectStorageConfigSchema, + type StorageProvider, + type StorageAcl, + type StorageClass, + type ObjectMetadata, + type PresignedUrlConfig, + type MultipartUploadConfig, + type AccessControlConfig, + type LifecyclePolicyRule, + type LifecyclePolicyConfig, + type BucketConfig, + type StorageConnection, + type ObjectStorageConfig, +} from './object-storage.zod'; + +describe('StorageProviderSchema', () => { + it('should accept valid storage providers', () => { + expect(() => StorageProviderSchema.parse('s3')).not.toThrow(); + expect(() => StorageProviderSchema.parse('azure_blob')).not.toThrow(); + expect(() => StorageProviderSchema.parse('gcs')).not.toThrow(); + expect(() => StorageProviderSchema.parse('minio')).not.toThrow(); + expect(() => StorageProviderSchema.parse('r2')).not.toThrow(); + }); + + it('should reject invalid storage providers', () => { + expect(() => StorageProviderSchema.parse('unknown')).toThrow(); + expect(() => StorageProviderSchema.parse('S3')).toThrow(); // case sensitive + }); +}); + +describe('StorageAclSchema', () => { + it('should accept valid ACL values', () => { + expect(() => StorageAclSchema.parse('private')).not.toThrow(); + expect(() => StorageAclSchema.parse('public_read')).not.toThrow(); + expect(() => StorageAclSchema.parse('bucket_owner_full_control')).not.toThrow(); + }); + + it('should reject invalid ACL values', () => { + expect(() => StorageAclSchema.parse('invalid_acl')).toThrow(); + }); +}); + +describe('StorageClassSchema', () => { + it('should accept valid storage classes', () => { + expect(() => StorageClassSchema.parse('standard')).not.toThrow(); + expect(() => StorageClassSchema.parse('intelligent')).not.toThrow(); + expect(() => StorageClassSchema.parse('glacier')).not.toThrow(); + expect(() => StorageClassSchema.parse('deep_archive')).not.toThrow(); + }); +}); + +describe('ObjectMetadataSchema', () => { + it('should accept minimal metadata', () => { + const metadata = ObjectMetadataSchema.parse({ + contentType: 'image/jpeg', + contentLength: 1024000, + }); + + expect(metadata.contentType).toBe('image/jpeg'); + expect(metadata.contentLength).toBe(1024000); + }); + + it('should accept full metadata with all fields', () => { + const metadata = ObjectMetadataSchema.parse({ + contentType: 'application/pdf', + contentLength: 2048000, + contentEncoding: 'gzip', + contentDisposition: 'attachment; filename="document.pdf"', + contentLanguage: 'en-US', + cacheControl: 'max-age=3600', + etag: '"abc123def456"', + lastModified: new Date('2024-01-01'), + versionId: 'v1.0', + storageClass: 'standard', + encryption: { + algorithm: 'AES256', + }, + custom: { + uploadedBy: 'user123', + department: 'marketing', + }, + }); + + expect(metadata.contentType).toBe('application/pdf'); + expect(metadata.custom?.uploadedBy).toBe('user123'); + }); + + it('should accept encryption with KMS key', () => { + const metadata = ObjectMetadataSchema.parse({ + contentType: 'text/plain', + contentLength: 100, + encryption: { + algorithm: 'aws:kms', + keyId: 'arn:aws:kms:us-east-1:123456789:key/abc-123', + }, + }); + + expect(metadata.encryption?.algorithm).toBe('aws:kms'); + expect(metadata.encryption?.keyId).toBeDefined(); + }); + + it('should reject negative content length', () => { + expect(() => ObjectMetadataSchema.parse({ + contentType: 'text/plain', + contentLength: -1, + })).toThrow(); + }); +}); + +describe('PresignedUrlConfigSchema', () => { + it('should accept GET operation config', () => { + const config = PresignedUrlConfigSchema.parse({ + operation: 'get', + expiresIn: 3600, + }); + + expect(config.operation).toBe('get'); + expect(config.expiresIn).toBe(3600); + }); + + it('should accept PUT operation with constraints', () => { + const config = PresignedUrlConfigSchema.parse({ + operation: 'put', + expiresIn: 900, + contentType: 'image/jpeg', + maxSize: 10485760, + }); + + expect(config.operation).toBe('put'); + expect(config.maxSize).toBe(10485760); + }); + + it('should accept response overrides', () => { + const config = PresignedUrlConfigSchema.parse({ + operation: 'get', + expiresIn: 1800, + responseContentType: 'application/octet-stream', + responseContentDisposition: 'attachment; filename="download.pdf"', + }); + + expect(config.responseContentType).toBe('application/octet-stream'); + }); + + it('should enforce max expiration of 7 days', () => { + expect(() => PresignedUrlConfigSchema.parse({ + operation: 'get', + expiresIn: 604800, // 7 days - should pass + })).not.toThrow(); + + expect(() => PresignedUrlConfigSchema.parse({ + operation: 'get', + expiresIn: 604801, // 7 days + 1 second - should fail + })).toThrow(); + }); + + it('should enforce minimum expiration', () => { + expect(() => PresignedUrlConfigSchema.parse({ + operation: 'get', + expiresIn: 0, + })).toThrow(); + }); +}); + +describe('MultipartUploadConfigSchema', () => { + it('should accept default config', () => { + const config = MultipartUploadConfigSchema.parse({}); + + expect(config.enabled).toBe(true); + expect(config.partSize).toBe(10 * 1024 * 1024); // 10MB + expect(config.maxParts).toBe(10000); + expect(config.threshold).toBe(100 * 1024 * 1024); // 100MB + expect(config.maxConcurrent).toBe(4); + }); + + it('should accept custom config', () => { + const config = MultipartUploadConfigSchema.parse({ + enabled: true, + partSize: 5 * 1024 * 1024, + maxParts: 5000, + threshold: 50 * 1024 * 1024, + maxConcurrent: 8, + abortIncompleteAfterDays: 7, + }); + + expect(config.partSize).toBe(5 * 1024 * 1024); + expect(config.abortIncompleteAfterDays).toBe(7); + }); + + it('should enforce minimum part size of 5MB', () => { + expect(() => MultipartUploadConfigSchema.parse({ + partSize: 5 * 1024 * 1024, // 5MB - should pass + })).not.toThrow(); + + expect(() => MultipartUploadConfigSchema.parse({ + partSize: 5 * 1024 * 1024 - 1, // Just under 5MB - should fail + })).toThrow(); + }); + + it('should enforce maximum part size of 5GB', () => { + expect(() => MultipartUploadConfigSchema.parse({ + partSize: 5 * 1024 * 1024 * 1024, // 5GB - should pass + })).not.toThrow(); + + expect(() => MultipartUploadConfigSchema.parse({ + partSize: 5 * 1024 * 1024 * 1024 + 1, // Just over 5GB - should fail + })).toThrow(); + }); + + it('should enforce max parts limit of 10000', () => { + expect(() => MultipartUploadConfigSchema.parse({ + maxParts: 10001, + })).toThrow(); + }); +}); + +describe('AccessControlConfigSchema', () => { + it('should accept default config', () => { + const config = AccessControlConfigSchema.parse({}); + + expect(config.acl).toBe('private'); + expect(config.corsEnabled).toBe(false); + }); + + it('should accept CORS configuration', () => { + const config = AccessControlConfigSchema.parse({ + acl: 'private', + corsEnabled: true, + allowedOrigins: ['https://app.example.com', 'https://www.example.com'], + allowedMethods: ['GET', 'PUT', 'POST'], + allowedHeaders: ['Content-Type', 'Authorization'], + exposeHeaders: ['ETag', 'Content-Length'], + maxAge: 3600, + }); + + expect(config.corsEnabled).toBe(true); + expect(config.allowedOrigins).toHaveLength(2); + expect(config.maxAge).toBe(3600); + }); + + it('should accept public access configuration', () => { + const config = AccessControlConfigSchema.parse({ + acl: 'public_read', + publicAccess: { + allowPublicRead: true, + allowPublicWrite: false, + allowPublicList: false, + }, + }); + + expect(config.publicAccess?.allowPublicRead).toBe(true); + expect(config.publicAccess?.allowPublicWrite).toBe(false); + }); + + it('should accept IP whitelist/blacklist', () => { + const config = AccessControlConfigSchema.parse({ + ipWhitelist: ['192.168.1.0/24', '10.0.0.1'], + ipBlacklist: ['1.2.3.4'], + }); + + expect(config.ipWhitelist).toHaveLength(2); + expect(config.ipBlacklist).toHaveLength(1); + }); +}); + +describe('LifecyclePolicyRuleSchema', () => { + it('should accept transition rule', () => { + const rule = LifecyclePolicyRuleSchema.parse({ + id: 'move_to_glacier', + enabled: true, + action: 'transition', + daysAfterCreation: 30, + targetStorageClass: 'glacier', + }); + + expect(rule.action).toBe('transition'); + expect(rule.targetStorageClass).toBe('glacier'); + }); + + it('should accept delete rule', () => { + const rule = LifecyclePolicyRuleSchema.parse({ + id: 'delete_old_files', + enabled: true, + action: 'delete', + daysAfterCreation: 365, + }); + + expect(rule.action).toBe('delete'); + expect(rule.daysAfterCreation).toBe(365); + }); + + it('should accept rule with prefix filter', () => { + const rule = LifecyclePolicyRuleSchema.parse({ + id: 'archive_temp', + enabled: true, + action: 'delete', + prefix: 'temp/', + daysAfterCreation: 7, + }); + + expect(rule.prefix).toBe('temp/'); + }); + + it('should accept rule with tag filters', () => { + const rule = LifecyclePolicyRuleSchema.parse({ + id: 'archive_tagged', + enabled: true, + action: 'transition', + tags: { status: 'archived', year: '2023' }, + daysAfterCreation: 90, + targetStorageClass: 'glacier', + }); + + expect(rule.tags?.status).toBe('archived'); + }); + + it('should validate rule ID format (snake_case)', () => { + expect(() => LifecyclePolicyRuleSchema.parse({ + id: 'valid_rule_name', + enabled: true, + action: 'delete', + daysAfterCreation: 30, + })).not.toThrow(); + + expect(() => LifecyclePolicyRuleSchema.parse({ + id: 'Invalid-Rule', + enabled: true, + action: 'delete', + daysAfterCreation: 30, + })).toThrow(); + }); +}); + +describe('LifecyclePolicyConfigSchema', () => { + it('should accept default config', () => { + const config = LifecyclePolicyConfigSchema.parse({}); + + expect(config.enabled).toBe(false); + expect(config.rules).toHaveLength(0); + }); + + it('should accept policy with multiple rules', () => { + const config = LifecyclePolicyConfigSchema.parse({ + enabled: true, + rules: [ + { + id: 'archive_old', + enabled: true, + action: 'transition', + daysAfterCreation: 90, + targetStorageClass: 'glacier', + }, + { + id: 'delete_temp', + enabled: true, + action: 'delete', + prefix: 'temp/', + daysAfterCreation: 7, + }, + ], + }); + + expect(config.enabled).toBe(true); + expect(config.rules).toHaveLength(2); + }); +}); + +describe('BucketConfigSchema', () => { + it('should accept minimal bucket config', () => { + const bucket = BucketConfigSchema.parse({ + name: 'user_uploads', + label: 'User Uploads', + bucketName: 'my-app-uploads', + provider: 's3', + }); + + expect(bucket.name).toBe('user_uploads'); + expect(bucket.provider).toBe('s3'); + expect(bucket.enabled).toBe(true); // default + expect(bucket.versioning).toBe(false); // default + expect(bucket.pathStyle).toBe(false); // default + }); + + it('should accept full bucket config with all features', () => { + const bucket = BucketConfigSchema.parse({ + name: 'production_files', + label: 'Production Files', + bucketName: 'prod-files', + region: 'us-east-1', + provider: 's3', + endpoint: 'https://s3.amazonaws.com', + pathStyle: false, + versioning: true, + encryption: { + enabled: true, + algorithm: 'aws:kms', + kmsKeyId: 'arn:aws:kms:us-east-1:123456789:key/abc', + }, + accessControl: { + acl: 'private', + corsEnabled: true, + allowedOrigins: ['https://app.example.com'], + allowedMethods: ['GET', 'PUT'], + }, + lifecyclePolicy: { + enabled: true, + rules: [ + { + id: 'archive_old', + enabled: true, + action: 'transition', + daysAfterCreation: 90, + targetStorageClass: 'glacier', + }, + ], + }, + multipartConfig: { + enabled: true, + threshold: 100 * 1024 * 1024, + }, + tags: { + environment: 'production', + team: 'engineering', + }, + description: 'Production file storage', + enabled: true, + }); + + expect(bucket.versioning).toBe(true); + expect(bucket.encryption?.enabled).toBe(true); + expect(bucket.lifecyclePolicy?.enabled).toBe(true); + }); + + it('should validate bucket name format (snake_case)', () => { + expect(() => BucketConfigSchema.parse({ + name: 'valid_bucket_name', + label: 'Valid Bucket', + bucketName: 'actual-bucket', + provider: 's3', + })).not.toThrow(); + + expect(() => BucketConfigSchema.parse({ + name: 'Invalid-Bucket', + label: 'Invalid Bucket', + bucketName: 'actual-bucket', + provider: 's3', + })).toThrow(); + }); + + it('should accept MinIO bucket with path-style URLs', () => { + const bucket = BucketConfigSchema.parse({ + name: 'minio_bucket', + label: 'MinIO Bucket', + bucketName: 'dev-files', + provider: 'minio', + endpoint: 'http://localhost:9000', + pathStyle: true, + }); + + expect(bucket.provider).toBe('minio'); + expect(bucket.pathStyle).toBe(true); + }); +}); + +describe('StorageConnectionSchema', () => { + it('should accept AWS S3 credentials', () => { + const connection = StorageConnectionSchema.parse({ + accessKeyId: 'AKIAIOSFODNN7EXAMPLE', + secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', + region: 'us-east-1', + }); + + expect(connection.accessKeyId).toBeDefined(); + expect(connection.region).toBe('us-east-1'); + }); + + it('should accept AWS temporary credentials', () => { + const connection = StorageConnectionSchema.parse({ + accessKeyId: 'ASIATEMP', + secretAccessKey: 'tempSecret', + sessionToken: 'FwoGZXIvYXdz...', + region: 'us-west-2', + }); + + expect(connection.sessionToken).toBeDefined(); + }); + + it('should accept Azure credentials', () => { + const connection = StorageConnectionSchema.parse({ + accountName: 'mystorageaccount', + accountKey: 'base64encodedkey==', + endpoint: 'https://mystorageaccount.blob.core.windows.net', + }); + + expect(connection.accountName).toBe('mystorageaccount'); + }); + + it('should accept Azure SAS token', () => { + const connection = StorageConnectionSchema.parse({ + accountName: 'mystorageaccount', + sasToken: 'sv=2021-01-01&ss=b&...', + }); + + expect(connection.sasToken).toBeDefined(); + }); + + it('should accept GCP credentials', () => { + const connection = StorageConnectionSchema.parse({ + projectId: 'my-gcp-project', + credentials: '{"type":"service_account","project_id":"my-project",...}', + }); + + expect(connection.projectId).toBe('my-gcp-project'); + }); + + it('should accept custom endpoint with SSL settings', () => { + const connection = StorageConnectionSchema.parse({ + endpoint: 'https://custom.storage.example.com', + useSSL: true, + timeout: 30000, + }); + + expect(connection.useSSL).toBe(true); + expect(connection.timeout).toBe(30000); + }); + + it('should default useSSL to true', () => { + const connection = StorageConnectionSchema.parse({ + accessKeyId: 'key', + secretAccessKey: 'secret', + }); + + expect(connection.useSSL).toBe(true); + }); +}); + +describe('ObjectStorageConfigSchema', () => { + it('should accept minimal storage config', () => { + const storage = ObjectStorageConfigSchema.parse({ + name: 'main_storage', + label: 'Main Storage', + provider: 's3', + connection: { + accessKeyId: 'key', + secretAccessKey: 'secret', + region: 'us-east-1', + }, + }); + + expect(storage.name).toBe('main_storage'); + expect(storage.provider).toBe('s3'); + expect(storage.enabled).toBe(true); // default + expect(storage.buckets).toHaveLength(0); // default + }); + + it('should accept full storage config with buckets', () => { + const storage = ObjectStorageConfigSchema.parse({ + name: 'production_storage', + label: 'Production Storage', + provider: 's3', + connection: { + accessKeyId: '${AWS_ACCESS_KEY_ID}', + secretAccessKey: '${AWS_SECRET_ACCESS_KEY}', + region: 'us-east-1', + }, + buckets: [ + { + name: 'user_uploads', + label: 'User Uploads', + bucketName: 'prod-uploads', + provider: 's3', + region: 'us-east-1', + }, + { + name: 'media_files', + label: 'Media Files', + bucketName: 'prod-media', + provider: 's3', + region: 'us-east-1', + }, + ], + defaultBucket: 'user_uploads', + enabled: true, + description: 'Production S3 storage', + }); + + expect(storage.buckets).toHaveLength(2); + expect(storage.defaultBucket).toBe('user_uploads'); + }); + + it('should validate storage name format (snake_case)', () => { + expect(() => ObjectStorageConfigSchema.parse({ + name: 'valid_storage_name', + label: 'Valid Storage', + provider: 's3', + connection: {}, + })).not.toThrow(); + + expect(() => ObjectStorageConfigSchema.parse({ + name: 'Invalid-Storage', + label: 'Invalid Storage', + provider: 's3', + connection: {}, + })).toThrow(); + }); + + it('should accept MinIO configuration', () => { + const storage = ObjectStorageConfigSchema.parse({ + name: 'minio_local', + label: 'MinIO Local', + provider: 'minio', + connection: { + accessKeyId: 'minioadmin', + secretAccessKey: 'minioadmin', + endpoint: 'http://localhost:9000', + useSSL: false, + }, + buckets: [ + { + name: 'dev_files', + label: 'Development Files', + bucketName: 'dev', + provider: 'minio', + endpoint: 'http://localhost:9000', + pathStyle: true, + }, + ], + }); + + expect(storage.provider).toBe('minio'); + expect(storage.connection.useSSL).toBe(false); + }); + + it('should accept Azure Blob Storage configuration', () => { + const storage = ObjectStorageConfigSchema.parse({ + name: 'azure_storage', + label: 'Azure Storage', + provider: 'azure_blob', + connection: { + accountName: 'myaccount', + accountKey: '${AZURE_STORAGE_KEY}', + }, + buckets: [ + { + name: 'backup_container', + label: 'Backups', + bucketName: 'backups', + provider: 'azure_blob', + region: 'eastus', + }, + ], + }); + + expect(storage.provider).toBe('azure_blob'); + }); + + it('should accept GCS configuration', () => { + const storage = ObjectStorageConfigSchema.parse({ + name: 'gcs_storage', + label: 'Google Cloud Storage', + provider: 'gcs', + connection: { + projectId: 'my-project', + credentials: '${GCP_CREDENTIALS_JSON}', + }, + buckets: [ + { + name: 'analytics_data', + label: 'Analytics Data', + bucketName: 'analytics', + provider: 'gcs', + region: 'us-central1', + }, + ], + }); + + expect(storage.provider).toBe('gcs'); + }); + + it('should accept disabled storage config', () => { + const storage = ObjectStorageConfigSchema.parse({ + name: 'disabled_storage', + label: 'Disabled Storage', + provider: 's3', + connection: {}, + enabled: false, + }); + + expect(storage.enabled).toBe(false); + }); +}); diff --git a/packages/spec/src/system/object-storage.zod.ts b/packages/spec/src/system/object-storage.zod.ts new file mode 100644 index 000000000..be2aace92 --- /dev/null +++ b/packages/spec/src/system/object-storage.zod.ts @@ -0,0 +1,583 @@ +import { z } from 'zod'; +import { SystemIdentifierSchema } from '../shared/identifiers.zod'; + +/** + * Object Storage Protocol + * + * Defines schemas for object storage systems (S3, Azure Blob, GCS, MinIO) + * that provide persistent file storage capabilities for ObjectStack applications. + * + * This protocol supports: + * - Multi-cloud storage providers + * - Bucket/container configuration + * - Access control and permissions + * - Lifecycle policies for data retention + * - Presigned URLs for secure direct access + * - Multipart uploads for large files + */ + +// ============================================================================ +// Enums +// ============================================================================ + +/** + * Storage Provider Types + * + * Supported cloud and self-hosted object storage providers. + */ +export const StorageProviderSchema = z.enum([ + 's3', // Amazon S3 + 'azure_blob', // Azure Blob Storage + 'gcs', // Google Cloud Storage + 'minio', // MinIO (self-hosted S3-compatible) + 'r2', // Cloudflare R2 + 'spaces', // DigitalOcean Spaces + 'wasabi', // Wasabi Hot Cloud Storage + 'backblaze', // Backblaze B2 + 'local', // Local filesystem (development only) +]).describe('Storage provider type'); + +export type StorageProvider = z.infer; + +/** + * Storage Access Control List (ACL) + * + * Predefined access control configurations for objects and buckets. + */ +export const StorageAclSchema = z.enum([ + 'private', // Owner has full control, no one else has access + 'public_read', // Owner has full control, everyone can read + 'public_read_write', // Owner has full control, everyone can read/write (not recommended) + 'authenticated_read', // Owner has full control, authenticated users can read + 'bucket_owner_read', // Object owner has full control, bucket owner can read + 'bucket_owner_full_control', // Both object and bucket owner have full control +]).describe('Storage access control level'); + +export type StorageAcl = z.infer; + +/** + * Storage Class / Tier + * + * Different storage tiers for cost optimization. + * Maps to provider-specific storage classes. + */ +export const StorageClassSchema = z.enum([ + 'standard', // Standard/hot storage for frequently accessed data + 'intelligent', // Intelligent tiering (auto-moves between hot/cool) + 'infrequent_access', // Infrequent access/cool storage + 'glacier', // Archive/cold storage (slower retrieval) + 'deep_archive', // Deep archive (cheapest, slowest retrieval) +]).describe('Storage class/tier for cost optimization'); + +export type StorageClass = z.infer; + +/** + * Lifecycle Transition Action + */ +export const LifecycleActionSchema = z.enum([ + 'transition', // Move to different storage class + 'delete', // Delete the object + 'abort', // Abort incomplete multipart uploads +]).describe('Lifecycle policy action type'); + +export type LifecycleAction = z.infer; + +// ============================================================================ +// Configuration Schemas +// ============================================================================ + +/** + * Object Metadata Schema + * + * Standard and custom metadata attached to stored objects. + * + * @example + * { + * contentType: 'image/jpeg', + * contentLength: 1024000, + * etag: '"abc123"', + * lastModified: new Date('2024-01-01'), + * custom: { + * uploadedBy: 'user123', + * department: 'marketing' + * } + * } + */ +export const ObjectMetadataSchema = z.object({ + contentType: z.string().describe('MIME type (e.g., image/jpeg, application/pdf)'), + contentLength: z.number().min(0).describe('File size in bytes'), + contentEncoding: z.string().optional().describe('Content encoding (e.g., gzip)'), + contentDisposition: z.string().optional().describe('Content disposition header'), + contentLanguage: z.string().optional().describe('Content language'), + cacheControl: z.string().optional().describe('Cache control directives'), + etag: z.string().optional().describe('Entity tag for versioning/caching'), + lastModified: z.date().optional().describe('Last modification timestamp'), + versionId: z.string().optional().describe('Object version identifier'), + storageClass: StorageClassSchema.optional().describe('Storage class/tier'), + encryption: z.object({ + algorithm: z.string().describe('Encryption algorithm (e.g., AES256, aws:kms)'), + keyId: z.string().optional().describe('KMS key ID if using managed encryption'), + }).optional().describe('Server-side encryption configuration'), + custom: z.record(z.string()).optional().describe('Custom user-defined metadata'), +}); + +export type ObjectMetadata = z.infer; + +/** + * Presigned URL Configuration + * + * Configuration for generating temporary URLs for direct access to objects. + * Useful for secure file uploads/downloads without exposing credentials. + * + * @example + * // Generate download URL valid for 1 hour + * { + * operation: 'get', + * expiresIn: 3600, + * contentType: 'image/jpeg' + * } + * + * @example + * // Generate upload URL valid for 15 minutes with size limit + * { + * operation: 'put', + * expiresIn: 900, + * contentType: 'application/pdf', + * maxSize: 10485760 + * } + */ +export const PresignedUrlConfigSchema = z.object({ + operation: z.enum(['get', 'put', 'delete', 'head']).describe('Allowed operation'), + expiresIn: z.number().min(1).max(604800).describe('Expiration time in seconds (max 7 days)'), + contentType: z.string().optional().describe('Required content type for PUT operations'), + maxSize: z.number().min(0).optional().describe('Maximum file size in bytes for PUT operations'), + responseContentType: z.string().optional().describe('Override content-type for GET operations'), + responseContentDisposition: z.string().optional().describe('Override content-disposition for GET operations'), +}); + +export type PresignedUrlConfig = z.infer; + +/** + * Multipart Upload Configuration + * + * Configuration for chunked uploads of large files. + * Enables resumable uploads and parallel transfer. + * + * @example + * // Enable multipart for files > 100MB with 10MB chunks + * { + * enabled: true, + * partSize: 10485760, + * maxParts: 10000, + * threshold: 104857600, + * maxConcurrent: 4 + * } + */ +export const MultipartUploadConfigSchema = z.object({ + enabled: z.boolean().default(true).describe('Enable multipart uploads'), + partSize: z.number().min(5 * 1024 * 1024).max(5 * 1024 * 1024 * 1024).default(10 * 1024 * 1024).describe('Part size in bytes (min 5MB, max 5GB)'), + maxParts: z.number().min(1).max(10000).default(10000).describe('Maximum number of parts (max 10,000)'), + threshold: z.number().min(0).default(100 * 1024 * 1024).describe('File size threshold to trigger multipart upload (bytes)'), + maxConcurrent: z.number().min(1).max(100).default(4).describe('Maximum concurrent part uploads'), + abortIncompleteAfterDays: z.number().min(1).optional().describe('Auto-abort incomplete uploads after N days'), +}); + +export type MultipartUploadConfig = z.infer; + +/** + * Access Control Configuration + * + * Fine-grained access control for buckets and objects. + * + * @example + * { + * acl: 'private', + * allowedOrigins: ['https://app.example.com'], + * allowedMethods: ['GET', 'PUT'], + * corsEnabled: true, + * publicAccess: { + * allowPublicRead: false, + * allowPublicWrite: false + * } + * } + */ +export const AccessControlConfigSchema = z.object({ + acl: StorageAclSchema.default('private').describe('Default access control level'), + allowedOrigins: z.array(z.string()).optional().describe('CORS allowed origins'), + allowedMethods: z.array(z.enum(['GET', 'PUT', 'POST', 'DELETE', 'HEAD'])).optional().describe('CORS allowed HTTP methods'), + allowedHeaders: z.array(z.string()).optional().describe('CORS allowed headers'), + exposeHeaders: z.array(z.string()).optional().describe('CORS exposed headers'), + maxAge: z.number().min(0).optional().describe('CORS preflight cache duration in seconds'), + corsEnabled: z.boolean().default(false).describe('Enable CORS configuration'), + publicAccess: z.object({ + allowPublicRead: z.boolean().default(false).describe('Allow public read access'), + allowPublicWrite: z.boolean().default(false).describe('Allow public write access'), + allowPublicList: z.boolean().default(false).describe('Allow public bucket listing'), + }).optional().describe('Public access control'), + ipWhitelist: z.array(z.string()).optional().describe('Allowed IP addresses/CIDR blocks'), + ipBlacklist: z.array(z.string()).optional().describe('Blocked IP addresses/CIDR blocks'), +}); + +export type AccessControlConfig = z.infer; + +/** + * Lifecycle Policy Rule + * + * Individual rule for automatic object lifecycle management. + * + * @example + * // Transition to infrequent access after 30 days + * { + * id: 'move_to_ia', + * enabled: true, + * action: 'transition', + * daysAfterCreation: 30, + * targetStorageClass: 'infrequent_access' + * } + * + * @example + * // Delete objects after 365 days + * { + * id: 'delete_old', + * enabled: true, + * action: 'delete', + * daysAfterCreation: 365 + * } + */ +export const LifecyclePolicyRuleSchema = z.object({ + id: SystemIdentifierSchema.describe('Rule identifier'), + enabled: z.boolean().default(true).describe('Enable this rule'), + action: LifecycleActionSchema.describe('Action to perform'), + prefix: z.string().optional().describe('Object key prefix filter (e.g., "uploads/")'), + tags: z.record(z.string()).optional().describe('Object tag filters'), + daysAfterCreation: z.number().min(0).optional().describe('Days after object creation'), + daysAfterModification: z.number().min(0).optional().describe('Days after last modification'), + targetStorageClass: StorageClassSchema.optional().describe('Target storage class for transition action'), +}); + +export type LifecyclePolicyRule = z.infer; + +/** + * Lifecycle Policy Configuration + * + * Collection of lifecycle rules for automatic data management. + * + * @example + * { + * enabled: true, + * rules: [ + * { + * id: 'archive_old_files', + * enabled: true, + * action: 'transition', + * daysAfterCreation: 90, + * targetStorageClass: 'glacier' + * }, + * { + * id: 'delete_temp_files', + * enabled: true, + * action: 'delete', + * prefix: 'temp/', + * daysAfterCreation: 7 + * } + * ] + * } + */ +export const LifecyclePolicyConfigSchema = z.object({ + enabled: z.boolean().default(false).describe('Enable lifecycle policies'), + rules: z.array(LifecyclePolicyRuleSchema).default([]).describe('Lifecycle rules'), +}); + +export type LifecyclePolicyConfig = z.infer; + +/** + * Bucket Configuration Schema + * + * Comprehensive configuration for a storage bucket/container. + * + * @example + * { + * name: 'user_uploads', + * label: 'User Uploads', + * bucketName: 'my-app-uploads', + * region: 'us-east-1', + * provider: 's3', + * versioning: true, + * accessControl: { + * acl: 'private', + * corsEnabled: true, + * allowedOrigins: ['https://app.example.com'] + * }, + * multipartConfig: { + * enabled: true, + * threshold: 104857600 + * } + * } + */ +export const BucketConfigSchema = z.object({ + name: SystemIdentifierSchema.describe('Bucket identifier in ObjectStack (snake_case)'), + label: z.string().describe('Display label'), + bucketName: z.string().describe('Actual bucket/container name in storage provider'), + region: z.string().optional().describe('Storage region (e.g., us-east-1, westus)'), + provider: StorageProviderSchema.describe('Storage provider'), + endpoint: z.string().optional().describe('Custom endpoint URL (for S3-compatible providers)'), + pathStyle: z.boolean().default(false).describe('Use path-style URLs (for S3-compatible providers)'), + + versioning: z.boolean().default(false).describe('Enable object versioning'), + encryption: z.object({ + enabled: z.boolean().default(false).describe('Enable server-side encryption'), + algorithm: z.enum(['AES256', 'aws:kms', 'azure:kms', 'gcp:kms']).default('AES256').describe('Encryption algorithm'), + kmsKeyId: z.string().optional().describe('KMS key ID for managed encryption'), + }).optional().describe('Server-side encryption configuration'), + + accessControl: AccessControlConfigSchema.optional().describe('Access control configuration'), + lifecyclePolicy: LifecyclePolicyConfigSchema.optional().describe('Lifecycle policy configuration'), + multipartConfig: MultipartUploadConfigSchema.optional().describe('Multipart upload configuration'), + + tags: z.record(z.string()).optional().describe('Bucket tags for organization'), + description: z.string().optional().describe('Bucket description'), + enabled: z.boolean().default(true).describe('Enable this bucket'), +}); + +export type BucketConfig = z.infer; + +/** + * Storage Connection Configuration + * + * Provider-specific connection credentials and settings. + * + * @example S3 + * { + * accessKeyId: '${AWS_ACCESS_KEY_ID}', + * secretAccessKey: '${AWS_SECRET_ACCESS_KEY}', + * sessionToken: '${AWS_SESSION_TOKEN}', + * region: 'us-east-1' + * } + * + * @example Azure + * { + * accountName: 'mystorageaccount', + * accountKey: '${AZURE_STORAGE_KEY}', + * endpoint: 'https://mystorageaccount.blob.core.windows.net' + * } + */ +export const StorageConnectionSchema = z.object({ + // AWS S3 / MinIO + accessKeyId: z.string().optional().describe('AWS access key ID or MinIO access key'), + secretAccessKey: z.string().optional().describe('AWS secret access key or MinIO secret key'), + sessionToken: z.string().optional().describe('AWS session token for temporary credentials'), + + // Azure Blob Storage + accountName: z.string().optional().describe('Azure storage account name'), + accountKey: z.string().optional().describe('Azure storage account key'), + sasToken: z.string().optional().describe('Azure SAS token'), + + // Google Cloud Storage + projectId: z.string().optional().describe('GCP project ID'), + credentials: z.string().optional().describe('GCP service account credentials JSON'), + + // Common + endpoint: z.string().optional().describe('Custom endpoint URL'), + region: z.string().optional().describe('Default region'), + useSSL: z.boolean().default(true).describe('Use SSL/TLS for connections'), + timeout: z.number().min(0).optional().describe('Connection timeout in milliseconds'), +}); + +export type StorageConnection = z.infer; + +/** + * Object Storage Configuration + * + * Complete object storage system configuration. + * + * @example + * { + * name: 'production_storage', + * label: 'Production File Storage', + * provider: 's3', + * connection: { + * accessKeyId: '${AWS_ACCESS_KEY_ID}', + * secretAccessKey: '${AWS_SECRET_ACCESS_KEY}', + * region: 'us-east-1' + * }, + * buckets: [ + * { + * name: 'user_uploads', + * label: 'User Uploads', + * bucketName: 'prod-uploads', + * provider: 's3', + * region: 'us-east-1' + * } + * ], + * defaultBucket: 'user_uploads' + * } + */ +export const ObjectStorageConfigSchema = z.object({ + name: SystemIdentifierSchema.describe('Storage configuration identifier'), + label: z.string().describe('Display label'), + provider: StorageProviderSchema.describe('Primary storage provider'), + connection: StorageConnectionSchema.describe('Connection credentials'), + buckets: z.array(BucketConfigSchema).default([]).describe('Configured buckets'), + defaultBucket: z.string().optional().describe('Default bucket name for operations'), + enabled: z.boolean().default(true).describe('Enable this storage configuration'), + description: z.string().optional().describe('Configuration description'), +}); + +export type ObjectStorageConfig = z.infer; + +// ============================================================================ +// Helper Examples +// ============================================================================ + +/** + * Example: AWS S3 Configuration + */ +export const s3StorageExample = ObjectStorageConfigSchema.parse({ + name: 'aws_s3_storage', + label: 'AWS S3 Production Storage', + provider: 's3', + connection: { + accessKeyId: '${AWS_ACCESS_KEY_ID}', + secretAccessKey: '${AWS_SECRET_ACCESS_KEY}', + region: 'us-east-1', + }, + buckets: [ + { + name: 'user_uploads', + label: 'User Uploads', + bucketName: 'my-app-user-uploads', + region: 'us-east-1', + provider: 's3', + versioning: true, + encryption: { + enabled: true, + algorithm: 'aws:kms', + kmsKeyId: '${AWS_KMS_KEY_ID}', + }, + accessControl: { + acl: 'private', + corsEnabled: true, + allowedOrigins: ['https://app.example.com'], + allowedMethods: ['GET', 'PUT', 'POST'], + }, + lifecyclePolicy: { + enabled: true, + rules: [ + { + id: 'archive_old_uploads', + enabled: true, + action: 'transition', + daysAfterCreation: 90, + targetStorageClass: 'glacier', + }, + ], + }, + multipartConfig: { + enabled: true, + partSize: 10 * 1024 * 1024, + threshold: 100 * 1024 * 1024, + maxConcurrent: 4, + }, + }, + ], + defaultBucket: 'user_uploads', + enabled: true, +}); + +/** + * Example: MinIO Configuration + */ +export const minioStorageExample = ObjectStorageConfigSchema.parse({ + name: 'minio_local', + label: 'MinIO Local Storage', + provider: 'minio', + connection: { + accessKeyId: 'minioadmin', + secretAccessKey: 'minioadmin', + endpoint: 'http://localhost:9000', + useSSL: false, + }, + buckets: [ + { + name: 'development_files', + label: 'Development Files', + bucketName: 'dev-files', + provider: 'minio', + endpoint: 'http://localhost:9000', + pathStyle: true, + accessControl: { + acl: 'private', + }, + }, + ], + defaultBucket: 'development_files', + enabled: true, +}); + +/** + * Example: Azure Blob Storage Configuration + */ +export const azureBlobStorageExample = ObjectStorageConfigSchema.parse({ + name: 'azure_blob_storage', + label: 'Azure Blob Storage', + provider: 'azure_blob', + connection: { + accountName: 'mystorageaccount', + accountKey: '${AZURE_STORAGE_KEY}', + endpoint: 'https://mystorageaccount.blob.core.windows.net', + }, + buckets: [ + { + name: 'media_files', + label: 'Media Files', + bucketName: 'media', + provider: 'azure_blob', + region: 'eastus', + accessControl: { + acl: 'public_read', + publicAccess: { + allowPublicRead: true, + allowPublicWrite: false, + allowPublicList: false, + }, + }, + }, + ], + defaultBucket: 'media_files', + enabled: true, +}); + +/** + * Example: Google Cloud Storage Configuration + */ +export const gcsStorageExample = ObjectStorageConfigSchema.parse({ + name: 'gcs_storage', + label: 'Google Cloud Storage', + provider: 'gcs', + connection: { + projectId: 'my-gcp-project', + credentials: '${GCP_SERVICE_ACCOUNT_JSON}', + }, + buckets: [ + { + name: 'backup_storage', + label: 'Backup Storage', + bucketName: 'my-app-backups', + region: 'us-central1', + provider: 'gcs', + lifecyclePolicy: { + enabled: true, + rules: [ + { + id: 'delete_old_backups', + enabled: true, + action: 'delete', + daysAfterCreation: 30, + }, + ], + }, + }, + ], + defaultBucket: 'backup_storage', + enabled: true, +}); From ef0b1ac1b5cb5637810ea41a43c165f017c320b7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 11:59:30 +0000 Subject: [PATCH 3/3] Address code review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update terminology: ipWhitelist/ipBlacklist → allowedIps/blockedIps - Add validation: minSize <= maxSize - Add validation: virusScanProvider requires virusScan enabled - Add validation: transition action requires targetStorageClass - Add comprehensive tests for all validation rules - Remove incorrectly placed integration/object-storage.mdx - All 2238 tests passing Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- .../docs/references/system/object-storage.mdx | 4 +- .../system/AccessControlConfig.json | 4 +- .../spec/json-schema/system/BucketConfig.json | 4 +- .../system/ObjectStorageConfig.json | 4 +- packages/spec/src/data/field.test.ts | 54 ++++++++++++++++++ packages/spec/src/data/field.zod.ts | 18 +++++- .../spec/src/system/object-storage.test.ts | 57 +++++++++++++++++-- .../spec/src/system/object-storage.zod.ts | 12 +++- 8 files changed, 141 insertions(+), 16 deletions(-) diff --git a/content/docs/references/system/object-storage.mdx b/content/docs/references/system/object-storage.mdx index 5857b4351..9f754b565 100644 --- a/content/docs/references/system/object-storage.mdx +++ b/content/docs/references/system/object-storage.mdx @@ -35,8 +35,8 @@ const result = AccessControlConfigSchema.parse(data); | **maxAge** | `number` | optional | CORS preflight cache duration in seconds | | **corsEnabled** | `boolean` | optional | Enable CORS configuration | | **publicAccess** | `object` | optional | Public access control | -| **ipWhitelist** | `string[]` | optional | Allowed IP addresses/CIDR blocks | -| **ipBlacklist** | `string[]` | optional | Blocked IP addresses/CIDR blocks | +| **allowedIps** | `string[]` | optional | Allowed IP addresses/CIDR blocks | +| **blockedIps** | `string[]` | optional | Blocked IP addresses/CIDR blocks | --- diff --git a/packages/spec/json-schema/system/AccessControlConfig.json b/packages/spec/json-schema/system/AccessControlConfig.json index e95212181..327e2d83e 100644 --- a/packages/spec/json-schema/system/AccessControlConfig.json +++ b/packages/spec/json-schema/system/AccessControlConfig.json @@ -84,14 +84,14 @@ "additionalProperties": false, "description": "Public access control" }, - "ipWhitelist": { + "allowedIps": { "type": "array", "items": { "type": "string" }, "description": "Allowed IP addresses/CIDR blocks" }, - "ipBlacklist": { + "blockedIps": { "type": "array", "items": { "type": "string" diff --git a/packages/spec/json-schema/system/BucketConfig.json b/packages/spec/json-schema/system/BucketConfig.json index 377f88cde..5f6954ff6 100644 --- a/packages/spec/json-schema/system/BucketConfig.json +++ b/packages/spec/json-schema/system/BucketConfig.json @@ -161,14 +161,14 @@ "additionalProperties": false, "description": "Public access control" }, - "ipWhitelist": { + "allowedIps": { "type": "array", "items": { "type": "string" }, "description": "Allowed IP addresses/CIDR blocks" }, - "ipBlacklist": { + "blockedIps": { "type": "array", "items": { "type": "string" diff --git a/packages/spec/json-schema/system/ObjectStorageConfig.json b/packages/spec/json-schema/system/ObjectStorageConfig.json index 3b04da9c1..a6df034a6 100644 --- a/packages/spec/json-schema/system/ObjectStorageConfig.json +++ b/packages/spec/json-schema/system/ObjectStorageConfig.json @@ -248,14 +248,14 @@ "additionalProperties": false, "description": "Public access control" }, - "ipWhitelist": { + "allowedIps": { "type": "array", "items": { "type": "string" }, "description": "Allowed IP addresses/CIDR blocks" }, - "ipBlacklist": { + "blockedIps": { "type": "array", "items": { "type": "string" diff --git a/packages/spec/src/data/field.test.ts b/packages/spec/src/data/field.test.ts index 69358f915..f8ba90140 100644 --- a/packages/spec/src/data/field.test.ts +++ b/packages/spec/src/data/field.test.ts @@ -1331,5 +1331,59 @@ describe('Field Factory Helpers', () => { maxVersions: 0, })).toThrow(); }); + + it('should reject minSize greater than maxSize', () => { + expect(() => FileAttachmentConfigSchema.parse({ + minSize: 1000, + maxSize: 500, + })).toThrow(); + + // Verify the specific error message + try { + FileAttachmentConfigSchema.parse({ + minSize: 1000, + maxSize: 500, + }); + } catch (error: any) { + expect(error.issues[0].message).toContain('minSize must be less than or equal to maxSize'); + } + }); + + it('should accept valid minSize and maxSize', () => { + const config = FileAttachmentConfigSchema.parse({ + minSize: 500, + maxSize: 1000, + }); + + expect(config.minSize).toBe(500); + expect(config.maxSize).toBe(1000); + }); + + it('should reject virusScanProvider without virusScan enabled', () => { + expect(() => FileAttachmentConfigSchema.parse({ + virusScan: false, + virusScanProvider: 'clamav', + })).toThrow(); + + // Verify the specific error message + try { + FileAttachmentConfigSchema.parse({ + virusScan: false, + virusScanProvider: 'clamav', + }); + } catch (error: any) { + expect(error.issues[0].message).toContain('virusScanProvider requires virusScan to be enabled'); + } + }); + + it('should accept virusScanProvider when virusScan is enabled', () => { + const config = FileAttachmentConfigSchema.parse({ + virusScan: true, + virusScanProvider: 'clamav', + }); + + expect(config.virusScan).toBe(true); + expect(config.virusScanProvider).toBe('clamav'); + }); }); }); diff --git a/packages/spec/src/data/field.zod.ts b/packages/spec/src/data/field.zod.ts index d7e9825b0..2212c3ef0 100644 --- a/packages/spec/src/data/field.zod.ts +++ b/packages/spec/src/data/field.zod.ts @@ -160,7 +160,7 @@ export const VectorConfigSchema = z.object({ * Configuration for file and attachment field types * * Provides comprehensive file upload capabilities with: - * - File type restrictions (whitelist/blacklist) + * - File type restrictions (allowed/blocked) * - File size limits (min/max) * - Virus scanning integration * - Storage provider integration @@ -241,6 +241,22 @@ export const FileAttachmentConfigSchema = z.object({ /** Access Control */ publicRead: z.boolean().default(false).describe('Allow public read access to uploaded files'), presignedUrlExpiry: z.number().min(60).max(604800).default(3600).describe('Presigned URL expiration in seconds (default: 1 hour)'), +}).refine((data) => { + // Validate minSize is less than or equal to maxSize + if (data.minSize !== undefined && data.maxSize !== undefined && data.minSize > data.maxSize) { + return false; + } + return true; +}, { + message: 'minSize must be less than or equal to maxSize', +}).refine((data) => { + // Validate virusScanProvider requires virusScan to be enabled + if (data.virusScanProvider !== undefined && data.virusScan !== true) { + return false; + } + return true; +}, { + message: 'virusScanProvider requires virusScan to be enabled', }); /** diff --git a/packages/spec/src/system/object-storage.test.ts b/packages/spec/src/system/object-storage.test.ts index b06d9a14c..6adb601c0 100644 --- a/packages/spec/src/system/object-storage.test.ts +++ b/packages/spec/src/system/object-storage.test.ts @@ -265,14 +265,14 @@ describe('AccessControlConfigSchema', () => { expect(config.publicAccess?.allowPublicWrite).toBe(false); }); - it('should accept IP whitelist/blacklist', () => { + it('should accept IP allow/block lists', () => { const config = AccessControlConfigSchema.parse({ - ipWhitelist: ['192.168.1.0/24', '10.0.0.1'], - ipBlacklist: ['1.2.3.4'], + allowedIps: ['192.168.1.0/24', '10.0.0.1'], + blockedIps: ['1.2.3.4'], }); - expect(config.ipWhitelist).toHaveLength(2); - expect(config.ipBlacklist).toHaveLength(1); + expect(config.allowedIps).toHaveLength(2); + expect(config.blockedIps).toHaveLength(1); }); }); @@ -342,6 +342,53 @@ describe('LifecyclePolicyRuleSchema', () => { daysAfterCreation: 30, })).toThrow(); }); + + it('should require targetStorageClass when action is transition', () => { + expect(() => LifecyclePolicyRuleSchema.parse({ + id: 'move_to_glacier', + enabled: true, + action: 'transition', + daysAfterCreation: 30, + // missing targetStorageClass + })).toThrow(); + + // Verify the specific error message + try { + LifecyclePolicyRuleSchema.parse({ + id: 'move_to_glacier', + enabled: true, + action: 'transition', + daysAfterCreation: 30, + }); + } catch (error: any) { + expect(error.issues[0].message).toContain('targetStorageClass is required'); + } + }); + + it('should accept transition rule with targetStorageClass', () => { + const rule = LifecyclePolicyRuleSchema.parse({ + id: 'move_to_glacier', + enabled: true, + action: 'transition', + daysAfterCreation: 30, + targetStorageClass: 'glacier', + }); + + expect(rule.action).toBe('transition'); + expect(rule.targetStorageClass).toBe('glacier'); + }); + + it('should allow delete action without targetStorageClass', () => { + const rule = LifecyclePolicyRuleSchema.parse({ + id: 'delete_old', + enabled: true, + action: 'delete', + daysAfterCreation: 365, + }); + + expect(rule.action).toBe('delete'); + expect(rule.targetStorageClass).toBeUndefined(); + }); }); describe('LifecyclePolicyConfigSchema', () => { diff --git a/packages/spec/src/system/object-storage.zod.ts b/packages/spec/src/system/object-storage.zod.ts index be2aace92..0d692e287 100644 --- a/packages/spec/src/system/object-storage.zod.ts +++ b/packages/spec/src/system/object-storage.zod.ts @@ -214,8 +214,8 @@ export const AccessControlConfigSchema = z.object({ allowPublicWrite: z.boolean().default(false).describe('Allow public write access'), allowPublicList: z.boolean().default(false).describe('Allow public bucket listing'), }).optional().describe('Public access control'), - ipWhitelist: z.array(z.string()).optional().describe('Allowed IP addresses/CIDR blocks'), - ipBlacklist: z.array(z.string()).optional().describe('Blocked IP addresses/CIDR blocks'), + allowedIps: z.array(z.string()).optional().describe('Allowed IP addresses/CIDR blocks'), + blockedIps: z.array(z.string()).optional().describe('Blocked IP addresses/CIDR blocks'), }); export type AccessControlConfig = z.infer; @@ -253,6 +253,14 @@ export const LifecyclePolicyRuleSchema = z.object({ daysAfterCreation: z.number().min(0).optional().describe('Days after object creation'), daysAfterModification: z.number().min(0).optional().describe('Days after last modification'), targetStorageClass: StorageClassSchema.optional().describe('Target storage class for transition action'), +}).refine((data) => { + // Validate that transition action has targetStorageClass + if (data.action === 'transition' && !data.targetStorageClass) { + return false; + } + return true; +}, { + message: 'targetStorageClass is required when action is "transition"', }); export type LifecyclePolicyRule = z.infer;