@delma/fylo 1.1.2 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +141 -62
  2. package/eslint.config.js +8 -4
  3. package/package.json +9 -7
  4. package/src/CLI +16 -14
  5. package/src/adapters/cipher.ts +12 -6
  6. package/src/adapters/redis.ts +193 -123
  7. package/src/adapters/s3.ts +6 -12
  8. package/src/core/collection.ts +5 -0
  9. package/src/core/directory.ts +120 -151
  10. package/src/core/extensions.ts +4 -2
  11. package/src/core/format.ts +390 -419
  12. package/src/core/parser.ts +167 -142
  13. package/src/core/query.ts +31 -26
  14. package/src/core/walker.ts +68 -61
  15. package/src/core/write-queue.ts +7 -4
  16. package/src/engines/s3-files.ts +1068 -0
  17. package/src/engines/types.ts +21 -0
  18. package/src/index.ts +754 -378
  19. package/src/migrate-cli.ts +22 -0
  20. package/src/migrate.ts +74 -0
  21. package/src/types/bun-runtime.d.ts +73 -0
  22. package/src/types/fylo.d.ts +115 -27
  23. package/src/types/node-runtime.d.ts +61 -0
  24. package/src/types/query.d.ts +6 -2
  25. package/src/types/vendor-modules.d.ts +8 -7
  26. package/src/worker.ts +7 -1
  27. package/src/workers/write-worker.ts +25 -24
  28. package/tests/collection/truncate.test.js +35 -0
  29. package/tests/{data.ts → data.js} +8 -21
  30. package/tests/{index.ts → index.js} +4 -9
  31. package/tests/integration/aws-s3-files.canary.test.js +22 -0
  32. package/tests/integration/{create.test.ts → create.test.js} +13 -31
  33. package/tests/integration/delete.test.js +95 -0
  34. package/tests/integration/{edge-cases.test.ts → edge-cases.test.js} +50 -124
  35. package/tests/integration/{encryption.test.ts → encryption.test.js} +20 -65
  36. package/tests/integration/{export.test.ts → export.test.js} +8 -23
  37. package/tests/integration/{join-modes.test.ts → join-modes.test.js} +37 -104
  38. package/tests/integration/migration.test.js +38 -0
  39. package/tests/integration/nested.test.js +142 -0
  40. package/tests/integration/operators.test.js +122 -0
  41. package/tests/integration/{queue.test.ts → queue.test.js} +24 -40
  42. package/tests/integration/read.test.js +119 -0
  43. package/tests/integration/rollback.test.js +60 -0
  44. package/tests/integration/s3-files.test.js +192 -0
  45. package/tests/integration/update.test.js +99 -0
  46. package/tests/mocks/{cipher.ts → cipher.js} +11 -26
  47. package/tests/mocks/redis.js +123 -0
  48. package/tests/mocks/{s3.ts → s3.js} +24 -58
  49. package/tests/schemas/album.json +1 -1
  50. package/tests/schemas/comment.json +1 -1
  51. package/tests/schemas/photo.json +1 -1
  52. package/tests/schemas/post.json +1 -1
  53. package/tests/schemas/tip.json +1 -1
  54. package/tests/schemas/todo.json +1 -1
  55. package/tests/schemas/user.d.ts +12 -12
  56. package/tests/schemas/user.json +1 -1
  57. package/tsconfig.json +4 -2
  58. package/tsconfig.typecheck.json +31 -0
  59. package/tests/collection/truncate.test.ts +0 -56
  60. package/tests/integration/delete.test.ts +0 -147
  61. package/tests/integration/nested.test.ts +0 -212
  62. package/tests/integration/operators.test.ts +0 -167
  63. package/tests/integration/read.test.ts +0 -203
  64. package/tests/integration/rollback.test.ts +0 -105
  65. package/tests/integration/update.test.ts +0 -130
  66. package/tests/mocks/redis.ts +0 -169
@@ -3,95 +3,68 @@ import Fylo from '../../src'
3
3
  import S3Mock from '../mocks/s3'
4
4
  import RedisMock from '../mocks/redis'
5
5
  import { CipherMock } from '../mocks/cipher'
6
-
7
6
  const COLLECTION = 'encrypted-test'
8
-
9
7
  const fylo = new Fylo()
10
-
11
8
  mock.module('../../src/adapters/s3', () => ({ S3: S3Mock }))
12
9
  mock.module('../../src/adapters/redis', () => ({ Redis: RedisMock }))
13
10
  mock.module('../../src/adapters/cipher', () => ({ Cipher: CipherMock }))
14
-
15
11
  beforeAll(async () => {
16
12
  await Fylo.createCollection(COLLECTION)
17
13
  await CipherMock.configure('test-secret-key')
18
14
  CipherMock.registerFields(COLLECTION, ['email', 'ssn', 'address'])
19
15
  })
20
-
21
16
  afterAll(async () => {
22
17
  CipherMock.reset()
23
18
  await Fylo.dropCollection(COLLECTION)
24
19
  })
25
-
26
- describe("Encryption", () => {
27
-
28
- let docId: _ttid
29
-
30
- test("PUT encrypted document", async () => {
31
-
20
+ describe('Encryption', () => {
21
+ let docId
22
+ test('PUT encrypted document', async () => {
32
23
  docId = await fylo.putData(COLLECTION, {
33
24
  name: 'Alice',
34
25
  email: 'alice@example.com',
35
26
  ssn: '123-45-6789',
36
27
  age: 30
37
28
  })
38
-
39
29
  expect(docId).toBeDefined()
40
30
  })
41
-
42
- test("GET decrypts fields transparently", async () => {
43
-
31
+ test('GET decrypts fields transparently', async () => {
44
32
  const result = await Fylo.getDoc(COLLECTION, docId).once()
45
33
  const doc = Object.values(result)[0]
46
-
47
34
  expect(doc.name).toBe('Alice')
48
35
  expect(doc.email).toBe('alice@example.com')
49
36
  expect(doc.ssn).toBe('123-45-6789')
50
37
  expect(doc.age).toBe(30)
51
38
  })
52
-
53
- test("encrypted values stored in S3 keys are not plaintext", () => {
54
-
39
+ test('encrypted values stored in S3 keys are not plaintext', () => {
55
40
  const bucket = S3Mock.getBucketFormat(COLLECTION)
56
-
57
- // Verify the bucket was created and doc round-trip works
58
- // (plaintext values should not appear as raw key segments)
59
41
  expect(bucket).toBeDefined()
60
42
  })
61
-
62
- test("$eq query works on encrypted field", async () => {
63
-
43
+ test('$eq query works on encrypted field', async () => {
64
44
  let found = false
65
-
66
45
  for await (const data of Fylo.findDocs(COLLECTION, {
67
46
  $ops: [{ email: { $eq: 'alice@example.com' } }]
68
47
  }).collect()) {
69
-
70
- if(typeof data === 'object') {
48
+ if (typeof data === 'object') {
71
49
  const doc = Object.values(data)[0]
72
50
  expect(doc.email).toBe('alice@example.com')
73
51
  found = true
74
52
  }
75
53
  }
76
-
77
54
  expect(found).toBe(true)
78
55
  })
79
-
80
- test("$ne throws on encrypted field", async () => {
81
-
56
+ test('$ne throws on encrypted field', async () => {
82
57
  try {
83
58
  const iter = Fylo.findDocs(COLLECTION, {
84
59
  $ops: [{ email: { $ne: 'bob@example.com' } }]
85
60
  }).collect()
86
61
  await iter.next()
87
- expect(true).toBe(false) // Should not reach here
62
+ expect(true).toBe(false)
88
63
  } catch (e) {
89
- expect((e as Error).message).toContain('not supported on encrypted field')
64
+ expect(e.message).toContain('not supported on encrypted field')
90
65
  }
91
66
  })
92
-
93
- test("$gt throws on encrypted field", async () => {
94
-
67
+ test('$gt throws on encrypted field', async () => {
95
68
  try {
96
69
  const iter = Fylo.findDocs(COLLECTION, {
97
70
  $ops: [{ ssn: { $gt: 0 } }]
@@ -99,12 +72,10 @@ describe("Encryption", () => {
99
72
  await iter.next()
100
73
  expect(true).toBe(false)
101
74
  } catch (e) {
102
- expect((e as Error).message).toContain('not supported on encrypted field')
75
+ expect(e.message).toContain('not supported on encrypted field')
103
76
  }
104
77
  })
105
-
106
- test("$like throws on encrypted field", async () => {
107
-
78
+ test('$like throws on encrypted field', async () => {
108
79
  try {
109
80
  const iter = Fylo.findDocs(COLLECTION, {
110
81
  $ops: [{ email: { $like: '%@example.com' } }]
@@ -112,30 +83,23 @@ describe("Encryption", () => {
112
83
  await iter.next()
113
84
  expect(true).toBe(false)
114
85
  } catch (e) {
115
- expect((e as Error).message).toContain('not supported on encrypted field')
86
+ expect(e.message).toContain('not supported on encrypted field')
116
87
  }
117
88
  })
118
-
119
- test("non-encrypted fields remain queryable with all operators", async () => {
120
-
89
+ test('non-encrypted fields remain queryable with all operators', async () => {
121
90
  let found = false
122
-
123
91
  for await (const data of Fylo.findDocs(COLLECTION, {
124
92
  $ops: [{ name: { $eq: 'Alice' } }]
125
93
  }).collect()) {
126
-
127
- if(typeof data === 'object') {
94
+ if (typeof data === 'object') {
128
95
  const doc = Object.values(data)[0]
129
96
  expect(doc.name).toBe('Alice')
130
97
  found = true
131
98
  }
132
99
  }
133
-
134
100
  expect(found).toBe(true)
135
101
  })
136
-
137
- test("nested encrypted field (address.city)", async () => {
138
-
102
+ test('nested encrypted field (address.city)', async () => {
139
103
  const id = await fylo.putData(COLLECTION, {
140
104
  name: 'Bob',
141
105
  email: 'bob@example.com',
@@ -143,34 +107,25 @@ describe("Encryption", () => {
143
107
  age: 25,
144
108
  address: { city: 'Toronto', zip: 'M5V 2T6' }
145
109
  })
146
-
147
110
  const result = await Fylo.getDoc(COLLECTION, id).once()
148
111
  const doc = Object.values(result)[0]
149
-
150
112
  expect(doc.address.city).toBe('Toronto')
151
113
  expect(doc.address.zip).toBe('M5V 2T6')
152
114
  })
153
-
154
- test("UPDATE preserves encryption", async () => {
155
-
115
+ test('UPDATE preserves encryption', async () => {
156
116
  await fylo.patchDoc(COLLECTION, {
157
117
  [docId]: { email: 'alice-new@example.com' }
158
- } as Record<_ttid, Record<string, string>>)
159
-
160
- // Find the updated doc (patchDoc generates new TTID)
118
+ })
161
119
  let found = false
162
-
163
120
  for await (const data of Fylo.findDocs(COLLECTION, {
164
121
  $ops: [{ email: { $eq: 'alice-new@example.com' } }]
165
122
  }).collect()) {
166
-
167
- if(typeof data === 'object') {
123
+ if (typeof data === 'object') {
168
124
  const doc = Object.values(data)[0]
169
125
  expect(doc.email).toBe('alice-new@example.com')
170
126
  found = true
171
127
  }
172
128
  }
173
-
174
129
  expect(found).toBe(true)
175
130
  })
176
131
  })
@@ -3,56 +3,41 @@ import Fylo from '../../src'
3
3
  import { postsURL } from '../data'
4
4
  import S3Mock from '../mocks/s3'
5
5
  import RedisMock from '../mocks/redis'
6
-
7
6
  const POSTS = 'exp-post'
8
7
  const IMPORT_LIMIT = 20
9
-
10
8
  let importedCount = 0
11
-
12
9
  const fylo = new Fylo()
13
-
14
10
  mock.module('../../src/adapters/s3', () => ({ S3: S3Mock }))
15
11
  mock.module('../../src/adapters/redis', () => ({ Redis: RedisMock }))
16
-
17
12
  beforeAll(async () => {
18
13
  await Fylo.createCollection(POSTS)
19
14
  try {
20
- importedCount = await fylo.importBulkData<_post>(POSTS, new URL(postsURL), IMPORT_LIMIT)
15
+ importedCount = await fylo.importBulkData(POSTS, new URL(postsURL), IMPORT_LIMIT)
21
16
  } catch {
22
17
  await fylo.rollback()
23
18
  }
24
19
  })
25
-
26
20
  afterAll(async () => {
27
21
  await Fylo.dropCollection(POSTS)
28
22
  })
29
-
30
- describe("NO-SQL", () => {
31
-
32
- test("EXPORT count matches import", async () => {
33
-
23
+ describe('NO-SQL', () => {
24
+ test('EXPORT count matches import', async () => {
34
25
  let exported = 0
35
-
36
- for await (const _doc of Fylo.exportBulkData<_post>(POSTS)) {
26
+ for await (const _doc of Fylo.exportBulkData(POSTS)) {
37
27
  exported++
38
28
  }
39
-
40
29
  expect(exported).toBe(importedCount)
41
30
  })
42
-
43
- test("EXPORT document shape", async () => {
44
-
45
- for await (const doc of Fylo.exportBulkData<_post>(POSTS)) {
31
+ test('EXPORT document shape', async () => {
32
+ for await (const doc of Fylo.exportBulkData(POSTS)) {
46
33
  expect(doc).toHaveProperty('title')
47
34
  expect(doc).toHaveProperty('userId')
48
35
  expect(doc).toHaveProperty('body')
49
36
  break
50
37
  }
51
38
  })
52
-
53
- test("EXPORT all documents are valid posts", async () => {
54
-
55
- for await (const doc of Fylo.exportBulkData<_post>(POSTS)) {
39
+ test('EXPORT all documents are valid posts', async () => {
40
+ for await (const doc of Fylo.exportBulkData(POSTS)) {
56
41
  expect(typeof doc.title).toBe('string')
57
42
  expect(typeof doc.userId).toBe('number')
58
43
  expect(doc.userId).toBeGreaterThan(0)
@@ -3,218 +3,151 @@ import Fylo from '../../src'
3
3
  import { albumURL, postsURL } from '../data'
4
4
  import S3Mock from '../mocks/s3'
5
5
  import RedisMock from '../mocks/redis'
6
-
7
- /**
8
- * Albums (userId 1–10) and posts (userId 1–10) share a userId field,
9
- * making them a natural fit for join tests across all four join modes.
10
- *
11
- * Join semantics in Fylo:
12
- * inner → only the join field values
13
- * left → full left-collection document
14
- * right → full right-collection document
15
- * outer → merged left + right documents
16
- */
17
-
18
6
  const ALBUMS = 'jm-album'
19
- const POSTS = 'jm-post'
20
-
7
+ const POSTS = 'jm-post'
21
8
  const fylo = new Fylo()
22
-
23
9
  mock.module('../../src/adapters/s3', () => ({ S3: S3Mock }))
24
10
  mock.module('../../src/adapters/redis', () => ({ Redis: RedisMock }))
25
-
26
11
  beforeAll(async () => {
27
12
  await Promise.all([Fylo.createCollection(ALBUMS), Fylo.createCollection(POSTS)])
28
13
  try {
29
14
  await Promise.all([
30
- fylo.importBulkData<_album>(ALBUMS, new URL(albumURL), 100),
31
- fylo.importBulkData<_post>(POSTS, new URL(postsURL), 100)
15
+ fylo.importBulkData(ALBUMS, new URL(albumURL), 100),
16
+ fylo.importBulkData(POSTS, new URL(postsURL), 100)
32
17
  ])
33
18
  } catch {
34
19
  await fylo.rollback()
35
20
  }
36
21
  })
37
-
38
22
  afterAll(async () => {
39
23
  await Promise.all([Fylo.dropCollection(ALBUMS), Fylo.dropCollection(POSTS)])
40
24
  })
41
-
42
- describe("NO-SQL", async () => {
43
-
44
- test("INNER JOIN — returns only join field values", async () => {
45
-
46
- const results = await Fylo.joinDocs<_album, _post>({
25
+ describe('NO-SQL', async () => {
26
+ test('INNER JOIN — returns only join field values', async () => {
27
+ const results = await Fylo.joinDocs({
47
28
  $leftCollection: ALBUMS,
48
29
  $rightCollection: POSTS,
49
30
  $mode: 'inner',
50
31
  $on: { userId: { $eq: 'userId' } }
51
- }) as Record<`${_ttid}, ${_ttid}`, { userId: number }>
52
-
32
+ })
53
33
  const pairs = Object.values(results)
54
-
55
34
  expect(pairs.length).toBeGreaterThan(0)
56
-
57
- // inner mode returns only the join fields, not full documents
58
35
  for (const pair of pairs) {
59
36
  expect(pair).toHaveProperty('userId')
60
37
  expect(typeof pair.userId).toBe('number')
61
38
  }
62
39
  })
63
-
64
- test("LEFT JOIN returns full left-collection document", async () => {
65
-
66
- const results = await Fylo.joinDocs<_album, _post>({
40
+ test('LEFT JOIN — returns full left-collection document', async () => {
41
+ const results = await Fylo.joinDocs({
67
42
  $leftCollection: ALBUMS,
68
43
  $rightCollection: POSTS,
69
44
  $mode: 'left',
70
45
  $on: { userId: { $eq: 'userId' } }
71
- }) as Record<`${_ttid}, ${_ttid}`, _album>
72
-
46
+ })
73
47
  const docs = Object.values(results)
74
-
75
48
  expect(docs.length).toBeGreaterThan(0)
76
-
77
- // left mode returns the full album (left collection) document
78
49
  for (const doc of docs) {
79
50
  expect(doc).toHaveProperty('title')
80
51
  expect(doc).toHaveProperty('userId')
81
52
  }
82
53
  })
83
-
84
- test("RIGHT JOIN returns full right-collection document", async () => {
85
-
86
- const results = await Fylo.joinDocs<_album, _post>({
54
+ test('RIGHT JOIN — returns full right-collection document', async () => {
55
+ const results = await Fylo.joinDocs({
87
56
  $leftCollection: ALBUMS,
88
57
  $rightCollection: POSTS,
89
58
  $mode: 'right',
90
59
  $on: { userId: { $eq: 'userId' } }
91
- }) as Record<`${_ttid}, ${_ttid}`, _post>
92
-
60
+ })
93
61
  const docs = Object.values(results)
94
-
95
62
  expect(docs.length).toBeGreaterThan(0)
96
-
97
- // right mode returns the full post (right collection) document
98
63
  for (const doc of docs) {
99
64
  expect(doc).toHaveProperty('title')
100
65
  expect(doc).toHaveProperty('body')
101
66
  expect(doc).toHaveProperty('userId')
102
67
  }
103
68
  })
104
-
105
- test("OUTER JOIN returns merged left + right document", async () => {
106
-
107
- const results = await Fylo.joinDocs<_album, _post>({
69
+ test('OUTER JOIN — returns merged left + right document', async () => {
70
+ const results = await Fylo.joinDocs({
108
71
  $leftCollection: ALBUMS,
109
72
  $rightCollection: POSTS,
110
73
  $mode: 'outer',
111
74
  $on: { userId: { $eq: 'userId' } }
112
- }) as Record<`${_ttid}, ${_ttid}`, _album & _post>
113
-
75
+ })
114
76
  const docs = Object.values(results)
115
-
116
77
  expect(docs.length).toBeGreaterThan(0)
117
-
118
- // outer mode merges both documents; both should have fields present
119
78
  for (const doc of docs) {
120
79
  expect(doc).toHaveProperty('userId')
121
80
  }
122
81
  })
123
-
124
- test("JOIN with $limit respects the result cap", async () => {
125
-
126
- const results = await Fylo.joinDocs<_album, _post>({
82
+ test('JOIN with $limit — respects the result cap', async () => {
83
+ const results = await Fylo.joinDocs({
127
84
  $leftCollection: ALBUMS,
128
85
  $rightCollection: POSTS,
129
86
  $mode: 'inner',
130
87
  $on: { userId: { $eq: 'userId' } },
131
88
  $limit: 5
132
89
  })
133
-
134
90
  expect(Object.keys(results).length).toBe(5)
135
91
  })
136
-
137
- test("JOIN with $select only requested fields are returned", async () => {
138
-
139
- const results = await Fylo.joinDocs<_album, _post>({
92
+ test('JOIN with $select — only requested fields are returned', async () => {
93
+ const results = await Fylo.joinDocs({
140
94
  $leftCollection: ALBUMS,
141
95
  $rightCollection: POSTS,
142
96
  $mode: 'left',
143
97
  $on: { userId: { $eq: 'userId' } },
144
98
  $select: ['title'],
145
99
  $limit: 10
146
- }) as Record<`${_ttid}, ${_ttid}`, Partial<_album>>
147
-
100
+ })
148
101
  const docs = Object.values(results)
149
-
150
102
  expect(docs.length).toBeGreaterThan(0)
151
-
152
103
  for (const doc of docs) {
153
104
  expect(doc).toHaveProperty('title')
154
105
  expect(doc).not.toHaveProperty('userId')
155
106
  }
156
107
  })
157
-
158
- test("JOIN with $groupby — groups results by field value", async () => {
159
-
160
- const results = await Fylo.joinDocs<_album, _post>({
108
+ test('JOIN with $groupby — groups results by field value', async () => {
109
+ const results = await Fylo.joinDocs({
161
110
  $leftCollection: ALBUMS,
162
111
  $rightCollection: POSTS,
163
112
  $mode: 'inner',
164
113
  $on: { userId: { $eq: 'userId' } },
165
114
  $groupby: 'userId'
166
- }) as Record<string, Record<string, unknown>>
167
-
115
+ })
168
116
  expect(Object.keys(results).length).toBeGreaterThan(0)
169
117
  })
170
-
171
- test("JOIN with $onlyIds returns IDs only", async () => {
172
-
173
- const results = await Fylo.joinDocs<_album, _post>({
118
+ test('JOIN with $onlyIds — returns IDs only', async () => {
119
+ const results = await Fylo.joinDocs({
174
120
  $leftCollection: ALBUMS,
175
121
  $rightCollection: POSTS,
176
122
  $mode: 'inner',
177
123
  $on: { userId: { $eq: 'userId' } },
178
124
  $onlyIds: true,
179
125
  $limit: 10
180
- }) as _ttid[]
181
-
126
+ })
182
127
  expect(Array.isArray(results)).toBe(true)
183
128
  expect(results.length).toBeGreaterThan(0)
184
129
  })
185
130
  })
186
-
187
- describe("SQL", async () => {
188
-
189
- test("INNER JOIN", async () => {
190
-
191
- const results = await fylo.executeSQL<_album>(
131
+ describe('SQL', async () => {
132
+ test('INNER JOIN', async () => {
133
+ const results = await fylo.executeSQL(
192
134
  `SELECT * FROM ${ALBUMS} INNER JOIN ${POSTS} ON userId = userId`
193
- ) as Record<`${_ttid}, ${_ttid}`, _album | _post>
194
-
135
+ )
195
136
  expect(Object.keys(results).length).toBeGreaterThan(0)
196
137
  })
197
-
198
- test("LEFT JOIN", async () => {
199
-
200
- const results = await fylo.executeSQL<_album>(
138
+ test('LEFT JOIN', async () => {
139
+ const results = await fylo.executeSQL(
201
140
  `SELECT * FROM ${ALBUMS} LEFT JOIN ${POSTS} ON userId = userId`
202
- ) as Record<`${_ttid}, ${_ttid}`, _album>
203
-
141
+ )
204
142
  const docs = Object.values(results)
205
-
206
143
  expect(docs.length).toBeGreaterThan(0)
207
144
  expect(docs[0]).toHaveProperty('title')
208
145
  })
209
-
210
- test("RIGHT JOIN", async () => {
211
-
212
- const results = await fylo.executeSQL<_post>(
146
+ test('RIGHT JOIN', async () => {
147
+ const results = await fylo.executeSQL(
213
148
  `SELECT * FROM ${ALBUMS} RIGHT JOIN ${POSTS} ON userId = userId`
214
- ) as Record<`${_ttid}, ${_ttid}`, _post>
215
-
149
+ )
216
150
  const docs = Object.values(results)
217
-
218
151
  expect(docs.length).toBeGreaterThan(0)
219
152
  expect(docs[0]).toHaveProperty('body')
220
153
  })
@@ -0,0 +1,38 @@
1
+ import { afterAll, beforeAll, describe, expect, mock, test } from 'bun:test'
2
+ import { mkdtemp, rm } from 'node:fs/promises'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import S3Mock from '../mocks/s3'
6
+ import RedisMock from '../mocks/redis'
7
+ mock.module('../../src/adapters/s3', () => ({ S3: S3Mock }))
8
+ mock.module('../../src/adapters/redis', () => ({ Redis: RedisMock }))
9
+ const { default: Fylo, migrateLegacyS3ToS3Files } = await import('../../src')
10
+ const root = await mkdtemp(path.join(os.tmpdir(), 'fylo-migrate-'))
11
+ const legacyFylo = new Fylo({ engine: 'legacy-s3' })
12
+ const s3FilesFylo = new Fylo({ engine: 's3-files', s3FilesRoot: root })
13
+ const COLLECTION = 'migration-posts'
14
+ describe('legacy-s3 to s3-files migration', () => {
15
+ beforeAll(async () => {
16
+ await Fylo.createCollection(COLLECTION)
17
+ await legacyFylo.putData(COLLECTION, { id: 1, title: 'Alpha' })
18
+ await legacyFylo.putData(COLLECTION, { id: 2, title: 'Beta' })
19
+ })
20
+ afterAll(async () => {
21
+ await rm(root, { recursive: true, force: true })
22
+ })
23
+ test('migrates legacy data and verifies parity', async () => {
24
+ const summary = await migrateLegacyS3ToS3Files({
25
+ collections: [COLLECTION],
26
+ s3FilesRoot: root,
27
+ verify: true
28
+ })
29
+ expect(summary[COLLECTION].migrated).toBe(2)
30
+ expect(summary[COLLECTION].verified).toBe(true)
31
+ const migrated = await s3FilesFylo.executeSQL(`SELECT * FROM ${COLLECTION}`)
32
+ expect(
33
+ Object.values(migrated)
34
+ .map((item) => item.title)
35
+ .sort()
36
+ ).toEqual(['Alpha', 'Beta'])
37
+ })
38
+ })