@harperfast/harper 5.2.0-alpha.5 → 5.2.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent/agent.ts +152 -10
- package/agent/bestPractices.ts +58 -0
- package/agent/mcpTools.ts +122 -0
- package/agent/operations.ts +10 -1
- package/agent/registryTools.ts +117 -0
- package/agent/session.ts +16 -7
- package/agent/tools/fsTools.ts +74 -27
- package/agent/tools/inspectorTool.ts +459 -0
- package/agent/toolset.ts +27 -7
- package/agent/types.ts +2 -0
- package/components/Application.ts +566 -85
- package/components/OptionsWatcher.ts +62 -2
- package/components/Scope.ts +31 -8
- package/components/componentLoader.ts +5 -1
- package/components/componentSecrets.ts +514 -55
- package/components/deploymentRecorder.ts +7 -6
- package/components/gitCredentialHelper.js +115 -0
- package/components/gitCredentialServer.ts +251 -0
- package/components/mcp/tools/application.ts +197 -23
- package/components/mcp/tools/operations.ts +1 -1
- package/components/mcp/tools/schemas/derive.ts +6 -2
- package/components/operations.js +21 -19
- package/components/operationsValidation.js +86 -21
- package/components/secretOperations.ts +110 -30
- package/config/harperConfigEnvVars.ts +90 -5
- package/config-root.schema.json +4 -0
- package/dataLayer/harperBridge/ResourceBridge.ts +7 -0
- package/dist/agent/agent.d.ts +24 -3
- package/dist/agent/agent.js +172 -10
- package/dist/agent/agent.js.map +1 -1
- package/dist/agent/bestPractices.d.ts +24 -0
- package/dist/agent/bestPractices.js +60 -0
- package/dist/agent/bestPractices.js.map +1 -0
- package/dist/agent/mcpTools.d.ts +24 -0
- package/dist/agent/mcpTools.js +115 -0
- package/dist/agent/mcpTools.js.map +1 -0
- package/dist/agent/operations.js +10 -1
- package/dist/agent/operations.js.map +1 -1
- package/dist/agent/registryTools.d.ts +45 -0
- package/dist/agent/registryTools.js +113 -0
- package/dist/agent/registryTools.js.map +1 -0
- package/dist/agent/session.js +16 -7
- package/dist/agent/session.js.map +1 -1
- package/dist/agent/tools/fsTools.js +70 -28
- package/dist/agent/tools/fsTools.js.map +1 -1
- package/dist/agent/tools/inspectorTool.d.ts +40 -0
- package/dist/agent/tools/inspectorTool.js +428 -0
- package/dist/agent/tools/inspectorTool.js.map +1 -0
- package/dist/agent/toolset.d.ts +16 -6
- package/dist/agent/toolset.js +17 -7
- package/dist/agent/toolset.js.map +1 -1
- package/dist/agent/types.d.ts +2 -0
- package/dist/components/Application.d.ts +73 -18
- package/dist/components/Application.js +475 -64
- package/dist/components/Application.js.map +1 -1
- package/dist/components/OptionsWatcher.d.ts +1 -1
- package/dist/components/OptionsWatcher.js +65 -2
- package/dist/components/OptionsWatcher.js.map +1 -1
- package/dist/components/Scope.d.ts +8 -6
- package/dist/components/Scope.js +22 -6
- package/dist/components/Scope.js.map +1 -1
- package/dist/components/componentLoader.js +5 -1
- package/dist/components/componentLoader.js.map +1 -1
- package/dist/components/componentSecrets.d.ts +33 -7
- package/dist/components/componentSecrets.js +473 -59
- package/dist/components/componentSecrets.js.map +1 -1
- package/dist/components/deploymentRecorder.d.ts +2 -2
- package/dist/components/deploymentRecorder.js +1 -1
- package/dist/components/deploymentRecorder.js.map +1 -1
- package/dist/components/gitCredentialHelper.d.ts +1 -0
- package/dist/components/gitCredentialHelper.js +113 -0
- package/dist/components/gitCredentialHelper.js.map +1 -0
- package/dist/components/gitCredentialServer.d.ts +33 -0
- package/dist/components/gitCredentialServer.js +236 -0
- package/dist/components/gitCredentialServer.js.map +1 -0
- package/dist/components/mcp/tools/application.d.ts +19 -0
- package/dist/components/mcp/tools/application.js +180 -22
- package/dist/components/mcp/tools/application.js.map +1 -1
- package/dist/components/mcp/tools/operations.d.ts +13 -0
- package/dist/components/mcp/tools/operations.js +1 -0
- package/dist/components/mcp/tools/operations.js.map +1 -1
- package/dist/components/mcp/tools/schemas/derive.js +6 -2
- package/dist/components/mcp/tools/schemas/derive.js.map +1 -1
- package/dist/components/operations.js +23 -21
- package/dist/components/operations.js.map +1 -1
- package/dist/components/operationsValidation.js +84 -21
- package/dist/components/operationsValidation.js.map +1 -1
- package/dist/components/secretOperations.d.ts +33 -11
- package/dist/components/secretOperations.js +90 -26
- package/dist/components/secretOperations.js.map +1 -1
- package/dist/config/harperConfigEnvVars.d.ts +21 -0
- package/dist/config/harperConfigEnvVars.js +95 -5
- package/dist/config/harperConfigEnvVars.js.map +1 -1
- package/dist/dataLayer/harperBridge/ResourceBridge.js +8 -0
- package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +14 -1
- package/dist/index.js.map +1 -1
- package/dist/resources/Resource.d.ts +19 -0
- package/dist/resources/Resource.js +98 -4
- package/dist/resources/Resource.js.map +1 -1
- package/dist/resources/Table.d.ts +12 -1
- package/dist/resources/Table.js +269 -53
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/analytics/read.js +28 -25
- package/dist/resources/analytics/read.js.map +1 -1
- package/dist/resources/databases.js +21 -0
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/defineResource.d.ts +180 -0
- package/dist/resources/defineResource.js +505 -0
- package/dist/resources/defineResource.js.map +1 -0
- package/dist/resources/defineTable.d.ts +221 -0
- package/dist/resources/defineTable.js +227 -0
- package/dist/resources/defineTable.js.map +1 -0
- package/dist/resources/jsonSchemaTypes.d.ts +2 -0
- package/dist/resources/jsonSchemaTypes.js +12 -4
- package/dist/resources/jsonSchemaTypes.js.map +1 -1
- package/dist/resources/openApi.js +69 -13
- package/dist/resources/openApi.js.map +1 -1
- package/dist/resources/search.js +6 -8
- package/dist/resources/search.js.map +1 -1
- package/dist/server/graphqlQuerying.js +4 -2
- package/dist/server/graphqlQuerying.js.map +1 -1
- package/dist/server/http.d.ts +12 -0
- package/dist/server/http.js +35 -15
- package/dist/server/http.js.map +1 -1
- package/dist/server/serverHelpers/serverUtilities.js +8 -5
- package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
- package/dist/server/serverHelpers/uwsServer.js +23 -0
- package/dist/server/serverHelpers/uwsServer.js.map +1 -1
- package/dist/sqlEngine/binder/bind.d.ts +72 -0
- package/dist/sqlEngine/binder/bind.js +289 -0
- package/dist/sqlEngine/binder/bind.js.map +1 -0
- package/dist/sqlEngine/config.d.ts +35 -0
- package/dist/sqlEngine/config.js +62 -0
- package/dist/sqlEngine/config.js.map +1 -0
- package/dist/sqlEngine/diff/differential.d.ts +23 -0
- package/dist/sqlEngine/diff/differential.js +90 -0
- package/dist/sqlEngine/diff/differential.js.map +1 -0
- package/dist/sqlEngine/errors.d.ts +22 -0
- package/dist/sqlEngine/errors.js +36 -0
- package/dist/sqlEngine/errors.js.map +1 -0
- package/dist/sqlEngine/executor/runMutation.d.ts +35 -0
- package/dist/sqlEngine/executor/runMutation.js +352 -0
- package/dist/sqlEngine/executor/runMutation.js.map +1 -0
- package/dist/sqlEngine/executor/runSelect.d.ts +7 -0
- package/dist/sqlEngine/executor/runSelect.js +14 -0
- package/dist/sqlEngine/executor/runSelect.js.map +1 -0
- package/dist/sqlEngine/expressions/compile.d.ts +24 -0
- package/dist/sqlEngine/expressions/compile.js +326 -0
- package/dist/sqlEngine/expressions/compile.js.map +1 -0
- package/dist/sqlEngine/functions/aggregates.d.ts +12 -0
- package/dist/sqlEngine/functions/aggregates.js +299 -0
- package/dist/sqlEngine/functions/aggregates.js.map +1 -0
- package/dist/sqlEngine/functions/registry.d.ts +36 -0
- package/dist/sqlEngine/functions/registry.js +33 -0
- package/dist/sqlEngine/functions/registry.js.map +1 -0
- package/dist/sqlEngine/functions/standard.d.ts +9 -0
- package/dist/sqlEngine/functions/standard.js +86 -0
- package/dist/sqlEngine/functions/standard.js.map +1 -0
- package/dist/sqlEngine/index.d.ts +29 -0
- package/dist/sqlEngine/index.js +62 -0
- package/dist/sqlEngine/index.js.map +1 -0
- package/dist/sqlEngine/logical/build.d.ts +20 -0
- package/dist/sqlEngine/logical/build.js +326 -0
- package/dist/sqlEngine/logical/build.js.map +1 -0
- package/dist/sqlEngine/logical/op.d.ts +105 -0
- package/dist/sqlEngine/logical/op.js +10 -0
- package/dist/sqlEngine/logical/op.js.map +1 -0
- package/dist/sqlEngine/optimizer/joinAnalysis.d.ts +37 -0
- package/dist/sqlEngine/optimizer/joinAnalysis.js +144 -0
- package/dist/sqlEngine/optimizer/joinAnalysis.js.map +1 -0
- package/dist/sqlEngine/optimizer/optimize.d.ts +13 -0
- package/dist/sqlEngine/optimizer/optimize.js +53 -0
- package/dist/sqlEngine/optimizer/optimize.js.map +1 -0
- package/dist/sqlEngine/optimizer/ruleEngine.d.ts +7 -0
- package/dist/sqlEngine/optimizer/ruleEngine.js +26 -0
- package/dist/sqlEngine/optimizer/ruleEngine.js.map +1 -0
- package/dist/sqlEngine/optimizer/rules/limitPushdown.d.ts +19 -0
- package/dist/sqlEngine/optimizer/rules/limitPushdown.js +55 -0
- package/dist/sqlEngine/optimizer/rules/limitPushdown.js.map +1 -0
- package/dist/sqlEngine/optimizer/rules/planJoins.d.ts +19 -0
- package/dist/sqlEngine/optimizer/rules/planJoins.js +62 -0
- package/dist/sqlEngine/optimizer/rules/planJoins.js.map +1 -0
- package/dist/sqlEngine/optimizer/rules/predicateNormalize.d.ts +18 -0
- package/dist/sqlEngine/optimizer/rules/predicateNormalize.js +80 -0
- package/dist/sqlEngine/optimizer/rules/predicateNormalize.js.map +1 -0
- package/dist/sqlEngine/optimizer/rules/predicatePushdown.d.ts +11 -0
- package/dist/sqlEngine/optimizer/rules/predicatePushdown.js +48 -0
- package/dist/sqlEngine/optimizer/rules/predicatePushdown.js.map +1 -0
- package/dist/sqlEngine/optimizer/rules/projectionPushdown.d.ts +23 -0
- package/dist/sqlEngine/optimizer/rules/projectionPushdown.js +160 -0
- package/dist/sqlEngine/optimizer/rules/projectionPushdown.js.map +1 -0
- package/dist/sqlEngine/optimizer/rules/sortFromIndex.d.ts +14 -0
- package/dist/sqlEngine/optimizer/rules/sortFromIndex.js +49 -0
- package/dist/sqlEngine/optimizer/rules/sortFromIndex.js.map +1 -0
- package/dist/sqlEngine/optimizer/rules/validateScannable.d.ts +15 -0
- package/dist/sqlEngine/optimizer/rules/validateScannable.js +60 -0
- package/dist/sqlEngine/optimizer/rules/validateScannable.js.map +1 -0
- package/dist/sqlEngine/optimizer/whereToConditions.d.ts +85 -0
- package/dist/sqlEngine/optimizer/whereToConditions.js +484 -0
- package/dist/sqlEngine/optimizer/whereToConditions.js.map +1 -0
- package/dist/sqlEngine/parser/ast.d.ts +139 -0
- package/dist/sqlEngine/parser/ast.js +10 -0
- package/dist/sqlEngine/parser/ast.js.map +1 -0
- package/dist/sqlEngine/parser/normalizer.d.ts +28 -0
- package/dist/sqlEngine/parser/normalizer.js +413 -0
- package/dist/sqlEngine/parser/normalizer.js.map +1 -0
- package/dist/sqlEngine/parser/parse.d.ts +14 -0
- package/dist/sqlEngine/parser/parse.js +20 -0
- package/dist/sqlEngine/parser/parse.js.map +1 -0
- package/dist/sqlEngine/physical/PhysicalDistinct.d.ts +9 -0
- package/dist/sqlEngine/physical/PhysicalDistinct.js +29 -0
- package/dist/sqlEngine/physical/PhysicalDistinct.js.map +1 -0
- package/dist/sqlEngine/physical/PhysicalFilter.d.ts +10 -0
- package/dist/sqlEngine/physical/PhysicalFilter.js +25 -0
- package/dist/sqlEngine/physical/PhysicalFilter.js.map +1 -0
- package/dist/sqlEngine/physical/PhysicalHashAggregate.d.ts +27 -0
- package/dist/sqlEngine/physical/PhysicalHashAggregate.js +100 -0
- package/dist/sqlEngine/physical/PhysicalHashAggregate.js.map +1 -0
- package/dist/sqlEngine/physical/PhysicalHashJoin.d.ts +29 -0
- package/dist/sqlEngine/physical/PhysicalHashJoin.js +93 -0
- package/dist/sqlEngine/physical/PhysicalHashJoin.js.map +1 -0
- package/dist/sqlEngine/physical/PhysicalIndexNestedLoopJoin.d.ts +36 -0
- package/dist/sqlEngine/physical/PhysicalIndexNestedLoopJoin.js +78 -0
- package/dist/sqlEngine/physical/PhysicalIndexNestedLoopJoin.js.map +1 -0
- package/dist/sqlEngine/physical/PhysicalIndexScan.d.ts +22 -0
- package/dist/sqlEngine/physical/PhysicalIndexScan.js +77 -0
- package/dist/sqlEngine/physical/PhysicalIndexScan.js.map +1 -0
- package/dist/sqlEngine/physical/PhysicalLimit.d.ts +5 -0
- package/dist/sqlEngine/physical/PhysicalLimit.js +28 -0
- package/dist/sqlEngine/physical/PhysicalLimit.js.map +1 -0
- package/dist/sqlEngine/physical/PhysicalNestedLoopJoin.d.ts +20 -0
- package/dist/sqlEngine/physical/PhysicalNestedLoopJoin.js +51 -0
- package/dist/sqlEngine/physical/PhysicalNestedLoopJoin.js.map +1 -0
- package/dist/sqlEngine/physical/PhysicalProject.d.ts +18 -0
- package/dist/sqlEngine/physical/PhysicalProject.js +117 -0
- package/dist/sqlEngine/physical/PhysicalProject.js.map +1 -0
- package/dist/sqlEngine/physical/PhysicalQualify.d.ts +13 -0
- package/dist/sqlEngine/physical/PhysicalQualify.js +29 -0
- package/dist/sqlEngine/physical/PhysicalQualify.js.map +1 -0
- package/dist/sqlEngine/physical/PhysicalSort.d.ts +12 -0
- package/dist/sqlEngine/physical/PhysicalSort.js +59 -0
- package/dist/sqlEngine/physical/PhysicalSort.js.map +1 -0
- package/dist/sqlEngine/physical/op.d.ts +11 -0
- package/dist/sqlEngine/physical/op.js +9 -0
- package/dist/sqlEngine/physical/op.js.map +1 -0
- package/dist/sqlEngine/physical/plan.d.ts +23 -0
- package/dist/sqlEngine/physical/plan.js +162 -0
- package/dist/sqlEngine/physical/plan.js.map +1 -0
- package/dist/sqlEngine/router.d.ts +26 -0
- package/dist/sqlEngine/router.js +48 -0
- package/dist/sqlEngine/router.js.map +1 -0
- package/dist/sqlEngine/types.d.ts +37 -0
- package/dist/sqlEngine/types.js +13 -0
- package/dist/sqlEngine/types.js.map +1 -0
- package/dist/sqlTranslator/index.js +7 -1
- package/dist/sqlTranslator/index.js.map +1 -1
- package/dist/utility/errors/hdbError.d.ts +21 -0
- package/dist/utility/errors/hdbError.js +23 -1
- package/dist/utility/errors/hdbError.js.map +1 -1
- package/dist/validation/configValidator.js +22 -6
- package/dist/validation/configValidator.js.map +1 -1
- package/dist/validation/deleteValidator.js +10 -2
- package/dist/validation/deleteValidator.js.map +1 -1
- package/index.ts +33 -0
- package/npm-shrinkwrap.json +9197 -15066
- package/package.json +3 -1
- package/resources/DESIGN.md +43 -15
- package/resources/Resource.ts +104 -4
- package/resources/Table.ts +298 -74
- package/resources/analytics/read.ts +30 -25
- package/resources/databases.ts +24 -0
- package/resources/defineResource.ts +651 -0
- package/resources/defineTable.ts +407 -0
- package/resources/jsonSchemaTypes.ts +12 -4
- package/resources/openApi.ts +68 -16
- package/resources/search.ts +5 -8
- package/server/graphqlQuerying.ts +4 -2
- package/server/http.ts +40 -16
- package/server/serverHelpers/serverUtilities.ts +19 -6
- package/server/serverHelpers/uwsServer.ts +24 -0
- package/sqlTranslator/index.ts +16 -6
- package/studio/web/assets/{Chat-Cv_2paZE.js → Chat-BZks8dVF.js} +2 -2
- package/studio/web/assets/{Chat-Cv_2paZE.js.map → Chat-BZks8dVF.js.map} +1 -1
- package/studio/web/assets/{FloatingChat-CPyPIcVR.js → FloatingChat-Dic8paVO.js} +4 -4
- package/studio/web/assets/{FloatingChat-CPyPIcVR.js.map → FloatingChat-Dic8paVO.js.map} +1 -1
- package/studio/web/assets/{applications-C0jT2vdZ.js → applications-uOXkeUIN.js} +2 -2
- package/studio/web/assets/{applications-C0jT2vdZ.js.map → applications-uOXkeUIN.js.map} +1 -1
- package/studio/web/assets/{index-D_GKOkhn.js → index-i-2wrKhv.js} +6 -6
- package/studio/web/assets/{index-D_GKOkhn.js.map → index-i-2wrKhv.js.map} +1 -1
- package/studio/web/assets/{index.lazy-CAmCIA65.js → index.lazy-Csk8eCoB.js} +4 -4
- package/studio/web/assets/{index.lazy-CAmCIA65.js.map → index.lazy-Csk8eCoB.js.map} +1 -1
- package/studio/web/assets/{profile-CaF-4aZe.js → profile-Sb3mGDl6.js} +2 -2
- package/studio/web/assets/{profile-CaF-4aZe.js.map → profile-Sb3mGDl6.js.map} +1 -1
- package/studio/web/assets/{setComponentFile-DLW1DHCt.js → setComponentFile-BgZcaPJ2.js} +2 -2
- package/studio/web/assets/{setComponentFile-DLW1DHCt.js.map → setComponentFile-BgZcaPJ2.js.map} +1 -1
- package/studio/web/assets/{setup-TGCErIhq.js → setup-DKtlLgmT.js} +2 -2
- package/studio/web/assets/{setup-TGCErIhq.js.map → setup-DKtlLgmT.js.map} +1 -1
- package/studio/web/assets/{status-DG0Maoao.js → status-B45iLeug.js} +2 -2
- package/studio/web/assets/{status-DG0Maoao.js.map → status-B45iLeug.js.map} +1 -1
- package/studio/web/assets/{swagger-ui-react-8T4SgQ1m.js → swagger-ui-react-Csu4026e.js} +2 -2
- package/studio/web/assets/{swagger-ui-react-8T4SgQ1m.js.map → swagger-ui-react-Csu4026e.js.map} +1 -1
- package/studio/web/assets/{tsMode-DJ6Sl24Q.js → tsMode-DVgxUr_l.js} +2 -2
- package/studio/web/assets/{tsMode-DJ6Sl24Q.js.map → tsMode-DVgxUr_l.js.map} +1 -1
- package/studio/web/assets/{useEntityRestURL-JO2mfWTQ.js → useEntityRestURL-yfDQMV1f.js} +2 -2
- package/studio/web/assets/{useEntityRestURL-JO2mfWTQ.js.map → useEntityRestURL-yfDQMV1f.js.map} +1 -1
- package/studio/web/index.html +1 -1
- package/utility/errors/hdbError.ts +32 -0
- package/validation/configValidator.ts +23 -6
- package/validation/deleteValidator.ts +11 -2
package/resources/Table.ts
CHANGED
|
@@ -28,7 +28,14 @@ import { when, promiseNormalize } from '../utility/when.ts';
|
|
|
28
28
|
import { DatabaseTransaction, ImmediateTransaction, TRANSACTION_STATE } from './DatabaseTransaction.ts';
|
|
29
29
|
import * as envMngr from '../utility/environment/environmentManager.ts';
|
|
30
30
|
import { addSubscription } from './transactionBroadcast.ts';
|
|
31
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
handleHDBError,
|
|
33
|
+
ClientError,
|
|
34
|
+
ServerError,
|
|
35
|
+
AccessViolation,
|
|
36
|
+
ValidationError,
|
|
37
|
+
type ValidationIssue,
|
|
38
|
+
} from '../utility/errors/hdbError.ts';
|
|
32
39
|
import * as signalling from '../utility/signalling.ts';
|
|
33
40
|
import { SchemaEventMsg, UserEventMsg } from '../server/threads/itc.js';
|
|
34
41
|
import { databases, table } from './databases.ts';
|
|
@@ -152,6 +159,43 @@ export function freezeRecord(value: any): void {
|
|
|
152
159
|
if (value !== null && typeof value === 'object' && !ArrayBuffer.isView(value) && !(value instanceof ArrayBuffer))
|
|
153
160
|
Object.freeze(value);
|
|
154
161
|
}
|
|
162
|
+
// Returns a read-only VIEW of `record` for evaluation (e.g. a record-scoped allowRead guard)
|
|
163
|
+
// without mutating the original. Locally-loaded records are already frozen by loadLocalRecord, so
|
|
164
|
+
// this is a no-op there; the source-revalidation path hands back the SAME object its deferred
|
|
165
|
+
// commit still writes to (createdAt/updatedAt) and persists, so that object can't be frozen
|
|
166
|
+
// directly. A write-blocking Proxy is used instead of a shallow copy: a copy would eagerly invoke
|
|
167
|
+
// every getter (breaking a caching table's lazy structPrototype decode) and silently drop
|
|
168
|
+
// non-enumerable properties like Array.length. Only ordinary objects/arrays are wrapped — Date,
|
|
169
|
+
// Map, Set, RegExp, etc. carry internal slots that throw "incompatible receiver" when their
|
|
170
|
+
// methods run against anything but the exact original instance (a Proxy included), so those are
|
|
171
|
+
// returned unwrapped rather than risk breaking an override that calls a method on one; a cached
|
|
172
|
+
// record CAN legitimately be one of these (getFromSource only requires `typeof === 'object'`).
|
|
173
|
+
export function frozenRecordView(record: any): any {
|
|
174
|
+
if (record === null || typeof record !== 'object' || ArrayBuffer.isView(record) || record instanceof ArrayBuffer)
|
|
175
|
+
return record;
|
|
176
|
+
if (Object.isFrozen(record)) return record;
|
|
177
|
+
const tag = Object.prototype.toString.call(record);
|
|
178
|
+
if (tag !== '[object Object]' && tag !== '[object Array]') return record;
|
|
179
|
+
return new Proxy(record, {
|
|
180
|
+
get(target, prop) {
|
|
181
|
+
// receiver = target (not this proxy) so a lazy-decode getter runs with `this` bound to
|
|
182
|
+
// the real record, matching its behavior when read directly.
|
|
183
|
+
return Reflect.get(target, prop, target);
|
|
184
|
+
},
|
|
185
|
+
set() {
|
|
186
|
+
return false;
|
|
187
|
+
},
|
|
188
|
+
defineProperty() {
|
|
189
|
+
return false;
|
|
190
|
+
},
|
|
191
|
+
deleteProperty() {
|
|
192
|
+
return false;
|
|
193
|
+
},
|
|
194
|
+
setPrototypeOf() {
|
|
195
|
+
return false;
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
}
|
|
155
199
|
export const INVALIDATED = 1;
|
|
156
200
|
export const EVICTED = 8; // note that 2 is reserved for timestamps
|
|
157
201
|
const TEST_WRITE_KEY_BUFFER = Buffer.allocUnsafeSlow(8192);
|
|
@@ -163,6 +207,12 @@ const REPLAY_YIELD_INTERVAL = 100; // yield to the event loop every N records du
|
|
|
163
207
|
// buffer the entire backward chain per record, synchronously, on every worker — pinning the JS heap
|
|
164
208
|
// until the worker OOMs (issue #1114). Beyond this depth we fall back to a bounded reconciliation.
|
|
165
209
|
const MAX_OUT_OF_ORDER_AUDIT_DEPTH = 1000;
|
|
210
|
+
// Cap on audit records inspected while backfilling a subscription's `previousCount` history,
|
|
211
|
+
// independent of `count` (the number of AUTHORIZED events collected). `count` only decrements on
|
|
212
|
+
// records that pass the row-level allowRead filter, so an all-deny override would otherwise force
|
|
213
|
+
// a full walk of the retained audit log (#1786) — this bounds that walk regardless of how many
|
|
214
|
+
// records the filter accepts.
|
|
215
|
+
const MAX_PREVIOUS_COUNT_SCAN = 10_000;
|
|
166
216
|
const FULL_PERMISSIONS = {
|
|
167
217
|
read: true,
|
|
168
218
|
insert: true,
|
|
@@ -2640,17 +2690,42 @@ export function makeTable(options) {
|
|
|
2640
2690
|
const txn = txnForContext(context);
|
|
2641
2691
|
if (!target) throw new Error('No query provided');
|
|
2642
2692
|
if (target.parseError) throw target.parseError; // if there was a parse error, we can throw it now
|
|
2693
|
+
// An application-overridden allowRead is a RECORD-scoped check (#1422 gap 2 / #1241): it is
|
|
2694
|
+
// evaluated once per record during query execution with `this` = the record, instead of once
|
|
2695
|
+
// at entry where `this` is a collection resource with nothing loaded. The framework defaults
|
|
2696
|
+
// (marked isDefaultAllowRead) are this-free table/RBAC checks and keep the entry evaluation.
|
|
2697
|
+
// Record-scoping is SYNC-only: async-declared overrides keep entry-check semantics (the
|
|
2698
|
+
// authorize wrapper awaits them and fails closed) rather than entering the sync traversal.
|
|
2699
|
+
const recordScopedAllowRead =
|
|
2700
|
+
target.checkPermission &&
|
|
2701
|
+
!(this.allowRead as any)?.isDefaultAllowRead &&
|
|
2702
|
+
(this.allowRead as any)?.constructor?.name !== 'AsyncFunction'
|
|
2703
|
+
? this.allowRead
|
|
2704
|
+
: undefined;
|
|
2643
2705
|
if (target.checkPermission) {
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
//
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2706
|
+
if (recordScopedAllowRead) {
|
|
2707
|
+
// Compose with role-level column RBAC: the DEFAULT table allowRead is also the
|
|
2708
|
+
// enforcement point for attribute_permissions (it narrows target.select). Run it here
|
|
2709
|
+
// for that side effect — before `select` is captured below — with its verdict
|
|
2710
|
+
// superseded by the per-record override. A per-record super.allowRead call could NOT
|
|
2711
|
+
// restore this: the select-driven output transform is built before iteration starts.
|
|
2712
|
+
try {
|
|
2713
|
+
TableResource.prototype.allowRead.call(this, (context as any).user, target, context);
|
|
2714
|
+
} catch {
|
|
2715
|
+
// narrowing is best-effort under an override; access is governed per record
|
|
2716
|
+
}
|
|
2717
|
+
} else {
|
|
2718
|
+
// requesting authorization verification
|
|
2719
|
+
let allowed;
|
|
2720
|
+
try {
|
|
2721
|
+
allowed = this.allowRead((context as any).user, target, context);
|
|
2722
|
+
} catch {
|
|
2723
|
+
// allow* threw — fail closed rather than letting the request proceed
|
|
2724
|
+
throw new AccessViolation((context as any).user);
|
|
2725
|
+
}
|
|
2726
|
+
if (!allowed) {
|
|
2727
|
+
throw new AccessViolation((context as any).user);
|
|
2728
|
+
}
|
|
2654
2729
|
}
|
|
2655
2730
|
}
|
|
2656
2731
|
if (context) context.lastModified = UNCACHEABLE_TIMESTAMP;
|
|
@@ -2854,16 +2929,46 @@ export function makeTable(options) {
|
|
|
2854
2929
|
// scans), the read transaction reads against the latest committed data without pinning a
|
|
2855
2930
|
// consistent snapshot, so the scan doesn't hold a snapshot that blocks compaction.
|
|
2856
2931
|
const readTxn = txn.useReadTxn(target.snapshot === false);
|
|
2857
|
-
// Record-level
|
|
2858
|
-
//
|
|
2859
|
-
//
|
|
2860
|
-
//
|
|
2861
|
-
//
|
|
2862
|
-
//
|
|
2863
|
-
|
|
2932
|
+
// Record-level allowRead guard (#1241/#1422): an application-overridden allowRead runs once
|
|
2933
|
+
// per record with `this` = the (frozen) record, participating in query execution — pushed
|
|
2934
|
+
// into HNSW traversal for vector sorts, applied as a post-filter otherwise. It must be
|
|
2935
|
+
// synchronous, side-effect free, and fast; a throw denies that record (fail closed, #1422
|
|
2936
|
+
// gap 1 parity). Dispatched via the method resolved from the resource — never via a
|
|
2937
|
+
// `record.allowRead` property lookup, so a record attribute named allowRead can't shadow it.
|
|
2938
|
+
let recordGuard: ((record: any) => boolean) | undefined;
|
|
2939
|
+
if (recordScopedAllowRead) {
|
|
2940
|
+
const user = (context as any)?.user;
|
|
2941
|
+
let warnedAsync = false;
|
|
2942
|
+
recordGuard = (record: any) => {
|
|
2943
|
+
let allowed;
|
|
2944
|
+
try {
|
|
2945
|
+
allowed = recordScopedAllowRead.call(record, user, target, context);
|
|
2946
|
+
} catch {
|
|
2947
|
+
return false; // fail closed on a throwing check
|
|
2948
|
+
}
|
|
2949
|
+
if (typeof allowed?.then === 'function') {
|
|
2950
|
+
// A sync-declared override that returns a thenable can't be awaited mid-traversal:
|
|
2951
|
+
// deny the record (fail closed, #1422 gap 1 semantics) rather than fail open on
|
|
2952
|
+
// promise truthiness or abort the whole query. Declared-async overrides never get
|
|
2953
|
+
// here — they keep the awaited entry check. We're not observing this rejection, so
|
|
2954
|
+
// attach a no-op handler — otherwise a rejected thenable reaches the global
|
|
2955
|
+
// unhandledRejection logger once per denied candidate. Use `.then(undefined, ...)`,
|
|
2956
|
+
// not `.catch` — a thenable is only guaranteed to have `.then`.
|
|
2957
|
+
allowed.then(undefined, () => {});
|
|
2958
|
+
if (!warnedAsync) {
|
|
2959
|
+
warnedAsync = true;
|
|
2960
|
+
logger.warn?.(
|
|
2961
|
+
`allowRead on ${tableName} returned a promise during per-record evaluation; records are denied (record-scoped allowRead must be synchronous)`
|
|
2962
|
+
);
|
|
2963
|
+
}
|
|
2964
|
+
return false;
|
|
2965
|
+
}
|
|
2966
|
+
return Boolean(allowed);
|
|
2967
|
+
};
|
|
2968
|
+
}
|
|
2864
2969
|
const recordAccess =
|
|
2865
|
-
|
|
2866
|
-
? {
|
|
2970
|
+
recordGuard || typeof target.vectorFilter === 'function'
|
|
2971
|
+
? { recordGuard, vectorFilter: target.vectorFilter }
|
|
2867
2972
|
: undefined;
|
|
2868
2973
|
const entries = executeConditions(
|
|
2869
2974
|
conditions,
|
|
@@ -2877,16 +2982,21 @@ export function makeTable(options) {
|
|
|
2877
2982
|
recordAccess
|
|
2878
2983
|
);
|
|
2879
2984
|
const ensure_loaded = (target as any).ensureLoaded !== false;
|
|
2880
|
-
// Authoritative
|
|
2881
|
-
// record, but on a caching table transformEntryForSelect may then revalidate an
|
|
2882
|
-
// row from source and return a DIFFERENT record. An authorization check
|
|
2883
|
-
// actually returned, so
|
|
2884
|
-
// evaluation stays as a prune that also bounds HNSW
|
|
2885
|
-
// intentionally keep the local-record
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2985
|
+
// Authoritative enforcement point (#1241): the guards inside executeConditions evaluate the
|
|
2986
|
+
// LOCAL record, but on a caching table transformEntryForSelect may then revalidate an
|
|
2987
|
+
// expired/invalidated row from source and return a DIFFERENT record. An authorization check
|
|
2988
|
+
// must hold on the record actually returned, so the record-scoped allowRead is re-checked
|
|
2989
|
+
// there, after materialization (the earlier evaluation stays as a prune that also bounds HNSW
|
|
2990
|
+
// traversal). vectorFilter and condition filters intentionally keep the local-record
|
|
2991
|
+
// semantics all query filters have on caching tables.
|
|
2992
|
+
//
|
|
2993
|
+
// A row that is past its TTL but not yet swept by the background eviction
|
|
2994
|
+
// scan is still physically present. A write that is about to overwrite it
|
|
2995
|
+
// anyway (e.g. the SQL engine locating UPDATE/DELETE targets) needs to see
|
|
2996
|
+
// it as a match — the same leniency a direct by-id put/patch already gets,
|
|
2997
|
+
// since those never run the ensureLoaded-gated freshness check this transform
|
|
2998
|
+
// otherwise applies unconditionally to every read.
|
|
2999
|
+
const includeExpired = (target as any).includeExpired === true;
|
|
2890
3000
|
const transformToRecord = TableResource.transformEntryForSelect(
|
|
2891
3001
|
select,
|
|
2892
3002
|
context,
|
|
@@ -2894,7 +3004,8 @@ export function makeTable(options) {
|
|
|
2894
3004
|
filtered,
|
|
2895
3005
|
ensure_loaded,
|
|
2896
3006
|
true,
|
|
2897
|
-
recordGuard
|
|
3007
|
+
recordGuard,
|
|
3008
|
+
includeExpired
|
|
2898
3009
|
);
|
|
2899
3010
|
let results = TableResource.transformToOrderedSelect(
|
|
2900
3011
|
entries,
|
|
@@ -3108,9 +3219,20 @@ export function makeTable(options) {
|
|
|
3108
3219
|
* @param recordGuard record-level read check (#1241) applied to the record actually being
|
|
3109
3220
|
* returned — i.e. AFTER any caching-source revalidation replaces a stale local copy — so an
|
|
3110
3221
|
* authorization verdict can't be made on bytes that differ from what the caller receives.
|
|
3222
|
+
* @param includeExpired when true, a row past its TTL but not yet swept is treated as a live
|
|
3223
|
+
* match rather than gone (used by the SQL engine's UPDATE/DELETE row-finder).
|
|
3111
3224
|
* @returns
|
|
3112
3225
|
*/
|
|
3113
|
-
static transformEntryForSelect(
|
|
3226
|
+
static transformEntryForSelect(
|
|
3227
|
+
select,
|
|
3228
|
+
context,
|
|
3229
|
+
readTxn,
|
|
3230
|
+
filtered,
|
|
3231
|
+
ensure_loaded?,
|
|
3232
|
+
canSkip?,
|
|
3233
|
+
recordGuard?,
|
|
3234
|
+
includeExpired?
|
|
3235
|
+
) {
|
|
3114
3236
|
let checkLoaded;
|
|
3115
3237
|
if (
|
|
3116
3238
|
ensure_loaded &&
|
|
@@ -3158,7 +3280,7 @@ export function makeTable(options) {
|
|
|
3158
3280
|
}
|
|
3159
3281
|
if (
|
|
3160
3282
|
(checkLoaded && entry?.metadataFlags & (INVALIDATED | EVICTED)) || // invalidated or evicted should go to load from source
|
|
3161
|
-
(entry?.expiresAt != undefined && entry?.expiresAt < Date.now())
|
|
3283
|
+
(!includeExpired && entry?.expiresAt != undefined && entry?.expiresAt < Date.now())
|
|
3162
3284
|
) {
|
|
3163
3285
|
// should expiration really apply?
|
|
3164
3286
|
if (context.onlyIfCached) {
|
|
@@ -3192,8 +3314,10 @@ export function makeTable(options) {
|
|
|
3192
3314
|
if (record == null) return canSkip ? SKIP : record;
|
|
3193
3315
|
// Record-level RBAC (#1241): enforced here because `record` is now the final, materialized
|
|
3194
3316
|
// record — a caching table's source revalidation (above) may have replaced the local copy the
|
|
3195
|
-
// query filters evaluated. Fail closed on the record actually being returned.
|
|
3196
|
-
|
|
3317
|
+
// query filters evaluated. Fail closed on the record actually being returned. The guard sees a
|
|
3318
|
+
// frozen view (frozenRecordView) so an allowRead override that writes through `this` can't
|
|
3319
|
+
// mutate a record the source-revalidation path's deferred commit still needs to write and encode.
|
|
3320
|
+
if (recordGuard && !recordGuard(frozenRecordView(record))) return canSkip ? SKIP : undefined;
|
|
3197
3321
|
if (select && !(select[0] === '*' && select.length === 1)) {
|
|
3198
3322
|
let promises: Promise<any>[];
|
|
3199
3323
|
const selectAttribute = (attribute, callback) => {
|
|
@@ -3362,6 +3486,57 @@ export function makeTable(options) {
|
|
|
3362
3486
|
let reloadResnapshotRunning = false;
|
|
3363
3487
|
let reloadResnapshotPending = false;
|
|
3364
3488
|
const thisId = requestTargetToId(request) ?? null; // treat undefined and null as the root
|
|
3489
|
+
// Record-scoped allowRead on subscription delivery (#1419, reviving #1524 on the unified
|
|
3490
|
+
// model): the subscribe entry check already granted the connection (topic ACL / collection
|
|
3491
|
+
// scope); here we ADDITIONALLY re-evaluate a sync application-overridden allowRead per
|
|
3492
|
+
// record-bearing event with `this` = the event's record, the delivery-path analog of the
|
|
3493
|
+
// per-record query guard. Gated to (1) an overridden allowRead (the default is
|
|
3494
|
+
// record-independent and already enforced at connect, so the common case stays zero-overhead)
|
|
3495
|
+
// and (2) an authorization-checked subscription — the entry check clears checkPermission
|
|
3496
|
+
// before we get here and an anonymous-but-checked subscription has no user to key on, so the
|
|
3497
|
+
// wrapper stamps a durable `rowLevelAuthChecked` flag; internal subscribers (replication,
|
|
3498
|
+
// system watchers) never set it and keep full delivery.
|
|
3499
|
+
// Fails closed: a throwing or thenable-returning override drops the event rather than leaks it.
|
|
3500
|
+
//
|
|
3501
|
+
// Known limitation (as in #1524): only put/invalidate events carry the authoritative full
|
|
3502
|
+
// record. delete (tombstone, null value), message payloads, and rawEvents partial values may
|
|
3503
|
+
// mis-decide an override keyed on row fields; closing the primary leak (record updates) is
|
|
3504
|
+
// the goal here — authorizing non-record-bearing event types against the full row is deferred.
|
|
3505
|
+
const subContext = this.getContext() as any;
|
|
3506
|
+
const subAllowRead = this.allowRead;
|
|
3507
|
+
const filterRowReads = (request as any)?.rowLevelAuthChecked && !(subAllowRead as any)?.isDefaultAllowRead;
|
|
3508
|
+
let warnedAsyncEvent = false;
|
|
3509
|
+
const allowsEvent = filterRowReads
|
|
3510
|
+
? (event: any): boolean => {
|
|
3511
|
+
if (event.type === 'end_txn' || event.type === 'reload') return true; // control markers, no record
|
|
3512
|
+
// Freeze the record before the override sees it, mirroring the query path — event.value
|
|
3513
|
+
// is the shared primaryStore object, so an override that writes to `this` would otherwise
|
|
3514
|
+
// mutate the cache for every reader on the node.
|
|
3515
|
+
if (event.value) freezeRecord(event.value);
|
|
3516
|
+
let decision;
|
|
3517
|
+
try {
|
|
3518
|
+
// Evaluate against the LIVE context user, not a subscribe-time snapshot: #1414 re-auth
|
|
3519
|
+
// updates subContext.user in place, so a role/claims change must be reflected per event.
|
|
3520
|
+
decision = subAllowRead.call(event.value ?? null, subContext.user, request, subContext);
|
|
3521
|
+
} catch {
|
|
3522
|
+
return false; // fail closed: a throwing override drops the event
|
|
3523
|
+
}
|
|
3524
|
+
if (decision != null && typeof decision.then === 'function') {
|
|
3525
|
+
// Not observing this rejection — attach a no-op handler so it doesn't reach the
|
|
3526
|
+
// global unhandledRejection logger once per dropped event. Use `.then(undefined,
|
|
3527
|
+
// ...)`, not `.catch` — a thenable is only guaranteed to have `.then`.
|
|
3528
|
+
decision.then(undefined, () => {});
|
|
3529
|
+
if (!warnedAsyncEvent) {
|
|
3530
|
+
warnedAsyncEvent = true;
|
|
3531
|
+
logger.warn?.(
|
|
3532
|
+
`allowRead on ${tableName} returned a promise during subscription event evaluation; events are dropped (record-scoped allowRead must be synchronous)`
|
|
3533
|
+
);
|
|
3534
|
+
}
|
|
3535
|
+
return false;
|
|
3536
|
+
}
|
|
3537
|
+
return Boolean(decision);
|
|
3538
|
+
}
|
|
3539
|
+
: null;
|
|
3365
3540
|
const subscription = addSubscription(
|
|
3366
3541
|
TableResource,
|
|
3367
3542
|
thisId,
|
|
@@ -3403,8 +3578,11 @@ export function makeTable(options) {
|
|
|
3403
3578
|
type,
|
|
3404
3579
|
beginTxn,
|
|
3405
3580
|
};
|
|
3581
|
+
// Queued events are filtered when the queue drains through send() below; events sent
|
|
3582
|
+
// directly (queue already drained) are filtered here. Each event is filtered once.
|
|
3406
3583
|
if (pendingRealTimeQueue) pendingRealTimeQueue.push(event);
|
|
3407
3584
|
else {
|
|
3585
|
+
if (allowsEvent && !allowsEvent(event)) return;
|
|
3408
3586
|
if (databaseName !== 'system') {
|
|
3409
3587
|
recordAction(auditRecord.size ?? 1, 'db-message', tableName, null);
|
|
3410
3588
|
}
|
|
@@ -3479,24 +3657,44 @@ export function makeTable(options) {
|
|
|
3479
3657
|
}
|
|
3480
3658
|
} else if (count) {
|
|
3481
3659
|
const history = [];
|
|
3660
|
+
let inspected = 0;
|
|
3482
3661
|
// we are collecting the history in reverse order to get the right count, then reversing to send
|
|
3483
3662
|
for (const auditRecord of auditStore.getRange({ start: 'z', end: false, reverse: true })) {
|
|
3484
3663
|
if (++recordsSinceYield >= REPLAY_YIELD_INTERVAL) {
|
|
3485
3664
|
recordsSinceYield = 0;
|
|
3486
3665
|
await rest();
|
|
3666
|
+
// Subscription.end() nulls `subscriptions` on close; stop backfilling a dead
|
|
3667
|
+
// subscriber rather than continuing to walk the retained audit log.
|
|
3668
|
+
if (!subscription.subscriptions) break;
|
|
3487
3669
|
}
|
|
3488
3670
|
try {
|
|
3489
3671
|
if (auditRecord.tableId !== tableId) continue;
|
|
3490
3672
|
const id = auditRecord.recordId;
|
|
3491
3673
|
if (thisId == null || isDescendantId(thisId, id)) {
|
|
3674
|
+
// Bound entries INSPECTED for THIS scope, independent of `count` (entries
|
|
3675
|
+
// AUTHORIZED) — an all-deny allowRead override must not force a full walk of
|
|
3676
|
+
// the retained audit log (#1786), even though it returns fewer than `count`
|
|
3677
|
+
// authorized events. Counted only once a record is known to be in scope (right
|
|
3678
|
+
// table, right id) so unrelated cross-table/cross-scope audit traffic in a busy
|
|
3679
|
+
// shared log can't spuriously cut a backfill short.
|
|
3680
|
+
if (++inspected > MAX_PREVIOUS_COUNT_SCAN) {
|
|
3681
|
+
logger.warn?.(
|
|
3682
|
+
`previousCount backfill on ${tableName} stopped after inspecting ${MAX_PREVIOUS_COUNT_SCAN} in-scope audit records without collecting ${request.previousCount} authorized event(s); returning ${history.length} instead`
|
|
3683
|
+
);
|
|
3684
|
+
break;
|
|
3685
|
+
}
|
|
3492
3686
|
const value = auditRecord.getValue(primaryStore, getFullRecord, auditRecord.localTime);
|
|
3493
|
-
|
|
3687
|
+
const historyEntry = {
|
|
3494
3688
|
id,
|
|
3495
3689
|
localTime: auditRecord.localTime,
|
|
3496
3690
|
value,
|
|
3497
3691
|
version: auditRecord.version,
|
|
3498
3692
|
type: auditRecord.type,
|
|
3499
|
-
}
|
|
3693
|
+
};
|
|
3694
|
+
// Filter denied rows BEFORE they consume a previousCount slot, so an authorized
|
|
3695
|
+
// subscriber still receives up to `count` readable events (#1419).
|
|
3696
|
+
if (allowsEvent && !allowsEvent(historyEntry)) continue;
|
|
3697
|
+
history.push(historyEntry);
|
|
3500
3698
|
if (--count <= 0) break;
|
|
3501
3699
|
}
|
|
3502
3700
|
} catch (error) {
|
|
@@ -3638,6 +3836,8 @@ export function makeTable(options) {
|
|
|
3638
3836
|
subscription.send(error);
|
|
3639
3837
|
});
|
|
3640
3838
|
function send(event: any) {
|
|
3839
|
+
// Covers the pendingRealTimeQueue drain and the reload re-snapshot (#495) delivery.
|
|
3840
|
+
if (allowsEvent && !allowsEvent(event)) return;
|
|
3641
3841
|
if (databaseName !== 'system') {
|
|
3642
3842
|
recordAction(event.size ?? 1, 'db-message', tableName, null);
|
|
3643
3843
|
}
|
|
@@ -3852,13 +4052,21 @@ export function makeTable(options) {
|
|
|
3852
4052
|
}
|
|
3853
4053
|
// #section: validation
|
|
3854
4054
|
validate(record: any, patch?: boolean) {
|
|
3855
|
-
|
|
4055
|
+
// Accumulate structured per-field issues so the 400 carries `{ path, code,
|
|
4056
|
+
// message }[]` matching the emitted OpenAPI, instead of a single joined string. The joined
|
|
4057
|
+
// message is still built for the HTTP title, preserving back-compat for callers that read it.
|
|
4058
|
+
let validationErrors: ValidationIssue[] | undefined;
|
|
4059
|
+
const addError = (path: string, code: string, message: string) => {
|
|
4060
|
+
(validationErrors || (validationErrors = [])).push({ path, code, message });
|
|
4061
|
+
};
|
|
3856
4062
|
const validateValue = (value, attribute: Attribute, name) => {
|
|
3857
4063
|
if (attribute.type && value != null) {
|
|
3858
4064
|
if (patch && value.__op__) value = value.value;
|
|
3859
4065
|
if (attribute.properties) {
|
|
3860
4066
|
if (typeof value !== 'object') {
|
|
3861
|
-
(
|
|
4067
|
+
addError(
|
|
4068
|
+
name,
|
|
4069
|
+
'type',
|
|
3862
4070
|
`Value ${stringify(value)} in property ${name} must be an object${
|
|
3863
4071
|
attribute.type ? ' (' + attribute.type + ')' : ''
|
|
3864
4072
|
}`
|
|
@@ -3869,7 +4077,9 @@ export function makeTable(options) {
|
|
|
3869
4077
|
const attribute = properties[i];
|
|
3870
4078
|
if (attribute.relationship || attribute.computed) {
|
|
3871
4079
|
if (record.hasOwnProperty(attribute.name)) {
|
|
3872
|
-
(
|
|
4080
|
+
addError(
|
|
4081
|
+
`${name}.${attribute.name}`,
|
|
4082
|
+
'computed',
|
|
3873
4083
|
`Computed property ${name}.${attribute.name} may not be directly assigned a value`
|
|
3874
4084
|
);
|
|
3875
4085
|
}
|
|
@@ -3881,7 +4091,9 @@ export function makeTable(options) {
|
|
|
3881
4091
|
if (attribute.sealed && value != null && typeof value === 'object') {
|
|
3882
4092
|
for (const key in value) {
|
|
3883
4093
|
if (!properties.find((property) => property.name === key)) {
|
|
3884
|
-
(
|
|
4094
|
+
addError(
|
|
4095
|
+
`${name}.${key}`,
|
|
4096
|
+
'unknown_property',
|
|
3885
4097
|
`Property ${key} is not allowed within object in property ${name}`
|
|
3886
4098
|
);
|
|
3887
4099
|
}
|
|
@@ -3891,13 +4103,17 @@ export function makeTable(options) {
|
|
|
3891
4103
|
switch (attribute.type) {
|
|
3892
4104
|
case 'Int':
|
|
3893
4105
|
if (typeof value !== 'number' || value >> 0 !== value)
|
|
3894
|
-
(
|
|
4106
|
+
addError(
|
|
4107
|
+
name,
|
|
4108
|
+
'type',
|
|
3895
4109
|
`Value ${stringify(value)} in property ${name} must be an integer (from -2147483648 to 2147483647)`
|
|
3896
4110
|
);
|
|
3897
4111
|
break;
|
|
3898
4112
|
case 'Long':
|
|
3899
4113
|
if (typeof value !== 'number' || !(Math.floor(value) === value && Math.abs(value) <= 9007199254740992))
|
|
3900
|
-
(
|
|
4114
|
+
addError(
|
|
4115
|
+
name,
|
|
4116
|
+
'type',
|
|
3901
4117
|
`Value ${stringify(
|
|
3902
4118
|
value
|
|
3903
4119
|
)} in property ${name} must be an integer (from -9007199254740992 to 9007199254740992)`
|
|
@@ -3905,53 +4121,46 @@ export function makeTable(options) {
|
|
|
3905
4121
|
break;
|
|
3906
4122
|
case 'Float':
|
|
3907
4123
|
if (typeof value !== 'number')
|
|
3908
|
-
(
|
|
3909
|
-
`Value ${stringify(value)} in property ${name} must be a number`
|
|
3910
|
-
);
|
|
4124
|
+
addError(name, 'type', `Value ${stringify(value)} in property ${name} must be a number`);
|
|
3911
4125
|
break;
|
|
3912
4126
|
case 'ID':
|
|
3913
4127
|
if (!(
|
|
3914
4128
|
typeof value === 'string' ||
|
|
3915
4129
|
(value?.length > 0 && value.every?.((value) => typeof value === 'string'))
|
|
3916
4130
|
))
|
|
3917
|
-
(
|
|
4131
|
+
addError(
|
|
4132
|
+
name,
|
|
4133
|
+
'type',
|
|
3918
4134
|
`Value ${stringify(value)} in property ${name} must be a string, or an array of strings`
|
|
3919
4135
|
);
|
|
3920
4136
|
break;
|
|
3921
4137
|
case 'String':
|
|
3922
4138
|
if (typeof value !== 'string')
|
|
3923
|
-
(
|
|
3924
|
-
`Value ${stringify(value)} in property ${name} must be a string`
|
|
3925
|
-
);
|
|
4139
|
+
addError(name, 'type', `Value ${stringify(value)} in property ${name} must be a string`);
|
|
3926
4140
|
break;
|
|
3927
4141
|
case 'Boolean':
|
|
3928
4142
|
if (typeof value !== 'boolean')
|
|
3929
|
-
(
|
|
3930
|
-
`Value ${stringify(value)} in property ${name} must be a boolean`
|
|
3931
|
-
);
|
|
4143
|
+
addError(name, 'type', `Value ${stringify(value)} in property ${name} must be a boolean`);
|
|
3932
4144
|
break;
|
|
3933
4145
|
case 'Date':
|
|
3934
4146
|
if (!(value instanceof Date)) {
|
|
3935
4147
|
if (typeof value === 'string' || typeof value === 'number') return new Date(value);
|
|
3936
|
-
else
|
|
3937
|
-
(validationErrors || (validationErrors = [])).push(
|
|
3938
|
-
`Value ${stringify(value)} in property ${name} must be a Date`
|
|
3939
|
-
);
|
|
4148
|
+
else addError(name, 'type', `Value ${stringify(value)} in property ${name} must be a Date`);
|
|
3940
4149
|
}
|
|
3941
4150
|
break;
|
|
3942
4151
|
case 'BigInt':
|
|
3943
4152
|
if (typeof value !== 'bigint') {
|
|
3944
4153
|
// do coercion because otherwise it is rather difficult to get numbers to consistently be bigints
|
|
3945
4154
|
if (typeof value === 'string' || typeof value === 'number') return BigInt(value);
|
|
3946
|
-
(
|
|
3947
|
-
`Value ${stringify(value)} in property ${name} must be a bigint`
|
|
3948
|
-
);
|
|
4155
|
+
addError(name, 'type', `Value ${stringify(value)} in property ${name} must be a bigint`);
|
|
3949
4156
|
}
|
|
3950
4157
|
break;
|
|
3951
4158
|
case 'Bytes':
|
|
3952
4159
|
if (!(value instanceof Uint8Array)) {
|
|
3953
4160
|
if (typeof value === 'string') return Buffer.from(value);
|
|
3954
|
-
(
|
|
4161
|
+
addError(
|
|
4162
|
+
name,
|
|
4163
|
+
'type',
|
|
3955
4164
|
`Value ${stringify(value)} in property ${name} must be a Buffer or Uint8Array`
|
|
3956
4165
|
);
|
|
3957
4166
|
}
|
|
@@ -3962,9 +4171,7 @@ export function makeTable(options) {
|
|
|
3962
4171
|
if (value instanceof Buffer) {
|
|
3963
4172
|
return createBlob(value, { type: 'text/plain' });
|
|
3964
4173
|
}
|
|
3965
|
-
(
|
|
3966
|
-
`Value ${stringify(value)} in property ${name} must be a Blob`
|
|
3967
|
-
);
|
|
4174
|
+
addError(name, 'type', `Value ${stringify(value)} in property ${name} must be a Blob`);
|
|
3968
4175
|
}
|
|
3969
4176
|
break;
|
|
3970
4177
|
case 'array':
|
|
@@ -3976,26 +4183,23 @@ export function makeTable(options) {
|
|
|
3976
4183
|
if (updated) value[i] = updated;
|
|
3977
4184
|
}
|
|
3978
4185
|
}
|
|
3979
|
-
} else
|
|
3980
|
-
(validationErrors || (validationErrors = [])).push(
|
|
3981
|
-
`Value ${stringify(value)} in property ${name} must be an Array`
|
|
3982
|
-
);
|
|
4186
|
+
} else addError(name, 'type', `Value ${stringify(value)} in property ${name} must be an Array`);
|
|
3983
4187
|
|
|
3984
4188
|
break;
|
|
3985
4189
|
}
|
|
3986
4190
|
}
|
|
3987
4191
|
}
|
|
3988
4192
|
if (attribute.nullable === false && value == null) {
|
|
3989
|
-
(
|
|
3990
|
-
`Property ${name} is required (and not does not allow null values)`
|
|
3991
|
-
);
|
|
4193
|
+
addError(name, 'required', `Property ${name} is required (and not does not allow null values)`);
|
|
3992
4194
|
}
|
|
3993
4195
|
};
|
|
3994
4196
|
for (let i = 0, l = attributes.length; i < l; i++) {
|
|
3995
4197
|
const attribute = attributes[i];
|
|
3996
4198
|
if (attribute.relationship || attribute.computed) {
|
|
3997
4199
|
if (Object.hasOwn(record, attribute.name)) {
|
|
3998
|
-
(
|
|
4200
|
+
addError(
|
|
4201
|
+
attribute.name,
|
|
4202
|
+
'computed',
|
|
3999
4203
|
`Computed property ${attribute.name} may not be directly assigned a value`
|
|
4000
4204
|
);
|
|
4001
4205
|
}
|
|
@@ -4009,13 +4213,13 @@ export function makeTable(options) {
|
|
|
4009
4213
|
if (sealed) {
|
|
4010
4214
|
for (const key in record) {
|
|
4011
4215
|
if (!attributes.find((attribute) => attribute.name === key)) {
|
|
4012
|
-
(
|
|
4216
|
+
addError(key, 'unknown_property', `Property ${key} is not allowed`);
|
|
4013
4217
|
}
|
|
4014
4218
|
}
|
|
4015
4219
|
}
|
|
4016
4220
|
|
|
4017
4221
|
if (validationErrors) {
|
|
4018
|
-
throw new
|
|
4222
|
+
throw new ValidationError(validationErrors, validationErrors.map((issue) => issue.message).join('. '));
|
|
4019
4223
|
}
|
|
4020
4224
|
}
|
|
4021
4225
|
// #section: stats-admin
|
|
@@ -4503,6 +4707,26 @@ export function makeTable(options) {
|
|
|
4503
4707
|
}
|
|
4504
4708
|
);
|
|
4505
4709
|
|
|
4710
|
+
// Mark the table-level allowRead as a framework default (it is a this-free table/RBAC check):
|
|
4711
|
+
// only an APPLICATION override is record-scoped and evaluated per record during query execution
|
|
4712
|
+
// (#1422 gap 2 / #1241). supportsRowLevelAllowRead tells the authorization wrapper in
|
|
4713
|
+
// Resource.ts that this class has the per-record machinery to defer collection reads to.
|
|
4714
|
+
(TableResource.prototype.allowRead as any).isDefaultAllowRead = true;
|
|
4715
|
+
(TableResource as any).supportsRowLevelAllowRead = true;
|
|
4716
|
+
// Records can be asked directly (`record.allowRead(user, target, context)`), delegating to the
|
|
4717
|
+
// table's allowRead with `this` = the record. Non-enumerable so it never serializes; defined on
|
|
4718
|
+
// the per-table record prototype alongside computed properties. Engine-level enforcement does
|
|
4719
|
+
// NOT route through this (it resolves the active resource class's allowRead, which honors
|
|
4720
|
+
// endpoint subclass overrides and can't be shadowed by a record attribute of the same name).
|
|
4721
|
+
if (primaryStore?.encoder?.structPrototype) {
|
|
4722
|
+
Object.defineProperty(primaryStore.encoder.structPrototype, 'allowRead', {
|
|
4723
|
+
value: function (user: User, target: RequestTarget, context: Context) {
|
|
4724
|
+
return TableResource.prototype.allowRead.call(this, user, target, context);
|
|
4725
|
+
},
|
|
4726
|
+
configurable: true,
|
|
4727
|
+
writable: true,
|
|
4728
|
+
});
|
|
4729
|
+
}
|
|
4506
4730
|
TableResource.updatedAttributes(); // on creation, update accessors as well
|
|
4507
4731
|
if (expirationMs) TableResource.setTTLExpiration(expirationMs / 1000);
|
|
4508
4732
|
if (expiresAtProperty) runRecordExpirationEviction();
|