@happyvertical/smrt-features 0.30.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/AGENTS.md ADDED
@@ -0,0 +1,28 @@
1
+ # smrt-features
2
+
3
+ Code-first feature flag system with global, app-level, and tenant-hierarchy resolution.
4
+
5
+ ## Core pieces
6
+
7
+ - `FeatureDefinition` (`_smrt_feature_definitions` table) — code-owned definition mirrored to DB at registration
8
+ - `FeatureOverride` (`_smrt_feature_overrides` table) — runtime override state, scoped by `(featureKey, scopeType, scopeId)`
9
+ - `FeatureResolver` — resolves the effective state of a feature in a given context, walking the scope chain
10
+ - `FeatureSyncService` — keeps `_smrt_feature_definitions` in sync with code-registered defaults at boot
11
+
12
+ ## Resolution chain (priority high → low)
13
+
14
+ 1. User scope (`scopeType: 'user'`) — when context includes a userId
15
+ 2. Tenant scope (`scopeType: 'tenant'`) — walks up the tenant hierarchy when a `FeatureTenantHierarchyProvider` is configured (DI); otherwise resolves a flat tenant scope
16
+ 3. Global scope (`scopeType: 'global'`, `scopeId: GLOBAL_FEATURE_SCOPE_ID`)
17
+ 4. Definition default (registered in code)
18
+
19
+ ## Conventions
20
+
21
+ - Feature keys should be namespaced by package or domain, e.g. `commerce.invoice.draft-mode`, `content.editor.ai-suggestions`
22
+ - Definitions are code-owned. Don't write `FeatureDefinition` rows directly — use `FeatureSyncService.syncDefinitions()` at startup with the manifest of expected features
23
+ - Overrides are write-time validated against the matching definition (effect must be a known `FeatureOverrideEffect`, scope must be valid for the definition's `allowedScopes`)
24
+ - Tenant-hierarchy resolution is an optional integration wired by dependency injection (`FeatureTenantHierarchyProvider`), not a static dependency — `smrt-features` never imports `@happyvertical/smrt-users`. Without a configured provider, tenant-hierarchy resolution falls back to a flat tenant scope. (The consumer that wants hierarchy installs `smrt-users` itself and supplies the provider; `smrt-users` is a dev-only dependency here for tests.)
25
+
26
+ ## Integration with `@happyvertical/smrt-users`
27
+
28
+ When `smrt-users` is present, `FeatureResolver` uses it to walk the tenant hierarchy (parent → grandparent → root). Configure via `FeatureTenantHierarchyProvider`. Without it, tenant lookup is single-level.
package/CLAUDE.md ADDED
@@ -0,0 +1 @@
1
+ @AGENTS.md
package/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright <2025> <Happy Vertical Corporation>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # @happyvertical/smrt-features
2
+
3
+ Code-first feature flags for SMRT applications, with global, app-level, and tenant-hierarchy resolution.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @happyvertical/smrt-features
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```typescript
14
+ import {
15
+ FeatureDefinition,
16
+ FeatureResolver,
17
+ FeatureSyncService,
18
+ } from '@happyvertical/smrt-features';
19
+
20
+ // 1. Sync code-defined feature definitions at startup
21
+ await FeatureSyncService.syncDefinitions({
22
+ manifest: [
23
+ {
24
+ key: 'commerce.invoice.draft-mode',
25
+ description: 'Allow users to save invoices as drafts before sending',
26
+ defaultEffect: 'enabled',
27
+ allowedScopes: ['global', 'tenant', 'user'],
28
+ },
29
+ // ...
30
+ ],
31
+ });
32
+
33
+ // 2. Resolve at runtime
34
+ const enabled = await FeatureResolver.isEnabled('commerce.invoice.draft-mode', {
35
+ tenantId: currentTenantId,
36
+ userId: currentUserId,
37
+ });
38
+ ```
39
+
40
+ ## Resolution
41
+
42
+ Features resolve in this priority order:
43
+
44
+ 1. **User override** (most specific) — `FeatureOverride` row with `scopeType: 'user'` for the active user
45
+ 2. **Tenant override** — `FeatureOverride` row with `scopeType: 'tenant'`. Walks the tenant hierarchy if `@happyvertical/smrt-users` is installed.
46
+ 3. **Global override** — `FeatureOverride` row with `scopeType: 'global'`
47
+ 4. **Code default** — registered via `FeatureSyncService` from the manifest
48
+
49
+ The first match wins.
50
+
51
+ ## Storage
52
+
53
+ - `_smrt_feature_definitions` — code-owned feature shape (default effect, allowed scopes, metadata)
54
+ - `_smrt_feature_overrides` — runtime overrides at any scope level
55
+
56
+ ## Documentation
57
+
58
+ - See [`AGENTS.md`](./AGENTS.md) for package-internal patterns
59
+ - See [`docs/standards.md`](../../docs/standards.md) for monorepo conventions
60
+ - See [`docs/architecture/`](../../docs/architecture/) for cross-package architecture
@@ -0,0 +1,203 @@
1
+ import { SmartObjectManifest } from '@happyvertical/smrt-core/manifest';
2
+ import { SmrtClassOptions } from '@happyvertical/smrt-core';
3
+ import { SmrtCollection } from '@happyvertical/smrt-core';
4
+ import { SmrtObject } from '@happyvertical/smrt-core';
5
+
6
+ export declare function createFeatureKey(qualifiedClassName: string, localId: string): string;
7
+
8
+ export declare class FeatureDefinition extends SmrtObject {
9
+ featureKey: string;
10
+ packageName: string;
11
+ qualifiedClassName: string;
12
+ className: string;
13
+ localId: string;
14
+ defaultEnabled: boolean;
15
+ label: string;
16
+ description: string;
17
+ metadata: string;
18
+ visibility: string;
19
+ constructor(options?: FeatureDefinitionOptions);
20
+ getMetadata(): FeatureMetadata;
21
+ setMetadata(metadata: FeatureMetadata | null): void;
22
+ }
23
+
24
+ export declare class FeatureDefinitionCollection extends SmrtCollection<FeatureDefinition> {
25
+ static readonly _itemClass: typeof FeatureDefinition;
26
+ findByFeatureKey(featureKey: string): Promise<FeatureDefinition | null>;
27
+ findByPackageName(packageName: string): Promise<FeatureDefinition[]>;
28
+ upsertDefinition(seed: FeatureDefinitionSeed): Promise<{
29
+ definition: FeatureDefinition;
30
+ status: 'created' | 'updated' | 'unchanged';
31
+ }>;
32
+ }
33
+
34
+ export declare interface FeatureDefinitionOptions {
35
+ id?: string;
36
+ featureKey?: string;
37
+ packageName?: string;
38
+ qualifiedClassName?: string;
39
+ className?: string;
40
+ localId?: string;
41
+ defaultEnabled?: boolean;
42
+ label?: string;
43
+ description?: string;
44
+ metadata?: FeatureMetadata | string | null;
45
+ visibility?: string;
46
+ createdAt?: Date;
47
+ updatedAt?: Date;
48
+ }
49
+
50
+ export declare interface FeatureDefinitionSeed {
51
+ featureKey: string;
52
+ packageName: string;
53
+ qualifiedClassName: string;
54
+ className: string;
55
+ localId: string;
56
+ defaultEnabled: boolean;
57
+ label?: string;
58
+ description?: string;
59
+ metadata?: FeatureMetadata;
60
+ visibility?: string;
61
+ }
62
+
63
+ export declare interface FeatureMetadata {
64
+ [key: string]: unknown;
65
+ }
66
+
67
+ export declare class FeatureOverride extends SmrtObject {
68
+ featureKey: string;
69
+ scopeType: FeatureScopeType;
70
+ scopeId: string;
71
+ effect: FeatureOverrideEffect;
72
+ constructor(options?: FeatureOverrideOptions);
73
+ isInherit(): boolean;
74
+ isEnabled(): boolean;
75
+ isDisabled(): boolean;
76
+ }
77
+
78
+ export declare class FeatureOverrideCollection extends SmrtCollection<FeatureOverride> {
79
+ static readonly _itemClass: typeof FeatureOverride;
80
+ findByFeatureKey(featureKey: string): Promise<FeatureOverride[]>;
81
+ findByFeatureAndScope(featureKey: string, scopeType: FeatureScopeType, scopeId: string): Promise<FeatureOverride | null>;
82
+ getGlobalOverride(featureKey: string): Promise<FeatureOverride | null>;
83
+ getTenantOverride(featureKey: string, tenantId: string): Promise<FeatureOverride | null>;
84
+ getOverrideMap(featureKey: string, scopeType: FeatureScopeType, scopeIds: string[]): Promise<Map<string, FeatureOverride>>;
85
+ setOverride(featureKey: string, scopeType: FeatureScopeType, scopeId: string, effect: FeatureOverrideEffect): Promise<FeatureOverride>;
86
+ setGlobalOverride(featureKey: string, effect: FeatureOverrideEffect): Promise<FeatureOverride>;
87
+ setTenantOverride(featureKey: string, tenantId: string, effect: FeatureOverrideEffect): Promise<FeatureOverride>;
88
+ removeOverride(featureKey: string, scopeType: FeatureScopeType, scopeId: string): Promise<boolean>;
89
+ }
90
+
91
+ export declare enum FeatureOverrideEffect {
92
+ INHERIT = "inherit",
93
+ ENABLE = "enable",
94
+ DISABLE = "disable"
95
+ }
96
+
97
+ export declare interface FeatureOverrideOptions {
98
+ id?: string;
99
+ featureKey?: string;
100
+ scopeType?: FeatureScopeType;
101
+ scopeId?: string;
102
+ effect?: FeatureOverrideEffect;
103
+ createdAt?: Date;
104
+ updatedAt?: Date;
105
+ }
106
+
107
+ export declare interface FeatureResolutionContext {
108
+ tenantId?: string;
109
+ }
110
+
111
+ export declare class FeatureResolver {
112
+ private readonly options;
113
+ private readonly resolverOptions;
114
+ private featureDefinitions;
115
+ private featureOverrides;
116
+ private initializationPromise;
117
+ private tenantHierarchyPromise;
118
+ constructor(options?: SmrtClassOptions, resolverOptions?: FeatureResolverOptions);
119
+ isEnabled(featureKey: string, context?: FeatureResolutionContext): Promise<boolean>;
120
+ isEnabledFor(classOrInstance: object | (new (...args: any[]) => any), localId: string, context?: FeatureResolutionContext): Promise<boolean>;
121
+ private ensureInitialized;
122
+ private resolveBaseState;
123
+ private applyOverride;
124
+ private getTenantHierarchy;
125
+ }
126
+
127
+ export declare interface FeatureResolverOptions {
128
+ tenantHierarchyLoader?: FeatureTenantHierarchyLoader;
129
+ }
130
+
131
+ export declare type FeatureScopeType = 'global' | 'tenant';
132
+
133
+ export declare interface FeatureSyncResult {
134
+ total: number;
135
+ created: number;
136
+ updated: number;
137
+ unchanged: number;
138
+ deleted: number;
139
+ featureKeys: string[];
140
+ }
141
+
142
+ export declare class FeatureSyncService {
143
+ private readonly options;
144
+ private featureDefinitions;
145
+ private initializationPromise;
146
+ constructor(options?: SmrtClassOptions);
147
+ syncDefinitions(options?: SyncDefinitionsOptions): Promise<FeatureSyncResult>;
148
+ syncManifest(manifest: SmartObjectManifest, options?: SyncManifestOptions): Promise<FeatureSyncResult>;
149
+ private ensureInitialized;
150
+ private collectDefinitionsFromRegistrations;
151
+ private collectTouchedPackagesFromRegistrations;
152
+ private collectRegistrations;
153
+ private applyDefinitions;
154
+ }
155
+
156
+ export declare type FeatureTenantHierarchyLoader = (options: SmrtClassOptions) => Promise<FeatureTenantHierarchyProvider | null>;
157
+
158
+ export declare interface FeatureTenantHierarchyProvider {
159
+ getChain(tenantId: string): Promise<FeatureTenantNode[]>;
160
+ }
161
+
162
+ export declare interface FeatureTenantNode {
163
+ id: string;
164
+ inheritPermissions: boolean;
165
+ cascadePermissions: boolean;
166
+ }
167
+
168
+ export declare interface FeatureUsersModule {
169
+ TenantCollection: {
170
+ create(options: SmrtClassOptions): Promise<{
171
+ get(criteria: {
172
+ id: string;
173
+ }): Promise<any>;
174
+ getAncestorsFromRoot(tenantId: string): Promise<any[]>;
175
+ }>;
176
+ };
177
+ }
178
+
179
+ export declare function findFeatureDefaultInRegistry(featureKey: string): boolean | undefined;
180
+
181
+ export declare const GLOBAL_FEATURE_SCOPE_ID = "*";
182
+
183
+ export declare function parseFeatureKey(featureKey: string): {
184
+ qualifiedClassName: string;
185
+ localId: string;
186
+ };
187
+
188
+ export declare function resolveFeatureKeyForTarget(classOrInstance: object | (new (...args: any[]) => any), localId: string): {
189
+ featureKey: string;
190
+ defaultEnabled: boolean;
191
+ };
192
+
193
+ export declare interface SyncDefinitionsOptions {
194
+ classNames?: string[];
195
+ constructors?: Array<new (...args: any[]) => any>;
196
+ pruneStale?: boolean;
197
+ }
198
+
199
+ export declare interface SyncManifestOptions {
200
+ pruneStale?: boolean;
201
+ }
202
+
203
+ export { }