@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
package/README.md CHANGED
@@ -5,13 +5,37 @@ Spine TS application. Use it when application code needs to validate a message,
5
5
  pack it into `google.protobuf.Any`, create a command or event envelope, or look
6
6
  up a generated message schema by its Spine type URL.
7
7
 
8
+ This is an experimental snapshot package. Use Node 24 or newer and generated
9
+ Spine message schemas.
10
+
11
+ ## Install and validate one message
12
+
13
+ Install the package with the generated model schemas your application uses:
14
+
15
+ ```sh
16
+ pnpm add @spine-event-engine/core@snapshot @spine-event-engine/proto@snapshot @bufbuild/protobuf
17
+ ```
18
+
19
+ Then validate and pack one generated message before adding routing or server assembly:
20
+
21
+ ```ts
22
+ import { create } from "@bufbuild/protobuf";
23
+ import { AnyMessages, Validate } from "@spine-event-engine/core";
24
+ import { UserIdSchema } from "@spine-event-engine/proto";
25
+
26
+ const userId = create(UserIdSchema, { value: "ava" });
27
+ Validate.check(UserIdSchema, userId);
28
+ const packed = AnyMessages.pack(UserIdSchema, userId);
29
+ console.log(AnyMessages.unpack(packed, UserIdSchema)?.value);
30
+ ```
31
+
8
32
  ## Message-interface tokens
9
33
 
10
34
  A generated interface export has one name in two TypeScript namespaces: use it
11
35
  as a type for message shape and as a value token in a repository `.route(...)`
12
36
  call. The To-Do `TaskEvent` token groups task events; its authored
13
37
  `TaskAssignmentEvent` counterpart groups assignment events. Start with the
14
- [To-Do walkthrough](../../examples/todo/USER_GUIDE.md) for the complete path.
38
+ [To-Do walkthrough](https://github.com/SpineEventEngine/spine-ts/blob/master/examples/todo/USER_GUIDE.md) for the complete path.
15
39
 
16
40
  For the detailed contract and integration notes, see
17
41
  [REFERENCE documentation for agents](REFERENCE.md).
@@ -30,9 +54,8 @@ pnpm typecheck:build
30
54
  ```
31
55
 
32
56
  Run this workspace-wide TypeScript build from the repository root. For an
33
- experimental npm consumer, install
34
- `@spine-event-engine/core@2.0.0-snapshot.2` or the explicit
35
- `@spine-event-engine/core@snapshot` tag.
57
+ experimental npm consumer, install `@spine-event-engine/core@snapshot`.
58
+ The snapshot tag can change before a stable release.
36
59
 
37
60
  ## ✅ Validate a message
38
61
 
@@ -115,6 +138,35 @@ that generated `BoardId`. An application can register another reversible
115
138
  mapping in `StringifierRegistry`. If compact Proto JSON encounters an `Any`,
116
139
  also call `setTypeRegistry()` with the application's generated `TypeRegistry`.
117
140
 
141
+ ## Query Projection state
142
+
143
+ Normal `spine-proto generate` emits a `_query.ts` companion for eligible Aggregate, Projection,
144
+ and Process Manager states. For example, Todo can import its generated `TaskListQuery` and build
145
+ a context-free query:
146
+
147
+ <!-- docs-snippet-path: examples/todo/src/index.ts -->
148
+
149
+ ```ts
150
+ import { TaskListQuery } from "../generated/spine/examples/todo/task_list_query.js";
151
+
152
+ // Find nonempty lists, starting with the most open tasks.
153
+ const query = TaskListQuery.create()
154
+ .openTaskCount()
155
+ .isAtLeast(1)
156
+ .orderBy("openTaskCount", "desc")
157
+ .limit(10)
158
+ .build();
159
+ ```
160
+
161
+ The companion registers its descriptor-backed columns when imported. Successive comparisons use
162
+ AND; `either(...)` accepts synchronous condition-only callbacks for OR branches. Apply IDs,
163
+ ordering, limits, and `build()` to the outer query. `byId(...)` uses the first state field.
164
+ `limit()` requires `orderBy()` and accepts a positive integer.
165
+ Ordered columns also expose `isGreaterThan`, `isAtLeast`, `isLessThan`, and `isAtMost`.
166
+ Every build captures a detached value;
167
+ the client or Process Manager supplies actor and tenant context when it executes the query.
168
+ `EntityQuery` remains available for callers using the existing schema-and-columns builder.
169
+
118
170
  ## 🚫 Throw a generated domain rejection
119
171
 
120
172
  The model generator creates typed rejection factories for top-level messages in
@@ -122,8 +174,9 @@ an application's `*rejections.proto` files. Application code imports that
122
174
  generated companion and throws its factory result. This example is from a
123
175
  source file in the Todo model package's `src` directory.
124
176
 
177
+ <!-- docs-snippet-path: examples/todo/src/index.ts -->
178
+
125
179
  ```ts
126
- // docs-snippet-path: examples/todo/src/index.ts
127
180
  import { create } from "@bufbuild/protobuf";
128
181
  import { TaskIdSchema } from "../generated/spine/examples/todo/task_id_pb.js";
129
182
  import { TaskAlreadyDone } from "../generated/spine/examples/todo/task_rejections.js";
@@ -145,7 +198,7 @@ by hand.
145
198
 
146
199
  ## 🔗 Learn more
147
200
 
148
- - [Protobuf package](../proto/README.md)
149
- - [Model-generation tools](../proto-tools/README.md)
150
- - [Server](../server/README.md)
201
+ - [Protobuf package](https://github.com/SpineEventEngine/spine-ts/blob/master/packages/proto/README.md)
202
+ - [Model-generation tools](https://github.com/SpineEventEngine/spine-ts/blob/master/packages/proto-tools/README.md)
203
+ - [Server](https://github.com/SpineEventEngine/spine-ts/blob/master/packages/server/README.md)
151
204
  - [Reference for coding agents](REFERENCE.md)
package/REFERENCE.md CHANGED
@@ -18,6 +18,30 @@ Import from `@spine-event-engine/core`. The package exports `Validate`,
18
18
  `StringifierRegistry`, the `Stringifier` contract, and their exported input,
19
19
  result, and metadata types.
20
20
 
21
+ Descriptor-backed `EntityColumn` and `EntityQuery` behavior is canonical in
22
+ core. The generated `GeneratedEntityColumns` helper is intentionally excluded
23
+ from the root: generated model code imports it from
24
+ `@spine-event-engine/core/codegen`. `@spine-event-engine/client-node` retains
25
+ its compatibility exports and its `/codegen` forwarding entry point.
26
+
27
+ Normal Proto generation emits `_query.ts` companions for eligible Entity states, including nested
28
+ states. Their exported `StateQuery.create()` methods expose marked columns and the `version`,
29
+ `archived`, and `deleted` system columns. `build()` returns an independent query description with
30
+ no actor or tenant context; it can be reused by a Process Manager, browser client, or subscription.
31
+ Successive comparisons form a conjunction, and `either(...)` combines synchronous condition-only
32
+ callback branches as a disjunction. Apply IDs, ordering, limits, and build to the outer query.
33
+ Generated field methods that conflict with `build`, `either`, `byId`, `orderBy`,
34
+ `limit`, `create`, `constructor`, or JavaScript object methods receive a `Column` suffix; a
35
+ numeric suffix resolves a further collision. Nested query exports join message names with `_`.
36
+ The first declared state field supplies the ID type even when its name is not `id`.
37
+
38
+ ## Subscription lifecycle SPI
39
+
40
+ Framework integrations that coordinate subscription activation import
41
+ `SUBSCRIPTION_ACTIVATION_HANDSHAKE_MS` from
42
+ `@spine-event-engine/core/spi/subscription-lifecycle`. This is not an
43
+ application subscription API and is not exported from the core root.
44
+
21
45
  ## Storage value helpers
22
46
 
23
47
  `Identifiers` packs and unpacks the generated-message and supported primitive
@@ -62,9 +86,13 @@ non-empty and whitespace-free. `AnyMessages.pack()` validates unless
62
86
  Spine-aware `Any`. `unpack()` and `unpackUsing()` return `undefined` for an
63
87
  unknown/mismatched URL or malformed bytes.
64
88
 
65
- `SignalEnvelopes.command()` and `.event()` clone caller-supplied IDs and
66
- contexts and pack the supplied domain message. They do not create IDs,
67
- timestamps, actor context, tenant context, storage records, or routing data.
89
+ `SignalEnvelopes.command()` and `.event()` generate fresh secure UUID v4 IDs,
90
+ clone caller-supplied contexts, and pack the supplied domain message. They use
91
+ `crypto.randomUUID()` when available, otherwise secure `crypto.getRandomValues()`;
92
+ they fail when neither secure API exists. They do not create timestamps, actor
93
+ context, tenant context, storage records, or routing data. Existing envelope
94
+ transport and storage continue to retain their supplied IDs without validating
95
+ that those existing values have UUID format.
68
96
 
69
97
  ## Registry
70
98
 
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Generated-code-only Entity column declaration helpers.
3
+ */
4
+ export { GeneratedEntityColumns } from "../entity/entity-column.js";
5
+ export { GeneratedEntityQueries } from "../query/generated-entity-query.js";
6
+ export { EntityQueryDescription } from "../query/entity-query.js";
7
+ export type { EntityQueryDraft } from "../query/entity-query.js";
8
+ export type { GeneratedQueryBuilder } from "../query/generated-entity-query.js";
9
+ export type { EntityColumnDefinition, EntityColumnDefinitionEntry, } from "../entity/entity-column.js";
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/codegen/index.ts"],"names":[],"mappings":"AAcA;;GAEG;AACH,OAAO,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AACpE,OAAO,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAC5E,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,YAAY,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AACjE,YAAY,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAChF,YAAY,EACV,sBAAsB,EACtB,2BAA2B,GAC5B,MAAM,4BAA4B,CAAC"}
@@ -0,0 +1,20 @@
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
+ /**
15
+ * Generated-code-only Entity column declaration helpers.
16
+ */
17
+ export { GeneratedEntityColumns } from "../entity/entity-column.js";
18
+ export { GeneratedEntityQueries } from "../query/generated-entity-query.js";
19
+ export { EntityQueryDescription } from "../query/entity-query.js";
20
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/codegen/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH;;GAEG;AACH,OAAO,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AACpE,OAAO,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAC5E,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC"}
@@ -0,0 +1,158 @@
1
+ import { type DescField, type Message, type MessageShape } from "@bufbuild/protobuf";
2
+ import type { GenMessage } from "@bufbuild/protobuf/codegenv2";
3
+ import { type Version } from "@spine-event-engine/proto";
4
+ /**
5
+ * Operators available for every Entity column.
6
+ */
7
+ export type EntityEqualityOperator = "equal";
8
+ /**
9
+ * Operators available for naturally ordered Entity column values.
10
+ */
11
+ export type EntityOrderingOperator = EntityEqualityOperator | "greaterThan" | "lessThan" | "greaterOrEqual" | "lessOrEqual";
12
+ /**
13
+ * Comparison family derived from a column's Protobuf field descriptor.
14
+ */
15
+ export type EntityComparison = "equality" | "ordering";
16
+ /**
17
+ * Runtime value category derived from a column's Protobuf field descriptor.
18
+ */
19
+ export type EntityColumnValueKind = "bigint" | "boolean" | "bytes" | "enum" | "message" | "number" | "string";
20
+ /**
21
+ * Represents one generated column declaration paired with its descriptor.
22
+ */
23
+ export interface EntityColumnDefinitionEntry<Comparison extends EntityComparison = EntityComparison> {
24
+ /**
25
+ * Identifies the generated Protobuf field declared as a column.
26
+ */
27
+ readonly field: DescField;
28
+ /**
29
+ * Identifies the comparison family supported by the field.
30
+ */
31
+ readonly comparison: Comparison;
32
+ }
33
+ type StateFieldName<Schema extends GenMessage<Message>> = Exclude<keyof MessageShape<Schema>, "$typeName" | "$unknown"> & string;
34
+ type CollectionFieldName<Schema extends GenMessage<Message>> = {
35
+ [Name in StateFieldName<Schema>]: NonNullable<MessageShape<Schema>[Name]> extends readonly unknown[] ? Name : NonNullable<MessageShape<Schema>[Name]> extends object ? string extends keyof NonNullable<MessageShape<Schema>[Name]> ? Name : never : never;
36
+ }[StateFieldName<Schema>];
37
+ type SupportedStateFieldName<Schema extends GenMessage<Message>> = Exclude<StateFieldName<Schema> & keyof Schema["field"], CollectionFieldName<Schema>>;
38
+ type EntityColumnEntries = Readonly<Record<string, EntityColumnDefinitionEntry>>;
39
+ type SupportedEntryConstraint<Schema extends GenMessage<Message>, Entries extends EntityColumnEntries> = Exclude<keyof Entries, SupportedStateFieldName<Schema>> extends never ? unknown : never;
40
+ /**
41
+ * Brands generated column definitions for nominal type safety.
42
+ */
43
+ declare const generatedDefinitionBrand: unique symbol;
44
+ /**
45
+ * Describes descriptor-backed column metadata emitted next to an Entity schema.
46
+ *
47
+ * The package root exports this type, but not its value constructor. Application
48
+ * code therefore consumes generated metadata instead of authoring string keys.
49
+ */
50
+ export interface EntityColumnDefinition<Schema extends GenMessage<Message>, Entries extends EntityColumnEntries> {
51
+ /**
52
+ * Brands metadata with its owning schema and generated entries.
53
+ */
54
+ readonly [generatedDefinitionBrand]: readonly [Schema, Entries];
55
+ /**
56
+ * Stores exact generated entries, validated again during registration.
57
+ * @internal
58
+ */
59
+ readonly entries: Entries;
60
+ }
61
+ /**
62
+ * Creates immutable Entity-column metadata emitted by the companion generator.
63
+ */
64
+ export declare const GeneratedEntityColumns: Readonly<{
65
+ /**
66
+ * Creates immutable metadata for one generated Entity schema.
67
+ *
68
+ * @param schema Generated Protobuf schema that owns the declared fields.
69
+ * @param entries Generated field descriptors and their comparison families.
70
+ * @returns Metadata accepted by {@link EntityColumn.register}.
71
+ */
72
+ define<Schema extends GenMessage<Message>, const Entries extends EntityColumnEntries>(schema: Schema, entries: Entries & SupportedEntryConstraint<NoInfer<Schema>, Entries>): EntityColumnDefinition<Schema, Entries>;
73
+ }>;
74
+ type OperatorsFor<Entry> = Entry extends EntityColumnDefinitionEntry<"ordering"> ? EntityOrderingOperator : EntityEqualityOperator;
75
+ /**
76
+ * Represents the typed column collection returned for one generated Entity definition.
77
+ */
78
+ export type EntityColumns<Schema extends GenMessage<Message>, Entries extends EntityColumnEntries> = Readonly<{
79
+ [Name in keyof Entries & StateFieldName<Schema>]: EntityColumn<Schema, Name, MessageShape<Schema>[Name], OperatorsFor<Entries[Name]>>;
80
+ } & {
81
+ /**
82
+ * Identifies the Entity version system column.
83
+ */
84
+ readonly version: EntityColumn<Schema, "version", Version>;
85
+ /**
86
+ * Identifies the Entity archived system column.
87
+ */
88
+ readonly archived: EntityColumn<Schema, "archived", boolean, EntityEqualityOperator>;
89
+ /**
90
+ * Identifies the Entity deleted system column.
91
+ */
92
+ readonly deleted: EntityColumn<Schema, "deleted", boolean, EntityEqualityOperator>;
93
+ }>;
94
+ /**
95
+ * Extracts the value type carried by an Entity column.
96
+ */
97
+ export type EntityColumnValue<Column extends EntityColumn> = Column extends EntityColumn<GenMessage<Message>, string, infer Value> ? Value : never;
98
+ /**
99
+ * Extracts the legal operator union carried by an Entity column.
100
+ */
101
+ export type EntityColumnOperator<Column extends EntityColumn> = Column extends EntityColumn<GenMessage<Message>, string, unknown, infer Operator> ? Operator : never;
102
+ /**
103
+ * An immutable, nominal, descriptor-backed Entity column.
104
+ *
105
+ * Instances can only be obtained by registering generated metadata for a
106
+ * Entity schema. This prevents consumers from constructing arbitrary
107
+ * string columns that have no corresponding Protobuf declaration.
108
+ */
109
+ export declare class EntityColumn<Schema extends GenMessage<Message> = GenMessage<Message>, Name extends string = string, Value = unknown, Operator extends EntityOrderingOperator = EntityOrderingOperator> {
110
+ private readonly entityColumnBrand;
111
+ /**
112
+ * Generated Protobuf schema that owns this column.
113
+ */
114
+ readonly schema: Schema;
115
+ /**
116
+ * Protobuf field name, or the canonical system-column name.
117
+ */
118
+ readonly name: string;
119
+ /**
120
+ * Protobuf-ES property name, or the canonical system-column name.
121
+ */
122
+ readonly localName: Name;
123
+ /**
124
+ * Whether the column came from the state descriptor or Entity storage metadata.
125
+ */
126
+ readonly source: "declared" | "system";
127
+ /**
128
+ * Exact declared field descriptor; system columns have no state field.
129
+ */
130
+ readonly descriptor: DescField | undefined;
131
+ /**
132
+ * Runtime value category derived from the descriptor.
133
+ */
134
+ readonly valueKind: EntityColumnValueKind;
135
+ /**
136
+ * Fully qualified message type for message-valued columns.
137
+ */
138
+ readonly messageType: string | undefined;
139
+ /**
140
+ * Supported comparison family.
141
+ */
142
+ readonly comparison: EntityComparison;
143
+ /**
144
+ * Exact operator set accepted for this column.
145
+ */
146
+ readonly operators: readonly Operator[];
147
+ private constructor();
148
+ /**
149
+ * Registers generated Entity-column metadata for a schema.
150
+ *
151
+ * @param schema Generated Entity schema that owns the metadata.
152
+ * @param definition Immutable generated field metadata.
153
+ * @returns Stable immutable columns for the Entity schema.
154
+ */
155
+ static register<Schema extends GenMessage<Message>, const Entries extends EntityColumnEntries>(schema: Schema, definition: EntityColumnDefinition<Schema, Entries>): EntityColumns<Schema, Entries>;
156
+ }
157
+ export {};
158
+ //# sourceMappingURL=entity-column.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entity-column.d.ts","sourceRoot":"","sources":["../../src/entity/entity-column.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,303 @@
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 { getOption, hasOption, } from "@bufbuild/protobuf";
15
+ import { column, entity, EntityOption_Kind } from "@spine-event-engine/proto";
16
+ import { EntityFieldClassification } from "../query/entity-field-classification.js";
17
+ /**
18
+ * Creates immutable Entity-column metadata emitted by the companion generator.
19
+ */
20
+ export const GeneratedEntityColumns = Object.freeze({
21
+ define(schema, entries) {
22
+ const captured = EntityColumnFacts.captureDefinition(schema, entries);
23
+ const copiedEntries = Object.fromEntries(Object.entries(entries).map(([name, entry]) => {
24
+ EntityColumnFacts.deepFreeze(entry.field);
25
+ return [name, Object.freeze({ field: entry.field, comparison: entry.comparison })];
26
+ }));
27
+ const definition = Object.freeze({
28
+ entries: Object.freeze(copiedEntries),
29
+ });
30
+ capturedDefinitions.set(definition, captured);
31
+ return definition;
32
+ },
33
+ });
34
+ const equalityOperators = Object.freeze(["equal"]);
35
+ const orderingOperators = Object.freeze([
36
+ "equal",
37
+ "greaterThan",
38
+ "lessThan",
39
+ "greaterOrEqual",
40
+ "lessOrEqual",
41
+ ]);
42
+ const systemNames = new Set(["version", "archived", "deleted"]);
43
+ const cache = new WeakMap();
44
+ const entityColumnConstructionToken = Symbol("EntityColumnConstructionToken");
45
+ const capturedDefinitions = new WeakMap();
46
+ /**
47
+ * An immutable, nominal, descriptor-backed Entity column.
48
+ *
49
+ * Instances can only be obtained by registering generated metadata for a
50
+ * Entity schema. This prevents consumers from constructing arbitrary
51
+ * string columns that have no corresponding Protobuf declaration.
52
+ */
53
+ export class EntityColumn {
54
+ /**
55
+ * Generated Protobuf schema that owns this column.
56
+ */
57
+ schema;
58
+ /**
59
+ * Protobuf field name, or the canonical system-column name.
60
+ */
61
+ name;
62
+ /**
63
+ * Protobuf-ES property name, or the canonical system-column name.
64
+ */
65
+ localName;
66
+ /**
67
+ * Whether the column came from the state descriptor or Entity storage metadata.
68
+ */
69
+ source;
70
+ /**
71
+ * Exact declared field descriptor; system columns have no state field.
72
+ */
73
+ descriptor;
74
+ /**
75
+ * Runtime value category derived from the descriptor.
76
+ */
77
+ valueKind;
78
+ /**
79
+ * Fully qualified message type for message-valued columns.
80
+ */
81
+ messageType;
82
+ /**
83
+ * Supported comparison family.
84
+ */
85
+ comparison;
86
+ /**
87
+ * Exact operator set accepted for this column.
88
+ */
89
+ operators;
90
+ constructor(input, token) {
91
+ if (token !== entityColumnConstructionToken) {
92
+ throw new TypeError("Entity columns can only be constructed during registration.");
93
+ }
94
+ this.schema = input.schema;
95
+ this.name = input.name;
96
+ this.localName = input.localName;
97
+ this.source = input.source;
98
+ this.descriptor = input.descriptor;
99
+ this.valueKind = input.valueKind;
100
+ this.messageType = input.messageType;
101
+ this.comparison = input.comparison;
102
+ this.operators = input.operators;
103
+ Object.freeze(this);
104
+ }
105
+ /**
106
+ * Registers generated Entity-column metadata for a schema.
107
+ *
108
+ * @param schema Generated Entity schema that owns the metadata.
109
+ * @param definition Immutable generated field metadata.
110
+ * @returns Stable immutable columns for the Entity schema.
111
+ */
112
+ static register(schema, definition) {
113
+ const existing = cache.get(schema);
114
+ if (existing?.definitions.has(definition) === true) {
115
+ return existing.columns;
116
+ }
117
+ const captured = capturedDefinitions.get(definition);
118
+ EntityColumnFacts.validateSchema(schema, captured);
119
+ const declared = EntityColumnFacts.validateDefinition(schema, definition, captured);
120
+ if (existing !== undefined) {
121
+ existing.definitions.add(definition);
122
+ return existing.columns;
123
+ }
124
+ const result = {};
125
+ for (const [localName, field, fieldName, metadata] of declared) {
126
+ result[localName] = new EntityColumn({
127
+ schema,
128
+ name: fieldName,
129
+ localName,
130
+ source: "declared",
131
+ descriptor: field,
132
+ valueKind: metadata.valueKind,
133
+ messageType: metadata.messageType,
134
+ comparison: metadata.comparison,
135
+ operators: EntityColumnFacts.operatorsFor(metadata.comparison),
136
+ }, entityColumnConstructionToken);
137
+ }
138
+ result.version = new EntityColumn({
139
+ schema,
140
+ name: "version",
141
+ localName: "version",
142
+ source: "system",
143
+ valueKind: "message",
144
+ messageType: "spine.core.Version",
145
+ comparison: "ordering",
146
+ operators: orderingOperators,
147
+ }, entityColumnConstructionToken);
148
+ for (const name of ["archived", "deleted"]) {
149
+ result[name] = new EntityColumn({
150
+ schema,
151
+ name,
152
+ localName: name,
153
+ source: "system",
154
+ valueKind: "boolean",
155
+ comparison: "equality",
156
+ operators: equalityOperators,
157
+ }, entityColumnConstructionToken);
158
+ }
159
+ const columns = Object.freeze(result);
160
+ cache.set(schema, {
161
+ definitions: new WeakSet([definition]),
162
+ columns,
163
+ });
164
+ return columns;
165
+ }
166
+ }
167
+ /**
168
+ * Internal descriptor facts used while generated column metadata is registered.
169
+ */
170
+ const EntityColumnFacts = Object.freeze({
171
+ // prettier-ignore
172
+ /**
173
+ * Validates that a schema declares one supported Entity kind.
174
+ */
175
+ validateSchema(schema, captured) {
176
+ const entityKind = captured?.schema === schema
177
+ ? captured.entityKind
178
+ : hasOption(schema, entity)
179
+ ? getOption(schema, entity).kind
180
+ : undefined;
181
+ if (entityKind !== EntityOption_Kind.AGGREGATE &&
182
+ entityKind !== EntityOption_Kind.PROJECTION &&
183
+ entityKind !== EntityOption_Kind.PROCESS_MANAGER) {
184
+ throw new TypeError(`Entity column schema "${schema.typeName}" must declare Entity kind.`);
185
+ }
186
+ },
187
+ /**
188
+ * Validates and describes every declared generated column.
189
+ */
190
+ validateDefinition(schema, definition, captured) {
191
+ const facts = captured?.schema === schema
192
+ ? captured
193
+ : EntityColumnFacts.captureDefinition(schema, definition.entries);
194
+ const annotated = new Map(facts.annotated);
195
+ const result = [];
196
+ for (const [localName, entry] of Object.entries(definition.entries)) {
197
+ if (systemNames.has(localName)) {
198
+ throw new TypeError(`Entity column definition cannot replace system column "${localName}".`);
199
+ }
200
+ if (entry === undefined)
201
+ continue;
202
+ const fieldFacts = facts.entries.get(localName) ?? EntityColumnFacts.captureField(entry.field);
203
+ if (fieldFacts.parent !== schema || fieldFacts.localName !== localName) {
204
+ throw new TypeError(`Entity column definition key "${localName}" must reference field "${localName}".`);
205
+ }
206
+ if (!fieldFacts.markedColumn) {
207
+ throw new TypeError(`Entity field "${localName}" is not marked (column).`);
208
+ }
209
+ const metadata = EntityColumnFacts.describeField(fieldFacts);
210
+ if (entry.comparison !== metadata.comparison) {
211
+ throw new TypeError(`Entity column "${localName}" requires ${metadata.comparison} comparison metadata.`);
212
+ }
213
+ annotated.delete(localName);
214
+ result.push([localName, fieldFacts.field, fieldFacts.name, metadata]);
215
+ }
216
+ const missing = annotated.keys().next().value;
217
+ if (missing !== undefined) {
218
+ throw new TypeError(`Entity column definition is missing annotated field "${missing}".`);
219
+ }
220
+ return result;
221
+ },
222
+ /**
223
+ * Derives runtime metadata from one validated descriptor.
224
+ */
225
+ describeField(field) {
226
+ const metadata = field.classification;
227
+ if (!metadata.supported && metadata.reason === "singular") {
228
+ throw new TypeError(`Entity column "${field.localName}" must be singular; repeated and map fields are unsupported.`);
229
+ }
230
+ if (!metadata.supported) {
231
+ throw new TypeError(`Entity column "${field.localName}" cannot belong to a oneof.`);
232
+ }
233
+ return metadata;
234
+ },
235
+ /**
236
+ * Captures immutable facts from one generated definition.
237
+ */
238
+ captureDefinition(schema, entries) {
239
+ const annotated = new Map(schema.fields
240
+ .filter((field) => hasOption(field, column) && getOption(field, column))
241
+ .map((field) => [field.localName, field]));
242
+ const capturedEntries = new Map();
243
+ for (const [localName, entry] of Object.entries(entries)) {
244
+ capturedEntries.set(localName, EntityColumnFacts.captureField(entry.field));
245
+ }
246
+ return Object.freeze({
247
+ schema,
248
+ entityKind: hasOption(schema, entity) ? getOption(schema, entity).kind : undefined,
249
+ annotated,
250
+ entries: capturedEntries,
251
+ });
252
+ },
253
+ /**
254
+ * Captures immutable facts from one generated field descriptor.
255
+ */
256
+ captureField(field) {
257
+ return Object.freeze({
258
+ field,
259
+ parent: field.parent,
260
+ localName: field.localName,
261
+ name: field.name,
262
+ markedColumn: hasOption(field, column) && getOption(field, column),
263
+ classification: Object.freeze(EntityFieldClassification.classify(field)),
264
+ });
265
+ },
266
+ /**
267
+ * Deeply freezes a descriptor graph without freezing typed-array views.
268
+ */
269
+ deepFreeze(root) {
270
+ const pending = [root];
271
+ const visited = new WeakSet();
272
+ while (pending.length > 0) {
273
+ const current = pending.pop();
274
+ if (current === undefined || visited.has(current))
275
+ continue;
276
+ visited.add(current);
277
+ if (ArrayBuffer.isView(current))
278
+ continue;
279
+ for (const key of Reflect.ownKeys(current)) {
280
+ const property = Object.getOwnPropertyDescriptor(current, key);
281
+ if (property !== undefined &&
282
+ "value" in property &&
283
+ EntityColumnFacts.isObject(property.value)) {
284
+ pending.push(property.value);
285
+ }
286
+ }
287
+ Object.freeze(current);
288
+ }
289
+ },
290
+ /**
291
+ * Checks whether a value can participate in a descriptor graph.
292
+ */
293
+ isObject(value) {
294
+ return (typeof value === "object" && value !== null) || typeof value === "function";
295
+ },
296
+ /**
297
+ * Returns the supported operator set for a comparison family.
298
+ */
299
+ operatorsFor(comparison) {
300
+ return comparison === "ordering" ? orderingOperators : equalityOperators;
301
+ },
302
+ });
303
+ //# sourceMappingURL=entity-column.js.map