@delma/fylo 2.0.1 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.md +206 -261
  2. package/dist/adapters/cipher.js +155 -0
  3. package/dist/adapters/cipher.js.map +1 -0
  4. package/dist/core/collection.js +6 -0
  5. package/dist/core/collection.js.map +1 -0
  6. package/dist/core/directory.js +48 -0
  7. package/dist/core/directory.js.map +1 -0
  8. package/dist/core/doc-id.js +15 -0
  9. package/dist/core/doc-id.js.map +1 -0
  10. package/dist/core/extensions.js +16 -0
  11. package/dist/core/extensions.js.map +1 -0
  12. package/dist/core/format.js +355 -0
  13. package/dist/core/format.js.map +1 -0
  14. package/dist/core/parser.js +764 -0
  15. package/dist/core/parser.js.map +1 -0
  16. package/dist/core/query.js +47 -0
  17. package/dist/core/query.js.map +1 -0
  18. package/dist/engines/s3-files/documents.js +62 -0
  19. package/dist/engines/s3-files/documents.js.map +1 -0
  20. package/dist/engines/s3-files/filesystem.js +165 -0
  21. package/dist/engines/s3-files/filesystem.js.map +1 -0
  22. package/dist/engines/s3-files/query.js +235 -0
  23. package/dist/engines/s3-files/query.js.map +1 -0
  24. package/dist/engines/s3-files/types.js +2 -0
  25. package/dist/engines/s3-files/types.js.map +1 -0
  26. package/dist/engines/s3-files.js +629 -0
  27. package/dist/engines/s3-files.js.map +1 -0
  28. package/dist/engines/types.js +2 -0
  29. package/dist/engines/types.js.map +1 -0
  30. package/dist/index.js +562 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/sync.js +18 -0
  33. package/dist/sync.js.map +1 -0
  34. package/dist/types/fylo.d.ts +179 -0
  35. package/{src → dist}/types/node-runtime.d.ts +1 -0
  36. package/package.json +3 -6
  37. package/.env.example +0 -16
  38. package/.github/copilot-instructions.md +0 -3
  39. package/.github/prompts/release.prompt.md +0 -10
  40. package/.github/workflows/ci.yml +0 -37
  41. package/.github/workflows/publish.yml +0 -91
  42. package/.prettierrc +0 -7
  43. package/AGENTS.md +0 -3
  44. package/CLAUDE.md +0 -3
  45. package/eslint.config.js +0 -32
  46. package/src/CLI +0 -39
  47. package/src/adapters/cipher.ts +0 -180
  48. package/src/adapters/redis.ts +0 -487
  49. package/src/adapters/s3.ts +0 -61
  50. package/src/core/collection.ts +0 -5
  51. package/src/core/directory.ts +0 -387
  52. package/src/core/extensions.ts +0 -21
  53. package/src/core/format.ts +0 -457
  54. package/src/core/parser.ts +0 -901
  55. package/src/core/query.ts +0 -53
  56. package/src/core/walker.ts +0 -174
  57. package/src/core/write-queue.ts +0 -59
  58. package/src/engines/s3-files.ts +0 -1068
  59. package/src/engines/types.ts +0 -21
  60. package/src/index.ts +0 -1727
  61. package/src/migrate-cli.ts +0 -22
  62. package/src/migrate.ts +0 -74
  63. package/src/types/fylo.d.ts +0 -261
  64. package/src/types/write-queue.ts +0 -42
  65. package/src/worker.ts +0 -18
  66. package/src/workers/write-worker.ts +0 -120
  67. package/tests/collection/truncate.test.js +0 -35
  68. package/tests/data.js +0 -97
  69. package/tests/index.js +0 -14
  70. package/tests/integration/aws-s3-files.canary.test.js +0 -22
  71. package/tests/integration/create.test.js +0 -39
  72. package/tests/integration/delete.test.js +0 -95
  73. package/tests/integration/edge-cases.test.js +0 -158
  74. package/tests/integration/encryption.test.js +0 -131
  75. package/tests/integration/export.test.js +0 -46
  76. package/tests/integration/join-modes.test.js +0 -154
  77. package/tests/integration/migration.test.js +0 -38
  78. package/tests/integration/nested.test.js +0 -142
  79. package/tests/integration/operators.test.js +0 -122
  80. package/tests/integration/queue.test.js +0 -83
  81. package/tests/integration/read.test.js +0 -119
  82. package/tests/integration/rollback.test.js +0 -60
  83. package/tests/integration/s3-files.test.js +0 -192
  84. package/tests/integration/update.test.js +0 -99
  85. package/tests/mocks/cipher.js +0 -40
  86. package/tests/mocks/redis.js +0 -123
  87. package/tests/mocks/s3.js +0 -80
  88. package/tests/schemas/album.d.ts +0 -5
  89. package/tests/schemas/album.json +0 -5
  90. package/tests/schemas/comment.d.ts +0 -7
  91. package/tests/schemas/comment.json +0 -7
  92. package/tests/schemas/photo.d.ts +0 -7
  93. package/tests/schemas/photo.json +0 -7
  94. package/tests/schemas/post.d.ts +0 -6
  95. package/tests/schemas/post.json +0 -6
  96. package/tests/schemas/tip.d.ts +0 -7
  97. package/tests/schemas/tip.json +0 -7
  98. package/tests/schemas/todo.d.ts +0 -6
  99. package/tests/schemas/todo.json +0 -6
  100. package/tests/schemas/user.d.ts +0 -23
  101. package/tests/schemas/user.json +0 -23
  102. package/tsconfig.json +0 -21
  103. package/tsconfig.typecheck.json +0 -31
  104. /package/{src → dist}/types/bun-runtime.d.ts +0 -0
  105. /package/{src → dist}/types/index.d.ts +0 -0
  106. /package/{src → dist}/types/query.d.ts +0 -0
  107. /package/{src → dist}/types/vendor-modules.d.ts +0 -0
@@ -1,22 +0,0 @@
1
- import { describe, expect, test } from 'bun:test'
2
- import Fylo from '../../src'
3
- const runCanary = process.env.FYLO_RUN_S3FILES_CANARY === 'true'
4
- const canaryTest = runCanary ? test : test.skip
5
- describe('aws s3-files canary', () => {
6
- canaryTest('mounted S3 Files root handles a real CRUD cycle', async () => {
7
- const collection = `canary_${Date.now()}`
8
- const fylo = new Fylo({
9
- engine: 's3-files',
10
- s3FilesRoot: process.env.FYLO_S3FILES_ROOT
11
- })
12
- await fylo.createCollection(collection)
13
- const id = await fylo.putData(collection, {
14
- title: 'canary',
15
- tags: ['aws', 's3-files']
16
- })
17
- const doc = await fylo.getDoc(collection, id).once()
18
- expect(doc[id].title).toBe('canary')
19
- await fylo.delDoc(collection, id)
20
- await fylo.dropCollection(collection)
21
- })
22
- })
@@ -1,39 +0,0 @@
1
- import { test, expect, describe, beforeAll, afterAll, mock } from 'bun:test'
2
- import Fylo from '../../src'
3
- import { albumURL, postsURL } from '../data'
4
- import S3Mock from '../mocks/s3'
5
- import RedisMock from '../mocks/redis'
6
- const POSTS = `post`
7
- const ALBUMS = `album`
8
- let postsCount = 0
9
- let albumsCount = 0
10
- const fylo = new Fylo()
11
- mock.module('../../src/adapters/s3', () => ({ S3: S3Mock }))
12
- mock.module('../../src/adapters/redis', () => ({ Redis: RedisMock }))
13
- beforeAll(async () => {
14
- await Promise.all([Fylo.createCollection(POSTS), fylo.executeSQL(`CREATE TABLE ${ALBUMS}`)])
15
- try {
16
- albumsCount = await fylo.importBulkData(ALBUMS, new URL(albumURL), 100)
17
- postsCount = await fylo.importBulkData(POSTS, new URL(postsURL), 100)
18
- } catch {
19
- await fylo.rollback()
20
- }
21
- })
22
- afterAll(async () => {
23
- await Promise.all([Fylo.dropCollection(POSTS), fylo.executeSQL(`DROP TABLE ${ALBUMS}`)])
24
- })
25
- describe('NO-SQL', async () => {
26
- test('PUT', async () => {
27
- let results = {}
28
- for await (const data of Fylo.findDocs(POSTS).collect()) {
29
- results = { ...results, ...data }
30
- }
31
- expect(Object.keys(results).length).toEqual(postsCount)
32
- })
33
- })
34
- describe('SQL', () => {
35
- test('INSERT', async () => {
36
- const results = await fylo.executeSQL(`SELECT * FROM ${ALBUMS}`)
37
- expect(Object.keys(results).length).toEqual(albumsCount)
38
- })
39
- })
@@ -1,95 +0,0 @@
1
- import { test, expect, describe, beforeAll, afterAll, mock } from 'bun:test'
2
- import Fylo from '../../src'
3
- import { commentsURL, usersURL } from '../data'
4
- import S3Mock from '../mocks/s3'
5
- import RedisMock from '../mocks/redis'
6
- const COMMENTS = `comment`
7
- const USERS = `user`
8
- let commentsResults = {}
9
- let usersResults = {}
10
- const fylo = new Fylo()
11
- mock.module('../../src/adapters/s3', () => ({ S3: S3Mock }))
12
- mock.module('../../src/adapters/redis', () => ({ Redis: RedisMock }))
13
- beforeAll(async () => {
14
- await Promise.all([Fylo.createCollection(COMMENTS), fylo.executeSQL(`CREATE TABLE ${USERS}`)])
15
- try {
16
- await Promise.all([
17
- fylo.importBulkData(COMMENTS, new URL(commentsURL), 100),
18
- fylo.importBulkData(USERS, new URL(usersURL), 100)
19
- ])
20
- } catch {
21
- await fylo.rollback()
22
- }
23
- for await (const data of Fylo.findDocs(COMMENTS, { $limit: 1 }).collect()) {
24
- commentsResults = { ...commentsResults, ...data }
25
- }
26
- usersResults = await fylo.executeSQL(`SELECT * FROM ${USERS} LIMIT 1`)
27
- })
28
- afterAll(async () => {
29
- await Promise.all([Fylo.dropCollection(COMMENTS), fylo.executeSQL(`DROP TABLE ${USERS}`)])
30
- })
31
- describe('NO-SQL', async () => {
32
- test('DELETE ONE', async () => {
33
- const id = Object.keys(commentsResults).shift()
34
- try {
35
- await fylo.delDoc(COMMENTS, id)
36
- } catch {
37
- await fylo.rollback()
38
- }
39
- commentsResults = {}
40
- for await (const data of Fylo.findDocs(COMMENTS).collect()) {
41
- commentsResults = { ...commentsResults, ...data }
42
- }
43
- const idx = Object.keys(commentsResults).findIndex((_id) => _id === id)
44
- expect(idx).toEqual(-1)
45
- })
46
- test('DELETE CLAUSE', async () => {
47
- try {
48
- await fylo.delDocs(COMMENTS, { $ops: [{ name: { $like: '%et%' } }] })
49
- } catch (e) {
50
- console.error(e)
51
- await fylo.rollback()
52
- }
53
- commentsResults = {}
54
- for await (const data of Fylo.findDocs(COMMENTS, {
55
- $ops: [{ name: { $like: '%et%' } }]
56
- }).collect()) {
57
- commentsResults = { ...commentsResults, ...data }
58
- }
59
- expect(Object.keys(commentsResults).length).toEqual(0)
60
- })
61
- test('DELETE ALL', async () => {
62
- try {
63
- await fylo.delDocs(COMMENTS)
64
- } catch {
65
- await fylo.rollback()
66
- }
67
- commentsResults = {}
68
- for await (const data of Fylo.findDocs(COMMENTS).collect()) {
69
- commentsResults = { ...commentsResults, ...data }
70
- }
71
- expect(Object.keys(commentsResults).length).toEqual(0)
72
- })
73
- })
74
- describe('SQL', async () => {
75
- test('DELETE CLAUSE', async () => {
76
- const name = Object.values(usersResults).shift().name
77
- try {
78
- await fylo.executeSQL(`DELETE FROM ${USERS} WHERE name = '${name}'`)
79
- } catch {
80
- await fylo.rollback()
81
- }
82
- usersResults = await fylo.executeSQL(`SELECT * FROM ${USERS} WHERE name = '${name}'`)
83
- const idx = Object.values(usersResults).findIndex((com) => com.name === name)
84
- expect(idx).toBe(-1)
85
- })
86
- test('DELETE ALL', async () => {
87
- try {
88
- await fylo.executeSQL(`DELETE FROM ${USERS}`)
89
- } catch {
90
- await fylo.rollback()
91
- }
92
- usersResults = await fylo.executeSQL(`SELECT * FROM ${USERS}`)
93
- expect(Object.keys(usersResults).length).toBe(0)
94
- })
95
- })
@@ -1,158 +0,0 @@
1
- import { test, expect, describe, beforeAll, afterAll, mock } from 'bun:test'
2
- import Fylo from '../../src'
3
- import TTID from '@delma/ttid'
4
- import S3Mock from '../mocks/s3'
5
- import RedisMock from '../mocks/redis'
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 }))
10
- beforeAll(async () => {
11
- await Fylo.createCollection(COLLECTION)
12
- })
13
- afterAll(async () => {
14
- await Fylo.dropCollection(COLLECTION)
15
- })
16
- describe('NO-SQL', () => {
17
- test('GET ONE — non-existent ID returns empty object', async () => {
18
- const fakeId = TTID.generate()
19
- const result = await Fylo.getDoc(COLLECTION, fakeId).once()
20
- expect(Object.keys(result).length).toBe(0)
21
- })
22
- test('PUT / GET — forward slashes in values round-trip correctly', async () => {
23
- const original = {
24
- userId: 1,
25
- id: 1,
26
- title: 'Slash Test',
27
- body: 'https://example.com/api/v1/resource'
28
- }
29
- const _id = await fylo.putData(COLLECTION, original)
30
- const result = await Fylo.getDoc(COLLECTION, _id).once()
31
- const doc = result[_id]
32
- expect(doc.body).toBe(original.body)
33
- await fylo.delDoc(COLLECTION, _id)
34
- })
35
- test('PUT / GET — values with multiple consecutive slashes round-trip correctly', async () => {
36
- const original = {
37
- userId: 1,
38
- id: 2,
39
- title: 'Double Slash',
40
- body: 'https://cdn.example.com//assets//image.png'
41
- }
42
- const _id = await fylo.putData(COLLECTION, original)
43
- const result = await Fylo.getDoc(COLLECTION, _id).once()
44
- expect(result[_id].body).toBe(original.body)
45
- await fylo.delDoc(COLLECTION, _id)
46
- })
47
- test('$ops — multiple conditions act as OR union', async () => {
48
- const cleanFylo = new Fylo()
49
- const id1 = await cleanFylo.putData(COLLECTION, {
50
- userId: 10,
51
- id: 100,
52
- title: 'Alpha',
53
- body: 'first'
54
- })
55
- const id2 = await cleanFylo.putData(COLLECTION, {
56
- userId: 20,
57
- id: 200,
58
- title: 'Beta',
59
- body: 'second'
60
- })
61
- const results = {}
62
- for await (const data of Fylo.findDocs(COLLECTION, {
63
- $ops: [{ userId: { $eq: 10 } }, { userId: { $eq: 20 } }]
64
- }).collect()) {
65
- Object.assign(results, data)
66
- }
67
- expect(results[id1]).toBeDefined()
68
- expect(results[id2]).toBeDefined()
69
- await cleanFylo.delDoc(COLLECTION, id1)
70
- await cleanFylo.delDoc(COLLECTION, id2)
71
- })
72
- test('$rename — renames fields in query output', async () => {
73
- const cleanFylo = new Fylo()
74
- const _id = await cleanFylo.putData(COLLECTION, {
75
- userId: 1,
76
- id: 300,
77
- title: 'Rename Me',
78
- body: 'some body'
79
- })
80
- let renamed = {}
81
- for await (const data of Fylo.findDocs(COLLECTION, {
82
- $ops: [{ id: { $eq: 300 } }],
83
- $rename: { title: 'name' }
84
- }).collect()) {
85
- renamed = Object.values(data)[0]
86
- }
87
- expect(renamed.name).toBe('Rename Me')
88
- expect(renamed.title).toBeUndefined()
89
- await cleanFylo.delDoc(COLLECTION, _id)
90
- })
91
- test('versioned putData — preserves creation-time prefix in TTID', async () => {
92
- const cleanFylo = new Fylo()
93
- const _id1 = await cleanFylo.putData(COLLECTION, {
94
- userId: 1,
95
- id: 400,
96
- title: 'Original',
97
- body: 'v1'
98
- })
99
- const _id2 = await cleanFylo.putData(COLLECTION, {
100
- [_id1]: { userId: 1, id: 400, title: 'Updated', body: 'v2' }
101
- })
102
- expect(_id2.split('-')[0]).toBe(_id1.split('-')[0])
103
- const result = await Fylo.getDoc(COLLECTION, _id2).once()
104
- const doc = result[_id2]
105
- expect(doc).toBeDefined()
106
- expect(doc.title).toBe('Updated')
107
- await cleanFylo.delDoc(COLLECTION, _id1)
108
- await cleanFylo.delDoc(COLLECTION, _id2)
109
- })
110
- test('versioned putData — original version is no longer retrievable by old full TTID', async () => {
111
- const cleanFylo = new Fylo()
112
- const _id1 = await cleanFylo.putData(COLLECTION, {
113
- userId: 1,
114
- id: 500,
115
- title: 'Old Version',
116
- body: 'original'
117
- })
118
- const _id2 = await cleanFylo.putData(COLLECTION, {
119
- [_id1]: { userId: 1, id: 500, title: 'New Version', body: 'updated' }
120
- })
121
- expect(_id1).not.toBe(_id2)
122
- await cleanFylo.delDoc(COLLECTION, _id2)
123
- })
124
- })
125
- describe('SQL', () => {
126
- test('UPDATE ONE — update a single document by querying its unique field', async () => {
127
- const cleanFylo = new Fylo()
128
- await cleanFylo.putData(COLLECTION, {
129
- userId: 1,
130
- id: 600,
131
- title: 'Before SQL Update',
132
- body: 'original'
133
- })
134
- const updated = await cleanFylo.executeSQL(
135
- `UPDATE ${COLLECTION} SET title = 'After SQL Update' WHERE id = 600`
136
- )
137
- expect(updated).toBe(1)
138
- const results = await cleanFylo.executeSQL(
139
- `SELECT * FROM ${COLLECTION} WHERE title = 'After SQL Update'`
140
- )
141
- expect(Object.keys(results).length).toBe(1)
142
- expect(Object.values(results)[0].title).toBe('After SQL Update')
143
- })
144
- test('DELETE ONE — delete a single document by querying its unique field', async () => {
145
- const cleanFylo = new Fylo()
146
- await cleanFylo.putData(COLLECTION, {
147
- userId: 1,
148
- id: 700,
149
- title: 'Delete Via SQL',
150
- body: 'should be removed'
151
- })
152
- await cleanFylo.executeSQL(`DELETE FROM ${COLLECTION} WHERE title = 'Delete Via SQL'`)
153
- const results = await cleanFylo.executeSQL(
154
- `SELECT * FROM ${COLLECTION} WHERE title = 'Delete Via SQL'`
155
- )
156
- expect(Object.keys(results).length).toBe(0)
157
- })
158
- })
@@ -1,131 +0,0 @@
1
- import { test, expect, describe, beforeAll, afterAll, mock } from 'bun:test'
2
- import Fylo from '../../src'
3
- import S3Mock from '../mocks/s3'
4
- import RedisMock from '../mocks/redis'
5
- import { CipherMock } from '../mocks/cipher'
6
- 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 }))
10
- mock.module('../../src/adapters/cipher', () => ({ Cipher: CipherMock }))
11
- beforeAll(async () => {
12
- await Fylo.createCollection(COLLECTION)
13
- await CipherMock.configure('test-secret-key')
14
- CipherMock.registerFields(COLLECTION, ['email', 'ssn', 'address'])
15
- })
16
- afterAll(async () => {
17
- CipherMock.reset()
18
- await Fylo.dropCollection(COLLECTION)
19
- })
20
- describe('Encryption', () => {
21
- let docId
22
- test('PUT encrypted document', async () => {
23
- docId = await fylo.putData(COLLECTION, {
24
- name: 'Alice',
25
- email: 'alice@example.com',
26
- ssn: '123-45-6789',
27
- age: 30
28
- })
29
- expect(docId).toBeDefined()
30
- })
31
- test('GET decrypts fields transparently', async () => {
32
- const result = await Fylo.getDoc(COLLECTION, docId).once()
33
- const doc = Object.values(result)[0]
34
- expect(doc.name).toBe('Alice')
35
- expect(doc.email).toBe('alice@example.com')
36
- expect(doc.ssn).toBe('123-45-6789')
37
- expect(doc.age).toBe(30)
38
- })
39
- test('encrypted values stored in S3 keys are not plaintext', () => {
40
- const bucket = S3Mock.getBucketFormat(COLLECTION)
41
- expect(bucket).toBeDefined()
42
- })
43
- test('$eq query works on encrypted field', async () => {
44
- let found = false
45
- for await (const data of Fylo.findDocs(COLLECTION, {
46
- $ops: [{ email: { $eq: 'alice@example.com' } }]
47
- }).collect()) {
48
- if (typeof data === 'object') {
49
- const doc = Object.values(data)[0]
50
- expect(doc.email).toBe('alice@example.com')
51
- found = true
52
- }
53
- }
54
- expect(found).toBe(true)
55
- })
56
- test('$ne throws on encrypted field', async () => {
57
- try {
58
- const iter = Fylo.findDocs(COLLECTION, {
59
- $ops: [{ email: { $ne: 'bob@example.com' } }]
60
- }).collect()
61
- await iter.next()
62
- expect(true).toBe(false)
63
- } catch (e) {
64
- expect(e.message).toContain('not supported on encrypted field')
65
- }
66
- })
67
- test('$gt throws on encrypted field', async () => {
68
- try {
69
- const iter = Fylo.findDocs(COLLECTION, {
70
- $ops: [{ ssn: { $gt: 0 } }]
71
- }).collect()
72
- await iter.next()
73
- expect(true).toBe(false)
74
- } catch (e) {
75
- expect(e.message).toContain('not supported on encrypted field')
76
- }
77
- })
78
- test('$like throws on encrypted field', async () => {
79
- try {
80
- const iter = Fylo.findDocs(COLLECTION, {
81
- $ops: [{ email: { $like: '%@example.com' } }]
82
- }).collect()
83
- await iter.next()
84
- expect(true).toBe(false)
85
- } catch (e) {
86
- expect(e.message).toContain('not supported on encrypted field')
87
- }
88
- })
89
- test('non-encrypted fields remain queryable with all operators', async () => {
90
- let found = false
91
- for await (const data of Fylo.findDocs(COLLECTION, {
92
- $ops: [{ name: { $eq: 'Alice' } }]
93
- }).collect()) {
94
- if (typeof data === 'object') {
95
- const doc = Object.values(data)[0]
96
- expect(doc.name).toBe('Alice')
97
- found = true
98
- }
99
- }
100
- expect(found).toBe(true)
101
- })
102
- test('nested encrypted field (address.city)', async () => {
103
- const id = await fylo.putData(COLLECTION, {
104
- name: 'Bob',
105
- email: 'bob@example.com',
106
- ssn: '987-65-4321',
107
- age: 25,
108
- address: { city: 'Toronto', zip: 'M5V 2T6' }
109
- })
110
- const result = await Fylo.getDoc(COLLECTION, id).once()
111
- const doc = Object.values(result)[0]
112
- expect(doc.address.city).toBe('Toronto')
113
- expect(doc.address.zip).toBe('M5V 2T6')
114
- })
115
- test('UPDATE preserves encryption', async () => {
116
- await fylo.patchDoc(COLLECTION, {
117
- [docId]: { email: 'alice-new@example.com' }
118
- })
119
- let found = false
120
- for await (const data of Fylo.findDocs(COLLECTION, {
121
- $ops: [{ email: { $eq: 'alice-new@example.com' } }]
122
- }).collect()) {
123
- if (typeof data === 'object') {
124
- const doc = Object.values(data)[0]
125
- expect(doc.email).toBe('alice-new@example.com')
126
- found = true
127
- }
128
- }
129
- expect(found).toBe(true)
130
- })
131
- })
@@ -1,46 +0,0 @@
1
- import { test, expect, describe, beforeAll, afterAll, mock } from 'bun:test'
2
- import Fylo from '../../src'
3
- import { postsURL } from '../data'
4
- import S3Mock from '../mocks/s3'
5
- import RedisMock from '../mocks/redis'
6
- const POSTS = 'exp-post'
7
- const IMPORT_LIMIT = 20
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 }))
12
- beforeAll(async () => {
13
- await Fylo.createCollection(POSTS)
14
- try {
15
- importedCount = await fylo.importBulkData(POSTS, new URL(postsURL), IMPORT_LIMIT)
16
- } catch {
17
- await fylo.rollback()
18
- }
19
- })
20
- afterAll(async () => {
21
- await Fylo.dropCollection(POSTS)
22
- })
23
- describe('NO-SQL', () => {
24
- test('EXPORT count matches import', async () => {
25
- let exported = 0
26
- for await (const _doc of Fylo.exportBulkData(POSTS)) {
27
- exported++
28
- }
29
- expect(exported).toBe(importedCount)
30
- })
31
- test('EXPORT document shape', async () => {
32
- for await (const doc of Fylo.exportBulkData(POSTS)) {
33
- expect(doc).toHaveProperty('title')
34
- expect(doc).toHaveProperty('userId')
35
- expect(doc).toHaveProperty('body')
36
- break
37
- }
38
- })
39
- test('EXPORT all documents are valid posts', async () => {
40
- for await (const doc of Fylo.exportBulkData(POSTS)) {
41
- expect(typeof doc.title).toBe('string')
42
- expect(typeof doc.userId).toBe('number')
43
- expect(doc.userId).toBeGreaterThan(0)
44
- }
45
- })
46
- })
@@ -1,154 +0,0 @@
1
- import { test, expect, describe, beforeAll, afterAll, mock } from 'bun:test'
2
- import Fylo from '../../src'
3
- import { albumURL, postsURL } from '../data'
4
- import S3Mock from '../mocks/s3'
5
- import RedisMock from '../mocks/redis'
6
- const ALBUMS = 'jm-album'
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 }))
11
- beforeAll(async () => {
12
- await Promise.all([Fylo.createCollection(ALBUMS), Fylo.createCollection(POSTS)])
13
- try {
14
- await Promise.all([
15
- fylo.importBulkData(ALBUMS, new URL(albumURL), 100),
16
- fylo.importBulkData(POSTS, new URL(postsURL), 100)
17
- ])
18
- } catch {
19
- await fylo.rollback()
20
- }
21
- })
22
- afterAll(async () => {
23
- await Promise.all([Fylo.dropCollection(ALBUMS), Fylo.dropCollection(POSTS)])
24
- })
25
- describe('NO-SQL', async () => {
26
- test('INNER JOIN — returns only join field values', async () => {
27
- const results = await Fylo.joinDocs({
28
- $leftCollection: ALBUMS,
29
- $rightCollection: POSTS,
30
- $mode: 'inner',
31
- $on: { userId: { $eq: 'userId' } }
32
- })
33
- const pairs = Object.values(results)
34
- expect(pairs.length).toBeGreaterThan(0)
35
- for (const pair of pairs) {
36
- expect(pair).toHaveProperty('userId')
37
- expect(typeof pair.userId).toBe('number')
38
- }
39
- })
40
- test('LEFT JOIN — returns full left-collection document', async () => {
41
- const results = await Fylo.joinDocs({
42
- $leftCollection: ALBUMS,
43
- $rightCollection: POSTS,
44
- $mode: 'left',
45
- $on: { userId: { $eq: 'userId' } }
46
- })
47
- const docs = Object.values(results)
48
- expect(docs.length).toBeGreaterThan(0)
49
- for (const doc of docs) {
50
- expect(doc).toHaveProperty('title')
51
- expect(doc).toHaveProperty('userId')
52
- }
53
- })
54
- test('RIGHT JOIN — returns full right-collection document', async () => {
55
- const results = await Fylo.joinDocs({
56
- $leftCollection: ALBUMS,
57
- $rightCollection: POSTS,
58
- $mode: 'right',
59
- $on: { userId: { $eq: 'userId' } }
60
- })
61
- const docs = Object.values(results)
62
- expect(docs.length).toBeGreaterThan(0)
63
- for (const doc of docs) {
64
- expect(doc).toHaveProperty('title')
65
- expect(doc).toHaveProperty('body')
66
- expect(doc).toHaveProperty('userId')
67
- }
68
- })
69
- test('OUTER JOIN — returns merged left + right document', async () => {
70
- const results = await Fylo.joinDocs({
71
- $leftCollection: ALBUMS,
72
- $rightCollection: POSTS,
73
- $mode: 'outer',
74
- $on: { userId: { $eq: 'userId' } }
75
- })
76
- const docs = Object.values(results)
77
- expect(docs.length).toBeGreaterThan(0)
78
- for (const doc of docs) {
79
- expect(doc).toHaveProperty('userId')
80
- }
81
- })
82
- test('JOIN with $limit — respects the result cap', async () => {
83
- const results = await Fylo.joinDocs({
84
- $leftCollection: ALBUMS,
85
- $rightCollection: POSTS,
86
- $mode: 'inner',
87
- $on: { userId: { $eq: 'userId' } },
88
- $limit: 5
89
- })
90
- expect(Object.keys(results).length).toBe(5)
91
- })
92
- test('JOIN with $select — only requested fields are returned', async () => {
93
- const results = await Fylo.joinDocs({
94
- $leftCollection: ALBUMS,
95
- $rightCollection: POSTS,
96
- $mode: 'left',
97
- $on: { userId: { $eq: 'userId' } },
98
- $select: ['title'],
99
- $limit: 10
100
- })
101
- const docs = Object.values(results)
102
- expect(docs.length).toBeGreaterThan(0)
103
- for (const doc of docs) {
104
- expect(doc).toHaveProperty('title')
105
- expect(doc).not.toHaveProperty('userId')
106
- }
107
- })
108
- test('JOIN with $groupby — groups results by field value', async () => {
109
- const results = await Fylo.joinDocs({
110
- $leftCollection: ALBUMS,
111
- $rightCollection: POSTS,
112
- $mode: 'inner',
113
- $on: { userId: { $eq: 'userId' } },
114
- $groupby: 'userId'
115
- })
116
- expect(Object.keys(results).length).toBeGreaterThan(0)
117
- })
118
- test('JOIN with $onlyIds — returns IDs only', async () => {
119
- const results = await Fylo.joinDocs({
120
- $leftCollection: ALBUMS,
121
- $rightCollection: POSTS,
122
- $mode: 'inner',
123
- $on: { userId: { $eq: 'userId' } },
124
- $onlyIds: true,
125
- $limit: 10
126
- })
127
- expect(Array.isArray(results)).toBe(true)
128
- expect(results.length).toBeGreaterThan(0)
129
- })
130
- })
131
- describe('SQL', async () => {
132
- test('INNER JOIN', async () => {
133
- const results = await fylo.executeSQL(
134
- `SELECT * FROM ${ALBUMS} INNER JOIN ${POSTS} ON userId = userId`
135
- )
136
- expect(Object.keys(results).length).toBeGreaterThan(0)
137
- })
138
- test('LEFT JOIN', async () => {
139
- const results = await fylo.executeSQL(
140
- `SELECT * FROM ${ALBUMS} LEFT JOIN ${POSTS} ON userId = userId`
141
- )
142
- const docs = Object.values(results)
143
- expect(docs.length).toBeGreaterThan(0)
144
- expect(docs[0]).toHaveProperty('title')
145
- })
146
- test('RIGHT JOIN', async () => {
147
- const results = await fylo.executeSQL(
148
- `SELECT * FROM ${ALBUMS} RIGHT JOIN ${POSTS} ON userId = userId`
149
- )
150
- const docs = Object.values(results)
151
- expect(docs.length).toBeGreaterThan(0)
152
- expect(docs[0]).toHaveProperty('body')
153
- })
154
- })
@@ -1,38 +0,0 @@
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
- })