digital-objects 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +4 -0
- package/CHANGELOG.md +25 -0
- package/LICENSE +21 -0
- package/README.md +476 -0
- package/dist/ai-database-adapter.d.ts +49 -0
- package/dist/ai-database-adapter.d.ts.map +1 -0
- package/dist/ai-database-adapter.js +89 -0
- package/dist/ai-database-adapter.js.map +1 -0
- package/dist/errors.d.ts +47 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +72 -0
- package/dist/errors.js.map +1 -0
- package/dist/http-schemas.d.ts +165 -0
- package/dist/http-schemas.d.ts.map +1 -0
- package/dist/http-schemas.js +55 -0
- package/dist/http-schemas.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/linguistic.d.ts +54 -0
- package/dist/linguistic.d.ts.map +1 -0
- package/dist/linguistic.js +226 -0
- package/dist/linguistic.js.map +1 -0
- package/dist/memory-provider.d.ts +46 -0
- package/dist/memory-provider.d.ts.map +1 -0
- package/dist/memory-provider.js +279 -0
- package/dist/memory-provider.js.map +1 -0
- package/dist/ns-client.d.ts +88 -0
- package/dist/ns-client.d.ts.map +1 -0
- package/dist/ns-client.js +253 -0
- package/dist/ns-client.js.map +1 -0
- package/dist/ns-exports.d.ts +23 -0
- package/dist/ns-exports.d.ts.map +1 -0
- package/dist/ns-exports.js +21 -0
- package/dist/ns-exports.js.map +1 -0
- package/dist/ns.d.ts +60 -0
- package/dist/ns.d.ts.map +1 -0
- package/dist/ns.js +818 -0
- package/dist/ns.js.map +1 -0
- package/dist/r2-persistence.d.ts +112 -0
- package/dist/r2-persistence.d.ts.map +1 -0
- package/dist/r2-persistence.js +252 -0
- package/dist/r2-persistence.js.map +1 -0
- package/dist/schema-validation.d.ts +80 -0
- package/dist/schema-validation.d.ts.map +1 -0
- package/dist/schema-validation.js +233 -0
- package/dist/schema-validation.js.map +1 -0
- package/dist/types.d.ts +184 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +26 -0
- package/dist/types.js.map +1 -0
- package/package.json +55 -0
- package/src/ai-database-adapter.test.ts +610 -0
- package/src/ai-database-adapter.ts +189 -0
- package/src/benchmark.test.ts +109 -0
- package/src/errors.ts +91 -0
- package/src/http-schemas.ts +67 -0
- package/src/index.ts +87 -0
- package/src/linguistic.test.ts +1107 -0
- package/src/linguistic.ts +253 -0
- package/src/memory-provider.ts +470 -0
- package/src/ns-client.test.ts +1360 -0
- package/src/ns-client.ts +342 -0
- package/src/ns-exports.ts +23 -0
- package/src/ns.test.ts +1381 -0
- package/src/ns.ts +1215 -0
- package/src/provider.test.ts +675 -0
- package/src/r2-persistence.test.ts +263 -0
- package/src/r2-persistence.ts +367 -0
- package/src/schema-validation.test.ts +167 -0
- package/src/schema-validation.ts +330 -0
- package/src/types.ts +252 -0
- package/test/action-status.test.ts +42 -0
- package/test/batch-limits.test.ts +165 -0
- package/test/docs.test.ts +48 -0
- package/test/errors.test.ts +148 -0
- package/test/http-validation.test.ts +401 -0
- package/test/ns-client-errors.test.ts +208 -0
- package/test/ns-namespace.test.ts +307 -0
- package/test/performance.test.ts +168 -0
- package/test/schema-validation-error.test.ts +213 -0
- package/test/schema-validation.test.ts +440 -0
- package/test/search-escaping.test.ts +359 -0
- package/test/security.test.ts +322 -0
- package/tsconfig.json +10 -0
- package/wrangler.jsonc +16 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest'
|
|
2
|
+
import { createMemoryProvider } from './memory-provider.js'
|
|
3
|
+
import type { DigitalObjectsProvider, FieldDefinition } from './types.js'
|
|
4
|
+
import { ValidationError } from './errors.js'
|
|
5
|
+
|
|
6
|
+
describe('Schema Validation', () => {
|
|
7
|
+
let provider: DigitalObjectsProvider
|
|
8
|
+
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
provider = createMemoryProvider()
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
describe('Thing validation against Noun schema', () => {
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
await provider.defineNoun({
|
|
16
|
+
name: 'User',
|
|
17
|
+
schema: {
|
|
18
|
+
name: { type: 'string', required: true },
|
|
19
|
+
email: { type: 'string', required: true },
|
|
20
|
+
age: { type: 'number' },
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('should accept valid data matching schema', async () => {
|
|
26
|
+
const thing = await provider.create(
|
|
27
|
+
'User',
|
|
28
|
+
{
|
|
29
|
+
name: 'Alice',
|
|
30
|
+
email: 'alice@example.com',
|
|
31
|
+
age: 30,
|
|
32
|
+
},
|
|
33
|
+
undefined,
|
|
34
|
+
{ validate: true }
|
|
35
|
+
)
|
|
36
|
+
expect(thing.data.name).toBe('Alice')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('should reject data missing required fields', async () => {
|
|
40
|
+
try {
|
|
41
|
+
await provider.create('User', { name: 'Alice' }, undefined, { validate: true })
|
|
42
|
+
expect.fail('Should have thrown')
|
|
43
|
+
} catch (error) {
|
|
44
|
+
expect(error).toBeInstanceOf(ValidationError)
|
|
45
|
+
const validationError = error as ValidationError
|
|
46
|
+
const fields = validationError.errors.map((e) => e.field)
|
|
47
|
+
expect(fields).toContain('email')
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('should reject data with wrong types', async () => {
|
|
52
|
+
try {
|
|
53
|
+
await provider.create(
|
|
54
|
+
'User',
|
|
55
|
+
{ name: 'Alice', email: 'a@b.com', age: 'thirty' },
|
|
56
|
+
undefined,
|
|
57
|
+
{
|
|
58
|
+
validate: true,
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
expect.fail('Should have thrown')
|
|
62
|
+
} catch (error) {
|
|
63
|
+
expect(error).toBeInstanceOf(ValidationError)
|
|
64
|
+
const validationError = error as ValidationError
|
|
65
|
+
const ageError = validationError.errors.find((e) => e.field === 'age')
|
|
66
|
+
expect(ageError).toBeDefined()
|
|
67
|
+
expect(ageError?.message).toMatch(/number/i)
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should allow extra fields not in schema', async () => {
|
|
72
|
+
const thing = await provider.create(
|
|
73
|
+
'User',
|
|
74
|
+
{
|
|
75
|
+
name: 'Alice',
|
|
76
|
+
email: 'alice@example.com',
|
|
77
|
+
nickname: 'Ali',
|
|
78
|
+
},
|
|
79
|
+
undefined,
|
|
80
|
+
{ validate: true }
|
|
81
|
+
)
|
|
82
|
+
expect(thing.data.nickname).toBe('Ali')
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
describe('Validation opt-in behavior', () => {
|
|
87
|
+
it('should skip validation when validate option is false', async () => {
|
|
88
|
+
await provider.defineNoun({
|
|
89
|
+
name: 'Strict',
|
|
90
|
+
schema: { required: { type: 'string', required: true } },
|
|
91
|
+
})
|
|
92
|
+
// Should NOT throw without validate: true
|
|
93
|
+
const thing = await provider.create('Strict', {})
|
|
94
|
+
expect(thing).toBeDefined()
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('should skip validation when no schema defined', async () => {
|
|
98
|
+
await provider.defineNoun({ name: 'Flexible' })
|
|
99
|
+
const thing = await provider.create('Flexible', { anything: 'goes' }, undefined, {
|
|
100
|
+
validate: true,
|
|
101
|
+
})
|
|
102
|
+
expect(thing.data.anything).toBe('goes')
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
describe('Nested object schemas', () => {
|
|
107
|
+
it('should validate nested objects', async () => {
|
|
108
|
+
await provider.defineNoun({
|
|
109
|
+
name: 'Profile',
|
|
110
|
+
schema: {
|
|
111
|
+
user: { type: 'object', required: true },
|
|
112
|
+
},
|
|
113
|
+
})
|
|
114
|
+
try {
|
|
115
|
+
await provider.create('Profile', { user: 'not-an-object' }, undefined, { validate: true })
|
|
116
|
+
expect.fail('Should have thrown')
|
|
117
|
+
} catch (error) {
|
|
118
|
+
expect(error).toBeInstanceOf(ValidationError)
|
|
119
|
+
const validationError = error as ValidationError
|
|
120
|
+
const userError = validationError.errors.find((e) => e.field === 'user')
|
|
121
|
+
expect(userError).toBeDefined()
|
|
122
|
+
expect(userError?.message).toMatch(/object/i)
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
describe('Array field validation', () => {
|
|
128
|
+
it('should validate array fields', async () => {
|
|
129
|
+
await provider.defineNoun({
|
|
130
|
+
name: 'Post',
|
|
131
|
+
schema: {
|
|
132
|
+
tags: { type: 'array' },
|
|
133
|
+
},
|
|
134
|
+
})
|
|
135
|
+
try {
|
|
136
|
+
await provider.create('Post', { tags: 'not-array' }, undefined, { validate: true })
|
|
137
|
+
expect.fail('Should have thrown')
|
|
138
|
+
} catch (error) {
|
|
139
|
+
expect(error).toBeInstanceOf(ValidationError)
|
|
140
|
+
const validationError = error as ValidationError
|
|
141
|
+
const tagsError = validationError.errors.find((e) => e.field === 'tags')
|
|
142
|
+
expect(tagsError).toBeDefined()
|
|
143
|
+
expect(tagsError?.message).toMatch(/array/i)
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
describe('Update validation', () => {
|
|
149
|
+
it('should validate updates when validate option is true', async () => {
|
|
150
|
+
await provider.defineNoun({
|
|
151
|
+
name: 'Item',
|
|
152
|
+
schema: { count: { type: 'number' } },
|
|
153
|
+
})
|
|
154
|
+
const item = await provider.create('Item', { count: 1 })
|
|
155
|
+
try {
|
|
156
|
+
await provider.update(item.id, { count: 'two' }, { validate: true })
|
|
157
|
+
expect.fail('Should have thrown')
|
|
158
|
+
} catch (error) {
|
|
159
|
+
expect(error).toBeInstanceOf(ValidationError)
|
|
160
|
+
const validationError = error as ValidationError
|
|
161
|
+
const countError = validationError.errors.find((e) => e.field === 'count')
|
|
162
|
+
expect(countError).toBeDefined()
|
|
163
|
+
expect(countError?.message).toMatch(/number/i)
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
})
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime schema validation for Digital Objects
|
|
3
|
+
*
|
|
4
|
+
* Validates Thing data against Noun schemas when validation is enabled.
|
|
5
|
+
* Provides clear, actionable error messages with field paths and suggestions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { FieldDefinition, ExtendedFieldDefinition, ValidationOptions } from './types.js'
|
|
9
|
+
import { ValidationError } from './errors.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Schema validation error with detailed context
|
|
13
|
+
*
|
|
14
|
+
* Provides comprehensive information about validation failures
|
|
15
|
+
* including field paths, type information, and suggestions for fixes.
|
|
16
|
+
*/
|
|
17
|
+
export interface SchemaValidationError {
|
|
18
|
+
/** Full path to the field (e.g., 'address.city' for nested objects) */
|
|
19
|
+
field: string
|
|
20
|
+
/** Human-readable error message */
|
|
21
|
+
message: string
|
|
22
|
+
/** Expected type or constraint */
|
|
23
|
+
expected: string
|
|
24
|
+
/** Actual value's type or description */
|
|
25
|
+
received: string
|
|
26
|
+
/** Optional suggestion for fixing the error */
|
|
27
|
+
suggestion?: string
|
|
28
|
+
/** Error code for programmatic handling */
|
|
29
|
+
code: ValidationErrorCode
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Error codes for programmatic error handling
|
|
34
|
+
*/
|
|
35
|
+
export type ValidationErrorCode =
|
|
36
|
+
| 'REQUIRED_FIELD'
|
|
37
|
+
| 'TYPE_MISMATCH'
|
|
38
|
+
| 'INVALID_FORMAT'
|
|
39
|
+
| 'UNKNOWN_FIELD'
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Result from validateOnly() for pre-flight validation
|
|
43
|
+
*/
|
|
44
|
+
export interface ValidationResult {
|
|
45
|
+
valid: boolean
|
|
46
|
+
errors: SchemaValidationError[]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Type guard to check if a field definition is an extended definition (object with type)
|
|
51
|
+
*/
|
|
52
|
+
function isExtendedFieldDefinition(def: FieldDefinition): def is ExtendedFieldDefinition {
|
|
53
|
+
return typeof def === 'object' && def !== null && 'type' in def
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get the actual JavaScript type of a value for validation purposes
|
|
58
|
+
*/
|
|
59
|
+
function getActualType(value: unknown): string {
|
|
60
|
+
if (value === null) return 'null'
|
|
61
|
+
if (Array.isArray(value)) return 'array'
|
|
62
|
+
if (value instanceof Date) return 'date'
|
|
63
|
+
return typeof value
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get a suggestion for fixing a type mismatch error
|
|
68
|
+
*/
|
|
69
|
+
function getSuggestion(expected: string, actual: string, value: unknown): string | undefined {
|
|
70
|
+
// String to number
|
|
71
|
+
if (expected === 'number' && actual === 'string') {
|
|
72
|
+
const num = Number(value)
|
|
73
|
+
if (!isNaN(num)) {
|
|
74
|
+
return `Convert to number: ${num}`
|
|
75
|
+
}
|
|
76
|
+
return 'Provide a valid number'
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Number to string
|
|
80
|
+
if (expected === 'string' && actual === 'number') {
|
|
81
|
+
return `Convert to string: "${value}"`
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// String to boolean
|
|
85
|
+
if (expected === 'boolean' && actual === 'string') {
|
|
86
|
+
const str = String(value).toLowerCase()
|
|
87
|
+
if (str === 'true' || str === 'false') {
|
|
88
|
+
return `Convert to boolean: ${str === 'true'}`
|
|
89
|
+
}
|
|
90
|
+
return 'Use true or false'
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Array expected
|
|
94
|
+
if (expected === 'array' && actual !== 'array') {
|
|
95
|
+
return 'Wrap value in an array: [value]'
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Object expected
|
|
99
|
+
if (expected === 'object' && actual !== 'object') {
|
|
100
|
+
return 'Provide an object: { ... }'
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Date/datetime (expected string-based type but got wrong type)
|
|
104
|
+
if ((expected === 'date' || expected === 'datetime') && actual !== 'string') {
|
|
105
|
+
return 'Provide a valid ISO 8601 date string'
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// URL (expected string-based type but got wrong type)
|
|
109
|
+
if (expected === 'url' && actual !== 'string') {
|
|
110
|
+
return 'Provide a valid URL starting with http:// or https://'
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Markdown
|
|
114
|
+
if (expected === 'markdown' && actual !== 'string') {
|
|
115
|
+
return 'Provide a string containing markdown content'
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return undefined
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Create a validation error with consistent formatting
|
|
123
|
+
*/
|
|
124
|
+
function createError(
|
|
125
|
+
field: string,
|
|
126
|
+
code: ValidationErrorCode,
|
|
127
|
+
expected: string,
|
|
128
|
+
received: string,
|
|
129
|
+
value?: unknown
|
|
130
|
+
): SchemaValidationError {
|
|
131
|
+
let message: string
|
|
132
|
+
|
|
133
|
+
switch (code) {
|
|
134
|
+
case 'REQUIRED_FIELD':
|
|
135
|
+
message = `Missing required field '${field}'`
|
|
136
|
+
break
|
|
137
|
+
case 'TYPE_MISMATCH':
|
|
138
|
+
message = `Field '${field}' has wrong type: expected ${expected}, got ${received}`
|
|
139
|
+
break
|
|
140
|
+
case 'INVALID_FORMAT':
|
|
141
|
+
message = `Field '${field}' has invalid format: expected ${expected}`
|
|
142
|
+
break
|
|
143
|
+
case 'UNKNOWN_FIELD':
|
|
144
|
+
message = `Unknown field '${field}'`
|
|
145
|
+
break
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const error: SchemaValidationError = {
|
|
149
|
+
field,
|
|
150
|
+
message,
|
|
151
|
+
expected,
|
|
152
|
+
received,
|
|
153
|
+
code,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Add suggestion if available
|
|
157
|
+
const suggestion = getSuggestion(expected, received, value)
|
|
158
|
+
if (suggestion) {
|
|
159
|
+
error.suggestion = suggestion
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return error
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Internal validation logic that collects errors
|
|
167
|
+
*
|
|
168
|
+
* @param data - The data to validate
|
|
169
|
+
* @param schema - The schema to validate against
|
|
170
|
+
* @param path - Current field path for nested objects
|
|
171
|
+
* @returns Array of validation errors
|
|
172
|
+
*/
|
|
173
|
+
function collectErrors(
|
|
174
|
+
data: Record<string, unknown>,
|
|
175
|
+
schema: Record<string, FieldDefinition>,
|
|
176
|
+
path: string = ''
|
|
177
|
+
): SchemaValidationError[] {
|
|
178
|
+
const errors: SchemaValidationError[] = []
|
|
179
|
+
|
|
180
|
+
for (const [field, def] of Object.entries(schema)) {
|
|
181
|
+
const fieldPath = path ? `${path}.${field}` : field
|
|
182
|
+
const value = data[field]
|
|
183
|
+
|
|
184
|
+
// Handle extended field definitions (object with type and required)
|
|
185
|
+
if (isExtendedFieldDefinition(def)) {
|
|
186
|
+
// Check required fields
|
|
187
|
+
if (def.required && (value === undefined || value === null)) {
|
|
188
|
+
errors.push(
|
|
189
|
+
createError(
|
|
190
|
+
fieldPath,
|
|
191
|
+
'REQUIRED_FIELD',
|
|
192
|
+
def.type,
|
|
193
|
+
value === null ? 'null' : 'undefined',
|
|
194
|
+
value
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
continue
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Skip validation if field is not present and not required
|
|
201
|
+
if (value === undefined) continue
|
|
202
|
+
|
|
203
|
+
// Type validation
|
|
204
|
+
const actualType = getActualType(value)
|
|
205
|
+
|
|
206
|
+
// Special handling for string-based types that should validate as strings
|
|
207
|
+
const stringBasedTypes = ['date', 'datetime', 'url', 'markdown', 'json']
|
|
208
|
+
const normalizedExpected = stringBasedTypes.includes(def.type) ? 'string' : def.type
|
|
209
|
+
|
|
210
|
+
if (normalizedExpected !== actualType) {
|
|
211
|
+
errors.push(createError(fieldPath, 'TYPE_MISMATCH', def.type, actualType, value))
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
// Simple field type (string like 'string', 'number', etc.)
|
|
215
|
+
// These are not required by default and just validate type if present
|
|
216
|
+
if (value === undefined) continue
|
|
217
|
+
|
|
218
|
+
const actualType = getActualType(value)
|
|
219
|
+
const expectedType = def.replace('?', '') // Remove optional marker
|
|
220
|
+
|
|
221
|
+
// Skip relation types (contain '.')
|
|
222
|
+
if (expectedType.includes('.')) continue
|
|
223
|
+
|
|
224
|
+
// Skip bracket-wrapped relation types
|
|
225
|
+
if (expectedType.startsWith('[') && expectedType.endsWith(']')) continue
|
|
226
|
+
|
|
227
|
+
// Special handling for string-based types that should validate as strings
|
|
228
|
+
const stringBasedTypes = ['date', 'datetime', 'url', 'markdown', 'json']
|
|
229
|
+
const normalizedExpected = stringBasedTypes.includes(expectedType) ? 'string' : expectedType
|
|
230
|
+
|
|
231
|
+
if (normalizedExpected !== actualType) {
|
|
232
|
+
errors.push(createError(fieldPath, 'TYPE_MISMATCH', expectedType, actualType, value))
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return errors
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Format validation errors for display in error messages
|
|
242
|
+
*/
|
|
243
|
+
function formatErrors(errors: SchemaValidationError[]): string {
|
|
244
|
+
if (errors.length === 0) return ''
|
|
245
|
+
|
|
246
|
+
const lines = errors.map((e) => {
|
|
247
|
+
let line = ` - ${e.message}`
|
|
248
|
+
if (e.suggestion) {
|
|
249
|
+
line += ` (${e.suggestion})`
|
|
250
|
+
}
|
|
251
|
+
return line
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
return lines.join('\n')
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Validates data against a schema definition without throwing
|
|
259
|
+
*
|
|
260
|
+
* Use this for pre-flight validation to check if data is valid
|
|
261
|
+
* before attempting create/update operations.
|
|
262
|
+
*
|
|
263
|
+
* @param data - The data to validate
|
|
264
|
+
* @param schema - The schema to validate against (from Noun.schema)
|
|
265
|
+
* @returns ValidationResult with valid boolean and errors array
|
|
266
|
+
*
|
|
267
|
+
* @example
|
|
268
|
+
* ```typescript
|
|
269
|
+
* const result = validateOnly(userData, userSchema)
|
|
270
|
+
* if (!result.valid) {
|
|
271
|
+
* console.log('Validation errors:', result.errors)
|
|
272
|
+
* // Handle errors gracefully
|
|
273
|
+
* }
|
|
274
|
+
* ```
|
|
275
|
+
*/
|
|
276
|
+
export function validateOnly(
|
|
277
|
+
data: Record<string, unknown>,
|
|
278
|
+
schema: Record<string, FieldDefinition> | undefined
|
|
279
|
+
): ValidationResult {
|
|
280
|
+
if (!schema) {
|
|
281
|
+
return { valid: true, errors: [] }
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const errors = collectErrors(data, schema)
|
|
285
|
+
return {
|
|
286
|
+
valid: errors.length === 0,
|
|
287
|
+
errors,
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Validates data against a schema definition
|
|
293
|
+
*
|
|
294
|
+
* @param data - The data to validate
|
|
295
|
+
* @param schema - The schema to validate against (from Noun.schema)
|
|
296
|
+
* @param options - Validation options (validate: true to enable)
|
|
297
|
+
* @throws Error if validation fails with detailed error messages
|
|
298
|
+
*
|
|
299
|
+
* @example
|
|
300
|
+
* ```typescript
|
|
301
|
+
* // Enable validation with options
|
|
302
|
+
* await provider.create('User', userData, undefined, { validate: true })
|
|
303
|
+
*
|
|
304
|
+
* // Error message example:
|
|
305
|
+
* // Validation failed (2 errors):
|
|
306
|
+
* // - Missing required field 'email'
|
|
307
|
+
* // - Field 'age' has wrong type: expected number, got string (Convert to number: 25)
|
|
308
|
+
* ```
|
|
309
|
+
*/
|
|
310
|
+
export function validateData(
|
|
311
|
+
data: Record<string, unknown>,
|
|
312
|
+
schema: Record<string, FieldDefinition> | undefined,
|
|
313
|
+
options?: ValidationOptions
|
|
314
|
+
): void {
|
|
315
|
+
// Skip validation if not enabled or no schema
|
|
316
|
+
if (!options?.validate || !schema) return
|
|
317
|
+
|
|
318
|
+
const errors = collectErrors(data, schema)
|
|
319
|
+
|
|
320
|
+
if (errors.length > 0) {
|
|
321
|
+
const errorCount = errors.length === 1 ? '1 error' : `${errors.length} errors`
|
|
322
|
+
throw new ValidationError(
|
|
323
|
+
`Validation failed (${errorCount})`,
|
|
324
|
+
errors.map((e) => ({ field: e.field, message: e.message }))
|
|
325
|
+
)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Re-export ValidationOptions for convenience
|
|
330
|
+
export type { ValidationOptions }
|