@delma/fylo 1.1.2 → 2.0.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 (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 +888 -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 +108 -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
@@ -1,147 +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
-
7
- const COMMENTS = `comment`
8
- const USERS = `user`
9
-
10
- let commentsResults: Record<_ttid, _comment> = {}
11
- let usersResults: Record<_ttid, _user> = {}
12
-
13
- const fylo = new Fylo()
14
-
15
- mock.module('../../src/adapters/s3', () => ({ S3: S3Mock }))
16
- mock.module('../../src/adapters/redis', () => ({ Redis: RedisMock }))
17
-
18
- beforeAll(async () => {
19
-
20
- await Promise.all([
21
- Fylo.createCollection(COMMENTS),
22
- fylo.executeSQL<_post>(`CREATE TABLE ${USERS}`)
23
- ])
24
-
25
- try {
26
-
27
- await Promise.all([
28
- fylo.importBulkData<_comment>(COMMENTS, new URL(commentsURL), 100),
29
- fylo.importBulkData<_user>(USERS, new URL(usersURL), 100)
30
- ])
31
-
32
- } catch {
33
- await fylo.rollback()
34
- }
35
-
36
- for await (const data of Fylo.findDocs<_comment>(COMMENTS, { $limit: 1 }).collect()) {
37
-
38
- commentsResults = { ...commentsResults, ...data as Record<_ttid, _comment> }
39
-
40
- }
41
-
42
- usersResults = await fylo.executeSQL<_user>(`SELECT * FROM ${USERS} LIMIT 1`) as Record<_ttid, _user>
43
- })
44
-
45
- afterAll(async () => {
46
- await Promise.all([Fylo.dropCollection(COMMENTS), fylo.executeSQL<_user>(`DROP TABLE ${USERS}`)])
47
- })
48
-
49
- describe("NO-SQL", async () => {
50
-
51
- test("DELETE ONE", async () => {
52
-
53
- const id = Object.keys(commentsResults).shift()!
54
-
55
- try {
56
- await fylo.delDoc(COMMENTS, id)
57
- } catch {
58
- await fylo.rollback()
59
- }
60
-
61
- commentsResults = {}
62
-
63
- for await (const data of Fylo.findDocs<_comment>(COMMENTS).collect()) {
64
-
65
- commentsResults = { ...commentsResults, ...data as Record<_ttid, _comment> }
66
- }
67
-
68
- const idx = Object.keys(commentsResults).findIndex(_id => _id === id)
69
-
70
- expect(idx).toEqual(-1)
71
-
72
- })
73
-
74
- test("DELETE CLAUSE", async () => {
75
-
76
- try {
77
- await fylo.delDocs<_comment>(COMMENTS, { $ops: [ { name: { $like: "%et%" } } ] })
78
- //console.log
79
- } catch(e) {
80
- console.error(e)
81
- await fylo.rollback()
82
- }
83
-
84
- commentsResults = {}
85
-
86
- for await (const data of Fylo.findDocs<_comment>(COMMENTS, { $ops: [ { name: { $like: "%et%" } } ] }).collect()) {
87
-
88
- // console.log(data)
89
-
90
- commentsResults = { ...commentsResults, ...data as Record<_ttid, _comment> }
91
- }
92
-
93
- expect(Object.keys(commentsResults).length).toEqual(0)
94
- })
95
-
96
- test("DELETE ALL", async () => {
97
-
98
- try {
99
- await fylo.delDocs<_comment>(COMMENTS)
100
- } catch {
101
- await fylo.rollback()
102
- }
103
-
104
- commentsResults = {}
105
-
106
- for await (const data of Fylo.findDocs<_comment>(COMMENTS).collect()) {
107
-
108
- commentsResults = { ...commentsResults, ...data as Record<_ttid, _comment> }
109
- }
110
-
111
- expect(Object.keys(commentsResults).length).toEqual(0)
112
- })
113
- })
114
-
115
-
116
- describe("SQL", async () => {
117
-
118
- test("DELETE CLAUSE", async () => {
119
-
120
- const name = Object.values(usersResults).shift()!.name
121
-
122
- try {
123
- await fylo.executeSQL<_user>(`DELETE FROM ${USERS} WHERE name = '${name}'`)
124
- } catch {
125
- await fylo.rollback()
126
- }
127
-
128
- usersResults = await fylo.executeSQL<_user>(`SELECT * FROM ${USERS} WHERE name = '${name}'`) as Record<_ttid, _user>
129
-
130
- const idx = Object.values(usersResults).findIndex(com => com.name === name)
131
-
132
- expect(idx).toBe(-1)
133
- })
134
-
135
- test("DELETE ALL", async () => {
136
-
137
- try {
138
- await fylo.executeSQL<_user>(`DELETE FROM ${USERS}`)
139
- } catch {
140
- await fylo.rollback()
141
- }
142
-
143
- usersResults = await fylo.executeSQL<_user>(`SELECT * FROM ${USERS}`) as Record<_ttid, _user>
144
-
145
- expect(Object.keys(usersResults).length).toBe(0)
146
- })
147
- })
@@ -1,212 +0,0 @@
1
- import { test, expect, describe, beforeAll, afterAll, mock } from 'bun:test'
2
- import Fylo from '../../src'
3
- import { usersURL } from '../data'
4
- import S3Mock from '../mocks/s3'
5
- import RedisMock from '../mocks/redis'
6
-
7
- /**
8
- * _user has deeply nested fields:
9
- * address.street, address.suite, address.city, address.zipcode
10
- * address.geo.lat, address.geo.lng
11
- * company.name, company.catchPhrase, company.bs
12
- *
13
- * This exercises the recursive branch in Dir.extractKeys and the nested
14
- * path reconstruction in Dir.constructData.
15
- */
16
-
17
- const USERS = 'nst-user'
18
-
19
- let insertedCount = 0
20
- let sampleId: _ttid
21
-
22
- const fylo = new Fylo()
23
-
24
- mock.module('../../src/adapters/s3', () => ({ S3: S3Mock }))
25
- mock.module('../../src/adapters/redis', () => ({ Redis: RedisMock }))
26
-
27
- beforeAll(async () => {
28
- await Fylo.createCollection(USERS)
29
- try {
30
- insertedCount = await fylo.importBulkData<_user>(USERS, new URL(usersURL))
31
- } catch {
32
- await fylo.rollback()
33
- }
34
-
35
- for await (const data of Fylo.findDocs<_user>(USERS, { $limit: 1, $onlyIds: true }).collect()) {
36
- sampleId = data as _ttid
37
- }
38
- })
39
-
40
- afterAll(async () => {
41
- await Fylo.dropCollection(USERS)
42
- })
43
-
44
- describe("NO-SQL", async () => {
45
-
46
- test("SELECT ALL — nested documents are returned", async () => {
47
-
48
- let results: Record<_ttid, _user> = {}
49
-
50
- for await (const data of Fylo.findDocs<_user>(USERS).collect()) {
51
- results = { ...results, ...data as Record<_ttid, _user> }
52
- }
53
-
54
- expect(Object.keys(results).length).toBe(insertedCount)
55
- })
56
-
57
- test("GET ONE — top-level fields are reconstructed correctly", async () => {
58
-
59
- const result = await Fylo.getDoc<_user>(USERS, sampleId).once()
60
- const user = result[sampleId]
61
-
62
- expect(user).toBeDefined()
63
- expect(typeof user.name).toBe('string')
64
- expect(typeof user.email).toBe('string')
65
- expect(typeof user.phone).toBe('string')
66
- })
67
-
68
- test("GET ONE — first-level nested object is reconstructed correctly", async () => {
69
-
70
- const result = await Fylo.getDoc<_user>(USERS, sampleId).once()
71
- const user = result[sampleId]
72
-
73
- expect(user.address).toBeDefined()
74
- expect(typeof user.address.city).toBe('string')
75
- expect(typeof user.address.street).toBe('string')
76
- expect(typeof user.address.zipcode).toBe('string')
77
- })
78
-
79
- test("GET ONE — deeply nested object is reconstructed correctly", async () => {
80
-
81
- const result = await Fylo.getDoc<_user>(USERS, sampleId).once()
82
- const user = result[sampleId]
83
-
84
- expect(user.address.geo).toBeDefined()
85
- expect(typeof user.address.geo.lat).toBe('number')
86
- expect(typeof user.address.geo.lng).toBe('number')
87
- })
88
-
89
- test("GET ONE — second nested object is reconstructed correctly", async () => {
90
-
91
- const result = await Fylo.getDoc<_user>(USERS, sampleId).once()
92
- const user = result[sampleId]
93
-
94
- expect(user.company).toBeDefined()
95
- expect(typeof user.company.name).toBe('string')
96
- expect(typeof user.company.catchPhrase).toBe('string')
97
- expect(typeof user.company.bs).toBe('string')
98
- })
99
-
100
- test("SELECT — nested values are not corrupted across documents", async () => {
101
-
102
- for await (const data of Fylo.findDocs<_user>(USERS).collect()) {
103
-
104
- const [, user] = Object.entries(data as Record<_ttid, _user>)[0]
105
-
106
- expect(user.address).toBeDefined()
107
- expect(user.address.geo).toBeDefined()
108
- expect(typeof user.address.geo.lat).toBe('number')
109
- expect(user.company).toBeDefined()
110
- }
111
- })
112
-
113
- test("$select — returns only requested top-level fields", async () => {
114
-
115
- let results: Record<_ttid, _user> = {}
116
-
117
- for await (const data of Fylo.findDocs<_user>(USERS, { $select: ['name', 'email'] }).collect()) {
118
- results = { ...results, ...data as Record<_ttid, _user> }
119
- }
120
-
121
- const users = Object.values(results)
122
- const onlyNameAndEmail = users.every(u => u.name && u.email && !u.phone && !u.address)
123
-
124
- expect(onlyNameAndEmail).toBe(true)
125
- })
126
-
127
- test("$eq on nested string field — query by city", async () => {
128
-
129
- const result = await Fylo.getDoc<_user>(USERS, sampleId).once()
130
- const targetCity = result[sampleId].address.city
131
-
132
- let results: Record<_ttid, _user> = {}
133
-
134
- for await (const data of Fylo.findDocs<_user>(USERS, {
135
- $ops: [{ ['address/city' as keyof _user]: { $eq: targetCity } }]
136
- }).collect()) {
137
- results = { ...results, ...data as Record<_ttid, _user> }
138
- }
139
-
140
- const matchingUsers = Object.values(results)
141
- const allMatch = matchingUsers.every(u => u.address.city === targetCity)
142
-
143
- expect(allMatch).toBe(true)
144
- expect(matchingUsers.length).toBeGreaterThan(0)
145
- })
146
- })
147
-
148
- describe("SQL — dot notation", async () => {
149
-
150
- test("WHERE with dot notation — first-level nested field", async () => {
151
-
152
- const result = await Fylo.getDoc<_user>(USERS, sampleId).once()
153
- const targetCity = result[sampleId].address.city
154
-
155
- const results = await fylo.executeSQL<_user>(
156
- `SELECT * FROM ${USERS} WHERE address.city = '${targetCity}'`
157
- ) as Record<_ttid, _user>
158
-
159
- const users = Object.values(results)
160
- const allMatch = users.every(u => u.address.city === targetCity)
161
-
162
- expect(allMatch).toBe(true)
163
- expect(users.length).toBeGreaterThan(0)
164
- })
165
-
166
- test("WHERE with dot notation — deeply nested field", async () => {
167
-
168
- const result = await Fylo.getDoc<_user>(USERS, sampleId).once()
169
- const targetLat = result[sampleId].address.geo.lat
170
-
171
- const results = await fylo.executeSQL<_user>(
172
- `SELECT * FROM ${USERS} WHERE address.geo.lat = '${targetLat}'`
173
- ) as Record<_ttid, _user>
174
-
175
- const users = Object.values(results)
176
- const allMatch = users.every(u => u.address.geo.lat === targetLat)
177
-
178
- expect(allMatch).toBe(true)
179
- expect(users.length).toBeGreaterThan(0)
180
- })
181
-
182
- test("WHERE with dot notation — second nested object", async () => {
183
-
184
- const result = await Fylo.getDoc<_user>(USERS, sampleId).once()
185
- const targetCompany = result[sampleId].company.name
186
-
187
- const results = await fylo.executeSQL<_user>(
188
- `SELECT * FROM ${USERS} WHERE company.name = '${targetCompany}'`
189
- ) as Record<_ttid, _user>
190
-
191
- const users = Object.values(results)
192
- const allMatch = users.every(u => u.company.name === targetCompany)
193
-
194
- expect(allMatch).toBe(true)
195
- expect(users.length).toBeGreaterThan(0)
196
- })
197
-
198
- test("SELECT with dot notation in WHERE — partial field selection", async () => {
199
-
200
- const result = await Fylo.getDoc<_user>(USERS, sampleId).once()
201
- const targetCity = result[sampleId].address.city
202
-
203
- const results = await fylo.executeSQL<_user>(
204
- `SELECT name, email FROM ${USERS} WHERE address.city = '${targetCity}'`
205
- ) as Record<_ttid, _user>
206
-
207
- const users = Object.values(results)
208
-
209
- expect(users.length).toBeGreaterThan(0)
210
- expect(users.every(u => u.name && u.email && !u.phone)).toBe(true)
211
- })
212
- })
@@ -1,167 +0,0 @@
1
- import { test, expect, describe, beforeAll, afterAll, mock } from 'bun:test'
2
- import Fylo from '../../src'
3
- import { albumURL } from '../data'
4
- import S3Mock from '../mocks/s3'
5
- import RedisMock from '../mocks/redis'
6
-
7
- /**
8
- * Albums from JSONPlaceholder: 100 records, userId 1–10, 10 albums per userId.
9
- *
10
- * NOTE: The numeric-range glob generator in query.ts produces digit-by-digit
11
- * character classes (e.g. $gt:5 → "[6-9]"). This correctly matches single-digit
12
- * values but misses two-digit values like 10. Tests below assert correct expected
13
- * values so any regression in the generator is surfaced immediately.
14
- */
15
-
16
- const ALBUMS = 'ops-album'
17
-
18
- const fylo = new Fylo()
19
-
20
- mock.module('../../src/adapters/s3', () => ({ S3: S3Mock }))
21
- mock.module('../../src/adapters/redis', () => ({ Redis: RedisMock }))
22
-
23
- beforeAll(async () => {
24
- await Fylo.createCollection(ALBUMS)
25
- try {
26
- await fylo.importBulkData<_album>(ALBUMS, new URL(albumURL), 100)
27
- } catch {
28
- await fylo.rollback()
29
- }
30
- })
31
-
32
- afterAll(async () => {
33
- await Fylo.dropCollection(ALBUMS)
34
- })
35
-
36
- describe("NO-SQL", async () => {
37
-
38
- test("$ne — excludes matching value", async () => {
39
-
40
- let results: Record<_ttid, _album> = {}
41
-
42
- for await (const data of Fylo.findDocs<_album>(ALBUMS, { $ops: [{ userId: { $ne: 1 } }] }).collect()) {
43
- results = { ...results, ...data as Record<_ttid, _album> }
44
- }
45
-
46
- const albums = Object.values(results)
47
- const hasUserId1 = albums.some(a => a.userId === 1)
48
-
49
- expect(hasUserId1).toBe(false)
50
- expect(albums.length).toBe(90)
51
- })
52
-
53
- test("$lt — returns documents where field is less than value", async () => {
54
-
55
- let results: Record<_ttid, _album> = {}
56
-
57
- for await (const data of Fylo.findDocs<_album>(ALBUMS, { $ops: [{ userId: { $lt: 5 } }] }).collect()) {
58
- results = { ...results, ...data as Record<_ttid, _album> }
59
- }
60
-
61
- const albums = Object.values(results)
62
- const allLessThan5 = albums.every(a => a.userId < 5)
63
-
64
- expect(allLessThan5).toBe(true)
65
- expect(albums.length).toBe(40) // userId 1,2,3,4 → 4×10
66
- })
67
-
68
- test("$lte — returns documents where field is less than or equal to value", async () => {
69
-
70
- let results: Record<_ttid, _album> = {}
71
-
72
- for await (const data of Fylo.findDocs<_album>(ALBUMS, { $ops: [{ userId: { $lte: 5 } }] }).collect()) {
73
- results = { ...results, ...data as Record<_ttid, _album> }
74
- }
75
-
76
- const albums = Object.values(results)
77
- const allLte5 = albums.every(a => a.userId <= 5)
78
-
79
- expect(allLte5).toBe(true)
80
- expect(albums.length).toBe(50) // userId 1,2,3,4,5 → 5×10
81
- })
82
-
83
- test("$gt — returns documents where field is greater than value", async () => {
84
-
85
- let results: Record<_ttid, _album> = {}
86
-
87
- for await (const data of Fylo.findDocs<_album>(ALBUMS, { $ops: [{ userId: { $gt: 5 } }] }).collect()) {
88
- results = { ...results, ...data as Record<_ttid, _album> }
89
- }
90
-
91
- const albums = Object.values(results)
92
- const allGt5 = albums.every(a => a.userId > 5)
93
-
94
- expect(allGt5).toBe(true)
95
- expect(albums.length).toBe(50) // userId 6,7,8,9,10 → 5×10
96
- })
97
-
98
- test("$gte — returns documents where field is greater than or equal to value", async () => {
99
-
100
- let results: Record<_ttid, _album> = {}
101
-
102
- for await (const data of Fylo.findDocs<_album>(ALBUMS, { $ops: [{ userId: { $gte: 5 } }] }).collect()) {
103
- results = { ...results, ...data as Record<_ttid, _album> }
104
- }
105
-
106
- const albums = Object.values(results)
107
- const allGte5 = albums.every(a => a.userId >= 5)
108
-
109
- expect(allGte5).toBe(true)
110
- expect(albums.length).toBe(60) // userId 5,6,7,8,9,10 → 6×10
111
- })
112
-
113
- test("$like — matches substring pattern", async () => {
114
-
115
- let results: Record<_ttid, _album> = {}
116
-
117
- for await (const data of Fylo.findDocs<_album>(ALBUMS, { $ops: [{ title: { $like: '%quidem%' } }] }).collect()) {
118
- results = { ...results, ...data as Record<_ttid, _album> }
119
- }
120
-
121
- const albums = Object.values(results)
122
- const allMatch = albums.every(a => a.title.includes('quidem'))
123
-
124
- expect(allMatch).toBe(true)
125
- expect(albums.length).toBeGreaterThan(0)
126
- })
127
-
128
- test("$like — prefix pattern", async () => {
129
-
130
- let results: Record<_ttid, _album> = {}
131
-
132
- for await (const data of Fylo.findDocs<_album>(ALBUMS, { $ops: [{ title: { $like: 'omnis%' } }] }).collect()) {
133
- results = { ...results, ...data as Record<_ttid, _album> }
134
- }
135
-
136
- const albums = Object.values(results)
137
- const allStartWith = albums.every(a => a.title.startsWith('omnis'))
138
-
139
- expect(allStartWith).toBe(true)
140
- expect(albums.length).toBeGreaterThan(0)
141
- })
142
- })
143
-
144
- describe("SQL", async () => {
145
-
146
- test("WHERE != — excludes matching value", async () => {
147
-
148
- const results = await fylo.executeSQL<_album>(`SELECT * FROM ${ALBUMS} WHERE userId != 1`) as Record<_ttid, _album>
149
-
150
- const albums = Object.values(results)
151
- const hasUserId1 = albums.some(a => a.userId === 1)
152
-
153
- expect(hasUserId1).toBe(false)
154
- expect(albums.length).toBe(90)
155
- })
156
-
157
- test("WHERE LIKE — matches substring pattern", async () => {
158
-
159
- const results = await fylo.executeSQL<_album>(`SELECT * FROM ${ALBUMS} WHERE title LIKE '%quidem%'`) as Record<_ttid, _album>
160
-
161
- const albums = Object.values(results)
162
- const allMatch = albums.every(a => a.title.includes('quidem'))
163
-
164
- expect(allMatch).toBe(true)
165
- expect(albums.length).toBeGreaterThan(0)
166
- })
167
- })