@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.
Files changed (74) hide show
  1. package/.github/copilot-instructions.md +1 -1
  2. package/.github/prompts/release.prompt.md +4 -43
  3. package/AGENTS.md +1 -1
  4. package/CLAUDE.md +1 -1
  5. package/README.md +141 -62
  6. package/eslint.config.js +8 -4
  7. package/package.json +9 -7
  8. package/src/CLI +16 -14
  9. package/src/adapters/cipher.ts +12 -6
  10. package/src/adapters/redis.ts +193 -123
  11. package/src/adapters/s3.ts +6 -12
  12. package/src/core/collection.ts +5 -0
  13. package/src/core/directory.ts +120 -151
  14. package/src/core/extensions.ts +4 -2
  15. package/src/core/format.ts +390 -419
  16. package/src/core/parser.ts +167 -142
  17. package/src/core/query.ts +31 -26
  18. package/src/core/walker.ts +68 -61
  19. package/src/core/write-queue.ts +7 -4
  20. package/src/engines/s3-files.ts +888 -0
  21. package/src/engines/types.ts +21 -0
  22. package/src/index.ts +754 -378
  23. package/src/migrate-cli.ts +22 -0
  24. package/src/migrate.ts +74 -0
  25. package/src/types/bun-runtime.d.ts +73 -0
  26. package/src/types/fylo.d.ts +115 -27
  27. package/src/types/node-runtime.d.ts +61 -0
  28. package/src/types/query.d.ts +6 -2
  29. package/src/types/vendor-modules.d.ts +8 -7
  30. package/src/worker.ts +7 -1
  31. package/src/workers/write-worker.ts +25 -24
  32. package/tests/collection/truncate.test.js +35 -0
  33. package/tests/{data.ts → data.js} +8 -21
  34. package/tests/{index.ts → index.js} +4 -9
  35. package/tests/integration/aws-s3-files.canary.test.js +22 -0
  36. package/tests/integration/{create.test.ts → create.test.js} +13 -31
  37. package/tests/integration/delete.test.js +95 -0
  38. package/tests/integration/{edge-cases.test.ts → edge-cases.test.js} +50 -124
  39. package/tests/integration/{encryption.test.ts → encryption.test.js} +20 -65
  40. package/tests/integration/{export.test.ts → export.test.js} +8 -23
  41. package/tests/integration/{join-modes.test.ts → join-modes.test.js} +37 -104
  42. package/tests/integration/migration.test.js +38 -0
  43. package/tests/integration/nested.test.js +142 -0
  44. package/tests/integration/operators.test.js +122 -0
  45. package/tests/integration/{queue.test.ts → queue.test.js} +24 -40
  46. package/tests/integration/read.test.js +119 -0
  47. package/tests/integration/rollback.test.js +60 -0
  48. package/tests/integration/s3-files.test.js +108 -0
  49. package/tests/integration/update.test.js +99 -0
  50. package/tests/mocks/{cipher.ts → cipher.js} +11 -26
  51. package/tests/mocks/redis.js +123 -0
  52. package/tests/mocks/{s3.ts → s3.js} +24 -58
  53. package/tests/schemas/album.json +1 -1
  54. package/tests/schemas/comment.json +1 -1
  55. package/tests/schemas/photo.json +1 -1
  56. package/tests/schemas/post.json +1 -1
  57. package/tests/schemas/tip.json +1 -1
  58. package/tests/schemas/todo.json +1 -1
  59. package/tests/schemas/user.d.ts +12 -12
  60. package/tests/schemas/user.json +1 -1
  61. package/tsconfig.json +4 -2
  62. package/tsconfig.typecheck.json +31 -0
  63. package/.github/prompts/issue.prompt.md +0 -19
  64. package/.github/prompts/pr.prompt.md +0 -18
  65. package/.github/prompts/review-pr.prompt.md +0 -19
  66. package/.github/prompts/sync-main.prompt.md +0 -14
  67. package/tests/collection/truncate.test.ts +0 -56
  68. package/tests/integration/delete.test.ts +0 -147
  69. package/tests/integration/nested.test.ts +0 -212
  70. package/tests/integration/operators.test.ts +0 -167
  71. package/tests/integration/read.test.ts +0 -203
  72. package/tests/integration/rollback.test.ts +0 -105
  73. package/tests/integration/update.test.ts +0 -130
  74. package/tests/mocks/redis.ts +0 -169
@@ -0,0 +1,35 @@
1
+ import { test, expect, describe, afterAll, mock } from 'bun:test'
2
+ import Fylo from '../../src'
3
+ import { postsURL, albumURL } from '../data'
4
+ import S3Mock from '../mocks/s3'
5
+ import RedisMock from '../mocks/redis'
6
+ const POSTS = `post`
7
+ const ALBUMS = `album`
8
+ afterAll(async () => {
9
+ await Promise.all([Fylo.dropCollection(ALBUMS), Fylo.dropCollection(POSTS)])
10
+ })
11
+ mock.module('../../src/adapters/s3', () => ({ S3: S3Mock }))
12
+ mock.module('../../src/adapters/redis', () => ({ Redis: RedisMock }))
13
+ describe('NO-SQL', () => {
14
+ test('TRUNCATE', async () => {
15
+ const fylo = new Fylo()
16
+ await Fylo.createCollection(POSTS)
17
+ await fylo.importBulkData(POSTS, new URL(postsURL))
18
+ await fylo.delDocs(POSTS)
19
+ const ids = []
20
+ for await (const data of Fylo.findDocs(POSTS, { $limit: 1, $onlyIds: true }).collect()) {
21
+ ids.push(data)
22
+ }
23
+ expect(ids.length).toBe(0)
24
+ })
25
+ })
26
+ describe('SQL', () => {
27
+ test('TRUNCATE', async () => {
28
+ const fylo = new Fylo()
29
+ await fylo.executeSQL(`CREATE TABLE ${ALBUMS}`)
30
+ await fylo.importBulkData(ALBUMS, new URL(albumURL))
31
+ await fylo.executeSQL(`DELETE FROM ${ALBUMS}`)
32
+ const ids = await fylo.executeSQL(`SELECT _id FROM ${ALBUMS} LIMIT 1`)
33
+ expect(ids.length).toBe(0)
34
+ })
35
+ })
@@ -1,13 +1,11 @@
1
- function makeDataUrl(data: unknown): string {
1
+ function makeDataUrl(data) {
2
2
  return `data:application/json,${encodeURIComponent(JSON.stringify(data))}`
3
3
  }
4
-
5
- function generateAlbums(): _album[] {
4
+ function generateAlbums() {
6
5
  return Array.from({ length: 100 }, (_, index) => {
7
6
  const id = index + 1
8
7
  const userId = Math.ceil(id / 10)
9
8
  const prefix = id <= 15 ? 'omnis' : id % 4 === 0 ? 'quidem' : 'album'
10
-
11
9
  return {
12
10
  id,
13
11
  userId,
@@ -15,12 +13,10 @@ function generateAlbums(): _album[] {
15
13
  }
16
14
  })
17
15
  }
18
-
19
- function generatePosts(): _post[] {
16
+ function generatePosts() {
20
17
  return Array.from({ length: 100 }, (_, index) => {
21
18
  const id = index + 1
22
19
  const userId = Math.ceil(id / 10)
23
-
24
20
  return {
25
21
  id,
26
22
  userId,
@@ -29,11 +25,9 @@ function generatePosts(): _post[] {
29
25
  }
30
26
  })
31
27
  }
32
-
33
- function generateComments(): _comment[] {
28
+ function generateComments() {
34
29
  return Array.from({ length: 100 }, (_, index) => {
35
30
  const id = index + 1
36
-
37
31
  return {
38
32
  id,
39
33
  postId: id,
@@ -43,12 +37,10 @@ function generateComments(): _comment[] {
43
37
  }
44
38
  })
45
39
  }
46
-
47
- function generatePhotos(): _photo[] {
40
+ function generatePhotos() {
48
41
  return Array.from({ length: 100 }, (_, index) => {
49
42
  const id = index + 1
50
43
  const title = id % 3 === 0 ? `test photo ${id}` : `photo ${id}`
51
-
52
44
  return {
53
45
  id,
54
46
  albumId: Math.ceil(id / 10),
@@ -58,11 +50,9 @@ function generatePhotos(): _photo[] {
58
50
  }
59
51
  })
60
52
  }
61
-
62
- function generateTodos(): _todo[] {
53
+ function generateTodos() {
63
54
  return Array.from({ length: 100 }, (_, index) => {
64
55
  const id = index + 1
65
-
66
56
  return {
67
57
  id,
68
58
  userId: Math.ceil(id / 10),
@@ -71,11 +61,9 @@ function generateTodos(): _todo[] {
71
61
  }
72
62
  })
73
63
  }
74
-
75
- function generateUsers(): _user[] {
64
+ function generateUsers() {
76
65
  return Array.from({ length: 10 }, (_, index) => {
77
66
  const id = index + 1
78
-
79
67
  return {
80
68
  id,
81
69
  name: `User ${id}`,
@@ -98,10 +86,9 @@ function generateUsers(): _user[] {
98
86
  catchPhrase: `Catch phrase ${id}`,
99
87
  bs: `business ${id}`
100
88
  }
101
- } as unknown as _user
89
+ }
102
90
  })
103
91
  }
104
-
105
92
  export const albumURL = makeDataUrl(generateAlbums())
106
93
  export const postsURL = makeDataUrl(generatePosts())
107
94
  export const commentsURL = makeDataUrl(generateComments())
@@ -1,19 +1,14 @@
1
1
  import { Redis } from '../src/adapters/redis'
2
2
  import ttid from '@delma/ttid'
3
-
4
3
  const redisPub = new Redis()
5
4
  const redisSub = new Redis()
6
-
7
5
  setTimeout(async () => {
8
- await redisPub.publish("bun", "insert", ttid.generate())
6
+ await redisPub.publish('bun', 'insert', ttid.generate())
9
7
  }, 2000)
10
-
11
8
  setTimeout(async () => {
12
- await redisPub.publish("bun", "insert", ttid.generate())
9
+ await redisPub.publish('bun', 'insert', ttid.generate())
13
10
  }, 3000)
14
-
15
11
  await Bun.sleep(1000)
16
-
17
- for await (const data of redisSub.subscribe("bun")) {
18
- console.log("Received:", data)
12
+ for await (const data of redisSub.subscribe('bun')) {
13
+ console.log('Received:', data)
19
14
  }
@@ -0,0 +1,22 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import Fylo from '../../src'
3
+ const runCanary = process.env.FYLO_RUN_S3FILES_CANARY === 'true'
4
+ const canaryTest = runCanary ? test : test.skip
5
+ describe('aws s3-files canary', () => {
6
+ canaryTest('mounted S3 Files root handles a real CRUD cycle', async () => {
7
+ const collection = `canary_${Date.now()}`
8
+ const fylo = new Fylo({
9
+ engine: 's3-files',
10
+ s3FilesRoot: process.env.FYLO_S3FILES_ROOT
11
+ })
12
+ await fylo.createCollection(collection)
13
+ const id = await fylo.putData(collection, {
14
+ title: 'canary',
15
+ tags: ['aws', 's3-files']
16
+ })
17
+ const doc = await fylo.getDoc(collection, id).once()
18
+ expect(doc[id].title).toBe('canary')
19
+ await fylo.delDoc(collection, id)
20
+ await fylo.dropCollection(collection)
21
+ })
22
+ })
@@ -3,55 +3,37 @@ import Fylo from '../../src'
3
3
  import { albumURL, postsURL } from '../data'
4
4
  import S3Mock from '../mocks/s3'
5
5
  import RedisMock from '../mocks/redis'
6
-
7
6
  const POSTS = `post`
8
7
  const ALBUMS = `album`
9
-
10
8
  let postsCount = 0
11
9
  let albumsCount = 0
12
-
13
10
  const fylo = new Fylo()
14
-
15
11
  mock.module('../../src/adapters/s3', () => ({ S3: S3Mock }))
16
12
  mock.module('../../src/adapters/redis', () => ({ Redis: RedisMock }))
17
-
18
13
  beforeAll(async () => {
19
-
20
- await Promise.all([Fylo.createCollection(POSTS), fylo.executeSQL<_user>(`CREATE TABLE ${ALBUMS}`)])
21
-
14
+ await Promise.all([Fylo.createCollection(POSTS), fylo.executeSQL(`CREATE TABLE ${ALBUMS}`)])
22
15
  try {
23
- albumsCount = await fylo.importBulkData<_album>(ALBUMS, new URL(albumURL), 100)
24
- postsCount = await fylo.importBulkData<_post>(POSTS, new URL(postsURL), 100)
16
+ albumsCount = await fylo.importBulkData(ALBUMS, new URL(albumURL), 100)
17
+ postsCount = await fylo.importBulkData(POSTS, new URL(postsURL), 100)
25
18
  } catch {
26
19
  await fylo.rollback()
27
20
  }
28
21
  })
29
-
30
22
  afterAll(async () => {
31
- await Promise.all([Fylo.dropCollection(POSTS), fylo.executeSQL<_album>(`DROP TABLE ${ALBUMS}`)])
23
+ await Promise.all([Fylo.dropCollection(POSTS), fylo.executeSQL(`DROP TABLE ${ALBUMS}`)])
32
24
  })
33
-
34
- describe("NO-SQL", async () => {
35
-
36
- test("PUT", async () => {
37
-
38
- let results: Record<_ttid, _post> = {}
39
-
40
- for await (const data of Fylo.findDocs<_post>(POSTS).collect()) {
41
-
42
- results = { ...results, ...data as Record<_ttid, _post> }
25
+ describe('NO-SQL', async () => {
26
+ test('PUT', async () => {
27
+ let results = {}
28
+ for await (const data of Fylo.findDocs(POSTS).collect()) {
29
+ results = { ...results, ...data }
43
30
  }
44
-
45
31
  expect(Object.keys(results).length).toEqual(postsCount)
46
32
  })
47
33
  })
48
-
49
- describe("SQL", () => {
50
-
51
- test("INSERT", async () => {
52
-
53
- const results = await fylo.executeSQL<_album>(`SELECT * FROM ${ALBUMS}`) as Record<_ttid, _album>
54
-
34
+ describe('SQL', () => {
35
+ test('INSERT', async () => {
36
+ const results = await fylo.executeSQL(`SELECT * FROM ${ALBUMS}`)
55
37
  expect(Object.keys(results).length).toEqual(albumsCount)
56
38
  })
57
- })
39
+ })
@@ -0,0 +1,95 @@
1
+ import { test, expect, describe, beforeAll, afterAll, mock } from 'bun:test'
2
+ import Fylo from '../../src'
3
+ import { commentsURL, usersURL } from '../data'
4
+ import S3Mock from '../mocks/s3'
5
+ import RedisMock from '../mocks/redis'
6
+ const COMMENTS = `comment`
7
+ const USERS = `user`
8
+ let commentsResults = {}
9
+ let usersResults = {}
10
+ const fylo = new Fylo()
11
+ mock.module('../../src/adapters/s3', () => ({ S3: S3Mock }))
12
+ mock.module('../../src/adapters/redis', () => ({ Redis: RedisMock }))
13
+ beforeAll(async () => {
14
+ await Promise.all([Fylo.createCollection(COMMENTS), fylo.executeSQL(`CREATE TABLE ${USERS}`)])
15
+ try {
16
+ await Promise.all([
17
+ fylo.importBulkData(COMMENTS, new URL(commentsURL), 100),
18
+ fylo.importBulkData(USERS, new URL(usersURL), 100)
19
+ ])
20
+ } catch {
21
+ await fylo.rollback()
22
+ }
23
+ for await (const data of Fylo.findDocs(COMMENTS, { $limit: 1 }).collect()) {
24
+ commentsResults = { ...commentsResults, ...data }
25
+ }
26
+ usersResults = await fylo.executeSQL(`SELECT * FROM ${USERS} LIMIT 1`)
27
+ })
28
+ afterAll(async () => {
29
+ await Promise.all([Fylo.dropCollection(COMMENTS), fylo.executeSQL(`DROP TABLE ${USERS}`)])
30
+ })
31
+ describe('NO-SQL', async () => {
32
+ test('DELETE ONE', async () => {
33
+ const id = Object.keys(commentsResults).shift()
34
+ try {
35
+ await fylo.delDoc(COMMENTS, id)
36
+ } catch {
37
+ await fylo.rollback()
38
+ }
39
+ commentsResults = {}
40
+ for await (const data of Fylo.findDocs(COMMENTS).collect()) {
41
+ commentsResults = { ...commentsResults, ...data }
42
+ }
43
+ const idx = Object.keys(commentsResults).findIndex((_id) => _id === id)
44
+ expect(idx).toEqual(-1)
45
+ })
46
+ test('DELETE CLAUSE', async () => {
47
+ try {
48
+ await fylo.delDocs(COMMENTS, { $ops: [{ name: { $like: '%et%' } }] })
49
+ } catch (e) {
50
+ console.error(e)
51
+ await fylo.rollback()
52
+ }
53
+ commentsResults = {}
54
+ for await (const data of Fylo.findDocs(COMMENTS, {
55
+ $ops: [{ name: { $like: '%et%' } }]
56
+ }).collect()) {
57
+ commentsResults = { ...commentsResults, ...data }
58
+ }
59
+ expect(Object.keys(commentsResults).length).toEqual(0)
60
+ })
61
+ test('DELETE ALL', async () => {
62
+ try {
63
+ await fylo.delDocs(COMMENTS)
64
+ } catch {
65
+ await fylo.rollback()
66
+ }
67
+ commentsResults = {}
68
+ for await (const data of Fylo.findDocs(COMMENTS).collect()) {
69
+ commentsResults = { ...commentsResults, ...data }
70
+ }
71
+ expect(Object.keys(commentsResults).length).toEqual(0)
72
+ })
73
+ })
74
+ describe('SQL', async () => {
75
+ test('DELETE CLAUSE', async () => {
76
+ const name = Object.values(usersResults).shift().name
77
+ try {
78
+ await fylo.executeSQL(`DELETE FROM ${USERS} WHERE name = '${name}'`)
79
+ } catch {
80
+ await fylo.rollback()
81
+ }
82
+ usersResults = await fylo.executeSQL(`SELECT * FROM ${USERS} WHERE name = '${name}'`)
83
+ const idx = Object.values(usersResults).findIndex((com) => com.name === name)
84
+ expect(idx).toBe(-1)
85
+ })
86
+ test('DELETE ALL', async () => {
87
+ try {
88
+ await fylo.executeSQL(`DELETE FROM ${USERS}`)
89
+ } catch {
90
+ await fylo.rollback()
91
+ }
92
+ usersResults = await fylo.executeSQL(`SELECT * FROM ${USERS}`)
93
+ expect(Object.keys(usersResults).length).toBe(0)
94
+ })
95
+ })
@@ -3,230 +3,156 @@ import Fylo from '../../src'
3
3
  import TTID from '@delma/ttid'
4
4
  import S3Mock from '../mocks/s3'
5
5
  import RedisMock from '../mocks/redis'
6
-
7
- /**
8
- * Edge case coverage:
9
- * - Non-existent ID returns empty object
10
- * - Values containing forward slashes survive the SLASH_ASCII (%2F) round-trip
11
- * - Multiple $ops entries act as OR (union across patterns)
12
- * - $rename renames fields in query output
13
- * - Versioned putData (existing TTID as key) preserves the creation-time prefix
14
- * - SQL UPDATE ONE — update a single document by ID via SQL
15
- * - SQL DELETE ONE — delete a single document by ID via SQL
16
- */
17
-
18
6
  const COLLECTION = 'ec-test'
19
-
20
7
  const fylo = new Fylo()
21
-
22
8
  mock.module('../../src/adapters/s3', () => ({ S3: S3Mock }))
23
9
  mock.module('../../src/adapters/redis', () => ({ Redis: RedisMock }))
24
-
25
10
  beforeAll(async () => {
26
11
  await Fylo.createCollection(COLLECTION)
27
12
  })
28
-
29
13
  afterAll(async () => {
30
14
  await Fylo.dropCollection(COLLECTION)
31
15
  })
32
-
33
- describe("NO-SQL", () => {
34
-
35
- test("GET ONE — non-existent ID returns empty object", async () => {
36
-
37
- const fakeId = TTID.generate() as _ttid
38
-
16
+ describe('NO-SQL', () => {
17
+ test('GET ONE — non-existent ID returns empty object', async () => {
18
+ const fakeId = TTID.generate()
39
19
  const result = await Fylo.getDoc(COLLECTION, fakeId).once()
40
-
41
20
  expect(Object.keys(result).length).toBe(0)
42
21
  })
43
-
44
- test("PUT / GET — forward slashes in values round-trip correctly", async () => {
45
-
22
+ test('PUT / GET — forward slashes in values round-trip correctly', async () => {
46
23
  const original = {
47
24
  userId: 1,
48
25
  id: 1,
49
26
  title: 'Slash Test',
50
27
  body: 'https://example.com/api/v1/resource'
51
28
  }
52
-
53
- const _id = await fylo.putData<_post>(COLLECTION, original)
54
-
55
- const result = await Fylo.getDoc<_post>(COLLECTION, _id).once()
29
+ const _id = await fylo.putData(COLLECTION, original)
30
+ const result = await Fylo.getDoc(COLLECTION, _id).once()
56
31
  const doc = result[_id]
57
-
58
32
  expect(doc.body).toBe(original.body)
59
-
60
33
  await fylo.delDoc(COLLECTION, _id)
61
34
  })
62
-
63
- test("PUT / GET — values with multiple consecutive slashes round-trip correctly", async () => {
64
-
35
+ test('PUT / GET — values with multiple consecutive slashes round-trip correctly', async () => {
65
36
  const original = {
66
37
  userId: 1,
67
38
  id: 2,
68
39
  title: 'Double Slash',
69
40
  body: 'https://cdn.example.com//assets//image.png'
70
41
  }
71
-
72
- const _id = await fylo.putData<_post>(COLLECTION, original)
73
-
74
- const result = await Fylo.getDoc<_post>(COLLECTION, _id).once()
75
-
42
+ const _id = await fylo.putData(COLLECTION, original)
43
+ const result = await Fylo.getDoc(COLLECTION, _id).once()
76
44
  expect(result[_id].body).toBe(original.body)
77
-
78
45
  await fylo.delDoc(COLLECTION, _id)
79
46
  })
80
-
81
- test("$ops — multiple conditions act as OR union", async () => {
82
-
47
+ test('$ops — multiple conditions act as OR union', async () => {
83
48
  const cleanFylo = new Fylo()
84
-
85
- const id1 = await cleanFylo.putData<_post>(COLLECTION, { userId: 10, id: 100, title: 'Alpha', body: 'first' })
86
- const id2 = await cleanFylo.putData<_post>(COLLECTION, { userId: 20, id: 200, title: 'Beta', body: 'second' })
87
-
88
- const results: Record<_ttid, _post> = {}
89
-
90
- for await (const data of Fylo.findDocs<_post>(COLLECTION, {
91
- $ops: [
92
- { userId: { $eq: 10 } },
93
- { userId: { $eq: 20 } }
94
- ]
49
+ const id1 = await cleanFylo.putData(COLLECTION, {
50
+ userId: 10,
51
+ id: 100,
52
+ title: 'Alpha',
53
+ body: 'first'
54
+ })
55
+ const id2 = await cleanFylo.putData(COLLECTION, {
56
+ userId: 20,
57
+ id: 200,
58
+ title: 'Beta',
59
+ body: 'second'
60
+ })
61
+ const results = {}
62
+ for await (const data of Fylo.findDocs(COLLECTION, {
63
+ $ops: [{ userId: { $eq: 10 } }, { userId: { $eq: 20 } }]
95
64
  }).collect()) {
96
65
  Object.assign(results, data)
97
66
  }
98
-
99
67
  expect(results[id1]).toBeDefined()
100
68
  expect(results[id2]).toBeDefined()
101
-
102
69
  await cleanFylo.delDoc(COLLECTION, id1)
103
70
  await cleanFylo.delDoc(COLLECTION, id2)
104
71
  })
105
-
106
- test("$rename — renames fields in query output", async () => {
107
-
72
+ test('$rename — renames fields in query output', async () => {
108
73
  const cleanFylo = new Fylo()
109
-
110
- const _id = await cleanFylo.putData<_post>(COLLECTION, {
74
+ const _id = await cleanFylo.putData(COLLECTION, {
111
75
  userId: 1,
112
76
  id: 300,
113
77
  title: 'Rename Me',
114
78
  body: 'some body'
115
79
  })
116
-
117
- let renamed: Partial<_post> & { name?: string } = {}
118
-
119
- for await (const data of Fylo.findDocs<_post>(COLLECTION, {
80
+ let renamed = {}
81
+ for await (const data of Fylo.findDocs(COLLECTION, {
120
82
  $ops: [{ id: { $eq: 300 } }],
121
- $rename: { title: 'name' } as Record<keyof Partial<_post>, string>
83
+ $rename: { title: 'name' }
122
84
  }).collect()) {
123
- renamed = Object.values(data as Record<_ttid, typeof renamed>)[0]
85
+ renamed = Object.values(data)[0]
124
86
  }
125
-
126
87
  expect(renamed.name).toBe('Rename Me')
127
88
  expect(renamed.title).toBeUndefined()
128
-
129
89
  await cleanFylo.delDoc(COLLECTION, _id)
130
90
  })
131
-
132
- test("versioned putData — preserves creation-time prefix in TTID", async () => {
133
-
91
+ test('versioned putData — preserves creation-time prefix in TTID', async () => {
134
92
  const cleanFylo = new Fylo()
135
-
136
- const _id1 = await cleanFylo.putData<_post>(COLLECTION, {
93
+ const _id1 = await cleanFylo.putData(COLLECTION, {
137
94
  userId: 1,
138
95
  id: 400,
139
96
  title: 'Original',
140
97
  body: 'v1'
141
98
  })
142
-
143
- const _id2 = await cleanFylo.putData<_post>(COLLECTION, {
99
+ const _id2 = await cleanFylo.putData(COLLECTION, {
144
100
  [_id1]: { userId: 1, id: 400, title: 'Updated', body: 'v2' }
145
101
  })
146
-
147
- // The creation-time segment (before the first '-') must be identical
148
102
  expect(_id2.split('-')[0]).toBe(_id1.split('-')[0])
149
-
150
- // The updated doc is retrievable via its own TTID
151
- const result = await Fylo.getDoc<_post>(COLLECTION, _id2).once()
103
+ const result = await Fylo.getDoc(COLLECTION, _id2).once()
152
104
  const doc = result[_id2]
153
-
154
105
  expect(doc).toBeDefined()
155
106
  expect(doc.title).toBe('Updated')
156
-
157
- // Clean up both versions
158
107
  await cleanFylo.delDoc(COLLECTION, _id1)
159
108
  await cleanFylo.delDoc(COLLECTION, _id2)
160
109
  })
161
-
162
- test("versioned putData — original version is no longer retrievable by old full TTID", async () => {
163
-
110
+ test('versioned putData — original version is no longer retrievable by old full TTID', async () => {
164
111
  const cleanFylo = new Fylo()
165
-
166
- const _id1 = await cleanFylo.putData<_post>(COLLECTION, {
112
+ const _id1 = await cleanFylo.putData(COLLECTION, {
167
113
  userId: 1,
168
114
  id: 500,
169
115
  title: 'Old Version',
170
116
  body: 'original'
171
117
  })
172
-
173
- const _id2 = await cleanFylo.putData<_post>(COLLECTION, {
118
+ const _id2 = await cleanFylo.putData(COLLECTION, {
174
119
  [_id1]: { userId: 1, id: 500, title: 'New Version', body: 'updated' }
175
120
  })
176
-
177
- // Both IDs are different (update appended a segment)
178
121
  expect(_id1).not.toBe(_id2)
179
-
180
122
  await cleanFylo.delDoc(COLLECTION, _id2)
181
123
  })
182
124
  })
183
-
184
- describe("SQL", () => {
185
-
186
- test("UPDATE ONE — update a single document by querying its unique field", async () => {
187
-
125
+ describe('SQL', () => {
126
+ test('UPDATE ONE — update a single document by querying its unique field', async () => {
188
127
  const cleanFylo = new Fylo()
189
-
190
- await cleanFylo.putData<_post>(COLLECTION, {
128
+ await cleanFylo.putData(COLLECTION, {
191
129
  userId: 1,
192
130
  id: 600,
193
131
  title: 'Before SQL Update',
194
132
  body: 'original'
195
133
  })
196
-
197
- const updated = await cleanFylo.executeSQL<_post>(
134
+ const updated = await cleanFylo.executeSQL(
198
135
  `UPDATE ${COLLECTION} SET title = 'After SQL Update' WHERE id = 600`
199
- ) as number
200
-
136
+ )
201
137
  expect(updated).toBe(1)
202
-
203
- const results = await cleanFylo.executeSQL<_post>(
138
+ const results = await cleanFylo.executeSQL(
204
139
  `SELECT * FROM ${COLLECTION} WHERE title = 'After SQL Update'`
205
- ) as Record<_ttid, _post>
206
-
140
+ )
207
141
  expect(Object.keys(results).length).toBe(1)
208
142
  expect(Object.values(results)[0].title).toBe('After SQL Update')
209
143
  })
210
-
211
- test("DELETE ONE — delete a single document by querying its unique field", async () => {
212
-
144
+ test('DELETE ONE — delete a single document by querying its unique field', async () => {
213
145
  const cleanFylo = new Fylo()
214
-
215
- await cleanFylo.putData<_post>(COLLECTION, {
146
+ await cleanFylo.putData(COLLECTION, {
216
147
  userId: 1,
217
148
  id: 700,
218
149
  title: 'Delete Via SQL',
219
150
  body: 'should be removed'
220
151
  })
221
-
222
- await cleanFylo.executeSQL<_post>(
223
- `DELETE FROM ${COLLECTION} WHERE title = 'Delete Via SQL'`
224
- )
225
-
226
- const results = await cleanFylo.executeSQL<_post>(
152
+ await cleanFylo.executeSQL(`DELETE FROM ${COLLECTION} WHERE title = 'Delete Via SQL'`)
153
+ const results = await cleanFylo.executeSQL(
227
154
  `SELECT * FROM ${COLLECTION} WHERE title = 'Delete Via SQL'`
228
- ) as Record<_ttid, _post>
229
-
155
+ )
230
156
  expect(Object.keys(results).length).toBe(0)
231
157
  })
232
158
  })