@zeyos/zx 2.1.0 → 2.2.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 (311) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +15 -0
  3. package/dist/types/compat/base.d.ts +156 -0
  4. package/dist/types/compat/globals.d.ts +20 -0
  5. package/dist/types/compat/index.d.ts +128 -0
  6. package/dist/types/compat/map/basic.d.ts +175 -0
  7. package/dist/types/compat/map/data.d.ts +102 -0
  8. package/dist/types/compat/map/date-time.d.ts +80 -0
  9. package/dist/types/compat/map/factory.d.ts +9 -0
  10. package/dist/types/compat/map/form.d.ts +98 -0
  11. package/dist/types/compat/map/helpers.d.ts +12 -0
  12. package/dist/types/compat/map/http.d.ts +36 -0
  13. package/dist/types/compat/map/layout.d.ts +106 -0
  14. package/dist/types/compat/map/options.d.ts +80 -0
  15. package/dist/types/compat/map/select.d.ts +95 -0
  16. package/dist/types/compat/map/stubs.d.ts +32 -0
  17. package/dist/types/compat/map/utilities.d.ts +37 -0
  18. package/dist/types/compat-entry.d.ts +5 -0
  19. package/dist/types/components/badge/badge.d.ts +46 -0
  20. package/dist/types/components/breadcrumb/breadcrumb.d.ts +200 -0
  21. package/dist/types/components/button/button.d.ts +42 -0
  22. package/dist/types/components/check-button/check-button.d.ts +98 -0
  23. package/dist/types/components/checklist/checklist.d.ts +143 -0
  24. package/dist/types/components/checklist/field-adapter.d.ts +5 -0
  25. package/dist/types/components/context-menu/context-menu.d.ts +130 -0
  26. package/dist/types/components/copy/copy-button.d.ts +48 -0
  27. package/dist/types/components/copy/copy-input.d.ts +91 -0
  28. package/dist/types/components/data-filter/data-filter.d.ts +250 -0
  29. package/dist/types/components/data-filter/filter-core.d.ts +52 -0
  30. package/dist/types/components/date-picker/date-picker.d.ts +137 -0
  31. package/dist/types/components/date-picker/date-range-picker.d.ts +350 -0
  32. package/dist/types/components/date-picker/field-adapter.d.ts +6 -0
  33. package/dist/types/components/date-picker/month-picker.d.ts +75 -0
  34. package/dist/types/components/date-picker/range-field-adapter.d.ts +7 -0
  35. package/dist/types/components/date-picker/time-picker.d.ts +127 -0
  36. package/dist/types/components/datebox/date-range-box.d.ts +218 -0
  37. package/dist/types/components/datebox/datebox.d.ts +160 -0
  38. package/dist/types/components/datebox/field-adapter.d.ts +5 -0
  39. package/dist/types/components/dialog/dialog.d.ts +284 -0
  40. package/dist/types/components/dropdown/dropdown.d.ts +118 -0
  41. package/dist/types/components/empty-state/empty-state.d.ts +38 -0
  42. package/dist/types/components/field/field.d.ts +231 -0
  43. package/dist/types/components/field-upload/field-adapter.d.ts +5 -0
  44. package/dist/types/components/field-upload/field-upload.d.ts +213 -0
  45. package/dist/types/components/fieldset/fieldset.d.ts +125 -0
  46. package/dist/types/components/finder/finder.d.ts +252 -0
  47. package/dist/types/components/form/form.d.ts +194 -0
  48. package/dist/types/components/groupbox/groupbox.d.ts +89 -0
  49. package/dist/types/components/layout/layout.d.ts +132 -0
  50. package/dist/types/components/loading/inline-loading.d.ts +86 -0
  51. package/dist/types/components/loading/progress-bar.d.ts +168 -0
  52. package/dist/types/components/loading/spinner.d.ts +34 -0
  53. package/dist/types/components/master-panel/master-panel.d.ts +112 -0
  54. package/dist/types/components/menu-button/menu-button.d.ts +142 -0
  55. package/dist/types/components/message/message.d.ts +212 -0
  56. package/dist/types/components/modal/modal.d.ts +105 -0
  57. package/dist/types/components/multi-value-editor/field-adapter.d.ts +5 -0
  58. package/dist/types/components/multi-value-editor/multi-value-editor.d.ts +66 -0
  59. package/dist/types/components/navigation-bar/navigation-bar.d.ts +229 -0
  60. package/dist/types/components/number-field/field-adapter.d.ts +5 -0
  61. package/dist/types/components/number-field/number-field.d.ts +292 -0
  62. package/dist/types/components/pagination/pagination.d.ts +267 -0
  63. package/dist/types/components/panel/panel.d.ts +194 -0
  64. package/dist/types/components/permission/permission.d.ts +93 -0
  65. package/dist/types/components/rating/field-adapter.d.ts +5 -0
  66. package/dist/types/components/rating/rating.d.ts +213 -0
  67. package/dist/types/components/search/search.d.ts +98 -0
  68. package/dist/types/components/select/field-adapter.d.ts +5 -0
  69. package/dist/types/components/select/filter.d.ts +11 -0
  70. package/dist/types/components/select/select.d.ts +235 -0
  71. package/dist/types/components/skeleton/skeleton.d.ts +97 -0
  72. package/dist/types/components/slider/field-adapter.d.ts +5 -0
  73. package/dist/types/components/slider/slider.d.ts +216 -0
  74. package/dist/types/components/split-view/split-view.d.ts +335 -0
  75. package/dist/types/components/stepper/stepper.d.ts +314 -0
  76. package/dist/types/components/stepper/steps.d.ts +131 -0
  77. package/dist/types/components/tabbox/tabbox.d.ts +230 -0
  78. package/dist/types/components/table/sort.d.ts +49 -0
  79. package/dist/types/components/table/table.d.ts +872 -0
  80. package/dist/types/components/tag-picker/field-adapter.d.ts +5 -0
  81. package/dist/types/components/tag-picker/tag-picker.d.ts +375 -0
  82. package/dist/types/components/timebox/field-adapter.d.ts +5 -0
  83. package/dist/types/components/timebox/timebox.d.ts +121 -0
  84. package/dist/types/components/toggle/field-adapter.d.ts +5 -0
  85. package/dist/types/components/toggle/toggle.d.ts +94 -0
  86. package/dist/types/components/toolbar/toolbar.d.ts +230 -0
  87. package/dist/types/components/tooltip/tooltip.d.ts +183 -0
  88. package/dist/types/components/tree/hierarchy.d.ts +113 -0
  89. package/dist/types/components/tree/tree.d.ts +383 -0
  90. package/dist/types/components/truncate/truncate.d.ts +61 -0
  91. package/dist/types/components/value-list/field-adapter.d.ts +5 -0
  92. package/dist/types/components/value-list/value-list.d.ts +143 -0
  93. package/dist/types/core/breakpoint.d.ts +68 -0
  94. package/dist/types/core/component.d.ts +115 -0
  95. package/dist/types/core/date.d.ts +50 -0
  96. package/dist/types/core/dom.d.ts +86 -0
  97. package/dist/types/core/export.d.ts +70 -0
  98. package/dist/types/core/fontawesome.d.ts +107 -0
  99. package/dist/types/core/format.d.ts +107 -0
  100. package/dist/types/core/http.d.ts +122 -0
  101. package/dist/types/core/i18n.d.ts +35 -0
  102. package/dist/types/core/icons.d.ts +137 -0
  103. package/dist/types/core/keyboard.d.ts +72 -0
  104. package/dist/types/core/position.d.ts +45 -0
  105. package/dist/types/core/storage.d.ts +46 -0
  106. package/dist/types/core/util.d.ts +117 -0
  107. package/dist/types/elements/define.d.ts +73 -0
  108. package/dist/types/elements/reflect.d.ts +27 -0
  109. package/dist/types/index.d.ts +79 -0
  110. package/dist/types/zeyos/connect.d.ts +65 -0
  111. package/dist/types/zeyos/form.d.ts +140 -0
  112. package/dist/types/zeyos/icons.d.ts +104 -0
  113. package/dist/types/zeyos/index.d.ts +12 -0
  114. package/dist/types/zeyos/modules.d.ts +110 -0
  115. package/dist/types/zeyos/query.d.ts +157 -0
  116. package/dist/types/zeyos/schema.d.ts +79 -0
  117. package/dist/types/zeyos/select.d.ts +167 -0
  118. package/dist/types/zeyos/table.d.ts +205 -0
  119. package/dist/zx-compat.esm.js +362 -33
  120. package/dist/zx-compat.esm.min.js +1 -1
  121. package/dist/zx-compat.global.js +359 -67
  122. package/dist/zx-compat.global.min.js +3 -3
  123. package/dist/zx-zeyos.esm.js +342 -43
  124. package/dist/zx-zeyos.esm.min.js +1 -1
  125. package/dist/zx.css +68 -6
  126. package/dist/zx.esm.js +346 -67
  127. package/dist/zx.esm.min.js +3 -3
  128. package/dist/zx.global.js +346 -67
  129. package/dist/zx.global.min.js +3 -3
  130. package/dist/zx.min.css +1 -1
  131. package/docs/api.json +48 -2
  132. package/docs/llms.md +28 -0
  133. package/package.json +37 -9
  134. package/src/compat/base.js +291 -0
  135. package/src/compat/globals.js +165 -0
  136. package/src/compat/index.js +84 -0
  137. package/src/compat/map/basic.js +464 -0
  138. package/src/compat/map/data.js +232 -0
  139. package/src/compat/map/date-time.js +177 -0
  140. package/src/compat/map/factory.js +45 -0
  141. package/src/compat/map/form.js +330 -0
  142. package/src/compat/map/helpers.js +46 -0
  143. package/src/compat/map/http.js +117 -0
  144. package/src/compat/map/layout.js +256 -0
  145. package/src/compat/map/options.js +267 -0
  146. package/src/compat/map/select.js +198 -0
  147. package/src/compat/map/stubs.js +26 -0
  148. package/src/compat/map/utilities.js +113 -0
  149. package/src/compat-entry.js +11 -0
  150. package/src/components/badge/badge.css +147 -0
  151. package/src/components/badge/badge.js +92 -0
  152. package/src/components/breadcrumb/breadcrumb.css +105 -0
  153. package/src/components/breadcrumb/breadcrumb.js +331 -0
  154. package/src/components/button/button.css +104 -0
  155. package/src/components/button/button.js +76 -0
  156. package/src/components/check-button/check-button.css +66 -0
  157. package/src/components/check-button/check-button.js +167 -0
  158. package/src/components/checklist/checklist.css +65 -0
  159. package/src/components/checklist/checklist.js +282 -0
  160. package/src/components/checklist/field-adapter.js +46 -0
  161. package/src/components/context-menu/context-menu.css +54 -0
  162. package/src/components/context-menu/context-menu.js +334 -0
  163. package/src/components/copy/copy-button.css +27 -0
  164. package/src/components/copy/copy-button.js +78 -0
  165. package/src/components/copy/copy-input.css +59 -0
  166. package/src/components/copy/copy-input.js +123 -0
  167. package/src/components/data-filter/data-filter.css +53 -0
  168. package/src/components/data-filter/data-filter.js +400 -0
  169. package/src/components/data-filter/filter-core.js +128 -0
  170. package/src/components/date-picker/date-picker.css +181 -0
  171. package/src/components/date-picker/date-picker.js +500 -0
  172. package/src/components/date-picker/date-range-picker.css +264 -0
  173. package/src/components/date-picker/date-range-picker.js +990 -0
  174. package/src/components/date-picker/field-adapter.js +36 -0
  175. package/src/components/date-picker/month-picker.js +223 -0
  176. package/src/components/date-picker/range-field-adapter.js +31 -0
  177. package/src/components/date-picker/time-picker.css +191 -0
  178. package/src/components/date-picker/time-picker.js +406 -0
  179. package/src/components/datebox/date-range-box.css +128 -0
  180. package/src/components/datebox/date-range-box.js +492 -0
  181. package/src/components/datebox/datebox.css +108 -0
  182. package/src/components/datebox/datebox.js +358 -0
  183. package/src/components/datebox/field-adapter.js +34 -0
  184. package/src/components/dialog/dialog.css +74 -0
  185. package/src/components/dialog/dialog.js +450 -0
  186. package/src/components/dropdown/dropdown.css +24 -0
  187. package/src/components/dropdown/dropdown.js +288 -0
  188. package/src/components/empty-state/empty-state.css +80 -0
  189. package/src/components/empty-state/empty-state.js +92 -0
  190. package/src/components/field/field.css +127 -0
  191. package/src/components/field/field.js +605 -0
  192. package/src/components/field-upload/field-adapter.js +21 -0
  193. package/src/components/field-upload/field-upload.css +108 -0
  194. package/src/components/field-upload/field-upload.js +475 -0
  195. package/src/components/fieldset/fieldset.css +37 -0
  196. package/src/components/fieldset/fieldset.js +307 -0
  197. package/src/components/finder/finder.css +98 -0
  198. package/src/components/finder/finder.js +509 -0
  199. package/src/components/form/form.css +32 -0
  200. package/src/components/form/form.js +302 -0
  201. package/src/components/groupbox/groupbox.css +51 -0
  202. package/src/components/groupbox/groupbox.js +199 -0
  203. package/src/components/layout/layout.css +117 -0
  204. package/src/components/layout/layout.js +136 -0
  205. package/src/components/loading/inline-loading.css +34 -0
  206. package/src/components/loading/inline-loading.js +140 -0
  207. package/src/components/loading/progress-bar.css +115 -0
  208. package/src/components/loading/progress-bar.js +259 -0
  209. package/src/components/loading/spinner.css +58 -0
  210. package/src/components/loading/spinner.js +57 -0
  211. package/src/components/master-panel/master-panel.css +51 -0
  212. package/src/components/master-panel/master-panel.js +203 -0
  213. package/src/components/menu-button/menu-button.css +49 -0
  214. package/src/components/menu-button/menu-button.js +294 -0
  215. package/src/components/message/message.css +122 -0
  216. package/src/components/message/message.js +499 -0
  217. package/src/components/modal/modal.css +47 -0
  218. package/src/components/modal/modal.js +203 -0
  219. package/src/components/multi-value-editor/field-adapter.js +44 -0
  220. package/src/components/multi-value-editor/multi-value-editor.css +56 -0
  221. package/src/components/multi-value-editor/multi-value-editor.js +170 -0
  222. package/src/components/navigation-bar/navigation-bar.css +130 -0
  223. package/src/components/navigation-bar/navigation-bar.js +450 -0
  224. package/src/components/number-field/field-adapter.js +40 -0
  225. package/src/components/number-field/number-field.css +77 -0
  226. package/src/components/number-field/number-field.js +545 -0
  227. package/src/components/pagination/pagination.css +95 -0
  228. package/src/components/pagination/pagination.js +481 -0
  229. package/src/components/panel/panel.css +123 -0
  230. package/src/components/panel/panel.js +336 -0
  231. package/src/components/permission/permission.css +47 -0
  232. package/src/components/permission/permission.js +198 -0
  233. package/src/components/rating/field-adapter.js +34 -0
  234. package/src/components/rating/rating.css +97 -0
  235. package/src/components/rating/rating.js +395 -0
  236. package/src/components/search/search.css +70 -0
  237. package/src/components/search/search.js +193 -0
  238. package/src/components/select/field-adapter.js +24 -0
  239. package/src/components/select/filter.js +39 -0
  240. package/src/components/select/select.css +180 -0
  241. package/src/components/select/select.js +663 -0
  242. package/src/components/skeleton/skeleton.css +76 -0
  243. package/src/components/skeleton/skeleton.js +139 -0
  244. package/src/components/slider/field-adapter.js +36 -0
  245. package/src/components/slider/slider.css +181 -0
  246. package/src/components/slider/slider.js +399 -0
  247. package/src/components/split-view/split-view.css +139 -0
  248. package/src/components/split-view/split-view.js +806 -0
  249. package/src/components/stepper/stepper.css +234 -0
  250. package/src/components/stepper/stepper.js +459 -0
  251. package/src/components/stepper/steps.js +131 -0
  252. package/src/components/tabbox/tabbox.css +281 -0
  253. package/src/components/tabbox/tabbox.js +484 -0
  254. package/src/components/table/sort.js +85 -0
  255. package/src/components/table/table.css +364 -0
  256. package/src/components/table/table.js +1693 -0
  257. package/src/components/tag-picker/field-adapter.js +35 -0
  258. package/src/components/tag-picker/tag-picker.css +169 -0
  259. package/src/components/tag-picker/tag-picker.js +837 -0
  260. package/src/components/timebox/field-adapter.js +24 -0
  261. package/src/components/timebox/timebox.css +51 -0
  262. package/src/components/timebox/timebox.js +276 -0
  263. package/src/components/toggle/field-adapter.js +24 -0
  264. package/src/components/toggle/toggle.css +66 -0
  265. package/src/components/toggle/toggle.js +150 -0
  266. package/src/components/toolbar/toolbar.css +81 -0
  267. package/src/components/toolbar/toolbar.js +511 -0
  268. package/src/components/tooltip/tooltip.css +36 -0
  269. package/src/components/tooltip/tooltip.js +420 -0
  270. package/src/components/tree/hierarchy.js +151 -0
  271. package/src/components/tree/tree.css +99 -0
  272. package/src/components/tree/tree.js +784 -0
  273. package/src/components/truncate/truncate.css +20 -0
  274. package/src/components/truncate/truncate.js +92 -0
  275. package/src/components/value-list/field-adapter.js +24 -0
  276. package/src/components/value-list/value-list.css +108 -0
  277. package/src/components/value-list/value-list.js +336 -0
  278. package/src/core/breakpoint.js +150 -0
  279. package/src/core/component.js +292 -0
  280. package/src/core/date.js +216 -0
  281. package/src/core/dom.js +260 -0
  282. package/src/core/export.js +204 -0
  283. package/src/core/fontawesome.js +177 -0
  284. package/src/core/format.js +283 -0
  285. package/src/core/http.js +246 -0
  286. package/src/core/i18n.js +83 -0
  287. package/src/core/icons.js +283 -0
  288. package/src/core/keyboard.js +240 -0
  289. package/src/core/position.js +177 -0
  290. package/src/core/storage.js +175 -0
  291. package/src/core/util.js +336 -0
  292. package/src/elements/define.js +715 -0
  293. package/src/elements/reflect.js +79 -0
  294. package/src/index.js +131 -0
  295. package/src/zeyos/connect.js +142 -0
  296. package/src/zeyos/form.js +254 -0
  297. package/src/zeyos/icons.js +92 -0
  298. package/src/zeyos/index.js +21 -0
  299. package/src/zeyos/modules.js +269 -0
  300. package/src/zeyos/query.js +199 -0
  301. package/src/zeyos/schema.js +474 -0
  302. package/src/zeyos/select.js +175 -0
  303. package/src/zeyos/table.js +272 -0
  304. package/styles/base.css +124 -0
  305. package/styles/icons.css +34 -0
  306. package/styles/tokens/dark.css +84 -0
  307. package/styles/tokens/density.css +9 -0
  308. package/styles/tokens/global.css +80 -0
  309. package/styles/tokens/modules.css +109 -0
  310. package/styles/tokens/semantic.css +49 -0
  311. package/styles/zx.css +61 -0
@@ -0,0 +1,474 @@
1
+ import { dateToUnixSeconds, unixSecondsToDate } from './query.js';
2
+
3
+ /** @typedef {Record<string, unknown>} FieldMeta */
4
+ /**
5
+ * @typedef {Object} FieldTypeStrategy
6
+ * @property {Record<string, unknown>|((meta: FieldMeta) => Record<string, unknown>)} field Zx Field descriptor fragment.
7
+ * @property {Record<string, unknown>|((meta: FieldMeta) => Record<string, unknown>)} column Zx Table column fragment.
8
+ */
9
+ /**
10
+ * @typedef {Object} ResolveFieldOptions
11
+ * @property {string[]} [fields] Curated allow-list; its order becomes the output order.
12
+ * @property {string[]} [exclude] Field ids to omit.
13
+ * @property {Record<string, string>} [labels] Per-field label overrides.
14
+ */
15
+
16
+ /**
17
+ * Extensible ZeyOS semantic/DB type registry. Keys are normalized to lowercase. Built-in choices:
18
+ * references use `zxselect`; monetary/numeric data uses `float`; Unix timestamps use date widgets;
19
+ * enums use native Field options; booleans use `toggle`; arrays use `valuelist`; and long structured
20
+ * text uses `textarea`. Extend it through {@link registerFieldType} rather than replacing entries.
21
+ * @type {Record<string, FieldTypeStrategy>}
22
+ */
23
+ export const TYPE_MAP = Object.create(null);
24
+
25
+ const warnedUnknownTypes = new Set();
26
+
27
+ /**
28
+ * Registers or replaces a field/column strategy for a semantic format or DB type.
29
+ * @param {string} typeOrFormat Semantic format or database type.
30
+ * @param {FieldTypeStrategy} strategy Field and column descriptor builders/fragments.
31
+ * @returns {FieldTypeStrategy} The registered strategy.
32
+ */
33
+ export function registerFieldType(typeOrFormat, strategy) {
34
+ const key = normalizeType(typeOrFormat);
35
+ if (!key) throw new TypeError('ZeyOS field type must not be empty');
36
+ if (!strategy || typeof strategy !== 'object') throw new TypeError('Field type strategy must be an object');
37
+ for (const part of ['field', 'column']) {
38
+ const value = strategy[part];
39
+ if (typeof value !== 'function' && (!value || typeof value !== 'object' || Array.isArray(value))) {
40
+ throw new TypeError(`Field type strategy requires a ${part} builder or descriptor`);
41
+ }
42
+ }
43
+ TYPE_MAP[key] = strategy;
44
+ return strategy;
45
+ }
46
+
47
+ /**
48
+ * Converts normalized ZeyOS field metadata to a Zx Field configuration. Semantic `format` wins
49
+ * over the database `type`; relations and enums are detected before scalar DB types. Read-only
50
+ * fields become disabled controls and NOT NULL/required metadata becomes `required: true`.
51
+ * @param {FieldMeta} meta Normalized metadata containing at least `id` or `name`.
52
+ * @returns {Record<string, unknown>} Plain Zx Field descriptor.
53
+ */
54
+ export function fieldToZxField(meta) {
55
+ const normalized = normalizeMeta(meta);
56
+ const strategy = strategyFor(normalized);
57
+ const fragment = strategyPart(strategy.field, normalized);
58
+ const descriptor = {
59
+ id: normalized.id,
60
+ type: 'text',
61
+ label: fieldLabel(normalized),
62
+ ...fragment
63
+ };
64
+ if (normalized.description != null) descriptor.description = String(normalized.description);
65
+ if (normalized.required === true || normalized.nullable === false) descriptor.required = true;
66
+ if (isReadonly(normalized)) {
67
+ descriptor.readonly = true;
68
+ descriptor.disabled = true;
69
+ descriptor.props = { ...(descriptor.props ?? {}), readOnly: true };
70
+ }
71
+ return descriptor;
72
+ }
73
+
74
+ /**
75
+ * Converts normalized ZeyOS field metadata to a Zx Table column configuration. Renderers return
76
+ * strings only (never DOM): references/enums show labels, numeric columns are right-aligned,
77
+ * booleans show a check, and Unix seconds are formatted as dates.
78
+ * @param {FieldMeta} meta Normalized metadata containing at least `id` or `name`.
79
+ * @returns {Record<string, unknown>} Plain Zx Table column descriptor.
80
+ */
81
+ export function fieldToZxColumn(meta) {
82
+ const normalized = normalizeMeta(meta);
83
+ const strategy = strategyFor(normalized);
84
+ return {
85
+ id: normalized.id,
86
+ label: fieldLabel(normalized),
87
+ sortable: normalized.sortable !== false,
88
+ ...strategyPart(strategy.column, normalized)
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Resolves a resource's runtime client schema into ordered normalized field metadata. The current
94
+ * client returns names from `schema.fields(resource)` and definitions from
95
+ * `schema.describe(resource).fields`; embedded `enum` data is authoritative. A compatible
96
+ * `schema.enums(resource, field)` is used when supplied by another client version/stub.
97
+ * @param {Record<string, any>} client Injected `@zeyos/client` instance.
98
+ * @param {string} resource Schema resource name such as `tickets`.
99
+ * @param {ResolveFieldOptions} [opts={}] Curation options.
100
+ * @returns {FieldMeta[]} Ordered normalized metadata.
101
+ */
102
+ export function resolveFields(client, resource, opts = {}) {
103
+ const schema = client?.schema;
104
+ if (!schema || typeof schema.fields !== 'function' || typeof schema.describe !== 'function') {
105
+ throw new TypeError('ZeyOS client must expose schema.fields() and schema.describe()');
106
+ }
107
+ const described = schema.describe(resource);
108
+ const describedFields = normalizeFieldCollection(described?.fields);
109
+ const listedFields = normalizeFieldCollection(schema.fields(resource));
110
+ const available = new Map();
111
+
112
+ for (const [id, listed] of listedFields) {
113
+ available.set(id, { ...listed, ...(describedFields.get(id) ?? {}) });
114
+ }
115
+ for (const [id, definition] of describedFields) {
116
+ if (!available.has(id)) available.set(id, { ...definition });
117
+ }
118
+
119
+ const requested = Array.isArray(opts.fields) ? opts.fields.map(String) : [...available.keys()];
120
+ const excluded = new Set(Array.isArray(opts.exclude) ? opts.exclude.map(String) : []);
121
+ const labels = opts.labels && typeof opts.labels === 'object' ? opts.labels : {};
122
+ const result = [];
123
+
124
+ for (const id of requested) {
125
+ if (excluded.has(id) || !available.has(id)) continue;
126
+ const definition = available.get(id) ?? {};
127
+ const meta = { ...definition, id, name: id, resource: String(resource) };
128
+ const enumValues = definition.enums ?? definition.enum ?? schemaEnum(schema, resource, id);
129
+ if (enumValues != null) {
130
+ meta.enums = enumValues;
131
+ if (meta.enum == null) meta.enum = enumValues;
132
+ }
133
+ if (Object.prototype.hasOwnProperty.call(labels, id)) meta.label = String(labels[id]);
134
+ result.push(meta);
135
+ }
136
+ return result;
137
+ }
138
+
139
+ const plainColumn = { field: { type: 'text' }, column: {} };
140
+ const intColumn = { field: { type: 'int' }, column: { align: 'right' } };
141
+ const floatColumn = {
142
+ field: { type: 'float' },
143
+ column: (meta) => ({ align: 'right', render: numberRenderer(meta) })
144
+ };
145
+ const moneyColumn = {
146
+ field: { type: 'float' },
147
+ column: (meta) => ({ align: 'right', render: numberRenderer(meta, true) })
148
+ };
149
+ const entityColumn = {
150
+ field: (meta) => ({ type: 'zxselect', props: { entity: referencedResource(meta) } }),
151
+ column: (meta) => ({ render: (row) => entityLabel(rowValue(row, meta), row, meta) })
152
+ };
153
+ const booleanColumn = {
154
+ field: { type: 'toggle' },
155
+ column: (meta) => ({
156
+ align: 'center',
157
+ render: (row) => Boolean(rowValue(row, meta)) ? '✓' : '',
158
+ sortValue: (row) => Number(Boolean(rowValue(row, meta)))
159
+ })
160
+ };
161
+ const enumColumn = {
162
+ field: (meta) => ({ type: enumFieldType(meta), options: enumOptions(meta) }),
163
+ column: (meta) => ({ render: enumRenderer(meta) })
164
+ };
165
+ const priorityColumn = {
166
+ field: { type: 'zxselect', props: { preset: 'priority' } },
167
+ column: (meta) => ({ render: enumRenderer(meta, priorityOptions()) })
168
+ };
169
+ const percentColumn = {
170
+ field: { type: 'float', props: { min: 0, max: 100 } },
171
+ column: (meta) => ({
172
+ align: 'right',
173
+ render: (row) => formatPercent(rowValue(row, meta)),
174
+ sortValue: (row) => finiteNumber(rowValue(row, meta))
175
+ })
176
+ };
177
+ const dateColumn = dateStrategy('date');
178
+ const dateTimeColumn = dateStrategy('datetime');
179
+
180
+ for (const type of ['string', 'text', 'character', 'character varying', 'varchar', 'email', 'tel', 'url', 'uri']) {
181
+ registerFieldType(type, plainColumn);
182
+ }
183
+ for (const type of ['integer', 'smallint', 'bigint', 'int', 'int2', 'int4', 'int8', 'int32', 'int64']) {
184
+ registerFieldType(type, intColumn);
185
+ }
186
+ for (const type of ['number', 'numeric', 'decimal', 'real', 'double', 'double precision', 'float']) {
187
+ registerFieldType(type, floatColumn);
188
+ }
189
+ for (const type of ['money', 'currency', 'price', 'pricebilling', 'priceprocurement', 'priceproduction']) {
190
+ registerFieldType(type, moneyColumn);
191
+ }
192
+ for (const type of ['entity', 't_entity']) registerFieldType(type, entityColumn);
193
+ for (const type of ['boolean', 'checked']) registerFieldType(type, booleanColumn);
194
+ for (const type of ['list', 'enum']) registerFieldType(type, enumColumn);
195
+ for (const type of ['progress', 'percent']) registerFieldType(type, percentColumn);
196
+ for (const type of ['date']) registerFieldType(type, dateColumn);
197
+ for (const type of ['datetime', 'date-time']) registerFieldType(type, dateTimeColumn);
198
+ registerFieldType('priority', priorityColumn);
199
+ registerFieldType('text[]', { field: { type: 'valuelist' }, column: {} });
200
+ registerFieldType('json', { field: { type: 'textarea' }, column: {} });
201
+ registerFieldType('longtext', { field: { type: 'textarea' }, column: {} });
202
+
203
+ // Typed-input semantics share a plain column, but add the appropriate native input hint.
204
+ for (const type of ['email', 'tel', 'url', 'uri']) {
205
+ registerFieldType(type, {
206
+ field: { type: 'text', props: { type: type === 'uri' ? 'url' : type } },
207
+ column: {}
208
+ });
209
+ }
210
+
211
+ /** @param {'date'|'datetime'} type @returns {FieldTypeStrategy} */
212
+ function dateStrategy(type) {
213
+ return {
214
+ field: { type },
215
+ column: (meta) => ({
216
+ render: (row) => formatUnixDate(rowValue(row, meta), type === 'datetime'),
217
+ sortValue: (row) => unixSortValue(rowValue(row, meta))
218
+ })
219
+ };
220
+ }
221
+
222
+ /** @param {FieldMeta} meta @returns {FieldTypeStrategy} */
223
+ function strategyFor(meta) {
224
+ const key = strategyKey(meta);
225
+ const strategy = TYPE_MAP[key];
226
+ if (strategy) return strategy;
227
+ if (!warnedUnknownTypes.has(key)) {
228
+ warnedUnknownTypes.add(key);
229
+ console.warn(`[zx.zeyos] Unknown field type or format "${key || 'unknown'}" for "${meta.id}"; using text.`);
230
+ }
231
+ return plainColumn;
232
+ }
233
+
234
+ /** @param {FieldMeta} meta @returns {string} */
235
+ function strategyKey(meta) {
236
+ const semantic = normalizeType(meta.format ?? meta.semanticFormat ?? meta.semanticType ?? meta.widget);
237
+ if (semantic && TYPE_MAP[semantic]) return semantic;
238
+ if (referencedResource(meta) || normalizeType(meta.type) === 't_entity') return 'entity';
239
+ if (String(meta.id).toLowerCase() === 'priority') return 'priority';
240
+ if (meta.enums != null || meta.enum != null) return 'enum';
241
+
242
+ const raw = normalizeType(meta.type);
243
+ if (raw === 'bigint' || raw === 'int64') {
244
+ const id = String(meta.id).toLowerCase();
245
+ if (id === 'date' && meta.indexed) return 'date';
246
+ if (/(?:date|time)$/.test(id)) return 'datetime';
247
+ }
248
+ if (isLongText(meta, raw)) return 'longtext';
249
+ if (raw && TYPE_MAP[raw]) return raw;
250
+ if (raw.endsWith('[]')) return raw === 'text[]' ? 'text[]' : 'text[]';
251
+ if (raw === 'json' || raw === 'jsonb') return 'json';
252
+ if (/^character varying(?:\(\d+\))?$/.test(raw)) return 'character varying';
253
+ // An unfamiliar semantic hint is not useful on its own; the runtime DB type is the
254
+ // reliable fallback exposed by the current client.
255
+ return raw || semantic;
256
+ }
257
+
258
+ /** @param {FieldMeta} meta @returns {FieldMeta & {id: string}} */
259
+ function normalizeMeta(meta) {
260
+ if (!meta || typeof meta !== 'object' || Array.isArray(meta)) throw new TypeError('Field metadata must be an object');
261
+ const id = meta.id ?? meta.name ?? meta.field ?? meta.identifier ?? meta.reference;
262
+ if (id == null || String(id).trim() === '') throw new TypeError('Field metadata requires an id or name');
263
+ return { ...meta, id: String(id) };
264
+ }
265
+
266
+ /** @param {unknown} part @param {FieldMeta} meta @returns {Record<string, unknown>} */
267
+ function strategyPart(part, meta) {
268
+ const value = typeof part === 'function' ? part(meta) : part;
269
+ return value && typeof value === 'object' && !Array.isArray(value) ? { ...value } : {};
270
+ }
271
+
272
+ /** @param {unknown} value @returns {string} */
273
+ function normalizeType(value) {
274
+ return String(value ?? '').trim().toLowerCase().replace(/\s+/g, ' ');
275
+ }
276
+
277
+ /** @param {FieldMeta} meta @returns {string} */
278
+ function fieldLabel(meta) {
279
+ const label = meta.label ?? meta.title ?? meta.displayName;
280
+ return label == null || label === '' ? humanize(meta.id) : String(label);
281
+ }
282
+
283
+ /** @param {unknown} value @returns {string} */
284
+ function humanize(value) {
285
+ const source = String(value ?? '').split('.').at(-1) ?? '';
286
+ if (source === 'ID') return 'ID';
287
+ const words = source
288
+ .replace(/([a-z\d])([A-Z])/g, '$1 $2')
289
+ .replace(/[_-]+/g, ' ')
290
+ .trim();
291
+ return words ? words.charAt(0).toUpperCase() + words.slice(1) : source;
292
+ }
293
+
294
+ /** @param {FieldMeta} meta @returns {boolean} */
295
+ function isReadonly(meta) {
296
+ return meta.readonly === true || meta.readOnly === true || meta.writable === false || meta.mutable === false;
297
+ }
298
+
299
+ /** @param {FieldMeta} meta @param {string} raw @returns {boolean} */
300
+ function isLongText(meta, raw) {
301
+ if (meta.long === true || meta.multiline === true) return true;
302
+ const length = Number(meta.maxLength ?? raw.match(/^character varying\((\d+)\)$/)?.[1]);
303
+ return Number.isFinite(length) && length > 255;
304
+ }
305
+
306
+ /** @param {FieldMeta} meta @returns {string|null} */
307
+ function referencedResource(meta) {
308
+ for (const candidate of [meta.entity, meta.fk, meta.relation, meta.reference, meta.references]) {
309
+ if (typeof candidate === 'string' && candidate) return candidate;
310
+ if (candidate && typeof candidate === 'object') {
311
+ const resource = candidate.resource ?? candidate.target ?? candidate.table ?? candidate.entity;
312
+ if (typeof resource === 'string' && resource) return resource;
313
+ }
314
+ }
315
+ return null;
316
+ }
317
+
318
+ /** @param {unknown} collection @returns {Map<string, FieldMeta>} */
319
+ function normalizeFieldCollection(collection) {
320
+ const fields = new Map();
321
+ if (Array.isArray(collection)) {
322
+ for (const entry of collection) {
323
+ if (typeof entry === 'string') fields.set(entry, {});
324
+ else if (entry && typeof entry === 'object') {
325
+ const id = entry.id ?? entry.name ?? entry.field;
326
+ if (id != null) fields.set(String(id), { ...entry });
327
+ }
328
+ }
329
+ } else if (collection && typeof collection === 'object') {
330
+ for (const [id, definition] of Object.entries(collection)) {
331
+ fields.set(id, definition && typeof definition === 'object' ? { ...definition } : {});
332
+ }
333
+ }
334
+ return fields;
335
+ }
336
+
337
+ /** @param {Record<string, any>} schema @param {string} resource @param {string} field @returns {unknown} */
338
+ function schemaEnum(schema, resource, field) {
339
+ if (typeof schema.enums !== 'function') return null;
340
+ const found = schema.enums(resource, field);
341
+ if (found && typeof found === 'object' && !Array.isArray(found) && Object.prototype.hasOwnProperty.call(found, field)) {
342
+ return found[field];
343
+ }
344
+ return found ?? null;
345
+ }
346
+
347
+ /** @param {FieldMeta} meta @returns {Record<string, string>} */
348
+ function enumOptions(meta) {
349
+ return normalizeEnumOptions(meta.enums ?? meta.enum ?? meta.options);
350
+ }
351
+
352
+ /** @param {unknown} values @returns {Record<string, string>} */
353
+ function normalizeEnumOptions(values) {
354
+ if (typeof values === 'string') {
355
+ try {
356
+ return normalizeEnumOptions(JSON.parse(values));
357
+ } catch {
358
+ return {};
359
+ }
360
+ }
361
+ if (Array.isArray(values)) {
362
+ return Object.fromEntries(values.map((entry, index) => {
363
+ if (Array.isArray(entry)) {
364
+ const [value = index, label = value] = entry;
365
+ return [String(value), String(label)];
366
+ }
367
+ if (entry && typeof entry === 'object') {
368
+ const value = entry.value ?? entry.id ?? index;
369
+ return [String(value), String(entry.label ?? entry.name ?? value)];
370
+ }
371
+ return [String(index), String(entry)];
372
+ }));
373
+ }
374
+ if (values && typeof values === 'object') {
375
+ return Object.fromEntries(Object.entries(values).map(([value, label]) => [value, String(label)]));
376
+ }
377
+ return {};
378
+ }
379
+
380
+ /** @param {FieldMeta} meta @returns {'optionlist'|'select'} */
381
+ function enumFieldType(meta) {
382
+ return Object.keys(enumOptions(meta)).length <= 5 ? 'optionlist' : 'select';
383
+ }
384
+
385
+ /** @returns {Record<string, string>} */
386
+ function priorityOptions() {
387
+ return { '0': 'LOWEST', '1': 'LOW', '2': 'MEDIUM', '3': 'HIGH', '4': 'HIGHEST' };
388
+ }
389
+
390
+ /** @param {FieldMeta} meta @param {Record<string, string>} [fallback] @returns {(row: Record<string, unknown>) => string} */
391
+ function enumRenderer(meta, fallback = {}) {
392
+ const labels = { ...fallback, ...enumOptions(meta) };
393
+ return (row) => {
394
+ const value = rowValue(row, meta);
395
+ if (value == null) return '';
396
+ return labels[String(value)] ?? String(value);
397
+ };
398
+ }
399
+
400
+ /** @param {FieldMeta} meta @param {boolean} [currency=false] @returns {(row: Record<string, unknown>) => string} */
401
+ function numberRenderer(meta, currency = false) {
402
+ let formatter;
403
+ try {
404
+ formatter = new Intl.NumberFormat(meta.locale, currency && meta.currency
405
+ ? { style: 'currency', currency: String(meta.currency) }
406
+ : {});
407
+ } catch {
408
+ formatter = new Intl.NumberFormat();
409
+ }
410
+ return (row) => {
411
+ const value = finiteNumber(rowValue(row, meta));
412
+ return value == null ? '' : formatter.format(value);
413
+ };
414
+ }
415
+
416
+ /** @param {unknown} value @returns {number|null} */
417
+ function finiteNumber(value) {
418
+ if (value == null || value === '') return null;
419
+ const number = Number(value);
420
+ return Number.isFinite(number) ? number : null;
421
+ }
422
+
423
+ /** @param {unknown} value @returns {string} */
424
+ function formatPercent(value) {
425
+ const number = finiteNumber(value);
426
+ return number == null ? '' : `${new Intl.NumberFormat().format(number)}%`;
427
+ }
428
+
429
+ /** @param {unknown} value @param {boolean} includeTime @returns {string} */
430
+ function formatUnixDate(value, includeTime) {
431
+ if (value == null || value === '') return '';
432
+ try {
433
+ const date = value instanceof Date ? value : unixSecondsToDate(/** @type {number|string} */ (value));
434
+ if (!date || !Number.isFinite(date.getTime())) return '';
435
+ return new Intl.DateTimeFormat(undefined, includeTime
436
+ ? { dateStyle: 'medium', timeStyle: 'short' }
437
+ : { dateStyle: 'medium' }).format(date);
438
+ } catch {
439
+ return '';
440
+ }
441
+ }
442
+
443
+ /** @param {unknown} value @returns {number|null} */
444
+ function unixSortValue(value) {
445
+ if (value instanceof Date) return dateToUnixSeconds(value);
446
+ return finiteNumber(value);
447
+ }
448
+
449
+ /** @param {Record<string, unknown>} row @param {FieldMeta} meta @returns {unknown} */
450
+ function rowValue(row, meta) {
451
+ if (!row || typeof row !== 'object') return undefined;
452
+ if (Object.prototype.hasOwnProperty.call(row, meta.id)) return row[meta.id];
453
+ const path = String(meta.path ?? meta.field ?? meta.id).split('.');
454
+ let value = row;
455
+ for (const part of path) {
456
+ if (!value || typeof value !== 'object') return undefined;
457
+ value = value[part];
458
+ }
459
+ return value;
460
+ }
461
+
462
+ /** @param {unknown} value @param {Record<string, unknown>} row @param {FieldMeta} meta @returns {string} */
463
+ function entityLabel(value, row, meta) {
464
+ if (value && typeof value === 'object') {
465
+ const label = value.label ?? value.name ?? value.title ?? value.number ?? value.ID ?? value.id;
466
+ return label == null ? '' : String(label);
467
+ }
468
+ const labelField = meta.labelField;
469
+ if (typeof labelField === 'string' && row[labelField] != null) return String(row[labelField]);
470
+ for (const key of [`${meta.id}.name`, `${meta.id}_label`, `${meta.id}Label`]) {
471
+ if (row[key] != null) return String(row[key]);
472
+ }
473
+ return value == null ? '' : String(value);
474
+ }
@@ -0,0 +1,175 @@
1
+ import { Select } from '../index.js';
2
+ import { connect } from './connect.js';
3
+ import { buildListQuery } from './query.js';
4
+
5
+ /**
6
+ * @typedef {Object} ZeyosSelectOptions
7
+ * @property {string[]|Record<string, string>} [fields] Resource fields or aliased projections.
8
+ * @property {string|((item: Record<string, unknown>) => string)} [labelKey] Display-label field or reader.
9
+ * @property {string|((item: Record<string, unknown>) => unknown)} [valueKey='ID'] Value field or reader.
10
+ * @property {string[]} [searchFields] Indexed text fields described to the query builder.
11
+ * @property {number} [limit=50] Maximum rows returned for one query.
12
+ * @property {Record<string, unknown>} [filters] Persistent ZeyOS field predicates.
13
+ * @property {unknown} [value=null] Initially selected id or record.
14
+ * @property {Record<string, unknown>[]} [items=[]] Optional seed records.
15
+ * @property {((item: Record<string, unknown>) => Node|string)|null} [renderItem=null] Option renderer.
16
+ * @property {((item: Record<string, unknown>) => string)|null} [renderValue=null] Selected-value renderer.
17
+ * @property {boolean} [disabled=false] Whether interaction is disabled.
18
+ * @property {string} [placeholder=''] Empty control text.
19
+ * @property {boolean} [clearable=false] Whether selection may be cleared.
20
+ * @property {number} [minQuery=0] Minimum async query length.
21
+ * @property {number} [debounce=200] Async query delay in milliseconds.
22
+ * @property {number} [listHeight=280] Maximum option-list height in pixels.
23
+ * @property {string|((item: Record<string, unknown>) => unknown)|null} [groupKey=null] Optional group reader.
24
+ * @property {Record<string, string>|Record<string, Record<string, string>>} [msg] Local messages.
25
+ * @property {(event: CustomEvent<Record<string, unknown>>) => void} [onchange] Change listener.
26
+ * @property {(event: CustomEvent<Record<string, unknown>>) => void} [onopen] Open listener.
27
+ * @property {(event: CustomEvent<Record<string, unknown>>) => void} [onclose] Close listener.
28
+ * @property {(event: CustomEvent<Record<string, unknown>>) => void} [onquery] Query listener.
29
+ * @property {(event: CustomEvent<Record<string, unknown>>) => void} [onloaded] Loaded listener.
30
+ */
31
+
32
+ /**
33
+ * @typedef {Object} ZeyosListResult
34
+ * @property {Record<string, unknown>[]} data Normalized records.
35
+ * @property {number} [count] Optional server-side count.
36
+ */
37
+
38
+ /**
39
+ * Builds the DOM-free Zx Select options used by {@link zeyosSelect}. The generated async filter
40
+ * sends full-text input as `query`, keeps predicates under `filters` (plural), and normalizes both
41
+ * array and `{data,count}` responses.
42
+ * @param {Record<string, any>} client Injected ZeyOS client instance.
43
+ * @param {string} resource Resource name such as `accounts`.
44
+ * @param {ZeyosSelectOptions} [opts={}] Query and Select options.
45
+ * @returns {Record<string, any>} Plain Zx Select options.
46
+ */
47
+ export function buildZeyosSelectConfig(client, resource, opts = {}) {
48
+ const {
49
+ fields,
50
+ labelKey = inferLabelKey(client, resource),
51
+ valueKey = 'ID',
52
+ searchFields = inferSearchFields(client, resource),
53
+ limit = 50,
54
+ filters = {},
55
+ value = null,
56
+ ...selectOpts
57
+ } = opts;
58
+ const facade = connect(client);
59
+ const projection = fields ?? defaultProjection(valueKey, labelKey, searchFields);
60
+
61
+ return {
62
+ ...selectOpts,
63
+ valueKey,
64
+ labelKey,
65
+ value,
66
+ filter: async (search) => {
67
+ const result = await facade.list(resource, buildListQuery({
68
+ search,
69
+ filters,
70
+ fields: projection,
71
+ searchFields,
72
+ limit
73
+ }));
74
+ return normalizeZeyosListResult(client, result).data;
75
+ }
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Creates an async Zx Select backed by the injected client's generated list operation. A non-null
81
+ * initial id is hydrated through the resource get operation; await `select.ready` when code must
82
+ * wait for that label before presenting the control.
83
+ * @param {Record<string, any>} client Injected ZeyOS client instance.
84
+ * @param {string} resource Resource name such as `accounts`.
85
+ * @param {ZeyosSelectOptions} [opts={}] Query and Select options.
86
+ * @returns {Select & {ready: Promise<Select>}} Configured Select with an initial-value readiness promise.
87
+ */
88
+ export function zeyosSelect(client, resource, opts = {}) {
89
+ const config = buildZeyosSelectConfig(client, resource, opts);
90
+ const requestedValue = opts.value ?? null;
91
+ const select = /** @type {Select & {ready: Promise<Select>}} */ (new Select(null, config));
92
+ select.ready = hydrateInitialValue(select, client, resource, requestedValue);
93
+ return select;
94
+ }
95
+
96
+ /**
97
+ * Normalizes list output without importing the client package. If the injected client exposes its
98
+ * own `normalizeListResult`, that implementation wins; otherwise arrays and `.data` are handled
99
+ * locally and numeric string counts are accepted.
100
+ * @param {Record<string, any>} client Injected ZeyOS client instance.
101
+ * @param {unknown} result Generated list-operation result.
102
+ * @returns {ZeyosListResult} Normalized rows and optional count.
103
+ */
104
+ export function normalizeZeyosListResult(client, result) {
105
+ if (typeof client?.normalizeListResult === 'function') {
106
+ return normalizeLocalListResult(client.normalizeListResult(result));
107
+ }
108
+ return normalizeLocalListResult(result);
109
+ }
110
+
111
+ /** @param {unknown} result @returns {ZeyosListResult} */
112
+ function normalizeLocalListResult(result) {
113
+ if (Array.isArray(result)) return { data: result };
114
+ if (!result || typeof result !== 'object') return { data: [] };
115
+ const data = Array.isArray(result.data) ? result.data : [];
116
+ const count = result.count == null || result.count === '' ? null : Number(result.count);
117
+ return Number.isFinite(count) ? { data, count } : { data };
118
+ }
119
+
120
+ /** @param {Select} select @param {Record<string, any>} client @param {string} resource @param {unknown} value @returns {Promise<Select>} */
121
+ async function hydrateInitialValue(select, client, resource, value) {
122
+ if (value == null) return select;
123
+ const record = value && typeof value === 'object'
124
+ ? value
125
+ : await connect(client).get(resource, value);
126
+ const normalized = normalizeRecord(record);
127
+ select.setItems([normalized]);
128
+ select.set(normalized, { silent: true });
129
+ return select;
130
+ }
131
+
132
+ /** @param {unknown} result @returns {Record<string, unknown>} */
133
+ function normalizeRecord(result) {
134
+ if (result && typeof result === 'object' && !Array.isArray(result)) {
135
+ const nested = result.data;
136
+ if (nested && typeof nested === 'object' && !Array.isArray(nested)) return nested;
137
+ return result;
138
+ }
139
+ throw new TypeError('ZeyOS get operation did not return a record');
140
+ }
141
+
142
+ /** @param {unknown} valueKey @param {unknown} labelKey @param {string[]} searchFields @returns {string[]|undefined} */
143
+ function defaultProjection(valueKey, labelKey, searchFields) {
144
+ const keys = [valueKey, labelKey, ...searchFields].filter((key) => typeof key === 'string' && key);
145
+ return keys.length ? [...new Set(keys)] : undefined;
146
+ }
147
+
148
+ /** @param {Record<string, any>} client @param {string} resource @returns {string} */
149
+ function inferLabelKey(client, resource) {
150
+ const names = schemaFieldNames(client, resource);
151
+ for (const candidate of ['name', 'lastname', 'customernum', 'transactionnum', 'number', 'title', 'ID']) {
152
+ if (names.includes(candidate)) return candidate;
153
+ }
154
+ return names[0] ?? 'ID';
155
+ }
156
+
157
+ /** @param {Record<string, any>} client @param {string} resource @returns {string[]} */
158
+ function inferSearchFields(client, resource) {
159
+ const fields = client?.schema?.describe?.(resource)?.fields;
160
+ if (!fields || typeof fields !== 'object' || Array.isArray(fields)) return [];
161
+ return Object.entries(fields).flatMap(([id, meta]) => {
162
+ if (!meta || typeof meta !== 'object') return [];
163
+ const type = String(meta.type ?? '').toLowerCase();
164
+ return meta.indexed && /text|character|varchar/.test(type) ? [id] : [];
165
+ });
166
+ }
167
+
168
+ /** @param {Record<string, any>} client @param {string} resource @returns {string[]} */
169
+ function schemaFieldNames(client, resource) {
170
+ const fields = client?.schema?.fields?.(resource);
171
+ if (Array.isArray(fields)) return fields.map(String);
172
+ if (fields && typeof fields === 'object') return Object.keys(fields);
173
+ const described = client?.schema?.describe?.(resource)?.fields;
174
+ return described && typeof described === 'object' ? Object.keys(described) : [];
175
+ }