@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.
- 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/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
package/src/core/query.ts
CHANGED
|
@@ -1,46 +1,51 @@
|
|
|
1
|
-
import { Cipher } from
|
|
1
|
+
import { Cipher } from '../adapters/cipher'
|
|
2
2
|
|
|
3
3
|
const ENCRYPTED_FIELD_OPS = ['$ne', '$gt', '$gte', '$lt', '$lte', '$like', '$contains'] as const
|
|
4
4
|
|
|
5
5
|
export class Query {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
static async getExprs<T extends Record<string, any>>(
|
|
7
|
+
collection: string,
|
|
8
|
+
query: _storeQuery<T>
|
|
9
|
+
) {
|
|
9
10
|
let exprs = new Set<string>()
|
|
10
11
|
|
|
11
|
-
if(query.$ops) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
for(const column in op) {
|
|
16
|
-
|
|
12
|
+
if (query.$ops) {
|
|
13
|
+
for (const op of query.$ops) {
|
|
14
|
+
for (const column in op) {
|
|
17
15
|
const col = op[column as keyof T]!
|
|
18
16
|
|
|
19
17
|
const fieldPath = String(column).replaceAll('.', '/')
|
|
20
|
-
const encrypted =
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
18
|
+
const encrypted =
|
|
19
|
+
Cipher.isConfigured() && Cipher.isEncryptedField(collection, fieldPath)
|
|
20
|
+
|
|
21
|
+
if (encrypted) {
|
|
22
|
+
for (const opKey of ENCRYPTED_FIELD_OPS) {
|
|
23
|
+
if (col[opKey] !== undefined) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`Operator ${opKey} is not supported on encrypted field "${String(column)}"`
|
|
26
|
+
)
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
if(col.$eq) {
|
|
31
|
-
const val = encrypted
|
|
31
|
+
if (col.$eq) {
|
|
32
|
+
const val = encrypted
|
|
33
|
+
? await Cipher.encrypt(String(col.$eq).replaceAll('/', '%2F'))
|
|
34
|
+
: col.$eq
|
|
32
35
|
exprs.add(`${column}/${val}/**/*`)
|
|
33
36
|
}
|
|
34
|
-
if(col.$ne) exprs.add(`${column}/**/*`)
|
|
35
|
-
if(col.$gt) exprs.add(`${column}/**/*`)
|
|
36
|
-
if(col.$gte) exprs.add(`${column}/**/*`)
|
|
37
|
-
if(col.$lt) exprs.add(`${column}/**/*`)
|
|
38
|
-
if(col.$lte) exprs.add(`${column}/**/*`)
|
|
39
|
-
if(col.$like) exprs.add(`${column}/${col.$like.replaceAll('%', '*')}/**/*`)
|
|
40
|
-
if(col.$contains !== undefined)
|
|
37
|
+
if (col.$ne) exprs.add(`${column}/**/*`)
|
|
38
|
+
if (col.$gt) exprs.add(`${column}/**/*`)
|
|
39
|
+
if (col.$gte) exprs.add(`${column}/**/*`)
|
|
40
|
+
if (col.$lt) exprs.add(`${column}/**/*`)
|
|
41
|
+
if (col.$lte) exprs.add(`${column}/**/*`)
|
|
42
|
+
if (col.$like) exprs.add(`${column}/${col.$like.replaceAll('%', '*')}/**/*`)
|
|
43
|
+
if (col.$contains !== undefined)
|
|
44
|
+
exprs.add(
|
|
45
|
+
`${column}/*/${String(col.$contains).split('/').join('%2F')}/**/*`
|
|
46
|
+
)
|
|
41
47
|
}
|
|
42
48
|
}
|
|
43
|
-
|
|
44
49
|
} else exprs = new Set([`**/*`])
|
|
45
50
|
|
|
46
51
|
return Array.from(exprs)
|
package/src/core/walker.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { S3 } from
|
|
2
|
-
import TTID from
|
|
3
|
-
import { Redis } from
|
|
1
|
+
import { S3 } from '../adapters/s3'
|
|
2
|
+
import TTID from '@delma/ttid'
|
|
3
|
+
import { Redis } from '../adapters/redis'
|
|
4
4
|
|
|
5
5
|
export class Walker {
|
|
6
|
-
|
|
7
6
|
private static readonly MAX_KEYS = 1000
|
|
8
7
|
|
|
9
8
|
private static _redis: Redis | null = null
|
|
@@ -13,8 +12,15 @@ export class Walker {
|
|
|
13
12
|
return Walker._redis
|
|
14
13
|
}
|
|
15
14
|
|
|
16
|
-
private static async *searchS3(
|
|
17
|
-
|
|
15
|
+
private static async *searchS3(
|
|
16
|
+
collection: string,
|
|
17
|
+
prefix: string,
|
|
18
|
+
pattern?: string
|
|
19
|
+
): AsyncGenerator<
|
|
20
|
+
{ _id: _ttid; data: string[] } | void,
|
|
21
|
+
void,
|
|
22
|
+
{ count: number; limit?: number }
|
|
23
|
+
> {
|
|
18
24
|
const uniqueIds = new Set<string>()
|
|
19
25
|
|
|
20
26
|
let token: string | undefined
|
|
@@ -24,39 +30,38 @@ export class Walker {
|
|
|
24
30
|
let limit = filter ? filter.limit : this.MAX_KEYS
|
|
25
31
|
|
|
26
32
|
do {
|
|
27
|
-
|
|
28
33
|
const res = await S3.list(collection, {
|
|
29
34
|
prefix,
|
|
30
35
|
maxKeys: pattern ? limit : undefined,
|
|
31
36
|
continuationToken: token
|
|
32
37
|
})
|
|
33
38
|
|
|
34
|
-
if(res.contents === undefined) break
|
|
35
|
-
|
|
36
|
-
const keys = res.contents.map(item => item.key!)
|
|
39
|
+
if (res.contents === undefined) break
|
|
37
40
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
for(const key of keys) {
|
|
41
|
+
const keys = res.contents.map((item) => item.key!)
|
|
41
42
|
|
|
43
|
+
if (pattern) {
|
|
44
|
+
for (const key of keys) {
|
|
42
45
|
const segements = key.split('/')
|
|
43
46
|
|
|
44
47
|
const _id = segements.pop()! as _ttid
|
|
45
48
|
|
|
46
|
-
if
|
|
47
|
-
|
|
49
|
+
if (
|
|
50
|
+
TTID.isTTID(_id) &&
|
|
51
|
+
!uniqueIds.has(_id) &&
|
|
52
|
+
pattern.length <= 1024 &&
|
|
53
|
+
new Bun.Glob(pattern).match(key)
|
|
54
|
+
) {
|
|
48
55
|
filter = yield { _id, data: await this.getDocData(collection, _id) }
|
|
49
56
|
|
|
50
57
|
limit = filter.limit ? filter.limit : this.MAX_KEYS
|
|
51
58
|
|
|
52
59
|
uniqueIds.add(_id)
|
|
53
60
|
|
|
54
|
-
if(filter.count === limit) break
|
|
61
|
+
if (filter.count === limit) break
|
|
55
62
|
}
|
|
56
63
|
}
|
|
57
|
-
|
|
58
64
|
} else {
|
|
59
|
-
|
|
60
65
|
const _id = prefix.split('/').pop()! as _ttid
|
|
61
66
|
|
|
62
67
|
yield { _id, data: keys }
|
|
@@ -65,35 +70,41 @@ export class Walker {
|
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
token = res.nextContinuationToken
|
|
68
|
-
|
|
69
|
-
} while(token !== undefined)
|
|
73
|
+
} while (token !== undefined)
|
|
70
74
|
}
|
|
71
75
|
|
|
72
|
-
static async *search(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
static async *search(
|
|
77
|
+
collection: string,
|
|
78
|
+
pattern: string,
|
|
79
|
+
{ listen = false, skip = false }: { listen: boolean; skip: boolean },
|
|
80
|
+
action: 'insert' | 'delete' = 'insert'
|
|
81
|
+
): AsyncGenerator<
|
|
82
|
+
{ _id: _ttid; data: string[] } | void,
|
|
83
|
+
void,
|
|
84
|
+
{ count: number; limit?: number }
|
|
85
|
+
> {
|
|
86
|
+
if (!skip) {
|
|
87
|
+
const segments = pattern.split('/')
|
|
88
|
+
const idx = segments.findIndex((seg) => seg.includes('*'))
|
|
89
|
+
const prefix = segments.slice(0, idx).join('/')
|
|
78
90
|
|
|
79
91
|
yield* this.searchS3(collection, prefix, pattern)
|
|
80
92
|
}
|
|
81
93
|
|
|
82
94
|
const eventIds = new Set<string>()
|
|
83
95
|
|
|
84
|
-
if
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
96
|
+
if (listen)
|
|
97
|
+
for await (const event of this.listen(collection, pattern)) {
|
|
98
|
+
if (event.action !== action && eventIds.has(event.id)) {
|
|
99
|
+
eventIds.delete(event.id)
|
|
100
|
+
} else if (event.action === action && !eventIds.has(event.id)) {
|
|
101
|
+
eventIds.add(event.id)
|
|
102
|
+
yield { _id: event.id, data: event.data }
|
|
103
|
+
}
|
|
91
104
|
}
|
|
92
|
-
}
|
|
93
105
|
}
|
|
94
106
|
|
|
95
107
|
static async getDocData(collection: string, _id: _ttid) {
|
|
96
|
-
|
|
97
108
|
const prefix = _id.split('-')[0]
|
|
98
109
|
|
|
99
110
|
const data: string[] = []
|
|
@@ -103,63 +114,59 @@ export class Walker {
|
|
|
103
114
|
const iter = this.searchS3(collection, prefix)
|
|
104
115
|
|
|
105
116
|
do {
|
|
106
|
-
|
|
107
117
|
const { value, done } = await iter.next()
|
|
108
118
|
|
|
109
|
-
if(done) {
|
|
119
|
+
if (done) {
|
|
110
120
|
finished = true
|
|
111
121
|
break
|
|
112
122
|
}
|
|
113
123
|
|
|
114
|
-
if(value) {
|
|
115
|
-
for(const key of value.data) {
|
|
116
|
-
if(key.startsWith(_id + '/')) data.push(key)
|
|
124
|
+
if (value) {
|
|
125
|
+
for (const key of value.data) {
|
|
126
|
+
if (key.startsWith(_id + '/')) data.push(key)
|
|
117
127
|
}
|
|
118
128
|
finished = true
|
|
119
129
|
break
|
|
120
130
|
}
|
|
121
|
-
|
|
122
|
-
} while(!finished)
|
|
131
|
+
} while (!finished)
|
|
123
132
|
|
|
124
133
|
return data
|
|
125
134
|
}
|
|
126
135
|
|
|
127
136
|
private static async *processPattern(collection: string, pattern: string) {
|
|
128
|
-
|
|
129
137
|
const stackIds = new Set<string>()
|
|
130
138
|
|
|
131
139
|
for await (const { action, keyId } of Walker.redis.subscribe(collection)) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
140
|
+
if (
|
|
141
|
+
action === 'insert' &&
|
|
142
|
+
!TTID.isTTID(keyId) &&
|
|
143
|
+
pattern.length <= 1024 &&
|
|
144
|
+
new Bun.Glob(pattern).match(keyId)
|
|
145
|
+
) {
|
|
135
146
|
const _id = keyId.split('/').pop()! as _ttid
|
|
136
147
|
|
|
137
|
-
if(!stackIds.has(_id)) {
|
|
138
|
-
|
|
148
|
+
if (!stackIds.has(_id)) {
|
|
139
149
|
stackIds.add(_id)
|
|
140
150
|
|
|
141
|
-
yield {
|
|
151
|
+
yield {
|
|
152
|
+
id: _id,
|
|
153
|
+
action: 'insert',
|
|
154
|
+
data: await this.getDocData(collection, _id)
|
|
155
|
+
}
|
|
142
156
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
yield { id: keyId as _ttid, action: "delete", data: [] }
|
|
147
|
-
|
|
148
|
-
} else if(TTID.isTTID(keyId) && stackIds.has(keyId)) {
|
|
149
|
-
|
|
157
|
+
} else if (action === 'delete' && TTID.isTTID(keyId)) {
|
|
158
|
+
yield { id: keyId as _ttid, action: 'delete', data: [] }
|
|
159
|
+
} else if (TTID.isTTID(keyId) && stackIds.has(keyId)) {
|
|
150
160
|
stackIds.delete(keyId)
|
|
151
161
|
}
|
|
152
162
|
}
|
|
153
163
|
}
|
|
154
164
|
|
|
155
165
|
static async *listen(collection: string, pattern: string | string[]) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
for(const p of pattern) {
|
|
166
|
+
if (Array.isArray(pattern)) {
|
|
167
|
+
for (const p of pattern) {
|
|
160
168
|
for await (const event of this.processPattern(collection, p)) yield event
|
|
161
169
|
}
|
|
162
|
-
|
|
163
170
|
} else {
|
|
164
171
|
for await (const event of this.processPattern(collection, pattern)) yield event
|
|
165
172
|
}
|
package/src/core/write-queue.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { WriteJob } from '../types/write-queue'
|
|
2
2
|
|
|
3
3
|
export class WriteQueue {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
static createInsertJob<T extends Record<string, any>>(
|
|
5
|
+
collection: string,
|
|
6
|
+
docId: _ttid,
|
|
7
|
+
payload: T
|
|
8
|
+
): WriteJob<T> {
|
|
6
9
|
const now = Date.now()
|
|
7
10
|
|
|
8
11
|
return {
|
|
@@ -21,8 +24,8 @@ export class WriteQueue {
|
|
|
21
24
|
static createUpdateJob<T extends Record<string, any>>(
|
|
22
25
|
collection: string,
|
|
23
26
|
docId: _ttid,
|
|
24
|
-
payload: { newDoc: Record<_ttid, Partial<T
|
|
25
|
-
): WriteJob<{ newDoc: Record<_ttid, Partial<T
|
|
27
|
+
payload: { newDoc: Record<_ttid, Partial<T>>; oldDoc?: Record<_ttid, T> }
|
|
28
|
+
): WriteJob<{ newDoc: Record<_ttid, Partial<T>>; oldDoc?: Record<_ttid, T> }> {
|
|
26
29
|
const now = Date.now()
|
|
27
30
|
|
|
28
31
|
return {
|