@furo/open-models 1.13.0 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/custom-elements.json +941 -608
- package/dist/Registry.d.ts +7 -0
- package/dist/Registry.js +9 -0
- package/dist/Registry.js.map +1 -1
- package/dist/decorators/EntityServiceTypes.d.ts +133 -0
- package/dist/decorators/EntityServiceTypes.js +2 -0
- package/dist/decorators/EntityServiceTypes.js.map +1 -0
- package/dist/decorators/FieldBindings.d.ts +104 -0
- package/dist/decorators/FieldBindings.js +229 -0
- package/dist/decorators/FieldBindings.js.map +1 -0
- package/dist/decorators/ModelDecorators.d.ts +100 -0
- package/dist/decorators/ModelDecorators.js +227 -0
- package/dist/decorators/ModelDecorators.js.map +1 -0
- package/dist/decorators/SchemaBuilder.d.ts +15 -0
- package/dist/decorators/SchemaBuilder.js +89 -0
- package/dist/decorators/SchemaBuilder.js.map +1 -0
- package/dist/decorators/ServiceDecorators.d.ts +79 -0
- package/dist/decorators/ServiceDecorators.js +203 -0
- package/dist/decorators/ServiceDecorators.js.map +1 -0
- package/dist/decorators/defaultServiceEventHandlers.d.ts +89 -0
- package/dist/decorators/defaultServiceEventHandlers.js +100 -0
- package/dist/decorators/defaultServiceEventHandlers.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event types dispatched by FieldNode models.
|
|
3
|
+
*/
|
|
4
|
+
export type ModelEventType = "update" | "field-value-changed" | "this-field-value-changed" | "field-value-updated" | "this-state-changed" | "state-changed" | "validity-changed" | "array-changed" | "this-array-changed" | "map-changed" | "this-map-changed" | "parent-readonly-set" | "parent-readonly-unset" | "model-injected";
|
|
5
|
+
/**
|
|
6
|
+
* Event map for FieldNode model events.
|
|
7
|
+
* Most events don't have detail - they just notify that something changed.
|
|
8
|
+
*/
|
|
9
|
+
export interface ModelEventMap {
|
|
10
|
+
update: undefined;
|
|
11
|
+
"field-value-changed": unknown;
|
|
12
|
+
"this-field-value-changed": undefined;
|
|
13
|
+
"field-value-updated": unknown;
|
|
14
|
+
"this-state-changed": undefined;
|
|
15
|
+
"state-changed": unknown;
|
|
16
|
+
"validity-changed": undefined;
|
|
17
|
+
"array-changed": unknown;
|
|
18
|
+
"this-array-changed": undefined;
|
|
19
|
+
"map-changed": unknown;
|
|
20
|
+
"this-map-changed": undefined;
|
|
21
|
+
"parent-readonly-set": undefined;
|
|
22
|
+
"parent-readonly-unset": undefined;
|
|
23
|
+
"model-injected": undefined;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Interface for FieldNode-like objects that support event listening and path navigation.
|
|
27
|
+
*/
|
|
28
|
+
interface FieldNodeLike {
|
|
29
|
+
__addEventListener(type: string, listener: (e: CustomEvent) => void): void;
|
|
30
|
+
__removeEventListener(type: string, listener: (e: CustomEvent) => void): void;
|
|
31
|
+
__getFieldNodeByPath?(path: string): FieldNodeLike | undefined;
|
|
32
|
+
value?: unknown;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* ### modelBindings Factory
|
|
36
|
+
*
|
|
37
|
+
* Creates type-safe decorators bound to a specific FieldNode model.
|
|
38
|
+
* Use this to bind component properties and methods to model events.
|
|
39
|
+
*
|
|
40
|
+
* Usage:
|
|
41
|
+
* ```typescript
|
|
42
|
+
* import { modelBindings } from "@x/furo/open-models/ModelDecorators";
|
|
43
|
+
* import { CubeEntityModel } from "./CubeEntityModel";
|
|
44
|
+
*
|
|
45
|
+
* const cubeModel = modelBindings(CubeEntityModel.model);
|
|
46
|
+
*
|
|
47
|
+
* class MyComponent extends LitElement {
|
|
48
|
+
* // Bind to a nested field value - updates when cube.length changes
|
|
49
|
+
* @cubeModel.bind("cube.length")
|
|
50
|
+
* @state()
|
|
51
|
+
* private cubeLength: number = 0;
|
|
52
|
+
*
|
|
53
|
+
* // Bind to model validity
|
|
54
|
+
* @cubeModel.bind("__isValid", "validity-changed")
|
|
55
|
+
* @state()
|
|
56
|
+
* private isValid: boolean = true;
|
|
57
|
+
*
|
|
58
|
+
* // React to any field value change on the model
|
|
59
|
+
* @cubeModel.onEvent("field-value-changed")
|
|
60
|
+
* private onAnyFieldChanged() {
|
|
61
|
+
* console.log("Something changed!");
|
|
62
|
+
* }
|
|
63
|
+
*
|
|
64
|
+
* // React to a specific field's changes
|
|
65
|
+
* @cubeModel.onFieldEvent("cube.length", "this-field-value-changed")
|
|
66
|
+
* private onLengthChanged() {
|
|
67
|
+
* console.log("Length changed!");
|
|
68
|
+
* }
|
|
69
|
+
* }
|
|
70
|
+
* ```
|
|
71
|
+
*
|
|
72
|
+
* @param model - The FieldNode model to bind to
|
|
73
|
+
* @returns Object with `bind`, `onEvent`, and `onFieldEvent` decorator factories
|
|
74
|
+
*/
|
|
75
|
+
export declare function modelBindings<TEventMap extends ModelEventMap = ModelEventMap>(model: FieldNodeLike): {
|
|
76
|
+
/**
|
|
77
|
+
* Binds a component property to a model field value.
|
|
78
|
+
* When the field changes, the property is automatically updated.
|
|
79
|
+
*
|
|
80
|
+
* @param path - Path to the field (e.g., "cube.length", "__isValid")
|
|
81
|
+
* @param eventType - Event to listen for (defaults to "this-field-value-changed")
|
|
82
|
+
*/
|
|
83
|
+
bind(path: string, eventType?: ModelEventType): (target: object, propertyKey: string) => void;
|
|
84
|
+
/**
|
|
85
|
+
* Binds a method to an event on the root model.
|
|
86
|
+
* When the event fires, the method is called.
|
|
87
|
+
*
|
|
88
|
+
* @param eventType - The event type to listen for
|
|
89
|
+
*/
|
|
90
|
+
onEvent<K extends keyof TEventMap & ModelEventType>(eventType: K): (target: object, propertyKey: string, descriptor: PropertyDescriptor) => void;
|
|
91
|
+
/**
|
|
92
|
+
* Binds a method to an event on a specific field.
|
|
93
|
+
* When the event fires on that field, the method is called.
|
|
94
|
+
*
|
|
95
|
+
* @param path - Path to the field (e.g., "cube.length")
|
|
96
|
+
* @param eventType - The event type to listen for
|
|
97
|
+
*/
|
|
98
|
+
onFieldEvent<K extends keyof TEventMap & ModelEventType>(path: string, eventType: K): (target: object, propertyKey: string, descriptor: PropertyDescriptor) => void;
|
|
99
|
+
};
|
|
100
|
+
export {};
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
const bindingsMetadata = new WeakMap();
|
|
2
|
+
// Symbol keys for instance storage
|
|
3
|
+
const MODEL_BIND_LISTENERS = Symbol.for("__modelBindListeners__");
|
|
4
|
+
const MODEL_EVENT_LISTENERS = Symbol.for("__modelEventListeners__");
|
|
5
|
+
const MODEL_BIND_PATCHED = Symbol.for("__modelBindPatched__");
|
|
6
|
+
const MODEL_EVENT_PATCHED = Symbol.for("__modelEventPatched__");
|
|
7
|
+
const MODEL_EVENT_METHODS = Symbol.for("__modelEventMethods__");
|
|
8
|
+
// ─────────────────────────────────────────────────────────────────
|
|
9
|
+
// modelBindings Factory
|
|
10
|
+
// ─────────────────────────────────────────────────────────────────
|
|
11
|
+
/**
|
|
12
|
+
* ### modelBindings Factory
|
|
13
|
+
*
|
|
14
|
+
* Creates type-safe decorators bound to a specific FieldNode model.
|
|
15
|
+
* Use this to bind component properties and methods to model events.
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* ```typescript
|
|
19
|
+
* import { modelBindings } from "@x/furo/open-models/ModelDecorators";
|
|
20
|
+
* import { CubeEntityModel } from "./CubeEntityModel";
|
|
21
|
+
*
|
|
22
|
+
* const cubeModel = modelBindings(CubeEntityModel.model);
|
|
23
|
+
*
|
|
24
|
+
* class MyComponent extends LitElement {
|
|
25
|
+
* // Bind to a nested field value - updates when cube.length changes
|
|
26
|
+
* @cubeModel.bind("cube.length")
|
|
27
|
+
* @state()
|
|
28
|
+
* private cubeLength: number = 0;
|
|
29
|
+
*
|
|
30
|
+
* // Bind to model validity
|
|
31
|
+
* @cubeModel.bind("__isValid", "validity-changed")
|
|
32
|
+
* @state()
|
|
33
|
+
* private isValid: boolean = true;
|
|
34
|
+
*
|
|
35
|
+
* // React to any field value change on the model
|
|
36
|
+
* @cubeModel.onEvent("field-value-changed")
|
|
37
|
+
* private onAnyFieldChanged() {
|
|
38
|
+
* console.log("Something changed!");
|
|
39
|
+
* }
|
|
40
|
+
*
|
|
41
|
+
* // React to a specific field's changes
|
|
42
|
+
* @cubeModel.onFieldEvent("cube.length", "this-field-value-changed")
|
|
43
|
+
* private onLengthChanged() {
|
|
44
|
+
* console.log("Length changed!");
|
|
45
|
+
* }
|
|
46
|
+
* }
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* @param model - The FieldNode model to bind to
|
|
50
|
+
* @returns Object with `bind`, `onEvent`, and `onFieldEvent` decorator factories
|
|
51
|
+
*/
|
|
52
|
+
export function modelBindings(model) {
|
|
53
|
+
return {
|
|
54
|
+
/**
|
|
55
|
+
* Binds a component property to a model field value.
|
|
56
|
+
* When the field changes, the property is automatically updated.
|
|
57
|
+
*
|
|
58
|
+
* @param path - Path to the field (e.g., "cube.length", "__isValid")
|
|
59
|
+
* @param eventType - Event to listen for (defaults to "this-field-value-changed")
|
|
60
|
+
*/
|
|
61
|
+
bind(path, eventType = "this-field-value-changed") {
|
|
62
|
+
return function bindDecorator(target, propertyKey) {
|
|
63
|
+
let metadata = bindingsMetadata.get(target);
|
|
64
|
+
if (!metadata) {
|
|
65
|
+
metadata = new Map();
|
|
66
|
+
bindingsMetadata.set(target, metadata);
|
|
67
|
+
}
|
|
68
|
+
metadata.set(propertyKey, { model, path, eventType });
|
|
69
|
+
patchBindLifecycle(target.constructor);
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
/**
|
|
73
|
+
* Binds a method to an event on the root model.
|
|
74
|
+
* When the event fires, the method is called.
|
|
75
|
+
*
|
|
76
|
+
* @param eventType - The event type to listen for
|
|
77
|
+
*/
|
|
78
|
+
onEvent(eventType) {
|
|
79
|
+
return function onEventDecorator(target, propertyKey, descriptor) {
|
|
80
|
+
const originalMethod = descriptor.value;
|
|
81
|
+
const ctor = target.constructor;
|
|
82
|
+
let methods = ctor[MODEL_EVENT_METHODS];
|
|
83
|
+
if (!methods) {
|
|
84
|
+
methods = [];
|
|
85
|
+
ctor[MODEL_EVENT_METHODS] = methods;
|
|
86
|
+
}
|
|
87
|
+
methods.push({ propertyKey, model, path: null, eventType, method: originalMethod });
|
|
88
|
+
patchEventLifecycle(ctor);
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
/**
|
|
92
|
+
* Binds a method to an event on a specific field.
|
|
93
|
+
* When the event fires on that field, the method is called.
|
|
94
|
+
*
|
|
95
|
+
* @param path - Path to the field (e.g., "cube.length")
|
|
96
|
+
* @param eventType - The event type to listen for
|
|
97
|
+
*/
|
|
98
|
+
onFieldEvent(path, eventType) {
|
|
99
|
+
return function onFieldEventDecorator(target, propertyKey, descriptor) {
|
|
100
|
+
const originalMethod = descriptor.value;
|
|
101
|
+
const ctor = target.constructor;
|
|
102
|
+
let methods = ctor[MODEL_EVENT_METHODS];
|
|
103
|
+
if (!methods) {
|
|
104
|
+
methods = [];
|
|
105
|
+
ctor[MODEL_EVENT_METHODS] = methods;
|
|
106
|
+
}
|
|
107
|
+
methods.push({ propertyKey, model, path, eventType, method: originalMethod });
|
|
108
|
+
patchEventLifecycle(ctor);
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// ─────────────────────────────────────────────────────────────────
|
|
114
|
+
// Lifecycle Patching
|
|
115
|
+
// ─────────────────────────────────────────────────────────────────
|
|
116
|
+
/**
|
|
117
|
+
* Get the field node for a path, handling both direct properties and nested paths.
|
|
118
|
+
*/
|
|
119
|
+
function getFieldForPath(model, path) {
|
|
120
|
+
// Check if it's a direct property (starts with __ or no dots)
|
|
121
|
+
if (path.startsWith("__") || !path.includes(".")) {
|
|
122
|
+
return model;
|
|
123
|
+
}
|
|
124
|
+
// Navigate to nested field (we trust the field exists per user guarantee)
|
|
125
|
+
if (model.__getFieldNodeByPath) {
|
|
126
|
+
return model.__getFieldNodeByPath(path);
|
|
127
|
+
}
|
|
128
|
+
return model;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Get the value for a path from the model.
|
|
132
|
+
*/
|
|
133
|
+
function getValueForPath(model, path) {
|
|
134
|
+
if (path.startsWith("__")) {
|
|
135
|
+
// Direct property access for internal properties
|
|
136
|
+
return model[path];
|
|
137
|
+
}
|
|
138
|
+
if (!path.includes(".")) {
|
|
139
|
+
// Direct child field
|
|
140
|
+
const field = model[path];
|
|
141
|
+
return field?.value ?? field;
|
|
142
|
+
}
|
|
143
|
+
// Nested path - get the field and return its value
|
|
144
|
+
if (model.__getFieldNodeByPath) {
|
|
145
|
+
const field = model.__getFieldNodeByPath(path);
|
|
146
|
+
return field?.value ?? field;
|
|
147
|
+
}
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Patch connectedCallback/disconnectedCallback for bind decorators.
|
|
152
|
+
*/
|
|
153
|
+
function patchBindLifecycle(ctor) {
|
|
154
|
+
if (ctor[MODEL_BIND_PATCHED]) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
ctor[MODEL_BIND_PATCHED] = true;
|
|
158
|
+
const originalConnected = ctor.prototype.connectedCallback;
|
|
159
|
+
const originalDisconnected = ctor.prototype.disconnectedCallback;
|
|
160
|
+
ctor.prototype.connectedCallback = function connectedCallback() {
|
|
161
|
+
originalConnected?.call(this);
|
|
162
|
+
const metadata = bindingsMetadata.get(Object.getPrototypeOf(this));
|
|
163
|
+
if (!metadata)
|
|
164
|
+
return;
|
|
165
|
+
const listeners = new Map();
|
|
166
|
+
this[MODEL_BIND_LISTENERS] = listeners;
|
|
167
|
+
metadata.forEach(({ model, path, eventType }, propKey) => {
|
|
168
|
+
const field = getFieldForPath(model, path);
|
|
169
|
+
// Set initial value
|
|
170
|
+
this[propKey] = getValueForPath(model, path);
|
|
171
|
+
const listener = () => {
|
|
172
|
+
this[propKey] = getValueForPath(model, path);
|
|
173
|
+
};
|
|
174
|
+
listeners.set(propKey, { listener, field, eventType });
|
|
175
|
+
field.__addEventListener(eventType, listener);
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
ctor.prototype.disconnectedCallback = function disconnectedCallback() {
|
|
179
|
+
const listeners = this[MODEL_BIND_LISTENERS];
|
|
180
|
+
if (listeners) {
|
|
181
|
+
listeners.forEach(({ listener, field, eventType }) => {
|
|
182
|
+
field.__removeEventListener(eventType, listener);
|
|
183
|
+
});
|
|
184
|
+
listeners.clear();
|
|
185
|
+
}
|
|
186
|
+
originalDisconnected?.call(this);
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Patch connectedCallback/disconnectedCallback for event decorators.
|
|
191
|
+
*/
|
|
192
|
+
function patchEventLifecycle(ctor) {
|
|
193
|
+
if (ctor[MODEL_EVENT_PATCHED]) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
ctor[MODEL_EVENT_PATCHED] = true;
|
|
197
|
+
const originalConnected = ctor.prototype.connectedCallback;
|
|
198
|
+
const originalDisconnected = ctor.prototype.disconnectedCallback;
|
|
199
|
+
ctor.prototype.connectedCallback = function connectedCallback() {
|
|
200
|
+
originalConnected?.call(this);
|
|
201
|
+
const methods = this.constructor[MODEL_EVENT_METHODS];
|
|
202
|
+
if (!methods)
|
|
203
|
+
return;
|
|
204
|
+
const listeners = new Map();
|
|
205
|
+
this[MODEL_EVENT_LISTENERS] = listeners;
|
|
206
|
+
methods.forEach(({ propertyKey, model, path, eventType, method }) => {
|
|
207
|
+
// If path is specified, listen on that field; otherwise listen on root model
|
|
208
|
+
const field = path ? getFieldForPath(model, path) : model;
|
|
209
|
+
const listener = (e) => {
|
|
210
|
+
method.call(this, e.detail);
|
|
211
|
+
};
|
|
212
|
+
listeners.set(propertyKey, { listener, field, eventType });
|
|
213
|
+
field.__addEventListener(eventType, listener);
|
|
214
|
+
});
|
|
215
|
+
};
|
|
216
|
+
ctor.prototype.disconnectedCallback = function disconnectedCallback() {
|
|
217
|
+
const listeners = this[MODEL_EVENT_LISTENERS];
|
|
218
|
+
if (listeners) {
|
|
219
|
+
listeners.forEach(({ listener, field, eventType }) => {
|
|
220
|
+
field.__removeEventListener(eventType, listener);
|
|
221
|
+
});
|
|
222
|
+
listeners.clear();
|
|
223
|
+
}
|
|
224
|
+
originalDisconnected?.call(this);
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
//# sourceMappingURL=ModelDecorators.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ModelDecorators.js","sourceRoot":"","sources":["../../src/decorators/ModelDecorators.ts"],"names":[],"mappings":"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","sourcesContent":["/* eslint-disable no-param-reassign */\nimport { LitElement, ReactiveElement } from \"lit\";\n\n// ─────────────────────────────────────────────────────────────────\n// Types\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Event types dispatched by FieldNode models.\n */\nexport type ModelEventType =\n | \"update\"\n | \"field-value-changed\"\n | \"this-field-value-changed\"\n | \"field-value-updated\"\n | \"this-state-changed\"\n | \"state-changed\"\n | \"validity-changed\"\n | \"array-changed\"\n | \"this-array-changed\"\n | \"map-changed\"\n | \"this-map-changed\"\n | \"parent-readonly-set\"\n | \"parent-readonly-unset\"\n | \"model-injected\";\n\n/**\n * Event map for FieldNode model events.\n * Most events don't have detail - they just notify that something changed.\n */\nexport interface ModelEventMap {\n update: undefined;\n \"field-value-changed\": unknown;\n \"this-field-value-changed\": undefined;\n \"field-value-updated\": unknown;\n \"this-state-changed\": undefined;\n \"state-changed\": unknown;\n \"validity-changed\": undefined;\n \"array-changed\": unknown;\n \"this-array-changed\": undefined;\n \"map-changed\": unknown;\n \"this-map-changed\": undefined;\n \"parent-readonly-set\": undefined;\n \"parent-readonly-unset\": undefined;\n \"model-injected\": undefined;\n}\n\n/**\n * Interface for FieldNode-like objects that support event listening and path navigation.\n */\ninterface FieldNodeLike {\n __addEventListener(type: string, listener: (e: CustomEvent) => void): void;\n __removeEventListener(type: string, listener: (e: CustomEvent) => void): void;\n __getFieldNodeByPath?(path: string): FieldNodeLike | undefined;\n value?: unknown;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Metadata Storage\n// ─────────────────────────────────────────────────────────────────\n\ninterface BindingMeta {\n model: FieldNodeLike;\n path: string;\n eventType: ModelEventType;\n}\nconst bindingsMetadata = new WeakMap<object, Map<string, BindingMeta>>();\n\ninterface EventBindingMeta {\n propertyKey: string;\n model: FieldNodeLike;\n path: string | null;\n eventType: ModelEventType;\n // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type\n method: Function;\n}\n\n// Symbol keys for instance storage\nconst MODEL_BIND_LISTENERS = Symbol.for(\"__modelBindListeners__\");\nconst MODEL_EVENT_LISTENERS = Symbol.for(\"__modelEventListeners__\");\nconst MODEL_BIND_PATCHED = Symbol.for(\"__modelBindPatched__\");\nconst MODEL_EVENT_PATCHED = Symbol.for(\"__modelEventPatched__\");\nconst MODEL_EVENT_METHODS = Symbol.for(\"__modelEventMethods__\");\n\n// ─────────────────────────────────────────────────────────────────\n// modelBindings Factory\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * ### modelBindings Factory\n *\n * Creates type-safe decorators bound to a specific FieldNode model.\n * Use this to bind component properties and methods to model events.\n *\n * Usage:\n * ```typescript\n * import { modelBindings } from \"@x/furo/open-models/ModelDecorators\";\n * import { CubeEntityModel } from \"./CubeEntityModel\";\n *\n * const cubeModel = modelBindings(CubeEntityModel.model);\n *\n * class MyComponent extends LitElement {\n * // Bind to a nested field value - updates when cube.length changes\n * @cubeModel.bind(\"cube.length\")\n * @state()\n * private cubeLength: number = 0;\n *\n * // Bind to model validity\n * @cubeModel.bind(\"__isValid\", \"validity-changed\")\n * @state()\n * private isValid: boolean = true;\n *\n * // React to any field value change on the model\n * @cubeModel.onEvent(\"field-value-changed\")\n * private onAnyFieldChanged() {\n * console.log(\"Something changed!\");\n * }\n *\n * // React to a specific field's changes\n * @cubeModel.onFieldEvent(\"cube.length\", \"this-field-value-changed\")\n * private onLengthChanged() {\n * console.log(\"Length changed!\");\n * }\n * }\n * ```\n *\n * @param model - The FieldNode model to bind to\n * @returns Object with `bind`, `onEvent`, and `onFieldEvent` decorator factories\n */\nexport function modelBindings<TEventMap extends ModelEventMap = ModelEventMap>(model: FieldNodeLike) {\n return {\n /**\n * Binds a component property to a model field value.\n * When the field changes, the property is automatically updated.\n *\n * @param path - Path to the field (e.g., \"cube.length\", \"__isValid\")\n * @param eventType - Event to listen for (defaults to \"this-field-value-changed\")\n */\n bind(path: string, eventType: ModelEventType = \"this-field-value-changed\") {\n return function bindDecorator(target: object, propertyKey: string) {\n let metadata = bindingsMetadata.get(target);\n if (!metadata) {\n metadata = new Map();\n bindingsMetadata.set(target, metadata);\n }\n metadata.set(propertyKey, { model, path, eventType });\n\n patchBindLifecycle(target.constructor as typeof ReactiveElement);\n };\n },\n\n /**\n * Binds a method to an event on the root model.\n * When the event fires, the method is called.\n *\n * @param eventType - The event type to listen for\n */\n onEvent<K extends keyof TEventMap & ModelEventType>(eventType: K) {\n return function onEventDecorator(target: object, propertyKey: string, descriptor: PropertyDescriptor) {\n const originalMethod = descriptor.value;\n const ctor = target.constructor as typeof ReactiveElement;\n\n let methods = (ctor as unknown as Record<symbol, EventBindingMeta[]>)[MODEL_EVENT_METHODS];\n if (!methods) {\n methods = [];\n (ctor as unknown as Record<symbol, EventBindingMeta[]>)[MODEL_EVENT_METHODS] = methods;\n }\n methods.push({ propertyKey, model, path: null, eventType, method: originalMethod });\n\n patchEventLifecycle(ctor);\n };\n },\n\n /**\n * Binds a method to an event on a specific field.\n * When the event fires on that field, the method is called.\n *\n * @param path - Path to the field (e.g., \"cube.length\")\n * @param eventType - The event type to listen for\n */\n onFieldEvent<K extends keyof TEventMap & ModelEventType>(path: string, eventType: K) {\n return function onFieldEventDecorator(target: object, propertyKey: string, descriptor: PropertyDescriptor) {\n const originalMethod = descriptor.value;\n const ctor = target.constructor as typeof ReactiveElement;\n\n let methods = (ctor as unknown as Record<symbol, EventBindingMeta[]>)[MODEL_EVENT_METHODS];\n if (!methods) {\n methods = [];\n (ctor as unknown as Record<symbol, EventBindingMeta[]>)[MODEL_EVENT_METHODS] = methods;\n }\n methods.push({ propertyKey, model, path, eventType, method: originalMethod });\n\n patchEventLifecycle(ctor);\n };\n },\n };\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Lifecycle Patching\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Get the field node for a path, handling both direct properties and nested paths.\n */\nfunction getFieldForPath(model: FieldNodeLike, path: string): FieldNodeLike {\n // Check if it's a direct property (starts with __ or no dots)\n if (path.startsWith(\"__\") || !path.includes(\".\")) {\n return model;\n }\n // Navigate to nested field (we trust the field exists per user guarantee)\n if (model.__getFieldNodeByPath) {\n return model.__getFieldNodeByPath(path) as FieldNodeLike;\n }\n return model;\n}\n\n/**\n * Get the value for a path from the model.\n */\nfunction getValueForPath(model: FieldNodeLike, path: string): unknown {\n if (path.startsWith(\"__\")) {\n // Direct property access for internal properties\n return (model as unknown as Record<string, unknown>)[path];\n }\n if (!path.includes(\".\")) {\n // Direct child field\n const field = (model as unknown as Record<string, unknown>)[path] as FieldNodeLike | undefined;\n return field?.value ?? field;\n }\n // Nested path - get the field and return its value\n if (model.__getFieldNodeByPath) {\n const field = model.__getFieldNodeByPath(path);\n return field?.value ?? field;\n }\n return undefined;\n}\n\n/**\n * Patch connectedCallback/disconnectedCallback for bind decorators.\n */\nfunction patchBindLifecycle(ctor: typeof ReactiveElement): void {\n if ((ctor as unknown as Record<symbol, boolean>)[MODEL_BIND_PATCHED]) {\n return;\n }\n (ctor as unknown as Record<symbol, boolean>)[MODEL_BIND_PATCHED] = true;\n\n const originalConnected = ctor.prototype.connectedCallback;\n const originalDisconnected = ctor.prototype.disconnectedCallback;\n\n ctor.prototype.connectedCallback = function connectedCallback(\n this: LitElement & Record<symbol, Map<string, { listener: (e: CustomEvent) => void; field: FieldNodeLike; eventType: string }>>\n ) {\n originalConnected?.call(this);\n\n const metadata = bindingsMetadata.get(Object.getPrototypeOf(this));\n if (!metadata) return;\n\n const listeners = new Map<string, { listener: (e: CustomEvent) => void; field: FieldNodeLike; eventType: string }>();\n this[MODEL_BIND_LISTENERS] = listeners;\n\n metadata.forEach(({ model, path, eventType }, propKey) => {\n const field = getFieldForPath(model, path);\n\n // Set initial value\n (this as unknown as Record<string, unknown>)[propKey] = getValueForPath(model, path);\n\n const listener = () => {\n (this as unknown as Record<string, unknown>)[propKey] = getValueForPath(model, path);\n };\n\n listeners.set(propKey, { listener, field, eventType });\n field.__addEventListener(eventType, listener);\n });\n };\n\n ctor.prototype.disconnectedCallback = function disconnectedCallback(\n this: LitElement & Record<symbol, Map<string, { listener: (e: CustomEvent) => void; field: FieldNodeLike; eventType: string }>>\n ) {\n const listeners = this[MODEL_BIND_LISTENERS];\n if (listeners) {\n listeners.forEach(({ listener, field, eventType }) => {\n field.__removeEventListener(eventType, listener);\n });\n listeners.clear();\n }\n\n originalDisconnected?.call(this);\n };\n}\n\n/**\n * Patch connectedCallback/disconnectedCallback for event decorators.\n */\nfunction patchEventLifecycle(ctor: typeof ReactiveElement): void {\n if ((ctor as unknown as Record<symbol, boolean>)[MODEL_EVENT_PATCHED]) {\n return;\n }\n (ctor as unknown as Record<symbol, boolean>)[MODEL_EVENT_PATCHED] = true;\n\n const originalConnected = ctor.prototype.connectedCallback;\n const originalDisconnected = ctor.prototype.disconnectedCallback;\n\n ctor.prototype.connectedCallback = function connectedCallback(\n this: LitElement & Record<symbol, Map<string, { listener: (e: CustomEvent) => void; field: FieldNodeLike; eventType: string }>>\n ) {\n originalConnected?.call(this);\n\n const methods = (this.constructor as unknown as Record<symbol, EventBindingMeta[]>)[MODEL_EVENT_METHODS];\n if (!methods) return;\n\n const listeners = new Map<string, { listener: (e: CustomEvent) => void; field: FieldNodeLike; eventType: string }>();\n this[MODEL_EVENT_LISTENERS] = listeners;\n\n methods.forEach(({ propertyKey, model, path, eventType, method }) => {\n // If path is specified, listen on that field; otherwise listen on root model\n const field = path ? getFieldForPath(model, path) : model;\n\n const listener = (e: CustomEvent) => {\n method.call(this, e.detail);\n };\n\n listeners.set(propertyKey, { listener, field, eventType });\n field.__addEventListener(eventType, listener);\n });\n };\n\n ctor.prototype.disconnectedCallback = function disconnectedCallback(\n this: LitElement & Record<symbol, Map<string, { listener: (e: CustomEvent) => void; field: FieldNodeLike; eventType: string }>>\n ) {\n const listeners = this[MODEL_EVENT_LISTENERS];\n if (listeners) {\n listeners.forEach(({ listener, field, eventType }) => {\n field.__removeEventListener(eventType, listener);\n });\n listeners.clear();\n }\n\n originalDisconnected?.call(this);\n };\n}\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { FieldNode } from "../FieldNode.js";
|
|
2
|
+
import type { JSONSchema7 } from "json-schema";
|
|
3
|
+
interface FieldNodeSchema {
|
|
4
|
+
$schema: string;
|
|
5
|
+
$id: string;
|
|
6
|
+
[key: string]: unknown;
|
|
7
|
+
}
|
|
8
|
+
export declare class SchemaBuilder {
|
|
9
|
+
static generate(model: FieldNode): JSONSchema7;
|
|
10
|
+
private static getProps;
|
|
11
|
+
private static getRequiredFields;
|
|
12
|
+
private static getConstraints;
|
|
13
|
+
static createFieldNodeFromSchema(schema: FieldNodeSchema): FieldNode;
|
|
14
|
+
}
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Registry } from "../Registry.js";
|
|
2
|
+
const primitivesMap = new Map([
|
|
3
|
+
["primitives.BOOLEAN", "boolean"],
|
|
4
|
+
["primitives.BYTES", "string"],
|
|
5
|
+
["primitives.DOUBLE", "number"],
|
|
6
|
+
["primitives.ENUM", "string"],
|
|
7
|
+
["primitives.FLOAT", "number"],
|
|
8
|
+
["primitives.INT32", "integer"],
|
|
9
|
+
["primitives.INT64", "integer"],
|
|
10
|
+
["primitives.SINT32", "integer"],
|
|
11
|
+
["primitives.SINT64", "integer"],
|
|
12
|
+
["primitives.STRING", "string"],
|
|
13
|
+
["primitives.UINT32", "integer"],
|
|
14
|
+
["primitives.UINT64", "integer"],
|
|
15
|
+
]);
|
|
16
|
+
export class SchemaBuilder {
|
|
17
|
+
static generate(model) {
|
|
18
|
+
const schema = {
|
|
19
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
20
|
+
$id: model.__meta.typeName,
|
|
21
|
+
type: "object",
|
|
22
|
+
description: model.__meta.description,
|
|
23
|
+
};
|
|
24
|
+
schema.properties = SchemaBuilder.getProps(model);
|
|
25
|
+
// required fields
|
|
26
|
+
const rq = SchemaBuilder.getRequiredFields(model.__meta.nodeFields);
|
|
27
|
+
if (rq.length > 0) {
|
|
28
|
+
schema.required = rq;
|
|
29
|
+
}
|
|
30
|
+
return schema;
|
|
31
|
+
}
|
|
32
|
+
static getProps(model) {
|
|
33
|
+
return Object.fromEntries(model.__meta.nodeFields.map(fieldDescriptor => {
|
|
34
|
+
const field = model.__getFieldNodeByPath(fieldDescriptor.fieldName);
|
|
35
|
+
if (field.__isPrimitive && field.__meta.typeName !== "primitives.ENUM") {
|
|
36
|
+
const spec = {
|
|
37
|
+
type: primitivesMap.get(field.__meta.typeName),
|
|
38
|
+
description: [fieldDescriptor.description, field.__meta.description].join(""),
|
|
39
|
+
...SchemaBuilder.getConstraints(fieldDescriptor.constraints),
|
|
40
|
+
};
|
|
41
|
+
return [fieldDescriptor.fieldName, spec];
|
|
42
|
+
}
|
|
43
|
+
// ENUM
|
|
44
|
+
if (field.__meta.typeName === "primitives.ENUM") {
|
|
45
|
+
const eargs = model.__getFieldNodeByPath("material").enumArg;
|
|
46
|
+
return [
|
|
47
|
+
fieldDescriptor.fieldName,
|
|
48
|
+
{
|
|
49
|
+
type: primitivesMap.get(field.__meta.typeName),
|
|
50
|
+
description: [fieldDescriptor.description, field.__meta.description].join(""),
|
|
51
|
+
enum: Array.from(Object.keys(eargs)),
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
const schema = {
|
|
56
|
+
type: "object",
|
|
57
|
+
description: [fieldDescriptor.description, field.__meta.description].join(""),
|
|
58
|
+
};
|
|
59
|
+
schema.properties = SchemaBuilder.getProps(field);
|
|
60
|
+
// required fields
|
|
61
|
+
const rq = SchemaBuilder.getRequiredFields(model.__meta.nodeFields);
|
|
62
|
+
if (rq.length > 0) {
|
|
63
|
+
schema.required = rq;
|
|
64
|
+
}
|
|
65
|
+
return [fieldDescriptor.fieldName, schema];
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
static getRequiredFields(descriptors) {
|
|
69
|
+
const req = [];
|
|
70
|
+
descriptors.forEach(descriptor => {
|
|
71
|
+
if (descriptor.constraints?.required) {
|
|
72
|
+
req.push(descriptor.fieldName);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
return req;
|
|
76
|
+
}
|
|
77
|
+
static getConstraints(constraints) {
|
|
78
|
+
if (constraints === undefined) {
|
|
79
|
+
return {};
|
|
80
|
+
}
|
|
81
|
+
const co = { ...constraints };
|
|
82
|
+
delete co.required;
|
|
83
|
+
return co;
|
|
84
|
+
}
|
|
85
|
+
static createFieldNodeFromSchema(schema) {
|
|
86
|
+
return Registry.createInstanceByTypeName(schema.$id, schema);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=SchemaBuilder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SchemaBuilder.js","sourceRoot":"","sources":["../../src/decorators/SchemaBuilder.ts"],"names":[],"mappings":"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","sourcesContent":["import { ENUM } from \"../primitives/ENUM\";\nimport { FieldConstraints } from \"../FieldConstraints\";\nimport { FieldNode } from \"../FieldNode\";\nimport type { FieldDescriptor } from \"../FieldNode\";\nimport { Registry } from \"../Registry\";\nimport type { JSONSchema7, JSONSchema7Definition } from \"json-schema\";\n\nconst primitivesMap: Map<string, string> = new Map([\n [\"primitives.BOOLEAN\", \"boolean\"],\n [\"primitives.BYTES\", \"string\"],\n [\"primitives.DOUBLE\", \"number\"],\n [\"primitives.ENUM\", \"string\"],\n [\"primitives.FLOAT\", \"number\"],\n [\"primitives.INT32\", \"integer\"],\n [\"primitives.INT64\", \"integer\"],\n [\"primitives.SINT32\", \"integer\"],\n [\"primitives.SINT64\", \"integer\"],\n [\"primitives.STRING\", \"string\"],\n [\"primitives.UINT32\", \"integer\"],\n [\"primitives.UINT64\", \"integer\"],\n]);\n\ninterface FieldNodeSchema {\n $schema: string;\n $id: string;\n [key: string]: unknown;\n}\n\nexport class SchemaBuilder {\n public static generate(model: FieldNode): JSONSchema7 {\n const schema: JSONSchema7 = {\n $schema: \"http://json-schema.org/draft-07/schema#\",\n $id: model.__meta.typeName,\n type: \"object\",\n description: model.__meta.description,\n };\n\n schema.properties = SchemaBuilder.getProps(model);\n\n // required fields\n const rq = SchemaBuilder.getRequiredFields(model.__meta.nodeFields);\n if (rq.length > 0) {\n schema.required = rq;\n }\n\n return schema;\n }\n\n private static getProps(model: FieldNode): Record<string, JSONSchema7Definition> {\n return Object.fromEntries(\n model.__meta.nodeFields.map(fieldDescriptor => {\n const field = model.__getFieldNodeByPath(fieldDescriptor.fieldName) as FieldNode;\n\n if (field.__isPrimitive && field.__meta.typeName !== \"primitives.ENUM\") {\n const spec = {\n type: primitivesMap.get(field.__meta.typeName!)!,\n description: [fieldDescriptor.description, field.__meta.description].join(\"\"),\n ...SchemaBuilder.getConstraints(fieldDescriptor.constraints),\n };\n\n return [fieldDescriptor.fieldName, spec];\n }\n // ENUM\n if (field.__meta.typeName === \"primitives.ENUM\") {\n const eargs = (model.__getFieldNodeByPath(\"material\") as ENUM<unknown>).enumArg;\n\n return [\n fieldDescriptor.fieldName,\n {\n type: primitivesMap.get(field.__meta.typeName!),\n description: [fieldDescriptor.description, field.__meta.description].join(\"\"),\n enum: Array.from(Object.keys(eargs)),\n },\n ];\n }\n\n const schema: JSONSchema7 = {\n type: \"object\",\n description: [fieldDescriptor.description, field.__meta.description].join(\"\"),\n };\n schema.properties = SchemaBuilder.getProps(field);\n // required fields\n const rq = SchemaBuilder.getRequiredFields(model.__meta.nodeFields);\n if (rq.length > 0) {\n schema.required = rq;\n }\n\n return [fieldDescriptor.fieldName, schema];\n })\n );\n }\n\n private static getRequiredFields(descriptors: FieldDescriptor[]): string[] {\n const req: string[] = [];\n descriptors.forEach(descriptor => {\n if (descriptor.constraints?.required) {\n req.push(descriptor.fieldName);\n }\n });\n return req;\n }\n\n private static getConstraints(constraints: FieldConstraints | undefined) {\n if (constraints === undefined) {\n return {};\n }\n const co = { ...constraints };\n delete co.required;\n return co;\n }\n\n public static createFieldNodeFromSchema(schema: FieldNodeSchema): FieldNode {\n return Registry.createInstanceByTypeName(schema.$id, schema);\n }\n}\n"]}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { EntityServiceEventMap } from "./EntityServiceTypes.js";
|
|
2
|
+
/**
|
|
3
|
+
* ### serviceBindings Factory
|
|
4
|
+
*
|
|
5
|
+
* Creates type-safe decorators bound to a specific service instance.
|
|
6
|
+
* Use this to bind component properties and methods to service events.
|
|
7
|
+
*
|
|
8
|
+
* The factory is generic and works with any `EventTarget`.
|
|
9
|
+
* Event types and their detail payloads are type-checked at compile time
|
|
10
|
+
* via the `TEventMap` type parameter.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* ```typescript
|
|
14
|
+
* import { cubeEntityService } from "./CubeEntityService";
|
|
15
|
+
* import { serviceBindings } from "./ServiceDecorators.js";
|
|
16
|
+
*
|
|
17
|
+
* const cube = serviceBindings(cubeEntityService);
|
|
18
|
+
*
|
|
19
|
+
* class MyComponent extends LitElement {
|
|
20
|
+
* // Property binding - type-safe event name, auto-extracts from detail
|
|
21
|
+
* @cube.bindToEvent("busy-changed")
|
|
22
|
+
* @state()
|
|
23
|
+
* private busy: boolean = false;
|
|
24
|
+
*
|
|
25
|
+
* // Event binding - type-safe event name and detail type
|
|
26
|
+
* @cube.onEvent("response-received")
|
|
27
|
+
* private onResponseReceived() {
|
|
28
|
+
* console.log("Data received!");
|
|
29
|
+
* }
|
|
30
|
+
*
|
|
31
|
+
* @cube.onEvent("error-5xx")
|
|
32
|
+
* private onError(detail: { serverResponse: Response }) {
|
|
33
|
+
* console.error("Server error:", detail.serverResponse);
|
|
34
|
+
* }
|
|
35
|
+
*
|
|
36
|
+
* // Compile error! "typo-event" is not a valid event type
|
|
37
|
+
* // @cube.onEvent("typo-event")
|
|
38
|
+
* }
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* ### Custom Event Maps
|
|
42
|
+
*
|
|
43
|
+
* To add custom events, extend the `EntityServiceEventMap`:
|
|
44
|
+
* ```typescript
|
|
45
|
+
* interface MyServiceEventMap extends EntityServiceEventMap {
|
|
46
|
+
* "custom-event": { data: string };
|
|
47
|
+
* }
|
|
48
|
+
*
|
|
49
|
+
* const myBindings = serviceBindings<MyServiceEventMap>(myService);
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* @typeParam TEventMap - The event map type (defaults to EntityServiceEventMap)
|
|
53
|
+
* @param service - The EventTarget service to bind to
|
|
54
|
+
* @returns Object with `bindToEvent` and `onEvent` decorator factories
|
|
55
|
+
*/
|
|
56
|
+
export declare function serviceBindings<TEventMap extends EntityServiceEventMap = EntityServiceEventMap>(service: EventTarget): {
|
|
57
|
+
/**
|
|
58
|
+
* Binds a property to a service event.
|
|
59
|
+
* When the event fires, the property is automatically updated from event.detail.
|
|
60
|
+
*
|
|
61
|
+
* @typeParam K - The event type (constrained to valid event names)
|
|
62
|
+
* @param eventType - The event name to listen for
|
|
63
|
+
* @param detailKey - Optional key to extract from event.detail (defaults to inferring from event type)
|
|
64
|
+
*/
|
|
65
|
+
bindToEvent<K extends keyof TEventMap & string>(eventType: K, detailKey?: string): (target: object, propertyKey: string) => void;
|
|
66
|
+
/**
|
|
67
|
+
* Binds a method to a service event.
|
|
68
|
+
* When the event fires, the method is called with event.detail as argument.
|
|
69
|
+
*
|
|
70
|
+
* The detail type is inferred from the event map:
|
|
71
|
+
* - `@cube.onEvent("busy-changed")` → method receives `{ busy: boolean }`
|
|
72
|
+
* - `@cube.onEvent("error-5xx")` → method receives `{ serverResponse: Response }`
|
|
73
|
+
* - `@cube.onEvent("response-received")` → method receives `{ response, serverResponse }`
|
|
74
|
+
*
|
|
75
|
+
* @typeParam K - The event type (constrained to valid event names)
|
|
76
|
+
* @param eventType - The event name to listen for
|
|
77
|
+
*/
|
|
78
|
+
onEvent<K extends keyof TEventMap & string>(eventType: K): (target: object, propertyKey: string, descriptor: PropertyDescriptor) => void;
|
|
79
|
+
};
|