odac 1.4.8 → 1.4.10

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 (37) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/docs/ai/README.md +2 -1
  3. package/docs/ai/skills/SKILL.md +2 -1
  4. package/docs/ai/skills/backend/authentication.md +12 -6
  5. package/docs/ai/skills/backend/database.md +85 -5
  6. package/docs/ai/skills/backend/migrations.md +23 -0
  7. package/docs/ai/skills/backend/odac-var.md +155 -0
  8. package/docs/ai/skills/backend/utilities.md +1 -1
  9. package/docs/ai/skills/frontend/forms.md +23 -1
  10. package/docs/backend/04-routing/09-websocket-quick-reference.md +21 -1
  11. package/docs/backend/04-routing/09-websocket.md +22 -1
  12. package/docs/backend/08-database/06-read-through-cache.md +206 -0
  13. package/docs/backend/10-authentication/01-authentication-basics.md +53 -0
  14. package/docs/backend/10-authentication/05-session-management.md +12 -3
  15. package/docs/backend/13-utilities/01-odac-var.md +13 -19
  16. package/docs/frontend/03-forms/01-form-handling.md +15 -2
  17. package/docs/index.json +1 -1
  18. package/package.json +1 -1
  19. package/src/Auth.js +17 -0
  20. package/src/Database/Migration.js +321 -10
  21. package/src/Database/ReadCache.js +174 -0
  22. package/src/Database/WriteBuffer.js +15 -1
  23. package/src/Database.js +78 -1
  24. package/src/Validator.js +1 -1
  25. package/src/Var.js +1 -0
  26. package/src/WebSocket.js +80 -23
  27. package/test/Database/Migration/migrate_column.test.js +311 -0
  28. package/test/Database/ReadCache/crossTable.test.js +179 -0
  29. package/test/Database/ReadCache/get.test.js +128 -0
  30. package/test/Database/ReadCache/invalidate.test.js +103 -0
  31. package/test/Database/ReadCache/proxy.test.js +184 -0
  32. package/test/Database/WriteBuffer/insert.test.js +118 -0
  33. package/test/Database/insert.test.js +98 -0
  34. package/test/WebSocket/Client/fragmentation.test.js +130 -0
  35. package/test/WebSocket/Client/limits.test.js +10 -4
  36. package/test/WebSocket/Client/readyState.test.js +154 -0
  37. package/docs/backend/10-authentication/01-user-logins-with-authjs.md +0 -55
@@ -0,0 +1,179 @@
1
+ 'use strict'
2
+
3
+ const cluster = require('node:cluster')
4
+
5
+ /**
6
+ * Tests cross-table cache invalidation for JOIN queries.
7
+ * Why: A cached query like posts.join('users').cache().select() must be invalidated
8
+ * when EITHER posts OR users is written to. Validates that cache keys are registered
9
+ * in all joined tables' indexes.
10
+ */
11
+
12
+ let knexLib, db
13
+
14
+ beforeEach(async () => {
15
+ jest.resetModules()
16
+
17
+ knexLib = require('knex')
18
+ db = knexLib({client: 'sqlite3', connection: {filename: ':memory:'}, useNullAsDefault: true})
19
+
20
+ await db.schema.createTable('posts', table => {
21
+ table.integer('id').primary()
22
+ table.string('title', 255)
23
+ table.integer('user_id')
24
+ })
25
+
26
+ await db.schema.createTable('users', table => {
27
+ table.integer('id').primary()
28
+ table.string('name', 255)
29
+ })
30
+
31
+ await db.schema.createTable('categories', table => {
32
+ table.integer('id').primary()
33
+ table.string('label', 255)
34
+ })
35
+
36
+ await db('users').insert([
37
+ {id: 1, name: 'Alice'},
38
+ {id: 2, name: 'Bob'}
39
+ ])
40
+
41
+ await db('categories').insert([{id: 1, label: 'Tech'}])
42
+
43
+ await db('posts').insert([
44
+ {id: 1, title: 'Post A', user_id: 1},
45
+ {id: 2, title: 'Post B', user_id: 2}
46
+ ])
47
+
48
+ Object.defineProperty(cluster, 'isPrimary', {value: true, configurable: true})
49
+
50
+ const Ipc = require('../../../src/Ipc')
51
+ global.Odac = {
52
+ Config: {
53
+ cache: {ttl: 60, maxKeys: 10000},
54
+ buffer: {flushInterval: 999999, checkpointInterval: 999999}
55
+ },
56
+ Storage: {
57
+ isReady: () => false,
58
+ put: jest.fn(),
59
+ remove: jest.fn(),
60
+ getRange: () => []
61
+ },
62
+ Ipc
63
+ }
64
+ await Ipc.init()
65
+
66
+ const writeBuffer = require('../../../src/Database/WriteBuffer')
67
+ await writeBuffer.init({default: db})
68
+
69
+ const readCache = require('../../../src/Database/ReadCache')
70
+ readCache.init()
71
+
72
+ const DB = require('../../../src/Database')
73
+ DB.connections = {default: db}
74
+ })
75
+
76
+ afterEach(async () => {
77
+ const writeBuffer = require('../../../src/Database/WriteBuffer')
78
+ await writeBuffer.close()
79
+ await Odac.Ipc.close()
80
+ await db.destroy()
81
+ delete global.Odac
82
+ })
83
+
84
+ describe('Cross-table cache invalidation (JOIN queries)', () => {
85
+ it('should register cache key in joined table index', async () => {
86
+ const readCache = require('../../../src/Database/ReadCache')
87
+
88
+ const qb = db('posts').join('users', 'posts.user_id', '=', 'users.id').select('posts.title', 'users.name')
89
+ const executeFn = () => qb.then(r => r)
90
+ await readCache.get('default', 'posts', qb, executeFn, 60)
91
+
92
+ // Cache key should be in BOTH posts and users indexes
93
+ const postKeys = await Odac.Ipc.smembers('rc:idx:default:posts')
94
+ const userKeys = await Odac.Ipc.smembers('rc:idx:default:users')
95
+
96
+ expect(postKeys).toHaveLength(1)
97
+ expect(userKeys).toHaveLength(1)
98
+ expect(postKeys[0]).toBe(userKeys[0])
99
+ })
100
+
101
+ it('should invalidate joined query when joined table is written to', async () => {
102
+ const DB = require('../../../src/Database')
103
+
104
+ // Cache a JOIN query via proxy
105
+ const result1 = await DB.posts.cache(60).join('users', 'posts.user_id', '=', 'users.id').select('posts.title', 'users.name')
106
+
107
+ expect(result1).toHaveLength(2)
108
+
109
+ // Verify cache exists in both indexes
110
+ let postKeys = await Odac.Ipc.smembers('rc:idx:default:posts')
111
+ let userKeys = await Odac.Ipc.smembers('rc:idx:default:users')
112
+ expect(postKeys).toHaveLength(1)
113
+ expect(userKeys).toHaveLength(1)
114
+
115
+ // Write to the JOINED table (users) — should invalidate the cached join query
116
+ await DB.users.where({id: 1}).update({name: 'Alice Updated'})
117
+
118
+ userKeys = await Odac.Ipc.smembers('rc:idx:default:users')
119
+ expect(userKeys).toHaveLength(0)
120
+
121
+ // The cache entry itself should be deleted — next read should hit DB
122
+ const result2 = await DB.posts.cache(60).join('users', 'posts.user_id', '=', 'users.id').select('posts.title', 'users.name')
123
+
124
+ expect(result2[0].name).toBe('Alice Updated')
125
+ })
126
+
127
+ it('should invalidate joined query when primary table is written to', async () => {
128
+ const DB = require('../../../src/Database')
129
+
130
+ await DB.posts.cache(60).join('users', 'posts.user_id', '=', 'users.id').select('posts.title', 'users.name')
131
+
132
+ // Write to the PRIMARY table (posts)
133
+ await DB.posts.where({id: 1}).update({title: 'Updated Post'})
134
+
135
+ const postKeys = await Odac.Ipc.smembers('rc:idx:default:posts')
136
+ expect(postKeys).toHaveLength(0)
137
+
138
+ // Fresh read should reflect the update
139
+ const result = await DB.posts.cache(60).join('users', 'posts.user_id', '=', 'users.id').select('posts.title', 'users.name')
140
+
141
+ expect(result[0].title).toBe('Updated Post')
142
+ })
143
+
144
+ it('should handle multiple joins', async () => {
145
+ const readCache = require('../../../src/Database/ReadCache')
146
+
147
+ const qb = db('posts')
148
+ .join('users', 'posts.user_id', '=', 'users.id')
149
+ .leftJoin('categories', 'posts.id', '=', 'categories.id')
150
+ .select('posts.title', 'users.name', 'categories.label')
151
+
152
+ const executeFn = () => qb.then(r => r)
153
+ await readCache.get('default', 'posts', qb, executeFn, 60)
154
+
155
+ // Cache key should be in ALL three table indexes
156
+ const postKeys = await Odac.Ipc.smembers('rc:idx:default:posts')
157
+ const userKeys = await Odac.Ipc.smembers('rc:idx:default:users')
158
+ const catKeys = await Odac.Ipc.smembers('rc:idx:default:categories')
159
+
160
+ expect(postKeys).toHaveLength(1)
161
+ expect(userKeys).toHaveLength(1)
162
+ expect(catKeys).toHaveLength(1)
163
+ expect(postKeys[0]).toBe(userKeys[0])
164
+ expect(postKeys[0]).toBe(catKeys[0])
165
+ })
166
+
167
+ it('should handle aliased table names in joins', async () => {
168
+ const readCache = require('../../../src/Database/ReadCache')
169
+
170
+ const qb = db('posts').join('users as u', 'posts.user_id', '=', 'u.id').select('posts.title', 'u.name')
171
+
172
+ const executeFn = () => qb.then(r => r)
173
+ await readCache.get('default', 'posts', qb, executeFn, 60)
174
+
175
+ // Should register under 'users', not 'users as u'
176
+ const userKeys = await Odac.Ipc.smembers('rc:idx:default:users')
177
+ expect(userKeys).toHaveLength(1)
178
+ })
179
+ })
@@ -0,0 +1,128 @@
1
+ 'use strict'
2
+
3
+ const cluster = require('node:cluster')
4
+
5
+ /**
6
+ * Tests ReadCache.get() — the core read-through logic.
7
+ * Why: Validates cache HIT/MISS behavior, TTL propagation, and maxKeys guard.
8
+ */
9
+
10
+ let knexLib, db
11
+
12
+ beforeEach(async () => {
13
+ jest.resetModules()
14
+
15
+ knexLib = require('knex')
16
+ db = knexLib({client: 'sqlite3', connection: {filename: ':memory:'}, useNullAsDefault: true})
17
+
18
+ await db.schema.createTable('posts', table => {
19
+ table.integer('id').primary()
20
+ table.string('title', 255)
21
+ table.boolean('active').defaultTo(true)
22
+ })
23
+
24
+ await db('posts').insert([
25
+ {id: 1, title: 'First Post', active: true},
26
+ {id: 2, title: 'Second Post', active: true},
27
+ {id: 3, title: 'Draft', active: false}
28
+ ])
29
+
30
+ Object.defineProperty(cluster, 'isPrimary', {value: true, configurable: true})
31
+
32
+ const Ipc = require('../../../src/Ipc')
33
+ global.Odac = {
34
+ Config: {cache: {ttl: 60, maxKeys: 10000}},
35
+ Ipc
36
+ }
37
+ await Ipc.init()
38
+ })
39
+
40
+ afterEach(async () => {
41
+ await Odac.Ipc.close()
42
+ await db.destroy()
43
+ delete global.Odac
44
+ })
45
+
46
+ describe('ReadCache.get()', () => {
47
+ let readCache
48
+
49
+ beforeEach(() => {
50
+ readCache = require('../../../src/Database/ReadCache')
51
+ readCache.init()
52
+ })
53
+
54
+ it('should return DB result on cache MISS and cache it', async () => {
55
+ const qb = db('posts').where({active: true}).select('id', 'title')
56
+ const executeFn = () => qb.then(r => r)
57
+ const result = await readCache.get('default', 'posts', qb, executeFn, 60)
58
+
59
+ expect(result).toHaveLength(2)
60
+ expect(result[0].title).toBe('First Post')
61
+
62
+ // Verify it was cached — check index
63
+ const keys = await Odac.Ipc.smembers('rc:idx:default:posts')
64
+ expect(keys).toHaveLength(1)
65
+ })
66
+
67
+ it('should return cached result on cache HIT without querying DB', async () => {
68
+ const qb1 = db('posts').where({active: true}).select('id', 'title')
69
+ const executeFn1 = () => qb1.then(r => r)
70
+ const result1 = await readCache.get('default', 'posts', qb1, executeFn1, 60)
71
+
72
+ // Modify DB directly — cache should NOT reflect this
73
+ await db('posts').where({id: 1}).update({title: 'Modified'})
74
+
75
+ const qb2 = db('posts').where({active: true}).select('id', 'title')
76
+ const executeFn2 = () => qb2.then(r => r)
77
+ const result2 = await readCache.get('default', 'posts', qb2, executeFn2, 60)
78
+
79
+ // Should still return the old cached value
80
+ expect(result2[0].title).toBe('First Post')
81
+ expect(result1).toEqual(result2)
82
+ })
83
+
84
+ it('should use config default TTL when ttl parameter is 0', async () => {
85
+ const qb = db('posts').where({id: 1}).first()
86
+ const executeFn = () => qb.then(r => r)
87
+ const result = await readCache.get('default', 'posts', qb, executeFn, 0)
88
+
89
+ expect(result.title).toBe('First Post')
90
+
91
+ // Should still be cached
92
+ const keys = await Odac.Ipc.smembers('rc:idx:default:posts')
93
+ expect(keys).toHaveLength(1)
94
+ })
95
+
96
+ it('should respect maxKeys limit', async () => {
97
+ // Re-init with maxKeys = 1
98
+ global.Odac.Config.cache = {ttl: 60, maxKeys: 1}
99
+
100
+ readCache = require('../../../src/Database/ReadCache')
101
+ readCache.init()
102
+
103
+ const qb1 = db('posts').where({id: 1}).first()
104
+ const executeFn1 = () => qb1.then(r => r)
105
+ await readCache.get('default', 'posts', qb1, executeFn1, 60)
106
+
107
+ const qb2 = db('posts').where({id: 2}).first()
108
+ const executeFn2 = () => qb2.then(r => r)
109
+ await readCache.get('default', 'posts', qb2, executeFn2, 60)
110
+
111
+ // Only 1 key should be in the index (first one cached, second skipped)
112
+ const keys = await Odac.Ipc.smembers('rc:idx:default:posts')
113
+ expect(keys).toHaveLength(1)
114
+ })
115
+
116
+ it('should generate different keys for different queries', async () => {
117
+ const qb1 = db('posts').where({id: 1}).first()
118
+ const qb2 = db('posts').where({id: 2}).first()
119
+ const executeFn1 = () => qb1.then(r => r)
120
+ const executeFn2 = () => qb2.then(r => r)
121
+
122
+ await readCache.get('default', 'posts', qb1, executeFn1, 60)
123
+ await readCache.get('default', 'posts', qb2, executeFn2, 60)
124
+
125
+ const keys = await Odac.Ipc.smembers('rc:idx:default:posts')
126
+ expect(keys).toHaveLength(2)
127
+ })
128
+ })
@@ -0,0 +1,103 @@
1
+ 'use strict'
2
+
3
+ const cluster = require('node:cluster')
4
+
5
+ /**
6
+ * Tests ReadCache.invalidate() — table-level cache purge.
7
+ * Why: Validates that all cached queries for a table are removed on invalidation,
8
+ * and that unrelated tables remain unaffected.
9
+ */
10
+
11
+ let knexLib, db
12
+
13
+ beforeEach(async () => {
14
+ jest.resetModules()
15
+
16
+ knexLib = require('knex')
17
+ db = knexLib({client: 'sqlite3', connection: {filename: ':memory:'}, useNullAsDefault: true})
18
+
19
+ await db.schema.createTable('posts', table => {
20
+ table.integer('id').primary()
21
+ table.string('title', 255)
22
+ })
23
+
24
+ await db.schema.createTable('users', table => {
25
+ table.integer('id').primary()
26
+ table.string('name', 255)
27
+ })
28
+
29
+ await db('posts').insert([
30
+ {id: 1, title: 'Post A'},
31
+ {id: 2, title: 'Post B'}
32
+ ])
33
+
34
+ await db('users').insert([{id: 1, name: 'Alice'}])
35
+
36
+ Object.defineProperty(cluster, 'isPrimary', {value: true, configurable: true})
37
+
38
+ const Ipc = require('../../../src/Ipc')
39
+ global.Odac = {
40
+ Config: {cache: {ttl: 60, maxKeys: 10000}},
41
+ Ipc
42
+ }
43
+ await Ipc.init()
44
+ })
45
+
46
+ afterEach(async () => {
47
+ await Odac.Ipc.close()
48
+ await db.destroy()
49
+ delete global.Odac
50
+ })
51
+
52
+ describe('ReadCache.invalidate()', () => {
53
+ let readCache
54
+
55
+ beforeEach(() => {
56
+ readCache = require('../../../src/Database/ReadCache')
57
+ readCache.init()
58
+ })
59
+
60
+ it('should purge all cached queries for the specified table', async () => {
61
+ // Cache two different queries on posts
62
+ const qb1 = db('posts').where({id: 1}).first()
63
+ const qb2 = db('posts').where({id: 2}).first()
64
+ await readCache.get('default', 'posts', qb1, () => qb1.then(r => r), 60)
65
+ await readCache.get('default', 'posts', qb2, () => qb2.then(r => r), 60)
66
+
67
+ const cachedKeys = await Odac.Ipc.smembers('rc:idx:default:posts')
68
+ expect(cachedKeys).toHaveLength(2)
69
+
70
+ // Invalidate
71
+ await readCache.invalidate('default', 'posts')
72
+
73
+ const keys = await Odac.Ipc.smembers('rc:idx:default:posts')
74
+ expect(keys).toHaveLength(0)
75
+
76
+ // Verify cache entries are actually deleted
77
+ for (const key of cachedKeys) {
78
+ const val = await Odac.Ipc.get(key)
79
+ expect(val).toBeNull()
80
+ }
81
+ })
82
+
83
+ it('should not affect cache of other tables', async () => {
84
+ const qbPosts = db('posts').where({id: 1}).first()
85
+ const qbUsers = db('users').where({id: 1}).first()
86
+ await readCache.get('default', 'posts', qbPosts, () => qbPosts.then(r => r), 60)
87
+ await readCache.get('default', 'users', qbUsers, () => qbUsers.then(r => r), 60)
88
+
89
+ // Invalidate only posts
90
+ await readCache.invalidate('default', 'posts')
91
+
92
+ const postKeys = await Odac.Ipc.smembers('rc:idx:default:posts')
93
+ const userKeys = await Odac.Ipc.smembers('rc:idx:default:users')
94
+
95
+ expect(postKeys).toHaveLength(0)
96
+ expect(userKeys).toHaveLength(1)
97
+ })
98
+
99
+ it('should be a no-op when no cache exists for the table', async () => {
100
+ // Should not throw
101
+ await expect(readCache.invalidate('default', 'nonexistent')).resolves.toBeUndefined()
102
+ })
103
+ })
@@ -0,0 +1,184 @@
1
+ 'use strict'
2
+
3
+ const cluster = require('node:cluster')
4
+
5
+ /**
6
+ * Tests the cache chain API exposed via Database.js proxy.
7
+ * Why: Validates that Odac.DB.posts.cache(60).where(...).select(...) pattern
8
+ * correctly delegates to ReadCache, and that write operations auto-invalidate.
9
+ */
10
+
11
+ let knexLib, db
12
+
13
+ beforeEach(async () => {
14
+ jest.resetModules()
15
+
16
+ knexLib = require('knex')
17
+ db = knexLib({client: 'sqlite3', connection: {filename: ':memory:'}, useNullAsDefault: true})
18
+
19
+ await db.schema.createTable('posts', table => {
20
+ table.integer('id').primary()
21
+ table.string('title', 255)
22
+ table.integer('views').defaultTo(0)
23
+ table.boolean('active').defaultTo(true)
24
+ })
25
+
26
+ await db('posts').insert([
27
+ {id: 1, title: 'First Post', views: 100, active: true},
28
+ {id: 2, title: 'Second Post', views: 200, active: true},
29
+ {id: 3, title: 'Draft', views: 0, active: false}
30
+ ])
31
+
32
+ Object.defineProperty(cluster, 'isPrimary', {value: true, configurable: true})
33
+
34
+ const Ipc = require('../../../src/Ipc')
35
+ global.Odac = {
36
+ Config: {
37
+ cache: {ttl: 60, maxKeys: 10000},
38
+ buffer: {flushInterval: 999999, checkpointInterval: 999999}
39
+ },
40
+ Storage: {
41
+ isReady: () => false,
42
+ put: jest.fn(),
43
+ remove: jest.fn(),
44
+ getRange: () => []
45
+ },
46
+ Ipc
47
+ }
48
+ await Ipc.init()
49
+
50
+ const writeBuffer = require('../../../src/Database/WriteBuffer')
51
+ await writeBuffer.init({default: db})
52
+
53
+ const readCache = require('../../../src/Database/ReadCache')
54
+ readCache.init()
55
+
56
+ const DB = require('../../../src/Database')
57
+ DB.connections = {default: db}
58
+ })
59
+
60
+ afterEach(async () => {
61
+ const writeBuffer = require('../../../src/Database/WriteBuffer')
62
+ await writeBuffer.close()
63
+ await Odac.Ipc.close()
64
+ await db.destroy()
65
+ delete global.Odac
66
+ })
67
+
68
+ describe('Database.js Proxy - cache(ttl).where().select()', () => {
69
+ it('should cache SELECT results with specified TTL', async () => {
70
+ const DB = require('../../../src/Database')
71
+
72
+ const result1 = await DB.posts.cache(60).where({active: true}).select('id', 'title')
73
+ expect(result1).toHaveLength(2)
74
+
75
+ // Modify DB directly
76
+ await db('posts').where({id: 1}).update({title: 'Modified'})
77
+
78
+ // Should return cached (stale) data
79
+ const result2 = await DB.posts.cache(60).where({active: true}).select('id', 'title')
80
+ expect(result2[0].title).toBe('First Post')
81
+ })
82
+
83
+ it('should cache with default TTL when called without argument', async () => {
84
+ const DB = require('../../../src/Database')
85
+
86
+ const result = await DB.posts.cache().where({id: 1}).first()
87
+ expect(result.title).toBe('First Post')
88
+
89
+ // Verify cached
90
+ const keys = await Odac.Ipc.smembers('rc:idx:default:posts')
91
+ expect(keys).toHaveLength(1)
92
+ })
93
+ })
94
+
95
+ describe('Database.js Proxy - cache.clear()', () => {
96
+ it('should manually clear table cache via Odac.DB.posts.cache.clear()', async () => {
97
+ const DB = require('../../../src/Database')
98
+
99
+ await DB.posts.cache(60).where({active: true}).select('id', 'title')
100
+
101
+ let keys = await Odac.Ipc.smembers('rc:idx:default:posts')
102
+ expect(keys).toHaveLength(1)
103
+
104
+ await DB.posts.cache.clear()
105
+
106
+ keys = await Odac.Ipc.smembers('rc:idx:default:posts')
107
+ expect(keys).toHaveLength(0)
108
+ })
109
+ })
110
+
111
+ describe('Database.js Proxy - automatic invalidation on write', () => {
112
+ it('should invalidate cache after update()', async () => {
113
+ const DB = require('../../../src/Database')
114
+
115
+ // Cache a query
116
+ await DB.posts.cache(60).where({active: true}).select('id', 'title')
117
+ let keys = await Odac.Ipc.smembers('rc:idx:default:posts')
118
+ expect(keys).toHaveLength(1)
119
+
120
+ // Update via proxy — should auto-invalidate
121
+ await DB.posts.where({id: 1}).update({title: 'Updated'})
122
+
123
+ keys = await Odac.Ipc.smembers('rc:idx:default:posts')
124
+ expect(keys).toHaveLength(0)
125
+
126
+ // Next cache() call should fetch fresh data
127
+ const result = await DB.posts.cache(60).where({id: 1}).first()
128
+ expect(result.title).toBe('Updated')
129
+ })
130
+
131
+ it('should invalidate cache after insert()', async () => {
132
+ const DB = require('../../../src/Database')
133
+
134
+ await DB.posts.cache(60).where({active: true}).select('id', 'title')
135
+ let keys = await Odac.Ipc.smembers('rc:idx:default:posts')
136
+ expect(keys).toHaveLength(1)
137
+
138
+ await DB.posts.insert({id: 4, title: 'New Post', views: 0, active: true})
139
+
140
+ keys = await Odac.Ipc.smembers('rc:idx:default:posts')
141
+ expect(keys).toHaveLength(0)
142
+ })
143
+
144
+ it('should invalidate cache after delete()', async () => {
145
+ const DB = require('../../../src/Database')
146
+
147
+ await DB.posts.cache(60).where({active: true}).select('id', 'title')
148
+ let keys = await Odac.Ipc.smembers('rc:idx:default:posts')
149
+ expect(keys).toHaveLength(1)
150
+
151
+ await DB.posts.where({id: 3}).delete()
152
+
153
+ keys = await Odac.Ipc.smembers('rc:idx:default:posts')
154
+ expect(keys).toHaveLength(0)
155
+ })
156
+
157
+ it('should invalidate cache after del() (alias)', async () => {
158
+ const DB = require('../../../src/Database')
159
+
160
+ await DB.posts.cache(60).where({active: true}).select('id', 'title')
161
+ let keys = await Odac.Ipc.smembers('rc:idx:default:posts')
162
+ expect(keys).toHaveLength(1)
163
+
164
+ await DB.posts.where({id: 3}).del()
165
+
166
+ keys = await Odac.Ipc.smembers('rc:idx:default:posts')
167
+ expect(keys).toHaveLength(0)
168
+ })
169
+ })
170
+
171
+ describe('Database.js Proxy - global cache.clear()', () => {
172
+ it('should clear cache via Odac.DB.cache.clear(connection, table)', async () => {
173
+ const DB = require('../../../src/Database')
174
+
175
+ await DB.posts.cache(60).where({active: true}).select('id', 'title')
176
+ let keys = await Odac.Ipc.smembers('rc:idx:default:posts')
177
+ expect(keys).toHaveLength(1)
178
+
179
+ await DB.cache.clear('default', 'posts')
180
+
181
+ keys = await Odac.Ipc.smembers('rc:idx:default:posts')
182
+ expect(keys).toHaveLength(0)
183
+ })
184
+ })