showwhat 1.0.1 → 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 +133 -15
- package/dist/index.d.ts +13 -6
- package/dist/index.js +47 -15
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
# showwhat
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Schema and rule engine for **feature flags** and **config** resolution
|
|
4
|
+
<br />Inspired by OpenAPI and Swagger
|
|
5
|
+
|
|
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.
|
|
4
19
|
|
|
5
20
|
## Installation
|
|
6
21
|
|
|
@@ -16,9 +31,43 @@ deno install npm:showwhat
|
|
|
16
31
|
|
|
17
32
|
## Quick start
|
|
18
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
|
+
|
|
19
64
|
```ts
|
|
20
|
-
import { MemoryData
|
|
65
|
+
import { MemoryData } from "showwhat";
|
|
66
|
+
|
|
67
|
+
// From a YAML string
|
|
68
|
+
const data = await MemoryData.fromYaml(fs.readFileSync("flags.yaml", "utf8"));
|
|
21
69
|
|
|
70
|
+
// Or from a plain object
|
|
22
71
|
const data = await MemoryData.fromObject({
|
|
23
72
|
definitions: {
|
|
24
73
|
checkout_v2: {
|
|
@@ -26,25 +75,95 @@ const data = await MemoryData.fromObject({
|
|
|
26
75
|
},
|
|
27
76
|
},
|
|
28
77
|
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 3. Resolve flags
|
|
81
|
+
|
|
82
|
+
Pass a runtime **context** and the keys you want to resolve:
|
|
29
83
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
84
|
+
```ts
|
|
85
|
+
import { showwhat } from "showwhat";
|
|
86
|
+
|
|
87
|
+
const results = await showwhat({
|
|
88
|
+
keys: ["checkout_v2", "max_upload_size"],
|
|
89
|
+
context: { env: "prod", tier_level: 3 },
|
|
33
90
|
options: { data },
|
|
34
91
|
});
|
|
35
|
-
console.log(result.value); // true
|
|
36
92
|
```
|
|
37
93
|
|
|
38
|
-
|
|
94
|
+
Omit `keys` to resolve all definitions at once.
|
|
39
95
|
|
|
40
|
-
|
|
41
|
-
- Built-in condition evaluators: `string`, `number`, `datetime`, `bool`, `env`, `startAt`, `endAt`
|
|
42
|
-
- Custom evaluators via `registerEvaluators()`
|
|
43
|
-
- YAML and JSON parsing with schema validation
|
|
44
|
-
- Pluggable data sources (`DefinitionReader` / `DefinitionWriter`)
|
|
45
|
-
- Typed error hierarchy
|
|
96
|
+
### 4. Use the results
|
|
46
97
|
|
|
47
|
-
|
|
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
|
+
```
|
|
146
|
+
|
|
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 @@ This package re-exports everything from `@showwhat/core`.
|
|
|
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,14 +1,21 @@
|
|
|
1
|
-
import { ResolverOptions, DefinitionReader,
|
|
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
|
-
|
|
8
|
-
|
|
7
|
+
type Resolutions = Record<string, Resolution | ResolutionError>;
|
|
8
|
+
declare function showwhat({ keys, context, deps, options, }: {
|
|
9
|
+
keys?: string[];
|
|
9
10
|
context: Context;
|
|
11
|
+
deps?: Dependencies;
|
|
10
12
|
options: ShowWhatOptions;
|
|
11
|
-
}): Promise<
|
|
12
|
-
declare function
|
|
13
|
+
}): Promise<Resolutions>;
|
|
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;
|
|
13
20
|
|
|
14
|
-
export { type ShowWhatOptions, registerEvaluators, showwhat };
|
|
21
|
+
export { type Resolutions, type ShowWhatOptions, mergePresets, registerEvaluators, showwhat };
|
package/dist/index.js
CHANGED
|
@@ -2,14 +2,35 @@
|
|
|
2
2
|
import {
|
|
3
3
|
ContextSchema,
|
|
4
4
|
builtinEvaluators,
|
|
5
|
-
DefinitionNotFoundError,
|
|
6
5
|
ValidationError,
|
|
6
|
+
DataError,
|
|
7
7
|
resolve
|
|
8
8
|
} from "@showwhat/core";
|
|
9
9
|
export * from "@showwhat/core";
|
|
10
|
+
async function fetchDefinitions(data, keys) {
|
|
11
|
+
if (!keys) {
|
|
12
|
+
try {
|
|
13
|
+
return await data.getAll();
|
|
14
|
+
} catch (err) {
|
|
15
|
+
throw new DataError("Failed to fetch definitions", err);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const definitions = {};
|
|
19
|
+
await Promise.all(
|
|
20
|
+
keys.map(async (key) => {
|
|
21
|
+
try {
|
|
22
|
+
definitions[key] = await data.get(key);
|
|
23
|
+
} catch (err) {
|
|
24
|
+
throw new DataError(`Failed to fetch definition "${key}"`, err);
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
);
|
|
28
|
+
return definitions;
|
|
29
|
+
}
|
|
10
30
|
async function showwhat({
|
|
11
|
-
|
|
31
|
+
keys,
|
|
12
32
|
context,
|
|
33
|
+
deps,
|
|
13
34
|
options
|
|
14
35
|
}) {
|
|
15
36
|
const contextResult = ContextSchema.safeParse(context);
|
|
@@ -19,23 +40,33 @@ async function showwhat({
|
|
|
19
40
|
"context"
|
|
20
41
|
);
|
|
21
42
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
const result = await resolve({
|
|
28
|
-
definitions: { [key]: def },
|
|
29
|
-
context: validatedContext,
|
|
43
|
+
return resolve({
|
|
44
|
+
definitions: await fetchDefinitions(options.data, keys),
|
|
45
|
+
context: contextResult.data,
|
|
46
|
+
deps,
|
|
30
47
|
options: {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
logger: options.logger
|
|
48
|
+
...options,
|
|
49
|
+
evaluators: options.evaluators ?? builtinEvaluators
|
|
34
50
|
}
|
|
35
51
|
});
|
|
36
|
-
return result[key];
|
|
37
52
|
}
|
|
38
|
-
|
|
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"]);
|
|
39
70
|
function registerEvaluators(extra) {
|
|
40
71
|
for (const key of Object.keys(extra)) {
|
|
41
72
|
if (COMPOSITE_TYPES.has(key)) {
|
|
@@ -45,6 +76,7 @@ function registerEvaluators(extra) {
|
|
|
45
76
|
return { ...builtinEvaluators, ...extra };
|
|
46
77
|
}
|
|
47
78
|
export {
|
|
79
|
+
mergePresets,
|
|
48
80
|
registerEvaluators,
|
|
49
81
|
showwhat
|
|
50
82
|
};
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {\n ContextSchema,\n builtinEvaluators,\n
|
|
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": "1.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": "1.0
|
|
44
|
+
"@showwhat/core": "2.1.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@types/node": "^25.
|
|
48
|
-
"@vitest/coverage-v8": "^4.
|
|
49
|
-
"tsup": "^8.
|
|
50
|
-
"typescript": "^5.
|
|
51
|
-
"vitest": "^4.
|
|
47
|
+
"@types/node": "^25.5.0",
|
|
48
|
+
"@vitest/coverage-v8": "^4.1.4",
|
|
49
|
+
"tsup": "^8.5.1",
|
|
50
|
+
"typescript": "^5.9.3",
|
|
51
|
+
"vitest": "^4.1.4"
|
|
52
52
|
},
|
|
53
53
|
"scripts": {
|
|
54
54
|
"build": "tsup",
|