@harperfast/harper-pro 5.0.0-alpha.2 → 5.0.0-alpha.4

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 (226) hide show
  1. package/core/.github/workflows/create-release.yaml +181 -0
  2. package/core/.github/workflows/notify-release-published.yaml +50 -0
  3. package/core/.github/workflows/publish-docker.yaml +174 -0
  4. package/core/.github/workflows/publish-npm.yaml +173 -0
  5. package/core/CONTRIBUTING.md +2 -0
  6. package/core/bin/harper.js +6 -0
  7. package/core/components/PluginModule.ts +0 -1
  8. package/core/components/componentLoader.ts +16 -4
  9. package/core/dev/sync-commits.js +18 -7
  10. package/core/integrationTests/server/operation-user-rbac.test.ts +484 -0
  11. package/core/package.json +4 -3
  12. package/core/resources/DatabaseTransaction.ts +2 -1
  13. package/core/resources/LMDBTransaction.ts +9 -4
  14. package/core/resources/RecordEncoder.ts +5 -3
  15. package/core/resources/RequestTarget.ts +1 -1
  16. package/core/resources/Resource.ts +1 -1
  17. package/core/resources/RocksTransactionLogStore.ts +106 -12
  18. package/core/resources/Table.ts +108 -114
  19. package/core/resources/dataLoader.ts +0 -1
  20. package/core/resources/databases.ts +1 -1
  21. package/core/resources/jsResource.ts +0 -2
  22. package/core/resources/registrationDeprecated.ts +8 -0
  23. package/core/resources/transactionBroadcast.ts +29 -25
  24. package/core/security/auth.ts +14 -1
  25. package/core/security/jsLoader.ts +9 -2
  26. package/core/security/permissionsTranslator.js +4 -0
  27. package/core/security/user.ts +20 -2
  28. package/core/server/REST.ts +29 -16
  29. package/core/server/Server.ts +6 -2
  30. package/core/server/http.ts +5 -7
  31. package/core/server/serverHelpers/Request.ts +5 -0
  32. package/core/server/serverHelpers/serverUtilities.ts +6 -2
  33. package/core/server/static.ts +0 -2
  34. package/core/unitTests/apiTests/cache-test.mjs +37 -0
  35. package/core/unitTests/commonTestErrors.js +4 -0
  36. package/core/unitTests/dataLayer/SQLSearch.test.js +2 -2
  37. package/core/unitTests/dataLayer/sql-update.test.js +2 -2
  38. package/core/unitTests/resources/auditLog.test.js +167 -0
  39. package/core/unitTests/resources/caching.test.js +79 -0
  40. package/core/unitTests/resources/permissions.test.js +7 -2
  41. package/core/unitTests/resources/txn-tracking.test.js +10 -4
  42. package/core/unitTests/resources/vectorIndex.test.js +1 -0
  43. package/core/unitTests/testApp/resources.js +30 -0
  44. package/core/unitTests/testApp/schema.graphql +5 -0
  45. package/core/unitTests/utility/operationPermissions.test.js +107 -0
  46. package/core/unitTests/utility/operation_authorization.test.js +130 -0
  47. package/core/unitTests/validation/role_validation.test.js +79 -0
  48. package/core/utility/common_utils.js +8 -4
  49. package/core/utility/errors/commonErrors.js +4 -0
  50. package/core/utility/hdbTerms.ts +1 -0
  51. package/core/utility/operationPermissions.ts +96 -0
  52. package/core/utility/operation_authorization.js +150 -49
  53. package/core/validation/role_validation.js +23 -0
  54. package/dist/bin/harper.js +1 -1
  55. package/dist/bin/harper.js.map +1 -1
  56. package/dist/cloneNode/cloneNode.js +55 -38
  57. package/dist/cloneNode/cloneNode.js.map +1 -1
  58. package/dist/core/bin/harper.js +2 -0
  59. package/dist/core/bin/harper.js.map +1 -1
  60. package/dist/core/components/ComponentV1.js +1 -0
  61. package/dist/core/components/ComponentV1.js.map +1 -1
  62. package/dist/core/components/EntryHandler.js +35 -1
  63. package/dist/core/components/EntryHandler.js.map +1 -1
  64. package/dist/core/components/PluginModule.js +1 -0
  65. package/dist/core/components/PluginModule.js.map +1 -1
  66. package/dist/core/components/Scope.js +2 -0
  67. package/dist/core/components/Scope.js.map +1 -1
  68. package/dist/core/components/componentLoader.js +12 -3
  69. package/dist/core/components/componentLoader.js.map +1 -1
  70. package/dist/core/components/status/api.js +1 -0
  71. package/dist/core/components/status/api.js.map +1 -1
  72. package/dist/core/components/status/crossThread.js +1 -0
  73. package/dist/core/components/status/crossThread.js.map +1 -1
  74. package/dist/core/config/RootConfigWatcher.js +34 -4
  75. package/dist/core/config/RootConfigWatcher.js.map +1 -1
  76. package/dist/core/dataLayer/harperBridge/ResourceBridge.js +1 -0
  77. package/dist/core/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
  78. package/dist/core/resources/DatabaseTransaction.js +3 -1
  79. package/dist/core/resources/DatabaseTransaction.js.map +1 -1
  80. package/dist/core/resources/ErrorResource.js +1 -0
  81. package/dist/core/resources/ErrorResource.js.map +1 -1
  82. package/dist/core/resources/LMDBTransaction.js +10 -5
  83. package/dist/core/resources/LMDBTransaction.js.map +1 -1
  84. package/dist/core/resources/RecordEncoder.js +5 -3
  85. package/dist/core/resources/RecordEncoder.js.map +1 -1
  86. package/dist/core/resources/RequestTarget.js +3 -0
  87. package/dist/core/resources/RequestTarget.js.map +1 -1
  88. package/dist/core/resources/Resource.js +2 -1
  89. package/dist/core/resources/Resource.js.map +1 -1
  90. package/dist/core/resources/ResourceInterface.js +3 -0
  91. package/dist/core/resources/ResourceInterface.js.map +1 -1
  92. package/dist/core/resources/ResourceInterfaceV2.js +2 -0
  93. package/dist/core/resources/ResourceInterfaceV2.js.map +1 -1
  94. package/dist/core/resources/ResourceV2.js +3 -0
  95. package/dist/core/resources/ResourceV2.js.map +1 -1
  96. package/dist/core/resources/Resources.js +1 -0
  97. package/dist/core/resources/Resources.js.map +1 -1
  98. package/dist/core/resources/RocksIndexStore.js +1 -0
  99. package/dist/core/resources/RocksIndexStore.js.map +1 -1
  100. package/dist/core/resources/RocksTransactionLogStore.js +102 -12
  101. package/dist/core/resources/RocksTransactionLogStore.js.map +1 -1
  102. package/dist/core/resources/Table.js +100 -111
  103. package/dist/core/resources/Table.js.map +1 -1
  104. package/dist/core/resources/blob.js +1 -0
  105. package/dist/core/resources/blob.js.map +1 -1
  106. package/dist/core/resources/dataLoader.js +3 -2
  107. package/dist/core/resources/dataLoader.js.map +1 -1
  108. package/dist/core/resources/databases.js +1 -1
  109. package/dist/core/resources/databases.js.map +1 -1
  110. package/dist/core/resources/jsResource.js +2 -2
  111. package/dist/core/resources/jsResource.js.map +1 -1
  112. package/dist/core/resources/openApi.js +1 -0
  113. package/dist/core/resources/openApi.js.map +1 -1
  114. package/dist/core/resources/registrationDeprecated.js +11 -0
  115. package/dist/core/resources/registrationDeprecated.js.map +1 -0
  116. package/dist/core/resources/replayLogs.js +2 -0
  117. package/dist/core/resources/replayLogs.js.map +1 -1
  118. package/dist/core/resources/search.js +1 -0
  119. package/dist/core/resources/search.js.map +1 -1
  120. package/dist/core/resources/transactionBroadcast.js +30 -27
  121. package/dist/core/resources/transactionBroadcast.js.map +1 -1
  122. package/dist/core/security/auth.js +15 -1
  123. package/dist/core/security/auth.js.map +1 -1
  124. package/dist/core/security/jsLoader.js +10 -2
  125. package/dist/core/security/jsLoader.js.map +1 -1
  126. package/dist/core/security/permissionsTranslator.js +4 -0
  127. package/dist/core/security/permissionsTranslator.js.map +1 -1
  128. package/dist/core/security/user.js +16 -1
  129. package/dist/core/security/user.js.map +1 -1
  130. package/dist/core/server/REST.js +35 -18
  131. package/dist/core/server/REST.js.map +1 -1
  132. package/dist/core/server/Server.js +2 -0
  133. package/dist/core/server/Server.js.map +1 -1
  134. package/dist/core/server/http.js +5 -3
  135. package/dist/core/server/http.js.map +1 -1
  136. package/dist/core/server/operationsServer.js +2 -1
  137. package/dist/core/server/operationsServer.js.map +1 -1
  138. package/dist/core/server/serverHelpers/Request.js +2 -0
  139. package/dist/core/server/serverHelpers/Request.js.map +1 -1
  140. package/dist/core/server/serverHelpers/serverUtilities.js +3 -2
  141. package/dist/core/server/serverHelpers/serverUtilities.js.map +1 -1
  142. package/dist/core/server/static.js +1 -2
  143. package/dist/core/server/static.js.map +1 -1
  144. package/dist/core/utility/common_utils.js +7 -5
  145. package/dist/core/utility/common_utils.js.map +1 -1
  146. package/dist/core/utility/errors/commonErrors.js +3 -0
  147. package/dist/core/utility/errors/commonErrors.js.map +1 -1
  148. package/dist/core/utility/hdbTerms.js +1 -0
  149. package/dist/core/utility/hdbTerms.js.map +1 -1
  150. package/dist/core/utility/operationPermissions.js +101 -0
  151. package/dist/core/utility/operationPermissions.js.map +1 -0
  152. package/dist/core/utility/operation_authorization.js +83 -49
  153. package/dist/core/utility/operation_authorization.js.map +1 -1
  154. package/dist/core/validation/role_validation.js +19 -1
  155. package/dist/core/validation/role_validation.js.map +1 -1
  156. package/dist/licensing/usageLicensing.js +243 -0
  157. package/dist/licensing/usageLicensing.js.map +1 -0
  158. package/dist/licensing/validation.js +149 -0
  159. package/dist/licensing/validation.js.map +1 -0
  160. package/dist/replication/replicationConnection.js +90 -24
  161. package/dist/replication/replicationConnection.js.map +1 -1
  162. package/dist/replication/replicator.js +6 -2
  163. package/dist/replication/replicator.js.map +1 -1
  164. package/dist/replication/setNode.js +0 -1
  165. package/dist/replication/setNode.js.map +1 -1
  166. package/dist/security/certificate.js +206 -6
  167. package/dist/security/certificate.js.map +1 -1
  168. package/dist/security/keyService.js +58 -0
  169. package/dist/security/keyService.js.map +1 -0
  170. package/dist/security/sshKeyOperations.js +344 -0
  171. package/dist/security/sshKeyOperations.js.map +1 -0
  172. package/licensing/usageLicensing.ts +260 -0
  173. package/licensing/validation.ts +191 -0
  174. package/npm-shrinkwrap.json +509 -463
  175. package/package.json +6 -3
  176. package/replication/replicationConnection.ts +99 -31
  177. package/replication/replicator.ts +8 -3
  178. package/replication/setNode.ts +0 -1
  179. package/security/certificate.ts +259 -7
  180. package/security/keyService.ts +74 -0
  181. package/security/sshKeyOperations.ts +405 -0
  182. package/static/defaultConfig.yaml +2 -0
  183. package/studio/web/HDBDogOnly.svg +78 -0
  184. package/studio/web/assets/PPRadioGrotesk-Bold-DDaUYG8E.woff +0 -0
  185. package/studio/web/assets/fa-brands-400-CEJbCg16.woff +0 -0
  186. package/studio/web/assets/fa-brands-400-CSYNqBb_.ttf +0 -0
  187. package/studio/web/assets/fa-brands-400-DnkPfk3o.eot +0 -0
  188. package/studio/web/assets/fa-brands-400-UxlILjvJ.woff2 +0 -0
  189. package/studio/web/assets/fa-brands-400-cH1MgKbP.svg +3717 -0
  190. package/studio/web/assets/fa-regular-400-BhTwtT8w.eot +0 -0
  191. package/studio/web/assets/fa-regular-400-D1vz6WBx.ttf +0 -0
  192. package/studio/web/assets/fa-regular-400-DFnMcJPd.woff +0 -0
  193. package/studio/web/assets/fa-regular-400-DGzu1beS.woff2 +0 -0
  194. package/studio/web/assets/fa-regular-400-gwj8Pxq-.svg +801 -0
  195. package/studio/web/assets/fa-solid-900-B4ZZ7kfP.svg +5034 -0
  196. package/studio/web/assets/fa-solid-900-B6Axprfb.eot +0 -0
  197. package/studio/web/assets/fa-solid-900-BUswJgRo.woff2 +0 -0
  198. package/studio/web/assets/fa-solid-900-DOXgCApm.woff +0 -0
  199. package/studio/web/assets/fa-solid-900-mxuxnBEa.ttf +0 -0
  200. package/studio/web/assets/index-CjpELGtS.js +37 -0
  201. package/studio/web/assets/index-CjpELGtS.js.map +1 -0
  202. package/studio/web/assets/index-VoSl--bG.js +235 -0
  203. package/studio/web/assets/index-VoSl--bG.js.map +1 -0
  204. package/studio/web/assets/index-Y2g_iFpU.css +1 -0
  205. package/studio/web/assets/index-jiPwkrsB.css +1 -0
  206. package/studio/web/assets/index-o10iYrkU.js +2 -0
  207. package/studio/web/assets/index-o10iYrkU.js.map +1 -0
  208. package/studio/web/assets/index.lazy-D5hjT1pS.js +266 -0
  209. package/studio/web/assets/index.lazy-D5hjT1pS.js.map +1 -0
  210. package/studio/web/assets/profiler-B9Edexdi.js +2 -0
  211. package/studio/web/assets/profiler-B9Edexdi.js.map +1 -0
  212. package/studio/web/assets/react-redux-DsVOxD2E.js +6 -0
  213. package/studio/web/assets/react-redux-DsVOxD2E.js.map +1 -0
  214. package/studio/web/assets/startRecording-Bwd6AYRS.js +3 -0
  215. package/studio/web/assets/startRecording-Bwd6AYRS.js.map +1 -0
  216. package/studio/web/fabric-signup-background.webp +0 -0
  217. package/studio/web/fabric-signup-text.png +0 -0
  218. package/studio/web/favicon_purple.png +0 -0
  219. package/studio/web/github-icon.svg +15 -0
  220. package/studio/web/harper-fabric_black.png +0 -0
  221. package/studio/web/harper-fabric_white.png +0 -0
  222. package/studio/web/harper-studio_white.png +0 -0
  223. package/studio/web/index.html +16 -0
  224. package/studio/web/running.css +148 -0
  225. package/studio/web/running.html +147 -0
  226. package/studio/web/running.js +111 -0
@@ -21,6 +21,15 @@ describe('Audit log', () => {
21
21
  subscription.on('data', (event) => {
22
22
  events.push(event);
23
23
  });
24
+ server.replication.mockRemoteMap = new Map([['local', 0]]);
25
+ server.replication.getIdOfRemoteNode = function (name) {
26
+ let id = server.replication.mockRemoteMap.get(name);
27
+ if (id === undefined) {
28
+ id = server.replication.mockRemoteMap.size;
29
+ server.replication.mockRemoteMap.set(name, id);
30
+ }
31
+ return id;
32
+ };
24
33
  });
25
34
  afterEach(function () {
26
35
  setAuditRetention(60000);
@@ -48,6 +57,7 @@ describe('Audit log', () => {
48
57
  for await (let entry of AuditedTable.getHistory()) {
49
58
  results.push(entry);
50
59
  }
60
+
51
61
  assert.equal(results.length, 0);
52
62
  assert.equal(AuditedTable.primaryStore.getEntry(1), undefined); // verify that the delete entry was removed
53
63
  // verify that the twice-written entry was not removed
@@ -89,4 +99,161 @@ describe('Audit log', () => {
89
99
  assert.equal(history[0].user, key.toString());
90
100
  assert.deepEqual(history[0].value.id, key);
91
101
  });
102
+ it('dynamically add new transaction logs to iterator', async function () {
103
+ if (!AuditedTable.auditStore.reusableIterable) return this.skip(); // only for rocksdb
104
+
105
+ // Create initial entries
106
+ await AuditedTable.put(10, { name: 'initial' });
107
+ await AuditedTable.put(11, { name: 'initial2' });
108
+
109
+ const results = [];
110
+ const iterator = AuditedTable.getHistory()[Symbol.asyncIterator]();
111
+
112
+ // Get first entry
113
+ let result = await iterator.next();
114
+ results.push(result.value);
115
+
116
+ // Emit a new transaction log event
117
+ AuditedTable.auditStore.rootStore.useLog('new-transaction-log');
118
+ await delay(20);
119
+ // Continue iterating - should include entries from new log if it has any
120
+ while (!(result = await iterator.next()).done) {
121
+ results.push(result.value);
122
+ }
123
+
124
+ // Verify we got at least the initial entries
125
+ assert(results.length >= 2, 'Should have at least the initial entries');
126
+ });
127
+ it('cleanup listener when iterator completes naturally', async function () {
128
+ if (!AuditedTable.auditStore.reusableIterable) return this.skip(); // only for rocksdb
129
+
130
+ await AuditedTable.put(20, { name: 'test' });
131
+
132
+ const originalOn = AuditedTable.auditStore.rootStore.on.bind(AuditedTable.auditStore.rootStore);
133
+ const originalOff = AuditedTable.auditStore.rootStore.off.bind(AuditedTable.auditStore.rootStore);
134
+ let activeListener = null;
135
+
136
+ AuditedTable.auditStore.rootStore.on = function (event, listener) {
137
+ if (event === 'new-transaction-log') {
138
+ activeListener = listener;
139
+ }
140
+ return originalOn(event, listener);
141
+ };
142
+
143
+ AuditedTable.auditStore.rootStore.off = function (event, listener) {
144
+ if (event === 'new-transaction-log' && listener === activeListener) {
145
+ activeListener = null;
146
+ }
147
+ return originalOff(event, listener);
148
+ };
149
+
150
+ // Create iterator and let it complete
151
+ for await (const _entry of AuditedTable.getHistory()) {
152
+ // iterate through all
153
+ }
154
+
155
+ // Restore original methods
156
+ AuditedTable.auditStore.rootStore.on = originalOn;
157
+ AuditedTable.auditStore.rootStore.off = originalOff;
158
+
159
+ // Verify listener was cleaned up
160
+ assert.equal(activeListener, null, 'Listener should be cleaned up after completion');
161
+ });
162
+ it('cleanup listener when breaking from iteration', async function () {
163
+ if (!AuditedTable.auditStore.reusableIterable) return this.skip(); // only for rocksdb
164
+
165
+ await AuditedTable.put(30, { name: 'test1' });
166
+ await AuditedTable.put(31, { name: 'test2' });
167
+ await AuditedTable.put(32, { name: 'test3' });
168
+
169
+ // Track listener cleanup
170
+ const originalOn = AuditedTable.auditStore.rootStore.on.bind(AuditedTable.auditStore.rootStore);
171
+ const originalOff = AuditedTable.auditStore.rootStore.off.bind(AuditedTable.auditStore.rootStore);
172
+ let activeListener = null;
173
+
174
+ AuditedTable.auditStore.rootStore.on = function (event, listener) {
175
+ if (event === 'new-transaction-log') {
176
+ activeListener = listener;
177
+ }
178
+ return originalOn(event, listener);
179
+ };
180
+
181
+ AuditedTable.auditStore.rootStore.off = function (event, listener) {
182
+ if (event === 'new-transaction-log' && listener === activeListener) {
183
+ activeListener = null;
184
+ }
185
+ return originalOff(event, listener);
186
+ };
187
+
188
+ // Break early from iteration
189
+ let count = 0;
190
+ for await (const _entry of AuditedTable.getHistory()) {
191
+ if (++count >= 2) break;
192
+ }
193
+
194
+ // Restore original methods
195
+ AuditedTable.auditStore.rootStore.on = originalOn;
196
+ AuditedTable.auditStore.rootStore.off = originalOff;
197
+
198
+ // Listener should be cleaned up after break
199
+ assert.equal(activeListener, null, 'Listener should be cleaned up after break');
200
+ });
201
+ it('exclude logs from new transaction log events', async function () {
202
+ if (!AuditedTable.auditStore.reusableIterable) return this.skip(); // only for rocksdb
203
+ await AuditedTable.put(40, { name: 'test' });
204
+
205
+ const excludedLog = 'excluded-log-' + Date.now();
206
+ const iterator = AuditedTable.auditStore.getRange({ excludeLogs: [excludedLog], start: 0 })[Symbol.iterator]();
207
+
208
+ // Start iteration
209
+ await iterator.next();
210
+
211
+ // Emit include log - should be include
212
+ let nodeId = AuditedTable.auditStore.ensureLogExists('new-transaction-log-2');
213
+ await delay(20);
214
+ await AuditedTable.put(41, { name: 'test' }, { nodeId });
215
+ // Emit excluded log - should be ignored
216
+ nodeId = AuditedTable.auditStore.ensureLogExists(excludedLog);
217
+ await delay(20);
218
+
219
+ await AuditedTable.put(42, { name: 'test' }, { nodeId });
220
+
221
+ let result = [];
222
+ // Finish iteration
223
+ let entry;
224
+ while (!(entry = await iterator.next()).done) {
225
+ result.push(entry.value);
226
+ }
227
+ assert(result.find((entry) => entry.recordId === 41));
228
+ //assert(!result.find((entry) => entry.recordId === 42));
229
+ assert(true, 'Should complete without including excluded log');
230
+ });
231
+ it('add and remove logs dynamically using iterator methods', async function () {
232
+ if (!AuditedTable.auditStore.reusableIterable) return this.skip(); // only for rocksdb
233
+
234
+ await AuditedTable.put(50, { name: 'test' });
235
+
236
+ const iterable = AuditedTable.auditStore.getRange({});
237
+ const iterator = iterable[Symbol.iterator]();
238
+
239
+ // Start iteration
240
+ iterator.next();
241
+
242
+ // Add a new log using the addLog method on the iterable
243
+ const newLogName = 'manual-log-' + Date.now();
244
+ AuditedTable.auditStore.ensureLogExists(newLogName);
245
+
246
+ // Verify the log was added to logByName
247
+ assert(AuditedTable.auditStore.logByName.has(newLogName), 'Log should be added to logByName');
248
+
249
+ // Remove the log using the removeLog method on the iterable
250
+ iterable.removeLog(newLogName);
251
+
252
+ // Continue iterating to completion
253
+ while (!(await iterator.next()).done) {
254
+ // continue
255
+ }
256
+
257
+ assert(true, 'Should complete successfully after adding and removing logs');
258
+ });
92
259
  });
@@ -364,4 +364,83 @@ describe('Caching', () => {
364
364
  timer = 0;
365
365
  }
366
366
  });
367
+
368
+ it('Extended class with sourcedFrom does not impact base class', async function () {
369
+ // Create a base table without a source
370
+ const BaseTable = table({
371
+ table: 'BaseTable',
372
+ database: 'test',
373
+ attributes: [{ name: 'id', isPrimaryKey: true }, { name: 'value' }],
374
+ });
375
+
376
+ // Create an extended class and give it a source
377
+ class ExtendedTable extends BaseTable {}
378
+
379
+ let extendedSourceCalls = 0;
380
+ ExtendedTable.sourcedFrom({
381
+ get(id) {
382
+ return new Promise((resolve) => {
383
+ extendedSourceCalls++;
384
+ resolve({
385
+ id,
386
+ value: 'extended-' + id,
387
+ });
388
+ });
389
+ },
390
+ });
391
+
392
+ // Verify the extended class has a source
393
+ assert(ExtendedTable.source);
394
+ assert.equal(typeof ExtendedTable.source.get, 'function');
395
+
396
+ // Verify the base class does NOT have a source
397
+ assert(!BaseTable.source);
398
+
399
+ // Test that the extended class uses its source
400
+ extendedSourceCalls = 0;
401
+ await ExtendedTable.invalidate(100);
402
+ const extendedResult = await ExtendedTable.get(100);
403
+ assert.equal(extendedResult.value, 'extended-100');
404
+ assert.equal(extendedSourceCalls, 1);
405
+
406
+ // Test that the base class doesn't call any source
407
+ await BaseTable.invalidate(101);
408
+ const baseResult = await BaseTable.get(101);
409
+ assert.equal(baseResult, undefined); // Should be undefined since there's no source
410
+ assert.equal(extendedSourceCalls, 1); // Should not have called extended source
411
+
412
+ // Create another extended class with a different source
413
+ class AnotherExtendedTable extends BaseTable {}
414
+
415
+ let anotherSourceCalls = 0;
416
+ AnotherExtendedTable.sourcedFrom({
417
+ get(id) {
418
+ return new Promise((resolve) => {
419
+ anotherSourceCalls++;
420
+ resolve({
421
+ id,
422
+ value: 'another-' + id,
423
+ });
424
+ });
425
+ },
426
+ });
427
+
428
+ // Verify each extended class has its own independent source
429
+ assert(AnotherExtendedTable.source);
430
+ assert(AnotherExtendedTable.source !== ExtendedTable.source);
431
+
432
+ // Test that each extended class uses its own source
433
+ await AnotherExtendedTable.invalidate(102);
434
+ const anotherResult = await AnotherExtendedTable.get(102);
435
+ assert.equal(anotherResult.value, 'another-102');
436
+ assert.equal(anotherSourceCalls, 1);
437
+ assert.equal(extendedSourceCalls, 1); // ExtendedTable source should not be called
438
+
439
+ // Verify ExtendedTable still uses its own source
440
+ await ExtendedTable.invalidate(103);
441
+ const extendedResult2 = await ExtendedTable.get(103);
442
+ assert.equal(extendedResult2.value, 'extended-103');
443
+ assert.equal(extendedSourceCalls, 2);
444
+ assert.equal(anotherSourceCalls, 1); // AnotherExtendedTable source should not be called
445
+ });
367
446
  });
@@ -30,8 +30,13 @@ describe('Permissions through Resource API', () => {
30
30
  ],
31
31
  });
32
32
  for (let i = 0; i < 10; i++) {
33
- RelatedTable.put({ id: 'related-id-' + i, name: 'related name-' + i });
34
- TestTable.put({ id: 'id-' + i, name: i > 0 ? 'name-' + i : null, prop1: 'test', relatedId: 'related-id-' + i });
33
+ await RelatedTable.put({ id: 'related-id-' + i, name: 'related name-' + i });
34
+ await TestTable.put({
35
+ id: 'id-' + i,
36
+ name: i > 0 ? 'name-' + i : null,
37
+ prop1: 'test',
38
+ relatedId: 'related-id-' + i,
39
+ });
35
40
  }
36
41
  restricted_user = {
37
42
  role: {
@@ -2,9 +2,11 @@ require('../testUtils');
2
2
  const assert = require('assert');
3
3
  const { setupTestDBPath } = require('../testUtils');
4
4
  const { setTxnExpiration } = require('#src/resources/DatabaseTransaction');
5
+ const { setTxnExpiration: setLMDBTxnExpiration } = require('#src/resources/LMDBTransaction');
5
6
  const { setMainIsWorker } = require('#js/server/threads/manageThreads');
6
7
  const { table } = require('#src/resources/databases');
7
8
  const { setTimeout: delay } = require('node:timers/promises');
9
+ const { RocksDatabase } = require('@harperfast/rocksdb-js');
8
10
  describe('Txn Expiration', () => {
9
11
  let SlowResource,
10
12
  performedDBInteractions = false;
@@ -31,15 +33,19 @@ describe('Txn Expiration', () => {
31
33
  });
32
34
  it('Slow txn will expire', async function () {
33
35
  await SlowResource.put(3, { name: 'three' });
34
- let trackedTxns = setTxnExpiration(20);
36
+ let trackedTxns =
37
+ SlowResource.primaryStore instanceof RocksDatabase ? setTxnExpiration(20) : setLMDBTxnExpiration(20);
38
+ await delay(50);
35
39
  let existingTxns = trackedTxns.size;
36
40
  let result = SlowResource.get(3);
37
41
  assert.equal(trackedTxns.size, existingTxns + 1);
38
42
  const txns = Array.from(trackedTxns);
39
43
  const lastTxn = txns[txns.length - 1];
40
- assert.equal(lastTxn.startedFrom.resourceName, 'SlowResource');
41
- assert.equal(lastTxn.startedFrom.method, 'get');
42
- assert.equal(lastTxn.timeout, 20);
44
+ if (SlowResource.primaryStore instanceof RocksDatabase) {
45
+ assert.equal(lastTxn.startedFrom.resourceName, 'SlowResource');
46
+ assert.equal(lastTxn.startedFrom.method, 'get');
47
+ assert.equal(lastTxn.timeout, 20);
48
+ }
43
49
  await Promise.race([delay(50), result]);
44
50
  assert(performedDBInteractions);
45
51
  assert.equal(trackedTxns.size, existingTxns);
@@ -3,6 +3,7 @@ const { table } = require('#src/resources/databases');
3
3
  const { HierarchicalNavigableSmallWorld } = require('#src/resources/indexes/HierarchicalNavigableSmallWorld');
4
4
 
5
5
  describe('HierarchicalNavigableSmallWorld indexing', () => {
6
+ if (process.env.HARPER_STORAGE_ENGINE === 'lmdb') return; // don't try to test lmdb
6
7
  let HNSWTest;
7
8
  let testInstance = new HierarchicalNavigableSmallWorld();
8
9
  let all = [];
@@ -163,6 +163,36 @@ tables.CacheOfResource.sourcedFrom({
163
163
  },
164
164
  });
165
165
 
166
+ // test transparent HTTP caching straight through
167
+ tables.CacheOfHttp.sourcedFrom({
168
+ async get(target) {
169
+ switch (target) {
170
+ case 'direct-fetch':
171
+ return fetch(`http://localhost:9926/FourProp/2`);
172
+ case 'fetch-body':
173
+ const response = await fetch(`http://localhost:9926/FourProp/2`);
174
+ return new Response(response.body, {
175
+ headers: {
176
+ 'Content-Type': 'text/plain',
177
+ },
178
+ });
179
+ case 'created-response':
180
+ return new Response('test', {
181
+ headers: { 'x-custom-header': 'custom value', 'cache-control': 'max-age=10, s-maxage=20' },
182
+ });
183
+ case 'html-response':
184
+ return new Response('<html>test</html>', {
185
+ headers: { 'content-type': 'text/html' },
186
+ });
187
+ case 'headers-in-data':
188
+ return {
189
+ headers: { 'x-custom-header': 'custom value' },
190
+ name: 'test-sibling-to-headers',
191
+ };
192
+ }
193
+ },
194
+ });
195
+
166
196
  export class FourPropWithHistory extends tables.FourProp {
167
197
  async subscribe(options) {
168
198
  let context = this.getContext();
@@ -56,6 +56,11 @@ type SimpleCache @table {
56
56
  type CacheOfResource @table @export {
57
57
  id: ID @primaryKey
58
58
  }
59
+ type CacheOfHttp @table @export {
60
+ headers: Any
61
+ data: Any
62
+ body: Blob
63
+ }
59
64
 
60
65
  type HasBigInt @table @export {
61
66
  id: BigInt @primaryKey
@@ -0,0 +1,107 @@
1
+ 'use strict';
2
+
3
+ const assert = require('assert');
4
+ const terms = require('#src/utility/hdbTerms');
5
+ const { OPERATION_PERMISSION_GROUPS, expandOperationsPerms } = require('#src/utility/operationPermissions');
6
+
7
+ describe('operationPermissions', function () {
8
+ describe('OPERATION_PERMISSION_GROUPS', function () {
9
+ it('read_only contains expected read operations', function () {
10
+ const ops = OPERATION_PERMISSION_GROUPS.read_only;
11
+ assert.ok(ops.includes(terms.OPERATIONS_ENUM.SEARCH));
12
+ assert.ok(ops.includes(terms.OPERATIONS_ENUM.SQL));
13
+ assert.ok(ops.includes(terms.OPERATIONS_ENUM.DESCRIBE_ALL));
14
+ assert.ok(ops.includes(terms.OPERATIONS_ENUM.USER_INFO));
15
+ });
16
+
17
+ it('read_only does not contain write operations', function () {
18
+ const ops = OPERATION_PERMISSION_GROUPS.read_only;
19
+ assert.ok(!ops.includes(terms.OPERATIONS_ENUM.INSERT));
20
+ assert.ok(!ops.includes(terms.OPERATIONS_ENUM.UPDATE));
21
+ assert.ok(!ops.includes(terms.OPERATIONS_ENUM.DELETE));
22
+ assert.ok(!ops.includes(terms.OPERATIONS_ENUM.CSV_DATA_LOAD));
23
+ });
24
+
25
+ it('standard_user contains everything in read_only', function () {
26
+ const readOnly = new Set(OPERATION_PERMISSION_GROUPS.read_only);
27
+ for (const op of readOnly) {
28
+ assert.ok(OPERATION_PERMISSION_GROUPS.standard_user.includes(op), `standard_user missing read_only op: ${op}`);
29
+ }
30
+ });
31
+
32
+ it('standard_user contains data manipulation operations', function () {
33
+ const ops = OPERATION_PERMISSION_GROUPS.standard_user;
34
+ assert.ok(ops.includes(terms.OPERATIONS_ENUM.INSERT));
35
+ assert.ok(ops.includes(terms.OPERATIONS_ENUM.UPDATE));
36
+ assert.ok(ops.includes(terms.OPERATIONS_ENUM.UPSERT));
37
+ assert.ok(ops.includes(terms.OPERATIONS_ENUM.DELETE));
38
+ });
39
+
40
+ it('standard_user does not contain token management operations', function () {
41
+ const ops = OPERATION_PERMISSION_GROUPS.standard_user;
42
+ assert.ok(!ops.includes(terms.OPERATIONS_ENUM.CREATE_AUTHENTICATION_TOKENS));
43
+ assert.ok(!ops.includes(terms.OPERATIONS_ENUM.REFRESH_OPERATION_TOKEN));
44
+ });
45
+
46
+ it('standard_user does not contain schema DDL operations', function () {
47
+ const ops = OPERATION_PERMISSION_GROUPS.standard_user;
48
+ assert.ok(!ops.includes(terms.OPERATIONS_ENUM.CREATE_ATTRIBUTE));
49
+ });
50
+
51
+ it('admin_read contains elevated read operations', function () {
52
+ const ops = OPERATION_PERMISSION_GROUPS.admin_read;
53
+ assert.ok(ops.includes(terms.OPERATIONS_ENUM.GET_CONFIGURATION));
54
+ assert.ok(ops.includes(terms.OPERATIONS_ENUM.READ_LOG));
55
+ assert.ok(ops.includes(terms.OPERATIONS_ENUM.READ_AUDIT_LOG));
56
+ assert.ok(ops.includes(terms.OPERATIONS_ENUM.GET_CUSTOM_FUNCTIONS));
57
+ assert.ok(ops.includes(terms.OPERATIONS_ENUM.GET_COMPONENTS));
58
+ });
59
+
60
+ it('admin_read does not contain data write operations', function () {
61
+ const ops = OPERATION_PERMISSION_GROUPS.admin_read;
62
+ assert.ok(!ops.includes(terms.OPERATIONS_ENUM.INSERT));
63
+ assert.ok(!ops.includes(terms.OPERATIONS_ENUM.DELETE));
64
+ });
65
+ });
66
+
67
+ describe('expandOperationsPerms()', function () {
68
+ it('expands a group name to its member operations', function () {
69
+ const result = expandOperationsPerms(['read_only']);
70
+ assert.ok(result instanceof Set);
71
+ assert.ok(result.has(terms.OPERATIONS_ENUM.SEARCH));
72
+ assert.ok(result.has(terms.OPERATIONS_ENUM.SQL));
73
+ assert.ok(result.has(terms.OPERATIONS_ENUM.DESCRIBE_ALL));
74
+ assert.ok(result.has(terms.OPERATIONS_ENUM.USER_INFO));
75
+ });
76
+
77
+ it('group does not include write operations', function () {
78
+ const result = expandOperationsPerms(['read_only']);
79
+ assert.ok(!result.has(terms.OPERATIONS_ENUM.INSERT));
80
+ assert.ok(!result.has(terms.OPERATIONS_ENUM.UPDATE));
81
+ assert.ok(!result.has(terms.OPERATIONS_ENUM.DELETE));
82
+ assert.ok(!result.has(terms.OPERATIONS_ENUM.CSV_DATA_LOAD));
83
+ });
84
+
85
+ it('passes through an individual operation name directly', function () {
86
+ const result = expandOperationsPerms([terms.OPERATIONS_ENUM.RESTART]);
87
+ assert.ok(result.has(terms.OPERATIONS_ENUM.RESTART));
88
+ assert.equal(result.size, 1);
89
+ });
90
+
91
+ it('combines a group and individual operations into a union set', function () {
92
+ const result = expandOperationsPerms([
93
+ 'read_only',
94
+ terms.OPERATIONS_ENUM.RESTART,
95
+ terms.OPERATIONS_ENUM.LIST_USERS,
96
+ ]);
97
+ assert.ok(result.has(terms.OPERATIONS_ENUM.SEARCH));
98
+ assert.ok(result.has(terms.OPERATIONS_ENUM.RESTART));
99
+ assert.ok(result.has(terms.OPERATIONS_ENUM.LIST_USERS));
100
+ });
101
+
102
+ it('returns an empty set for an empty array', function () {
103
+ const result = expandOperationsPerms([]);
104
+ assert.equal(result.size, 0);
105
+ });
106
+ });
107
+ });
@@ -16,8 +16,11 @@ const write = require('#js/dataLayer/insert');
16
16
  const user = require('#src/security/user');
17
17
  const alasql = require('alasql');
18
18
  const search = require('#js/dataLayer/search');
19
+ const restart = require('#js/bin/restart');
20
+ const configUtils = require('#js/config/configUtils');
19
21
  const jobs = require('#js/server/jobs/jobs');
20
22
  const terms = require('#src/utility/hdbTerms');
23
+
21
24
  const schema = require('#js/dataLayer/schema');
22
25
  const PermissionResponseObject = require('#js/security/data_objects/PermissionResponseObject');
23
26
  const PermissionTableResponseObject = require('#js/security/data_objects/PermissionTableResponseObject');
@@ -1490,3 +1493,130 @@ describe('Test operation_authorization', function () {
1490
1493
  });
1491
1494
  });
1492
1495
  });
1496
+
1497
+ describe('Test operations permissions', function () {
1498
+ before(() => {
1499
+ global.hdb_schema = global.hdb_schema || {};
1500
+ testUtils.setGlobalSchema('id', TEST_SCHEMA, TEST_TABLE, TEST_ATTRIBUTES);
1501
+ });
1502
+
1503
+ after(() => {
1504
+ global.hdb_schema = undefined;
1505
+ });
1506
+
1507
+ // Helper: build a verifyPerms request JSON with operations set
1508
+ function makeOpUserRequest(operations, tablePerms = {}) {
1509
+ const req = getRequestJson(TEST_JSON);
1510
+ req.hdb_user.role.permission = {
1511
+ super_user: false,
1512
+ operations: operations,
1513
+ [TEST_SCHEMA]: {
1514
+ describe: true,
1515
+ tables: {
1516
+ [TEST_TABLE]: {
1517
+ describe: true,
1518
+ read: true,
1519
+ insert: false,
1520
+ update: false,
1521
+ delete: false,
1522
+ attribute_permissions: [],
1523
+ ...tablePerms,
1524
+ },
1525
+ },
1526
+ },
1527
+ };
1528
+ return req;
1529
+ }
1530
+
1531
+ // Helper: build a request for a schema-less SU-only operation
1532
+ function makeOpUserSystemRequest(operations) {
1533
+ return {
1534
+ operation: 'restart',
1535
+ hdb_user: {
1536
+ active: true,
1537
+ role: {
1538
+ id: 'op-user-test-id',
1539
+ permission: {
1540
+ super_user: false,
1541
+ operations: operations,
1542
+ },
1543
+ role: 'op_test_role',
1544
+ __updatedtime__: (roleUpdatedTimeCounter += 1),
1545
+ },
1546
+ username: 'op_test_user',
1547
+ },
1548
+ };
1549
+ }
1550
+
1551
+ describe('verifyPerms() — operations allowlist', function () {
1552
+ it('(NOMINAL) op in read_only group with table READ perm — search allowed', function () {
1553
+ const req_json = makeOpUserRequest(['read_only'], { read: true });
1554
+ req_json.operation = terms.OPERATIONS_ENUM.SEARCH_BY_CONDITIONS;
1555
+ const result = op_auth.verifyPerms(req_json, search.searchByConditions.name);
1556
+ assert.equal(result, null);
1557
+ });
1558
+
1559
+ it('op NOT in operations list — insert blocked even with table perms', function () {
1560
+ const req_json = makeOpUserRequest(['read_only'], { insert: true });
1561
+ const result = op_auth.verifyPerms(req_json, write.insert.name);
1562
+ assert.notEqual(result, null);
1563
+ assert.equal(result.unauthorized_access.length, 1);
1564
+ // Error message uses the API name (terms.OPERATIONS_ENUM.INSERT = 'insert'), not the internal
1565
+ // function name (write.insert.name = 'insertData'), because that's what users put in operations.
1566
+ assert.ok(
1567
+ JSON.stringify(result).includes(TEST_OPERATION_AUTH_ERROR.OP_NOT_IN_OPERATIONS(terms.OPERATIONS_ENUM.INSERT))
1568
+ );
1569
+ });
1570
+
1571
+ it('SU-only op in operations — restart granted without super_user', function () {
1572
+ const req_json = makeOpUserSystemRequest([terms.OPERATIONS_ENUM.RESTART]);
1573
+ const result = op_auth.verifyPerms(req_json, restart.restart.name);
1574
+ assert.equal(result, null);
1575
+ });
1576
+
1577
+ it('SU-only op NOT in operations — restart denied', function () {
1578
+ const req_json = makeOpUserSystemRequest(['read_only']);
1579
+ const result = op_auth.verifyPerms(req_json, restart.restart.name);
1580
+ assert.notEqual(result, null);
1581
+ assert.equal(result.unauthorized_access.length, 1);
1582
+ });
1583
+
1584
+ it('get_configuration granted via operations (SU-only bypass)', function () {
1585
+ const req_json = makeOpUserSystemRequest([terms.OPERATIONS_ENUM.GET_CONFIGURATION]);
1586
+ req_json.operation = terms.OPERATIONS_ENUM.GET_CONFIGURATION;
1587
+ const result = op_auth.verifyPerms(req_json, configUtils.getConfiguration.name);
1588
+ assert.equal(result, null);
1589
+ });
1590
+
1591
+ it('dual gate — op in operations but table READ perm false — search denied at CRUD level', function () {
1592
+ const req_json = makeOpUserRequest(['read_only'], { read: false });
1593
+ req_json.operation = terms.OPERATIONS_ENUM.SEARCH;
1594
+ const result = op_auth.verifyPerms(req_json, search.search.name);
1595
+ assert.notEqual(result, null);
1596
+ });
1597
+
1598
+ it('no operations set — SU-only op still denied for non-SU user (no regression)', function () {
1599
+ const req_json = {
1600
+ operation: terms.OPERATIONS_ENUM.RESTART,
1601
+ hdb_user: {
1602
+ active: true,
1603
+ role: {
1604
+ id: 'plain-role-id',
1605
+ permission: { super_user: false },
1606
+ role: 'plain_role',
1607
+ __updatedtime__: (roleUpdatedTimeCounter += 1),
1608
+ },
1609
+ username: 'plain_user',
1610
+ },
1611
+ };
1612
+ const result = op_auth.verifyPerms(req_json, restart.restart.name);
1613
+ assert.notEqual(result, null);
1614
+ assert.equal(result.unauthorized_access.length, 1);
1615
+ });
1616
+
1617
+ it('no operations set — normal data op works as before (no regression)', function () {
1618
+ const result = op_auth.verifyPerms(TEST_JSON, write.insert.name);
1619
+ assert.equal(result, null);
1620
+ });
1621
+ });
1622
+ });