@object-ui/core 3.4.0 → 4.0.1
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/CHANGELOG.md +13 -0
- package/README.md +35 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/utils/freeze-schema.d.ts +86 -0
- package/dist/utils/freeze-schema.js +146 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -58,6 +58,41 @@ const userName = scope.get('user.name') // 'John'
|
|
|
58
58
|
const isAdmin = scope.evaluate('${user.role === "admin"}') // true
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
### System Views (`defineView`)
|
|
62
|
+
|
|
63
|
+
Schemas authored in source code are part of the product contract and must
|
|
64
|
+
not be mutated at runtime. Wrap them with `defineView()` to deep-freeze the
|
|
65
|
+
graph and tag it as a *System View*.
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
import { defineView, cloneAsOverride, isSystemView } from '@object-ui/core'
|
|
69
|
+
|
|
70
|
+
export const userListView = defineView({
|
|
71
|
+
type: 'list',
|
|
72
|
+
data: { object: 'User' },
|
|
73
|
+
columns: [{ name: 'email' }],
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
userListView.columns.push({ name: 'name' }) // ❌ TypeError (strict mode)
|
|
77
|
+
isSystemView(userListView) // ✅ true
|
|
78
|
+
|
|
79
|
+
// To produce a Tenant- or User-level override, derive a mutable copy:
|
|
80
|
+
const draft = cloneAsOverride(userListView)
|
|
81
|
+
draft.columns.push({ name: 'name' }) // ✅ allowed
|
|
82
|
+
isSystemView(draft) // false — clone is no longer System
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**View tiers (recommended layering):**
|
|
86
|
+
|
|
87
|
+
| Tier | Source | Mutable? | API |
|
|
88
|
+
| ----------- | --------------------- | -------- | --------------------------- |
|
|
89
|
+
| System View | code (`import` / `as const`) | ❌ frozen | `defineView()` |
|
|
90
|
+
| Tenant View | backend / DB | ⚠️ admin only | `cloneAsOverride()` + persist |
|
|
91
|
+
| User View | localStorage / API | ✅ user-editable | `cloneAsOverride()` + persist |
|
|
92
|
+
|
|
93
|
+
`Date`, `RegExp`, `Map`, `Set`, and class instances passed via `props` are
|
|
94
|
+
intentionally **not** frozen so infrastructure objects keep working.
|
|
95
|
+
|
|
61
96
|
## Philosophy
|
|
62
97
|
|
|
63
98
|
This package is designed to be **framework-agnostic**. It contains:
|
package/dist/index.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ export * from './errors/index.js';
|
|
|
26
26
|
export * from './utils/debug.js';
|
|
27
27
|
export * from './utils/debug-collector.js';
|
|
28
28
|
export * from './utils/merge-views-into-objects.js';
|
|
29
|
+
export * from './utils/freeze-schema.js';
|
|
29
30
|
export * from './protocols/index.js';
|
|
30
31
|
/**
|
|
31
32
|
* @deprecated Import `composeStacks` from `@objectstack/spec` instead.
|
package/dist/index.js
CHANGED
|
@@ -25,6 +25,7 @@ export * from './errors/index.js';
|
|
|
25
25
|
export * from './utils/debug.js';
|
|
26
26
|
export * from './utils/debug-collector.js';
|
|
27
27
|
export * from './utils/merge-views-into-objects.js';
|
|
28
|
+
export * from './utils/freeze-schema.js';
|
|
28
29
|
export * from './protocols/index.js';
|
|
29
30
|
/**
|
|
30
31
|
* @deprecated Import `composeStacks` from `@objectstack/spec` instead.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* System View immutability layer.
|
|
10
|
+
*
|
|
11
|
+
* A "System View" is any UI schema authored in source code (imported `.ts`/`.json`,
|
|
12
|
+
* `as const` literals, or returned from a `defineView()` call). Such schemas are
|
|
13
|
+
* part of the product contract and MUST NOT be mutated at runtime. Mutation
|
|
14
|
+
* would cause behavior drift, break TypeScript inference, and bypass code review.
|
|
15
|
+
*
|
|
16
|
+
* Tenant- and user-level overrides should produce a *new* object via
|
|
17
|
+
* `cloneAsOverride()` rather than mutating the source schema.
|
|
18
|
+
*
|
|
19
|
+
* Design notes:
|
|
20
|
+
* - Uses `Object.freeze` recursively (shallow-frozen objects can still be
|
|
21
|
+
* mutated through nested references, which would defeat the purpose).
|
|
22
|
+
* - Skips `Date`, `RegExp`, `Map`, `Set`, and class instances to avoid
|
|
23
|
+
* freezing infrastructure objects users may pass through `props`.
|
|
24
|
+
* - Tags the root with a non-enumerable Symbol so the renderer / DevTools
|
|
25
|
+
* can detect the origin without polluting JSON serialization.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* Symbol marker stamped on every System View root. Non-enumerable so it
|
|
29
|
+
* never appears in `JSON.stringify`, `Object.keys`, or `{...spread}`.
|
|
30
|
+
*/
|
|
31
|
+
export declare const SYSTEM_VIEW_MARKER: unique symbol;
|
|
32
|
+
/**
|
|
33
|
+
* Recursively type values as `readonly`. Equivalent to TS' built-in
|
|
34
|
+
* `Readonly<T>` but applied at every depth.
|
|
35
|
+
*/
|
|
36
|
+
export type DeepReadonly<T> = T extends (...args: any[]) => any ? T : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepReadonly<U>> : T extends object ? {
|
|
37
|
+
readonly [K in keyof T]: DeepReadonly<T[K]>;
|
|
38
|
+
} : T;
|
|
39
|
+
/**
|
|
40
|
+
* A schema that has been frozen by `defineView()`. The marker symbol is
|
|
41
|
+
* non-enumerable and therefore invisible to consumers, but its presence
|
|
42
|
+
* lets us discriminate at runtime.
|
|
43
|
+
*/
|
|
44
|
+
export type SystemView<T> = DeepReadonly<T> & {
|
|
45
|
+
readonly [SYSTEM_VIEW_MARKER]?: true;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Recursively freeze an object graph. Cycles are tolerated via a `WeakSet`
|
|
49
|
+
* guard. Returns the same reference, narrowed to `DeepReadonly<T>`.
|
|
50
|
+
*/
|
|
51
|
+
export declare function deepFreeze<T>(value: T, seen?: WeakSet<object>): DeepReadonly<T>;
|
|
52
|
+
/**
|
|
53
|
+
* Mark and freeze a code-loaded schema as a System View.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* import { defineView } from '@object-ui/core';
|
|
58
|
+
*
|
|
59
|
+
* export const userListView = defineView({
|
|
60
|
+
* type: 'list',
|
|
61
|
+
* data: { object: 'User' },
|
|
62
|
+
* columns: [{ name: 'email' }],
|
|
63
|
+
* });
|
|
64
|
+
*
|
|
65
|
+
* // Type-checked AND runtime-checked: throws in strict mode, no-op silently otherwise.
|
|
66
|
+
* userListView.columns.push({ name: 'name' });
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* To produce a mutable derivative (Tenant or User View), call
|
|
70
|
+
* `cloneAsOverride(userListView)`.
|
|
71
|
+
*/
|
|
72
|
+
export declare function defineView<T extends object>(schema: T): SystemView<T>;
|
|
73
|
+
/**
|
|
74
|
+
* Runtime check: was this object produced by `defineView()` (or loaded from a
|
|
75
|
+
* source that forwards the marker)?
|
|
76
|
+
*/
|
|
77
|
+
export declare function isSystemView(value: unknown): boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Produce a deep, mutable clone of a System View so callers can apply
|
|
80
|
+
* Tenant/User overrides without touching the source schema.
|
|
81
|
+
*
|
|
82
|
+
* Implementation note: uses `structuredClone` when available (Node 17+, all
|
|
83
|
+
* evergreen browsers) and falls back to a JSON round-trip. The marker
|
|
84
|
+
* symbol is intentionally NOT copied — the clone is no longer a System View.
|
|
85
|
+
*/
|
|
86
|
+
export declare function cloneAsOverride<T>(view: T): T;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* System View immutability layer.
|
|
10
|
+
*
|
|
11
|
+
* A "System View" is any UI schema authored in source code (imported `.ts`/`.json`,
|
|
12
|
+
* `as const` literals, or returned from a `defineView()` call). Such schemas are
|
|
13
|
+
* part of the product contract and MUST NOT be mutated at runtime. Mutation
|
|
14
|
+
* would cause behavior drift, break TypeScript inference, and bypass code review.
|
|
15
|
+
*
|
|
16
|
+
* Tenant- and user-level overrides should produce a *new* object via
|
|
17
|
+
* `cloneAsOverride()` rather than mutating the source schema.
|
|
18
|
+
*
|
|
19
|
+
* Design notes:
|
|
20
|
+
* - Uses `Object.freeze` recursively (shallow-frozen objects can still be
|
|
21
|
+
* mutated through nested references, which would defeat the purpose).
|
|
22
|
+
* - Skips `Date`, `RegExp`, `Map`, `Set`, and class instances to avoid
|
|
23
|
+
* freezing infrastructure objects users may pass through `props`.
|
|
24
|
+
* - Tags the root with a non-enumerable Symbol so the renderer / DevTools
|
|
25
|
+
* can detect the origin without polluting JSON serialization.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* Symbol marker stamped on every System View root. Non-enumerable so it
|
|
29
|
+
* never appears in `JSON.stringify`, `Object.keys`, or `{...spread}`.
|
|
30
|
+
*/
|
|
31
|
+
export const SYSTEM_VIEW_MARKER = Symbol.for('@object-ui/core/system-view');
|
|
32
|
+
/**
|
|
33
|
+
* Values that should NOT be frozen — they are infrastructure objects whose
|
|
34
|
+
* internal mutability is required for correct operation.
|
|
35
|
+
*/
|
|
36
|
+
function isFreezableObject(value) {
|
|
37
|
+
if (value === null || typeof value !== 'object')
|
|
38
|
+
return false;
|
|
39
|
+
if (Object.isFrozen(value))
|
|
40
|
+
return false;
|
|
41
|
+
// Built-in types whose internals must remain mutable.
|
|
42
|
+
if (value instanceof Date)
|
|
43
|
+
return false;
|
|
44
|
+
if (value instanceof RegExp)
|
|
45
|
+
return false;
|
|
46
|
+
if (value instanceof Map)
|
|
47
|
+
return false;
|
|
48
|
+
if (value instanceof Set)
|
|
49
|
+
return false;
|
|
50
|
+
if (value instanceof WeakMap)
|
|
51
|
+
return false;
|
|
52
|
+
if (value instanceof WeakSet)
|
|
53
|
+
return false;
|
|
54
|
+
if (value instanceof Promise)
|
|
55
|
+
return false;
|
|
56
|
+
if (typeof value.then === 'function')
|
|
57
|
+
return false;
|
|
58
|
+
// Skip class instances (anything not a plain object or array).
|
|
59
|
+
if (!Array.isArray(value)) {
|
|
60
|
+
const proto = Object.getPrototypeOf(value);
|
|
61
|
+
if (proto !== Object.prototype && proto !== null)
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Recursively freeze an object graph. Cycles are tolerated via a `WeakSet`
|
|
68
|
+
* guard. Returns the same reference, narrowed to `DeepReadonly<T>`.
|
|
69
|
+
*/
|
|
70
|
+
export function deepFreeze(value, seen = new WeakSet()) {
|
|
71
|
+
if (!isFreezableObject(value))
|
|
72
|
+
return value;
|
|
73
|
+
if (seen.has(value))
|
|
74
|
+
return value;
|
|
75
|
+
seen.add(value);
|
|
76
|
+
// Freeze children first so the root reflects a fully-frozen graph.
|
|
77
|
+
for (const key of Object.keys(value)) {
|
|
78
|
+
const child = value[key];
|
|
79
|
+
if (isFreezableObject(child)) {
|
|
80
|
+
deepFreeze(child, seen);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
Object.freeze(value);
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Mark and freeze a code-loaded schema as a System View.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```ts
|
|
91
|
+
* import { defineView } from '@object-ui/core';
|
|
92
|
+
*
|
|
93
|
+
* export const userListView = defineView({
|
|
94
|
+
* type: 'list',
|
|
95
|
+
* data: { object: 'User' },
|
|
96
|
+
* columns: [{ name: 'email' }],
|
|
97
|
+
* });
|
|
98
|
+
*
|
|
99
|
+
* // Type-checked AND runtime-checked: throws in strict mode, no-op silently otherwise.
|
|
100
|
+
* userListView.columns.push({ name: 'name' });
|
|
101
|
+
* ```
|
|
102
|
+
*
|
|
103
|
+
* To produce a mutable derivative (Tenant or User View), call
|
|
104
|
+
* `cloneAsOverride(userListView)`.
|
|
105
|
+
*/
|
|
106
|
+
export function defineView(schema) {
|
|
107
|
+
if (schema == null || typeof schema !== 'object') {
|
|
108
|
+
throw new TypeError('[ObjectUI] defineView() expects a non-null object schema.');
|
|
109
|
+
}
|
|
110
|
+
// Stamp the marker on the root only — nested nodes share origin via lineage.
|
|
111
|
+
// Non-enumerable keeps it out of JSON, spreads, and Object.keys.
|
|
112
|
+
if (!Object.prototype.hasOwnProperty.call(schema, SYSTEM_VIEW_MARKER)) {
|
|
113
|
+
Object.defineProperty(schema, SYSTEM_VIEW_MARKER, {
|
|
114
|
+
value: true,
|
|
115
|
+
enumerable: false,
|
|
116
|
+
configurable: false,
|
|
117
|
+
writable: false,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return deepFreeze(schema);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Runtime check: was this object produced by `defineView()` (or loaded from a
|
|
124
|
+
* source that forwards the marker)?
|
|
125
|
+
*/
|
|
126
|
+
export function isSystemView(value) {
|
|
127
|
+
return (value != null &&
|
|
128
|
+
typeof value === 'object' &&
|
|
129
|
+
value[SYSTEM_VIEW_MARKER] === true);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Produce a deep, mutable clone of a System View so callers can apply
|
|
133
|
+
* Tenant/User overrides without touching the source schema.
|
|
134
|
+
*
|
|
135
|
+
* Implementation note: uses `structuredClone` when available (Node 17+, all
|
|
136
|
+
* evergreen browsers) and falls back to a JSON round-trip. The marker
|
|
137
|
+
* symbol is intentionally NOT copied — the clone is no longer a System View.
|
|
138
|
+
*/
|
|
139
|
+
export function cloneAsOverride(view) {
|
|
140
|
+
if (view == null || typeof view !== 'object')
|
|
141
|
+
return view;
|
|
142
|
+
const clone = typeof structuredClone === 'function'
|
|
143
|
+
? structuredClone(view)
|
|
144
|
+
: JSON.parse(JSON.stringify(view));
|
|
145
|
+
return clone;
|
|
146
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"@objectstack/spec": "^4.0.4",
|
|
29
29
|
"lodash": "^4.18.1",
|
|
30
30
|
"zod": "^4.4.3",
|
|
31
|
-
"@object-ui/types": "
|
|
31
|
+
"@object-ui/types": "4.0.1"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"typescript": "^6.0.3",
|