@cybertale/resolver 0.1.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.
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <module type="WEB_MODULE" version="4">
3
+ <component name="NewModuleRootManager">
4
+ <content url="file://$MODULE_DIR$" />
5
+ <orderEntry type="inheritedJdk" />
6
+ <orderEntry type="sourceFolder" forTests="false" />
7
+ </component>
8
+ </module>
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectModuleManager">
4
+ <modules>
5
+ <module fileurl="file://$PROJECT_DIR$/.idea/cybertale-resolver.iml" filepath="$PROJECT_DIR$/.idea/cybertale-resolver.iml" />
6
+ </modules>
7
+ </component>
8
+ </project>
package/.idea/php.xml ADDED
@@ -0,0 +1,19 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="MessDetectorOptionsConfiguration">
4
+ <option name="transferred" value="true" />
5
+ </component>
6
+ <component name="PHPCSFixerOptionsConfiguration">
7
+ <option name="transferred" value="true" />
8
+ </component>
9
+ <component name="PHPCodeSnifferOptionsConfiguration">
10
+ <option name="highlightLevel" value="WARNING" />
11
+ <option name="transferred" value="true" />
12
+ </component>
13
+ <component name="PhpStanOptionsConfiguration">
14
+ <option name="transferred" value="true" />
15
+ </component>
16
+ <component name="PsalmOptionsConfiguration">
17
+ <option name="transferred" value="true" />
18
+ </component>
19
+ </project>
package/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # 📦 `@cybertale/resolver`
2
+
3
+ **A lightweight, framework-agnostic resolver engine** used to transform, compute, update, and manage `ObjectTemplate` structures from `@cybertale/interface`.
4
+
5
+ This package extracts core logic from UI components (Vue/React) into a clean, reusable, testable library.
6
+
7
+ ---
8
+
9
+ <p align="center">
10
+ <img src="https://img.shields.io/npm/v/%40cybertale%2Fresolver.svg?style=for-the-badge" />
11
+ <img src="https://img.shields.io/npm/dm/%40cybertale%2Fresolver.svg?style=for-the-badge" />
12
+ <img src="https://img.shields.io/bundlephobia/minzip/%40cybertale%2Fresolver?style=for-the-badge" />
13
+ <img src="https://img.shields.io/github/license/cybertale/resolver?style=for-the-badge" />
14
+ </p>
15
+
16
+ ## 🚀 Why this exists
17
+ Modern UI frameworks shouldn't contain domain logic.
18
+
19
+ Before this package:
20
+ - Buttons processed JSON in the component
21
+ - Fields decided validation class
22
+ - SelectList parsed arrays
23
+ - Many duplicated helpers across components
24
+
25
+ Now all that logic is extracted into:
26
+ ```
27
+ @cybertale/resolver
28
+ ```
29
+
30
+ Your UI becomes dumb — your resolver becomes smart.
31
+
32
+ ---
33
+
34
+ # 🧩 Features
35
+
36
+ ### ✔ JSON parsing & normalization
37
+ ### ✔ Template stat utilities
38
+ ### ✔ Template actions
39
+ ### ✔ Computed helpers
40
+ ### ✔ Finalization helpers
41
+
42
+ ---
43
+
44
+ # 📥 Installation
45
+
46
+ ```bash
47
+ npm install @cybertale/resolver
48
+ ```
49
+
50
+ ---
51
+
52
+ # 🧱 Architecture Overview
53
+
54
+ ```
55
+ resolver/
56
+ ├── transform/
57
+ ├── form/
58
+ ├── compute/
59
+ ├── finalize/
60
+ └── handlers/
61
+ ```
62
+
63
+ ---
64
+
65
+ # 📚 Usage Examples
66
+
67
+ ## Extracted Field Logic (Vue)
68
+
69
+ ```ts
70
+ import { getValueFromTemplate } from '@cybertale/resolver/form/value'
71
+ ```
72
+
73
+ ## Updating Template Data
74
+
75
+ ```ts
76
+ import { updateValueForTemplate } from '@cybertale/resolver/handlers/update'
77
+ ```
78
+
79
+ ---
80
+
81
+ # 🤝 Contributing
82
+
83
+ 1. Clone repo
84
+ 2. Install deps
85
+ 3. Run tests
86
+ 4. Submit PR
87
+
88
+ ---
89
+
90
+ # 📜 License
91
+
92
+ MIT © Cybertale
package/package.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "@cybertale/resolver",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "types": "./src/index.ts",
6
+ "exports": {
7
+ ".": "./src/index.ts"
8
+ },
9
+ "peerDependencies": {
10
+ "@cybertale/interface": "*"
11
+ }
12
+ }
package/src/index.ts ADDED
@@ -0,0 +1,25 @@
1
+
2
+ // Barrel exports for @cybertale/template
3
+
4
+ // transform
5
+ export * as JsonTransform from './resolver/transform/json'
6
+ export * as StatTransform from './resolver/transform/stat'
7
+
8
+ // form
9
+ export * as FormValue from './resolver/form/value'
10
+ export * as FormInput from './resolver/form/input'
11
+ export * as FormSelect from './resolver/form/select'
12
+
13
+ // compute
14
+ export * as ComputeLabel from './resolver/compute/label'
15
+ export * as ComputeTooltip from './resolver/compute/tooltip'
16
+ export * as ComputeValidation from './resolver/compute/validation'
17
+
18
+ // finalize
19
+ export * as FinalizeTemplate from './resolver/finalize/template'
20
+ export * as FinalizeDefaults from './resolver/finalize/defaults'
21
+
22
+ // handlers
23
+ export * as HandlersRemove from './resolver/handlers/remove'
24
+ export * as HandlersInsert from './resolver/handlers/insert'
25
+ export * as HandlersUpdate from './resolver/handlers/update'
@@ -0,0 +1,42 @@
1
+
2
+ import type { ObjectTemplate, StatTypeEnum } from '@cybertale/interface'
3
+ import { getStringStat } from '../transform/stat'
4
+ import { isJSONObject, parseJSON } from '../transform/json'
5
+
6
+ export interface LabelData {
7
+ iconClass?: string
8
+ title?: string
9
+ styleData?: string
10
+ contentValue?: string
11
+ contentClass?: string
12
+ translate?: string
13
+ }
14
+
15
+ /**
16
+ * Attempts to parse Stat.Label as a LabelData object; if it is a plain string,
17
+ * it is returned as `title` for convenience.
18
+ */
19
+ export function getLabelData(
20
+ object: ObjectTemplate,
21
+ labelStat: StatTypeEnum,
22
+ ): LabelData {
23
+ const raw = getStringStat(object, labelStat)
24
+ if (!raw) return { title: '' }
25
+
26
+ if (isJSONObject(raw)) {
27
+ const parsed = parseJSON<LabelData>(raw)
28
+ if (parsed) return parsed
29
+ }
30
+
31
+ return { title: raw }
32
+ }
33
+
34
+ /**
35
+ * Returns the plain label text (usually LabelData.title or raw string).
36
+ */
37
+ export function getLabelText(
38
+ object: ObjectTemplate,
39
+ labelStat: StatTypeEnum,
40
+ ): string {
41
+ return getLabelData(object, labelStat).title ?? ''
42
+ }
@@ -0,0 +1,47 @@
1
+
2
+ import type { ObjectTemplate, StatTypeEnum } from '@cybertale/interface'
3
+ import { getStringStat } from '../transform/stat'
4
+ import { isJSONObject, parseJSON } from '../transform/json'
5
+
6
+ export interface TooltipData {
7
+ toggleBy: string
8
+ value: string
9
+ translate?: string
10
+ }
11
+
12
+ /**
13
+ * Tooltip is stored either as a simple string or as a JSON-encoded TooltipData.
14
+ */
15
+ export function getTooltip(
16
+ object: ObjectTemplate,
17
+ tooltipStat: StatTypeEnum,
18
+ ): TooltipData | string | null {
19
+ const raw = getStringStat(object, tooltipStat)
20
+ if (!raw) return null
21
+
22
+ if (isJSONObject(raw)) {
23
+ return parseJSON<TooltipData>(raw)
24
+ }
25
+
26
+ return raw
27
+ }
28
+
29
+ export function getTooltipToggleBy(
30
+ object: ObjectTemplate,
31
+ tooltipStat: StatTypeEnum,
32
+ ): string {
33
+ const tooltip = getTooltip(object, tooltipStat)
34
+ if (!tooltip) return 'tooltip'
35
+ if (typeof tooltip === 'string') return 'tooltip'
36
+ return tooltip.toggleBy || 'tooltip'
37
+ }
38
+
39
+ export function getTooltipText(
40
+ object: ObjectTemplate,
41
+ tooltipStat: StatTypeEnum,
42
+ ): string {
43
+ const tooltip = getTooltip(object, tooltipStat)
44
+ if (!tooltip) return ''
45
+ if (typeof tooltip === 'string') return tooltip
46
+ return tooltip.value
47
+ }
@@ -0,0 +1,21 @@
1
+
2
+ import type { ObjectTemplate, StatTypeEnum } from '@cybertale/interface'
3
+ import { getRawStat, getStringStat } from '../transform/stat'
4
+
5
+ /**
6
+ * Computes a Bootstrap-esque validation class ('is-valid' | 'is-invalid' | '').
7
+ * This mirrors the repeated logic you had in several components.
8
+ */
9
+ export function computeValidationClass(
10
+ object: ObjectTemplate,
11
+ isValidStat: StatTypeEnum,
12
+ errorMessageStat: StatTypeEnum,
13
+ ): string {
14
+ const isValidRaw = getRawStat(object, isValidStat)
15
+ const errorMessage = getStringStat(object, errorMessageStat)
16
+
17
+ if (isValidRaw === undefined || isValidRaw === '') return ''
18
+ if (isValidRaw) return 'is-valid'
19
+ if (errorMessage) return 'is-invalid'
20
+ return ''
21
+ }
@@ -0,0 +1,30 @@
1
+ import type { ObjectTemplate, StatTypeEnum } from '@cybertale/interface'
2
+ import { getRawStat } from '../transform/stat'
3
+
4
+ /**
5
+ * Applies default values to stats that are currently empty / undefined.
6
+ * `defaults` is a map of StatTypeEnum -> default Data value.
7
+ */
8
+ export function applyDefaults(
9
+ template: ObjectTemplate,
10
+ defaults: Partial<Record<StatTypeEnum, unknown>>,
11
+ ): ObjectTemplate {
12
+ for (const [key, value] of Object.entries(defaults)) {
13
+ const statKey = Number(key) as StatTypeEnum
14
+
15
+ // Only apply if undefined or empty
16
+ if (getRawStat(template, statKey) == null && template.Stats) {
17
+ const stringValue = value !== undefined && value !== null
18
+ ? String(value) // <—— FIX HERE
19
+ : ''
20
+
21
+ if (!template.Stats[statKey]) {
22
+ // Create new stat entry
23
+ template.Stats[statKey] = { Data: stringValue } as any
24
+ } else {
25
+ template.Stats[statKey].Data = stringValue
26
+ }
27
+ }
28
+ }
29
+ return template
30
+ }
@@ -0,0 +1,25 @@
1
+
2
+ import type { ObjectTemplate } from '@cybertale/interface'
3
+
4
+ /**
5
+ * Finalization hook for a single ObjectTemplate before it is sent to the backend
6
+ * or stored. For now this is a light pass-through, but centralizing this
7
+ * allows you to add future cleanup in one place (e.g. stripping transient stats).
8
+ */
9
+ export function finalizeTemplate(template: ObjectTemplate): ObjectTemplate {
10
+ // Shallow clone to avoid accidental external mutation
11
+ return new (template.constructor as typeof ObjectTemplate)(
12
+ template.Region,
13
+ template.ObjectEnum,
14
+ template.SubObjectEnum,
15
+ template.ActionEnum,
16
+ template.Stats,
17
+ )
18
+ }
19
+
20
+ /**
21
+ * Convenience function for finalizing an array of templates.
22
+ */
23
+ export function finalizeTemplates(templates: ObjectTemplate[]): ObjectTemplate[] {
24
+ return templates.map(finalizeTemplate)
25
+ }
@@ -0,0 +1,14 @@
1
+
2
+ import type { ObjectTemplate, StatTypeEnum } from '@cybertale/interface'
3
+ import { getStringStat } from '../transform/stat'
4
+
5
+ /**
6
+ * Returns the input type for a given template.
7
+ * In your current code this is usually driven by StatTypeEnum.ElementType.
8
+ */
9
+ export function getInputTypeFromTemplate(
10
+ object: ObjectTemplate,
11
+ elementTypeStat: StatTypeEnum,
12
+ ): string {
13
+ return getStringStat(object, elementTypeStat)
14
+ }
@@ -0,0 +1,49 @@
1
+
2
+ import type { ObjectTemplate, StatTypeEnum } from '@cybertale/interface'
3
+ import { getStringStat } from '../transform/stat'
4
+ import { parseJSON, isJSONArray } from '../transform/json'
5
+
6
+ export interface SelectItem {
7
+ id: string | number
8
+ name?: string
9
+ [key: string]: unknown
10
+ }
11
+
12
+ /**
13
+ * Parses the ItemList stat (usually JSON) into a typed array.
14
+ */
15
+ export function getSelectItems(
16
+ object: ObjectTemplate,
17
+ itemListStat: StatTypeEnum,
18
+ ): SelectItem[] {
19
+ const raw = getStringStat(object, itemListStat)
20
+ if (!raw || !isJSONArray(raw)) return []
21
+ const parsed = parseJSON<unknown[]>(raw) ?? []
22
+ return parsed.filter((item): item is SelectItem => {
23
+ return !!item && typeof (item as any).id !== 'undefined'
24
+ })
25
+ }
26
+
27
+ /**
28
+ * Gets the currently selected item id from the template.
29
+ * This simply returns Stat.Value (or similar), but extracted in one place.
30
+ */
31
+ export function getSelectedId(
32
+ object: ObjectTemplate,
33
+ valueStat: StatTypeEnum,
34
+ ): string {
35
+ return getStringStat(object, valueStat)
36
+ }
37
+
38
+ /**
39
+ * Returns the selected SelectItem, if found.
40
+ */
41
+ export function getSelectedItem(
42
+ object: ObjectTemplate,
43
+ itemListStat: StatTypeEnum,
44
+ valueStat: StatTypeEnum,
45
+ ): SelectItem | null {
46
+ const items = getSelectItems(object, itemListStat)
47
+ const selectedId = getSelectedId(object, valueStat)
48
+ return items.find(item => String(item.id) === selectedId) ?? null
49
+ }
@@ -0,0 +1,54 @@
1
+ import type { ObjectTemplate } from '@cybertale/interface'
2
+ import { StatTypeEnum } from '@cybertale/interface'
3
+ import { getStringStat, hasStat } from '../transform/stat'
4
+ import { isJSONArray, parseJSON } from '../transform/json'
5
+
6
+ /**
7
+ * Generic value resolution from ObjectTemplate stats.
8
+ *
9
+ * This encapsulates the common pattern you used in multiple components:
10
+ * - Stat.Value stores an array (JSON string)
11
+ * - Stat.Option (or other index stat) selects one element from that array
12
+ * - OptionIndices may itself be a JSON array of indices
13
+ */
14
+ export function getValueFromTemplate(
15
+ object: ObjectTemplate,
16
+ valueStat: StatTypeEnum,
17
+ indexStat: StatTypeEnum = (StatTypeEnum as any).Option,
18
+ ): unknown {
19
+ const raw = getStringStat(object, valueStat)
20
+ if (!raw) return ''
21
+
22
+ if (!hasStat(object, indexStat) || !isJSONArray(raw)) {
23
+ return raw
24
+ }
25
+
26
+ const data = parseJSON<unknown[]>(raw)
27
+ if (!data) return ''
28
+
29
+ const indexRaw = getStringStat(object, indexStat)
30
+
31
+ // JSON-array index case
32
+ if (isJSONArray(indexRaw)) {
33
+ const optionIndicesStat = (StatTypeEnum as any).OptionIndices as StatTypeEnum
34
+ const indices = parseJSON<number[]>(indexRaw) ?? []
35
+
36
+ const optionIndex = Number(getStringStat(object, optionIndicesStat))
37
+ const mappedIndex = indices[optionIndex]
38
+
39
+ // FIXED: redundant typeof removed, replaced with correct bounds + null check
40
+ if (mappedIndex != null && mappedIndex in data) {
41
+ return data[mappedIndex]
42
+ }
43
+
44
+ return ''
45
+ }
46
+
47
+ // Simple numeric index
48
+ const index = Number(indexRaw)
49
+ if (Number.isNaN(index) || !(index in data)) {
50
+ return ''
51
+ }
52
+
53
+ return data[index]
54
+ }
@@ -0,0 +1,33 @@
1
+
2
+ import type { ObjectTemplate } from '@cybertale/interface'
3
+
4
+ /**
5
+ * Inserts a single ObjectTemplate at the given index and returns a new array.
6
+ */
7
+ export function insertTemplateAt(
8
+ templates: ObjectTemplate[],
9
+ index: number,
10
+ template: ObjectTemplate,
11
+ ): ObjectTemplate[] {
12
+ return [
13
+ ...templates.slice(0, index),
14
+ template,
15
+ ...templates.slice(index),
16
+ ]
17
+ }
18
+
19
+ /**
20
+ * Inserts multiple ObjectTemplates starting at the given index and returns
21
+ * a new array.
22
+ */
23
+ export function insertTemplatesAt(
24
+ templates: ObjectTemplate[],
25
+ index: number,
26
+ newTemplates: ObjectTemplate[],
27
+ ): ObjectTemplate[] {
28
+ return [
29
+ ...templates.slice(0, index),
30
+ ...newTemplates,
31
+ ...templates.slice(index),
32
+ ]
33
+ }
@@ -0,0 +1,52 @@
1
+
2
+ import type { ObjectTemplate, StatTypeEnum } from '@cybertale/interface'
3
+ import { getStringStat, hasStat } from '../transform/stat'
4
+
5
+ /**
6
+ * Finds the index of an ObjectTemplate by a tag-like stat. Supports the
7
+ * 'tag|something' pattern you use for button payloads.
8
+ */
9
+ export function findTemplateIndexByStat(
10
+ templates: ObjectTemplate[],
11
+ value: string,
12
+ searchByStat: StatTypeEnum,
13
+ ): number {
14
+ return templates.findIndex(t => {
15
+ const statValue = getStringStat(t, searchByStat)
16
+ return statValue === value || statValue === value.split('|')[1]
17
+ })
18
+ }
19
+
20
+ /**
21
+ * Removes the first template whose `searchByStat` matches `value`.
22
+ * Returns a new array (non-mutating).
23
+ */
24
+ export function removeByStatValue(
25
+ templates: ObjectTemplate[],
26
+ value: string,
27
+ searchByStat: StatTypeEnum,
28
+ ): ObjectTemplate[] {
29
+ const index = findTemplateIndexByStat(templates, value, searchByStat)
30
+ if (index === -1) return templates
31
+ return [
32
+ ...templates.slice(0, index),
33
+ ...templates.slice(index + 1),
34
+ ]
35
+ }
36
+
37
+ /**
38
+ * Removes all templates where Stat.DependsOn.Data includes the provided key.
39
+ * This mirrors your old removeElementFromArray implementation, but returns
40
+ * a new array instead of mutating in-place.
41
+ */
42
+ export function removeByDependsOn(
43
+ templates: ObjectTemplate[],
44
+ dependsOn: string,
45
+ dependsOnStat: StatTypeEnum,
46
+ ): ObjectTemplate[] {
47
+ return templates.filter(t => {
48
+ if (!hasStat(t, dependsOnStat)) return true
49
+ const dep = getStringStat(t, dependsOnStat)
50
+ return !dep.includes(dependsOn)
51
+ })
52
+ }
@@ -0,0 +1,55 @@
1
+
2
+ import type { ObjectTemplate } from '@cybertale/interface'
3
+ import { StatTypeEnum } from '@cybertale/interface'
4
+ import { findTemplateIndexByStat } from './remove'
5
+ import { isJSONArray, parseJSON } from '../transform/json'
6
+ import { getStringStat } from '../transform/stat'
7
+
8
+ /**
9
+ * Updates a value-like stat on the template that matches the payload by some key stat.
10
+ * This mirrors your updateValueData logic, including JSON-array handling.
11
+ */
12
+ export function updateValueForTemplate(
13
+ templates: ObjectTemplate[],
14
+ payload: ObjectTemplate,
15
+ options?: {
16
+ valueStat?: StatTypeEnum
17
+ searchByStat?: StatTypeEnum
18
+ valueIndicesStat?: StatTypeEnum
19
+ },
20
+ ): ObjectTemplate[] {
21
+ const valueStat = options?.valueStat ?? (StatTypeEnum as any).Value
22
+ const searchByStat = options?.searchByStat ?? (StatTypeEnum as any).Tag
23
+ const valueIndicesStat = options?.valueIndicesStat ?? (StatTypeEnum as any).ValueIndices
24
+
25
+ const index = findTemplateIndexByStat(templates, getStringStat(payload, searchByStat), searchByStat)
26
+ if (index === -1) return templates
27
+
28
+ const target = templates[index]
29
+ const currentRaw = getStringStat(target, valueStat)
30
+ const payloadValue = getStringStat(payload, options?.valueStat ?? valueStat)
31
+
32
+ // If current value is a JSON array, update at ValueIndices
33
+ if (isJSONArray(currentRaw)) {
34
+ const arr = parseJSON<unknown[]>(currentRaw) ?? []
35
+ const indicesRaw = getStringStat(payload, valueIndicesStat)
36
+ const idx = Number(indicesRaw)
37
+ if (!Number.isNaN(idx)) {
38
+ arr[idx] = payloadValue
39
+ }
40
+ // Write back as JSON string
41
+ if (target.Stats) {
42
+ target.Stats[valueStat].Data = JSON.stringify(arr)
43
+ }
44
+ } else {
45
+ // Simple scalar assignment
46
+ if (target.Stats) {
47
+ target.Stats[valueStat].Data = payloadValue
48
+ }
49
+ }
50
+
51
+ // Return a new array instance to preserve immutability semantics
52
+ const clone = [...templates]
53
+ clone[index] = target
54
+ return clone
55
+ }
@@ -0,0 +1,23 @@
1
+
2
+ // JSON-related transformation utilities for ObjectTemplate stats.
3
+ // These are intentionally generic and environment-agnostic so they can be reused
4
+ // both in browser and Node contexts.
5
+
6
+ export function parseJSON<T = unknown>(value: string | null | undefined): T | null {
7
+ if (value == null || value === "") return null
8
+ try {
9
+ return JSON.parse(value) as T
10
+ } catch {
11
+ return null
12
+ }
13
+ }
14
+
15
+ export function isJSONArray(value: string | null | undefined): boolean {
16
+ const parsed = parseJSON(value)
17
+ return Array.isArray(parsed)
18
+ }
19
+
20
+ export function isJSONObject(value: string | null | undefined): boolean {
21
+ const parsed = parseJSON(value)
22
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
23
+ }
@@ -0,0 +1,32 @@
1
+
2
+ import type { ObjectTemplate, StatTypeEnum } from '@cybertale/interface'
3
+ import { parseJSON } from './json'
4
+
5
+ // Safe stat accessors for ObjectTemplate. These centralize the common
6
+ // patterns used across UI and resolver code.
7
+
8
+ export function hasStat(object: ObjectTemplate, stat: StatTypeEnum): boolean {
9
+ return !!object.Stats && object.Stats[stat] !== undefined
10
+ }
11
+
12
+ export function getRawStat(object: ObjectTemplate, stat: StatTypeEnum): unknown {
13
+ return object.Stats?.[stat]?.Data
14
+ }
15
+
16
+ export function getStringStat(object: ObjectTemplate, stat: StatTypeEnum): string {
17
+ const raw = getRawStat(object, stat)
18
+ if (raw == null) return ''
19
+ return String(raw)
20
+ }
21
+
22
+ export function getBooleanStat(object: ObjectTemplate, stat: StatTypeEnum): boolean {
23
+ const raw = getRawStat(object, stat)
24
+ // Treat empty string / null / undefined as false, anything else as true.
25
+ return !!raw
26
+ }
27
+
28
+ export function getJSONStat<T = unknown>(object: ObjectTemplate, stat: StatTypeEnum): T | null {
29
+ const raw = getRawStat(object, stat)
30
+ if (typeof raw !== 'string') return null
31
+ return parseJSON<T>(raw)
32
+ }