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,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'