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,263 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest'
2
+ import type { R2Bucket, R2Object, R2ObjectBody } from '@cloudflare/workers-types'
3
+ import { createMemoryProvider } from './memory-provider.js'
4
+ import type { DigitalObjectsProvider } from './types.js'
5
+
6
+ // Mock R2 bucket for testing
7
+ function createMockR2Bucket(): R2Bucket & { storage: Map<string, string> } {
8
+ const storage = new Map<string, string>()
9
+
10
+ return {
11
+ storage,
12
+ async put(key: string, value: string | ArrayBuffer | ReadableStream) {
13
+ const content = typeof value === 'string' ? value : JSON.stringify(value)
14
+ storage.set(key, content)
15
+ return {
16
+ key,
17
+ version: '1',
18
+ size: content.length,
19
+ etag: 'mock-etag',
20
+ httpEtag: '"mock-etag"',
21
+ checksums: {},
22
+ uploaded: new Date(),
23
+ httpMetadata: {},
24
+ customMetadata: {},
25
+ writeHttpMetadata: () => {},
26
+ } as R2Object
27
+ },
28
+ async get(key: string) {
29
+ const content = storage.get(key)
30
+ if (!content) return null
31
+ return {
32
+ key,
33
+ version: '1',
34
+ size: content.length,
35
+ etag: 'mock-etag',
36
+ httpEtag: '"mock-etag"',
37
+ checksums: {},
38
+ uploaded: new Date(),
39
+ httpMetadata: {},
40
+ customMetadata: {},
41
+ body: new ReadableStream(),
42
+ bodyUsed: false,
43
+ arrayBuffer: async () => new TextEncoder().encode(content).buffer,
44
+ text: async () => content,
45
+ json: async () => JSON.parse(content),
46
+ blob: async () => new Blob([content]),
47
+ writeHttpMetadata: () => {},
48
+ } as R2ObjectBody
49
+ },
50
+ async delete(key: string | string[]) {
51
+ const keys = Array.isArray(key) ? key : [key]
52
+ for (const k of keys) {
53
+ storage.delete(k)
54
+ }
55
+ },
56
+ async list(options?: { prefix?: string }) {
57
+ const keys = Array.from(storage.keys())
58
+ .filter((k) => !options?.prefix || k.startsWith(options.prefix))
59
+ .map((key) => ({
60
+ key,
61
+ version: '1',
62
+ size: storage.get(key)?.length ?? 0,
63
+ etag: 'mock-etag',
64
+ httpEtag: '"mock-etag"',
65
+ checksums: {},
66
+ uploaded: new Date(),
67
+ httpMetadata: {},
68
+ customMetadata: {},
69
+ }))
70
+ return {
71
+ objects: keys,
72
+ truncated: false,
73
+ cursor: undefined,
74
+ delimitedPrefixes: [],
75
+ }
76
+ },
77
+ async head(key: string) {
78
+ if (!storage.has(key)) return null
79
+ return {
80
+ key,
81
+ version: '1',
82
+ size: storage.get(key)?.length ?? 0,
83
+ etag: 'mock-etag',
84
+ httpEtag: '"mock-etag"',
85
+ checksums: {},
86
+ uploaded: new Date(),
87
+ httpMetadata: {},
88
+ customMetadata: {},
89
+ writeHttpMetadata: () => {},
90
+ } as R2Object
91
+ },
92
+ createMultipartUpload: vi.fn(),
93
+ resumeMultipartUpload: vi.fn(),
94
+ } as unknown as R2Bucket & { storage: Map<string, string> }
95
+ }
96
+
97
+ // Import the persistence functions
98
+ import {
99
+ createSnapshot,
100
+ restoreSnapshot,
101
+ appendWAL,
102
+ replayWAL,
103
+ compactWAL,
104
+ exportJSONL,
105
+ importJSONL,
106
+ exportToR2,
107
+ importFromR2,
108
+ } from './r2-persistence.js'
109
+
110
+ describe('R2 Persistence', () => {
111
+ let provider: DigitalObjectsProvider
112
+ let r2: R2Bucket & { storage: Map<string, string> }
113
+
114
+ beforeEach(async () => {
115
+ provider = createMemoryProvider()
116
+ r2 = createMockR2Bucket()
117
+
118
+ // Set up some test data
119
+ await provider.defineNoun({ name: 'Post' })
120
+ await provider.defineNoun({ name: 'Author' })
121
+ await provider.defineVerb({ name: 'write' })
122
+ await provider.defineVerb({ name: 'publish' })
123
+
124
+ const author = await provider.create('Author', { name: 'Alice' })
125
+ const post1 = await provider.create('Post', { title: 'First Post', status: 'published' })
126
+ const post2 = await provider.create('Post', { title: 'Second Post', status: 'draft' })
127
+
128
+ await provider.perform('write', author.id, post1.id)
129
+ await provider.perform('write', author.id, post2.id)
130
+ await provider.perform('publish', undefined, post1.id)
131
+ })
132
+
133
+ describe('Snapshot', () => {
134
+ it('should create a complete snapshot', async () => {
135
+ const snapshot = await createSnapshot(provider, r2, 'test-ns')
136
+
137
+ expect(r2.storage.has('snapshots/test-ns/latest.json')).toBe(true)
138
+ const stored = JSON.parse(r2.storage.get('snapshots/test-ns/latest.json')!)
139
+ expect(stored.nouns).toHaveLength(2)
140
+ expect(stored.verbs).toHaveLength(2)
141
+ expect(stored.things).toHaveLength(3)
142
+ expect(stored.actions).toHaveLength(3)
143
+ })
144
+
145
+ it('should restore from snapshot', async () => {
146
+ await createSnapshot(provider, r2, 'test-ns')
147
+
148
+ // Create fresh provider
149
+ const newProvider = createMemoryProvider()
150
+ await restoreSnapshot(newProvider, r2, 'test-ns')
151
+
152
+ const nouns = await newProvider.listNouns()
153
+ expect(nouns).toHaveLength(2)
154
+
155
+ const posts = await newProvider.list('Post')
156
+ expect(posts).toHaveLength(2)
157
+ })
158
+
159
+ it('should create timestamped snapshots', async () => {
160
+ const result = await createSnapshot(provider, r2, 'test-ns', { timestamp: true })
161
+
162
+ expect(result.key).toMatch(/snapshots\/test-ns\/\d+\.json/)
163
+ })
164
+ })
165
+
166
+ describe('WAL (Write-Ahead Log)', () => {
167
+ it('should append operations to WAL', async () => {
168
+ await appendWAL(r2, 'test-ns', {
169
+ type: 'create',
170
+ noun: 'Post',
171
+ id: 'test-id',
172
+ data: { title: 'New Post' },
173
+ timestamp: Date.now(),
174
+ })
175
+
176
+ const walFiles = await r2.list({ prefix: 'wal/test-ns/' })
177
+ expect(walFiles.objects.length).toBeGreaterThan(0)
178
+ })
179
+
180
+ it('should replay WAL on top of snapshot', async () => {
181
+ // Create snapshot
182
+ await createSnapshot(provider, r2, 'test-ns')
183
+
184
+ // Append some operations
185
+ const timestamp = Date.now()
186
+ await appendWAL(r2, 'test-ns', {
187
+ type: 'create',
188
+ noun: 'Post',
189
+ id: 'wal-post-id',
190
+ data: { title: 'WAL Post' },
191
+ timestamp,
192
+ })
193
+
194
+ // Restore snapshot first, then replay WAL on fresh provider
195
+ const newProvider = createMemoryProvider()
196
+ await restoreSnapshot(newProvider, r2, 'test-ns')
197
+ await replayWAL(newProvider, r2, 'test-ns')
198
+
199
+ const posts = await newProvider.list('Post')
200
+ expect(posts).toHaveLength(3) // 2 from snapshot + 1 from WAL
201
+ })
202
+
203
+ it('should compact WAL after snapshot', async () => {
204
+ // Add multiple WAL entries
205
+ for (let i = 0; i < 10; i++) {
206
+ await appendWAL(r2, 'test-ns', {
207
+ type: 'create',
208
+ noun: 'Post',
209
+ id: `post-${i}`,
210
+ data: { title: `Post ${i}` },
211
+ timestamp: Date.now() + i,
212
+ })
213
+ }
214
+
215
+ // Compact (create snapshot + delete old WAL)
216
+ await createSnapshot(provider, r2, 'test-ns')
217
+ await compactWAL(r2, 'test-ns')
218
+
219
+ const walFiles = await r2.list({ prefix: 'wal/test-ns/' })
220
+ expect(walFiles.objects).toHaveLength(0)
221
+ })
222
+ })
223
+
224
+ describe('JSONL Export/Import', () => {
225
+ it('should export all data as JSONL', async () => {
226
+ const jsonl = await exportJSONL(provider)
227
+
228
+ const lines = jsonl.trim().split('\n')
229
+ expect(lines.length).toBe(10) // 2 nouns + 2 verbs + 3 things + 3 actions
230
+
231
+ // Each line should be valid JSON
232
+ for (const line of lines) {
233
+ expect(() => JSON.parse(line)).not.toThrow()
234
+ }
235
+ })
236
+
237
+ it('should import from JSONL', async () => {
238
+ const jsonl = await exportJSONL(provider)
239
+
240
+ const newProvider = createMemoryProvider()
241
+ await importJSONL(newProvider, jsonl)
242
+
243
+ const nouns = await newProvider.listNouns()
244
+ expect(nouns).toHaveLength(2)
245
+ })
246
+
247
+ it('should export to R2', async () => {
248
+ await exportToR2(provider, r2, 'exports/test-ns.jsonl')
249
+
250
+ expect(r2.storage.has('exports/test-ns.jsonl')).toBe(true)
251
+ })
252
+
253
+ it('should import from R2', async () => {
254
+ await exportToR2(provider, r2, 'exports/test-ns.jsonl')
255
+
256
+ const newProvider = createMemoryProvider()
257
+ await importFromR2(newProvider, r2, 'exports/test-ns.jsonl')
258
+
259
+ const things = await newProvider.list('Post')
260
+ expect(things).toHaveLength(2)
261
+ })
262
+ })
263
+ })
@@ -0,0 +1,367 @@
1
+ /**
2
+ * R2 Persistence Layer
3
+ *
4
+ * Provides backup, restore, and export functionality for digital-objects
5
+ * using Cloudflare R2 object storage.
6
+ */
7
+
8
+ /// <reference types="@cloudflare/workers-types" />
9
+
10
+ import type { DigitalObjectsProvider, Noun, Verb, Thing, Action } from './types.js'
11
+
12
+ /**
13
+ * Snapshot data structure
14
+ */
15
+ export interface Snapshot {
16
+ version: number
17
+ timestamp: number
18
+ namespace: string
19
+ nouns: Noun[]
20
+ verbs: Verb[]
21
+ things: Thing<unknown>[]
22
+ actions: Action<unknown>[]
23
+ }
24
+
25
+ /**
26
+ * WAL entry types
27
+ */
28
+ export type WALEntry =
29
+ | { type: 'defineNoun'; data: Noun; timestamp: number }
30
+ | { type: 'defineVerb'; data: Verb; timestamp: number }
31
+ | { type: 'create'; noun: string; id: string; data: unknown; timestamp: number }
32
+ | { type: 'update'; id: string; data: unknown; timestamp: number }
33
+ | { type: 'delete'; id: string; timestamp: number }
34
+ | {
35
+ type: 'perform'
36
+ verb: string
37
+ subject?: string
38
+ object?: string
39
+ data?: unknown
40
+ timestamp: number
41
+ }
42
+
43
+ export interface SnapshotOptions {
44
+ /** Include timestamp in filename */
45
+ timestamp?: boolean
46
+ }
47
+
48
+ export interface SnapshotResult {
49
+ key: string
50
+ size: number
51
+ timestamp: number
52
+ }
53
+
54
+ /**
55
+ * Create a complete snapshot of all data
56
+ */
57
+ export async function createSnapshot(
58
+ provider: DigitalObjectsProvider,
59
+ r2: R2Bucket,
60
+ namespace: string,
61
+ options?: SnapshotOptions
62
+ ): Promise<SnapshotResult> {
63
+ const timestamp = Date.now()
64
+
65
+ // Collect all data
66
+ const [nouns, verbs] = await Promise.all([provider.listNouns(), provider.listVerbs()])
67
+
68
+ // Collect things for each noun type
69
+ const things: Thing<unknown>[] = []
70
+ for (const noun of nouns) {
71
+ const nounThings = await provider.list(noun.name)
72
+ things.push(...nounThings)
73
+ }
74
+
75
+ // Collect all actions
76
+ const actions = await provider.listActions()
77
+
78
+ const snapshot: Snapshot = {
79
+ version: 1,
80
+ timestamp,
81
+ namespace,
82
+ nouns,
83
+ verbs,
84
+ things,
85
+ actions,
86
+ }
87
+
88
+ const content = JSON.stringify(snapshot, null, 2)
89
+ const key = options?.timestamp
90
+ ? `snapshots/${namespace}/${timestamp}.json`
91
+ : `snapshots/${namespace}/latest.json`
92
+
93
+ await r2.put(key, content)
94
+
95
+ return {
96
+ key,
97
+ size: content.length,
98
+ timestamp,
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Restore provider state from a snapshot
104
+ */
105
+ export async function restoreSnapshot(
106
+ provider: DigitalObjectsProvider,
107
+ r2: R2Bucket,
108
+ namespace: string,
109
+ snapshotKey?: string
110
+ ): Promise<void> {
111
+ const key = snapshotKey ?? `snapshots/${namespace}/latest.json`
112
+ const obj = await r2.get(key)
113
+
114
+ if (!obj) {
115
+ throw new Error(`Snapshot not found: ${key}`)
116
+ }
117
+
118
+ const snapshot = await obj.json<Snapshot>()
119
+
120
+ // Restore nouns
121
+ for (const noun of snapshot.nouns) {
122
+ await provider.defineNoun({
123
+ name: noun.name,
124
+ singular: noun.singular,
125
+ plural: noun.plural,
126
+ description: noun.description,
127
+ schema: noun.schema,
128
+ })
129
+ }
130
+
131
+ // Restore verbs
132
+ for (const verb of snapshot.verbs) {
133
+ await provider.defineVerb({
134
+ name: verb.name,
135
+ action: verb.action,
136
+ act: verb.act,
137
+ activity: verb.activity,
138
+ event: verb.event,
139
+ reverseBy: verb.reverseBy,
140
+ inverse: verb.inverse,
141
+ description: verb.description,
142
+ })
143
+ }
144
+
145
+ // Restore things
146
+ for (const thing of snapshot.things) {
147
+ await provider.create(thing.noun, thing.data, thing.id)
148
+ }
149
+
150
+ // Restore actions
151
+ for (const action of snapshot.actions) {
152
+ await provider.perform(action.verb, action.subject, action.object, action.data)
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Append an operation to the WAL
158
+ */
159
+ export async function appendWAL(r2: R2Bucket, namespace: string, entry: WALEntry): Promise<void> {
160
+ const key = `wal/${namespace}/${entry.timestamp}.json`
161
+ await r2.put(key, JSON.stringify(entry))
162
+ }
163
+
164
+ /**
165
+ * Replay WAL entries on top of current state
166
+ */
167
+ export async function replayWAL(
168
+ provider: DigitalObjectsProvider,
169
+ r2: R2Bucket,
170
+ namespace: string,
171
+ afterTimestamp?: number
172
+ ): Promise<number> {
173
+ const list = await r2.list({ prefix: `wal/${namespace}/` })
174
+
175
+ // Sort by timestamp (filename)
176
+ const entries = list.objects
177
+ .map((obj) => ({
178
+ key: obj.key,
179
+ timestamp: parseInt(obj.key.split('/').pop()?.replace('.json', '') ?? '0'),
180
+ }))
181
+ .filter((e) => !afterTimestamp || e.timestamp > afterTimestamp)
182
+ .sort((a, b) => a.timestamp - b.timestamp)
183
+
184
+ let replayed = 0
185
+
186
+ for (const { key } of entries) {
187
+ const obj = await r2.get(key)
188
+ if (!obj) continue
189
+
190
+ const entry = await obj.json<WALEntry>()
191
+
192
+ switch (entry.type) {
193
+ case 'defineNoun':
194
+ await provider.defineNoun(entry.data)
195
+ break
196
+ case 'defineVerb':
197
+ await provider.defineVerb(entry.data)
198
+ break
199
+ case 'create':
200
+ await provider.create(entry.noun, entry.data, entry.id)
201
+ break
202
+ case 'update':
203
+ await provider.update(entry.id, entry.data as Record<string, unknown>)
204
+ break
205
+ case 'delete':
206
+ await provider.delete(entry.id)
207
+ break
208
+ case 'perform':
209
+ await provider.perform(entry.verb, entry.subject, entry.object, entry.data)
210
+ break
211
+ }
212
+
213
+ replayed++
214
+ }
215
+
216
+ return replayed
217
+ }
218
+
219
+ /**
220
+ * Compact WAL by deleting entries older than the latest snapshot
221
+ */
222
+ export async function compactWAL(
223
+ r2: R2Bucket,
224
+ namespace: string,
225
+ beforeTimestamp?: number
226
+ ): Promise<number> {
227
+ const list = await r2.list({ prefix: `wal/${namespace}/` })
228
+
229
+ const toDelete = list.objects
230
+ .filter((obj) => {
231
+ if (!beforeTimestamp) return true
232
+ const ts = parseInt(obj.key.split('/').pop()?.replace('.json', '') ?? '0')
233
+ return ts < beforeTimestamp
234
+ })
235
+ .map((obj) => obj.key)
236
+
237
+ if (toDelete.length > 0) {
238
+ await r2.delete(toDelete)
239
+ }
240
+
241
+ return toDelete.length
242
+ }
243
+
244
+ /**
245
+ * Export all data as JSONL (JSON Lines) format
246
+ */
247
+ export async function exportJSONL(provider: DigitalObjectsProvider): Promise<string> {
248
+ const lines: string[] = []
249
+
250
+ // Export nouns
251
+ const nouns = await provider.listNouns()
252
+ for (const noun of nouns) {
253
+ lines.push(JSON.stringify({ type: 'noun', data: noun }))
254
+ }
255
+
256
+ // Export verbs
257
+ const verbs = await provider.listVerbs()
258
+ for (const verb of verbs) {
259
+ lines.push(JSON.stringify({ type: 'verb', data: verb }))
260
+ }
261
+
262
+ // Export things
263
+ for (const noun of nouns) {
264
+ const things = await provider.list(noun.name)
265
+ for (const thing of things) {
266
+ lines.push(JSON.stringify({ type: 'thing', data: thing }))
267
+ }
268
+ }
269
+
270
+ // Export actions
271
+ const actions = await provider.listActions()
272
+ for (const action of actions) {
273
+ lines.push(JSON.stringify({ type: 'action', data: action }))
274
+ }
275
+
276
+ return lines.join('\n')
277
+ }
278
+
279
+ /**
280
+ * Import data from JSONL format
281
+ */
282
+ export async function importJSONL(
283
+ provider: DigitalObjectsProvider,
284
+ jsonl: string
285
+ ): Promise<{ nouns: number; verbs: number; things: number; actions: number }> {
286
+ const stats = { nouns: 0, verbs: 0, things: 0, actions: 0 }
287
+
288
+ const lines = jsonl.trim().split('\n').filter(Boolean)
289
+
290
+ for (const line of lines) {
291
+ const entry = JSON.parse(line) as { type: string; data: unknown }
292
+
293
+ switch (entry.type) {
294
+ case 'noun': {
295
+ const noun = entry.data as Noun
296
+ await provider.defineNoun({
297
+ name: noun.name,
298
+ singular: noun.singular,
299
+ plural: noun.plural,
300
+ description: noun.description,
301
+ schema: noun.schema,
302
+ })
303
+ stats.nouns++
304
+ break
305
+ }
306
+ case 'verb': {
307
+ const verb = entry.data as Verb
308
+ await provider.defineVerb({
309
+ name: verb.name,
310
+ action: verb.action,
311
+ act: verb.act,
312
+ activity: verb.activity,
313
+ event: verb.event,
314
+ reverseBy: verb.reverseBy,
315
+ inverse: verb.inverse,
316
+ description: verb.description,
317
+ })
318
+ stats.verbs++
319
+ break
320
+ }
321
+ case 'thing': {
322
+ const thing = entry.data as Thing
323
+ await provider.create(thing.noun, thing.data, thing.id)
324
+ stats.things++
325
+ break
326
+ }
327
+ case 'action': {
328
+ const action = entry.data as Action
329
+ await provider.perform(action.verb, action.subject, action.object, action.data)
330
+ stats.actions++
331
+ break
332
+ }
333
+ }
334
+ }
335
+
336
+ return stats
337
+ }
338
+
339
+ /**
340
+ * Export to R2 as JSONL
341
+ */
342
+ export async function exportToR2(
343
+ provider: DigitalObjectsProvider,
344
+ r2: R2Bucket,
345
+ key: string
346
+ ): Promise<{ key: string; size: number }> {
347
+ const jsonl = await exportJSONL(provider)
348
+ await r2.put(key, jsonl)
349
+ return { key, size: jsonl.length }
350
+ }
351
+
352
+ /**
353
+ * Import from R2 JSONL file
354
+ */
355
+ export async function importFromR2(
356
+ provider: DigitalObjectsProvider,
357
+ r2: R2Bucket,
358
+ key: string
359
+ ): Promise<{ nouns: number; verbs: number; things: number; actions: number }> {
360
+ const obj = await r2.get(key)
361
+ if (!obj) {
362
+ throw new Error(`File not found: ${key}`)
363
+ }
364
+
365
+ const jsonl = await obj.text()
366
+ return importJSONL(provider, jsonl)
367
+ }