@delma/fylo 2.1.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +27 -0
  2. package/dist/adapters/cipher.js +155 -0
  3. package/dist/adapters/cipher.js.map +1 -0
  4. package/dist/core/collection.js +6 -0
  5. package/dist/core/collection.js.map +1 -0
  6. package/{src/core/directory.ts → dist/core/directory.js} +28 -35
  7. package/dist/core/directory.js.map +1 -0
  8. package/dist/core/doc-id.js +15 -0
  9. package/dist/core/doc-id.js.map +1 -0
  10. package/dist/core/extensions.js +16 -0
  11. package/dist/core/extensions.js.map +1 -0
  12. package/dist/core/format.js +355 -0
  13. package/dist/core/format.js.map +1 -0
  14. package/dist/core/parser.js +764 -0
  15. package/dist/core/parser.js.map +1 -0
  16. package/dist/core/query.js +47 -0
  17. package/dist/core/query.js.map +1 -0
  18. package/dist/engines/s3-files/documents.js +62 -0
  19. package/dist/engines/s3-files/documents.js.map +1 -0
  20. package/dist/engines/s3-files/filesystem.js +165 -0
  21. package/dist/engines/s3-files/filesystem.js.map +1 -0
  22. package/dist/engines/s3-files/query.js +235 -0
  23. package/dist/engines/s3-files/query.js.map +1 -0
  24. package/dist/engines/s3-files/types.js +2 -0
  25. package/dist/engines/s3-files/types.js.map +1 -0
  26. package/dist/engines/s3-files.js +629 -0
  27. package/dist/engines/s3-files.js.map +1 -0
  28. package/dist/engines/types.js +2 -0
  29. package/dist/engines/types.js.map +1 -0
  30. package/dist/index.js +562 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/sync.js +18 -0
  33. package/dist/sync.js.map +1 -0
  34. package/{src → dist}/types/fylo.d.ts +14 -1
  35. package/package.json +2 -2
  36. package/.env.example +0 -16
  37. package/.github/copilot-instructions.md +0 -3
  38. package/.github/prompts/release.prompt.md +0 -10
  39. package/.github/workflows/ci.yml +0 -37
  40. package/.github/workflows/publish.yml +0 -91
  41. package/.prettierrc +0 -7
  42. package/AGENTS.md +0 -3
  43. package/CLAUDE.md +0 -3
  44. package/eslint.config.js +0 -32
  45. package/src/CLI +0 -39
  46. package/src/adapters/cipher.ts +0 -180
  47. package/src/core/collection.ts +0 -5
  48. package/src/core/extensions.ts +0 -21
  49. package/src/core/format.ts +0 -457
  50. package/src/core/parser.ts +0 -901
  51. package/src/core/query.ts +0 -53
  52. package/src/engines/s3-files/documents.ts +0 -65
  53. package/src/engines/s3-files/filesystem.ts +0 -172
  54. package/src/engines/s3-files/query.ts +0 -291
  55. package/src/engines/s3-files/types.ts +0 -42
  56. package/src/engines/s3-files.ts +0 -769
  57. package/src/engines/types.ts +0 -21
  58. package/src/index.ts +0 -632
  59. package/src/sync.ts +0 -58
  60. package/tests/collection/truncate.test.js +0 -36
  61. package/tests/data.js +0 -97
  62. package/tests/helpers/root.js +0 -7
  63. package/tests/integration/aws-s3-files.canary.test.js +0 -22
  64. package/tests/integration/create.test.js +0 -39
  65. package/tests/integration/delete.test.js +0 -97
  66. package/tests/integration/edge-cases.test.js +0 -162
  67. package/tests/integration/encryption.test.js +0 -148
  68. package/tests/integration/export.test.js +0 -46
  69. package/tests/integration/join-modes.test.js +0 -154
  70. package/tests/integration/nested.test.js +0 -144
  71. package/tests/integration/operators.test.js +0 -136
  72. package/tests/integration/read.test.js +0 -123
  73. package/tests/integration/rollback.test.js +0 -30
  74. package/tests/integration/s3-files.performance.test.js +0 -75
  75. package/tests/integration/s3-files.test.js +0 -205
  76. package/tests/integration/sync.test.js +0 -154
  77. package/tests/integration/update.test.js +0 -105
  78. package/tests/mocks/cipher.js +0 -40
  79. package/tests/schemas/album.d.ts +0 -5
  80. package/tests/schemas/album.json +0 -5
  81. package/tests/schemas/comment.d.ts +0 -7
  82. package/tests/schemas/comment.json +0 -7
  83. package/tests/schemas/photo.d.ts +0 -7
  84. package/tests/schemas/photo.json +0 -7
  85. package/tests/schemas/post.d.ts +0 -6
  86. package/tests/schemas/post.json +0 -6
  87. package/tests/schemas/tip.d.ts +0 -7
  88. package/tests/schemas/tip.json +0 -7
  89. package/tests/schemas/todo.d.ts +0 -6
  90. package/tests/schemas/todo.json +0 -6
  91. package/tests/schemas/user.d.ts +0 -23
  92. package/tests/schemas/user.json +0 -23
  93. package/tsconfig.json +0 -21
  94. package/tsconfig.typecheck.json +0 -31
  95. /package/{src → dist}/types/bun-runtime.d.ts +0 -0
  96. /package/{src → dist}/types/index.d.ts +0 -0
  97. /package/{src → dist}/types/node-runtime.d.ts +0 -0
  98. /package/{src → dist}/types/query.d.ts +0 -0
  99. /package/{src → dist}/types/vendor-modules.d.ts +0 -0
@@ -1,21 +0,0 @@
1
- export type FyloStorageEngineKind = 's3-files'
2
-
3
- export interface StorageEngine {
4
- read(path: string): Promise<string>
5
- write(path: string, data: string): Promise<void>
6
- delete(path: string): Promise<void>
7
- list(path: string): Promise<string[]>
8
- mkdir(path: string): Promise<void>
9
- rmdir(path: string): Promise<void>
10
- exists(path: string): Promise<boolean>
11
- }
12
-
13
- export interface LockManager {
14
- acquire(collection: string, docId: _ttid, owner: string, ttlMs?: number): Promise<boolean>
15
- release(collection: string, docId: _ttid, owner: string): Promise<void>
16
- }
17
-
18
- export interface EventBus<T> {
19
- publish(collection: string, event: T): Promise<void>
20
- listen(collection: string): AsyncGenerator<T, void, unknown>
21
- }
package/src/index.ts DELETED
@@ -1,632 +0,0 @@
1
- /* eslint-disable @typescript-eslint/explicit-function-return-type */
2
- import path from 'node:path'
3
- import { Parser } from './core/parser'
4
- import TTID from '@delma/ttid'
5
- import Gen from '@delma/chex'
6
- import { Cipher } from './adapters/cipher'
7
- import { S3FilesEngine } from './engines/s3-files'
8
- import type { FyloOptions } from './sync'
9
- import './core/format'
10
- import './core/extensions'
11
-
12
- export { FyloSyncError } from './sync'
13
- export type {
14
- FyloDeleteSyncEvent,
15
- FyloOptions,
16
- FyloSyncHooks,
17
- FyloSyncMode,
18
- FyloWriteSyncEvent
19
- } from './sync'
20
-
21
- export default class Fylo {
22
- private static LOGGING = process.env.LOGGING
23
-
24
- private static MAX_CPUS = navigator.hardwareConcurrency
25
-
26
- private static readonly STRICT = process.env.STRICT
27
-
28
- private static ttidLock: Promise<void> = Promise.resolve()
29
-
30
- private static readonly SCHEMA_DIR = process.env.SCHEMA_DIR
31
-
32
- /** Collections whose schema `$encrypted` config has already been loaded. */
33
- private static readonly loadedEncryption: Set<string> = new Set()
34
-
35
- private readonly engine: S3FilesEngine
36
-
37
- constructor(options: FyloOptions = {}) {
38
- this.engine = new S3FilesEngine(options.root ?? options.s3FilesRoot ?? Fylo.defaultRoot(), {
39
- sync: options.sync,
40
- syncMode: options.syncMode
41
- })
42
- }
43
-
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
- )
50
- }
51
-
52
- private static get defaultEngine() {
53
- return new S3FilesEngine(Fylo.defaultRoot())
54
- }
55
-
56
- /**
57
- * Executes a SQL query and returns the results.
58
- * @param SQL The SQL query to execute.
59
- * @returns The results of the query.
60
- */
61
- async executeSQL<
62
- T extends Record<string, any>,
63
- U extends Record<string, any> = Record<string, unknown>
64
- >(SQL: string) {
65
- const op = SQL.match(/^(SELECT|INSERT|UPDATE|DELETE|CREATE|DROP)/i)
66
-
67
- if (!op) throw new Error('Missing SQL Operation')
68
-
69
- switch (op.shift()) {
70
- case 'CREATE':
71
- return await this.createCollection(
72
- (Parser.parse(SQL) as _storeDelete<T>).$collection!
73
- )
74
- case 'DROP':
75
- return await this.dropCollection(
76
- (Parser.parse(SQL) as _storeDelete<T>).$collection!
77
- )
78
- case 'SELECT': {
79
- const query = Parser.parse<T>(SQL) as _storeQuery<T>
80
- if (SQL.includes('JOIN')) return await this.joinDocs(query as _join<T, U>)
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)
88
- }
89
-
90
- return docs
91
- }
92
- case 'INSERT': {
93
- const insert = Parser.parse<T>(SQL) as _storeInsert<T>
94
- const insCol = insert.$collection
95
- delete insert.$collection
96
- return await this.putData(insCol!, insert.$values)
97
- }
98
- case 'UPDATE': {
99
- const update = Parser.parse<T>(SQL) as _storeUpdate<T>
100
- const updateCol = update.$collection
101
- delete update.$collection
102
- return await this.patchDocs(updateCol!, update)
103
- }
104
- case 'DELETE': {
105
- const del = Parser.parse<T>(SQL) as _storeDelete<T>
106
- const delCol = del.$collection
107
- delete del.$collection
108
- return await this.delDocs(delCol!, del)
109
- }
110
- default:
111
- throw new Error('Invalid Operation')
112
- }
113
- }
114
-
115
- /**
116
- * Creates a new collection on the configured filesystem root.
117
- * @param collection The name of the collection.
118
- */
119
- static async createCollection(collection: string) {
120
- await Fylo.defaultEngine.createCollection(collection)
121
- }
122
-
123
- /**
124
- * Drops an existing collection from the configured filesystem root.
125
- * @param collection The name of the collection.
126
- */
127
- static async dropCollection(collection: string) {
128
- await Fylo.defaultEngine.dropCollection(collection)
129
- }
130
-
131
- async createCollection(collection: string) {
132
- return await this.engine.createCollection(collection)
133
- }
134
-
135
- async dropCollection(collection: string) {
136
- return await this.engine.dropCollection(collection)
137
- }
138
-
139
- /**
140
- * Loads encrypted field config from a collection's JSON schema if not already loaded.
141
- * Reads the `$encrypted` array from the schema and registers fields with Cipher.
142
- * Auto-configures the Cipher key from `ENCRYPTION_KEY` env var on first use.
143
- */
144
- private static async loadEncryption(collection: string): Promise<void> {
145
- if (Fylo.loadedEncryption.has(collection)) return
146
- Fylo.loadedEncryption.add(collection)
147
-
148
- if (!Fylo.SCHEMA_DIR) return
149
-
150
- try {
151
- const res = await import(`${Fylo.SCHEMA_DIR}/${collection}.json`)
152
- const schema = res.default as Record<string, unknown>
153
- const encrypted = schema.$encrypted
154
-
155
- if (Array.isArray(encrypted) && encrypted.length > 0) {
156
- if (!Cipher.isConfigured()) {
157
- const secret = process.env.ENCRYPTION_KEY
158
- if (!secret)
159
- throw new Error(
160
- 'Schema declares $encrypted fields but ENCRYPTION_KEY env var is not set'
161
- )
162
- if (secret.length < 32)
163
- throw new Error('ENCRYPTION_KEY must be at least 32 characters long')
164
- await Cipher.configure(secret)
165
- }
166
- Cipher.registerFields(collection, encrypted as string[])
167
- }
168
- } catch {
169
- // No schema file found — no encryption for this collection
170
- }
171
- }
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
-
179
- getDoc<T extends Record<string, any>>(collection: string, _id: _ttid, onlyId: boolean = false) {
180
- return this.engine.getDoc<T>(collection, _id, onlyId)
181
- }
182
-
183
- findDocs<T extends Record<string, any>>(collection: string, query?: _storeQuery<T>) {
184
- return this.engine.findDocs<T>(collection, query)
185
- }
186
-
187
- async joinDocs<T extends Record<string, any>, U extends Record<string, any>>(
188
- join: _join<T, U>
189
- ) {
190
- return await this.engine.joinDocs(join)
191
- }
192
-
193
- async *exportBulkData<T extends Record<string, any>>(collection: string) {
194
- yield* this.engine.exportBulkData<T>(collection)
195
- }
196
-
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
- )
201
- }
202
-
203
- async getJobStatus(_jobId: string) {
204
- return this.unsupportedLegacyApi('getJobStatus')
205
- }
206
-
207
- async getDocStatus(_collection: string, _docId: _ttid) {
208
- return this.unsupportedLegacyApi('getDocStatus')
209
- }
210
-
211
- async getDeadLetters(_count: number = 10) {
212
- return this.unsupportedLegacyApi('getDeadLetters')
213
- }
214
-
215
- async getQueueStats() {
216
- return this.unsupportedLegacyApi('getQueueStats')
217
- }
218
-
219
- async replayDeadLetter(_streamId: string) {
220
- return this.unsupportedLegacyApi('replayDeadLetter')
221
- }
222
-
223
- async processQueuedWrites(_count: number = 1, _recover: boolean = false) {
224
- return this.unsupportedLegacyApi('processQueuedWrites')
225
- }
226
-
227
- /**
228
- * Imports data from a URL into a collection.
229
- * @param collection The name of the collection.
230
- * @param url The URL of the data to import.
231
- * @param limit The maximum number of documents to import.
232
- */
233
- async importBulkData<T extends Record<string, any>>(
234
- collection: string,
235
- url: URL,
236
- limit?: number
237
- ) {
238
- const res = await fetch(url)
239
-
240
- if (!res.headers.get('content-type')?.includes('application/json'))
241
- throw new Error('Response is not JSON')
242
-
243
- let count = 0
244
- let batchNum = 0
245
-
246
- const flush = async (batch: T[]) => {
247
- if (!batch.length) return
248
-
249
- const items =
250
- limit && count + batch.length > limit ? batch.slice(0, limit - count) : batch
251
-
252
- batchNum++
253
-
254
- const start = Date.now()
255
- await this.batchPutData(collection, items)
256
- count += items.length
257
-
258
- if (count % 10000 === 0) console.log('Count:', count)
259
-
260
- if (Fylo.LOGGING) {
261
- const bytes = JSON.stringify(items).length
262
- const elapsed = Date.now() - start
263
- const bytesPerSec = (bytes / (elapsed / 1000)).toFixed(2)
264
- console.log(
265
- `Batch ${batchNum} of ${bytes} bytes took ${elapsed === Infinity ? 'Infinity' : elapsed}ms (${bytesPerSec} bytes/sec)`
266
- )
267
- }
268
- }
269
-
270
- let isJsonArray: boolean | null = null
271
- const jsonArrayChunks: Uint8Array[] = []
272
- let jsonArrayLength = 0
273
-
274
- let pending = new Uint8Array(0)
275
- let batch: T[] = []
276
-
277
- for await (const chunk of res.body as unknown as AsyncIterable<Uint8Array>) {
278
- if (isJsonArray === null) isJsonArray = chunk[0] === 0x5b
279
-
280
- if (isJsonArray) {
281
- jsonArrayChunks.push(chunk)
282
- jsonArrayLength += chunk.length
283
- continue
284
- }
285
-
286
- const merged = new Uint8Array(pending.length + chunk.length)
287
- merged.set(pending)
288
- merged.set(chunk, pending.length)
289
-
290
- const { values, read } = Bun.JSONL.parseChunk(merged)
291
- pending = merged.subarray(read)
292
-
293
- for (const item of values) {
294
- batch.push(item as T)
295
- if (batch.length === Fylo.MAX_CPUS) {
296
- await flush(batch)
297
- batch = []
298
- if (limit && count >= limit) return count
299
- }
300
- }
301
- }
302
-
303
- if (isJsonArray) {
304
- const body = new Uint8Array(jsonArrayLength)
305
- let offset = 0
306
- for (const c of jsonArrayChunks) {
307
- body.set(c, offset)
308
- offset += c.length
309
- }
310
-
311
- const data = JSON.parse(new TextDecoder().decode(body))
312
- const items: T[] = Array.isArray(data) ? data : [data]
313
-
314
- for (let i = 0; i < items.length; i += Fylo.MAX_CPUS) {
315
- if (limit && count >= limit) break
316
- await flush(items.slice(i, i + Fylo.MAX_CPUS))
317
- }
318
- } else {
319
- if (pending.length > 0) {
320
- const { values } = Bun.JSONL.parseChunk(pending)
321
- for (const item of values) batch.push(item as T)
322
- }
323
-
324
- if (batch.length > 0) await flush(batch)
325
- }
326
-
327
- return count
328
- }
329
-
330
- /**
331
- * Gets an exported stream of documents from a collection.
332
- */
333
- static async *exportBulkData<T extends Record<string, any>>(collection: string) {
334
- yield* Fylo.defaultEngine.exportBulkData<T>(collection)
335
- }
336
-
337
- /**
338
- * Gets a document from a collection.
339
- * @param collection The name of the collection.
340
- * @param _id The ID of the document.
341
- * @param onlyId Whether to only return the ID of the document.
342
- * @returns The document or the ID of the document.
343
- */
344
- static getDoc<T extends Record<string, any>>(
345
- collection: string,
346
- _id: _ttid,
347
- onlyId: boolean = false
348
- ) {
349
- return Fylo.defaultEngine.getDoc<T>(collection, _id, onlyId)
350
- }
351
-
352
- /**
353
- * Puts multiple documents into a collection.
354
- * @param collection The name of the collection.
355
- * @param batch The documents to put.
356
- * @returns The IDs of the documents.
357
- */
358
- async batchPutData<T extends Record<string, any>>(collection: string, batch: Array<T>) {
359
- const batches: Array<Array<T>> = []
360
- const ids: _ttid[] = []
361
-
362
- if (batch.length > navigator.hardwareConcurrency) {
363
- for (let i = 0; i < batch.length; i += navigator.hardwareConcurrency) {
364
- batches.push(batch.slice(i, i + navigator.hardwareConcurrency))
365
- }
366
- } else batches.push(batch)
367
-
368
- for (const itemBatch of batches) {
369
- const res = await Promise.allSettled(
370
- itemBatch.map((data) => this.putData(collection, data))
371
- )
372
-
373
- for (const _id of res
374
- .filter((item) => item.status === 'fulfilled')
375
- .map((item) => item.value)) {
376
- ids.push(_id)
377
- }
378
- }
379
-
380
- return ids
381
- }
382
-
383
- async queuePutData<T extends Record<string, any>>(
384
- _collection: string,
385
- _data: Record<_ttid, T> | T
386
- ) {
387
- return this.unsupportedLegacyApi('queuePutData')
388
- }
389
-
390
- async queuePatchDoc<T extends Record<string, any>>(
391
- _collection: string,
392
- _newDoc: Record<_ttid, Partial<T>>,
393
- _oldDoc: Record<_ttid, T> = {}
394
- ) {
395
- return this.unsupportedLegacyApi('queuePatchDoc')
396
- }
397
-
398
- async queueDelDoc(_collection: string, _id: _ttid) {
399
- return this.unsupportedLegacyApi('queueDelDoc')
400
- }
401
-
402
- /**
403
- * Puts a document into a collection.
404
- * @param collection The name of the collection.
405
- * @param data The document to put.
406
- * @returns The ID of the document.
407
- */
408
- private static async uniqueTTID(existingId?: string): Promise<_ttid> {
409
- let _id!: _ttid
410
- const prev = Fylo.ttidLock
411
- Fylo.ttidLock = prev.then(async () => {
412
- _id = existingId ? TTID.generate(existingId) : TTID.generate()
413
- })
414
- await Fylo.ttidLock
415
-
416
- return _id
417
- }
418
-
419
- private async prepareInsert<T extends Record<string, any>>(
420
- collection: string,
421
- data: Record<_ttid, T> | T
422
- ) {
423
- await Fylo.loadEncryption(collection)
424
-
425
- const currId = Object.keys(data).shift()!
426
- const _id = TTID.isTTID(currId)
427
- ? await Fylo.uniqueTTID(currId)
428
- : await Fylo.uniqueTTID(undefined)
429
-
430
- let doc = TTID.isTTID(currId) ? (Object.values(data).shift() as T) : (data as T)
431
-
432
- if (Fylo.STRICT) doc = (await Gen.validateData(collection, doc)) as T
433
-
434
- return { _id, doc }
435
- }
436
-
437
- private async executePutDataDirect<T extends Record<string, any>>(
438
- collection: string,
439
- _id: _ttid,
440
- doc: T
441
- ) {
442
- await this.engine.putDocument(collection, _id, doc)
443
-
444
- if (Fylo.LOGGING) console.log(`Finished Writing ${_id}`)
445
-
446
- return _id
447
- }
448
-
449
- private async executePatchDocDirect<T extends Record<string, any>>(
450
- collection: string,
451
- newDoc: Record<_ttid, Partial<T>>,
452
- oldDoc: Record<_ttid, T> = {}
453
- ) {
454
- await Fylo.loadEncryption(collection)
455
-
456
- const _id = Object.keys(newDoc).shift() as _ttid
457
-
458
- if (!_id) throw new Error('this document does not contain an TTID')
459
-
460
- let existingDoc = oldDoc[_id]
461
- if (!existingDoc) {
462
- const existing = await this.engine.getDoc<T>(collection, _id).once()
463
- existingDoc = existing[_id]
464
- }
465
- if (!existingDoc) return _id
466
-
467
- const currData = { ...existingDoc, ...newDoc[_id] } as T
468
- let docToWrite: T = currData
469
- const _newId = await Fylo.uniqueTTID(_id)
470
- if (Fylo.STRICT) docToWrite = (await Gen.validateData(collection, currData)) as T
471
-
472
- const nextId = await this.engine.patchDocument(
473
- collection,
474
- _id,
475
- _newId,
476
- docToWrite,
477
- existingDoc
478
- )
479
-
480
- if (Fylo.LOGGING) console.log(`Finished Updating ${_id} to ${nextId}`)
481
-
482
- return nextId
483
- }
484
-
485
- private async executeDelDocDirect(collection: string, _id: _ttid) {
486
- await this.engine.deleteDocument(collection, _id)
487
-
488
- if (Fylo.LOGGING) console.log(`Finished Deleting ${_id}`)
489
- }
490
-
491
- async putData<T extends Record<string, any>>(collection: string, data: T): Promise<_ttid>
492
- async putData<T extends Record<string, any>>(
493
- collection: string,
494
- data: Record<_ttid, T>
495
- ): Promise<_ttid>
496
- async putData<T extends Record<string, any>>(
497
- collection: string,
498
- data: Record<_ttid, T> | T,
499
- options: { wait?: boolean; timeoutMs?: number } = {}
500
- ): Promise<_ttid> {
501
- if (options.wait === false) {
502
- this.unsupportedLegacyApi('putData(..., { wait: false })')
503
- }
504
-
505
- const { _id, doc } = await this.prepareInsert(collection, data)
506
- await this.executePutDataDirect(collection, _id, doc)
507
- return _id
508
- }
509
-
510
- /**
511
- * Patches a document in a collection.
512
- * @param collection The name of the collection.
513
- * @param newDoc The new document data.
514
- * @param oldDoc The old document data.
515
- * @returns The number of documents patched.
516
- */
517
- async patchDoc<T extends Record<string, any>>(
518
- collection: string,
519
- newDoc: Record<_ttid, Partial<T>>,
520
- oldDoc: Record<_ttid, T> = {},
521
- options: { wait?: boolean; timeoutMs?: number } = {}
522
- ): Promise<_ttid> {
523
- if (options.wait === false) {
524
- this.unsupportedLegacyApi('patchDoc(..., { wait: false })')
525
- }
526
-
527
- return await this.executePatchDocDirect(collection, newDoc, oldDoc)
528
- }
529
-
530
- /**
531
- * Patches documents in a collection.
532
- * @param collection The name of the collection.
533
- * @param updateSchema The update schema.
534
- * @returns The number of documents patched.
535
- */
536
- async patchDocs<T extends Record<string, any>>(
537
- collection: string,
538
- updateSchema: _storeUpdate<T>
539
- ) {
540
- await Fylo.loadEncryption(collection)
541
-
542
- let count = 0
543
- const promises: Promise<_ttid>[] = []
544
-
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
- )
552
- count++
553
- }
554
- }
555
- }
556
-
557
- await Promise.all(promises)
558
-
559
- return count
560
- }
561
-
562
- /**
563
- * Deletes a document from a collection.
564
- * @param collection The name of the collection.
565
- * @param _id The ID of the document.
566
- * @returns The number of documents deleted.
567
- */
568
- async delDoc(
569
- collection: string,
570
- _id: _ttid,
571
- options: { wait?: boolean; timeoutMs?: number } = {}
572
- ): Promise<void> {
573
- if (options.wait === false) {
574
- this.unsupportedLegacyApi('delDoc(..., { wait: false })')
575
- }
576
-
577
- await this.executeDelDocDirect(collection, _id)
578
- }
579
-
580
- /**
581
- * Deletes documents from a collection.
582
- * @param collection The name of the collection.
583
- * @param deleteSchema The delete schema.
584
- * @returns The number of documents deleted.
585
- */
586
- async delDocs<T extends Record<string, any>>(
587
- collection: string,
588
- deleteSchema?: _storeDelete<T>
589
- ) {
590
- await Fylo.loadEncryption(collection)
591
-
592
- let count = 0
593
- const promises: Promise<void>[] = []
594
-
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))
602
- count++
603
- }
604
- }
605
- }
606
-
607
- await Promise.all(promises)
608
-
609
- return count
610
- }
611
-
612
- /**
613
- * Joins documents from two collections.
614
- * @param join The join schema.
615
- * @returns The joined documents.
616
- */
617
- static async joinDocs<T extends Record<string, any>, U extends Record<string, any>>(
618
- join: _join<T, U>
619
- ) {
620
- return await Fylo.defaultEngine.joinDocs(join)
621
- }
622
-
623
- /**
624
- * Finds documents in a collection.
625
- * @param collection The name of the collection.
626
- * @param query The query schema.
627
- * @returns The found documents.
628
- */
629
- static findDocs<T extends Record<string, any>>(collection: string, query?: _storeQuery<T>) {
630
- return Fylo.defaultEngine.findDocs<T>(collection, query)
631
- }
632
- }
package/src/sync.ts DELETED
@@ -1,58 +0,0 @@
1
- export type FyloSyncMode = 'await-sync' | 'fire-and-forget'
2
-
3
- export interface FyloWriteSyncEvent<T extends Record<string, any> = Record<string, any>> {
4
- operation: 'put' | 'patch'
5
- collection: string
6
- docId: _ttid
7
- previousDocId?: _ttid
8
- path: string
9
- data: T
10
- }
11
-
12
- export interface FyloDeleteSyncEvent {
13
- operation: 'delete' | 'patch'
14
- collection: string
15
- docId: _ttid
16
- path: string
17
- }
18
-
19
- export interface FyloSyncHooks<T extends Record<string, any> = Record<string, any>> {
20
- onWrite?: (event: FyloWriteSyncEvent<T>) => Promise<void> | void
21
- onDelete?: (event: FyloDeleteSyncEvent) => Promise<void> | void
22
- }
23
-
24
- export interface FyloOptions<T extends Record<string, any> = Record<string, any>> {
25
- root?: string
26
- s3FilesRoot?: string
27
- sync?: FyloSyncHooks<T>
28
- syncMode?: FyloSyncMode
29
- }
30
-
31
- export class FyloSyncError extends Error {
32
- readonly collection: string
33
- readonly docId: _ttid
34
- readonly path: string
35
- readonly operation: string
36
-
37
- constructor(args: {
38
- collection: string
39
- docId: _ttid
40
- path: string
41
- operation: string
42
- cause: unknown
43
- }) {
44
- super(
45
- `FYLO sync failed after the local filesystem operation succeeded for ${args.operation} ${args.collection}/${args.docId}. Local state is already committed at ${args.path}.`,
46
- { cause: args.cause }
47
- )
48
- this.name = 'FyloSyncError'
49
- this.collection = args.collection
50
- this.docId = args.docId
51
- this.path = args.path
52
- this.operation = args.operation
53
- }
54
- }
55
-
56
- export function resolveSyncMode(syncMode?: FyloSyncMode): FyloSyncMode {
57
- return syncMode ?? 'await-sync'
58
- }