diff --git a/content/docs/references/api/graphql.mdx b/content/docs/references/api/graphql.mdx new file mode 100644 index 00000000000..9b0bd77a5e4 --- /dev/null +++ b/content/docs/references/api/graphql.mdx @@ -0,0 +1,269 @@ +--- +title: Graphql +description: Graphql protocol schemas +--- + +# Graphql + + +**Source:** `packages/spec/src/api/graphql.zod.ts` + + +## TypeScript Usage + +```typescript +import { GraphQLConfigSchema, GraphQLDataLoaderConfigSchema, GraphQLDirectiveConfigSchema, GraphQLDirectiveLocationSchema, GraphQLMutationConfigSchema, GraphQLPersistedQuerySchema, GraphQLQueryComplexitySchema, GraphQLQueryConfigSchema, GraphQLQueryDepthLimitSchema, GraphQLRateLimitSchema, GraphQLResolverConfigSchema, GraphQLScalarTypeSchema, GraphQLSubscriptionConfigSchema, GraphQLTypeConfigSchema } from '@objectstack/spec/api'; +import type { GraphQLConfig, GraphQLDataLoaderConfig, GraphQLDirectiveConfig, GraphQLDirectiveLocation, GraphQLMutationConfig, GraphQLPersistedQuery, GraphQLQueryComplexity, GraphQLQueryConfig, GraphQLQueryDepthLimit, GraphQLRateLimit, GraphQLResolverConfig, GraphQLScalarType, GraphQLSubscriptionConfig, GraphQLTypeConfig } from '@objectstack/spec/api'; + +// Validate data +const result = GraphQLConfigSchema.parse(data); +``` + +--- + +## GraphQLConfig + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional | Enable GraphQL API | +| **path** | `string` | optional | GraphQL endpoint path | +| **playground** | `object` | optional | GraphQL Playground configuration | +| **schema** | `object` | optional | Schema generation configuration | +| **dataLoaders** | `object[]` | optional | DataLoader configurations | +| **security** | `object` | optional | Security configuration | + +--- + +## GraphQLDataLoaderConfig + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | DataLoader name | +| **source** | `string` | ✅ | Source object or datasource | +| **batchFunction** | `object` | ✅ | Batch function configuration | +| **cache** | `object` | optional | DataLoader caching | +| **options** | `object` | optional | DataLoader options | + +--- + +## GraphQLDirectiveConfig + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Directive name (camelCase) | +| **description** | `string` | optional | Directive description | +| **locations** | `Enum<'QUERY' \| 'MUTATION' \| 'SUBSCRIPTION' \| 'FIELD' \| 'FRAGMENT_DEFINITION' \| 'FRAGMENT_SPREAD' \| 'INLINE_FRAGMENT' \| 'VARIABLE_DEFINITION' \| 'SCHEMA' \| 'SCALAR' \| 'OBJECT' \| 'FIELD_DEFINITION' \| 'ARGUMENT_DEFINITION' \| 'INTERFACE' \| 'UNION' \| 'ENUM' \| 'ENUM_VALUE' \| 'INPUT_OBJECT' \| 'INPUT_FIELD_DEFINITION'>[]` | ✅ | Directive locations | +| **args** | `Record` | optional | Directive arguments | +| **repeatable** | `boolean` | optional | Can be applied multiple times | +| **implementation** | `object` | optional | Directive implementation | + +--- + +## GraphQLDirectiveLocation + +### Allowed Values + +* `QUERY` +* `MUTATION` +* `SUBSCRIPTION` +* `FIELD` +* `FRAGMENT_DEFINITION` +* `FRAGMENT_SPREAD` +* `INLINE_FRAGMENT` +* `VARIABLE_DEFINITION` +* `SCHEMA` +* `SCALAR` +* `OBJECT` +* `FIELD_DEFINITION` +* `ARGUMENT_DEFINITION` +* `INTERFACE` +* `UNION` +* `ENUM` +* `ENUM_VALUE` +* `INPUT_OBJECT` +* `INPUT_FIELD_DEFINITION` + +--- + +## GraphQLMutationConfig + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Mutation field name (camelCase recommended) | +| **object** | `string` | ✅ | Source ObjectQL object name | +| **type** | `Enum<'create' \| 'update' \| 'delete' \| 'upsert' \| 'custom'>` | ✅ | Mutation type | +| **description** | `string` | optional | Mutation description | +| **input** | `object` | optional | Input configuration | +| **output** | `object` | optional | Output configuration | +| **transaction** | `object` | optional | Transaction configuration | +| **authRequired** | `boolean` | optional | Require authentication | +| **permissions** | `string[]` | optional | Required permissions | +| **hooks** | `object` | optional | Lifecycle hooks | + +--- + +## GraphQLPersistedQuery + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional | Enable persisted queries | +| **mode** | `Enum<'optional' \| 'required'>` | optional | Persisted query mode (optional: allow both, required: only persisted) | +| **store** | `object` | optional | Query store configuration | +| **apq** | `object` | optional | Automatic Persisted Queries configuration | +| **allowlist** | `object` | optional | Query allow list configuration | +| **security** | `object` | optional | Security configuration | + +--- + +## GraphQLQueryComplexity + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional | Enable query complexity limiting | +| **maxComplexity** | `integer` | optional | Maximum query complexity | +| **defaultFieldComplexity** | `integer` | optional | Default complexity per field | +| **fieldComplexity** | `Record` | optional | Per-field complexity configuration | +| **listMultiplier** | `number` | optional | Multiplier for list fields | +| **onComplexityExceeded** | `Enum<'reject' \| 'log' \| 'warn'>` | optional | Action when complexity exceeded | +| **errorMessage** | `string` | optional | Custom error message for complexity violations | + +--- + +## GraphQLQueryConfig + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Query field name (camelCase recommended) | +| **object** | `string` | ✅ | Source ObjectQL object name | +| **type** | `Enum<'get' \| 'list' \| 'search'>` | ✅ | Query type | +| **description** | `string` | optional | Query description | +| **args** | `Record` | optional | Query arguments | +| **filtering** | `object` | optional | Filtering capabilities | +| **sorting** | `object` | optional | Sorting capabilities | +| **pagination** | `object` | optional | Pagination configuration | +| **fields** | `object` | optional | Field selection configuration | +| **authRequired** | `boolean` | optional | Require authentication | +| **permissions** | `string[]` | optional | Required permissions | +| **cache** | `object` | optional | Query caching | + +--- + +## GraphQLQueryDepthLimit + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional | Enable query depth limiting | +| **maxDepth** | `integer` | optional | Maximum query depth | +| **ignoreFields** | `string[]` | optional | Fields excluded from depth calculation | +| **onDepthExceeded** | `Enum<'reject' \| 'log' \| 'warn'>` | optional | Action when depth exceeded | +| **errorMessage** | `string` | optional | Custom error message for depth violations | + +--- + +## GraphQLRateLimit + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional | Enable rate limiting | +| **strategy** | `Enum<'token_bucket' \| 'fixed_window' \| 'sliding_window' \| 'cost_based'>` | optional | Rate limiting strategy | +| **global** | `object` | optional | Global rate limits | +| **perUser** | `object` | optional | Per-user rate limits | +| **costBased** | `object` | optional | Cost-based rate limiting | +| **operations** | `Record` | optional | Per-operation rate limits | +| **onLimitExceeded** | `Enum<'reject' \| 'queue' \| 'log'>` | optional | Action when rate limit exceeded | +| **errorMessage** | `string` | optional | Custom error message for rate limit violations | +| **includeHeaders** | `boolean` | optional | Include rate limit headers in response | + +--- + +## GraphQLResolverConfig + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **path** | `string` | ✅ | Resolver path (Type.field) | +| **type** | `Enum<'datasource' \| 'computed' \| 'script' \| 'proxy'>` | ✅ | Resolver implementation type | +| **implementation** | `object` | optional | Implementation configuration | +| **cache** | `object` | optional | Resolver caching | + +--- + +## GraphQLScalarType + +### Allowed Values + +* `ID` +* `String` +* `Int` +* `Float` +* `Boolean` +* `DateTime` +* `Date` +* `Time` +* `JSON` +* `JSONObject` +* `Upload` +* `URL` +* `Email` +* `PhoneNumber` +* `Currency` +* `Decimal` +* `BigInt` +* `Long` +* `UUID` +* `Base64` +* `Void` + +--- + +## GraphQLSubscriptionConfig + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Subscription field name (camelCase recommended) | +| **object** | `string` | ✅ | Source ObjectQL object name | +| **events** | `Enum<'created' \| 'updated' \| 'deleted' \| 'custom'>[]` | ✅ | Events to subscribe to | +| **description** | `string` | optional | Subscription description | +| **filter** | `object` | optional | Subscription filtering | +| **payload** | `object` | optional | Payload configuration | +| **authRequired** | `boolean` | optional | Require authentication | +| **permissions** | `string[]` | optional | Required permissions | +| **rateLimit** | `object` | optional | Subscription rate limiting | + +--- + +## GraphQLTypeConfig + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | GraphQL type name (PascalCase recommended) | +| **object** | `string` | ✅ | Source ObjectQL object name | +| **description** | `string` | optional | Type description | +| **fields** | `object` | optional | Field configuration | +| **interfaces** | `string[]` | optional | GraphQL interface names | +| **isInterface** | `boolean` | optional | Define as GraphQL interface | +| **directives** | `object[]` | optional | GraphQL directives | + diff --git a/content/docs/references/api/index.mdx b/content/docs/references/api/index.mdx index 764e3f46cc2..980afbd4b36 100644 --- a/content/docs/references/api/index.mdx +++ b/content/docs/references/api/index.mdx @@ -11,6 +11,7 @@ This section contains all protocol schemas for the api layer of ObjectStack. + diff --git a/content/docs/references/api/meta.json b/content/docs/references/api/meta.json index 969dca1e903..7b29372f8d8 100644 --- a/content/docs/references/api/meta.json +++ b/content/docs/references/api/meta.json @@ -4,6 +4,7 @@ "contract", "discovery", "endpoint", + "graphql", "odata", "realtime", "router" diff --git a/packages/spec/json-schema/api/GraphQLConfig.json b/packages/spec/json-schema/api/GraphQLConfig.json new file mode 100644 index 00000000000..979bb487473 --- /dev/null +++ b/packages/spec/json-schema/api/GraphQLConfig.json @@ -0,0 +1,1389 @@ +{ + "$ref": "#/definitions/GraphQLConfig", + "definitions": { + "GraphQLConfig": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable GraphQL API" + }, + "path": { + "type": "string", + "default": "/graphql", + "description": "GraphQL endpoint path" + }, + "playground": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable GraphQL Playground" + }, + "path": { + "type": "string", + "default": "/playground", + "description": "Playground path" + } + }, + "additionalProperties": false, + "description": "GraphQL Playground configuration" + }, + "schema": { + "type": "object", + "properties": { + "autoGenerateTypes": { + "type": "boolean", + "default": true, + "description": "Auto-generate types from Objects" + }, + "types": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "GraphQL type name (PascalCase recommended)" + }, + "object": { + "type": "string", + "description": "Source ObjectQL object name" + }, + "description": { + "type": "string", + "description": "Type description" + }, + "fields": { + "type": "object", + "properties": { + "include": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Fields to include" + }, + "exclude": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Fields to exclude (e.g., sensitive fields)" + }, + "mappings": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "graphqlName": { + "type": "string", + "description": "Custom GraphQL field name" + }, + "graphqlType": { + "type": "string", + "description": "Override GraphQL type" + }, + "description": { + "type": "string", + "description": "Field description" + }, + "deprecationReason": { + "type": "string", + "description": "Why field is deprecated" + }, + "nullable": { + "type": "boolean", + "description": "Override nullable" + } + }, + "additionalProperties": false + }, + "description": "Field-level customizations" + } + }, + "additionalProperties": false, + "description": "Field configuration" + }, + "interfaces": { + "type": "array", + "items": { + "type": "string" + }, + "description": "GraphQL interface names" + }, + "isInterface": { + "type": "boolean", + "default": false, + "description": "Define as GraphQL interface" + }, + "directives": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Directive name" + }, + "args": { + "type": "object", + "additionalProperties": {}, + "description": "Directive arguments" + } + }, + "required": [ + "name" + ], + "additionalProperties": false + }, + "description": "GraphQL directives" + } + }, + "required": [ + "name", + "object" + ], + "additionalProperties": false + }, + "description": "Type configurations" + }, + "queries": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Query field name (camelCase recommended)" + }, + "object": { + "type": "string", + "description": "Source ObjectQL object name" + }, + "type": { + "type": "string", + "enum": [ + "get", + "list", + "search" + ], + "description": "Query type" + }, + "description": { + "type": "string", + "description": "Query description" + }, + "args": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "GraphQL type (e.g., \"ID!\", \"String\", \"Int\")" + }, + "description": { + "type": "string", + "description": "Argument description" + }, + "defaultValue": { + "description": "Default value" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "description": "Query arguments" + }, + "filtering": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Allow filtering" + }, + "fields": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Filterable fields" + }, + "operators": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "eq", + "ne", + "gt", + "gte", + "lt", + "lte", + "in", + "notIn", + "contains", + "startsWith", + "endsWith", + "isNull", + "isNotNull" + ] + }, + "description": "Allowed filter operators" + } + }, + "additionalProperties": false, + "description": "Filtering capabilities" + }, + "sorting": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Allow sorting" + }, + "fields": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Sortable fields" + }, + "defaultSort": { + "type": "object", + "properties": { + "field": { + "type": "string" + }, + "direction": { + "type": "string", + "enum": [ + "ASC", + "DESC" + ] + } + }, + "required": [ + "field", + "direction" + ], + "additionalProperties": false, + "description": "Default sort order" + } + }, + "additionalProperties": false, + "description": "Sorting capabilities" + }, + "pagination": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable pagination" + }, + "type": { + "type": "string", + "enum": [ + "offset", + "cursor", + "relay" + ], + "default": "offset", + "description": "Pagination style" + }, + "defaultLimit": { + "type": "integer", + "minimum": 1, + "default": 20, + "description": "Default page size" + }, + "maxLimit": { + "type": "integer", + "minimum": 1, + "default": 100, + "description": "Maximum page size" + } + }, + "additionalProperties": false, + "description": "Pagination configuration" + }, + "fields": { + "type": "object", + "properties": { + "required": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Required fields (always returned)" + }, + "selectable": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Selectable fields" + } + }, + "additionalProperties": false, + "description": "Field selection configuration" + }, + "authRequired": { + "type": "boolean", + "default": true, + "description": "Require authentication" + }, + "permissions": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Required permissions" + }, + "cache": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable caching" + }, + "ttl": { + "type": "integer", + "minimum": 0, + "description": "Cache TTL in seconds" + }, + "key": { + "type": "string", + "description": "Cache key template" + } + }, + "additionalProperties": false, + "description": "Query caching" + } + }, + "required": [ + "name", + "object", + "type" + ], + "additionalProperties": false + }, + "description": "Query configurations" + }, + "mutations": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Mutation field name (camelCase recommended)" + }, + "object": { + "type": "string", + "description": "Source ObjectQL object name" + }, + "type": { + "type": "string", + "enum": [ + "create", + "update", + "delete", + "upsert", + "custom" + ], + "description": "Mutation type" + }, + "description": { + "type": "string", + "description": "Mutation description" + }, + "input": { + "type": "object", + "properties": { + "typeName": { + "type": "string", + "description": "Custom input type name" + }, + "fields": { + "type": "object", + "properties": { + "include": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Fields to include" + }, + "exclude": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Fields to exclude" + }, + "required": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Required input fields" + } + }, + "additionalProperties": false, + "description": "Input field configuration" + }, + "validation": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable input validation" + }, + "rules": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Custom validation rules" + } + }, + "additionalProperties": false, + "description": "Input validation" + } + }, + "additionalProperties": false, + "description": "Input configuration" + }, + "output": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "object", + "payload", + "boolean", + "custom" + ], + "default": "object", + "description": "Output type" + }, + "includeEnvelope": { + "type": "boolean", + "default": false, + "description": "Wrap in success/error payload" + }, + "customType": { + "type": "string", + "description": "Custom output type name" + } + }, + "additionalProperties": false, + "description": "Output configuration" + }, + "transaction": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Use database transaction" + }, + "isolationLevel": { + "type": "string", + "enum": [ + "read_uncommitted", + "read_committed", + "repeatable_read", + "serializable" + ], + "description": "Transaction isolation level" + } + }, + "additionalProperties": false, + "description": "Transaction configuration" + }, + "authRequired": { + "type": "boolean", + "default": true, + "description": "Require authentication" + }, + "permissions": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Required permissions" + }, + "hooks": { + "type": "object", + "properties": { + "before": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Pre-mutation hooks" + }, + "after": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Post-mutation hooks" + } + }, + "additionalProperties": false, + "description": "Lifecycle hooks" + } + }, + "required": [ + "name", + "object", + "type" + ], + "additionalProperties": false + }, + "description": "Mutation configurations" + }, + "subscriptions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Subscription field name (camelCase recommended)" + }, + "object": { + "type": "string", + "description": "Source ObjectQL object name" + }, + "events": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "created", + "updated", + "deleted", + "custom" + ] + }, + "description": "Events to subscribe to" + }, + "description": { + "type": "string", + "description": "Subscription description" + }, + "filter": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Allow filtering subscriptions" + }, + "fields": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Filterable fields" + } + }, + "additionalProperties": false, + "description": "Subscription filtering" + }, + "payload": { + "type": "object", + "properties": { + "includeEntity": { + "type": "boolean", + "default": true, + "description": "Include entity in payload" + }, + "includePreviousValues": { + "type": "boolean", + "default": false, + "description": "Include previous field values" + }, + "includeMeta": { + "type": "boolean", + "default": true, + "description": "Include metadata (timestamp, user, etc.)" + } + }, + "additionalProperties": false, + "description": "Payload configuration" + }, + "authRequired": { + "type": "boolean", + "default": true, + "description": "Require authentication" + }, + "permissions": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Required permissions" + }, + "rateLimit": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable rate limiting" + }, + "maxSubscriptionsPerUser": { + "type": "integer", + "minimum": 1, + "default": 10, + "description": "Max concurrent subscriptions per user" + }, + "throttleMs": { + "type": "integer", + "minimum": 0, + "description": "Throttle interval in milliseconds" + } + }, + "additionalProperties": false, + "description": "Subscription rate limiting" + } + }, + "required": [ + "name", + "object", + "events" + ], + "additionalProperties": false + }, + "description": "Subscription configurations" + }, + "resolvers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "Resolver path (Type.field)" + }, + "type": { + "type": "string", + "enum": [ + "datasource", + "computed", + "script", + "proxy" + ], + "description": "Resolver implementation type" + }, + "implementation": { + "type": "object", + "properties": { + "datasource": { + "type": "string", + "description": "Datasource ID" + }, + "query": { + "type": "string", + "description": "Query/SQL to execute" + }, + "expression": { + "type": "string", + "description": "Computation expression" + }, + "dependencies": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Dependent fields" + }, + "script": { + "type": "string", + "description": "Script ID or inline code" + }, + "url": { + "type": "string", + "description": "Proxy URL" + }, + "method": { + "type": "string", + "enum": [ + "GET", + "POST", + "PUT", + "DELETE" + ], + "description": "HTTP method" + } + }, + "additionalProperties": false, + "description": "Implementation configuration" + }, + "cache": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable resolver caching" + }, + "ttl": { + "type": "integer", + "minimum": 0, + "description": "Cache TTL in seconds" + }, + "keyArgs": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Arguments to include in cache key" + } + }, + "additionalProperties": false, + "description": "Resolver caching" + } + }, + "required": [ + "path", + "type" + ], + "additionalProperties": false + }, + "description": "Custom resolver configurations" + }, + "directives": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z][a-zA-Z0-9]*$", + "description": "Directive name (camelCase)" + }, + "description": { + "type": "string", + "description": "Directive description" + }, + "locations": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "QUERY", + "MUTATION", + "SUBSCRIPTION", + "FIELD", + "FRAGMENT_DEFINITION", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT", + "VARIABLE_DEFINITION", + "SCHEMA", + "SCALAR", + "OBJECT", + "FIELD_DEFINITION", + "ARGUMENT_DEFINITION", + "INTERFACE", + "UNION", + "ENUM", + "ENUM_VALUE", + "INPUT_OBJECT", + "INPUT_FIELD_DEFINITION" + ] + }, + "description": "Directive locations" + }, + "args": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Argument type" + }, + "description": { + "type": "string", + "description": "Argument description" + }, + "defaultValue": { + "description": "Default value" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "description": "Directive arguments" + }, + "repeatable": { + "type": "boolean", + "default": false, + "description": "Can be applied multiple times" + }, + "implementation": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "auth", + "validation", + "transform", + "cache", + "deprecation", + "custom" + ], + "description": "Directive type" + }, + "handler": { + "type": "string", + "description": "Handler function name or script" + } + }, + "required": [ + "type" + ], + "additionalProperties": false, + "description": "Directive implementation" + } + }, + "required": [ + "name", + "locations" + ], + "additionalProperties": false + }, + "description": "Custom directive configurations" + } + }, + "additionalProperties": false, + "description": "Schema generation configuration" + }, + "dataLoaders": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "DataLoader name" + }, + "source": { + "type": "string", + "description": "Source object or datasource" + }, + "batchFunction": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "findByIds", + "query", + "script", + "custom" + ], + "description": "Batch function type" + }, + "keyField": { + "type": "string", + "description": "Field to batch on (e.g., \"id\")" + }, + "query": { + "type": "string", + "description": "Query template" + }, + "script": { + "type": "string", + "description": "Script ID" + }, + "maxBatchSize": { + "type": "integer", + "minimum": 1, + "default": 100, + "description": "Maximum batch size" + } + }, + "required": [ + "type" + ], + "additionalProperties": false, + "description": "Batch function configuration" + }, + "cache": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable per-request caching" + }, + "keyFn": { + "type": "string", + "description": "Custom cache key function" + } + }, + "additionalProperties": false, + "description": "DataLoader caching" + }, + "options": { + "type": "object", + "properties": { + "batch": { + "type": "boolean", + "default": true, + "description": "Enable batching" + }, + "cache": { + "type": "boolean", + "default": true, + "description": "Enable caching" + }, + "maxCacheSize": { + "type": "integer", + "minimum": 0, + "description": "Max cache entries" + } + }, + "additionalProperties": false, + "description": "DataLoader options" + } + }, + "required": [ + "name", + "source", + "batchFunction" + ], + "additionalProperties": false + }, + "description": "DataLoader configurations" + }, + "security": { + "type": "object", + "properties": { + "depthLimit": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable query depth limiting" + }, + "maxDepth": { + "type": "integer", + "minimum": 1, + "default": 10, + "description": "Maximum query depth" + }, + "ignoreFields": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Fields excluded from depth calculation" + }, + "onDepthExceeded": { + "type": "string", + "enum": [ + "reject", + "log", + "warn" + ], + "default": "reject", + "description": "Action when depth exceeded" + }, + "errorMessage": { + "type": "string", + "description": "Custom error message for depth violations" + } + }, + "additionalProperties": false, + "description": "Query depth limiting" + }, + "complexity": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable query complexity limiting" + }, + "maxComplexity": { + "type": "integer", + "minimum": 1, + "default": 1000, + "description": "Maximum query complexity" + }, + "defaultFieldComplexity": { + "type": "integer", + "minimum": 0, + "default": 1, + "description": "Default complexity per field" + }, + "fieldComplexity": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "object", + "properties": { + "base": { + "type": "integer", + "minimum": 0, + "description": "Base complexity" + }, + "multiplier": { + "type": "string", + "description": "Argument multiplier (e.g., \"limit\")" + }, + "calculator": { + "type": "string", + "description": "Custom calculator function" + } + }, + "required": [ + "base" + ], + "additionalProperties": false + } + ] + }, + "description": "Per-field complexity configuration" + }, + "listMultiplier": { + "type": "number", + "minimum": 0, + "default": 10, + "description": "Multiplier for list fields" + }, + "onComplexityExceeded": { + "type": "string", + "enum": [ + "reject", + "log", + "warn" + ], + "default": "reject", + "description": "Action when complexity exceeded" + }, + "errorMessage": { + "type": "string", + "description": "Custom error message for complexity violations" + } + }, + "additionalProperties": false, + "description": "Query complexity calculation" + }, + "rateLimit": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable rate limiting" + }, + "strategy": { + "type": "string", + "enum": [ + "token_bucket", + "fixed_window", + "sliding_window", + "cost_based" + ], + "default": "token_bucket", + "description": "Rate limiting strategy" + }, + "global": { + "type": "object", + "properties": { + "maxRequests": { + "type": "integer", + "minimum": 1, + "default": 1000, + "description": "Maximum requests per window" + }, + "windowMs": { + "type": "integer", + "minimum": 1000, + "default": 60000, + "description": "Time window in milliseconds" + } + }, + "additionalProperties": false, + "description": "Global rate limits" + }, + "perUser": { + "type": "object", + "properties": { + "maxRequests": { + "type": "integer", + "minimum": 1, + "default": 100, + "description": "Maximum requests per user per window" + }, + "windowMs": { + "type": "integer", + "minimum": 1000, + "default": 60000, + "description": "Time window in milliseconds" + } + }, + "additionalProperties": false, + "description": "Per-user rate limits" + }, + "costBased": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable cost-based rate limiting" + }, + "maxCost": { + "type": "integer", + "minimum": 1, + "default": 10000, + "description": "Maximum cost per window" + }, + "windowMs": { + "type": "integer", + "minimum": 1000, + "default": 60000, + "description": "Time window in milliseconds" + }, + "useComplexityAsCost": { + "type": "boolean", + "default": true, + "description": "Use query complexity as cost" + } + }, + "additionalProperties": false, + "description": "Cost-based rate limiting" + }, + "operations": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "maxRequests": { + "type": "integer", + "minimum": 1, + "description": "Max requests for this operation" + }, + "windowMs": { + "type": "integer", + "minimum": 1000, + "description": "Time window" + } + }, + "required": [ + "maxRequests", + "windowMs" + ], + "additionalProperties": false + }, + "description": "Per-operation rate limits" + }, + "onLimitExceeded": { + "type": "string", + "enum": [ + "reject", + "queue", + "log" + ], + "default": "reject", + "description": "Action when rate limit exceeded" + }, + "errorMessage": { + "type": "string", + "description": "Custom error message for rate limit violations" + }, + "includeHeaders": { + "type": "boolean", + "default": true, + "description": "Include rate limit headers in response" + } + }, + "additionalProperties": false, + "description": "Rate limiting" + }, + "persistedQueries": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable persisted queries" + }, + "mode": { + "type": "string", + "enum": [ + "optional", + "required" + ], + "default": "optional", + "description": "Persisted query mode (optional: allow both, required: only persisted)" + }, + "store": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "memory", + "redis", + "database", + "file" + ], + "default": "memory", + "description": "Query store type" + }, + "connection": { + "type": "string", + "description": "Store connection string or path" + }, + "ttl": { + "type": "integer", + "minimum": 0, + "description": "TTL in seconds for stored queries" + } + }, + "additionalProperties": false, + "description": "Query store configuration" + }, + "apq": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable Automatic Persisted Queries" + }, + "hashAlgorithm": { + "type": "string", + "enum": [ + "sha256", + "sha1", + "md5" + ], + "default": "sha256", + "description": "Hash algorithm for query IDs" + }, + "cache": { + "type": "object", + "properties": { + "ttl": { + "type": "integer", + "minimum": 0, + "default": 3600, + "description": "Cache TTL in seconds" + }, + "maxSize": { + "type": "integer", + "minimum": 1, + "description": "Maximum number of cached queries" + } + }, + "additionalProperties": false, + "description": "APQ cache configuration" + } + }, + "additionalProperties": false, + "description": "Automatic Persisted Queries configuration" + }, + "allowlist": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable query allow list (reject queries not in list)" + }, + "queries": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Query ID or hash" + }, + "operation": { + "type": "string", + "description": "Operation name" + }, + "query": { + "type": "string", + "description": "Query string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + }, + "description": "Allowed queries" + }, + "source": { + "type": "string", + "description": "External allow list source (file path or URL)" + } + }, + "additionalProperties": false, + "description": "Query allow list configuration" + }, + "security": { + "type": "object", + "properties": { + "maxQuerySize": { + "type": "integer", + "minimum": 1, + "description": "Maximum query string size in bytes" + }, + "rejectIntrospection": { + "type": "boolean", + "default": false, + "description": "Reject introspection queries" + } + }, + "additionalProperties": false, + "description": "Security configuration" + } + }, + "additionalProperties": false, + "description": "Persisted queries" + } + }, + "additionalProperties": false, + "description": "Security configuration" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/api/GraphQLDataLoaderConfig.json b/packages/spec/json-schema/api/GraphQLDataLoaderConfig.json new file mode 100644 index 00000000000..8a5f640c65a --- /dev/null +++ b/packages/spec/json-schema/api/GraphQLDataLoaderConfig.json @@ -0,0 +1,101 @@ +{ + "$ref": "#/definitions/GraphQLDataLoaderConfig", + "definitions": { + "GraphQLDataLoaderConfig": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "DataLoader name" + }, + "source": { + "type": "string", + "description": "Source object or datasource" + }, + "batchFunction": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "findByIds", + "query", + "script", + "custom" + ], + "description": "Batch function type" + }, + "keyField": { + "type": "string", + "description": "Field to batch on (e.g., \"id\")" + }, + "query": { + "type": "string", + "description": "Query template" + }, + "script": { + "type": "string", + "description": "Script ID" + }, + "maxBatchSize": { + "type": "integer", + "minimum": 1, + "default": 100, + "description": "Maximum batch size" + } + }, + "required": [ + "type" + ], + "additionalProperties": false, + "description": "Batch function configuration" + }, + "cache": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable per-request caching" + }, + "keyFn": { + "type": "string", + "description": "Custom cache key function" + } + }, + "additionalProperties": false, + "description": "DataLoader caching" + }, + "options": { + "type": "object", + "properties": { + "batch": { + "type": "boolean", + "default": true, + "description": "Enable batching" + }, + "cache": { + "type": "boolean", + "default": true, + "description": "Enable caching" + }, + "maxCacheSize": { + "type": "integer", + "minimum": 0, + "description": "Max cache entries" + } + }, + "additionalProperties": false, + "description": "DataLoader options" + } + }, + "required": [ + "name", + "source", + "batchFunction" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/api/GraphQLDirectiveConfig.json b/packages/spec/json-schema/api/GraphQLDirectiveConfig.json new file mode 100644 index 00000000000..9ac1bdb2d19 --- /dev/null +++ b/packages/spec/json-schema/api/GraphQLDirectiveConfig.json @@ -0,0 +1,108 @@ +{ + "$ref": "#/definitions/GraphQLDirectiveConfig", + "definitions": { + "GraphQLDirectiveConfig": { + "type": "object", + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z][a-zA-Z0-9]*$", + "description": "Directive name (camelCase)" + }, + "description": { + "type": "string", + "description": "Directive description" + }, + "locations": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "QUERY", + "MUTATION", + "SUBSCRIPTION", + "FIELD", + "FRAGMENT_DEFINITION", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT", + "VARIABLE_DEFINITION", + "SCHEMA", + "SCALAR", + "OBJECT", + "FIELD_DEFINITION", + "ARGUMENT_DEFINITION", + "INTERFACE", + "UNION", + "ENUM", + "ENUM_VALUE", + "INPUT_OBJECT", + "INPUT_FIELD_DEFINITION" + ] + }, + "description": "Directive locations" + }, + "args": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Argument type" + }, + "description": { + "type": "string", + "description": "Argument description" + }, + "defaultValue": { + "description": "Default value" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "description": "Directive arguments" + }, + "repeatable": { + "type": "boolean", + "default": false, + "description": "Can be applied multiple times" + }, + "implementation": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "auth", + "validation", + "transform", + "cache", + "deprecation", + "custom" + ], + "description": "Directive type" + }, + "handler": { + "type": "string", + "description": "Handler function name or script" + } + }, + "required": [ + "type" + ], + "additionalProperties": false, + "description": "Directive implementation" + } + }, + "required": [ + "name", + "locations" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/api/GraphQLDirectiveLocation.json b/packages/spec/json-schema/api/GraphQLDirectiveLocation.json new file mode 100644 index 00000000000..0cf630e2726 --- /dev/null +++ b/packages/spec/json-schema/api/GraphQLDirectiveLocation.json @@ -0,0 +1,30 @@ +{ + "$ref": "#/definitions/GraphQLDirectiveLocation", + "definitions": { + "GraphQLDirectiveLocation": { + "type": "string", + "enum": [ + "QUERY", + "MUTATION", + "SUBSCRIPTION", + "FIELD", + "FRAGMENT_DEFINITION", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT", + "VARIABLE_DEFINITION", + "SCHEMA", + "SCALAR", + "OBJECT", + "FIELD_DEFINITION", + "ARGUMENT_DEFINITION", + "INTERFACE", + "UNION", + "ENUM", + "ENUM_VALUE", + "INPUT_OBJECT", + "INPUT_FIELD_DEFINITION" + ] + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/api/GraphQLMutationConfig.json b/packages/spec/json-schema/api/GraphQLMutationConfig.json new file mode 100644 index 00000000000..5777879b800 --- /dev/null +++ b/packages/spec/json-schema/api/GraphQLMutationConfig.json @@ -0,0 +1,180 @@ +{ + "$ref": "#/definitions/GraphQLMutationConfig", + "definitions": { + "GraphQLMutationConfig": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Mutation field name (camelCase recommended)" + }, + "object": { + "type": "string", + "description": "Source ObjectQL object name" + }, + "type": { + "type": "string", + "enum": [ + "create", + "update", + "delete", + "upsert", + "custom" + ], + "description": "Mutation type" + }, + "description": { + "type": "string", + "description": "Mutation description" + }, + "input": { + "type": "object", + "properties": { + "typeName": { + "type": "string", + "description": "Custom input type name" + }, + "fields": { + "type": "object", + "properties": { + "include": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Fields to include" + }, + "exclude": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Fields to exclude" + }, + "required": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Required input fields" + } + }, + "additionalProperties": false, + "description": "Input field configuration" + }, + "validation": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable input validation" + }, + "rules": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Custom validation rules" + } + }, + "additionalProperties": false, + "description": "Input validation" + } + }, + "additionalProperties": false, + "description": "Input configuration" + }, + "output": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "object", + "payload", + "boolean", + "custom" + ], + "default": "object", + "description": "Output type" + }, + "includeEnvelope": { + "type": "boolean", + "default": false, + "description": "Wrap in success/error payload" + }, + "customType": { + "type": "string", + "description": "Custom output type name" + } + }, + "additionalProperties": false, + "description": "Output configuration" + }, + "transaction": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Use database transaction" + }, + "isolationLevel": { + "type": "string", + "enum": [ + "read_uncommitted", + "read_committed", + "repeatable_read", + "serializable" + ], + "description": "Transaction isolation level" + } + }, + "additionalProperties": false, + "description": "Transaction configuration" + }, + "authRequired": { + "type": "boolean", + "default": true, + "description": "Require authentication" + }, + "permissions": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Required permissions" + }, + "hooks": { + "type": "object", + "properties": { + "before": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Pre-mutation hooks" + }, + "after": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Post-mutation hooks" + } + }, + "additionalProperties": false, + "description": "Lifecycle hooks" + } + }, + "required": [ + "name", + "object", + "type" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/api/GraphQLPersistedQuery.json b/packages/spec/json-schema/api/GraphQLPersistedQuery.json new file mode 100644 index 00000000000..cbfe6062c7d --- /dev/null +++ b/packages/spec/json-schema/api/GraphQLPersistedQuery.json @@ -0,0 +1,151 @@ +{ + "$ref": "#/definitions/GraphQLPersistedQuery", + "definitions": { + "GraphQLPersistedQuery": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable persisted queries" + }, + "mode": { + "type": "string", + "enum": [ + "optional", + "required" + ], + "default": "optional", + "description": "Persisted query mode (optional: allow both, required: only persisted)" + }, + "store": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "memory", + "redis", + "database", + "file" + ], + "default": "memory", + "description": "Query store type" + }, + "connection": { + "type": "string", + "description": "Store connection string or path" + }, + "ttl": { + "type": "integer", + "minimum": 0, + "description": "TTL in seconds for stored queries" + } + }, + "additionalProperties": false, + "description": "Query store configuration" + }, + "apq": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable Automatic Persisted Queries" + }, + "hashAlgorithm": { + "type": "string", + "enum": [ + "sha256", + "sha1", + "md5" + ], + "default": "sha256", + "description": "Hash algorithm for query IDs" + }, + "cache": { + "type": "object", + "properties": { + "ttl": { + "type": "integer", + "minimum": 0, + "default": 3600, + "description": "Cache TTL in seconds" + }, + "maxSize": { + "type": "integer", + "minimum": 1, + "description": "Maximum number of cached queries" + } + }, + "additionalProperties": false, + "description": "APQ cache configuration" + } + }, + "additionalProperties": false, + "description": "Automatic Persisted Queries configuration" + }, + "allowlist": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable query allow list (reject queries not in list)" + }, + "queries": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Query ID or hash" + }, + "operation": { + "type": "string", + "description": "Operation name" + }, + "query": { + "type": "string", + "description": "Query string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + }, + "description": "Allowed queries" + }, + "source": { + "type": "string", + "description": "External allow list source (file path or URL)" + } + }, + "additionalProperties": false, + "description": "Query allow list configuration" + }, + "security": { + "type": "object", + "properties": { + "maxQuerySize": { + "type": "integer", + "minimum": 1, + "description": "Maximum query string size in bytes" + }, + "rejectIntrospection": { + "type": "boolean", + "default": false, + "description": "Reject introspection queries" + } + }, + "additionalProperties": false, + "description": "Security configuration" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/api/GraphQLQueryComplexity.json b/packages/spec/json-schema/api/GraphQLQueryComplexity.json new file mode 100644 index 00000000000..34ba299a5b8 --- /dev/null +++ b/packages/spec/json-schema/api/GraphQLQueryComplexity.json @@ -0,0 +1,83 @@ +{ + "$ref": "#/definitions/GraphQLQueryComplexity", + "definitions": { + "GraphQLQueryComplexity": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable query complexity limiting" + }, + "maxComplexity": { + "type": "integer", + "minimum": 1, + "default": 1000, + "description": "Maximum query complexity" + }, + "defaultFieldComplexity": { + "type": "integer", + "minimum": 0, + "default": 1, + "description": "Default complexity per field" + }, + "fieldComplexity": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "object", + "properties": { + "base": { + "type": "integer", + "minimum": 0, + "description": "Base complexity" + }, + "multiplier": { + "type": "string", + "description": "Argument multiplier (e.g., \"limit\")" + }, + "calculator": { + "type": "string", + "description": "Custom calculator function" + } + }, + "required": [ + "base" + ], + "additionalProperties": false + } + ] + }, + "description": "Per-field complexity configuration" + }, + "listMultiplier": { + "type": "number", + "minimum": 0, + "default": 10, + "description": "Multiplier for list fields" + }, + "onComplexityExceeded": { + "type": "string", + "enum": [ + "reject", + "log", + "warn" + ], + "default": "reject", + "description": "Action when complexity exceeded" + }, + "errorMessage": { + "type": "string", + "description": "Custom error message for complexity violations" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/api/GraphQLQueryConfig.json b/packages/spec/json-schema/api/GraphQLQueryConfig.json new file mode 100644 index 00000000000..e9cad9ea40d --- /dev/null +++ b/packages/spec/json-schema/api/GraphQLQueryConfig.json @@ -0,0 +1,231 @@ +{ + "$ref": "#/definitions/GraphQLQueryConfig", + "definitions": { + "GraphQLQueryConfig": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Query field name (camelCase recommended)" + }, + "object": { + "type": "string", + "description": "Source ObjectQL object name" + }, + "type": { + "type": "string", + "enum": [ + "get", + "list", + "search" + ], + "description": "Query type" + }, + "description": { + "type": "string", + "description": "Query description" + }, + "args": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "GraphQL type (e.g., \"ID!\", \"String\", \"Int\")" + }, + "description": { + "type": "string", + "description": "Argument description" + }, + "defaultValue": { + "description": "Default value" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "description": "Query arguments" + }, + "filtering": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Allow filtering" + }, + "fields": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Filterable fields" + }, + "operators": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "eq", + "ne", + "gt", + "gte", + "lt", + "lte", + "in", + "notIn", + "contains", + "startsWith", + "endsWith", + "isNull", + "isNotNull" + ] + }, + "description": "Allowed filter operators" + } + }, + "additionalProperties": false, + "description": "Filtering capabilities" + }, + "sorting": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Allow sorting" + }, + "fields": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Sortable fields" + }, + "defaultSort": { + "type": "object", + "properties": { + "field": { + "type": "string" + }, + "direction": { + "type": "string", + "enum": [ + "ASC", + "DESC" + ] + } + }, + "required": [ + "field", + "direction" + ], + "additionalProperties": false, + "description": "Default sort order" + } + }, + "additionalProperties": false, + "description": "Sorting capabilities" + }, + "pagination": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable pagination" + }, + "type": { + "type": "string", + "enum": [ + "offset", + "cursor", + "relay" + ], + "default": "offset", + "description": "Pagination style" + }, + "defaultLimit": { + "type": "integer", + "minimum": 1, + "default": 20, + "description": "Default page size" + }, + "maxLimit": { + "type": "integer", + "minimum": 1, + "default": 100, + "description": "Maximum page size" + } + }, + "additionalProperties": false, + "description": "Pagination configuration" + }, + "fields": { + "type": "object", + "properties": { + "required": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Required fields (always returned)" + }, + "selectable": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Selectable fields" + } + }, + "additionalProperties": false, + "description": "Field selection configuration" + }, + "authRequired": { + "type": "boolean", + "default": true, + "description": "Require authentication" + }, + "permissions": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Required permissions" + }, + "cache": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable caching" + }, + "ttl": { + "type": "integer", + "minimum": 0, + "description": "Cache TTL in seconds" + }, + "key": { + "type": "string", + "description": "Cache key template" + } + }, + "additionalProperties": false, + "description": "Query caching" + } + }, + "required": [ + "name", + "object", + "type" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/api/GraphQLQueryDepthLimit.json b/packages/spec/json-schema/api/GraphQLQueryDepthLimit.json new file mode 100644 index 00000000000..f15b683e2ae --- /dev/null +++ b/packages/spec/json-schema/api/GraphQLQueryDepthLimit.json @@ -0,0 +1,44 @@ +{ + "$ref": "#/definitions/GraphQLQueryDepthLimit", + "definitions": { + "GraphQLQueryDepthLimit": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable query depth limiting" + }, + "maxDepth": { + "type": "integer", + "minimum": 1, + "default": 10, + "description": "Maximum query depth" + }, + "ignoreFields": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Fields excluded from depth calculation" + }, + "onDepthExceeded": { + "type": "string", + "enum": [ + "reject", + "log", + "warn" + ], + "default": "reject", + "description": "Action when depth exceeded" + }, + "errorMessage": { + "type": "string", + "description": "Custom error message for depth violations" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/api/GraphQLRateLimit.json b/packages/spec/json-schema/api/GraphQLRateLimit.json new file mode 100644 index 00000000000..2c053e6227b --- /dev/null +++ b/packages/spec/json-schema/api/GraphQLRateLimit.json @@ -0,0 +1,138 @@ +{ + "$ref": "#/definitions/GraphQLRateLimit", + "definitions": { + "GraphQLRateLimit": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable rate limiting" + }, + "strategy": { + "type": "string", + "enum": [ + "token_bucket", + "fixed_window", + "sliding_window", + "cost_based" + ], + "default": "token_bucket", + "description": "Rate limiting strategy" + }, + "global": { + "type": "object", + "properties": { + "maxRequests": { + "type": "integer", + "minimum": 1, + "default": 1000, + "description": "Maximum requests per window" + }, + "windowMs": { + "type": "integer", + "minimum": 1000, + "default": 60000, + "description": "Time window in milliseconds" + } + }, + "additionalProperties": false, + "description": "Global rate limits" + }, + "perUser": { + "type": "object", + "properties": { + "maxRequests": { + "type": "integer", + "minimum": 1, + "default": 100, + "description": "Maximum requests per user per window" + }, + "windowMs": { + "type": "integer", + "minimum": 1000, + "default": 60000, + "description": "Time window in milliseconds" + } + }, + "additionalProperties": false, + "description": "Per-user rate limits" + }, + "costBased": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable cost-based rate limiting" + }, + "maxCost": { + "type": "integer", + "minimum": 1, + "default": 10000, + "description": "Maximum cost per window" + }, + "windowMs": { + "type": "integer", + "minimum": 1000, + "default": 60000, + "description": "Time window in milliseconds" + }, + "useComplexityAsCost": { + "type": "boolean", + "default": true, + "description": "Use query complexity as cost" + } + }, + "additionalProperties": false, + "description": "Cost-based rate limiting" + }, + "operations": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "maxRequests": { + "type": "integer", + "minimum": 1, + "description": "Max requests for this operation" + }, + "windowMs": { + "type": "integer", + "minimum": 1000, + "description": "Time window" + } + }, + "required": [ + "maxRequests", + "windowMs" + ], + "additionalProperties": false + }, + "description": "Per-operation rate limits" + }, + "onLimitExceeded": { + "type": "string", + "enum": [ + "reject", + "queue", + "log" + ], + "default": "reject", + "description": "Action when rate limit exceeded" + }, + "errorMessage": { + "type": "string", + "description": "Custom error message for rate limit violations" + }, + "includeHeaders": { + "type": "boolean", + "default": true, + "description": "Include rate limit headers in response" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/api/GraphQLResolverConfig.json b/packages/spec/json-schema/api/GraphQLResolverConfig.json new file mode 100644 index 00000000000..d0a411372ca --- /dev/null +++ b/packages/spec/json-schema/api/GraphQLResolverConfig.json @@ -0,0 +1,98 @@ +{ + "$ref": "#/definitions/GraphQLResolverConfig", + "definitions": { + "GraphQLResolverConfig": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "Resolver path (Type.field)" + }, + "type": { + "type": "string", + "enum": [ + "datasource", + "computed", + "script", + "proxy" + ], + "description": "Resolver implementation type" + }, + "implementation": { + "type": "object", + "properties": { + "datasource": { + "type": "string", + "description": "Datasource ID" + }, + "query": { + "type": "string", + "description": "Query/SQL to execute" + }, + "expression": { + "type": "string", + "description": "Computation expression" + }, + "dependencies": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Dependent fields" + }, + "script": { + "type": "string", + "description": "Script ID or inline code" + }, + "url": { + "type": "string", + "description": "Proxy URL" + }, + "method": { + "type": "string", + "enum": [ + "GET", + "POST", + "PUT", + "DELETE" + ], + "description": "HTTP method" + } + }, + "additionalProperties": false, + "description": "Implementation configuration" + }, + "cache": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable resolver caching" + }, + "ttl": { + "type": "integer", + "minimum": 0, + "description": "Cache TTL in seconds" + }, + "keyArgs": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Arguments to include in cache key" + } + }, + "additionalProperties": false, + "description": "Resolver caching" + } + }, + "required": [ + "path", + "type" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/api/GraphQLScalarType.json b/packages/spec/json-schema/api/GraphQLScalarType.json new file mode 100644 index 00000000000..836231b7a42 --- /dev/null +++ b/packages/spec/json-schema/api/GraphQLScalarType.json @@ -0,0 +1,32 @@ +{ + "$ref": "#/definitions/GraphQLScalarType", + "definitions": { + "GraphQLScalarType": { + "type": "string", + "enum": [ + "ID", + "String", + "Int", + "Float", + "Boolean", + "DateTime", + "Date", + "Time", + "JSON", + "JSONObject", + "Upload", + "URL", + "Email", + "PhoneNumber", + "Currency", + "Decimal", + "BigInt", + "Long", + "UUID", + "Base64", + "Void" + ] + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/api/GraphQLSubscriptionConfig.json b/packages/spec/json-schema/api/GraphQLSubscriptionConfig.json new file mode 100644 index 00000000000..4ded12bbfe7 --- /dev/null +++ b/packages/spec/json-schema/api/GraphQLSubscriptionConfig.json @@ -0,0 +1,118 @@ +{ + "$ref": "#/definitions/GraphQLSubscriptionConfig", + "definitions": { + "GraphQLSubscriptionConfig": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Subscription field name (camelCase recommended)" + }, + "object": { + "type": "string", + "description": "Source ObjectQL object name" + }, + "events": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "created", + "updated", + "deleted", + "custom" + ] + }, + "description": "Events to subscribe to" + }, + "description": { + "type": "string", + "description": "Subscription description" + }, + "filter": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Allow filtering subscriptions" + }, + "fields": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Filterable fields" + } + }, + "additionalProperties": false, + "description": "Subscription filtering" + }, + "payload": { + "type": "object", + "properties": { + "includeEntity": { + "type": "boolean", + "default": true, + "description": "Include entity in payload" + }, + "includePreviousValues": { + "type": "boolean", + "default": false, + "description": "Include previous field values" + }, + "includeMeta": { + "type": "boolean", + "default": true, + "description": "Include metadata (timestamp, user, etc.)" + } + }, + "additionalProperties": false, + "description": "Payload configuration" + }, + "authRequired": { + "type": "boolean", + "default": true, + "description": "Require authentication" + }, + "permissions": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Required permissions" + }, + "rateLimit": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable rate limiting" + }, + "maxSubscriptionsPerUser": { + "type": "integer", + "minimum": 1, + "default": 10, + "description": "Max concurrent subscriptions per user" + }, + "throttleMs": { + "type": "integer", + "minimum": 0, + "description": "Throttle interval in milliseconds" + } + }, + "additionalProperties": false, + "description": "Subscription rate limiting" + } + }, + "required": [ + "name", + "object", + "events" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/api/GraphQLTypeConfig.json b/packages/spec/json-schema/api/GraphQLTypeConfig.json new file mode 100644 index 00000000000..f79f34e6611 --- /dev/null +++ b/packages/spec/json-schema/api/GraphQLTypeConfig.json @@ -0,0 +1,113 @@ +{ + "$ref": "#/definitions/GraphQLTypeConfig", + "definitions": { + "GraphQLTypeConfig": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "GraphQL type name (PascalCase recommended)" + }, + "object": { + "type": "string", + "description": "Source ObjectQL object name" + }, + "description": { + "type": "string", + "description": "Type description" + }, + "fields": { + "type": "object", + "properties": { + "include": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Fields to include" + }, + "exclude": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Fields to exclude (e.g., sensitive fields)" + }, + "mappings": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "graphqlName": { + "type": "string", + "description": "Custom GraphQL field name" + }, + "graphqlType": { + "type": "string", + "description": "Override GraphQL type" + }, + "description": { + "type": "string", + "description": "Field description" + }, + "deprecationReason": { + "type": "string", + "description": "Why field is deprecated" + }, + "nullable": { + "type": "boolean", + "description": "Override nullable" + } + }, + "additionalProperties": false + }, + "description": "Field-level customizations" + } + }, + "additionalProperties": false, + "description": "Field configuration" + }, + "interfaces": { + "type": "array", + "items": { + "type": "string" + }, + "description": "GraphQL interface names" + }, + "isInterface": { + "type": "boolean", + "default": false, + "description": "Define as GraphQL interface" + }, + "directives": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Directive name" + }, + "args": { + "type": "object", + "additionalProperties": {}, + "description": "Directive arguments" + } + }, + "required": [ + "name" + ], + "additionalProperties": false + }, + "description": "GraphQL directives" + } + }, + "required": [ + "name", + "object" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/src/api/graphql.test.ts b/packages/spec/src/api/graphql.test.ts new file mode 100644 index 00000000000..6566f2db29c --- /dev/null +++ b/packages/spec/src/api/graphql.test.ts @@ -0,0 +1,962 @@ +import { describe, it, expect } from 'vitest'; +import { + GraphQLScalarType, + GraphQLTypeConfigSchema, + GraphQLQueryConfigSchema, + GraphQLMutationConfigSchema, + GraphQLSubscriptionConfigSchema, + GraphQLResolverConfigSchema, + GraphQLDataLoaderConfigSchema, + GraphQLDirectiveConfigSchema, + GraphQLDirectiveLocation, + GraphQLQueryDepthLimitSchema, + GraphQLQueryComplexitySchema, + GraphQLRateLimitSchema, + GraphQLPersistedQuerySchema, + GraphQLConfigSchema, + GraphQLConfig, + mapFieldTypeToGraphQL, +} from './graphql.zod'; + +describe('GraphQLScalarType', () => { + it('should accept built-in GraphQL scalar types', () => { + const builtInTypes = ['ID', 'String', 'Int', 'Float', 'Boolean']; + + builtInTypes.forEach(type => { + expect(() => GraphQLScalarType.parse(type)).not.toThrow(); + }); + }); + + it('should accept extended scalar types', () => { + const extendedTypes = ['DateTime', 'Date', 'JSON', 'URL', 'Email', 'UUID']; + + extendedTypes.forEach(type => { + expect(() => GraphQLScalarType.parse(type)).not.toThrow(); + }); + }); + + it('should reject invalid scalar types', () => { + expect(() => GraphQLScalarType.parse('CustomType')).toThrow(); + expect(() => GraphQLScalarType.parse('NotAScalar')).toThrow(); + }); +}); + +describe('GraphQLTypeConfigSchema', () => { + it('should accept minimal type configuration', () => { + const config = GraphQLTypeConfigSchema.parse({ + name: 'Customer', + object: 'customer', + }); + + expect(config.name).toBe('Customer'); + expect(config.object).toBe('customer'); + }); + + it('should accept type configuration with field mappings', () => { + const config = GraphQLTypeConfigSchema.parse({ + name: 'Customer', + object: 'customer', + description: 'Customer type', + fields: { + include: ['id', 'name', 'email'], + exclude: ['password', 'ssn'], + mappings: { + email: { + graphqlName: 'emailAddress', + description: 'Customer email', + }, + }, + }, + }); + + expect(config.fields?.include).toContain('email'); + expect(config.fields?.exclude).toContain('password'); + expect(config.fields?.mappings?.email?.graphqlName).toBe('emailAddress'); + }); + + it('should accept type with interfaces', () => { + const config = GraphQLTypeConfigSchema.parse({ + name: 'Customer', + object: 'customer', + interfaces: ['Node', 'Timestamped'], + }); + + expect(config.interfaces).toHaveLength(2); + }); + + it('should accept interface definition', () => { + const config = GraphQLTypeConfigSchema.parse({ + name: 'Node', + object: 'base_entity', + isInterface: true, + }); + + expect(config.isInterface).toBe(true); + }); + + it('should accept type with directives', () => { + const config = GraphQLTypeConfigSchema.parse({ + name: 'Customer', + object: 'customer', + directives: [ + { name: 'auth', args: { requires: 'ADMIN' } }, + { name: 'cache', args: { maxAge: 60 } }, + ], + }); + + expect(config.directives).toHaveLength(2); + }); +}); + +describe('GraphQLQueryConfigSchema', () => { + it('should accept minimal query configuration', () => { + const config = GraphQLQueryConfigSchema.parse({ + name: 'customer', + object: 'customer', + type: 'get', + }); + + expect(config.name).toBe('customer'); + expect(config.type).toBe('get'); + }); + + it('should accept list query with filtering', () => { + const config = GraphQLQueryConfigSchema.parse({ + name: 'customers', + object: 'customer', + type: 'list', + filtering: { + enabled: true, + fields: ['name', 'email', 'status'], + operators: ['eq', 'ne', 'contains', 'in'], + }, + }); + + expect(config.filtering?.enabled).toBe(true); + expect(config.filtering?.fields).toContain('name'); + }); + + it('should accept query with sorting', () => { + const config = GraphQLQueryConfigSchema.parse({ + name: 'customers', + object: 'customer', + type: 'list', + sorting: { + enabled: true, + fields: ['name', 'createdAt'], + defaultSort: { + field: 'createdAt', + direction: 'DESC', + }, + }, + }); + + expect(config.sorting?.defaultSort?.direction).toBe('DESC'); + }); + + it('should accept query with pagination', () => { + const config = GraphQLQueryConfigSchema.parse({ + name: 'customers', + object: 'customer', + type: 'list', + pagination: { + enabled: true, + type: 'cursor', + defaultLimit: 50, + maxLimit: 200, + }, + }); + + expect(config.pagination?.type).toBe('cursor'); + expect(config.pagination?.maxLimit).toBe(200); + }); + + it('should accept different pagination types', () => { + const types: Array<'offset' | 'cursor' | 'relay'> = ['offset', 'cursor', 'relay']; + + types.forEach(type => { + const config = GraphQLQueryConfigSchema.parse({ + name: 'test', + object: 'test', + type: 'list', + pagination: { enabled: true, type }, + }); + expect(config.pagination?.type).toBe(type); + }); + }); + + it('should accept query with arguments', () => { + const config = GraphQLQueryConfigSchema.parse({ + name: 'customer', + object: 'customer', + type: 'get', + args: { + id: { + type: 'ID!', + description: 'Customer ID', + }, + }, + }); + + expect(config.args?.id?.type).toBe('ID!'); + }); + + it('should accept query with caching', () => { + const config = GraphQLQueryConfigSchema.parse({ + name: 'customer', + object: 'customer', + type: 'get', + cache: { + enabled: true, + ttl: 300, + key: 'customer:{id}', + }, + }); + + expect(config.cache?.enabled).toBe(true); + expect(config.cache?.ttl).toBe(300); + }); + + it('should apply default authRequired', () => { + const config = GraphQLQueryConfigSchema.parse({ + name: 'test', + object: 'test', + type: 'get', + }); + + expect(config.authRequired).toBe(true); + }); +}); + +describe('GraphQLMutationConfigSchema', () => { + it('should accept minimal mutation configuration', () => { + const config = GraphQLMutationConfigSchema.parse({ + name: 'createCustomer', + object: 'customer', + type: 'create', + }); + + expect(config.name).toBe('createCustomer'); + expect(config.type).toBe('create'); + }); + + it('should accept different mutation types', () => { + const types: Array<'create' | 'update' | 'delete' | 'upsert' | 'custom'> = + ['create', 'update', 'delete', 'upsert', 'custom']; + + types.forEach(type => { + const config = GraphQLMutationConfigSchema.parse({ + name: 'testMutation', + object: 'test', + type, + }); + expect(config.type).toBe(type); + }); + }); + + it('should accept mutation with input configuration', () => { + const config = GraphQLMutationConfigSchema.parse({ + name: 'createCustomer', + object: 'customer', + type: 'create', + input: { + typeName: 'CreateCustomerInput', + fields: { + include: ['name', 'email', 'phone'], + exclude: ['id', 'createdAt'], + required: ['name', 'email'], + }, + validation: { + enabled: true, + rules: ['email_format', 'phone_format'], + }, + }, + }); + + expect(config.input?.typeName).toBe('CreateCustomerInput'); + expect(config.input?.fields?.required).toContain('email'); + }); + + it('should accept mutation with output configuration', () => { + const config = GraphQLMutationConfigSchema.parse({ + name: 'createCustomer', + object: 'customer', + type: 'create', + output: { + type: 'payload', + includeEnvelope: true, + }, + }); + + expect(config.output?.type).toBe('payload'); + expect(config.output?.includeEnvelope).toBe(true); + }); + + it('should accept mutation with transaction configuration', () => { + const config = GraphQLMutationConfigSchema.parse({ + name: 'createOrder', + object: 'order', + type: 'create', + transaction: { + enabled: true, + isolationLevel: 'serializable', + }, + }); + + expect(config.transaction?.enabled).toBe(true); + expect(config.transaction?.isolationLevel).toBe('serializable'); + }); + + it('should accept mutation with hooks', () => { + const config = GraphQLMutationConfigSchema.parse({ + name: 'createCustomer', + object: 'customer', + type: 'create', + hooks: { + before: ['validate_email', 'check_duplicates'], + after: ['send_welcome_email', 'create_audit_log'], + }, + }); + + expect(config.hooks?.before).toHaveLength(2); + expect(config.hooks?.after).toContain('send_welcome_email'); + }); +}); + +describe('GraphQLSubscriptionConfigSchema', () => { + it('should accept minimal subscription configuration', () => { + const config = GraphQLSubscriptionConfigSchema.parse({ + name: 'customerCreated', + object: 'customer', + events: ['created'], + }); + + expect(config.name).toBe('customerCreated'); + expect(config.events).toContain('created'); + }); + + it('should accept subscription with multiple events', () => { + const config = GraphQLSubscriptionConfigSchema.parse({ + name: 'customerChanged', + object: 'customer', + events: ['created', 'updated', 'deleted'], + }); + + expect(config.events).toHaveLength(3); + }); + + it('should accept subscription with filtering', () => { + const config = GraphQLSubscriptionConfigSchema.parse({ + name: 'customerUpdated', + object: 'customer', + events: ['updated'], + filter: { + enabled: true, + fields: ['status', 'priority'], + }, + }); + + expect(config.filter?.enabled).toBe(true); + }); + + it('should accept subscription with payload configuration', () => { + const config = GraphQLSubscriptionConfigSchema.parse({ + name: 'customerUpdated', + object: 'customer', + events: ['updated'], + payload: { + includeEntity: true, + includePreviousValues: true, + includeMeta: true, + }, + }); + + expect(config.payload?.includePreviousValues).toBe(true); + }); + + it('should accept subscription with rate limiting', () => { + const config = GraphQLSubscriptionConfigSchema.parse({ + name: 'customerUpdated', + object: 'customer', + events: ['updated'], + rateLimit: { + enabled: true, + maxSubscriptionsPerUser: 5, + throttleMs: 1000, + }, + }); + + expect(config.rateLimit?.maxSubscriptionsPerUser).toBe(5); + }); +}); + +describe('GraphQLResolverConfigSchema', () => { + it('should accept minimal resolver configuration', () => { + const config = GraphQLResolverConfigSchema.parse({ + path: 'Query.customers', + type: 'datasource', + }); + + expect(config.path).toBe('Query.customers'); + expect(config.type).toBe('datasource'); + }); + + it('should accept different resolver types', () => { + const types: Array<'datasource' | 'computed' | 'script' | 'proxy'> = + ['datasource', 'computed', 'script', 'proxy']; + + types.forEach(type => { + const config = GraphQLResolverConfigSchema.parse({ + path: 'Query.test', + type, + }); + expect(config.type).toBe(type); + }); + }); + + it('should accept datasource resolver', () => { + const config = GraphQLResolverConfigSchema.parse({ + path: 'Query.customers', + type: 'datasource', + implementation: { + datasource: 'external_db', + query: 'SELECT * FROM customers WHERE id = $1', + }, + }); + + expect(config.implementation?.datasource).toBe('external_db'); + }); + + it('should accept computed resolver', () => { + const config = GraphQLResolverConfigSchema.parse({ + path: 'Customer.fullName', + type: 'computed', + implementation: { + expression: 'concat(firstName, " ", lastName)', + dependencies: ['firstName', 'lastName'], + }, + }); + + expect(config.implementation?.dependencies).toContain('firstName'); + }); + + it('should accept resolver with caching', () => { + const config = GraphQLResolverConfigSchema.parse({ + path: 'Query.customer', + type: 'datasource', + cache: { + enabled: true, + ttl: 600, + keyArgs: ['id'], + }, + }); + + expect(config.cache?.enabled).toBe(true); + expect(config.cache?.keyArgs).toContain('id'); + }); +}); + +describe('GraphQLDataLoaderConfigSchema', () => { + it('should accept minimal DataLoader configuration', () => { + const config = GraphQLDataLoaderConfigSchema.parse({ + name: 'customerLoader', + source: 'customer', + batchFunction: { + type: 'findByIds', + keyField: 'id', + }, + }); + + expect(config.name).toBe('customerLoader'); + expect(config.batchFunction.type).toBe('findByIds'); + }); + + it('should accept different batch function types', () => { + const types: Array<'findByIds' | 'query' | 'script' | 'custom'> = + ['findByIds', 'query', 'script', 'custom']; + + types.forEach(type => { + const config = GraphQLDataLoaderConfigSchema.parse({ + name: 'testLoader', + source: 'test', + batchFunction: { type }, + }); + expect(config.batchFunction.type).toBe(type); + }); + }); + + it('should apply default maxBatchSize', () => { + const config = GraphQLDataLoaderConfigSchema.parse({ + name: 'testLoader', + source: 'test', + batchFunction: { + type: 'findByIds', + }, + }); + + expect(config.batchFunction.maxBatchSize).toBe(100); + }); + + it('should accept DataLoader with caching options', () => { + const config = GraphQLDataLoaderConfigSchema.parse({ + name: 'customerLoader', + source: 'customer', + batchFunction: { + type: 'findByIds', + }, + cache: { + enabled: true, + keyFn: 'customKeyFunction', + }, + options: { + batch: true, + cache: true, + maxCacheSize: 1000, + }, + }); + + expect(config.cache?.enabled).toBe(true); + expect(config.options?.maxCacheSize).toBe(1000); + }); +}); + +describe('GraphQLDirectiveConfigSchema', () => { + it('should accept minimal directive configuration', () => { + const config = GraphQLDirectiveConfigSchema.parse({ + name: 'auth', + locations: ['FIELD_DEFINITION'], + }); + + expect(config.name).toBe('auth'); + expect(config.locations).toContain('FIELD_DEFINITION'); + }); + + it('should accept directive with arguments', () => { + const config = GraphQLDirectiveConfigSchema.parse({ + name: 'auth', + locations: ['FIELD_DEFINITION', 'OBJECT'], + args: { + requires: { + type: 'String!', + description: 'Required permission', + }, + }, + }); + + expect(config.args?.requires?.type).toBe('String!'); + }); + + it('should accept repeatable directive', () => { + const config = GraphQLDirectiveConfigSchema.parse({ + name: 'validate', + locations: ['FIELD_DEFINITION'], + repeatable: true, + }); + + expect(config.repeatable).toBe(true); + }); + + it('should validate directive name format', () => { + expect(() => GraphQLDirectiveConfigSchema.parse({ + name: 'myDirective', + locations: ['FIELD'], + })).not.toThrow(); + + expect(() => GraphQLDirectiveConfigSchema.parse({ + name: 'MyDirective', + locations: ['FIELD'], + })).toThrow(); + + expect(() => GraphQLDirectiveConfigSchema.parse({ + name: 'my-directive', + locations: ['FIELD'], + })).toThrow(); + }); + + it('should accept directive with implementation', () => { + const config = GraphQLDirectiveConfigSchema.parse({ + name: 'cache', + locations: ['FIELD_DEFINITION'], + implementation: { + type: 'cache', + handler: 'cacheHandler', + }, + }); + + expect(config.implementation?.type).toBe('cache'); + }); +}); + +describe('GraphQLQueryDepthLimitSchema', () => { + it('should apply default values', () => { + const config = GraphQLQueryDepthLimitSchema.parse({}); + + expect(config.enabled).toBe(true); + expect(config.maxDepth).toBe(10); + expect(config.onDepthExceeded).toBe('reject'); + }); + + it('should accept custom depth limit', () => { + const config = GraphQLQueryDepthLimitSchema.parse({ + enabled: true, + maxDepth: 5, + ignoreFields: ['__typename', 'id'], + onDepthExceeded: 'log', + errorMessage: 'Query too deep', + }); + + expect(config.maxDepth).toBe(5); + expect(config.ignoreFields).toContain('__typename'); + expect(config.onDepthExceeded).toBe('log'); + }); +}); + +describe('GraphQLQueryComplexitySchema', () => { + it('should apply default values', () => { + const config = GraphQLQueryComplexitySchema.parse({}); + + expect(config.enabled).toBe(true); + expect(config.maxComplexity).toBe(1000); + expect(config.defaultFieldComplexity).toBe(1); + expect(config.listMultiplier).toBe(10); + }); + + it('should accept custom complexity configuration', () => { + const config = GraphQLQueryComplexitySchema.parse({ + enabled: true, + maxComplexity: 5000, + defaultFieldComplexity: 2, + listMultiplier: 20, + }); + + expect(config.maxComplexity).toBe(5000); + expect(config.listMultiplier).toBe(20); + }); + + it('should accept field-specific complexity as number', () => { + const config = GraphQLQueryComplexitySchema.parse({ + fieldComplexity: { + 'Query.customers': 10, + 'Customer.orders': 5, + }, + }); + + expect(config.fieldComplexity?.['Query.customers']).toBe(10); + }); + + it('should accept field-specific complexity as object', () => { + const config = GraphQLQueryComplexitySchema.parse({ + fieldComplexity: { + 'Query.customers': { + base: 10, + multiplier: 'limit', + calculator: 'customCalculator', + }, + }, + }); + + const complexityConfig = config.fieldComplexity?.['Query.customers']; + if (typeof complexityConfig === 'object' && 'base' in complexityConfig) { + expect(complexityConfig.base).toBe(10); + expect(complexityConfig.multiplier).toBe('limit'); + } + }); +}); + +describe('GraphQLRateLimitSchema', () => { + it('should apply default values', () => { + const config = GraphQLRateLimitSchema.parse({}); + + expect(config.enabled).toBe(true); + expect(config.strategy).toBe('token_bucket'); + expect(config.onLimitExceeded).toBe('reject'); + expect(config.includeHeaders).toBe(true); + }); + + it('should accept different strategies', () => { + const strategies: Array<'token_bucket' | 'fixed_window' | 'sliding_window' | 'cost_based'> = + ['token_bucket', 'fixed_window', 'sliding_window', 'cost_based']; + + strategies.forEach(strategy => { + const config = GraphQLRateLimitSchema.parse({ strategy }); + expect(config.strategy).toBe(strategy); + }); + }); + + it('should accept global rate limits', () => { + const config = GraphQLRateLimitSchema.parse({ + global: { + maxRequests: 5000, + windowMs: 3600000, + }, + }); + + expect(config.global?.maxRequests).toBe(5000); + }); + + it('should accept per-user rate limits', () => { + const config = GraphQLRateLimitSchema.parse({ + perUser: { + maxRequests: 100, + windowMs: 60000, + }, + }); + + expect(config.perUser?.maxRequests).toBe(100); + }); + + it('should accept cost-based rate limiting', () => { + const config = GraphQLRateLimitSchema.parse({ + costBased: { + enabled: true, + maxCost: 10000, + windowMs: 60000, + useComplexityAsCost: true, + }, + }); + + expect(config.costBased?.enabled).toBe(true); + expect(config.costBased?.useComplexityAsCost).toBe(true); + }); + + it('should accept operation-specific limits', () => { + const config = GraphQLRateLimitSchema.parse({ + operations: { + 'createOrder': { + maxRequests: 10, + windowMs: 60000, + }, + 'deleteCustomer': { + maxRequests: 5, + windowMs: 60000, + }, + }, + }); + + expect(config.operations?.createOrder?.maxRequests).toBe(10); + }); +}); + +describe('GraphQLPersistedQuerySchema', () => { + it('should apply default values', () => { + const config = GraphQLPersistedQuerySchema.parse({}); + + expect(config.enabled).toBe(false); + expect(config.mode).toBe('optional'); + }); + + it('should accept different modes', () => { + const modes: Array<'optional' | 'required'> = ['optional', 'required']; + + modes.forEach(mode => { + const config = GraphQLPersistedQuerySchema.parse({ mode }); + expect(config.mode).toBe(mode); + }); + }); + + it('should accept store configuration', () => { + const config = GraphQLPersistedQuerySchema.parse({ + store: { + type: 'redis', + connection: 'redis://localhost:6379', + ttl: 86400, + }, + }); + + expect(config.store.type).toBe('redis'); + expect(config.store.ttl).toBe(86400); + }); + + it('should accept APQ configuration', () => { + const config = GraphQLPersistedQuerySchema.parse({ + apq: { + enabled: true, + hashAlgorithm: 'sha256', + cache: { + ttl: 3600, + maxSize: 1000, + }, + }, + }); + + expect(config.apq?.enabled).toBe(true); + expect(config.apq?.hashAlgorithm).toBe('sha256'); + }); + + it('should accept allowlist configuration', () => { + const config = GraphQLPersistedQuerySchema.parse({ + allowlist: { + enabled: true, + queries: [ + { id: 'query1', operation: 'GetCustomer' }, + { id: 'query2', operation: 'ListOrders' }, + ], + source: '/path/to/allowlist.json', + }, + }); + + expect(config.allowlist?.enabled).toBe(true); + expect(config.allowlist?.queries).toHaveLength(2); + }); + + it('should accept security configuration', () => { + const config = GraphQLPersistedQuerySchema.parse({ + security: { + maxQuerySize: 10000, + rejectIntrospection: true, + }, + }); + + expect(config.security?.rejectIntrospection).toBe(true); + }); +}); + +describe('GraphQLConfigSchema', () => { + it('should apply default values', () => { + const config = GraphQLConfigSchema.parse({}); + + expect(config.enabled).toBe(true); + expect(config.path).toBe('/graphql'); + }); + + it('should accept complete GraphQL configuration', () => { + const config = GraphQLConfigSchema.parse({ + enabled: true, + path: '/api/graphql', + playground: { + enabled: true, + path: '/graphql-playground', + }, + schema: { + autoGenerateTypes: true, + types: [ + { + name: 'Customer', + object: 'customer', + }, + ], + queries: [ + { + name: 'customers', + object: 'customer', + type: 'list', + }, + ], + mutations: [ + { + name: 'createCustomer', + object: 'customer', + type: 'create', + }, + ], + }, + security: { + depthLimit: { + enabled: true, + maxDepth: 10, + }, + complexity: { + enabled: true, + maxComplexity: 1000, + }, + rateLimit: { + enabled: true, + strategy: 'token_bucket', + }, + persistedQueries: { + enabled: true, + mode: 'optional', + store: { + type: 'memory', + }, + }, + }, + }); + + expect(config.path).toBe('/api/graphql'); + expect(config.schema?.types).toHaveLength(1); + expect(config.security?.depthLimit?.enabled).toBe(true); + }); + + it('should use GraphQLConfig factory', () => { + const config = GraphQLConfig.create({ + enabled: true, + path: '/graphql', + }); + + expect(config.enabled).toBe(true); + }); +}); + +describe('mapFieldTypeToGraphQL', () => { + it('should map text types to String', () => { + expect(mapFieldTypeToGraphQL('text')).toBe('String'); + expect(mapFieldTypeToGraphQL('textarea')).toBe('String'); + expect(mapFieldTypeToGraphQL('markdown')).toBe('String'); + }); + + it('should map email to Email scalar', () => { + expect(mapFieldTypeToGraphQL('email')).toBe('Email'); + }); + + it('should map url to URL scalar', () => { + expect(mapFieldTypeToGraphQL('url')).toBe('URL'); + }); + + it('should map number types to Float', () => { + expect(mapFieldTypeToGraphQL('number')).toBe('Float'); + expect(mapFieldTypeToGraphQL('percent')).toBe('Float'); + }); + + it('should map currency to Currency scalar', () => { + expect(mapFieldTypeToGraphQL('currency')).toBe('Currency'); + }); + + it('should map date types correctly', () => { + expect(mapFieldTypeToGraphQL('date')).toBe('Date'); + expect(mapFieldTypeToGraphQL('datetime')).toBe('DateTime'); + expect(mapFieldTypeToGraphQL('time')).toBe('Time'); + }); + + it('should map boolean types to Boolean', () => { + expect(mapFieldTypeToGraphQL('boolean')).toBe('Boolean'); + expect(mapFieldTypeToGraphQL('toggle')).toBe('Boolean'); + }); + + it('should map select to String', () => { + expect(mapFieldTypeToGraphQL('select')).toBe('String'); + }); + + it('should map multiselect to [String]', () => { + expect(mapFieldTypeToGraphQL('multiselect')).toBe('[String]'); + }); + + it('should map lookup types to ID', () => { + expect(mapFieldTypeToGraphQL('lookup')).toBe('ID'); + expect(mapFieldTypeToGraphQL('master_detail')).toBe('ID'); + }); + + it('should map media types to URL', () => { + expect(mapFieldTypeToGraphQL('image')).toBe('URL'); + expect(mapFieldTypeToGraphQL('file')).toBe('URL'); + expect(mapFieldTypeToGraphQL('avatar')).toBe('URL'); + }); + + it('should map location and address to JSONObject', () => { + expect(mapFieldTypeToGraphQL('location')).toBe('JSONObject'); + expect(mapFieldTypeToGraphQL('address')).toBe('JSONObject'); + }); + + it('should map json to JSON scalar', () => { + expect(mapFieldTypeToGraphQL('json')).toBe('JSON'); + }); + + it('should map vector to [Float]', () => { + expect(mapFieldTypeToGraphQL('vector')).toBe('[Float]'); + }); + + it('should default to String for unknown types', () => { + // @ts-expect-error Testing unknown type + expect(mapFieldTypeToGraphQL('unknown_type')).toBe('String'); + }); +}); diff --git a/packages/spec/src/api/graphql.zod.ts b/packages/spec/src/api/graphql.zod.ts new file mode 100644 index 00000000000..158c8ea65c9 --- /dev/null +++ b/packages/spec/src/api/graphql.zod.ts @@ -0,0 +1,907 @@ +import { z } from 'zod'; +import { FieldType } from '../data/field.zod'; + +/** + * GraphQL Protocol Support + * + * GraphQL is a query language for APIs and a runtime for executing those queries. + * It provides a complete and understandable description of the data in your API, + * gives clients the power to ask for exactly what they need, and enables powerful + * developer tools. + * + * ## Overview + * + * GraphQL provides: + * - Type-safe schema definition + * - Precise data fetching (no over/under-fetching) + * - Introspection and documentation + * - Real-time subscriptions + * - Batched queries with DataLoader + * + * ## Use Cases + * + * 1. **Modern API Development** + * - Mobile and web applications + * - Microservices federation + * - Real-time dashboards + * + * 2. **Data Aggregation** + * - Multi-source data integration + * - Complex nested queries + * - Efficient data loading + * + * 3. **Developer Experience** + * - Self-documenting API + * - Type safety and validation + * - GraphQL playground + * + * @see https://graphql.org/ + * @see https://spec.graphql.org/ + * + * @example GraphQL Query + * ```graphql + * query GetCustomer($id: ID!) { + * customer(id: $id) { + * id + * name + * email + * orders(limit: 10, status: "active") { + * id + * total + * items { + * product { + * name + * price + * } + * } + * } + * } + * } + * ``` + * + * @example GraphQL Mutation + * ```graphql + * mutation CreateOrder($input: CreateOrderInput!) { + * createOrder(input: $input) { + * id + * orderNumber + * status + * } + * } + * ``` + */ + +// ========================================== +// 1. GraphQL Type System +// ========================================== + +/** + * GraphQL Scalar Types + * + * Built-in scalar types in GraphQL plus custom scalars. + */ +export const GraphQLScalarType = z.enum([ + // Built-in GraphQL Scalars + 'ID', + 'String', + 'Int', + 'Float', + 'Boolean', + + // Extended Scalars (common custom types) + 'DateTime', + 'Date', + 'Time', + 'JSON', + 'JSONObject', + 'Upload', + 'URL', + 'Email', + 'PhoneNumber', + 'Currency', + 'Decimal', + 'BigInt', + 'Long', + 'UUID', + 'Base64', + 'Void', +]); + +export type GraphQLScalarType = z.infer; + +/** + * GraphQL Type Configuration + * + * Configuration for generating GraphQL types from Object definitions. + */ +export const GraphQLTypeConfigSchema = z.object({ + /** Type name in GraphQL schema */ + name: z.string().describe('GraphQL type name (PascalCase recommended)'), + + /** Source Object name */ + object: z.string().describe('Source ObjectQL object name'), + + /** Description for GraphQL schema documentation */ + description: z.string().optional().describe('Type description'), + + /** Fields to include/exclude */ + fields: z.object({ + /** Include only these fields (allow list) */ + include: z.array(z.string()).optional().describe('Fields to include'), + + /** Exclude these fields (deny list) */ + exclude: z.array(z.string()).optional().describe('Fields to exclude (e.g., sensitive fields)'), + + /** Custom field mappings */ + mappings: z.record(z.object({ + graphqlName: z.string().optional().describe('Custom GraphQL field name'), + graphqlType: z.string().optional().describe('Override GraphQL type'), + description: z.string().optional().describe('Field description'), + deprecationReason: z.string().optional().describe('Why field is deprecated'), + nullable: z.boolean().optional().describe('Override nullable'), + })).optional().describe('Field-level customizations'), + }).optional().describe('Field configuration'), + + /** Interfaces this type implements */ + interfaces: z.array(z.string()).optional().describe('GraphQL interface names'), + + /** Whether this is an interface definition */ + isInterface: z.boolean().optional().default(false).describe('Define as GraphQL interface'), + + /** Custom directives */ + directives: z.array(z.object({ + name: z.string().describe('Directive name'), + args: z.record(z.any()).optional().describe('Directive arguments'), + })).optional().describe('GraphQL directives'), +}); + +export type GraphQLTypeConfig = z.infer; + +// ========================================== +// 2. Query Generation Configuration +// ========================================== + +/** + * GraphQL Query Configuration + * + * Configuration for auto-generating query fields from Objects. + */ +export const GraphQLQueryConfigSchema = z.object({ + /** Query name */ + name: z.string().describe('Query field name (camelCase recommended)'), + + /** Source Object */ + object: z.string().describe('Source ObjectQL object name'), + + /** Query type: single record or list */ + type: z.enum(['get', 'list', 'search']).describe('Query type'), + + /** Description */ + description: z.string().optional().describe('Query description'), + + /** Input arguments */ + args: z.record(z.object({ + type: z.string().describe('GraphQL type (e.g., "ID!", "String", "Int")'), + description: z.string().optional().describe('Argument description'), + defaultValue: z.any().optional().describe('Default value'), + })).optional().describe('Query arguments'), + + /** Filtering configuration */ + filtering: z.object({ + enabled: z.boolean().default(true).describe('Allow filtering'), + fields: z.array(z.string()).optional().describe('Filterable fields'), + operators: z.array(z.enum([ + 'eq', 'ne', 'gt', 'gte', 'lt', 'lte', + 'in', 'notIn', 'contains', 'startsWith', 'endsWith', + 'isNull', 'isNotNull', + ])).optional().describe('Allowed filter operators'), + }).optional().describe('Filtering capabilities'), + + /** Sorting configuration */ + sorting: z.object({ + enabled: z.boolean().default(true).describe('Allow sorting'), + fields: z.array(z.string()).optional().describe('Sortable fields'), + defaultSort: z.object({ + field: z.string(), + direction: z.enum(['ASC', 'DESC']), + }).optional().describe('Default sort order'), + }).optional().describe('Sorting capabilities'), + + /** Pagination configuration */ + pagination: z.object({ + enabled: z.boolean().default(true).describe('Enable pagination'), + type: z.enum(['offset', 'cursor', 'relay']).default('offset').describe('Pagination style'), + defaultLimit: z.number().int().min(1).default(20).describe('Default page size'), + maxLimit: z.number().int().min(1).default(100).describe('Maximum page size'), + }).optional().describe('Pagination configuration'), + + /** Field selection */ + fields: z.object({ + /** Always include these fields */ + required: z.array(z.string()).optional().describe('Required fields (always returned)'), + + /** Allow selecting these fields */ + selectable: z.array(z.string()).optional().describe('Selectable fields'), + }).optional().describe('Field selection configuration'), + + /** Authorization */ + authRequired: z.boolean().default(true).describe('Require authentication'), + permissions: z.array(z.string()).optional().describe('Required permissions'), + + /** Caching */ + cache: z.object({ + enabled: z.boolean().default(false).describe('Enable caching'), + ttl: z.number().int().min(0).optional().describe('Cache TTL in seconds'), + key: z.string().optional().describe('Cache key template'), + }).optional().describe('Query caching'), +}); + +export type GraphQLQueryConfig = z.infer; + +// ========================================== +// 3. Mutation Generation Configuration +// ========================================== + +/** + * GraphQL Mutation Configuration + * + * Configuration for auto-generating mutation fields from Objects. + */ +export const GraphQLMutationConfigSchema = z.object({ + /** Mutation name */ + name: z.string().describe('Mutation field name (camelCase recommended)'), + + /** Source Object */ + object: z.string().describe('Source ObjectQL object name'), + + /** Mutation type */ + type: z.enum(['create', 'update', 'delete', 'upsert', 'custom']).describe('Mutation type'), + + /** Description */ + description: z.string().optional().describe('Mutation description'), + + /** Input type configuration */ + input: z.object({ + /** Input type name */ + typeName: z.string().optional().describe('Custom input type name'), + + /** Fields to include in input */ + fields: z.object({ + include: z.array(z.string()).optional().describe('Fields to include'), + exclude: z.array(z.string()).optional().describe('Fields to exclude'), + required: z.array(z.string()).optional().describe('Required input fields'), + }).optional().describe('Input field configuration'), + + /** Validation */ + validation: z.object({ + enabled: z.boolean().default(true).describe('Enable input validation'), + rules: z.array(z.string()).optional().describe('Custom validation rules'), + }).optional().describe('Input validation'), + }).optional().describe('Input configuration'), + + /** Return type configuration */ + output: z.object({ + /** Type of output */ + type: z.enum(['object', 'payload', 'boolean', 'custom']).default('object').describe('Output type'), + + /** Include success/error envelope */ + includeEnvelope: z.boolean().optional().default(false).describe('Wrap in success/error payload'), + + /** Custom output type */ + customType: z.string().optional().describe('Custom output type name'), + }).optional().describe('Output configuration'), + + /** Transaction handling */ + transaction: z.object({ + enabled: z.boolean().default(true).describe('Use database transaction'), + isolationLevel: z.enum(['read_uncommitted', 'read_committed', 'repeatable_read', 'serializable']).optional().describe('Transaction isolation level'), + }).optional().describe('Transaction configuration'), + + /** Authorization */ + authRequired: z.boolean().default(true).describe('Require authentication'), + permissions: z.array(z.string()).optional().describe('Required permissions'), + + /** Hooks */ + hooks: z.object({ + before: z.array(z.string()).optional().describe('Pre-mutation hooks'), + after: z.array(z.string()).optional().describe('Post-mutation hooks'), + }).optional().describe('Lifecycle hooks'), +}); + +export type GraphQLMutationConfig = z.infer; + +// ========================================== +// 4. Subscription Configuration +// ========================================== + +/** + * GraphQL Subscription Configuration + * + * Configuration for real-time GraphQL subscriptions. + */ +export const GraphQLSubscriptionConfigSchema = z.object({ + /** Subscription name */ + name: z.string().describe('Subscription field name (camelCase recommended)'), + + /** Source Object */ + object: z.string().describe('Source ObjectQL object name'), + + /** Subscription trigger events */ + events: z.array(z.enum(['created', 'updated', 'deleted', 'custom'])).describe('Events to subscribe to'), + + /** Description */ + description: z.string().optional().describe('Subscription description'), + + /** Filtering */ + filter: z.object({ + enabled: z.boolean().default(true).describe('Allow filtering subscriptions'), + fields: z.array(z.string()).optional().describe('Filterable fields'), + }).optional().describe('Subscription filtering'), + + /** Payload configuration */ + payload: z.object({ + /** Include the modified entity */ + includeEntity: z.boolean().default(true).describe('Include entity in payload'), + + /** Include previous values (for updates) */ + includePreviousValues: z.boolean().optional().default(false).describe('Include previous field values'), + + /** Include mutation metadata */ + includeMeta: z.boolean().optional().default(true).describe('Include metadata (timestamp, user, etc.)'), + }).optional().describe('Payload configuration'), + + /** Authorization */ + authRequired: z.boolean().default(true).describe('Require authentication'), + permissions: z.array(z.string()).optional().describe('Required permissions'), + + /** Rate limiting for subscriptions */ + rateLimit: z.object({ + enabled: z.boolean().default(true).describe('Enable rate limiting'), + maxSubscriptionsPerUser: z.number().int().min(1).default(10).describe('Max concurrent subscriptions per user'), + throttleMs: z.number().int().min(0).optional().describe('Throttle interval in milliseconds'), + }).optional().describe('Subscription rate limiting'), +}); + +export type GraphQLSubscriptionConfig = z.infer; + +// ========================================== +// 5. Resolver Configuration +// ========================================== + +/** + * GraphQL Resolver Configuration + * + * Configuration for custom resolver logic. + */ +export const GraphQLResolverConfigSchema = z.object({ + /** Field path (e.g., "Query.users", "Mutation.createUser") */ + path: z.string().describe('Resolver path (Type.field)'), + + /** Resolver implementation type */ + type: z.enum(['datasource', 'computed', 'script', 'proxy']).describe('Resolver implementation type'), + + /** Implementation details */ + implementation: z.object({ + /** For datasource type */ + datasource: z.string().optional().describe('Datasource ID'), + query: z.string().optional().describe('Query/SQL to execute'), + + /** For computed type */ + expression: z.string().optional().describe('Computation expression'), + dependencies: z.array(z.string()).optional().describe('Dependent fields'), + + /** For script type */ + script: z.string().optional().describe('Script ID or inline code'), + + /** For proxy type */ + url: z.string().optional().describe('Proxy URL'), + method: z.enum(['GET', 'POST', 'PUT', 'DELETE']).optional().describe('HTTP method'), + }).optional().describe('Implementation configuration'), + + /** Caching */ + cache: z.object({ + enabled: z.boolean().default(false).describe('Enable resolver caching'), + ttl: z.number().int().min(0).optional().describe('Cache TTL in seconds'), + keyArgs: z.array(z.string()).optional().describe('Arguments to include in cache key'), + }).optional().describe('Resolver caching'), +}); + +export type GraphQLResolverConfig = z.infer; + +// ========================================== +// 6. DataLoader Configuration +// ========================================== + +/** + * GraphQL DataLoader Configuration + * + * Configuration for batching and caching with DataLoader pattern. + * Prevents N+1 query problems in GraphQL. + */ +export const GraphQLDataLoaderConfigSchema = z.object({ + /** Loader name */ + name: z.string().describe('DataLoader name'), + + /** Source Object or datasource */ + source: z.string().describe('Source object or datasource'), + + /** Batch function configuration */ + batchFunction: z.object({ + /** Type of batch operation */ + type: z.enum(['findByIds', 'query', 'script', 'custom']).describe('Batch function type'), + + /** For findByIds */ + keyField: z.string().optional().describe('Field to batch on (e.g., "id")'), + + /** For query */ + query: z.string().optional().describe('Query template'), + + /** For script */ + script: z.string().optional().describe('Script ID'), + + /** Maximum batch size */ + maxBatchSize: z.number().int().min(1).optional().default(100).describe('Maximum batch size'), + }).describe('Batch function configuration'), + + /** Caching */ + cache: z.object({ + enabled: z.boolean().default(true).describe('Enable per-request caching'), + + /** Cache key function */ + keyFn: z.string().optional().describe('Custom cache key function'), + }).optional().describe('DataLoader caching'), + + /** Options */ + options: z.object({ + /** Batch multiple requests in single tick */ + batch: z.boolean().default(true).describe('Enable batching'), + + /** Cache loaded values */ + cache: z.boolean().default(true).describe('Enable caching'), + + /** Maximum cache size */ + maxCacheSize: z.number().int().min(0).optional().describe('Max cache entries'), + }).optional().describe('DataLoader options'), +}); + +export type GraphQLDataLoaderConfig = z.infer; + +// ========================================== +// 7. GraphQL Directive Schema +// ========================================== + +/** + * GraphQL Directive Location + * + * Where a directive can be used in the schema. + */ +export const GraphQLDirectiveLocation = z.enum([ + // Executable Directive Locations + 'QUERY', + 'MUTATION', + 'SUBSCRIPTION', + 'FIELD', + 'FRAGMENT_DEFINITION', + 'FRAGMENT_SPREAD', + 'INLINE_FRAGMENT', + 'VARIABLE_DEFINITION', + + // Type System Directive Locations + 'SCHEMA', + 'SCALAR', + 'OBJECT', + 'FIELD_DEFINITION', + 'ARGUMENT_DEFINITION', + 'INTERFACE', + 'UNION', + 'ENUM', + 'ENUM_VALUE', + 'INPUT_OBJECT', + 'INPUT_FIELD_DEFINITION', +]); + +export type GraphQLDirectiveLocation = z.infer; + +/** + * GraphQL Directive Configuration + * + * Custom directives for schema metadata and behavior. + */ +export const GraphQLDirectiveConfigSchema = z.object({ + /** Directive name */ + name: z.string().regex(/^[a-z][a-zA-Z0-9]*$/).describe('Directive name (camelCase)'), + + /** Description */ + description: z.string().optional().describe('Directive description'), + + /** Where directive can be used */ + locations: z.array(GraphQLDirectiveLocation).describe('Directive locations'), + + /** Arguments */ + args: z.record(z.object({ + type: z.string().describe('Argument type'), + description: z.string().optional().describe('Argument description'), + defaultValue: z.any().optional().describe('Default value'), + })).optional().describe('Directive arguments'), + + /** Is repeatable */ + repeatable: z.boolean().optional().default(false).describe('Can be applied multiple times'), + + /** Implementation */ + implementation: z.object({ + /** Directive behavior type */ + type: z.enum(['auth', 'validation', 'transform', 'cache', 'deprecation', 'custom']).describe('Directive type'), + + /** Handler function */ + handler: z.string().optional().describe('Handler function name or script'), + }).optional().describe('Directive implementation'), +}); + +export type GraphQLDirectiveConfig = z.infer; + +// ========================================== +// 8. GraphQL Security - Query Depth Limiting +// ========================================== + +/** + * Query Depth Limiting Configuration + * + * Prevents deeply nested queries that could cause performance issues. + */ +export const GraphQLQueryDepthLimitSchema = z.object({ + /** Enable depth limiting */ + enabled: z.boolean().default(true).describe('Enable query depth limiting'), + + /** Maximum allowed depth */ + maxDepth: z.number().int().min(1).default(10).describe('Maximum query depth'), + + /** Fields to ignore in depth calculation */ + ignoreFields: z.array(z.string()).optional().describe('Fields excluded from depth calculation'), + + /** Callback on depth exceeded */ + onDepthExceeded: z.enum(['reject', 'log', 'warn']).default('reject').describe('Action when depth exceeded'), + + /** Custom error message */ + errorMessage: z.string().optional().describe('Custom error message for depth violations'), +}); + +export type GraphQLQueryDepthLimit = z.infer; + +// ========================================== +// 9. GraphQL Security - Query Complexity +// ========================================== + +/** + * Query Complexity Calculation Configuration + * + * Assigns complexity scores to fields and limits total query complexity. + * Prevents expensive queries from overloading the server. + */ +export const GraphQLQueryComplexitySchema = z.object({ + /** Enable complexity limiting */ + enabled: z.boolean().default(true).describe('Enable query complexity limiting'), + + /** Maximum allowed complexity score */ + maxComplexity: z.number().int().min(1).default(1000).describe('Maximum query complexity'), + + /** Default field complexity */ + defaultFieldComplexity: z.number().int().min(0).default(1).describe('Default complexity per field'), + + /** Field-specific complexity scores */ + fieldComplexity: z.record(z.union([ + z.number().int().min(0), + z.object({ + /** Base complexity */ + base: z.number().int().min(0).describe('Base complexity'), + + /** Multiplier based on arguments */ + multiplier: z.string().optional().describe('Argument multiplier (e.g., "limit")'), + + /** Custom complexity calculation */ + calculator: z.string().optional().describe('Custom calculator function'), + }), + ])).optional().describe('Per-field complexity configuration'), + + /** List multiplier */ + listMultiplier: z.number().min(0).default(10).describe('Multiplier for list fields'), + + /** Callback on complexity exceeded */ + onComplexityExceeded: z.enum(['reject', 'log', 'warn']).default('reject').describe('Action when complexity exceeded'), + + /** Custom error message */ + errorMessage: z.string().optional().describe('Custom error message for complexity violations'), +}); + +export type GraphQLQueryComplexity = z.infer; + +// ========================================== +// 10. GraphQL Security - Rate Limiting +// ========================================== + +/** + * GraphQL Rate Limiting Configuration + * + * Rate limiting for GraphQL operations. + */ +export const GraphQLRateLimitSchema = z.object({ + /** Enable rate limiting */ + enabled: z.boolean().default(true).describe('Enable rate limiting'), + + /** Rate limit strategy */ + strategy: z.enum(['token_bucket', 'fixed_window', 'sliding_window', 'cost_based']).default('token_bucket').describe('Rate limiting strategy'), + + /** Global rate limits */ + global: z.object({ + /** Requests per time window */ + maxRequests: z.number().int().min(1).default(1000).describe('Maximum requests per window'), + + /** Time window in milliseconds */ + windowMs: z.number().int().min(1000).default(60000).describe('Time window in milliseconds'), + }).optional().describe('Global rate limits'), + + /** Per-user rate limits */ + perUser: z.object({ + /** Requests per time window */ + maxRequests: z.number().int().min(1).default(100).describe('Maximum requests per user per window'), + + /** Time window in milliseconds */ + windowMs: z.number().int().min(1000).default(60000).describe('Time window in milliseconds'), + }).optional().describe('Per-user rate limits'), + + /** Cost-based rate limiting */ + costBased: z.object({ + /** Enable cost-based limiting */ + enabled: z.boolean().default(false).describe('Enable cost-based rate limiting'), + + /** Maximum cost per time window */ + maxCost: z.number().int().min(1).default(10000).describe('Maximum cost per window'), + + /** Time window in milliseconds */ + windowMs: z.number().int().min(1000).default(60000).describe('Time window in milliseconds'), + + /** Use complexity as cost */ + useComplexityAsCost: z.boolean().default(true).describe('Use query complexity as cost'), + }).optional().describe('Cost-based rate limiting'), + + /** Operation-specific limits */ + operations: z.record(z.object({ + maxRequests: z.number().int().min(1).describe('Max requests for this operation'), + windowMs: z.number().int().min(1000).describe('Time window'), + })).optional().describe('Per-operation rate limits'), + + /** Callback on limit exceeded */ + onLimitExceeded: z.enum(['reject', 'queue', 'log']).default('reject').describe('Action when rate limit exceeded'), + + /** Custom error message */ + errorMessage: z.string().optional().describe('Custom error message for rate limit violations'), + + /** Headers to include in response */ + includeHeaders: z.boolean().default(true).describe('Include rate limit headers in response'), +}); + +export type GraphQLRateLimit = z.infer; + +// ========================================== +// 11. GraphQL Security - Persisted Queries +// ========================================== + +/** + * Persisted Queries Configuration + * + * Only allow pre-registered queries to execute (allow list approach). + * Improves security and performance. + */ +export const GraphQLPersistedQuerySchema = z.object({ + /** Enable persisted queries */ + enabled: z.boolean().default(false).describe('Enable persisted queries'), + + /** Enforcement mode */ + mode: z.enum(['optional', 'required']).default('optional').describe('Persisted query mode (optional: allow both, required: only persisted)'), + + /** Query store configuration */ + store: z.object({ + /** Store type */ + type: z.enum(['memory', 'redis', 'database', 'file']).default('memory').describe('Query store type'), + + /** Store connection string */ + connection: z.string().optional().describe('Store connection string or path'), + + /** TTL for cached queries */ + ttl: z.number().int().min(0).optional().describe('TTL in seconds for stored queries'), + }).optional().describe('Query store configuration'), + + /** Automatic Persisted Queries (APQ) */ + apq: z.object({ + /** Enable APQ */ + enabled: z.boolean().default(true).describe('Enable Automatic Persisted Queries'), + + /** Hash algorithm */ + hashAlgorithm: z.enum(['sha256', 'sha1', 'md5']).default('sha256').describe('Hash algorithm for query IDs'), + + /** Cache control */ + cache: z.object({ + /** Cache TTL */ + ttl: z.number().int().min(0).default(3600).describe('Cache TTL in seconds'), + + /** Max cache size */ + maxSize: z.number().int().min(1).optional().describe('Maximum number of cached queries'), + }).optional().describe('APQ cache configuration'), + }).optional().describe('Automatic Persisted Queries configuration'), + + /** Query allow list */ + allowlist: z.object({ + /** Enable allow list mode */ + enabled: z.boolean().default(false).describe('Enable query allow list (reject queries not in list)'), + + /** Allowed query IDs */ + queries: z.array(z.object({ + id: z.string().describe('Query ID or hash'), + operation: z.string().optional().describe('Operation name'), + query: z.string().optional().describe('Query string'), + })).optional().describe('Allowed queries'), + + /** External allow list source */ + source: z.string().optional().describe('External allow list source (file path or URL)'), + }).optional().describe('Query allow list configuration'), + + /** Security */ + security: z.object({ + /** Maximum query size */ + maxQuerySize: z.number().int().min(1).optional().describe('Maximum query string size in bytes'), + + /** Reject introspection in production */ + rejectIntrospection: z.boolean().default(false).describe('Reject introspection queries'), + }).optional().describe('Security configuration'), +}); + +export type GraphQLPersistedQuery = z.infer; + +// ========================================== +// 12. Complete GraphQL Configuration +// ========================================== + +/** + * Complete GraphQL Configuration + * + * Root configuration for GraphQL API generation and security. + */ +export const GraphQLConfigSchema = z.object({ + /** Enable GraphQL API */ + enabled: z.boolean().default(true).describe('Enable GraphQL API'), + + /** GraphQL endpoint path */ + path: z.string().default('/graphql').describe('GraphQL endpoint path'), + + /** GraphQL Playground */ + playground: z.object({ + enabled: z.boolean().default(true).describe('Enable GraphQL Playground'), + path: z.string().default('/playground').describe('Playground path'), + }).optional().describe('GraphQL Playground configuration'), + + /** Schema generation */ + schema: z.object({ + /** Auto-generate types from Objects */ + autoGenerateTypes: z.boolean().default(true).describe('Auto-generate types from Objects'), + + /** Type configurations */ + types: z.array(GraphQLTypeConfigSchema).optional().describe('Type configurations'), + + /** Query configurations */ + queries: z.array(GraphQLQueryConfigSchema).optional().describe('Query configurations'), + + /** Mutation configurations */ + mutations: z.array(GraphQLMutationConfigSchema).optional().describe('Mutation configurations'), + + /** Subscription configurations */ + subscriptions: z.array(GraphQLSubscriptionConfigSchema).optional().describe('Subscription configurations'), + + /** Custom resolvers */ + resolvers: z.array(GraphQLResolverConfigSchema).optional().describe('Custom resolver configurations'), + + /** Custom directives */ + directives: z.array(GraphQLDirectiveConfigSchema).optional().describe('Custom directive configurations'), + }).optional().describe('Schema generation configuration'), + + /** DataLoader configurations */ + dataLoaders: z.array(GraphQLDataLoaderConfigSchema).optional().describe('DataLoader configurations'), + + /** Security configuration */ + security: z.object({ + /** Query depth limiting */ + depthLimit: GraphQLQueryDepthLimitSchema.optional().describe('Query depth limiting'), + + /** Query complexity */ + complexity: GraphQLQueryComplexitySchema.optional().describe('Query complexity calculation'), + + /** Rate limiting */ + rateLimit: GraphQLRateLimitSchema.optional().describe('Rate limiting'), + + /** Persisted queries */ + persistedQueries: GraphQLPersistedQuerySchema.optional().describe('Persisted queries'), + }).optional().describe('Security configuration'), +}); + +export const GraphQLConfig = Object.assign(GraphQLConfigSchema, { + create: >(config: T) => config, +}); + +export type GraphQLConfig = z.infer; + +// ========================================== +// Helper Functions +// ========================================== + +/** + * Helper to map ObjectQL field type to GraphQL scalar type + */ +export const mapFieldTypeToGraphQL = (fieldType: z.infer): string => { + const mapping: Record = { + // Core Text + 'text': 'String', + 'textarea': 'String', + 'email': 'Email', + 'url': 'URL', + 'phone': 'PhoneNumber', + 'password': 'String', + + // Rich Content + 'markdown': 'String', + 'html': 'String', + 'richtext': 'String', + + // Numbers + 'number': 'Float', + 'currency': 'Currency', + 'percent': 'Float', + + // Date & Time + 'date': 'Date', + 'datetime': 'DateTime', + 'time': 'Time', + + // Logic + 'boolean': 'Boolean', + 'toggle': 'Boolean', + + // Selection + 'select': 'String', + 'multiselect': '[String]', + 'radio': 'String', + 'checkboxes': '[String]', + + // Relational + 'lookup': 'ID', + 'master_detail': 'ID', + 'tree': 'ID', + + // Media + 'image': 'URL', + 'file': 'URL', + 'avatar': 'URL', + 'video': 'URL', + 'audio': 'URL', + + // Calculated + 'formula': 'String', + 'summary': 'Float', + 'autonumber': 'String', + + // Enhanced Types + 'location': 'JSONObject', + 'address': 'JSONObject', + 'code': 'String', + 'json': 'JSON', + 'color': 'String', + 'rating': 'Float', + 'slider': 'Float', + 'signature': 'String', + 'qrcode': 'String', + 'progress': 'Float', + 'tags': '[String]', + + // AI/ML + 'vector': '[Float]', + }; + + return mapping[fieldType] || 'String'; +}; diff --git a/packages/spec/src/api/index.ts b/packages/spec/src/api/index.ts index 5ddaad289e8..3b754fe9bd3 100644 --- a/packages/spec/src/api/index.ts +++ b/packages/spec/src/api/index.ts @@ -13,6 +13,7 @@ export * from './discovery.zod'; export * from './realtime.zod'; export * from './router.zod'; export * from './odata.zod'; +export * from './graphql.zod'; export * from './protocol';