@cratis/arc 20.59.1 → 20.60.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/dist/cjs/queries/NullObservableQueryConnection.js +7 -2
- package/dist/cjs/queries/NullObservableQueryConnection.js.map +1 -1
- package/dist/cjs/queries/ObservableQueryFor.js +11 -1
- package/dist/cjs/queries/ObservableQueryFor.js.map +1 -1
- package/dist/cjs/queries/QueryFor.js +1 -21
- package/dist/cjs/queries/QueryFor.js.map +1 -1
- package/dist/cjs/queries/QueryResult.js +30 -0
- package/dist/cjs/queries/QueryResult.js.map +1 -1
- package/dist/esm/queries/NullObservableQueryConnection.d.ts +3 -1
- package/dist/esm/queries/NullObservableQueryConnection.d.ts.map +1 -1
- package/dist/esm/queries/NullObservableQueryConnection.js +7 -2
- package/dist/esm/queries/NullObservableQueryConnection.js.map +1 -1
- package/dist/esm/queries/ObservableQueryFor.d.ts +2 -0
- package/dist/esm/queries/ObservableQueryFor.d.ts.map +1 -1
- package/dist/esm/queries/ObservableQueryFor.js +11 -1
- package/dist/esm/queries/ObservableQueryFor.js.map +1 -1
- package/dist/esm/queries/QueryFor.d.ts.map +1 -1
- package/dist/esm/queries/QueryFor.js +1 -21
- package/dist/esm/queries/QueryFor.js.map +1 -1
- package/dist/esm/queries/QueryResult.d.ts +5 -0
- package/dist/esm/queries/QueryResult.d.ts.map +1 -1
- package/dist/esm/queries/QueryResult.js +30 -0
- package/dist/esm/queries/QueryResult.js.map +1 -1
- package/dist/esm/queries/for_ObservableQueryFor/given/an_observable_query_with_validator.d.ts +23 -0
- package/dist/esm/queries/for_ObservableQueryFor/given/an_observable_query_with_validator.d.ts.map +1 -0
- package/dist/esm/queries/for_ObservableQueryFor/given/an_observable_query_with_validator.js +28 -0
- package/dist/esm/queries/for_ObservableQueryFor/given/an_observable_query_with_validator.js.map +1 -0
- package/dist/esm/queries/for_ObservableQueryFor/when_performing/with_client_validation_failing.d.ts +2 -0
- package/dist/esm/queries/for_ObservableQueryFor/when_performing/with_client_validation_failing.d.ts.map +1 -0
- package/dist/esm/queries/for_ObservableQueryFor/when_performing/with_client_validation_failing.js +21 -0
- package/dist/esm/queries/for_ObservableQueryFor/when_performing/with_client_validation_failing.js.map +1 -0
- package/dist/esm/queries/for_ObservableQueryFor/when_subscribing/with_client_validation_failing.d.ts +2 -0
- package/dist/esm/queries/for_ObservableQueryFor/when_subscribing/with_client_validation_failing.d.ts.map +1 -0
- package/dist/esm/queries/for_ObservableQueryFor/when_subscribing/with_client_validation_failing.js +18 -0
- package/dist/esm/queries/for_ObservableQueryFor/when_subscribing/with_client_validation_failing.js.map +1 -0
- package/dist/esm/queries/for_ObservableQueryFor/when_subscribing/with_client_validation_passing.d.ts +2 -0
- package/dist/esm/queries/for_ObservableQueryFor/when_subscribing/with_client_validation_passing.d.ts.map +1 -0
- package/dist/esm/queries/for_ObservableQueryFor/when_subscribing/with_client_validation_passing.js +21 -0
- package/dist/esm/queries/for_ObservableQueryFor/when_subscribing/with_client_validation_passing.js.map +1 -0
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/queries/NullObservableQueryConnection.ts +5 -2
- package/queries/ObservableQueryFor.ts +15 -1
- package/queries/QueryFor.ts +1 -21
- package/queries/QueryResult.ts +34 -0
- package/queries/for_ObservableQueryFor/given/an_observable_query_with_validator.ts +41 -0
- package/queries/for_ObservableQueryFor/when_performing/with_client_validation_failing.ts +30 -0
- package/queries/for_ObservableQueryFor/when_subscribing/with_client_validation_failing.ts +26 -0
- package/queries/for_ObservableQueryFor/when_subscribing/with_client_validation_passing.ts +32 -0
package/package.json
CHANGED
|
@@ -13,8 +13,11 @@ export class NullObservableQueryConnection<TDataType> implements IObservableQuer
|
|
|
13
13
|
/**
|
|
14
14
|
* Initializes a new instance of the {@link NullObservableQueryConnection} class.
|
|
15
15
|
* @param {TDataType} defaultValue The default value to serve.
|
|
16
|
+
* @param {QueryResult<TDataType>} result Optional result to serve instead of an empty one. Supply it when the
|
|
17
|
+
* subscription failed for a reason the subscriber must be able to see — client-side validation in particular,
|
|
18
|
+
* which would otherwise be indistinguishable from a query that simply has no data yet.
|
|
16
19
|
*/
|
|
17
|
-
constructor(readonly defaultValue: TDataType) {
|
|
20
|
+
constructor(readonly defaultValue: TDataType, readonly result?: QueryResult<TDataType>) {
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
/** @inheritdoc */
|
|
@@ -29,7 +32,7 @@ export class NullObservableQueryConnection<TDataType> implements IObservableQuer
|
|
|
29
32
|
|
|
30
33
|
/** @inheritdoc */
|
|
31
34
|
connect(dataReceived: DataReceived<TDataType>) {
|
|
32
|
-
dataReceived(QueryResult.empty(this.defaultValue));
|
|
35
|
+
dataReceived(this.result ?? QueryResult.empty(this.defaultValue));
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
/** @inheritdoc */
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { IObservableQueryFor, OnNextResult } from './IObservableQueryFor';
|
|
5
5
|
import { ObservableQuerySubscription } from './ObservableQuerySubscription';
|
|
6
6
|
import { ValidateRequestArguments } from './ValidateRequestArguments';
|
|
7
|
+
import { QueryValidator } from './QueryValidator';
|
|
7
8
|
import { IObservableQueryConnection } from './IObservableQueryConnection';
|
|
8
9
|
import { NullObservableQueryConnection } from './NullObservableQueryConnection';
|
|
9
10
|
import { createObservableQueryConnection } from './ObservableQueryConnectionFactory';
|
|
@@ -38,6 +39,7 @@ export abstract class ObservableQueryFor<TDataType, TParameters = object> implem
|
|
|
38
39
|
abstract readonly route: string;
|
|
39
40
|
abstract readonly defaultValue: TDataType;
|
|
40
41
|
readonly roles: string[] = [];
|
|
42
|
+
readonly validation?: QueryValidator<any>;
|
|
41
43
|
/** Backend fully-qualified query name used when subscribing via the SSE hub. Overridden in generated proxies. */
|
|
42
44
|
readonly queryName?: string;
|
|
43
45
|
abstract readonly parameterDescriptors: ParameterDescriptor[];
|
|
@@ -97,7 +99,14 @@ export abstract class ObservableQueryFor<TDataType, TParameters = object> implem
|
|
|
97
99
|
this._connection.disconnect();
|
|
98
100
|
}
|
|
99
101
|
|
|
100
|
-
|
|
102
|
+
const clientValidationErrors = this.validation?.validate(args as object || {}) || [];
|
|
103
|
+
if (clientValidationErrors.length > 0) {
|
|
104
|
+
// Serve the failure through the connection rather than throwing, so a subscriber sees an invalid result
|
|
105
|
+
// on its normal callback path instead of the empty result an unestablished connection would emit.
|
|
106
|
+
this._connection = new NullObservableQueryConnection(
|
|
107
|
+
this.defaultValue,
|
|
108
|
+
QueryResult.validationFailed(clientValidationErrors, this));
|
|
109
|
+
} else if (!this.validateArguments(args)) {
|
|
101
110
|
this._connection = new NullObservableQueryConnection(this.defaultValue);
|
|
102
111
|
} else {
|
|
103
112
|
this._connection = createObservableQueryConnection({
|
|
@@ -143,6 +152,11 @@ export abstract class ObservableQueryFor<TDataType, TParameters = object> implem
|
|
|
143
152
|
async perform(args?: TParameters): Promise<QueryResult<TDataType>> {
|
|
144
153
|
const noSuccess = { ...QueryResult.noSuccess, ...{ data: this.defaultValue } } as QueryResult<TDataType>;
|
|
145
154
|
|
|
155
|
+
const clientValidationErrors = this.validation?.validate(args as object || {}) || [];
|
|
156
|
+
if (clientValidationErrors.length > 0) {
|
|
157
|
+
return QueryResult.validationFailed(clientValidationErrors, this);
|
|
158
|
+
}
|
|
159
|
+
|
|
146
160
|
if (!this.validateArguments(args)) {
|
|
147
161
|
return new Promise<QueryResult<TDataType>>((resolve) => {
|
|
148
162
|
resolve(noSuccess);
|
package/queries/QueryFor.ts
CHANGED
|
@@ -87,27 +87,7 @@ export abstract class QueryFor<TDataType, TParameters = object> implements IQuer
|
|
|
87
87
|
|
|
88
88
|
const clientValidationErrors = this.validation?.validate(args as object || {}) || [];
|
|
89
89
|
if (clientValidationErrors.length > 0) {
|
|
90
|
-
return
|
|
91
|
-
data: this.defaultValue as object,
|
|
92
|
-
isSuccess: false,
|
|
93
|
-
isAuthorized: true,
|
|
94
|
-
isValid: false,
|
|
95
|
-
hasExceptions: false,
|
|
96
|
-
validationResults: clientValidationErrors.map(_ => ({
|
|
97
|
-
severity: _.severity,
|
|
98
|
-
message: _.message,
|
|
99
|
-
members: _.members,
|
|
100
|
-
state: _.state
|
|
101
|
-
})),
|
|
102
|
-
exceptionMessages: [],
|
|
103
|
-
exceptionStackTrace: '',
|
|
104
|
-
paging: {
|
|
105
|
-
totalItems: 0,
|
|
106
|
-
totalPages: 0,
|
|
107
|
-
page: 0,
|
|
108
|
-
size: 0
|
|
109
|
-
}
|
|
110
|
-
}, this.modelType, this.enumerable) as QueryResult<TDataType>;
|
|
90
|
+
return QueryResult.validationFailed(clientValidationErrors, this);
|
|
111
91
|
}
|
|
112
92
|
|
|
113
93
|
if (!ValidateRequestArguments(this.constructor.name, this.requiredRequestParameters, args as object)) {
|
package/queries/QueryResult.ts
CHANGED
|
@@ -71,6 +71,40 @@ export class QueryResult<TDataType = object> implements IQueryResult<TDataType>
|
|
|
71
71
|
}, Object, false);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Creates a {@link QueryResult} representing a query rejected by client-side validation, mirroring
|
|
76
|
+
* {@link CommandResult.validationFailed} so a caller reads a failed query the same way it reads a failed command.
|
|
77
|
+
* @param {ValidationResult[]} validationResults The validation results that caused the failure.
|
|
78
|
+
* @param {object} query The query being rejected, which describes how to shape its own result.
|
|
79
|
+
* @returns {QueryResult<TDataType>} A result that is neither successful nor valid.
|
|
80
|
+
*/
|
|
81
|
+
static validationFailed<TDataType>(
|
|
82
|
+
validationResults: ValidationResult[],
|
|
83
|
+
query: { readonly defaultValue: TDataType; readonly modelType: Constructor; readonly enumerable: boolean }): QueryResult<TDataType> {
|
|
84
|
+
const { defaultValue, modelType, enumerable } = query;
|
|
85
|
+
return new QueryResult({
|
|
86
|
+
data: defaultValue as object,
|
|
87
|
+
isSuccess: false,
|
|
88
|
+
isAuthorized: true,
|
|
89
|
+
isValid: false,
|
|
90
|
+
hasExceptions: false,
|
|
91
|
+
validationResults: validationResults.map(_ => ({
|
|
92
|
+
severity: _.severity,
|
|
93
|
+
message: _.message,
|
|
94
|
+
members: _.members,
|
|
95
|
+
state: _.state
|
|
96
|
+
})),
|
|
97
|
+
exceptionMessages: [],
|
|
98
|
+
exceptionStackTrace: '',
|
|
99
|
+
paging: {
|
|
100
|
+
totalItems: 0,
|
|
101
|
+
totalPages: 0,
|
|
102
|
+
page: 0,
|
|
103
|
+
size: 0
|
|
104
|
+
}
|
|
105
|
+
}, modelType, enumerable) as QueryResult<TDataType>;
|
|
106
|
+
}
|
|
107
|
+
|
|
74
108
|
static unauthorized<TDataType>(): QueryResult<TDataType> {
|
|
75
109
|
return new QueryResult({
|
|
76
110
|
data: null as unknown as object,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { ObservableQueryFor } from '../../ObservableQueryFor';
|
|
5
|
+
import { QueryValidator } from '../../QueryValidator';
|
|
6
|
+
import { ParameterDescriptor } from '../../../reflection/ParameterDescriptor';
|
|
7
|
+
import '../../../validation/RuleBuilderExtensions';
|
|
8
|
+
|
|
9
|
+
export interface ITestParams {
|
|
10
|
+
minAge: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class TestObservableQueryValidator extends QueryValidator<ITestParams> {
|
|
14
|
+
constructor() {
|
|
15
|
+
super();
|
|
16
|
+
this.ruleFor((c: ITestParams) => c.minAge).greaterThanOrEqual(0).withMessage('Age must be positive');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class TestObservableQuery extends ObservableQueryFor<string, ITestParams> {
|
|
21
|
+
readonly route = '/test';
|
|
22
|
+
readonly validation = new TestObservableQueryValidator();
|
|
23
|
+
readonly parameterDescriptors: ParameterDescriptor[] = [];
|
|
24
|
+
readonly defaultValue = '';
|
|
25
|
+
|
|
26
|
+
constructor() {
|
|
27
|
+
super(String, false);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get requiredRequestParameters(): string[] {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class an_observable_query_with_validator {
|
|
36
|
+
query: TestObservableQuery;
|
|
37
|
+
|
|
38
|
+
constructor() {
|
|
39
|
+
this.query = new TestObservableQuery();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { an_observable_query_with_validator } from '../given/an_observable_query_with_validator';
|
|
5
|
+
import { QueryResult } from '../../QueryResult';
|
|
6
|
+
import { given } from '../../../given';
|
|
7
|
+
|
|
8
|
+
describe('when performing with client validation failing', given(an_observable_query_with_validator, context => {
|
|
9
|
+
let result: QueryResult<string>;
|
|
10
|
+
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
result = await context.query.perform({ minAge: -5 });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should not be success', () => {
|
|
16
|
+
result.isSuccess.should.be.false;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should not be valid', () => {
|
|
20
|
+
result.isValid.should.be.false;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should have validation results', () => {
|
|
24
|
+
result.validationResults.should.not.be.empty;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should have error for minAge', () => {
|
|
28
|
+
result.validationResults.some(_ => _.members.includes('minAge')).should.be.true;
|
|
29
|
+
});
|
|
30
|
+
}));
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { an_observable_query_with_validator } from '../given/an_observable_query_with_validator';
|
|
5
|
+
import { QueryResult } from '../../QueryResult';
|
|
6
|
+
import { given } from '../../../given';
|
|
7
|
+
|
|
8
|
+
describe('when subscribing with client validation failing', given(an_observable_query_with_validator, context => {
|
|
9
|
+
let result: QueryResult<string>;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
context.query.subscribe(_ => result = _, { minAge: -5 });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should push a result to the subscriber', () => {
|
|
16
|
+
result.should.not.be.undefined;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should not be valid', () => {
|
|
20
|
+
result.isValid.should.be.false;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should have error for minAge', () => {
|
|
24
|
+
result.validationResults.some(_ => _.members.includes('minAge')).should.be.true;
|
|
25
|
+
});
|
|
26
|
+
}));
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { an_observable_query_with_validator } from '../given/an_observable_query_with_validator';
|
|
5
|
+
import { given } from '../../../given';
|
|
6
|
+
|
|
7
|
+
import * as sinon from 'sinon';
|
|
8
|
+
import { ObservableQuerySubscription } from '../../ObservableQuerySubscription';
|
|
9
|
+
|
|
10
|
+
describe('when subscribing with client validation passing', given(an_observable_query_with_validator, context => {
|
|
11
|
+
let callback: sinon.SinonStub;
|
|
12
|
+
let subscription: ObservableQuerySubscription<string>;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
context.query.setOrigin('https://example.com'); // Set origin to avoid document access
|
|
16
|
+
callback = sinon.stub();
|
|
17
|
+
|
|
18
|
+
subscription = context.query.subscribe(callback, { minAge: 18 });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
if (subscription) {
|
|
23
|
+
subscription.unsubscribe();
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// A real connection is established rather than one that immediately serves a result, which is what
|
|
28
|
+
// distinguishes a subscription that passed validation from one that was rejected by it.
|
|
29
|
+
it('should not call callback immediately', () => {
|
|
30
|
+
callback.called.should.be.false;
|
|
31
|
+
});
|
|
32
|
+
}));
|