@spine-event-engine/server 2.0.0-snapshot.2 → 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 +225 -518
- package/REFERENCE.md +498 -49
- package/dist/browser/browser-server.d.ts +95 -0
- package/dist/browser/browser-server.d.ts.map +1 -0
- package/dist/{server → browser}/browser-server.js +93 -7
- package/dist/browser/browser-server.js.map +1 -0
- package/dist/browser/durable-subscription-bindings.d.ts.map +1 -0
- package/dist/browser/durable-subscription-bindings.js.map +1 -0
- package/dist/browser/index.d.ts +12 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +24 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/browser/options.d.ts +183 -0
- package/dist/browser/options.d.ts.map +1 -0
- package/dist/browser/options.js +15 -0
- package/dist/browser/options.js.map +1 -0
- package/dist/bus/command-bus.d.ts +2 -0
- package/dist/bus/command-bus.d.ts.map +1 -1
- package/dist/bus/command-bus.js +36 -1
- package/dist/bus/command-bus.js.map +1 -1
- package/dist/bus/event-dispatcher-registry.d.ts +2 -2
- package/dist/bus/event-dispatcher-registry.js +3 -3
- package/dist/bus/event-dispatcher-registry.js.map +1 -1
- package/dist/context/bounded-context.d.ts +238 -83
- package/dist/context/bounded-context.d.ts.map +1 -1
- package/dist/context/bounded-context.js +1083 -286
- 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/entity-inbox.d.ts.map +1 -1
- package/dist/context/entity-inbox.js +24 -17
- package/dist/context/entity-inbox.js.map +1 -1
- package/dist/context/local-inbox-handoff.d.ts +7 -3
- package/dist/context/local-inbox-handoff.d.ts.map +1 -1
- package/dist/context/local-inbox-handoff.js +32 -26
- package/dist/context/local-inbox-handoff.js.map +1 -1
- package/dist/context/projection-handoff.d.ts.map +1 -1
- package/dist/context/projection-handoff.js +36 -31
- package/dist/context/projection-handoff.js.map +1 -1
- package/dist/context/tenant-index.d.ts.map +1 -1
- package/dist/context/tenant-index.js +72 -2
- package/dist/context/tenant-index.js.map +1 -1
- package/dist/delivery/delivery-ports.d.ts +14 -4
- package/dist/delivery/delivery-ports.d.ts.map +1 -1
- package/dist/delivery/delivery.d.ts +19 -21
- package/dist/delivery/delivery.d.ts.map +1 -1
- package/dist/delivery/delivery.js +541 -158
- package/dist/delivery/delivery.js.map +1 -1
- package/dist/delivery/inbox-storage.d.ts +12 -3
- package/dist/delivery/inbox-storage.d.ts.map +1 -1
- package/dist/delivery/inbox-storage.js +160 -67
- package/dist/delivery/inbox-storage.js.map +1 -1
- package/dist/delivery/inbox.d.ts +9 -0
- package/dist/delivery/inbox.d.ts.map +1 -1
- package/dist/delivery/inbox.js +11 -0
- package/dist/delivery/inbox.js.map +1 -1
- package/dist/delivery/sharded-work-registry.d.ts.map +1 -1
- package/dist/delivery/sharded-work-registry.js +2 -1
- package/dist/delivery/sharded-work-registry.js.map +1 -1
- package/dist/entity/entity-metadata.d.ts +14 -5
- package/dist/entity/entity-metadata.d.ts.map +1 -1
- package/dist/entity/entity-metadata.js +38 -2
- package/dist/entity/entity-metadata.js.map +1 -1
- package/dist/entity/entity-storage-descriptor.d.ts +25 -5
- 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-transaction.d.ts +46 -42
- package/dist/entity/entity-transaction.d.ts.map +1 -1
- package/dist/entity/entity-transaction.js +130 -35
- package/dist/entity/entity-transaction.js.map +1 -1
- package/dist/entity/entity.d.ts +243 -98
- package/dist/entity/entity.d.ts.map +1 -1
- package/dist/entity/entity.js +405 -226
- 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/handler/command-registration-readiness.d.ts +6 -6
- package/dist/handler/command-registration-readiness.d.ts.map +1 -1
- package/dist/handler/command-registration-readiness.js +4 -2
- package/dist/handler/command-registration-readiness.js.map +1 -1
- package/dist/handler/declared-rejections.d.ts +11 -0
- package/dist/handler/declared-rejections.d.ts.map +1 -0
- package/dist/handler/declared-rejections.js +34 -0
- package/dist/handler/declared-rejections.js.map +1 -0
- package/dist/handler/event-registration-readiness.d.ts +13 -56
- package/dist/handler/event-registration-readiness.d.ts.map +1 -1
- package/dist/handler/event-registration-readiness.js +23 -43
- package/dist/handler/event-registration-readiness.js.map +1 -1
- package/dist/handler/generated-handler-registry.d.ts +72 -33
- package/dist/handler/generated-handler-registry.d.ts.map +1 -1
- package/dist/handler/generated-handler-registry.js +421 -43
- package/dist/handler/generated-handler-registry.js.map +1 -1
- package/dist/handler/generated-registry-discovery.d.ts.map +1 -1
- package/dist/handler/generated-registry-discovery.js +2 -4
- package/dist/handler/generated-registry-discovery.js.map +1 -1
- package/dist/handler/handler-decorators.d.ts +91 -37
- package/dist/handler/handler-decorators.d.ts.map +1 -1
- package/dist/handler/handler-decorators.js +124 -31
- package/dist/handler/handler-decorators.js.map +1 -1
- package/dist/handler/handler-metadata.d.ts +105 -60
- package/dist/handler/handler-metadata.d.ts.map +1 -1
- package/dist/handler/handler-metadata.js +275 -57
- package/dist/handler/handler-metadata.js.map +1 -1
- package/dist/handler/registration-readiness-metadata.d.ts +4 -0
- package/dist/handler/registration-readiness-metadata.d.ts.map +1 -1
- package/dist/handler/registration-readiness-metadata.js +84 -2
- package/dist/handler/registration-readiness-metadata.js.map +1 -1
- package/dist/handler/standalone.d.ts +25 -0
- package/dist/handler/standalone.d.ts.map +1 -0
- package/dist/handler/standalone.js +38 -0
- package/dist/handler/standalone.js.map +1 -0
- package/dist/index.d.ts +9 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- 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/primitive-id.d.ts +12 -23
- package/dist/repository/primitive-id.d.ts.map +1 -1
- package/dist/repository/primitive-id.js +2 -27
- package/dist/repository/primitive-id.js.map +1 -1
- package/dist/repository/repository.d.ts +317 -87
- package/dist/repository/repository.d.ts.map +1 -1
- package/dist/repository/repository.js +3746 -856
- 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/runtime/runtime.js +1 -1
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/signal-metadata.d.ts +22 -47
- package/dist/runtime/signal-metadata.d.ts.map +1 -1
- package/dist/runtime/signal-metadata.js +34 -69
- package/dist/runtime/signal-metadata.js.map +1 -1
- package/dist/runtime/signal-publisher.d.ts +144 -0
- package/dist/runtime/signal-publisher.d.ts.map +1 -0
- package/dist/runtime/signal-publisher.js +248 -0
- package/dist/runtime/signal-publisher.js.map +1 -0
- package/dist/runtime/standalone-handler-runtime.d.ts +130 -0
- package/dist/runtime/standalone-handler-runtime.d.ts.map +1 -0
- package/dist/runtime/standalone-handler-runtime.js +382 -0
- package/dist/runtime/standalone-handler-runtime.js.map +1 -0
- package/dist/server/environment-attachment.d.ts.map +1 -1
- package/dist/server/environment-attachment.js +2 -1
- package/dist/server/environment-attachment.js.map +1 -1
- package/dist/server/environment-delivery-worker.d.ts.map +1 -1
- package/dist/server/environment-delivery-worker.js +21 -12
- package/dist/server/environment-delivery-worker.js.map +1 -1
- package/dist/server/managed-server-application.d.ts +4 -1
- package/dist/server/managed-server-application.d.ts.map +1 -1
- package/dist/server/managed-server-application.js +2 -2
- package/dist/server/managed-server-application.js.map +1 -1
- package/dist/server/node-coordinator.js +2 -2
- package/dist/server/node-coordinator.js.map +1 -1
- package/dist/server/server-log.d.ts.map +1 -1
- package/dist/server/server-log.js +4 -0
- package/dist/server/server-log.js.map +1 -1
- package/dist/server/server.d.ts +9 -177
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +428 -178
- package/dist/server/server.js.map +1 -1
- package/dist/services/query-reader.d.ts +19 -0
- package/dist/services/query-reader.d.ts.map +1 -0
- package/dist/services/query-reader.js +43 -0
- package/dist/services/query-reader.js.map +1 -0
- 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 +1047 -969
- package/dist/services/spine-services.js.map +1 -1
- package/dist/spi/delivery.d.ts +5 -0
- package/dist/spi/delivery.d.ts.map +1 -0
- package/dist/spi/delivery.js +18 -0
- package/dist/spi/delivery.js.map +1 -0
- package/dist/spi/handler-registry.d.ts +8 -0
- package/dist/spi/handler-registry.d.ts.map +1 -0
- package/dist/spi/handler-registry.js +15 -0
- package/dist/spi/handler-registry.js.map +1 -0
- package/dist/stand/stand.d.ts +73 -1
- package/dist/stand/stand.d.ts.map +1 -1
- package/dist/stand/stand.js +221 -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 +5 -12
- package/dist/stand/subscription-observer.js.map +1 -1
- package/dist/stand/subscription-registry.js +1 -1
- package/dist/stand/subscription-registry.js.map +1 -1
- package/dist/testing/index.d.ts +25 -3
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +23 -3
- package/dist/testing/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +33 -23
- package/dist/handler/build-time-handler-analyzer.d.ts +0 -150
- package/dist/handler/build-time-handler-analyzer.d.ts.map +0 -1
- package/dist/handler/build-time-handler-analyzer.js +0 -1101
- package/dist/handler/build-time-handler-analyzer.js.map +0 -1
- package/dist/handler/generated-registry-writer.d.ts +0 -58
- package/dist/handler/generated-registry-writer.d.ts.map +0 -1
- package/dist/handler/generated-registry-writer.js +0 -458
- package/dist/handler/generated-registry-writer.js.map +0 -1
- package/dist/handler/handler-codegen.d.ts +0 -48
- package/dist/handler/handler-codegen.d.ts.map +0 -1
- package/dist/handler/handler-codegen.js +0 -214
- package/dist/handler/handler-codegen.js.map +0 -1
- package/dist/server/browser-server.d.ts +0 -49
- package/dist/server/browser-server.d.ts.map +0 -1
- package/dist/server/browser-server.js.map +0 -1
- package/dist/server/durable-subscription-bindings.d.ts.map +0 -1
- package/dist/server/durable-subscription-bindings.js.map +0 -1
- /package/dist/{server → browser}/durable-subscription-bindings.d.ts +0 -0
- /package/dist/{server → browser}/durable-subscription-bindings.js +0 -0
|
@@ -26,11 +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
|
+
import { TenantBoundary } from "@spine-event-engine/storage/provider";
|
|
30
31
|
import { boundedContextAccess } from "../context/bounded-context.js";
|
|
32
|
+
import { EffectiveTenants } from "../context/effective-tenant.js";
|
|
31
33
|
import { CommandValidationError } from "../bus/command-errors.js";
|
|
32
34
|
import { TransitionValidationError } from "../repository/command-errors.js";
|
|
33
35
|
import {} from "../stand/stand.js";
|
|
36
|
+
import { QueryReader } from "./query-reader.js";
|
|
37
|
+
import { RegisteredTargets } from "./registered-targets.js";
|
|
34
38
|
import { emitServerWarning } from "../server/server-log.js";
|
|
35
39
|
import { managedChildSubscriptionAccess } from "../server/managed-child-subscription.js";
|
|
36
40
|
import { InMemorySubscriptionRegistry, } from "../stand/subscription-registry.js";
|
|
@@ -40,16 +44,19 @@ const serviceInstances = new WeakSet();
|
|
|
40
44
|
* Small route registrar for the first public Spine gRPC service slice.
|
|
41
45
|
*
|
|
42
46
|
* `QueryService.Read` supports ID filters for any registered state route,
|
|
43
|
-
* projection-state `include_all = true` reads,
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
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.
|
|
53
60
|
*
|
|
54
61
|
* `SubscriptionService.Subscribe` accepts known state targets with
|
|
55
62
|
* `include_all` or validated ID/field filters and known event targets exposed
|
|
@@ -59,8 +66,8 @@ const serviceInstances = new WeakSet();
|
|
|
59
66
|
* subscriptions attach to `Stand`, while
|
|
60
67
|
* event subscriptions attach to a framework-internal `EventBus` listener.
|
|
61
68
|
* Filtered state topics deliver matching states, emit `no_longer_matching`
|
|
62
|
-
* when previous state matched and new state does not, and
|
|
63
|
-
*
|
|
69
|
+
* when previous state matched and new state does not, and deliver complete states.
|
|
70
|
+
* Event topics stream wire-level `event_updates`
|
|
64
71
|
* containing cloned framework `Event` envelopes; rejection updates omit
|
|
65
72
|
* rejected-command payload forms and stack trace from their client-facing
|
|
66
73
|
* context.
|
|
@@ -91,31 +98,19 @@ export class SpineServices {
|
|
|
91
98
|
this.#contexts = Object.freeze([...options.contexts]);
|
|
92
99
|
this.#queueLimit = ServiceValues.positiveInteger(options.queueLimit ?? ServiceValues.defaultQueueLimit);
|
|
93
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() {
|
|
94
108
|
for (const context of this.#contexts) {
|
|
95
109
|
for (const typeUrl of context.commandBus().acceptedCommandTypes()) {
|
|
96
110
|
if (!this.#commandRoutes.has(typeUrl)) {
|
|
97
111
|
this.#commandRoutes.set(typeUrl, { context, typeUrl });
|
|
98
112
|
}
|
|
99
113
|
}
|
|
100
|
-
for (const repository of context.registeredRepositories()) {
|
|
101
|
-
const schema = repository.stateSchema;
|
|
102
|
-
const typeUrl = TypeUrls.derive(schema);
|
|
103
|
-
const declaredColumns = repository.metadata.columns.map((column) => column.name);
|
|
104
|
-
const systemColumns = ["version", "archived", "deleted"];
|
|
105
|
-
this.#stateRoutes.set(typeUrl, {
|
|
106
|
-
allowedColumnNames: new Set([...declaredColumns, ...systemColumns]),
|
|
107
|
-
columnFields: new Map(repository.metadata.columns.flatMap((column) => {
|
|
108
|
-
const field = schema.fields.find((candidate) => candidate.name === column.name);
|
|
109
|
-
return field === undefined ? [] : [[column.name, field]];
|
|
110
|
-
})),
|
|
111
|
-
context,
|
|
112
|
-
entityFamily: repository.entityFamily,
|
|
113
|
-
idField: ServiceValues.stateRouteIdField(schema, repository.idField),
|
|
114
|
-
kind: "state",
|
|
115
|
-
schema,
|
|
116
|
-
typeUrl,
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
114
|
for (const typeUrl of context.eventBus().acceptedEventTypes()) {
|
|
120
115
|
if (!this.#eventRoutes.has(typeUrl)) {
|
|
121
116
|
this.#eventRoutes.set(typeUrl, { context, kind: "event", typeUrl });
|
|
@@ -123,6 +118,31 @@ export class SpineServices {
|
|
|
123
118
|
}
|
|
124
119
|
}
|
|
125
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
|
+
}
|
|
126
146
|
/**
|
|
127
147
|
* Registers CommandService, QueryService, and SubscriptionService routes.
|
|
128
148
|
*
|
|
@@ -143,6 +163,12 @@ export class SpineServices {
|
|
|
143
163
|
});
|
|
144
164
|
return router;
|
|
145
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
|
+
*/
|
|
146
172
|
async #post(command) {
|
|
147
173
|
const messageId = command.id && AnyMessages.pack(CommandIdSchema, command.id, { validate: false });
|
|
148
174
|
const typeUrl = command.message?.typeUrl;
|
|
@@ -181,6 +207,12 @@ export class SpineServices {
|
|
|
181
207
|
});
|
|
182
208
|
}
|
|
183
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
|
+
*/
|
|
184
216
|
async #read(query) {
|
|
185
217
|
const target = query.target;
|
|
186
218
|
const route = this.#readRoute(target);
|
|
@@ -191,13 +223,26 @@ export class SpineServices {
|
|
|
191
223
|
if (queryError !== undefined) {
|
|
192
224
|
return ServiceValues.queryErrorResponse(queryError.type, queryError.message);
|
|
193
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) {
|
|
194
237
|
const tenantId = ServiceValues.tenantValue(query.context?.tenantId);
|
|
195
238
|
const tenantError = ServiceValues.tenantMismatch(route.context.isMultitenant, tenantId, "query");
|
|
196
239
|
if (tenantError !== undefined) {
|
|
197
240
|
return ServiceValues.queryErrorResponse(tenantError.type, tenantError.message);
|
|
198
241
|
}
|
|
199
242
|
try {
|
|
200
|
-
|
|
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);
|
|
201
246
|
}
|
|
202
247
|
catch {
|
|
203
248
|
return ServiceValues.queryErrorResponse("QUERY_READ_ERROR", "Query read failed.");
|
|
@@ -207,13 +252,7 @@ export class SpineServices {
|
|
|
207
252
|
return target === undefined ? undefined : this.#stateRoutes.get(target.type);
|
|
208
253
|
}
|
|
209
254
|
async #query(route, plan, tenantId) {
|
|
210
|
-
const
|
|
211
|
-
...plan,
|
|
212
|
-
candidateLimit: ServiceValues.queryResultLimit,
|
|
213
|
-
};
|
|
214
|
-
const results = await route.context
|
|
215
|
-
.stand()
|
|
216
|
-
.queryPlanVersioned(route.schema, boundedPlan, ServiceValues.tenantOptions(tenantId));
|
|
255
|
+
const results = await QueryReader.read(route.context.stand(), route.schema, plan, tenantId, ServiceValues.queryResultLimit);
|
|
217
256
|
return create(QueryResponseSchema, {
|
|
218
257
|
response: ServiceValues.okResponse(),
|
|
219
258
|
message: results.map((result) => ServiceValues.packVersionedState(route.schema, result)),
|
|
@@ -501,12 +540,23 @@ export class SpineServices {
|
|
|
501
540
|
* @internal
|
|
502
541
|
*/
|
|
503
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
|
+
*/
|
|
504
548
|
clearLogger(services) {
|
|
505
549
|
if (!serviceInstances.has(services)) {
|
|
506
550
|
throw new TypeError("SpineServices logger requires a SpineServices instance.");
|
|
507
551
|
}
|
|
508
552
|
serviceLoggers.delete(services);
|
|
509
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
|
+
*/
|
|
510
560
|
installLogger(services, logger) {
|
|
511
561
|
if (!serviceInstances.has(services)) {
|
|
512
562
|
throw new TypeError("SpineServices logger requires a SpineServices instance.");
|
|
@@ -514,21 +564,44 @@ export const spineServicesAccess = Object.freeze({
|
|
|
514
564
|
serviceLoggers.set(services, logger);
|
|
515
565
|
},
|
|
516
566
|
});
|
|
567
|
+
/**
|
|
568
|
+
* Buffers subscription updates and wakes active stream readers.
|
|
569
|
+
*/
|
|
517
570
|
class SubscriptionDelivery {
|
|
518
571
|
#queue = [];
|
|
519
572
|
#waiters = [];
|
|
520
573
|
#queueLimit;
|
|
521
574
|
#subscription;
|
|
522
575
|
#closed = false;
|
|
576
|
+
/**
|
|
577
|
+
* Sets the bounded update queue capacity.
|
|
578
|
+
*
|
|
579
|
+
* @param queueLimit Maximum pending updates before closure.
|
|
580
|
+
*/
|
|
523
581
|
constructor(queueLimit) {
|
|
524
582
|
this.#queueLimit = queueLimit;
|
|
525
583
|
}
|
|
584
|
+
/**
|
|
585
|
+
* Checks whether a source attachment is active.
|
|
586
|
+
*
|
|
587
|
+
* @returns `true` after an attachment is installed.
|
|
588
|
+
*/
|
|
526
589
|
get active() {
|
|
527
590
|
return this.#subscription !== undefined;
|
|
528
591
|
}
|
|
592
|
+
/**
|
|
593
|
+
* Checks whether this delivery queue has closed.
|
|
594
|
+
*
|
|
595
|
+
* @returns `true` after closure.
|
|
596
|
+
*/
|
|
529
597
|
get closed() {
|
|
530
598
|
return this.#closed;
|
|
531
599
|
}
|
|
600
|
+
/**
|
|
601
|
+
* Attaches a source subscription or immediately removes it after close.
|
|
602
|
+
*
|
|
603
|
+
* @param subscription Source attachment to track.
|
|
604
|
+
*/
|
|
532
605
|
attach(subscription) {
|
|
533
606
|
if (this.#closed) {
|
|
534
607
|
subscription.unsubscribe();
|
|
@@ -536,6 +609,11 @@ class SubscriptionDelivery {
|
|
|
536
609
|
}
|
|
537
610
|
this.#subscription = subscription;
|
|
538
611
|
}
|
|
612
|
+
/**
|
|
613
|
+
* Queues one update or wakes the next waiting stream reader.
|
|
614
|
+
*
|
|
615
|
+
* @param update State or Event update to deliver.
|
|
616
|
+
*/
|
|
539
617
|
push(update) {
|
|
540
618
|
if (this.#closed) {
|
|
541
619
|
return;
|
|
@@ -552,6 +630,11 @@ class SubscriptionDelivery {
|
|
|
552
630
|
waiter(update);
|
|
553
631
|
}
|
|
554
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
|
+
*/
|
|
555
638
|
next() {
|
|
556
639
|
const update = this.#queue.shift();
|
|
557
640
|
if (update !== undefined || this.#closed) {
|
|
@@ -559,6 +642,9 @@ class SubscriptionDelivery {
|
|
|
559
642
|
}
|
|
560
643
|
return new Promise((resolve) => this.#waiters.push(resolve));
|
|
561
644
|
}
|
|
645
|
+
/**
|
|
646
|
+
* Closes the source subscription and completes all waiting readers.
|
|
647
|
+
*/
|
|
562
648
|
close() {
|
|
563
649
|
if (this.#closed) {
|
|
564
650
|
return;
|
|
@@ -573,1014 +659,1006 @@ class SubscriptionDelivery {
|
|
|
573
659
|
}
|
|
574
660
|
}
|
|
575
661
|
/**
|
|
576
|
-
*
|
|
662
|
+
* Builds an OK status for service responses.
|
|
577
663
|
*/
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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();
|
|
601
710
|
}
|
|
602
|
-
|
|
603
|
-
|
|
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.");
|
|
604
716
|
}
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
});
|
|
717
|
+
const idEntries = filters.idFilter?.id;
|
|
718
|
+
if (idEntries?.some((id) => id === undefined) === true) {
|
|
719
|
+
return invalidQueryError("QueryService.Read id_filter entries are required.");
|
|
609
720
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
return formatError;
|
|
614
|
-
}
|
|
615
|
-
switch (target.criterion.case) {
|
|
616
|
-
case "includeAll":
|
|
617
|
-
if (!target.criterion.value) {
|
|
618
|
-
return invalidCriterionError();
|
|
619
|
-
}
|
|
620
|
-
return undefined;
|
|
621
|
-
case "filters":
|
|
622
|
-
return validateFilters(target.criterion.value, route);
|
|
623
|
-
default:
|
|
624
|
-
return invalidCriterionError();
|
|
625
|
-
}
|
|
721
|
+
const idError = validateQueryIds(idEntries, route);
|
|
722
|
+
if (idError !== undefined) {
|
|
723
|
+
return idError;
|
|
626
724
|
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
if (idCount > QueryLimits.idCount) {
|
|
630
|
-
return invalidQueryError("QueryService.Read id_filter may contain at most 100 IDs.");
|
|
631
|
-
}
|
|
632
|
-
const idEntries = filters.idFilter?.id;
|
|
633
|
-
if (idEntries?.some((id) => id === undefined) === true) {
|
|
634
|
-
return invalidQueryError("QueryService.Read id_filter entries are required.");
|
|
635
|
-
}
|
|
636
|
-
const idError = validateQueryIds(idEntries, route);
|
|
637
|
-
if (idError !== undefined) {
|
|
638
|
-
return idError;
|
|
639
|
-
}
|
|
640
|
-
if (filters.filter.length > QueryLimits.compositeCount) {
|
|
641
|
-
return invalidQueryError("QueryService.Read may contain at most 8 composite filters.");
|
|
642
|
-
}
|
|
643
|
-
const filterError = validateCompositeFilters(filters.filter, route);
|
|
644
|
-
if (filterError !== undefined) {
|
|
645
|
-
return filterError;
|
|
646
|
-
}
|
|
647
|
-
return idCount === 0 && countSimpleFilters(filters.filter) === 0
|
|
648
|
-
? {
|
|
649
|
-
type: "INVALID_QUERY",
|
|
650
|
-
message: "QueryService.Read requires an ID filter or column filter.",
|
|
651
|
-
}
|
|
652
|
-
: undefined;
|
|
725
|
+
if (filters.filter.length > QueryLimits.compositeCount) {
|
|
726
|
+
return invalidQueryError("QueryService.Read may contain at most 8 composite filters.");
|
|
653
727
|
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
let compositeCount = 0;
|
|
658
|
-
let simpleCount = 0;
|
|
659
|
-
while (pending.length > 0) {
|
|
660
|
-
const current = pending.pop();
|
|
661
|
-
if (current === undefined)
|
|
662
|
-
break;
|
|
663
|
-
const filter = current.filter;
|
|
664
|
-
if (seen.has(filter))
|
|
665
|
-
return unsupportedFilterError("QueryService.Read filters must not contain cycles.");
|
|
666
|
-
seen.add(filter);
|
|
667
|
-
compositeCount += 1;
|
|
668
|
-
simpleCount += filter.filter.length;
|
|
669
|
-
if (filter.filter.length === 0 && filter.compositeFilter.length === 0) {
|
|
670
|
-
return invalidQueryError("QueryService.Read composite filter must not be empty.");
|
|
671
|
-
}
|
|
672
|
-
if (compositeCount > QueryLimits.compositeCount || current.depth > 8) {
|
|
673
|
-
return invalidQueryError("QueryService.Read may contain at most 8 composite filters.");
|
|
674
|
-
}
|
|
675
|
-
if (simpleCount > QueryLimits.filterCount) {
|
|
676
|
-
return invalidQueryError("QueryService.Read may contain at most 16 simple column filters.");
|
|
677
|
-
}
|
|
678
|
-
if (compositeCount + pending.length + filter.compositeFilter.length >
|
|
679
|
-
QueryLimits.compositeCount) {
|
|
680
|
-
return invalidQueryError("QueryService.Read may contain at most 8 composite filters.");
|
|
681
|
-
}
|
|
682
|
-
if (current.depth >= 8 && filter.compositeFilter.length > 0) {
|
|
683
|
-
return invalidQueryError("QueryService.Read may contain at most 8 composite filters.");
|
|
684
|
-
}
|
|
685
|
-
const operator = filter.operator;
|
|
686
|
-
if (operator !== CompositeFilter_CompositeOperator.ALL &&
|
|
687
|
-
operator !== CompositeFilter_CompositeOperator.EITHER) {
|
|
688
|
-
return unsupportedFilterError("QueryService.Read composite operator must be ALL or EITHER.");
|
|
689
|
-
}
|
|
690
|
-
const simpleError = validateSimpleFilters(filter.filter, route);
|
|
691
|
-
if (simpleError !== undefined) {
|
|
692
|
-
return simpleError;
|
|
693
|
-
}
|
|
694
|
-
for (const child of filter.compositeFilter) {
|
|
695
|
-
pending.push({ filter: child, depth: current.depth + 1 });
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
return undefined;
|
|
728
|
+
const filterError = validateCompositeFilters(filters.filter, route);
|
|
729
|
+
if (filterError !== undefined) {
|
|
730
|
+
return filterError;
|
|
699
731
|
}
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
}
|
|
705
|
-
if (ids.some((id) => id === undefined || AnyMessages.unpack(id, schema) === undefined)) {
|
|
706
|
-
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.",
|
|
707
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) {
|
|
708
766
|
return undefined;
|
|
709
767
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
let count = 0;
|
|
713
|
-
while (pending.length > 0) {
|
|
714
|
-
const filter = pending.pop();
|
|
715
|
-
if (filter === undefined)
|
|
716
|
-
break;
|
|
717
|
-
count += filter.filter.length;
|
|
718
|
-
pending.push(...filter.compositeFilter);
|
|
719
|
-
}
|
|
720
|
-
return count;
|
|
721
|
-
}
|
|
722
|
-
function validateSimpleFilters(filters, route) {
|
|
723
|
-
for (const filter of filters) {
|
|
724
|
-
if (wireComparison(filter.operator) === undefined) {
|
|
725
|
-
return unsupportedFilterError("QueryService.Read comparison operator is not supported.");
|
|
726
|
-
}
|
|
727
|
-
const column = filter.fieldPath?.fieldName[0];
|
|
728
|
-
if (column === undefined || column.trim().length === 0) {
|
|
729
|
-
return unsupportedFilterError("QueryService.Read column filter field is required.");
|
|
730
|
-
}
|
|
731
|
-
if (filter.fieldPath?.fieldName.length !== 1) {
|
|
732
|
-
return unsupportedFilterError("QueryService.Read supports only top-level column filters.");
|
|
733
|
-
}
|
|
734
|
-
if (!route.allowedColumnNames.has(column)) {
|
|
735
|
-
return unsupportedFilterError(`QueryService.Read column filter "${column}" is not a declared column.`);
|
|
736
|
-
}
|
|
737
|
-
if (filter.value === undefined) {
|
|
738
|
-
return unsupportedFilterError("QueryService.Read column filter value is required.");
|
|
739
|
-
}
|
|
740
|
-
const field = route.columnFields.get(column);
|
|
741
|
-
if (field === undefined
|
|
742
|
-
? !systemValueMatches(column, filter.value)
|
|
743
|
-
: !filterValueMatches(field, filter.value)) {
|
|
744
|
-
return unsupportedFilterError(`QueryService.Read column filter "${column}" has the wrong value type.`);
|
|
745
|
-
}
|
|
746
|
-
if (filter.operator !== Filter_Operator.EQUAL &&
|
|
747
|
-
(field === undefined ? column !== "version" : !supportsRange(field))) {
|
|
748
|
-
return unsupportedFilterError(`QueryService.Read column filter "${column}" does not support range comparison.`);
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
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}.`);
|
|
752
770
|
}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
if (
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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.");
|
|
770
789
|
}
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
message: "QueryService.Read limit may be at most 1000.",
|
|
775
|
-
};
|
|
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.");
|
|
776
793
|
}
|
|
777
|
-
if (
|
|
778
|
-
return
|
|
779
|
-
type: "INVALID_QUERY",
|
|
780
|
-
message: "QueryService.Read limit requires ordering.",
|
|
781
|
-
};
|
|
794
|
+
if (filter.fieldPath?.fieldName.length !== 1) {
|
|
795
|
+
return unsupportedFilterError("QueryService.Read supports only top-level column filters.");
|
|
782
796
|
}
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
return unsupportedFormatError("QueryService.Read order_by column is required.");
|
|
786
|
-
}
|
|
787
|
-
if (!route.allowedColumnNames.has(order.column)) {
|
|
788
|
-
return unsupportedFormatError(`QueryService.Read order_by column "${order.column}" is not a declared column.`);
|
|
789
|
-
}
|
|
790
|
-
const field = route.columnFields.get(order.column);
|
|
791
|
-
if (field === undefined ? order.column !== "version" : !supportsRange(field)) {
|
|
792
|
-
return unsupportedFormatError(`QueryService.Read order_by column "${order.column}" is not orderable.`);
|
|
793
|
-
}
|
|
794
|
-
if (order.direction !== OrderBy_Direction.ASCENDING &&
|
|
795
|
-
order.direction !== OrderBy_Direction.DESCENDING) {
|
|
796
|
-
return unsupportedFormatError("QueryService.Read order_by direction must be ASCENDING or DESCENDING.");
|
|
797
|
-
}
|
|
797
|
+
if (!route.allowedColumnNames.has(column)) {
|
|
798
|
+
return unsupportedFilterError(`QueryService.Read column filter "${column}" is not a declared column.`);
|
|
798
799
|
}
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
function createReadPlan(target, query, route) {
|
|
802
|
-
const predicate = target.criterion.case === "filters"
|
|
803
|
-
? normalizedFilters(target.criterion.value, route)
|
|
804
|
-
: undefined;
|
|
805
|
-
const format = query.format;
|
|
806
|
-
return Object.freeze({
|
|
807
|
-
...(predicate === undefined ? {} : { predicate }),
|
|
808
|
-
...(format?.fieldMask === undefined
|
|
809
|
-
? {}
|
|
810
|
-
: {
|
|
811
|
-
mask: {
|
|
812
|
-
paths: format.fieldMask.paths.map((path) => requiredQueryMask(route.schema, path)),
|
|
813
|
-
},
|
|
814
|
-
}),
|
|
815
|
-
...(format === undefined || format.orderBy.length === 0
|
|
816
|
-
? {}
|
|
817
|
-
: {
|
|
818
|
-
order: format.orderBy.map((order) => ({
|
|
819
|
-
column: order.column,
|
|
820
|
-
direction: order.direction === OrderBy_Direction.DESCENDING
|
|
821
|
-
? "desc"
|
|
822
|
-
: "asc",
|
|
823
|
-
})),
|
|
824
|
-
}),
|
|
825
|
-
...(format === undefined || format.limit === 0 ? {} : { limit: format.limit }),
|
|
826
|
-
});
|
|
827
|
-
}
|
|
828
|
-
function normalizedFilters(filters, route) {
|
|
829
|
-
const predicates = [];
|
|
830
|
-
if (filters.idFilter !== undefined) {
|
|
831
|
-
predicates.push({
|
|
832
|
-
kind: "ids",
|
|
833
|
-
ids: filters.idFilter.id.map((id) => decodeQueryIdValue(id, route)),
|
|
834
|
-
});
|
|
835
|
-
}
|
|
836
|
-
predicates.push(...filters.filter.map((filter) => normalizedComposite(filter, route)));
|
|
837
|
-
const onlyPredicate = predicates[0];
|
|
838
|
-
return predicates.length === 1 && onlyPredicate !== undefined
|
|
839
|
-
? onlyPredicate
|
|
840
|
-
: { kind: "all", predicates };
|
|
841
|
-
}
|
|
842
|
-
function decodeQueryIdValue(value, route) {
|
|
843
|
-
const schema = route.idField.message;
|
|
844
|
-
return schema === undefined ? decodeAnyValue(value) : AnyMessages.unpack(value, schema);
|
|
845
|
-
}
|
|
846
|
-
function normalizedComposite(filter, route) {
|
|
847
|
-
const predicates = [
|
|
848
|
-
...filter.filter.map((child) => ({
|
|
849
|
-
kind: "comparison",
|
|
850
|
-
column: requiredFilterColumn(child),
|
|
851
|
-
operator: requiredComparison(child),
|
|
852
|
-
value: decodeColumnValue(child, route),
|
|
853
|
-
})),
|
|
854
|
-
...filter.compositeFilter.map((child) => normalizedComposite(child, route)),
|
|
855
|
-
];
|
|
856
|
-
return {
|
|
857
|
-
kind: filter.operator === CompositeFilter_CompositeOperator.EITHER ? "either" : "all",
|
|
858
|
-
predicates,
|
|
859
|
-
};
|
|
860
|
-
}
|
|
861
|
-
function requiredFilterColumn(filter) {
|
|
862
|
-
const column = filter.fieldPath?.fieldName[0];
|
|
863
|
-
if (column !== undefined)
|
|
864
|
-
return column;
|
|
865
|
-
throw new TypeError("Validated query filter has no column.");
|
|
866
|
-
}
|
|
867
|
-
function requiredComparison(filter) {
|
|
868
|
-
const operator = wireComparison(filter.operator);
|
|
869
|
-
if (operator !== undefined)
|
|
870
|
-
return operator;
|
|
871
|
-
throw new TypeError("Validated query filter has no supported comparison operator.");
|
|
872
|
-
}
|
|
873
|
-
function decodeColumnValue(filter, route) {
|
|
874
|
-
const column = filter.fieldPath?.fieldName[0] ?? "";
|
|
875
|
-
if (column === "version" && filter.value !== undefined) {
|
|
876
|
-
const value = AnyMessages.unpack(filter.value, VersionSchema);
|
|
877
|
-
if (value !== undefined)
|
|
878
|
-
return value;
|
|
879
|
-
throw new TypeError('Validated query column "version" has an invalid value.');
|
|
880
|
-
}
|
|
881
|
-
if ((column === "archived" || column === "deleted") && filter.value !== undefined) {
|
|
882
|
-
const value = AnyMessages.unpack(filter.value, BoolValueSchema);
|
|
883
|
-
if (value !== undefined)
|
|
884
|
-
return value.value;
|
|
885
|
-
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.");
|
|
886
802
|
}
|
|
887
803
|
const field = route.columnFields.get(column);
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
if (schema === undefined || value.typeUrl !== TypeUrls.derive(schema))
|
|
893
|
-
return false;
|
|
894
|
-
const decoded = field.fieldKind === "message" ? decodeAnyValue(value, schema) : decodeAnyValue(value);
|
|
895
|
-
if (field.fieldKind === "message") {
|
|
896
|
-
return isMessage(decoded) && decoded.$typeName === field.message.typeName;
|
|
897
|
-
}
|
|
898
|
-
if (field.fieldKind === "enum")
|
|
899
|
-
return typeof decoded === "number" && Number.isInteger(decoded);
|
|
900
|
-
if (field.fieldKind !== "scalar")
|
|
901
|
-
return false;
|
|
902
|
-
if (field.scalar === ScalarType.BOOL)
|
|
903
|
-
return typeof decoded === "boolean";
|
|
904
|
-
if (field.scalar === ScalarType.BYTES)
|
|
905
|
-
return decoded instanceof Uint8Array;
|
|
906
|
-
if (field.scalar === ScalarType.STRING)
|
|
907
|
-
return typeof decoded === "string";
|
|
908
|
-
if (field.scalar === ScalarType.INT64 ||
|
|
909
|
-
field.scalar === ScalarType.UINT64 ||
|
|
910
|
-
field.scalar === ScalarType.SFIXED64 ||
|
|
911
|
-
field.scalar === ScalarType.FIXED64 ||
|
|
912
|
-
field.scalar === ScalarType.SINT64) {
|
|
913
|
-
return typeof decoded === "bigint";
|
|
914
|
-
}
|
|
915
|
-
return typeof decoded === "number" && Number.isFinite(decoded);
|
|
916
|
-
}
|
|
917
|
-
function filterValueSchema(field) {
|
|
918
|
-
if (field.fieldKind === "message")
|
|
919
|
-
return field.message;
|
|
920
|
-
if (field.fieldKind === "enum")
|
|
921
|
-
return Int32ValueSchema;
|
|
922
|
-
if (field.fieldKind !== "scalar")
|
|
923
|
-
return undefined;
|
|
924
|
-
if (field.scalar === ScalarType.BOOL)
|
|
925
|
-
return BoolValueSchema;
|
|
926
|
-
if (field.scalar === ScalarType.BYTES)
|
|
927
|
-
return BytesValueSchema;
|
|
928
|
-
if (field.scalar === ScalarType.DOUBLE)
|
|
929
|
-
return DoubleValueSchema;
|
|
930
|
-
if (field.scalar === ScalarType.FLOAT)
|
|
931
|
-
return FloatValueSchema;
|
|
932
|
-
if (field.scalar === ScalarType.INT64 ||
|
|
933
|
-
field.scalar === ScalarType.SFIXED64 ||
|
|
934
|
-
field.scalar === ScalarType.SINT64) {
|
|
935
|
-
return Int64ValueSchema;
|
|
936
|
-
}
|
|
937
|
-
if (field.scalar === ScalarType.UINT64 || field.scalar === ScalarType.FIXED64) {
|
|
938
|
-
return UInt64ValueSchema;
|
|
939
|
-
}
|
|
940
|
-
if (field.scalar === ScalarType.UINT32 || field.scalar === ScalarType.FIXED32) {
|
|
941
|
-
return UInt32ValueSchema;
|
|
942
|
-
}
|
|
943
|
-
if (field.scalar === ScalarType.STRING)
|
|
944
|
-
return StringValueSchema;
|
|
945
|
-
return Int32ValueSchema;
|
|
946
|
-
}
|
|
947
|
-
function systemValueMatches(column, value) {
|
|
948
|
-
if (column === "version") {
|
|
949
|
-
return (value.typeUrl === TypeUrls.derive(VersionSchema) &&
|
|
950
|
-
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.`);
|
|
951
808
|
}
|
|
952
|
-
if (
|
|
953
|
-
|
|
954
|
-
|
|
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.`);
|
|
955
812
|
}
|
|
956
|
-
return false;
|
|
957
813
|
}
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
field.scalar !== ScalarType.BYTES);
|
|
966
|
-
}
|
|
967
|
-
function wireComparison(operator) {
|
|
968
|
-
if (operator === Filter_Operator.EQUAL)
|
|
969
|
-
return "equal";
|
|
970
|
-
if (operator === Filter_Operator.GREATER_THAN)
|
|
971
|
-
return "greaterThan";
|
|
972
|
-
if (operator === Filter_Operator.LESS_THAN)
|
|
973
|
-
return "lessThan";
|
|
974
|
-
if (operator === Filter_Operator.GREATER_OR_EQUAL)
|
|
975
|
-
return "greaterOrEqual";
|
|
976
|
-
if (operator === Filter_Operator.LESS_OR_EQUAL)
|
|
977
|
-
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) {
|
|
978
821
|
return undefined;
|
|
979
822
|
}
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
const local = [];
|
|
983
|
-
for (const segment of path.split(".")) {
|
|
984
|
-
const field = findMessageField(current, segment);
|
|
985
|
-
if (field === undefined)
|
|
986
|
-
return undefined;
|
|
987
|
-
local.push(field.localName);
|
|
988
|
-
current = field.message;
|
|
989
|
-
}
|
|
990
|
-
return local.join(".");
|
|
991
|
-
}
|
|
992
|
-
function requiredQueryMask(schema, path) {
|
|
993
|
-
const resolved = resolveQueryMask(schema, path);
|
|
994
|
-
if (resolved !== undefined)
|
|
995
|
-
return resolved;
|
|
996
|
-
throw new TypeError(`Validated query mask path "${path}" is invalid.`);
|
|
823
|
+
if (format.orderBy.length > QueryLimits.orderCount) {
|
|
824
|
+
return unsupportedFormatError("QueryService.Read order_by may contain at most 8 entries.");
|
|
997
825
|
}
|
|
998
|
-
|
|
999
|
-
return invalidQueryError(message);
|
|
1000
|
-
}
|
|
1001
|
-
function unsupportedFormatError(message) {
|
|
1002
|
-
return invalidQueryError(message);
|
|
1003
|
-
}
|
|
1004
|
-
function invalidQueryError(message) {
|
|
826
|
+
if (format.limit > QueryLimits.resultCount) {
|
|
1005
827
|
return {
|
|
1006
828
|
type: "INVALID_QUERY",
|
|
1007
|
-
message,
|
|
829
|
+
message: "QueryService.Read limit may be at most 1000.",
|
|
1008
830
|
};
|
|
1009
831
|
}
|
|
1010
|
-
|
|
832
|
+
if (format.limit > 0 && format.orderBy.length === 0) {
|
|
1011
833
|
return {
|
|
1012
834
|
type: "INVALID_QUERY",
|
|
1013
|
-
message: "QueryService.Read requires
|
|
835
|
+
message: "QueryService.Read limit requires ordering.",
|
|
1014
836
|
};
|
|
1015
837
|
}
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
maskPathLength: 128,
|
|
1040
|
-
pathComponentCount: 16,
|
|
1041
|
-
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 }),
|
|
1042
861
|
});
|
|
1043
|
-
|
|
1044
|
-
|
|
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
|
+
});
|
|
1045
870
|
}
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
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;
|
|
1051
932
|
}
|
|
1052
|
-
|
|
1053
|
-
return
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
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);
|
|
1060
986
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
const settled = new Promise((settle, fail) => {
|
|
1065
|
-
reject = fail;
|
|
1066
|
-
resolve = () => {
|
|
1067
|
-
settle();
|
|
1068
|
-
};
|
|
1069
|
-
});
|
|
1070
|
-
return { reject, resolve, settled };
|
|
987
|
+
if (column === "archived" || column === "deleted") {
|
|
988
|
+
return (value.typeUrl === TypeUrls.derive(BoolValueSchema) &&
|
|
989
|
+
AnyMessages.unpack(value, BoolValueSchema) !== undefined);
|
|
1071
990
|
}
|
|
1072
|
-
|
|
1073
|
-
|
|
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.");
|
|
1074
1062
|
}
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
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);
|
|
1085
1091
|
}
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
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) {
|
|
1109
1114
|
throw new ConnectError("SubscriptionService.Subscribe requires filters or include_all = true.", Code.InvalidArgument);
|
|
1110
|
-
|
|
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);
|
|
1111
1125
|
}
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
}
|
|
1121
|
-
return;
|
|
1122
|
-
case "filters":
|
|
1123
|
-
throw new ConnectError("SubscriptionService.Subscribe event topics support only include_all in this runtime slice.", Code.InvalidArgument);
|
|
1124
|
-
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) {
|
|
1125
1134
|
throw new ConnectError("SubscriptionService.Subscribe requires filters or include_all = true.", Code.InvalidArgument);
|
|
1126
|
-
|
|
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);
|
|
1127
1141
|
}
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
return undefined;
|
|
1147
|
-
}
|
|
1148
|
-
if (statePredicate(update.state)) {
|
|
1149
|
-
return "state";
|
|
1150
|
-
}
|
|
1151
|
-
if (update.previousState !== undefined && statePredicate(update.previousState)) {
|
|
1152
|
-
return "noLongerMatching";
|
|
1153
|
-
}
|
|
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))) {
|
|
1154
1160
|
return undefined;
|
|
1155
|
-
},
|
|
1156
|
-
};
|
|
1157
|
-
}
|
|
1158
|
-
function createSubscriptionPredicate(filters, route) {
|
|
1159
|
-
const predicates = filters.map((filter) => createCompositePredicate(filter, route));
|
|
1160
|
-
return (state) => predicates.every((predicate) => predicate(state));
|
|
1161
|
-
}
|
|
1162
|
-
function validateSubscriptionFilterTree(filters) {
|
|
1163
|
-
if (filters.length > SubscriptionLimits.compositeCount) {
|
|
1164
|
-
throw new ConnectError("SubscriptionService.Subscribe may contain at most 8 composite filters.", Code.InvalidArgument);
|
|
1165
|
-
}
|
|
1166
|
-
const stack = [];
|
|
1167
|
-
for (const filter of filters) {
|
|
1168
|
-
stack.push({ filter, depth: 0 });
|
|
1169
|
-
}
|
|
1170
|
-
const counts = {
|
|
1171
|
-
compositeCount: 0,
|
|
1172
|
-
simpleCount: 0,
|
|
1173
|
-
};
|
|
1174
|
-
while (stack.length > 0) {
|
|
1175
|
-
const current = stack.pop();
|
|
1176
|
-
if (current === undefined) {
|
|
1177
|
-
continue;
|
|
1178
1161
|
}
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
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);
|
|
1182
1193
|
}
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
switch (filter.operator) {
|
|
1188
|
-
case CompositeFilter_CompositeOperator.ALL:
|
|
1189
|
-
case CompositeFilter_CompositeOperator.EITHER:
|
|
1190
|
-
break;
|
|
1191
|
-
default:
|
|
1192
|
-
throw new ConnectError("SubscriptionService.Subscribe supports only ALL or EITHER composite filters.", Code.InvalidArgument);
|
|
1193
|
-
}
|
|
1194
|
-
if (depth === SubscriptionLimits.compositeDepth && filter.compositeFilter.length > 0) {
|
|
1195
|
-
throw new ConnectError("SubscriptionService.Subscribe composite filters may nest at most 8 levels.", Code.InvalidArgument);
|
|
1196
|
-
}
|
|
1197
|
-
counts.compositeCount += 1;
|
|
1198
|
-
if (counts.compositeCount > SubscriptionLimits.compositeCount) {
|
|
1199
|
-
throw new ConnectError("SubscriptionService.Subscribe may contain at most 8 composite filters.", Code.InvalidArgument);
|
|
1200
|
-
}
|
|
1201
|
-
counts.simpleCount += filter.filter.length;
|
|
1202
|
-
if (counts.simpleCount > SubscriptionLimits.filterCount) {
|
|
1203
|
-
throw new ConnectError("SubscriptionService.Subscribe may contain at most 16 simple field filters.", Code.InvalidArgument);
|
|
1204
|
-
}
|
|
1205
|
-
if ((filter.compositeFilter.length > 1 || depth + 1 < SubscriptionLimits.compositeDepth) &&
|
|
1206
|
-
counts.compositeCount + filter.compositeFilter.length > SubscriptionLimits.compositeCount) {
|
|
1207
|
-
throw new ConnectError("SubscriptionService.Subscribe may contain at most 8 composite filters.", Code.InvalidArgument);
|
|
1208
|
-
}
|
|
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);
|
|
1209
1198
|
}
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
}
|
|
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);
|
|
1217
1205
|
}
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
...filter.filter.map((simple) => createSimplePredicate(simple, route)),
|
|
1221
|
-
...filter.compositeFilter.map((nested) => createCompositePredicate(nested, route)),
|
|
1222
|
-
];
|
|
1223
|
-
if (children.length === 0) {
|
|
1224
|
-
return () => true;
|
|
1225
|
-
}
|
|
1226
|
-
return filter.operator === CompositeFilter_CompositeOperator.ALL
|
|
1227
|
-
? (state) => children.every((predicate) => predicate(state))
|
|
1228
|
-
: (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);
|
|
1229
1208
|
}
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
}
|
|
1234
|
-
if (filter.value === undefined) {
|
|
1235
|
-
throw new ConnectError("SubscriptionService.Subscribe field filter value is required.", Code.InvalidArgument);
|
|
1236
|
-
}
|
|
1237
|
-
validateSubscriptionPath(filter.fieldPath?.fieldName ?? [], "field filter");
|
|
1238
|
-
const resolved = resolveMessagePath(route.schema, filter.fieldPath?.fieldName ?? [], "field filter");
|
|
1239
|
-
const expected = decodeFieldFilterValue(filter.value, resolved.leafSchema);
|
|
1240
|
-
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);
|
|
1241
1212
|
}
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
return undefined;
|
|
1246
|
-
}
|
|
1247
|
-
if (paths.length > SubscriptionLimits.maskPathCount) {
|
|
1248
|
-
throw new ConnectError("SubscriptionService.Subscribe field_mask may contain at most 32 paths.", Code.InvalidArgument);
|
|
1249
|
-
}
|
|
1250
|
-
if (paths.some((path) => path.length > SubscriptionLimits.maskPathLength)) {
|
|
1251
|
-
throw new ConnectError("SubscriptionService.Subscribe field_mask paths may contain at most 128 characters.", Code.InvalidArgument);
|
|
1252
|
-
}
|
|
1253
|
-
return paths.map((path) => {
|
|
1254
|
-
const segments = path.split(".");
|
|
1255
|
-
validateSubscriptionPath(segments, "field_mask");
|
|
1256
|
-
return resolveMessagePath(route.schema, segments, "field_mask").localPath.join(".");
|
|
1257
|
-
});
|
|
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);
|
|
1258
1216
|
}
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
}
|
|
1263
|
-
if (fieldPath.length > SubscriptionLimits.pathComponentCount) {
|
|
1264
|
-
throw new ConnectError(`SubscriptionService.Subscribe ${label} path may contain at most 16 components.`, Code.InvalidArgument);
|
|
1265
|
-
}
|
|
1266
|
-
if (fieldPath.some((field) => field.length > SubscriptionLimits.pathSegmentLength)) {
|
|
1267
|
-
throw new ConnectError(`SubscriptionService.Subscribe ${label} path components may contain at most 128 characters.`, Code.InvalidArgument);
|
|
1268
|
-
}
|
|
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);
|
|
1269
1220
|
}
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
return decodeAnyValue(value);
|
|
1276
|
-
}
|
|
1277
|
-
const decoded = AnyMessages.unpack(value, route.idField.message);
|
|
1278
|
-
if (decoded === undefined) {
|
|
1279
|
-
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);
|
|
1280
1226
|
}
|
|
1281
|
-
|
|
1227
|
+
stack.push({ filter: nested, depth: depth + 1 });
|
|
1282
1228
|
}
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
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);
|
|
1292
1245
|
}
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
return undefined;
|
|
1296
|
-
}
|
|
1297
|
-
if (schema !== undefined) {
|
|
1298
|
-
const decoded = AnyMessages.unpack(value, schema);
|
|
1299
|
-
if (decoded !== undefined) {
|
|
1300
|
-
return decoded;
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
for (const decoder of VALUE_DECODERS) {
|
|
1304
|
-
const decoded = decoder(value);
|
|
1305
|
-
if (decoded !== undefined) {
|
|
1306
|
-
return decoded;
|
|
1307
|
-
}
|
|
1308
|
-
}
|
|
1309
|
-
return value;
|
|
1246
|
+
if (filter.value === undefined) {
|
|
1247
|
+
throw new ConnectError("SubscriptionService.Subscribe field filter value is required.", Code.InvalidArgument);
|
|
1310
1248
|
}
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
currentSchema = index === fieldPath.length - 1 ? undefined : field.message;
|
|
1323
|
-
if (currentSchema === undefined && index < fieldPath.length - 1) {
|
|
1324
|
-
throw new ConnectError(`SubscriptionService.Subscribe ${label} "${fieldPath.join(".")}" is not a message path.`, Code.InvalidArgument);
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
return Object.freeze({
|
|
1328
|
-
localPath,
|
|
1329
|
-
...(leafSchema === undefined ? {} : { leafSchema }),
|
|
1330
|
-
});
|
|
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);
|
|
1331
1260
|
}
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
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);
|
|
1335
1263
|
}
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
for (const segment of path) {
|
|
1339
|
-
if (typeof current !== "object" || current === null) {
|
|
1340
|
-
return undefined;
|
|
1341
|
-
}
|
|
1342
|
-
current = Reflect.get(current, segment);
|
|
1343
|
-
}
|
|
1344
|
-
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);
|
|
1345
1266
|
}
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
if (isAny(actual) && isAny(expected)) {
|
|
1351
|
-
return bytesEqual(toBinary(AnySchema, actual), toBinary(AnySchema, expected));
|
|
1352
|
-
}
|
|
1353
|
-
if (actual instanceof Uint8Array && expected instanceof Uint8Array) {
|
|
1354
|
-
return bytesEqual(actual, expected);
|
|
1355
|
-
}
|
|
1356
|
-
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);
|
|
1357
1271
|
}
|
|
1358
|
-
|
|
1359
|
-
return
|
|
1272
|
+
if (route.idField.message === undefined) {
|
|
1273
|
+
return decodeAnyValue(value);
|
|
1360
1274
|
}
|
|
1361
|
-
|
|
1362
|
-
|
|
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);
|
|
1363
1278
|
}
|
|
1364
|
-
|
|
1365
|
-
|
|
1279
|
+
return decoded;
|
|
1280
|
+
}
|
|
1281
|
+
function decodeFieldFilterValue(value, schema) {
|
|
1282
|
+
if (value === undefined || schema === undefined) {
|
|
1283
|
+
return decodeAnyValue(value);
|
|
1366
1284
|
}
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
version: result.version ?? create(VersionSchema),
|
|
1371
|
-
});
|
|
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);
|
|
1372
1288
|
}
|
|
1373
|
-
|
|
1374
|
-
|
|
1289
|
+
return decoded;
|
|
1290
|
+
}
|
|
1291
|
+
function decodeAnyValue(value, schema) {
|
|
1292
|
+
if (value === undefined) {
|
|
1293
|
+
return undefined;
|
|
1375
1294
|
}
|
|
1376
|
-
|
|
1377
|
-
|
|
1295
|
+
if (schema !== undefined) {
|
|
1296
|
+
const decoded = AnyMessages.unpack(value, schema);
|
|
1297
|
+
if (decoded !== undefined) {
|
|
1298
|
+
return decoded;
|
|
1299
|
+
}
|
|
1378
1300
|
}
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
case "pastMessage":
|
|
1384
|
-
return tenantValue(event.context.origin.value.actorContext?.tenantId);
|
|
1385
|
-
default:
|
|
1386
|
-
return undefined;
|
|
1301
|
+
for (const decoder of VALUE_DECODERS) {
|
|
1302
|
+
const decoded = decoder(value);
|
|
1303
|
+
if (decoded !== undefined) {
|
|
1304
|
+
return decoded;
|
|
1387
1305
|
}
|
|
1388
1306
|
}
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
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
|
+
}
|
|
1394
1327
|
}
|
|
1395
|
-
|
|
1396
|
-
|
|
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) {
|
|
1397
1341
|
return undefined;
|
|
1398
|
-
try {
|
|
1399
|
-
return TenantBoundary.from(tenantId).tenantId;
|
|
1400
1342
|
}
|
|
1401
|
-
|
|
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:
|
|
1402
1395
|
return undefined;
|
|
1403
|
-
}
|
|
1404
1396
|
}
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
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;
|
|
1414
1409
|
}
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
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) {
|
|
1424
1426
|
return tenantId === undefined
|
|
1425
|
-
?
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1427
|
+
? {
|
|
1428
|
+
type: "TENANT_REQUIRED",
|
|
1429
|
+
message: `Tenant is required for this ${subject}.`,
|
|
1430
|
+
}
|
|
1431
|
+
: undefined;
|
|
1430
1432
|
}
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
}
|
|
1441
|
-
if (error instanceof CommandValidationError) {
|
|
1442
|
-
return {
|
|
1443
|
-
type: "COMMAND_VALIDATION_ERROR",
|
|
1444
|
-
message: "Command payload validation failed.",
|
|
1445
|
-
details: AnyMessages.pack(ValidationErrorSchema, error.validationError, {
|
|
1446
|
-
validate: false,
|
|
1447
|
-
}),
|
|
1448
|
-
};
|
|
1449
|
-
}
|
|
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) {
|
|
1450
1442
|
return {
|
|
1451
|
-
type:
|
|
1452
|
-
message:
|
|
1443
|
+
type: error.type,
|
|
1444
|
+
message: error.clientMessage,
|
|
1445
|
+
details: AnyMessages.pack(ValidationErrorSchema, error.validationError, {
|
|
1446
|
+
validate: false,
|
|
1447
|
+
}),
|
|
1453
1448
|
};
|
|
1454
1449
|
}
|
|
1455
|
-
|
|
1456
|
-
return
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
(value) => AnyMessages.unpack(value, Int64ValueSchema)?.value,
|
|
1464
|
-
(value) => AnyMessages.unpack(value, UInt64ValueSchema)?.value,
|
|
1465
|
-
(value) => AnyMessages.unpack(value, FloatValueSchema)?.value,
|
|
1466
|
-
(value) => AnyMessages.unpack(value, DoubleValueSchema)?.value,
|
|
1467
|
-
(value) => AnyMessages.unpack(value, BytesValueSchema)?.value,
|
|
1468
|
-
]);
|
|
1469
|
-
function createEntityUpdate(record, update) {
|
|
1470
|
-
const matcher = record.matcher;
|
|
1471
|
-
if (matcher === undefined)
|
|
1472
|
-
return undefined;
|
|
1473
|
-
const match = matcher.match(update);
|
|
1474
|
-
if (match === undefined) {
|
|
1475
|
-
return undefined;
|
|
1476
|
-
}
|
|
1477
|
-
return create(SubscriptionUpdateSchema, {
|
|
1478
|
-
subscription: clone(SubscriptionSchema, record.subscription),
|
|
1479
|
-
response: okResponse(),
|
|
1480
|
-
update: {
|
|
1481
|
-
case: "entityUpdates",
|
|
1482
|
-
value: create(EntityUpdatesSchema, {
|
|
1483
|
-
update: [
|
|
1484
|
-
create(EntityStateUpdateSchema, {
|
|
1485
|
-
id: packEntityId(record.route, update.id),
|
|
1486
|
-
kind: match === "state"
|
|
1487
|
-
? {
|
|
1488
|
-
case: "state",
|
|
1489
|
-
value: AnyMessages.pack(record.route.schema, maskedState(record, update.state), {
|
|
1490
|
-
validate: false,
|
|
1491
|
-
}),
|
|
1492
|
-
}
|
|
1493
|
-
: {
|
|
1494
|
-
case: "noLongerMatching",
|
|
1495
|
-
value: true,
|
|
1496
|
-
},
|
|
1497
|
-
}),
|
|
1498
|
-
],
|
|
1499
|
-
}),
|
|
1500
|
-
},
|
|
1501
|
-
});
|
|
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
|
+
};
|
|
1502
1458
|
}
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
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;
|
|
1514
1560
|
}
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
if (
|
|
1535
|
-
return
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
if (
|
|
1541
|
-
return
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
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
|
+
}
|
|
1554
1616
|
}
|
|
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
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
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
|
+
});
|
|
1586
1664
|
//# sourceMappingURL=spine-services.js.map
|