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
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [1.0.0] - 2026-01-15
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Core model: Nouns, Verbs, Things, Actions
|
|
7
|
+
- MemoryProvider for in-memory storage
|
|
8
|
+
- NS Durable Object with SQLite persistence
|
|
9
|
+
- NSClient HTTP client
|
|
10
|
+
- R2 persistence (snapshots, WAL, JSONL)
|
|
11
|
+
- ai-database adapter (DBProvider interface)
|
|
12
|
+
- Schema validation with opt-in validation
|
|
13
|
+
- Batch operations (createMany, updateMany, deleteMany, performMany)
|
|
14
|
+
- Graph traversal (related, edges)
|
|
15
|
+
- Linguistic derivation (pluralization, verb conjugation)
|
|
16
|
+
- Query limits (DEFAULT_LIMIT=100, MAX_LIMIT=1000)
|
|
17
|
+
- Custom error classes (NotFoundError, ValidationError, ConflictError)
|
|
18
|
+
|
|
19
|
+
### Security
|
|
20
|
+
- SQL injection prevention with orderBy validation
|
|
21
|
+
- GDPR compliance with deleteAction support
|
|
22
|
+
|
|
23
|
+
### Documentation
|
|
24
|
+
- Comprehensive MDX documentation in content/digital-objects/
|
|
25
|
+
- README with quick start and examples
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 .org.ai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
# digital-objects
|
|
2
|
+
|
|
3
|
+
Unified storage primitive for AI primitives - a linguistically-aware entity and graph system.
|
|
4
|
+
|
|
5
|
+
> **Documentation:** For comprehensive guides and examples, see the [Digital Objects documentation](https://primitives.org.ai/digital-objects).
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
`digital-objects` provides a coherent model for defining and managing entities (nouns/things) and relationships (verbs/actions). It automatically derives linguistic forms (pluralization, verb conjugation) and unifies events, graph edges, and audit trails into a single "action" concept.
|
|
10
|
+
|
|
11
|
+
## Core Concepts
|
|
12
|
+
|
|
13
|
+
### Nouns and Things
|
|
14
|
+
|
|
15
|
+
**Nouns** define entity types. **Things** are instances of nouns.
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { createMemoryProvider } from 'digital-objects'
|
|
19
|
+
|
|
20
|
+
const provider = createMemoryProvider()
|
|
21
|
+
|
|
22
|
+
// Define a noun (entity type)
|
|
23
|
+
const postNoun = await provider.defineNoun({
|
|
24
|
+
name: 'Post',
|
|
25
|
+
description: 'A blog post',
|
|
26
|
+
schema: {
|
|
27
|
+
title: 'string',
|
|
28
|
+
body: 'markdown',
|
|
29
|
+
publishedAt: 'datetime?',
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
// Linguistic forms are auto-derived:
|
|
34
|
+
// postNoun.singular = 'post'
|
|
35
|
+
// postNoun.plural = 'posts'
|
|
36
|
+
// postNoun.slug = 'post'
|
|
37
|
+
|
|
38
|
+
// Create a thing (entity instance)
|
|
39
|
+
const post = await provider.create('Post', {
|
|
40
|
+
title: 'Hello World',
|
|
41
|
+
body: '# Welcome\n\nThis is my first post.',
|
|
42
|
+
})
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Verbs and Actions
|
|
46
|
+
|
|
47
|
+
**Verbs** define action types. **Actions** represent events, graph edges, and audit records - unified.
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
// Define a verb
|
|
51
|
+
const publishVerb = await provider.defineVerb({
|
|
52
|
+
name: 'publish',
|
|
53
|
+
inverse: 'unpublish',
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
// Conjugations are auto-derived:
|
|
57
|
+
// publishVerb.action = 'publish' (imperative)
|
|
58
|
+
// publishVerb.act = 'publishes' (3rd person)
|
|
59
|
+
// publishVerb.activity = 'publishing' (gerund)
|
|
60
|
+
// publishVerb.event = 'published' (past participle)
|
|
61
|
+
// publishVerb.reverseBy = 'publishedBy'
|
|
62
|
+
// publishVerb.reverseAt = 'publishedAt'
|
|
63
|
+
|
|
64
|
+
// Perform an action (creates an event + edge)
|
|
65
|
+
const action = await provider.perform(
|
|
66
|
+
'publish', // verb
|
|
67
|
+
author.id, // subject (who/from)
|
|
68
|
+
post.id, // object (what/to)
|
|
69
|
+
{ featured: true } // metadata
|
|
70
|
+
)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Actions as Graph Edges
|
|
74
|
+
|
|
75
|
+
Actions form a directed graph. Use `related()` and `edges()` for traversal:
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
// Get all things this author has published
|
|
79
|
+
const publishedPosts = await provider.related(author.id, 'publish', 'out')
|
|
80
|
+
|
|
81
|
+
// Get who published this post
|
|
82
|
+
const publishers = await provider.related(post.id, 'publish', 'in')
|
|
83
|
+
|
|
84
|
+
// Get all relationships (both directions)
|
|
85
|
+
const allRelated = await provider.related(post.id, undefined, 'both')
|
|
86
|
+
|
|
87
|
+
// Get edge details (the actions themselves)
|
|
88
|
+
const publishActions = await provider.edges(author.id, 'publish', 'out')
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Linguistic Derivation
|
|
92
|
+
|
|
93
|
+
The package automatically derives linguistic forms from base words.
|
|
94
|
+
|
|
95
|
+
### Noun Derivation
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
import { deriveNoun, pluralize, singularize } from 'digital-objects'
|
|
99
|
+
|
|
100
|
+
deriveNoun('Post') // { singular: 'post', plural: 'posts', slug: 'post' }
|
|
101
|
+
deriveNoun('BlogPost') // { singular: 'blog post', plural: 'blog posts', slug: 'blog-post' }
|
|
102
|
+
deriveNoun('Person') // { singular: 'person', plural: 'people', slug: 'person' }
|
|
103
|
+
|
|
104
|
+
pluralize('category') // 'categories'
|
|
105
|
+
pluralize('child') // 'children'
|
|
106
|
+
singularize('posts') // 'post'
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Verb Derivation
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
import { deriveVerb } from 'digital-objects'
|
|
113
|
+
|
|
114
|
+
deriveVerb('create')
|
|
115
|
+
// {
|
|
116
|
+
// action: 'create',
|
|
117
|
+
// act: 'creates',
|
|
118
|
+
// activity: 'creating',
|
|
119
|
+
// event: 'created',
|
|
120
|
+
// reverseBy: 'createdBy',
|
|
121
|
+
// reverseAt: 'createdAt'
|
|
122
|
+
// }
|
|
123
|
+
|
|
124
|
+
deriveVerb('write')
|
|
125
|
+
// {
|
|
126
|
+
// action: 'write',
|
|
127
|
+
// act: 'writes',
|
|
128
|
+
// activity: 'writing',
|
|
129
|
+
// event: 'written', (irregular)
|
|
130
|
+
// reverseBy: 'writtenBy',
|
|
131
|
+
// reverseAt: 'writtenAt'
|
|
132
|
+
// }
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Providers
|
|
136
|
+
|
|
137
|
+
### MemoryProvider
|
|
138
|
+
|
|
139
|
+
In-memory implementation for testing and development:
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
import { createMemoryProvider, MemoryProvider } from 'digital-objects'
|
|
143
|
+
|
|
144
|
+
// Factory function
|
|
145
|
+
const provider = createMemoryProvider()
|
|
146
|
+
|
|
147
|
+
// Or instantiate directly
|
|
148
|
+
const provider = new MemoryProvider()
|
|
149
|
+
|
|
150
|
+
// Clean up
|
|
151
|
+
await provider.close()
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### NS (Namespace Durable Object)
|
|
155
|
+
|
|
156
|
+
SQLite-backed implementation for Cloudflare Workers with multi-tenant support:
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
// In wrangler.toml:
|
|
160
|
+
// [[durable_objects.bindings]]
|
|
161
|
+
// name = "NS"
|
|
162
|
+
// class_name = "NS"
|
|
163
|
+
|
|
164
|
+
// Import for Workers
|
|
165
|
+
import { NS } from 'digital-objects/ns'
|
|
166
|
+
|
|
167
|
+
// Access via HTTP
|
|
168
|
+
export default {
|
|
169
|
+
async fetch(request: Request, env: Env) {
|
|
170
|
+
const namespace = 'tenant-123'
|
|
171
|
+
const id = env.NS.idFromName(namespace)
|
|
172
|
+
const stub = env.NS.get(id)
|
|
173
|
+
return stub.fetch(request)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// HTTP API endpoints:
|
|
178
|
+
// POST /nouns - Define a noun
|
|
179
|
+
// GET /nouns/:name - Get a noun
|
|
180
|
+
// GET /nouns - List nouns
|
|
181
|
+
// POST /verbs - Define a verb
|
|
182
|
+
// POST /things - Create a thing
|
|
183
|
+
// GET /things/:id - Get a thing
|
|
184
|
+
// PATCH /things/:id - Update a thing
|
|
185
|
+
// DELETE /things/:id - Delete a thing
|
|
186
|
+
// POST /actions - Perform an action
|
|
187
|
+
// GET /related/:id - Get related things
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### NS Client
|
|
191
|
+
|
|
192
|
+
HTTP client for accessing NS from other services:
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
import { createNSClient } from 'digital-objects'
|
|
196
|
+
|
|
197
|
+
const client = createNSClient({
|
|
198
|
+
baseUrl: 'https://ns.example.com',
|
|
199
|
+
namespace: 'my-namespace',
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
const post = await client.create('Post', { title: 'Hello' })
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## R2 Persistence
|
|
206
|
+
|
|
207
|
+
Backup, restore, and export functionality using Cloudflare R2.
|
|
208
|
+
|
|
209
|
+
### Snapshots
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
import { createSnapshot, restoreSnapshot } from 'digital-objects'
|
|
213
|
+
|
|
214
|
+
// Create a full snapshot
|
|
215
|
+
const result = await createSnapshot(provider, r2, 'my-namespace', {
|
|
216
|
+
timestamp: true, // Include timestamp in filename
|
|
217
|
+
})
|
|
218
|
+
// result.key = 'snapshots/my-namespace/1234567890.json'
|
|
219
|
+
|
|
220
|
+
// Restore from snapshot
|
|
221
|
+
await restoreSnapshot(provider, r2, 'my-namespace')
|
|
222
|
+
// or specify a snapshot key:
|
|
223
|
+
await restoreSnapshot(provider, r2, 'my-namespace', 'snapshots/my-namespace/1234567890.json')
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Write-Ahead Log (WAL)
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
import { appendWAL, replayWAL, compactWAL } from 'digital-objects'
|
|
230
|
+
|
|
231
|
+
// Append operation to WAL
|
|
232
|
+
await appendWAL(r2, 'my-namespace', {
|
|
233
|
+
type: 'create',
|
|
234
|
+
noun: 'Post',
|
|
235
|
+
id: post.id,
|
|
236
|
+
data: post.data,
|
|
237
|
+
timestamp: Date.now(),
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
// Replay WAL entries
|
|
241
|
+
const replayed = await replayWAL(provider, r2, 'my-namespace', lastSnapshotTimestamp)
|
|
242
|
+
|
|
243
|
+
// Clean up old WAL entries
|
|
244
|
+
const deleted = await compactWAL(r2, 'my-namespace', snapshotTimestamp)
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### JSONL Export/Import
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
import { exportJSONL, importJSONL, exportToR2, importFromR2 } from 'digital-objects'
|
|
251
|
+
|
|
252
|
+
// Export to string
|
|
253
|
+
const jsonl = await exportJSONL(provider)
|
|
254
|
+
|
|
255
|
+
// Import from string
|
|
256
|
+
const stats = await importJSONL(provider, jsonl)
|
|
257
|
+
// stats = { nouns: 5, verbs: 3, things: 100, actions: 250 }
|
|
258
|
+
|
|
259
|
+
// Export directly to R2
|
|
260
|
+
await exportToR2(provider, r2, 'exports/backup.jsonl')
|
|
261
|
+
|
|
262
|
+
// Import directly from R2
|
|
263
|
+
await importFromR2(provider, r2, 'exports/backup.jsonl')
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## ai-database Adapter
|
|
267
|
+
|
|
268
|
+
Use digital-objects as a storage backend for ai-database:
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
import { createMemoryProvider, createDBProviderAdapter } from 'digital-objects'
|
|
272
|
+
|
|
273
|
+
const doProvider = createMemoryProvider()
|
|
274
|
+
const dbProvider = createDBProviderAdapter(doProvider)
|
|
275
|
+
|
|
276
|
+
// Now use ai-database's interface
|
|
277
|
+
const user = await dbProvider.create('User', undefined, {
|
|
278
|
+
name: 'Alice',
|
|
279
|
+
email: 'alice@example.com',
|
|
280
|
+
})
|
|
281
|
+
// Returns: { $id: 'uuid', $type: 'User', name: 'Alice', email: '...' }
|
|
282
|
+
|
|
283
|
+
// Get by ID
|
|
284
|
+
const found = await dbProvider.get('User', user.$id)
|
|
285
|
+
|
|
286
|
+
// List with filters
|
|
287
|
+
const admins = await dbProvider.list('User', {
|
|
288
|
+
where: { role: 'admin' },
|
|
289
|
+
orderBy: 'name',
|
|
290
|
+
limit: 10,
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
// Create relationships
|
|
294
|
+
await dbProvider.relate('User', user.$id, 'author', 'Post', post.$id)
|
|
295
|
+
|
|
296
|
+
// Query relationships
|
|
297
|
+
const userPosts = await dbProvider.related('Post', user.$id, 'author')
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## Type Definitions
|
|
301
|
+
|
|
302
|
+
### Noun
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
interface Noun {
|
|
306
|
+
name: string // 'Post'
|
|
307
|
+
singular: string // 'post'
|
|
308
|
+
plural: string // 'posts'
|
|
309
|
+
slug: string // 'post' (URL-safe)
|
|
310
|
+
description?: string
|
|
311
|
+
schema?: Record<string, FieldDefinition>
|
|
312
|
+
createdAt: Date
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Verb
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
interface Verb {
|
|
320
|
+
name: string // 'create'
|
|
321
|
+
action: string // 'create' (imperative)
|
|
322
|
+
act: string // 'creates' (3rd person)
|
|
323
|
+
activity: string // 'creating' (gerund)
|
|
324
|
+
event: string // 'created' (past participle)
|
|
325
|
+
reverseBy?: string // 'createdBy'
|
|
326
|
+
reverseAt?: string // 'createdAt'
|
|
327
|
+
inverse?: string // 'delete'
|
|
328
|
+
description?: string
|
|
329
|
+
createdAt: Date
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### Thing
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
interface Thing<T = Record<string, unknown>> {
|
|
337
|
+
id: string
|
|
338
|
+
noun: string // References noun.name
|
|
339
|
+
data: T
|
|
340
|
+
createdAt: Date
|
|
341
|
+
updatedAt: Date
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### Action
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
interface Action<T = Record<string, unknown>> {
|
|
349
|
+
id: string
|
|
350
|
+
verb: string // References verb.name
|
|
351
|
+
subject?: string // Thing ID (actor/from)
|
|
352
|
+
object?: string // Thing ID (target/to)
|
|
353
|
+
data?: T // Payload/metadata
|
|
354
|
+
status: ActionStatus
|
|
355
|
+
createdAt: Date
|
|
356
|
+
completedAt?: Date
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
type ActionStatus = 'pending' | 'active' | 'completed' | 'failed' | 'cancelled'
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### Field Types
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
type FieldDefinition =
|
|
366
|
+
| 'string' | 'number' | 'boolean' | 'date' | 'datetime' | 'json' | 'markdown' | 'url'
|
|
367
|
+
| `${string}.${string}` // Relation: 'Author.posts'
|
|
368
|
+
| `[${string}.${string}]` // Array relation: '[Tag.posts]'
|
|
369
|
+
| `${PrimitiveType}?` // Optional field
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
## Schema Validation
|
|
373
|
+
|
|
374
|
+
Digital Objects provides runtime schema validation with clear, actionable error messages.
|
|
375
|
+
|
|
376
|
+
### Enable Validation
|
|
377
|
+
|
|
378
|
+
Validation is opt-in. Pass `{ validate: true }` to `create()` or `update()`:
|
|
379
|
+
|
|
380
|
+
```typescript
|
|
381
|
+
// Define a noun with schema
|
|
382
|
+
await provider.defineNoun({
|
|
383
|
+
name: 'User',
|
|
384
|
+
schema: {
|
|
385
|
+
email: { type: 'string', required: true },
|
|
386
|
+
age: 'number',
|
|
387
|
+
bio: 'string?', // Optional
|
|
388
|
+
},
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
// Validation enabled - will throw on errors
|
|
392
|
+
await provider.create('User', { name: 'Alice' }, undefined, { validate: true })
|
|
393
|
+
// Error: Validation failed (1 error):
|
|
394
|
+
// - Missing required field 'email'
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### Pre-flight Validation
|
|
398
|
+
|
|
399
|
+
Use `validateOnly()` to check data before attempting operations:
|
|
400
|
+
|
|
401
|
+
```typescript
|
|
402
|
+
import { validateOnly } from 'digital-objects'
|
|
403
|
+
|
|
404
|
+
const schema = {
|
|
405
|
+
email: { type: 'string', required: true },
|
|
406
|
+
age: 'number',
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const result = validateOnly({ age: '25' }, schema)
|
|
410
|
+
|
|
411
|
+
if (!result.valid) {
|
|
412
|
+
console.log('Errors:', result.errors)
|
|
413
|
+
// [
|
|
414
|
+
// {
|
|
415
|
+
// field: 'email',
|
|
416
|
+
// message: "Missing required field 'email'",
|
|
417
|
+
// code: 'REQUIRED_FIELD',
|
|
418
|
+
// expected: 'string',
|
|
419
|
+
// received: 'undefined'
|
|
420
|
+
// },
|
|
421
|
+
// {
|
|
422
|
+
// field: 'age',
|
|
423
|
+
// message: "Field 'age' has wrong type: expected number, got string",
|
|
424
|
+
// code: 'TYPE_MISMATCH',
|
|
425
|
+
// expected: 'number',
|
|
426
|
+
// received: 'string',
|
|
427
|
+
// suggestion: 'Convert to number: 25'
|
|
428
|
+
// }
|
|
429
|
+
// ]
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### Error Codes
|
|
434
|
+
|
|
435
|
+
| Code | Description |
|
|
436
|
+
|------|-------------|
|
|
437
|
+
| `REQUIRED_FIELD` | A required field is missing or null |
|
|
438
|
+
| `TYPE_MISMATCH` | Value has wrong type for the field |
|
|
439
|
+
| `INVALID_FORMAT` | Value format doesn't match expected pattern |
|
|
440
|
+
| `UNKNOWN_FIELD` | Field not defined in schema (future feature) |
|
|
441
|
+
|
|
442
|
+
### Suggestions
|
|
443
|
+
|
|
444
|
+
Validation errors include helpful suggestions when possible:
|
|
445
|
+
|
|
446
|
+
```typescript
|
|
447
|
+
// String that looks like a number
|
|
448
|
+
{ age: '25' }
|
|
449
|
+
// suggestion: "Convert to number: 25"
|
|
450
|
+
|
|
451
|
+
// Number when string expected
|
|
452
|
+
{ name: 42 }
|
|
453
|
+
// suggestion: 'Convert to string: "42"'
|
|
454
|
+
|
|
455
|
+
// String boolean
|
|
456
|
+
{ active: 'true' }
|
|
457
|
+
// suggestion: "Convert to boolean: true"
|
|
458
|
+
|
|
459
|
+
// Wrong type for array
|
|
460
|
+
{ tags: 'single-tag' }
|
|
461
|
+
// suggestion: "Wrap value in an array: [value]"
|
|
462
|
+
|
|
463
|
+
// Wrong type for date
|
|
464
|
+
{ createdAt: 1705312800000 }
|
|
465
|
+
// suggestion: "Provide a valid ISO 8601 date string"
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
## Installation
|
|
469
|
+
|
|
470
|
+
```bash
|
|
471
|
+
npm install digital-objects
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
## License
|
|
475
|
+
|
|
476
|
+
MIT
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
import type { DigitalObjectsProvider } from './types.js';
|
|
8
|
+
export interface ListOptions {
|
|
9
|
+
limit?: number;
|
|
10
|
+
offset?: number;
|
|
11
|
+
where?: Record<string, unknown>;
|
|
12
|
+
orderBy?: string;
|
|
13
|
+
order?: 'asc' | 'desc';
|
|
14
|
+
}
|
|
15
|
+
export interface SearchOptions extends ListOptions {
|
|
16
|
+
fields?: string[];
|
|
17
|
+
}
|
|
18
|
+
export interface SemanticSearchOptions extends SearchOptions {
|
|
19
|
+
embedding?: number[];
|
|
20
|
+
minScore?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface HybridSearchOptions extends SearchOptions {
|
|
23
|
+
semanticWeight?: number;
|
|
24
|
+
ftsWeight?: number;
|
|
25
|
+
minScore?: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* ai-database DBProvider interface (simplified)
|
|
29
|
+
*/
|
|
30
|
+
export interface DBProvider {
|
|
31
|
+
get(type: string, id: string): Promise<Record<string, unknown> | null>;
|
|
32
|
+
list(type: string, options?: ListOptions): Promise<Record<string, unknown>[]>;
|
|
33
|
+
search(type: string, query: string, options?: SearchOptions): Promise<Record<string, unknown>[]>;
|
|
34
|
+
create(type: string, id: string | undefined, data: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
35
|
+
update(type: string, id: string, data: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
36
|
+
delete(type: string, id: string): Promise<boolean>;
|
|
37
|
+
related(type: string, id: string, relation: string): Promise<Record<string, unknown>[]>;
|
|
38
|
+
relate(fromType: string, fromId: string, relation: string, toType: string, toId: string, metadata?: {
|
|
39
|
+
matchMode?: 'exact' | 'fuzzy';
|
|
40
|
+
similarity?: number;
|
|
41
|
+
matchedType?: string;
|
|
42
|
+
}): Promise<void>;
|
|
43
|
+
unrelate(fromType: string, fromId: string, relation: string, toType: string, toId: string): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Create a DBProvider adapter from a DigitalObjectsProvider
|
|
47
|
+
*/
|
|
48
|
+
export declare function createDBProviderAdapter(provider: DigitalObjectsProvider): DBProvider;
|
|
49
|
+
//# sourceMappingURL=ai-database-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-database-adapter.d.ts","sourceRoot":"","sources":["../src/ai-database-adapter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,sBAAsB,EAGvB,MAAM,YAAY,CAAA;AAGnB,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,KAAK,GAAG,MAAM,CAAA;CACvB;AAED,MAAM,WAAW,aAAc,SAAQ,WAAW;IAChD,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;CAClB;AAED,MAAM,WAAW,qBAAsB,SAAQ,aAAa;IAC1D,SAAS,CAAC,EAAE,MAAM,EAAE,CAAA;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,mBAAoB,SAAQ,aAAa;IACxD,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IAEzB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAAA;IACtE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAA;IAC7E,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAA;IAChG,MAAM,CACJ,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,GAAG,SAAS,EACtB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;IACnC,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;IACjG,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAGlD,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAA;IACvF,MAAM,CACJ,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,QAAQ,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,GACtF,OAAO,CAAC,IAAI,CAAC,CAAA;IAChB,QAAQ,CACN,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,IAAI,CAAC,CAAA;CACjB;AAuBD;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,sBAAsB,GAAG,UAAU,CAgGpF"}
|
|
@@ -0,0 +1,89 @@
|
|
|
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
|
+
* Convert Thing to entity format (with $id, $type)
|
|
9
|
+
*/
|
|
10
|
+
function thingToEntity(thing) {
|
|
11
|
+
return {
|
|
12
|
+
$id: thing.id,
|
|
13
|
+
$type: thing.noun,
|
|
14
|
+
...thing.data,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Extract data from entity (remove $id, $type)
|
|
19
|
+
*/
|
|
20
|
+
function entityToData(entity) {
|
|
21
|
+
const { $id, $type, ...data } = entity;
|
|
22
|
+
return data;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Create a DBProvider adapter from a DigitalObjectsProvider
|
|
26
|
+
*/
|
|
27
|
+
export function createDBProviderAdapter(provider) {
|
|
28
|
+
return {
|
|
29
|
+
async get(type, id) {
|
|
30
|
+
const thing = await provider.get(id);
|
|
31
|
+
if (!thing || thing.noun !== type)
|
|
32
|
+
return null;
|
|
33
|
+
return thingToEntity(thing);
|
|
34
|
+
},
|
|
35
|
+
async list(type, options) {
|
|
36
|
+
const things = await provider.list(type, options);
|
|
37
|
+
return things.map(thingToEntity);
|
|
38
|
+
},
|
|
39
|
+
async search(type, query, options) {
|
|
40
|
+
const things = await provider.search(query, { ...options, where: { ...options?.where } });
|
|
41
|
+
// Filter by type since search is global
|
|
42
|
+
return things.filter((t) => t.noun === type).map(thingToEntity);
|
|
43
|
+
},
|
|
44
|
+
async create(type, id, data) {
|
|
45
|
+
// Ensure noun is defined
|
|
46
|
+
const existingNoun = await provider.getNoun(type);
|
|
47
|
+
if (!existingNoun) {
|
|
48
|
+
await provider.defineNoun({ name: type });
|
|
49
|
+
}
|
|
50
|
+
const thing = await provider.create(type, entityToData(data), id);
|
|
51
|
+
return thingToEntity(thing);
|
|
52
|
+
},
|
|
53
|
+
async update(_type, id, data) {
|
|
54
|
+
const thing = await provider.update(id, entityToData(data));
|
|
55
|
+
return thingToEntity(thing);
|
|
56
|
+
},
|
|
57
|
+
async delete(_type, id) {
|
|
58
|
+
return provider.delete(id);
|
|
59
|
+
},
|
|
60
|
+
async related(type, id, relation) {
|
|
61
|
+
// ai-database expects related entities of a specific type via a relation
|
|
62
|
+
// digital-objects uses verb as the relation type
|
|
63
|
+
const things = await provider.related(id, relation, 'both');
|
|
64
|
+
// Filter by expected type
|
|
65
|
+
return things.filter((t) => t.noun === type).map(thingToEntity);
|
|
66
|
+
},
|
|
67
|
+
async relate(_fromType, fromId, relation, _toType, toId, metadata) {
|
|
68
|
+
// Ensure verb is defined
|
|
69
|
+
const existingVerb = await provider.getVerb(relation);
|
|
70
|
+
if (!existingVerb) {
|
|
71
|
+
await provider.defineVerb({ name: relation });
|
|
72
|
+
}
|
|
73
|
+
await provider.perform(relation, fromId, toId, metadata);
|
|
74
|
+
},
|
|
75
|
+
async unrelate(_fromType, fromId, relation, _toType, toId) {
|
|
76
|
+
// Find the action(s) matching this relation and delete them
|
|
77
|
+
const actions = await provider.listActions({
|
|
78
|
+
verb: relation,
|
|
79
|
+
subject: fromId,
|
|
80
|
+
object: toId,
|
|
81
|
+
});
|
|
82
|
+
// Delete all matching actions (for GDPR compliance)
|
|
83
|
+
for (const action of actions) {
|
|
84
|
+
await provider.deleteAction(action.id);
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=ai-database-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-database-adapter.js","sourceRoot":"","sources":["../src/ai-database-adapter.ts"],"names":[],"mappings":"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"}
|