@spine-event-engine/server 2.0.0-snapshot.16 → 2.0.0-snapshot.20

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