@delma/fylo 2.0.1 → 2.1.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 (107) hide show
  1. package/README.md +206 -261
  2. package/dist/adapters/cipher.js +155 -0
  3. package/dist/adapters/cipher.js.map +1 -0
  4. package/dist/core/collection.js +6 -0
  5. package/dist/core/collection.js.map +1 -0
  6. package/dist/core/directory.js +48 -0
  7. package/dist/core/directory.js.map +1 -0
  8. package/dist/core/doc-id.js +15 -0
  9. package/dist/core/doc-id.js.map +1 -0
  10. package/dist/core/extensions.js +16 -0
  11. package/dist/core/extensions.js.map +1 -0
  12. package/dist/core/format.js +355 -0
  13. package/dist/core/format.js.map +1 -0
  14. package/dist/core/parser.js +764 -0
  15. package/dist/core/parser.js.map +1 -0
  16. package/dist/core/query.js +47 -0
  17. package/dist/core/query.js.map +1 -0
  18. package/dist/engines/s3-files/documents.js +62 -0
  19. package/dist/engines/s3-files/documents.js.map +1 -0
  20. package/dist/engines/s3-files/filesystem.js +165 -0
  21. package/dist/engines/s3-files/filesystem.js.map +1 -0
  22. package/dist/engines/s3-files/query.js +235 -0
  23. package/dist/engines/s3-files/query.js.map +1 -0
  24. package/dist/engines/s3-files/types.js +2 -0
  25. package/dist/engines/s3-files/types.js.map +1 -0
  26. package/dist/engines/s3-files.js +629 -0
  27. package/dist/engines/s3-files.js.map +1 -0
  28. package/dist/engines/types.js +2 -0
  29. package/dist/engines/types.js.map +1 -0
  30. package/dist/index.js +562 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/sync.js +18 -0
  33. package/dist/sync.js.map +1 -0
  34. package/dist/types/fylo.d.ts +179 -0
  35. package/{src → dist}/types/node-runtime.d.ts +1 -0
  36. package/package.json +3 -6
  37. package/.env.example +0 -16
  38. package/.github/copilot-instructions.md +0 -3
  39. package/.github/prompts/release.prompt.md +0 -10
  40. package/.github/workflows/ci.yml +0 -37
  41. package/.github/workflows/publish.yml +0 -91
  42. package/.prettierrc +0 -7
  43. package/AGENTS.md +0 -3
  44. package/CLAUDE.md +0 -3
  45. package/eslint.config.js +0 -32
  46. package/src/CLI +0 -39
  47. package/src/adapters/cipher.ts +0 -180
  48. package/src/adapters/redis.ts +0 -487
  49. package/src/adapters/s3.ts +0 -61
  50. package/src/core/collection.ts +0 -5
  51. package/src/core/directory.ts +0 -387
  52. package/src/core/extensions.ts +0 -21
  53. package/src/core/format.ts +0 -457
  54. package/src/core/parser.ts +0 -901
  55. package/src/core/query.ts +0 -53
  56. package/src/core/walker.ts +0 -174
  57. package/src/core/write-queue.ts +0 -59
  58. package/src/engines/s3-files.ts +0 -1068
  59. package/src/engines/types.ts +0 -21
  60. package/src/index.ts +0 -1727
  61. package/src/migrate-cli.ts +0 -22
  62. package/src/migrate.ts +0 -74
  63. package/src/types/fylo.d.ts +0 -261
  64. package/src/types/write-queue.ts +0 -42
  65. package/src/worker.ts +0 -18
  66. package/src/workers/write-worker.ts +0 -120
  67. package/tests/collection/truncate.test.js +0 -35
  68. package/tests/data.js +0 -97
  69. package/tests/index.js +0 -14
  70. package/tests/integration/aws-s3-files.canary.test.js +0 -22
  71. package/tests/integration/create.test.js +0 -39
  72. package/tests/integration/delete.test.js +0 -95
  73. package/tests/integration/edge-cases.test.js +0 -158
  74. package/tests/integration/encryption.test.js +0 -131
  75. package/tests/integration/export.test.js +0 -46
  76. package/tests/integration/join-modes.test.js +0 -154
  77. package/tests/integration/migration.test.js +0 -38
  78. package/tests/integration/nested.test.js +0 -142
  79. package/tests/integration/operators.test.js +0 -122
  80. package/tests/integration/queue.test.js +0 -83
  81. package/tests/integration/read.test.js +0 -119
  82. package/tests/integration/rollback.test.js +0 -60
  83. package/tests/integration/s3-files.test.js +0 -192
  84. package/tests/integration/update.test.js +0 -99
  85. package/tests/mocks/cipher.js +0 -40
  86. package/tests/mocks/redis.js +0 -123
  87. package/tests/mocks/s3.js +0 -80
  88. package/tests/schemas/album.d.ts +0 -5
  89. package/tests/schemas/album.json +0 -5
  90. package/tests/schemas/comment.d.ts +0 -7
  91. package/tests/schemas/comment.json +0 -7
  92. package/tests/schemas/photo.d.ts +0 -7
  93. package/tests/schemas/photo.json +0 -7
  94. package/tests/schemas/post.d.ts +0 -6
  95. package/tests/schemas/post.json +0 -6
  96. package/tests/schemas/tip.d.ts +0 -7
  97. package/tests/schemas/tip.json +0 -7
  98. package/tests/schemas/todo.d.ts +0 -6
  99. package/tests/schemas/todo.json +0 -6
  100. package/tests/schemas/user.d.ts +0 -23
  101. package/tests/schemas/user.json +0 -23
  102. package/tsconfig.json +0 -21
  103. package/tsconfig.typecheck.json +0 -31
  104. /package/{src → dist}/types/bun-runtime.d.ts +0 -0
  105. /package/{src → dist}/types/index.d.ts +0 -0
  106. /package/{src → dist}/types/query.d.ts +0 -0
  107. /package/{src → dist}/types/vendor-modules.d.ts +0 -0
@@ -1,387 +0,0 @@
1
- import { Walker } from './walker'
2
- import TTID from '@delma/ttid'
3
- import { S3 } from '../adapters/s3'
4
- import { Redis } from '../adapters/redis'
5
- import { Cipher } from '../adapters/cipher'
6
-
7
- export class Dir {
8
- private static readonly KEY_LIMIT = 1024
9
-
10
- private static readonly SLASH_ASCII = '%2F'
11
-
12
- private readonly transactions: Array<{
13
- action: (...args: string[]) => Promise<void>
14
- args: string[]
15
- }>
16
-
17
- private static _redis: Redis | null = null
18
-
19
- private static get redis(): Redis {
20
- if (!Dir._redis) Dir._redis = new Redis()
21
- return Dir._redis
22
- }
23
-
24
- constructor() {
25
- this.transactions = []
26
- }
27
-
28
- static async claimTTID(_id: _ttid, ttlSeconds: number = 10): Promise<boolean> {
29
- return await Dir.redis.claimTTID(_id, ttlSeconds)
30
- }
31
-
32
- static async reconstructData(collection: string, items: string[]) {
33
- items = await this.readValues(collection, items)
34
-
35
- let fieldVal: Record<string, string> = {}
36
-
37
- for (const data of items) {
38
- const segs = data.split('/')
39
- const val = segs.pop()!
40
- const fieldPath = segs.join('/')
41
-
42
- // Decrypt value if field is encrypted — fieldPath starts with TTID segment
43
- // so strip it to get the actual field name for the check
44
- const fieldOnly = segs.slice(1).join('/')
45
- if (Cipher.isConfigured() && Cipher.isEncryptedField(collection, fieldOnly)) {
46
- fieldVal[fieldPath] = await Cipher.decrypt(val)
47
- } else {
48
- fieldVal[fieldPath] = val
49
- }
50
- }
51
-
52
- return this.constructData(fieldVal)
53
- }
54
-
55
- private static async readValues(collection: string, items: string[]) {
56
- for (let i = 0; i < items.length; i++) {
57
- const segments = items[i].split('/')
58
-
59
- const filename = segments.pop()!
60
-
61
- if (TTID.isUUID(filename)) {
62
- const file = S3.file(collection, items[i])
63
- const val = await file.text()
64
-
65
- items[i] = `${segments.join('/')}/${val}`
66
- }
67
- }
68
-
69
- return items
70
- }
71
-
72
- private static async filterByTimestamp(
73
- _id: _ttid,
74
- indexes: string[],
75
- { updated, created }: { updated?: _timestamp; created?: _timestamp }
76
- ) {
77
- const { createdAt, updatedAt } = TTID.decodeTime(_id)
78
-
79
- if (updated && updatedAt) {
80
- if ((updated.$gt || updated.$gte) && (updated.$lt || updated.$lte)) {
81
- if (updated.$gt && updated.$lt) {
82
- if (updated.$gt! > updated.$lt!) throw new Error('Invalid updated query')
83
-
84
- indexes = updatedAt > updated.$gt! && updatedAt < updated.$lt! ? indexes : []
85
- } else if (updated.$gt && updated.$lte) {
86
- if (updated.$gt! > updated.$lte!) throw new Error('Invalid updated query')
87
-
88
- indexes = updatedAt > updated.$gt! && updatedAt <= updated.$lte! ? indexes : []
89
- } else if (updated.$gte && updated.$lt) {
90
- if (updated.$gte! > updated.$lt!) throw new Error('Invalid updated query')
91
-
92
- indexes = updatedAt >= updated.$gte! && updatedAt < updated.$lt! ? indexes : []
93
- } else if (updated.$gte && updated.$lte) {
94
- if (updated.$gte! > updated.$lte!) throw new Error('Invalid updated query')
95
-
96
- indexes =
97
- updatedAt >= updated.$gte! && updatedAt <= updated.$lte! ? indexes : []
98
- }
99
- } else if ((updated.$gt || updated.$gte) && !updated.$lt && !updated.$lte) {
100
- indexes = updated.$gt
101
- ? updatedAt > updated.$gt!
102
- ? indexes
103
- : []
104
- : updatedAt >= updated.$gte!
105
- ? indexes
106
- : []
107
- } else if (!updated.$gt && !updated.$gte && (updated.$lt || updated.$lte)) {
108
- indexes = updated.$lt
109
- ? updatedAt < updated.$lt!
110
- ? indexes
111
- : []
112
- : updatedAt <= updated.$lte!
113
- ? indexes
114
- : []
115
- }
116
- }
117
-
118
- if (created) {
119
- if ((created.$gt || created.$gte) && (created.$lt || created.$lte)) {
120
- if (created.$gt && created.$lt) {
121
- if (created.$gt! > created.$lt!) throw new Error('Invalid created query')
122
-
123
- indexes = createdAt > created.$gt! && createdAt < created.$lt! ? indexes : []
124
- } else if (created.$gt && created.$lte) {
125
- if (created.$gt! > created.$lte!) throw new Error('Invalid updated query')
126
-
127
- indexes = createdAt > created.$gt! && createdAt <= created.$lte! ? indexes : []
128
- } else if (created.$gte && created.$lt) {
129
- if (created.$gte! > created.$lt!) throw new Error('Invalid updated query')
130
-
131
- indexes = createdAt >= created.$gte! && createdAt < created.$lt! ? indexes : []
132
- } else if (created.$gte && created.$lte) {
133
- if (created.$gte! > created.$lte!) throw new Error('Invalid updated query')
134
-
135
- indexes =
136
- createdAt >= created.$gte! && createdAt <= created.$lte! ? indexes : []
137
- }
138
- } else if ((created.$gt || created.$gte) && !created.$lt && !created.$lte) {
139
- if (created.$gt) indexes = createdAt > created.$gt! ? indexes : []
140
- else if (created.$gte) indexes = createdAt >= created.$gte! ? indexes : []
141
- } else if (!created.$gt && !created.$gte && (created.$lt || created.$lte)) {
142
- if (created.$lt) indexes = createdAt < created.$lt! ? indexes : []
143
- else if (created.$lte) indexes = createdAt <= created.$lte! ? indexes : []
144
- }
145
- }
146
-
147
- return indexes.length > 0
148
- }
149
-
150
- static async *searchDocs<T extends Record<string, any>>(
151
- collection: string,
152
- pattern: string | string[],
153
- { updated, created }: { updated?: _timestamp; created?: _timestamp },
154
- { listen = false, skip = false }: { listen: boolean; skip: boolean },
155
- deleted: boolean = false
156
- ): AsyncGenerator<Record<_ttid, T> | _ttid | void, void, { count: number; limit?: number }> {
157
- const data = yield
158
- let count = data.count
159
- let limit = data.limit
160
-
161
- const constructData = async (collection: string, _id: _ttid, items: string[]) => {
162
- if (created || updated) {
163
- if (await this.filterByTimestamp(_id, items, { created, updated })) {
164
- const data = await this.reconstructData(collection, items)
165
-
166
- return { [_id]: data } as Record<_ttid, T>
167
- } else return {}
168
- } else {
169
- const data = await this.reconstructData(collection, items)
170
-
171
- return { [_id]: data } as Record<_ttid, T>
172
- }
173
- }
174
-
175
- const processQuery = async function* (
176
- p: string
177
- ): AsyncGenerator<
178
- Record<_ttid, T> | _ttid | void,
179
- void,
180
- { count: number; limit?: number }
181
- > {
182
- let finished = false
183
-
184
- if (listen && !deleted) {
185
- const iter = Walker.search(collection, p, { listen, skip })
186
-
187
- do {
188
- const { value, done } = await iter.next({ count, limit })
189
-
190
- if (done) finished = true
191
-
192
- if (value) {
193
- const data = yield await constructData(collection, value._id, value.data)
194
- count = data.count
195
- limit = data.limit
196
- }
197
- } while (!finished)
198
- } else if (listen && deleted) {
199
- const iter = Walker.search(collection, p, { listen, skip }, 'delete')
200
-
201
- do {
202
- const { value, done } = await iter.next({ count, limit })
203
-
204
- if (done) finished = true
205
-
206
- if (value) {
207
- const data = yield value._id
208
- count = data.count
209
- limit = data.limit
210
- }
211
- } while (!finished)
212
- } else {
213
- const iter = Walker.search(collection, p, { listen, skip })
214
-
215
- do {
216
- const { value, done } = await iter.next({ count, limit })
217
-
218
- if (done) finished = true
219
-
220
- if (value) {
221
- const data = yield await constructData(collection, value._id, value.data)
222
- count = data.count
223
- limit = data.limit
224
- }
225
- } while (!finished)
226
- }
227
- }
228
-
229
- if (Array.isArray(pattern)) {
230
- for (const p of pattern) yield* processQuery(p)
231
- } else yield* processQuery(pattern)
232
- }
233
-
234
- async putKeys(
235
- collection: string,
236
- { dataKey, indexKey }: { dataKey: string; indexKey: string }
237
- ) {
238
- let dataBody: string | undefined
239
- let indexBody: string | undefined
240
-
241
- if (dataKey.length > Dir.KEY_LIMIT) {
242
- const dataSegs = dataKey.split('/')
243
-
244
- dataBody = dataSegs.pop()!
245
-
246
- indexKey = `${dataSegs.join('/')}/${Bun.randomUUIDv7()}`
247
- }
248
-
249
- if (indexKey.length > Dir.KEY_LIMIT) {
250
- const indexSegs = indexKey.split('/')
251
-
252
- const _id = indexSegs.pop()! as _ttid
253
-
254
- indexBody = indexSegs.pop()!
255
-
256
- dataKey = `${indexSegs.join('/')}/${_id}`
257
- }
258
-
259
- await Promise.all([
260
- S3.put(collection, dataKey, dataBody ?? ''),
261
- S3.put(collection, indexKey, indexBody ?? '')
262
- ])
263
-
264
- this.transactions.push({
265
- action: S3.delete,
266
- args: [collection, dataKey]
267
- })
268
-
269
- this.transactions.push({
270
- action: S3.delete,
271
- args: [collection, indexKey]
272
- })
273
-
274
- await Dir.redis.publish(collection, 'insert', indexKey)
275
- }
276
-
277
- async executeRollback() {
278
- do {
279
- const transaction = this.transactions.pop()
280
-
281
- if (transaction) {
282
- const { action, args } = transaction
283
-
284
- await action(...args)
285
- }
286
- } while (this.transactions.length > 0)
287
- }
288
-
289
- async deleteKeys(collection: string, dataKey: string) {
290
- const segments = dataKey.split('/')
291
-
292
- const _id = segments.shift()!
293
-
294
- const indexKey = `${segments.join('/')}/${_id}`
295
-
296
- const dataFile = S3.file(collection, dataKey)
297
- const indexFile = S3.file(collection, indexKey)
298
-
299
- let dataBody: string | undefined
300
- let indexBody: string | undefined
301
-
302
- if (dataFile.size > 0) dataBody = await dataFile.text()
303
- if (indexFile.size > 0) indexBody = await indexFile.text()
304
-
305
- await Promise.all([S3.delete(collection, indexKey), S3.delete(collection, dataKey)])
306
-
307
- this.transactions.push({
308
- action: S3.put,
309
- args: [collection, dataKey, dataBody ?? '']
310
- })
311
-
312
- this.transactions.push({
313
- action: S3.put,
314
- args: [collection, indexKey, indexBody ?? '']
315
- })
316
-
317
- await Dir.redis.publish(collection, 'delete', _id)
318
- }
319
-
320
- static async extractKeys<T>(collection: string, _id: _ttid, data: T, parentField?: string) {
321
- const keys: { data: string[]; indexes: string[] } = { data: [], indexes: [] }
322
-
323
- const obj = { ...data }
324
-
325
- for (const field in obj) {
326
- const newField = parentField ? `${parentField}/${field}` : field
327
-
328
- if (typeof obj[field] === 'object' && !Array.isArray(obj[field])) {
329
- const items = await this.extractKeys(collection, _id, obj[field], newField)
330
- keys.data.push(...items.data)
331
- keys.indexes.push(...items.indexes)
332
- } else if (typeof obj[field] === 'object' && Array.isArray(obj[field])) {
333
- const items: (string | number | boolean)[] = obj[field]
334
- if (items.some((item) => typeof item === 'object'))
335
- throw new Error(`Cannot have an array of objects`)
336
- for (let i = 0; i < items.length; i++) {
337
- let val = String(items[i]).split('/').join(this.SLASH_ASCII)
338
- if (Cipher.isConfigured() && Cipher.isEncryptedField(collection, newField))
339
- val = await Cipher.encrypt(val, true)
340
- keys.data.push(`${_id}/${newField}/${i}/${val}`)
341
- keys.indexes.push(`${newField}/${i}/${val}/${_id}`)
342
- }
343
- } else {
344
- let val = String(obj[field]).replaceAll('/', this.SLASH_ASCII)
345
- if (Cipher.isConfigured() && Cipher.isEncryptedField(collection, newField))
346
- val = await Cipher.encrypt(val, true)
347
- keys.data.push(`${_id}/${newField}/${val}`)
348
- keys.indexes.push(`${newField}/${val}/${_id}`)
349
- }
350
- }
351
-
352
- return keys
353
- }
354
-
355
- static constructData(fieldVal: Record<string, string>) {
356
- const data: Record<string, any> = {}
357
-
358
- for (let fullField in fieldVal) {
359
- const fields = fullField.split('/').slice(1)
360
-
361
- let curr = data
362
-
363
- while (fields.length > 1) {
364
- const field = fields.shift()!
365
-
366
- if (typeof curr[field] !== 'object' || curr[field] === null)
367
- curr[field] = isNaN(Number(fields[0])) ? {} : []
368
-
369
- curr = curr[field]
370
- }
371
-
372
- const lastKey = fields.shift()!
373
-
374
- curr[lastKey] = this.parseValue(fieldVal[fullField].replaceAll(this.SLASH_ASCII, '/'))
375
- }
376
-
377
- return data
378
- }
379
-
380
- static parseValue(value: string) {
381
- try {
382
- return JSON.parse(value)
383
- } catch {
384
- return value
385
- }
386
- }
387
- }
@@ -1,21 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
-
3
- Object.appendGroup = function (
4
- target: Record<string, any>,
5
- source: Record<string, any>
6
- ): Record<string, any> {
7
- const result = { ...target }
8
-
9
- for (const [sourceId, sourceGroup] of Object.entries(source)) {
10
- if (!result[sourceId]) {
11
- result[sourceId] = sourceGroup
12
- continue
13
- }
14
-
15
- for (const [groupId, groupDoc] of Object.entries(sourceGroup)) {
16
- result[sourceId][groupId] = groupDoc
17
- }
18
- }
19
-
20
- return result
21
- }