Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ jobs:
- 9000:8000

contember-engine:
image: contember/engine:2.1.0-rc.6
image: contember/engine:2.2.0-alpha.17
env:
NODE_ENV: development
CONTEMBER_PORT: "4000"
Expand Down Expand Up @@ -121,7 +121,7 @@ jobs:
-e CONTEMBER_PROJECT_NAME=example \
-v ${{ github.workspace }}:/src \
-w /src/packages/example \
contember/cli:2.1.0-rc.6 \
contember/cli:2.2.0-alpha.17 \
migrations:execute --yes
CONTEMBER_API_URL=http://localhost:1581 CONTEMBER_API_TOKEN=0000000000000000000000000000000000000000 bun run packages/example/seed.ts

Expand Down
36 changes: 18 additions & 18 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
services:
contember-engine:
image: contember/engine:2.1.0-rc.6
image: contember/engine:2.2.0-alpha.17

environment:
NODE_ENV: 'development'
Expand Down Expand Up @@ -43,7 +43,7 @@ services:
condition: service_healthy

contember-cli:
image: contember/cli:2.1.0-rc.6
image: contember/cli:2.2.0-alpha.17
user: '1000:1000'

deploy:
Expand Down
6 changes: 3 additions & 3 deletions packages/bindx-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
],
"license": "MIT",
"dependencies": {
"@contember/graphql-builder": "2.1.0-rc.6",
"@contember/graphql-client": "2.1.0-rc.6",
"@contember/schema": "2.1.0-rc.6"
"@contember/graphql-builder": "2.2.0-alpha.17",
"@contember/graphql-client": "2.2.0-alpha.17",
"@contember/schema": "2.2.0-alpha.17"
},
"repository": {
"type": "git",
Expand Down
1 change: 1 addition & 0 deletions packages/bindx-form/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export type ColumnType =
| 'String'
| 'Integer'
| 'Double'
| 'Numeric'
| 'Date'
| 'DateTime'
| 'Time'
Expand Down
4 changes: 2 additions & 2 deletions packages/bindx-generator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
],
"license": "MIT",
"dependencies": {
"@contember/schema": "2.1.0-rc.6",
"@contember/schema-utils": "2.1.0-rc.6"
"@contember/schema": "2.2.0-alpha.17",
"@contember/schema-utils": "2.2.0-alpha.17"
},
"devDependencies": {
"@types/node": "^25.0.3"
Expand Down
3 changes: 3 additions & 0 deletions packages/bindx-generator/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ export const columnToTsType = (column: Model.AnyColumn): string => {
return 'number'
case Model.ColumnType.Double:
return 'number'
// An exact decimal travels as a string, so it keeps every digit.
case Model.ColumnType.Numeric:
return 'string'
case Model.ColumnType.Bool:
return 'boolean'
case Model.ColumnType.DateTime:
Expand Down
22 changes: 22 additions & 0 deletions packages/bindx-generator/tests/columnToTsType.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { describe, expect, test } from 'bun:test'
import { columnToTsType } from '../src/index'
import { Model } from '@contember/schema'

const column = (type: Model.ColumnType, columnType: string, list?: boolean): Model.AnyColumn => ({
name: 'value',
columnName: 'value',
type,
columnType,
nullable: true,
...(list === undefined ? {} : { list }),
})

describe('columnToTsType', () => {
test('types an exact decimal as a string', () => {
expect(columnToTsType(column(Model.ColumnType.Numeric, 'numeric'))).toBe('string')
})

test('types a list of exact decimals as a readonly string array', () => {
expect(columnToTsType(column(Model.ColumnType.Numeric, 'numeric', true))).toBe('readonly string[]')
})
})
2 changes: 1 addition & 1 deletion packages/bindx-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"dependencies": {
"@contember/bindx": "workspace:*",
"@contember/bindx-client": "workspace:*",
"@contember/graphql-client": "2.1.0-rc.6",
"@contember/graphql-client": "2.2.0-alpha.17",
"@radix-ui/primitive": "^1.1.3",
"@radix-ui/react-slot": "^1.2.4"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/bindx/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"license": "MIT",
"dependencies": {
"@contember/bindx-client": "workspace:*",
"@contember/graphql-client": "2.1.0-rc.6"
"@contember/graphql-client": "2.2.0-alpha.17"
},
"repository": {
"type": "git",
Expand Down
2 changes: 1 addition & 1 deletion packages/bindx/src/adapter/ContemberAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import type { SchemaRegistry } from '../schema/SchemaRegistry.js'
*/
export interface ContemberAdapterOptions {
/** GraphQL client instance */
client: GraphQlClient
client: Pick<GraphQlClient, 'execute'>
/** Schema registry for relation target resolution */
schemaRegistry: SchemaRegistry
}
Expand Down
2 changes: 2 additions & 0 deletions packages/bindx/src/errors/errorPathResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,8 @@ function getErrorCode(errorType?: string): string | undefined {
return 'INVALID_DATA'
case 'SqlError':
return 'SQL_ERROR'
case 'ResourceExhausted':
return 'RESOURCE_EXHAUSTED'
default:
return undefined
}
Expand Down
6 changes: 5 additions & 1 deletion packages/bindx/src/errors/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const ExecutionErrorTypes: Record<ExecutionErrorType, ExecutionErrorType>
NonUniqueWhereInput: 'NonUniqueWhereInput',
InvalidDataInput: 'InvalidDataInput',
SqlError: 'SqlError',
ResourceExhausted: 'ResourceExhausted',
}

/**
Expand All @@ -37,13 +38,14 @@ export function isExecutionErrorType(value: string): value is ExecutionErrorType
/**
* Error classification for retry logic.
*/
export type ErrorCategory = 'validation' | 'constraint' | 'not_found' | 'transient' | 'unknown'
export type ErrorCategory = 'validation' | 'constraint' | 'not_found' | 'resource_limit' | 'transient' | 'unknown'

/**
* Classifies an execution error type into a category.
* - 'validation': Data validation errors (not retryable)
* - 'constraint': Database constraint violations (not retryable without data change)
* - 'not_found': Entity not found or access denied (not retryable)
* - 'resource_limit': The request exceeded a server resource budget; the same request exceeds it again (not retryable)
* - 'transient': Temporary errors that may succeed on retry
* - 'unknown': Unclassified errors
*/
Expand All @@ -60,6 +62,8 @@ export function classifyError(errorType?: string): ErrorCategory {
return 'validation'
case 'NotFoundOrDenied':
return 'not_found'
case 'ResourceExhausted':
return 'resource_limit'
case 'SqlError':
// SQL errors could be transient (deadlock, connection issues) or permanent
return 'transient'
Expand Down
4 changes: 2 additions & 2 deletions packages/example/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"version": "0.0.0",
"type": "module",
"devDependencies": {
"@contember/schema": "2.1.0-rc.6",
"@contember/schema-definition": "2.1.0-rc.6"
"@contember/schema": "2.2.0-alpha.17",
"@contember/schema-definition": "2.2.0-alpha.17"
},
"dependencies": {
"graphql": "^16.8.1"
Expand Down
8 changes: 4 additions & 4 deletions packages/example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@
"@contember/bindx-repeater": "workspace:*",
"@contember/bindx-uploader": "workspace:*",
"@contember/bindx-react": "workspace:*",
"@contember/client-content": "2.1.0-rc.6",
"@contember/graphql-client": "2.1.0-rc.6",
"@contember/schema": "2.1.0-rc.6",
"@contember/schema-definition": "2.1.0-rc.6",
"@contember/client-content": "2.2.0-alpha.17",
"@contember/graphql-client": "2.2.0-alpha.17",
"@contember/schema": "2.2.0-alpha.17",
"@contember/schema-definition": "2.2.0-alpha.17",
"@tailwindcss/vite": "^4.2.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
Expand Down
6 changes: 5 additions & 1 deletion tests/unit/errors/errorClassification.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ describe('error classification', () => {
['InvalidDataInput', 'validation'],
['NonUniqueWhereInput', 'validation'],
['NotFoundOrDenied', 'not_found'],
['ResourceExhausted', 'resource_limit'],
['SqlError', 'transient'],
])('should classify %s as %s', (errorType, expectedCategory) => {
expect(classifyError(errorType)).toBe(expectedCategory)
Expand All @@ -41,6 +42,7 @@ describe('error classification', () => {
['constraint', false],
['validation', false],
['not_found', false],
['resource_limit', false],
['transient', true],
['unknown', true],
])('should return %s for category %s', (category, expectedRetryable) => {
Expand All @@ -57,6 +59,7 @@ describe('error classification', () => {
expect(isExecutionErrorType('NonUniqueWhereInput')).toBe(true)
expect(isExecutionErrorType('InvalidDataInput')).toBe(true)
expect(isExecutionErrorType('SqlError')).toBe(true)
expect(isExecutionErrorType('ResourceExhausted')).toBe(true)
})

test('should return false for invalid error types', () => {
Expand All @@ -68,14 +71,15 @@ describe('error classification', () => {

describe('ExecutionErrorTypes', () => {
test('should contain all error types', () => {
expect(Object.keys(ExecutionErrorTypes)).toHaveLength(7)
expect(Object.keys(ExecutionErrorTypes)).toHaveLength(8)
expect(ExecutionErrorTypes.NotNullConstraintViolation).toBe('NotNullConstraintViolation')
expect(ExecutionErrorTypes.UniqueConstraintViolation).toBe('UniqueConstraintViolation')
expect(ExecutionErrorTypes.ForeignKeyConstraintViolation).toBe('ForeignKeyConstraintViolation')
expect(ExecutionErrorTypes.NotFoundOrDenied).toBe('NotFoundOrDenied')
expect(ExecutionErrorTypes.NonUniqueWhereInput).toBe('NonUniqueWhereInput')
expect(ExecutionErrorTypes.InvalidDataInput).toBe('InvalidDataInput')
expect(ExecutionErrorTypes.SqlError).toBe('SqlError')
expect(ExecutionErrorTypes.ResourceExhausted).toBe('ResourceExhausted')
})
})

Expand Down
23 changes: 23 additions & 0 deletions tests/unit/errors/errorPathResolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,29 @@ describe('errorPathResolver', () => {
})
})

test('should report an exhausted resource budget as a non-retryable entity-level error', () => {
const store = new SnapshotStore()

const mutationResult: ContemberMutationResult = {
ok: false,
errorMessage: null,
errors: [{
paths: [],
message: 'Request memory budget exhausted',
type: 'ResourceExhausted',
}],
validation: { valid: true, errors: [] },
}

const resolved = resolveAllErrors(mutationResult, 'Article', 'a-1', createContext(store))

expect(resolved).toHaveLength(1)
expect(resolved[0]!.target).toEqual({ entityType: 'Article', entityId: 'a-1', type: 'entity' })
expect(resolved[0]!.error.code).toBe('RESOURCE_EXHAUSTED')
expect(resolved[0]!.error.category).toBe('resource_limit')
expect(resolved[0]!.error.retryable).toBe(false)
})

test('should resolve multiple errors to different entities', () => {
const store = new SnapshotStore()
store.getOrCreateRelation('Article', 'a-1', 'author', {
Expand Down
21 changes: 21 additions & 0 deletions tests/unit/form/numericColumn.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { describe, expect, test } from 'bun:test'
import { renderHook } from '@testing-library/react'
import { type FormInputHandlerContext, type TypeHandlerMap, useFormInputHandler } from '@contember/bindx-form'

const ctx: FormInputHandlerContext = { setState: () => {}, currentValue: null, setError: () => {} }

describe('a Numeric column in a form', () => {
test('keeps every digit of an exact decimal', () => {
const { result } = renderHook(() => useFormInputHandler({ columnType: 'Numeric' }))

expect(result.current.parseValue('12345678901234567890.10', ctx)).toBe('12345678901234567890.10')
expect(result.current.formatValue('0.10', ctx)).toBe('0.10')
})

test('takes a handler of its own', () => {
const handler = { parseValue: (value: string) => value.trim(), formatValue: (value: unknown) => String(value) }
const handlers: TypeHandlerMap = { Numeric: () => handler }

expect(handlers.Numeric?.()).toBe(handler)
})
})
Loading