@spine-event-engine/server 2.0.0-snapshot.16 → 2.0.0-snapshot.20
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/README.md +17 -3
- package/REFERENCE.md +66 -12
- package/dist/context/bounded-context.d.ts +72 -14
- package/dist/context/bounded-context.d.ts.map +1 -1
- package/dist/context/bounded-context.js +130 -49
- package/dist/context/bounded-context.js.map +1 -1
- package/dist/context/effective-tenant.d.ts +9 -0
- package/dist/context/effective-tenant.d.ts.map +1 -0
- package/dist/context/effective-tenant.js +58 -0
- package/dist/context/effective-tenant.js.map +1 -0
- package/dist/context/tenant-index.d.ts.map +1 -1
- package/dist/context/tenant-index.js +70 -1
- package/dist/context/tenant-index.js.map +1 -1
- package/dist/delivery/delivery.d.ts.map +1 -1
- package/dist/delivery/delivery.js +270 -26
- package/dist/delivery/delivery.js.map +1 -1
- package/dist/delivery/inbox-storage.d.ts +7 -0
- package/dist/delivery/inbox-storage.d.ts.map +1 -1
- package/dist/delivery/inbox-storage.js +110 -0
- package/dist/delivery/inbox-storage.js.map +1 -1
- package/dist/entity/entity-metadata.d.ts +2 -2
- package/dist/entity/entity-metadata.js +2 -2
- package/dist/entity/entity-metadata.js.map +1 -1
- package/dist/entity/entity-storage-descriptor.d.ts +24 -4
- package/dist/entity/entity-storage-descriptor.d.ts.map +1 -1
- package/dist/entity/entity-storage-descriptor.js +69 -23
- package/dist/entity/entity-storage-descriptor.js.map +1 -1
- package/dist/entity/entity.d.ts +19 -18
- package/dist/entity/entity.d.ts.map +1 -1
- package/dist/entity/entity.js +61 -24
- package/dist/entity/entity.js.map +1 -1
- package/dist/entity/spec-scanner.d.ts +13 -1
- package/dist/entity/spec-scanner.d.ts.map +1 -1
- package/dist/entity/spec-scanner.js +20 -3
- package/dist/entity/spec-scanner.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/repository/command-routing.d.ts +63 -10
- package/dist/repository/command-routing.d.ts.map +1 -1
- package/dist/repository/command-routing.js +41 -8
- package/dist/repository/command-routing.js.map +1 -1
- package/dist/repository/event-routing.d.ts +64 -9
- package/dist/repository/event-routing.d.ts.map +1 -1
- package/dist/repository/event-routing.js +42 -7
- package/dist/repository/event-routing.js.map +1 -1
- package/dist/repository/repository.d.ts +96 -10
- package/dist/repository/repository.d.ts.map +1 -1
- package/dist/repository/repository.js +437 -133
- package/dist/repository/repository.js.map +1 -1
- package/dist/repository/state-update-routing.d.ts +67 -8
- package/dist/repository/state-update-routing.d.ts.map +1 -1
- package/dist/repository/state-update-routing.js +45 -6
- package/dist/repository/state-update-routing.js.map +1 -1
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +407 -109
- package/dist/server/server.js.map +1 -1
- package/dist/services/registered-targets.d.ts +73 -0
- package/dist/services/registered-targets.d.ts.map +1 -0
- package/dist/services/registered-targets.js +127 -0
- package/dist/services/registered-targets.js.map +1 -0
- package/dist/services/spine-services.d.ts +26 -12
- package/dist/services/spine-services.d.ts.map +1 -1
- package/dist/services/spine-services.js +1044 -962
- package/dist/services/spine-services.js.map +1 -1
- package/dist/stand/stand.d.ts +73 -1
- package/dist/stand/stand.d.ts.map +1 -1
- package/dist/stand/stand.js +220 -48
- package/dist/stand/stand.js.map +1 -1
- package/dist/stand/subscription-observer.d.ts.map +1 -1
- package/dist/stand/subscription-observer.js +4 -11
- package/dist/stand/subscription-observer.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +12 -12
|
@@ -26,13 +26,15 @@ import { EntityStateUpdateSchema, EntityUpdatesSchema, EventUpdatesSchema, Subsc
|
|
|
26
26
|
import { SubscriptionService } from "@spine-event-engine/proto/client";
|
|
27
27
|
import { AckSchema } from "@spine-event-engine/proto";
|
|
28
28
|
import { ResponseSchema, StatusSchema, } from "@spine-event-engine/proto";
|
|
29
|
-
import {
|
|
29
|
+
import {} from "@spine-event-engine/storage";
|
|
30
30
|
import { TenantBoundary } from "@spine-event-engine/storage/provider";
|
|
31
31
|
import { boundedContextAccess } from "../context/bounded-context.js";
|
|
32
|
+
import { EffectiveTenants } from "../context/effective-tenant.js";
|
|
32
33
|
import { CommandValidationError } from "../bus/command-errors.js";
|
|
33
34
|
import { TransitionValidationError } from "../repository/command-errors.js";
|
|
34
35
|
import {} from "../stand/stand.js";
|
|
35
36
|
import { QueryReader } from "./query-reader.js";
|
|
37
|
+
import { RegisteredTargets } from "./registered-targets.js";
|
|
36
38
|
import { emitServerWarning } from "../server/server-log.js";
|
|
37
39
|
import { managedChildSubscriptionAccess } from "../server/managed-child-subscription.js";
|
|
38
40
|
import { InMemorySubscriptionRegistry, } from "../stand/subscription-registry.js";
|
|
@@ -42,16 +44,19 @@ const serviceInstances = new WeakSet();
|
|
|
42
44
|
* Small route registrar for the first public Spine gRPC service slice.
|
|
43
45
|
*
|
|
44
46
|
* `QueryService.Read` supports ID filters for any registered state route,
|
|
45
|
-
* projection-state `include_all = true` reads,
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
47
|
+
* projection-state `include_all = true` reads, and column filters with
|
|
48
|
+
* `EQUAL`, `GREATER_THAN`, `LESS_THAN`, `GREATER_OR_EQUAL`, or `LESS_OR_EQUAL`
|
|
49
|
+
* comparisons inside nested `ALL`/`EITHER` composites. Filter paths name one
|
|
50
|
+
* declared `(column)` proto field or supported system column, and values must
|
|
51
|
+
* match its type. Range comparisons and ordering require a string or numeric
|
|
52
|
+
* scalar column, `Timestamp`, or `version`; ordering accepts only `ASCENDING`
|
|
53
|
+
* or `DESCENDING`. An absent response format or zero
|
|
54
|
+
* wire limit receives an implicit 1,000-row cap without requiring ordering; a
|
|
55
|
+
* positive limit of at most 1,000 requires at least one ordering directive.
|
|
56
|
+
* Queries allow at most 100 IDs, 16 simple filters, 8 composites, and 8 orderings.
|
|
57
|
+
* Use proto column names such as `open_task_count`, not generated TS local names
|
|
58
|
+
* such as `openTaskCount`. Invalid columns, values, operators, and query shapes
|
|
59
|
+
* return stable `INVALID_QUERY` responses before Stand storage is read.
|
|
55
60
|
*
|
|
56
61
|
* `SubscriptionService.Subscribe` accepts known state targets with
|
|
57
62
|
* `include_all` or validated ID/field filters and known event targets exposed
|
|
@@ -61,8 +66,8 @@ const serviceInstances = new WeakSet();
|
|
|
61
66
|
* subscriptions attach to `Stand`, while
|
|
62
67
|
* event subscriptions attach to a framework-internal `EventBus` listener.
|
|
63
68
|
* Filtered state topics deliver matching states, emit `no_longer_matching`
|
|
64
|
-
* when previous state matched and new state does not, and
|
|
65
|
-
*
|
|
69
|
+
* when previous state matched and new state does not, and deliver complete states.
|
|
70
|
+
* Event topics stream wire-level `event_updates`
|
|
66
71
|
* containing cloned framework `Event` envelopes; rejection updates omit
|
|
67
72
|
* rejected-command payload forms and stack trace from their client-facing
|
|
68
73
|
* context.
|
|
@@ -93,31 +98,19 @@ export class SpineServices {
|
|
|
93
98
|
this.#contexts = Object.freeze([...options.contexts]);
|
|
94
99
|
this.#queueLimit = ServiceValues.positiveInteger(options.queueLimit ?? ServiceValues.defaultQueueLimit);
|
|
95
100
|
this.#subscriptionLimit = ServiceValues.subscriptionLimit(options.subscriptionLimit ?? ServiceValues.defaultSubscriptionLimit);
|
|
101
|
+
this.#registerSignalRoutes();
|
|
102
|
+
this.#registerStateRoutes(new RegisteredTargets(this.#contexts));
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Registers Command and Event routes exposed by the assembled contexts.
|
|
106
|
+
*/
|
|
107
|
+
#registerSignalRoutes() {
|
|
96
108
|
for (const context of this.#contexts) {
|
|
97
109
|
for (const typeUrl of context.commandBus().acceptedCommandTypes()) {
|
|
98
110
|
if (!this.#commandRoutes.has(typeUrl)) {
|
|
99
111
|
this.#commandRoutes.set(typeUrl, { context, typeUrl });
|
|
100
112
|
}
|
|
101
113
|
}
|
|
102
|
-
for (const repository of context.registeredRepositories()) {
|
|
103
|
-
const schema = repository.stateSchema;
|
|
104
|
-
const typeUrl = TypeUrls.derive(schema);
|
|
105
|
-
const declaredColumns = repository.metadata.columns.map((column) => column.name);
|
|
106
|
-
const systemColumns = ["version", "archived", "deleted"];
|
|
107
|
-
this.#stateRoutes.set(typeUrl, {
|
|
108
|
-
allowedColumnNames: new Set([...declaredColumns, ...systemColumns]),
|
|
109
|
-
columnFields: new Map(repository.metadata.columns.flatMap((column) => {
|
|
110
|
-
const field = schema.fields.find((candidate) => candidate.name === column.name);
|
|
111
|
-
return field === undefined ? [] : [[column.name, field]];
|
|
112
|
-
})),
|
|
113
|
-
context,
|
|
114
|
-
entityFamily: repository.entityFamily,
|
|
115
|
-
idField: ServiceValues.stateRouteIdField(schema, repository.idField),
|
|
116
|
-
kind: "state",
|
|
117
|
-
schema,
|
|
118
|
-
typeUrl,
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
114
|
for (const typeUrl of context.eventBus().acceptedEventTypes()) {
|
|
122
115
|
if (!this.#eventRoutes.has(typeUrl)) {
|
|
123
116
|
this.#eventRoutes.set(typeUrl, { context, kind: "event", typeUrl });
|
|
@@ -125,6 +118,31 @@ export class SpineServices {
|
|
|
125
118
|
}
|
|
126
119
|
}
|
|
127
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* Registers state routes from the validated server-wide Entity lookup.
|
|
123
|
+
*
|
|
124
|
+
* @param targets Validated state registrations.
|
|
125
|
+
*/
|
|
126
|
+
#registerStateRoutes(targets) {
|
|
127
|
+
for (const { context, repository, typeUrl } of targets.all()) {
|
|
128
|
+
const schema = repository.stateSchema;
|
|
129
|
+
const declaredColumns = repository.metadata.columns.map((column) => column.name);
|
|
130
|
+
const systemColumns = ["version", "archived", "deleted"];
|
|
131
|
+
this.#stateRoutes.set(typeUrl, {
|
|
132
|
+
allowedColumnNames: new Set([...declaredColumns, ...systemColumns]),
|
|
133
|
+
columnFields: new Map(repository.metadata.columns.flatMap((column) => {
|
|
134
|
+
const field = schema.fields.find((candidate) => candidate.name === column.name);
|
|
135
|
+
return field === undefined ? [] : [[column.name, field]];
|
|
136
|
+
})),
|
|
137
|
+
context,
|
|
138
|
+
entityFamily: repository.entityFamily,
|
|
139
|
+
idField: ServiceValues.stateRouteIdField(schema, repository.idField),
|
|
140
|
+
kind: "state",
|
|
141
|
+
schema,
|
|
142
|
+
typeUrl,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
128
146
|
/**
|
|
129
147
|
* Registers CommandService, QueryService, and SubscriptionService routes.
|
|
130
148
|
*
|
|
@@ -145,6 +163,12 @@ export class SpineServices {
|
|
|
145
163
|
});
|
|
146
164
|
return router;
|
|
147
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Resolves a public Command route before attempting dispatch.
|
|
168
|
+
*
|
|
169
|
+
* @param command Public Command request.
|
|
170
|
+
* @returns Acknowledgement for route validation or dispatch.
|
|
171
|
+
*/
|
|
148
172
|
async #post(command) {
|
|
149
173
|
const messageId = command.id && AnyMessages.pack(CommandIdSchema, command.id, { validate: false });
|
|
150
174
|
const typeUrl = command.message?.typeUrl;
|
|
@@ -183,6 +207,12 @@ export class SpineServices {
|
|
|
183
207
|
});
|
|
184
208
|
}
|
|
185
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* Validates a public state query before reading its registered Stand.
|
|
212
|
+
*
|
|
213
|
+
* @param query Public query request.
|
|
214
|
+
* @returns States or a stable query error response.
|
|
215
|
+
*/
|
|
186
216
|
async #read(query) {
|
|
187
217
|
const target = query.target;
|
|
188
218
|
const route = this.#readRoute(target);
|
|
@@ -193,13 +223,26 @@ export class SpineServices {
|
|
|
193
223
|
if (queryError !== undefined) {
|
|
194
224
|
return ServiceValues.queryErrorResponse(queryError.type, queryError.message);
|
|
195
225
|
}
|
|
226
|
+
return this.#readValidated(query, target, route);
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Resolves the effective tenant and executes an already validated read.
|
|
230
|
+
*
|
|
231
|
+
* @param query Validated public query.
|
|
232
|
+
* @param target Registered query target.
|
|
233
|
+
* @param route Route containing the target Stand and schema.
|
|
234
|
+
* @returns States or a stable tenant/read error response.
|
|
235
|
+
*/
|
|
236
|
+
async #readValidated(query, target, route) {
|
|
196
237
|
const tenantId = ServiceValues.tenantValue(query.context?.tenantId);
|
|
197
238
|
const tenantError = ServiceValues.tenantMismatch(route.context.isMultitenant, tenantId, "query");
|
|
198
239
|
if (tenantError !== undefined) {
|
|
199
240
|
return ServiceValues.queryErrorResponse(tenantError.type, tenantError.message);
|
|
200
241
|
}
|
|
201
242
|
try {
|
|
202
|
-
|
|
243
|
+
const effective = EffectiveTenants.current(route.context.isMultitenant, tenantId);
|
|
244
|
+
const destination = EffectiveTenants.destination(effective, route.context.isMultitenant);
|
|
245
|
+
return await this.#query(route, ServiceValues.createReadPlan(target, query, route), destination);
|
|
203
246
|
}
|
|
204
247
|
catch {
|
|
205
248
|
return ServiceValues.queryErrorResponse("QUERY_READ_ERROR", "Query read failed.");
|
|
@@ -497,12 +540,23 @@ export class SpineServices {
|
|
|
497
540
|
* @internal
|
|
498
541
|
*/
|
|
499
542
|
export const spineServicesAccess = Object.freeze({
|
|
543
|
+
/**
|
|
544
|
+
* Clears the logger installed for a closed service adapter.
|
|
545
|
+
*
|
|
546
|
+
* @param services Adapter whose logger is removed.
|
|
547
|
+
*/
|
|
500
548
|
clearLogger(services) {
|
|
501
549
|
if (!serviceInstances.has(services)) {
|
|
502
550
|
throw new TypeError("SpineServices logger requires a SpineServices instance.");
|
|
503
551
|
}
|
|
504
552
|
serviceLoggers.delete(services);
|
|
505
553
|
},
|
|
554
|
+
/**
|
|
555
|
+
* Sets the server logger for service diagnostics.
|
|
556
|
+
*
|
|
557
|
+
* @param services Adapter receiving the logger.
|
|
558
|
+
* @param logger Server logging layer.
|
|
559
|
+
*/
|
|
506
560
|
installLogger(services, logger) {
|
|
507
561
|
if (!serviceInstances.has(services)) {
|
|
508
562
|
throw new TypeError("SpineServices logger requires a SpineServices instance.");
|
|
@@ -510,21 +564,44 @@ export const spineServicesAccess = Object.freeze({
|
|
|
510
564
|
serviceLoggers.set(services, logger);
|
|
511
565
|
},
|
|
512
566
|
});
|
|
567
|
+
/**
|
|
568
|
+
* Buffers subscription updates and wakes active stream readers.
|
|
569
|
+
*/
|
|
513
570
|
class SubscriptionDelivery {
|
|
514
571
|
#queue = [];
|
|
515
572
|
#waiters = [];
|
|
516
573
|
#queueLimit;
|
|
517
574
|
#subscription;
|
|
518
575
|
#closed = false;
|
|
576
|
+
/**
|
|
577
|
+
* Sets the bounded update queue capacity.
|
|
578
|
+
*
|
|
579
|
+
* @param queueLimit Maximum pending updates before closure.
|
|
580
|
+
*/
|
|
519
581
|
constructor(queueLimit) {
|
|
520
582
|
this.#queueLimit = queueLimit;
|
|
521
583
|
}
|
|
584
|
+
/**
|
|
585
|
+
* Checks whether a source attachment is active.
|
|
586
|
+
*
|
|
587
|
+
* @returns `true` after an attachment is installed.
|
|
588
|
+
*/
|
|
522
589
|
get active() {
|
|
523
590
|
return this.#subscription !== undefined;
|
|
524
591
|
}
|
|
592
|
+
/**
|
|
593
|
+
* Checks whether this delivery queue has closed.
|
|
594
|
+
*
|
|
595
|
+
* @returns `true` after closure.
|
|
596
|
+
*/
|
|
525
597
|
get closed() {
|
|
526
598
|
return this.#closed;
|
|
527
599
|
}
|
|
600
|
+
/**
|
|
601
|
+
* Attaches a source subscription or immediately removes it after close.
|
|
602
|
+
*
|
|
603
|
+
* @param subscription Source attachment to track.
|
|
604
|
+
*/
|
|
528
605
|
attach(subscription) {
|
|
529
606
|
if (this.#closed) {
|
|
530
607
|
subscription.unsubscribe();
|
|
@@ -532,6 +609,11 @@ class SubscriptionDelivery {
|
|
|
532
609
|
}
|
|
533
610
|
this.#subscription = subscription;
|
|
534
611
|
}
|
|
612
|
+
/**
|
|
613
|
+
* Queues one update or wakes the next waiting stream reader.
|
|
614
|
+
*
|
|
615
|
+
* @param update State or Event update to deliver.
|
|
616
|
+
*/
|
|
535
617
|
push(update) {
|
|
536
618
|
if (this.#closed) {
|
|
537
619
|
return;
|
|
@@ -548,6 +630,11 @@ class SubscriptionDelivery {
|
|
|
548
630
|
waiter(update);
|
|
549
631
|
}
|
|
550
632
|
}
|
|
633
|
+
/**
|
|
634
|
+
* Reads the next queued update, waiting while the stream is open and empty.
|
|
635
|
+
*
|
|
636
|
+
* @returns Next update, or `undefined` after closure.
|
|
637
|
+
*/
|
|
551
638
|
next() {
|
|
552
639
|
const update = this.#queue.shift();
|
|
553
640
|
if (update !== undefined || this.#closed) {
|
|
@@ -555,6 +642,9 @@ class SubscriptionDelivery {
|
|
|
555
642
|
}
|
|
556
643
|
return new Promise((resolve) => this.#waiters.push(resolve));
|
|
557
644
|
}
|
|
645
|
+
/**
|
|
646
|
+
* Closes the source subscription and completes all waiting readers.
|
|
647
|
+
*/
|
|
558
648
|
close() {
|
|
559
649
|
if (this.#closed) {
|
|
560
650
|
return;
|
|
@@ -569,1014 +659,1006 @@ class SubscriptionDelivery {
|
|
|
569
659
|
}
|
|
570
660
|
}
|
|
571
661
|
/**
|
|
572
|
-
*
|
|
662
|
+
* Builds an OK status for service responses.
|
|
573
663
|
*/
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
664
|
+
function okStatus() {
|
|
665
|
+
return create(StatusSchema, {
|
|
666
|
+
status: {
|
|
667
|
+
case: "ok",
|
|
668
|
+
value: create(EmptySchema),
|
|
669
|
+
},
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
function errorStatus(type, message, details) {
|
|
673
|
+
return create(StatusSchema, {
|
|
674
|
+
status: {
|
|
675
|
+
case: "error",
|
|
676
|
+
value: create(ErrorSchema, {
|
|
677
|
+
type,
|
|
678
|
+
message,
|
|
679
|
+
...(details === undefined ? {} : { details }),
|
|
680
|
+
}),
|
|
681
|
+
},
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
function okResponse() {
|
|
685
|
+
return create(ResponseSchema, { status: okStatus() });
|
|
686
|
+
}
|
|
687
|
+
function errorResponse(type, message) {
|
|
688
|
+
return create(ResponseSchema, { status: errorStatus(type, message) });
|
|
689
|
+
}
|
|
690
|
+
function queryErrorResponse(type, message) {
|
|
691
|
+
return create(QueryResponseSchema, {
|
|
692
|
+
response: errorResponse(type, message),
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
function validateReadQuery(query, target, route) {
|
|
696
|
+
const formatError = formatReadError(query.format, route);
|
|
697
|
+
if (formatError !== undefined) {
|
|
698
|
+
return formatError;
|
|
699
|
+
}
|
|
700
|
+
switch (target.criterion.case) {
|
|
701
|
+
case "includeAll":
|
|
702
|
+
if (!target.criterion.value) {
|
|
703
|
+
return invalidCriterionError();
|
|
704
|
+
}
|
|
705
|
+
return undefined;
|
|
706
|
+
case "filters":
|
|
707
|
+
return validateFilters(target.criterion.value, route);
|
|
708
|
+
default:
|
|
709
|
+
return invalidCriterionError();
|
|
597
710
|
}
|
|
598
|
-
|
|
599
|
-
|
|
711
|
+
}
|
|
712
|
+
function validateFilters(filters, route) {
|
|
713
|
+
const idCount = filters.idFilter?.id.length ?? 0;
|
|
714
|
+
if (idCount > QueryLimits.idCount) {
|
|
715
|
+
return invalidQueryError("QueryService.Read id_filter may contain at most 100 IDs.");
|
|
600
716
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
});
|
|
717
|
+
const idEntries = filters.idFilter?.id;
|
|
718
|
+
if (idEntries?.some((id) => id === undefined) === true) {
|
|
719
|
+
return invalidQueryError("QueryService.Read id_filter entries are required.");
|
|
605
720
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
return formatError;
|
|
610
|
-
}
|
|
611
|
-
switch (target.criterion.case) {
|
|
612
|
-
case "includeAll":
|
|
613
|
-
if (!target.criterion.value) {
|
|
614
|
-
return invalidCriterionError();
|
|
615
|
-
}
|
|
616
|
-
return undefined;
|
|
617
|
-
case "filters":
|
|
618
|
-
return validateFilters(target.criterion.value, route);
|
|
619
|
-
default:
|
|
620
|
-
return invalidCriterionError();
|
|
621
|
-
}
|
|
721
|
+
const idError = validateQueryIds(idEntries, route);
|
|
722
|
+
if (idError !== undefined) {
|
|
723
|
+
return idError;
|
|
622
724
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
if (idCount > QueryLimits.idCount) {
|
|
626
|
-
return invalidQueryError("QueryService.Read id_filter may contain at most 100 IDs.");
|
|
627
|
-
}
|
|
628
|
-
const idEntries = filters.idFilter?.id;
|
|
629
|
-
if (idEntries?.some((id) => id === undefined) === true) {
|
|
630
|
-
return invalidQueryError("QueryService.Read id_filter entries are required.");
|
|
631
|
-
}
|
|
632
|
-
const idError = validateQueryIds(idEntries, route);
|
|
633
|
-
if (idError !== undefined) {
|
|
634
|
-
return idError;
|
|
635
|
-
}
|
|
636
|
-
if (filters.filter.length > QueryLimits.compositeCount) {
|
|
637
|
-
return invalidQueryError("QueryService.Read may contain at most 8 composite filters.");
|
|
638
|
-
}
|
|
639
|
-
const filterError = validateCompositeFilters(filters.filter, route);
|
|
640
|
-
if (filterError !== undefined) {
|
|
641
|
-
return filterError;
|
|
642
|
-
}
|
|
643
|
-
return idCount === 0 && countSimpleFilters(filters.filter) === 0
|
|
644
|
-
? {
|
|
645
|
-
type: "INVALID_QUERY",
|
|
646
|
-
message: "QueryService.Read requires an ID filter or column filter.",
|
|
647
|
-
}
|
|
648
|
-
: undefined;
|
|
725
|
+
if (filters.filter.length > QueryLimits.compositeCount) {
|
|
726
|
+
return invalidQueryError("QueryService.Read may contain at most 8 composite filters.");
|
|
649
727
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
let compositeCount = 0;
|
|
654
|
-
let simpleCount = 0;
|
|
655
|
-
while (pending.length > 0) {
|
|
656
|
-
const current = pending.pop();
|
|
657
|
-
if (current === undefined)
|
|
658
|
-
break;
|
|
659
|
-
const filter = current.filter;
|
|
660
|
-
if (seen.has(filter))
|
|
661
|
-
return unsupportedFilterError("QueryService.Read filters must not contain cycles.");
|
|
662
|
-
seen.add(filter);
|
|
663
|
-
compositeCount += 1;
|
|
664
|
-
simpleCount += filter.filter.length;
|
|
665
|
-
if (filter.filter.length === 0 && filter.compositeFilter.length === 0) {
|
|
666
|
-
return invalidQueryError("QueryService.Read composite filter must not be empty.");
|
|
667
|
-
}
|
|
668
|
-
if (compositeCount > QueryLimits.compositeCount || current.depth > 8) {
|
|
669
|
-
return invalidQueryError("QueryService.Read may contain at most 8 composite filters.");
|
|
670
|
-
}
|
|
671
|
-
if (simpleCount > QueryLimits.filterCount) {
|
|
672
|
-
return invalidQueryError("QueryService.Read may contain at most 16 simple column filters.");
|
|
673
|
-
}
|
|
674
|
-
if (compositeCount + pending.length + filter.compositeFilter.length >
|
|
675
|
-
QueryLimits.compositeCount) {
|
|
676
|
-
return invalidQueryError("QueryService.Read may contain at most 8 composite filters.");
|
|
677
|
-
}
|
|
678
|
-
if (current.depth >= 8 && filter.compositeFilter.length > 0) {
|
|
679
|
-
return invalidQueryError("QueryService.Read may contain at most 8 composite filters.");
|
|
680
|
-
}
|
|
681
|
-
const operator = filter.operator;
|
|
682
|
-
if (operator !== CompositeFilter_CompositeOperator.ALL &&
|
|
683
|
-
operator !== CompositeFilter_CompositeOperator.EITHER) {
|
|
684
|
-
return unsupportedFilterError("QueryService.Read composite operator must be ALL or EITHER.");
|
|
685
|
-
}
|
|
686
|
-
const simpleError = validateSimpleFilters(filter.filter, route);
|
|
687
|
-
if (simpleError !== undefined) {
|
|
688
|
-
return simpleError;
|
|
689
|
-
}
|
|
690
|
-
for (const child of filter.compositeFilter) {
|
|
691
|
-
pending.push({ filter: child, depth: current.depth + 1 });
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
return undefined;
|
|
728
|
+
const filterError = validateCompositeFilters(filters.filter, route);
|
|
729
|
+
if (filterError !== undefined) {
|
|
730
|
+
return filterError;
|
|
695
731
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
}
|
|
701
|
-
if (ids.some((id) => id === undefined || AnyMessages.unpack(id, schema) === undefined)) {
|
|
702
|
-
return invalidQueryError(`QueryService.Read id_filter values must pack ${schema.typeName}.`);
|
|
732
|
+
return idCount === 0 && countSimpleFilters(filters.filter) === 0
|
|
733
|
+
? {
|
|
734
|
+
type: "INVALID_QUERY",
|
|
735
|
+
message: "QueryService.Read requires an ID filter or column filter.",
|
|
703
736
|
}
|
|
737
|
+
: undefined;
|
|
738
|
+
}
|
|
739
|
+
function validateCompositeFilters(filters, route) {
|
|
740
|
+
const pending = filters.map((filter) => ({ filter, depth: 0 }));
|
|
741
|
+
const seen = new WeakSet();
|
|
742
|
+
let compositeCount = 0;
|
|
743
|
+
let simpleCount = 0;
|
|
744
|
+
while (pending.length > 0) {
|
|
745
|
+
const current = pending.pop();
|
|
746
|
+
if (current === undefined)
|
|
747
|
+
break;
|
|
748
|
+
const filter = current.filter;
|
|
749
|
+
if (seen.has(filter))
|
|
750
|
+
return unsupportedFilterError("QueryService.Read filters must not contain cycles.");
|
|
751
|
+
seen.add(filter);
|
|
752
|
+
compositeCount += 1;
|
|
753
|
+
simpleCount += filter.filter.length;
|
|
754
|
+
const error = ServiceValues.validateCompositeNode(filter, current.depth, compositeCount, simpleCount, pending.length, route);
|
|
755
|
+
if (error !== undefined)
|
|
756
|
+
return error;
|
|
757
|
+
for (const child of filter.compositeFilter) {
|
|
758
|
+
pending.push({ filter: child, depth: current.depth + 1 });
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return undefined;
|
|
762
|
+
}
|
|
763
|
+
function validateQueryIds(ids, route) {
|
|
764
|
+
const schema = route.idField.message;
|
|
765
|
+
if (schema === undefined || ids === undefined) {
|
|
704
766
|
return undefined;
|
|
705
767
|
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
let count = 0;
|
|
709
|
-
while (pending.length > 0) {
|
|
710
|
-
const filter = pending.pop();
|
|
711
|
-
if (filter === undefined)
|
|
712
|
-
break;
|
|
713
|
-
count += filter.filter.length;
|
|
714
|
-
pending.push(...filter.compositeFilter);
|
|
715
|
-
}
|
|
716
|
-
return count;
|
|
717
|
-
}
|
|
718
|
-
function validateSimpleFilters(filters, route) {
|
|
719
|
-
for (const filter of filters) {
|
|
720
|
-
if (wireComparison(filter.operator) === undefined) {
|
|
721
|
-
return unsupportedFilterError("QueryService.Read comparison operator is not supported.");
|
|
722
|
-
}
|
|
723
|
-
const column = filter.fieldPath?.fieldName[0];
|
|
724
|
-
if (column === undefined || column.trim().length === 0) {
|
|
725
|
-
return unsupportedFilterError("QueryService.Read column filter field is required.");
|
|
726
|
-
}
|
|
727
|
-
if (filter.fieldPath?.fieldName.length !== 1) {
|
|
728
|
-
return unsupportedFilterError("QueryService.Read supports only top-level column filters.");
|
|
729
|
-
}
|
|
730
|
-
if (!route.allowedColumnNames.has(column)) {
|
|
731
|
-
return unsupportedFilterError(`QueryService.Read column filter "${column}" is not a declared column.`);
|
|
732
|
-
}
|
|
733
|
-
if (filter.value === undefined) {
|
|
734
|
-
return unsupportedFilterError("QueryService.Read column filter value is required.");
|
|
735
|
-
}
|
|
736
|
-
const field = route.columnFields.get(column);
|
|
737
|
-
if (field === undefined
|
|
738
|
-
? !systemValueMatches(column, filter.value)
|
|
739
|
-
: !filterValueMatches(field, filter.value)) {
|
|
740
|
-
return unsupportedFilterError(`QueryService.Read column filter "${column}" has the wrong value type.`);
|
|
741
|
-
}
|
|
742
|
-
if (filter.operator !== Filter_Operator.EQUAL &&
|
|
743
|
-
(field === undefined ? column !== "version" : !supportsRange(field))) {
|
|
744
|
-
return unsupportedFilterError(`QueryService.Read column filter "${column}" does not support range comparison.`);
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
return undefined;
|
|
768
|
+
if (ids.some((id) => id === undefined || AnyMessages.unpack(id, schema) === undefined)) {
|
|
769
|
+
return invalidQueryError(`QueryService.Read id_filter values must pack ${schema.typeName}.`);
|
|
748
770
|
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
if (
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
771
|
+
return undefined;
|
|
772
|
+
}
|
|
773
|
+
function countSimpleFilters(filters) {
|
|
774
|
+
const pending = [...filters];
|
|
775
|
+
let count = 0;
|
|
776
|
+
while (pending.length > 0) {
|
|
777
|
+
const filter = pending.pop();
|
|
778
|
+
if (filter === undefined)
|
|
779
|
+
break;
|
|
780
|
+
count += filter.filter.length;
|
|
781
|
+
pending.push(...filter.compositeFilter);
|
|
782
|
+
}
|
|
783
|
+
return count;
|
|
784
|
+
}
|
|
785
|
+
function validateSimpleFilters(filters, route) {
|
|
786
|
+
for (const filter of filters) {
|
|
787
|
+
if (wireComparison(filter.operator) === undefined) {
|
|
788
|
+
return unsupportedFilterError("QueryService.Read comparison operator is not supported.");
|
|
766
789
|
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
message: "QueryService.Read limit may be at most 1000.",
|
|
771
|
-
};
|
|
790
|
+
const column = filter.fieldPath?.fieldName[0];
|
|
791
|
+
if (column === undefined || column.trim().length === 0) {
|
|
792
|
+
return unsupportedFilterError("QueryService.Read column filter field is required.");
|
|
772
793
|
}
|
|
773
|
-
if (
|
|
774
|
-
return
|
|
775
|
-
type: "INVALID_QUERY",
|
|
776
|
-
message: "QueryService.Read limit requires ordering.",
|
|
777
|
-
};
|
|
794
|
+
if (filter.fieldPath?.fieldName.length !== 1) {
|
|
795
|
+
return unsupportedFilterError("QueryService.Read supports only top-level column filters.");
|
|
778
796
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
return unsupportedFormatError("QueryService.Read order_by column is required.");
|
|
782
|
-
}
|
|
783
|
-
if (!route.allowedColumnNames.has(order.column)) {
|
|
784
|
-
return unsupportedFormatError(`QueryService.Read order_by column "${order.column}" is not a declared column.`);
|
|
785
|
-
}
|
|
786
|
-
const field = route.columnFields.get(order.column);
|
|
787
|
-
if (field === undefined ? order.column !== "version" : !supportsRange(field)) {
|
|
788
|
-
return unsupportedFormatError(`QueryService.Read order_by column "${order.column}" is not orderable.`);
|
|
789
|
-
}
|
|
790
|
-
if (order.direction !== OrderBy_Direction.ASCENDING &&
|
|
791
|
-
order.direction !== OrderBy_Direction.DESCENDING) {
|
|
792
|
-
return unsupportedFormatError("QueryService.Read order_by direction must be ASCENDING or DESCENDING.");
|
|
793
|
-
}
|
|
797
|
+
if (!route.allowedColumnNames.has(column)) {
|
|
798
|
+
return unsupportedFilterError(`QueryService.Read column filter "${column}" is not a declared column.`);
|
|
794
799
|
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
function createReadPlan(target, query, route) {
|
|
798
|
-
const predicate = target.criterion.case === "filters"
|
|
799
|
-
? normalizedFilters(target.criterion.value, route)
|
|
800
|
-
: undefined;
|
|
801
|
-
const format = query.format;
|
|
802
|
-
return Object.freeze({
|
|
803
|
-
...(predicate === undefined ? {} : { predicate }),
|
|
804
|
-
...(format?.fieldMask === undefined
|
|
805
|
-
? {}
|
|
806
|
-
: {
|
|
807
|
-
mask: {
|
|
808
|
-
paths: format.fieldMask.paths.map((path) => requiredQueryMask(route.schema, path)),
|
|
809
|
-
},
|
|
810
|
-
}),
|
|
811
|
-
...(format === undefined || format.orderBy.length === 0
|
|
812
|
-
? {}
|
|
813
|
-
: {
|
|
814
|
-
order: format.orderBy.map((order) => ({
|
|
815
|
-
column: order.column,
|
|
816
|
-
direction: order.direction === OrderBy_Direction.DESCENDING
|
|
817
|
-
? "desc"
|
|
818
|
-
: "asc",
|
|
819
|
-
})),
|
|
820
|
-
}),
|
|
821
|
-
...(format === undefined || format.limit === 0 ? {} : { limit: format.limit }),
|
|
822
|
-
});
|
|
823
|
-
}
|
|
824
|
-
function normalizedFilters(filters, route) {
|
|
825
|
-
const predicates = [];
|
|
826
|
-
if (filters.idFilter !== undefined) {
|
|
827
|
-
predicates.push({
|
|
828
|
-
kind: "ids",
|
|
829
|
-
ids: filters.idFilter.id.map((id) => decodeQueryIdValue(id, route)),
|
|
830
|
-
});
|
|
831
|
-
}
|
|
832
|
-
predicates.push(...filters.filter.map((filter) => normalizedComposite(filter, route)));
|
|
833
|
-
const onlyPredicate = predicates[0];
|
|
834
|
-
return predicates.length === 1 && onlyPredicate !== undefined
|
|
835
|
-
? onlyPredicate
|
|
836
|
-
: { kind: "all", predicates };
|
|
837
|
-
}
|
|
838
|
-
function decodeQueryIdValue(value, route) {
|
|
839
|
-
const schema = route.idField.message;
|
|
840
|
-
return schema === undefined ? decodeAnyValue(value) : AnyMessages.unpack(value, schema);
|
|
841
|
-
}
|
|
842
|
-
function normalizedComposite(filter, route) {
|
|
843
|
-
const predicates = [
|
|
844
|
-
...filter.filter.map((child) => ({
|
|
845
|
-
kind: "comparison",
|
|
846
|
-
column: requiredFilterColumn(child),
|
|
847
|
-
operator: requiredComparison(child),
|
|
848
|
-
value: decodeColumnValue(child, route),
|
|
849
|
-
})),
|
|
850
|
-
...filter.compositeFilter.map((child) => normalizedComposite(child, route)),
|
|
851
|
-
];
|
|
852
|
-
return {
|
|
853
|
-
kind: filter.operator === CompositeFilter_CompositeOperator.EITHER ? "either" : "all",
|
|
854
|
-
predicates,
|
|
855
|
-
};
|
|
856
|
-
}
|
|
857
|
-
function requiredFilterColumn(filter) {
|
|
858
|
-
const column = filter.fieldPath?.fieldName[0];
|
|
859
|
-
if (column !== undefined)
|
|
860
|
-
return column;
|
|
861
|
-
throw new TypeError("Validated query filter has no column.");
|
|
862
|
-
}
|
|
863
|
-
function requiredComparison(filter) {
|
|
864
|
-
const operator = wireComparison(filter.operator);
|
|
865
|
-
if (operator !== undefined)
|
|
866
|
-
return operator;
|
|
867
|
-
throw new TypeError("Validated query filter has no supported comparison operator.");
|
|
868
|
-
}
|
|
869
|
-
function decodeColumnValue(filter, route) {
|
|
870
|
-
const column = filter.fieldPath?.fieldName[0] ?? "";
|
|
871
|
-
if (column === "version" && filter.value !== undefined) {
|
|
872
|
-
const value = AnyMessages.unpack(filter.value, VersionSchema);
|
|
873
|
-
if (value !== undefined)
|
|
874
|
-
return value;
|
|
875
|
-
throw new TypeError('Validated query column "version" has an invalid value.');
|
|
876
|
-
}
|
|
877
|
-
if ((column === "archived" || column === "deleted") && filter.value !== undefined) {
|
|
878
|
-
const value = AnyMessages.unpack(filter.value, BoolValueSchema);
|
|
879
|
-
if (value !== undefined)
|
|
880
|
-
return value.value;
|
|
881
|
-
throw new TypeError(`Validated query column "${column}" has an invalid value.`);
|
|
800
|
+
if (filter.value === undefined) {
|
|
801
|
+
return unsupportedFilterError("QueryService.Read column filter value is required.");
|
|
882
802
|
}
|
|
883
803
|
const field = route.columnFields.get(column);
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
if (schema === undefined || value.typeUrl !== TypeUrls.derive(schema))
|
|
889
|
-
return false;
|
|
890
|
-
const decoded = field.fieldKind === "message" ? decodeAnyValue(value, schema) : decodeAnyValue(value);
|
|
891
|
-
if (field.fieldKind === "message") {
|
|
892
|
-
return isMessage(decoded) && decoded.$typeName === field.message.typeName;
|
|
893
|
-
}
|
|
894
|
-
if (field.fieldKind === "enum")
|
|
895
|
-
return typeof decoded === "number" && Number.isInteger(decoded);
|
|
896
|
-
if (field.fieldKind !== "scalar")
|
|
897
|
-
return false;
|
|
898
|
-
if (field.scalar === ScalarType.BOOL)
|
|
899
|
-
return typeof decoded === "boolean";
|
|
900
|
-
if (field.scalar === ScalarType.BYTES)
|
|
901
|
-
return decoded instanceof Uint8Array;
|
|
902
|
-
if (field.scalar === ScalarType.STRING)
|
|
903
|
-
return typeof decoded === "string";
|
|
904
|
-
if (field.scalar === ScalarType.INT64 ||
|
|
905
|
-
field.scalar === ScalarType.UINT64 ||
|
|
906
|
-
field.scalar === ScalarType.SFIXED64 ||
|
|
907
|
-
field.scalar === ScalarType.FIXED64 ||
|
|
908
|
-
field.scalar === ScalarType.SINT64) {
|
|
909
|
-
return typeof decoded === "bigint";
|
|
910
|
-
}
|
|
911
|
-
return typeof decoded === "number" && Number.isFinite(decoded);
|
|
912
|
-
}
|
|
913
|
-
function filterValueSchema(field) {
|
|
914
|
-
if (field.fieldKind === "message")
|
|
915
|
-
return field.message;
|
|
916
|
-
if (field.fieldKind === "enum")
|
|
917
|
-
return Int32ValueSchema;
|
|
918
|
-
if (field.fieldKind !== "scalar")
|
|
919
|
-
return undefined;
|
|
920
|
-
if (field.scalar === ScalarType.BOOL)
|
|
921
|
-
return BoolValueSchema;
|
|
922
|
-
if (field.scalar === ScalarType.BYTES)
|
|
923
|
-
return BytesValueSchema;
|
|
924
|
-
if (field.scalar === ScalarType.DOUBLE)
|
|
925
|
-
return DoubleValueSchema;
|
|
926
|
-
if (field.scalar === ScalarType.FLOAT)
|
|
927
|
-
return FloatValueSchema;
|
|
928
|
-
if (field.scalar === ScalarType.INT64 ||
|
|
929
|
-
field.scalar === ScalarType.SFIXED64 ||
|
|
930
|
-
field.scalar === ScalarType.SINT64) {
|
|
931
|
-
return Int64ValueSchema;
|
|
932
|
-
}
|
|
933
|
-
if (field.scalar === ScalarType.UINT64 || field.scalar === ScalarType.FIXED64) {
|
|
934
|
-
return UInt64ValueSchema;
|
|
935
|
-
}
|
|
936
|
-
if (field.scalar === ScalarType.UINT32 || field.scalar === ScalarType.FIXED32) {
|
|
937
|
-
return UInt32ValueSchema;
|
|
938
|
-
}
|
|
939
|
-
if (field.scalar === ScalarType.STRING)
|
|
940
|
-
return StringValueSchema;
|
|
941
|
-
return Int32ValueSchema;
|
|
942
|
-
}
|
|
943
|
-
function systemValueMatches(column, value) {
|
|
944
|
-
if (column === "version") {
|
|
945
|
-
return (value.typeUrl === TypeUrls.derive(VersionSchema) &&
|
|
946
|
-
AnyMessages.unpack(value, VersionSchema) !== undefined);
|
|
804
|
+
if (field === undefined
|
|
805
|
+
? !systemValueMatches(column, filter.value)
|
|
806
|
+
: !filterValueMatches(field, filter.value)) {
|
|
807
|
+
return unsupportedFilterError(`QueryService.Read column filter "${column}" has the wrong value type.`);
|
|
947
808
|
}
|
|
948
|
-
if (
|
|
949
|
-
|
|
950
|
-
|
|
809
|
+
if (filter.operator !== Filter_Operator.EQUAL &&
|
|
810
|
+
(field === undefined ? column !== "version" : !supportsRange(field))) {
|
|
811
|
+
return unsupportedFilterError(`QueryService.Read column filter "${column}" does not support range comparison.`);
|
|
951
812
|
}
|
|
952
|
-
return false;
|
|
953
813
|
}
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
field.scalar !== ScalarType.BYTES);
|
|
962
|
-
}
|
|
963
|
-
function wireComparison(operator) {
|
|
964
|
-
if (operator === Filter_Operator.EQUAL)
|
|
965
|
-
return "equal";
|
|
966
|
-
if (operator === Filter_Operator.GREATER_THAN)
|
|
967
|
-
return "greaterThan";
|
|
968
|
-
if (operator === Filter_Operator.LESS_THAN)
|
|
969
|
-
return "lessThan";
|
|
970
|
-
if (operator === Filter_Operator.GREATER_OR_EQUAL)
|
|
971
|
-
return "greaterOrEqual";
|
|
972
|
-
if (operator === Filter_Operator.LESS_OR_EQUAL)
|
|
973
|
-
return "lessOrEqual";
|
|
814
|
+
return undefined;
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Checks query ordering and limits against the registered state route.
|
|
818
|
+
*/
|
|
819
|
+
function formatReadError(format, route) {
|
|
820
|
+
if (format === undefined) {
|
|
974
821
|
return undefined;
|
|
975
822
|
}
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
const local = [];
|
|
979
|
-
for (const segment of path.split(".")) {
|
|
980
|
-
const field = findMessageField(current, segment);
|
|
981
|
-
if (field === undefined)
|
|
982
|
-
return undefined;
|
|
983
|
-
local.push(field.localName);
|
|
984
|
-
current = field.message;
|
|
985
|
-
}
|
|
986
|
-
return local.join(".");
|
|
987
|
-
}
|
|
988
|
-
function requiredQueryMask(schema, path) {
|
|
989
|
-
const resolved = resolveQueryMask(schema, path);
|
|
990
|
-
if (resolved !== undefined)
|
|
991
|
-
return resolved;
|
|
992
|
-
throw new TypeError(`Validated query mask path "${path}" is invalid.`);
|
|
993
|
-
}
|
|
994
|
-
function unsupportedFilterError(message) {
|
|
995
|
-
return invalidQueryError(message);
|
|
823
|
+
if (format.orderBy.length > QueryLimits.orderCount) {
|
|
824
|
+
return unsupportedFormatError("QueryService.Read order_by may contain at most 8 entries.");
|
|
996
825
|
}
|
|
997
|
-
|
|
998
|
-
return invalidQueryError(message);
|
|
999
|
-
}
|
|
1000
|
-
function invalidQueryError(message) {
|
|
826
|
+
if (format.limit > QueryLimits.resultCount) {
|
|
1001
827
|
return {
|
|
1002
828
|
type: "INVALID_QUERY",
|
|
1003
|
-
message,
|
|
829
|
+
message: "QueryService.Read limit may be at most 1000.",
|
|
1004
830
|
};
|
|
1005
831
|
}
|
|
1006
|
-
|
|
832
|
+
if (format.limit > 0 && format.orderBy.length === 0) {
|
|
1007
833
|
return {
|
|
1008
834
|
type: "INVALID_QUERY",
|
|
1009
|
-
message: "QueryService.Read requires
|
|
835
|
+
message: "QueryService.Read limit requires ordering.",
|
|
1010
836
|
};
|
|
1011
837
|
}
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
maskPathLength: 128,
|
|
1036
|
-
pathComponentCount: 16,
|
|
1037
|
-
pathSegmentLength: 128,
|
|
838
|
+
return ServiceValues.validateReadOrder(format.orderBy, route);
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Converts an accepted wire query into a storage plan for complete states.
|
|
842
|
+
*/
|
|
843
|
+
function createReadPlan(target, query, route) {
|
|
844
|
+
const predicate = target.criterion.case === "filters"
|
|
845
|
+
? normalizedFilters(target.criterion.value, route)
|
|
846
|
+
: undefined;
|
|
847
|
+
const format = query.format;
|
|
848
|
+
return Object.freeze({
|
|
849
|
+
...(predicate === undefined ? {} : { predicate }),
|
|
850
|
+
...(format === undefined || format.orderBy.length === 0
|
|
851
|
+
? {}
|
|
852
|
+
: {
|
|
853
|
+
order: format.orderBy.map((order) => ({
|
|
854
|
+
column: order.column,
|
|
855
|
+
direction: order.direction === OrderBy_Direction.DESCENDING
|
|
856
|
+
? "desc"
|
|
857
|
+
: "asc",
|
|
858
|
+
})),
|
|
859
|
+
}),
|
|
860
|
+
...(format === undefined || format.limit === 0 ? {} : { limit: format.limit }),
|
|
1038
861
|
});
|
|
1039
|
-
|
|
1040
|
-
|
|
862
|
+
}
|
|
863
|
+
function normalizedFilters(filters, route) {
|
|
864
|
+
const predicates = [];
|
|
865
|
+
if (filters.idFilter !== undefined) {
|
|
866
|
+
predicates.push({
|
|
867
|
+
kind: "ids",
|
|
868
|
+
ids: filters.idFilter.id.map((id) => decodeQueryIdValue(id, route)),
|
|
869
|
+
});
|
|
1041
870
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
871
|
+
predicates.push(...filters.filter.map((filter) => normalizedComposite(filter, route)));
|
|
872
|
+
const onlyPredicate = predicates[0];
|
|
873
|
+
return predicates.length === 1 && onlyPredicate !== undefined
|
|
874
|
+
? onlyPredicate
|
|
875
|
+
: { kind: "all", predicates };
|
|
876
|
+
}
|
|
877
|
+
function decodeQueryIdValue(value, route) {
|
|
878
|
+
const schema = route.idField.message;
|
|
879
|
+
return schema === undefined ? decodeAnyValue(value) : AnyMessages.unpack(value, schema);
|
|
880
|
+
}
|
|
881
|
+
function normalizedComposite(filter, route) {
|
|
882
|
+
const predicates = [
|
|
883
|
+
...filter.filter.map((child) => ({
|
|
884
|
+
kind: "comparison",
|
|
885
|
+
column: requiredFilterColumn(child),
|
|
886
|
+
operator: requiredComparison(child),
|
|
887
|
+
value: decodeColumnValue(child, route),
|
|
888
|
+
})),
|
|
889
|
+
...filter.compositeFilter.map((child) => normalizedComposite(child, route)),
|
|
890
|
+
];
|
|
891
|
+
return {
|
|
892
|
+
kind: filter.operator === CompositeFilter_CompositeOperator.EITHER ? "either" : "all",
|
|
893
|
+
predicates,
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
function requiredFilterColumn(filter) {
|
|
897
|
+
const column = filter.fieldPath?.fieldName[0];
|
|
898
|
+
if (column !== undefined)
|
|
899
|
+
return column;
|
|
900
|
+
throw new TypeError("Validated query filter has no column.");
|
|
901
|
+
}
|
|
902
|
+
function requiredComparison(filter) {
|
|
903
|
+
const operator = wireComparison(filter.operator);
|
|
904
|
+
if (operator !== undefined)
|
|
905
|
+
return operator;
|
|
906
|
+
throw new TypeError("Validated query filter has no supported comparison operator.");
|
|
907
|
+
}
|
|
908
|
+
function decodeColumnValue(filter, route) {
|
|
909
|
+
const column = filter.fieldPath?.fieldName[0] ?? "";
|
|
910
|
+
if (column === "version" && filter.value !== undefined) {
|
|
911
|
+
const value = AnyMessages.unpack(filter.value, VersionSchema);
|
|
912
|
+
if (value !== undefined)
|
|
913
|
+
return value;
|
|
914
|
+
throw new TypeError('Validated query column "version" has an invalid value.');
|
|
915
|
+
}
|
|
916
|
+
if ((column === "archived" || column === "deleted") && filter.value !== undefined) {
|
|
917
|
+
const value = AnyMessages.unpack(filter.value, BoolValueSchema);
|
|
918
|
+
if (value !== undefined)
|
|
919
|
+
return value.value;
|
|
920
|
+
throw new TypeError(`Validated query column "${column}" has an invalid value.`);
|
|
921
|
+
}
|
|
922
|
+
const field = route.columnFields.get(column);
|
|
923
|
+
return decodeAnyValue(filter.value, field?.fieldKind === "message" ? field.message : undefined);
|
|
924
|
+
}
|
|
925
|
+
function filterValueMatches(field, value) {
|
|
926
|
+
const schema = filterValueSchema(field);
|
|
927
|
+
if (schema === undefined || value.typeUrl !== TypeUrls.derive(schema))
|
|
928
|
+
return false;
|
|
929
|
+
const decoded = field.fieldKind === "message" ? decodeAnyValue(value, schema) : decodeAnyValue(value);
|
|
930
|
+
if (field.fieldKind === "message") {
|
|
931
|
+
return isMessage(decoded) && decoded.$typeName === field.message.typeName;
|
|
1047
932
|
}
|
|
1048
|
-
|
|
1049
|
-
return
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
933
|
+
if (field.fieldKind === "enum")
|
|
934
|
+
return typeof decoded === "number" && Number.isInteger(decoded);
|
|
935
|
+
if (field.fieldKind !== "scalar")
|
|
936
|
+
return false;
|
|
937
|
+
if (field.scalar === ScalarType.BOOL)
|
|
938
|
+
return typeof decoded === "boolean";
|
|
939
|
+
if (field.scalar === ScalarType.BYTES)
|
|
940
|
+
return decoded instanceof Uint8Array;
|
|
941
|
+
if (field.scalar === ScalarType.STRING)
|
|
942
|
+
return typeof decoded === "string";
|
|
943
|
+
if (field.scalar === ScalarType.INT64 ||
|
|
944
|
+
field.scalar === ScalarType.UINT64 ||
|
|
945
|
+
field.scalar === ScalarType.SFIXED64 ||
|
|
946
|
+
field.scalar === ScalarType.FIXED64 ||
|
|
947
|
+
field.scalar === ScalarType.SINT64) {
|
|
948
|
+
return typeof decoded === "bigint";
|
|
949
|
+
}
|
|
950
|
+
return typeof decoded === "number" && Number.isFinite(decoded);
|
|
951
|
+
}
|
|
952
|
+
function filterValueSchema(field) {
|
|
953
|
+
if (field.fieldKind === "message")
|
|
954
|
+
return field.message;
|
|
955
|
+
if (field.fieldKind === "enum")
|
|
956
|
+
return Int32ValueSchema;
|
|
957
|
+
if (field.fieldKind !== "scalar")
|
|
958
|
+
return undefined;
|
|
959
|
+
if (field.scalar === ScalarType.BOOL)
|
|
960
|
+
return BoolValueSchema;
|
|
961
|
+
if (field.scalar === ScalarType.BYTES)
|
|
962
|
+
return BytesValueSchema;
|
|
963
|
+
if (field.scalar === ScalarType.DOUBLE)
|
|
964
|
+
return DoubleValueSchema;
|
|
965
|
+
if (field.scalar === ScalarType.FLOAT)
|
|
966
|
+
return FloatValueSchema;
|
|
967
|
+
if (field.scalar === ScalarType.INT64 ||
|
|
968
|
+
field.scalar === ScalarType.SFIXED64 ||
|
|
969
|
+
field.scalar === ScalarType.SINT64) {
|
|
970
|
+
return Int64ValueSchema;
|
|
971
|
+
}
|
|
972
|
+
if (field.scalar === ScalarType.UINT64 || field.scalar === ScalarType.FIXED64) {
|
|
973
|
+
return UInt64ValueSchema;
|
|
974
|
+
}
|
|
975
|
+
if (field.scalar === ScalarType.UINT32 || field.scalar === ScalarType.FIXED32) {
|
|
976
|
+
return UInt32ValueSchema;
|
|
977
|
+
}
|
|
978
|
+
if (field.scalar === ScalarType.STRING)
|
|
979
|
+
return StringValueSchema;
|
|
980
|
+
return Int32ValueSchema;
|
|
981
|
+
}
|
|
982
|
+
function systemValueMatches(column, value) {
|
|
983
|
+
if (column === "version") {
|
|
984
|
+
return (value.typeUrl === TypeUrls.derive(VersionSchema) &&
|
|
985
|
+
AnyMessages.unpack(value, VersionSchema) !== undefined);
|
|
1056
986
|
}
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
const settled = new Promise((settle, fail) => {
|
|
1061
|
-
reject = fail;
|
|
1062
|
-
resolve = () => {
|
|
1063
|
-
settle();
|
|
1064
|
-
};
|
|
1065
|
-
});
|
|
1066
|
-
return { reject, resolve, settled };
|
|
987
|
+
if (column === "archived" || column === "deleted") {
|
|
988
|
+
return (value.typeUrl === TypeUrls.derive(BoolValueSchema) &&
|
|
989
|
+
AnyMessages.unpack(value, BoolValueSchema) !== undefined);
|
|
1067
990
|
}
|
|
1068
|
-
|
|
1069
|
-
|
|
991
|
+
return false;
|
|
992
|
+
}
|
|
993
|
+
function supportsRange(field) {
|
|
994
|
+
if (field.fieldKind === "message") {
|
|
995
|
+
return (field.message.typeName === "google.protobuf.Timestamp" ||
|
|
996
|
+
field.message.typeName === "spine.core.Version");
|
|
997
|
+
}
|
|
998
|
+
return (field.fieldKind === "scalar" &&
|
|
999
|
+
field.scalar !== ScalarType.BOOL &&
|
|
1000
|
+
field.scalar !== ScalarType.BYTES);
|
|
1001
|
+
}
|
|
1002
|
+
function wireComparison(operator) {
|
|
1003
|
+
if (operator === Filter_Operator.EQUAL)
|
|
1004
|
+
return "equal";
|
|
1005
|
+
if (operator === Filter_Operator.GREATER_THAN)
|
|
1006
|
+
return "greaterThan";
|
|
1007
|
+
if (operator === Filter_Operator.LESS_THAN)
|
|
1008
|
+
return "lessThan";
|
|
1009
|
+
if (operator === Filter_Operator.GREATER_OR_EQUAL)
|
|
1010
|
+
return "greaterOrEqual";
|
|
1011
|
+
if (operator === Filter_Operator.LESS_OR_EQUAL)
|
|
1012
|
+
return "lessOrEqual";
|
|
1013
|
+
return undefined;
|
|
1014
|
+
}
|
|
1015
|
+
function unsupportedFilterError(message) {
|
|
1016
|
+
return invalidQueryError(message);
|
|
1017
|
+
}
|
|
1018
|
+
function unsupportedFormatError(message) {
|
|
1019
|
+
return invalidQueryError(message);
|
|
1020
|
+
}
|
|
1021
|
+
function invalidQueryError(message) {
|
|
1022
|
+
return {
|
|
1023
|
+
type: "INVALID_QUERY",
|
|
1024
|
+
message,
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
function invalidCriterionError() {
|
|
1028
|
+
return {
|
|
1029
|
+
type: "INVALID_QUERY",
|
|
1030
|
+
message: "QueryService.Read requires filters or include_all = true.",
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
const DEFAULT_QUEUE_LIMIT = 100;
|
|
1034
|
+
const DEFAULT_SUBSCRIPTION_LIMIT = 100;
|
|
1035
|
+
/**
|
|
1036
|
+
* Holds bounded request limits for QueryService.
|
|
1037
|
+
*/
|
|
1038
|
+
const QueryLimits = Object.freeze({
|
|
1039
|
+
idCount: 100,
|
|
1040
|
+
filterCount: 16,
|
|
1041
|
+
compositeCount: 8,
|
|
1042
|
+
orderCount: 8,
|
|
1043
|
+
resultCount: 1_000,
|
|
1044
|
+
});
|
|
1045
|
+
/**
|
|
1046
|
+
* Holds bounded request limits for SubscriptionService.
|
|
1047
|
+
*/
|
|
1048
|
+
const SubscriptionLimits = Object.freeze({
|
|
1049
|
+
idCount: QueryLimits.idCount,
|
|
1050
|
+
compositeCount: 8,
|
|
1051
|
+
filterCount: 16,
|
|
1052
|
+
compositeDepth: 8,
|
|
1053
|
+
pathComponentCount: 16,
|
|
1054
|
+
pathSegmentLength: 128,
|
|
1055
|
+
});
|
|
1056
|
+
function positiveInteger(value) {
|
|
1057
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 1;
|
|
1058
|
+
}
|
|
1059
|
+
function subscriptionLimit(value) {
|
|
1060
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
1061
|
+
throw new TypeError("SpineServices subscriptionLimit must be a positive safe integer.");
|
|
1070
1062
|
}
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1063
|
+
return value;
|
|
1064
|
+
}
|
|
1065
|
+
function createSubscriptionRecord(input) {
|
|
1066
|
+
return {
|
|
1067
|
+
id: input.id,
|
|
1068
|
+
subscription: clone(SubscriptionSchema, input.subscription),
|
|
1069
|
+
tenantId: input.tenantId === undefined ? undefined : clone(TenantIdSchema, input.tenantId),
|
|
1070
|
+
delivery: new SubscriptionDelivery(input.queueLimit),
|
|
1071
|
+
...input.shape,
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
function createSubscriptionRemoval() {
|
|
1075
|
+
let reject;
|
|
1076
|
+
let resolve;
|
|
1077
|
+
const settled = new Promise((settle, fail) => {
|
|
1078
|
+
reject = fail;
|
|
1079
|
+
resolve = () => {
|
|
1080
|
+
settle();
|
|
1081
|
+
};
|
|
1082
|
+
});
|
|
1083
|
+
return { reject, resolve, settled };
|
|
1084
|
+
}
|
|
1085
|
+
function uniqueContexts(contexts) {
|
|
1086
|
+
return [...new Set(contexts)];
|
|
1087
|
+
}
|
|
1088
|
+
function validateTopic(topic) {
|
|
1089
|
+
if (topic.id?.value === undefined || topic.id.value.trim().length === 0) {
|
|
1090
|
+
throw new ConnectError("Subscription topic ID is required.", Code.InvalidArgument);
|
|
1081
1091
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
default:
|
|
1092
|
+
if (topic.context === undefined) {
|
|
1093
|
+
throw new ConnectError("Subscription topic context is required.", Code.InvalidArgument);
|
|
1094
|
+
}
|
|
1095
|
+
if (topic.target?.criterion.case === undefined) {
|
|
1096
|
+
throw new ConnectError("Subscription topic criterion is required.", Code.InvalidArgument);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* Validates a topic and prepares complete state or event delivery.
|
|
1101
|
+
*/
|
|
1102
|
+
function createSubscriptionShape(topic, route) {
|
|
1103
|
+
const target = topic.target;
|
|
1104
|
+
if (target === undefined) {
|
|
1105
|
+
throw new ConnectError("Subscription topic target is required.", Code.InvalidArgument);
|
|
1106
|
+
}
|
|
1107
|
+
if (route.kind === "event") {
|
|
1108
|
+
validateEventSubscriptionTopic(target);
|
|
1109
|
+
return { kind: "event", route };
|
|
1110
|
+
}
|
|
1111
|
+
switch (target.criterion.case) {
|
|
1112
|
+
case "includeAll":
|
|
1113
|
+
if (!target.criterion.value) {
|
|
1105
1114
|
throw new ConnectError("SubscriptionService.Subscribe requires filters or include_all = true.", Code.InvalidArgument);
|
|
1106
|
-
|
|
1115
|
+
}
|
|
1116
|
+
return { kind: "state", route, matcher: { match: () => "state" } };
|
|
1117
|
+
case "filters":
|
|
1118
|
+
return {
|
|
1119
|
+
kind: "state",
|
|
1120
|
+
route,
|
|
1121
|
+
matcher: createFilteredSubscriptionMatcher(target.criterion.value, route),
|
|
1122
|
+
};
|
|
1123
|
+
default:
|
|
1124
|
+
throw new ConnectError("SubscriptionService.Subscribe requires filters or include_all = true.", Code.InvalidArgument);
|
|
1107
1125
|
}
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
}
|
|
1117
|
-
return;
|
|
1118
|
-
case "filters":
|
|
1119
|
-
throw new ConnectError("SubscriptionService.Subscribe event topics support only include_all in this runtime slice.", Code.InvalidArgument);
|
|
1120
|
-
default:
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Checks the supported event topic criteria.
|
|
1129
|
+
*/
|
|
1130
|
+
function validateEventSubscriptionTopic(target) {
|
|
1131
|
+
switch (target.criterion.case) {
|
|
1132
|
+
case "includeAll":
|
|
1133
|
+
if (!target.criterion.value) {
|
|
1121
1134
|
throw new ConnectError("SubscriptionService.Subscribe requires filters or include_all = true.", Code.InvalidArgument);
|
|
1122
|
-
|
|
1135
|
+
}
|
|
1136
|
+
return;
|
|
1137
|
+
case "filters":
|
|
1138
|
+
throw new ConnectError("SubscriptionService.Subscribe event topics support only include_all in this runtime slice.", Code.InvalidArgument);
|
|
1139
|
+
default:
|
|
1140
|
+
throw new ConnectError("SubscriptionService.Subscribe requires filters or include_all = true.", Code.InvalidArgument);
|
|
1123
1141
|
}
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
return undefined;
|
|
1143
|
-
}
|
|
1144
|
-
if (statePredicate(update.state)) {
|
|
1145
|
-
return "state";
|
|
1146
|
-
}
|
|
1147
|
-
if (update.previousState !== undefined && statePredicate(update.previousState)) {
|
|
1148
|
-
return "noLongerMatching";
|
|
1149
|
-
}
|
|
1142
|
+
}
|
|
1143
|
+
/**
|
|
1144
|
+
* Matches complete state updates against a topic's ID and field filters.
|
|
1145
|
+
*/
|
|
1146
|
+
function createFilteredSubscriptionMatcher(filters, route) {
|
|
1147
|
+
ServiceValues.validateSubscriptionFilters(filters);
|
|
1148
|
+
const idValues = filters.idFilter?.id.map((id) => decodeSubscriptionIdValue(id, route));
|
|
1149
|
+
const statePredicate = createSubscriptionPredicate(filters.filter, route);
|
|
1150
|
+
return {
|
|
1151
|
+
/**
|
|
1152
|
+
* Matches one state update against the topic's ID and field filters.
|
|
1153
|
+
*
|
|
1154
|
+
* @param update State update to inspect.
|
|
1155
|
+
* @returns Delivery kind, if the update changes a matching state.
|
|
1156
|
+
*/
|
|
1157
|
+
match(update) {
|
|
1158
|
+
if (idValues !== undefined &&
|
|
1159
|
+
!idValues.some((id) => valuesEqual(update.id, id, route.idField.message))) {
|
|
1150
1160
|
return undefined;
|
|
1151
|
-
},
|
|
1152
|
-
};
|
|
1153
|
-
}
|
|
1154
|
-
function createSubscriptionPredicate(filters, route) {
|
|
1155
|
-
const predicates = filters.map((filter) => createCompositePredicate(filter, route));
|
|
1156
|
-
return (state) => predicates.every((predicate) => predicate(state));
|
|
1157
|
-
}
|
|
1158
|
-
function validateSubscriptionFilterTree(filters) {
|
|
1159
|
-
if (filters.length > SubscriptionLimits.compositeCount) {
|
|
1160
|
-
throw new ConnectError("SubscriptionService.Subscribe may contain at most 8 composite filters.", Code.InvalidArgument);
|
|
1161
|
-
}
|
|
1162
|
-
const stack = [];
|
|
1163
|
-
for (const filter of filters) {
|
|
1164
|
-
stack.push({ filter, depth: 0 });
|
|
1165
|
-
}
|
|
1166
|
-
const counts = {
|
|
1167
|
-
compositeCount: 0,
|
|
1168
|
-
simpleCount: 0,
|
|
1169
|
-
};
|
|
1170
|
-
while (stack.length > 0) {
|
|
1171
|
-
const current = stack.pop();
|
|
1172
|
-
if (current === undefined) {
|
|
1173
|
-
continue;
|
|
1174
1161
|
}
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1162
|
+
if (statePredicate(update.state))
|
|
1163
|
+
return "state";
|
|
1164
|
+
if (update.previousState !== undefined && statePredicate(update.previousState))
|
|
1165
|
+
return "noLongerMatching";
|
|
1166
|
+
return undefined;
|
|
1167
|
+
},
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
function createSubscriptionPredicate(filters, route) {
|
|
1171
|
+
const predicates = filters.map((filter) => createCompositePredicate(filter, route));
|
|
1172
|
+
return (state) => predicates.every((predicate) => predicate(state));
|
|
1173
|
+
}
|
|
1174
|
+
function validateSubscriptionFilterTree(filters) {
|
|
1175
|
+
if (filters.length > SubscriptionLimits.compositeCount) {
|
|
1176
|
+
throw new ConnectError("SubscriptionService.Subscribe may contain at most 8 composite filters.", Code.InvalidArgument);
|
|
1177
|
+
}
|
|
1178
|
+
const stack = [];
|
|
1179
|
+
for (const filter of filters) {
|
|
1180
|
+
stack.push({ filter, depth: 0 });
|
|
1181
|
+
}
|
|
1182
|
+
const counts = {
|
|
1183
|
+
compositeCount: 0,
|
|
1184
|
+
simpleCount: 0,
|
|
1185
|
+
};
|
|
1186
|
+
while (stack.length > 0) {
|
|
1187
|
+
const current = stack.pop();
|
|
1188
|
+
if (current === undefined) {
|
|
1189
|
+
continue;
|
|
1190
|
+
}
|
|
1191
|
+
validateSubscriptionFilterNode(current.filter, current.depth, counts);
|
|
1192
|
+
enqueueSubscriptionFilterChildren(current.filter, current.depth, stack);
|
|
1178
1193
|
}
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
switch (filter.operator) {
|
|
1184
|
-
case CompositeFilter_CompositeOperator.ALL:
|
|
1185
|
-
case CompositeFilter_CompositeOperator.EITHER:
|
|
1186
|
-
break;
|
|
1187
|
-
default:
|
|
1188
|
-
throw new ConnectError("SubscriptionService.Subscribe supports only ALL or EITHER composite filters.", Code.InvalidArgument);
|
|
1189
|
-
}
|
|
1190
|
-
if (depth === SubscriptionLimits.compositeDepth && filter.compositeFilter.length > 0) {
|
|
1191
|
-
throw new ConnectError("SubscriptionService.Subscribe composite filters may nest at most 8 levels.", Code.InvalidArgument);
|
|
1192
|
-
}
|
|
1193
|
-
counts.compositeCount += 1;
|
|
1194
|
-
if (counts.compositeCount > SubscriptionLimits.compositeCount) {
|
|
1195
|
-
throw new ConnectError("SubscriptionService.Subscribe may contain at most 8 composite filters.", Code.InvalidArgument);
|
|
1196
|
-
}
|
|
1197
|
-
counts.simpleCount += filter.filter.length;
|
|
1198
|
-
if (counts.simpleCount > SubscriptionLimits.filterCount) {
|
|
1199
|
-
throw new ConnectError("SubscriptionService.Subscribe may contain at most 16 simple field filters.", Code.InvalidArgument);
|
|
1200
|
-
}
|
|
1201
|
-
if ((filter.compositeFilter.length > 1 || depth + 1 < SubscriptionLimits.compositeDepth) &&
|
|
1202
|
-
counts.compositeCount + filter.compositeFilter.length > SubscriptionLimits.compositeCount) {
|
|
1203
|
-
throw new ConnectError("SubscriptionService.Subscribe may contain at most 8 composite filters.", Code.InvalidArgument);
|
|
1204
|
-
}
|
|
1194
|
+
}
|
|
1195
|
+
function validateSubscriptionFilterNode(filter, depth, counts) {
|
|
1196
|
+
if (depth > SubscriptionLimits.compositeDepth) {
|
|
1197
|
+
throw new ConnectError("SubscriptionService.Subscribe composite filters may nest at most 8 levels.", Code.InvalidArgument);
|
|
1205
1198
|
}
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
}
|
|
1199
|
+
switch (filter.operator) {
|
|
1200
|
+
case CompositeFilter_CompositeOperator.ALL:
|
|
1201
|
+
case CompositeFilter_CompositeOperator.EITHER:
|
|
1202
|
+
break;
|
|
1203
|
+
default:
|
|
1204
|
+
throw new ConnectError("SubscriptionService.Subscribe supports only ALL or EITHER composite filters.", Code.InvalidArgument);
|
|
1213
1205
|
}
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
...filter.filter.map((simple) => createSimplePredicate(simple, route)),
|
|
1217
|
-
...filter.compositeFilter.map((nested) => createCompositePredicate(nested, route)),
|
|
1218
|
-
];
|
|
1219
|
-
if (children.length === 0) {
|
|
1220
|
-
return () => true;
|
|
1221
|
-
}
|
|
1222
|
-
return filter.operator === CompositeFilter_CompositeOperator.ALL
|
|
1223
|
-
? (state) => children.every((predicate) => predicate(state))
|
|
1224
|
-
: (state) => children.some((predicate) => predicate(state));
|
|
1206
|
+
if (depth === SubscriptionLimits.compositeDepth && filter.compositeFilter.length > 0) {
|
|
1207
|
+
throw new ConnectError("SubscriptionService.Subscribe composite filters may nest at most 8 levels.", Code.InvalidArgument);
|
|
1225
1208
|
}
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
}
|
|
1230
|
-
if (filter.value === undefined) {
|
|
1231
|
-
throw new ConnectError("SubscriptionService.Subscribe field filter value is required.", Code.InvalidArgument);
|
|
1232
|
-
}
|
|
1233
|
-
validateSubscriptionPath(filter.fieldPath?.fieldName ?? [], "field filter");
|
|
1234
|
-
const resolved = resolveMessagePath(route.schema, filter.fieldPath?.fieldName ?? [], "field filter");
|
|
1235
|
-
const expected = decodeFieldFilterValue(filter.value, resolved.leafSchema);
|
|
1236
|
-
return (state) => valuesEqual(readPathValue(state, resolved.localPath), expected, resolved.leafSchema);
|
|
1209
|
+
counts.compositeCount += 1;
|
|
1210
|
+
if (counts.compositeCount > SubscriptionLimits.compositeCount) {
|
|
1211
|
+
throw new ConnectError("SubscriptionService.Subscribe may contain at most 8 composite filters.", Code.InvalidArgument);
|
|
1237
1212
|
}
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
return undefined;
|
|
1242
|
-
}
|
|
1243
|
-
if (paths.length > SubscriptionLimits.maskPathCount) {
|
|
1244
|
-
throw new ConnectError("SubscriptionService.Subscribe field_mask may contain at most 32 paths.", Code.InvalidArgument);
|
|
1245
|
-
}
|
|
1246
|
-
if (paths.some((path) => path.length > SubscriptionLimits.maskPathLength)) {
|
|
1247
|
-
throw new ConnectError("SubscriptionService.Subscribe field_mask paths may contain at most 128 characters.", Code.InvalidArgument);
|
|
1248
|
-
}
|
|
1249
|
-
return paths.map((path) => {
|
|
1250
|
-
const segments = path.split(".");
|
|
1251
|
-
validateSubscriptionPath(segments, "field_mask");
|
|
1252
|
-
return resolveMessagePath(route.schema, segments, "field_mask").localPath.join(".");
|
|
1253
|
-
});
|
|
1213
|
+
counts.simpleCount += filter.filter.length;
|
|
1214
|
+
if (counts.simpleCount > SubscriptionLimits.filterCount) {
|
|
1215
|
+
throw new ConnectError("SubscriptionService.Subscribe may contain at most 16 simple field filters.", Code.InvalidArgument);
|
|
1254
1216
|
}
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
}
|
|
1259
|
-
if (fieldPath.length > SubscriptionLimits.pathComponentCount) {
|
|
1260
|
-
throw new ConnectError(`SubscriptionService.Subscribe ${label} path may contain at most 16 components.`, Code.InvalidArgument);
|
|
1261
|
-
}
|
|
1262
|
-
if (fieldPath.some((field) => field.length > SubscriptionLimits.pathSegmentLength)) {
|
|
1263
|
-
throw new ConnectError(`SubscriptionService.Subscribe ${label} path components may contain at most 128 characters.`, Code.InvalidArgument);
|
|
1264
|
-
}
|
|
1217
|
+
if ((filter.compositeFilter.length > 1 || depth + 1 < SubscriptionLimits.compositeDepth) &&
|
|
1218
|
+
counts.compositeCount + filter.compositeFilter.length > SubscriptionLimits.compositeCount) {
|
|
1219
|
+
throw new ConnectError("SubscriptionService.Subscribe may contain at most 8 composite filters.", Code.InvalidArgument);
|
|
1265
1220
|
}
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
return decodeAnyValue(value);
|
|
1272
|
-
}
|
|
1273
|
-
const decoded = AnyMessages.unpack(value, route.idField.message);
|
|
1274
|
-
if (decoded === undefined) {
|
|
1275
|
-
throw new ConnectError(`SubscriptionService.Subscribe id_filter values must pack ${route.idField.message.typeName}.`, Code.InvalidArgument);
|
|
1221
|
+
}
|
|
1222
|
+
function enqueueSubscriptionFilterChildren(filter, depth, stack) {
|
|
1223
|
+
for (const nested of filter.compositeFilter) {
|
|
1224
|
+
if (depth + 1 > SubscriptionLimits.compositeDepth) {
|
|
1225
|
+
throw new ConnectError("SubscriptionService.Subscribe composite filters may nest at most 8 levels.", Code.InvalidArgument);
|
|
1276
1226
|
}
|
|
1277
|
-
|
|
1227
|
+
stack.push({ filter: nested, depth: depth + 1 });
|
|
1278
1228
|
}
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1229
|
+
}
|
|
1230
|
+
function createCompositePredicate(filter, route) {
|
|
1231
|
+
const children = [
|
|
1232
|
+
...filter.filter.map((simple) => createSimplePredicate(simple, route)),
|
|
1233
|
+
...filter.compositeFilter.map((nested) => createCompositePredicate(nested, route)),
|
|
1234
|
+
];
|
|
1235
|
+
if (children.length === 0) {
|
|
1236
|
+
return () => true;
|
|
1237
|
+
}
|
|
1238
|
+
return filter.operator === CompositeFilter_CompositeOperator.ALL
|
|
1239
|
+
? (state) => children.every((predicate) => predicate(state))
|
|
1240
|
+
: (state) => children.some((predicate) => predicate(state));
|
|
1241
|
+
}
|
|
1242
|
+
function createSimplePredicate(filter, route) {
|
|
1243
|
+
if (filter.operator !== Filter_Operator.EQUAL) {
|
|
1244
|
+
throw new ConnectError("SubscriptionService.Subscribe supports only EQUAL field filters.", Code.InvalidArgument);
|
|
1288
1245
|
}
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
return undefined;
|
|
1292
|
-
}
|
|
1293
|
-
if (schema !== undefined) {
|
|
1294
|
-
const decoded = AnyMessages.unpack(value, schema);
|
|
1295
|
-
if (decoded !== undefined) {
|
|
1296
|
-
return decoded;
|
|
1297
|
-
}
|
|
1298
|
-
}
|
|
1299
|
-
for (const decoder of VALUE_DECODERS) {
|
|
1300
|
-
const decoded = decoder(value);
|
|
1301
|
-
if (decoded !== undefined) {
|
|
1302
|
-
return decoded;
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
|
-
return value;
|
|
1246
|
+
if (filter.value === undefined) {
|
|
1247
|
+
throw new ConnectError("SubscriptionService.Subscribe field filter value is required.", Code.InvalidArgument);
|
|
1306
1248
|
}
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
currentSchema = index === fieldPath.length - 1 ? undefined : field.message;
|
|
1319
|
-
if (currentSchema === undefined && index < fieldPath.length - 1) {
|
|
1320
|
-
throw new ConnectError(`SubscriptionService.Subscribe ${label} "${fieldPath.join(".")}" is not a message path.`, Code.InvalidArgument);
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1323
|
-
return Object.freeze({
|
|
1324
|
-
localPath,
|
|
1325
|
-
...(leafSchema === undefined ? {} : { leafSchema }),
|
|
1326
|
-
});
|
|
1249
|
+
validateSubscriptionPath(filter.fieldPath?.fieldName ?? [], "field filter");
|
|
1250
|
+
const resolved = resolveMessagePath(route.schema, filter.fieldPath?.fieldName ?? [], "field filter");
|
|
1251
|
+
const expected = decodeFieldFilterValue(filter.value, resolved.leafSchema);
|
|
1252
|
+
return (state) => valuesEqual(readPathValue(state, resolved.localPath), expected, resolved.leafSchema);
|
|
1253
|
+
}
|
|
1254
|
+
/**
|
|
1255
|
+
* Checks a state filter path before resolving its fields.
|
|
1256
|
+
*/
|
|
1257
|
+
function validateSubscriptionPath(fieldPath, label) {
|
|
1258
|
+
if (fieldPath.length === 0 || fieldPath.some((field) => field.trim().length === 0)) {
|
|
1259
|
+
throw new ConnectError(`SubscriptionService.Subscribe ${label} path is required.`, Code.InvalidArgument);
|
|
1327
1260
|
}
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
return fields.find((field) => field.name === name || field.localName === name);
|
|
1261
|
+
if (fieldPath.length > SubscriptionLimits.pathComponentCount) {
|
|
1262
|
+
throw new ConnectError(`SubscriptionService.Subscribe ${label} path may contain at most 16 components.`, Code.InvalidArgument);
|
|
1331
1263
|
}
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
for (const segment of path) {
|
|
1335
|
-
if (typeof current !== "object" || current === null) {
|
|
1336
|
-
return undefined;
|
|
1337
|
-
}
|
|
1338
|
-
current = Reflect.get(current, segment);
|
|
1339
|
-
}
|
|
1340
|
-
return current;
|
|
1264
|
+
if (fieldPath.some((field) => field.length > SubscriptionLimits.pathSegmentLength)) {
|
|
1265
|
+
throw new ConnectError(`SubscriptionService.Subscribe ${label} path components may contain at most 128 characters.`, Code.InvalidArgument);
|
|
1341
1266
|
}
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
if (isAny(actual) && isAny(expected)) {
|
|
1347
|
-
return bytesEqual(toBinary(AnySchema, actual), toBinary(AnySchema, expected));
|
|
1348
|
-
}
|
|
1349
|
-
if (actual instanceof Uint8Array && expected instanceof Uint8Array) {
|
|
1350
|
-
return bytesEqual(actual, expected);
|
|
1351
|
-
}
|
|
1352
|
-
return Object.is(actual, expected);
|
|
1267
|
+
}
|
|
1268
|
+
function decodeSubscriptionIdValue(value, route) {
|
|
1269
|
+
if (!isAny(value)) {
|
|
1270
|
+
throw new ConnectError("SubscriptionService.Subscribe id_filter values must be packed Any messages.", Code.InvalidArgument);
|
|
1353
1271
|
}
|
|
1354
|
-
|
|
1355
|
-
return
|
|
1272
|
+
if (route.idField.message === undefined) {
|
|
1273
|
+
return decodeAnyValue(value);
|
|
1356
1274
|
}
|
|
1357
|
-
|
|
1358
|
-
|
|
1275
|
+
const decoded = AnyMessages.unpack(value, route.idField.message);
|
|
1276
|
+
if (decoded === undefined) {
|
|
1277
|
+
throw new ConnectError(`SubscriptionService.Subscribe id_filter values must pack ${route.idField.message.typeName}.`, Code.InvalidArgument);
|
|
1359
1278
|
}
|
|
1360
|
-
|
|
1361
|
-
|
|
1279
|
+
return decoded;
|
|
1280
|
+
}
|
|
1281
|
+
function decodeFieldFilterValue(value, schema) {
|
|
1282
|
+
if (value === undefined || schema === undefined) {
|
|
1283
|
+
return decodeAnyValue(value);
|
|
1362
1284
|
}
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
version: result.version ?? create(VersionSchema),
|
|
1367
|
-
});
|
|
1285
|
+
const decoded = AnyMessages.unpack(value, schema);
|
|
1286
|
+
if (decoded === undefined) {
|
|
1287
|
+
throw new ConnectError(`SubscriptionService.Subscribe field filter value must pack ${schema.typeName}.`, Code.InvalidArgument);
|
|
1368
1288
|
}
|
|
1369
|
-
|
|
1370
|
-
|
|
1289
|
+
return decoded;
|
|
1290
|
+
}
|
|
1291
|
+
function decodeAnyValue(value, schema) {
|
|
1292
|
+
if (value === undefined) {
|
|
1293
|
+
return undefined;
|
|
1371
1294
|
}
|
|
1372
|
-
|
|
1373
|
-
|
|
1295
|
+
if (schema !== undefined) {
|
|
1296
|
+
const decoded = AnyMessages.unpack(value, schema);
|
|
1297
|
+
if (decoded !== undefined) {
|
|
1298
|
+
return decoded;
|
|
1299
|
+
}
|
|
1374
1300
|
}
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
case "pastMessage":
|
|
1380
|
-
return tenantValue(event.context.origin.value.actorContext?.tenantId);
|
|
1381
|
-
default:
|
|
1382
|
-
return undefined;
|
|
1301
|
+
for (const decoder of VALUE_DECODERS) {
|
|
1302
|
+
const decoded = decoder(value);
|
|
1303
|
+
if (decoded !== undefined) {
|
|
1304
|
+
return decoded;
|
|
1383
1305
|
}
|
|
1384
1306
|
}
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1307
|
+
return value;
|
|
1308
|
+
}
|
|
1309
|
+
/**
|
|
1310
|
+
* Resolves a state filter path to generated message fields.
|
|
1311
|
+
*/
|
|
1312
|
+
function resolveMessagePath(schema, fieldPath, label) {
|
|
1313
|
+
const localPath = [];
|
|
1314
|
+
let currentSchema = schema;
|
|
1315
|
+
let leafSchema;
|
|
1316
|
+
for (const [index, name] of fieldPath.entries()) {
|
|
1317
|
+
const field = findMessageField(currentSchema, name);
|
|
1318
|
+
if (field === undefined) {
|
|
1319
|
+
throw new ConnectError(`SubscriptionService.Subscribe ${label} "${fieldPath.join(".")}" is not a state field.`, Code.InvalidArgument);
|
|
1320
|
+
}
|
|
1321
|
+
localPath.push(field.localName);
|
|
1322
|
+
leafSchema = field.message;
|
|
1323
|
+
currentSchema = index === fieldPath.length - 1 ? undefined : field.message;
|
|
1324
|
+
if (currentSchema === undefined && index < fieldPath.length - 1) {
|
|
1325
|
+
throw new ConnectError(`SubscriptionService.Subscribe ${label} "${fieldPath.join(".")}" is not a message path.`, Code.InvalidArgument);
|
|
1326
|
+
}
|
|
1390
1327
|
}
|
|
1391
|
-
|
|
1392
|
-
|
|
1328
|
+
return Object.freeze({
|
|
1329
|
+
localPath,
|
|
1330
|
+
...(leafSchema === undefined ? {} : { leafSchema }),
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
function findMessageField(schema, name) {
|
|
1334
|
+
const fields = (schema?.fields ?? []);
|
|
1335
|
+
return fields.find((field) => field.name === name || field.localName === name);
|
|
1336
|
+
}
|
|
1337
|
+
function readPathValue(value, path) {
|
|
1338
|
+
let current = value;
|
|
1339
|
+
for (const segment of path) {
|
|
1340
|
+
if (typeof current !== "object" || current === null) {
|
|
1393
1341
|
return undefined;
|
|
1394
|
-
try {
|
|
1395
|
-
return TenantBoundary.from(tenantId).tenantId;
|
|
1396
1342
|
}
|
|
1397
|
-
|
|
1343
|
+
current = Reflect.get(current, segment);
|
|
1344
|
+
}
|
|
1345
|
+
return current;
|
|
1346
|
+
}
|
|
1347
|
+
function valuesEqual(actual, expected, schema) {
|
|
1348
|
+
if (schema !== undefined && isMessage(actual) && isMessage(expected)) {
|
|
1349
|
+
return bytesEqual(toBinary(schema, actual), toBinary(schema, expected));
|
|
1350
|
+
}
|
|
1351
|
+
if (isAny(actual) && isAny(expected)) {
|
|
1352
|
+
return bytesEqual(toBinary(AnySchema, actual), toBinary(AnySchema, expected));
|
|
1353
|
+
}
|
|
1354
|
+
if (actual instanceof Uint8Array && expected instanceof Uint8Array) {
|
|
1355
|
+
return bytesEqual(actual, expected);
|
|
1356
|
+
}
|
|
1357
|
+
return Object.is(actual, expected);
|
|
1358
|
+
}
|
|
1359
|
+
function isMessage(value) {
|
|
1360
|
+
return typeof value === "object" && value !== null && "$typeName" in value;
|
|
1361
|
+
}
|
|
1362
|
+
function isAny(value) {
|
|
1363
|
+
return isMessage(value) && value.$typeName === "google.protobuf.Any";
|
|
1364
|
+
}
|
|
1365
|
+
function bytesEqual(left, right) {
|
|
1366
|
+
return left.length === right.length && left.every((byte, index) => byte === right[index]);
|
|
1367
|
+
}
|
|
1368
|
+
/**
|
|
1369
|
+
* Packs a state snapshot and version for a public query result.
|
|
1370
|
+
*
|
|
1371
|
+
* @typeParam Schema Generated state schema.
|
|
1372
|
+
* @param schema Schema used to pack the state.
|
|
1373
|
+
* @param result Stored state and its version.
|
|
1374
|
+
* @returns Wire state/version pair.
|
|
1375
|
+
*/
|
|
1376
|
+
function packVersionedState(schema, result) {
|
|
1377
|
+
return create(EntityStateVersionSchema, {
|
|
1378
|
+
state: AnyMessages.pack(schema, result.state, { validate: false }),
|
|
1379
|
+
version: result.version ?? create(VersionSchema),
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1382
|
+
function commandTenant(command) {
|
|
1383
|
+
return tenantValue(command.context?.actorContext?.tenantId);
|
|
1384
|
+
}
|
|
1385
|
+
function topicTenant(topic) {
|
|
1386
|
+
return tenantValue(topic?.context?.tenantId);
|
|
1387
|
+
}
|
|
1388
|
+
function eventTenant(event) {
|
|
1389
|
+
switch (event.context?.origin.case) {
|
|
1390
|
+
case "importContext":
|
|
1391
|
+
return tenantValue(event.context.origin.value.tenantId);
|
|
1392
|
+
case "pastMessage":
|
|
1393
|
+
return tenantValue(event.context.origin.value.actorContext?.tenantId);
|
|
1394
|
+
default:
|
|
1398
1395
|
return undefined;
|
|
1399
|
-
}
|
|
1400
1396
|
}
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1397
|
+
}
|
|
1398
|
+
function eventTenantMatches(record, event) {
|
|
1399
|
+
const tenantId = eventTenant(event);
|
|
1400
|
+
return (record.tenantId === undefined ||
|
|
1401
|
+
(tenantId !== undefined &&
|
|
1402
|
+
TenantBoundary.from(tenantId).key === TenantBoundary.from(record.tenantId).key));
|
|
1403
|
+
}
|
|
1404
|
+
function tenantValue(tenantId) {
|
|
1405
|
+
if (tenantId === undefined)
|
|
1406
|
+
return undefined;
|
|
1407
|
+
try {
|
|
1408
|
+
return TenantBoundary.from(tenantId).tenantId;
|
|
1410
1409
|
}
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1410
|
+
catch {
|
|
1411
|
+
return undefined;
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
function stateRouteIdField(schema, idField) {
|
|
1415
|
+
const field = findMessageField(schema, idField?.name ?? "") ??
|
|
1416
|
+
findMessageField(schema, idField?.localName ?? "") ??
|
|
1417
|
+
schema.fields[0] ??
|
|
1418
|
+
undefined;
|
|
1419
|
+
if (field === undefined) {
|
|
1420
|
+
throw new ConnectError("Subscription target ID field is not available.", Code.InvalidArgument);
|
|
1421
|
+
}
|
|
1422
|
+
return field;
|
|
1423
|
+
}
|
|
1424
|
+
function tenantMismatch(multitenant, tenantId, subject) {
|
|
1425
|
+
if (multitenant) {
|
|
1420
1426
|
return tenantId === undefined
|
|
1421
|
-
?
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1427
|
+
? {
|
|
1428
|
+
type: "TENANT_REQUIRED",
|
|
1429
|
+
message: `Tenant is required for this ${subject}.`,
|
|
1430
|
+
}
|
|
1431
|
+
: undefined;
|
|
1426
1432
|
}
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
}
|
|
1437
|
-
if (error instanceof CommandValidationError) {
|
|
1438
|
-
return {
|
|
1439
|
-
type: "COMMAND_VALIDATION_ERROR",
|
|
1440
|
-
message: "Command payload validation failed.",
|
|
1441
|
-
details: AnyMessages.pack(ValidationErrorSchema, error.validationError, {
|
|
1442
|
-
validate: false,
|
|
1443
|
-
}),
|
|
1444
|
-
};
|
|
1445
|
-
}
|
|
1433
|
+
return tenantId === undefined
|
|
1434
|
+
? undefined
|
|
1435
|
+
: {
|
|
1436
|
+
type: "TENANT_INAPPLICABLE",
|
|
1437
|
+
message: `Tenant is not applicable for this ${subject}.`,
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
function commandPostError(error) {
|
|
1441
|
+
if (error instanceof TransitionValidationError) {
|
|
1446
1442
|
return {
|
|
1447
|
-
type:
|
|
1448
|
-
message:
|
|
1443
|
+
type: error.type,
|
|
1444
|
+
message: error.clientMessage,
|
|
1445
|
+
details: AnyMessages.pack(ValidationErrorSchema, error.validationError, {
|
|
1446
|
+
validate: false,
|
|
1447
|
+
}),
|
|
1449
1448
|
};
|
|
1450
1449
|
}
|
|
1451
|
-
|
|
1452
|
-
return
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
(value) => AnyMessages.unpack(value, Int64ValueSchema)?.value,
|
|
1460
|
-
(value) => AnyMessages.unpack(value, UInt64ValueSchema)?.value,
|
|
1461
|
-
(value) => AnyMessages.unpack(value, FloatValueSchema)?.value,
|
|
1462
|
-
(value) => AnyMessages.unpack(value, DoubleValueSchema)?.value,
|
|
1463
|
-
(value) => AnyMessages.unpack(value, BytesValueSchema)?.value,
|
|
1464
|
-
]);
|
|
1465
|
-
function createEntityUpdate(record, update) {
|
|
1466
|
-
const matcher = record.matcher;
|
|
1467
|
-
if (matcher === undefined)
|
|
1468
|
-
return undefined;
|
|
1469
|
-
const match = matcher.match(update);
|
|
1470
|
-
if (match === undefined) {
|
|
1471
|
-
return undefined;
|
|
1472
|
-
}
|
|
1473
|
-
return create(SubscriptionUpdateSchema, {
|
|
1474
|
-
subscription: clone(SubscriptionSchema, record.subscription),
|
|
1475
|
-
response: okResponse(),
|
|
1476
|
-
update: {
|
|
1477
|
-
case: "entityUpdates",
|
|
1478
|
-
value: create(EntityUpdatesSchema, {
|
|
1479
|
-
update: [
|
|
1480
|
-
create(EntityStateUpdateSchema, {
|
|
1481
|
-
id: packEntityId(record.route, update.id),
|
|
1482
|
-
kind: match === "state"
|
|
1483
|
-
? {
|
|
1484
|
-
case: "state",
|
|
1485
|
-
value: AnyMessages.pack(record.route.schema, maskedState(record, update.state), {
|
|
1486
|
-
validate: false,
|
|
1487
|
-
}),
|
|
1488
|
-
}
|
|
1489
|
-
: {
|
|
1490
|
-
case: "noLongerMatching",
|
|
1491
|
-
value: true,
|
|
1492
|
-
},
|
|
1493
|
-
}),
|
|
1494
|
-
],
|
|
1495
|
-
}),
|
|
1496
|
-
},
|
|
1497
|
-
});
|
|
1450
|
+
if (error instanceof CommandValidationError) {
|
|
1451
|
+
return {
|
|
1452
|
+
type: "COMMAND_VALIDATION_ERROR",
|
|
1453
|
+
message: "Command payload validation failed.",
|
|
1454
|
+
details: AnyMessages.pack(ValidationErrorSchema, error.validationError, {
|
|
1455
|
+
validate: false,
|
|
1456
|
+
}),
|
|
1457
|
+
};
|
|
1498
1458
|
}
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1459
|
+
return {
|
|
1460
|
+
type: "COMMAND_POST_ERROR",
|
|
1461
|
+
message: "Command post failed.",
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
function tenantOptions(tenantId) {
|
|
1465
|
+
return tenantId === undefined ? {} : { tenantId: clone(TenantIdSchema, tenantId) };
|
|
1466
|
+
}
|
|
1467
|
+
const VALUE_DECODERS = Object.freeze([
|
|
1468
|
+
(value) => AnyMessages.unpack(value, StringValueSchema)?.value,
|
|
1469
|
+
(value) => AnyMessages.unpack(value, BoolValueSchema)?.value,
|
|
1470
|
+
(value) => AnyMessages.unpack(value, Int32ValueSchema)?.value,
|
|
1471
|
+
(value) => AnyMessages.unpack(value, UInt32ValueSchema)?.value,
|
|
1472
|
+
(value) => AnyMessages.unpack(value, Int64ValueSchema)?.value,
|
|
1473
|
+
(value) => AnyMessages.unpack(value, UInt64ValueSchema)?.value,
|
|
1474
|
+
(value) => AnyMessages.unpack(value, FloatValueSchema)?.value,
|
|
1475
|
+
(value) => AnyMessages.unpack(value, DoubleValueSchema)?.value,
|
|
1476
|
+
(value) => AnyMessages.unpack(value, BytesValueSchema)?.value,
|
|
1477
|
+
]);
|
|
1478
|
+
/**
|
|
1479
|
+
* Builds a subscription update with a complete state or a removal signal.
|
|
1480
|
+
*/
|
|
1481
|
+
function createEntityUpdate(record, update) {
|
|
1482
|
+
const matcher = record.matcher;
|
|
1483
|
+
if (matcher === undefined)
|
|
1484
|
+
return undefined;
|
|
1485
|
+
const match = matcher.match(update);
|
|
1486
|
+
if (match === undefined)
|
|
1487
|
+
return undefined;
|
|
1488
|
+
const kind = match === "state"
|
|
1489
|
+
? {
|
|
1490
|
+
case: "state",
|
|
1491
|
+
value: AnyMessages.pack(record.route.schema, clone(record.route.schema, update.state), {
|
|
1492
|
+
validate: false,
|
|
1493
|
+
}),
|
|
1494
|
+
}
|
|
1495
|
+
: { case: "noLongerMatching", value: true };
|
|
1496
|
+
return create(SubscriptionUpdateSchema, {
|
|
1497
|
+
subscription: clone(SubscriptionSchema, record.subscription),
|
|
1498
|
+
response: okResponse(),
|
|
1499
|
+
update: {
|
|
1500
|
+
case: "entityUpdates",
|
|
1501
|
+
value: create(EntityUpdatesSchema, {
|
|
1502
|
+
update: [
|
|
1503
|
+
create(EntityStateUpdateSchema, {
|
|
1504
|
+
id: packEntityId(record.route, update.id),
|
|
1505
|
+
kind,
|
|
1506
|
+
}),
|
|
1507
|
+
],
|
|
1508
|
+
}),
|
|
1509
|
+
},
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
function createEventUpdate(record, event) {
|
|
1513
|
+
return create(SubscriptionUpdateSchema, {
|
|
1514
|
+
subscription: clone(SubscriptionSchema, record.subscription),
|
|
1515
|
+
response: okResponse(),
|
|
1516
|
+
update: {
|
|
1517
|
+
case: "eventUpdates",
|
|
1518
|
+
value: create(EventUpdatesSchema, {
|
|
1519
|
+
event: [cloneClientEvent(event)],
|
|
1520
|
+
}),
|
|
1521
|
+
},
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1524
|
+
function cloneClientEvent(event) {
|
|
1525
|
+
const clientEvent = clone(EventSchema, event);
|
|
1526
|
+
const rejection = clientEvent.context?.rejection;
|
|
1527
|
+
if (rejection !== undefined) {
|
|
1528
|
+
rejection.command = undefined;
|
|
1529
|
+
// Clear the legacy wire payload as part of client-side security redaction.
|
|
1530
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
1531
|
+
rejection.commandMessage = undefined;
|
|
1532
|
+
rejection.stacktrace = "";
|
|
1533
|
+
}
|
|
1534
|
+
return clientEvent;
|
|
1535
|
+
}
|
|
1536
|
+
function packString(value) {
|
|
1537
|
+
return AnyMessages.pack(StringValueSchema, create(StringValueSchema, { value }));
|
|
1538
|
+
}
|
|
1539
|
+
function packEntityId(route, id) {
|
|
1540
|
+
if (isAny(id)) {
|
|
1541
|
+
return clone(AnySchema, id);
|
|
1542
|
+
}
|
|
1543
|
+
if (route.idField.message !== undefined && isMessage(id)) {
|
|
1544
|
+
return AnyMessages.pack(route.idField.message, id, { validate: false });
|
|
1545
|
+
}
|
|
1546
|
+
if (id instanceof Uint8Array) {
|
|
1547
|
+
return AnyMessages.pack(BytesValueSchema, create(BytesValueSchema, { value: id }));
|
|
1548
|
+
}
|
|
1549
|
+
switch (typeof id) {
|
|
1550
|
+
case "string":
|
|
1551
|
+
return packString(id);
|
|
1552
|
+
case "boolean":
|
|
1553
|
+
return AnyMessages.pack(BoolValueSchema, create(BoolValueSchema, { value: id }));
|
|
1554
|
+
case "number":
|
|
1555
|
+
return AnyMessages.pack(DoubleValueSchema, create(DoubleValueSchema, { value: id }));
|
|
1556
|
+
case "bigint":
|
|
1557
|
+
return AnyMessages.pack(Int64ValueSchema, create(Int64ValueSchema, { value: id }));
|
|
1558
|
+
default:
|
|
1559
|
+
return undefined;
|
|
1510
1560
|
}
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
if (
|
|
1531
|
-
return
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
if (
|
|
1537
|
-
return
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1561
|
+
}
|
|
1562
|
+
/**
|
|
1563
|
+
* Groups non-public service contract, query, and subscription operations.
|
|
1564
|
+
*/
|
|
1565
|
+
const ServiceValues = Object.freeze({
|
|
1566
|
+
/**
|
|
1567
|
+
* Checks one composite query filter and its current traversal bounds.
|
|
1568
|
+
*
|
|
1569
|
+
* @param filter The composite filter being traversed.
|
|
1570
|
+
* @param depth The nesting level of this filter.
|
|
1571
|
+
* @param compositeCount The number of composites visited so far.
|
|
1572
|
+
* @param simpleCount The number of simple filters visited so far.
|
|
1573
|
+
* @param pendingCount The number of composites waiting to be checked.
|
|
1574
|
+
* @param route The registered state type and columns used to validate filters.
|
|
1575
|
+
* @returns The first invalid-query error, if the filter is invalid.
|
|
1576
|
+
*/
|
|
1577
|
+
validateCompositeNode(filter, depth, compositeCount, simpleCount, pendingCount, route) {
|
|
1578
|
+
if (filter.filter.length === 0 && filter.compositeFilter.length === 0)
|
|
1579
|
+
return invalidQueryError("QueryService.Read composite filter must not be empty.");
|
|
1580
|
+
if (compositeCount > QueryLimits.compositeCount || depth > 8)
|
|
1581
|
+
return invalidQueryError("QueryService.Read may contain at most 8 composite filters.");
|
|
1582
|
+
if (simpleCount > QueryLimits.filterCount)
|
|
1583
|
+
return invalidQueryError("QueryService.Read may contain at most 16 simple column filters.");
|
|
1584
|
+
if (compositeCount + pendingCount + filter.compositeFilter.length > QueryLimits.compositeCount)
|
|
1585
|
+
return invalidQueryError("QueryService.Read may contain at most 8 composite filters.");
|
|
1586
|
+
if (depth >= 8 && filter.compositeFilter.length > 0)
|
|
1587
|
+
return invalidQueryError("QueryService.Read may contain at most 8 composite filters.");
|
|
1588
|
+
if (filter.operator !== CompositeFilter_CompositeOperator.ALL &&
|
|
1589
|
+
filter.operator !== CompositeFilter_CompositeOperator.EITHER)
|
|
1590
|
+
return unsupportedFilterError("QueryService.Read composite operator must be ALL or EITHER.");
|
|
1591
|
+
return validateSimpleFilters(filter.filter, route);
|
|
1592
|
+
},
|
|
1593
|
+
/**
|
|
1594
|
+
* Checks that every wire ordering names a sortable registered column.
|
|
1595
|
+
*
|
|
1596
|
+
* @param orders The wire ordering clauses in request order.
|
|
1597
|
+
* @param route The registered columns available to this state type.
|
|
1598
|
+
* @returns The first unsupported-format error, if an ordering is invalid.
|
|
1599
|
+
*/
|
|
1600
|
+
validateReadOrder(orders, route) {
|
|
1601
|
+
for (const order of orders) {
|
|
1602
|
+
if (order.column.trim().length === 0) {
|
|
1603
|
+
return unsupportedFormatError("QueryService.Read order_by column is required.");
|
|
1604
|
+
}
|
|
1605
|
+
if (!route.allowedColumnNames.has(order.column)) {
|
|
1606
|
+
return unsupportedFormatError(`QueryService.Read order_by column "${order.column}" is not a declared column.`);
|
|
1607
|
+
}
|
|
1608
|
+
const field = route.columnFields.get(order.column);
|
|
1609
|
+
if (field === undefined ? order.column !== "version" : !supportsRange(field)) {
|
|
1610
|
+
return unsupportedFormatError(`QueryService.Read order_by column "${order.column}" is not orderable.`);
|
|
1611
|
+
}
|
|
1612
|
+
if (order.direction !== OrderBy_Direction.ASCENDING &&
|
|
1613
|
+
order.direction !== OrderBy_Direction.DESCENDING) {
|
|
1614
|
+
return unsupportedFormatError("QueryService.Read order_by direction must be ASCENDING or DESCENDING.");
|
|
1615
|
+
}
|
|
1550
1616
|
}
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1617
|
+
return undefined;
|
|
1618
|
+
},
|
|
1619
|
+
/**
|
|
1620
|
+
* Checks that a state topic has a usable ID or field filter.
|
|
1621
|
+
*
|
|
1622
|
+
* @param filters The ID and field criteria from the subscription topic.
|
|
1623
|
+
*/
|
|
1624
|
+
validateSubscriptionFilters(filters) {
|
|
1625
|
+
if (filters.idFilter?.id.length === 0) {
|
|
1626
|
+
throw new ConnectError("SubscriptionService.Subscribe id_filter requires at least one ID.", Code.InvalidArgument);
|
|
1627
|
+
}
|
|
1628
|
+
if (filters.idFilter !== undefined && filters.idFilter.id.length > SubscriptionLimits.idCount) {
|
|
1629
|
+
throw new ConnectError("SubscriptionService.Subscribe id_filter may contain at most 100 IDs.", Code.InvalidArgument);
|
|
1630
|
+
}
|
|
1631
|
+
if (filters.idFilter === undefined && filters.filter.length === 0) {
|
|
1632
|
+
throw new ConnectError("SubscriptionService.Subscribe requires an ID filter or field filter.", Code.InvalidArgument);
|
|
1633
|
+
}
|
|
1634
|
+
validateSubscriptionFilterTree(filters.filter);
|
|
1635
|
+
},
|
|
1636
|
+
commandPostError,
|
|
1637
|
+
commandTenant,
|
|
1638
|
+
createEntityUpdate,
|
|
1639
|
+
createEventUpdate,
|
|
1640
|
+
createReadPlan,
|
|
1641
|
+
createSubscriptionRecord,
|
|
1642
|
+
createSubscriptionRemoval,
|
|
1643
|
+
createSubscriptionShape,
|
|
1644
|
+
defaultQueueLimit: DEFAULT_QUEUE_LIMIT,
|
|
1645
|
+
defaultSubscriptionLimit: DEFAULT_SUBSCRIPTION_LIMIT,
|
|
1646
|
+
errorStatus,
|
|
1647
|
+
eventTenantMatches,
|
|
1648
|
+
okResponse,
|
|
1649
|
+
okStatus,
|
|
1650
|
+
positiveInteger,
|
|
1651
|
+
packVersionedState,
|
|
1652
|
+
queryErrorResponse,
|
|
1653
|
+
queryResultLimit: QueryLimits.resultCount,
|
|
1654
|
+
stateRouteIdField,
|
|
1655
|
+
subscriptionLimit,
|
|
1656
|
+
tenantMismatch,
|
|
1657
|
+
tenantOptions,
|
|
1658
|
+
tenantValue,
|
|
1659
|
+
topicTenant,
|
|
1660
|
+
uniqueContexts,
|
|
1661
|
+
validateReadQuery,
|
|
1662
|
+
validateTopic,
|
|
1663
|
+
});
|
|
1582
1664
|
//# sourceMappingURL=spine-services.js.map
|