@delma/fylo 2.0.1 → 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 -690
- 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 +57 -44
- 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,22 +1,22 @@
|
|
|
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 TTID from '@delma/ttid'
|
|
4
|
-
import
|
|
5
|
-
import RedisMock from '../mocks/redis'
|
|
5
|
+
import { createTestRoot } from '../helpers/root'
|
|
6
6
|
const COLLECTION = 'ec-test'
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
mock.module('../../src/adapters/redis', () => ({ Redis: RedisMock }))
|
|
7
|
+
const root = await createTestRoot('fylo-edge-')
|
|
8
|
+
const fylo = new Fylo({ root })
|
|
10
9
|
beforeAll(async () => {
|
|
11
|
-
await
|
|
10
|
+
await fylo.createCollection(COLLECTION)
|
|
12
11
|
})
|
|
13
12
|
afterAll(async () => {
|
|
14
|
-
await
|
|
13
|
+
await fylo.dropCollection(COLLECTION)
|
|
14
|
+
await rm(root, { recursive: true, force: true })
|
|
15
15
|
})
|
|
16
16
|
describe('NO-SQL', () => {
|
|
17
17
|
test('GET ONE — non-existent ID returns empty object', async () => {
|
|
18
18
|
const fakeId = TTID.generate()
|
|
19
|
-
const result = await
|
|
19
|
+
const result = await fylo.getDoc(COLLECTION, fakeId).once()
|
|
20
20
|
expect(Object.keys(result).length).toBe(0)
|
|
21
21
|
})
|
|
22
22
|
test('PUT / GET — forward slashes in values round-trip correctly', async () => {
|
|
@@ -27,7 +27,7 @@ describe('NO-SQL', () => {
|
|
|
27
27
|
body: 'https://example.com/api/v1/resource'
|
|
28
28
|
}
|
|
29
29
|
const _id = await fylo.putData(COLLECTION, original)
|
|
30
|
-
const result = await
|
|
30
|
+
const result = await fylo.getDoc(COLLECTION, _id).once()
|
|
31
31
|
const doc = result[_id]
|
|
32
32
|
expect(doc.body).toBe(original.body)
|
|
33
33
|
await fylo.delDoc(COLLECTION, _id)
|
|
@@ -40,12 +40,12 @@ describe('NO-SQL', () => {
|
|
|
40
40
|
body: 'https://cdn.example.com//assets//image.png'
|
|
41
41
|
}
|
|
42
42
|
const _id = await fylo.putData(COLLECTION, original)
|
|
43
|
-
const result = await
|
|
43
|
+
const result = await fylo.getDoc(COLLECTION, _id).once()
|
|
44
44
|
expect(result[_id].body).toBe(original.body)
|
|
45
45
|
await fylo.delDoc(COLLECTION, _id)
|
|
46
46
|
})
|
|
47
47
|
test('$ops — multiple conditions act as OR union', async () => {
|
|
48
|
-
const cleanFylo = new Fylo()
|
|
48
|
+
const cleanFylo = new Fylo({ root })
|
|
49
49
|
const id1 = await cleanFylo.putData(COLLECTION, {
|
|
50
50
|
userId: 10,
|
|
51
51
|
id: 100,
|
|
@@ -59,9 +59,11 @@ describe('NO-SQL', () => {
|
|
|
59
59
|
body: 'second'
|
|
60
60
|
})
|
|
61
61
|
const results = {}
|
|
62
|
-
for await (const data of
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
for await (const data of fylo
|
|
63
|
+
.findDocs(COLLECTION, {
|
|
64
|
+
$ops: [{ userId: { $eq: 10 } }, { userId: { $eq: 20 } }]
|
|
65
|
+
})
|
|
66
|
+
.collect()) {
|
|
65
67
|
Object.assign(results, data)
|
|
66
68
|
}
|
|
67
69
|
expect(results[id1]).toBeDefined()
|
|
@@ -70,7 +72,7 @@ describe('NO-SQL', () => {
|
|
|
70
72
|
await cleanFylo.delDoc(COLLECTION, id2)
|
|
71
73
|
})
|
|
72
74
|
test('$rename — renames fields in query output', async () => {
|
|
73
|
-
const cleanFylo = new Fylo()
|
|
75
|
+
const cleanFylo = new Fylo({ root })
|
|
74
76
|
const _id = await cleanFylo.putData(COLLECTION, {
|
|
75
77
|
userId: 1,
|
|
76
78
|
id: 300,
|
|
@@ -78,10 +80,12 @@ describe('NO-SQL', () => {
|
|
|
78
80
|
body: 'some body'
|
|
79
81
|
})
|
|
80
82
|
let renamed = {}
|
|
81
|
-
for await (const data of
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
for await (const data of fylo
|
|
84
|
+
.findDocs(COLLECTION, {
|
|
85
|
+
$ops: [{ id: { $eq: 300 } }],
|
|
86
|
+
$rename: { title: 'name' }
|
|
87
|
+
})
|
|
88
|
+
.collect()) {
|
|
85
89
|
renamed = Object.values(data)[0]
|
|
86
90
|
}
|
|
87
91
|
expect(renamed.name).toBe('Rename Me')
|
|
@@ -89,7 +93,7 @@ describe('NO-SQL', () => {
|
|
|
89
93
|
await cleanFylo.delDoc(COLLECTION, _id)
|
|
90
94
|
})
|
|
91
95
|
test('versioned putData — preserves creation-time prefix in TTID', async () => {
|
|
92
|
-
const cleanFylo = new Fylo()
|
|
96
|
+
const cleanFylo = new Fylo({ root })
|
|
93
97
|
const _id1 = await cleanFylo.putData(COLLECTION, {
|
|
94
98
|
userId: 1,
|
|
95
99
|
id: 400,
|
|
@@ -100,7 +104,7 @@ describe('NO-SQL', () => {
|
|
|
100
104
|
[_id1]: { userId: 1, id: 400, title: 'Updated', body: 'v2' }
|
|
101
105
|
})
|
|
102
106
|
expect(_id2.split('-')[0]).toBe(_id1.split('-')[0])
|
|
103
|
-
const result = await
|
|
107
|
+
const result = await fylo.getDoc(COLLECTION, _id2).once()
|
|
104
108
|
const doc = result[_id2]
|
|
105
109
|
expect(doc).toBeDefined()
|
|
106
110
|
expect(doc.title).toBe('Updated')
|
|
@@ -108,7 +112,7 @@ describe('NO-SQL', () => {
|
|
|
108
112
|
await cleanFylo.delDoc(COLLECTION, _id2)
|
|
109
113
|
})
|
|
110
114
|
test('versioned putData — original version is no longer retrievable by old full TTID', async () => {
|
|
111
|
-
const cleanFylo = new Fylo()
|
|
115
|
+
const cleanFylo = new Fylo({ root })
|
|
112
116
|
const _id1 = await cleanFylo.putData(COLLECTION, {
|
|
113
117
|
userId: 1,
|
|
114
118
|
id: 500,
|
|
@@ -124,7 +128,7 @@ describe('NO-SQL', () => {
|
|
|
124
128
|
})
|
|
125
129
|
describe('SQL', () => {
|
|
126
130
|
test('UPDATE ONE — update a single document by querying its unique field', async () => {
|
|
127
|
-
const cleanFylo = new Fylo()
|
|
131
|
+
const cleanFylo = new Fylo({ root })
|
|
128
132
|
await cleanFylo.putData(COLLECTION, {
|
|
129
133
|
userId: 1,
|
|
130
134
|
id: 600,
|
|
@@ -142,7 +146,7 @@ describe('SQL', () => {
|
|
|
142
146
|
expect(Object.values(results)[0].title).toBe('After SQL Update')
|
|
143
147
|
})
|
|
144
148
|
test('DELETE ONE — delete a single document by querying its unique field', async () => {
|
|
145
|
-
const cleanFylo = new Fylo()
|
|
149
|
+
const cleanFylo = new Fylo({ root })
|
|
146
150
|
await cleanFylo.putData(COLLECTION, {
|
|
147
151
|
userId: 1,
|
|
148
152
|
id: 700,
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
import { test, expect, describe, beforeAll, afterAll, mock } from 'bun:test'
|
|
2
|
+
import { readFile, rm } from 'node:fs/promises'
|
|
3
|
+
import path from 'node:path'
|
|
2
4
|
import Fylo from '../../src'
|
|
3
|
-
import
|
|
4
|
-
import RedisMock from '../mocks/redis'
|
|
5
|
+
import { createTestRoot } from '../helpers/root'
|
|
5
6
|
import { CipherMock } from '../mocks/cipher'
|
|
6
7
|
const COLLECTION = 'encrypted-test'
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
mock.module('../../src/adapters/redis', () => ({ Redis: RedisMock }))
|
|
8
|
+
const root = await createTestRoot('fylo-encryption-')
|
|
9
|
+
const fylo = new Fylo({ root })
|
|
10
10
|
mock.module('../../src/adapters/cipher', () => ({ Cipher: CipherMock }))
|
|
11
11
|
beforeAll(async () => {
|
|
12
|
-
await
|
|
12
|
+
await fylo.createCollection(COLLECTION)
|
|
13
13
|
await CipherMock.configure('test-secret-key')
|
|
14
14
|
CipherMock.registerFields(COLLECTION, ['email', 'ssn', 'address'])
|
|
15
15
|
})
|
|
16
16
|
afterAll(async () => {
|
|
17
17
|
CipherMock.reset()
|
|
18
|
-
await
|
|
18
|
+
await fylo.dropCollection(COLLECTION)
|
|
19
|
+
await rm(root, { recursive: true, force: true })
|
|
19
20
|
})
|
|
20
21
|
describe('Encryption', () => {
|
|
21
22
|
let docId
|
|
@@ -29,22 +30,28 @@ describe('Encryption', () => {
|
|
|
29
30
|
expect(docId).toBeDefined()
|
|
30
31
|
})
|
|
31
32
|
test('GET decrypts fields transparently', async () => {
|
|
32
|
-
const result = await
|
|
33
|
+
const result = await fylo.getDoc(COLLECTION, docId).once()
|
|
33
34
|
const doc = Object.values(result)[0]
|
|
34
35
|
expect(doc.name).toBe('Alice')
|
|
35
36
|
expect(doc.email).toBe('alice@example.com')
|
|
36
37
|
expect(doc.ssn).toBe('123-45-6789')
|
|
37
38
|
expect(doc.age).toBe(30)
|
|
38
39
|
})
|
|
39
|
-
test('encrypted values stored in
|
|
40
|
-
const
|
|
41
|
-
|
|
40
|
+
test('encrypted values stored in the doc file are not plaintext', async () => {
|
|
41
|
+
const raw = await readFile(
|
|
42
|
+
path.join(root, COLLECTION, '.fylo', 'docs', docId.slice(0, 2), `${docId}.json`),
|
|
43
|
+
'utf8'
|
|
44
|
+
)
|
|
45
|
+
expect(raw).not.toContain('alice@example.com')
|
|
46
|
+
expect(raw).not.toContain('123-45-6789')
|
|
42
47
|
})
|
|
43
48
|
test('$eq query works on encrypted field', async () => {
|
|
44
49
|
let found = false
|
|
45
|
-
for await (const data of
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
for await (const data of fylo
|
|
51
|
+
.findDocs(COLLECTION, {
|
|
52
|
+
$ops: [{ email: { $eq: 'alice@example.com' } }]
|
|
53
|
+
})
|
|
54
|
+
.collect()) {
|
|
48
55
|
if (typeof data === 'object') {
|
|
49
56
|
const doc = Object.values(data)[0]
|
|
50
57
|
expect(doc.email).toBe('alice@example.com')
|
|
@@ -55,9 +62,11 @@ describe('Encryption', () => {
|
|
|
55
62
|
})
|
|
56
63
|
test('$ne throws on encrypted field', async () => {
|
|
57
64
|
try {
|
|
58
|
-
const iter =
|
|
59
|
-
|
|
60
|
-
|
|
65
|
+
const iter = fylo
|
|
66
|
+
.findDocs(COLLECTION, {
|
|
67
|
+
$ops: [{ email: { $ne: 'bob@example.com' } }]
|
|
68
|
+
})
|
|
69
|
+
.collect()
|
|
61
70
|
await iter.next()
|
|
62
71
|
expect(true).toBe(false)
|
|
63
72
|
} catch (e) {
|
|
@@ -66,9 +75,11 @@ describe('Encryption', () => {
|
|
|
66
75
|
})
|
|
67
76
|
test('$gt throws on encrypted field', async () => {
|
|
68
77
|
try {
|
|
69
|
-
const iter =
|
|
70
|
-
|
|
71
|
-
|
|
78
|
+
const iter = fylo
|
|
79
|
+
.findDocs(COLLECTION, {
|
|
80
|
+
$ops: [{ ssn: { $gt: 0 } }]
|
|
81
|
+
})
|
|
82
|
+
.collect()
|
|
72
83
|
await iter.next()
|
|
73
84
|
expect(true).toBe(false)
|
|
74
85
|
} catch (e) {
|
|
@@ -77,9 +88,11 @@ describe('Encryption', () => {
|
|
|
77
88
|
})
|
|
78
89
|
test('$like throws on encrypted field', async () => {
|
|
79
90
|
try {
|
|
80
|
-
const iter =
|
|
81
|
-
|
|
82
|
-
|
|
91
|
+
const iter = fylo
|
|
92
|
+
.findDocs(COLLECTION, {
|
|
93
|
+
$ops: [{ email: { $like: '%@example.com' } }]
|
|
94
|
+
})
|
|
95
|
+
.collect()
|
|
83
96
|
await iter.next()
|
|
84
97
|
expect(true).toBe(false)
|
|
85
98
|
} catch (e) {
|
|
@@ -88,9 +101,11 @@ describe('Encryption', () => {
|
|
|
88
101
|
})
|
|
89
102
|
test('non-encrypted fields remain queryable with all operators', async () => {
|
|
90
103
|
let found = false
|
|
91
|
-
for await (const data of
|
|
92
|
-
|
|
93
|
-
|
|
104
|
+
for await (const data of fylo
|
|
105
|
+
.findDocs(COLLECTION, {
|
|
106
|
+
$ops: [{ name: { $eq: 'Alice' } }]
|
|
107
|
+
})
|
|
108
|
+
.collect()) {
|
|
94
109
|
if (typeof data === 'object') {
|
|
95
110
|
const doc = Object.values(data)[0]
|
|
96
111
|
expect(doc.name).toBe('Alice')
|
|
@@ -107,7 +122,7 @@ describe('Encryption', () => {
|
|
|
107
122
|
age: 25,
|
|
108
123
|
address: { city: 'Toronto', zip: 'M5V 2T6' }
|
|
109
124
|
})
|
|
110
|
-
const result = await
|
|
125
|
+
const result = await fylo.getDoc(COLLECTION, id).once()
|
|
111
126
|
const doc = Object.values(result)[0]
|
|
112
127
|
expect(doc.address.city).toBe('Toronto')
|
|
113
128
|
expect(doc.address.zip).toBe('M5V 2T6')
|
|
@@ -117,9 +132,11 @@ describe('Encryption', () => {
|
|
|
117
132
|
[docId]: { email: 'alice-new@example.com' }
|
|
118
133
|
})
|
|
119
134
|
let found = false
|
|
120
|
-
for await (const data of
|
|
121
|
-
|
|
122
|
-
|
|
135
|
+
for await (const data of fylo
|
|
136
|
+
.findDocs(COLLECTION, {
|
|
137
|
+
$ops: [{ email: { $eq: 'alice-new@example.com' } }]
|
|
138
|
+
})
|
|
139
|
+
.collect()) {
|
|
123
140
|
if (typeof data === 'object') {
|
|
124
141
|
const doc = Object.values(data)[0]
|
|
125
142
|
expect(doc.email).toBe('alice-new@example.com')
|
|
@@ -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 { postsURL } from '../data'
|
|
4
|
-
import
|
|
5
|
-
import RedisMock from '../mocks/redis'
|
|
5
|
+
import { createTestRoot } from '../helpers/root'
|
|
6
6
|
const POSTS = 'exp-post'
|
|
7
7
|
const IMPORT_LIMIT = 20
|
|
8
8
|
let importedCount = 0
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
mock.module('../../src/adapters/redis', () => ({ Redis: RedisMock }))
|
|
9
|
+
const root = await createTestRoot('fylo-export-')
|
|
10
|
+
const fylo = new Fylo({ root })
|
|
12
11
|
beforeAll(async () => {
|
|
13
|
-
await
|
|
12
|
+
await fylo.createCollection(POSTS)
|
|
14
13
|
try {
|
|
15
14
|
importedCount = await fylo.importBulkData(POSTS, new URL(postsURL), IMPORT_LIMIT)
|
|
16
15
|
} catch {
|
|
@@ -18,18 +17,19 @@ beforeAll(async () => {
|
|
|
18
17
|
}
|
|
19
18
|
})
|
|
20
19
|
afterAll(async () => {
|
|
21
|
-
await
|
|
20
|
+
await fylo.dropCollection(POSTS)
|
|
21
|
+
await rm(root, { recursive: true, force: true })
|
|
22
22
|
})
|
|
23
23
|
describe('NO-SQL', () => {
|
|
24
24
|
test('EXPORT count matches import', async () => {
|
|
25
25
|
let exported = 0
|
|
26
|
-
for await (const _doc of
|
|
26
|
+
for await (const _doc of fylo.exportBulkData(POSTS)) {
|
|
27
27
|
exported++
|
|
28
28
|
}
|
|
29
29
|
expect(exported).toBe(importedCount)
|
|
30
30
|
})
|
|
31
31
|
test('EXPORT document shape', async () => {
|
|
32
|
-
for await (const doc of
|
|
32
|
+
for await (const doc of fylo.exportBulkData(POSTS)) {
|
|
33
33
|
expect(doc).toHaveProperty('title')
|
|
34
34
|
expect(doc).toHaveProperty('userId')
|
|
35
35
|
expect(doc).toHaveProperty('body')
|
|
@@ -37,7 +37,7 @@ describe('NO-SQL', () => {
|
|
|
37
37
|
}
|
|
38
38
|
})
|
|
39
39
|
test('EXPORT all documents are valid posts', async () => {
|
|
40
|
-
for await (const doc of
|
|
40
|
+
for await (const doc of fylo.exportBulkData(POSTS)) {
|
|
41
41
|
expect(typeof doc.title).toBe('string')
|
|
42
42
|
expect(typeof doc.userId).toBe('number')
|
|
43
43
|
expect(doc.userId).toBeGreaterThan(0)
|
|
@@ -1,15 +1,14 @@
|
|
|
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 ALBUMS = 'jm-album'
|
|
7
7
|
const POSTS = 'jm-post'
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
mock.module('../../src/adapters/redis', () => ({ Redis: RedisMock }))
|
|
8
|
+
const root = await createTestRoot('fylo-join-')
|
|
9
|
+
const fylo = new Fylo({ root })
|
|
11
10
|
beforeAll(async () => {
|
|
12
|
-
await Promise.all([
|
|
11
|
+
await Promise.all([fylo.createCollection(ALBUMS), fylo.createCollection(POSTS)])
|
|
13
12
|
try {
|
|
14
13
|
await Promise.all([
|
|
15
14
|
fylo.importBulkData(ALBUMS, new URL(albumURL), 100),
|
|
@@ -20,11 +19,12 @@ beforeAll(async () => {
|
|
|
20
19
|
}
|
|
21
20
|
})
|
|
22
21
|
afterAll(async () => {
|
|
23
|
-
await Promise.all([
|
|
22
|
+
await Promise.all([fylo.dropCollection(ALBUMS), fylo.dropCollection(POSTS)])
|
|
23
|
+
await rm(root, { recursive: true, force: true })
|
|
24
24
|
})
|
|
25
25
|
describe('NO-SQL', async () => {
|
|
26
26
|
test('INNER JOIN — returns only join field values', async () => {
|
|
27
|
-
const results = await
|
|
27
|
+
const results = await fylo.joinDocs({
|
|
28
28
|
$leftCollection: ALBUMS,
|
|
29
29
|
$rightCollection: POSTS,
|
|
30
30
|
$mode: 'inner',
|
|
@@ -38,7 +38,7 @@ describe('NO-SQL', async () => {
|
|
|
38
38
|
}
|
|
39
39
|
})
|
|
40
40
|
test('LEFT JOIN — returns full left-collection document', async () => {
|
|
41
|
-
const results = await
|
|
41
|
+
const results = await fylo.joinDocs({
|
|
42
42
|
$leftCollection: ALBUMS,
|
|
43
43
|
$rightCollection: POSTS,
|
|
44
44
|
$mode: 'left',
|
|
@@ -52,7 +52,7 @@ describe('NO-SQL', async () => {
|
|
|
52
52
|
}
|
|
53
53
|
})
|
|
54
54
|
test('RIGHT JOIN — returns full right-collection document', async () => {
|
|
55
|
-
const results = await
|
|
55
|
+
const results = await fylo.joinDocs({
|
|
56
56
|
$leftCollection: ALBUMS,
|
|
57
57
|
$rightCollection: POSTS,
|
|
58
58
|
$mode: 'right',
|
|
@@ -67,7 +67,7 @@ describe('NO-SQL', async () => {
|
|
|
67
67
|
}
|
|
68
68
|
})
|
|
69
69
|
test('OUTER JOIN — returns merged left + right document', async () => {
|
|
70
|
-
const results = await
|
|
70
|
+
const results = await fylo.joinDocs({
|
|
71
71
|
$leftCollection: ALBUMS,
|
|
72
72
|
$rightCollection: POSTS,
|
|
73
73
|
$mode: 'outer',
|
|
@@ -80,7 +80,7 @@ describe('NO-SQL', async () => {
|
|
|
80
80
|
}
|
|
81
81
|
})
|
|
82
82
|
test('JOIN with $limit — respects the result cap', async () => {
|
|
83
|
-
const results = await
|
|
83
|
+
const results = await fylo.joinDocs({
|
|
84
84
|
$leftCollection: ALBUMS,
|
|
85
85
|
$rightCollection: POSTS,
|
|
86
86
|
$mode: 'inner',
|
|
@@ -90,7 +90,7 @@ describe('NO-SQL', async () => {
|
|
|
90
90
|
expect(Object.keys(results).length).toBe(5)
|
|
91
91
|
})
|
|
92
92
|
test('JOIN with $select — only requested fields are returned', async () => {
|
|
93
|
-
const results = await
|
|
93
|
+
const results = await fylo.joinDocs({
|
|
94
94
|
$leftCollection: ALBUMS,
|
|
95
95
|
$rightCollection: POSTS,
|
|
96
96
|
$mode: 'left',
|
|
@@ -106,7 +106,7 @@ describe('NO-SQL', async () => {
|
|
|
106
106
|
}
|
|
107
107
|
})
|
|
108
108
|
test('JOIN with $groupby — groups results by field value', async () => {
|
|
109
|
-
const results = await
|
|
109
|
+
const results = await fylo.joinDocs({
|
|
110
110
|
$leftCollection: ALBUMS,
|
|
111
111
|
$rightCollection: POSTS,
|
|
112
112
|
$mode: 'inner',
|
|
@@ -116,7 +116,7 @@ describe('NO-SQL', async () => {
|
|
|
116
116
|
expect(Object.keys(results).length).toBeGreaterThan(0)
|
|
117
117
|
})
|
|
118
118
|
test('JOIN with $onlyIds — returns IDs only', async () => {
|
|
119
|
-
const results = await
|
|
119
|
+
const results = await fylo.joinDocs({
|
|
120
120
|
$leftCollection: ALBUMS,
|
|
121
121
|
$rightCollection: POSTS,
|
|
122
122
|
$mode: 'inner',
|
|
@@ -1,38 +1,38 @@
|
|
|
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 { usersURL } from '../data'
|
|
4
|
-
import
|
|
5
|
-
import RedisMock from '../mocks/redis'
|
|
5
|
+
import { createTestRoot } from '../helpers/root'
|
|
6
6
|
const USERS = 'nst-user'
|
|
7
7
|
let insertedCount = 0
|
|
8
8
|
let sampleId
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
mock.module('../../src/adapters/redis', () => ({ Redis: RedisMock }))
|
|
9
|
+
const root = await createTestRoot('fylo-nested-')
|
|
10
|
+
const fylo = new Fylo({ root })
|
|
12
11
|
beforeAll(async () => {
|
|
13
|
-
await
|
|
12
|
+
await fylo.createCollection(USERS)
|
|
14
13
|
try {
|
|
15
14
|
insertedCount = await fylo.importBulkData(USERS, new URL(usersURL))
|
|
16
15
|
} catch {
|
|
17
16
|
await fylo.rollback()
|
|
18
17
|
}
|
|
19
|
-
for await (const data of
|
|
18
|
+
for await (const data of fylo.findDocs(USERS, { $limit: 1, $onlyIds: true }).collect()) {
|
|
20
19
|
sampleId = data
|
|
21
20
|
}
|
|
22
21
|
})
|
|
23
22
|
afterAll(async () => {
|
|
24
|
-
await
|
|
23
|
+
await fylo.dropCollection(USERS)
|
|
24
|
+
await rm(root, { recursive: true, force: true })
|
|
25
25
|
})
|
|
26
26
|
describe('NO-SQL', async () => {
|
|
27
27
|
test('SELECT ALL — nested documents are returned', async () => {
|
|
28
28
|
let results = {}
|
|
29
|
-
for await (const data of
|
|
29
|
+
for await (const data of fylo.findDocs(USERS).collect()) {
|
|
30
30
|
results = { ...results, ...data }
|
|
31
31
|
}
|
|
32
32
|
expect(Object.keys(results).length).toBe(insertedCount)
|
|
33
33
|
})
|
|
34
34
|
test('GET ONE — top-level fields are reconstructed correctly', async () => {
|
|
35
|
-
const result = await
|
|
35
|
+
const result = await fylo.getDoc(USERS, sampleId).once()
|
|
36
36
|
const user = result[sampleId]
|
|
37
37
|
expect(user).toBeDefined()
|
|
38
38
|
expect(typeof user.name).toBe('string')
|
|
@@ -40,7 +40,7 @@ describe('NO-SQL', async () => {
|
|
|
40
40
|
expect(typeof user.phone).toBe('string')
|
|
41
41
|
})
|
|
42
42
|
test('GET ONE — first-level nested object is reconstructed correctly', async () => {
|
|
43
|
-
const result = await
|
|
43
|
+
const result = await fylo.getDoc(USERS, sampleId).once()
|
|
44
44
|
const user = result[sampleId]
|
|
45
45
|
expect(user.address).toBeDefined()
|
|
46
46
|
expect(typeof user.address.city).toBe('string')
|
|
@@ -48,14 +48,14 @@ describe('NO-SQL', async () => {
|
|
|
48
48
|
expect(typeof user.address.zipcode).toBe('string')
|
|
49
49
|
})
|
|
50
50
|
test('GET ONE — deeply nested object is reconstructed correctly', async () => {
|
|
51
|
-
const result = await
|
|
51
|
+
const result = await fylo.getDoc(USERS, sampleId).once()
|
|
52
52
|
const user = result[sampleId]
|
|
53
53
|
expect(user.address.geo).toBeDefined()
|
|
54
54
|
expect(typeof user.address.geo.lat).toBe('number')
|
|
55
55
|
expect(typeof user.address.geo.lng).toBe('number')
|
|
56
56
|
})
|
|
57
57
|
test('GET ONE — second nested object is reconstructed correctly', async () => {
|
|
58
|
-
const result = await
|
|
58
|
+
const result = await fylo.getDoc(USERS, sampleId).once()
|
|
59
59
|
const user = result[sampleId]
|
|
60
60
|
expect(user.company).toBeDefined()
|
|
61
61
|
expect(typeof user.company.name).toBe('string')
|
|
@@ -63,7 +63,7 @@ describe('NO-SQL', async () => {
|
|
|
63
63
|
expect(typeof user.company.bs).toBe('string')
|
|
64
64
|
})
|
|
65
65
|
test('SELECT — nested values are not corrupted across documents', async () => {
|
|
66
|
-
for await (const data of
|
|
66
|
+
for await (const data of fylo.findDocs(USERS).collect()) {
|
|
67
67
|
const [, user] = Object.entries(data)[0]
|
|
68
68
|
expect(user.address).toBeDefined()
|
|
69
69
|
expect(user.address.geo).toBeDefined()
|
|
@@ -73,7 +73,7 @@ describe('NO-SQL', async () => {
|
|
|
73
73
|
})
|
|
74
74
|
test('$select — returns only requested top-level fields', async () => {
|
|
75
75
|
let results = {}
|
|
76
|
-
for await (const data of
|
|
76
|
+
for await (const data of fylo.findDocs(USERS, { $select: ['name', 'email'] }).collect()) {
|
|
77
77
|
results = { ...results, ...data }
|
|
78
78
|
}
|
|
79
79
|
const users = Object.values(results)
|
|
@@ -81,12 +81,14 @@ describe('NO-SQL', async () => {
|
|
|
81
81
|
expect(onlyNameAndEmail).toBe(true)
|
|
82
82
|
})
|
|
83
83
|
test('$eq on nested string field — query by city', async () => {
|
|
84
|
-
const result = await
|
|
84
|
+
const result = await fylo.getDoc(USERS, sampleId).once()
|
|
85
85
|
const targetCity = result[sampleId].address.city
|
|
86
86
|
let results = {}
|
|
87
|
-
for await (const data of
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
for await (const data of fylo
|
|
88
|
+
.findDocs(USERS, {
|
|
89
|
+
$ops: [{ ['address/city']: { $eq: targetCity } }]
|
|
90
|
+
})
|
|
91
|
+
.collect()) {
|
|
90
92
|
results = { ...results, ...data }
|
|
91
93
|
}
|
|
92
94
|
const matchingUsers = Object.values(results)
|
|
@@ -97,7 +99,7 @@ describe('NO-SQL', async () => {
|
|
|
97
99
|
})
|
|
98
100
|
describe('SQL — dot notation', async () => {
|
|
99
101
|
test('WHERE with dot notation — first-level nested field', async () => {
|
|
100
|
-
const result = await
|
|
102
|
+
const result = await fylo.getDoc(USERS, sampleId).once()
|
|
101
103
|
const targetCity = result[sampleId].address.city
|
|
102
104
|
const results = await fylo.executeSQL(
|
|
103
105
|
`SELECT * FROM ${USERS} WHERE address.city = '${targetCity}'`
|
|
@@ -108,7 +110,7 @@ describe('SQL — dot notation', async () => {
|
|
|
108
110
|
expect(users.length).toBeGreaterThan(0)
|
|
109
111
|
})
|
|
110
112
|
test('WHERE with dot notation — deeply nested field', async () => {
|
|
111
|
-
const result = await
|
|
113
|
+
const result = await fylo.getDoc(USERS, sampleId).once()
|
|
112
114
|
const targetLat = result[sampleId].address.geo.lat
|
|
113
115
|
const results = await fylo.executeSQL(
|
|
114
116
|
`SELECT * FROM ${USERS} WHERE address.geo.lat = '${targetLat}'`
|
|
@@ -119,7 +121,7 @@ describe('SQL — dot notation', async () => {
|
|
|
119
121
|
expect(users.length).toBeGreaterThan(0)
|
|
120
122
|
})
|
|
121
123
|
test('WHERE with dot notation — second nested object', async () => {
|
|
122
|
-
const result = await
|
|
124
|
+
const result = await fylo.getDoc(USERS, sampleId).once()
|
|
123
125
|
const targetCompany = result[sampleId].company.name
|
|
124
126
|
const results = await fylo.executeSQL(
|
|
125
127
|
`SELECT * FROM ${USERS} WHERE company.name = '${targetCompany}'`
|
|
@@ -130,7 +132,7 @@ describe('SQL — dot notation', async () => {
|
|
|
130
132
|
expect(users.length).toBeGreaterThan(0)
|
|
131
133
|
})
|
|
132
134
|
test('SELECT with dot notation in WHERE — partial field selection', async () => {
|
|
133
|
-
const result = await
|
|
135
|
+
const result = await fylo.getDoc(USERS, sampleId).once()
|
|
134
136
|
const targetCity = result[sampleId].address.city
|
|
135
137
|
const results = await fylo.executeSQL(
|
|
136
138
|
`SELECT name, email FROM ${USERS} WHERE address.city = '${targetCity}'`
|