@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.
Files changed (43) hide show
  1. package/README.md +185 -267
  2. package/package.json +2 -5
  3. package/src/core/directory.ts +22 -354
  4. package/src/engines/s3-files/documents.ts +65 -0
  5. package/src/engines/s3-files/filesystem.ts +172 -0
  6. package/src/engines/s3-files/query.ts +291 -0
  7. package/src/engines/s3-files/types.ts +42 -0
  8. package/src/engines/s3-files.ts +391 -510
  9. package/src/engines/types.ts +1 -1
  10. package/src/index.ts +142 -1237
  11. package/src/sync.ts +58 -0
  12. package/src/types/fylo.d.ts +66 -161
  13. package/src/types/node-runtime.d.ts +1 -0
  14. package/tests/collection/truncate.test.js +11 -10
  15. package/tests/helpers/root.js +7 -0
  16. package/tests/integration/create.test.js +9 -9
  17. package/tests/integration/delete.test.js +16 -14
  18. package/tests/integration/edge-cases.test.js +29 -25
  19. package/tests/integration/encryption.test.js +47 -30
  20. package/tests/integration/export.test.js +11 -11
  21. package/tests/integration/join-modes.test.js +16 -16
  22. package/tests/integration/nested.test.js +26 -24
  23. package/tests/integration/operators.test.js +43 -29
  24. package/tests/integration/read.test.js +25 -21
  25. package/tests/integration/rollback.test.js +21 -51
  26. package/tests/integration/s3-files.performance.test.js +75 -0
  27. package/tests/integration/s3-files.test.js +115 -18
  28. package/tests/integration/sync.test.js +154 -0
  29. package/tests/integration/update.test.js +24 -18
  30. package/src/adapters/redis.ts +0 -487
  31. package/src/adapters/s3.ts +0 -61
  32. package/src/core/walker.ts +0 -174
  33. package/src/core/write-queue.ts +0 -59
  34. package/src/migrate-cli.ts +0 -22
  35. package/src/migrate.ts +0 -74
  36. package/src/types/write-queue.ts +0 -42
  37. package/src/worker.ts +0 -18
  38. package/src/workers/write-worker.ts +0 -120
  39. package/tests/index.js +0 -14
  40. package/tests/integration/migration.test.js +0 -38
  41. package/tests/integration/queue.test.js +0 -83
  42. package/tests/mocks/redis.js +0 -123
  43. package/tests/mocks/s3.js +0 -80
@@ -1,22 +1,22 @@
1
- import { test, expect, describe, beforeAll, afterAll, mock } from 'bun:test'
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 S3Mock from '../mocks/s3'
5
- import RedisMock from '../mocks/redis'
5
+ import { createTestRoot } from '../helpers/root'
6
6
  const COLLECTION = 'ec-test'
7
- const fylo = new Fylo()
8
- mock.module('../../src/adapters/s3', () => ({ S3: S3Mock }))
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 Fylo.createCollection(COLLECTION)
10
+ await fylo.createCollection(COLLECTION)
12
11
  })
13
12
  afterAll(async () => {
14
- await Fylo.dropCollection(COLLECTION)
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 Fylo.getDoc(COLLECTION, fakeId).once()
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 Fylo.getDoc(COLLECTION, _id).once()
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 Fylo.getDoc(COLLECTION, _id).once()
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 Fylo.findDocs(COLLECTION, {
63
- $ops: [{ userId: { $eq: 10 } }, { userId: { $eq: 20 } }]
64
- }).collect()) {
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 Fylo.findDocs(COLLECTION, {
82
- $ops: [{ id: { $eq: 300 } }],
83
- $rename: { title: 'name' }
84
- }).collect()) {
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 Fylo.getDoc(COLLECTION, _id2).once()
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 S3Mock from '../mocks/s3'
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 fylo = new Fylo()
8
- mock.module('../../src/adapters/s3', () => ({ S3: S3Mock }))
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 Fylo.createCollection(COLLECTION)
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 Fylo.dropCollection(COLLECTION)
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 Fylo.getDoc(COLLECTION, docId).once()
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 S3 keys are not plaintext', () => {
40
- const bucket = S3Mock.getBucketFormat(COLLECTION)
41
- expect(bucket).toBeDefined()
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 Fylo.findDocs(COLLECTION, {
46
- $ops: [{ email: { $eq: 'alice@example.com' } }]
47
- }).collect()) {
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 = Fylo.findDocs(COLLECTION, {
59
- $ops: [{ email: { $ne: 'bob@example.com' } }]
60
- }).collect()
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 = Fylo.findDocs(COLLECTION, {
70
- $ops: [{ ssn: { $gt: 0 } }]
71
- }).collect()
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 = Fylo.findDocs(COLLECTION, {
81
- $ops: [{ email: { $like: '%@example.com' } }]
82
- }).collect()
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 Fylo.findDocs(COLLECTION, {
92
- $ops: [{ name: { $eq: 'Alice' } }]
93
- }).collect()) {
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 Fylo.getDoc(COLLECTION, id).once()
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 Fylo.findDocs(COLLECTION, {
121
- $ops: [{ email: { $eq: 'alice-new@example.com' } }]
122
- }).collect()) {
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, mock } from 'bun:test'
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 S3Mock from '../mocks/s3'
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 fylo = new Fylo()
10
- mock.module('../../src/adapters/s3', () => ({ S3: S3Mock }))
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 Fylo.createCollection(POSTS)
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 Fylo.dropCollection(POSTS)
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 Fylo.exportBulkData(POSTS)) {
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 Fylo.exportBulkData(POSTS)) {
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 Fylo.exportBulkData(POSTS)) {
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, mock } from 'bun:test'
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 S3Mock from '../mocks/s3'
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 fylo = new Fylo()
9
- mock.module('../../src/adapters/s3', () => ({ S3: S3Mock }))
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([Fylo.createCollection(ALBUMS), Fylo.createCollection(POSTS)])
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([Fylo.dropCollection(ALBUMS), Fylo.dropCollection(POSTS)])
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 Fylo.joinDocs({
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 Fylo.joinDocs({
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 Fylo.joinDocs({
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 Fylo.joinDocs({
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 Fylo.joinDocs({
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 Fylo.joinDocs({
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 Fylo.joinDocs({
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 Fylo.joinDocs({
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, mock } from 'bun:test'
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 S3Mock from '../mocks/s3'
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 fylo = new Fylo()
10
- mock.module('../../src/adapters/s3', () => ({ S3: S3Mock }))
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 Fylo.createCollection(USERS)
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 Fylo.findDocs(USERS, { $limit: 1, $onlyIds: true }).collect()) {
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 Fylo.dropCollection(USERS)
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 Fylo.findDocs(USERS).collect()) {
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 Fylo.getDoc(USERS, sampleId).once()
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 Fylo.getDoc(USERS, sampleId).once()
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 Fylo.getDoc(USERS, sampleId).once()
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 Fylo.getDoc(USERS, sampleId).once()
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 Fylo.findDocs(USERS).collect()) {
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 Fylo.findDocs(USERS, { $select: ['name', 'email'] }).collect()) {
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 Fylo.getDoc(USERS, sampleId).once()
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 Fylo.findDocs(USERS, {
88
- $ops: [{ ['address/city']: { $eq: targetCity } }]
89
- }).collect()) {
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 Fylo.getDoc(USERS, sampleId).once()
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 Fylo.getDoc(USERS, sampleId).once()
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 Fylo.getDoc(USERS, sampleId).once()
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 Fylo.getDoc(USERS, sampleId).once()
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}'`