@cratis/arc 20.60.0 → 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.
Files changed (49) hide show
  1. package/dist/cjs/queries/NullObservableQueryConnection.js +7 -2
  2. package/dist/cjs/queries/NullObservableQueryConnection.js.map +1 -1
  3. package/dist/cjs/queries/ObservableQueryFor.js +11 -1
  4. package/dist/cjs/queries/ObservableQueryFor.js.map +1 -1
  5. package/dist/cjs/queries/QueryFor.js +1 -21
  6. package/dist/cjs/queries/QueryFor.js.map +1 -1
  7. package/dist/cjs/queries/QueryResult.js +30 -0
  8. package/dist/cjs/queries/QueryResult.js.map +1 -1
  9. package/dist/esm/queries/NullObservableQueryConnection.d.ts +3 -1
  10. package/dist/esm/queries/NullObservableQueryConnection.d.ts.map +1 -1
  11. package/dist/esm/queries/NullObservableQueryConnection.js +7 -2
  12. package/dist/esm/queries/NullObservableQueryConnection.js.map +1 -1
  13. package/dist/esm/queries/ObservableQueryFor.d.ts +2 -0
  14. package/dist/esm/queries/ObservableQueryFor.d.ts.map +1 -1
  15. package/dist/esm/queries/ObservableQueryFor.js +11 -1
  16. package/dist/esm/queries/ObservableQueryFor.js.map +1 -1
  17. package/dist/esm/queries/QueryFor.d.ts.map +1 -1
  18. package/dist/esm/queries/QueryFor.js +1 -21
  19. package/dist/esm/queries/QueryFor.js.map +1 -1
  20. package/dist/esm/queries/QueryResult.d.ts +5 -0
  21. package/dist/esm/queries/QueryResult.d.ts.map +1 -1
  22. package/dist/esm/queries/QueryResult.js +30 -0
  23. package/dist/esm/queries/QueryResult.js.map +1 -1
  24. package/dist/esm/queries/for_ObservableQueryFor/given/an_observable_query_with_validator.d.ts +23 -0
  25. package/dist/esm/queries/for_ObservableQueryFor/given/an_observable_query_with_validator.d.ts.map +1 -0
  26. package/dist/esm/queries/for_ObservableQueryFor/given/an_observable_query_with_validator.js +28 -0
  27. package/dist/esm/queries/for_ObservableQueryFor/given/an_observable_query_with_validator.js.map +1 -0
  28. package/dist/esm/queries/for_ObservableQueryFor/when_performing/with_client_validation_failing.d.ts +2 -0
  29. package/dist/esm/queries/for_ObservableQueryFor/when_performing/with_client_validation_failing.d.ts.map +1 -0
  30. package/dist/esm/queries/for_ObservableQueryFor/when_performing/with_client_validation_failing.js +21 -0
  31. package/dist/esm/queries/for_ObservableQueryFor/when_performing/with_client_validation_failing.js.map +1 -0
  32. package/dist/esm/queries/for_ObservableQueryFor/when_subscribing/with_client_validation_failing.d.ts +2 -0
  33. package/dist/esm/queries/for_ObservableQueryFor/when_subscribing/with_client_validation_failing.d.ts.map +1 -0
  34. package/dist/esm/queries/for_ObservableQueryFor/when_subscribing/with_client_validation_failing.js +18 -0
  35. package/dist/esm/queries/for_ObservableQueryFor/when_subscribing/with_client_validation_failing.js.map +1 -0
  36. package/dist/esm/queries/for_ObservableQueryFor/when_subscribing/with_client_validation_passing.d.ts +2 -0
  37. package/dist/esm/queries/for_ObservableQueryFor/when_subscribing/with_client_validation_passing.d.ts.map +1 -0
  38. package/dist/esm/queries/for_ObservableQueryFor/when_subscribing/with_client_validation_passing.js +21 -0
  39. package/dist/esm/queries/for_ObservableQueryFor/when_subscribing/with_client_validation_passing.js.map +1 -0
  40. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  41. package/package.json +1 -1
  42. package/queries/NullObservableQueryConnection.ts +5 -2
  43. package/queries/ObservableQueryFor.ts +15 -1
  44. package/queries/QueryFor.ts +1 -21
  45. package/queries/QueryResult.ts +34 -0
  46. package/queries/for_ObservableQueryFor/given/an_observable_query_with_validator.ts +41 -0
  47. package/queries/for_ObservableQueryFor/when_performing/with_client_validation_failing.ts +30 -0
  48. package/queries/for_ObservableQueryFor/when_subscribing/with_client_validation_failing.ts +26 -0
  49. package/queries/for_ObservableQueryFor/when_subscribing/with_client_validation_passing.ts +32 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cratis/arc",
3
- "version": "20.60.0",
3
+ "version": "20.60.1",
4
4
  "description": "",
5
5
  "author": "Cratis",
6
6
  "license": "MIT",
@@ -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
- if (!this.validateArguments(args)) {
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);
@@ -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 new QueryResult({
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)) {
@@ -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
+ }));