@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.
- package/README.md +38 -0
- package/package.json +1 -1
- 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 =
|
|
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 '
|
|
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 (
|
|
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 =
|
|
3101
|
-
|
|
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:
|
|
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 '
|
|
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 (
|
|
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 =
|
|
11136
|
-
|
|
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
|
|
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
|
-
|
|
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({
|