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