@configdirector/nuxt-sdk 1.5.0 → 1.6.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/dist/module.json CHANGED
@@ -4,5 +4,5 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=4.0.0"
6
6
  },
7
- "version": "1.5.0"
7
+ "version": "1.6.0"
8
8
  }
@@ -6938,9 +6938,6 @@ const equals = (value, target) => value !== null && target !== null && semver.eq
6938
6938
  const ordered = (value, target, compare) => value !== null && target !== null && compare(value, target);
6939
6939
  //#endregion
6940
6940
  //#region ../config-evaluator/src/render.ts
6941
- /**
6942
- * Rendering a resolved context value to text.
6943
- */
6944
6941
  /** A value the context does not carry. Compared as "", so a negative operator can still match. */
6945
6942
  const ABSENT = Symbol("absent");
6946
6943
  /** An attribute this SDK version does not know about. Not compared at all. */
@@ -6962,6 +6959,12 @@ const renderScalar = (value) => {
6962
6959
  };
6963
6960
  /** The raw value, for comparisons that inspect its JSON type rather than its text. */
6964
6961
  const unwrap = (value) => value === ABSENT ? void 0 : value;
6962
+ const resolvedTypeOf = (value) => {
6963
+ if (value === UNKNOWN_ATTRIBUTE) return "unknown-attribute";
6964
+ if (value === ABSENT) return "absent";
6965
+ if (isScalar(value)) return "scalar";
6966
+ return Array.isArray(value) ? "array" : "object";
6967
+ };
6965
6968
  //#endregion
6966
6969
  //#region ../config-evaluator/src/array-comparison.ts
6967
6970
  /**
@@ -6992,8 +6995,28 @@ var ConditionEvaluator = class {
6992
6995
  * a condition can still match or not match on its own terms.
6993
6996
  */
6994
6997
  evaluate(condition, context) {
6998
+ return this.explain(condition, context).matched;
6999
+ }
7000
+ /**
7001
+ * Decide whether a condition holds for a context, and say what the condition was compared
7002
+ * against: the raw resolved value and its shape.
7003
+ */
7004
+ explain(condition, context) {
6995
7005
  const value = this.resolve(condition, context);
6996
- if (value === UNKNOWN_ATTRIBUTE) return false;
7006
+ const resolvedType = resolvedTypeOf(value);
7007
+ const resolvedValue = value === ABSENT || value === UNKNOWN_ATTRIBUTE ? void 0 : value;
7008
+ if (value === UNKNOWN_ATTRIBUTE) return {
7009
+ matched: false,
7010
+ resolvedValue,
7011
+ resolvedType
7012
+ };
7013
+ return {
7014
+ matched: this.compare(condition, value),
7015
+ resolvedValue,
7016
+ resolvedType
7017
+ };
7018
+ }
7019
+ compare(condition, value) {
6997
7020
  const targetValues = condition.targetValues ?? [];
6998
7021
  switch (condition.targetType) {
6999
7022
  case "text": return compareText(render(value), condition.operator, targetValues);
@@ -7047,61 +7070,130 @@ var ConfigEvaluator = class {
7047
7070
  id: config.id,
7048
7071
  key: config.key,
7049
7072
  type: config.type,
7050
- value: this.getConfigValue(config, context)
7073
+ value: this.explain(config, context).value
7051
7074
  };
7052
7075
  }
7053
- getConfigValue(config, context) {
7076
+ /**
7077
+ * Evaluate a config for a context and record how the value was reached: every rule in the
7078
+ * order it was walked with its outcome, the conditions that were checked with what they
7079
+ * resolved to, and the share a rollout assigned the context to. `evaluate` is this walk with
7080
+ * only the value kept, so the two cannot disagree.
7081
+ */
7082
+ explain(config, context) {
7054
7083
  const rules = [...config.target?.rules ?? []].sort((a, b) => (a.order ?? Number.MAX_SAFE_INTEGER) - (b.order ?? Number.MAX_SAFE_INTEGER));
7084
+ const explanations = [];
7085
+ let served;
7055
7086
  for (const rule of rules) {
7056
- const result = this.evaluateRule(rule, config, context);
7057
- if (result.success) return result.value;
7087
+ if (served) {
7088
+ explanations.push({
7089
+ ruleId: rule.id,
7090
+ outcome: "not-evaluated",
7091
+ conditions: [],
7092
+ bucket: void 0
7093
+ });
7094
+ continue;
7095
+ }
7096
+ const result = this.explainRule(rule, config, context);
7097
+ explanations.push(result.explanation);
7098
+ if (result.value !== void 0) served = {
7099
+ ruleId: rule.id,
7100
+ value: result.value
7101
+ };
7058
7102
  }
7059
- return config.target?.defaultValue;
7103
+ return {
7104
+ value: served?.value ?? config.target?.defaultValue,
7105
+ servedBy: served ? {
7106
+ kind: "rule",
7107
+ ruleId: served.ruleId
7108
+ } : { kind: "default" },
7109
+ rules: explanations
7110
+ };
7060
7111
  }
7061
- evaluateRule(rule, config, context) {
7112
+ explainRule(rule, config, context) {
7113
+ const explanation = {
7114
+ ruleId: rule.id,
7115
+ outcome: "not-matched",
7116
+ conditions: [],
7117
+ bucket: void 0
7118
+ };
7062
7119
  try {
7063
- if (rule.type == "percentage") return this.evaluatePercentageRule(rule, config, context);
7064
- else if (rule.type == "conditional") return this.evaluateConditionalRule(rule, config, context);
7120
+ let value;
7121
+ if (rule.type == "percentage") value = this.explainPercentage(rule.percentages ?? [], config, context, explanation);
7122
+ else if (rule.type == "conditional") value = this.explainConditionalRule(rule, config, context, explanation);
7123
+ if (value !== void 0) explanation.outcome = "matched";
7124
+ return {
7125
+ explanation,
7126
+ value
7127
+ };
7065
7128
  } catch (error) {
7066
7129
  this.logger.warn(`There was an error while evaluating a targeting rule '${rule?.id}' for '${config?.key}'. The rule will be disregarded.`, {
7067
7130
  error,
7068
7131
  configKey: config?.key,
7069
7132
  ruleId: rule?.id
7070
7133
  });
7134
+ return {
7135
+ explanation: {
7136
+ ruleId: rule.id,
7137
+ outcome: "errored",
7138
+ conditions: [],
7139
+ bucket: void 0
7140
+ },
7141
+ value: void 0
7142
+ };
7071
7143
  }
7072
- return { success: false };
7073
7144
  }
7074
- evaluatePercentageRule(rule, config, context) {
7075
- return this.evaluatePercentage(rule.percentages ?? [], config, context);
7076
- }
7077
- evaluatePercentage(percentages, config, context) {
7145
+ explainPercentage(percentages, config, context, explanation) {
7146
+ const contextIdentifier = context?.context?.id;
7147
+ const identifier = contextIdentifier ?? crypto.randomUUID();
7078
7148
  const assignedPercentage = assignPercentage({
7079
7149
  configId: config.id,
7080
- contextIdentifier: context?.context?.id ?? crypto.randomUUID()
7150
+ contextIdentifier: identifier
7081
7151
  });
7152
+ const shares = [];
7082
7153
  let sum = 0;
7083
- let bucket = void 0;
7154
+ let selected = void 0;
7084
7155
  for (const percentage of percentages) {
7085
- if (assignedPercentage < percentage.percentage + sum) {
7086
- bucket = percentage;
7087
- break;
7088
- }
7089
- sum += percentage.percentage;
7156
+ const to = sum + percentage.percentage;
7157
+ shares.push({
7158
+ percentageId: percentage.id,
7159
+ from: sum,
7160
+ to,
7161
+ value: percentage.value?.toString()
7162
+ });
7163
+ if (selected === void 0 && assignedPercentage < to) selected = percentage;
7164
+ sum = to;
7090
7165
  }
7091
- if (bucket?.value != null) return {
7092
- success: true,
7093
- value: bucket.value.toString()
7166
+ explanation.bucket = {
7167
+ identifier,
7168
+ identifierWasGenerated: contextIdentifier == null,
7169
+ assignedPercentage,
7170
+ shares,
7171
+ selectedPercentageId: selected?.id
7094
7172
  };
7095
- return { success: false };
7096
- }
7097
- evaluateConditionalRule(rule, config, context) {
7098
- const conditionsMet = (rule.conditions ?? []).every((candidate) => this.conditionEvaluator.evaluate(candidate, context));
7099
- if (conditionsMet && rule.target == "value" && rule.value != null) return {
7100
- success: true,
7101
- value: rule.value.toString()
7102
- };
7103
- else if (conditionsMet && rule.target == "percentage") return this.evaluatePercentage(rule.percentages ?? [], config, context);
7104
- return { success: false };
7173
+ return selected?.value != null ? selected.value.toString() : void 0;
7174
+ }
7175
+ explainConditionalRule(rule, config, context, explanation) {
7176
+ let failed = false;
7177
+ for (const condition of rule.conditions ?? []) {
7178
+ if (failed) {
7179
+ explanation.conditions.push({
7180
+ conditionId: condition.id,
7181
+ outcome: "not-evaluated"
7182
+ });
7183
+ continue;
7184
+ }
7185
+ const check = this.conditionEvaluator.explain(condition, context);
7186
+ explanation.conditions.push({
7187
+ conditionId: condition.id,
7188
+ outcome: check.matched ? "matched" : "not-matched",
7189
+ resolvedValue: check.resolvedValue,
7190
+ resolvedType: check.resolvedType
7191
+ });
7192
+ failed = !check.matched;
7193
+ }
7194
+ if (failed) return;
7195
+ if (rule.target == "value") return rule.value != null ? rule.value.toString() : void 0;
7196
+ if (rule.target == "percentage") return this.explainPercentage(rule.percentages ?? [], config, context, explanation);
7105
7197
  }
7106
7198
  };
7107
7199
  //#endregion
@@ -7124,6 +7216,11 @@ const getRequestedType = (defaultValue) => {
7124
7216
  const isNumericNativeType = (requestedType) => {
7125
7217
  return requestedType === "number" || requestedType === "bigint";
7126
7218
  };
7219
+ const configTypesNotReadableAsString = new Set([
7220
+ "boolean",
7221
+ "integer",
7222
+ "float"
7223
+ ]);
7127
7224
  const parseConfigValue = (configState, defaultValue) => {
7128
7225
  const value = configState.value;
7129
7226
  const requestedType = getRequestedType(defaultValue);
@@ -7135,13 +7232,22 @@ const parseConfigValue = (configState, defaultValue) => {
7135
7232
  reason: "value-missing"
7136
7233
  };
7137
7234
  if (configState.type === "json") return parseJson(value, defaultValue, requestedType, configState.valueId);
7138
- if (typeof defaultValue === "string") return {
7139
- parsedValue: value,
7140
- parsedValueId: configState.valueId,
7141
- requestedType,
7142
- usedDefault: false,
7143
- reason: "found-match"
7144
- };
7235
+ if (typeof defaultValue === "string") {
7236
+ if (configTypesNotReadableAsString.has(configState.type)) return {
7237
+ parsedValue: defaultValue,
7238
+ parsedValueId: void 0,
7239
+ requestedType,
7240
+ usedDefault: true,
7241
+ reason: "type-mismatch"
7242
+ };
7243
+ return {
7244
+ parsedValue: value,
7245
+ parsedValueId: configState.valueId,
7246
+ requestedType,
7247
+ usedDefault: false,
7248
+ reason: "found-match"
7249
+ };
7250
+ }
7145
7251
  if (typeof defaultValue === "boolean" && (configState.type === "boolean" || configState.type === "string" || configState.type === "enum")) {
7146
7252
  const boolValue = parseConfigBoolean(value);
7147
7253
  const hasBoolean = typeof boolValue === "boolean";
@@ -7970,7 +8076,7 @@ var plugin_default = defineNitroPlugin((nitroApp) => {
7970
8076
  const { serverSdkKey, baseUrl, connection } = runtimeConfig.configdirector;
7971
8077
  const client = new DefaultConfigDirectorClient(serverSdkKey, {
7972
8078
  sdkName: "nuxt-sdk",
7973
- sdkVersion: "1.5.0"
8079
+ sdkVersion: "1.6.0"
7974
8080
  }, {
7975
8081
  logger,
7976
8082
  connection: {
@@ -504,6 +504,11 @@ const getRequestedType = (defaultValue) => {
504
504
  const isNumericNativeType = (requestedType) => {
505
505
  return requestedType === "number" || requestedType === "bigint";
506
506
  };
507
+ const configTypesNotReadableAsString = new Set([
508
+ "boolean",
509
+ "integer",
510
+ "float"
511
+ ]);
507
512
  const parseConfigValue = (configState, defaultValue) => {
508
513
  const value = configState.value;
509
514
  const requestedType = getRequestedType(defaultValue);
@@ -515,13 +520,22 @@ const parseConfigValue = (configState, defaultValue) => {
515
520
  reason: "value-missing"
516
521
  };
517
522
  if (configState.type === "json") return parseJson(value, defaultValue, requestedType, configState.valueId);
518
- if (typeof defaultValue === "string") return {
519
- parsedValue: value,
520
- parsedValueId: configState.valueId,
521
- requestedType,
522
- usedDefault: false,
523
- reason: "found-match"
524
- };
523
+ if (typeof defaultValue === "string") {
524
+ if (configTypesNotReadableAsString.has(configState.type)) return {
525
+ parsedValue: defaultValue,
526
+ parsedValueId: void 0,
527
+ requestedType,
528
+ usedDefault: true,
529
+ reason: "type-mismatch"
530
+ };
531
+ return {
532
+ parsedValue: value,
533
+ parsedValueId: configState.valueId,
534
+ requestedType,
535
+ usedDefault: false,
536
+ reason: "found-match"
537
+ };
538
+ }
525
539
  if (typeof defaultValue === "boolean" && (configState.type === "boolean" || configState.type === "string" || configState.type === "enum")) {
526
540
  const boolValue = parseConfigBoolean(value);
527
541
  const hasBoolean = typeof boolValue === "boolean";
@@ -1463,7 +1477,7 @@ var plugin_client_default = defineNuxtPlugin((nuxtApp) => {
1463
1477
  const { clientSdkKey, appName, appVersion, baseUrl } = runtimeConfig.public.configdirector;
1464
1478
  const client = createBrowserClient(clientSdkKey, {
1465
1479
  sdkName: "nuxt-sdk",
1466
- sdkVersion: "1.5.0"
1480
+ sdkVersion: "1.6.0"
1467
1481
  }, {
1468
1482
  metadata: {
1469
1483
  appName,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@configdirector/nuxt-sdk",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Nuxt SDK for ConfigDirector. ConfigDirector is a remote configuration and feature flag service.",
5
5
  "license": "MIT",
6
6
  "author": "ConfigDirector",