@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
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { migrateLegacyS3ToS3Files } from './migrate'
|
|
3
|
+
|
|
4
|
+
const [, , ...args] = process.argv
|
|
5
|
+
|
|
6
|
+
const collections = args.filter((arg) => !arg.startsWith('--'))
|
|
7
|
+
const recreateCollections = !args.includes('--keep-existing')
|
|
8
|
+
const verify = !args.includes('--no-verify')
|
|
9
|
+
|
|
10
|
+
if (collections.length === 0) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
'Usage: fylo.migrate <collection> [collection...] [--keep-existing] [--no-verify]'
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const summary = await migrateLegacyS3ToS3Files({
|
|
17
|
+
collections,
|
|
18
|
+
recreateCollections,
|
|
19
|
+
verify
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
console.log(JSON.stringify(summary, null, 2))
|
package/src/migrate.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import Fylo from './index'
|
|
2
|
+
import { S3FilesEngine } from './engines/s3-files'
|
|
3
|
+
|
|
4
|
+
type MigrateOptions = {
|
|
5
|
+
collections: string[]
|
|
6
|
+
s3FilesRoot?: string
|
|
7
|
+
recreateCollections?: boolean
|
|
8
|
+
verify?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function normalize<T>(value: T): T {
|
|
12
|
+
if (Array.isArray(value)) return value.map((item) => normalize(item)).sort() as T
|
|
13
|
+
if (value && typeof value === 'object') {
|
|
14
|
+
return Object.keys(value as Record<string, unknown>)
|
|
15
|
+
.sort()
|
|
16
|
+
.reduce(
|
|
17
|
+
(acc, key) => {
|
|
18
|
+
acc[key] = normalize((value as Record<string, unknown>)[key])
|
|
19
|
+
return acc
|
|
20
|
+
},
|
|
21
|
+
{} as Record<string, unknown>
|
|
22
|
+
) as T
|
|
23
|
+
}
|
|
24
|
+
return value
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function migrateLegacyS3ToS3Files({
|
|
28
|
+
collections,
|
|
29
|
+
s3FilesRoot = process.env.FYLO_S3FILES_ROOT,
|
|
30
|
+
recreateCollections = true,
|
|
31
|
+
verify = true
|
|
32
|
+
}: MigrateOptions) {
|
|
33
|
+
if (!s3FilesRoot) throw new Error('s3FilesRoot is required')
|
|
34
|
+
|
|
35
|
+
const source = new Fylo({ engine: 'legacy-s3' })
|
|
36
|
+
const target = new S3FilesEngine(s3FilesRoot)
|
|
37
|
+
|
|
38
|
+
const summary: Record<string, { migrated: number; verified: boolean }> = {}
|
|
39
|
+
|
|
40
|
+
for (const collection of collections) {
|
|
41
|
+
if (recreateCollections) {
|
|
42
|
+
await target.dropCollection(collection)
|
|
43
|
+
await target.createCollection(collection)
|
|
44
|
+
} else if (!(await target.hasCollection(collection))) {
|
|
45
|
+
await target.createCollection(collection)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const docs = (await source.executeSQL<Record<string, any>>(
|
|
49
|
+
`SELECT * FROM ${collection}`
|
|
50
|
+
)) as Record<_ttid, Record<string, any>>
|
|
51
|
+
|
|
52
|
+
for (const [docId, doc] of Object.entries(docs) as Array<[_ttid, Record<string, any>]>) {
|
|
53
|
+
await target.putDocument(collection, docId, doc)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let verified = false
|
|
57
|
+
|
|
58
|
+
if (verify) {
|
|
59
|
+
const targetFylo = new Fylo({ engine: 's3-files', s3FilesRoot })
|
|
60
|
+
const migratedDocs = (await targetFylo.executeSQL<Record<string, any>>(
|
|
61
|
+
`SELECT * FROM ${collection}`
|
|
62
|
+
)) as Record<_ttid, Record<string, any>>
|
|
63
|
+
verified = JSON.stringify(normalize(docs)) === JSON.stringify(normalize(migratedDocs))
|
|
64
|
+
if (!verified) throw new Error(`Verification failed for ${collection}`)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
summary[collection] = {
|
|
68
|
+
migrated: Object.keys(docs).length,
|
|
69
|
+
verified
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return summary
|
|
74
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
declare namespace Bun {
|
|
2
|
+
function sleep(ms: number): Promise<void>
|
|
3
|
+
function randomUUIDv7(): string
|
|
4
|
+
|
|
5
|
+
class Glob {
|
|
6
|
+
constructor(pattern: string)
|
|
7
|
+
match(input: string): boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
namespace JSONL {
|
|
11
|
+
function parseChunk(input: Uint8Array): { values: unknown[]; read: number }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface S3ListObjectsOptions {
|
|
15
|
+
delimiter?: string
|
|
16
|
+
prefix?: string
|
|
17
|
+
maxKeys?: number
|
|
18
|
+
continuationToken?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface S3ListObjectsResponse {
|
|
22
|
+
commonPrefixes?: Array<{ prefix?: string }>
|
|
23
|
+
contents?: Array<{ key?: string }>
|
|
24
|
+
isTruncated?: boolean
|
|
25
|
+
nextContinuationToken?: string
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
declare module 'bun' {
|
|
30
|
+
export const $: (
|
|
31
|
+
strings: TemplateStringsArray,
|
|
32
|
+
...values: any[]
|
|
33
|
+
) => {
|
|
34
|
+
quiet(): Promise<unknown>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class S3Client {
|
|
38
|
+
static file(path: string, options: Record<string, any>): any
|
|
39
|
+
static list(
|
|
40
|
+
options: Bun.S3ListObjectsOptions | undefined,
|
|
41
|
+
config: Record<string, any>
|
|
42
|
+
): Promise<Bun.S3ListObjectsResponse>
|
|
43
|
+
static write(path: string, data: string, config: Record<string, any>): Promise<void>
|
|
44
|
+
static delete(path: string, config: Record<string, any>): Promise<void>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class RedisClient {
|
|
48
|
+
connected: boolean
|
|
49
|
+
onconnect?: () => void
|
|
50
|
+
onclose?: (err: Error) => void
|
|
51
|
+
|
|
52
|
+
constructor(url: string, options?: Record<string, any>)
|
|
53
|
+
|
|
54
|
+
connect(): void
|
|
55
|
+
send(command: string, args: string[]): Promise<any>
|
|
56
|
+
publish(channel: string, message: string): Promise<unknown>
|
|
57
|
+
subscribe(channel: string, listener: (message: string) => void): Promise<void>
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
declare module 'bun:sqlite' {
|
|
62
|
+
export class Database {
|
|
63
|
+
constructor(filename: string, options?: Record<string, any>)
|
|
64
|
+
exec(sql: string): void
|
|
65
|
+
close(): void
|
|
66
|
+
query(sql: string): {
|
|
67
|
+
run(...args: any[]): any
|
|
68
|
+
get(...args: any[]): any
|
|
69
|
+
all(...args: any[]): any[]
|
|
70
|
+
}
|
|
71
|
+
transaction<T extends (...args: any[]) => any>(fn: T): T
|
|
72
|
+
}
|
|
73
|
+
}
|
package/src/types/fylo.d.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
interface _getDoc {
|
|
2
|
-
[Symbol.asyncIterator]<T>(): AsyncGenerator<_ttid | Record<_ttid, T>, void, unknown
|
|
2
|
+
[Symbol.asyncIterator]<T>(): AsyncGenerator<_ttid | Record<_ttid, T>, void, unknown>
|
|
3
3
|
once<T>(): Promise<Record<_ttid, T>>
|
|
4
4
|
onDelete(): AsyncGenerator<_ttid, void, unknown>
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
interface _findDocs {
|
|
8
|
-
[Symbol.asyncIterator]<T>(): AsyncGenerator<
|
|
8
|
+
[Symbol.asyncIterator]<T>(): AsyncGenerator<
|
|
9
|
+
_ttid | Record<_ttid, T> | Record<string, _ttid[]> | Record<_ttid, Partial<T>> | undefined,
|
|
10
|
+
void,
|
|
11
|
+
unknown
|
|
12
|
+
>
|
|
9
13
|
once<T>(): Promise<Record<_ttid, T>>
|
|
10
14
|
onDelete(): AsyncGenerator<_ttid, void, unknown>
|
|
11
15
|
}
|
|
@@ -17,18 +21,29 @@ interface _queuedWriteResult {
|
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
interface ObjectConstructor {
|
|
20
|
-
appendGroup: (target: Record<string, any>, source: Record<string, any>) => Record<string, any
|
|
24
|
+
appendGroup: (target: Record<string, any>, source: Record<string, any>) => Record<string, any>
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
interface Console {
|
|
24
28
|
format: (docs: Record<string, any>) => void
|
|
25
29
|
}
|
|
26
30
|
|
|
27
|
-
type _joinDocs<T, U> =
|
|
31
|
+
type _joinDocs<T, U> =
|
|
32
|
+
| _ttid[]
|
|
33
|
+
| Record<string, _ttid[]>
|
|
34
|
+
| Record<string, Record<_ttid, Partial<T | U>>>
|
|
35
|
+
| Record<`${_ttid}, ${_ttid}`, T | U | (T & U) | (Partial<T> & Partial<U>)>
|
|
28
36
|
|
|
29
|
-
|
|
37
|
+
type _fyloEngineKind = 'legacy-s3' | 's3-files'
|
|
30
38
|
|
|
39
|
+
interface _fyloOptions {
|
|
40
|
+
engine?: _fyloEngineKind
|
|
41
|
+
s3FilesRoot?: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
declare module '@delma/fylo' {
|
|
31
45
|
export default class {
|
|
46
|
+
constructor(options?: _fyloOptions)
|
|
32
47
|
|
|
33
48
|
/**
|
|
34
49
|
* Rolls back all transcations in current instance
|
|
@@ -41,8 +56,10 @@ declare module "@delma/fylo" {
|
|
|
41
56
|
* @param SQL The SQL query to execute.
|
|
42
57
|
* @returns The results of the query.
|
|
43
58
|
*/
|
|
44
|
-
executeSQL<T extends Record<string, any>, U extends Record<string, any> = {}>(
|
|
45
|
-
|
|
59
|
+
executeSQL<T extends Record<string, any>, U extends Record<string, any> = {}>(
|
|
60
|
+
SQL: string
|
|
61
|
+
): Promise<number | void | any[] | _ttid | Record<any, any>>
|
|
62
|
+
|
|
46
63
|
/**
|
|
47
64
|
* Creates a new schema for a collection.
|
|
48
65
|
* @param collection The name of the collection.
|
|
@@ -68,7 +85,9 @@ declare module "@delma/fylo" {
|
|
|
68
85
|
* @param collection The name of the collection.
|
|
69
86
|
* @returns The current data exported from the collection.
|
|
70
87
|
*/
|
|
71
|
-
exportBulkData<T extends Record<string, any>>(
|
|
88
|
+
exportBulkData<T extends Record<string, any>>(
|
|
89
|
+
collection: string
|
|
90
|
+
): AsyncGenerator<T, void, unknown>
|
|
72
91
|
|
|
73
92
|
/**
|
|
74
93
|
* Gets a document from a collection.
|
|
@@ -85,11 +104,21 @@ declare module "@delma/fylo" {
|
|
|
85
104
|
* @param batch The documents to put.
|
|
86
105
|
* @returns The IDs of the documents.
|
|
87
106
|
*/
|
|
88
|
-
batchPutData<T extends Record<string, any>>(
|
|
107
|
+
batchPutData<T extends Record<string, any>>(
|
|
108
|
+
collection: string,
|
|
109
|
+
batch: Array<T>
|
|
110
|
+
): Promise<_ttid[]>
|
|
89
111
|
|
|
90
|
-
queuePutData<T extends Record<string, any>>(
|
|
112
|
+
queuePutData<T extends Record<string, any>>(
|
|
113
|
+
collection: string,
|
|
114
|
+
data: Record<_ttid, T> | T
|
|
115
|
+
): Promise<_queuedWriteResult>
|
|
91
116
|
|
|
92
|
-
queuePatchDoc<T extends Record<string, any>>(
|
|
117
|
+
queuePatchDoc<T extends Record<string, any>>(
|
|
118
|
+
collection: string,
|
|
119
|
+
newDoc: Record<_ttid, Partial<T>>,
|
|
120
|
+
oldDoc?: Record<_ttid, T>
|
|
121
|
+
): Promise<_queuedWriteResult>
|
|
93
122
|
|
|
94
123
|
queueDelDoc(collection: string, _id: _ttid): Promise<_queuedWriteResult>
|
|
95
124
|
|
|
@@ -99,7 +128,7 @@ declare module "@delma/fylo" {
|
|
|
99
128
|
|
|
100
129
|
getDeadLetters(count?: number): Promise<Array<Record<string, any>>>
|
|
101
130
|
|
|
102
|
-
getQueueStats(): Promise<{ queued: number
|
|
131
|
+
getQueueStats(): Promise<{ queued: number; pending: number; deadLetters: number }>
|
|
103
132
|
|
|
104
133
|
replayDeadLetter(streamId: string): Promise<Record<string, any> | null>
|
|
105
134
|
|
|
@@ -112,11 +141,30 @@ declare module "@delma/fylo" {
|
|
|
112
141
|
* @returns The ID of the document.
|
|
113
142
|
*/
|
|
114
143
|
putData<T extends Record<string, any>>(collection: string, data: T): Promise<_ttid>
|
|
115
|
-
putData<T extends Record<string, any>>(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
putData<T extends Record<string, any>>(
|
|
144
|
+
putData<T extends Record<string, any>>(
|
|
145
|
+
collection: string,
|
|
146
|
+
data: Record<_ttid, T>
|
|
147
|
+
): Promise<_ttid>
|
|
148
|
+
putData<T extends Record<string, any>>(
|
|
149
|
+
collection: string,
|
|
150
|
+
data: T,
|
|
151
|
+
options: { wait?: true; timeoutMs?: number }
|
|
152
|
+
): Promise<_ttid>
|
|
153
|
+
putData<T extends Record<string, any>>(
|
|
154
|
+
collection: string,
|
|
155
|
+
data: Record<_ttid, T>,
|
|
156
|
+
options: { wait?: true; timeoutMs?: number }
|
|
157
|
+
): Promise<_ttid>
|
|
158
|
+
putData<T extends Record<string, any>>(
|
|
159
|
+
collection: string,
|
|
160
|
+
data: T,
|
|
161
|
+
options: { wait: false; timeoutMs?: number }
|
|
162
|
+
): Promise<_queuedWriteResult>
|
|
163
|
+
putData<T extends Record<string, any>>(
|
|
164
|
+
collection: string,
|
|
165
|
+
data: Record<_ttid, T>,
|
|
166
|
+
options: { wait: false; timeoutMs?: number }
|
|
167
|
+
): Promise<_queuedWriteResult>
|
|
120
168
|
|
|
121
169
|
/**
|
|
122
170
|
* Patches a document in a collection.
|
|
@@ -125,9 +173,23 @@ declare module "@delma/fylo" {
|
|
|
125
173
|
* @param oldDoc The old document data.
|
|
126
174
|
* @returns The number of documents patched.
|
|
127
175
|
*/
|
|
128
|
-
patchDoc<T extends Record<string, any>>(
|
|
129
|
-
|
|
130
|
-
|
|
176
|
+
patchDoc<T extends Record<string, any>>(
|
|
177
|
+
collection: string,
|
|
178
|
+
newDoc: Record<_ttid, Partial<T>>,
|
|
179
|
+
oldDoc?: Record<_ttid, T>
|
|
180
|
+
): Promise<_ttid>
|
|
181
|
+
patchDoc<T extends Record<string, any>>(
|
|
182
|
+
collection: string,
|
|
183
|
+
newDoc: Record<_ttid, Partial<T>>,
|
|
184
|
+
oldDoc: Record<_ttid, T> | undefined,
|
|
185
|
+
options: { wait?: true; timeoutMs?: number }
|
|
186
|
+
): Promise<_ttid>
|
|
187
|
+
patchDoc<T extends Record<string, any>>(
|
|
188
|
+
collection: string,
|
|
189
|
+
newDoc: Record<_ttid, Partial<T>>,
|
|
190
|
+
oldDoc: Record<_ttid, T> | undefined,
|
|
191
|
+
options: { wait: false; timeoutMs?: number }
|
|
192
|
+
): Promise<_queuedWriteResult>
|
|
131
193
|
|
|
132
194
|
/**
|
|
133
195
|
* Patches documents in a collection.
|
|
@@ -135,7 +197,10 @@ declare module "@delma/fylo" {
|
|
|
135
197
|
* @param updateSchema The update schema.
|
|
136
198
|
* @returns The number of documents patched.
|
|
137
199
|
*/
|
|
138
|
-
patchDocs<T extends Record<string, any>>(
|
|
200
|
+
patchDocs<T extends Record<string, any>>(
|
|
201
|
+
collection: string,
|
|
202
|
+
updateSchema: _storeUpdate<T>
|
|
203
|
+
): Promise<number>
|
|
139
204
|
|
|
140
205
|
/**
|
|
141
206
|
* Deletes a document from a collection.
|
|
@@ -144,8 +209,16 @@ declare module "@delma/fylo" {
|
|
|
144
209
|
* @returns The number of documents deleted.
|
|
145
210
|
*/
|
|
146
211
|
delDoc(collection: string, _id: _ttid): Promise<void>
|
|
147
|
-
delDoc(
|
|
148
|
-
|
|
212
|
+
delDoc(
|
|
213
|
+
collection: string,
|
|
214
|
+
_id: _ttid,
|
|
215
|
+
options: { wait?: true; timeoutMs?: number }
|
|
216
|
+
): Promise<void>
|
|
217
|
+
delDoc(
|
|
218
|
+
collection: string,
|
|
219
|
+
_id: _ttid,
|
|
220
|
+
options: { wait: false; timeoutMs?: number }
|
|
221
|
+
): Promise<_queuedWriteResult>
|
|
149
222
|
|
|
150
223
|
/**
|
|
151
224
|
* Deletes documents from a collection.
|
|
@@ -153,21 +226,36 @@ declare module "@delma/fylo" {
|
|
|
153
226
|
* @param deleteSchema The delete schema.
|
|
154
227
|
* @returns The number of documents deleted.
|
|
155
228
|
*/
|
|
156
|
-
delDocs<T extends Record<string, any>>(
|
|
229
|
+
delDocs<T extends Record<string, any>>(
|
|
230
|
+
collection: string,
|
|
231
|
+
deleteSchema?: _storeDelete<T>
|
|
232
|
+
): Promise<number>
|
|
157
233
|
|
|
158
234
|
/**
|
|
159
235
|
* Joins documents from two collections.
|
|
160
236
|
* @param join The join schema.
|
|
161
237
|
* @returns The joined documents.
|
|
162
238
|
*/
|
|
163
|
-
static joinDocs<T extends Record<string, any>, U extends Record<string, any>>(
|
|
164
|
-
|
|
239
|
+
static joinDocs<T extends Record<string, any>, U extends Record<string, any>>(
|
|
240
|
+
join: _join<T, U>
|
|
241
|
+
): Promise<_joinDocs<T, U>>
|
|
242
|
+
|
|
165
243
|
/**
|
|
166
244
|
* Finds documents in a collection.
|
|
167
245
|
* @param collection The name of the collection.
|
|
168
246
|
* @param query The query schema.
|
|
169
247
|
* @returns The found documents.
|
|
170
248
|
*/
|
|
171
|
-
static findDocs<T extends Record<string, any>>(
|
|
249
|
+
static findDocs<T extends Record<string, any>>(
|
|
250
|
+
collection: string,
|
|
251
|
+
query?: _storeQuery<T>
|
|
252
|
+
): _findDocs
|
|
172
253
|
}
|
|
254
|
+
|
|
255
|
+
export function migrateLegacyS3ToS3Files(options: {
|
|
256
|
+
collections: string[]
|
|
257
|
+
s3FilesRoot?: string
|
|
258
|
+
recreateCollections?: boolean
|
|
259
|
+
verify?: boolean
|
|
260
|
+
}): Promise<Record<string, { migrated: number; verified: boolean }>>
|
|
173
261
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
declare const process: {
|
|
2
|
+
env: Record<string, string | undefined>
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
declare const Buffer: {
|
|
6
|
+
alloc(size: number): Uint8Array & {
|
|
7
|
+
toString(encoding?: string): string
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
declare namespace NodeJS {
|
|
12
|
+
interface ErrnoException extends Error {
|
|
13
|
+
code?: string
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
declare module 'node:path' {
|
|
18
|
+
const path: {
|
|
19
|
+
join(...parts: string[]): string
|
|
20
|
+
dirname(target: string): string
|
|
21
|
+
basename(target: string, suffix?: string): string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default path
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
declare module 'node:crypto' {
|
|
28
|
+
export function createHash(algorithm: string): {
|
|
29
|
+
update(value: string): {
|
|
30
|
+
digest(encoding: 'hex'): string
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
declare module 'node:fs/promises' {
|
|
36
|
+
export function mkdir(path: string, options?: { recursive?: boolean }): Promise<void>
|
|
37
|
+
export function readFile(path: string, encoding: string): Promise<string>
|
|
38
|
+
export function readdir(
|
|
39
|
+
path: string,
|
|
40
|
+
options?: { withFileTypes?: boolean }
|
|
41
|
+
): Promise<
|
|
42
|
+
Array<{
|
|
43
|
+
name: string
|
|
44
|
+
isDirectory(): boolean
|
|
45
|
+
}>
|
|
46
|
+
>
|
|
47
|
+
export function rm(
|
|
48
|
+
path: string,
|
|
49
|
+
options?: { recursive?: boolean; force?: boolean }
|
|
50
|
+
): Promise<void>
|
|
51
|
+
export function stat(path: string): Promise<{ size: number }>
|
|
52
|
+
export function writeFile(path: string, data: string, encoding: string): Promise<void>
|
|
53
|
+
export function open(
|
|
54
|
+
path: string,
|
|
55
|
+
flags: string
|
|
56
|
+
): Promise<{
|
|
57
|
+
write(data: string): Promise<void>
|
|
58
|
+
read(buffer: Uint8Array, offset: number, length: number, position: number): Promise<void>
|
|
59
|
+
close(): Promise<void>
|
|
60
|
+
}>
|
|
61
|
+
}
|
package/src/types/query.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ type _join<T, U> = {
|
|
|
33
33
|
$select?: Array<keyof T | keyof U>
|
|
34
34
|
$leftCollection: string
|
|
35
35
|
$rightCollection: string
|
|
36
|
-
$mode:
|
|
36
|
+
$mode: 'inner' | 'left' | 'right' | 'outer'
|
|
37
37
|
$on: _on<T, U>
|
|
38
38
|
$limit?: number
|
|
39
39
|
$onlyIds?: boolean
|
|
@@ -53,7 +53,11 @@ interface _storeQuery<T extends Record<string, any>> {
|
|
|
53
53
|
$created?: _timestamp
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
interface _condition {
|
|
56
|
+
interface _condition {
|
|
57
|
+
column: string
|
|
58
|
+
operator: string
|
|
59
|
+
value: string | number | boolean | null
|
|
60
|
+
}
|
|
57
61
|
|
|
58
62
|
type _storeUpdate<T extends Record<string, any>> = {
|
|
59
63
|
$collection?: string
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
declare module
|
|
2
|
-
|
|
1
|
+
declare module '@delma/ttid' {
|
|
3
2
|
export type _ttid = string | `${string}-${string}` | `${string}-${string}-${string}`
|
|
4
3
|
|
|
5
4
|
export interface _timestamps {
|
|
@@ -16,16 +15,18 @@ declare module "@delma/ttid" {
|
|
|
16
15
|
}
|
|
17
16
|
}
|
|
18
17
|
|
|
19
|
-
declare module
|
|
20
|
-
|
|
18
|
+
declare module '@delma/chex' {
|
|
21
19
|
export default class Gen {
|
|
22
20
|
static generateDeclaration(json: unknown, interfaceName?: string): string
|
|
23
21
|
static sanitizePropertyName(key: string): string
|
|
24
22
|
static fromJsonString(jsonString: string, interfaceName?: string): string
|
|
25
23
|
static fromObject(obj: unknown, interfaceName?: string): string
|
|
26
|
-
static validateData<T extends Record<string, unknown>>(
|
|
24
|
+
static validateData<T extends Record<string, unknown>>(
|
|
25
|
+
collection: string,
|
|
26
|
+
data: T
|
|
27
|
+
): Promise<T>
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
type _ttid = import(
|
|
31
|
-
type _timestamps = import(
|
|
31
|
+
type _ttid = import('@delma/ttid')._ttid
|
|
32
|
+
type _timestamps = import('@delma/ttid')._timestamps
|
package/src/worker.ts
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { WriteWorker } from './workers/write-worker'
|
|
3
3
|
|
|
4
|
+
if (process.env.FYLO_STORAGE_ENGINE === 's3-files') {
|
|
5
|
+
throw new Error('fylo.worker is not supported in s3-files engine')
|
|
6
|
+
}
|
|
7
|
+
|
|
4
8
|
const worker = new WriteWorker(process.env.FYLO_WORKER_ID)
|
|
5
9
|
|
|
6
10
|
await worker.run({
|
|
7
11
|
batchSize: process.env.FYLO_WORKER_BATCH_SIZE ? Number(process.env.FYLO_WORKER_BATCH_SIZE) : 1,
|
|
8
12
|
blockMs: process.env.FYLO_WORKER_BLOCK_MS ? Number(process.env.FYLO_WORKER_BLOCK_MS) : 1000,
|
|
9
13
|
recoverOnStart: process.env.FYLO_WORKER_RECOVER_ON_START !== 'false',
|
|
10
|
-
recoverIdleMs: process.env.FYLO_WORKER_RECOVER_IDLE_MS
|
|
14
|
+
recoverIdleMs: process.env.FYLO_WORKER_RECOVER_IDLE_MS
|
|
15
|
+
? Number(process.env.FYLO_WORKER_RECOVER_IDLE_MS)
|
|
16
|
+
: 30_000,
|
|
11
17
|
stopWhenIdle: process.env.FYLO_WORKER_STOP_WHEN_IDLE === 'true'
|
|
12
18
|
})
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import Fylo from
|
|
2
|
-
import { Redis } from
|
|
3
|
-
import type { StreamJobEntry, WriteJob } from
|
|
1
|
+
import Fylo from '../index'
|
|
2
|
+
import { Redis } from '../adapters/redis'
|
|
3
|
+
import type { StreamJobEntry, WriteJob } from '../types/write-queue'
|
|
4
4
|
|
|
5
5
|
export class WriteWorker {
|
|
6
|
-
|
|
7
6
|
private static readonly MAX_WRITE_ATTEMPTS = Number(process.env.FYLO_WRITE_MAX_ATTEMPTS ?? 3)
|
|
8
7
|
|
|
9
8
|
private static readonly WRITE_RETRY_BASE_MS = Number(process.env.FYLO_WRITE_RETRY_BASE_MS ?? 10)
|
|
@@ -22,21 +21,21 @@ export class WriteWorker {
|
|
|
22
21
|
|
|
23
22
|
async recoverPending(minIdleMs: number = 30_000, count: number = 10) {
|
|
24
23
|
const jobs = await this.redis.claimPendingJobs(this.workerId, minIdleMs, count)
|
|
25
|
-
for(const job of jobs) await this.processJob(job)
|
|
24
|
+
for (const job of jobs) await this.processJob(job)
|
|
26
25
|
return jobs.length
|
|
27
26
|
}
|
|
28
27
|
|
|
29
28
|
async processNext(count: number = 1, blockMs: number = 1000) {
|
|
30
29
|
const jobs = await this.redis.readWriteJobs(this.workerId, count, blockMs)
|
|
31
|
-
for(const job of jobs) await this.processJob(job)
|
|
30
|
+
for (const job of jobs) await this.processJob(job)
|
|
32
31
|
return jobs.length
|
|
33
32
|
}
|
|
34
33
|
|
|
35
34
|
async processJob({ streamId, job }: StreamJobEntry) {
|
|
36
|
-
if(job.nextAttemptAt && job.nextAttemptAt > Date.now()) return false
|
|
35
|
+
if (job.nextAttemptAt && job.nextAttemptAt > Date.now()) return false
|
|
37
36
|
|
|
38
37
|
const locked = await this.redis.acquireDocLock(job.collection, job.docId, job.jobId)
|
|
39
|
-
if(!locked) return false
|
|
38
|
+
if (!locked) return false
|
|
40
39
|
|
|
41
40
|
try {
|
|
42
41
|
await this.redis.setJobStatus(job.jobId, 'processing', {
|
|
@@ -52,29 +51,33 @@ export class WriteWorker {
|
|
|
52
51
|
await this.redis.ackWriteJob(streamId)
|
|
53
52
|
|
|
54
53
|
return true
|
|
55
|
-
|
|
56
|
-
} catch(err) {
|
|
54
|
+
} catch (err) {
|
|
57
55
|
const attempts = job.attempts + 1
|
|
58
56
|
const message = err instanceof Error ? err.message : String(err)
|
|
59
57
|
|
|
60
|
-
if(attempts >= WriteWorker.MAX_WRITE_ATTEMPTS) {
|
|
58
|
+
if (attempts >= WriteWorker.MAX_WRITE_ATTEMPTS) {
|
|
61
59
|
await this.redis.setJobStatus(job.jobId, 'dead-letter', {
|
|
62
60
|
workerId: this.workerId,
|
|
63
61
|
attempts,
|
|
64
62
|
error: message
|
|
65
63
|
})
|
|
66
64
|
await this.redis.setDocStatus(job.collection, job.docId, 'dead-letter', job.jobId)
|
|
67
|
-
await this.redis.deadLetterWriteJob(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
65
|
+
await this.redis.deadLetterWriteJob(
|
|
66
|
+
streamId,
|
|
67
|
+
{
|
|
68
|
+
...job,
|
|
69
|
+
attempts,
|
|
70
|
+
status: 'dead-letter',
|
|
71
|
+
workerId: this.workerId,
|
|
72
|
+
error: message
|
|
73
|
+
},
|
|
74
|
+
message
|
|
75
|
+
)
|
|
74
76
|
return false
|
|
75
77
|
}
|
|
76
78
|
|
|
77
|
-
const nextAttemptAt =
|
|
79
|
+
const nextAttemptAt =
|
|
80
|
+
Date.now() + WriteWorker.WRITE_RETRY_BASE_MS * Math.max(1, 2 ** (attempts - 1))
|
|
78
81
|
|
|
79
82
|
await this.redis.setJobStatus(job.jobId, 'failed', {
|
|
80
83
|
workerId: this.workerId,
|
|
@@ -85,7 +88,6 @@ export class WriteWorker {
|
|
|
85
88
|
await this.redis.setDocStatus(job.collection, job.docId, 'failed', job.jobId)
|
|
86
89
|
|
|
87
90
|
return false
|
|
88
|
-
|
|
89
91
|
} finally {
|
|
90
92
|
await this.redis.releaseDocLock(job.collection, job.docId, job.jobId)
|
|
91
93
|
}
|
|
@@ -108,12 +110,11 @@ export class WriteWorker {
|
|
|
108
110
|
recoverIdleMs?: number
|
|
109
111
|
stopWhenIdle?: boolean
|
|
110
112
|
} = {}) {
|
|
113
|
+
if (recoverOnStart) await this.recoverPending(recoverIdleMs, batchSize)
|
|
111
114
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
while(true) {
|
|
115
|
+
while (true) {
|
|
115
116
|
const processed = await this.processNext(batchSize, blockMs)
|
|
116
|
-
if(stopWhenIdle && processed === 0) break
|
|
117
|
+
if (stopWhenIdle && processed === 0) break
|
|
117
118
|
}
|
|
118
119
|
}
|
|
119
120
|
}
|