showwhat 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,21 @@
1
1
  # showwhat
2
2
 
3
- Feature flags and config you own. Platform-agnostic.
3
+ Schema and rule engine for **feature flags** and **config** resolution
4
+ <br />Inspired by OpenAPI and Swagger
4
5
 
5
- `showwhat` combines a YAML/JSON definition format with a schema-validated, extensible rule engine. Define flags and config as variations with conditions, evaluate them in your app, and store definitions wherever you want.
6
+ ## Features
7
+
8
+ - Define flags and config as [definitions](https://showwhat.yeojz.dev/docs/definitions) and declare which [variation](https://showwhat.yeojz.dev/docs/variations) is served based on [conditions](https://showwhat.yeojz.dev/docs/conditions).
9
+ - TypeScript-first with Zod validation.
10
+ - Supports `booleans`, `strings`, `numbers`, `arrays` and `objects` as resolved variation values.
11
+ - Supports both `yaml` or `json`.
12
+ - Runtime evaluation against user defined [context](https://showwhat.yeojz.dev/docs/context).
13
+ - Supports [annotations](https://showwhat.yeojz.dev/docs/annotations) for condition chaining and cross-dependency.
14
+ - Ability to define [presets](https://showwhat.yeojz.dev/docs/presets.html) for condition reuse.
15
+ - Extensible with [custom conditions](https://showwhat.yeojz.dev/docs/custom-conditions).
16
+ - Store definitions in files and manage them in version control or serve them from an API.
17
+
18
+ A browser based schema [configurator](https://showwhat.yeojz.dev/configurator/) is also provided / available.
6
19
 
7
20
  ## Installation
8
21
 
@@ -18,9 +31,43 @@ deno install npm:showwhat
18
31
 
19
32
  ## Quick start
20
33
 
34
+ ### 1. Define your flags
35
+
36
+ Definitions can be YAML files or plain objects. Each definition has **variations** evaluated top-to-bottom — the first match wins.
37
+
38
+ ```yaml
39
+ # flags.yaml
40
+ definitions:
41
+ checkout_v2:
42
+ variations:
43
+ - value: true
44
+ conditions:
45
+ - type: env
46
+ value: prod
47
+ - value: false # default — no conditions means always matches
48
+
49
+ max_upload_size:
50
+ variations:
51
+ - value: 100
52
+ conditions:
53
+ - type: number
54
+ key: tier_level
55
+ op: gte
56
+ value: 2
57
+ - value: 25
58
+ ```
59
+
60
+ ### 2. Load definitions
61
+
62
+ Use `MemoryData` to load definitions from YAML strings or plain objects:
63
+
21
64
  ```ts
22
- import { MemoryData, showwhat } from "showwhat";
65
+ import { MemoryData } from "showwhat";
23
66
 
67
+ // From a YAML string
68
+ const data = await MemoryData.fromYaml(fs.readFileSync("flags.yaml", "utf8"));
69
+
70
+ // Or from a plain object
24
71
  const data = await MemoryData.fromObject({
25
72
  definitions: {
26
73
  checkout_v2: {
@@ -28,23 +75,95 @@ const data = await MemoryData.fromObject({
28
75
  },
29
76
  },
30
77
  });
78
+ ```
79
+
80
+ ### 3. Resolve flags
81
+
82
+ Pass a runtime **context** and the keys you want to resolve:
83
+
84
+ ```ts
85
+ import { showwhat } from "showwhat";
31
86
 
32
87
  const results = await showwhat({
33
- keys: ["checkout_v2"],
34
- context: { env: "prod" },
88
+ keys: ["checkout_v2", "max_upload_size"],
89
+ context: { env: "prod", tier_level: 3 },
35
90
  options: { data },
36
91
  });
37
- console.log(results.checkout_v2.value); // true
38
92
  ```
39
93
 
40
- ## Features
94
+ Omit `keys` to resolve all definitions at once.
95
+
96
+ ### 4. Use the results
97
+
98
+ Every result entry is either `{ success: true, value }` or `{ success: false, error }`:
99
+
100
+ ```ts
101
+ const flag = results["checkout_v2"];
102
+ if (flag.success) {
103
+ console.log(flag.value); // true
104
+ }
105
+
106
+ const upload = results["max_upload_size"];
107
+ if (upload.success) {
108
+ console.log(upload.value); // 100
109
+ }
110
+ ```
111
+
112
+ ### Built-in condition types
113
+
114
+ | Type | Description | Example |
115
+ | ---------- | ------------------------------------ | -------------------------------------------------------------------- |
116
+ | `env` | Shorthand for matching `context.env` | `{ type: env, value: prod }` |
117
+ | `string` | Compare any string key | `{ type: string, key: tier, op: eq, value: pro }` |
118
+ | `number` | Compare any numeric key | `{ type: number, key: level, op: gte, value: 2 }` |
119
+ | `bool` | Compare any boolean key | `{ type: bool, key: mobile, value: true }` |
120
+ | `datetime` | Compare any datetime key | `{ type: datetime, key: at, op: gt, value: "2025-01-01T00:00:00Z" }` |
121
+ | `startAt` | Passes when `context.at >= value` | `{ type: startAt, value: "2025-06-01T00:00:00Z" }` |
122
+ | `endAt` | Passes when `context.at < value` | `{ type: endAt, value: "2025-07-01T00:00:00Z" }` |
123
+ | `and` | All child conditions must pass | `{ type: and, conditions: [...] }` |
124
+ | `or` | Any child condition must pass | `{ type: or, conditions: [...] }` |
125
+
126
+ ### Presets
127
+
128
+ Presets define reusable condition shorthands to keep definitions DRY:
129
+
130
+ ```yaml
131
+ definitions:
132
+ premium_feature:
133
+ variations:
134
+ - value: true
135
+ conditions:
136
+ - type: tier
137
+ op: in
138
+ value: [pro, enterprise]
139
+ - value: false
140
+
141
+ presets:
142
+ tier:
143
+ type: string
144
+ key: tier
145
+ ```
41
146
 
42
- - `showwhat()` resolution engine for flags and config values
43
- - Built-in condition evaluators: `string`, `number`, `datetime`, `bool`, `env`, `startAt`, `endAt`
44
- - Custom evaluators via `registerEvaluators()`
45
- - YAML and JSON parsing with schema validation
46
- - Pluggable data sources (`DefinitionReader` / `DefinitionWriter`)
47
- - Typed error hierarchy
147
+ ### Custom conditions
148
+
149
+ Register your own evaluators to extend the built-in condition types:
150
+
151
+ ```ts
152
+ import { showwhat, registerEvaluators, MemoryData } from "showwhat";
153
+
154
+ const evaluators = registerEvaluators({
155
+ percentage: async ({ condition, context }) => {
156
+ const hash = someHash(context.userId);
157
+ return hash % 100 < condition.value;
158
+ },
159
+ });
160
+
161
+ const results = await showwhat({
162
+ keys: ["gradual_rollout"],
163
+ context: { userId: "user-123" },
164
+ options: { data, evaluators },
165
+ });
166
+ ```
48
167
 
49
168
  ## Documentation
50
169
 
@@ -52,7 +171,6 @@ console.log(results.checkout_v2.value); // true
52
171
  - [Conditions](https://showwhat.yeojz.dev/docs/conditions) — built-in condition types and usage
53
172
  - [Context](https://showwhat.yeojz.dev/docs/context) — runtime context object
54
173
  - [Definitions](https://showwhat.yeojz.dev/docs/definitions) — definition structure and variations
55
- - [Core API](https://showwhat.yeojz.dev/docs/core) — `showwhat()`, `resolve()`, parsing, and more
56
174
  - [Errors](https://showwhat.yeojz.dev/docs/errors) — error types and when they are thrown
57
175
  - [Custom Conditions](https://showwhat.yeojz.dev/docs/custom-conditions) — writing your own evaluators
58
176
  - [Custom Data Sources](https://showwhat.yeojz.dev/docs/custom-data-sources) — implementing `DefinitionReader`
package/dist/index.d.ts CHANGED
@@ -1,16 +1,21 @@
1
- import { Resolution, ResolutionError, ResolverOptions, DefinitionReader, ConditionEvaluators, BuiltinCondition, ContextValue, Context, Dependencies } from '@showwhat/core';
1
+ import { Resolution, ResolutionError, ResolverOptions, DefinitionReader, PresetReader, Presets, ConditionEvaluators, Context, Dependencies } from '@showwhat/core';
2
2
  export * from '@showwhat/core';
3
3
 
4
4
  type ShowWhatOptions = ResolverOptions & {
5
5
  data: DefinitionReader;
6
6
  };
7
7
  type Resolutions = Record<string, Resolution | ResolutionError>;
8
- declare function showwhat<T extends Record<string, ContextValue> = Record<string, ContextValue>, D extends Record<string, unknown> = Record<string, unknown>>({ keys, context, deps, options, }: {
8
+ declare function showwhat({ keys, context, deps, options, }: {
9
9
  keys?: string[];
10
- context: Context<T>;
11
- deps?: Dependencies<D>;
10
+ context: Context;
11
+ deps?: Dependencies;
12
12
  options: ShowWhatOptions;
13
13
  }): Promise<Resolutions>;
14
- declare function registerEvaluators<T extends string>(extra: ConditionEvaluators<T>): ConditionEvaluators<BuiltinCondition["type"] | T>;
14
+ declare function mergePresets({ key, presets, overrides, }: {
15
+ key?: string;
16
+ presets?: PresetReader;
17
+ overrides?: Presets;
18
+ }): Promise<Presets>;
19
+ declare function registerEvaluators(extra: ConditionEvaluators): ConditionEvaluators;
15
20
 
16
- export { type Resolutions, type ShowWhatOptions, registerEvaluators, showwhat };
21
+ export { type Resolutions, type ShowWhatOptions, mergePresets, registerEvaluators, showwhat };
package/dist/index.js CHANGED
@@ -50,7 +50,23 @@ async function showwhat({
50
50
  }
51
51
  });
52
52
  }
53
- var COMPOSITE_TYPES = /* @__PURE__ */ new Set(["and", "or"]);
53
+ async function mergePresets({
54
+ key,
55
+ presets,
56
+ overrides
57
+ }) {
58
+ if (!presets) {
59
+ return {
60
+ ...overrides
61
+ };
62
+ }
63
+ const base = key ? await presets.getPresets(key) : await presets.getPresets();
64
+ return {
65
+ ...base,
66
+ ...overrides
67
+ };
68
+ }
69
+ var COMPOSITE_TYPES = /* @__PURE__ */ new Set(["and", "or", "checkAnnotations"]);
54
70
  function registerEvaluators(extra) {
55
71
  for (const key of Object.keys(extra)) {
56
72
  if (COMPOSITE_TYPES.has(key)) {
@@ -60,6 +76,7 @@ function registerEvaluators(extra) {
60
76
  return { ...builtinEvaluators, ...extra };
61
77
  }
62
78
  export {
79
+ mergePresets,
63
80
  registerEvaluators,
64
81
  showwhat
65
82
  };
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {\n ContextSchema,\n builtinEvaluators,\n ValidationError,\n DataError,\n resolve,\n} from \"@showwhat/core\";\nimport type {\n BuiltinCondition,\n ConditionEvaluators,\n Context,\n ContextValue,\n Definitions,\n DefinitionReader,\n Dependencies,\n Resolution,\n ResolutionError,\n ResolverOptions,\n} from \"@showwhat/core\";\n\nexport * from \"@showwhat/core\";\n\nexport type ShowWhatOptions = ResolverOptions & {\n data: DefinitionReader;\n};\n\nexport type Resolutions = Record<string, Resolution | ResolutionError>;\n\nasync function fetchDefinitions(data: DefinitionReader, keys?: string[]): Promise<Definitions> {\n if (!keys) {\n try {\n return await data.getAll();\n } catch (err) {\n throw new DataError(\"Failed to fetch definitions\", err);\n }\n }\n\n const definitions = {} as Record<string, Definitions[string] | null>;\n await Promise.all(\n keys.map(async (key) => {\n try {\n definitions[key] = await data.get(key);\n } catch (err) {\n throw new DataError(`Failed to fetch definition \"${key}\"`, err);\n }\n }),\n );\n return definitions as Definitions;\n}\n\nexport async function showwhat<\n T extends Record<string, ContextValue> = Record<string, ContextValue>,\n D extends Record<string, unknown> = Record<string, unknown>,\n>({\n keys,\n context,\n deps,\n options,\n}: {\n keys?: string[];\n context: Context<T>;\n deps?: Dependencies<D>;\n options: ShowWhatOptions;\n}): Promise<Resolutions> {\n const contextResult = ContextSchema.safeParse(context);\n if (!contextResult.success) {\n throw new ValidationError(\n contextResult.error.issues.map((i) => `[${i.path.join(\".\")}] ${i.message}`).join(\"; \"),\n \"context\",\n );\n }\n\n return resolve({\n definitions: await fetchDefinitions(options.data, keys),\n context: contextResult.data as Context<T>,\n deps,\n options: {\n ...options,\n evaluators: options.evaluators ?? builtinEvaluators,\n },\n });\n}\n\nconst COMPOSITE_TYPES = new Set([\"and\", \"or\"]);\n\nexport function registerEvaluators<T extends string>(\n extra: ConditionEvaluators<T>,\n): ConditionEvaluators<BuiltinCondition[\"type\"] | T> {\n for (const key of Object.keys(extra)) {\n if (COMPOSITE_TYPES.has(key)) {\n throw new Error(`Cannot register reserved condition type \"${key}\"`);\n }\n }\n return { ...builtinEvaluators, ...extra };\n}\n"],"mappings":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAcP,cAAc;AAQd,eAAe,iBAAiB,MAAwB,MAAuC;AAC7F,MAAI,CAAC,MAAM;AACT,QAAI;AACF,aAAO,MAAM,KAAK,OAAO;AAAA,IAC3B,SAAS,KAAK;AACZ,YAAM,IAAI,UAAU,+BAA+B,GAAG;AAAA,IACxD;AAAA,EACF;AAEA,QAAM,cAAc,CAAC;AACrB,QAAM,QAAQ;AAAA,IACZ,KAAK,IAAI,OAAO,QAAQ;AACtB,UAAI;AACF,oBAAY,GAAG,IAAI,MAAM,KAAK,IAAI,GAAG;AAAA,MACvC,SAAS,KAAK;AACZ,cAAM,IAAI,UAAU,+BAA+B,GAAG,KAAK,GAAG;AAAA,MAChE;AAAA,IACF,CAAC;AAAA,EACH;AACA,SAAO;AACT;AAEA,eAAsB,SAGpB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAKyB;AACvB,QAAM,gBAAgB,cAAc,UAAU,OAAO;AACrD,MAAI,CAAC,cAAc,SAAS;AAC1B,UAAM,IAAI;AAAA,MACR,cAAc,MAAM,OAAO,IAAI,CAAC,MAAM,IAAI,EAAE,KAAK,KAAK,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,KAAK,IAAI;AAAA,MACrF;AAAA,IACF;AAAA,EACF;AAEA,SAAO,QAAQ;AAAA,IACb,aAAa,MAAM,iBAAiB,QAAQ,MAAM,IAAI;AAAA,IACtD,SAAS,cAAc;AAAA,IACvB;AAAA,IACA,SAAS;AAAA,MACP,GAAG;AAAA,MACH,YAAY,QAAQ,cAAc;AAAA,IACpC;AAAA,EACF,CAAC;AACH;AAEA,IAAM,kBAAkB,oBAAI,IAAI,CAAC,OAAO,IAAI,CAAC;AAEtC,SAAS,mBACd,OACmD;AACnD,aAAW,OAAO,OAAO,KAAK,KAAK,GAAG;AACpC,QAAI,gBAAgB,IAAI,GAAG,GAAG;AAC5B,YAAM,IAAI,MAAM,4CAA4C,GAAG,GAAG;AAAA,IACpE;AAAA,EACF;AACA,SAAO,EAAE,GAAG,mBAAmB,GAAG,MAAM;AAC1C;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {\n ContextSchema,\n builtinEvaluators,\n ValidationError,\n DataError,\n resolve,\n} from \"@showwhat/core\";\nimport type {\n ConditionEvaluators,\n Context,\n Definitions,\n DefinitionReader,\n Dependencies,\n Presets,\n PresetReader,\n Resolution,\n ResolutionError,\n ResolverOptions,\n} from \"@showwhat/core\";\n\nexport * from \"@showwhat/core\";\n\nexport type ShowWhatOptions = ResolverOptions & {\n data: DefinitionReader;\n};\n\nexport type Resolutions = Record<string, Resolution | ResolutionError>;\n\nasync function fetchDefinitions(data: DefinitionReader, keys?: string[]): Promise<Definitions> {\n if (!keys) {\n try {\n return await data.getAll();\n } catch (err) {\n throw new DataError(\"Failed to fetch definitions\", err);\n }\n }\n\n const definitions = {} as Record<string, Definitions[string] | null>;\n await Promise.all(\n keys.map(async (key) => {\n try {\n definitions[key] = await data.get(key);\n } catch (err) {\n throw new DataError(`Failed to fetch definition \"${key}\"`, err);\n }\n }),\n );\n return definitions as Definitions;\n}\n\nexport async function showwhat({\n keys,\n context,\n deps,\n options,\n}: {\n keys?: string[];\n context: Context;\n deps?: Dependencies;\n options: ShowWhatOptions;\n}): Promise<Resolutions> {\n const contextResult = ContextSchema.safeParse(context);\n if (!contextResult.success) {\n throw new ValidationError(\n contextResult.error.issues.map((i) => `[${i.path.join(\".\")}] ${i.message}`).join(\"; \"),\n \"context\",\n );\n }\n\n return resolve({\n definitions: await fetchDefinitions(options.data, keys),\n context: contextResult.data as Context,\n deps,\n options: {\n ...options,\n evaluators: options.evaluators ?? builtinEvaluators,\n },\n });\n}\n\nexport async function mergePresets({\n key,\n presets,\n overrides,\n}: {\n key?: string;\n presets?: PresetReader;\n overrides?: Presets;\n}): Promise<Presets> {\n if (!presets) {\n return {\n ...overrides,\n };\n }\n\n const base = key ? await presets.getPresets(key) : await presets.getPresets();\n\n return {\n ...base,\n ...overrides,\n };\n}\n\nconst COMPOSITE_TYPES = new Set([\"and\", \"or\", \"checkAnnotations\"]);\n\nexport function registerEvaluators(extra: ConditionEvaluators): ConditionEvaluators {\n for (const key of Object.keys(extra)) {\n if (COMPOSITE_TYPES.has(key)) {\n throw new Error(`Cannot register reserved condition type \"${key}\"`);\n }\n }\n return { ...builtinEvaluators, ...extra };\n}\n"],"mappings":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAcP,cAAc;AAQd,eAAe,iBAAiB,MAAwB,MAAuC;AAC7F,MAAI,CAAC,MAAM;AACT,QAAI;AACF,aAAO,MAAM,KAAK,OAAO;AAAA,IAC3B,SAAS,KAAK;AACZ,YAAM,IAAI,UAAU,+BAA+B,GAAG;AAAA,IACxD;AAAA,EACF;AAEA,QAAM,cAAc,CAAC;AACrB,QAAM,QAAQ;AAAA,IACZ,KAAK,IAAI,OAAO,QAAQ;AACtB,UAAI;AACF,oBAAY,GAAG,IAAI,MAAM,KAAK,IAAI,GAAG;AAAA,MACvC,SAAS,KAAK;AACZ,cAAM,IAAI,UAAU,+BAA+B,GAAG,KAAK,GAAG;AAAA,MAChE;AAAA,IACF,CAAC;AAAA,EACH;AACA,SAAO;AACT;AAEA,eAAsB,SAAS;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAKyB;AACvB,QAAM,gBAAgB,cAAc,UAAU,OAAO;AACrD,MAAI,CAAC,cAAc,SAAS;AAC1B,UAAM,IAAI;AAAA,MACR,cAAc,MAAM,OAAO,IAAI,CAAC,MAAM,IAAI,EAAE,KAAK,KAAK,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,KAAK,IAAI;AAAA,MACrF;AAAA,IACF;AAAA,EACF;AAEA,SAAO,QAAQ;AAAA,IACb,aAAa,MAAM,iBAAiB,QAAQ,MAAM,IAAI;AAAA,IACtD,SAAS,cAAc;AAAA,IACvB;AAAA,IACA,SAAS;AAAA,MACP,GAAG;AAAA,MACH,YAAY,QAAQ,cAAc;AAAA,IACpC;AAAA,EACF,CAAC;AACH;AAEA,eAAsB,aAAa;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AACF,GAIqB;AACnB,MAAI,CAAC,SAAS;AACZ,WAAO;AAAA,MACL,GAAG;AAAA,IACL;AAAA,EACF;AAEA,QAAM,OAAO,MAAM,MAAM,QAAQ,WAAW,GAAG,IAAI,MAAM,QAAQ,WAAW;AAE5E,SAAO;AAAA,IACL,GAAG;AAAA,IACH,GAAG;AAAA,EACL;AACF;AAEA,IAAM,kBAAkB,oBAAI,IAAI,CAAC,OAAO,MAAM,kBAAkB,CAAC;AAE1D,SAAS,mBAAmB,OAAiD;AAClF,aAAW,OAAO,OAAO,KAAK,KAAK,GAAG;AACpC,QAAI,gBAAgB,IAAI,GAAG,GAAG;AAC5B,YAAM,IAAI,MAAM,4CAA4C,GAAG,GAAG;AAAA,IACpE;AAAA,EACF;AACA,SAAO,EAAE,GAAG,mBAAmB,GAAG,MAAM;AAC1C;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "showwhat",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "A schema-based resolution engine for configuration and feature flags",
5
5
  "keywords": [
6
6
  "feature-flags",
@@ -41,14 +41,14 @@
41
41
  "dist"
42
42
  ],
43
43
  "dependencies": {
44
- "@showwhat/core": "2.0.0"
44
+ "@showwhat/core": "2.1.0"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/node": "^25.5.0",
48
- "@vitest/coverage-v8": "^4.1.2",
48
+ "@vitest/coverage-v8": "^4.1.4",
49
49
  "tsup": "^8.5.1",
50
50
  "typescript": "^5.9.3",
51
- "vitest": "^4.1.2"
51
+ "vitest": "^4.1.4"
52
52
  },
53
53
  "scripts": {
54
54
  "build": "tsup",