@delma/fylo 2.0.1 → 2.1.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/README.md +185 -267
- package/package.json +2 -5
- package/src/core/directory.ts +22 -354
- package/src/engines/s3-files/documents.ts +65 -0
- package/src/engines/s3-files/filesystem.ts +172 -0
- package/src/engines/s3-files/query.ts +291 -0
- package/src/engines/s3-files/types.ts +42 -0
- package/src/engines/s3-files.ts +391 -690
- package/src/engines/types.ts +1 -1
- package/src/index.ts +142 -1237
- package/src/sync.ts +58 -0
- package/src/types/fylo.d.ts +66 -161
- package/src/types/node-runtime.d.ts +1 -0
- package/tests/collection/truncate.test.js +11 -10
- package/tests/helpers/root.js +7 -0
- package/tests/integration/create.test.js +9 -9
- package/tests/integration/delete.test.js +16 -14
- package/tests/integration/edge-cases.test.js +29 -25
- package/tests/integration/encryption.test.js +47 -30
- package/tests/integration/export.test.js +11 -11
- package/tests/integration/join-modes.test.js +16 -16
- package/tests/integration/nested.test.js +26 -24
- package/tests/integration/operators.test.js +43 -29
- package/tests/integration/read.test.js +25 -21
- package/tests/integration/rollback.test.js +21 -51
- package/tests/integration/s3-files.performance.test.js +75 -0
- package/tests/integration/s3-files.test.js +57 -44
- package/tests/integration/sync.test.js +154 -0
- package/tests/integration/update.test.js +24 -18
- package/src/adapters/redis.ts +0 -487
- package/src/adapters/s3.ts +0 -61
- package/src/core/walker.ts +0 -174
- package/src/core/write-queue.ts +0 -59
- package/src/migrate-cli.ts +0 -22
- package/src/migrate.ts +0 -74
- package/src/types/write-queue.ts +0 -42
- package/src/worker.ts +0 -18
- package/src/workers/write-worker.ts +0 -120
- package/tests/index.js +0 -14
- package/tests/integration/migration.test.js +0 -38
- package/tests/integration/queue.test.js +0 -83
- package/tests/mocks/redis.js +0 -123
- package/tests/mocks/s3.js +0 -80
package/src/index.ts
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
|
2
|
-
import
|
|
2
|
+
import path from 'node:path'
|
|
3
3
|
import { Parser } from './core/parser'
|
|
4
|
-
import { Dir } from './core/directory'
|
|
5
4
|
import TTID from '@delma/ttid'
|
|
6
5
|
import Gen from '@delma/chex'
|
|
7
|
-
import { Walker } from './core/walker'
|
|
8
|
-
import { S3 } from './adapters/s3'
|
|
9
6
|
import { Cipher } from './adapters/cipher'
|
|
10
|
-
import { Redis } from './adapters/redis'
|
|
11
|
-
import { WriteQueue } from './core/write-queue'
|
|
12
7
|
import { S3FilesEngine } from './engines/s3-files'
|
|
8
|
+
import type { FyloOptions } from './sync'
|
|
13
9
|
import './core/format'
|
|
14
10
|
import './core/extensions'
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
|
|
12
|
+
export { FyloSyncError } from './sync'
|
|
13
|
+
export type {
|
|
14
|
+
FyloDeleteSyncEvent,
|
|
15
|
+
FyloOptions,
|
|
16
|
+
FyloSyncHooks,
|
|
17
|
+
FyloSyncMode,
|
|
18
|
+
FyloWriteSyncEvent
|
|
19
|
+
} from './sync'
|
|
18
20
|
|
|
19
21
|
export default class Fylo {
|
|
20
22
|
private static LOGGING = process.env.LOGGING
|
|
@@ -30,48 +32,25 @@ export default class Fylo {
|
|
|
30
32
|
/** Collections whose schema `$encrypted` config has already been loaded. */
|
|
31
33
|
private static readonly loadedEncryption: Set<string> = new Set()
|
|
32
34
|
|
|
33
|
-
private
|
|
34
|
-
|
|
35
|
-
private static rollbackWarningShown = false
|
|
36
|
-
|
|
37
|
-
private static readonly MAX_WRITE_ATTEMPTS = Number(process.env.FYLO_WRITE_MAX_ATTEMPTS ?? 3)
|
|
38
|
-
|
|
39
|
-
private static readonly WRITE_RETRY_BASE_MS = Number(process.env.FYLO_WRITE_RETRY_BASE_MS ?? 10)
|
|
40
|
-
|
|
41
|
-
private readonly dir: Dir
|
|
42
|
-
|
|
43
|
-
private readonly engineKind: FyloStorageEngineKind
|
|
44
|
-
|
|
45
|
-
private readonly s3Files?: S3FilesEngine
|
|
46
|
-
|
|
47
|
-
constructor(options: { engine?: FyloStorageEngineKind; s3FilesRoot?: string } = {}) {
|
|
48
|
-
this.dir = new Dir()
|
|
49
|
-
this.engineKind = options.engine ?? Fylo.defaultEngineKind()
|
|
50
|
-
if (this.engineKind === 's3-files') this.s3Files = new S3FilesEngine(options.s3FilesRoot)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
private static get queueRedis(): Redis {
|
|
54
|
-
if (!Fylo._queueRedis) Fylo._queueRedis = new Redis()
|
|
55
|
-
return Fylo._queueRedis
|
|
56
|
-
}
|
|
35
|
+
private readonly engine: S3FilesEngine
|
|
57
36
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return new S3FilesEngine(process.env.FYLO_S3FILES_ROOT)
|
|
37
|
+
constructor(options: FyloOptions = {}) {
|
|
38
|
+
this.engine = new S3FilesEngine(options.root ?? options.s3FilesRoot ?? Fylo.defaultRoot(), {
|
|
39
|
+
sync: options.sync,
|
|
40
|
+
syncMode: options.syncMode
|
|
41
|
+
})
|
|
64
42
|
}
|
|
65
43
|
|
|
66
|
-
private
|
|
67
|
-
|
|
68
|
-
|
|
44
|
+
private static defaultRoot() {
|
|
45
|
+
return (
|
|
46
|
+
process.env.FYLO_ROOT ??
|
|
47
|
+
process.env.FYLO_S3FILES_ROOT ??
|
|
48
|
+
path.join(process.cwd(), '.fylo-data')
|
|
49
|
+
)
|
|
69
50
|
}
|
|
70
51
|
|
|
71
|
-
private
|
|
72
|
-
|
|
73
|
-
throw new Error(`${feature} is not supported in s3-files engine`)
|
|
74
|
-
return Fylo.queueRedis
|
|
52
|
+
private static get defaultEngine() {
|
|
53
|
+
return new S3FilesEngine(Fylo.defaultRoot())
|
|
75
54
|
}
|
|
76
55
|
|
|
77
56
|
/**
|
|
@@ -96,77 +75,65 @@ export default class Fylo {
|
|
|
96
75
|
return await this.dropCollection(
|
|
97
76
|
(Parser.parse(SQL) as _storeDelete<T>).$collection!
|
|
98
77
|
)
|
|
99
|
-
case 'SELECT':
|
|
78
|
+
case 'SELECT': {
|
|
100
79
|
const query = Parser.parse<T>(SQL) as _storeQuery<T>
|
|
101
80
|
if (SQL.includes('JOIN')) return await this.joinDocs(query as _join<T, U>)
|
|
102
|
-
const selCol =
|
|
103
|
-
delete
|
|
104
|
-
let docs: Record<string, unknown> | Array<_ttid> = query.$onlyIds
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
query as _storeQuery<T>
|
|
110
|
-
).collect()) {
|
|
111
|
-
if (typeof data === 'object') {
|
|
112
|
-
docs = Object.appendGroup(docs, data)
|
|
113
|
-
} else (docs as Array<_ttid>).push(data as _ttid)
|
|
81
|
+
const selCol = query.$collection
|
|
82
|
+
delete query.$collection
|
|
83
|
+
let docs: Record<string, unknown> | Array<_ttid> = query.$onlyIds ? [] : {}
|
|
84
|
+
|
|
85
|
+
for await (const data of this.findDocs(selCol! as string, query).collect()) {
|
|
86
|
+
if (typeof data === 'object') docs = Object.appendGroup(docs, data)
|
|
87
|
+
else (docs as Array<_ttid>).push(data as _ttid)
|
|
114
88
|
}
|
|
89
|
+
|
|
115
90
|
return docs
|
|
116
|
-
|
|
91
|
+
}
|
|
92
|
+
case 'INSERT': {
|
|
117
93
|
const insert = Parser.parse<T>(SQL) as _storeInsert<T>
|
|
118
94
|
const insCol = insert.$collection
|
|
119
95
|
delete insert.$collection
|
|
120
96
|
return await this.putData(insCol!, insert.$values)
|
|
121
|
-
|
|
97
|
+
}
|
|
98
|
+
case 'UPDATE': {
|
|
122
99
|
const update = Parser.parse<T>(SQL) as _storeUpdate<T>
|
|
123
100
|
const updateCol = update.$collection
|
|
124
101
|
delete update.$collection
|
|
125
102
|
return await this.patchDocs(updateCol!, update)
|
|
126
|
-
|
|
103
|
+
}
|
|
104
|
+
case 'DELETE': {
|
|
127
105
|
const del = Parser.parse<T>(SQL) as _storeDelete<T>
|
|
128
106
|
const delCol = del.$collection
|
|
129
107
|
delete del.$collection
|
|
130
108
|
return await this.delDocs(delCol!, del)
|
|
109
|
+
}
|
|
131
110
|
default:
|
|
132
111
|
throw new Error('Invalid Operation')
|
|
133
112
|
}
|
|
134
113
|
}
|
|
135
114
|
|
|
136
115
|
/**
|
|
137
|
-
* Creates a new
|
|
116
|
+
* Creates a new collection on the configured filesystem root.
|
|
138
117
|
* @param collection The name of the collection.
|
|
139
118
|
*/
|
|
140
119
|
static async createCollection(collection: string) {
|
|
141
|
-
|
|
142
|
-
await Fylo.defaultS3Files.createCollection(collection)
|
|
143
|
-
return
|
|
144
|
-
}
|
|
145
|
-
await S3.createBucket(collection)
|
|
120
|
+
await Fylo.defaultEngine.createCollection(collection)
|
|
146
121
|
}
|
|
147
122
|
|
|
148
123
|
/**
|
|
149
|
-
* Drops an existing
|
|
124
|
+
* Drops an existing collection from the configured filesystem root.
|
|
150
125
|
* @param collection The name of the collection.
|
|
151
126
|
*/
|
|
152
127
|
static async dropCollection(collection: string) {
|
|
153
|
-
|
|
154
|
-
await Fylo.defaultS3Files.dropCollection(collection)
|
|
155
|
-
return
|
|
156
|
-
}
|
|
157
|
-
await S3.deleteBucket(collection)
|
|
128
|
+
await Fylo.defaultEngine.dropCollection(collection)
|
|
158
129
|
}
|
|
159
130
|
|
|
160
131
|
async createCollection(collection: string) {
|
|
161
|
-
|
|
162
|
-
return await this.assertS3FilesEngine().createCollection(collection)
|
|
163
|
-
return await Fylo.createCollection(collection)
|
|
132
|
+
return await this.engine.createCollection(collection)
|
|
164
133
|
}
|
|
165
134
|
|
|
166
135
|
async dropCollection(collection: string) {
|
|
167
|
-
|
|
168
|
-
return await this.assertS3FilesEngine().dropCollection(collection)
|
|
169
|
-
return await Fylo.dropCollection(collection)
|
|
136
|
+
return await this.engine.dropCollection(collection)
|
|
170
137
|
}
|
|
171
138
|
|
|
172
139
|
/**
|
|
@@ -203,176 +170,58 @@ export default class Fylo {
|
|
|
203
170
|
}
|
|
204
171
|
}
|
|
205
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Compatibility helper. FYLO now writes synchronously to the filesystem,
|
|
175
|
+
* so there is no queued transactional rollback path to execute.
|
|
176
|
+
*/
|
|
177
|
+
async rollback() {}
|
|
178
|
+
|
|
206
179
|
getDoc<T extends Record<string, any>>(collection: string, _id: _ttid, onlyId: boolean = false) {
|
|
207
|
-
|
|
208
|
-
return this.assertS3FilesEngine().getDoc<T>(collection, _id, onlyId)
|
|
209
|
-
return Fylo.getDoc<T>(collection, _id, onlyId)
|
|
180
|
+
return this.engine.getDoc<T>(collection, _id, onlyId)
|
|
210
181
|
}
|
|
211
182
|
|
|
212
183
|
findDocs<T extends Record<string, any>>(collection: string, query?: _storeQuery<T>) {
|
|
213
|
-
|
|
214
|
-
return this.assertS3FilesEngine().findDocs<T>(collection, query)
|
|
215
|
-
return Fylo.findDocs<T>(collection, query)
|
|
184
|
+
return this.engine.findDocs<T>(collection, query)
|
|
216
185
|
}
|
|
217
186
|
|
|
218
187
|
async joinDocs<T extends Record<string, any>, U extends Record<string, any>>(
|
|
219
188
|
join: _join<T, U>
|
|
220
189
|
) {
|
|
221
|
-
|
|
222
|
-
return await Fylo.joinDocs(join)
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Rolls back all transcations in current instance
|
|
227
|
-
* @deprecated Prefer queued write recovery, retries, dead letters, or compensating writes.
|
|
228
|
-
*/
|
|
229
|
-
async rollback() {
|
|
230
|
-
if (this.engineKind === 's3-files') return
|
|
231
|
-
if (!Fylo.rollbackWarningShown) {
|
|
232
|
-
Fylo.rollbackWarningShown = true
|
|
233
|
-
console.warn(
|
|
234
|
-
'[FYLO] rollback() is deprecated for queued-write flows. Prefer job recovery, dead letters, or compensating writes.'
|
|
235
|
-
)
|
|
236
|
-
}
|
|
237
|
-
await this.dir.executeRollback()
|
|
190
|
+
return await this.engine.joinDocs(join)
|
|
238
191
|
}
|
|
239
192
|
|
|
240
|
-
async
|
|
241
|
-
|
|
193
|
+
async *exportBulkData<T extends Record<string, any>>(collection: string) {
|
|
194
|
+
yield* this.engine.exportBulkData<T>(collection)
|
|
242
195
|
}
|
|
243
196
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
async getDeadLetters(count: number = 10) {
|
|
249
|
-
return await this.ensureLegacyQueue('getDeadLetters').readDeadLetters(count)
|
|
197
|
+
private unsupportedLegacyApi(feature: string): never {
|
|
198
|
+
throw new Error(
|
|
199
|
+
`${feature} was removed. FYLO now writes synchronously to the filesystem and expects external sync tooling for cloud replication.`
|
|
200
|
+
)
|
|
250
201
|
}
|
|
251
202
|
|
|
252
|
-
async
|
|
253
|
-
return
|
|
203
|
+
async getJobStatus(_jobId: string) {
|
|
204
|
+
return this.unsupportedLegacyApi('getJobStatus')
|
|
254
205
|
}
|
|
255
206
|
|
|
256
|
-
async
|
|
257
|
-
return
|
|
207
|
+
async getDocStatus(_collection: string, _docId: _ttid) {
|
|
208
|
+
return this.unsupportedLegacyApi('getDocStatus')
|
|
258
209
|
}
|
|
259
210
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
do {
|
|
264
|
-
const job = await this.getJobStatus(jobId)
|
|
265
|
-
if (job && (job.status === 'committed' || job.status === 'failed')) return job
|
|
266
|
-
await Bun.sleep(intervalMs)
|
|
267
|
-
} while (Date.now() - start < timeoutMs)
|
|
268
|
-
|
|
269
|
-
throw new Error(`Timed out waiting for job ${jobId}`)
|
|
211
|
+
async getDeadLetters(_count: number = 10) {
|
|
212
|
+
return this.unsupportedLegacyApi('getDeadLetters')
|
|
270
213
|
}
|
|
271
214
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
{
|
|
275
|
-
wait = true
|
|
276
|
-
}: {
|
|
277
|
-
wait?: boolean
|
|
278
|
-
timeoutMs?: number
|
|
279
|
-
} = {},
|
|
280
|
-
resolveValue?: () => Promise<T> | T
|
|
281
|
-
): Promise<T | QueuedWriteResult> {
|
|
282
|
-
if (this.engineKind === 's3-files') {
|
|
283
|
-
if (!wait) return queued
|
|
284
|
-
return resolveValue ? await resolveValue() : queued
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
if (!wait) return queued
|
|
288
|
-
|
|
289
|
-
const processed = await this.processQueuedWrites(1)
|
|
290
|
-
|
|
291
|
-
if (processed === 0) throw new Error(`No worker available to process job ${queued.jobId}`)
|
|
292
|
-
|
|
293
|
-
const job = await this.getJobStatus(queued.jobId)
|
|
294
|
-
|
|
295
|
-
if (job?.status === 'failed') {
|
|
296
|
-
throw new Error(job.error ?? `Queued job ${queued.jobId} failed`)
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
return resolveValue ? await resolveValue() : queued
|
|
215
|
+
async getQueueStats() {
|
|
216
|
+
return this.unsupportedLegacyApi('getQueueStats')
|
|
300
217
|
}
|
|
301
218
|
|
|
302
|
-
async
|
|
303
|
-
this.
|
|
304
|
-
const jobs = recover
|
|
305
|
-
? await Fylo.queueRedis.claimPendingJobs(Bun.randomUUIDv7(), 30_000, count)
|
|
306
|
-
: await Fylo.queueRedis.readWriteJobs(Bun.randomUUIDv7(), count)
|
|
307
|
-
|
|
308
|
-
let processed = 0
|
|
309
|
-
|
|
310
|
-
for (const job of jobs) {
|
|
311
|
-
if (await this.processQueuedJob(job)) processed++
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
return processed
|
|
219
|
+
async replayDeadLetter(_streamId: string) {
|
|
220
|
+
return this.unsupportedLegacyApi('replayDeadLetter')
|
|
315
221
|
}
|
|
316
222
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const locked = await Fylo.queueRedis.acquireDocLock(job.collection, job.docId, job.jobId)
|
|
321
|
-
if (!locked) return false
|
|
322
|
-
|
|
323
|
-
try {
|
|
324
|
-
await Fylo.queueRedis.setJobStatus(job.jobId, 'processing', {
|
|
325
|
-
attempts: job.attempts + 1
|
|
326
|
-
})
|
|
327
|
-
await Fylo.queueRedis.setDocStatus(job.collection, job.docId, 'processing', job.jobId)
|
|
328
|
-
|
|
329
|
-
await this.executeQueuedWrite(job)
|
|
330
|
-
|
|
331
|
-
await Fylo.queueRedis.setJobStatus(job.jobId, 'committed')
|
|
332
|
-
await Fylo.queueRedis.setDocStatus(job.collection, job.docId, 'committed', job.jobId)
|
|
333
|
-
await Fylo.queueRedis.ackWriteJob(streamId)
|
|
334
|
-
return true
|
|
335
|
-
} catch (err) {
|
|
336
|
-
const attempts = job.attempts + 1
|
|
337
|
-
const message = err instanceof Error ? err.message : String(err)
|
|
338
|
-
|
|
339
|
-
if (attempts >= Fylo.MAX_WRITE_ATTEMPTS) {
|
|
340
|
-
await Fylo.queueRedis.setJobStatus(job.jobId, 'dead-letter', {
|
|
341
|
-
error: message,
|
|
342
|
-
attempts
|
|
343
|
-
})
|
|
344
|
-
await Fylo.queueRedis.setDocStatus(
|
|
345
|
-
job.collection,
|
|
346
|
-
job.docId,
|
|
347
|
-
'dead-letter',
|
|
348
|
-
job.jobId
|
|
349
|
-
)
|
|
350
|
-
await Fylo.queueRedis.deadLetterWriteJob(
|
|
351
|
-
streamId,
|
|
352
|
-
{
|
|
353
|
-
...job,
|
|
354
|
-
attempts,
|
|
355
|
-
status: 'dead-letter',
|
|
356
|
-
error: message
|
|
357
|
-
},
|
|
358
|
-
message
|
|
359
|
-
)
|
|
360
|
-
return false
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
const nextAttemptAt =
|
|
364
|
-
Date.now() + Fylo.WRITE_RETRY_BASE_MS * Math.max(1, 2 ** (attempts - 1))
|
|
365
|
-
|
|
366
|
-
await Fylo.queueRedis.setJobStatus(job.jobId, 'failed', {
|
|
367
|
-
error: message,
|
|
368
|
-
attempts,
|
|
369
|
-
nextAttemptAt
|
|
370
|
-
})
|
|
371
|
-
await Fylo.queueRedis.setDocStatus(job.collection, job.docId, 'failed', job.jobId)
|
|
372
|
-
return false
|
|
373
|
-
} finally {
|
|
374
|
-
await Fylo.queueRedis.releaseDocLock(job.collection, job.docId, job.jobId)
|
|
375
|
-
}
|
|
223
|
+
async processQueuedWrites(_count: number = 1, _recover: boolean = false) {
|
|
224
|
+
return this.unsupportedLegacyApi('processQueuedWrites')
|
|
376
225
|
}
|
|
377
226
|
|
|
378
227
|
/**
|
|
@@ -418,12 +267,6 @@ export default class Fylo {
|
|
|
418
267
|
}
|
|
419
268
|
}
|
|
420
269
|
|
|
421
|
-
// Detect format from the first byte of the body:
|
|
422
|
-
// 0x5b ('[') → JSON array: buffer the full body, then parse and process in slices.
|
|
423
|
-
// Otherwise → NDJSON stream: parse incrementally with Bun.JSONL.parseChunk, which
|
|
424
|
-
// accepts Uint8Array directly (zero-copy for ASCII) and tracks the split-line
|
|
425
|
-
// remainder internally via the returned `read` offset — no manual incomplete-
|
|
426
|
-
// line state machine needed.
|
|
427
270
|
let isJsonArray: boolean | null = null
|
|
428
271
|
const jsonArrayChunks: Uint8Array[] = []
|
|
429
272
|
let jsonArrayLength = 0
|
|
@@ -440,8 +283,6 @@ export default class Fylo {
|
|
|
440
283
|
continue
|
|
441
284
|
}
|
|
442
285
|
|
|
443
|
-
// Prepend any leftover bytes from the previous iteration (an unterminated line),
|
|
444
|
-
// then parse. `read` points past the last complete line; `pending` holds the rest.
|
|
445
286
|
const merged = new Uint8Array(pending.length + chunk.length)
|
|
446
287
|
merged.set(pending)
|
|
447
288
|
merged.set(chunk, pending.length)
|
|
@@ -460,7 +301,6 @@ export default class Fylo {
|
|
|
460
301
|
}
|
|
461
302
|
|
|
462
303
|
if (isJsonArray) {
|
|
463
|
-
// Reassemble buffered chunks into a single Uint8Array and parse as JSON.
|
|
464
304
|
const body = new Uint8Array(jsonArrayLength)
|
|
465
305
|
let offset = 0
|
|
466
306
|
for (const c of jsonArrayChunks) {
|
|
@@ -476,7 +316,6 @@ export default class Fylo {
|
|
|
476
316
|
await flush(items.slice(i, i + Fylo.MAX_CPUS))
|
|
477
317
|
}
|
|
478
318
|
} else {
|
|
479
|
-
// Flush the in-progress batch and any final line that had no trailing newline.
|
|
480
319
|
if (pending.length > 0) {
|
|
481
320
|
const { values } = Bun.JSONL.parseChunk(pending)
|
|
482
321
|
for (const item of values) batch.push(item as T)
|
|
@@ -489,50 +328,10 @@ export default class Fylo {
|
|
|
489
328
|
}
|
|
490
329
|
|
|
491
330
|
/**
|
|
492
|
-
*
|
|
493
|
-
* @param collection The name of the collection.
|
|
494
|
-
* @returns The current data exported from the collection.
|
|
331
|
+
* Gets an exported stream of documents from a collection.
|
|
495
332
|
*/
|
|
496
333
|
static async *exportBulkData<T extends Record<string, any>>(collection: string) {
|
|
497
|
-
|
|
498
|
-
yield* Fylo.defaultS3Files.exportBulkData<T>(collection)
|
|
499
|
-
return
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
// Kick off the first S3 list immediately so there is no idle time at the start.
|
|
503
|
-
let listPromise: Promise<Bun.S3ListObjectsResponse> | null = S3.list(collection, {
|
|
504
|
-
delimiter: '/'
|
|
505
|
-
})
|
|
506
|
-
|
|
507
|
-
while (listPromise !== null) {
|
|
508
|
-
const data: Bun.S3ListObjectsResponse = await listPromise
|
|
509
|
-
|
|
510
|
-
if (!data.commonPrefixes?.length) break
|
|
511
|
-
|
|
512
|
-
const ids = data.commonPrefixes
|
|
513
|
-
.map((item) => item.prefix!.split('/')[0]!)
|
|
514
|
-
.filter((key) => TTID.isTTID(key)) as _ttid[]
|
|
515
|
-
|
|
516
|
-
// Start fetching the next page immediately — before awaiting doc reads —
|
|
517
|
-
// so the S3 list round-trip overlaps with document reconstruction.
|
|
518
|
-
listPromise =
|
|
519
|
-
data.isTruncated && data.nextContinuationToken
|
|
520
|
-
? S3.list(collection, {
|
|
521
|
-
delimiter: '/',
|
|
522
|
-
continuationToken: data.nextContinuationToken
|
|
523
|
-
})
|
|
524
|
-
: null
|
|
525
|
-
|
|
526
|
-
const results = await Promise.allSettled(
|
|
527
|
-
ids.map((id) => this.getDoc<T>(collection, id).once())
|
|
528
|
-
)
|
|
529
|
-
|
|
530
|
-
for (const result of results) {
|
|
531
|
-
if (result.status === 'fulfilled') {
|
|
532
|
-
for (const id in result.value) yield result.value[id]
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
}
|
|
334
|
+
yield* Fylo.defaultEngine.exportBulkData<T>(collection)
|
|
536
335
|
}
|
|
537
336
|
|
|
538
337
|
/**
|
|
@@ -547,93 +346,7 @@ export default class Fylo {
|
|
|
547
346
|
_id: _ttid,
|
|
548
347
|
onlyId: boolean = false
|
|
549
348
|
) {
|
|
550
|
-
|
|
551
|
-
return Fylo.defaultS3Files.getDoc<T>(collection, _id, onlyId)
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
return {
|
|
555
|
-
/**
|
|
556
|
-
* Async iterator (listener) for the document.
|
|
557
|
-
*/
|
|
558
|
-
async *[Symbol.asyncIterator]() {
|
|
559
|
-
const doc = await this.once()
|
|
560
|
-
|
|
561
|
-
if (Object.keys(doc).length > 0) yield doc
|
|
562
|
-
|
|
563
|
-
let finished = false
|
|
564
|
-
|
|
565
|
-
const iter = Dir.searchDocs<T>(
|
|
566
|
-
collection,
|
|
567
|
-
`**/${_id.split('-')[0]}*`,
|
|
568
|
-
{},
|
|
569
|
-
{ listen: true, skip: true }
|
|
570
|
-
)
|
|
571
|
-
|
|
572
|
-
do {
|
|
573
|
-
const { value, done } = await iter.next({ count: 0 })
|
|
574
|
-
|
|
575
|
-
if (value === undefined && !done) continue
|
|
576
|
-
|
|
577
|
-
if (done) {
|
|
578
|
-
finished = true
|
|
579
|
-
break
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
const doc = value as Record<_ttid, T>
|
|
583
|
-
|
|
584
|
-
const keys = Object.keys(doc)
|
|
585
|
-
|
|
586
|
-
if (onlyId && keys.length > 0) {
|
|
587
|
-
yield keys.shift()!
|
|
588
|
-
continue
|
|
589
|
-
} else if (keys.length > 0) {
|
|
590
|
-
yield doc
|
|
591
|
-
continue
|
|
592
|
-
}
|
|
593
|
-
} while (!finished)
|
|
594
|
-
},
|
|
595
|
-
|
|
596
|
-
/**
|
|
597
|
-
* Gets the document once.
|
|
598
|
-
*/
|
|
599
|
-
async once() {
|
|
600
|
-
const items = await Walker.getDocData(collection, _id)
|
|
601
|
-
|
|
602
|
-
if (items.length === 0) return {}
|
|
603
|
-
|
|
604
|
-
const data = await Dir.reconstructData(collection, items)
|
|
605
|
-
|
|
606
|
-
return { [_id]: data } as Record<_ttid, T>
|
|
607
|
-
},
|
|
608
|
-
|
|
609
|
-
/**
|
|
610
|
-
* Async iterator (listener) for the document's deletion.
|
|
611
|
-
*/
|
|
612
|
-
async *onDelete() {
|
|
613
|
-
let finished = false
|
|
614
|
-
|
|
615
|
-
const iter = Dir.searchDocs<T>(
|
|
616
|
-
collection,
|
|
617
|
-
`**/${_id.split('-')[0]}*`,
|
|
618
|
-
{},
|
|
619
|
-
{ listen: true, skip: true },
|
|
620
|
-
true
|
|
621
|
-
)
|
|
622
|
-
|
|
623
|
-
do {
|
|
624
|
-
const { value, done } = await iter.next({ count: 0 })
|
|
625
|
-
|
|
626
|
-
if (value === undefined && !done) continue
|
|
627
|
-
|
|
628
|
-
if (done) {
|
|
629
|
-
finished = true
|
|
630
|
-
break
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
yield value as _ttid
|
|
634
|
-
} while (!finished)
|
|
635
|
-
}
|
|
636
|
-
}
|
|
349
|
+
return Fylo.defaultEngine.getDoc<T>(collection, _id, onlyId)
|
|
637
350
|
}
|
|
638
351
|
|
|
639
352
|
/**
|
|
@@ -652,9 +365,9 @@ export default class Fylo {
|
|
|
652
365
|
}
|
|
653
366
|
} else batches.push(batch)
|
|
654
367
|
|
|
655
|
-
for (const
|
|
368
|
+
for (const itemBatch of batches) {
|
|
656
369
|
const res = await Promise.allSettled(
|
|
657
|
-
|
|
370
|
+
itemBatch.map((data) => this.putData(collection, data))
|
|
658
371
|
)
|
|
659
372
|
|
|
660
373
|
for (const _id of res
|
|
@@ -668,60 +381,22 @@ export default class Fylo {
|
|
|
668
381
|
}
|
|
669
382
|
|
|
670
383
|
async queuePutData<T extends Record<string, any>>(
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
)
|
|
674
|
-
|
|
675
|
-
throw new Error('queuePutData is not supported in s3-files engine')
|
|
676
|
-
|
|
677
|
-
const { _id, doc } = await this.prepareInsert(collection, data)
|
|
678
|
-
const job = WriteQueue.createInsertJob(collection, _id, doc)
|
|
679
|
-
|
|
680
|
-
await Fylo.queueRedis.enqueueWrite(job)
|
|
681
|
-
|
|
682
|
-
return {
|
|
683
|
-
jobId: job.jobId,
|
|
684
|
-
docId: _id,
|
|
685
|
-
status: 'queued'
|
|
686
|
-
}
|
|
384
|
+
_collection: string,
|
|
385
|
+
_data: Record<_ttid, T> | T
|
|
386
|
+
) {
|
|
387
|
+
return this.unsupportedLegacyApi('queuePutData')
|
|
687
388
|
}
|
|
688
389
|
|
|
689
390
|
async queuePatchDoc<T extends Record<string, any>>(
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
)
|
|
694
|
-
|
|
695
|
-
throw new Error('queuePatchDoc is not supported in s3-files engine')
|
|
696
|
-
|
|
697
|
-
const docId = Object.keys(newDoc).shift() as _ttid
|
|
698
|
-
|
|
699
|
-
if (!docId) throw new Error('this document does not contain an TTID')
|
|
700
|
-
|
|
701
|
-
const job = WriteQueue.createUpdateJob(collection, docId, { newDoc, oldDoc })
|
|
702
|
-
|
|
703
|
-
await Fylo.queueRedis.enqueueWrite(job)
|
|
704
|
-
|
|
705
|
-
return {
|
|
706
|
-
jobId: job.jobId,
|
|
707
|
-
docId,
|
|
708
|
-
status: 'queued'
|
|
709
|
-
}
|
|
391
|
+
_collection: string,
|
|
392
|
+
_newDoc: Record<_ttid, Partial<T>>,
|
|
393
|
+
_oldDoc: Record<_ttid, T> = {}
|
|
394
|
+
) {
|
|
395
|
+
return this.unsupportedLegacyApi('queuePatchDoc')
|
|
710
396
|
}
|
|
711
397
|
|
|
712
|
-
async queueDelDoc(
|
|
713
|
-
|
|
714
|
-
throw new Error('queueDelDoc is not supported in s3-files engine')
|
|
715
|
-
|
|
716
|
-
const job = WriteQueue.createDeleteJob(collection, _id)
|
|
717
|
-
|
|
718
|
-
await Fylo.queueRedis.enqueueWrite(job)
|
|
719
|
-
|
|
720
|
-
return {
|
|
721
|
-
jobId: job.jobId,
|
|
722
|
-
docId: _id,
|
|
723
|
-
status: 'queued'
|
|
724
|
-
}
|
|
398
|
+
async queueDelDoc(_collection: string, _id: _ttid) {
|
|
399
|
+
return this.unsupportedLegacyApi('queueDelDoc')
|
|
725
400
|
}
|
|
726
401
|
|
|
727
402
|
/**
|
|
@@ -730,19 +405,11 @@ export default class Fylo {
|
|
|
730
405
|
* @param data The document to put.
|
|
731
406
|
* @returns The ID of the document.
|
|
732
407
|
*/
|
|
733
|
-
private static async uniqueTTID(
|
|
734
|
-
existingId?: string,
|
|
735
|
-
claimInRedis: boolean = true
|
|
736
|
-
): Promise<_ttid> {
|
|
737
|
-
// Serialize TTID generation so concurrent callers (e.g. batchPutData)
|
|
738
|
-
// never invoke TTID.generate() at the same sub-millisecond instant.
|
|
408
|
+
private static async uniqueTTID(existingId?: string): Promise<_ttid> {
|
|
739
409
|
let _id!: _ttid
|
|
740
410
|
const prev = Fylo.ttidLock
|
|
741
411
|
Fylo.ttidLock = prev.then(async () => {
|
|
742
412
|
_id = existingId ? TTID.generate(existingId) : TTID.generate()
|
|
743
|
-
// Claim in Redis for cross-process uniqueness (no-op if Redis unavailable)
|
|
744
|
-
if (claimInRedis && !(await Dir.claimTTID(_id)))
|
|
745
|
-
throw new Error('TTID collision — retry')
|
|
746
413
|
})
|
|
747
414
|
await Fylo.ttidLock
|
|
748
415
|
|
|
@@ -756,10 +423,9 @@ export default class Fylo {
|
|
|
756
423
|
await Fylo.loadEncryption(collection)
|
|
757
424
|
|
|
758
425
|
const currId = Object.keys(data).shift()!
|
|
759
|
-
const claimInRedis = this.engineKind !== 's3-files'
|
|
760
426
|
const _id = TTID.isTTID(currId)
|
|
761
|
-
? await Fylo.uniqueTTID(currId
|
|
762
|
-
: await Fylo.uniqueTTID(undefined
|
|
427
|
+
? await Fylo.uniqueTTID(currId)
|
|
428
|
+
: await Fylo.uniqueTTID(undefined)
|
|
763
429
|
|
|
764
430
|
let doc = TTID.isTTID(currId) ? (Object.values(data).shift() as T) : (data as T)
|
|
765
431
|
|
|
@@ -773,23 +439,7 @@ export default class Fylo {
|
|
|
773
439
|
_id: _ttid,
|
|
774
440
|
doc: T
|
|
775
441
|
) {
|
|
776
|
-
|
|
777
|
-
await this.assertS3FilesEngine().putDocument(collection, _id, doc)
|
|
778
|
-
return _id
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
const keys = await Dir.extractKeys(collection, _id, doc)
|
|
782
|
-
|
|
783
|
-
const results = await Promise.allSettled(
|
|
784
|
-
keys.data.map((item, i) =>
|
|
785
|
-
this.dir.putKeys(collection, { dataKey: item, indexKey: keys.indexes[i] })
|
|
786
|
-
)
|
|
787
|
-
)
|
|
788
|
-
|
|
789
|
-
if (results.some((res) => res.status === 'rejected')) {
|
|
790
|
-
await this.dir.executeRollback()
|
|
791
|
-
throw new Error(`Unable to write to ${collection} collection`)
|
|
792
|
-
}
|
|
442
|
+
await this.engine.putDocument(collection, _id, doc)
|
|
793
443
|
|
|
794
444
|
if (Fylo.LOGGING) console.log(`Finished Writing ${_id}`)
|
|
795
445
|
|
|
@@ -805,93 +455,35 @@ export default class Fylo {
|
|
|
805
455
|
|
|
806
456
|
const _id = Object.keys(newDoc).shift() as _ttid
|
|
807
457
|
|
|
808
|
-
let _newId = _id
|
|
809
|
-
|
|
810
458
|
if (!_id) throw new Error('this document does not contain an TTID')
|
|
811
459
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
existingDoc = existing[_id]
|
|
817
|
-
}
|
|
818
|
-
if (!existingDoc) return _id
|
|
819
|
-
|
|
820
|
-
const currData = { ...existingDoc, ...newDoc[_id] } as T
|
|
821
|
-
let docToWrite: T = currData
|
|
822
|
-
_newId = await Fylo.uniqueTTID(_id, false)
|
|
823
|
-
if (Fylo.STRICT) docToWrite = (await Gen.validateData(collection, currData)) as T
|
|
824
|
-
return await this.assertS3FilesEngine().patchDocument(
|
|
825
|
-
collection,
|
|
826
|
-
_id,
|
|
827
|
-
_newId,
|
|
828
|
-
docToWrite,
|
|
829
|
-
existingDoc
|
|
830
|
-
)
|
|
460
|
+
let existingDoc = oldDoc[_id]
|
|
461
|
+
if (!existingDoc) {
|
|
462
|
+
const existing = await this.engine.getDoc<T>(collection, _id).once()
|
|
463
|
+
existingDoc = existing[_id]
|
|
831
464
|
}
|
|
465
|
+
if (!existingDoc) return _id
|
|
832
466
|
|
|
833
|
-
const
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
if (Object.keys(oldDoc).length === 0) {
|
|
838
|
-
const data = await Dir.reconstructData(collection, dataKeys)
|
|
839
|
-
|
|
840
|
-
oldDoc = { [_id]: data } as Record<_ttid, T>
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
if (Object.keys(oldDoc).length === 0) return _newId
|
|
844
|
-
|
|
845
|
-
const currData = { ...oldDoc[_id] }
|
|
846
|
-
|
|
847
|
-
for (const field in newDoc[_id]) currData[field] = newDoc[_id][field]!
|
|
848
|
-
|
|
849
|
-
_newId = await Fylo.uniqueTTID(_id, this.engineKind === 'legacy-s3')
|
|
850
|
-
|
|
851
|
-
let docToWrite: T = currData as T
|
|
852
|
-
|
|
467
|
+
const currData = { ...existingDoc, ...newDoc[_id] } as T
|
|
468
|
+
let docToWrite: T = currData
|
|
469
|
+
const _newId = await Fylo.uniqueTTID(_id)
|
|
853
470
|
if (Fylo.STRICT) docToWrite = (await Gen.validateData(collection, currData)) as T
|
|
854
471
|
|
|
855
|
-
const
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
)
|
|
863
|
-
)
|
|
864
|
-
])
|
|
865
|
-
|
|
866
|
-
if (
|
|
867
|
-
deleteResults.some((r) => r.status === 'rejected') ||
|
|
868
|
-
putResults.some((r) => r.status === 'rejected')
|
|
869
|
-
) {
|
|
870
|
-
await this.dir.executeRollback()
|
|
871
|
-
throw new Error(`Unable to update ${collection} collection`)
|
|
872
|
-
}
|
|
472
|
+
const nextId = await this.engine.patchDocument(
|
|
473
|
+
collection,
|
|
474
|
+
_id,
|
|
475
|
+
_newId,
|
|
476
|
+
docToWrite,
|
|
477
|
+
existingDoc
|
|
478
|
+
)
|
|
873
479
|
|
|
874
|
-
if (Fylo.LOGGING) console.log(`Finished Updating ${_id} to ${
|
|
480
|
+
if (Fylo.LOGGING) console.log(`Finished Updating ${_id} to ${nextId}`)
|
|
875
481
|
|
|
876
|
-
return
|
|
482
|
+
return nextId
|
|
877
483
|
}
|
|
878
484
|
|
|
879
485
|
private async executeDelDocDirect(collection: string, _id: _ttid) {
|
|
880
|
-
|
|
881
|
-
await this.assertS3FilesEngine().deleteDocument(collection, _id)
|
|
882
|
-
return
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
const keys = await Walker.getDocData(collection, _id)
|
|
886
|
-
|
|
887
|
-
const results = await Promise.allSettled(
|
|
888
|
-
keys.map((key) => this.dir.deleteKeys(collection, key))
|
|
889
|
-
)
|
|
890
|
-
|
|
891
|
-
if (results.some((res) => res.status === 'rejected')) {
|
|
892
|
-
await this.dir.executeRollback()
|
|
893
|
-
throw new Error(`Unable to delete from ${collection} collection`)
|
|
894
|
-
}
|
|
486
|
+
await this.engine.deleteDocument(collection, _id)
|
|
895
487
|
|
|
896
488
|
if (Fylo.LOGGING) console.log(`Finished Deleting ${_id}`)
|
|
897
489
|
}
|
|
@@ -901,60 +493,18 @@ export default class Fylo {
|
|
|
901
493
|
collection: string,
|
|
902
494
|
data: Record<_ttid, T>
|
|
903
495
|
): Promise<_ttid>
|
|
904
|
-
async putData<T extends Record<string, any>>(
|
|
905
|
-
collection: string,
|
|
906
|
-
data: T,
|
|
907
|
-
options: { wait?: true; timeoutMs?: number }
|
|
908
|
-
): Promise<_ttid>
|
|
909
|
-
async putData<T extends Record<string, any>>(
|
|
910
|
-
collection: string,
|
|
911
|
-
data: Record<_ttid, T>,
|
|
912
|
-
options: { wait?: true; timeoutMs?: number }
|
|
913
|
-
): Promise<_ttid>
|
|
914
|
-
async putData<T extends Record<string, any>>(
|
|
915
|
-
collection: string,
|
|
916
|
-
data: T,
|
|
917
|
-
options: { wait: false; timeoutMs?: number }
|
|
918
|
-
): Promise<QueuedWriteResult>
|
|
919
|
-
async putData<T extends Record<string, any>>(
|
|
920
|
-
collection: string,
|
|
921
|
-
data: Record<_ttid, T>,
|
|
922
|
-
options: { wait: false; timeoutMs?: number }
|
|
923
|
-
): Promise<QueuedWriteResult>
|
|
924
496
|
async putData<T extends Record<string, any>>(
|
|
925
497
|
collection: string,
|
|
926
498
|
data: Record<_ttid, T> | T,
|
|
927
499
|
options: { wait?: boolean; timeoutMs?: number } = {}
|
|
928
|
-
): Promise<_ttid
|
|
929
|
-
if (
|
|
930
|
-
|
|
931
|
-
throw new Error('wait:false is not supported in s3-files engine')
|
|
932
|
-
const { _id, doc } = await this.prepareInsert(collection, data)
|
|
933
|
-
await this.executePutDataDirect(collection, _id, doc)
|
|
934
|
-
return _id
|
|
500
|
+
): Promise<_ttid> {
|
|
501
|
+
if (options.wait === false) {
|
|
502
|
+
this.unsupportedLegacyApi('putData(..., { wait: false })')
|
|
935
503
|
}
|
|
936
504
|
|
|
937
|
-
const
|
|
938
|
-
|
|
939
|
-
return
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
async executeQueuedWrite(job: WriteJob) {
|
|
943
|
-
switch (job.operation) {
|
|
944
|
-
case 'insert':
|
|
945
|
-
await Fylo.loadEncryption(job.collection)
|
|
946
|
-
return await this.executePutDataDirect(job.collection, job.docId, job.payload)
|
|
947
|
-
case 'update':
|
|
948
|
-
return await this.executePatchDocDirect(
|
|
949
|
-
job.collection,
|
|
950
|
-
job.payload.newDoc as Record<_ttid, Partial<Record<string, any>>>,
|
|
951
|
-
job.payload.oldDoc as Record<_ttid, Record<string, any>> | undefined
|
|
952
|
-
)
|
|
953
|
-
case 'delete':
|
|
954
|
-
return await this.executeDelDocDirect(job.collection, job.payload._id as _ttid)
|
|
955
|
-
default:
|
|
956
|
-
throw new Error(`Unsupported queued write operation: ${job.operation}`)
|
|
957
|
-
}
|
|
505
|
+
const { _id, doc } = await this.prepareInsert(collection, data)
|
|
506
|
+
await this.executePutDataDirect(collection, _id, doc)
|
|
507
|
+
return _id
|
|
958
508
|
}
|
|
959
509
|
|
|
960
510
|
/**
|
|
@@ -964,41 +514,17 @@ export default class Fylo {
|
|
|
964
514
|
* @param oldDoc The old document data.
|
|
965
515
|
* @returns The number of documents patched.
|
|
966
516
|
*/
|
|
967
|
-
async patchDoc<T extends Record<string, any>>(
|
|
968
|
-
collection: string,
|
|
969
|
-
newDoc: Record<_ttid, Partial<T>>,
|
|
970
|
-
oldDoc?: Record<_ttid, T>
|
|
971
|
-
): Promise<_ttid>
|
|
972
|
-
async patchDoc<T extends Record<string, any>>(
|
|
973
|
-
collection: string,
|
|
974
|
-
newDoc: Record<_ttid, Partial<T>>,
|
|
975
|
-
oldDoc: Record<_ttid, T> | undefined,
|
|
976
|
-
options: { wait?: true; timeoutMs?: number }
|
|
977
|
-
): Promise<_ttid>
|
|
978
|
-
async patchDoc<T extends Record<string, any>>(
|
|
979
|
-
collection: string,
|
|
980
|
-
newDoc: Record<_ttid, Partial<T>>,
|
|
981
|
-
oldDoc: Record<_ttid, T> | undefined,
|
|
982
|
-
options: { wait: false; timeoutMs?: number }
|
|
983
|
-
): Promise<QueuedWriteResult>
|
|
984
517
|
async patchDoc<T extends Record<string, any>>(
|
|
985
518
|
collection: string,
|
|
986
519
|
newDoc: Record<_ttid, Partial<T>>,
|
|
987
520
|
oldDoc: Record<_ttid, T> = {},
|
|
988
521
|
options: { wait?: boolean; timeoutMs?: number } = {}
|
|
989
|
-
): Promise<_ttid
|
|
990
|
-
if (
|
|
991
|
-
|
|
992
|
-
throw new Error('wait:false is not supported in s3-files engine')
|
|
993
|
-
const _id = await this.executePatchDocDirect(collection, newDoc, oldDoc)
|
|
994
|
-
return _id
|
|
522
|
+
): Promise<_ttid> {
|
|
523
|
+
if (options.wait === false) {
|
|
524
|
+
this.unsupportedLegacyApi('patchDoc(..., { wait: false })')
|
|
995
525
|
}
|
|
996
|
-
const queued = await this.queuePatchDoc(collection, newDoc, oldDoc)
|
|
997
526
|
|
|
998
|
-
return await this.
|
|
999
|
-
const job = await this.getJobStatus(queued.jobId)
|
|
1000
|
-
return (job?.docId ?? queued.docId) as _ttid
|
|
1001
|
-
})
|
|
527
|
+
return await this.executePatchDocDirect(collection, newDoc, oldDoc)
|
|
1002
528
|
}
|
|
1003
529
|
|
|
1004
530
|
/**
|
|
@@ -1013,73 +539,19 @@ export default class Fylo {
|
|
|
1013
539
|
) {
|
|
1014
540
|
await Fylo.loadEncryption(collection)
|
|
1015
541
|
|
|
1016
|
-
const processDoc = (doc: Record<_ttid, T>, updateSchema: _storeUpdate<T>) => {
|
|
1017
|
-
for (const _id in doc)
|
|
1018
|
-
return this.patchDoc(collection, { [_id]: updateSchema.$set }, doc)
|
|
1019
|
-
|
|
1020
|
-
return
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
542
|
let count = 0
|
|
1024
|
-
|
|
1025
543
|
const promises: Promise<_ttid>[] = []
|
|
1026
544
|
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
await Promise.all(promises)
|
|
1039
|
-
return count
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
let finished = false
|
|
1043
|
-
|
|
1044
|
-
const exprs = await Query.getExprs(collection, updateSchema.$where ?? {})
|
|
1045
|
-
|
|
1046
|
-
if (exprs.length === 1 && exprs[0] === `**/*`) {
|
|
1047
|
-
for (const doc of await Fylo.allDocs<T>(collection, updateSchema.$where)) {
|
|
1048
|
-
const promise = processDoc(doc, updateSchema)
|
|
1049
|
-
|
|
1050
|
-
if (promise) {
|
|
1051
|
-
promises.push(promise)
|
|
545
|
+
for await (const value of this.findDocs<T>(collection, updateSchema.$where).collect()) {
|
|
546
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
547
|
+
const [_id, current] = Object.entries(value as Record<_ttid, T>)[0] ?? []
|
|
548
|
+
if (_id && current) {
|
|
549
|
+
promises.push(
|
|
550
|
+
this.patchDoc(collection, { [_id]: updateSchema.$set }, { [_id]: current })
|
|
551
|
+
)
|
|
1052
552
|
count++
|
|
1053
553
|
}
|
|
1054
554
|
}
|
|
1055
|
-
} else {
|
|
1056
|
-
const iter = Dir.searchDocs<T>(
|
|
1057
|
-
collection,
|
|
1058
|
-
exprs,
|
|
1059
|
-
{
|
|
1060
|
-
updated: updateSchema?.$where?.$updated,
|
|
1061
|
-
created: updateSchema?.$where?.$created
|
|
1062
|
-
},
|
|
1063
|
-
{ listen: false, skip: false }
|
|
1064
|
-
)
|
|
1065
|
-
|
|
1066
|
-
do {
|
|
1067
|
-
const { value, done } = await iter.next({ count })
|
|
1068
|
-
|
|
1069
|
-
if (value === undefined && !done) continue
|
|
1070
|
-
|
|
1071
|
-
if (done) {
|
|
1072
|
-
finished = true
|
|
1073
|
-
break
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
const promise = processDoc(value as Record<_ttid, T>, updateSchema)
|
|
1077
|
-
|
|
1078
|
-
if (promise) {
|
|
1079
|
-
promises.push(promise)
|
|
1080
|
-
count++
|
|
1081
|
-
}
|
|
1082
|
-
} while (!finished)
|
|
1083
555
|
}
|
|
1084
556
|
|
|
1085
557
|
await Promise.all(promises)
|
|
@@ -1093,31 +565,16 @@ export default class Fylo {
|
|
|
1093
565
|
* @param _id The ID of the document.
|
|
1094
566
|
* @returns The number of documents deleted.
|
|
1095
567
|
*/
|
|
1096
|
-
async delDoc(collection: string, _id: _ttid): Promise<void>
|
|
1097
|
-
async delDoc(
|
|
1098
|
-
collection: string,
|
|
1099
|
-
_id: _ttid,
|
|
1100
|
-
options: { wait?: true; timeoutMs?: number }
|
|
1101
|
-
): Promise<void>
|
|
1102
|
-
async delDoc(
|
|
1103
|
-
collection: string,
|
|
1104
|
-
_id: _ttid,
|
|
1105
|
-
options: { wait: false; timeoutMs?: number }
|
|
1106
|
-
): Promise<QueuedWriteResult>
|
|
1107
568
|
async delDoc(
|
|
1108
569
|
collection: string,
|
|
1109
570
|
_id: _ttid,
|
|
1110
571
|
options: { wait?: boolean; timeoutMs?: number } = {}
|
|
1111
|
-
): Promise<void
|
|
1112
|
-
if (
|
|
1113
|
-
|
|
1114
|
-
throw new Error('wait:false is not supported in s3-files engine')
|
|
1115
|
-
await this.executeDelDocDirect(collection, _id)
|
|
1116
|
-
return
|
|
572
|
+
): Promise<void> {
|
|
573
|
+
if (options.wait === false) {
|
|
574
|
+
this.unsupportedLegacyApi('delDoc(..., { wait: false })')
|
|
1117
575
|
}
|
|
1118
|
-
const queued = await this.queueDelDoc(collection, _id)
|
|
1119
576
|
|
|
1120
|
-
await this.
|
|
577
|
+
await this.executeDelDocDirect(collection, _id)
|
|
1121
578
|
}
|
|
1122
579
|
|
|
1123
580
|
/**
|
|
@@ -1132,73 +589,19 @@ export default class Fylo {
|
|
|
1132
589
|
) {
|
|
1133
590
|
await Fylo.loadEncryption(collection)
|
|
1134
591
|
|
|
1135
|
-
const processDoc = (doc: Record<_ttid, T>) => {
|
|
1136
|
-
for (const _id in doc) {
|
|
1137
|
-
if (TTID.isTTID(_id)) {
|
|
1138
|
-
return this.delDoc(collection, _id)
|
|
1139
|
-
}
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
return
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
592
|
let count = 0
|
|
1146
|
-
|
|
1147
593
|
const promises: Promise<void>[] = []
|
|
1148
594
|
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
await Promise.all(promises)
|
|
1161
|
-
return count
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
let finished = false
|
|
1165
|
-
|
|
1166
|
-
const exprs = await Query.getExprs(collection, deleteSchema ?? {})
|
|
1167
|
-
|
|
1168
|
-
if (exprs.length === 1 && exprs[0] === `**/*`) {
|
|
1169
|
-
for (const doc of await Fylo.allDocs<T>(collection, deleteSchema)) {
|
|
1170
|
-
const promise = processDoc(doc)
|
|
1171
|
-
|
|
1172
|
-
if (promise) {
|
|
1173
|
-
promises.push(promise)
|
|
595
|
+
for await (const value of this.findDocs<T>(collection, deleteSchema).collect()) {
|
|
596
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
597
|
+
const _id = Object.keys(value as Record<_ttid, T>).find((docId) =>
|
|
598
|
+
TTID.isTTID(docId)
|
|
599
|
+
)
|
|
600
|
+
if (_id) {
|
|
601
|
+
promises.push(this.delDoc(collection, _id))
|
|
1174
602
|
count++
|
|
1175
603
|
}
|
|
1176
604
|
}
|
|
1177
|
-
} else {
|
|
1178
|
-
const iter = Dir.searchDocs<T>(
|
|
1179
|
-
collection,
|
|
1180
|
-
exprs,
|
|
1181
|
-
{ updated: deleteSchema?.$updated, created: deleteSchema?.$created },
|
|
1182
|
-
{ listen: false, skip: false }
|
|
1183
|
-
)
|
|
1184
|
-
|
|
1185
|
-
do {
|
|
1186
|
-
const { value, done } = await iter.next({ count })
|
|
1187
|
-
|
|
1188
|
-
if (value === undefined && !done) continue
|
|
1189
|
-
|
|
1190
|
-
if (done) {
|
|
1191
|
-
finished = true
|
|
1192
|
-
break
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
const promise = processDoc(value as Record<_ttid, T>)
|
|
1196
|
-
|
|
1197
|
-
if (promise) {
|
|
1198
|
-
promises.push(promise)
|
|
1199
|
-
count++
|
|
1200
|
-
}
|
|
1201
|
-
} while (!finished)
|
|
1202
605
|
}
|
|
1203
606
|
|
|
1204
607
|
await Promise.all(promises)
|
|
@@ -1206,28 +609,6 @@ export default class Fylo {
|
|
|
1206
609
|
return count
|
|
1207
610
|
}
|
|
1208
611
|
|
|
1209
|
-
private static selectValues<T extends Record<string, any>>(selection: Array<keyof T>, data: T) {
|
|
1210
|
-
for (const field in data) {
|
|
1211
|
-
if (!selection.includes(field as keyof T)) delete data[field]
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
return data
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
private static renameFields<T extends Record<string, any>>(
|
|
1218
|
-
rename: Record<keyof T, string>,
|
|
1219
|
-
data: T
|
|
1220
|
-
) {
|
|
1221
|
-
for (const field in data) {
|
|
1222
|
-
if (rename[field]) {
|
|
1223
|
-
data[rename[field]] = data[field]
|
|
1224
|
-
delete data[field]
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
|
-
return data
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
612
|
/**
|
|
1232
613
|
* Joins documents from two collections.
|
|
1233
614
|
* @param join The join schema.
|
|
@@ -1236,272 +617,7 @@ export default class Fylo {
|
|
|
1236
617
|
static async joinDocs<T extends Record<string, any>, U extends Record<string, any>>(
|
|
1237
618
|
join: _join<T, U>
|
|
1238
619
|
) {
|
|
1239
|
-
|
|
1240
|
-
return await Fylo.defaultS3Files.joinDocs(join)
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
const docs: Record<`${_ttid}, ${_ttid}`, T | U | (T & U) | (Partial<T> & Partial<U>)> = {}
|
|
1244
|
-
|
|
1245
|
-
const compareFields = async (
|
|
1246
|
-
leftField: keyof T,
|
|
1247
|
-
rightField: keyof U,
|
|
1248
|
-
compare: (leftVal: string, rightVal: string) => boolean
|
|
1249
|
-
) => {
|
|
1250
|
-
if (join.$leftCollection === join.$rightCollection)
|
|
1251
|
-
throw new Error('Left and right collections cannot be the same')
|
|
1252
|
-
|
|
1253
|
-
let leftToken: string | undefined
|
|
1254
|
-
const leftFieldIndexes: string[] = []
|
|
1255
|
-
|
|
1256
|
-
do {
|
|
1257
|
-
const leftData = await S3.list(join.$leftCollection, {
|
|
1258
|
-
prefix: String(leftField)
|
|
1259
|
-
})
|
|
1260
|
-
|
|
1261
|
-
if (!leftData.contents) break
|
|
1262
|
-
|
|
1263
|
-
leftFieldIndexes.push(...leftData.contents!.map((content) => content.key!))
|
|
1264
|
-
|
|
1265
|
-
leftToken = leftData.nextContinuationToken
|
|
1266
|
-
} while (leftToken !== undefined)
|
|
1267
|
-
|
|
1268
|
-
let rightToken: string | undefined
|
|
1269
|
-
const rightFieldIndexes: string[] = []
|
|
1270
|
-
|
|
1271
|
-
do {
|
|
1272
|
-
const rightData = await S3.list(join.$rightCollection, {
|
|
1273
|
-
prefix: String(rightField)
|
|
1274
|
-
})
|
|
1275
|
-
|
|
1276
|
-
if (!rightData.contents) break
|
|
1277
|
-
|
|
1278
|
-
rightFieldIndexes.push(...rightData.contents!.map((content) => content.key!))
|
|
1279
|
-
|
|
1280
|
-
rightToken = rightData.nextContinuationToken
|
|
1281
|
-
} while (rightToken !== undefined)
|
|
1282
|
-
|
|
1283
|
-
for (const leftIdx of leftFieldIndexes) {
|
|
1284
|
-
const leftSegs = leftIdx.split('/')
|
|
1285
|
-
const left_id = leftSegs.pop()! as _ttid
|
|
1286
|
-
const leftVal = leftSegs.pop()!
|
|
1287
|
-
|
|
1288
|
-
const leftCollection = join.$leftCollection
|
|
1289
|
-
|
|
1290
|
-
const allVals = new Set<string>()
|
|
1291
|
-
|
|
1292
|
-
for (const rightIdx of rightFieldIndexes) {
|
|
1293
|
-
const rightSegs = rightIdx.split('/')
|
|
1294
|
-
const right_id = rightSegs.pop()! as _ttid
|
|
1295
|
-
const rightVal = rightSegs.pop()!
|
|
1296
|
-
|
|
1297
|
-
const rightCollection = join.$rightCollection
|
|
1298
|
-
|
|
1299
|
-
if (compare(rightVal, leftVal) && !allVals.has(rightVal)) {
|
|
1300
|
-
allVals.add(rightVal)
|
|
1301
|
-
|
|
1302
|
-
switch (join.$mode) {
|
|
1303
|
-
case 'inner':
|
|
1304
|
-
docs[`${left_id}, ${right_id}`] = {
|
|
1305
|
-
[leftField]: Dir.parseValue(leftVal),
|
|
1306
|
-
[rightField]: Dir.parseValue(rightVal)
|
|
1307
|
-
} as Partial<T> & Partial<U>
|
|
1308
|
-
break
|
|
1309
|
-
case 'left':
|
|
1310
|
-
const leftDoc = await this.getDoc<T>(leftCollection, left_id).once()
|
|
1311
|
-
if (Object.keys(leftDoc).length > 0) {
|
|
1312
|
-
let leftData = leftDoc[left_id]
|
|
1313
|
-
if (join.$select)
|
|
1314
|
-
leftData = this.selectValues<T>(
|
|
1315
|
-
join.$select as Array<keyof T>,
|
|
1316
|
-
leftData
|
|
1317
|
-
)
|
|
1318
|
-
if (join.$rename)
|
|
1319
|
-
leftData = this.renameFields<T>(join.$rename, leftData)
|
|
1320
|
-
docs[`${left_id}, ${right_id}`] = leftData as T
|
|
1321
|
-
}
|
|
1322
|
-
break
|
|
1323
|
-
case 'right':
|
|
1324
|
-
const rightDoc = await this.getDoc<U>(
|
|
1325
|
-
rightCollection,
|
|
1326
|
-
right_id
|
|
1327
|
-
).once()
|
|
1328
|
-
if (Object.keys(rightDoc).length > 0) {
|
|
1329
|
-
let rightData = rightDoc[right_id]
|
|
1330
|
-
if (join.$select)
|
|
1331
|
-
rightData = this.selectValues<U>(
|
|
1332
|
-
join.$select as Array<keyof U>,
|
|
1333
|
-
rightData
|
|
1334
|
-
)
|
|
1335
|
-
if (join.$rename)
|
|
1336
|
-
rightData = this.renameFields<U>(join.$rename, rightData)
|
|
1337
|
-
docs[`${left_id}, ${right_id}`] = rightData as U
|
|
1338
|
-
}
|
|
1339
|
-
break
|
|
1340
|
-
case 'outer':
|
|
1341
|
-
let leftFullData: T = {} as T
|
|
1342
|
-
let rightFullData: U = {} as U
|
|
1343
|
-
|
|
1344
|
-
const leftFullDoc = await this.getDoc<T>(
|
|
1345
|
-
leftCollection,
|
|
1346
|
-
left_id
|
|
1347
|
-
).once()
|
|
1348
|
-
|
|
1349
|
-
if (Object.keys(leftFullDoc).length > 0) {
|
|
1350
|
-
let leftData = leftFullDoc[left_id]
|
|
1351
|
-
if (join.$select)
|
|
1352
|
-
leftData = this.selectValues<T>(
|
|
1353
|
-
join.$select as Array<keyof T>,
|
|
1354
|
-
leftData
|
|
1355
|
-
)
|
|
1356
|
-
if (join.$rename)
|
|
1357
|
-
leftData = this.renameFields<T>(join.$rename, leftData)
|
|
1358
|
-
leftFullData = { ...leftData, ...leftFullData } as T
|
|
1359
|
-
}
|
|
1360
|
-
|
|
1361
|
-
const rightFullDoc = await this.getDoc<U>(
|
|
1362
|
-
rightCollection,
|
|
1363
|
-
right_id
|
|
1364
|
-
).once()
|
|
1365
|
-
|
|
1366
|
-
if (Object.keys(rightFullDoc).length > 0) {
|
|
1367
|
-
let rightData = rightFullDoc[right_id]
|
|
1368
|
-
if (join.$select)
|
|
1369
|
-
rightData = this.selectValues<U>(
|
|
1370
|
-
join.$select as Array<keyof U>,
|
|
1371
|
-
rightData
|
|
1372
|
-
)
|
|
1373
|
-
if (join.$rename)
|
|
1374
|
-
rightData = this.renameFields<U>(join.$rename, rightData)
|
|
1375
|
-
rightFullData = { ...rightData, ...rightFullData } as U
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
docs[`${left_id}, ${right_id}`] = {
|
|
1379
|
-
...leftFullData,
|
|
1380
|
-
...rightFullData
|
|
1381
|
-
} as T & U
|
|
1382
|
-
break
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
if (join.$limit && Object.keys(docs).length === join.$limit) break
|
|
1386
|
-
}
|
|
1387
|
-
}
|
|
1388
|
-
|
|
1389
|
-
if (join.$limit && Object.keys(docs).length === join.$limit) break
|
|
1390
|
-
}
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
|
-
for (const field in join.$on) {
|
|
1394
|
-
if (join.$on[field]!.$eq)
|
|
1395
|
-
await compareFields(
|
|
1396
|
-
field,
|
|
1397
|
-
join.$on[field]!.$eq,
|
|
1398
|
-
(leftVal, rightVal) => leftVal === rightVal
|
|
1399
|
-
)
|
|
1400
|
-
|
|
1401
|
-
if (join.$on[field]!.$ne)
|
|
1402
|
-
await compareFields(
|
|
1403
|
-
field,
|
|
1404
|
-
join.$on[field]!.$ne,
|
|
1405
|
-
(leftVal, rightVal) => leftVal !== rightVal
|
|
1406
|
-
)
|
|
1407
|
-
|
|
1408
|
-
if (join.$on[field]!.$gt)
|
|
1409
|
-
await compareFields(
|
|
1410
|
-
field,
|
|
1411
|
-
join.$on[field]!.$gt,
|
|
1412
|
-
(leftVal, rightVal) => Number(leftVal) > Number(rightVal)
|
|
1413
|
-
)
|
|
1414
|
-
|
|
1415
|
-
if (join.$on[field]!.$lt)
|
|
1416
|
-
await compareFields(
|
|
1417
|
-
field,
|
|
1418
|
-
join.$on[field]!.$lt,
|
|
1419
|
-
(leftVal, rightVal) => Number(leftVal) < Number(rightVal)
|
|
1420
|
-
)
|
|
1421
|
-
|
|
1422
|
-
if (join.$on[field]!.$gte)
|
|
1423
|
-
await compareFields(
|
|
1424
|
-
field,
|
|
1425
|
-
join.$on[field]!.$gte,
|
|
1426
|
-
(leftVal, rightVal) => Number(leftVal) >= Number(rightVal)
|
|
1427
|
-
)
|
|
1428
|
-
|
|
1429
|
-
if (join.$on[field]!.$lte)
|
|
1430
|
-
await compareFields(
|
|
1431
|
-
field,
|
|
1432
|
-
join.$on[field]!.$lte,
|
|
1433
|
-
(leftVal, rightVal) => Number(leftVal) <= Number(rightVal)
|
|
1434
|
-
)
|
|
1435
|
-
}
|
|
1436
|
-
|
|
1437
|
-
if (join.$groupby) {
|
|
1438
|
-
const groupedDocs: Record<string, Record<string, Partial<T | U>>> = {} as Record<
|
|
1439
|
-
string,
|
|
1440
|
-
Record<string, Partial<T | U>>
|
|
1441
|
-
>
|
|
1442
|
-
|
|
1443
|
-
for (const ids in docs) {
|
|
1444
|
-
const data = docs[ids as `${_ttid}, ${_ttid}`]
|
|
1445
|
-
|
|
1446
|
-
// @ts-expect-error - Object.groupBy not yet in TS lib types
|
|
1447
|
-
const grouping = Object.groupBy([data], (elem) => elem[join.$groupby!])
|
|
1448
|
-
|
|
1449
|
-
for (const group in grouping) {
|
|
1450
|
-
if (groupedDocs[group]) groupedDocs[group][ids] = data
|
|
1451
|
-
else groupedDocs[group] = { [ids]: data }
|
|
1452
|
-
}
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
|
-
if (join.$onlyIds) {
|
|
1456
|
-
const groupedIds: Record<string, _ttid[]> = {}
|
|
1457
|
-
|
|
1458
|
-
for (const group in groupedDocs) {
|
|
1459
|
-
const doc = groupedDocs[group]
|
|
1460
|
-
groupedIds[group] = Object.keys(doc).flat()
|
|
1461
|
-
}
|
|
1462
|
-
|
|
1463
|
-
return groupedIds
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
|
-
return groupedDocs
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
if (join.$onlyIds) return Array.from(new Set(Object.keys(docs).flat()))
|
|
1470
|
-
|
|
1471
|
-
return docs
|
|
1472
|
-
}
|
|
1473
|
-
|
|
1474
|
-
private static async allDocs<T extends Record<string, any>>(
|
|
1475
|
-
collection: string,
|
|
1476
|
-
query?: _storeQuery<T>
|
|
1477
|
-
) {
|
|
1478
|
-
if (Fylo.defaultEngineKind() === 's3-files') {
|
|
1479
|
-
const results: Array<Record<_ttid, T>> = []
|
|
1480
|
-
for await (const data of Fylo.defaultS3Files.findDocs<T>(collection, query).collect()) {
|
|
1481
|
-
if (typeof data === 'object' && !Array.isArray(data))
|
|
1482
|
-
results.push(data as Record<_ttid, T>)
|
|
1483
|
-
}
|
|
1484
|
-
return results
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
const res = await S3.list(collection, {
|
|
1488
|
-
delimiter: '/',
|
|
1489
|
-
maxKeys: !query || !query.$limit ? undefined : query.$limit
|
|
1490
|
-
})
|
|
1491
|
-
|
|
1492
|
-
const ids =
|
|
1493
|
-
(res.commonPrefixes
|
|
1494
|
-
?.map((item) => item.prefix!.split('/')[0]!)
|
|
1495
|
-
.filter((key) => TTID.isTTID(key)) as _ttid[]) ?? ([] as _ttid[])
|
|
1496
|
-
|
|
1497
|
-
const docs = await Promise.allSettled(
|
|
1498
|
-
ids.map((id) => Fylo.getDoc<T>(collection, id).once())
|
|
1499
|
-
)
|
|
1500
|
-
|
|
1501
|
-
return docs
|
|
1502
|
-
.filter((item) => item.status === 'fulfilled')
|
|
1503
|
-
.map((item) => item.value)
|
|
1504
|
-
.filter((doc) => Object.keys(doc).length > 0)
|
|
620
|
+
return await Fylo.defaultEngine.joinDocs(join)
|
|
1505
621
|
}
|
|
1506
622
|
|
|
1507
623
|
/**
|
|
@@ -1511,217 +627,6 @@ export default class Fylo {
|
|
|
1511
627
|
* @returns The found documents.
|
|
1512
628
|
*/
|
|
1513
629
|
static findDocs<T extends Record<string, any>>(collection: string, query?: _storeQuery<T>) {
|
|
1514
|
-
|
|
1515
|
-
return Fylo.defaultS3Files.findDocs<T>(collection, query)
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
|
-
const processDoc = (doc: Record<_ttid, T>, query?: _storeQuery<T>) => {
|
|
1519
|
-
if (Object.keys(doc).length > 0) {
|
|
1520
|
-
// Post-filter for operators that cannot be expressed as globs ($ne, $gt, $gte, $lt, $lte).
|
|
1521
|
-
// $ops use OR semantics: a document passes if it matches at least one op.
|
|
1522
|
-
if (query?.$ops) {
|
|
1523
|
-
for (const [_id, data] of Object.entries(doc)) {
|
|
1524
|
-
let matchesAny = false
|
|
1525
|
-
for (const op of query.$ops) {
|
|
1526
|
-
let opMatches = true
|
|
1527
|
-
for (const col in op) {
|
|
1528
|
-
const val = (data as Record<string, unknown>)[col]
|
|
1529
|
-
const cond = op[col as keyof T]!
|
|
1530
|
-
if (cond.$ne !== undefined && val == cond.$ne) {
|
|
1531
|
-
opMatches = false
|
|
1532
|
-
break
|
|
1533
|
-
}
|
|
1534
|
-
if (cond.$gt !== undefined && !(Number(val) > cond.$gt)) {
|
|
1535
|
-
opMatches = false
|
|
1536
|
-
break
|
|
1537
|
-
}
|
|
1538
|
-
if (cond.$gte !== undefined && !(Number(val) >= cond.$gte)) {
|
|
1539
|
-
opMatches = false
|
|
1540
|
-
break
|
|
1541
|
-
}
|
|
1542
|
-
if (cond.$lt !== undefined && !(Number(val) < cond.$lt)) {
|
|
1543
|
-
opMatches = false
|
|
1544
|
-
break
|
|
1545
|
-
}
|
|
1546
|
-
if (cond.$lte !== undefined && !(Number(val) <= cond.$lte)) {
|
|
1547
|
-
opMatches = false
|
|
1548
|
-
break
|
|
1549
|
-
}
|
|
1550
|
-
}
|
|
1551
|
-
if (opMatches) {
|
|
1552
|
-
matchesAny = true
|
|
1553
|
-
break
|
|
1554
|
-
}
|
|
1555
|
-
}
|
|
1556
|
-
if (!matchesAny) delete doc[_id as _ttid]
|
|
1557
|
-
}
|
|
1558
|
-
if (Object.keys(doc).length === 0) return
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
|
-
for (let [_id, data] of Object.entries(doc)) {
|
|
1562
|
-
if (query && query.$select && query.$select.length > 0) {
|
|
1563
|
-
data = this.selectValues<T>(query.$select as Array<keyof T>, data)
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
|
-
if (query && query.$rename) data = this.renameFields<T>(query.$rename, data)
|
|
1567
|
-
|
|
1568
|
-
doc[_id] = data
|
|
1569
|
-
}
|
|
1570
|
-
|
|
1571
|
-
if (query && query.$groupby) {
|
|
1572
|
-
const docGroup: Record<string, Record<string, Partial<T>>> = {}
|
|
1573
|
-
|
|
1574
|
-
for (const [id, data] of Object.entries(doc)) {
|
|
1575
|
-
const groupValue = data[query.$groupby] as string
|
|
1576
|
-
|
|
1577
|
-
if (groupValue) {
|
|
1578
|
-
delete data[query.$groupby]
|
|
1579
|
-
|
|
1580
|
-
docGroup[groupValue] = {
|
|
1581
|
-
[id]: data as Partial<T>
|
|
1582
|
-
} as Record<_ttid, Partial<T>>
|
|
1583
|
-
}
|
|
1584
|
-
}
|
|
1585
|
-
|
|
1586
|
-
if (query && query.$onlyIds) {
|
|
1587
|
-
for (const [groupValue, doc] of Object.entries(docGroup)) {
|
|
1588
|
-
for (const id in doc as Record<_ttid, T>) {
|
|
1589
|
-
// @ts-expect-error - dynamic key assignment on grouped object
|
|
1590
|
-
docGroup[groupValue][id] = null
|
|
1591
|
-
}
|
|
1592
|
-
}
|
|
1593
|
-
|
|
1594
|
-
return docGroup
|
|
1595
|
-
}
|
|
1596
|
-
|
|
1597
|
-
return docGroup
|
|
1598
|
-
}
|
|
1599
|
-
|
|
1600
|
-
if (query && query.$onlyIds) {
|
|
1601
|
-
return Object.keys(doc).shift()
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
return doc
|
|
1605
|
-
}
|
|
1606
|
-
|
|
1607
|
-
return
|
|
1608
|
-
}
|
|
1609
|
-
|
|
1610
|
-
return {
|
|
1611
|
-
/**
|
|
1612
|
-
* Async iterator (listener) for the documents.
|
|
1613
|
-
*/
|
|
1614
|
-
async *[Symbol.asyncIterator]() {
|
|
1615
|
-
await Fylo.loadEncryption(collection)
|
|
1616
|
-
|
|
1617
|
-
const expression = await Query.getExprs(collection, query ?? {})
|
|
1618
|
-
|
|
1619
|
-
if (expression.length === 1 && expression[0] === `**/*`) {
|
|
1620
|
-
for (const doc of await Fylo.allDocs<T>(collection, query))
|
|
1621
|
-
yield processDoc(doc, query)
|
|
1622
|
-
}
|
|
1623
|
-
|
|
1624
|
-
let count = 0
|
|
1625
|
-
let finished = false
|
|
1626
|
-
|
|
1627
|
-
const iter = Dir.searchDocs<T>(
|
|
1628
|
-
collection,
|
|
1629
|
-
expression,
|
|
1630
|
-
{ updated: query?.$updated, created: query?.$created },
|
|
1631
|
-
{ listen: true, skip: true }
|
|
1632
|
-
)
|
|
1633
|
-
|
|
1634
|
-
do {
|
|
1635
|
-
const { value, done } = await iter.next({ count, limit: query?.$limit })
|
|
1636
|
-
|
|
1637
|
-
if (value === undefined && !done) continue
|
|
1638
|
-
|
|
1639
|
-
if (done) {
|
|
1640
|
-
finished = true
|
|
1641
|
-
break
|
|
1642
|
-
}
|
|
1643
|
-
|
|
1644
|
-
const result = processDoc(value as Record<_ttid, T>, query)
|
|
1645
|
-
if (result !== undefined) {
|
|
1646
|
-
count++
|
|
1647
|
-
yield result
|
|
1648
|
-
}
|
|
1649
|
-
} while (!finished)
|
|
1650
|
-
},
|
|
1651
|
-
|
|
1652
|
-
/**
|
|
1653
|
-
* Async iterator for the documents.
|
|
1654
|
-
*/
|
|
1655
|
-
async *collect() {
|
|
1656
|
-
await Fylo.loadEncryption(collection)
|
|
1657
|
-
|
|
1658
|
-
const expression = await Query.getExprs(collection, query ?? {})
|
|
1659
|
-
|
|
1660
|
-
if (expression.length === 1 && expression[0] === `**/*`) {
|
|
1661
|
-
for (const doc of await Fylo.allDocs<T>(collection, query))
|
|
1662
|
-
yield processDoc(doc, query)
|
|
1663
|
-
} else {
|
|
1664
|
-
let count = 0
|
|
1665
|
-
let finished = false
|
|
1666
|
-
|
|
1667
|
-
const iter = Dir.searchDocs<T>(
|
|
1668
|
-
collection,
|
|
1669
|
-
expression,
|
|
1670
|
-
{ updated: query?.$updated, created: query?.$created },
|
|
1671
|
-
{ listen: false, skip: false }
|
|
1672
|
-
)
|
|
1673
|
-
|
|
1674
|
-
do {
|
|
1675
|
-
const { value, done } = await iter.next({ count, limit: query?.$limit })
|
|
1676
|
-
|
|
1677
|
-
if (value === undefined && !done) continue
|
|
1678
|
-
|
|
1679
|
-
if (done) {
|
|
1680
|
-
finished = true
|
|
1681
|
-
break
|
|
1682
|
-
}
|
|
1683
|
-
|
|
1684
|
-
const result = processDoc(value as Record<_ttid, T>, query)
|
|
1685
|
-
if (result !== undefined) {
|
|
1686
|
-
count++
|
|
1687
|
-
yield result
|
|
1688
|
-
}
|
|
1689
|
-
} while (!finished)
|
|
1690
|
-
}
|
|
1691
|
-
},
|
|
1692
|
-
|
|
1693
|
-
/**
|
|
1694
|
-
* Async iterator (listener) for the document's deletion.
|
|
1695
|
-
*/
|
|
1696
|
-
async *onDelete() {
|
|
1697
|
-
await Fylo.loadEncryption(collection)
|
|
1698
|
-
|
|
1699
|
-
let count = 0
|
|
1700
|
-
let finished = false
|
|
1701
|
-
|
|
1702
|
-
const iter = Dir.searchDocs<T>(
|
|
1703
|
-
collection,
|
|
1704
|
-
await Query.getExprs(collection, query ?? {}),
|
|
1705
|
-
{},
|
|
1706
|
-
{ listen: true, skip: true },
|
|
1707
|
-
true
|
|
1708
|
-
)
|
|
1709
|
-
|
|
1710
|
-
do {
|
|
1711
|
-
const { value, done } = await iter.next({ count })
|
|
1712
|
-
|
|
1713
|
-
if (value === undefined && !done) continue
|
|
1714
|
-
|
|
1715
|
-
if (done) {
|
|
1716
|
-
finished = true
|
|
1717
|
-
break
|
|
1718
|
-
}
|
|
1719
|
-
|
|
1720
|
-
if (value) yield value as _ttid
|
|
1721
|
-
} while (!finished)
|
|
1722
|
-
}
|
|
1723
|
-
}
|
|
630
|
+
return Fylo.defaultEngine.findDocs<T>(collection, query)
|
|
1724
631
|
}
|
|
1725
632
|
}
|
|
1726
|
-
|
|
1727
|
-
export { migrateLegacyS3ToS3Files } from './migrate'
|