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,189 @@
1
+ /**
2
+ * ai-database Adapter
3
+ *
4
+ * Wraps a DigitalObjectsProvider to provide the ai-database DBProvider interface.
5
+ * This enables ai-database to use digital-objects as its storage backend.
6
+ */
7
+
8
+ import type { DigitalObjectsProvider, Thing, ListOptions as DOListOptions } from './types.js'
9
+
10
+ // These types match ai-database's DBProvider interface
11
+ export interface ListOptions {
12
+ limit?: number
13
+ offset?: number
14
+ where?: Record<string, unknown>
15
+ orderBy?: string
16
+ order?: 'asc' | 'desc'
17
+ }
18
+
19
+ export interface SearchOptions extends ListOptions {
20
+ fields?: string[]
21
+ }
22
+
23
+ export interface SemanticSearchOptions extends SearchOptions {
24
+ embedding?: number[]
25
+ minScore?: number
26
+ }
27
+
28
+ export interface HybridSearchOptions extends SearchOptions {
29
+ semanticWeight?: number
30
+ ftsWeight?: number
31
+ minScore?: number
32
+ }
33
+
34
+ /**
35
+ * ai-database DBProvider interface (simplified)
36
+ */
37
+ export interface DBProvider {
38
+ // Entity operations
39
+ get(type: string, id: string): Promise<Record<string, unknown> | null>
40
+ list(type: string, options?: ListOptions): Promise<Record<string, unknown>[]>
41
+ search(type: string, query: string, options?: SearchOptions): Promise<Record<string, unknown>[]>
42
+ create(
43
+ type: string,
44
+ id: string | undefined,
45
+ data: Record<string, unknown>
46
+ ): Promise<Record<string, unknown>>
47
+ update(type: string, id: string, data: Record<string, unknown>): Promise<Record<string, unknown>>
48
+ delete(type: string, id: string): Promise<boolean>
49
+
50
+ // Relation operations
51
+ related(type: string, id: string, relation: string): Promise<Record<string, unknown>[]>
52
+ relate(
53
+ fromType: string,
54
+ fromId: string,
55
+ relation: string,
56
+ toType: string,
57
+ toId: string,
58
+ metadata?: { matchMode?: 'exact' | 'fuzzy'; similarity?: number; matchedType?: string }
59
+ ): Promise<void>
60
+ unrelate(
61
+ fromType: string,
62
+ fromId: string,
63
+ relation: string,
64
+ toType: string,
65
+ toId: string
66
+ ): Promise<void>
67
+ }
68
+
69
+ /**
70
+ * Convert Thing to entity format (with $id, $type)
71
+ */
72
+ function thingToEntity<T>(
73
+ thing: Thing<T>
74
+ ): Record<string, unknown> & { $id: string; $type: string } {
75
+ return {
76
+ $id: thing.id,
77
+ $type: thing.noun,
78
+ ...thing.data,
79
+ } as Record<string, unknown> & { $id: string; $type: string }
80
+ }
81
+
82
+ /**
83
+ * Extract data from entity (remove $id, $type)
84
+ */
85
+ function entityToData(entity: Record<string, unknown>): Record<string, unknown> {
86
+ const { $id, $type, ...data } = entity
87
+ return data
88
+ }
89
+
90
+ /**
91
+ * Create a DBProvider adapter from a DigitalObjectsProvider
92
+ */
93
+ export function createDBProviderAdapter(provider: DigitalObjectsProvider): DBProvider {
94
+ return {
95
+ async get(type: string, id: string): Promise<Record<string, unknown> | null> {
96
+ const thing = await provider.get(id)
97
+ if (!thing || thing.noun !== type) return null
98
+ return thingToEntity(thing)
99
+ },
100
+
101
+ async list(type: string, options?: ListOptions): Promise<Record<string, unknown>[]> {
102
+ const things = await provider.list(type, options as DOListOptions)
103
+ return things.map(thingToEntity)
104
+ },
105
+
106
+ async search(
107
+ type: string,
108
+ query: string,
109
+ options?: SearchOptions
110
+ ): Promise<Record<string, unknown>[]> {
111
+ const things = await provider.search(query, { ...options, where: { ...options?.where } })
112
+ // Filter by type since search is global
113
+ return things.filter((t) => t.noun === type).map(thingToEntity)
114
+ },
115
+
116
+ async create(
117
+ type: string,
118
+ id: string | undefined,
119
+ data: Record<string, unknown>
120
+ ): Promise<Record<string, unknown>> {
121
+ // Ensure noun is defined
122
+ const existingNoun = await provider.getNoun(type)
123
+ if (!existingNoun) {
124
+ await provider.defineNoun({ name: type })
125
+ }
126
+
127
+ const thing = await provider.create(type, entityToData(data), id)
128
+ return thingToEntity(thing)
129
+ },
130
+
131
+ async update(
132
+ _type: string,
133
+ id: string,
134
+ data: Record<string, unknown>
135
+ ): Promise<Record<string, unknown>> {
136
+ const thing = await provider.update(id, entityToData(data))
137
+ return thingToEntity(thing)
138
+ },
139
+
140
+ async delete(_type: string, id: string): Promise<boolean> {
141
+ return provider.delete(id)
142
+ },
143
+
144
+ async related(type: string, id: string, relation: string): Promise<Record<string, unknown>[]> {
145
+ // ai-database expects related entities of a specific type via a relation
146
+ // digital-objects uses verb as the relation type
147
+ const things = await provider.related(id, relation, 'both')
148
+ // Filter by expected type
149
+ return things.filter((t) => t.noun === type).map(thingToEntity)
150
+ },
151
+
152
+ async relate(
153
+ _fromType: string,
154
+ fromId: string,
155
+ relation: string,
156
+ _toType: string,
157
+ toId: string,
158
+ metadata?: { matchMode?: 'exact' | 'fuzzy'; similarity?: number; matchedType?: string }
159
+ ): Promise<void> {
160
+ // Ensure verb is defined
161
+ const existingVerb = await provider.getVerb(relation)
162
+ if (!existingVerb) {
163
+ await provider.defineVerb({ name: relation })
164
+ }
165
+
166
+ await provider.perform(relation, fromId, toId, metadata)
167
+ },
168
+
169
+ async unrelate(
170
+ _fromType: string,
171
+ fromId: string,
172
+ relation: string,
173
+ _toType: string,
174
+ toId: string
175
+ ): Promise<void> {
176
+ // Find the action(s) matching this relation and delete them
177
+ const actions = await provider.listActions({
178
+ verb: relation,
179
+ subject: fromId,
180
+ object: toId,
181
+ })
182
+
183
+ // Delete all matching actions (for GDPR compliance)
184
+ for (const action of actions) {
185
+ await provider.deleteAction(action.id)
186
+ }
187
+ },
188
+ }
189
+ }
@@ -0,0 +1,109 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest'
2
+ import { createMemoryProvider } from './memory-provider.js'
3
+ import type { DigitalObjectsProvider } from './types.js'
4
+
5
+ describe('Performance Benchmarks', () => {
6
+ let provider: DigitalObjectsProvider
7
+
8
+ beforeEach(async () => {
9
+ provider = createMemoryProvider()
10
+ await provider.defineNoun({ name: 'Item' })
11
+ await provider.defineVerb({ name: 'link' })
12
+ })
13
+
14
+ describe('create() performance', () => {
15
+ it('should create 1000 things in under 100ms', async () => {
16
+ const start = performance.now()
17
+
18
+ for (let i = 0; i < 1000; i++) {
19
+ await provider.create('Item', { index: i, name: `Item ${i}` })
20
+ }
21
+
22
+ const elapsed = performance.now() - start
23
+ console.log(
24
+ `create() 1000 items: ${elapsed.toFixed(2)}ms (${((1000 / elapsed) * 1000).toFixed(
25
+ 0
26
+ )} ops/sec)`
27
+ )
28
+ expect(elapsed).toBeLessThan(100)
29
+ })
30
+ })
31
+
32
+ describe('list() performance', () => {
33
+ beforeEach(async () => {
34
+ // Create 10k items
35
+ for (let i = 0; i < 10000; i++) {
36
+ await provider.create('Item', { index: i })
37
+ }
38
+ })
39
+
40
+ it('should list 100 items from 10k in under 10ms', async () => {
41
+ const start = performance.now()
42
+
43
+ const items = await provider.list('Item', { limit: 100 })
44
+
45
+ const elapsed = performance.now() - start
46
+ console.log(`list() 100 from 10k: ${elapsed.toFixed(2)}ms`)
47
+ expect(items.length).toBe(100)
48
+ expect(elapsed).toBeLessThan(10)
49
+ })
50
+
51
+ it('should list with where filter in under 50ms', async () => {
52
+ const start = performance.now()
53
+
54
+ const items = await provider.list('Item', { where: { index: 5000 } })
55
+
56
+ const elapsed = performance.now() - start
57
+ console.log(`list() with where: ${elapsed.toFixed(2)}ms`)
58
+ expect(elapsed).toBeLessThan(50)
59
+ })
60
+ })
61
+
62
+ describe('related() performance', () => {
63
+ beforeEach(async () => {
64
+ // Create graph: 100 items with 10 relations each
65
+ const items = []
66
+ for (let i = 0; i < 100; i++) {
67
+ items.push(await provider.create('Item', { index: i }))
68
+ }
69
+ for (let i = 0; i < 100; i++) {
70
+ for (let j = 0; j < 10; j++) {
71
+ const target = (i + j + 1) % 100
72
+ await provider.perform('link', items[i].id, items[target].id)
73
+ }
74
+ }
75
+ })
76
+
77
+ it('should traverse relations in under 20ms', async () => {
78
+ const items = await provider.list('Item', { limit: 1 })
79
+ const start = performance.now()
80
+
81
+ const related = await provider.related(items[0].id, 'link', 'out')
82
+
83
+ const elapsed = performance.now() - start
84
+ console.log(`related() traversal: ${elapsed.toFixed(2)}ms, found ${related.length} items`)
85
+ expect(elapsed).toBeLessThan(20)
86
+ })
87
+ })
88
+
89
+ describe('search() performance', () => {
90
+ beforeEach(async () => {
91
+ for (let i = 0; i < 1000; i++) {
92
+ await provider.create('Item', {
93
+ name: `Item ${i}`,
94
+ description: `This is a searchable description for item number ${i}`,
95
+ })
96
+ }
97
+ })
98
+
99
+ it('should search 1000 items in under 50ms', async () => {
100
+ const start = performance.now()
101
+
102
+ const results = await provider.search('searchable', { limit: 100 })
103
+
104
+ const elapsed = performance.now() - start
105
+ console.log(`search() 1000 items: ${elapsed.toFixed(2)}ms, found ${results.length} results`)
106
+ expect(elapsed).toBeLessThan(50)
107
+ })
108
+ })
109
+ })
package/src/errors.ts ADDED
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Standardized error classes for digital-objects
3
+ *
4
+ * These errors provide consistent error handling across all providers
5
+ * (MemoryProvider, NS Durable Object) with proper HTTP status codes.
6
+ */
7
+
8
+ /**
9
+ * Base error class for all digital-objects errors
10
+ */
11
+ export class DigitalObjectsError extends Error {
12
+ constructor(message: string, public code: string, public statusCode: number = 500) {
13
+ super(message)
14
+ this.name = 'DigitalObjectsError'
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Thrown when a resource is not found
20
+ */
21
+ export class NotFoundError extends DigitalObjectsError {
22
+ constructor(type: string, id: string) {
23
+ super(`${type} not found: ${id}`, 'NOT_FOUND', 404)
24
+ this.name = 'NotFoundError'
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Thrown when validation fails
30
+ */
31
+ export class ValidationError extends DigitalObjectsError {
32
+ constructor(message: string, public errors: Array<{ field: string; message: string }>) {
33
+ super(message, 'VALIDATION_ERROR', 400)
34
+ this.name = 'ValidationError'
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Thrown when there's a conflict (e.g., duplicate ID)
40
+ */
41
+ export class ConflictError extends DigitalObjectsError {
42
+ constructor(message: string) {
43
+ super(message, 'CONFLICT', 409)
44
+ this.name = 'ConflictError'
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Thrown when a server error occurs (5xx status codes)
50
+ */
51
+ export class ServerError extends DigitalObjectsError {
52
+ constructor(message: string, statusCode: number = 500) {
53
+ super(message, 'SERVER_ERROR', statusCode)
54
+ this.name = 'ServerError'
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Thrown when a network error occurs (connection refused, timeout, etc.)
60
+ */
61
+ export class NetworkError extends DigitalObjectsError {
62
+ constructor(message: string, public cause?: Error) {
63
+ super(message, 'NETWORK_ERROR', 503)
64
+ this.name = 'NetworkError'
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Convert an error to an HTTP-safe JSON response body
70
+ */
71
+ export function errorToResponse(error: unknown): { body: object; status: number } {
72
+ if (error instanceof DigitalObjectsError) {
73
+ return {
74
+ body: {
75
+ error: error.code,
76
+ message: error.message,
77
+ ...(error instanceof ValidationError ? { errors: error.errors } : {}),
78
+ },
79
+ status: error.statusCode,
80
+ }
81
+ }
82
+
83
+ // Don't expose internal error details to clients
84
+ return {
85
+ body: {
86
+ error: 'INTERNAL_ERROR',
87
+ message: 'An internal error occurred',
88
+ },
89
+ status: 500,
90
+ }
91
+ }
@@ -0,0 +1,67 @@
1
+ import { z } from 'zod'
2
+
3
+ export const NounDefinitionSchema = z.object({
4
+ name: z.string().min(1),
5
+ singular: z.string().optional(),
6
+ plural: z.string().optional(),
7
+ description: z.string().optional(),
8
+ schema: z.record(z.any()).optional(),
9
+ })
10
+
11
+ export const VerbDefinitionSchema = z.object({
12
+ name: z.string().min(1),
13
+ action: z.string().optional(),
14
+ act: z.string().optional(),
15
+ activity: z.string().optional(),
16
+ event: z.string().optional(),
17
+ reverseBy: z.string().optional(),
18
+ inverse: z.string().optional(),
19
+ description: z.string().optional(),
20
+ })
21
+
22
+ export const CreateThingSchema = z.object({
23
+ noun: z.string().min(1),
24
+ data: z.record(z.any()),
25
+ id: z.string().optional(),
26
+ })
27
+
28
+ export const UpdateThingSchema = z.object({
29
+ data: z.record(z.any()),
30
+ })
31
+
32
+ export const PerformActionSchema = z.object({
33
+ verb: z.string().min(1),
34
+ subject: z.string().optional(),
35
+ object: z.string().optional(),
36
+ data: z.any().optional(),
37
+ })
38
+
39
+ // Batch operation schemas
40
+ export const BatchCreateThingsSchema = z.object({
41
+ noun: z.string().min(1),
42
+ items: z.array(z.record(z.any())),
43
+ })
44
+
45
+ export const BatchUpdateThingsSchema = z.object({
46
+ updates: z.array(
47
+ z.object({
48
+ id: z.string().min(1),
49
+ data: z.record(z.any()),
50
+ })
51
+ ),
52
+ })
53
+
54
+ export const BatchDeleteThingsSchema = z.object({
55
+ ids: z.array(z.string().min(1)),
56
+ })
57
+
58
+ export const BatchPerformActionsSchema = z.object({
59
+ actions: z.array(
60
+ z.object({
61
+ verb: z.string().min(1),
62
+ subject: z.string().optional(),
63
+ object: z.string().optional(),
64
+ data: z.any().optional(),
65
+ })
66
+ ),
67
+ })
package/src/index.ts ADDED
@@ -0,0 +1,87 @@
1
+ /**
2
+ * digital-objects - Unified storage primitive for AI primitives
3
+ *
4
+ * Core concepts:
5
+ * - **Nouns**: Entity type definitions (singular/plural/schema)
6
+ * - **Verbs**: Action definitions (conjugations, reverse forms)
7
+ * - **Things**: Entity instances (the actual data)
8
+ * - **Actions**: Events + Relationships + Audit Trail (unified graph edges)
9
+ *
10
+ * Providers:
11
+ * - `MemoryProvider`: In-memory for tests
12
+ * - `NS`: SQLite in Cloudflare Durable Objects (import from 'digital-objects/ns')
13
+ *
14
+ * @packageDocumentation
15
+ */
16
+
17
+ // Types
18
+ export type {
19
+ Noun,
20
+ NounDefinition,
21
+ Verb,
22
+ VerbDefinition,
23
+ Thing,
24
+ Action,
25
+ ActionStatusType,
26
+ FieldDefinition,
27
+ PrimitiveType,
28
+ ListOptions,
29
+ ActionOptions,
30
+ DigitalObjectsProvider,
31
+ Direction,
32
+ } from './types.js'
33
+
34
+ // Validation utilities and constants
35
+ export { validateDirection, MAX_BATCH_SIZE, ActionStatus } from './types.js'
36
+
37
+ // Memory Provider
38
+ export { MemoryProvider, createMemoryProvider } from './memory-provider.js'
39
+
40
+ // Linguistic utilities
41
+ export { deriveNoun, deriveVerb, pluralize, singularize } from './linguistic.js'
42
+
43
+ // NS Client (for HTTP access to NS Durable Object)
44
+ export { NSClient, createNSClient } from './ns-client.js'
45
+ export type { NSClientOptions } from './ns-client.js'
46
+
47
+ // R2 Persistence (backup/restore to Cloudflare R2)
48
+ export {
49
+ createSnapshot,
50
+ restoreSnapshot,
51
+ appendWAL,
52
+ replayWAL,
53
+ compactWAL,
54
+ exportJSONL,
55
+ importJSONL,
56
+ exportToR2,
57
+ importFromR2,
58
+ } from './r2-persistence.js'
59
+ export type { Snapshot, WALEntry, SnapshotOptions, SnapshotResult } from './r2-persistence.js'
60
+
61
+ // ai-database Adapter
62
+ export { createDBProviderAdapter } from './ai-database-adapter.js'
63
+ export type {
64
+ DBProvider,
65
+ ListOptions as DBListOptions,
66
+ SearchOptions,
67
+ SemanticSearchOptions,
68
+ HybridSearchOptions,
69
+ } from './ai-database-adapter.js'
70
+
71
+ // Errors
72
+ export {
73
+ DigitalObjectsError,
74
+ NotFoundError,
75
+ ValidationError,
76
+ ConflictError,
77
+ errorToResponse,
78
+ } from './errors.js'
79
+
80
+ // Schema Validation
81
+ export { validateOnly, validateData } from './schema-validation.js'
82
+ export type {
83
+ SchemaValidationError,
84
+ ValidationErrorCode,
85
+ ValidationResult,
86
+ ValidationOptions,
87
+ } from './schema-validation.js'