@delma/fylo 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +185 -267
- package/package.json +2 -5
- package/src/core/directory.ts +22 -354
- package/src/engines/s3-files/documents.ts +65 -0
- package/src/engines/s3-files/filesystem.ts +172 -0
- package/src/engines/s3-files/query.ts +291 -0
- package/src/engines/s3-files/types.ts +42 -0
- package/src/engines/s3-files.ts +391 -510
- package/src/engines/types.ts +1 -1
- package/src/index.ts +142 -1237
- package/src/sync.ts +58 -0
- package/src/types/fylo.d.ts +66 -161
- package/src/types/node-runtime.d.ts +1 -0
- package/tests/collection/truncate.test.js +11 -10
- package/tests/helpers/root.js +7 -0
- package/tests/integration/create.test.js +9 -9
- package/tests/integration/delete.test.js +16 -14
- package/tests/integration/edge-cases.test.js +29 -25
- package/tests/integration/encryption.test.js +47 -30
- package/tests/integration/export.test.js +11 -11
- package/tests/integration/join-modes.test.js +16 -16
- package/tests/integration/nested.test.js +26 -24
- package/tests/integration/operators.test.js +43 -29
- package/tests/integration/read.test.js +25 -21
- package/tests/integration/rollback.test.js +21 -51
- package/tests/integration/s3-files.performance.test.js +75 -0
- package/tests/integration/s3-files.test.js +115 -18
- package/tests/integration/sync.test.js +154 -0
- package/tests/integration/update.test.js +24 -18
- package/src/adapters/redis.ts +0 -487
- package/src/adapters/s3.ts +0 -61
- package/src/core/walker.ts +0 -174
- package/src/core/write-queue.ts +0 -59
- package/src/migrate-cli.ts +0 -22
- package/src/migrate.ts +0 -74
- package/src/types/write-queue.ts +0 -42
- package/src/worker.ts +0 -18
- package/src/workers/write-worker.ts +0 -120
- package/tests/index.js +0 -14
- package/tests/integration/migration.test.js +0 -38
- package/tests/integration/queue.test.js +0 -83
- package/tests/mocks/redis.js +0 -123
- package/tests/mocks/s3.js +0 -80
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import { test, expect, describe, beforeAll, afterAll
|
|
1
|
+
import { test, expect, describe, beforeAll, afterAll } from 'bun:test'
|
|
2
|
+
import { rm } from 'node:fs/promises'
|
|
2
3
|
import Fylo from '../../src'
|
|
3
4
|
import { albumURL } from '../data'
|
|
4
|
-
import
|
|
5
|
-
import RedisMock from '../mocks/redis'
|
|
5
|
+
import { createTestRoot } from '../helpers/root'
|
|
6
6
|
const ALBUMS = 'ops-album'
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
mock.module('../../src/adapters/redis', () => ({ Redis: RedisMock }))
|
|
7
|
+
const root = await createTestRoot('fylo-operators-')
|
|
8
|
+
const fylo = new Fylo({ root })
|
|
10
9
|
beforeAll(async () => {
|
|
11
|
-
await
|
|
10
|
+
await fylo.createCollection(ALBUMS)
|
|
12
11
|
try {
|
|
13
12
|
await fylo.importBulkData(ALBUMS, new URL(albumURL), 100)
|
|
14
13
|
} catch {
|
|
@@ -16,14 +15,17 @@ beforeAll(async () => {
|
|
|
16
15
|
}
|
|
17
16
|
})
|
|
18
17
|
afterAll(async () => {
|
|
19
|
-
await
|
|
18
|
+
await fylo.dropCollection(ALBUMS)
|
|
19
|
+
await rm(root, { recursive: true, force: true })
|
|
20
20
|
})
|
|
21
21
|
describe('NO-SQL', async () => {
|
|
22
22
|
test('$ne — excludes matching value', async () => {
|
|
23
23
|
let results = {}
|
|
24
|
-
for await (const data of
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
for await (const data of fylo
|
|
25
|
+
.findDocs(ALBUMS, {
|
|
26
|
+
$ops: [{ userId: { $ne: 1 } }]
|
|
27
|
+
})
|
|
28
|
+
.collect()) {
|
|
27
29
|
results = { ...results, ...data }
|
|
28
30
|
}
|
|
29
31
|
const albums = Object.values(results)
|
|
@@ -33,9 +35,11 @@ describe('NO-SQL', async () => {
|
|
|
33
35
|
})
|
|
34
36
|
test('$lt — returns documents where field is less than value', async () => {
|
|
35
37
|
let results = {}
|
|
36
|
-
for await (const data of
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
for await (const data of fylo
|
|
39
|
+
.findDocs(ALBUMS, {
|
|
40
|
+
$ops: [{ userId: { $lt: 5 } }]
|
|
41
|
+
})
|
|
42
|
+
.collect()) {
|
|
39
43
|
results = { ...results, ...data }
|
|
40
44
|
}
|
|
41
45
|
const albums = Object.values(results)
|
|
@@ -45,9 +49,11 @@ describe('NO-SQL', async () => {
|
|
|
45
49
|
})
|
|
46
50
|
test('$lte — returns documents where field is less than or equal to value', async () => {
|
|
47
51
|
let results = {}
|
|
48
|
-
for await (const data of
|
|
49
|
-
|
|
50
|
-
|
|
52
|
+
for await (const data of fylo
|
|
53
|
+
.findDocs(ALBUMS, {
|
|
54
|
+
$ops: [{ userId: { $lte: 5 } }]
|
|
55
|
+
})
|
|
56
|
+
.collect()) {
|
|
51
57
|
results = { ...results, ...data }
|
|
52
58
|
}
|
|
53
59
|
const albums = Object.values(results)
|
|
@@ -57,9 +63,11 @@ describe('NO-SQL', async () => {
|
|
|
57
63
|
})
|
|
58
64
|
test('$gt — returns documents where field is greater than value', async () => {
|
|
59
65
|
let results = {}
|
|
60
|
-
for await (const data of
|
|
61
|
-
|
|
62
|
-
|
|
66
|
+
for await (const data of fylo
|
|
67
|
+
.findDocs(ALBUMS, {
|
|
68
|
+
$ops: [{ userId: { $gt: 5 } }]
|
|
69
|
+
})
|
|
70
|
+
.collect()) {
|
|
63
71
|
results = { ...results, ...data }
|
|
64
72
|
}
|
|
65
73
|
const albums = Object.values(results)
|
|
@@ -69,9 +77,11 @@ describe('NO-SQL', async () => {
|
|
|
69
77
|
})
|
|
70
78
|
test('$gte — returns documents where field is greater than or equal to value', async () => {
|
|
71
79
|
let results = {}
|
|
72
|
-
for await (const data of
|
|
73
|
-
|
|
74
|
-
|
|
80
|
+
for await (const data of fylo
|
|
81
|
+
.findDocs(ALBUMS, {
|
|
82
|
+
$ops: [{ userId: { $gte: 5 } }]
|
|
83
|
+
})
|
|
84
|
+
.collect()) {
|
|
75
85
|
results = { ...results, ...data }
|
|
76
86
|
}
|
|
77
87
|
const albums = Object.values(results)
|
|
@@ -81,9 +91,11 @@ describe('NO-SQL', async () => {
|
|
|
81
91
|
})
|
|
82
92
|
test('$like — matches substring pattern', async () => {
|
|
83
93
|
let results = {}
|
|
84
|
-
for await (const data of
|
|
85
|
-
|
|
86
|
-
|
|
94
|
+
for await (const data of fylo
|
|
95
|
+
.findDocs(ALBUMS, {
|
|
96
|
+
$ops: [{ title: { $like: '%quidem%' } }]
|
|
97
|
+
})
|
|
98
|
+
.collect()) {
|
|
87
99
|
results = { ...results, ...data }
|
|
88
100
|
}
|
|
89
101
|
const albums = Object.values(results)
|
|
@@ -93,9 +105,11 @@ describe('NO-SQL', async () => {
|
|
|
93
105
|
})
|
|
94
106
|
test('$like — prefix pattern', async () => {
|
|
95
107
|
let results = {}
|
|
96
|
-
for await (const data of
|
|
97
|
-
|
|
98
|
-
|
|
108
|
+
for await (const data of fylo
|
|
109
|
+
.findDocs(ALBUMS, {
|
|
110
|
+
$ops: [{ title: { $like: 'omnis%' } }]
|
|
111
|
+
})
|
|
112
|
+
.collect()) {
|
|
99
113
|
results = { ...results, ...data }
|
|
100
114
|
}
|
|
101
115
|
const albums = Object.values(results)
|
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
import { test, expect, describe, beforeAll, afterAll
|
|
1
|
+
import { test, expect, describe, beforeAll, afterAll } from 'bun:test'
|
|
2
|
+
import { rm } from 'node:fs/promises'
|
|
2
3
|
import Fylo from '../../src'
|
|
3
4
|
import { albumURL, postsURL } from '../data'
|
|
4
|
-
import
|
|
5
|
-
import RedisMock from '../mocks/redis'
|
|
5
|
+
import { createTestRoot } from '../helpers/root'
|
|
6
6
|
const POSTS = `post`
|
|
7
7
|
const ALBUMS = `album`
|
|
8
8
|
let count = 0
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
mock.module('../../src/adapters/redis', () => ({ Redis: RedisMock }))
|
|
9
|
+
const root = await createTestRoot('fylo-read-')
|
|
10
|
+
const fylo = new Fylo({ root })
|
|
12
11
|
beforeAll(async () => {
|
|
13
|
-
await Promise.all([
|
|
12
|
+
await Promise.all([fylo.createCollection(ALBUMS), fylo.executeSQL(`CREATE TABLE ${POSTS}`)])
|
|
14
13
|
try {
|
|
15
14
|
count = await fylo.importBulkData(ALBUMS, new URL(albumURL), 100)
|
|
16
15
|
await fylo.importBulkData(POSTS, new URL(postsURL), 100)
|
|
@@ -19,19 +18,20 @@ beforeAll(async () => {
|
|
|
19
18
|
}
|
|
20
19
|
})
|
|
21
20
|
afterAll(async () => {
|
|
22
|
-
await Promise.all([
|
|
21
|
+
await Promise.all([fylo.dropCollection(ALBUMS), fylo.dropCollection(POSTS)])
|
|
22
|
+
await rm(root, { recursive: true, force: true })
|
|
23
23
|
})
|
|
24
24
|
describe('NO-SQL', async () => {
|
|
25
25
|
test('SELECT ALL', async () => {
|
|
26
26
|
let results = {}
|
|
27
|
-
for await (const data of
|
|
27
|
+
for await (const data of fylo.findDocs(ALBUMS).collect()) {
|
|
28
28
|
results = { ...results, ...data }
|
|
29
29
|
}
|
|
30
30
|
expect(Object.keys(results).length).toBe(count)
|
|
31
31
|
})
|
|
32
32
|
test('SELECT PARTIAL', async () => {
|
|
33
33
|
let results = {}
|
|
34
|
-
for await (const data of
|
|
34
|
+
for await (const data of fylo.findDocs(ALBUMS, { $select: ['title'] }).collect()) {
|
|
35
35
|
results = { ...results, ...data }
|
|
36
36
|
}
|
|
37
37
|
const allAlbums = Object.values(results)
|
|
@@ -40,18 +40,20 @@ describe('NO-SQL', async () => {
|
|
|
40
40
|
})
|
|
41
41
|
test('GET ONE', async () => {
|
|
42
42
|
const ids = []
|
|
43
|
-
for await (const data of
|
|
43
|
+
for await (const data of fylo.findDocs(ALBUMS, { $limit: 1, $onlyIds: true }).collect()) {
|
|
44
44
|
ids.push(data)
|
|
45
45
|
}
|
|
46
|
-
const result = await
|
|
46
|
+
const result = await fylo.getDoc(ALBUMS, ids[0]).once()
|
|
47
47
|
const _id = Object.keys(result).shift()
|
|
48
48
|
expect(ids[0]).toEqual(_id)
|
|
49
49
|
})
|
|
50
50
|
test('SELECT CLAUSE', async () => {
|
|
51
51
|
let results = {}
|
|
52
|
-
for await (const data of
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
for await (const data of fylo
|
|
53
|
+
.findDocs(ALBUMS, {
|
|
54
|
+
$ops: [{ userId: { $eq: 2 } }]
|
|
55
|
+
})
|
|
56
|
+
.collect()) {
|
|
55
57
|
results = { ...results, ...data }
|
|
56
58
|
}
|
|
57
59
|
const allAlbums = Object.values(results)
|
|
@@ -60,23 +62,25 @@ describe('NO-SQL', async () => {
|
|
|
60
62
|
})
|
|
61
63
|
test('SELECT LIMIT', async () => {
|
|
62
64
|
let results = {}
|
|
63
|
-
for await (const data of
|
|
65
|
+
for await (const data of fylo.findDocs(ALBUMS, { $limit: 5 }).collect()) {
|
|
64
66
|
results = { ...results, ...data }
|
|
65
67
|
}
|
|
66
68
|
expect(Object.keys(results).length).toBe(5)
|
|
67
69
|
})
|
|
68
70
|
test('SELECT GROUP BY', async () => {
|
|
69
71
|
let results = {}
|
|
70
|
-
for await (const data of
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
for await (const data of fylo
|
|
73
|
+
.findDocs(ALBUMS, {
|
|
74
|
+
$groupby: 'userId',
|
|
75
|
+
$onlyIds: true
|
|
76
|
+
})
|
|
77
|
+
.collect()) {
|
|
74
78
|
results = Object.appendGroup(results, data)
|
|
75
79
|
}
|
|
76
80
|
expect(Object.keys(results).length).toBeGreaterThan(0)
|
|
77
81
|
})
|
|
78
82
|
test('SELECT JOIN', async () => {
|
|
79
|
-
const results = await
|
|
83
|
+
const results = await fylo.joinDocs({
|
|
80
84
|
$leftCollection: ALBUMS,
|
|
81
85
|
$rightCollection: POSTS,
|
|
82
86
|
$mode: 'inner',
|
|
@@ -1,60 +1,30 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
|
|
2
|
+
import { mkdtemp, rm } from 'node:fs/promises'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
2
5
|
import Fylo from '../../src'
|
|
3
|
-
|
|
4
|
-
|
|
6
|
+
|
|
7
|
+
const root = await mkdtemp(path.join(os.tmpdir(), 'fylo-rollback-'))
|
|
5
8
|
const POSTS = 'rb-post'
|
|
6
|
-
const fylo = new Fylo()
|
|
7
|
-
|
|
8
|
-
mock.module('../../src/adapters/redis', () => ({ Redis: RedisMock }))
|
|
9
|
+
const fylo = new Fylo({ root })
|
|
10
|
+
|
|
9
11
|
beforeAll(async () => {
|
|
10
|
-
await
|
|
12
|
+
await fylo.createCollection(POSTS)
|
|
11
13
|
})
|
|
14
|
+
|
|
12
15
|
afterAll(async () => {
|
|
13
|
-
await
|
|
16
|
+
await rm(root, { recursive: true, force: true })
|
|
14
17
|
})
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
title: 'Rollback Me',
|
|
21
|
-
body: 'This document should disappear after rollback'
|
|
18
|
+
|
|
19
|
+
describe('rollback compatibility', () => {
|
|
20
|
+
test('rollback is a safe no-op in filesystem-first FYLO', async () => {
|
|
21
|
+
const id = await fylo.putData(POSTS, {
|
|
22
|
+
title: 'Still here'
|
|
22
23
|
})
|
|
23
|
-
|
|
24
|
-
expect(
|
|
25
|
-
|
|
26
|
-
const after = await
|
|
27
|
-
expect(
|
|
28
|
-
})
|
|
29
|
-
test('DELETE then rollback — document is restored', async () => {
|
|
30
|
-
const freshFylo = new Fylo()
|
|
31
|
-
const _id = await freshFylo.putData(POSTS, {
|
|
32
|
-
userId: 99,
|
|
33
|
-
id: 9002,
|
|
34
|
-
title: 'Restore Me',
|
|
35
|
-
body: 'This document should reappear after delete rollback'
|
|
36
|
-
})
|
|
37
|
-
const deleteInstance = new Fylo()
|
|
38
|
-
await deleteInstance.delDoc(POSTS, _id)
|
|
39
|
-
const after = await Fylo.getDoc(POSTS, _id).once()
|
|
40
|
-
expect(Object.keys(after).length).toBe(0)
|
|
41
|
-
await deleteInstance.rollback()
|
|
42
|
-
const restored = await Fylo.getDoc(POSTS, _id).once()
|
|
43
|
-
expect(Object.keys(restored).length).toBe(1)
|
|
44
|
-
expect(restored[_id].title).toBe('Restore Me')
|
|
45
|
-
})
|
|
46
|
-
test('batch INSERT then rollback — all documents are removed', async () => {
|
|
47
|
-
const batchFylo = new Fylo()
|
|
48
|
-
const batch = [
|
|
49
|
-
{ userId: 98, id: 9003, title: 'Batch A', body: 'body a' },
|
|
50
|
-
{ userId: 98, id: 9004, title: 'Batch B', body: 'body b' },
|
|
51
|
-
{ userId: 98, id: 9005, title: 'Batch C', body: 'body c' }
|
|
52
|
-
]
|
|
53
|
-
const ids = await batchFylo.batchPutData(POSTS, batch)
|
|
54
|
-
const beforeResults = await Promise.all(ids.map((id) => Fylo.getDoc(POSTS, id).once()))
|
|
55
|
-
expect(beforeResults.every((r) => Object.keys(r).length === 1)).toBe(true)
|
|
56
|
-
await batchFylo.rollback()
|
|
57
|
-
const afterResults = await Promise.all(ids.map((id) => Fylo.getDoc(POSTS, id).once()))
|
|
58
|
-
expect(afterResults.every((r) => Object.keys(r).length === 0)).toBe(true)
|
|
24
|
+
|
|
25
|
+
await expect(fylo.rollback()).resolves.toBeUndefined()
|
|
26
|
+
|
|
27
|
+
const after = await fylo.getDoc(POSTS, id).once()
|
|
28
|
+
expect(after[id].title).toBe('Still here')
|
|
59
29
|
})
|
|
60
30
|
})
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
|
|
2
|
+
import { mkdtemp, rm, stat } from 'node:fs/promises'
|
|
3
|
+
import { performance } from 'node:perf_hooks'
|
|
4
|
+
import os from 'node:os'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import Fylo from '../../src'
|
|
7
|
+
|
|
8
|
+
const runPerf = process.env.FYLO_RUN_PERF_TESTS === 'true'
|
|
9
|
+
|
|
10
|
+
describe.skipIf(!runPerf)('s3-files engine performance', () => {
|
|
11
|
+
let root = path.join(os.tmpdir(), `fylo-s3files-perf-${Date.now()}`)
|
|
12
|
+
const collection = 's3files-perf'
|
|
13
|
+
let fylo = new Fylo({ root })
|
|
14
|
+
|
|
15
|
+
beforeAll(async () => {
|
|
16
|
+
root = await mkdtemp(root)
|
|
17
|
+
fylo = new Fylo({ root })
|
|
18
|
+
await fylo.createCollection(collection)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
afterAll(async () => {
|
|
22
|
+
await rm(root, { recursive: true, force: true })
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('keeps a single durable index file while querying a large dataset', async () => {
|
|
26
|
+
const totalDocs = 2000
|
|
27
|
+
const insertStart = performance.now()
|
|
28
|
+
|
|
29
|
+
for (let index = 0; index < totalDocs; index++) {
|
|
30
|
+
await fylo.putData(collection, {
|
|
31
|
+
title: `doc-${index}`,
|
|
32
|
+
group: index % 10,
|
|
33
|
+
tags: [`tag-${index % 5}`, `batch-${Math.floor(index / 100)}`],
|
|
34
|
+
meta: { score: index }
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const insertMs = performance.now() - insertStart
|
|
39
|
+
|
|
40
|
+
const exactStart = performance.now()
|
|
41
|
+
let exactResults = {}
|
|
42
|
+
for await (const data of fylo
|
|
43
|
+
.findDocs(collection, {
|
|
44
|
+
$ops: [{ title: { $eq: 'doc-1555' } }]
|
|
45
|
+
})
|
|
46
|
+
.collect()) {
|
|
47
|
+
exactResults = { ...exactResults, ...data }
|
|
48
|
+
}
|
|
49
|
+
const exactMs = performance.now() - exactStart
|
|
50
|
+
|
|
51
|
+
const rangeStart = performance.now()
|
|
52
|
+
let rangeCount = 0
|
|
53
|
+
for await (const data of fylo
|
|
54
|
+
.findDocs(collection, {
|
|
55
|
+
$ops: [{ ['meta.score']: { $gte: 1900 } }]
|
|
56
|
+
})
|
|
57
|
+
.collect()) {
|
|
58
|
+
rangeCount += Object.keys(data).length
|
|
59
|
+
}
|
|
60
|
+
const rangeMs = performance.now() - rangeStart
|
|
61
|
+
|
|
62
|
+
const indexFile = path.join(root, collection, '.fylo', 'indexes', `${collection}.idx.json`)
|
|
63
|
+
const indexStats = await stat(indexFile)
|
|
64
|
+
|
|
65
|
+
expect(Object.keys(exactResults)).toHaveLength(1)
|
|
66
|
+
expect(rangeCount).toBe(100)
|
|
67
|
+
expect(indexStats.isFile()).toBe(true)
|
|
68
|
+
|
|
69
|
+
console.log(
|
|
70
|
+
`[FYLO perf] docs=${totalDocs} insertMs=${insertMs.toFixed(1)} exactMs=${exactMs.toFixed(
|
|
71
|
+
1
|
|
72
|
+
)} rangeMs=${rangeMs.toFixed(1)} indexBytes=${indexStats.size}`
|
|
73
|
+
)
|
|
74
|
+
}, 120_000)
|
|
75
|
+
})
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
|
|
2
|
-
import { mkdtemp, rm, stat } from 'node:fs/promises'
|
|
2
|
+
import { mkdtemp, readFile, rm, stat } from 'node:fs/promises'
|
|
3
3
|
import os from 'node:os'
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
import Fylo from '../../src'
|
|
6
6
|
const root = await mkdtemp(path.join(os.tmpdir(), 'fylo-s3files-'))
|
|
7
|
-
const fylo = new Fylo({
|
|
7
|
+
const fylo = new Fylo({ root })
|
|
8
8
|
const POSTS = 's3files-posts'
|
|
9
9
|
const USERS = 's3files-users'
|
|
10
10
|
describe('s3-files engine', () => {
|
|
@@ -59,10 +59,109 @@ describe('s3-files engine', () => {
|
|
|
59
59
|
const result = await fylo.getDoc(POSTS, id).once()
|
|
60
60
|
expect(result[id].body).toBe(longBody)
|
|
61
61
|
})
|
|
62
|
-
test('stores
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
test('stores only user document data in the file body', async () => {
|
|
63
|
+
const id = await fylo.putData(POSTS, {
|
|
64
|
+
title: 'Lean doc',
|
|
65
|
+
body: 'payload only'
|
|
66
|
+
})
|
|
67
|
+
const raw = JSON.parse(
|
|
68
|
+
await readFile(
|
|
69
|
+
path.join(root, POSTS, '.fylo', 'docs', id.slice(0, 2), `${id}.json`),
|
|
70
|
+
'utf8'
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
expect(raw).toEqual({
|
|
75
|
+
title: 'Lean doc',
|
|
76
|
+
body: 'payload only'
|
|
77
|
+
})
|
|
78
|
+
expect(raw.id).toBeUndefined()
|
|
79
|
+
expect(raw.createdAt).toBeUndefined()
|
|
80
|
+
expect(raw.updatedAt).toBeUndefined()
|
|
81
|
+
})
|
|
82
|
+
test('stores indexes in a single collection file instead of SQLite or per-entry files', async () => {
|
|
83
|
+
const indexStat = await stat(
|
|
84
|
+
path.join(root, POSTS, '.fylo', 'indexes', `${POSTS}.idx.json`)
|
|
85
|
+
)
|
|
86
|
+
expect(indexStat.isFile()).toBe(true)
|
|
87
|
+
await expect(stat(path.join(root, POSTS, '.fylo', 'index.db'))).rejects.toThrow()
|
|
88
|
+
})
|
|
89
|
+
test('uses the collection index file to support exact, range, and contains queries', async () => {
|
|
90
|
+
const queryCollection = 's3files-query'
|
|
91
|
+
await fylo.createCollection(queryCollection)
|
|
92
|
+
|
|
93
|
+
const bunId = await fylo.putData(queryCollection, {
|
|
94
|
+
title: 'Bun launch',
|
|
95
|
+
tags: ['bun', 'aws'],
|
|
96
|
+
meta: { score: 10 }
|
|
97
|
+
})
|
|
98
|
+
const nodeId = await fylo.putData(queryCollection, {
|
|
99
|
+
title: 'Node launch',
|
|
100
|
+
tags: ['node'],
|
|
101
|
+
meta: { score: 2 }
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
let eqResults = {}
|
|
105
|
+
for await (const data of fylo
|
|
106
|
+
.findDocs(queryCollection, {
|
|
107
|
+
$ops: [{ title: { $eq: 'Bun launch' } }]
|
|
108
|
+
})
|
|
109
|
+
.collect()) {
|
|
110
|
+
eqResults = { ...eqResults, ...data }
|
|
111
|
+
}
|
|
112
|
+
expect(Object.keys(eqResults)).toEqual([bunId])
|
|
113
|
+
|
|
114
|
+
let rangeResults = {}
|
|
115
|
+
for await (const data of fylo
|
|
116
|
+
.findDocs(queryCollection, {
|
|
117
|
+
$ops: [{ ['meta.score']: { $gte: 5 } }]
|
|
118
|
+
})
|
|
119
|
+
.collect()) {
|
|
120
|
+
rangeResults = { ...rangeResults, ...data }
|
|
121
|
+
}
|
|
122
|
+
expect(Object.keys(rangeResults)).toEqual([bunId])
|
|
123
|
+
|
|
124
|
+
let containsResults = {}
|
|
125
|
+
for await (const data of fylo
|
|
126
|
+
.findDocs(queryCollection, {
|
|
127
|
+
$ops: [{ tags: { $contains: 'aws' } }]
|
|
128
|
+
})
|
|
129
|
+
.collect()) {
|
|
130
|
+
containsResults = { ...containsResults, ...data }
|
|
131
|
+
}
|
|
132
|
+
expect(Object.keys(containsResults)).toEqual([bunId])
|
|
133
|
+
expect(containsResults[nodeId]).toBeUndefined()
|
|
134
|
+
|
|
135
|
+
const index = JSON.parse(
|
|
136
|
+
await readFile(
|
|
137
|
+
path.join(root, queryCollection, '.fylo', 'indexes', `${queryCollection}.idx.json`),
|
|
138
|
+
'utf8'
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
const rows = index.docs[bunId]
|
|
142
|
+
|
|
143
|
+
expect(rows).toEqual(
|
|
144
|
+
expect.arrayContaining([
|
|
145
|
+
expect.objectContaining({
|
|
146
|
+
fieldPath: 'title',
|
|
147
|
+
rawValue: 'Bun launch',
|
|
148
|
+
valueType: 'string',
|
|
149
|
+
numericValue: null
|
|
150
|
+
}),
|
|
151
|
+
expect.objectContaining({
|
|
152
|
+
fieldPath: 'meta/score',
|
|
153
|
+
rawValue: '10',
|
|
154
|
+
valueType: 'number',
|
|
155
|
+
numericValue: 10
|
|
156
|
+
}),
|
|
157
|
+
expect.objectContaining({
|
|
158
|
+
fieldPath: 'tags/1',
|
|
159
|
+
rawValue: 'aws',
|
|
160
|
+
valueType: 'string',
|
|
161
|
+
numericValue: null
|
|
162
|
+
})
|
|
163
|
+
])
|
|
164
|
+
)
|
|
66
165
|
})
|
|
67
166
|
test('joins work in s3-files mode', async () => {
|
|
68
167
|
const userId = await fylo.putData(USERS, { id: 42, name: 'Ada' })
|
|
@@ -79,30 +178,28 @@ describe('s3-files engine', () => {
|
|
|
79
178
|
})
|
|
80
179
|
test('queue APIs are explicitly unsupported', async () => {
|
|
81
180
|
await expect(fylo.queuePutData(POSTS, { title: 'no queue' })).rejects.toThrow(
|
|
82
|
-
'queuePutData
|
|
83
|
-
)
|
|
84
|
-
await expect(fylo.processQueuedWrites(1)).rejects.toThrow(
|
|
85
|
-
'processQueuedWrites is not supported'
|
|
181
|
+
'queuePutData was removed'
|
|
86
182
|
)
|
|
183
|
+
await expect(fylo.processQueuedWrites(1)).rejects.toThrow('processQueuedWrites was removed')
|
|
87
184
|
})
|
|
88
185
|
test('rejects collection names that are unsafe for cross-platform filesystems', async () => {
|
|
89
186
|
await expect(fylo.createCollection('bad/name')).rejects.toThrow('Invalid collection name')
|
|
90
187
|
await expect(fylo.createCollection('bad\\name')).rejects.toThrow('Invalid collection name')
|
|
91
188
|
await expect(fylo.createCollection('bad:name')).rejects.toThrow('Invalid collection name')
|
|
92
189
|
})
|
|
93
|
-
test('static helpers can use
|
|
94
|
-
const
|
|
95
|
-
const
|
|
96
|
-
process.env.
|
|
190
|
+
test('static helpers can use filesystem root env defaults', async () => {
|
|
191
|
+
const prevFyloRoot = process.env.FYLO_ROOT
|
|
192
|
+
const prevS3FilesRoot = process.env.FYLO_S3FILES_ROOT
|
|
193
|
+
process.env.FYLO_ROOT = root
|
|
97
194
|
process.env.FYLO_S3FILES_ROOT = root
|
|
98
195
|
const collection = 's3files-static'
|
|
99
196
|
await Fylo.createCollection(collection)
|
|
100
197
|
const id = await fylo.putData(collection, { title: 'Static path' })
|
|
101
198
|
const result = await Fylo.getDoc(collection, id).once()
|
|
102
199
|
expect(result[id].title).toBe('Static path')
|
|
103
|
-
if (
|
|
104
|
-
else process.env.
|
|
105
|
-
if (
|
|
106
|
-
else process.env.FYLO_S3FILES_ROOT =
|
|
200
|
+
if (prevFyloRoot === undefined) delete process.env.FYLO_ROOT
|
|
201
|
+
else process.env.FYLO_ROOT = prevFyloRoot
|
|
202
|
+
if (prevS3FilesRoot === undefined) delete process.env.FYLO_S3FILES_ROOT
|
|
203
|
+
else process.env.FYLO_S3FILES_ROOT = prevS3FilesRoot
|
|
107
204
|
})
|
|
108
205
|
})
|