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.
Files changed (87) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/CHANGELOG.md +25 -0
  3. package/LICENSE +21 -0
  4. package/README.md +476 -0
  5. package/dist/ai-database-adapter.d.ts +49 -0
  6. package/dist/ai-database-adapter.d.ts.map +1 -0
  7. package/dist/ai-database-adapter.js +89 -0
  8. package/dist/ai-database-adapter.js.map +1 -0
  9. package/dist/errors.d.ts +47 -0
  10. package/dist/errors.d.ts.map +1 -0
  11. package/dist/errors.js +72 -0
  12. package/dist/errors.js.map +1 -0
  13. package/dist/http-schemas.d.ts +165 -0
  14. package/dist/http-schemas.d.ts.map +1 -0
  15. package/dist/http-schemas.js +55 -0
  16. package/dist/http-schemas.js.map +1 -0
  17. package/dist/index.d.ts +29 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +32 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/linguistic.d.ts +54 -0
  22. package/dist/linguistic.d.ts.map +1 -0
  23. package/dist/linguistic.js +226 -0
  24. package/dist/linguistic.js.map +1 -0
  25. package/dist/memory-provider.d.ts +46 -0
  26. package/dist/memory-provider.d.ts.map +1 -0
  27. package/dist/memory-provider.js +279 -0
  28. package/dist/memory-provider.js.map +1 -0
  29. package/dist/ns-client.d.ts +88 -0
  30. package/dist/ns-client.d.ts.map +1 -0
  31. package/dist/ns-client.js +253 -0
  32. package/dist/ns-client.js.map +1 -0
  33. package/dist/ns-exports.d.ts +23 -0
  34. package/dist/ns-exports.d.ts.map +1 -0
  35. package/dist/ns-exports.js +21 -0
  36. package/dist/ns-exports.js.map +1 -0
  37. package/dist/ns.d.ts +60 -0
  38. package/dist/ns.d.ts.map +1 -0
  39. package/dist/ns.js +818 -0
  40. package/dist/ns.js.map +1 -0
  41. package/dist/r2-persistence.d.ts +112 -0
  42. package/dist/r2-persistence.d.ts.map +1 -0
  43. package/dist/r2-persistence.js +252 -0
  44. package/dist/r2-persistence.js.map +1 -0
  45. package/dist/schema-validation.d.ts +80 -0
  46. package/dist/schema-validation.d.ts.map +1 -0
  47. package/dist/schema-validation.js +233 -0
  48. package/dist/schema-validation.js.map +1 -0
  49. package/dist/types.d.ts +184 -0
  50. package/dist/types.d.ts.map +1 -0
  51. package/dist/types.js +26 -0
  52. package/dist/types.js.map +1 -0
  53. package/package.json +55 -0
  54. package/src/ai-database-adapter.test.ts +610 -0
  55. package/src/ai-database-adapter.ts +189 -0
  56. package/src/benchmark.test.ts +109 -0
  57. package/src/errors.ts +91 -0
  58. package/src/http-schemas.ts +67 -0
  59. package/src/index.ts +87 -0
  60. package/src/linguistic.test.ts +1107 -0
  61. package/src/linguistic.ts +253 -0
  62. package/src/memory-provider.ts +470 -0
  63. package/src/ns-client.test.ts +1360 -0
  64. package/src/ns-client.ts +342 -0
  65. package/src/ns-exports.ts +23 -0
  66. package/src/ns.test.ts +1381 -0
  67. package/src/ns.ts +1215 -0
  68. package/src/provider.test.ts +675 -0
  69. package/src/r2-persistence.test.ts +263 -0
  70. package/src/r2-persistence.ts +367 -0
  71. package/src/schema-validation.test.ts +167 -0
  72. package/src/schema-validation.ts +330 -0
  73. package/src/types.ts +252 -0
  74. package/test/action-status.test.ts +42 -0
  75. package/test/batch-limits.test.ts +165 -0
  76. package/test/docs.test.ts +48 -0
  77. package/test/errors.test.ts +148 -0
  78. package/test/http-validation.test.ts +401 -0
  79. package/test/ns-client-errors.test.ts +208 -0
  80. package/test/ns-namespace.test.ts +307 -0
  81. package/test/performance.test.ts +168 -0
  82. package/test/schema-validation-error.test.ts +213 -0
  83. package/test/schema-validation.test.ts +440 -0
  84. package/test/search-escaping.test.ts +359 -0
  85. package/test/security.test.ts +322 -0
  86. package/tsconfig.json +10 -0
  87. package/wrangler.jsonc +16 -0
@@ -0,0 +1,440 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import {
3
+ validateData,
4
+ validateOnly,
5
+ type SchemaValidationError,
6
+ type ValidationResult,
7
+ type ValidationErrorCode,
8
+ } from '../src/schema-validation.js'
9
+ import type { FieldDefinition } from '../src/types.js'
10
+ import { createMemoryProvider } from '../src/memory-provider.js'
11
+ import { ValidationError } from '../src/errors.js'
12
+
13
+ describe('Schema Validation', () => {
14
+ describe('validateOnly', () => {
15
+ it('should return valid:true for data matching schema', () => {
16
+ const schema: Record<string, FieldDefinition> = {
17
+ name: 'string',
18
+ age: 'number',
19
+ }
20
+
21
+ const result = validateOnly({ name: 'Alice', age: 25 }, schema)
22
+
23
+ expect(result.valid).toBe(true)
24
+ expect(result.errors).toHaveLength(0)
25
+ })
26
+
27
+ it('should return valid:true when no schema is provided', () => {
28
+ const result = validateOnly({ anything: 'goes' }, undefined)
29
+
30
+ expect(result.valid).toBe(true)
31
+ expect(result.errors).toHaveLength(0)
32
+ })
33
+
34
+ it('should return errors for type mismatches', () => {
35
+ const schema: Record<string, FieldDefinition> = {
36
+ name: 'string',
37
+ age: 'number',
38
+ }
39
+
40
+ const result = validateOnly({ name: 'Alice', age: '25' }, schema)
41
+
42
+ expect(result.valid).toBe(false)
43
+ expect(result.errors).toHaveLength(1)
44
+ expect(result.errors[0].field).toBe('age')
45
+ expect(result.errors[0].code).toBe('TYPE_MISMATCH')
46
+ })
47
+
48
+ it('should return errors for missing required fields', () => {
49
+ const schema: Record<string, FieldDefinition> = {
50
+ email: { type: 'string', required: true },
51
+ }
52
+
53
+ const result = validateOnly({}, schema)
54
+
55
+ expect(result.valid).toBe(false)
56
+ expect(result.errors).toHaveLength(1)
57
+ expect(result.errors[0].field).toBe('email')
58
+ expect(result.errors[0].code).toBe('REQUIRED_FIELD')
59
+ })
60
+
61
+ it('should allow missing optional fields', () => {
62
+ const schema: Record<string, FieldDefinition> = {
63
+ name: 'string',
64
+ bio: 'string?', // Optional
65
+ }
66
+
67
+ const result = validateOnly({ name: 'Alice' }, schema)
68
+
69
+ expect(result.valid).toBe(true)
70
+ expect(result.errors).toHaveLength(0)
71
+ })
72
+
73
+ it('should skip validation for relation types', () => {
74
+ const schema: Record<string, FieldDefinition> = {
75
+ authorId: 'Author.id',
76
+ tags: '[Tag.posts]',
77
+ }
78
+
79
+ const result = validateOnly({ authorId: 'abc', tags: ['t1', 't2'] }, schema)
80
+
81
+ expect(result.valid).toBe(true)
82
+ expect(result.errors).toHaveLength(0)
83
+ })
84
+ })
85
+
86
+ describe('validateData', () => {
87
+ it('should not throw when validation is disabled', () => {
88
+ const schema: Record<string, FieldDefinition> = {
89
+ email: { type: 'string', required: true },
90
+ }
91
+
92
+ // No options = validation disabled
93
+ expect(() => validateData({}, schema)).not.toThrow()
94
+
95
+ // validate: false = validation disabled
96
+ expect(() => validateData({}, schema, { validate: false })).not.toThrow()
97
+ })
98
+
99
+ it('should throw on validation failure when enabled', () => {
100
+ const schema: Record<string, FieldDefinition> = {
101
+ email: { type: 'string', required: true },
102
+ }
103
+
104
+ expect(() => validateData({}, schema, { validate: true })).toThrow(/Validation failed/)
105
+ })
106
+
107
+ it('should include error count in message', () => {
108
+ const schema: Record<string, FieldDefinition> = {
109
+ email: { type: 'string', required: true },
110
+ name: { type: 'string', required: true },
111
+ }
112
+
113
+ try {
114
+ validateData({}, schema, { validate: true })
115
+ expect.fail('Should have thrown')
116
+ } catch (error) {
117
+ expect((error as Error).message).toMatch(/2 errors/)
118
+ }
119
+ })
120
+
121
+ it('should use singular error count for single error', () => {
122
+ const schema: Record<string, FieldDefinition> = {
123
+ email: { type: 'string', required: true },
124
+ }
125
+
126
+ try {
127
+ validateData({}, schema, { validate: true })
128
+ expect.fail('Should have thrown')
129
+ } catch (error) {
130
+ expect((error as Error).message).toMatch(/1 error/)
131
+ }
132
+ })
133
+ })
134
+
135
+ describe('Error Messages', () => {
136
+ it('should include field path in error message', () => {
137
+ const schema: Record<string, FieldDefinition> = {
138
+ age: 'number',
139
+ }
140
+
141
+ const result = validateOnly({ age: 'twenty-five' }, schema)
142
+
143
+ expect(result.errors[0].message).toContain("'age'")
144
+ })
145
+
146
+ it('should include expected and received types', () => {
147
+ const schema: Record<string, FieldDefinition> = {
148
+ count: 'number',
149
+ }
150
+
151
+ const result = validateOnly({ count: 'five' }, schema)
152
+
153
+ expect(result.errors[0].expected).toBe('number')
154
+ expect(result.errors[0].received).toBe('string')
155
+ })
156
+
157
+ it('should have clear missing field message', () => {
158
+ const schema: Record<string, FieldDefinition> = {
159
+ email: { type: 'string', required: true },
160
+ }
161
+
162
+ const result = validateOnly({}, schema)
163
+
164
+ expect(result.errors[0].message).toBe("Missing required field 'email'")
165
+ })
166
+
167
+ it('should have clear type mismatch message', () => {
168
+ const schema: Record<string, FieldDefinition> = {
169
+ age: 'number',
170
+ }
171
+
172
+ const result = validateOnly({ age: 'not-a-number' }, schema)
173
+
174
+ expect(result.errors[0].message).toBe(
175
+ "Field 'age' has wrong type: expected number, got string"
176
+ )
177
+ })
178
+ })
179
+
180
+ describe('Suggestions', () => {
181
+ it('should suggest conversion for string to number', () => {
182
+ const schema: Record<string, FieldDefinition> = {
183
+ age: 'number',
184
+ }
185
+
186
+ const result = validateOnly({ age: '25' }, schema)
187
+
188
+ expect(result.errors[0].suggestion).toBe('Convert to number: 25')
189
+ })
190
+
191
+ it('should suggest valid number for non-numeric string', () => {
192
+ const schema: Record<string, FieldDefinition> = {
193
+ age: 'number',
194
+ }
195
+
196
+ const result = validateOnly({ age: 'twenty-five' }, schema)
197
+
198
+ expect(result.errors[0].suggestion).toBe('Provide a valid number')
199
+ })
200
+
201
+ it('should suggest conversion for number to string', () => {
202
+ const schema: Record<string, FieldDefinition> = {
203
+ name: 'string',
204
+ }
205
+
206
+ const result = validateOnly({ name: 42 }, schema)
207
+
208
+ expect(result.errors[0].suggestion).toBe('Convert to string: "42"')
209
+ })
210
+
211
+ it('should suggest conversion for string boolean', () => {
212
+ const schema: Record<string, FieldDefinition> = {
213
+ active: 'boolean',
214
+ }
215
+
216
+ const result = validateOnly({ active: 'true' }, schema)
217
+
218
+ expect(result.errors[0].suggestion).toBe('Convert to boolean: true')
219
+ })
220
+
221
+ it('should suggest wrapping value in array', () => {
222
+ const schema: Record<string, FieldDefinition> = {
223
+ items: { type: 'array', required: true },
224
+ }
225
+
226
+ const result = validateOnly({ items: 'single-item' }, schema)
227
+
228
+ expect(result.errors[0].suggestion).toBe('Wrap value in an array: [value]')
229
+ })
230
+
231
+ it('should suggest object format', () => {
232
+ const schema: Record<string, FieldDefinition> = {
233
+ metadata: { type: 'object', required: true },
234
+ }
235
+
236
+ const result = validateOnly({ metadata: 'not-an-object' }, schema)
237
+
238
+ expect(result.errors[0].suggestion).toBe('Provide an object: { ... }')
239
+ })
240
+ })
241
+
242
+ describe('Special Types', () => {
243
+ it('should validate date/datetime as string', () => {
244
+ const schema: Record<string, FieldDefinition> = {
245
+ createdAt: 'datetime',
246
+ publishDate: 'date',
247
+ }
248
+
249
+ // Valid - strings are accepted
250
+ const validResult = validateOnly(
251
+ {
252
+ createdAt: '2024-01-15T10:00:00Z',
253
+ publishDate: '2024-01-15',
254
+ },
255
+ schema
256
+ )
257
+ expect(validResult.valid).toBe(true)
258
+
259
+ // Invalid - numbers are not accepted
260
+ const invalidResult = validateOnly({ createdAt: 1705312800000 }, schema)
261
+ expect(invalidResult.valid).toBe(false)
262
+ expect(invalidResult.errors[0].suggestion).toBe('Provide a valid ISO 8601 date string')
263
+ })
264
+
265
+ it('should validate url as string', () => {
266
+ const schema: Record<string, FieldDefinition> = {
267
+ website: 'url',
268
+ }
269
+
270
+ const validResult = validateOnly({ website: 'https://example.com' }, schema)
271
+ expect(validResult.valid).toBe(true)
272
+
273
+ const invalidResult = validateOnly({ website: 123 }, schema)
274
+ expect(invalidResult.valid).toBe(false)
275
+ expect(invalidResult.errors[0].suggestion).toBe(
276
+ 'Provide a valid URL starting with http:// or https://'
277
+ )
278
+ })
279
+
280
+ it('should validate markdown as string', () => {
281
+ const schema: Record<string, FieldDefinition> = {
282
+ content: 'markdown',
283
+ }
284
+
285
+ const validResult = validateOnly({ content: '# Hello\n\nWorld' }, schema)
286
+ expect(validResult.valid).toBe(true)
287
+
288
+ const invalidResult = validateOnly({ content: { text: 'hello' } }, schema)
289
+ expect(invalidResult.valid).toBe(false)
290
+ })
291
+
292
+ it('should validate json as string', () => {
293
+ const schema: Record<string, FieldDefinition> = {
294
+ config: 'json',
295
+ }
296
+
297
+ const validResult = validateOnly({ config: '{"key": "value"}' }, schema)
298
+ expect(validResult.valid).toBe(true)
299
+ })
300
+ })
301
+
302
+ describe('Extended Field Definitions', () => {
303
+ it('should validate required fields', () => {
304
+ const schema: Record<string, FieldDefinition> = {
305
+ email: { type: 'string', required: true },
306
+ name: { type: 'string', required: false },
307
+ bio: { type: 'string' }, // required defaults to false
308
+ }
309
+
310
+ // Missing required field
311
+ const result1 = validateOnly({ name: 'Alice', bio: 'Hello' }, schema)
312
+ expect(result1.valid).toBe(false)
313
+ expect(result1.errors).toHaveLength(1)
314
+ expect(result1.errors[0].field).toBe('email')
315
+
316
+ // With required field
317
+ const result2 = validateOnly({ email: 'alice@example.com' }, schema)
318
+ expect(result2.valid).toBe(true)
319
+ })
320
+
321
+ it('should handle null as missing for required fields', () => {
322
+ const schema: Record<string, FieldDefinition> = {
323
+ email: { type: 'string', required: true },
324
+ }
325
+
326
+ const result = validateOnly({ email: null }, schema)
327
+ expect(result.valid).toBe(false)
328
+ expect(result.errors[0].received).toBe('null')
329
+ })
330
+ })
331
+
332
+ describe('Integration with MemoryProvider', () => {
333
+ it('should validate on create when enabled', async () => {
334
+ const provider = createMemoryProvider()
335
+
336
+ await provider.defineNoun({
337
+ name: 'User',
338
+ schema: {
339
+ email: { type: 'string', required: true },
340
+ age: 'number',
341
+ },
342
+ })
343
+
344
+ // Without validation - should succeed
345
+ const user1 = await provider.create('User', { name: 'Anonymous' })
346
+ expect(user1.id).toBeDefined()
347
+
348
+ // With validation - should fail
349
+ await expect(
350
+ provider.create('User', { name: 'Anonymous' }, undefined, { validate: true })
351
+ ).rejects.toThrow(/Validation failed/)
352
+ })
353
+
354
+ it('should include field errors in ValidationError', async () => {
355
+ const provider = createMemoryProvider()
356
+
357
+ await provider.defineNoun({
358
+ name: 'Product',
359
+ schema: {
360
+ price: 'number',
361
+ },
362
+ })
363
+
364
+ try {
365
+ await provider.create('Product', { price: '19.99' }, undefined, { validate: true })
366
+ expect.fail('Should have thrown')
367
+ } catch (error) {
368
+ expect(error).toBeInstanceOf(ValidationError)
369
+ const validationError = error as ValidationError
370
+ expect(validationError.errors).toHaveLength(1)
371
+ expect(validationError.errors[0].field).toBe('price')
372
+ expect(validationError.errors[0].message).toMatch(/number/i)
373
+ }
374
+ })
375
+ })
376
+
377
+ describe('Error Codes', () => {
378
+ it('should use REQUIRED_FIELD code for missing required fields', () => {
379
+ const schema: Record<string, FieldDefinition> = {
380
+ email: { type: 'string', required: true },
381
+ }
382
+
383
+ const result = validateOnly({}, schema)
384
+ expect(result.errors[0].code).toBe('REQUIRED_FIELD')
385
+ })
386
+
387
+ it('should use TYPE_MISMATCH code for wrong types', () => {
388
+ const schema: Record<string, FieldDefinition> = {
389
+ count: 'number',
390
+ }
391
+
392
+ const result = validateOnly({ count: 'five' }, schema)
393
+ expect(result.errors[0].code).toBe('TYPE_MISMATCH')
394
+ })
395
+ })
396
+
397
+ describe('Multiple Errors', () => {
398
+ it('should collect all validation errors', () => {
399
+ const schema: Record<string, FieldDefinition> = {
400
+ email: { type: 'string', required: true },
401
+ age: 'number',
402
+ active: 'boolean',
403
+ }
404
+
405
+ const result = validateOnly({ age: 'not-a-number', active: 'yes' }, schema)
406
+
407
+ expect(result.errors).toHaveLength(3)
408
+
409
+ const codes = result.errors.map((e) => e.code)
410
+ expect(codes).toContain('REQUIRED_FIELD')
411
+ expect(codes.filter((c) => c === 'TYPE_MISMATCH')).toHaveLength(2)
412
+ })
413
+
414
+ it('should include multiple errors in ValidationError errors array', () => {
415
+ const schema: Record<string, FieldDefinition> = {
416
+ email: { type: 'string', required: true },
417
+ age: 'number',
418
+ }
419
+
420
+ try {
421
+ validateData({ age: 'twenty' }, schema, { validate: true })
422
+ expect.fail('Should have thrown')
423
+ } catch (error) {
424
+ expect(error).toBeInstanceOf(ValidationError)
425
+ const validationError = error as ValidationError
426
+ expect(validationError.errors).toHaveLength(2)
427
+
428
+ const fields = validationError.errors.map((e) => e.field)
429
+ expect(fields).toContain('email')
430
+ expect(fields).toContain('age')
431
+
432
+ const emailError = validationError.errors.find((e) => e.field === 'email')
433
+ expect(emailError?.message).toMatch(/required/i)
434
+
435
+ const ageError = validationError.errors.find((e) => e.field === 'age')
436
+ expect(ageError?.message).toMatch(/wrong type/i)
437
+ }
438
+ })
439
+ })
440
+ })