@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.
- package/README.md +141 -62
- package/eslint.config.js +8 -4
- package/package.json +9 -7
- package/src/CLI +16 -14
- package/src/adapters/cipher.ts +12 -6
- package/src/adapters/redis.ts +193 -123
- package/src/adapters/s3.ts +6 -12
- package/src/core/collection.ts +5 -0
- package/src/core/directory.ts +120 -151
- package/src/core/extensions.ts +4 -2
- package/src/core/format.ts +390 -419
- package/src/core/parser.ts +167 -142
- package/src/core/query.ts +31 -26
- package/src/core/walker.ts +68 -61
- package/src/core/write-queue.ts +7 -4
- package/src/engines/s3-files.ts +1068 -0
- package/src/engines/types.ts +21 -0
- package/src/index.ts +754 -378
- package/src/migrate-cli.ts +22 -0
- package/src/migrate.ts +74 -0
- package/src/types/bun-runtime.d.ts +73 -0
- package/src/types/fylo.d.ts +115 -27
- package/src/types/node-runtime.d.ts +61 -0
- package/src/types/query.d.ts +6 -2
- package/src/types/vendor-modules.d.ts +8 -7
- package/src/worker.ts +7 -1
- package/src/workers/write-worker.ts +25 -24
- package/tests/collection/truncate.test.js +35 -0
- package/tests/{data.ts → data.js} +8 -21
- package/tests/{index.ts → index.js} +4 -9
- package/tests/integration/aws-s3-files.canary.test.js +22 -0
- package/tests/integration/{create.test.ts → create.test.js} +13 -31
- package/tests/integration/delete.test.js +95 -0
- package/tests/integration/{edge-cases.test.ts → edge-cases.test.js} +50 -124
- package/tests/integration/{encryption.test.ts → encryption.test.js} +20 -65
- package/tests/integration/{export.test.ts → export.test.js} +8 -23
- package/tests/integration/{join-modes.test.ts → join-modes.test.js} +37 -104
- package/tests/integration/migration.test.js +38 -0
- package/tests/integration/nested.test.js +142 -0
- package/tests/integration/operators.test.js +122 -0
- package/tests/integration/{queue.test.ts → queue.test.js} +24 -40
- package/tests/integration/read.test.js +119 -0
- package/tests/integration/rollback.test.js +60 -0
- package/tests/integration/s3-files.test.js +192 -0
- package/tests/integration/update.test.js +99 -0
- package/tests/mocks/{cipher.ts → cipher.js} +11 -26
- package/tests/mocks/redis.js +123 -0
- package/tests/mocks/{s3.ts → s3.js} +24 -58
- package/tests/schemas/album.json +1 -1
- package/tests/schemas/comment.json +1 -1
- package/tests/schemas/photo.json +1 -1
- package/tests/schemas/post.json +1 -1
- package/tests/schemas/tip.json +1 -1
- package/tests/schemas/todo.json +1 -1
- package/tests/schemas/user.d.ts +12 -12
- package/tests/schemas/user.json +1 -1
- package/tsconfig.json +4 -2
- package/tsconfig.typecheck.json +31 -0
- package/tests/collection/truncate.test.ts +0 -56
- package/tests/integration/delete.test.ts +0 -147
- package/tests/integration/nested.test.ts +0 -212
- package/tests/integration/operators.test.ts +0 -167
- package/tests/integration/read.test.ts +0 -203
- package/tests/integration/rollback.test.ts +0 -105
- package/tests/integration/update.test.ts +0 -130
- package/tests/mocks/redis.ts +0 -169
|
@@ -0,0 +1,142 @@
|
|
|
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
|
+
const USERS = 'nst-user'
|
|
7
|
+
let insertedCount = 0
|
|
8
|
+
let sampleId
|
|
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(USERS)
|
|
14
|
+
try {
|
|
15
|
+
insertedCount = await fylo.importBulkData(USERS, new URL(usersURL))
|
|
16
|
+
} catch {
|
|
17
|
+
await fylo.rollback()
|
|
18
|
+
}
|
|
19
|
+
for await (const data of Fylo.findDocs(USERS, { $limit: 1, $onlyIds: true }).collect()) {
|
|
20
|
+
sampleId = data
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
afterAll(async () => {
|
|
24
|
+
await Fylo.dropCollection(USERS)
|
|
25
|
+
})
|
|
26
|
+
describe('NO-SQL', async () => {
|
|
27
|
+
test('SELECT ALL — nested documents are returned', async () => {
|
|
28
|
+
let results = {}
|
|
29
|
+
for await (const data of Fylo.findDocs(USERS).collect()) {
|
|
30
|
+
results = { ...results, ...data }
|
|
31
|
+
}
|
|
32
|
+
expect(Object.keys(results).length).toBe(insertedCount)
|
|
33
|
+
})
|
|
34
|
+
test('GET ONE — top-level fields are reconstructed correctly', async () => {
|
|
35
|
+
const result = await Fylo.getDoc(USERS, sampleId).once()
|
|
36
|
+
const user = result[sampleId]
|
|
37
|
+
expect(user).toBeDefined()
|
|
38
|
+
expect(typeof user.name).toBe('string')
|
|
39
|
+
expect(typeof user.email).toBe('string')
|
|
40
|
+
expect(typeof user.phone).toBe('string')
|
|
41
|
+
})
|
|
42
|
+
test('GET ONE — first-level nested object is reconstructed correctly', async () => {
|
|
43
|
+
const result = await Fylo.getDoc(USERS, sampleId).once()
|
|
44
|
+
const user = result[sampleId]
|
|
45
|
+
expect(user.address).toBeDefined()
|
|
46
|
+
expect(typeof user.address.city).toBe('string')
|
|
47
|
+
expect(typeof user.address.street).toBe('string')
|
|
48
|
+
expect(typeof user.address.zipcode).toBe('string')
|
|
49
|
+
})
|
|
50
|
+
test('GET ONE — deeply nested object is reconstructed correctly', async () => {
|
|
51
|
+
const result = await Fylo.getDoc(USERS, sampleId).once()
|
|
52
|
+
const user = result[sampleId]
|
|
53
|
+
expect(user.address.geo).toBeDefined()
|
|
54
|
+
expect(typeof user.address.geo.lat).toBe('number')
|
|
55
|
+
expect(typeof user.address.geo.lng).toBe('number')
|
|
56
|
+
})
|
|
57
|
+
test('GET ONE — second nested object is reconstructed correctly', async () => {
|
|
58
|
+
const result = await Fylo.getDoc(USERS, sampleId).once()
|
|
59
|
+
const user = result[sampleId]
|
|
60
|
+
expect(user.company).toBeDefined()
|
|
61
|
+
expect(typeof user.company.name).toBe('string')
|
|
62
|
+
expect(typeof user.company.catchPhrase).toBe('string')
|
|
63
|
+
expect(typeof user.company.bs).toBe('string')
|
|
64
|
+
})
|
|
65
|
+
test('SELECT — nested values are not corrupted across documents', async () => {
|
|
66
|
+
for await (const data of Fylo.findDocs(USERS).collect()) {
|
|
67
|
+
const [, user] = Object.entries(data)[0]
|
|
68
|
+
expect(user.address).toBeDefined()
|
|
69
|
+
expect(user.address.geo).toBeDefined()
|
|
70
|
+
expect(typeof user.address.geo.lat).toBe('number')
|
|
71
|
+
expect(user.company).toBeDefined()
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
test('$select — returns only requested top-level fields', async () => {
|
|
75
|
+
let results = {}
|
|
76
|
+
for await (const data of Fylo.findDocs(USERS, { $select: ['name', 'email'] }).collect()) {
|
|
77
|
+
results = { ...results, ...data }
|
|
78
|
+
}
|
|
79
|
+
const users = Object.values(results)
|
|
80
|
+
const onlyNameAndEmail = users.every((u) => u.name && u.email && !u.phone && !u.address)
|
|
81
|
+
expect(onlyNameAndEmail).toBe(true)
|
|
82
|
+
})
|
|
83
|
+
test('$eq on nested string field — query by city', async () => {
|
|
84
|
+
const result = await Fylo.getDoc(USERS, sampleId).once()
|
|
85
|
+
const targetCity = result[sampleId].address.city
|
|
86
|
+
let results = {}
|
|
87
|
+
for await (const data of Fylo.findDocs(USERS, {
|
|
88
|
+
$ops: [{ ['address/city']: { $eq: targetCity } }]
|
|
89
|
+
}).collect()) {
|
|
90
|
+
results = { ...results, ...data }
|
|
91
|
+
}
|
|
92
|
+
const matchingUsers = Object.values(results)
|
|
93
|
+
const allMatch = matchingUsers.every((u) => u.address.city === targetCity)
|
|
94
|
+
expect(allMatch).toBe(true)
|
|
95
|
+
expect(matchingUsers.length).toBeGreaterThan(0)
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
describe('SQL — dot notation', async () => {
|
|
99
|
+
test('WHERE with dot notation — first-level nested field', async () => {
|
|
100
|
+
const result = await Fylo.getDoc(USERS, sampleId).once()
|
|
101
|
+
const targetCity = result[sampleId].address.city
|
|
102
|
+
const results = await fylo.executeSQL(
|
|
103
|
+
`SELECT * FROM ${USERS} WHERE address.city = '${targetCity}'`
|
|
104
|
+
)
|
|
105
|
+
const users = Object.values(results)
|
|
106
|
+
const allMatch = users.every((u) => u.address.city === targetCity)
|
|
107
|
+
expect(allMatch).toBe(true)
|
|
108
|
+
expect(users.length).toBeGreaterThan(0)
|
|
109
|
+
})
|
|
110
|
+
test('WHERE with dot notation — deeply nested field', async () => {
|
|
111
|
+
const result = await Fylo.getDoc(USERS, sampleId).once()
|
|
112
|
+
const targetLat = result[sampleId].address.geo.lat
|
|
113
|
+
const results = await fylo.executeSQL(
|
|
114
|
+
`SELECT * FROM ${USERS} WHERE address.geo.lat = '${targetLat}'`
|
|
115
|
+
)
|
|
116
|
+
const users = Object.values(results)
|
|
117
|
+
const allMatch = users.every((u) => u.address.geo.lat === targetLat)
|
|
118
|
+
expect(allMatch).toBe(true)
|
|
119
|
+
expect(users.length).toBeGreaterThan(0)
|
|
120
|
+
})
|
|
121
|
+
test('WHERE with dot notation — second nested object', async () => {
|
|
122
|
+
const result = await Fylo.getDoc(USERS, sampleId).once()
|
|
123
|
+
const targetCompany = result[sampleId].company.name
|
|
124
|
+
const results = await fylo.executeSQL(
|
|
125
|
+
`SELECT * FROM ${USERS} WHERE company.name = '${targetCompany}'`
|
|
126
|
+
)
|
|
127
|
+
const users = Object.values(results)
|
|
128
|
+
const allMatch = users.every((u) => u.company.name === targetCompany)
|
|
129
|
+
expect(allMatch).toBe(true)
|
|
130
|
+
expect(users.length).toBeGreaterThan(0)
|
|
131
|
+
})
|
|
132
|
+
test('SELECT with dot notation in WHERE — partial field selection', async () => {
|
|
133
|
+
const result = await Fylo.getDoc(USERS, sampleId).once()
|
|
134
|
+
const targetCity = result[sampleId].address.city
|
|
135
|
+
const results = await fylo.executeSQL(
|
|
136
|
+
`SELECT name, email FROM ${USERS} WHERE address.city = '${targetCity}'`
|
|
137
|
+
)
|
|
138
|
+
const users = Object.values(results)
|
|
139
|
+
expect(users.length).toBeGreaterThan(0)
|
|
140
|
+
expect(users.every((u) => u.name && u.email && !u.phone)).toBe(true)
|
|
141
|
+
})
|
|
142
|
+
})
|
|
@@ -0,0 +1,122 @@
|
|
|
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
|
+
const ALBUMS = 'ops-album'
|
|
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(ALBUMS)
|
|
12
|
+
try {
|
|
13
|
+
await fylo.importBulkData(ALBUMS, new URL(albumURL), 100)
|
|
14
|
+
} catch {
|
|
15
|
+
await fylo.rollback()
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
afterAll(async () => {
|
|
19
|
+
await Fylo.dropCollection(ALBUMS)
|
|
20
|
+
})
|
|
21
|
+
describe('NO-SQL', async () => {
|
|
22
|
+
test('$ne — excludes matching value', async () => {
|
|
23
|
+
let results = {}
|
|
24
|
+
for await (const data of Fylo.findDocs(ALBUMS, {
|
|
25
|
+
$ops: [{ userId: { $ne: 1 } }]
|
|
26
|
+
}).collect()) {
|
|
27
|
+
results = { ...results, ...data }
|
|
28
|
+
}
|
|
29
|
+
const albums = Object.values(results)
|
|
30
|
+
const hasUserId1 = albums.some((a) => a.userId === 1)
|
|
31
|
+
expect(hasUserId1).toBe(false)
|
|
32
|
+
expect(albums.length).toBe(90)
|
|
33
|
+
})
|
|
34
|
+
test('$lt — returns documents where field is less than value', async () => {
|
|
35
|
+
let results = {}
|
|
36
|
+
for await (const data of Fylo.findDocs(ALBUMS, {
|
|
37
|
+
$ops: [{ userId: { $lt: 5 } }]
|
|
38
|
+
}).collect()) {
|
|
39
|
+
results = { ...results, ...data }
|
|
40
|
+
}
|
|
41
|
+
const albums = Object.values(results)
|
|
42
|
+
const allLessThan5 = albums.every((a) => a.userId < 5)
|
|
43
|
+
expect(allLessThan5).toBe(true)
|
|
44
|
+
expect(albums.length).toBe(40)
|
|
45
|
+
})
|
|
46
|
+
test('$lte — returns documents where field is less than or equal to value', async () => {
|
|
47
|
+
let results = {}
|
|
48
|
+
for await (const data of Fylo.findDocs(ALBUMS, {
|
|
49
|
+
$ops: [{ userId: { $lte: 5 } }]
|
|
50
|
+
}).collect()) {
|
|
51
|
+
results = { ...results, ...data }
|
|
52
|
+
}
|
|
53
|
+
const albums = Object.values(results)
|
|
54
|
+
const allLte5 = albums.every((a) => a.userId <= 5)
|
|
55
|
+
expect(allLte5).toBe(true)
|
|
56
|
+
expect(albums.length).toBe(50)
|
|
57
|
+
})
|
|
58
|
+
test('$gt — returns documents where field is greater than value', async () => {
|
|
59
|
+
let results = {}
|
|
60
|
+
for await (const data of Fylo.findDocs(ALBUMS, {
|
|
61
|
+
$ops: [{ userId: { $gt: 5 } }]
|
|
62
|
+
}).collect()) {
|
|
63
|
+
results = { ...results, ...data }
|
|
64
|
+
}
|
|
65
|
+
const albums = Object.values(results)
|
|
66
|
+
const allGt5 = albums.every((a) => a.userId > 5)
|
|
67
|
+
expect(allGt5).toBe(true)
|
|
68
|
+
expect(albums.length).toBe(50)
|
|
69
|
+
})
|
|
70
|
+
test('$gte — returns documents where field is greater than or equal to value', async () => {
|
|
71
|
+
let results = {}
|
|
72
|
+
for await (const data of Fylo.findDocs(ALBUMS, {
|
|
73
|
+
$ops: [{ userId: { $gte: 5 } }]
|
|
74
|
+
}).collect()) {
|
|
75
|
+
results = { ...results, ...data }
|
|
76
|
+
}
|
|
77
|
+
const albums = Object.values(results)
|
|
78
|
+
const allGte5 = albums.every((a) => a.userId >= 5)
|
|
79
|
+
expect(allGte5).toBe(true)
|
|
80
|
+
expect(albums.length).toBe(60)
|
|
81
|
+
})
|
|
82
|
+
test('$like — matches substring pattern', async () => {
|
|
83
|
+
let results = {}
|
|
84
|
+
for await (const data of Fylo.findDocs(ALBUMS, {
|
|
85
|
+
$ops: [{ title: { $like: '%quidem%' } }]
|
|
86
|
+
}).collect()) {
|
|
87
|
+
results = { ...results, ...data }
|
|
88
|
+
}
|
|
89
|
+
const albums = Object.values(results)
|
|
90
|
+
const allMatch = albums.every((a) => a.title.includes('quidem'))
|
|
91
|
+
expect(allMatch).toBe(true)
|
|
92
|
+
expect(albums.length).toBeGreaterThan(0)
|
|
93
|
+
})
|
|
94
|
+
test('$like — prefix pattern', async () => {
|
|
95
|
+
let results = {}
|
|
96
|
+
for await (const data of Fylo.findDocs(ALBUMS, {
|
|
97
|
+
$ops: [{ title: { $like: 'omnis%' } }]
|
|
98
|
+
}).collect()) {
|
|
99
|
+
results = { ...results, ...data }
|
|
100
|
+
}
|
|
101
|
+
const albums = Object.values(results)
|
|
102
|
+
const allStartWith = albums.every((a) => a.title.startsWith('omnis'))
|
|
103
|
+
expect(allStartWith).toBe(true)
|
|
104
|
+
expect(albums.length).toBeGreaterThan(0)
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
describe('SQL', async () => {
|
|
108
|
+
test('WHERE != — excludes matching value', async () => {
|
|
109
|
+
const results = await fylo.executeSQL(`SELECT * FROM ${ALBUMS} WHERE userId != 1`)
|
|
110
|
+
const albums = Object.values(results)
|
|
111
|
+
const hasUserId1 = albums.some((a) => a.userId === 1)
|
|
112
|
+
expect(hasUserId1).toBe(false)
|
|
113
|
+
expect(albums.length).toBe(90)
|
|
114
|
+
})
|
|
115
|
+
test('WHERE LIKE — matches substring pattern', async () => {
|
|
116
|
+
const results = await fylo.executeSQL(`SELECT * FROM ${ALBUMS} WHERE title LIKE '%quidem%'`)
|
|
117
|
+
const albums = Object.values(results)
|
|
118
|
+
const allMatch = albums.every((a) => a.title.includes('quidem'))
|
|
119
|
+
expect(allMatch).toBe(true)
|
|
120
|
+
expect(albums.length).toBeGreaterThan(0)
|
|
121
|
+
})
|
|
122
|
+
})
|
|
@@ -2,98 +2,82 @@ import { test, expect, describe, beforeAll, afterAll, mock } from 'bun:test'
|
|
|
2
2
|
import Fylo from '../../src'
|
|
3
3
|
import S3Mock from '../mocks/s3'
|
|
4
4
|
import RedisMock from '../mocks/redis'
|
|
5
|
-
|
|
6
5
|
const POSTS = 'queued-post'
|
|
7
|
-
|
|
8
6
|
mock.module('../../src/adapters/s3', () => ({ S3: S3Mock }))
|
|
9
7
|
mock.module('../../src/adapters/redis', () => ({ Redis: RedisMock }))
|
|
10
|
-
|
|
11
8
|
const fylo = new Fylo()
|
|
12
|
-
|
|
13
9
|
beforeAll(async () => {
|
|
14
10
|
await Fylo.createCollection(POSTS)
|
|
15
11
|
})
|
|
16
|
-
|
|
17
12
|
afterAll(async () => {
|
|
18
13
|
await Fylo.dropCollection(POSTS)
|
|
19
14
|
})
|
|
20
|
-
|
|
21
15
|
describe('queue writes', () => {
|
|
22
16
|
test('queuePutData enqueues and worker commits insert', async () => {
|
|
23
17
|
const queued = await fylo.queuePutData(POSTS, {
|
|
24
18
|
title: 'Queued Title',
|
|
25
19
|
body: 'Queued Body'
|
|
26
20
|
})
|
|
27
|
-
|
|
28
21
|
const queuedStatus = await fylo.getJobStatus(queued.jobId)
|
|
29
22
|
expect(queuedStatus?.status).toBe('queued')
|
|
30
|
-
|
|
31
23
|
const processed = await fylo.processQueuedWrites(1)
|
|
32
24
|
expect(processed).toBe(1)
|
|
33
|
-
|
|
34
|
-
const doc = await Fylo.getDoc<_post>(POSTS, queued.docId, false).once()
|
|
25
|
+
const doc = await Fylo.getDoc(POSTS, queued.docId, false).once()
|
|
35
26
|
expect(Object.keys(doc).length).toBe(1)
|
|
36
27
|
})
|
|
37
|
-
|
|
38
28
|
test('putData can return immediately when wait is false', async () => {
|
|
39
|
-
const queued = await fylo.putData(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
29
|
+
const queued = await fylo.putData(
|
|
30
|
+
POSTS,
|
|
31
|
+
{
|
|
32
|
+
title: 'Async Title',
|
|
33
|
+
body: 'Async Body'
|
|
34
|
+
},
|
|
35
|
+
{ wait: false }
|
|
36
|
+
)
|
|
44
37
|
expect(typeof queued).toBe('object')
|
|
45
38
|
expect('jobId' in queued).toBe(true)
|
|
46
|
-
|
|
47
|
-
const before = await Fylo.getDoc<_post>(POSTS, queued.docId, false).once()
|
|
39
|
+
const before = await Fylo.getDoc(POSTS, queued.docId, false).once()
|
|
48
40
|
expect(Object.keys(before).length).toBe(0)
|
|
49
|
-
|
|
50
41
|
const processed = await fylo.processQueuedWrites(1)
|
|
51
42
|
expect(processed).toBe(1)
|
|
52
|
-
|
|
53
43
|
const stats = await fylo.getQueueStats()
|
|
54
44
|
expect(stats.deadLetters).toBe(0)
|
|
55
|
-
|
|
56
|
-
const after = await Fylo.getDoc<_post>(POSTS, queued.docId, false).once()
|
|
45
|
+
const after = await Fylo.getDoc(POSTS, queued.docId, false).once()
|
|
57
46
|
expect(Object.keys(after).length).toBe(1)
|
|
58
47
|
})
|
|
59
|
-
|
|
60
48
|
test('failed jobs can be recovered and eventually dead-lettered', async () => {
|
|
61
49
|
const originalExecute = fylo.executeQueuedWrite.bind(fylo)
|
|
62
|
-
|
|
50
|
+
fylo.executeQueuedWrite = async () => {
|
|
63
51
|
throw new Error('simulated write failure')
|
|
64
52
|
}
|
|
65
|
-
|
|
66
53
|
try {
|
|
67
|
-
const queued = await fylo.putData(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
54
|
+
const queued = await fylo.putData(
|
|
55
|
+
POSTS,
|
|
56
|
+
{
|
|
57
|
+
title: 'Broken Title',
|
|
58
|
+
body: 'Broken Body'
|
|
59
|
+
},
|
|
60
|
+
{ wait: false }
|
|
61
|
+
)
|
|
72
62
|
expect(await fylo.processQueuedWrites(1)).toBe(0)
|
|
73
63
|
expect((await fylo.getJobStatus(queued.jobId))?.status).toBe('failed')
|
|
74
|
-
|
|
75
64
|
await Bun.sleep(15)
|
|
76
65
|
expect(await fylo.processQueuedWrites(1, true)).toBe(0)
|
|
77
66
|
expect((await fylo.getJobStatus(queued.jobId))?.status).toBe('failed')
|
|
78
|
-
|
|
79
67
|
await Bun.sleep(25)
|
|
80
68
|
expect(await fylo.processQueuedWrites(1, true)).toBe(0)
|
|
81
69
|
expect((await fylo.getJobStatus(queued.jobId))?.status).toBe('dead-letter')
|
|
82
|
-
|
|
83
70
|
const deadLetters = await fylo.getDeadLetters()
|
|
84
71
|
expect((await fylo.getQueueStats()).deadLetters).toBeGreaterThan(0)
|
|
85
|
-
expect(deadLetters.some(item => item.job.jobId === queued.jobId)).toBe(true)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const replayed = await fylo.replayDeadLetter(deadLetters[0]!.streamId)
|
|
72
|
+
expect(deadLetters.some((item) => item.job.jobId === queued.jobId)).toBe(true)
|
|
73
|
+
fylo.executeQueuedWrite = originalExecute
|
|
74
|
+
const replayed = await fylo.replayDeadLetter(deadLetters[0].streamId)
|
|
90
75
|
expect(replayed?.jobId).toBe(queued.jobId)
|
|
91
76
|
expect((await fylo.getQueueStats()).deadLetters).toBe(0)
|
|
92
|
-
|
|
93
77
|
expect(await fylo.processQueuedWrites(1)).toBe(1)
|
|
94
78
|
expect((await fylo.getJobStatus(queued.jobId))?.status).toBe('committed')
|
|
95
79
|
} finally {
|
|
96
|
-
|
|
80
|
+
fylo.executeQueuedWrite = originalExecute
|
|
97
81
|
}
|
|
98
82
|
})
|
|
99
83
|
})
|
|
@@ -0,0 +1,119 @@
|
|
|
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 count = 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 Promise.all([Fylo.createCollection(ALBUMS), fylo.executeSQL(`CREATE TABLE ${POSTS}`)])
|
|
14
|
+
try {
|
|
15
|
+
count = await fylo.importBulkData(ALBUMS, new URL(albumURL), 100)
|
|
16
|
+
await fylo.importBulkData(POSTS, new URL(postsURL), 100)
|
|
17
|
+
} catch {
|
|
18
|
+
await fylo.rollback()
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
afterAll(async () => {
|
|
22
|
+
await Promise.all([Fylo.dropCollection(ALBUMS), Fylo.dropCollection(POSTS)])
|
|
23
|
+
})
|
|
24
|
+
describe('NO-SQL', async () => {
|
|
25
|
+
test('SELECT ALL', async () => {
|
|
26
|
+
let results = {}
|
|
27
|
+
for await (const data of Fylo.findDocs(ALBUMS).collect()) {
|
|
28
|
+
results = { ...results, ...data }
|
|
29
|
+
}
|
|
30
|
+
expect(Object.keys(results).length).toBe(count)
|
|
31
|
+
})
|
|
32
|
+
test('SELECT PARTIAL', async () => {
|
|
33
|
+
let results = {}
|
|
34
|
+
for await (const data of Fylo.findDocs(ALBUMS, { $select: ['title'] }).collect()) {
|
|
35
|
+
results = { ...results, ...data }
|
|
36
|
+
}
|
|
37
|
+
const allAlbums = Object.values(results)
|
|
38
|
+
const onlyTtitle = allAlbums.every((user) => user.title && !user.userId)
|
|
39
|
+
expect(onlyTtitle).toBe(true)
|
|
40
|
+
})
|
|
41
|
+
test('GET ONE', async () => {
|
|
42
|
+
const ids = []
|
|
43
|
+
for await (const data of Fylo.findDocs(ALBUMS, { $limit: 1, $onlyIds: true }).collect()) {
|
|
44
|
+
ids.push(data)
|
|
45
|
+
}
|
|
46
|
+
const result = await Fylo.getDoc(ALBUMS, ids[0]).once()
|
|
47
|
+
const _id = Object.keys(result).shift()
|
|
48
|
+
expect(ids[0]).toEqual(_id)
|
|
49
|
+
})
|
|
50
|
+
test('SELECT CLAUSE', async () => {
|
|
51
|
+
let results = {}
|
|
52
|
+
for await (const data of Fylo.findDocs(ALBUMS, {
|
|
53
|
+
$ops: [{ userId: { $eq: 2 } }]
|
|
54
|
+
}).collect()) {
|
|
55
|
+
results = { ...results, ...data }
|
|
56
|
+
}
|
|
57
|
+
const allAlbums = Object.values(results)
|
|
58
|
+
const onlyUserId = allAlbums.every((user) => user.userId === 2)
|
|
59
|
+
expect(onlyUserId).toBe(true)
|
|
60
|
+
})
|
|
61
|
+
test('SELECT LIMIT', async () => {
|
|
62
|
+
let results = {}
|
|
63
|
+
for await (const data of Fylo.findDocs(ALBUMS, { $limit: 5 }).collect()) {
|
|
64
|
+
results = { ...results, ...data }
|
|
65
|
+
}
|
|
66
|
+
expect(Object.keys(results).length).toBe(5)
|
|
67
|
+
})
|
|
68
|
+
test('SELECT GROUP BY', async () => {
|
|
69
|
+
let results = {}
|
|
70
|
+
for await (const data of Fylo.findDocs(ALBUMS, {
|
|
71
|
+
$groupby: 'userId',
|
|
72
|
+
$onlyIds: true
|
|
73
|
+
}).collect()) {
|
|
74
|
+
results = Object.appendGroup(results, data)
|
|
75
|
+
}
|
|
76
|
+
expect(Object.keys(results).length).toBeGreaterThan(0)
|
|
77
|
+
})
|
|
78
|
+
test('SELECT JOIN', async () => {
|
|
79
|
+
const results = await Fylo.joinDocs({
|
|
80
|
+
$leftCollection: ALBUMS,
|
|
81
|
+
$rightCollection: POSTS,
|
|
82
|
+
$mode: 'inner',
|
|
83
|
+
$on: { userId: { $eq: 'id' } }
|
|
84
|
+
})
|
|
85
|
+
expect(Object.keys(results).length).toBeGreaterThan(0)
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
describe('SQL', async () => {
|
|
89
|
+
test('SELECT PARTIAL', async () => {
|
|
90
|
+
const results = await fylo.executeSQL(`SELECT title FROM ${ALBUMS}`)
|
|
91
|
+
const allAlbums = Object.values(results)
|
|
92
|
+
const onlyTtitle = allAlbums.every((user) => user.title && !user.userId)
|
|
93
|
+
expect(onlyTtitle).toBe(true)
|
|
94
|
+
})
|
|
95
|
+
test('SELECT CLAUSE', async () => {
|
|
96
|
+
const results = await fylo.executeSQL(`SELECT * FROM ${ALBUMS} WHERE user_id = 2`)
|
|
97
|
+
const allAlbums = Object.values(results)
|
|
98
|
+
const onlyUserId = allAlbums.every((user) => user.userId === 2)
|
|
99
|
+
expect(onlyUserId).toBe(true)
|
|
100
|
+
})
|
|
101
|
+
test('SELECT ALL', async () => {
|
|
102
|
+
const results = await fylo.executeSQL(`SELECT * FROM ${ALBUMS}`)
|
|
103
|
+
expect(Object.keys(results).length).toBe(count)
|
|
104
|
+
})
|
|
105
|
+
test('SELECT LIMIT', async () => {
|
|
106
|
+
const results = await fylo.executeSQL(`SELECT * FROM ${ALBUMS} LIMIT 5`)
|
|
107
|
+
expect(Object.keys(results).length).toBe(5)
|
|
108
|
+
})
|
|
109
|
+
test('SELECT GROUP BY', async () => {
|
|
110
|
+
const results = await fylo.executeSQL(`SELECT * FROM ${ALBUMS} GROUP BY userId`)
|
|
111
|
+
expect(Object.keys(results).length).toBeGreaterThan(0)
|
|
112
|
+
})
|
|
113
|
+
test('SELECT JOIN', async () => {
|
|
114
|
+
const results = await fylo.executeSQL(
|
|
115
|
+
`SELECT * FROM ${ALBUMS} INNER JOIN ${POSTS} ON userId = id`
|
|
116
|
+
)
|
|
117
|
+
expect(Object.keys(results).length).toBeGreaterThan(0)
|
|
118
|
+
})
|
|
119
|
+
})
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
const POSTS = 'rb-post'
|
|
6
|
+
const fylo = new Fylo()
|
|
7
|
+
mock.module('../../src/adapters/s3', () => ({ S3: S3Mock }))
|
|
8
|
+
mock.module('../../src/adapters/redis', () => ({ Redis: RedisMock }))
|
|
9
|
+
beforeAll(async () => {
|
|
10
|
+
await Fylo.createCollection(POSTS)
|
|
11
|
+
})
|
|
12
|
+
afterAll(async () => {
|
|
13
|
+
await Fylo.dropCollection(POSTS)
|
|
14
|
+
})
|
|
15
|
+
describe('NO-SQL', () => {
|
|
16
|
+
test('INSERT then rollback — document is not retrievable', async () => {
|
|
17
|
+
const _id = await fylo.putData(POSTS, {
|
|
18
|
+
userId: 99,
|
|
19
|
+
id: 9001,
|
|
20
|
+
title: 'Rollback Me',
|
|
21
|
+
body: 'This document should disappear after rollback'
|
|
22
|
+
})
|
|
23
|
+
const before = await Fylo.getDoc(POSTS, _id).once()
|
|
24
|
+
expect(Object.keys(before).length).toBe(1)
|
|
25
|
+
await fylo.rollback()
|
|
26
|
+
const after = await Fylo.getDoc(POSTS, _id).once()
|
|
27
|
+
expect(Object.keys(after).length).toBe(0)
|
|
28
|
+
})
|
|
29
|
+
test('DELETE then rollback — document is restored', async () => {
|
|
30
|
+
const freshFylo = new Fylo()
|
|
31
|
+
const _id = await freshFylo.putData(POSTS, {
|
|
32
|
+
userId: 99,
|
|
33
|
+
id: 9002,
|
|
34
|
+
title: 'Restore Me',
|
|
35
|
+
body: 'This document should reappear after delete rollback'
|
|
36
|
+
})
|
|
37
|
+
const deleteInstance = new Fylo()
|
|
38
|
+
await deleteInstance.delDoc(POSTS, _id)
|
|
39
|
+
const after = await Fylo.getDoc(POSTS, _id).once()
|
|
40
|
+
expect(Object.keys(after).length).toBe(0)
|
|
41
|
+
await deleteInstance.rollback()
|
|
42
|
+
const restored = await Fylo.getDoc(POSTS, _id).once()
|
|
43
|
+
expect(Object.keys(restored).length).toBe(1)
|
|
44
|
+
expect(restored[_id].title).toBe('Restore Me')
|
|
45
|
+
})
|
|
46
|
+
test('batch INSERT then rollback — all documents are removed', async () => {
|
|
47
|
+
const batchFylo = new Fylo()
|
|
48
|
+
const batch = [
|
|
49
|
+
{ userId: 98, id: 9003, title: 'Batch A', body: 'body a' },
|
|
50
|
+
{ userId: 98, id: 9004, title: 'Batch B', body: 'body b' },
|
|
51
|
+
{ userId: 98, id: 9005, title: 'Batch C', body: 'body c' }
|
|
52
|
+
]
|
|
53
|
+
const ids = await batchFylo.batchPutData(POSTS, batch)
|
|
54
|
+
const beforeResults = await Promise.all(ids.map((id) => Fylo.getDoc(POSTS, id).once()))
|
|
55
|
+
expect(beforeResults.every((r) => Object.keys(r).length === 1)).toBe(true)
|
|
56
|
+
await batchFylo.rollback()
|
|
57
|
+
const afterResults = await Promise.all(ids.map((id) => Fylo.getDoc(POSTS, id).once()))
|
|
58
|
+
expect(afterResults.every((r) => Object.keys(r).length === 0)).toBe(true)
|
|
59
|
+
})
|
|
60
|
+
})
|