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