@spine-event-engine/core 2.0.0-snapshot.2 → 2.0.0-snapshot.21

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 (40) hide show
  1. package/README.md +61 -8
  2. package/REFERENCE.md +31 -3
  3. package/dist/codegen/index.d.ts +10 -0
  4. package/dist/codegen/index.d.ts.map +1 -0
  5. package/dist/codegen/index.js +20 -0
  6. package/dist/codegen/index.js.map +1 -0
  7. package/dist/entity/entity-column.d.ts +158 -0
  8. package/dist/entity/entity-column.d.ts.map +1 -0
  9. package/dist/entity/entity-column.js +303 -0
  10. package/dist/entity/entity-column.js.map +1 -0
  11. package/dist/index.d.ts +84 -13
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +331 -7
  14. package/dist/index.js.map +1 -1
  15. package/dist/internal/subscription-lifecycle.d.ts +0 -1
  16. package/dist/internal/subscription-lifecycle.d.ts.map +1 -1
  17. package/dist/internal/subscription-lifecycle.js +0 -1
  18. package/dist/internal/subscription-lifecycle.js.map +1 -1
  19. package/dist/query/entity-field-classification.d.ts +20 -0
  20. package/dist/query/entity-field-classification.d.ts.map +1 -0
  21. package/dist/query/entity-field-classification.js +62 -0
  22. package/dist/query/entity-field-classification.js.map +1 -0
  23. package/dist/query/entity-query.d.ts +405 -0
  24. package/dist/query/entity-query.d.ts.map +1 -0
  25. package/dist/query/entity-query.js +841 -0
  26. package/dist/query/entity-query.js.map +1 -0
  27. package/dist/query/generated-entity-query.d.ts +197 -0
  28. package/dist/query/generated-entity-query.d.ts.map +1 -0
  29. package/dist/query/generated-entity-query.js +202 -0
  30. package/dist/query/generated-entity-query.js.map +1 -0
  31. package/dist/spi/entity-query-plan.d.ts +7 -0
  32. package/dist/spi/entity-query-plan.d.ts.map +1 -0
  33. package/dist/spi/entity-query-plan.js +15 -0
  34. package/dist/spi/entity-query-plan.js.map +1 -0
  35. package/dist/spi/subscription-lifecycle.d.ts +5 -0
  36. package/dist/spi/subscription-lifecycle.d.ts.map +1 -0
  37. package/dist/spi/subscription-lifecycle.js +18 -0
  38. package/dist/spi/subscription-lifecycle.js.map +1 -0
  39. package/dist/tsconfig.tsbuildinfo +1 -1
  40. package/package.json +14 -7
@@ -0,0 +1,841 @@
1
+ /*
2
+ * Copyright 2026, CodeMatters. All rights reserved.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5
+ * in compliance with the License. You may obtain a copy of the License at
6
+ *
7
+ * https://www.apache.org/licenses/LICENSE-2.0
8
+ *
9
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
10
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11
+ * or implied. See the License for the specific language governing permissions and limitations under
12
+ * the License.
13
+ */
14
+ import { clone, create, fromBinary, getOption, hasOption, ScalarType, toBinary, } from "@bufbuild/protobuf";
15
+ import { BoolValueSchema, AnySchema, BytesValueSchema, DoubleValueSchema, FloatValueSchema, Int32ValueSchema, Int64ValueSchema, StringValueSchema, UInt32ValueSchema, UInt64ValueSchema, } from "@bufbuild/protobuf/wkt";
16
+ import { ActorContextSchema, VersionSchema, type_url_prefix, } from "@spine-event-engine/proto";
17
+ import { CompositeFilterSchema, CompositeFilter_CompositeOperator, FilterSchema, Filter_Operator, IdFilterSchema, OrderBySchema, OrderBy_Direction, QueryIdSchema, QuerySchema, ResponseFormatSchema, TargetFiltersSchema, TargetSchema, } from "@spine-event-engine/proto/client";
18
+ import { EntityColumn, } from "../entity/entity-column.js";
19
+ import { EntityFieldClassification } from "./entity-field-classification.js";
20
+ const maximumPredicateDepth = 64;
21
+ const maximumPredicateNodes = 10_000;
22
+ const queryHost = globalThis;
23
+ /**
24
+ * Builds a typed Entity query for the frozen Spine wire contract.
25
+ *
26
+ * @typeParam Schema Generated Entity state schema selected by the query.
27
+ * @typeParam Columns Registered columns for that state schema.
28
+ */
29
+ export class EntityQueryBuilder {
30
+ #schema;
31
+ #context;
32
+ #columns;
33
+ #ids = [];
34
+ #predicates = [];
35
+ #order = [];
36
+ #limit;
37
+ /**
38
+ * Creates a builder for one Entity query.
39
+ *
40
+ * @param input Schema, registered columns, and actor context for the query.
41
+ */
42
+ constructor(input) {
43
+ this.#schema = input.schema;
44
+ this.#columns = input.columns;
45
+ this.#context = clone(ActorContextSchema, input.context);
46
+ }
47
+ /**
48
+ * Adds Entity IDs to the query target.
49
+ *
50
+ * @param ids Entity IDs to include.
51
+ * @returns This builder.
52
+ */
53
+ byId(...ids) {
54
+ if (ids.length === 0 || ids.some((id) => id === undefined)) {
55
+ throw new TypeError("Entity query ID filter must not be empty.");
56
+ }
57
+ this.#ids.push(...ids);
58
+ return this;
59
+ }
60
+ /**
61
+ * Adds a typed predicate. Repeated calls are combined with `ALL`.
62
+ *
63
+ * @typeParam Predicate Predicate type checked against the registered columns.
64
+ * @param predicate Predicate owned by this builder's registered columns.
65
+ * @returns This builder.
66
+ */
67
+ where(predicate) {
68
+ if (this.#predicates.length >= maximumPredicateNodes) {
69
+ throw new TypeError(`Entity query predicate exceeds maximum node count ${String(maximumPredicateNodes)}.`);
70
+ }
71
+ this.#predicates.push(predicate);
72
+ return this;
73
+ }
74
+ /**
75
+ * Adds one ordering clause in caller order.
76
+ *
77
+ * @typeParam Column Registered ordered column type.
78
+ * @param column Ordered column owned by this builder's Entity schema.
79
+ * @param direction Sort direction, ascending by default.
80
+ * @returns This builder.
81
+ */
82
+ orderBy(column, direction = "asc") {
83
+ EntityQueryWire.requireOwnedColumn(this.#schema, this.#columns, column);
84
+ this.#order.push({ column, direction });
85
+ return this;
86
+ }
87
+ /**
88
+ * Sets a positive result limit that requires ordering.
89
+ *
90
+ * @param value Positive maximum number of returned entities.
91
+ * @returns This builder.
92
+ */
93
+ limit(value) {
94
+ if (!Number.isSafeInteger(value) || value <= 0) {
95
+ throw new TypeError("Entity query limit must be a positive integer.");
96
+ }
97
+ this.#limit = value;
98
+ return this;
99
+ }
100
+ /**
101
+ * Builds the `spine.client.Query` message.
102
+ *
103
+ * @returns The wire query message.
104
+ */
105
+ build() {
106
+ if (this.#limit !== undefined && this.#order.length === 0) {
107
+ throw new TypeError("Entity query limit requires ordering.");
108
+ }
109
+ const filters = this.#filters();
110
+ const format = this.#format();
111
+ return create(QuerySchema, {
112
+ id: create(QueryIdSchema, { value: EntityQueryWire.nextId() }),
113
+ target: create(TargetSchema, {
114
+ type: EntityQueryWire.typeUrl(this.#schema),
115
+ criterion: filters === undefined
116
+ ? { case: "includeAll", value: true }
117
+ : { case: "filters", value: filters },
118
+ }),
119
+ context: clone(ActorContextSchema, this.#context),
120
+ ...(format === undefined ? {} : { format }),
121
+ });
122
+ }
123
+ /**
124
+ * Builds the storage-neutral execution plan for this typed query.
125
+ *
126
+ * @returns An immutable plan equivalent to {@link build}'s wire query.
127
+ */
128
+ buildPlan() {
129
+ if (this.#limit !== undefined && this.#order.length === 0) {
130
+ throw new TypeError("Entity query limit requires ordering.");
131
+ }
132
+ const predicates = EntityQueryCompiler.compilePlanGroups(this.#predicates, this.#schema, this.#columns);
133
+ const all = [
134
+ ...(this.#ids.length === 0
135
+ ? []
136
+ : [Object.freeze({ kind: "ids", ids: Object.freeze([...this.#ids]) })]),
137
+ ...predicates,
138
+ ];
139
+ const predicate = all.length === 0
140
+ ? undefined
141
+ : all.length === 1
142
+ ? all[0]
143
+ : Object.freeze({ kind: "all", predicates: Object.freeze(all) });
144
+ return Object.freeze({
145
+ ...(predicate === undefined ? {} : { predicate }),
146
+ ...(this.#order.length === 0
147
+ ? {}
148
+ : {
149
+ order: Object.freeze(this.#order.map(({ column, direction }) => Object.freeze({ column: column.name, direction }))),
150
+ }),
151
+ ...(this.#limit === undefined ? {} : { limit: this.#limit }),
152
+ });
153
+ }
154
+ #filters() {
155
+ const idField = this.#schema.fields[0];
156
+ if (this.#ids.length > 0 && idField === undefined) {
157
+ throw new TypeError("Entity query target has no ID field.");
158
+ }
159
+ const predicates = EntityQueryCompiler.compileGroups(this.#predicates, this.#schema, this.#columns);
160
+ if (this.#ids.length === 0 && predicates.length === 0)
161
+ return undefined;
162
+ return create(TargetFiltersSchema, {
163
+ ...(this.#ids.length === 0
164
+ ? {}
165
+ : {
166
+ idFilter: create(IdFilterSchema, {
167
+ id: this.#ids.map((id) => EntityQueryWire.packField(idField, id)),
168
+ }),
169
+ }),
170
+ filter: [...predicates],
171
+ });
172
+ }
173
+ /**
174
+ * Builds wire ordering and limit settings when the query uses them.
175
+ */
176
+ #format() {
177
+ if (this.#order.length === 0 && this.#limit === undefined) {
178
+ return undefined;
179
+ }
180
+ return create(ResponseFormatSchema, {
181
+ orderBy: this.#order.map(({ column, direction }) => create(OrderBySchema, {
182
+ column: column.name,
183
+ direction: direction === "desc" ? OrderBy_Direction.DESCENDING : OrderBy_Direction.ASCENDING,
184
+ })),
185
+ limit: this.#limit ?? 0,
186
+ });
187
+ }
188
+ }
189
+ /**
190
+ * Context-free Entity query that can be executed by different actor and tenant scopes.
191
+ *
192
+ * @typeParam Schema Generated state schema selected by this query.
193
+ * @typeParam Id Identifier type declared by the state's first field.
194
+ */
195
+ export class EntityQueryDescription {
196
+ #wire;
197
+ #plan;
198
+ #schema;
199
+ /**
200
+ * Captures the compiled wire query and storage plan without execution context.
201
+ *
202
+ * @param schema Generated state schema selected by the query.
203
+ * @param wire Compiled wire query without actor context or request ID.
204
+ * @param plan Compiled storage-neutral query plan.
205
+ */
206
+ constructor(schema, wire, plan) {
207
+ this.#schema = schema;
208
+ this.#wire = toBinary(QuerySchema, wire);
209
+ this.#plan = queryHost.structuredClone(plan);
210
+ Object.freeze(this);
211
+ }
212
+ /**
213
+ * Returns the generated state schema selected by this query.
214
+ *
215
+ * @returns Generated state schema.
216
+ */
217
+ get schema() {
218
+ return this.#schema;
219
+ }
220
+ /**
221
+ * Creates a fresh wire query without an actor or tenant context.
222
+ *
223
+ * @returns Wire query with a new request ID.
224
+ */
225
+ build() {
226
+ const wire = fromBinary(QuerySchema, this.#wire);
227
+ wire.id = create(QueryIdSchema, { value: EntityQueryWire.nextId() });
228
+ return wire;
229
+ }
230
+ /**
231
+ * Returns a detached storage-neutral plan for local execution.
232
+ *
233
+ * @returns Independent copy of the compiled plan.
234
+ */
235
+ buildPlan() {
236
+ return queryHost.structuredClone(this.#plan);
237
+ }
238
+ }
239
+ /**
240
+ * Builds an Entity query without selecting an actor or tenant.
241
+ *
242
+ * @typeParam Schema Generated Entity state schema.
243
+ * @typeParam Columns Registered columns for that schema.
244
+ * @typeParam Name Local name of the canonical first identifier field.
245
+ */
246
+ export class EntityQueryDraft {
247
+ #schema;
248
+ #builder;
249
+ /**
250
+ * Creates a context-free draft for one registered Entity state.
251
+ *
252
+ * @param input State schema, columns, and canonical identifier field name.
253
+ */
254
+ constructor(input) {
255
+ if (input.schema.fields[0]?.localName !== input.idField) {
256
+ throw new TypeError("Entity query ID field must be the state's first declared field.");
257
+ }
258
+ this.#schema = input.schema;
259
+ this.#builder = new EntityQueryBuilder({
260
+ schema: input.schema,
261
+ columns: input.columns,
262
+ context: create(ActorContextSchema),
263
+ });
264
+ }
265
+ /**
266
+ * Adds typed identifiers to the query target.
267
+ *
268
+ * @param ids Identifier values declared by the first state field.
269
+ * @returns This draft.
270
+ */
271
+ byId(...ids) {
272
+ this.#builder.byId(...ids);
273
+ return this;
274
+ }
275
+ /**
276
+ * Adds a typed comparison or group predicate.
277
+ *
278
+ * @typeParam Predicate Predicate checked against the registered columns.
279
+ * @param predicate Predicate to append with conjunction.
280
+ * @returns This draft.
281
+ */
282
+ where(predicate) {
283
+ this.#builder.where(predicate);
284
+ return this;
285
+ }
286
+ /**
287
+ * Adds ordering by a registered ordered column.
288
+ *
289
+ * @typeParam Column Registered ordered column type.
290
+ * @param column Ordered column.
291
+ * @param direction Ascending or descending direction.
292
+ * @returns This draft.
293
+ */
294
+ orderBy(column, direction = "asc") {
295
+ this.#builder.orderBy(column, direction);
296
+ return this;
297
+ }
298
+ /**
299
+ * Sets a positive result limit that requires ordering.
300
+ *
301
+ * @param value Maximum number of matching states.
302
+ * @returns This draft.
303
+ */
304
+ limit(value) {
305
+ this.#builder.limit(value);
306
+ return this;
307
+ }
308
+ /**
309
+ * Builds an independent query description for later execution.
310
+ *
311
+ * @returns Context-free query value with detached wire and plan copies.
312
+ */
313
+ build() {
314
+ const wire = this.#builder.build();
315
+ wire.context = undefined;
316
+ wire.id = undefined;
317
+ return new EntityQueryDescription(this.#schema, wire, this.#builder.buildPlan());
318
+ }
319
+ }
320
+ /**
321
+ * Creates typed predicates and builders for Entity queries.
322
+ */
323
+ export const EntityQuery = Object.freeze({
324
+ /**
325
+ * Creates a validated equality comparison for a registered column.
326
+ *
327
+ * @typeParam Column Column type used by the comparison.
328
+ * @param column Column to compare.
329
+ * @param value Value to match.
330
+ * @returns Immutable equality predicate.
331
+ */
332
+ eq(column, value) {
333
+ return EntityQueryCompiler.comparison(column, "equal", value);
334
+ },
335
+ /**
336
+ * Creates a validated exclusive lower-bound comparison.
337
+ *
338
+ * @typeParam Column Ordered column type used by the comparison.
339
+ * @param column Ordered column to compare.
340
+ * @param value Exclusive lower bound.
341
+ * @returns Immutable greater-than predicate.
342
+ */
343
+ gt(column, value) {
344
+ return EntityQueryCompiler.comparison(column, "greaterThan", value);
345
+ },
346
+ /**
347
+ * Creates a validated exclusive upper-bound comparison.
348
+ *
349
+ * @typeParam Column Ordered column type used by the comparison.
350
+ * @param column Ordered column to compare.
351
+ * @param value Exclusive upper bound.
352
+ * @returns Immutable less-than predicate.
353
+ */
354
+ lt(column, value) {
355
+ return EntityQueryCompiler.comparison(column, "lessThan", value);
356
+ },
357
+ /**
358
+ * Creates a validated inclusive lower-bound comparison.
359
+ *
360
+ * @typeParam Column Ordered column type used by the comparison.
361
+ * @param column Ordered column to compare.
362
+ * @param value Inclusive lower bound.
363
+ * @returns Immutable greater-or-equal predicate.
364
+ */
365
+ ge(column, value) {
366
+ return EntityQueryCompiler.comparison(column, "greaterOrEqual", value);
367
+ },
368
+ /**
369
+ * Creates a validated inclusive upper-bound comparison.
370
+ *
371
+ * @typeParam Column Ordered column type used by the comparison.
372
+ * @param column Ordered column to compare.
373
+ * @param value Inclusive upper bound.
374
+ * @returns Immutable less-or-equal predicate.
375
+ */
376
+ le(column, value) {
377
+ return EntityQueryCompiler.comparison(column, "lessOrEqual", value);
378
+ },
379
+ /**
380
+ * Combines a required predicate and the remaining predicates with conjunction.
381
+ *
382
+ * @typeParam First Type of the required first predicate.
383
+ * @typeParam Rest Tuple of additional predicate types.
384
+ * @param first Required first predicate.
385
+ * @param rest Additional predicates in caller order.
386
+ * @returns Immutable conjunction group.
387
+ */
388
+ all(first, ...rest) {
389
+ return EntityQueryCompiler.group("all", first, rest);
390
+ },
391
+ /**
392
+ * Combines a required predicate and the remaining predicates with disjunction.
393
+ *
394
+ * @typeParam First Type of the required first predicate.
395
+ * @typeParam Rest Tuple of additional predicate types.
396
+ * @param first Required first predicate.
397
+ * @param rest Additional predicates in caller order.
398
+ * @returns Immutable disjunction group.
399
+ */
400
+ either(first, ...rest) {
401
+ return EntityQueryCompiler.group("either", first, rest);
402
+ },
403
+ /**
404
+ * Creates a query builder bound to a state schema, its columns, and actor context.
405
+ *
406
+ * @typeParam Schema Generated Entity state schema.
407
+ * @typeParam Columns Registered columns for that schema.
408
+ * @param input Schema, columns, and actor context for the query.
409
+ * @returns A mutable typed query builder.
410
+ */
411
+ select(input) {
412
+ return new EntityQueryBuilder(input);
413
+ },
414
+ /**
415
+ * Creates a context-free draft bound to the state's first identifier field.
416
+ *
417
+ * @typeParam Schema Generated Entity state schema.
418
+ * @typeParam Columns Registered columns for that schema.
419
+ * @typeParam Name Local name of the state's first identifier field.
420
+ * @param input State schema, columns, and canonical identifier field name.
421
+ * @returns New context-free query draft.
422
+ */
423
+ describe(input) {
424
+ return new EntityQueryDraft(input);
425
+ },
426
+ });
427
+ /**
428
+ * Internal compiler for predicates, descriptors, and wire query messages.
429
+ */
430
+ const EntityQueryCompiler = Object.freeze({
431
+ /**
432
+ * Validates predicate graphs and compiles them to storage-neutral plan nodes.
433
+ *
434
+ * @typeParam Schema Generated Entity state schema.
435
+ * @param roots Top-level predicates in query order.
436
+ * @param schema State schema selected by the query.
437
+ * @param columns Registered columns allowed in comparisons.
438
+ * @returns Validated immutable plan predicates.
439
+ */
440
+ compilePlanGroups(roots, schema, columns) {
441
+ // Reuse the iterative wire traversal as the single validation boundary. The
442
+ // storage plan has the same predicate graph but must not recurse before
443
+ // cycle, depth, and breadth limits have been checked.
444
+ EntityQueryCompiler.compileGroups(roots, schema, columns);
445
+ return roots.map((predicate) => EntityQueryCompiler.compilePlanPredicate(predicate, schema, columns));
446
+ },
447
+ /**
448
+ * Converts a validated predicate tree to a storage-neutral plan node.
449
+ *
450
+ * @typeParam Schema Generated Entity state schema.
451
+ * @param value Predicate tree to convert.
452
+ * @param schema State schema selected by the query.
453
+ * @param columns Registered columns allowed in comparisons.
454
+ * @returns Immutable comparison or logical group plan.
455
+ */
456
+ compilePlanPredicate(value, schema, columns) {
457
+ const predicate = EntityQueryWire.requirePredicate(value);
458
+ if (predicate.kind === "comparison") {
459
+ EntityQueryWire.requireOwnedColumn(schema, columns, predicate.column);
460
+ EntityQueryWire.requireValue(predicate.column, predicate.value);
461
+ return Object.freeze({
462
+ kind: "comparison",
463
+ column: predicate.column.name,
464
+ operator: predicate.operator,
465
+ value: predicate.value,
466
+ });
467
+ }
468
+ if (predicate.predicates.length === 0) {
469
+ throw new TypeError(`${predicate.kind.toUpperCase()} predicate must not be empty.`);
470
+ }
471
+ return Object.freeze({
472
+ kind: predicate.kind,
473
+ predicates: Object.freeze(predicate.predicates.map((child) => EntityQueryCompiler.compilePlanPredicate(child, schema, columns))),
474
+ });
475
+ },
476
+ /**
477
+ * Validates a comparison operator and value before freezing its predicate.
478
+ *
479
+ * @typeParam Column Column type used by the comparison.
480
+ * @param column Column that supports the selected operator.
481
+ * @param operator Comparison supported by the column.
482
+ * @param value Typed comparison value.
483
+ * @returns Immutable comparison predicate.
484
+ */
485
+ comparison(column, operator, value) {
486
+ if (!column.operators.includes(operator)) {
487
+ throw new TypeError(`Entity column "${column.name}" does not support ${operator}.`);
488
+ }
489
+ EntityQueryWire.requireValue(column, value);
490
+ return Object.freeze({ kind: "comparison", column, operator, value });
491
+ },
492
+ /**
493
+ * Builds an immutable conjunction or disjunction in caller order.
494
+ *
495
+ * @typeParam First Type of the required first predicate.
496
+ * @typeParam Rest Tuple of additional predicate types.
497
+ * @param kind Logical operation for the group.
498
+ * @param first Required first predicate.
499
+ * @param rest Additional predicates.
500
+ * @returns Immutable typed predicate group.
501
+ */
502
+ group(kind, first, rest) {
503
+ return Object.freeze({
504
+ kind,
505
+ predicates: Object.freeze([first, ...rest]),
506
+ });
507
+ },
508
+ /**
509
+ * Validates a predicate graph and encodes it as wire composite filters.
510
+ *
511
+ * @typeParam Schema Generated Entity state schema.
512
+ * @param roots Top-level predicates in query order.
513
+ * @param schema State schema selected by the query.
514
+ * @param columns Registered columns allowed in comparisons.
515
+ * @returns Composite filters for the wire query.
516
+ */
517
+ compileGroups(roots, schema, columns) {
518
+ const compiled = new WeakMap();
519
+ const seen = new WeakSet();
520
+ const pending = [];
521
+ let scheduled = 0;
522
+ for (let index = roots.length - 1; index >= 0; index -= 1) {
523
+ pending.push({ predicate: roots[index], depth: 0, expanded: false });
524
+ scheduled += 1;
525
+ }
526
+ while (pending.length > 0) {
527
+ const current = pending.pop();
528
+ if (current === undefined)
529
+ break;
530
+ const predicate = EntityQueryWire.requirePredicate(current.predicate);
531
+ if (current.expanded) {
532
+ if (predicate.kind === "comparison") {
533
+ throw new TypeError("Entity query predicate compilation state is invalid.");
534
+ }
535
+ const simple = [];
536
+ const nested = [];
537
+ for (const child of predicate.predicates) {
538
+ const childResult = compiled.get(child);
539
+ if (childResult === undefined)
540
+ throw new TypeError("Entity query predicate is incomplete.");
541
+ if (childResult.kind === "comparison")
542
+ simple.push(childResult.filter);
543
+ else
544
+ nested.push(childResult.filter);
545
+ }
546
+ compiled.set(predicate, {
547
+ kind: "group",
548
+ filter: create(CompositeFilterSchema, {
549
+ operator: predicate.kind === "either"
550
+ ? CompositeFilter_CompositeOperator.EITHER
551
+ : CompositeFilter_CompositeOperator.ALL,
552
+ filter: simple,
553
+ compositeFilter: nested,
554
+ }),
555
+ });
556
+ continue;
557
+ }
558
+ if (seen.has(predicate))
559
+ throw new TypeError("Entity query predicate must not contain cycles.");
560
+ seen.add(predicate);
561
+ if (current.depth > maximumPredicateDepth) {
562
+ throw new TypeError(`Entity query predicate exceeds maximum depth ${String(maximumPredicateDepth)}.`);
563
+ }
564
+ if (predicate.kind === "comparison") {
565
+ EntityQueryWire.requireOwnedColumn(schema, columns, predicate.column);
566
+ compiled.set(predicate, {
567
+ kind: "comparison",
568
+ filter: EntityQueryWire.compileComparison(predicate),
569
+ });
570
+ continue;
571
+ }
572
+ if (predicate.predicates.length === 0) {
573
+ throw new TypeError(`${predicate.kind.toUpperCase()} predicate must not be empty.`);
574
+ }
575
+ if (scheduled + predicate.predicates.length > maximumPredicateNodes) {
576
+ throw new TypeError(`Entity query predicate exceeds maximum node count ${String(maximumPredicateNodes)}.`);
577
+ }
578
+ scheduled += predicate.predicates.length;
579
+ pending.push({ ...current, expanded: true });
580
+ for (let index = predicate.predicates.length - 1; index >= 0; index -= 1) {
581
+ if (!Object.hasOwn(predicate.predicates, index)) {
582
+ throw new TypeError("Entity query predicate entries must be defined.");
583
+ }
584
+ pending.push({
585
+ predicate: predicate.predicates[index],
586
+ depth: current.depth + 1,
587
+ expanded: false,
588
+ });
589
+ }
590
+ }
591
+ return roots.map((root) => {
592
+ const result = compiled.get(root);
593
+ if (result === undefined)
594
+ throw new TypeError("Entity query predicate is incomplete.");
595
+ return result.kind === "group"
596
+ ? result.filter
597
+ : create(CompositeFilterSchema, {
598
+ operator: CompositeFilter_CompositeOperator.ALL,
599
+ filter: [result.filter],
600
+ });
601
+ });
602
+ },
603
+ });
604
+ /**
605
+ * Validates and compiles Entity query details.
606
+ */
607
+ const EntityQueryWire = Object.freeze({
608
+ /**
609
+ * Checks the shape and kind of a predicate supplied at runtime.
610
+ *
611
+ * @param value Value to validate as a predicate.
612
+ * @returns Validated comparison or group predicate.
613
+ */
614
+ requirePredicate(value) {
615
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
616
+ throw new TypeError("Entity query predicate must be an object.");
617
+ }
618
+ const predicate = value;
619
+ if (predicate.kind === "comparison") {
620
+ if (!(predicate.column instanceof EntityColumn)) {
621
+ throw new TypeError("Entity query comparison column is required.");
622
+ }
623
+ return predicate;
624
+ }
625
+ if (predicate.kind !== "all" && predicate.kind !== "either") {
626
+ throw new TypeError("Entity query predicate kind must be recognized.");
627
+ }
628
+ if (!Array.isArray(predicate.predicates)) {
629
+ throw new TypeError(`${predicate.kind.toUpperCase()} predicate predicates must be an array.`);
630
+ }
631
+ return predicate;
632
+ },
633
+ /**
634
+ * Encodes a leaf comparison as a wire filter.
635
+ *
636
+ * @param predicate Validated column comparison.
637
+ * @returns Wire filter with packed value and operator.
638
+ */
639
+ compileComparison(predicate) {
640
+ return create(FilterSchema, {
641
+ fieldPath: { fieldName: [predicate.column.name] },
642
+ value: EntityQueryWire.packColumn(predicate.column, predicate.value),
643
+ operator: EntityQueryWire.wireOperator(predicate.operator),
644
+ });
645
+ },
646
+ /**
647
+ * Maps a supported column comparison to its wire enum value.
648
+ *
649
+ * @param operator Comparison operator to encode.
650
+ * @returns Matching wire filter operator.
651
+ */
652
+ wireOperator(operator) {
653
+ switch (operator) {
654
+ case "equal":
655
+ return Filter_Operator.EQUAL;
656
+ case "greaterThan":
657
+ return Filter_Operator.GREATER_THAN;
658
+ case "lessThan":
659
+ return Filter_Operator.LESS_THAN;
660
+ case "greaterOrEqual":
661
+ return Filter_Operator.GREATER_OR_EQUAL;
662
+ case "lessOrEqual":
663
+ return Filter_Operator.LESS_OR_EQUAL;
664
+ default:
665
+ throw new TypeError("Entity query comparison operator is not recognized.");
666
+ }
667
+ },
668
+ /**
669
+ * Packs a system or application column value into a wire Any message.
670
+ *
671
+ * @param column Column whose value is being encoded.
672
+ * @param value Typed comparison value.
673
+ * @returns Packed wire value for the column.
674
+ */
675
+ packColumn(column, value) {
676
+ if (column.source === "system") {
677
+ if (column.name === "version")
678
+ return EntityQueryWire.packMessage(VersionSchema, value);
679
+ return EntityQueryWire.packMessage(BoolValueSchema, create(BoolValueSchema, { value: value }));
680
+ }
681
+ if (column.descriptor === undefined) {
682
+ throw new TypeError("Entity query application column descriptor is required.");
683
+ }
684
+ return EntityQueryWire.packField(column.descriptor, value);
685
+ },
686
+ /**
687
+ * Packs a descriptor field value using its message, enum, or scalar wrapper.
688
+ *
689
+ * @param field Descriptor for the compared state field.
690
+ * @param value Field value to encode.
691
+ * @returns Packed wire value for the field.
692
+ */
693
+ packField(field, value) {
694
+ if (field === undefined)
695
+ throw new TypeError("Entity query field descriptor is required.");
696
+ EntityQueryWire.requireFieldValue(field, value);
697
+ if (field.fieldKind === "message") {
698
+ return EntityQueryWire.packMessage(field.message, value);
699
+ }
700
+ if (field.fieldKind === "enum") {
701
+ return EntityQueryWire.packMessage(Int32ValueSchema, create(Int32ValueSchema, { value: value }));
702
+ }
703
+ if (field.fieldKind !== "scalar")
704
+ throw new TypeError("Entity query field kind is unsupported.");
705
+ const schema = EntityQueryWire.scalarSchema(field.scalar);
706
+ return EntityQueryWire.packMessage(schema, create(schema, { value }));
707
+ },
708
+ /**
709
+ * Checks an ID or column operand against its declared Protobuf field kind.
710
+ *
711
+ * @param field Declared state field used by the query.
712
+ * @param value Operand to validate before wire packing.
713
+ */
714
+ requireFieldValue(field, value) {
715
+ const facts = EntityFieldClassification.classify(field);
716
+ if (!facts.supported)
717
+ throw new TypeError(`Entity query field "${field.name}" is unsupported.`);
718
+ const valid = facts.valueKind === "message"
719
+ ? typeof value === "object" &&
720
+ value !== null &&
721
+ Reflect.get(value, "$typeName") === facts.messageType
722
+ : facts.valueKind === "bytes"
723
+ ? value instanceof Uint8Array
724
+ : facts.valueKind === "enum" || facts.valueKind === "number"
725
+ ? typeof value === "number" && Number.isFinite(value)
726
+ : typeof value === facts.valueKind;
727
+ if (!valid)
728
+ throw new TypeError(`Entity query value for "${field.name}" has the wrong type.`);
729
+ },
730
+ /**
731
+ * Returns the Protobuf wrapper schema for a scalar field type.
732
+ *
733
+ * @param scalar Protobuf scalar type of the field.
734
+ * @returns Wrapper schema used for its query value.
735
+ */
736
+ scalarSchema(scalar) {
737
+ switch (scalar) {
738
+ case ScalarType.BOOL:
739
+ return BoolValueSchema;
740
+ case ScalarType.BYTES:
741
+ return BytesValueSchema;
742
+ case ScalarType.DOUBLE:
743
+ return DoubleValueSchema;
744
+ case ScalarType.FLOAT:
745
+ return FloatValueSchema;
746
+ case ScalarType.INT64:
747
+ case ScalarType.SFIXED64:
748
+ case ScalarType.SINT64:
749
+ return Int64ValueSchema;
750
+ case ScalarType.UINT64:
751
+ case ScalarType.FIXED64:
752
+ return UInt64ValueSchema;
753
+ case ScalarType.UINT32:
754
+ case ScalarType.FIXED32:
755
+ return UInt32ValueSchema;
756
+ case ScalarType.STRING:
757
+ return StringValueSchema;
758
+ default:
759
+ return Int32ValueSchema;
760
+ }
761
+ },
762
+ /**
763
+ * Verifies that a column is registered for the selected state schema.
764
+ *
765
+ * @typeParam Schema Generated Entity state schema.
766
+ * @param schema State schema selected by the query.
767
+ * @param columns Registered columns for that schema.
768
+ * @param column Column referenced by a predicate or order clause.
769
+ */
770
+ requireOwnedColumn(schema, columns, column) {
771
+ if (column.schema !== schema ||
772
+ !Object.values(columns).some((candidate) => candidate === column)) {
773
+ throw new TypeError("Entity query column does not belong to the selected target.");
774
+ }
775
+ },
776
+ /**
777
+ * Validates a comparison value against its column's declared value kind.
778
+ *
779
+ * @param column Column that declares the required value kind.
780
+ * @param value Comparison value to validate.
781
+ */
782
+ requireValue(column, value) {
783
+ const valid = value !== undefined &&
784
+ (column.valueKind === "message"
785
+ ? typeof value === "object" &&
786
+ value !== null &&
787
+ Reflect.get(value, "$typeName") === column.messageType
788
+ : column.valueKind === "bytes"
789
+ ? value instanceof Uint8Array
790
+ : column.valueKind === "enum" || column.valueKind === "number"
791
+ ? typeof value === "number" && Number.isFinite(value)
792
+ : typeof value === column.valueKind);
793
+ if (!valid)
794
+ throw new TypeError(`Entity query value for "${column.name}" has the wrong type.`);
795
+ },
796
+ /**
797
+ * Finds a generated field by wire or local name.
798
+ *
799
+ * @param schema Message schema to search.
800
+ * @param name Wire or local field name.
801
+ * @returns Matching field descriptor, if present.
802
+ */
803
+ findField(schema, name) {
804
+ return schema.fields.find((field) => field.name === name || field.localName === name);
805
+ },
806
+ /**
807
+ * Creates a time-prefixed random identifier for a wire query.
808
+ *
809
+ * @returns New query ID string.
810
+ */
811
+ nextId() {
812
+ return `query-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
813
+ },
814
+ /**
815
+ * Builds a type URL from a schema's configured prefix and type name.
816
+ *
817
+ * @param schema Message schema whose URL is required.
818
+ * @returns Fully qualified Protobuf type URL.
819
+ */
820
+ typeUrl(schema) {
821
+ const prefix = hasOption(schema.file, type_url_prefix)
822
+ ? getOption(schema.file, type_url_prefix)
823
+ : "type.googleapis.com";
824
+ return `${prefix.replace(/\/+$/u, "")}/${schema.typeName}`;
825
+ },
826
+ /**
827
+ * Serializes a message value into a wire Any without unknown fields.
828
+ *
829
+ * @typeParam Schema Generated schema of the message value.
830
+ * @param schema Schema used to serialize the value.
831
+ * @param value Message value matching the schema.
832
+ * @returns Any message containing the type URL and binary value.
833
+ */
834
+ packMessage(schema, value) {
835
+ return create(AnySchema, {
836
+ typeUrl: EntityQueryWire.typeUrl(schema),
837
+ value: toBinary(schema, value, { writeUnknownFields: false }),
838
+ });
839
+ },
840
+ });
841
+ //# sourceMappingURL=entity-query.js.map