@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.
- package/README.md +206 -261
- package/dist/adapters/cipher.js +155 -0
- package/dist/adapters/cipher.js.map +1 -0
- package/dist/core/collection.js +6 -0
- package/dist/core/collection.js.map +1 -0
- package/dist/core/directory.js +48 -0
- package/dist/core/directory.js.map +1 -0
- package/dist/core/doc-id.js +15 -0
- package/dist/core/doc-id.js.map +1 -0
- package/dist/core/extensions.js +16 -0
- package/dist/core/extensions.js.map +1 -0
- package/dist/core/format.js +355 -0
- package/dist/core/format.js.map +1 -0
- package/dist/core/parser.js +764 -0
- package/dist/core/parser.js.map +1 -0
- package/dist/core/query.js +47 -0
- package/dist/core/query.js.map +1 -0
- package/dist/engines/s3-files/documents.js +62 -0
- package/dist/engines/s3-files/documents.js.map +1 -0
- package/dist/engines/s3-files/filesystem.js +165 -0
- package/dist/engines/s3-files/filesystem.js.map +1 -0
- package/dist/engines/s3-files/query.js +235 -0
- package/dist/engines/s3-files/query.js.map +1 -0
- package/dist/engines/s3-files/types.js +2 -0
- package/dist/engines/s3-files/types.js.map +1 -0
- package/dist/engines/s3-files.js +629 -0
- package/dist/engines/s3-files.js.map +1 -0
- package/dist/engines/types.js +2 -0
- package/dist/engines/types.js.map +1 -0
- package/dist/index.js +562 -0
- package/dist/index.js.map +1 -0
- package/dist/sync.js +18 -0
- package/dist/sync.js.map +1 -0
- package/dist/types/fylo.d.ts +179 -0
- package/{src → dist}/types/node-runtime.d.ts +1 -0
- package/package.json +3 -6
- package/.env.example +0 -16
- package/.github/copilot-instructions.md +0 -3
- package/.github/prompts/release.prompt.md +0 -10
- package/.github/workflows/ci.yml +0 -37
- package/.github/workflows/publish.yml +0 -91
- package/.prettierrc +0 -7
- package/AGENTS.md +0 -3
- package/CLAUDE.md +0 -3
- package/eslint.config.js +0 -32
- package/src/CLI +0 -39
- package/src/adapters/cipher.ts +0 -180
- package/src/adapters/redis.ts +0 -487
- package/src/adapters/s3.ts +0 -61
- package/src/core/collection.ts +0 -5
- package/src/core/directory.ts +0 -387
- package/src/core/extensions.ts +0 -21
- package/src/core/format.ts +0 -457
- package/src/core/parser.ts +0 -901
- package/src/core/query.ts +0 -53
- package/src/core/walker.ts +0 -174
- package/src/core/write-queue.ts +0 -59
- package/src/engines/s3-files.ts +0 -1068
- package/src/engines/types.ts +0 -21
- package/src/index.ts +0 -1727
- package/src/migrate-cli.ts +0 -22
- package/src/migrate.ts +0 -74
- package/src/types/fylo.d.ts +0 -261
- package/src/types/write-queue.ts +0 -42
- package/src/worker.ts +0 -18
- package/src/workers/write-worker.ts +0 -120
- package/tests/collection/truncate.test.js +0 -35
- package/tests/data.js +0 -97
- package/tests/index.js +0 -14
- package/tests/integration/aws-s3-files.canary.test.js +0 -22
- package/tests/integration/create.test.js +0 -39
- package/tests/integration/delete.test.js +0 -95
- package/tests/integration/edge-cases.test.js +0 -158
- package/tests/integration/encryption.test.js +0 -131
- package/tests/integration/export.test.js +0 -46
- package/tests/integration/join-modes.test.js +0 -154
- package/tests/integration/migration.test.js +0 -38
- package/tests/integration/nested.test.js +0 -142
- package/tests/integration/operators.test.js +0 -122
- package/tests/integration/queue.test.js +0 -83
- package/tests/integration/read.test.js +0 -119
- package/tests/integration/rollback.test.js +0 -60
- package/tests/integration/s3-files.test.js +0 -192
- package/tests/integration/update.test.js +0 -99
- package/tests/mocks/cipher.js +0 -40
- package/tests/mocks/redis.js +0 -123
- package/tests/mocks/s3.js +0 -80
- package/tests/schemas/album.d.ts +0 -5
- package/tests/schemas/album.json +0 -5
- package/tests/schemas/comment.d.ts +0 -7
- package/tests/schemas/comment.json +0 -7
- package/tests/schemas/photo.d.ts +0 -7
- package/tests/schemas/photo.json +0 -7
- package/tests/schemas/post.d.ts +0 -6
- package/tests/schemas/post.json +0 -6
- package/tests/schemas/tip.d.ts +0 -7
- package/tests/schemas/tip.json +0 -7
- package/tests/schemas/todo.d.ts +0 -6
- package/tests/schemas/todo.json +0 -6
- package/tests/schemas/user.d.ts +0 -23
- package/tests/schemas/user.json +0 -23
- package/tsconfig.json +0 -21
- package/tsconfig.typecheck.json +0 -31
- /package/{src → dist}/types/bun-runtime.d.ts +0 -0
- /package/{src → dist}/types/index.d.ts +0 -0
- /package/{src → dist}/types/query.d.ts +0 -0
- /package/{src → dist}/types/vendor-modules.d.ts +0 -0
package/README.md
CHANGED
|
@@ -1,368 +1,313 @@
|
|
|
1
|
-
#
|
|
1
|
+
# FYLO
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
5
|
+
The important mental model is simple:
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
13
|
+
## Why this design?
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
We wanted three things:
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
- low durable storage overhead
|
|
18
|
+
- fast application queries
|
|
19
|
+
- a system that is still understandable by normal engineers
|
|
17
20
|
|
|
18
|
-
|
|
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
|
-
##
|
|
43
|
+
## Basic usage
|
|
25
44
|
|
|
26
|
-
```
|
|
45
|
+
```ts
|
|
27
46
|
import Fylo from '@delma/fylo'
|
|
28
47
|
|
|
29
|
-
const
|
|
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
|
-
|
|
52
|
+
await fylo.createCollection('users')
|
|
38
53
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
66
|
+
FYLO is filesystem-first now.
|
|
86
67
|
|
|
87
|
-
|
|
88
|
-
await Fylo.createCollection('users')
|
|
68
|
+
You can configure the root in one of two ways:
|
|
89
69
|
|
|
90
|
-
|
|
91
|
-
|
|
70
|
+
```bash
|
|
71
|
+
export FYLO_ROOT=/mnt/fylo
|
|
72
|
+
```
|
|
92
73
|
|
|
93
|
-
|
|
94
|
-
const user = await Fylo.getDoc<_user>('users', _id).once()
|
|
74
|
+
Or:
|
|
95
75
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
76
|
+
```ts
|
|
77
|
+
const fylo = new Fylo({ root: '/mnt/fylo' })
|
|
78
|
+
```
|
|
100
79
|
|
|
101
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
$set: { age: 31 }
|
|
108
|
-
})
|
|
82
|
+
```text
|
|
83
|
+
<current working directory>/.fylo-data
|
|
84
|
+
```
|
|
109
85
|
|
|
110
|
-
|
|
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
|
-
|
|
114
|
-
const deleted = await fylo.delDocs<_user>('users', {
|
|
115
|
-
$ops: [{ name: { $like: '%Doe%' } }]
|
|
116
|
-
})
|
|
88
|
+
### Environment variables
|
|
117
89
|
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
99
|
+
## Security-sensitive behavior
|
|
123
100
|
|
|
124
|
-
|
|
101
|
+
### Encrypted fields
|
|
125
102
|
|
|
126
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
-
const status = await fylo.getJobStatus(queued.jobId)
|
|
109
|
+
### Bulk imports
|
|
137
110
|
|
|
138
|
-
|
|
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
|
-
|
|
113
|
+
You can tighten the import boundary with explicit options:
|
|
143
114
|
|
|
144
|
-
|
|
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
|
-
|
|
123
|
+
Only set `allowPrivateNetwork: true` when the import URL is fully trusted by your application.
|
|
147
124
|
|
|
148
|
-
|
|
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
|
-
|
|
127
|
+
FYLO does **not** ship its own cloud sync engine.
|
|
153
128
|
|
|
154
|
-
|
|
129
|
+
That is intentional.
|
|
155
130
|
|
|
156
|
-
|
|
131
|
+
The package owns:
|
|
157
132
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
133
|
+
- document storage behavior
|
|
134
|
+
- query behavior
|
|
135
|
+
- index maintenance
|
|
161
136
|
|
|
162
|
-
|
|
137
|
+
You own:
|
|
163
138
|
|
|
164
|
-
|
|
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
|
-
|
|
141
|
+
That means you can choose the sync tool that matches your infrastructure:
|
|
167
142
|
|
|
168
|
-
|
|
143
|
+
- AWS S3 Files
|
|
144
|
+
- `aws s3 sync`
|
|
145
|
+
- `rclone`
|
|
146
|
+
- storage vendor tooling
|
|
147
|
+
- platform-specific replication
|
|
169
148
|
|
|
170
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
```typescript
|
|
177
|
-
import { migrateLegacyS3ToS3Files } from '@delma/fylo'
|
|
151
|
+
```ts
|
|
152
|
+
import Fylo from '@delma/fylo'
|
|
178
153
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
174
|
+
There are two sync modes:
|
|
187
175
|
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
179
|
+
Important detail for junior engineers:
|
|
192
180
|
|
|
193
|
-
|
|
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
|
-
|
|
184
|
+
## CRUD examples
|
|
196
185
|
|
|
197
|
-
|
|
186
|
+
### Create
|
|
198
187
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
###
|
|
196
|
+
### Read one
|
|
205
197
|
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
$ops: [{ status: { $eq: 'active' } }]
|
|
210
|
-
}
|
|
198
|
+
```ts
|
|
199
|
+
const user = await fylo.getDoc('users', userId).once()
|
|
200
|
+
```
|
|
211
201
|
|
|
212
|
-
|
|
213
|
-
{
|
|
214
|
-
$ops: [{ status: { $ne: 'archived' } }]
|
|
215
|
-
}
|
|
202
|
+
### Find many
|
|
216
203
|
|
|
217
|
-
|
|
218
|
-
{
|
|
219
|
-
$ops: [{ age: { $gte: 18, $lt: 65 } }]
|
|
220
|
-
}
|
|
204
|
+
```ts
|
|
205
|
+
const results = {}
|
|
221
206
|
|
|
222
|
-
|
|
223
|
-
{
|
|
224
|
-
|
|
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
|
-
|
|
228
|
-
{
|
|
229
|
-
$ops: [{ tags: { $contains: 'urgent' } }]
|
|
230
|
-
}
|
|
216
|
+
### Update one
|
|
231
217
|
|
|
232
|
-
|
|
233
|
-
{
|
|
234
|
-
|
|
235
|
-
|
|
218
|
+
```ts
|
|
219
|
+
const nextId = await fylo.patchDoc('users', {
|
|
220
|
+
[userId]: {
|
|
221
|
+
team: 'core-platform'
|
|
222
|
+
}
|
|
223
|
+
})
|
|
236
224
|
```
|
|
237
225
|
|
|
238
|
-
###
|
|
226
|
+
### Delete one
|
|
239
227
|
|
|
240
|
-
```
|
|
241
|
-
|
|
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
|
-
|
|
232
|
+
## SQL support
|
|
252
233
|
|
|
253
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
236
|
+
```ts
|
|
237
|
+
await fylo.executeSQL(`
|
|
238
|
+
CREATE TABLE posts
|
|
239
|
+
`)
|
|
263
240
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
250
|
+
## Query behavior
|
|
271
251
|
|
|
272
|
-
|
|
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
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
261
|
+
This is why FYLO behaves more like an application document store than a data warehouse.
|
|
289
262
|
|
|
290
|
-
|
|
263
|
+
## Realtime behavior
|
|
291
264
|
|
|
292
|
-
|
|
265
|
+
FYLO keeps a filesystem event journal per collection.
|
|
293
266
|
|
|
294
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
279
|
+
FYLO no longer centers:
|
|
308
280
|
|
|
309
|
-
|
|
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
|
-
|
|
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
|
-
|
|
288
|
+
## Recovery story
|
|
316
289
|
|
|
317
|
-
|
|
290
|
+
This part is important:
|
|
318
291
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
```
|
|
292
|
+
- document files are the truth
|
|
293
|
+
- index files can be rebuilt
|
|
322
294
|
|
|
323
|
-
|
|
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
|
|
329
|
-
bun run build
|
|
330
|
-
bun
|
|
331
|
-
bun run lint # ESLint
|
|
300
|
+
bun run typecheck
|
|
301
|
+
bun run build
|
|
302
|
+
bun test
|
|
332
303
|
```
|
|
333
304
|
|
|
334
|
-
|
|
305
|
+
## Performance testing
|
|
306
|
+
|
|
307
|
+
FYLO includes an opt-in scale test for the filesystem engine:
|
|
335
308
|
|
|
336
309
|
```bash
|
|
337
|
-
|
|
310
|
+
FYLO_RUN_PERF_TESTS=true bun test tests/integration/s3-files.performance.test.js
|
|
338
311
|
```
|
|
339
312
|
|
|
340
|
-
This
|
|
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.
|