@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.
Files changed (66) hide show
  1. package/README.md +141 -62
  2. package/eslint.config.js +8 -4
  3. package/package.json +9 -7
  4. package/src/CLI +16 -14
  5. package/src/adapters/cipher.ts +12 -6
  6. package/src/adapters/redis.ts +193 -123
  7. package/src/adapters/s3.ts +6 -12
  8. package/src/core/collection.ts +5 -0
  9. package/src/core/directory.ts +120 -151
  10. package/src/core/extensions.ts +4 -2
  11. package/src/core/format.ts +390 -419
  12. package/src/core/parser.ts +167 -142
  13. package/src/core/query.ts +31 -26
  14. package/src/core/walker.ts +68 -61
  15. package/src/core/write-queue.ts +7 -4
  16. package/src/engines/s3-files.ts +1068 -0
  17. package/src/engines/types.ts +21 -0
  18. package/src/index.ts +754 -378
  19. package/src/migrate-cli.ts +22 -0
  20. package/src/migrate.ts +74 -0
  21. package/src/types/bun-runtime.d.ts +73 -0
  22. package/src/types/fylo.d.ts +115 -27
  23. package/src/types/node-runtime.d.ts +61 -0
  24. package/src/types/query.d.ts +6 -2
  25. package/src/types/vendor-modules.d.ts +8 -7
  26. package/src/worker.ts +7 -1
  27. package/src/workers/write-worker.ts +25 -24
  28. package/tests/collection/truncate.test.js +35 -0
  29. package/tests/{data.ts → data.js} +8 -21
  30. package/tests/{index.ts → index.js} +4 -9
  31. package/tests/integration/aws-s3-files.canary.test.js +22 -0
  32. package/tests/integration/{create.test.ts → create.test.js} +13 -31
  33. package/tests/integration/delete.test.js +95 -0
  34. package/tests/integration/{edge-cases.test.ts → edge-cases.test.js} +50 -124
  35. package/tests/integration/{encryption.test.ts → encryption.test.js} +20 -65
  36. package/tests/integration/{export.test.ts → export.test.js} +8 -23
  37. package/tests/integration/{join-modes.test.ts → join-modes.test.js} +37 -104
  38. package/tests/integration/migration.test.js +38 -0
  39. package/tests/integration/nested.test.js +142 -0
  40. package/tests/integration/operators.test.js +122 -0
  41. package/tests/integration/{queue.test.ts → queue.test.js} +24 -40
  42. package/tests/integration/read.test.js +119 -0
  43. package/tests/integration/rollback.test.js +60 -0
  44. package/tests/integration/s3-files.test.js +192 -0
  45. package/tests/integration/update.test.js +99 -0
  46. package/tests/mocks/{cipher.ts → cipher.js} +11 -26
  47. package/tests/mocks/redis.js +123 -0
  48. package/tests/mocks/{s3.ts → s3.js} +24 -58
  49. package/tests/schemas/album.json +1 -1
  50. package/tests/schemas/comment.json +1 -1
  51. package/tests/schemas/photo.json +1 -1
  52. package/tests/schemas/post.json +1 -1
  53. package/tests/schemas/tip.json +1 -1
  54. package/tests/schemas/todo.json +1 -1
  55. package/tests/schemas/user.d.ts +12 -12
  56. package/tests/schemas/user.json +1 -1
  57. package/tsconfig.json +4 -2
  58. package/tsconfig.typecheck.json +31 -0
  59. package/tests/collection/truncate.test.ts +0 -56
  60. package/tests/integration/delete.test.ts +0 -147
  61. package/tests/integration/nested.test.ts +0 -212
  62. package/tests/integration/operators.test.ts +0 -167
  63. package/tests/integration/read.test.ts +0 -203
  64. package/tests/integration/rollback.test.ts +0 -105
  65. package/tests/integration/update.test.ts +0 -130
  66. package/tests/mocks/redis.ts +0 -169
package/src/core/query.ts CHANGED
@@ -1,46 +1,51 @@
1
- import { Cipher } from "../adapters/cipher"
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
- static async getExprs<T extends Record<string, any>>(collection: string, query: _storeQuery<T>) {
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
- for(const op of query.$ops) {
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 = Cipher.isConfigured() && Cipher.isEncryptedField(collection, fieldPath)
21
-
22
- if(encrypted) {
23
- for(const opKey of ENCRYPTED_FIELD_OPS) {
24
- if(col[opKey] !== undefined) {
25
- throw new Error(`Operator ${opKey} is not supported on encrypted field "${String(column)}"`)
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 ? await Cipher.encrypt(String(col.$eq).replaceAll('/', '%2F')) : col.$eq
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) exprs.add(`${column}/*/${String(col.$contains).split('/').join('%2F')}/**/*`)
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)
@@ -1,9 +1,8 @@
1
- import { S3 } from "../adapters/s3"
2
- import TTID from "@delma/ttid"
3
- import { Redis } from "../adapters/redis"
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(collection: string, prefix: string, pattern?: string): AsyncGenerator<{ _id: _ttid, data: string[] } | void, void, { count: number, limit?: number }> {
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
- if(pattern) {
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((TTID.isTTID(_id) && !uniqueIds.has(_id)) && pattern.length <= 1024 && new Bun.Glob(pattern).match(key)) {
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(collection: string, pattern: string, { listen = false, skip = false }: { listen: boolean, skip: boolean }, action: "insert" | "delete" = "insert"): AsyncGenerator<{ _id: _ttid, data: string[] } | void, void, { count: number, limit?: number }> {
73
-
74
- if(!skip) {
75
- const segments = pattern.split('/');
76
- const idx = segments.findIndex(seg => seg.includes('*'));
77
- const prefix = segments.slice(0, idx).join('/');
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(listen) for await (const event of this.listen(collection, pattern)) {
85
-
86
- if(event.action !== action && eventIds.has(event.id)) {
87
- eventIds.delete(event.id)
88
- } else if(event.action === action && !eventIds.has(event.id)) {
89
- eventIds.add(event.id)
90
- yield { _id: event.id, data: event.data }
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
- if(action === 'insert' && !TTID.isTTID(keyId) && pattern.length <= 1024 && new Bun.Glob(pattern).match(keyId)) {
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 { id: _id, action: "insert", data: await this.getDocData(collection, _id) }
151
+ yield {
152
+ id: _id,
153
+ action: 'insert',
154
+ data: await this.getDocData(collection, _id)
155
+ }
142
156
  }
143
-
144
- } else if(action === 'delete' && TTID.isTTID(keyId)) {
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
- if(Array.isArray(pattern)) {
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
  }
@@ -1,8 +1,11 @@
1
1
  import type { WriteJob } from '../types/write-queue'
2
2
 
3
3
  export class WriteQueue {
4
-
5
- static createInsertJob<T extends Record<string, any>>(collection: string, docId: _ttid, payload: T): WriteJob<T> {
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>>, oldDoc?: Record<_ttid, T> }
25
- ): WriteJob<{ newDoc: Record<_ttid, Partial<T>>, oldDoc?: Record<_ttid, 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 {