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
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory implementation of DigitalObjectsProvider
|
|
3
|
+
*
|
|
4
|
+
* Used for testing and development. All data is stored in Maps.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
DigitalObjectsProvider,
|
|
9
|
+
Noun,
|
|
10
|
+
NounDefinition,
|
|
11
|
+
Verb,
|
|
12
|
+
VerbDefinition,
|
|
13
|
+
Thing,
|
|
14
|
+
Action,
|
|
15
|
+
ActionStatusType,
|
|
16
|
+
ListOptions,
|
|
17
|
+
ActionOptions,
|
|
18
|
+
ValidationOptions,
|
|
19
|
+
Direction,
|
|
20
|
+
} from './types.js'
|
|
21
|
+
import {
|
|
22
|
+
DEFAULT_LIMIT,
|
|
23
|
+
MAX_LIMIT,
|
|
24
|
+
MAX_BATCH_SIZE,
|
|
25
|
+
validateDirection,
|
|
26
|
+
ActionStatus,
|
|
27
|
+
} from './types.js'
|
|
28
|
+
import { deriveNoun, deriveVerb } from './linguistic.js'
|
|
29
|
+
import { validateData } from './schema-validation.js'
|
|
30
|
+
import { NotFoundError, ValidationError } from './errors.js'
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Calculate effective limit with safety bounds
|
|
34
|
+
*/
|
|
35
|
+
function effectiveLimit(requestedLimit?: number): number {
|
|
36
|
+
return Math.min(requestedLimit ?? DEFAULT_LIMIT, MAX_LIMIT)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function generateId(): string {
|
|
40
|
+
return crypto.randomUUID()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Dangerous field names that could enable prototype pollution or other attacks
|
|
44
|
+
const DANGEROUS_FIELDS = ['__proto__', 'constructor', 'prototype']
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Validates a where clause field name to prevent JSON path traversal and prototype pollution.
|
|
48
|
+
* Throws ValidationError if the field name is invalid.
|
|
49
|
+
*
|
|
50
|
+
* Rejects:
|
|
51
|
+
* - Dots (.) in field names (JSON path traversal)
|
|
52
|
+
* - __proto__, constructor, prototype (prototype pollution)
|
|
53
|
+
* - Special JSON path characters ([, ], $, @)
|
|
54
|
+
*/
|
|
55
|
+
function validateWhereField(field: string): void {
|
|
56
|
+
// Check for dangerous prototype-related field names
|
|
57
|
+
if (DANGEROUS_FIELDS.includes(field)) {
|
|
58
|
+
throw new ValidationError(`Invalid where field: '${field}' is not allowed`, [
|
|
59
|
+
{ field, message: `Field name '${field}' is not allowed for security reasons` },
|
|
60
|
+
])
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Check for dots (JSON path traversal)
|
|
64
|
+
if (field.includes('.')) {
|
|
65
|
+
throw new ValidationError(`Invalid where field: '${field}' contains dots`, [
|
|
66
|
+
{ field, message: 'Field names cannot contain dots (JSON path traversal prevention)' },
|
|
67
|
+
])
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Check for special JSON path characters
|
|
71
|
+
if (/[\[\]$@]/.test(field)) {
|
|
72
|
+
throw new ValidationError(`Invalid where field: '${field}' contains special characters`, [
|
|
73
|
+
{ field, message: 'Field names cannot contain special JSON path characters ([, ], $, @)' },
|
|
74
|
+
])
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Must match valid identifier pattern (starts with letter or underscore, followed by alphanumeric or underscore)
|
|
78
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(field)) {
|
|
79
|
+
throw new ValidationError(`Invalid where field: '${field}'`, [
|
|
80
|
+
{
|
|
81
|
+
field,
|
|
82
|
+
message: 'Field name must be a valid identifier (letters, numbers, underscores only)',
|
|
83
|
+
},
|
|
84
|
+
])
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export class MemoryProvider implements DigitalObjectsProvider {
|
|
89
|
+
private nouns = new Map<string, Noun>()
|
|
90
|
+
private verbs = new Map<string, Verb>()
|
|
91
|
+
private things = new Map<string, Thing>()
|
|
92
|
+
private actions = new Map<string, Action>()
|
|
93
|
+
|
|
94
|
+
// ==================== Nouns ====================
|
|
95
|
+
|
|
96
|
+
async defineNoun(def: NounDefinition): Promise<Noun> {
|
|
97
|
+
const derived = deriveNoun(def.name)
|
|
98
|
+
const noun: Noun = {
|
|
99
|
+
name: def.name,
|
|
100
|
+
singular: def.singular ?? derived.singular,
|
|
101
|
+
plural: def.plural ?? derived.plural,
|
|
102
|
+
slug: derived.slug,
|
|
103
|
+
description: def.description,
|
|
104
|
+
schema: def.schema,
|
|
105
|
+
createdAt: new Date(),
|
|
106
|
+
}
|
|
107
|
+
this.nouns.set(noun.name, noun)
|
|
108
|
+
return noun
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async getNoun(name: string): Promise<Noun | null> {
|
|
112
|
+
return this.nouns.get(name) ?? null
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async listNouns(): Promise<Noun[]> {
|
|
116
|
+
return Array.from(this.nouns.values())
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ==================== Verbs ====================
|
|
120
|
+
|
|
121
|
+
async defineVerb(def: VerbDefinition): Promise<Verb> {
|
|
122
|
+
const derived = deriveVerb(def.name)
|
|
123
|
+
const verb: Verb = {
|
|
124
|
+
name: def.name,
|
|
125
|
+
action: def.action ?? derived.action,
|
|
126
|
+
act: def.act ?? derived.act,
|
|
127
|
+
activity: def.activity ?? derived.activity,
|
|
128
|
+
event: def.event ?? derived.event,
|
|
129
|
+
reverseBy: def.reverseBy ?? derived.reverseBy,
|
|
130
|
+
reverseAt: derived.reverseAt,
|
|
131
|
+
reverseIn: def.reverseIn ?? derived.reverseIn,
|
|
132
|
+
inverse: def.inverse,
|
|
133
|
+
description: def.description,
|
|
134
|
+
createdAt: new Date(),
|
|
135
|
+
}
|
|
136
|
+
this.verbs.set(verb.name, verb)
|
|
137
|
+
return verb
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async getVerb(name: string): Promise<Verb | null> {
|
|
141
|
+
return this.verbs.get(name) ?? null
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async listVerbs(): Promise<Verb[]> {
|
|
145
|
+
return Array.from(this.verbs.values())
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ==================== Things ====================
|
|
149
|
+
|
|
150
|
+
async create<T>(
|
|
151
|
+
noun: string,
|
|
152
|
+
data: T,
|
|
153
|
+
id?: string,
|
|
154
|
+
options?: ValidationOptions
|
|
155
|
+
): Promise<Thing<T>> {
|
|
156
|
+
// Validate data against noun schema if validation is enabled
|
|
157
|
+
if (options?.validate) {
|
|
158
|
+
const nounDef = this.nouns.get(noun)
|
|
159
|
+
validateData(data as Record<string, unknown>, nounDef?.schema, options)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const thing: Thing<T> = {
|
|
163
|
+
id: id ?? generateId(),
|
|
164
|
+
noun,
|
|
165
|
+
data,
|
|
166
|
+
createdAt: new Date(),
|
|
167
|
+
updatedAt: new Date(),
|
|
168
|
+
}
|
|
169
|
+
this.things.set(thing.id, thing as Thing)
|
|
170
|
+
return thing
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async get<T>(id: string): Promise<Thing<T> | null> {
|
|
174
|
+
return (this.things.get(id) as Thing<T>) ?? null
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Batch fetch multiple things by IDs
|
|
179
|
+
* More efficient than N individual get() calls
|
|
180
|
+
*/
|
|
181
|
+
async getMany<T>(ids: string[]): Promise<Thing<T>[]> {
|
|
182
|
+
if (ids.length === 0) return []
|
|
183
|
+
|
|
184
|
+
const results: Thing<T>[] = []
|
|
185
|
+
for (const id of ids) {
|
|
186
|
+
const thing = this.things.get(id)
|
|
187
|
+
if (thing) {
|
|
188
|
+
results.push(thing as Thing<T>)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return results
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async list<T>(noun: string, options?: ListOptions): Promise<Thing<T>[]> {
|
|
195
|
+
let results = Array.from(this.things.values()).filter((t) => t.noun === noun) as Thing<T>[]
|
|
196
|
+
|
|
197
|
+
if (options?.where) {
|
|
198
|
+
// Use Object.getOwnPropertyNames to also catch __proto__ which is not enumerable with Object.keys
|
|
199
|
+
const whereKeys = Object.getOwnPropertyNames(options.where)
|
|
200
|
+
// Validate all field names before filtering
|
|
201
|
+
for (const key of whereKeys) {
|
|
202
|
+
validateWhereField(key)
|
|
203
|
+
}
|
|
204
|
+
results = results.filter((t) => {
|
|
205
|
+
for (const key of whereKeys) {
|
|
206
|
+
const value = (options.where as Record<string, unknown>)[key]
|
|
207
|
+
if ((t.data as Record<string, unknown>)[key] !== value) {
|
|
208
|
+
return false
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return true
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (options?.orderBy) {
|
|
216
|
+
const key = options.orderBy
|
|
217
|
+
const dir = options.order === 'desc' ? -1 : 1
|
|
218
|
+
results.sort((a, b) => {
|
|
219
|
+
const aVal = (a.data as Record<string, unknown>)[key] as
|
|
220
|
+
| string
|
|
221
|
+
| number
|
|
222
|
+
| boolean
|
|
223
|
+
| null
|
|
224
|
+
| undefined
|
|
225
|
+
const bVal = (b.data as Record<string, unknown>)[key] as
|
|
226
|
+
| string
|
|
227
|
+
| number
|
|
228
|
+
| boolean
|
|
229
|
+
| null
|
|
230
|
+
| undefined
|
|
231
|
+
if (aVal == null && bVal == null) return 0
|
|
232
|
+
if (aVal == null) return 1 * dir
|
|
233
|
+
if (bVal == null) return -1 * dir
|
|
234
|
+
if (aVal < bVal) return -1 * dir
|
|
235
|
+
if (aVal > bVal) return 1 * dir
|
|
236
|
+
return 0
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (options?.offset) {
|
|
241
|
+
results = results.slice(options.offset)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Apply limit with safety bounds
|
|
245
|
+
const limit = effectiveLimit(options?.limit)
|
|
246
|
+
results = results.slice(0, limit)
|
|
247
|
+
|
|
248
|
+
return results
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async find<T>(noun: string, where: Partial<T>): Promise<Thing<T>[]> {
|
|
252
|
+
return this.list<T>(noun, { where: where as Record<string, unknown> })
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async update<T>(id: string, data: Partial<T>, options?: ValidationOptions): Promise<Thing<T>> {
|
|
256
|
+
const existing = this.things.get(id)
|
|
257
|
+
if (!existing) {
|
|
258
|
+
throw new NotFoundError('Thing', id)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Merge data for validation
|
|
262
|
+
const mergedData = { ...existing.data, ...data } as T
|
|
263
|
+
|
|
264
|
+
// Validate merged data against noun schema if validation is enabled
|
|
265
|
+
if (options?.validate) {
|
|
266
|
+
const nounDef = this.nouns.get(existing.noun)
|
|
267
|
+
validateData(mergedData as Record<string, unknown>, nounDef?.schema, options)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Ensure updatedAt is always strictly newer than createdAt
|
|
271
|
+
const now = new Date()
|
|
272
|
+
const updatedAt =
|
|
273
|
+
now.getTime() <= existing.createdAt.getTime()
|
|
274
|
+
? new Date(existing.createdAt.getTime() + 1)
|
|
275
|
+
: now
|
|
276
|
+
|
|
277
|
+
const updated: Thing<T> = {
|
|
278
|
+
...existing,
|
|
279
|
+
data: mergedData,
|
|
280
|
+
updatedAt,
|
|
281
|
+
}
|
|
282
|
+
this.things.set(id, updated as Thing)
|
|
283
|
+
return updated
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async delete(id: string): Promise<boolean> {
|
|
287
|
+
return this.things.delete(id)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async search<T>(query: string, options?: ListOptions): Promise<Thing<T>[]> {
|
|
291
|
+
const q = query.toLowerCase()
|
|
292
|
+
let results = Array.from(this.things.values()).filter((t) =>
|
|
293
|
+
JSON.stringify(t.data).toLowerCase().includes(q)
|
|
294
|
+
) as Thing<T>[]
|
|
295
|
+
|
|
296
|
+
// Apply limit with safety bounds
|
|
297
|
+
const limit = effectiveLimit(options?.limit)
|
|
298
|
+
results = results.slice(0, limit)
|
|
299
|
+
|
|
300
|
+
return results
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ==================== Actions ====================
|
|
304
|
+
|
|
305
|
+
async perform<T>(verb: string, subject?: string, object?: string, data?: T): Promise<Action<T>> {
|
|
306
|
+
const action: Action<T> = {
|
|
307
|
+
id: generateId(),
|
|
308
|
+
verb,
|
|
309
|
+
subject,
|
|
310
|
+
object,
|
|
311
|
+
data,
|
|
312
|
+
status: ActionStatus.COMPLETED,
|
|
313
|
+
createdAt: new Date(),
|
|
314
|
+
completedAt: new Date(),
|
|
315
|
+
}
|
|
316
|
+
this.actions.set(action.id, action as Action)
|
|
317
|
+
return action
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async getAction<T>(id: string): Promise<Action<T> | null> {
|
|
321
|
+
return (this.actions.get(id) as Action<T>) ?? null
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async listActions<T>(options?: ActionOptions): Promise<Action<T>[]> {
|
|
325
|
+
let results = Array.from(this.actions.values()) as Action<T>[]
|
|
326
|
+
|
|
327
|
+
if (options?.verb) {
|
|
328
|
+
results = results.filter((a) => a.verb === options.verb)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (options?.subject) {
|
|
332
|
+
results = results.filter((a) => a.subject === options.subject)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (options?.object) {
|
|
336
|
+
results = results.filter((a) => a.object === options.object)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (options?.status) {
|
|
340
|
+
const statuses = Array.isArray(options.status) ? options.status : [options.status]
|
|
341
|
+
results = results.filter((a) => statuses.includes(a.status))
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Apply limit with safety bounds
|
|
345
|
+
const limit = effectiveLimit(options?.limit)
|
|
346
|
+
results = results.slice(0, limit)
|
|
347
|
+
|
|
348
|
+
return results
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async deleteAction(id: string): Promise<boolean> {
|
|
352
|
+
return this.actions.delete(id)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ==================== Graph Traversal ====================
|
|
356
|
+
|
|
357
|
+
async related<T>(
|
|
358
|
+
id: string,
|
|
359
|
+
verb?: string,
|
|
360
|
+
direction: Direction = 'out',
|
|
361
|
+
options?: ListOptions
|
|
362
|
+
): Promise<Thing<T>[]> {
|
|
363
|
+
const validDirection = validateDirection(direction)
|
|
364
|
+
const edgesList = await this.edges(id, verb, validDirection)
|
|
365
|
+
const relatedIds = new Set<string>()
|
|
366
|
+
|
|
367
|
+
for (const edge of edgesList) {
|
|
368
|
+
if (direction === 'out' || direction === 'both') {
|
|
369
|
+
if (edge.subject === id && edge.object) {
|
|
370
|
+
relatedIds.add(edge.object)
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (direction === 'in' || direction === 'both') {
|
|
374
|
+
if (edge.object === id && edge.subject) {
|
|
375
|
+
relatedIds.add(edge.subject)
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Use batch query instead of N individual get() calls (fixes N+1 pattern)
|
|
381
|
+
let results = await this.getMany<T>([...relatedIds])
|
|
382
|
+
|
|
383
|
+
// Apply limit with safety bounds
|
|
384
|
+
const limit = effectiveLimit(options?.limit)
|
|
385
|
+
results = results.slice(0, limit)
|
|
386
|
+
|
|
387
|
+
return results
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async edges<T>(
|
|
391
|
+
id: string,
|
|
392
|
+
verb?: string,
|
|
393
|
+
direction: Direction = 'out',
|
|
394
|
+
options?: ListOptions
|
|
395
|
+
): Promise<Action<T>[]> {
|
|
396
|
+
const validDirection = validateDirection(direction)
|
|
397
|
+
let results = Array.from(this.actions.values()) as Action<T>[]
|
|
398
|
+
|
|
399
|
+
if (verb) {
|
|
400
|
+
results = results.filter((a) => a.verb === verb)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
results = results.filter((a) => {
|
|
404
|
+
if (validDirection === 'out') return a.subject === id
|
|
405
|
+
if (validDirection === 'in') return a.object === id
|
|
406
|
+
return a.subject === id || a.object === id
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
// Apply limit with safety bounds
|
|
410
|
+
const limit = effectiveLimit(options?.limit)
|
|
411
|
+
results = results.slice(0, limit)
|
|
412
|
+
|
|
413
|
+
return results
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// ==================== Batch Operations ====================
|
|
417
|
+
|
|
418
|
+
async createMany<T>(noun: string, items: T[]): Promise<Thing<T>[]> {
|
|
419
|
+
if (items.length > MAX_BATCH_SIZE) {
|
|
420
|
+
throw new ValidationError(`Batch size ${items.length} exceeds maximum of ${MAX_BATCH_SIZE}`, [
|
|
421
|
+
{ field: 'items', message: `Batch size exceeds maximum of ${MAX_BATCH_SIZE}` },
|
|
422
|
+
])
|
|
423
|
+
}
|
|
424
|
+
return Promise.all(items.map((item) => this.create(noun, item)))
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async updateMany<T>(updates: Array<{ id: string; data: Partial<T> }>): Promise<Thing<T>[]> {
|
|
428
|
+
if (updates.length > MAX_BATCH_SIZE) {
|
|
429
|
+
throw new ValidationError(
|
|
430
|
+
`Batch size ${updates.length} exceeds maximum of ${MAX_BATCH_SIZE}`,
|
|
431
|
+
[{ field: 'items', message: `Batch size exceeds maximum of ${MAX_BATCH_SIZE}` }]
|
|
432
|
+
)
|
|
433
|
+
}
|
|
434
|
+
return Promise.all(updates.map((u) => this.update(u.id, u.data)))
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async deleteMany(ids: string[]): Promise<boolean[]> {
|
|
438
|
+
if (ids.length > MAX_BATCH_SIZE) {
|
|
439
|
+
throw new ValidationError(`Batch size ${ids.length} exceeds maximum of ${MAX_BATCH_SIZE}`, [
|
|
440
|
+
{ field: 'ids', message: `Batch size exceeds maximum of ${MAX_BATCH_SIZE}` },
|
|
441
|
+
])
|
|
442
|
+
}
|
|
443
|
+
return Promise.all(ids.map((id) => this.delete(id)))
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async performMany<T>(
|
|
447
|
+
actions: Array<{ verb: string; subject?: string; object?: string; data?: T }>
|
|
448
|
+
): Promise<Action<T>[]> {
|
|
449
|
+
if (actions.length > MAX_BATCH_SIZE) {
|
|
450
|
+
throw new ValidationError(
|
|
451
|
+
`Batch size ${actions.length} exceeds maximum of ${MAX_BATCH_SIZE}`,
|
|
452
|
+
[{ field: 'actions', message: `Batch size exceeds maximum of ${MAX_BATCH_SIZE}` }]
|
|
453
|
+
)
|
|
454
|
+
}
|
|
455
|
+
return Promise.all(actions.map((a) => this.perform(a.verb, a.subject, a.object, a.data)))
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// ==================== Lifecycle ====================
|
|
459
|
+
|
|
460
|
+
async close(): Promise<void> {
|
|
461
|
+
this.nouns.clear()
|
|
462
|
+
this.verbs.clear()
|
|
463
|
+
this.things.clear()
|
|
464
|
+
this.actions.clear()
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export function createMemoryProvider(): DigitalObjectsProvider {
|
|
469
|
+
return new MemoryProvider()
|
|
470
|
+
}
|