@asteby/metacore-runtime-react 9.1.0 → 10.0.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +175 -0
  2. package/dist/addon-layout-context.d.ts +49 -0
  3. package/dist/addon-layout-context.d.ts.map +1 -0
  4. package/dist/addon-layout-context.js +94 -0
  5. package/dist/addon-loader.d.ts +14 -2
  6. package/dist/addon-loader.d.ts.map +1 -1
  7. package/dist/addon-loader.js +7 -1
  8. package/dist/dynamic-form-schema.d.ts +5 -0
  9. package/dist/dynamic-form-schema.d.ts.map +1 -1
  10. package/dist/dynamic-form-schema.js +34 -0
  11. package/dist/dynamic-form.d.ts.map +1 -1
  12. package/dist/dynamic-form.js +18 -2
  13. package/dist/dynamic-relation.d.ts.map +1 -1
  14. package/dist/dynamic-relation.js +59 -22
  15. package/dist/hotswap-reload-policy.d.ts +155 -0
  16. package/dist/hotswap-reload-policy.d.ts.map +1 -0
  17. package/dist/hotswap-reload-policy.js +227 -0
  18. package/dist/index.d.ts +6 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +6 -0
  21. package/dist/manifest-hotswap-subscriber.d.ts +83 -0
  22. package/dist/manifest-hotswap-subscriber.d.ts.map +1 -0
  23. package/dist/manifest-hotswap-subscriber.js +104 -0
  24. package/dist/metadata-cache.d.ts +35 -0
  25. package/dist/metadata-cache.d.ts.map +1 -1
  26. package/dist/metadata-cache.js +55 -0
  27. package/dist/types.d.ts +20 -0
  28. package/dist/types.d.ts.map +1 -1
  29. package/dist/use-options-resolver.d.ts +87 -0
  30. package/dist/use-options-resolver.d.ts.map +1 -0
  31. package/dist/use-options-resolver.js +147 -0
  32. package/dist/use-org-config-bridge.d.ts +28 -0
  33. package/dist/use-org-config-bridge.d.ts.map +1 -0
  34. package/dist/use-org-config-bridge.js +50 -0
  35. package/package.json +4 -4
  36. package/src/__tests__/hotswap-reload-policy.test.ts +249 -0
  37. package/src/__tests__/manifest-hotswap-subscriber.test.ts +179 -0
  38. package/src/__tests__/use-options-resolver.test.ts +127 -0
  39. package/src/__tests__/wasm-client-sri.test.ts +82 -0
  40. package/src/addon-layout-context.tsx +137 -0
  41. package/src/addon-loader.tsx +21 -1
  42. package/src/dynamic-form-schema.ts +36 -0
  43. package/src/dynamic-form.tsx +40 -2
  44. package/src/dynamic-relation.tsx +55 -20
  45. package/src/hotswap-reload-policy.ts +360 -0
  46. package/src/index.ts +43 -0
  47. package/src/manifest-hotswap-subscriber.ts +164 -0
  48. package/src/metadata-cache.ts +86 -0
  49. package/src/types.ts +24 -0
  50. package/src/use-options-resolver.ts +232 -0
  51. package/src/use-org-config-bridge.ts +60 -0
@@ -0,0 +1,83 @@
1
+ import { type AddonKeyMatcher } from './metadata-cache';
2
+ /**
3
+ * `type` string the kernel/bridge emits. Exported so consumers can subscribe
4
+ * manually if they prefer to skip the hook/wire helper. Matches
5
+ * `bridge.WSManifestChangedType` in metacore-kernel.
6
+ */
7
+ export declare const ADDON_MANIFEST_CHANGED_TYPE: "ADDON_MANIFEST_CHANGED";
8
+ /**
9
+ * Shape of the WebSocket message the kernel emits. The keys mirror the
10
+ * `bridge.manifestChangedPayload` map in metacore-kernel/bridge — keep them
11
+ * in sync if the bridge ever evolves.
12
+ */
13
+ export interface AddonManifestChangedMessage {
14
+ type: typeof ADDON_MANIFEST_CHANGED_TYPE;
15
+ payload: {
16
+ orgId?: string;
17
+ addonKey: string;
18
+ oldHash?: string;
19
+ newHash?: string;
20
+ version?: string;
21
+ timestamp?: string;
22
+ };
23
+ }
24
+ /**
25
+ * Structural client contract. The SDK's `@asteby/metacore-websocket`
26
+ * provider exposes `subscribe(type, handler)` (see `useWebSocket().subscribe`)
27
+ * that satisfies this interface — but any object with a compatible method
28
+ * works, so hosts that wrap their own transport (link's MQTT bridge, the
29
+ * kitchen-display ZeroMQ stub, …) can plug in without depending on the
30
+ * SDK websocket package.
31
+ */
32
+ export interface ManifestHotSwapClient {
33
+ subscribe: (type: string, handler: (message: AddonManifestChangedMessage) => void) => () => void;
34
+ }
35
+ export interface WireHotSwapInvalidationOptions {
36
+ /**
37
+ * Optional matcher overriding the default cache-key heuristic
38
+ * (see `defaultAddonKeyMatcher`). Hosts that namespace cached
39
+ * `model` keys under prefixes other than `${addonKey}.|:|/` should
40
+ * supply one.
41
+ */
42
+ matcher?: AddonKeyMatcher;
43
+ /**
44
+ * Optional side-effect hook invoked after the cache invalidation.
45
+ * Useful for hosts that want to log/observe hot-swaps or trigger a
46
+ * `window.location.reload()` when the running addon's bundle hash
47
+ * changes (see the module-level comment above for the trade-off).
48
+ * `removed` is the number of cache entries flushed for this addon.
49
+ */
50
+ onSwap?: (msg: AddonManifestChangedMessage, removed: number) => void;
51
+ }
52
+ /**
53
+ * Imperative wire-up — no React required. Hosts that own a long-lived
54
+ * WebSocket client (link, ops, the kitchen-display Tauri shell) call this
55
+ * once at boot, after the client has been created. The returned function
56
+ * unsubscribes; most hosts will never call it because the subscription
57
+ * lives for the lifetime of the app.
58
+ *
59
+ * const ws = createWebSocket(...)
60
+ * const unsubscribe = wireHotSwapInvalidation(ws)
61
+ * // …later, if needed:
62
+ * unsubscribe()
63
+ *
64
+ * Accepts `undefined` so the call site does not need to branch when the
65
+ * client is constructed lazily — it returns a no-op unsubscribe.
66
+ */
67
+ export declare function wireHotSwapInvalidation(client: ManifestHotSwapClient | undefined | null, options?: WireHotSwapInvalidationOptions): () => void;
68
+ /**
69
+ * React-flavoured wrapper around {@link wireHotSwapInvalidation}. Mount it
70
+ * once high in the tree (typically next to the WebSocket provider) so the
71
+ * subscription lifetime matches the host shell. Passing `undefined` for
72
+ * the client is supported — the hook becomes a no-op until a real client
73
+ * is available, mirroring how `useWebSocket().subscribe` behaves before
74
+ * the socket opens.
75
+ *
76
+ * function HostShell() {
77
+ * const ws = useWebSocket()
78
+ * useManifestHotSwapSubscriber(ws)
79
+ * return <Outlet />
80
+ * }
81
+ */
82
+ export declare function useManifestHotSwapSubscriber(client: ManifestHotSwapClient | undefined | null, options?: WireHotSwapInvalidationOptions): void;
83
+ //# sourceMappingURL=manifest-hotswap-subscriber.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest-hotswap-subscriber.d.ts","sourceRoot":"","sources":["../src/manifest-hotswap-subscriber.ts"],"names":[],"mappings":"AA6CA,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAEzE;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,EAAG,wBAAiC,CAAA;AAE5E;;;;GAIG;AACH,MAAM,WAAW,2BAA2B;IACxC,IAAI,EAAE,OAAO,2BAA2B,CAAA;IACxC,OAAO,EAAE;QACL,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,QAAQ,EAAE,MAAM,CAAA;QAChB,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,SAAS,CAAC,EAAE,MAAM,CAAA;KACrB,CAAA;CACJ;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,qBAAqB;IAClC,SAAS,EAAE,CACP,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,CAAC,OAAO,EAAE,2BAA2B,KAAK,IAAI,KACtD,MAAM,IAAI,CAAA;CAClB;AAED,MAAM,WAAW,8BAA8B;IAC3C;;;;;OAKG;IACH,OAAO,CAAC,EAAE,eAAe,CAAA;IACzB;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,2BAA2B,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CACvE;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,uBAAuB,CACnC,MAAM,EAAE,qBAAqB,GAAG,SAAS,GAAG,IAAI,EAChD,OAAO,GAAE,8BAAmC,GAC7C,MAAM,IAAI,CAeZ;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,4BAA4B,CACxC,MAAM,EAAE,qBAAqB,GAAG,SAAS,GAAG,IAAI,EAChD,OAAO,GAAE,8BAAmC,GAC7C,IAAI,CAON"}
@@ -0,0 +1,104 @@
1
+ // manifest-hotswap-subscriber — client-side closure of RFC-0001 D4.
2
+ //
3
+ // The kernel installer (see metacore-kernel/installer/broadcaster.go) emits a
4
+ // WebSocket message every time it observes a manifest-hash change at persist
5
+ // time. The bridge (metacore-kernel/bridge/installer_broadcaster.go) fans it
6
+ // out per-org over `ws.Hub.SendToUsers` with this exact envelope:
7
+ //
8
+ // {
9
+ // "type": "ADDON_MANIFEST_CHANGED",
10
+ // "payload": {
11
+ // "orgId": "<uuid>",
12
+ // "addonKey": "kitchen_display",
13
+ // "oldHash": "sha256:...",
14
+ // "newHash": "sha256:...",
15
+ // "version": "1.2.0",
16
+ // "timestamp": "2026-05-11T12:34:56Z"
17
+ // }
18
+ // }
19
+ //
20
+ // (The original RFC scratchpad used snake_case keys; the bridge canonicalised
21
+ // them to camelCase so the SDK's metadata-cache reducer can consume the
22
+ // payload verbatim. This module matches the bridge — the source of truth.)
23
+ //
24
+ // This module wires the message into `useMetadataCache().invalidateAddon` so
25
+ // every cached table/modal metadata entry belonging to the bumped addon is
26
+ // dropped from the zustand store. The next mount of any `DynamicTable` or
27
+ // `DynamicCRUDPage` will refetch fresh metadata, picking up whatever the
28
+ // new manifest changed (new fields, renamed actions, dropped relations…).
29
+ //
30
+ // IMPORTANT — a freshly-bumped addon whose `remoteEntry.js` URL changes will
31
+ // also need its federation container reloaded. Cache invalidation handles
32
+ // metadata; it does NOT swap the JS already executing in memory. If your
33
+ // host wants the new code to take effect immediately, either:
34
+ //
35
+ // 1. force a full `window.location.reload()` when an `addon:manifest:changed`
36
+ // event arrives for the addon currently mounted, or
37
+ // 2. for immersive addons (`layout: "immersive"`) where users tolerate a
38
+ // flicker, key the addon route component on `newHash` so React unmounts
39
+ // and remounts the AddonLoader with the new remoteEntry URL.
40
+ //
41
+ // The subscriber here intentionally stops at metadata-cache: deciding when
42
+ // to reload is a host policy decision and we keep this module side-effect
43
+ // free beyond the cache.
44
+ import { useEffect } from 'react';
45
+ import { useMetadataCache } from './metadata-cache';
46
+ /**
47
+ * `type` string the kernel/bridge emits. Exported so consumers can subscribe
48
+ * manually if they prefer to skip the hook/wire helper. Matches
49
+ * `bridge.WSManifestChangedType` in metacore-kernel.
50
+ */
51
+ export const ADDON_MANIFEST_CHANGED_TYPE = 'ADDON_MANIFEST_CHANGED';
52
+ /**
53
+ * Imperative wire-up — no React required. Hosts that own a long-lived
54
+ * WebSocket client (link, ops, the kitchen-display Tauri shell) call this
55
+ * once at boot, after the client has been created. The returned function
56
+ * unsubscribes; most hosts will never call it because the subscription
57
+ * lives for the lifetime of the app.
58
+ *
59
+ * const ws = createWebSocket(...)
60
+ * const unsubscribe = wireHotSwapInvalidation(ws)
61
+ * // …later, if needed:
62
+ * unsubscribe()
63
+ *
64
+ * Accepts `undefined` so the call site does not need to branch when the
65
+ * client is constructed lazily — it returns a no-op unsubscribe.
66
+ */
67
+ export function wireHotSwapInvalidation(client, options = {}) {
68
+ if (!client)
69
+ return () => { };
70
+ const { matcher, onSwap } = options;
71
+ const unsubscribe = client.subscribe(ADDON_MANIFEST_CHANGED_TYPE, (message) => {
72
+ const addonKey = message?.payload?.addonKey;
73
+ if (!addonKey)
74
+ return;
75
+ const removed = useMetadataCache
76
+ .getState()
77
+ .invalidateAddon(addonKey, matcher);
78
+ onSwap?.(message, removed);
79
+ });
80
+ return unsubscribe;
81
+ }
82
+ /**
83
+ * React-flavoured wrapper around {@link wireHotSwapInvalidation}. Mount it
84
+ * once high in the tree (typically next to the WebSocket provider) so the
85
+ * subscription lifetime matches the host shell. Passing `undefined` for
86
+ * the client is supported — the hook becomes a no-op until a real client
87
+ * is available, mirroring how `useWebSocket().subscribe` behaves before
88
+ * the socket opens.
89
+ *
90
+ * function HostShell() {
91
+ * const ws = useWebSocket()
92
+ * useManifestHotSwapSubscriber(ws)
93
+ * return <Outlet />
94
+ * }
95
+ */
96
+ export function useManifestHotSwapSubscriber(client, options = {}) {
97
+ const { matcher, onSwap } = options;
98
+ useEffect(() => {
99
+ if (!client)
100
+ return;
101
+ const unsubscribe = wireHotSwapInvalidation(client, { matcher, onSwap });
102
+ return unsubscribe;
103
+ }, [client, matcher, onSwap]);
104
+ }
@@ -4,6 +4,21 @@ export interface MetadataApiClient {
4
4
  data: any;
5
5
  }>;
6
6
  }
7
+ /**
8
+ * Predicate matching a cache key against an addon. The default
9
+ * implementation (see {@link defaultAddonKeyMatcher}) treats `addonKey`
10
+ * itself plus any key beginning with `${addonKey}.`, `${addonKey}:` or
11
+ * `${addonKey}/` as belonging to the addon. Hosts that namespace their
12
+ * `model` strings differently can pass a custom matcher to
13
+ * {@link MetadataCacheState.invalidateAddon}.
14
+ */
15
+ export type AddonKeyMatcher = (cacheKey: string, addonKey: string) => boolean;
16
+ /**
17
+ * Default matcher used by {@link MetadataCacheState.invalidateAddon}.
18
+ * Mirrors the convention used by the kernel installer when it scopes
19
+ * model metadata under an addon's key.
20
+ */
21
+ export declare function defaultAddonKeyMatcher(cacheKey: string, addonKey: string): boolean;
7
22
  interface MetadataCacheState {
8
23
  cache: Record<string, TableMetadata>;
9
24
  modalCache: Record<string, TableMetadata>;
@@ -16,6 +31,26 @@ interface MetadataCacheState {
16
31
  hasMetadata: (key: string) => boolean;
17
32
  hasModalMetadata: (key: string) => boolean;
18
33
  prefetchAll: (api: MetadataApiClient) => Promise<void>;
34
+ /**
35
+ * Remove cached entries belonging to a specific addon. Used by the
36
+ * hot-swap subscriber when the kernel announces a manifest change.
37
+ *
38
+ * Returns the number of entries removed across both caches, which is
39
+ * useful for tests and observability — `0` means the cache had nothing
40
+ * to flush (the addon either hadn't been hit yet or uses a key
41
+ * convention the default matcher doesn't recognise; pass a custom
42
+ * `matcher` if your host namespaces differently).
43
+ *
44
+ * Also resets `prefetched` to `false` so the next mount re-runs
45
+ * `prefetchAll()` and the `metadataVersion` is allowed to advance to
46
+ * the kernel's freshly-bumped hash.
47
+ */
48
+ invalidateAddon: (addonKey: string, matcher?: AddonKeyMatcher) => number;
49
+ /**
50
+ * Remove every cached entry. Heavier hammer for hosts that prefer a
51
+ * blanket flush on hot-swap rather than per-addon scoping.
52
+ */
53
+ clearAll: () => void;
19
54
  }
20
55
  export declare const useMetadataCache: import("zustand").UseBoundStore<Omit<import("zustand").StoreApi<MetadataCacheState>, "setState" | "persist"> & {
21
56
  setState(partial: MetadataCacheState | Partial<MetadataCacheState> | ((state: MetadataCacheState) => MetadataCacheState | Partial<MetadataCacheState>), replace?: false | undefined): unknown;
@@ -1 +1 @@
1
- {"version":3,"file":"metadata-cache.d.ts","sourceRoot":"","sources":["../src/metadata-cache.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAE5C,MAAM,WAAW,iBAAiB;IAC9B,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,KAAK,OAAO,CAAC;QAAE,IAAI,EAAE,GAAG,CAAA;KAAE,CAAC,CAAA;CAC7D;AAED,UAAU,kBAAkB;IACxB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA;IACpC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA;IACzC,eAAe,EAAE,MAAM,CAAA;IACvB,UAAU,EAAE,OAAO,CAAA;IACnB,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,aAAa,GAAG,SAAS,CAAA;IACvD,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,aAAa,GAAG,SAAS,CAAA;IAC5D,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,KAAK,IAAI,CAAA;IAC3D,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,KAAK,IAAI,CAAA;IAChE,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAA;IACrC,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAA;IAC1C,WAAW,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CACzD;AAED,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;EAwE5B,CAAA"}
1
+ {"version":3,"file":"metadata-cache.d.ts","sourceRoot":"","sources":["../src/metadata-cache.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAE5C,MAAM,WAAW,iBAAiB;IAC9B,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,KAAK,OAAO,CAAC;QAAE,IAAI,EAAE,GAAG,CAAA;KAAE,CAAC,CAAA;CAC7D;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAA;AAE7E;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAQlF;AAED,UAAU,kBAAkB;IACxB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA;IACpC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA;IACzC,eAAe,EAAE,MAAM,CAAA;IACvB,UAAU,EAAE,OAAO,CAAA;IACnB,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,aAAa,GAAG,SAAS,CAAA;IACvD,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,aAAa,GAAG,SAAS,CAAA;IAC5D,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,KAAK,IAAI,CAAA;IAC3D,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,KAAK,IAAI,CAAA;IAChE,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAA;IACrC,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAA;IAC1C,WAAW,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACtD;;;;;;;;;;;;;OAaG;IACH,eAAe,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,KAAK,MAAM,CAAA;IACxE;;;OAGG;IACH,QAAQ,EAAE,MAAM,IAAI,CAAA;CACvB;AAED,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;EAiH5B,CAAA"}
@@ -8,6 +8,20 @@
8
8
  // never invokes prefetchAll, the `api` dep is not required.
9
9
  import { create } from 'zustand';
10
10
  import { persist } from 'zustand/middleware';
11
+ /**
12
+ * Default matcher used by {@link MetadataCacheState.invalidateAddon}.
13
+ * Mirrors the convention used by the kernel installer when it scopes
14
+ * model metadata under an addon's key.
15
+ */
16
+ export function defaultAddonKeyMatcher(cacheKey, addonKey) {
17
+ if (!addonKey)
18
+ return false;
19
+ if (cacheKey === addonKey)
20
+ return true;
21
+ return (cacheKey.startsWith(`${addonKey}.`) ||
22
+ cacheKey.startsWith(`${addonKey}:`) ||
23
+ cacheKey.startsWith(`${addonKey}/`));
24
+ }
11
25
  export const useMetadataCache = create()(persist((set, get) => ({
12
26
  cache: {},
13
27
  modalCache: {},
@@ -27,6 +41,47 @@ export const useMetadataCache = create()(persist((set, get) => ({
27
41
  },
28
42
  hasMetadata: (key) => key in get().cache,
29
43
  hasModalMetadata: (key) => key in get().modalCache,
44
+ invalidateAddon: (addonKey, matcher = defaultAddonKeyMatcher) => {
45
+ if (!addonKey)
46
+ return 0;
47
+ let removed = 0;
48
+ const state = get();
49
+ const nextCache = {};
50
+ for (const [key, value] of Object.entries(state.cache)) {
51
+ if (matcher(key, addonKey)) {
52
+ removed += 1;
53
+ continue;
54
+ }
55
+ nextCache[key] = value;
56
+ }
57
+ const nextModalCache = {};
58
+ for (const [key, value] of Object.entries(state.modalCache)) {
59
+ if (matcher(key, addonKey)) {
60
+ removed += 1;
61
+ continue;
62
+ }
63
+ nextModalCache[key] = value;
64
+ }
65
+ if (removed === 0)
66
+ return 0;
67
+ set({
68
+ cache: nextCache,
69
+ modalCache: nextModalCache,
70
+ // Allow prefetchAll() to re-run so we pick up the new
71
+ // metadataVersion the kernel emits alongside the
72
+ // bumped manifest.
73
+ prefetched: false,
74
+ });
75
+ return removed;
76
+ },
77
+ clearAll: () => {
78
+ set({
79
+ cache: {},
80
+ modalCache: {},
81
+ metadataVersion: '',
82
+ prefetched: false,
83
+ });
84
+ },
30
85
  prefetchAll: async (api) => {
31
86
  if (get().prefetched)
32
87
  return;
package/dist/types.d.ts CHANGED
@@ -74,6 +74,20 @@ export interface ColumnDefinition {
74
74
  icon?: string;
75
75
  color?: string;
76
76
  }[];
77
+ /**
78
+ * FK target model. When the kernel auto-derives this from a
79
+ * belongs_to relation (or an author sets it explicitly), the SDK
80
+ * resolves the column's options against `/api/options/<ref>?field=id`
81
+ * via `useOptionsResolver`. Wins over `searchEndpoint` for select
82
+ * widgets — `searchEndpoint` stays as the legacy escape hatch.
83
+ */
84
+ ref?: string;
85
+ /**
86
+ * Server-side validation rules the SDK can also pre-flight in the
87
+ * form layer. `custom` may be a literal slug or a $org.<key>
88
+ * reference resolved through the OrgConfigProvider.
89
+ */
90
+ validation?: FieldValidation;
77
91
  }
78
92
  export interface ActionCondition {
79
93
  field: string;
@@ -101,6 +115,12 @@ export interface ActionFieldDef {
101
115
  searchEndpoint?: string;
102
116
  validation?: FieldValidation;
103
117
  widget?: FieldWidget | string;
118
+ /**
119
+ * FK target model — same semantics as ColumnDefinition.ref. When
120
+ * present, DynamicForm resolves the field's options through
121
+ * `useOptionsResolver` against `/api/options/<ref>?field=id`.
122
+ */
123
+ ref?: string;
104
124
  }
105
125
  export interface ActionDefinition {
106
126
  key: string;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,aAAa;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,gBAAgB,EAAE,CAAA;IAC3B,OAAO,EAAE,gBAAgB,EAAE,CAAA;IAC3B,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAA;IAC5B,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,cAAc,EAAE,MAAM,CAAA;IACtB,iBAAiB,EAAE,MAAM,CAAA;IACzB,iBAAiB,EAAE,OAAO,CAAA;IAC1B,UAAU,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,QAAQ,GAAG,SAAS,GAAG,YAAY,GAAG,cAAc,GAAG,MAAM,CAAA;IACnE,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IACrF,cAAc,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAA;AAEjF,MAAM,WAAW,gBAAgB;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,qBAAqB,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,GAAG,eAAe,GAAG,OAAO,CAAA;IAC3I,QAAQ,EAAE,OAAO,CAAA;IACjB,UAAU,EAAE,OAAO,CAAA;IACnB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB;;;;OAIG;IACH,UAAU,CAAC,EAAE,gBAAgB,CAAA;IAC7B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACjC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IAC3E;;;;;;OAMG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;;;OAIG;IACH,UAAU,CAAC,EAAE,eAAe,CAAA;CAC/B;AAED,MAAM,WAAW,eAAe;IAC5B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG,QAAQ,CAAA;IACxC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;CAC3B;AASD,MAAM,WAAW,eAAe;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,MAAM,CAAA;CAClB;AAID,MAAM,MAAM,WAAW,GACjB,MAAM,GACN,UAAU,GACV,UAAU,GACV,OAAO,GACP,QAAQ,GACR,MAAM,GACN,QAAQ,GACR,QAAQ,CAAA;AAEd,MAAM,WAAW,cAAc;IAC3B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IAC5C,YAAY,CAAC,EAAE,GAAG,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,UAAU,CAAC,EAAE,eAAe,CAAA;IAC5B,MAAM,CAAC,EAAE,WAAW,GAAG,MAAM,CAAA;IAC7B;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,gBAAgB;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAA;IACpD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,eAAe,CAAA;IAC3B,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,MAAM,CAAC,EAAE,cAAc,EAAE,CAAA;IACzB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,UAAU,CAAC,EAAE,OAAO,CAAA;CACvB;AAED,MAAM,WAAW,WAAW,CAAC,CAAC;IAC1B,OAAO,EAAE,OAAO,CAAA;IAChB,IAAI,EAAE,CAAC,CAAA;IACP,IAAI,CAAC,EAAE,cAAc,CAAA;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,cAAc;IAC3B,YAAY,EAAE,MAAM,CAAA;IACpB,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;CAChB;AAKD,MAAM,WAAW,cAAc;IAC3B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,MAAM,CAAC,EAAE,cAAc,EAAE,CAAA;IACzB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,UAAU,CAAC,EAAE,OAAO,CAAA;CACvB"}
@@ -0,0 +1,87 @@
1
+ export interface ResolvedOption {
2
+ /** Canonical id (server-side primary key). */
3
+ id: string | number;
4
+ /** Same as `id` — preserved for legacy frontend parity. */
5
+ value: string | number;
6
+ /** Display string. */
7
+ label: string;
8
+ /** Same as `label` — preserved for legacy frontend parity. */
9
+ name: string;
10
+ description?: string | null;
11
+ image?: string | null;
12
+ color?: string | null;
13
+ icon?: string | null;
14
+ }
15
+ export interface OptionsMeta {
16
+ /** 'static' for inline options, 'dynamic' for FK-resolved lists. */
17
+ type: 'static' | 'dynamic' | string;
18
+ /** Number of options the server returned in this batch. */
19
+ count: number;
20
+ }
21
+ export interface UseOptionsResolverArgs {
22
+ /**
23
+ * The owning model whose options endpoint is queried. Pass the model
24
+ * key (e.g. 'sales_orders'). Required — passing an empty string puts
25
+ * the hook in idle mode and no fetch fires.
26
+ */
27
+ modelKey: string;
28
+ /**
29
+ * Field on `modelKey` to resolve. Maps to `?field=<fieldKey>`.
30
+ */
31
+ fieldKey: string;
32
+ /**
33
+ * Optional FK target. When set the hook resolves against
34
+ * `/api/options/<ref>?field=id` instead of `/api/options/<modelKey>`.
35
+ * This is the canonical path the kernel auto-derives from
36
+ * `ColumnDef.Ref`. Prefer this over `endpoint`.
37
+ */
38
+ ref?: string;
39
+ /**
40
+ * Free-text query forwarded as `?q=`. Empty values are skipped so the
41
+ * server returns the first page unfiltered.
42
+ */
43
+ query?: string;
44
+ /**
45
+ * Server-side pagination cap. Defaults to 50 (kernel
46
+ * DefaultOptionsLimit) if omitted.
47
+ */
48
+ limit?: number;
49
+ /**
50
+ * Toggle to disable fetching entirely (e.g. while a parent row is
51
+ * still loading). Defaults to true.
52
+ */
53
+ enabled?: boolean;
54
+ /**
55
+ * Escape hatch for callers that need a non-canonical URL — e.g.
56
+ * legacy `/options/<custom>?...`. When set it overrides `ref` and
57
+ * `modelKey` for the fetch path. The query string is built from
58
+ * `fieldKey` / `query` / `limit` exactly the same way.
59
+ */
60
+ endpoint?: string;
61
+ }
62
+ export interface UseOptionsResolverResult {
63
+ options: ResolvedOption[];
64
+ meta: OptionsMeta | null;
65
+ loading: boolean;
66
+ error: Error | null;
67
+ /** Forces a refetch. Useful after a parent record updates. */
68
+ refetch: () => void;
69
+ }
70
+ /**
71
+ * Resolves select options for a field via the canonical
72
+ * `/api/options/:model?field=…` endpoint. Returns the v0.9.0 envelope
73
+ * `{ data, meta: { type, count } }` projected into a stable shape.
74
+ *
75
+ * The hook is intentionally minimal: it does NOT debounce `query`
76
+ * (callers should hold the controlled value and pass it post-debounce)
77
+ * and does NOT cache across hook instances (apps that need shared state
78
+ * compose this with TanStack Query in their own layer).
79
+ */
80
+ export declare function useOptionsResolver(args: UseOptionsResolverArgs): UseOptionsResolverResult;
81
+ /**
82
+ * Normalizes the wire shape into ResolvedOption. The kernel returns dual
83
+ * id/value and label/name fields for legacy parity — we accept either
84
+ * and surface a stable shape downstream.
85
+ */
86
+ export declare function projectOption(raw: any): ResolvedOption;
87
+ //# sourceMappingURL=use-options-resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-options-resolver.d.ts","sourceRoot":"","sources":["../src/use-options-resolver.ts"],"names":[],"mappings":"AAeA,MAAM,WAAW,cAAc;IAC3B,8CAA8C;IAC9C,EAAE,EAAE,MAAM,GAAG,MAAM,CAAA;IACnB,2DAA2D;IAC3D,KAAK,EAAE,MAAM,GAAG,MAAM,CAAA;IACtB,sBAAsB;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB;AAED,MAAM,WAAW,WAAW;IACxB,oEAAoE;IACpE,IAAI,EAAE,QAAQ,GAAG,SAAS,GAAG,MAAM,CAAA;IACnC,2DAA2D;IAC3D,KAAK,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,sBAAsB;IACnC;;;;OAIG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB;;;;;OAKG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,wBAAwB;IACrC,OAAO,EAAE,cAAc,EAAE,CAAA;IACzB,IAAI,EAAE,WAAW,GAAG,IAAI,CAAA;IACxB,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;IACnB,8DAA8D;IAC9D,OAAO,EAAE,MAAM,IAAI,CAAA;CACtB;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,sBAAsB,GAAG,wBAAwB,CAiHzF;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,GAAG,GAAG,cAAc,CAatD"}
@@ -0,0 +1,147 @@
1
+ // useOptionsResolver — single hook the SDK uses to fetch select options
2
+ // for a metadata-driven field. Replaces the ad-hoc `/data/<model>` reads
3
+ // that DynamicForm and DynamicRelation used to do.
4
+ //
5
+ // Contract (matches kernel ≥ v0.9.0):
6
+ // GET /api/options/:model?field=<key>&q=<text>&limit=<n>
7
+ // → { success: true, data: Option[], meta: { type: 'static'|'dynamic', count } }
8
+ //
9
+ // The hook prefers `ColumnDef.Ref` (auto-derived by the kernel from
10
+ // belongs_to relations) over a hand-wired `searchEndpoint`. Apps that
11
+ // adopt Ref via the kernel auto-derivation get the right behaviour for
12
+ // free; legacy callers that still ship `searchEndpoint` keep working.
13
+ import { useEffect, useMemo, useRef, useState } from 'react';
14
+ import { useApi } from './api-context';
15
+ /**
16
+ * Resolves select options for a field via the canonical
17
+ * `/api/options/:model?field=…` endpoint. Returns the v0.9.0 envelope
18
+ * `{ data, meta: { type, count } }` projected into a stable shape.
19
+ *
20
+ * The hook is intentionally minimal: it does NOT debounce `query`
21
+ * (callers should hold the controlled value and pass it post-debounce)
22
+ * and does NOT cache across hook instances (apps that need shared state
23
+ * compose this with TanStack Query in their own layer).
24
+ */
25
+ export function useOptionsResolver(args) {
26
+ const { modelKey, fieldKey, ref, query, limit, enabled = true, endpoint, } = args;
27
+ const api = useApi();
28
+ const [options, setOptions] = useState([]);
29
+ const [meta, setMeta] = useState(null);
30
+ const [loading, setLoading] = useState(false);
31
+ const [error, setError] = useState(null);
32
+ // refreshKey is bumped by `refetch` to force the effect to re-run
33
+ // even when none of the input args changed.
34
+ const [refreshKey, setRefreshKey] = useState(0);
35
+ // The URL the hook hits. Ref wins over modelKey because the kernel's
36
+ // auto-derivation makes ref the canonical pointer; a manual endpoint
37
+ // wins over both as the explicit override.
38
+ const url = useMemo(() => {
39
+ if (endpoint)
40
+ return endpoint;
41
+ if (ref)
42
+ return `/options/${ref}`;
43
+ if (!modelKey)
44
+ return '';
45
+ return `/options/${modelKey}`;
46
+ }, [endpoint, ref, modelKey]);
47
+ // The field to query. When using `ref` the canonical lookup field is
48
+ // `id` (FK targets the target model's PK), unless the caller wants
49
+ // to override that explicitly via `fieldKey`. We only inject the `id`
50
+ // default when `ref` is set AND `fieldKey` is empty.
51
+ const effectiveField = useMemo(() => {
52
+ if (fieldKey)
53
+ return fieldKey;
54
+ if (ref)
55
+ return 'id';
56
+ return '';
57
+ }, [fieldKey, ref]);
58
+ // Track the in-flight controller so a new fetch can abort the
59
+ // previous one — matters for typeahead callers passing changing `query`.
60
+ const abortRef = useRef(null);
61
+ useEffect(() => {
62
+ if (!enabled || !url || !effectiveField) {
63
+ setOptions([]);
64
+ setMeta(null);
65
+ setLoading(false);
66
+ setError(null);
67
+ return;
68
+ }
69
+ // Cancel any pending request before issuing a new one.
70
+ abortRef.current?.abort();
71
+ const controller = new AbortController();
72
+ abortRef.current = controller;
73
+ setLoading(true);
74
+ setError(null);
75
+ const params = { field: effectiveField };
76
+ if (query)
77
+ params.q = query;
78
+ if (typeof limit === 'number' && limit > 0)
79
+ params.limit = limit;
80
+ api.get(url, { params, signal: controller.signal })
81
+ .then((res) => {
82
+ if (controller.signal.aborted)
83
+ return;
84
+ const body = res.data;
85
+ if (!body || body.success !== true) {
86
+ throw new Error(body?.message || 'options resolver: unsuccessful response');
87
+ }
88
+ const rawOptions = Array.isArray(body.data) ? body.data : [];
89
+ const projected = rawOptions.map(projectOption);
90
+ setOptions(projected);
91
+ // v0.9.0 envelope: meta.type / meta.count. We tolerate
92
+ // older deployments that still emit a root-level `type`
93
+ // by reading either spot — the projection prefers the
94
+ // canonical location so the SDK guides apps to the new
95
+ // shape without breaking grace-period upgrades.
96
+ const metaPayload = body.meta && typeof body.meta === 'object'
97
+ ? body.meta
98
+ : { type: body.type, count: rawOptions.length };
99
+ setMeta({
100
+ type: metaPayload?.type ?? 'dynamic',
101
+ count: typeof metaPayload?.count === 'number'
102
+ ? metaPayload.count
103
+ : rawOptions.length,
104
+ });
105
+ })
106
+ .catch((err) => {
107
+ if (controller.signal.aborted)
108
+ return;
109
+ setError(err instanceof Error ? err : new Error(String(err)));
110
+ setOptions([]);
111
+ setMeta(null);
112
+ })
113
+ .finally(() => {
114
+ if (!controller.signal.aborted)
115
+ setLoading(false);
116
+ });
117
+ return () => {
118
+ controller.abort();
119
+ };
120
+ }, [api, url, effectiveField, query, limit, enabled, refreshKey]);
121
+ return {
122
+ options,
123
+ meta,
124
+ loading,
125
+ error,
126
+ refetch: () => setRefreshKey((k) => k + 1),
127
+ };
128
+ }
129
+ /**
130
+ * Normalizes the wire shape into ResolvedOption. The kernel returns dual
131
+ * id/value and label/name fields for legacy parity — we accept either
132
+ * and surface a stable shape downstream.
133
+ */
134
+ export function projectOption(raw) {
135
+ const id = raw?.id ?? raw?.value ?? '';
136
+ const label = String(raw?.label ?? raw?.name ?? id ?? '');
137
+ return {
138
+ id,
139
+ value: raw?.value ?? id,
140
+ label,
141
+ name: String(raw?.name ?? label),
142
+ description: raw?.description ?? null,
143
+ image: raw?.image ?? null,
144
+ color: raw?.color ?? null,
145
+ icon: raw?.icon ?? null,
146
+ };
147
+ }
@@ -0,0 +1,28 @@
1
+ export interface OrgConfigBridge {
2
+ /** Resolves a `$org.<key>` reference (or plain key) to a literal id. */
3
+ resolveValidator: (refOrKey: string) => string | null;
4
+ /** When true the app actually has a provider mounted. */
5
+ available: boolean;
6
+ }
7
+ /**
8
+ * Apps that consume `runtime-react` AND `@asteby/metacore-app-providers`
9
+ * call this once near the root (typically inside the OrgConfigProvider
10
+ * children) so the SDK reads the same resolver. Hosts without an org
11
+ * provider can ignore this entirely; the SDK's null bridge keeps every
12
+ * call returning `null` so $org.<key> tokens stay verbatim in the form
13
+ * — same fallback the kernel uses for unresolved references.
14
+ */
15
+ export declare function setOrgConfigBridge(bridge: OrgConfigBridge | null): void;
16
+ /**
17
+ * Returns the active bridge. Pure read — no React hook so it can be
18
+ * called from non-component code (zod schema builders, helpers).
19
+ */
20
+ export declare function getOrgConfigBridge(): OrgConfigBridge;
21
+ /**
22
+ * Resolves a Validation token into the validator identifier the SDK
23
+ * should apply. Returns the resolved literal when the org config knows
24
+ * the key, or the original token when it doesn't (so apps can decide).
25
+ * Plain literals (no `$org.` prefix) pass through.
26
+ */
27
+ export declare function resolveValidatorToken(token: string | undefined | null): string | null;
28
+ //# sourceMappingURL=use-org-config-bridge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-org-config-bridge.d.ts","sourceRoot":"","sources":["../src/use-org-config-bridge.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,eAAe;IAC5B,wEAAwE;IACxE,gBAAgB,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAA;IACrD,yDAAyD;IACzD,SAAS,EAAE,OAAO,CAAA;CACrB;AASD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,QAEhE;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,eAAe,CAEpD;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAKrF"}