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/src/ns-client.ts
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NS Client - HTTP client wrapper for NS Durable Object
|
|
3
|
+
*
|
|
4
|
+
* Allows using NS like a regular DigitalObjectsProvider via HTTP.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
DigitalObjectsProvider,
|
|
9
|
+
Noun,
|
|
10
|
+
NounDefinition,
|
|
11
|
+
Verb,
|
|
12
|
+
VerbDefinition,
|
|
13
|
+
Thing,
|
|
14
|
+
Action,
|
|
15
|
+
ListOptions,
|
|
16
|
+
ActionOptions,
|
|
17
|
+
} from './types.js'
|
|
18
|
+
import { NotFoundError, ServerError, NetworkError } from './errors.js'
|
|
19
|
+
|
|
20
|
+
export interface NSClientOptions {
|
|
21
|
+
/** Base URL of the NS worker */
|
|
22
|
+
baseUrl: string
|
|
23
|
+
/** Namespace ID */
|
|
24
|
+
namespace?: string
|
|
25
|
+
/** Custom fetch function (useful for Workers Service Bindings) */
|
|
26
|
+
fetch?: typeof fetch
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* NSClient - Class-based HTTP client for NS Durable Object
|
|
31
|
+
*
|
|
32
|
+
* Provides a DigitalObjectsProvider interface over HTTP, allowing
|
|
33
|
+
* Workers to communicate with NS Durable Objects via fetch.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* // From a Cloudflare Worker
|
|
38
|
+
* const client = new NSClient({
|
|
39
|
+
* baseUrl: 'https://ns.example.com',
|
|
40
|
+
* namespace: 'my-tenant',
|
|
41
|
+
* })
|
|
42
|
+
*
|
|
43
|
+
* const post = await client.create('Post', { title: 'Hello' })
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* // With Service Binding
|
|
49
|
+
* const client = new NSClient({
|
|
50
|
+
* baseUrl: 'http://ns', // Service binding name
|
|
51
|
+
* namespace: 'my-tenant',
|
|
52
|
+
* fetch: env.NS.fetch.bind(env.NS), // Service binding fetch
|
|
53
|
+
* })
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export class NSClient implements DigitalObjectsProvider {
|
|
57
|
+
private readonly baseUrl: string
|
|
58
|
+
private readonly namespace: string
|
|
59
|
+
private readonly fetchFn: typeof fetch
|
|
60
|
+
|
|
61
|
+
constructor(options: NSClientOptions) {
|
|
62
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, '')
|
|
63
|
+
this.namespace = options.namespace ?? 'default'
|
|
64
|
+
this.fetchFn = options.fetch ?? fetch
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private url(path: string, extraParams?: URLSearchParams): string {
|
|
68
|
+
const params = new URLSearchParams()
|
|
69
|
+
params.set('ns', this.namespace)
|
|
70
|
+
if (extraParams) {
|
|
71
|
+
extraParams.forEach((value, key) => params.set(key, value))
|
|
72
|
+
}
|
|
73
|
+
return `${this.baseUrl}${path}?${params}`
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private async request<T>(
|
|
77
|
+
path: string,
|
|
78
|
+
init?: RequestInit,
|
|
79
|
+
extraParams?: URLSearchParams
|
|
80
|
+
): Promise<T> {
|
|
81
|
+
let res: Response
|
|
82
|
+
try {
|
|
83
|
+
res = await this.fetchFn(this.url(path, extraParams), {
|
|
84
|
+
...init,
|
|
85
|
+
headers: {
|
|
86
|
+
'Content-Type': 'application/json',
|
|
87
|
+
...init?.headers,
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
} catch (error) {
|
|
91
|
+
// Network errors (connection refused, timeout, etc.)
|
|
92
|
+
const message = error instanceof Error ? error.message : 'Network request failed'
|
|
93
|
+
throw new NetworkError(message, error instanceof Error ? error : undefined)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!res.ok) {
|
|
97
|
+
const text = await res.text()
|
|
98
|
+
|
|
99
|
+
// 404 - Not Found
|
|
100
|
+
if (res.status === 404) {
|
|
101
|
+
throw new NotFoundError('Resource', path)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 5xx - Server errors
|
|
105
|
+
if (res.status >= 500) {
|
|
106
|
+
throw new ServerError(`NS request failed: ${res.status} ${text}`, res.status)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Other HTTP errors (4xx except 404)
|
|
110
|
+
throw new ServerError(`NS request failed: ${res.status} ${text}`, res.status)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return res.json() as Promise<T>
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ==================== Nouns ====================
|
|
117
|
+
|
|
118
|
+
async defineNoun(def: NounDefinition): Promise<Noun> {
|
|
119
|
+
return this.request('/nouns', {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
body: JSON.stringify(def),
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async getNoun(name: string): Promise<Noun | null> {
|
|
126
|
+
try {
|
|
127
|
+
return await this.request(`/nouns/${encodeURIComponent(name)}`)
|
|
128
|
+
} catch (error) {
|
|
129
|
+
if (error instanceof NotFoundError) {
|
|
130
|
+
return null
|
|
131
|
+
}
|
|
132
|
+
throw error
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async listNouns(): Promise<Noun[]> {
|
|
137
|
+
return this.request('/nouns')
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ==================== Verbs ====================
|
|
141
|
+
|
|
142
|
+
async defineVerb(def: VerbDefinition): Promise<Verb> {
|
|
143
|
+
return this.request('/verbs', {
|
|
144
|
+
method: 'POST',
|
|
145
|
+
body: JSON.stringify(def),
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async getVerb(name: string): Promise<Verb | null> {
|
|
150
|
+
try {
|
|
151
|
+
return await this.request(`/verbs/${encodeURIComponent(name)}`)
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (error instanceof NotFoundError) {
|
|
154
|
+
return null
|
|
155
|
+
}
|
|
156
|
+
throw error
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async listVerbs(): Promise<Verb[]> {
|
|
161
|
+
return this.request('/verbs')
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ==================== Things ====================
|
|
165
|
+
|
|
166
|
+
async create<T>(noun: string, data: T, id?: string): Promise<Thing<T>> {
|
|
167
|
+
return this.request('/things', {
|
|
168
|
+
method: 'POST',
|
|
169
|
+
body: JSON.stringify({ noun, data, id }),
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async get<T>(id: string): Promise<Thing<T> | null> {
|
|
174
|
+
try {
|
|
175
|
+
return await this.request(`/things/${encodeURIComponent(id)}`)
|
|
176
|
+
} catch (error) {
|
|
177
|
+
if (error instanceof NotFoundError) {
|
|
178
|
+
return null
|
|
179
|
+
}
|
|
180
|
+
throw error
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async list<T>(noun: string, options?: ListOptions): Promise<Thing<T>[]> {
|
|
185
|
+
const params = new URLSearchParams({ noun })
|
|
186
|
+
if (options?.limit) params.set('limit', String(options.limit))
|
|
187
|
+
if (options?.offset) params.set('offset', String(options.offset))
|
|
188
|
+
if (options?.orderBy) params.set('orderBy', options.orderBy)
|
|
189
|
+
if (options?.order) params.set('order', options.order)
|
|
190
|
+
|
|
191
|
+
const results = await this.request<Thing<T>[]>('/things', undefined, params)
|
|
192
|
+
|
|
193
|
+
// Apply where filter client-side (NS server doesn't support where in URL)
|
|
194
|
+
if (options?.where) {
|
|
195
|
+
return results.filter((thing) => {
|
|
196
|
+
for (const [key, value] of Object.entries(options.where!)) {
|
|
197
|
+
if ((thing.data as Record<string, unknown>)[key] !== value) {
|
|
198
|
+
return false
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return true
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return results
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async find<T>(noun: string, where: Partial<T>): Promise<Thing<T>[]> {
|
|
209
|
+
return this.list<T>(noun, { where: where as Record<string, unknown> })
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async update<T>(id: string, data: Partial<T>): Promise<Thing<T>> {
|
|
213
|
+
return this.request(`/things/${encodeURIComponent(id)}`, {
|
|
214
|
+
method: 'PATCH',
|
|
215
|
+
body: JSON.stringify(data),
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async delete(id: string): Promise<boolean> {
|
|
220
|
+
const result = await this.request<{ deleted: boolean }>(`/things/${encodeURIComponent(id)}`, {
|
|
221
|
+
method: 'DELETE',
|
|
222
|
+
})
|
|
223
|
+
return result.deleted
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async search<T>(query: string, options?: ListOptions): Promise<Thing<T>[]> {
|
|
227
|
+
const params = new URLSearchParams({ q: query })
|
|
228
|
+
if (options?.limit) params.set('limit', String(options.limit))
|
|
229
|
+
return this.request('/search', undefined, params)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ==================== Actions ====================
|
|
233
|
+
|
|
234
|
+
async perform<T>(verb: string, subject?: string, object?: string, data?: T): Promise<Action<T>> {
|
|
235
|
+
return this.request('/actions', {
|
|
236
|
+
method: 'POST',
|
|
237
|
+
body: JSON.stringify({ verb, subject, object, data }),
|
|
238
|
+
})
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async getAction<T>(id: string): Promise<Action<T> | null> {
|
|
242
|
+
try {
|
|
243
|
+
return await this.request(`/actions/${encodeURIComponent(id)}`)
|
|
244
|
+
} catch (error) {
|
|
245
|
+
if (error instanceof NotFoundError) {
|
|
246
|
+
return null
|
|
247
|
+
}
|
|
248
|
+
throw error
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async listActions<T>(options?: ActionOptions): Promise<Action<T>[]> {
|
|
253
|
+
const params = new URLSearchParams()
|
|
254
|
+
if (options?.verb) params.set('verb', options.verb)
|
|
255
|
+
if (options?.subject) params.set('subject', options.subject)
|
|
256
|
+
if (options?.object) params.set('object', options.object)
|
|
257
|
+
if (options?.limit) params.set('limit', String(options.limit))
|
|
258
|
+
if (options?.status) {
|
|
259
|
+
const status = Array.isArray(options.status) ? options.status[0] : options.status
|
|
260
|
+
if (status) params.set('status', status)
|
|
261
|
+
}
|
|
262
|
+
return this.request('/actions', undefined, params)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async deleteAction(id: string): Promise<boolean> {
|
|
266
|
+
const result = await this.request<{ deleted: boolean }>(`/actions/${encodeURIComponent(id)}`, {
|
|
267
|
+
method: 'DELETE',
|
|
268
|
+
})
|
|
269
|
+
return result.deleted
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ==================== Graph Traversal ====================
|
|
273
|
+
|
|
274
|
+
async related<T>(
|
|
275
|
+
id: string,
|
|
276
|
+
verb?: string,
|
|
277
|
+
direction?: 'out' | 'in' | 'both'
|
|
278
|
+
): Promise<Thing<T>[]> {
|
|
279
|
+
const params = new URLSearchParams()
|
|
280
|
+
if (verb) params.set('verb', verb)
|
|
281
|
+
if (direction) params.set('direction', direction)
|
|
282
|
+
return this.request(`/related/${encodeURIComponent(id)}`, undefined, params)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async edges<T>(
|
|
286
|
+
id: string,
|
|
287
|
+
verb?: string,
|
|
288
|
+
direction?: 'out' | 'in' | 'both'
|
|
289
|
+
): Promise<Action<T>[]> {
|
|
290
|
+
const params = new URLSearchParams()
|
|
291
|
+
if (verb) params.set('verb', verb)
|
|
292
|
+
if (direction) params.set('direction', direction)
|
|
293
|
+
return this.request(`/edges/${encodeURIComponent(id)}`, undefined, params)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ==================== Batch Operations ====================
|
|
297
|
+
|
|
298
|
+
async createMany<T>(noun: string, items: T[]): Promise<Thing<T>[]> {
|
|
299
|
+
return this.request('/batch/things', {
|
|
300
|
+
method: 'POST',
|
|
301
|
+
body: JSON.stringify({ noun, items }),
|
|
302
|
+
})
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async updateMany<T>(updates: Array<{ id: string; data: Partial<T> }>): Promise<Thing<T>[]> {
|
|
306
|
+
return this.request('/batch/things', {
|
|
307
|
+
method: 'PATCH',
|
|
308
|
+
body: JSON.stringify({ updates }),
|
|
309
|
+
})
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async deleteMany(ids: string[]): Promise<boolean[]> {
|
|
313
|
+
return this.request('/batch/things', {
|
|
314
|
+
method: 'DELETE',
|
|
315
|
+
body: JSON.stringify({ ids }),
|
|
316
|
+
})
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async performMany<T>(
|
|
320
|
+
actions: Array<{ verb: string; subject?: string; object?: string; data?: T }>
|
|
321
|
+
): Promise<Action<T>[]> {
|
|
322
|
+
return this.request('/batch/actions', {
|
|
323
|
+
method: 'POST',
|
|
324
|
+
body: JSON.stringify({ actions }),
|
|
325
|
+
})
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ==================== Lifecycle ====================
|
|
329
|
+
|
|
330
|
+
async close(): Promise<void> {
|
|
331
|
+
// No-op for HTTP client
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Create an NS client that talks to the Durable Object via HTTP
|
|
337
|
+
*
|
|
338
|
+
* @deprecated Use `new NSClient(options)` instead for better TypeScript support
|
|
339
|
+
*/
|
|
340
|
+
export function createNSClient(options: NSClientOptions): DigitalObjectsProvider {
|
|
341
|
+
return new NSClient(options)
|
|
342
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NS - Namespace Durable Object
|
|
3
|
+
*
|
|
4
|
+
* Import from 'digital-objects/ns' when deploying to Cloudflare Workers.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* // wrangler.jsonc
|
|
9
|
+
* {
|
|
10
|
+
* "durable_objects": {
|
|
11
|
+
* "bindings": [{ "name": "NS", "class_name": "NS" }]
|
|
12
|
+
* }
|
|
13
|
+
* }
|
|
14
|
+
*
|
|
15
|
+
* // worker.ts
|
|
16
|
+
* export { NS } from 'digital-objects/ns'
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export { NS } from './ns.js'
|
|
21
|
+
export type { Env } from './ns.js'
|
|
22
|
+
export { createNSClient } from './ns-client.js'
|
|
23
|
+
export type { NSClientOptions } from './ns-client.js'
|