@delma/fylo 1.1.1 → 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.
- package/.github/copilot-instructions.md +1 -1
- package/.github/prompts/release.prompt.md +4 -43
- package/AGENTS.md +1 -1
- package/CLAUDE.md +1 -1
- 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 +888 -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 +108 -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/.github/prompts/issue.prompt.md +0 -19
- package/.github/prompts/pr.prompt.md +0 -18
- package/.github/prompts/review-pr.prompt.md +0 -19
- package/.github/prompts/sync-main.prompt.md +0 -14
- 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
|
@@ -1,203 +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
|
-
|
|
7
|
-
const POSTS = `post`
|
|
8
|
-
const ALBUMS = `album`
|
|
9
|
-
|
|
10
|
-
let count = 0
|
|
11
|
-
|
|
12
|
-
const fylo = new Fylo()
|
|
13
|
-
|
|
14
|
-
mock.module('../../src/adapters/s3', () => ({ S3: S3Mock }))
|
|
15
|
-
mock.module('../../src/adapters/redis', () => ({ Redis: RedisMock }))
|
|
16
|
-
|
|
17
|
-
beforeAll(async () => {
|
|
18
|
-
|
|
19
|
-
await Promise.all([Fylo.createCollection(ALBUMS), fylo.executeSQL<_post>(`CREATE TABLE ${POSTS}`)])
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
count = await fylo.importBulkData<_album>(ALBUMS, new URL(albumURL), 100)
|
|
23
|
-
await fylo.importBulkData<_post>(POSTS, new URL(postsURL), 100)
|
|
24
|
-
} catch {
|
|
25
|
-
await fylo.rollback()
|
|
26
|
-
}
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
afterAll(async () => {
|
|
30
|
-
await Promise.all([Fylo.dropCollection(ALBUMS), Fylo.dropCollection(POSTS)])
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
describe("NO-SQL", async () => {
|
|
34
|
-
|
|
35
|
-
test("SELECT ALL", async () => {
|
|
36
|
-
|
|
37
|
-
let results: Record<_ttid, _album> = {}
|
|
38
|
-
|
|
39
|
-
for await (const data of Fylo.findDocs<_album>(ALBUMS).collect()) {
|
|
40
|
-
|
|
41
|
-
results = { ...results, ... data as Record<_ttid, _album> }
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
//console.format(results)
|
|
45
|
-
|
|
46
|
-
expect(Object.keys(results).length).toBe(count)
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
test("SELECT PARTIAL", async () => {
|
|
50
|
-
|
|
51
|
-
let results: Record<_ttid, _album> = {}
|
|
52
|
-
|
|
53
|
-
for await (const data of Fylo.findDocs<_album>(ALBUMS, { $select: ["title"] }).collect()) {
|
|
54
|
-
|
|
55
|
-
results = { ...results, ... data as Record<_ttid, _album> }
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
//console.format(results)
|
|
59
|
-
|
|
60
|
-
const allAlbums = Object.values(results)
|
|
61
|
-
|
|
62
|
-
const onlyTtitle = allAlbums.every(user => user.title && !user.userId)
|
|
63
|
-
|
|
64
|
-
expect(onlyTtitle).toBe(true)
|
|
65
|
-
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
test("GET ONE", async () => {
|
|
69
|
-
|
|
70
|
-
const ids: _ttid[] = []
|
|
71
|
-
|
|
72
|
-
for await (const data of Fylo.findDocs<_album>(ALBUMS, { $limit: 1, $onlyIds: true }).collect()) {
|
|
73
|
-
|
|
74
|
-
ids.push(data as _ttid)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const result = await Fylo.getDoc<_album>(ALBUMS, ids[0]).once()
|
|
78
|
-
|
|
79
|
-
const _id = Object.keys(result).shift()!
|
|
80
|
-
|
|
81
|
-
expect(ids[0]).toEqual(_id)
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
test("SELECT CLAUSE", async () => {
|
|
85
|
-
|
|
86
|
-
let results: Record<_ttid, _album> = {}
|
|
87
|
-
|
|
88
|
-
for await (const data of Fylo.findDocs<_album>(ALBUMS, { $ops: [{ userId: { $eq: 2 } }] }).collect()) {
|
|
89
|
-
|
|
90
|
-
results = { ...results, ...data as Record<_ttid, _album> }
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
//console.format(results)
|
|
94
|
-
|
|
95
|
-
const allAlbums = Object.values(results)
|
|
96
|
-
|
|
97
|
-
const onlyUserId = allAlbums.every(user => user.userId === 2)
|
|
98
|
-
|
|
99
|
-
expect(onlyUserId).toBe(true)
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
test("SELECT LIMIT", async () => {
|
|
103
|
-
|
|
104
|
-
let results: Record<_ttid, _album> = {}
|
|
105
|
-
|
|
106
|
-
for await (const data of Fylo.findDocs<_album>(ALBUMS, { $limit: 5 }).collect()) {
|
|
107
|
-
|
|
108
|
-
results = { ...results, ...data as Record<_ttid, _album> }
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
//console.format(results)
|
|
112
|
-
|
|
113
|
-
expect(Object.keys(results).length).toBe(5)
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
test("SELECT GROUP BY", async () => {
|
|
117
|
-
|
|
118
|
-
let results: Record<_album[keyof _album], Record<_ttid, Partial<_album>>> = {} as Record<_album[keyof _album], Record<_ttid, Partial<_album>>>
|
|
119
|
-
|
|
120
|
-
for await (const data of Fylo.findDocs<_album>(ALBUMS, { $groupby: "userId", $onlyIds: true }).collect()) {
|
|
121
|
-
|
|
122
|
-
results = Object.appendGroup(results, (data as unknown as Record<string, Record<string, Record<_ttid, null>>>))
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
//console.format(results)
|
|
126
|
-
|
|
127
|
-
expect(Object.keys(results).length).toBeGreaterThan(0)
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
test("SELECT JOIN", async () => {
|
|
131
|
-
|
|
132
|
-
const results = await Fylo.joinDocs<_album, _post>({ $leftCollection: ALBUMS, $rightCollection: POSTS, $mode: "inner", $on: { "userId": { $eq: "id" } } }) as Record<`${_ttid}, ${_ttid}`, _album | _post>
|
|
133
|
-
|
|
134
|
-
//console.format(results)
|
|
135
|
-
|
|
136
|
-
expect(Object.keys(results).length).toBeGreaterThan(0)
|
|
137
|
-
})
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
describe("SQL", async () => {
|
|
141
|
-
|
|
142
|
-
test("SELECT PARTIAL", async () => {
|
|
143
|
-
|
|
144
|
-
const results = await fylo.executeSQL<_album>(`SELECT title FROM ${ALBUMS}`) as Record<_ttid, _album>
|
|
145
|
-
|
|
146
|
-
//console.format(results)
|
|
147
|
-
|
|
148
|
-
const allAlbums = Object.values(results)
|
|
149
|
-
|
|
150
|
-
const onlyTtitle = allAlbums.every(user => user.title && !user.userId)
|
|
151
|
-
|
|
152
|
-
expect(onlyTtitle).toBe(true)
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
test("SELECT CLAUSE", async () => {
|
|
156
|
-
|
|
157
|
-
const results = await fylo.executeSQL<_album>(`SELECT * FROM ${ALBUMS} WHERE user_id = 2`) as Record<_ttid, _album>
|
|
158
|
-
|
|
159
|
-
//console.format(results)
|
|
160
|
-
|
|
161
|
-
const allAlbums = Object.values(results)
|
|
162
|
-
|
|
163
|
-
const onlyUserId = allAlbums.every(user => user.userId === 2)
|
|
164
|
-
|
|
165
|
-
expect(onlyUserId).toBe(true)
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
test("SELECT ALL", async () => {
|
|
169
|
-
|
|
170
|
-
const results = await fylo.executeSQL<_album>(`SELECT * FROM ${ALBUMS}`) as Record<_ttid, _album>
|
|
171
|
-
|
|
172
|
-
//console.format(results)
|
|
173
|
-
|
|
174
|
-
expect(Object.keys(results).length).toBe(count)
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
test("SELECT LIMIT", async () => {
|
|
178
|
-
|
|
179
|
-
const results = await fylo.executeSQL<_album>(`SELECT * FROM ${ALBUMS} LIMIT 5`) as Record<_ttid, _album>
|
|
180
|
-
|
|
181
|
-
//console.format(results)
|
|
182
|
-
|
|
183
|
-
expect(Object.keys(results).length).toBe(5)
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
test("SELECT GROUP BY", async () => {
|
|
187
|
-
|
|
188
|
-
const results = await fylo.executeSQL<_album>(`SELECT * FROM ${ALBUMS} GROUP BY userId`) as unknown as Record<string, Record<keyof _album, Record<_album[keyof _album], _album>>>
|
|
189
|
-
|
|
190
|
-
//console.format(results)
|
|
191
|
-
|
|
192
|
-
expect(Object.keys(results).length).toBeGreaterThan(0)
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
test("SELECT JOIN", async () => {
|
|
196
|
-
|
|
197
|
-
const results = await fylo.executeSQL<_album>(`SELECT * FROM ${ALBUMS} INNER JOIN ${POSTS} ON userId = id`) as Record<`${_ttid}, ${_ttid}`, _album | _post>
|
|
198
|
-
|
|
199
|
-
//console.format(results)
|
|
200
|
-
|
|
201
|
-
expect(Object.keys(results).length).toBeGreaterThan(0)
|
|
202
|
-
})
|
|
203
|
-
})
|
|
@@ -1,105 +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
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Rollback mechanism:
|
|
8
|
-
* - putKeys() pushes S3.delete ops into the transactions stack
|
|
9
|
-
* - deleteKeys() pushes S3.put ops (restore) into the stack
|
|
10
|
-
* - executeRollback() pops and executes them in reverse order
|
|
11
|
-
*
|
|
12
|
-
* After rollback, the written data should no longer be retrievable.
|
|
13
|
-
* After a delete rollback, the deleted data should be restored.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
const POSTS = 'rb-post'
|
|
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(POSTS)
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
afterAll(async () => {
|
|
28
|
-
await Fylo.dropCollection(POSTS)
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
describe("NO-SQL", () => {
|
|
32
|
-
|
|
33
|
-
test("INSERT then rollback — document is not retrievable", async () => {
|
|
34
|
-
|
|
35
|
-
const _id = await fylo.putData<_post>(POSTS, {
|
|
36
|
-
userId: 99,
|
|
37
|
-
id: 9001,
|
|
38
|
-
title: 'Rollback Me',
|
|
39
|
-
body: 'This document should disappear after rollback'
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
// Verify the document was actually written
|
|
43
|
-
const before = await Fylo.getDoc<_post>(POSTS, _id).once()
|
|
44
|
-
expect(Object.keys(before).length).toBe(1)
|
|
45
|
-
|
|
46
|
-
// Rollback undoes all writes made on this Fylo instance
|
|
47
|
-
await fylo.rollback()
|
|
48
|
-
|
|
49
|
-
const after = await Fylo.getDoc<_post>(POSTS, _id).once()
|
|
50
|
-
expect(Object.keys(after).length).toBe(0)
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
test("DELETE then rollback — document is restored", async () => {
|
|
54
|
-
|
|
55
|
-
// Use a fresh Fylo instance so the transactions stack is clean
|
|
56
|
-
const freshFylo = new Fylo()
|
|
57
|
-
|
|
58
|
-
const _id = await freshFylo.putData<_post>(POSTS, {
|
|
59
|
-
userId: 99,
|
|
60
|
-
id: 9002,
|
|
61
|
-
title: 'Restore Me',
|
|
62
|
-
body: 'This document should reappear after delete rollback'
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
// Clear the insert transactions so rollback only covers the delete
|
|
66
|
-
// (call rollback would delete the just-written doc, defeating the purpose)
|
|
67
|
-
// Instead we use a second instance that has a clean transaction stack
|
|
68
|
-
const deleteInstance = new Fylo()
|
|
69
|
-
|
|
70
|
-
await deleteInstance.delDoc(POSTS, _id)
|
|
71
|
-
|
|
72
|
-
// Confirm it's gone
|
|
73
|
-
const after = await Fylo.getDoc<_post>(POSTS, _id).once()
|
|
74
|
-
expect(Object.keys(after).length).toBe(0)
|
|
75
|
-
|
|
76
|
-
// Rollback the delete — should restore the document
|
|
77
|
-
await deleteInstance.rollback()
|
|
78
|
-
|
|
79
|
-
const restored = await Fylo.getDoc<_post>(POSTS, _id).once()
|
|
80
|
-
expect(Object.keys(restored).length).toBe(1)
|
|
81
|
-
expect(restored[_id].title).toBe('Restore Me')
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
test("batch INSERT then rollback — all documents are removed", async () => {
|
|
85
|
-
|
|
86
|
-
const batchFylo = new Fylo()
|
|
87
|
-
|
|
88
|
-
const batch: _post[] = [
|
|
89
|
-
{ userId: 98, id: 9003, title: 'Batch A', body: 'body a' },
|
|
90
|
-
{ userId: 98, id: 9004, title: 'Batch B', body: 'body b' },
|
|
91
|
-
{ userId: 98, id: 9005, title: 'Batch C', body: 'body c' }
|
|
92
|
-
]
|
|
93
|
-
|
|
94
|
-
const ids = await batchFylo.batchPutData<_post>(POSTS, batch)
|
|
95
|
-
|
|
96
|
-
// Confirm all written
|
|
97
|
-
const beforeResults = await Promise.all(ids.map(id => Fylo.getDoc<_post>(POSTS, id).once()))
|
|
98
|
-
expect(beforeResults.every(r => Object.keys(r).length === 1)).toBe(true)
|
|
99
|
-
|
|
100
|
-
await batchFylo.rollback()
|
|
101
|
-
|
|
102
|
-
const afterResults = await Promise.all(ids.map(id => Fylo.getDoc<_post>(POSTS, id).once()))
|
|
103
|
-
expect(afterResults.every(r => Object.keys(r).length === 0)).toBe(true)
|
|
104
|
-
})
|
|
105
|
-
})
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { test, expect, describe, beforeAll, afterAll, mock } from 'bun:test'
|
|
2
|
-
import Fylo from '../../src'
|
|
3
|
-
import { photosURL, todosURL } from '../data'
|
|
4
|
-
import S3Mock from '../mocks/s3'
|
|
5
|
-
import RedisMock from '../mocks/redis'
|
|
6
|
-
|
|
7
|
-
const PHOTOS = `photo`
|
|
8
|
-
const TODOS = `todo`
|
|
9
|
-
|
|
10
|
-
const fylo = new Fylo()
|
|
11
|
-
|
|
12
|
-
mock.module('../../src/adapters/s3', () => ({ S3: S3Mock }))
|
|
13
|
-
mock.module('../../src/adapters/redis', () => ({ Redis: RedisMock }))
|
|
14
|
-
|
|
15
|
-
beforeAll(async () => {
|
|
16
|
-
|
|
17
|
-
await Promise.all([Fylo.createCollection(PHOTOS), fylo.executeSQL<_todo>(`CREATE TABLE ${TODOS}`)])
|
|
18
|
-
|
|
19
|
-
try {
|
|
20
|
-
await fylo.importBulkData<_photo>(PHOTOS, new URL(photosURL), 100)
|
|
21
|
-
await fylo.importBulkData<_todo>(TODOS, new URL(todosURL), 100)
|
|
22
|
-
} catch {
|
|
23
|
-
await fylo.rollback()
|
|
24
|
-
}
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
afterAll(async () => {
|
|
28
|
-
await Promise.all([Fylo.dropCollection(PHOTOS), fylo.executeSQL<_todo>(`DROP TABLE ${TODOS}`)])
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
describe("NO-SQL", async () => {
|
|
32
|
-
|
|
33
|
-
test("UPDATE ONE", async () => {
|
|
34
|
-
|
|
35
|
-
const ids: _ttid[] = []
|
|
36
|
-
|
|
37
|
-
for await (const data of Fylo.findDocs<_photo>(PHOTOS, { $limit: 1, $onlyIds: true }).collect()) {
|
|
38
|
-
|
|
39
|
-
ids.push(data as _ttid)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
try {
|
|
43
|
-
await fylo.patchDoc<_photo>(PHOTOS, { [ids.shift() as _ttid]: { title: "All Mighty" }})
|
|
44
|
-
} catch {
|
|
45
|
-
await fylo.rollback()
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
let results: Record<_ttid, _photo> = {}
|
|
49
|
-
|
|
50
|
-
for await (const data of Fylo.findDocs<_photo>(PHOTOS, { $ops: [{ title: { $eq: "All Mighty" } }]}).collect()) {
|
|
51
|
-
|
|
52
|
-
results = { ...results, ...data as Record<_ttid, _photo> }
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
expect(Object.keys(results).length).toBe(1)
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
test("UPDATE CLAUSE", async () => {
|
|
59
|
-
|
|
60
|
-
let count = -1
|
|
61
|
-
|
|
62
|
-
try {
|
|
63
|
-
count = await fylo.patchDocs<_photo>(PHOTOS, { $set: { title: "All Mighti" }, $where: { $ops: [{ title: { $like: "%est%" } }] } })
|
|
64
|
-
} catch {
|
|
65
|
-
await fylo.rollback()
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
let results: Record<_ttid, _photo> = {}
|
|
69
|
-
|
|
70
|
-
for await (const data of Fylo.findDocs<_photo>(PHOTOS, { $ops: [ { title: { $eq: "All Mighti" } } ] }).collect()) {
|
|
71
|
-
|
|
72
|
-
results = { ...results, ...data as Record<_ttid, _photo> }
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
expect(Object.keys(results).length).toBe(count)
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
test("UPDATE ALL", async () => {
|
|
79
|
-
|
|
80
|
-
let count = -1
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
count = await fylo.patchDocs<_photo>(PHOTOS, { $set: { title: "All Mighter" } })
|
|
84
|
-
} catch {
|
|
85
|
-
await fylo.rollback()
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
let results: Record<_ttid, _photo> = {}
|
|
89
|
-
|
|
90
|
-
for await (const data of Fylo.findDocs<_photo>(PHOTOS, { $ops: [ { title: { $eq: "All Mighter" } } ] }).collect()) {
|
|
91
|
-
|
|
92
|
-
results = { ...results, ...data as Record<_ttid, _photo> }
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
expect(Object.keys(results).length).toBe(count)
|
|
96
|
-
}, 20000)
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
describe("SQL", async () => {
|
|
100
|
-
|
|
101
|
-
test("UPDATE CLAUSE", async () => {
|
|
102
|
-
|
|
103
|
-
let count = -1
|
|
104
|
-
|
|
105
|
-
try {
|
|
106
|
-
count = await fylo.executeSQL<_todo>(`UPDATE ${TODOS} SET title = 'All Mighty' WHERE title LIKE '%est%'`) as number
|
|
107
|
-
} catch {
|
|
108
|
-
await fylo.rollback()
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const results = await fylo.executeSQL<_todo>(`SELECT * FROM ${TODOS} WHERE title = 'All Mighty'`) as Record<_ttid, _todo>
|
|
112
|
-
|
|
113
|
-
expect(Object.keys(results).length).toBe(count)
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
test("UPDATE ALL", async () => {
|
|
117
|
-
|
|
118
|
-
let count = -1
|
|
119
|
-
|
|
120
|
-
try {
|
|
121
|
-
count = await fylo.executeSQL<_todo>(`UPDATE ${TODOS} SET title = 'All Mightier'`) as number
|
|
122
|
-
} catch {
|
|
123
|
-
await fylo.rollback()
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const results = await fylo.executeSQL<_todo>(`SELECT * FROM ${TODOS} WHERE title = 'All Mightier'`) as Record<_ttid, _todo>
|
|
127
|
-
|
|
128
|
-
expect(Object.keys(results).length).toBe(count)
|
|
129
|
-
}, 20000)
|
|
130
|
-
})
|
package/tests/mocks/redis.ts
DELETED
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* No-op Redis mock. All methods are silent no-ops so tests never need a
|
|
3
|
-
* running Redis instance. subscribe yields nothing so listener code paths
|
|
4
|
-
* simply exit immediately.
|
|
5
|
-
*/
|
|
6
|
-
export default class RedisMock {
|
|
7
|
-
|
|
8
|
-
private static stream: Array<{
|
|
9
|
-
streamId: string
|
|
10
|
-
jobId: string
|
|
11
|
-
collection: string
|
|
12
|
-
docId: _ttid
|
|
13
|
-
operation: string
|
|
14
|
-
claimedBy?: string
|
|
15
|
-
}> = []
|
|
16
|
-
|
|
17
|
-
private static jobs = new Map<string, Record<string, any>>()
|
|
18
|
-
|
|
19
|
-
private static docs = new Map<string, Record<string, string>>()
|
|
20
|
-
|
|
21
|
-
private static locks = new Map<string, string>()
|
|
22
|
-
|
|
23
|
-
private static deadLetters: Array<{ streamId: string, jobId: string, reason?: string, failedAt: number }> = []
|
|
24
|
-
|
|
25
|
-
private static nextId = 0
|
|
26
|
-
|
|
27
|
-
async publish(_collection: string, _action: 'insert' | 'delete', _keyId: string | _ttid): Promise<void> {}
|
|
28
|
-
|
|
29
|
-
async claimTTID(_id: _ttid, _ttlSeconds: number = 10): Promise<boolean> { return true }
|
|
30
|
-
|
|
31
|
-
async enqueueWrite(job: Record<string, any>) {
|
|
32
|
-
RedisMock.jobs.set(job.jobId, { ...job, nextAttemptAt: job.nextAttemptAt ?? Date.now() })
|
|
33
|
-
RedisMock.docs.set(`fylo:doc:${job.collection}:${job.docId}`, {
|
|
34
|
-
status: 'queued',
|
|
35
|
-
lastJobId: job.jobId,
|
|
36
|
-
updatedAt: String(Date.now())
|
|
37
|
-
})
|
|
38
|
-
const streamId = String(++RedisMock.nextId)
|
|
39
|
-
RedisMock.stream.push({
|
|
40
|
-
streamId,
|
|
41
|
-
jobId: job.jobId,
|
|
42
|
-
collection: job.collection,
|
|
43
|
-
docId: job.docId,
|
|
44
|
-
operation: job.operation
|
|
45
|
-
})
|
|
46
|
-
return streamId
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async readWriteJobs(workerId: string, count: number = 1): Promise<Array<{ streamId: string, job: Record<string, any> }>> {
|
|
50
|
-
const available = RedisMock.stream
|
|
51
|
-
.filter(entry => !entry.claimedBy)
|
|
52
|
-
.slice(0, count)
|
|
53
|
-
|
|
54
|
-
for(const entry of available) entry.claimedBy = workerId
|
|
55
|
-
|
|
56
|
-
return available.map(entry => ({
|
|
57
|
-
streamId: entry.streamId,
|
|
58
|
-
job: { ...RedisMock.jobs.get(entry.jobId)! }
|
|
59
|
-
}))
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async ackWriteJob(streamId: string) {
|
|
63
|
-
RedisMock.stream = RedisMock.stream.filter(item => item.streamId !== streamId)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async deadLetterWriteJob(streamId: string, job: Record<string, any>, reason?: string) {
|
|
67
|
-
RedisMock.deadLetters.push({
|
|
68
|
-
streamId: String(RedisMock.deadLetters.length + 1),
|
|
69
|
-
jobId: job.jobId,
|
|
70
|
-
reason,
|
|
71
|
-
failedAt: Date.now()
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
await this.ackWriteJob(streamId)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async claimPendingJobs(workerId: string, _minIdleMs: number = 30_000, count: number = 10) {
|
|
78
|
-
const pending = RedisMock.stream
|
|
79
|
-
.filter(entry => entry.claimedBy)
|
|
80
|
-
.slice(0, count)
|
|
81
|
-
|
|
82
|
-
for(const entry of pending) entry.claimedBy = workerId
|
|
83
|
-
|
|
84
|
-
return pending.map(entry => ({
|
|
85
|
-
streamId: entry.streamId,
|
|
86
|
-
job: { ...RedisMock.jobs.get(entry.jobId)! }
|
|
87
|
-
}))
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async setJobStatus(jobId: string, status: string, extra: Record<string, any> = {}) {
|
|
91
|
-
const job = RedisMock.jobs.get(jobId)
|
|
92
|
-
if(job) Object.assign(job, extra, { status, updatedAt: Date.now() })
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
async setDocStatus(collection: string, docId: _ttid, status: string, jobId?: string) {
|
|
96
|
-
const key = `fylo:doc:${collection}:${docId}`
|
|
97
|
-
const curr = RedisMock.docs.get(key) ?? {}
|
|
98
|
-
RedisMock.docs.set(key, {
|
|
99
|
-
...curr,
|
|
100
|
-
status,
|
|
101
|
-
updatedAt: String(Date.now()),
|
|
102
|
-
...(jobId ? { lastJobId: jobId } : {})
|
|
103
|
-
})
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
async getJob(jobId: string): Promise<Record<string, any> | null> {
|
|
107
|
-
const job = RedisMock.jobs.get(jobId)
|
|
108
|
-
return job ? { ...job } : null
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
async getDocStatus(collection: string, docId: _ttid): Promise<Record<string, string> | null> {
|
|
112
|
-
return RedisMock.docs.get(`fylo:doc:${collection}:${docId}`) ?? null
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
async readDeadLetters(count: number = 10) {
|
|
116
|
-
return RedisMock.deadLetters.slice(0, count).map(item => ({
|
|
117
|
-
streamId: item.streamId,
|
|
118
|
-
job: { ...RedisMock.jobs.get(item.jobId)! },
|
|
119
|
-
reason: item.reason,
|
|
120
|
-
failedAt: item.failedAt
|
|
121
|
-
}))
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
async replayDeadLetter(streamId: string) {
|
|
125
|
-
const item = RedisMock.deadLetters.find(entry => entry.streamId === streamId)
|
|
126
|
-
if(!item) return null
|
|
127
|
-
|
|
128
|
-
const job = RedisMock.jobs.get(item.jobId)
|
|
129
|
-
if(!job) return null
|
|
130
|
-
|
|
131
|
-
const replayed = {
|
|
132
|
-
...job,
|
|
133
|
-
status: 'queued',
|
|
134
|
-
error: undefined,
|
|
135
|
-
workerId: undefined,
|
|
136
|
-
attempts: 0,
|
|
137
|
-
updatedAt: Date.now(),
|
|
138
|
-
nextAttemptAt: Date.now()
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
RedisMock.jobs.set(item.jobId, replayed)
|
|
142
|
-
await this.enqueueWrite(replayed)
|
|
143
|
-
RedisMock.deadLetters = RedisMock.deadLetters.filter(entry => entry.streamId !== streamId)
|
|
144
|
-
|
|
145
|
-
return { ...replayed }
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
async getQueueStats() {
|
|
149
|
-
return {
|
|
150
|
-
queued: RedisMock.stream.length,
|
|
151
|
-
pending: RedisMock.stream.filter(entry => entry.claimedBy).length,
|
|
152
|
-
deadLetters: RedisMock.deadLetters.length
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
async acquireDocLock(collection: string, docId: _ttid, jobId: string) {
|
|
157
|
-
const key = `fylo:lock:${collection}:${docId}`
|
|
158
|
-
if(RedisMock.locks.has(key)) return false
|
|
159
|
-
RedisMock.locks.set(key, jobId)
|
|
160
|
-
return true
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
async releaseDocLock(collection: string, docId: _ttid, jobId: string) {
|
|
164
|
-
const key = `fylo:lock:${collection}:${docId}`
|
|
165
|
-
if(RedisMock.locks.get(key) === jobId) RedisMock.locks.delete(key)
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
async *subscribe(_collection: string): AsyncGenerator<never, void, unknown> {}
|
|
169
|
-
}
|