@routevn/creator-model 1.10.4 → 1.11.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 (3) hide show
  1. package/README.md +38 -0
  2. package/package.json +1 -1
  3. package/src/model.js +1225 -35
package/src/model.js CHANGED
@@ -341,7 +341,7 @@ const ANIMATION_EASING_KEYS = [
341
341
  "easeInOutElastic",
342
342
  ];
343
343
  const VARIABLE_SCOPE_KEYS = ["context", "device", "account"];
344
- const VARIABLE_TYPE_KEYS = ["string", "number", "boolean"];
344
+ const VARIABLE_TYPE_KEYS = ["string", "number", "boolean", "object"];
345
345
  const LAYOUT_TYPE_KEYS = [
346
346
  "general",
347
347
  "save-load",
@@ -403,7 +403,7 @@ const SAVE_LOAD_DATE_FORMATS = new Set([
403
403
  "DD MMM YYYY",
404
404
  "YYYY年MM月DD日",
405
405
  ]);
406
- export const SCHEMA_VERSION = 10;
406
+ export const SCHEMA_VERSION = 11;
407
407
  const LAYOUT_CONTAINER_ELEMENT_TYPES = [
408
408
  "folder",
409
409
  "container",
@@ -2908,6 +2908,1079 @@ const validateVariableTypedValue = ({
2908
2908
  if (variableType === "boolean" && typeof value !== "boolean") {
2909
2909
  return invalidFromErrorFactory(errorFactory, `${path} must be a boolean`);
2910
2910
  }
2911
+
2912
+ if (
2913
+ variableType === "object" &&
2914
+ (value === null || typeof value !== "object")
2915
+ ) {
2916
+ return invalidFromErrorFactory(
2917
+ errorFactory,
2918
+ `${path} must be a non-null object or array`,
2919
+ );
2920
+ }
2921
+
2922
+ if (variableType === "object") {
2923
+ return validateComputedDataValue({
2924
+ value,
2925
+ path,
2926
+ errorFactory,
2927
+ });
2928
+ }
2929
+ };
2930
+
2931
+ const COMPUTED_EXPRESSION_FIXED_OPERAND_COUNTS = Object.freeze({
2932
+ add: 2,
2933
+ sub: 2,
2934
+ mul: 2,
2935
+ div: 2,
2936
+ mod: 2,
2937
+ neg: 1,
2938
+ round: 1,
2939
+ floor: 1,
2940
+ ceil: 1,
2941
+ min: 2,
2942
+ max: 2,
2943
+ clamp: 3,
2944
+ eq: 2,
2945
+ neq: 2,
2946
+ gt: 2,
2947
+ gte: 2,
2948
+ lt: 2,
2949
+ lte: 2,
2950
+ in: 2,
2951
+ not: 1,
2952
+ length: 1,
2953
+ includes: 2,
2954
+ });
2955
+ const COMPUTED_EXPRESSION_VARIADIC_OPERATORS = new Set([
2956
+ "and",
2957
+ "or",
2958
+ "all",
2959
+ "any",
2960
+ ]);
2961
+ const COMPUTED_EXPRESSION_NUMERIC_OPERAND_OPERATORS = new Set([
2962
+ "add",
2963
+ "sub",
2964
+ "mul",
2965
+ "div",
2966
+ "mod",
2967
+ "neg",
2968
+ "round",
2969
+ "floor",
2970
+ "ceil",
2971
+ "min",
2972
+ "max",
2973
+ "clamp",
2974
+ ]);
2975
+ const COMPUTED_EXPRESSION_NUMERIC_RESULT_OPERATORS = new Set([
2976
+ ...COMPUTED_EXPRESSION_NUMERIC_OPERAND_OPERATORS,
2977
+ "length",
2978
+ ]);
2979
+ const COMPUTED_EXPRESSION_BOOLEAN_RESULT_OPERATORS = new Set([
2980
+ "eq",
2981
+ "neq",
2982
+ "gt",
2983
+ "gte",
2984
+ "lt",
2985
+ "lte",
2986
+ "in",
2987
+ "and",
2988
+ "or",
2989
+ "all",
2990
+ "any",
2991
+ "not",
2992
+ "includes",
2993
+ ]);
2994
+ const COMPUTED_CONDITION_FIXED_OPERAND_COUNTS = Object.freeze({
2995
+ eq: 2,
2996
+ neq: 2,
2997
+ gt: 2,
2998
+ gte: 2,
2999
+ lt: 2,
3000
+ lte: 2,
3001
+ in: 2,
3002
+ add: 2,
3003
+ sub: 2,
3004
+ });
3005
+ const COMPUTED_CONDITION_VARIADIC_OPERATORS = new Set(["all", "any"]);
3006
+
3007
+ const getComputedValueType = (value) => {
3008
+ if (value === null) {
3009
+ return "null";
3010
+ }
3011
+ if (typeof value === "object") {
3012
+ return "object";
3013
+ }
3014
+ return typeof value;
3015
+ };
3016
+
3017
+ const validComputedResult = (valueType) => ({
3018
+ valid: true,
3019
+ valueType,
3020
+ });
3021
+
3022
+ const validateComputedDataValue = ({
3023
+ value,
3024
+ path,
3025
+ errorFactory,
3026
+ ancestors = new Set(),
3027
+ }) => {
3028
+ if (
3029
+ value === null ||
3030
+ typeof value === "string" ||
3031
+ typeof value === "boolean"
3032
+ ) {
3033
+ return VALID_RESULT;
3034
+ }
3035
+
3036
+ if (typeof value === "number") {
3037
+ if (Number.isFinite(value)) {
3038
+ return VALID_RESULT;
3039
+ }
3040
+ return invalidFromErrorFactory(
3041
+ errorFactory,
3042
+ `${path} must use finite numeric values`,
3043
+ );
3044
+ }
3045
+
3046
+ if (Array.isArray(value)) {
3047
+ if (ancestors.has(value)) {
3048
+ return invalidFromErrorFactory(
3049
+ errorFactory,
3050
+ `${path} must not contain cyclic data`,
3051
+ );
3052
+ }
3053
+ ancestors.add(value);
3054
+
3055
+ for (const [index, item] of value.entries()) {
3056
+ const result = validateComputedDataValue({
3057
+ value: item,
3058
+ path: `${path}[${index}]`,
3059
+ errorFactory,
3060
+ ancestors,
3061
+ });
3062
+ if (result?.valid === false) {
3063
+ return result;
3064
+ }
3065
+ }
3066
+ ancestors.delete(value);
3067
+ return VALID_RESULT;
3068
+ }
3069
+
3070
+ if (
3071
+ !isPlainObject(value) ||
3072
+ ![Object.prototype, null].includes(Object.getPrototypeOf(value))
3073
+ ) {
3074
+ return invalidFromErrorFactory(
3075
+ errorFactory,
3076
+ `${path} must contain JSON-compatible data`,
3077
+ );
3078
+ }
3079
+
3080
+ if (ancestors.has(value)) {
3081
+ return invalidFromErrorFactory(
3082
+ errorFactory,
3083
+ `${path} must not contain cyclic data`,
3084
+ );
3085
+ }
3086
+ ancestors.add(value);
3087
+
3088
+ for (const [key, item] of Object.entries(value)) {
3089
+ const result = validateComputedDataValue({
3090
+ value: item,
3091
+ path: `${path}.${key}`,
3092
+ errorFactory,
3093
+ ancestors,
3094
+ });
3095
+ if (result?.valid === false) {
3096
+ return result;
3097
+ }
3098
+ }
3099
+
3100
+ ancestors.delete(value);
3101
+ return VALID_RESULT;
3102
+ };
3103
+
3104
+ const invalidComputedReferenceParseResult = Object.freeze({
3105
+ valid: false,
3106
+ });
3107
+
3108
+ const decodeComputedReferenceQuotedPart = (rawValue) => {
3109
+ if (rawValue[0] === '"') {
3110
+ try {
3111
+ return {
3112
+ valid: true,
3113
+ value: JSON.parse(rawValue),
3114
+ };
3115
+ } catch {
3116
+ return invalidComputedReferenceParseResult;
3117
+ }
3118
+ }
3119
+
3120
+ let value = "";
3121
+ const escapedValues = {
3122
+ "\\": "\\",
3123
+ '"': '"',
3124
+ "'": "'",
3125
+ "/": "/",
3126
+ n: "\n",
3127
+ r: "\r",
3128
+ t: "\t",
3129
+ b: "\b",
3130
+ f: "\f",
3131
+ };
3132
+
3133
+ for (let index = 1; index < rawValue.length - 1; index += 1) {
3134
+ const character = rawValue[index];
3135
+ if (character !== "\\") {
3136
+ if (character.charCodeAt(0) < 0x20) {
3137
+ return invalidComputedReferenceParseResult;
3138
+ }
3139
+ value += character;
3140
+ continue;
3141
+ }
3142
+
3143
+ index += 1;
3144
+ if (index >= rawValue.length - 1) {
3145
+ return invalidComputedReferenceParseResult;
3146
+ }
3147
+
3148
+ const escapedCharacter = rawValue[index];
3149
+ if (escapedCharacter === "u") {
3150
+ const hexValue = rawValue.slice(index + 1, index + 5);
3151
+ if (hexValue.length !== 4 || !/^[0-9a-fA-F]{4}$/.test(hexValue)) {
3152
+ return invalidComputedReferenceParseResult;
3153
+ }
3154
+ value += String.fromCharCode(Number.parseInt(hexValue, 16));
3155
+ index += 4;
3156
+ continue;
3157
+ }
3158
+
3159
+ if (!Object.hasOwn(escapedValues, escapedCharacter)) {
3160
+ return invalidComputedReferenceParseResult;
3161
+ }
3162
+ value += escapedValues[escapedCharacter];
3163
+ }
3164
+
3165
+ return {
3166
+ valid: true,
3167
+ value,
3168
+ };
3169
+ };
3170
+
3171
+ const parseComputedReferencePath = (value) => {
3172
+ if (
3173
+ typeof value !== "string" ||
3174
+ value.length === 0 ||
3175
+ value.trim() !== value
3176
+ ) {
3177
+ return invalidComputedReferenceParseResult;
3178
+ }
3179
+
3180
+ const parts = [];
3181
+ let index = 0;
3182
+
3183
+ const readBarePart = () => {
3184
+ const startIndex = index;
3185
+ while (
3186
+ index < value.length &&
3187
+ value[index] !== "." &&
3188
+ value[index] !== "[" &&
3189
+ value[index] !== "]"
3190
+ ) {
3191
+ index += 1;
3192
+ }
3193
+
3194
+ const part = value.slice(startIndex, index);
3195
+ if (part.length === 0 || part.trim() !== part || /\s/.test(part)) {
3196
+ return false;
3197
+ }
3198
+ parts.push(part);
3199
+ return true;
3200
+ };
3201
+
3202
+ const readBracketPart = () => {
3203
+ index += 1;
3204
+ while (index < value.length && /\s/.test(value[index])) {
3205
+ index += 1;
3206
+ }
3207
+
3208
+ const firstCharacter = value[index];
3209
+ if (/\d/.test(firstCharacter ?? "")) {
3210
+ const startIndex = index;
3211
+ while (index < value.length && /\d/.test(value[index])) {
3212
+ index += 1;
3213
+ }
3214
+ const rawPart = value.slice(startIndex, index);
3215
+ while (index < value.length && /\s/.test(value[index])) {
3216
+ index += 1;
3217
+ }
3218
+ if (
3219
+ value[index] !== "]" ||
3220
+ (rawPart.length > 1 && rawPart.startsWith("0"))
3221
+ ) {
3222
+ return false;
3223
+ }
3224
+ index += 1;
3225
+ parts.push(rawPart);
3226
+ return true;
3227
+ }
3228
+
3229
+ if (firstCharacter !== '"' && firstCharacter !== "'") {
3230
+ return false;
3231
+ }
3232
+
3233
+ const quote = firstCharacter;
3234
+ const startIndex = index;
3235
+ index += 1;
3236
+ let escaped = false;
3237
+ while (index < value.length) {
3238
+ const character = value[index];
3239
+ if (escaped) {
3240
+ escaped = false;
3241
+ } else if (character === "\\") {
3242
+ escaped = true;
3243
+ } else if (character === quote) {
3244
+ const decodedPart = decodeComputedReferenceQuotedPart(
3245
+ value.slice(startIndex, index + 1),
3246
+ );
3247
+ if (!decodedPart.valid) {
3248
+ return false;
3249
+ }
3250
+
3251
+ index += 1;
3252
+ while (index < value.length && /\s/.test(value[index])) {
3253
+ index += 1;
3254
+ }
3255
+ if (value[index] !== "]") {
3256
+ return false;
3257
+ }
3258
+ index += 1;
3259
+ parts.push(decodedPart.value);
3260
+ return true;
3261
+ }
3262
+ index += 1;
3263
+ }
3264
+
3265
+ return false;
3266
+ };
3267
+
3268
+ if (!readBarePart()) {
3269
+ return invalidComputedReferenceParseResult;
3270
+ }
3271
+
3272
+ while (index < value.length) {
3273
+ if (value[index] === ".") {
3274
+ index += 1;
3275
+ if (!readBarePart()) {
3276
+ return invalidComputedReferenceParseResult;
3277
+ }
3278
+ continue;
3279
+ }
3280
+
3281
+ if (value[index] === "[") {
3282
+ if (!readBracketPart()) {
3283
+ return invalidComputedReferenceParseResult;
3284
+ }
3285
+ continue;
3286
+ }
3287
+
3288
+ return invalidComputedReferenceParseResult;
3289
+ }
3290
+
3291
+ return {
3292
+ valid: true,
3293
+ parts,
3294
+ };
3295
+ };
3296
+
3297
+ const validateComputedReferencePath = ({
3298
+ value,
3299
+ path,
3300
+ errorFactory,
3301
+ variables,
3302
+ dependencies,
3303
+ }) => {
3304
+ if (!isNonEmptyString(value)) {
3305
+ return invalidFromErrorFactory(
3306
+ errorFactory,
3307
+ `${path} must be a non-empty string path`,
3308
+ );
3309
+ }
3310
+
3311
+ const parsedPath = parseComputedReferencePath(value);
3312
+ if (!parsedPath.valid) {
3313
+ return invalidFromErrorFactory(
3314
+ errorFactory,
3315
+ `${path} has an invalid reference path`,
3316
+ );
3317
+ }
3318
+
3319
+ const [root, referencedId, ...nestedPath] = parsedPath.parts;
3320
+ if (root !== "variables" && root !== "runtime") {
3321
+ return invalidFromErrorFactory(
3322
+ errorFactory,
3323
+ `${path} must reference a concrete variables.* or runtime.* path`,
3324
+ );
3325
+ }
3326
+
3327
+ if (!isNonEmptyString(referencedId)) {
3328
+ return invalidFromErrorFactory(
3329
+ errorFactory,
3330
+ `${path} must reference a concrete ${root} member`,
3331
+ );
3332
+ }
3333
+
3334
+ if (root === "runtime" || variables === undefined) {
3335
+ return validComputedResult(undefined);
3336
+ }
3337
+
3338
+ const referencedVariable = Object.hasOwn(variables, referencedId)
3339
+ ? variables[referencedId]
3340
+ : undefined;
3341
+ if (
3342
+ !isPlainObject(referencedVariable) ||
3343
+ referencedVariable.type !== "variable"
3344
+ ) {
3345
+ return invalidFromErrorFactory(
3346
+ errorFactory,
3347
+ `${path} references unknown variable '${referencedId}'`,
3348
+ );
3349
+ }
3350
+
3351
+ if (Object.hasOwn(referencedVariable, "computed")) {
3352
+ dependencies?.add(referencedId);
3353
+ }
3354
+
3355
+ return validComputedResult(
3356
+ nestedPath.length === 0 ? referencedVariable.variableType : undefined,
3357
+ );
3358
+ };
3359
+
3360
+ const validateComputedExpression = ({
3361
+ expression,
3362
+ path,
3363
+ errorFactory,
3364
+ variables,
3365
+ dependencies,
3366
+ ancestors = new Set(),
3367
+ }) => {
3368
+ if (expression === null) {
3369
+ return validComputedResult("null");
3370
+ }
3371
+
3372
+ if (typeof expression !== "object") {
3373
+ if (typeof expression === "number") {
3374
+ if (Number.isFinite(expression)) {
3375
+ return validComputedResult("number");
3376
+ }
3377
+ return invalidFromErrorFactory(
3378
+ errorFactory,
3379
+ `${path} must use finite numeric literals`,
3380
+ );
3381
+ }
3382
+
3383
+ if (
3384
+ typeof expression === "string" ||
3385
+ typeof expression === "boolean"
3386
+ ) {
3387
+ return validComputedResult(typeof expression);
3388
+ }
3389
+
3390
+ return invalidFromErrorFactory(
3391
+ errorFactory,
3392
+ `${path} must use JSON-compatible primitive literals`,
3393
+ );
3394
+ }
3395
+
3396
+ if (Array.isArray(expression)) {
3397
+ return invalidFromErrorFactory(
3398
+ errorFactory,
3399
+ `${path} arrays must be wrapped in a literal operator or authored as value`,
3400
+ );
3401
+ }
3402
+
3403
+ if (ancestors.has(expression)) {
3404
+ return invalidFromErrorFactory(
3405
+ errorFactory,
3406
+ `${path} must not contain cyclic expression data`,
3407
+ );
3408
+ }
3409
+ const nextAncestors = new Set(ancestors);
3410
+ nextAncestors.add(expression);
3411
+
3412
+ const entries = Object.entries(expression);
3413
+ if (entries.length !== 1) {
3414
+ return invalidFromErrorFactory(
3415
+ errorFactory,
3416
+ `${path} must contain exactly one expression operator`,
3417
+ );
3418
+ }
3419
+
3420
+ const [[operator, operands]] = entries;
3421
+ if (operator === "var") {
3422
+ const result = validateComputedReferencePath({
3423
+ value: operands,
3424
+ path: `${path}.var`,
3425
+ errorFactory,
3426
+ variables,
3427
+ dependencies,
3428
+ });
3429
+ return result;
3430
+ }
3431
+
3432
+ if (operator === "literal") {
3433
+ const result = validateComputedDataValue({
3434
+ value: operands,
3435
+ path: `${path}.literal`,
3436
+ errorFactory,
3437
+ });
3438
+ return result?.valid === false
3439
+ ? result
3440
+ : validComputedResult(getComputedValueType(operands));
3441
+ }
3442
+
3443
+ const fixedOperandCount = COMPUTED_EXPRESSION_FIXED_OPERAND_COUNTS[operator];
3444
+ const isVariadic = COMPUTED_EXPRESSION_VARIADIC_OPERATORS.has(operator);
3445
+ if (fixedOperandCount === undefined && !isVariadic) {
3446
+ return invalidFromErrorFactory(
3447
+ errorFactory,
3448
+ `${path} contains unsupported expression operator '${operator}'`,
3449
+ );
3450
+ }
3451
+
3452
+ if (!Array.isArray(operands)) {
3453
+ return invalidFromErrorFactory(
3454
+ errorFactory,
3455
+ `${path}.${operator} must be an operand array`,
3456
+ );
3457
+ }
3458
+
3459
+ if (
3460
+ (fixedOperandCount !== undefined &&
3461
+ operands.length !== fixedOperandCount) ||
3462
+ (isVariadic && operands.length === 0)
3463
+ ) {
3464
+ const operandRequirement = isVariadic
3465
+ ? "at least one operand"
3466
+ : `exactly ${fixedOperandCount} operands`;
3467
+ return invalidFromErrorFactory(
3468
+ errorFactory,
3469
+ `${path}.${operator} requires ${operandRequirement}`,
3470
+ );
3471
+ }
3472
+
3473
+ const operandTypes = [];
3474
+ for (const [index, operand] of operands.entries()) {
3475
+ const result = validateComputedExpression({
3476
+ expression: operand,
3477
+ path: `${path}.${operator}[${index}]`,
3478
+ errorFactory,
3479
+ variables,
3480
+ dependencies,
3481
+ ancestors: nextAncestors,
3482
+ });
3483
+ if (result?.valid === false) {
3484
+ return result;
3485
+ }
3486
+ operandTypes.push(result.valueType);
3487
+ }
3488
+
3489
+ if (
3490
+ COMPUTED_EXPRESSION_NUMERIC_OPERAND_OPERATORS.has(operator) &&
3491
+ operandTypes.some(
3492
+ (operandType) => operandType !== undefined && operandType !== "number",
3493
+ )
3494
+ ) {
3495
+ return invalidFromErrorFactory(
3496
+ errorFactory,
3497
+ `${path}.${operator} requires numeric operands`,
3498
+ );
3499
+ }
3500
+
3501
+ if (COMPUTED_EXPRESSION_NUMERIC_RESULT_OPERATORS.has(operator)) {
3502
+ return validComputedResult("number");
3503
+ }
3504
+ if (COMPUTED_EXPRESSION_BOOLEAN_RESULT_OPERATORS.has(operator)) {
3505
+ return validComputedResult("boolean");
3506
+ }
3507
+ return validComputedResult(undefined);
3508
+ };
3509
+
3510
+ const validateComputedCondition = ({
3511
+ condition,
3512
+ path,
3513
+ errorFactory,
3514
+ variables,
3515
+ dependencies,
3516
+ isRoot = true,
3517
+ ancestors = new Set(),
3518
+ }) => {
3519
+ if (isRoot && typeof condition === "string") {
3520
+ return invalidFromErrorFactory(
3521
+ errorFactory,
3522
+ `${path} string conditions are not supported`,
3523
+ );
3524
+ }
3525
+
3526
+ if (condition === null) {
3527
+ return validComputedResult("null");
3528
+ }
3529
+
3530
+ if (typeof condition !== "object") {
3531
+ if (typeof condition === "number") {
3532
+ if (Number.isFinite(condition)) {
3533
+ return validComputedResult("number");
3534
+ }
3535
+ return invalidFromErrorFactory(
3536
+ errorFactory,
3537
+ `${path} must use finite numeric literals`,
3538
+ );
3539
+ }
3540
+
3541
+ if (
3542
+ typeof condition === "string" ||
3543
+ typeof condition === "boolean"
3544
+ ) {
3545
+ return validComputedResult(typeof condition);
3546
+ }
3547
+
3548
+ return invalidFromErrorFactory(
3549
+ errorFactory,
3550
+ `${path} must use JSON-compatible primitive literals`,
3551
+ );
3552
+ }
3553
+
3554
+ if (Array.isArray(condition)) {
3555
+ return invalidFromErrorFactory(
3556
+ errorFactory,
3557
+ `${path} arrays must be wrapped in a condition operator or literal`,
3558
+ );
3559
+ }
3560
+
3561
+ if (ancestors.has(condition)) {
3562
+ return invalidFromErrorFactory(
3563
+ errorFactory,
3564
+ `${path} must not contain cyclic condition data`,
3565
+ );
3566
+ }
3567
+ const nextAncestors = new Set(ancestors);
3568
+ nextAncestors.add(condition);
3569
+
3570
+ const entries = Object.entries(condition);
3571
+ if (entries.length !== 1) {
3572
+ return invalidFromErrorFactory(
3573
+ errorFactory,
3574
+ `${path} must contain exactly one condition operator`,
3575
+ );
3576
+ }
3577
+
3578
+ const [[operator, operands]] = entries;
3579
+ if (operator === "var") {
3580
+ const result = validateComputedReferencePath({
3581
+ value: operands,
3582
+ path: `${path}.var`,
3583
+ errorFactory,
3584
+ variables,
3585
+ dependencies,
3586
+ });
3587
+ return result;
3588
+ }
3589
+ if (operator === "literal") {
3590
+ const result = validateComputedDataValue({
3591
+ value: operands,
3592
+ path: `${path}.literal`,
3593
+ errorFactory,
3594
+ });
3595
+ return result?.valid === false
3596
+ ? result
3597
+ : validComputedResult(getComputedValueType(operands));
3598
+ }
3599
+ if (operator === "call") {
3600
+ return invalidFromErrorFactory(
3601
+ errorFactory,
3602
+ `${path} function calls are not supported`,
3603
+ );
3604
+ }
3605
+ if (operator === "not") {
3606
+ const result = validateComputedCondition({
3607
+ condition: operands,
3608
+ path: `${path}.not`,
3609
+ errorFactory,
3610
+ variables,
3611
+ dependencies,
3612
+ isRoot: false,
3613
+ ancestors: nextAncestors,
3614
+ });
3615
+ return result?.valid === false ? result : validComputedResult("boolean");
3616
+ }
3617
+
3618
+ const fixedOperandCount = COMPUTED_CONDITION_FIXED_OPERAND_COUNTS[operator];
3619
+ const isVariadic = COMPUTED_CONDITION_VARIADIC_OPERATORS.has(operator);
3620
+ if (fixedOperandCount === undefined && !isVariadic) {
3621
+ return invalidFromErrorFactory(
3622
+ errorFactory,
3623
+ `${path} contains unsupported condition operator '${operator}'`,
3624
+ );
3625
+ }
3626
+ if (!Array.isArray(operands)) {
3627
+ return invalidFromErrorFactory(
3628
+ errorFactory,
3629
+ `${path}.${operator} must be an operand array`,
3630
+ );
3631
+ }
3632
+ if (
3633
+ (fixedOperandCount !== undefined &&
3634
+ operands.length !== fixedOperandCount) ||
3635
+ (isVariadic && operands.length === 0)
3636
+ ) {
3637
+ const operandRequirement = isVariadic
3638
+ ? "at least one operand"
3639
+ : `exactly ${fixedOperandCount} operands`;
3640
+ return invalidFromErrorFactory(
3641
+ errorFactory,
3642
+ `${path}.${operator} requires ${operandRequirement}`,
3643
+ );
3644
+ }
3645
+
3646
+ const operandTypes = [];
3647
+ for (const [index, operand] of operands.entries()) {
3648
+ const result = validateComputedCondition({
3649
+ condition: operand,
3650
+ path: `${path}.${operator}[${index}]`,
3651
+ errorFactory,
3652
+ variables,
3653
+ dependencies,
3654
+ isRoot: false,
3655
+ ancestors: nextAncestors,
3656
+ });
3657
+ if (result?.valid === false) {
3658
+ return result;
3659
+ }
3660
+ operandTypes.push(result.valueType);
3661
+ }
3662
+
3663
+ if (
3664
+ (operator === "add" || operator === "sub") &&
3665
+ operandTypes.some(
3666
+ (operandType) => operandType !== undefined && operandType !== "number",
3667
+ )
3668
+ ) {
3669
+ return invalidFromErrorFactory(
3670
+ errorFactory,
3671
+ `${path}.${operator} requires numeric operands`,
3672
+ );
3673
+ }
3674
+
3675
+ return validComputedResult(
3676
+ operator === "add" || operator === "sub" ? "number" : "boolean",
3677
+ );
3678
+ };
3679
+
3680
+ const validateComputedResultConfig = ({
3681
+ resultConfig,
3682
+ variableType,
3683
+ path,
3684
+ errorFactory,
3685
+ variables,
3686
+ dependencies,
3687
+ allowedKeys = ["expr", "value"],
3688
+ }) => {
3689
+ if (!isPlainObject(resultConfig)) {
3690
+ return invalidFromErrorFactory(errorFactory, `${path} must be an object`);
3691
+ }
3692
+
3693
+ {
3694
+ const result = validateAllowedKeys({
3695
+ value: resultConfig,
3696
+ allowedKeys,
3697
+ path,
3698
+ errorFactory,
3699
+ });
3700
+ if (result?.valid === false) {
3701
+ return result;
3702
+ }
3703
+ }
3704
+
3705
+ const hasExpression = Object.hasOwn(resultConfig, "expr");
3706
+ const hasValue = Object.hasOwn(resultConfig, "value");
3707
+ if (hasExpression === hasValue) {
3708
+ return invalidFromErrorFactory(
3709
+ errorFactory,
3710
+ `${path} must contain exactly one of expr or value`,
3711
+ );
3712
+ }
3713
+
3714
+ if (hasValue) {
3715
+ const dataResult = validateComputedDataValue({
3716
+ value: resultConfig.value,
3717
+ path: `${path}.value`,
3718
+ errorFactory,
3719
+ });
3720
+ if (dataResult?.valid === false) {
3721
+ return dataResult;
3722
+ }
3723
+ return validateVariableTypedValue({
3724
+ value: resultConfig.value,
3725
+ variableType,
3726
+ path: `${path}.value`,
3727
+ errorFactory,
3728
+ });
3729
+ }
3730
+
3731
+ const expressionResult = validateComputedExpression({
3732
+ expression: resultConfig.expr,
3733
+ path: `${path}.expr`,
3734
+ errorFactory,
3735
+ variables,
3736
+ dependencies,
3737
+ });
3738
+ if (expressionResult?.valid === false) {
3739
+ return expressionResult;
3740
+ }
3741
+ if (
3742
+ expressionResult.valueType !== undefined &&
3743
+ expressionResult.valueType !== variableType
3744
+ ) {
3745
+ return invalidFromErrorFactory(
3746
+ errorFactory,
3747
+ `${path}.expr must resolve to ${variableType}`,
3748
+ );
3749
+ }
3750
+ return VALID_RESULT;
3751
+ };
3752
+
3753
+ const validateVariableComputedConfig = ({
3754
+ computed,
3755
+ variableType,
3756
+ path,
3757
+ errorFactory,
3758
+ variables,
3759
+ dependencies,
3760
+ }) => {
3761
+ if (!isPlainObject(computed)) {
3762
+ return invalidFromErrorFactory(errorFactory, `${path} must be an object`);
3763
+ }
3764
+
3765
+ if (Object.hasOwn(computed, "branches")) {
3766
+ {
3767
+ const result = validateAllowedKeys({
3768
+ value: computed,
3769
+ allowedKeys: ["branches", "default"],
3770
+ path,
3771
+ errorFactory,
3772
+ });
3773
+ if (result?.valid === false) {
3774
+ return result;
3775
+ }
3776
+ }
3777
+ if (!Array.isArray(computed.branches) || computed.branches.length === 0) {
3778
+ return invalidFromErrorFactory(
3779
+ errorFactory,
3780
+ `${path}.branches must be a non-empty array`,
3781
+ );
3782
+ }
3783
+ if (!isPlainObject(computed.default)) {
3784
+ return invalidFromErrorFactory(
3785
+ errorFactory,
3786
+ `${path}.default must be an object`,
3787
+ );
3788
+ }
3789
+
3790
+ for (const [index, branch] of computed.branches.entries()) {
3791
+ const branchPath = `${path}.branches[${index}]`;
3792
+ if (!isPlainObject(branch)) {
3793
+ return invalidFromErrorFactory(
3794
+ errorFactory,
3795
+ `${branchPath} must be an object`,
3796
+ );
3797
+ }
3798
+ if (!Object.hasOwn(branch, "when")) {
3799
+ return invalidFromErrorFactory(
3800
+ errorFactory,
3801
+ `${branchPath}.when is required`,
3802
+ );
3803
+ }
3804
+ const conditionResult = validateComputedCondition({
3805
+ condition: branch.when,
3806
+ path: `${branchPath}.when`,
3807
+ errorFactory,
3808
+ variables,
3809
+ dependencies,
3810
+ });
3811
+ if (conditionResult?.valid === false) {
3812
+ return conditionResult;
3813
+ }
3814
+ const branchResult = validateComputedResultConfig({
3815
+ resultConfig: branch,
3816
+ variableType,
3817
+ path: branchPath,
3818
+ errorFactory,
3819
+ variables,
3820
+ dependencies,
3821
+ allowedKeys: ["when", "expr", "value"],
3822
+ });
3823
+ if (branchResult?.valid === false) {
3824
+ return branchResult;
3825
+ }
3826
+ }
3827
+
3828
+ return validateComputedResultConfig({
3829
+ resultConfig: computed.default,
3830
+ variableType,
3831
+ path: `${path}.default`,
3832
+ errorFactory,
3833
+ variables,
3834
+ dependencies,
3835
+ });
3836
+ }
3837
+
3838
+ return validateComputedResultConfig({
3839
+ resultConfig: computed,
3840
+ variableType,
3841
+ path,
3842
+ errorFactory,
3843
+ variables,
3844
+ dependencies,
3845
+ });
3846
+ };
3847
+
3848
+ const validateComputedVariableGraph = ({
3849
+ items,
3850
+ path,
3851
+ errorFactory,
3852
+ }) => {
3853
+ const dependencyGraph = new Map();
3854
+
3855
+ for (const [variableId, variable] of Object.entries(items)) {
3856
+ if (
3857
+ variable?.type !== "variable" ||
3858
+ !Object.hasOwn(variable, "computed")
3859
+ ) {
3860
+ continue;
3861
+ }
3862
+
3863
+ const dependencies = new Set();
3864
+ const result = validateVariableComputedConfig({
3865
+ computed: variable.computed,
3866
+ variableType: variable.variableType,
3867
+ path: `${path}.${variableId}.computed`,
3868
+ errorFactory,
3869
+ variables: items,
3870
+ dependencies,
3871
+ });
3872
+ if (result?.valid === false) {
3873
+ return result;
3874
+ }
3875
+ dependencyGraph.set(variableId, dependencies);
3876
+ }
3877
+
3878
+ const visited = new Set();
3879
+ for (const startVariableId of dependencyGraph.keys()) {
3880
+ if (visited.has(startVariableId)) {
3881
+ continue;
3882
+ }
3883
+
3884
+ const frames = [
3885
+ {
3886
+ variableId: startVariableId,
3887
+ dependencies: [...(dependencyGraph.get(startVariableId) ?? [])],
3888
+ nextDependencyIndex: 0,
3889
+ },
3890
+ ];
3891
+ const activeIndexes = new Map([[startVariableId, 0]]);
3892
+
3893
+ while (frames.length > 0) {
3894
+ const frame = frames.at(-1);
3895
+ if (frame.nextDependencyIndex >= frame.dependencies.length) {
3896
+ frames.pop();
3897
+ activeIndexes.delete(frame.variableId);
3898
+ visited.add(frame.variableId);
3899
+ continue;
3900
+ }
3901
+
3902
+ const dependencyId = frame.dependencies[frame.nextDependencyIndex];
3903
+ frame.nextDependencyIndex += 1;
3904
+ if (visited.has(dependencyId)) {
3905
+ continue;
3906
+ }
3907
+
3908
+ const cycleStartIndex = activeIndexes.get(dependencyId);
3909
+ if (cycleStartIndex !== undefined) {
3910
+ const cycle = [
3911
+ ...frames
3912
+ .slice(cycleStartIndex)
3913
+ .map(({ variableId }) => variableId),
3914
+ dependencyId,
3915
+ ].join(" -> ");
3916
+ return invalidFromErrorFactory(
3917
+ errorFactory,
3918
+ `${path} contains computed variable cycle: ${cycle}`,
3919
+ );
3920
+ }
3921
+
3922
+ if (!dependencyGraph.has(dependencyId)) {
3923
+ visited.add(dependencyId);
3924
+ continue;
3925
+ }
3926
+
3927
+ activeIndexes.set(dependencyId, frames.length);
3928
+ frames.push({
3929
+ variableId: dependencyId,
3930
+ dependencies: [...(dependencyGraph.get(dependencyId) ?? [])],
3931
+ nextDependencyIndex: 0,
3932
+ });
3933
+ }
3934
+ }
3935
+
3936
+ return VALID_RESULT;
3937
+ };
3938
+
3939
+ const validateVariableStoredOrComputedData = ({
3940
+ data,
3941
+ variableType,
3942
+ path,
3943
+ errorFactory,
3944
+ }) => {
3945
+ const isComputed = Object.hasOwn(data, "computed");
3946
+ if (isComputed) {
3947
+ if (Object.hasOwn(data, "default") || Object.hasOwn(data, "value")) {
3948
+ return invalidFromErrorFactory(
3949
+ errorFactory,
3950
+ `${path} computed variables must not contain default or value`,
3951
+ );
3952
+ }
3953
+ if (data.isEnum !== undefined || data.enumValues !== undefined) {
3954
+ return invalidFromErrorFactory(
3955
+ errorFactory,
3956
+ `${path} computed variables must not contain enum metadata`,
3957
+ );
3958
+ }
3959
+ return validateVariableComputedConfig({
3960
+ computed: data.computed,
3961
+ variableType,
3962
+ path: `${path}.computed`,
3963
+ errorFactory,
3964
+ });
3965
+ }
3966
+
3967
+ {
3968
+ const result = validateVariableTypedValue({
3969
+ value: data.default,
3970
+ variableType,
3971
+ path: `${path}.default`,
3972
+ errorFactory,
3973
+ });
3974
+ if (result?.valid === false) {
3975
+ return result;
3976
+ }
3977
+ }
3978
+ return validateVariableTypedValue({
3979
+ value: data.value,
3980
+ variableType,
3981
+ path: `${path}.value`,
3982
+ errorFactory,
3983
+ });
2911
3984
  };
2912
3985
 
2913
3986
  const normalizeVariableEnumValues = (values = []) => {
@@ -2995,6 +4068,13 @@ const validateVariableItems = ({ items, path, errorFactory }) => {
2995
4068
  const itemType = item?.type;
2996
4069
  const variableType = item?.variableType;
2997
4070
 
4071
+ if (itemId === "__proto__") {
4072
+ return invalidFromErrorFactory(
4073
+ errorFactory,
4074
+ `${itemPath} uses reserved variable id '__proto__'`,
4075
+ );
4076
+ }
4077
+
2998
4078
  if (itemType !== "folder" && itemType !== "variable") {
2999
4079
  return invalidFromErrorFactory(
3000
4080
  errorFactory,
@@ -3020,6 +4100,7 @@ const validateVariableItems = ({ items, path, errorFactory }) => {
3020
4100
  "value",
3021
4101
  "isEnum",
3022
4102
  "enumValues",
4103
+ "computed",
3023
4104
  ],
3024
4105
  path: itemPath,
3025
4106
  errorFactory,
@@ -3061,7 +4142,7 @@ const validateVariableItems = ({ items, path, errorFactory }) => {
3061
4142
  if (!VARIABLE_TYPE_KEYS.includes(variableType)) {
3062
4143
  return invalidFromErrorFactory(
3063
4144
  errorFactory,
3064
- `${itemPath}.variableType must be 'string', 'number', or 'boolean'`,
4145
+ `${itemPath}.variableType must be 'string', 'number', 'boolean', or 'object'`,
3065
4146
  );
3066
4147
  }
3067
4148
 
@@ -3089,7 +4170,14 @@ const validateVariableItems = ({ items, path, errorFactory }) => {
3089
4170
  }
3090
4171
  }
3091
4172
 
3092
- if (!VARIABLE_SCOPE_KEYS.includes(item.scope)) {
4173
+ if (item.computed !== undefined) {
4174
+ if (item.scope !== undefined) {
4175
+ return invalidFromErrorFactory(
4176
+ errorFactory,
4177
+ `${itemPath}.scope must be omitted for computed variables`,
4178
+ );
4179
+ }
4180
+ } else if (!VARIABLE_SCOPE_KEYS.includes(item.scope)) {
3093
4181
  return invalidFromErrorFactory(
3094
4182
  errorFactory,
3095
4183
  `${itemPath}.scope must be 'context', 'device', or 'account'`,
@@ -3097,21 +4185,10 @@ const validateVariableItems = ({ items, path, errorFactory }) => {
3097
4185
  }
3098
4186
 
3099
4187
  {
3100
- const result = validateVariableTypedValue({
3101
- value: item.default,
3102
- variableType,
3103
- path: `${itemPath}.default`,
3104
- errorFactory,
3105
- });
3106
- if (result?.valid === false) {
3107
- return result;
3108
- }
3109
- }
3110
- {
3111
- const result = validateVariableTypedValue({
3112
- value: item.value,
4188
+ const result = validateVariableStoredOrComputedData({
4189
+ data: item,
3113
4190
  variableType,
3114
- path: `${itemPath}.value`,
4191
+ path: itemPath,
3115
4192
  errorFactory,
3116
4193
  });
3117
4194
  if (result?.valid === false) {
@@ -3120,6 +4197,12 @@ const validateVariableItems = ({ items, path, errorFactory }) => {
3120
4197
  }
3121
4198
  }
3122
4199
  }
4200
+
4201
+ return validateComputedVariableGraph({
4202
+ items,
4203
+ path,
4204
+ errorFactory,
4205
+ });
3123
4206
  };
3124
4207
 
3125
4208
  const validateTextStyleShadow = ({ shadow, path, errorFactory }) => {
@@ -11070,6 +12153,7 @@ const validateVariableCreateData = ({ data, errorFactory }) => {
11070
12153
  "value",
11071
12154
  "isEnum",
11072
12155
  "enumValues",
12156
+ "computed",
11073
12157
  ],
11074
12158
  path: "payload.data",
11075
12159
  errorFactory,
@@ -11097,7 +12181,7 @@ const validateVariableCreateData = ({ data, errorFactory }) => {
11097
12181
  if (!VARIABLE_TYPE_KEYS.includes(data.variableType)) {
11098
12182
  return invalidFromErrorFactory(
11099
12183
  errorFactory,
11100
- "payload.data.variableType must be 'string', 'number', or 'boolean'",
12184
+ "payload.data.variableType must be 'string', 'number', 'boolean', or 'object'",
11101
12185
  );
11102
12186
  }
11103
12187
 
@@ -11124,7 +12208,14 @@ const validateVariableCreateData = ({ data, errorFactory }) => {
11124
12208
  }
11125
12209
  }
11126
12210
 
11127
- if (!VARIABLE_SCOPE_KEYS.includes(data.scope)) {
12211
+ if (data.computed !== undefined) {
12212
+ if (data.scope !== undefined) {
12213
+ return invalidFromErrorFactory(
12214
+ errorFactory,
12215
+ "payload.data.scope must be omitted for computed variables",
12216
+ );
12217
+ }
12218
+ } else if (!VARIABLE_SCOPE_KEYS.includes(data.scope)) {
11128
12219
  return invalidFromErrorFactory(
11129
12220
  errorFactory,
11130
12221
  "payload.data.scope must be 'context', 'device', or 'account'",
@@ -11132,21 +12223,10 @@ const validateVariableCreateData = ({ data, errorFactory }) => {
11132
12223
  }
11133
12224
 
11134
12225
  {
11135
- const result = validateVariableTypedValue({
11136
- value: data.default,
11137
- variableType: data.variableType,
11138
- path: "payload.data.default",
11139
- errorFactory,
11140
- });
11141
- if (result?.valid === false) {
11142
- return result;
11143
- }
11144
- }
11145
- {
11146
- const result = validateVariableTypedValue({
11147
- value: data.value,
12226
+ const result = validateVariableStoredOrComputedData({
12227
+ data,
11148
12228
  variableType: data.variableType,
11149
- path: "payload.data.value",
12229
+ path: "payload.data",
11150
12230
  errorFactory,
11151
12231
  });
11152
12232
  if (result?.valid === false) {
@@ -11169,6 +12249,7 @@ const validateVariableUpdateData = ({ data, errorFactory }) => {
11169
12249
  "value",
11170
12250
  "isEnum",
11171
12251
  "enumValues",
12252
+ "computed",
11172
12253
  ],
11173
12254
  path: "payload.data",
11174
12255
  errorFactory,
@@ -11217,6 +12298,15 @@ const validateVariableUpdateData = ({ data, errorFactory }) => {
11217
12298
  }
11218
12299
  }
11219
12300
 
12301
+ if (data.computed !== undefined) {
12302
+ if (!isPlainObject(data.computed)) {
12303
+ return invalidFromErrorFactory(
12304
+ errorFactory,
12305
+ "payload.data.computed must be an object when provided",
12306
+ );
12307
+ }
12308
+ }
12309
+
11220
12310
  if (data.scope !== undefined && !VARIABLE_SCOPE_KEYS.includes(data.scope)) {
11221
12311
  return invalidFromErrorFactory(
11222
12312
  errorFactory,
@@ -13038,6 +14128,7 @@ const createFolderedCollectionCommandDefinitions = ({
13038
14128
  validateDeleteState = () => {},
13039
14129
  afterDelete = () => {},
13040
14130
  includeUpdate = true,
14131
+ reservedItemIds = [],
13041
14132
  }) => {
13042
14133
  const existingMessage = `payload.${idField} must reference an existing ${itemLabel}`;
13043
14134
  const duplicateMessage = `payload.${idField} must not already exist`;
@@ -13078,6 +14169,12 @@ const createFolderedCollectionCommandDefinitions = ({
13078
14169
  );
13079
14170
  }
13080
14171
 
14172
+ if (reservedItemIds.includes(payload[idField])) {
14173
+ return invalidPayload(
14174
+ `payload.${idField} must not use reserved id '${payload[idField]}'`,
14175
+ );
14176
+ }
14177
+
13081
14178
  if (
13082
14179
  payload.parentId !== undefined &&
13083
14180
  payload.parentId !== null &&
@@ -18514,6 +19611,7 @@ const COMMAND_DEFINITIONS = [
18514
19611
  itemLabel: "variable item",
18515
19612
  createDataValidator: validateVariableCreateData,
18516
19613
  updateDataValidator: validateVariableUpdateData,
19614
+ reservedItemIds: ["__proto__"],
18517
19615
  createItem: ({ payload }) => {
18518
19616
  const data = structuredClone(payload.data);
18519
19617
  if (!Array.isArray(data.tagIds) || data.tagIds.length === 0) {
@@ -18538,7 +19636,7 @@ const COMMAND_DEFINITIONS = [
18538
19636
  return;
18539
19637
  }
18540
19638
 
18541
- return validateTagIdsAgainstScope({
19639
+ const tagResult = validateTagIdsAgainstScope({
18542
19640
  state,
18543
19641
  tagIds: payload.data.tagIds,
18544
19642
  scopeKey: "variables",
@@ -18547,6 +19645,25 @@ const COMMAND_DEFINITIONS = [
18547
19645
  variableId: payload.variableId,
18548
19646
  },
18549
19647
  });
19648
+ if (!tagResult.valid) {
19649
+ return tagResult;
19650
+ }
19651
+
19652
+ if (!Object.hasOwn(payload.data, "computed")) {
19653
+ return VALID_RESULT;
19654
+ }
19655
+
19656
+ return validateComputedVariableGraph({
19657
+ items: {
19658
+ ...state.variables.items,
19659
+ [payload.variableId]: {
19660
+ id: payload.variableId,
19661
+ ...payload.data,
19662
+ },
19663
+ },
19664
+ path: "state.variables.items",
19665
+ errorFactory: createPreconditionValidationError,
19666
+ });
18550
19667
  },
18551
19668
  validateUpdateState: ({ state, payload, currentItem }) => {
18552
19669
  if (
@@ -18570,6 +19687,36 @@ const COMMAND_DEFINITIONS = [
18570
19687
  );
18571
19688
  }
18572
19689
 
19690
+ const currentItemIsComputed = Object.hasOwn(currentItem, "computed");
19691
+ if (
19692
+ currentItemIsComputed &&
19693
+ ["scope", "default", "value", "isEnum", "enumValues"].some((key) =>
19694
+ Object.hasOwn(payload.data, key),
19695
+ )
19696
+ ) {
19697
+ return invalidPrecondition(
19698
+ "computed variables cannot update scope, stored value, or enum fields",
19699
+ );
19700
+ }
19701
+
19702
+ if (!currentItemIsComputed && Object.hasOwn(payload.data, "computed")) {
19703
+ return invalidPrecondition(
19704
+ "stored variables cannot be converted to computed variables",
19705
+ );
19706
+ }
19707
+
19708
+ if (currentItemIsComputed && Object.hasOwn(payload.data, "computed")) {
19709
+ const result = validateVariableComputedConfig({
19710
+ computed: payload.data.computed,
19711
+ variableType: currentItem.variableType,
19712
+ path: "payload.data.computed",
19713
+ errorFactory: createPreconditionValidationError,
19714
+ });
19715
+ if (result?.valid === false) {
19716
+ return result;
19717
+ }
19718
+ }
19719
+
18573
19720
  if (currentItem.type !== "folder") {
18574
19721
  {
18575
19722
  const result = validateTagIdsAgainstScope({
@@ -18614,6 +19761,49 @@ const COMMAND_DEFINITIONS = [
18614
19761
  }
18615
19762
  }
18616
19763
  }
19764
+
19765
+ if (currentItemIsComputed && Object.hasOwn(payload.data, "computed")) {
19766
+ return validateComputedVariableGraph({
19767
+ items: {
19768
+ ...state.variables.items,
19769
+ [payload.variableId]: applyVariableUpdate({
19770
+ currentItem,
19771
+ data: payload.data,
19772
+ }),
19773
+ },
19774
+ path: "state.variables.items",
19775
+ errorFactory: createPreconditionValidationError,
19776
+ });
19777
+ }
19778
+
19779
+ return VALID_RESULT;
19780
+ },
19781
+ validateDeleteState: ({ state, payload }) => {
19782
+ const deletedIds = new Set();
19783
+ for (const variableId of payload.variableIds) {
19784
+ const node = findTreeNode({
19785
+ nodes: state.variables.tree,
19786
+ nodeId: variableId,
19787
+ });
19788
+ if (!node) {
19789
+ deletedIds.add(variableId);
19790
+ continue;
19791
+ }
19792
+ for (const descendantId of collectTreeDescendantIds({ node })) {
19793
+ deletedIds.add(descendantId);
19794
+ }
19795
+ }
19796
+
19797
+ const remainingItems = Object.fromEntries(
19798
+ Object.entries(state.variables.items).filter(
19799
+ ([variableId]) => !deletedIds.has(variableId),
19800
+ ),
19801
+ );
19802
+ return validateComputedVariableGraph({
19803
+ items: remainingItems,
19804
+ path: "state.variables.items",
19805
+ errorFactory: createPreconditionValidationError,
19806
+ });
18617
19807
  },
18618
19808
  }),
18619
19809
  ...createFolderedCollectionCommandDefinitions({