@delma/fylo 2.0.1 → 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 (107) hide show
  1. package/README.md +206 -261
  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/dist/core/directory.js +48 -0
  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/dist/types/fylo.d.ts +179 -0
  35. package/{src → dist}/types/node-runtime.d.ts +1 -0
  36. package/package.json +3 -6
  37. package/.env.example +0 -16
  38. package/.github/copilot-instructions.md +0 -3
  39. package/.github/prompts/release.prompt.md +0 -10
  40. package/.github/workflows/ci.yml +0 -37
  41. package/.github/workflows/publish.yml +0 -91
  42. package/.prettierrc +0 -7
  43. package/AGENTS.md +0 -3
  44. package/CLAUDE.md +0 -3
  45. package/eslint.config.js +0 -32
  46. package/src/CLI +0 -39
  47. package/src/adapters/cipher.ts +0 -180
  48. package/src/adapters/redis.ts +0 -487
  49. package/src/adapters/s3.ts +0 -61
  50. package/src/core/collection.ts +0 -5
  51. package/src/core/directory.ts +0 -387
  52. package/src/core/extensions.ts +0 -21
  53. package/src/core/format.ts +0 -457
  54. package/src/core/parser.ts +0 -901
  55. package/src/core/query.ts +0 -53
  56. package/src/core/walker.ts +0 -174
  57. package/src/core/write-queue.ts +0 -59
  58. package/src/engines/s3-files.ts +0 -1068
  59. package/src/engines/types.ts +0 -21
  60. package/src/index.ts +0 -1727
  61. package/src/migrate-cli.ts +0 -22
  62. package/src/migrate.ts +0 -74
  63. package/src/types/fylo.d.ts +0 -261
  64. package/src/types/write-queue.ts +0 -42
  65. package/src/worker.ts +0 -18
  66. package/src/workers/write-worker.ts +0 -120
  67. package/tests/collection/truncate.test.js +0 -35
  68. package/tests/data.js +0 -97
  69. package/tests/index.js +0 -14
  70. package/tests/integration/aws-s3-files.canary.test.js +0 -22
  71. package/tests/integration/create.test.js +0 -39
  72. package/tests/integration/delete.test.js +0 -95
  73. package/tests/integration/edge-cases.test.js +0 -158
  74. package/tests/integration/encryption.test.js +0 -131
  75. package/tests/integration/export.test.js +0 -46
  76. package/tests/integration/join-modes.test.js +0 -154
  77. package/tests/integration/migration.test.js +0 -38
  78. package/tests/integration/nested.test.js +0 -142
  79. package/tests/integration/operators.test.js +0 -122
  80. package/tests/integration/queue.test.js +0 -83
  81. package/tests/integration/read.test.js +0 -119
  82. package/tests/integration/rollback.test.js +0 -60
  83. package/tests/integration/s3-files.test.js +0 -192
  84. package/tests/integration/update.test.js +0 -99
  85. package/tests/mocks/cipher.js +0 -40
  86. package/tests/mocks/redis.js +0 -123
  87. package/tests/mocks/s3.js +0 -80
  88. package/tests/schemas/album.d.ts +0 -5
  89. package/tests/schemas/album.json +0 -5
  90. package/tests/schemas/comment.d.ts +0 -7
  91. package/tests/schemas/comment.json +0 -7
  92. package/tests/schemas/photo.d.ts +0 -7
  93. package/tests/schemas/photo.json +0 -7
  94. package/tests/schemas/post.d.ts +0 -6
  95. package/tests/schemas/post.json +0 -6
  96. package/tests/schemas/tip.d.ts +0 -7
  97. package/tests/schemas/tip.json +0 -7
  98. package/tests/schemas/todo.d.ts +0 -6
  99. package/tests/schemas/todo.json +0 -6
  100. package/tests/schemas/user.d.ts +0 -23
  101. package/tests/schemas/user.json +0 -23
  102. package/tsconfig.json +0 -21
  103. package/tsconfig.typecheck.json +0 -31
  104. /package/{src → dist}/types/bun-runtime.d.ts +0 -0
  105. /package/{src → dist}/types/index.d.ts +0 -0
  106. /package/{src → dist}/types/query.d.ts +0 -0
  107. /package/{src → dist}/types/vendor-modules.d.ts +0 -0
package/README.md CHANGED
@@ -1,368 +1,313 @@
1
- # Fylo
1
+ # FYLO
2
2
 
3
- NoSQL document store with SQL parsing, real-time listeners, and Bun-first workflows.
3
+ FYLO is a Bun-native document store that keeps **one canonical file per document** and builds a **collection index file** to make queries fast.
4
4
 
5
- Fylo `2.0.1` supports two storage engines:
5
+ The important mental model is simple:
6
6
 
7
- - `legacy-s3`: the existing S3 + Redis architecture with queued writes, bucket-per-collection storage, and Redis-backed pub/sub/locks.
8
- - `s3-files`: a new AWS S3 Files mode that stores canonical documents on a mounted S3 Files filesystem, keeps query indexes in a collection-level SQLite database under `.fylo/index.db`, and uses filesystem locks plus an append-only event journal instead of Redis.
7
+ - document files are the source of truth
8
+ - the index file is just an accelerator
9
+ - if the index ever gets out of date, FYLO can rebuild it from the documents
9
10
 
10
- The legacy engine still stores documents as **S3 key paths** — not file contents. Each document produces two keys per field: a **data key** (`{ttid}/{field}/{value}`) for full-doc retrieval and an **index key** (`{field}/{value}/{ttid}`) for query lookups. This enables fast reads and filtered queries without a traditional database engine.
11
+ FYLO now ships with **one engine**: a filesystem-first storage model designed to work well with AWS S3 Files and other synced filesystem setups.
11
12
 
12
- Built for **serverless** runtimes (AWS Lambda, Cloudflare Workers) — no persistent in-memory state, lazy connections, minimal cold-start overhead.
13
+ ## Why this design?
13
14
 
14
- In `legacy-s3`, writes are coordinated through Redis before they are flushed to S3. By default the high-level CRUD methods wait for the queued write to be processed so existing code can continue to behave synchronously. If you want fire-and-forget semantics, pass `{ wait: false }` and process queued jobs with a worker or `processQueuedWrites()`.
15
+ We wanted three things:
15
16
 
16
- In `s3-files`, writes are immediate and synchronous. Queue APIs, worker APIs, and Redis-backed job tracking are intentionally unsupported.
17
+ - low durable storage overhead
18
+ - fast application queries
19
+ - a system that is still understandable by normal engineers
17
20
 
18
- ## Install
21
+ That is why FYLO does **not** create one tiny durable file per indexed field and does **not** depend on Redis-backed queued writes anymore.
22
+
23
+ Instead, each collection looks like this:
24
+
25
+ ```text
26
+ <root>/<collection>/
27
+ .fylo/
28
+ docs/
29
+ 4U/
30
+ 4UUB32VGUDW.json
31
+ indexes/
32
+ <collection>.idx.json
33
+ events/
34
+ <collection>.ndjson
35
+ ```
36
+
37
+ ## Installation
19
38
 
20
39
  ```bash
21
40
  bun add @delma/fylo
22
41
  ```
23
42
 
24
- ## Engine Selection
43
+ ## Basic usage
25
44
 
26
- ```typescript
45
+ ```ts
27
46
  import Fylo from '@delma/fylo'
28
47
 
29
- const legacy = new Fylo()
30
-
31
- const s3Files = new Fylo({
32
- engine: 's3-files',
33
- s3FilesRoot: '/mnt/fylo'
48
+ const fylo = new Fylo({
49
+ root: '/mnt/fylo'
34
50
  })
35
- ```
36
51
 
37
- Static helpers such as `Fylo.createCollection()` and `Fylo.findDocs()` use environment defaults:
52
+ await fylo.createCollection('users')
38
53
 
39
- ```bash
40
- export FYLO_STORAGE_ENGINE=s3-files
41
- export FYLO_S3FILES_ROOT=/mnt/fylo
54
+ const id = await fylo.putData('users', {
55
+ name: 'Ada',
56
+ role: 'admin',
57
+ tags: ['engineering', 'platform']
58
+ })
59
+
60
+ const doc = await fylo.getDoc('users', id).once()
61
+ console.log(doc[id])
42
62
  ```
43
63
 
44
- ## Environment Variables
45
-
46
- | Variable | Purpose |
47
- | ------------------------------------------------ | ------------------------------------------------------------------------------------ |
48
- | `FYLO_STORAGE_ENGINE` | `legacy-s3` (default) or `s3-files` |
49
- | `FYLO_S3FILES_ROOT` | Mounted S3 Files root directory used by the `s3-files` engine |
50
- | `BUCKET_PREFIX` | S3 bucket name prefix |
51
- | `S3_ACCESS_KEY_ID` / `AWS_ACCESS_KEY_ID` | S3 credentials |
52
- | `S3_SECRET_ACCESS_KEY` / `AWS_SECRET_ACCESS_KEY` | S3 credentials |
53
- | `S3_REGION` / `AWS_REGION` | S3 region |
54
- | `S3_ENDPOINT` / `AWS_ENDPOINT` | S3 endpoint (for LocalStack, MinIO, etc.) |
55
- | `REDIS_URL` | Redis connection URL used for pub/sub, document locks, and queued write coordination |
56
- | `FYLO_WRITE_MAX_ATTEMPTS` | Maximum retry attempts before a queued job is dead-lettered |
57
- | `FYLO_WRITE_RETRY_BASE_MS` | Base retry delay used for exponential backoff between recovery attempts |
58
- | `FYLO_WORKER_ID` | Optional stable identifier for a write worker process |
59
- | `FYLO_WORKER_BATCH_SIZE` | Number of queued jobs a worker pulls per read loop |
60
- | `FYLO_WORKER_BLOCK_MS` | Redis stream block time for waiting on new jobs |
61
- | `FYLO_WORKER_RECOVER_ON_START` | Whether the worker reclaims stale pending jobs on startup |
62
- | `FYLO_WORKER_RECOVER_IDLE_MS` | Minimum idle time before a pending job is reclaimed |
63
- | `FYLO_WORKER_STOP_WHEN_IDLE` | Exit the worker loop when no jobs are available |
64
- | `LOGGING` | Enable debug logging |
65
- | `STRICT` | Enable schema validation via CHEX |
66
-
67
- ### S3 Files requirements
68
-
69
- When `FYLO_STORAGE_ENGINE=s3-files`, FYLO expects:
70
-
71
- - an already provisioned AWS S3 Files file system
72
- - the mounted root directory to be available to the Bun process
73
- - bucket versioning enabled on the underlying S3 bucket
74
- - Linux/AWS compute assumptions that match AWS S3 Files mounting requirements
75
-
76
- FYLO no longer talks to the S3 API directly in this mode, but S3 remains the underlying source of truth because that is how S3 Files works.
77
-
78
- ## Usage
79
-
80
- ### CRUD — NoSQL API
81
-
82
- ```typescript
83
- import Fylo from '@delma/fylo'
64
+ ## Configuration
84
65
 
85
- const fylo = new Fylo()
66
+ FYLO is filesystem-first now.
86
67
 
87
- // Collections
88
- await Fylo.createCollection('users')
68
+ You can configure the root in one of two ways:
89
69
 
90
- // Create
91
- const _id = await fylo.putData<_user>('users', { name: 'John Doe', age: 30 })
70
+ ```bash
71
+ export FYLO_ROOT=/mnt/fylo
72
+ ```
92
73
 
93
- // Read one
94
- const user = await Fylo.getDoc<_user>('users', _id).once()
74
+ Or:
95
75
 
96
- // Read many
97
- for await (const doc of Fylo.findDocs<_user>('users', { $limit: 10 }).collect()) {
98
- console.log(doc)
99
- }
76
+ ```ts
77
+ const fylo = new Fylo({ root: '/mnt/fylo' })
78
+ ```
100
79
 
101
- // Update one
102
- await fylo.patchDoc<_user>('users', { [_id]: { age: 31 } })
80
+ If you do not configure a root, FYLO uses a project-local default:
103
81
 
104
- // Update many
105
- const updated = await fylo.patchDocs<_user>('users', {
106
- $where: { $ops: [{ age: { $gte: 30 } }] },
107
- $set: { age: 31 }
108
- })
82
+ ```text
83
+ <current working directory>/.fylo-data
84
+ ```
109
85
 
110
- // Delete one
111
- await fylo.delDoc('users', _id)
86
+ For compatibility with older `s3-files` experiments, FYLO still accepts `s3FilesRoot` and still reads `FYLO_S3FILES_ROOT` as a fallback.
112
87
 
113
- // Delete many
114
- const deleted = await fylo.delDocs<_user>('users', {
115
- $ops: [{ name: { $like: '%Doe%' } }]
116
- })
88
+ ### Environment variables
117
89
 
118
- // Drop
119
- await Fylo.dropCollection('users')
120
- ```
90
+ | Variable | Purpose |
91
+ | ------------------- | ---------------------------------------------------------------- |
92
+ | `FYLO_ROOT` | Preferred filesystem root for collections |
93
+ | `FYLO_S3FILES_ROOT` | Backward-compatible alias for `FYLO_ROOT` |
94
+ | `SCHEMA_DIR` | Directory containing JSON validation schemas |
95
+ | `STRICT` | When truthy, validate documents with `@delma/chex` before writes |
96
+ | `ENCRYPTION_KEY` | Required when schemas declare `$encrypted` fields |
97
+ | `CIPHER_SALT` | Recommended unique salt for field encryption key derivation |
121
98
 
122
- ### Queued Writes
99
+ ## Security-sensitive behavior
123
100
 
124
- `legacy-s3` only.
101
+ ### Encrypted fields
125
102
 
126
- ```typescript
127
- const fylo = new Fylo()
103
+ Schemas can declare encrypted fields with a `$encrypted` array. When a collection schema declares encrypted fields, FYLO fails closed unless `ENCRYPTION_KEY` is set and at least 32 characters long.
128
104
 
129
- // Default behavior waits for the queued write to finish.
130
- const _id = await fylo.putData('users', { name: 'John Doe' })
105
+ Encrypted document values are stored with AES-GCM. Exact-match queries on encrypted fields use keyed HMAC blind indexes, so equality and frequency can still be inferred from index tokens, but plaintext field values are not written to document files, index files, or event journals.
131
106
 
132
- // Async mode returns the queued job immediately.
133
- const queued = await fylo.putData('users', { name: 'Jane Doe' }, { wait: false })
107
+ If you are upgrading encrypted collections from a version before `2.1.1`, rewrite encrypted documents or otherwise rebuild affected indexes before relying on `$eq` queries for encrypted fields. Older encrypted document bodies can still be read, but old deterministic encrypted index entries do not match the new HMAC blind-index format.
134
108
 
135
- // Poll status if you need to track progress.
136
- const status = await fylo.getJobStatus(queued.jobId)
109
+ ### Bulk imports
137
110
 
138
- // Process pending writes in-process when you are not running a separate worker.
139
- await fylo.processQueuedWrites()
140
- ```
111
+ `importBulkData()` is intended for trusted JSON or JSONL sources. By default, HTTP(S) imports reject localhost, private, loopback, link-local, and other private-network addresses, and responses are capped at 50 MiB.
141
112
 
142
- When `wait: false` is used, the job is durable in Redis but the document is not visible in S3 until a worker commits it.
113
+ You can tighten the import boundary with explicit options:
143
114
 
144
- Queued jobs that fail are left pending for recovery. Recovered jobs retry up to `FYLO_WRITE_MAX_ATTEMPTS` times before being moved to a dead-letter stream. You can inspect dead letters with `getDeadLetters()` and reclaim stale pending jobs with `processQueuedWrites(count, true)`.
115
+ ```ts
116
+ await fylo.importBulkData('users', new URL('https://data.example.com/users.json'), {
117
+ limit: 1000,
118
+ maxBytes: 5 * 1024 * 1024,
119
+ allowedHosts: ['data.example.com']
120
+ })
121
+ ```
145
122
 
146
- Operational helpers:
123
+ Only set `allowPrivateNetwork: true` when the import URL is fully trusted by your application.
147
124
 
148
- - `getQueueStats()` returns current queue, pending, and dead-letter counts
149
- - `getDeadLetters()` lists exhausted jobs
150
- - `replayDeadLetter(streamId)` moves a dead-lettered job back into the main queue
125
+ ## Syncing to S3-compatible storage
151
126
 
152
- ### Worker
127
+ FYLO does **not** ship its own cloud sync engine.
153
128
 
154
- `legacy-s3` only.
129
+ That is intentional.
155
130
 
156
- Run a dedicated write worker when you want queued writes to be flushed outside the request path:
131
+ The package owns:
157
132
 
158
- ```bash
159
- bun run worker
160
- ```
133
+ - document storage behavior
134
+ - query behavior
135
+ - index maintenance
161
136
 
162
- The worker entrypoint lives at [worker.ts](/Users/iyor/Library/CloudStorage/Dropbox/myProjects/FYLO/src/worker.ts) and continuously drains the Redis stream, recovers stale pending jobs on startup, and respects the retry/dead-letter settings above.
137
+ You own:
163
138
 
164
- If `FYLO_STORAGE_ENGINE=s3-files`, `fylo.worker` exits with an explicit unsupported-engine error.
139
+ - how that root directory gets synced to AWS S3 Files, S3-compatible storage, or any other file-backed replication layer you trust
165
140
 
166
- ### Migration
141
+ That means you can choose the sync tool that matches your infrastructure:
167
142
 
168
- Move legacy collections into an S3 Files-backed root with:
143
+ - AWS S3 Files
144
+ - `aws s3 sync`
145
+ - `rclone`
146
+ - storage vendor tooling
147
+ - platform-specific replication
169
148
 
170
- ```bash
171
- fylo.migrate users posts
172
- ```
149
+ If you want FYLO to notify your own S3 client on document writes, you can plug in sync hooks:
173
150
 
174
- Programmatic usage:
175
-
176
- ```typescript
177
- import { migrateLegacyS3ToS3Files } from '@delma/fylo'
151
+ ```ts
152
+ import Fylo from '@delma/fylo'
178
153
 
179
- await migrateLegacyS3ToS3Files({
180
- collections: ['users', 'posts'],
181
- s3FilesRoot: '/mnt/fylo',
182
- verify: true
154
+ const fylo = new Fylo({
155
+ root: '/mnt/fylo',
156
+ syncMode: 'await-sync',
157
+ sync: {
158
+ async onWrite(event) {
159
+ const file = Bun.file(event.path)
160
+ await myS3Client.putObject({
161
+ key: `${event.collection}/${event.docId}.json`,
162
+ body: await file.arrayBuffer()
163
+ })
164
+ },
165
+ async onDelete(event) {
166
+ await myS3Client.deleteObject({
167
+ key: `${event.collection}/${event.docId}.json`
168
+ })
169
+ }
170
+ }
183
171
  })
184
172
  ```
185
173
 
186
- ### CRUD SQL API
174
+ There are two sync modes:
187
175
 
188
- ```typescript
189
- const fylo = new Fylo()
176
+ - `await-sync`: FYLO waits for your hook and throws if the remote sync fails
177
+ - `fire-and-forget`: FYLO commits locally first and runs your hook in the background
190
178
 
191
- await fylo.executeSQL(`CREATE TABLE users`)
179
+ Important detail for junior engineers:
192
180
 
193
- const _id = await fylo.executeSQL<_user>(`INSERT INTO users (name, age) VALUES ('John Doe', 30)`)
181
+ - the filesystem write is still the source of truth
182
+ - a sync hook is a replication helper, not the database itself
194
183
 
195
- const docs = await fylo.executeSQL<_user>(`SELECT * FROM users LIMIT 10`)
184
+ ## CRUD examples
196
185
 
197
- await fylo.executeSQL<_user>(`UPDATE users SET age = 31 WHERE name = 'John Doe'`)
186
+ ### Create
198
187
 
199
- await fylo.executeSQL<_user>(`DELETE FROM users WHERE name LIKE '%Doe%'`)
200
-
201
- await fylo.executeSQL(`DROP TABLE users`)
188
+ ```ts
189
+ const userId = await fylo.putData('users', {
190
+ name: 'Jane Doe',
191
+ age: 29,
192
+ team: 'platform'
193
+ })
202
194
  ```
203
195
 
204
- ### Query Operators
196
+ ### Read one
205
197
 
206
- ```typescript
207
- // Equality
208
- {
209
- $ops: [{ status: { $eq: 'active' } }]
210
- }
198
+ ```ts
199
+ const user = await fylo.getDoc('users', userId).once()
200
+ ```
211
201
 
212
- // Not equal
213
- {
214
- $ops: [{ status: { $ne: 'archived' } }]
215
- }
202
+ ### Find many
216
203
 
217
- // Numeric range
218
- {
219
- $ops: [{ age: { $gte: 18, $lt: 65 } }]
220
- }
204
+ ```ts
205
+ const results = {}
221
206
 
222
- // Pattern matching
223
- {
224
- $ops: [{ email: { $like: '%@gmail.com' } }]
207
+ for await (const doc of fylo
208
+ .findDocs('users', {
209
+ $ops: [{ age: { $gte: 18 } }]
210
+ })
211
+ .collect()) {
212
+ Object.assign(results, doc)
225
213
  }
214
+ ```
226
215
 
227
- // Array contains
228
- {
229
- $ops: [{ tags: { $contains: 'urgent' } }]
230
- }
216
+ ### Update one
231
217
 
232
- // Multiple ops use OR semantics — matches if any op is satisfied
233
- {
234
- $ops: [{ status: { $eq: 'active' } }, { priority: { $gte: 5 } }]
235
- }
218
+ ```ts
219
+ const nextId = await fylo.patchDoc('users', {
220
+ [userId]: {
221
+ team: 'core-platform'
222
+ }
223
+ })
236
224
  ```
237
225
 
238
- ### Joins
226
+ ### Delete one
239
227
 
240
- ```typescript
241
- const results = await Fylo.joinDocs<_post, _user>({
242
- $leftCollection: 'posts',
243
- $rightCollection: 'users',
244
- $mode: 'inner', // "inner" | "left" | "right" | "outer"
245
- $on: { userId: { $eq: 'id' } },
246
- $select: ['title', 'name'],
247
- $limit: 50
248
- })
228
+ ```ts
229
+ await fylo.delDoc('users', nextId)
249
230
  ```
250
231
 
251
- ### Real-Time Streaming
232
+ ## SQL support
252
233
 
253
- ```typescript
254
- // Stream new/updated documents
255
- for await (const doc of Fylo.findDocs<_user>('users')) {
256
- console.log(doc)
257
- }
234
+ FYLO also supports SQL-like commands for app-facing document work:
258
235
 
259
- // Stream deletions
260
- for await (const _id of Fylo.findDocs<_user>('users').onDelete()) {
261
- console.log('deleted:', _id)
262
- }
236
+ ```ts
237
+ await fylo.executeSQL(`
238
+ CREATE TABLE posts
239
+ `)
263
240
 
264
- // Watch a single document
265
- for await (const doc of Fylo.getDoc<_user>('users', _id)) {
266
- console.log(doc)
267
- }
241
+ await fylo.executeSQL(`
242
+ INSERT INTO posts VALUES { "title": "Hello", "published": true }
243
+ `)
244
+
245
+ const posts = await fylo.executeSQL(`
246
+ SELECT * FROM posts WHERE published = true
247
+ `)
268
248
  ```
269
249
 
270
- ### Bulk Import / Export
250
+ ## Query behavior
271
251
 
272
- ```typescript
273
- const fylo = new Fylo()
252
+ FYLO queries use the collection index file first when they can, then hydrate only the matching documents.
274
253
 
275
- // Import from JSON array or NDJSON URL
276
- const count = await fylo.importBulkData<_user>(
277
- 'users',
278
- new URL('https://example.com/users.json'),
279
- 1000
280
- )
254
+ That means:
281
255
 
282
- // Export all documents
283
- for await (const doc of Fylo.exportBulkData<_user>('users')) {
284
- console.log(doc)
285
- }
286
- ```
256
+ - exact matches are fast
257
+ - range queries are narrowed before document reads
258
+ - contains-style queries can use indexed candidates
259
+ - final document validation still happens before returning results
287
260
 
288
- ### Rollback
261
+ This is why FYLO behaves more like an application document store than a data warehouse.
289
262
 
290
- `rollback()` is now a legacy escape hatch.
263
+ ## Realtime behavior
291
264
 
292
- Fylo still keeps best-effort rollback data for writes performed by the current instance. This is mainly useful for in-process failures and test workflows:
265
+ FYLO keeps a filesystem event journal per collection.
293
266
 
294
- ```typescript
295
- const fylo = new Fylo()
296
- await fylo.putData('users', { name: 'test' })
297
- await fylo.rollback() // undoes all writes in this instance
298
- ```
267
+ That is what powers listeners such as:
299
268
 
300
- For queued writes, prefer:
269
+ ```ts
270
+ for await (const doc of fylo.findDocs('users', {
271
+ $ops: [{ role: { $eq: 'admin' } }]
272
+ })) {
273
+ console.log(doc)
274
+ }
275
+ ```
301
276
 
302
- - `getJobStatus()` to inspect an individual write
303
- - `processQueuedWrites(count, true)` to recover stale pending jobs
304
- - `getDeadLetters()` to inspect exhausted jobs
305
- - compensating writes instead of `rollback()` after a commit
277
+ ## What FYLO no longer does
306
278
 
307
- `rollback()` may be removed from the main queued-write path in a future major release.
279
+ FYLO no longer centers:
308
280
 
309
- ### CLI
281
+ - Redis-backed queued writes
282
+ - worker-based write draining
283
+ - legacy bucket-per-collection S3 storage
284
+ - built-in migration commands between old and new engines
310
285
 
311
- ```bash
312
- fylo.query "SELECT * FROM users WHERE age > 25 LIMIT 10"
313
- ```
286
+ If you see older references to those ideas in historic discussions, treat them as previous design stages, not the current product direction.
314
287
 
315
- ### Schema Validation
288
+ ## Recovery story
316
289
 
317
- When `STRICT` is set, documents are validated against CHEX schemas before writes:
290
+ This part is important:
318
291
 
319
- ```bash
320
- STRICT=true bun run start
321
- ```
292
+ - document files are the truth
293
+ - index files can be rebuilt
322
294
 
323
- Schemas are `.d.ts` interface declarations generated by [`@delma/chex`](https://github.com/Chidelma/CHEX).
295
+ That means FYLO is designed so that the system can recover from index drift without treating the index as a sacred durable database.
324
296
 
325
297
  ## Development
326
298
 
327
299
  ```bash
328
- bun test # Run all tests
329
- bun run build # Compile TypeScript
330
- bun run typecheck # Type-check without emitting
331
- bun run lint # ESLint
300
+ bun run typecheck
301
+ bun run build
302
+ bun test
332
303
  ```
333
304
 
334
- ### Local S3 (LocalStack)
305
+ ## Performance testing
306
+
307
+ FYLO includes an opt-in scale test for the filesystem engine:
335
308
 
336
309
  ```bash
337
- docker compose up aws
310
+ FYLO_RUN_PERF_TESTS=true bun test tests/integration/s3-files.performance.test.js
338
311
  ```
339
312
 
340
- This starts LocalStack on `localhost:4566`. Set `S3_ENDPOINT=http://localhost:4566` to route S3 calls locally.
341
-
342
- ## Security
343
-
344
- ### What Fylo does NOT provide
345
-
346
- Fylo is a low-level storage abstraction. The following must be implemented by the integrating application:
347
-
348
- - **Authentication** — Fylo has no concept of users or sessions. Any caller with access to the Fylo instance can read and write any collection.
349
- - **Authorization** — `executeSQL` and all document operations accept any collection name with no permission check. In multi-tenant applications, a caller can access any collection unless the integrator enforces a boundary above Fylo.
350
- - **Rate limiting** — There is no built-in request throttling. An attacker with access to the instance can flood S3 with requests or trigger expensive operations without restriction. Add rate limiting and document-size limits in your service layer.
351
-
352
- ### Secure configuration
353
-
354
- | Concern | Guidance |
355
- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
356
- | AWS credentials | Never commit credentials to version control. Use IAM instance roles or inject via CI secrets. Rotate any credentials that have been exposed. |
357
- | `ENCRYPTION_KEY` | Must be at least 32 characters. Use a high-entropy random value. |
358
- | `CIPHER_SALT` | Set a unique random value per deployment to prevent cross-instance precomputation attacks. |
359
- | `REDIS_URL` | Always set explicitly. Use `rediss://` (TLS) in production with authentication credentials in the URL. |
360
- | Collection names | Must match `^[a-z0-9][a-z0-9\-]*[a-z0-9]$`. Names are validated before any shell or S3 operation. |
361
-
362
- ### Encrypted fields
363
-
364
- Fields listed in `$encrypted` in a collection schema are encrypted with AES-256-CBC. By default a random IV is used per write (non-deterministic). Pass `deterministic: true` to `Cipher.encrypt()` only for fields that require `$eq`/`$ne` queries — deterministic encryption leaks value equality to observers of stored ciphertext.
365
-
366
- ## License
367
-
368
- MIT
313
+ This is useful when you want to see how index size and query latency behave as collections grow.