@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.
- package/core/.github/workflows/create-release.yaml +181 -0
- package/core/.github/workflows/notify-release-published.yaml +50 -0
- package/core/.github/workflows/publish-docker.yaml +174 -0
- package/core/.github/workflows/publish-npm.yaml +173 -0
- package/core/CONTRIBUTING.md +2 -0
- package/core/bin/harper.js +6 -0
- package/core/components/PluginModule.ts +0 -1
- package/core/components/componentLoader.ts +16 -4
- package/core/dev/sync-commits.js +18 -7
- package/core/integrationTests/server/operation-user-rbac.test.ts +484 -0
- package/core/package.json +4 -3
- package/core/resources/DatabaseTransaction.ts +2 -1
- package/core/resources/LMDBTransaction.ts +9 -4
- package/core/resources/RecordEncoder.ts +5 -3
- package/core/resources/RequestTarget.ts +1 -1
- package/core/resources/Resource.ts +1 -1
- package/core/resources/RocksTransactionLogStore.ts +106 -12
- package/core/resources/Table.ts +108 -114
- package/core/resources/dataLoader.ts +0 -1
- package/core/resources/databases.ts +1 -1
- package/core/resources/jsResource.ts +0 -2
- package/core/resources/registrationDeprecated.ts +8 -0
- package/core/resources/transactionBroadcast.ts +29 -25
- package/core/security/auth.ts +14 -1
- package/core/security/jsLoader.ts +9 -2
- package/core/security/permissionsTranslator.js +4 -0
- package/core/security/user.ts +20 -2
- package/core/server/REST.ts +29 -16
- package/core/server/Server.ts +6 -2
- package/core/server/http.ts +5 -7
- package/core/server/serverHelpers/Request.ts +5 -0
- package/core/server/serverHelpers/serverUtilities.ts +6 -2
- package/core/server/static.ts +0 -2
- package/core/unitTests/apiTests/cache-test.mjs +37 -0
- package/core/unitTests/commonTestErrors.js +4 -0
- package/core/unitTests/dataLayer/SQLSearch.test.js +2 -2
- package/core/unitTests/dataLayer/sql-update.test.js +2 -2
- package/core/unitTests/resources/auditLog.test.js +167 -0
- package/core/unitTests/resources/caching.test.js +79 -0
- package/core/unitTests/resources/permissions.test.js +7 -2
- package/core/unitTests/resources/txn-tracking.test.js +10 -4
- package/core/unitTests/resources/vectorIndex.test.js +1 -0
- package/core/unitTests/testApp/resources.js +30 -0
- package/core/unitTests/testApp/schema.graphql +5 -0
- package/core/unitTests/utility/operationPermissions.test.js +107 -0
- package/core/unitTests/utility/operation_authorization.test.js +130 -0
- package/core/unitTests/validation/role_validation.test.js +79 -0
- package/core/utility/common_utils.js +8 -4
- package/core/utility/errors/commonErrors.js +4 -0
- package/core/utility/hdbTerms.ts +1 -0
- package/core/utility/operationPermissions.ts +96 -0
- package/core/utility/operation_authorization.js +150 -49
- package/core/validation/role_validation.js +23 -0
- package/dist/bin/harper.js +1 -1
- package/dist/bin/harper.js.map +1 -1
- package/dist/cloneNode/cloneNode.js +55 -38
- package/dist/cloneNode/cloneNode.js.map +1 -1
- package/dist/core/bin/harper.js +2 -0
- package/dist/core/bin/harper.js.map +1 -1
- package/dist/core/components/ComponentV1.js +1 -0
- package/dist/core/components/ComponentV1.js.map +1 -1
- package/dist/core/components/EntryHandler.js +35 -1
- package/dist/core/components/EntryHandler.js.map +1 -1
- package/dist/core/components/PluginModule.js +1 -0
- package/dist/core/components/PluginModule.js.map +1 -1
- package/dist/core/components/Scope.js +2 -0
- package/dist/core/components/Scope.js.map +1 -1
- package/dist/core/components/componentLoader.js +12 -3
- package/dist/core/components/componentLoader.js.map +1 -1
- package/dist/core/components/status/api.js +1 -0
- package/dist/core/components/status/api.js.map +1 -1
- package/dist/core/components/status/crossThread.js +1 -0
- package/dist/core/components/status/crossThread.js.map +1 -1
- package/dist/core/config/RootConfigWatcher.js +34 -4
- package/dist/core/config/RootConfigWatcher.js.map +1 -1
- package/dist/core/dataLayer/harperBridge/ResourceBridge.js +1 -0
- package/dist/core/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
- package/dist/core/resources/DatabaseTransaction.js +3 -1
- package/dist/core/resources/DatabaseTransaction.js.map +1 -1
- package/dist/core/resources/ErrorResource.js +1 -0
- package/dist/core/resources/ErrorResource.js.map +1 -1
- package/dist/core/resources/LMDBTransaction.js +10 -5
- package/dist/core/resources/LMDBTransaction.js.map +1 -1
- package/dist/core/resources/RecordEncoder.js +5 -3
- package/dist/core/resources/RecordEncoder.js.map +1 -1
- package/dist/core/resources/RequestTarget.js +3 -0
- package/dist/core/resources/RequestTarget.js.map +1 -1
- package/dist/core/resources/Resource.js +2 -1
- package/dist/core/resources/Resource.js.map +1 -1
- package/dist/core/resources/ResourceInterface.js +3 -0
- package/dist/core/resources/ResourceInterface.js.map +1 -1
- package/dist/core/resources/ResourceInterfaceV2.js +2 -0
- package/dist/core/resources/ResourceInterfaceV2.js.map +1 -1
- package/dist/core/resources/ResourceV2.js +3 -0
- package/dist/core/resources/ResourceV2.js.map +1 -1
- package/dist/core/resources/Resources.js +1 -0
- package/dist/core/resources/Resources.js.map +1 -1
- package/dist/core/resources/RocksIndexStore.js +1 -0
- package/dist/core/resources/RocksIndexStore.js.map +1 -1
- package/dist/core/resources/RocksTransactionLogStore.js +102 -12
- package/dist/core/resources/RocksTransactionLogStore.js.map +1 -1
- package/dist/core/resources/Table.js +100 -111
- package/dist/core/resources/Table.js.map +1 -1
- package/dist/core/resources/blob.js +1 -0
- package/dist/core/resources/blob.js.map +1 -1
- package/dist/core/resources/dataLoader.js +3 -2
- package/dist/core/resources/dataLoader.js.map +1 -1
- package/dist/core/resources/databases.js +1 -1
- package/dist/core/resources/databases.js.map +1 -1
- package/dist/core/resources/jsResource.js +2 -2
- package/dist/core/resources/jsResource.js.map +1 -1
- package/dist/core/resources/openApi.js +1 -0
- package/dist/core/resources/openApi.js.map +1 -1
- package/dist/core/resources/registrationDeprecated.js +11 -0
- package/dist/core/resources/registrationDeprecated.js.map +1 -0
- package/dist/core/resources/replayLogs.js +2 -0
- package/dist/core/resources/replayLogs.js.map +1 -1
- package/dist/core/resources/search.js +1 -0
- package/dist/core/resources/search.js.map +1 -1
- package/dist/core/resources/transactionBroadcast.js +30 -27
- package/dist/core/resources/transactionBroadcast.js.map +1 -1
- package/dist/core/security/auth.js +15 -1
- package/dist/core/security/auth.js.map +1 -1
- package/dist/core/security/jsLoader.js +10 -2
- package/dist/core/security/jsLoader.js.map +1 -1
- package/dist/core/security/permissionsTranslator.js +4 -0
- package/dist/core/security/permissionsTranslator.js.map +1 -1
- package/dist/core/security/user.js +16 -1
- package/dist/core/security/user.js.map +1 -1
- package/dist/core/server/REST.js +35 -18
- package/dist/core/server/REST.js.map +1 -1
- package/dist/core/server/Server.js +2 -0
- package/dist/core/server/Server.js.map +1 -1
- package/dist/core/server/http.js +5 -3
- package/dist/core/server/http.js.map +1 -1
- package/dist/core/server/operationsServer.js +2 -1
- package/dist/core/server/operationsServer.js.map +1 -1
- package/dist/core/server/serverHelpers/Request.js +2 -0
- package/dist/core/server/serverHelpers/Request.js.map +1 -1
- package/dist/core/server/serverHelpers/serverUtilities.js +3 -2
- package/dist/core/server/serverHelpers/serverUtilities.js.map +1 -1
- package/dist/core/server/static.js +1 -2
- package/dist/core/server/static.js.map +1 -1
- package/dist/core/utility/common_utils.js +7 -5
- package/dist/core/utility/common_utils.js.map +1 -1
- package/dist/core/utility/errors/commonErrors.js +3 -0
- package/dist/core/utility/errors/commonErrors.js.map +1 -1
- package/dist/core/utility/hdbTerms.js +1 -0
- package/dist/core/utility/hdbTerms.js.map +1 -1
- package/dist/core/utility/operationPermissions.js +101 -0
- package/dist/core/utility/operationPermissions.js.map +1 -0
- package/dist/core/utility/operation_authorization.js +83 -49
- package/dist/core/utility/operation_authorization.js.map +1 -1
- package/dist/core/validation/role_validation.js +19 -1
- package/dist/core/validation/role_validation.js.map +1 -1
- package/dist/licensing/usageLicensing.js +243 -0
- package/dist/licensing/usageLicensing.js.map +1 -0
- package/dist/licensing/validation.js +149 -0
- package/dist/licensing/validation.js.map +1 -0
- package/dist/replication/replicationConnection.js +90 -24
- package/dist/replication/replicationConnection.js.map +1 -1
- package/dist/replication/replicator.js +6 -2
- package/dist/replication/replicator.js.map +1 -1
- package/dist/replication/setNode.js +0 -1
- package/dist/replication/setNode.js.map +1 -1
- package/dist/security/certificate.js +206 -6
- package/dist/security/certificate.js.map +1 -1
- package/dist/security/keyService.js +58 -0
- package/dist/security/keyService.js.map +1 -0
- package/dist/security/sshKeyOperations.js +344 -0
- package/dist/security/sshKeyOperations.js.map +1 -0
- package/licensing/usageLicensing.ts +260 -0
- package/licensing/validation.ts +191 -0
- package/npm-shrinkwrap.json +509 -463
- package/package.json +6 -3
- package/replication/replicationConnection.ts +99 -31
- package/replication/replicator.ts +8 -3
- package/replication/setNode.ts +0 -1
- package/security/certificate.ts +259 -7
- package/security/keyService.ts +74 -0
- package/security/sshKeyOperations.ts +405 -0
- package/static/defaultConfig.yaml +2 -0
- package/studio/web/HDBDogOnly.svg +78 -0
- package/studio/web/assets/PPRadioGrotesk-Bold-DDaUYG8E.woff +0 -0
- package/studio/web/assets/fa-brands-400-CEJbCg16.woff +0 -0
- package/studio/web/assets/fa-brands-400-CSYNqBb_.ttf +0 -0
- package/studio/web/assets/fa-brands-400-DnkPfk3o.eot +0 -0
- package/studio/web/assets/fa-brands-400-UxlILjvJ.woff2 +0 -0
- package/studio/web/assets/fa-brands-400-cH1MgKbP.svg +3717 -0
- package/studio/web/assets/fa-regular-400-BhTwtT8w.eot +0 -0
- package/studio/web/assets/fa-regular-400-D1vz6WBx.ttf +0 -0
- package/studio/web/assets/fa-regular-400-DFnMcJPd.woff +0 -0
- package/studio/web/assets/fa-regular-400-DGzu1beS.woff2 +0 -0
- package/studio/web/assets/fa-regular-400-gwj8Pxq-.svg +801 -0
- package/studio/web/assets/fa-solid-900-B4ZZ7kfP.svg +5034 -0
- package/studio/web/assets/fa-solid-900-B6Axprfb.eot +0 -0
- package/studio/web/assets/fa-solid-900-BUswJgRo.woff2 +0 -0
- package/studio/web/assets/fa-solid-900-DOXgCApm.woff +0 -0
- package/studio/web/assets/fa-solid-900-mxuxnBEa.ttf +0 -0
- package/studio/web/assets/index-CjpELGtS.js +37 -0
- package/studio/web/assets/index-CjpELGtS.js.map +1 -0
- package/studio/web/assets/index-VoSl--bG.js +235 -0
- package/studio/web/assets/index-VoSl--bG.js.map +1 -0
- package/studio/web/assets/index-Y2g_iFpU.css +1 -0
- package/studio/web/assets/index-jiPwkrsB.css +1 -0
- package/studio/web/assets/index-o10iYrkU.js +2 -0
- package/studio/web/assets/index-o10iYrkU.js.map +1 -0
- package/studio/web/assets/index.lazy-D5hjT1pS.js +266 -0
- package/studio/web/assets/index.lazy-D5hjT1pS.js.map +1 -0
- package/studio/web/assets/profiler-B9Edexdi.js +2 -0
- package/studio/web/assets/profiler-B9Edexdi.js.map +1 -0
- package/studio/web/assets/react-redux-DsVOxD2E.js +6 -0
- package/studio/web/assets/react-redux-DsVOxD2E.js.map +1 -0
- package/studio/web/assets/startRecording-Bwd6AYRS.js +3 -0
- package/studio/web/assets/startRecording-Bwd6AYRS.js.map +1 -0
- package/studio/web/fabric-signup-background.webp +0 -0
- package/studio/web/fabric-signup-text.png +0 -0
- package/studio/web/favicon_purple.png +0 -0
- package/studio/web/github-icon.svg +15 -0
- package/studio/web/harper-fabric_black.png +0 -0
- package/studio/web/harper-fabric_white.png +0 -0
- package/studio/web/harper-studio_white.png +0 -0
- package/studio/web/index.html +16 -0
- package/studio/web/running.css +148 -0
- package/studio/web/running.html +147 -0
- package/studio/web/running.js +111 -0
package/core/dev/sync-commits.js
CHANGED
|
@@ -127,9 +127,12 @@ function generateCommitsToPick(startCommit) {
|
|
|
127
127
|
const commits = execSync(`git rev-list --reverse --first-parent ${startCommit}..old/main`)
|
|
128
128
|
.toString()
|
|
129
129
|
.trim()
|
|
130
|
-
.split('\n')
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
.split('\n')
|
|
131
|
+
.filter((c) => c !== '');
|
|
132
|
+
if (commits.length > 0) {
|
|
133
|
+
// write to file in case a human needs to take over
|
|
134
|
+
fs.writeFileSync('commits-to-pick.txt', commits.join('\n') + '\n');
|
|
135
|
+
}
|
|
133
136
|
return commits;
|
|
134
137
|
}
|
|
135
138
|
|
|
@@ -142,16 +145,24 @@ function isMergeCommit(commit) {
|
|
|
142
145
|
return true;
|
|
143
146
|
}
|
|
144
147
|
|
|
145
|
-
function
|
|
146
|
-
process.stdout.write('Finding commits to sync... ');
|
|
147
|
-
fetchCommits('old');
|
|
148
|
-
pullRemoteBranch('origin', 'main');
|
|
148
|
+
function createSyncBranch() {
|
|
149
149
|
const syncDate = new Date();
|
|
150
150
|
const month = String(syncDate.getMonth() + 1).padStart(2, '0');
|
|
151
151
|
const day = String(syncDate.getDate()).padStart(2, '0');
|
|
152
152
|
checkoutNewBranch(`sync-${month}${day}${syncDate.getFullYear()}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function doItRockapella(startCommit) {
|
|
156
|
+
process.stdout.write('Finding commits to sync... ');
|
|
157
|
+
fetchCommits('old');
|
|
158
|
+
pullRemoteBranch('origin', 'main');
|
|
153
159
|
const commits = generateCommitsToPick(startCommit);
|
|
154
160
|
console.log('✅');
|
|
161
|
+
if (commits.length === 0) {
|
|
162
|
+
console.log('No commits to sync. Exiting.');
|
|
163
|
+
letsBail(0);
|
|
164
|
+
}
|
|
165
|
+
createSyncBranch();
|
|
155
166
|
console.log(`\n${commits.length} commits found:`);
|
|
156
167
|
for (const commit of commits) {
|
|
157
168
|
if (isMergeCommit(commit)) {
|
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* operations RBAC integration tests.
|
|
3
|
+
*
|
|
4
|
+
* Tests the `operations` permission field on roles, which provides an
|
|
5
|
+
* operation-level allowlist enabling non-super_user roles to call specific
|
|
6
|
+
* operations (including SU-only ones) without full super_user access.
|
|
7
|
+
*
|
|
8
|
+
* Dual gate: operations restricts which ops are reachable, and table
|
|
9
|
+
* CRUD permissions still apply for data operations — both must pass.
|
|
10
|
+
*/
|
|
11
|
+
import { suite, test, before, after } from 'node:test';
|
|
12
|
+
import { strictEqual, ok } from 'node:assert/strict';
|
|
13
|
+
|
|
14
|
+
import { setupHarper, teardownHarper, type ContextWithHarper } from '../utils/harperLifecycle.ts';
|
|
15
|
+
|
|
16
|
+
const DATABASE = 'test_db';
|
|
17
|
+
const TABLE = 'dogs';
|
|
18
|
+
const HASH_ATTR = 'id';
|
|
19
|
+
|
|
20
|
+
const READ_ONLY_ROLE = 'read_only_ops_role';
|
|
21
|
+
const READ_ONLY_USER = 'readonly_user';
|
|
22
|
+
const READ_ONLY_PASS = 'Test1234!';
|
|
23
|
+
|
|
24
|
+
const SU_OPS_ROLE = 'su_ops_role';
|
|
25
|
+
const SU_OPS_USER = 'su_ops_user';
|
|
26
|
+
const SU_OPS_PASS = 'Test1234!';
|
|
27
|
+
|
|
28
|
+
const COMBINED_ROLE = 'combined_ops_role';
|
|
29
|
+
const COMBINED_USER = 'combined_user';
|
|
30
|
+
const COMBINED_PASS = 'Test1234!';
|
|
31
|
+
|
|
32
|
+
const STANDARD_USER_ROLE = 'standard_user_ops_role';
|
|
33
|
+
const STANDARD_USER_USER = 'standard_user_user';
|
|
34
|
+
const STANDARD_USER_PASS = 'Test1234!';
|
|
35
|
+
|
|
36
|
+
suite('operations RBAC', (ctx: ContextWithHarper) => {
|
|
37
|
+
before(async () => {
|
|
38
|
+
await setupHarper(ctx, { config: {}, env: {} });
|
|
39
|
+
|
|
40
|
+
const adminAuth = `Basic ${Buffer.from(`${ctx.harper.admin.username}:${ctx.harper.admin.password}`).toString('base64')}`;
|
|
41
|
+
|
|
42
|
+
async function op(body: object) {
|
|
43
|
+
const res = await fetch(ctx.harper.operationsAPIURL, {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': adminAuth },
|
|
46
|
+
body: JSON.stringify(body),
|
|
47
|
+
});
|
|
48
|
+
return res;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Create database, table, and seed data
|
|
52
|
+
await op({ operation: 'create_database', database: DATABASE });
|
|
53
|
+
await op({ operation: 'create_table', schema: DATABASE, table: TABLE, hash_attribute: HASH_ATTR });
|
|
54
|
+
await op({
|
|
55
|
+
operation: 'insert',
|
|
56
|
+
schema: DATABASE,
|
|
57
|
+
table: TABLE,
|
|
58
|
+
records: [
|
|
59
|
+
{ id: 1, name: 'Rex', breed: 'German Shepherd' },
|
|
60
|
+
{ id: 2, name: 'Buddy', breed: 'Labrador' },
|
|
61
|
+
],
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Create read_only_ops_role: operations restricts to read-only ops,
|
|
65
|
+
// with explicit READ permission on the test table (dual gate)
|
|
66
|
+
await op({
|
|
67
|
+
operation: 'add_role',
|
|
68
|
+
role: READ_ONLY_ROLE,
|
|
69
|
+
permission: {
|
|
70
|
+
operations: ['read_only'],
|
|
71
|
+
[DATABASE]: {
|
|
72
|
+
tables: {
|
|
73
|
+
[TABLE]: {
|
|
74
|
+
read: true,
|
|
75
|
+
insert: false,
|
|
76
|
+
update: false,
|
|
77
|
+
delete: false,
|
|
78
|
+
attribute_permissions: [],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Create su_ops_role: can call specific SU-only ops without being super_user
|
|
86
|
+
await op({
|
|
87
|
+
operation: 'add_role',
|
|
88
|
+
role: SU_OPS_ROLE,
|
|
89
|
+
permission: {
|
|
90
|
+
operations: ['get_configuration', 'system_information', 'list_users'],
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Create combined_ops_role: both read_only data ops AND a specific SU-only op in one role
|
|
95
|
+
await op({
|
|
96
|
+
operation: 'add_role',
|
|
97
|
+
role: COMBINED_ROLE,
|
|
98
|
+
permission: {
|
|
99
|
+
operations: ['read_only', 'get_configuration'],
|
|
100
|
+
[DATABASE]: {
|
|
101
|
+
tables: {
|
|
102
|
+
[TABLE]: {
|
|
103
|
+
read: true,
|
|
104
|
+
insert: false,
|
|
105
|
+
update: false,
|
|
106
|
+
delete: false,
|
|
107
|
+
attribute_permissions: [],
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Create standard_user_ops_role: full CRUD data access + two SU-only ops,
|
|
115
|
+
// demonstrating the "all normally available ops + targeted admin ops" pattern
|
|
116
|
+
await op({
|
|
117
|
+
operation: 'add_role',
|
|
118
|
+
role: STANDARD_USER_ROLE,
|
|
119
|
+
permission: {
|
|
120
|
+
operations: ['standard_user', 'get_configuration', 'system_information'],
|
|
121
|
+
[DATABASE]: {
|
|
122
|
+
tables: {
|
|
123
|
+
[TABLE]: {
|
|
124
|
+
read: true,
|
|
125
|
+
insert: true,
|
|
126
|
+
update: true,
|
|
127
|
+
delete: true,
|
|
128
|
+
attribute_permissions: [],
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Create test users
|
|
136
|
+
await op({
|
|
137
|
+
operation: 'add_user',
|
|
138
|
+
role: READ_ONLY_ROLE,
|
|
139
|
+
username: READ_ONLY_USER,
|
|
140
|
+
password: READ_ONLY_PASS,
|
|
141
|
+
active: true,
|
|
142
|
+
});
|
|
143
|
+
await op({ operation: 'add_user', role: SU_OPS_ROLE, username: SU_OPS_USER, password: SU_OPS_PASS, active: true });
|
|
144
|
+
await op({
|
|
145
|
+
operation: 'add_user',
|
|
146
|
+
role: COMBINED_ROLE,
|
|
147
|
+
username: COMBINED_USER,
|
|
148
|
+
password: COMBINED_PASS,
|
|
149
|
+
active: true,
|
|
150
|
+
});
|
|
151
|
+
await op({
|
|
152
|
+
operation: 'add_user',
|
|
153
|
+
role: STANDARD_USER_ROLE,
|
|
154
|
+
username: STANDARD_USER_USER,
|
|
155
|
+
password: STANDARD_USER_PASS,
|
|
156
|
+
active: true,
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
after(async () => {
|
|
161
|
+
await teardownHarper(ctx);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// -- helpers --
|
|
165
|
+
|
|
166
|
+
function authHeader(username: string, password: string) {
|
|
167
|
+
return `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function callOp(username: string, password: string, body: object) {
|
|
171
|
+
return fetch(ctx.harper.operationsAPIURL, {
|
|
172
|
+
method: 'POST',
|
|
173
|
+
headers: {
|
|
174
|
+
'Content-Type': 'application/json',
|
|
175
|
+
'Authorization': authHeader(username, password),
|
|
176
|
+
},
|
|
177
|
+
body: JSON.stringify(body),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// -- read_only_ops_role tests --
|
|
182
|
+
|
|
183
|
+
suite('read_only_ops_role', () => {
|
|
184
|
+
test('search_by_hash is allowed (in read_only group + table READ perm)', async () => {
|
|
185
|
+
const res = await callOp(READ_ONLY_USER, READ_ONLY_PASS, {
|
|
186
|
+
operation: 'search_by_hash',
|
|
187
|
+
schema: DATABASE,
|
|
188
|
+
table: TABLE,
|
|
189
|
+
hash_values: [1],
|
|
190
|
+
get_attributes: ['*'],
|
|
191
|
+
});
|
|
192
|
+
strictEqual(res.status, 200);
|
|
193
|
+
const body = (await res.json()) as any[];
|
|
194
|
+
ok(Array.isArray(body), 'Expected array response');
|
|
195
|
+
strictEqual(body[0].id, 1);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('sql SELECT is allowed (in read_only group + table READ perm)', async () => {
|
|
199
|
+
const res = await callOp(READ_ONLY_USER, READ_ONLY_PASS, {
|
|
200
|
+
operation: 'sql',
|
|
201
|
+
sql: `SELECT * FROM ${DATABASE}.${TABLE} WHERE id = 1`,
|
|
202
|
+
});
|
|
203
|
+
strictEqual(res.status, 200);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('describe_all is allowed (in read_only group)', async () => {
|
|
207
|
+
const res = await callOp(READ_ONLY_USER, READ_ONLY_PASS, {
|
|
208
|
+
operation: 'describe_all',
|
|
209
|
+
});
|
|
210
|
+
strictEqual(res.status, 200);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test('insert is denied (not in operations list)', async () => {
|
|
214
|
+
const res = await callOp(READ_ONLY_USER, READ_ONLY_PASS, {
|
|
215
|
+
operation: 'insert',
|
|
216
|
+
schema: DATABASE,
|
|
217
|
+
table: TABLE,
|
|
218
|
+
records: [{ id: 99, name: 'Max', breed: 'Poodle' }],
|
|
219
|
+
});
|
|
220
|
+
strictEqual(res.status, 403);
|
|
221
|
+
const body = (await res.json()) as any;
|
|
222
|
+
ok(
|
|
223
|
+
body.unauthorized_access?.[0]?.includes("'insert' is not permitted for this role's operations configuration"),
|
|
224
|
+
`Unexpected denial reason: ${JSON.stringify(body.unauthorized_access)}`
|
|
225
|
+
);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test('update is denied (not in operations list)', async () => {
|
|
229
|
+
const res = await callOp(READ_ONLY_USER, READ_ONLY_PASS, {
|
|
230
|
+
operation: 'update',
|
|
231
|
+
schema: DATABASE,
|
|
232
|
+
table: TABLE,
|
|
233
|
+
records: [{ id: 1, name: 'Rex Updated' }],
|
|
234
|
+
});
|
|
235
|
+
strictEqual(res.status, 403);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test('delete is denied (not in operations list)', async () => {
|
|
239
|
+
const res = await callOp(READ_ONLY_USER, READ_ONLY_PASS, {
|
|
240
|
+
operation: 'delete',
|
|
241
|
+
schema: DATABASE,
|
|
242
|
+
table: TABLE,
|
|
243
|
+
hash_values: [1],
|
|
244
|
+
});
|
|
245
|
+
strictEqual(res.status, 403);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('get_configuration is denied (SU-only op not in operations list)', async () => {
|
|
249
|
+
const res = await callOp(READ_ONLY_USER, READ_ONLY_PASS, {
|
|
250
|
+
operation: 'get_configuration',
|
|
251
|
+
});
|
|
252
|
+
strictEqual(res.status, 403);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// -- su_ops_role tests --
|
|
257
|
+
|
|
258
|
+
suite('su_ops_role', () => {
|
|
259
|
+
test('get_configuration is allowed (SU-only op granted via operations)', async () => {
|
|
260
|
+
const res = await callOp(SU_OPS_USER, SU_OPS_PASS, {
|
|
261
|
+
operation: 'get_configuration',
|
|
262
|
+
});
|
|
263
|
+
strictEqual(res.status, 200);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('system_information is allowed (SU-only op granted via operations)', async () => {
|
|
267
|
+
const res = await callOp(SU_OPS_USER, SU_OPS_PASS, {
|
|
268
|
+
operation: 'system_information',
|
|
269
|
+
});
|
|
270
|
+
strictEqual(res.status, 200);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test('list_users is allowed (SU-only op granted via operations)', async () => {
|
|
274
|
+
const res = await callOp(SU_OPS_USER, SU_OPS_PASS, {
|
|
275
|
+
operation: 'list_users',
|
|
276
|
+
});
|
|
277
|
+
strictEqual(res.status, 200);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('insert is denied (not in operations list)', async () => {
|
|
281
|
+
const res = await callOp(SU_OPS_USER, SU_OPS_PASS, {
|
|
282
|
+
operation: 'insert',
|
|
283
|
+
schema: DATABASE,
|
|
284
|
+
table: TABLE,
|
|
285
|
+
records: [{ id: 99, name: 'Max', breed: 'Poodle' }],
|
|
286
|
+
});
|
|
287
|
+
strictEqual(res.status, 403);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test('restart is denied (SU-only op not in operations list)', async () => {
|
|
291
|
+
const res = await callOp(SU_OPS_USER, SU_OPS_PASS, {
|
|
292
|
+
operation: 'restart',
|
|
293
|
+
});
|
|
294
|
+
strictEqual(res.status, 403);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test('search_by_hash is denied (not in operations list)', async () => {
|
|
298
|
+
const res = await callOp(SU_OPS_USER, SU_OPS_PASS, {
|
|
299
|
+
operation: 'search_by_hash',
|
|
300
|
+
schema: DATABASE,
|
|
301
|
+
table: TABLE,
|
|
302
|
+
hash_values: [1],
|
|
303
|
+
get_attributes: ['*'],
|
|
304
|
+
});
|
|
305
|
+
strictEqual(res.status, 403);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// -- role validation tests --
|
|
310
|
+
|
|
311
|
+
suite('add_role validation', () => {
|
|
312
|
+
test('non-array operations is rejected with 400', async () => {
|
|
313
|
+
const res = await callOp(ctx.harper.admin.username, ctx.harper.admin.password, {
|
|
314
|
+
operation: 'add_role',
|
|
315
|
+
role: 'bad_role_1',
|
|
316
|
+
permission: {
|
|
317
|
+
operations: true,
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
strictEqual(res.status, 400);
|
|
321
|
+
const body = (await res.json()) as any;
|
|
322
|
+
ok(JSON.stringify(body).includes('must be an array'), `Unexpected response: ${JSON.stringify(body)}`);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test('invalid operation name in operations is rejected with 400', async () => {
|
|
326
|
+
const res = await callOp(ctx.harper.admin.username, ctx.harper.admin.password, {
|
|
327
|
+
operation: 'add_role',
|
|
328
|
+
role: 'bad_role_2',
|
|
329
|
+
permission: {
|
|
330
|
+
operations: ['bogus_nonexistent_op'],
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
strictEqual(res.status, 400);
|
|
334
|
+
const body = (await res.json()) as any;
|
|
335
|
+
ok(JSON.stringify(body).includes('bogus_nonexistent_op'), `Unexpected response: ${JSON.stringify(body)}`);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test('valid operations with read_only group is accepted', async () => {
|
|
339
|
+
const res = await callOp(ctx.harper.admin.username, ctx.harper.admin.password, {
|
|
340
|
+
operation: 'add_role',
|
|
341
|
+
role: 'valid_role_1',
|
|
342
|
+
permission: {
|
|
343
|
+
operations: ['read_only'],
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
strictEqual(res.status, 200);
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// -- combined role: both data ops (read_only group) and SU-only op granted together --
|
|
351
|
+
|
|
352
|
+
suite('combined_ops_role (read_only + SU-only op)', () => {
|
|
353
|
+
test('search_by_hash is allowed (data op via read_only group + table READ perm)', async () => {
|
|
354
|
+
const res = await callOp(COMBINED_USER, COMBINED_PASS, {
|
|
355
|
+
operation: 'search_by_hash',
|
|
356
|
+
schema: DATABASE,
|
|
357
|
+
table: TABLE,
|
|
358
|
+
hash_values: [1],
|
|
359
|
+
get_attributes: ['*'],
|
|
360
|
+
});
|
|
361
|
+
strictEqual(res.status, 200);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test('get_configuration is allowed (SU-only bypass via operations)', async () => {
|
|
365
|
+
const res = await callOp(COMBINED_USER, COMBINED_PASS, {
|
|
366
|
+
operation: 'get_configuration',
|
|
367
|
+
});
|
|
368
|
+
strictEqual(res.status, 200);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test('insert is denied (not in operations list despite table existing in perms)', async () => {
|
|
372
|
+
const res = await callOp(COMBINED_USER, COMBINED_PASS, {
|
|
373
|
+
operation: 'insert',
|
|
374
|
+
schema: DATABASE,
|
|
375
|
+
table: TABLE,
|
|
376
|
+
records: [{ id: 98, name: 'Daisy', breed: 'Corgi' }],
|
|
377
|
+
});
|
|
378
|
+
strictEqual(res.status, 403);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test('restart is denied (SU-only op not in operations list)', async () => {
|
|
382
|
+
const res = await callOp(COMBINED_USER, COMBINED_PASS, {
|
|
383
|
+
operation: 'restart',
|
|
384
|
+
});
|
|
385
|
+
strictEqual(res.status, 403);
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// -- standard_user group: all non-SU data ops + targeted SU ops --
|
|
390
|
+
// This mirrors the docs example: "all normally available access + two SU operations"
|
|
391
|
+
|
|
392
|
+
suite('standard_user_ops_role (standard_user group + targeted SU ops)', () => {
|
|
393
|
+
test('search_by_hash is allowed (in standard_user group + table READ perm)', async () => {
|
|
394
|
+
const res = await callOp(STANDARD_USER_USER, STANDARD_USER_PASS, {
|
|
395
|
+
operation: 'search_by_hash',
|
|
396
|
+
schema: DATABASE,
|
|
397
|
+
table: TABLE,
|
|
398
|
+
hash_values: [1],
|
|
399
|
+
get_attributes: ['*'],
|
|
400
|
+
});
|
|
401
|
+
strictEqual(res.status, 200);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test('insert is allowed (in standard_user group + table INSERT perm)', async () => {
|
|
405
|
+
const res = await callOp(STANDARD_USER_USER, STANDARD_USER_PASS, {
|
|
406
|
+
operation: 'insert',
|
|
407
|
+
schema: DATABASE,
|
|
408
|
+
table: TABLE,
|
|
409
|
+
records: [{ id: 10, name: 'Luna', breed: 'Husky' }],
|
|
410
|
+
});
|
|
411
|
+
strictEqual(res.status, 200);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test('update is allowed (in standard_user group + table UPDATE perm)', async () => {
|
|
415
|
+
const res = await callOp(STANDARD_USER_USER, STANDARD_USER_PASS, {
|
|
416
|
+
operation: 'update',
|
|
417
|
+
schema: DATABASE,
|
|
418
|
+
table: TABLE,
|
|
419
|
+
records: [{ id: 10, name: 'Luna Updated' }],
|
|
420
|
+
});
|
|
421
|
+
strictEqual(res.status, 200);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test('delete is allowed (in standard_user group + table DELETE perm)', async () => {
|
|
425
|
+
const res = await callOp(STANDARD_USER_USER, STANDARD_USER_PASS, {
|
|
426
|
+
operation: 'delete',
|
|
427
|
+
schema: DATABASE,
|
|
428
|
+
table: TABLE,
|
|
429
|
+
hash_values: [10],
|
|
430
|
+
});
|
|
431
|
+
strictEqual(res.status, 200);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test('get_configuration is allowed (SU-only op explicitly granted)', async () => {
|
|
435
|
+
const res = await callOp(STANDARD_USER_USER, STANDARD_USER_PASS, {
|
|
436
|
+
operation: 'get_configuration',
|
|
437
|
+
});
|
|
438
|
+
strictEqual(res.status, 200);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
test('system_information is allowed (SU-only op explicitly granted)', async () => {
|
|
442
|
+
const res = await callOp(STANDARD_USER_USER, STANDARD_USER_PASS, {
|
|
443
|
+
operation: 'system_information',
|
|
444
|
+
});
|
|
445
|
+
strictEqual(res.status, 200);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
test('restart is denied (SU-only op not in operations list)', async () => {
|
|
449
|
+
const res = await callOp(STANDARD_USER_USER, STANDARD_USER_PASS, {
|
|
450
|
+
operation: 'restart',
|
|
451
|
+
});
|
|
452
|
+
strictEqual(res.status, 403);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
test('drop_database is denied (SU-only op not in operations list)', async () => {
|
|
456
|
+
const res = await callOp(STANDARD_USER_USER, STANDARD_USER_PASS, {
|
|
457
|
+
operation: 'drop_database',
|
|
458
|
+
database: DATABASE,
|
|
459
|
+
});
|
|
460
|
+
strictEqual(res.status, 403);
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
// -- regression: admin super_user behavior unchanged --
|
|
465
|
+
|
|
466
|
+
suite('admin (super_user) regression', () => {
|
|
467
|
+
test('admin can insert (no operations restriction)', async () => {
|
|
468
|
+
const res = await callOp(ctx.harper.admin.username, ctx.harper.admin.password, {
|
|
469
|
+
operation: 'insert',
|
|
470
|
+
schema: DATABASE,
|
|
471
|
+
table: TABLE,
|
|
472
|
+
records: [{ id: 50, name: 'Bella', breed: 'Beagle' }],
|
|
473
|
+
});
|
|
474
|
+
strictEqual(res.status, 200);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
test('admin can get_configuration (super_user unrestricted)', async () => {
|
|
478
|
+
const res = await callOp(ctx.harper.admin.username, ctx.harper.admin.password, {
|
|
479
|
+
operation: 'get_configuration',
|
|
480
|
+
});
|
|
481
|
+
strictEqual(res.status, 200);
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
});
|
package/core/package.json
CHANGED
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"scripts": {
|
|
33
33
|
"build": "tsc --project tsconfig.build.json",
|
|
34
34
|
"build:watch": "npm run build -- --watch --incremental",
|
|
35
|
+
"package": "npm run build; npm shrinkwrap && npm pack",
|
|
35
36
|
"lint": "oxlint --deny-warnings .",
|
|
36
37
|
"lint:required": "oxlint --quiet .",
|
|
37
38
|
"lint:fix": "npm run lint -- --fix",
|
|
@@ -41,8 +42,8 @@
|
|
|
41
42
|
"test:integration": "node integrationTests/utils/scripts/run.ts",
|
|
42
43
|
"test:unit": "mocha",
|
|
43
44
|
"test:unit:main": "mocha 'unitTests/**/*test.*js' --exclude 'unitTests/apiTests/**/*' --exclude 'unitTests/dataLayer/harperBridge/**/*' --exclude 'unitTests/resources/**/*'",
|
|
44
|
-
"test:unit:all": "npm run test:unit:main && npm run test:unit:apitests && npm run test:unit:resources",
|
|
45
|
-
"test:unit:lmdb": "HARPER_STORAGE_ENGINE=lmdb npm run test:unit:
|
|
45
|
+
"test:unit:all": "npm run test:unit:main && npm run test:unit:apitests && npm run test:unit:resources && npm run test:unit:lmdb",
|
|
46
|
+
"test:unit:lmdb": "HARPER_STORAGE_ENGINE=lmdb npm run test:unit:resources && HARPER_STORAGE_ENGINE=lmdb npm run test:unit:apitests",
|
|
46
47
|
"test:unit:components": "mocha 'unitTests/components/**/*.js'",
|
|
47
48
|
"test:unit:resources": "mocha 'unitTests/resources/**/*.js'",
|
|
48
49
|
"test:unit:bin": "mocha 'unitTests/bin/**/*.js'",
|
|
@@ -139,7 +140,7 @@
|
|
|
139
140
|
"@fastify/cors": "~9.0.1",
|
|
140
141
|
"@fastify/static": "~7.0.4",
|
|
141
142
|
"@harperfast/extended-iterable": "^1.0.1",
|
|
142
|
-
"@harperfast/rocksdb-js": "^0.1.
|
|
143
|
+
"@harperfast/rocksdb-js": "^0.1.10",
|
|
143
144
|
"@turf/area": "6.5.0",
|
|
144
145
|
"@turf/boolean-contains": "6.5.0",
|
|
145
146
|
"@turf/boolean-disjoint": "6.5.0",
|
|
@@ -213,13 +213,14 @@ export class DatabaseTransaction implements Transaction {
|
|
|
213
213
|
if (!outstandingCommit) {
|
|
214
214
|
outstandingCommit = commitResolution;
|
|
215
215
|
outstandingCommitStart = performance.now();
|
|
216
|
-
outstandingCommit.
|
|
216
|
+
outstandingCommit.finally(() => {
|
|
217
217
|
outstandingCommit = null;
|
|
218
218
|
});
|
|
219
219
|
}
|
|
220
220
|
const completions = [];
|
|
221
221
|
return commitResolution.then(
|
|
222
222
|
() => {
|
|
223
|
+
this.transaction.onCommit?.();
|
|
223
224
|
this.transaction = null; // the native transaction is done (reset if needed)
|
|
224
225
|
if (this.next) {
|
|
225
226
|
completions.push(this.next.commit(options));
|
|
@@ -42,6 +42,7 @@ export class LMDBTransaction extends DatabaseTransaction {
|
|
|
42
42
|
getReadTxn(): ReadTransaction {
|
|
43
43
|
// used optimistically
|
|
44
44
|
this.readTxnRefCount = (this.readTxnRefCount || 0) + 1;
|
|
45
|
+
this.timeout = txnExpiration; // reset the timeout
|
|
45
46
|
if (this.stale) this.stale = false;
|
|
46
47
|
if (this.readTxn) {
|
|
47
48
|
if (this.readTxn.openTimer) this.readTxn.openTimer = 0;
|
|
@@ -321,15 +322,19 @@ let timer;
|
|
|
321
322
|
function startMonitoringTxns() {
|
|
322
323
|
timer = setInterval(function () {
|
|
323
324
|
for (const txn of trackedTxns) {
|
|
324
|
-
if (txn.
|
|
325
|
+
if (txn.timeout <= 0) {
|
|
325
326
|
const url = txn.getContext()?.url;
|
|
326
327
|
harperLogger.error(
|
|
327
|
-
`Transaction was open too long and has been
|
|
328
|
+
`Transaction was open too long and has been committed, from table: ${
|
|
328
329
|
txn.db?.name + (url ? ' path: ' + url : '')
|
|
329
330
|
}`
|
|
330
331
|
);
|
|
331
|
-
|
|
332
|
-
|
|
332
|
+
// reset the transaction
|
|
333
|
+
txn.commit();
|
|
334
|
+
txn.timeout = txnExpiration;
|
|
335
|
+
} else {
|
|
336
|
+
txn.timeout -= txnExpiration;
|
|
337
|
+
}
|
|
333
338
|
}
|
|
334
339
|
}, txnExpiration).unref();
|
|
335
340
|
}
|
|
@@ -558,6 +558,7 @@ export function recordUpdater(store, tableId, auditStore) {
|
|
|
558
558
|
store.encoder.structureUpdate = null;
|
|
559
559
|
}
|
|
560
560
|
const structureVersion = store.encoder.structures.length + (store.encoder.typedStructs?.length ?? 0);
|
|
561
|
+
const nodeId = options?.nodeId ?? server.replication?.getThisNodeId(auditStore) ?? 0;
|
|
561
562
|
if (resolveRecord && existingEntry?.localTime) {
|
|
562
563
|
const replacingId = existingEntry?.localTime;
|
|
563
564
|
const replacingEntry = auditStore.get(replacingId, tableId, id);
|
|
@@ -570,7 +571,7 @@ export function recordUpdater(store, tableId, auditStore) {
|
|
|
570
571
|
tableId,
|
|
571
572
|
recordId: id,
|
|
572
573
|
previousVersion,
|
|
573
|
-
nodeId
|
|
574
|
+
nodeId,
|
|
574
575
|
user: username,
|
|
575
576
|
type,
|
|
576
577
|
encodedRecord: lastValueEncoding,
|
|
@@ -580,7 +581,7 @@ export function recordUpdater(store, tableId, auditStore) {
|
|
|
580
581
|
expiresAt,
|
|
581
582
|
structureVersion,
|
|
582
583
|
},
|
|
583
|
-
{ ifVersion: ifVersion, transaction: options.transaction }
|
|
584
|
+
{ ifVersion: ifVersion, transaction: options.transaction, nodeId }
|
|
584
585
|
);
|
|
585
586
|
return result;
|
|
586
587
|
}
|
|
@@ -592,7 +593,7 @@ export function recordUpdater(store, tableId, auditStore) {
|
|
|
592
593
|
tableId,
|
|
593
594
|
recordId: id,
|
|
594
595
|
previousVersion: store instanceof RocksDatabase ? existingEntry?.version : existingEntry?.localTime ? 1 : 0,
|
|
595
|
-
nodeId
|
|
596
|
+
nodeId,
|
|
596
597
|
user: username,
|
|
597
598
|
type,
|
|
598
599
|
encodedRecord: lastValueEncoding,
|
|
@@ -609,6 +610,7 @@ export function recordUpdater(store, tableId, auditStore) {
|
|
|
609
610
|
instructedWrite: true,
|
|
610
611
|
ifVersion,
|
|
611
612
|
transaction: options.transaction,
|
|
613
|
+
nodeId,
|
|
612
614
|
}
|
|
613
615
|
);
|
|
614
616
|
}
|
|
@@ -29,7 +29,7 @@ export class RequestTarget extends URLSearchParams {
|
|
|
29
29
|
declare operator?: 'AND' | 'OR';
|
|
30
30
|
/** The sort attribute and direction to use */
|
|
31
31
|
/** @ts-expect-error USP has a sort method, we hide it */
|
|
32
|
-
|
|
32
|
+
sort?: Sort = null;
|
|
33
33
|
/** The selected attributes to return */
|
|
34
34
|
declare select?: Select;
|
|
35
35
|
/** Return an explanation of the query order */
|
|
@@ -191,7 +191,7 @@ export class Resource<Record extends object = any> implements ResourceInterface<
|
|
|
191
191
|
}
|
|
192
192
|
static invalidate = transactional(
|
|
193
193
|
function (resource: Resource, query: RequestTarget, _request: Context, _data: any) {
|
|
194
|
-
return resource.invalidate ? resource.invalidate(query) : missingMethod(resource, '
|
|
194
|
+
return resource.invalidate ? resource.invalidate(query) : missingMethod(resource, 'invalidate');
|
|
195
195
|
},
|
|
196
196
|
{ hasContent: false, type: 'update', method: 'invalidate' }
|
|
197
197
|
);
|