Skip to content

Commit

Permalink
feat: make variable key types overridable via module declaration (#1002)
Browse files Browse the repository at this point in the history
Co-authored-by: SimeonC <[email protected]>
  • Loading branch information
ajwootto and SimeonC authored Dec 2, 2024
1 parent 38ba179 commit 5ea2ba0
Show file tree
Hide file tree
Showing 17 changed files with 157 additions and 76 deletions.
1 change: 1 addition & 0 deletions lib/shared/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export * from './types/config/models'
export * from './utils'
export * from './types/ConfigSource'
export * from './types/UserError'
export * from './types/variableKeys'
46 changes: 46 additions & 0 deletions lib/shared/types/src/types/variableKeys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { VariableTypeAlias, VariableValue } from './config/models'

/**
* Used to support strong typing of variable keys in the SDK.
* Usage;
* ```ts
* import '@devcycle/types';
* declare module '@devcycle/types' {
* interface CustomVariableDefinitions {
* 'flag-one': boolean;
* }
* }
* ```
* Or when using the cli generated types;
* ```ts
* import '@devcycle/types';
* declare module '@devcycle/types' {
* interface CustomVariableDefinitions extends DVCVariableTypes {}
* }
* ```
*/
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface CustomVariableDefinitions {}
type DynamicBaseVariableDefinitions =
keyof CustomVariableDefinitions extends never
? {
[key: string]: VariableValue
}
: CustomVariableDefinitions
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface VariableDefinitions extends DynamicBaseVariableDefinitions {}
export type VariableKey = string & keyof VariableDefinitions

// type that determines whether the CustomVariableDefinitions interface has any keys defined, meaning
// that we're using custom variable types
export type CustomVariablesDefined =
keyof CustomVariableDefinitions extends never ? false : true

// type helper which turns a default value type into the type defined in custom variable types, if those exist
// otherwise run it through VariableTypeAlias
export type InferredVariableType<
K extends VariableKey,
DefaultValue extends VariableDefinitions[K],
> = CustomVariablesDefined extends true
? VariableDefinitions[K]
: VariableTypeAlias<DefaultValue>
9 changes: 3 additions & 6 deletions sdk/js-cloud-server/src/cloudClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ import {
DevCycleServerSDKOptions,
DVCLogger,
getVariableTypeFromValue,
InferredVariableType,
VariableDefinitions,
VariableTypeAlias,
type VariableValue,
} from '@devcycle/types'
import {
getAllFeatures,
Expand Down Expand Up @@ -68,10 +69,6 @@ const throwIfUserError = (err: unknown) => {
throw err
}

export interface VariableDefinitions {
[key: string]: VariableValue
}

export class DevCycleCloudClient<
Variables extends VariableDefinitions = VariableDefinitions,
> {
Expand Down Expand Up @@ -163,7 +160,7 @@ export class DevCycleCloudClient<
user: DevCycleUser,
key: K,
defaultValue: T,
): Promise<VariableTypeAlias<T>> {
): Promise<InferredVariableType<K, T>> {
return (await this.variable(user, key, defaultValue)).value
}

Expand Down
21 changes: 15 additions & 6 deletions sdk/js-cloud-server/src/models/variable.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { VariableType, VariableTypeAlias } from '@devcycle/types'
import {
InferredVariableType,
VariableKey,
VariableType,
VariableTypeAlias,
} from '@devcycle/types'
import { DVCVariableInterface, DVCVariableValue } from '../types'
import {
checkParamDefined,
Expand All @@ -14,11 +19,13 @@ export type VariableParam<T extends DVCVariableValue> = {
evalReason?: unknown
}

export class DVCVariable<T extends DVCVariableValue>
implements DVCVariableInterface
export class DVCVariable<
T extends DVCVariableValue,
K extends VariableKey = VariableKey,
> implements DVCVariableInterface
{
key: string
value: VariableTypeAlias<T>
key: K
value: InferredVariableType<K, T>
readonly defaultValue: T
readonly isDefaulted: boolean
readonly type: 'String' | 'Number' | 'Boolean' | 'JSON'
Expand All @@ -29,7 +36,9 @@ export class DVCVariable<T extends DVCVariableValue>
checkParamDefined('key', key)
checkParamDefined('defaultValue', defaultValue)
checkParamType('key', key, typeEnum.string)
this.key = key.toLowerCase()
// kind of cheating here with the type assertion but we're basically assuming that all variable keys in
// generated types are lowercase since the system enforces that elsewhere
this.key = key.toLowerCase() as K
this.isDefaulted = value === undefined || value === null
this.value =
value === undefined || value === null
Expand Down
8 changes: 6 additions & 2 deletions sdk/js/src/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
DevCycleUser,
ErrorCallback,
DVCFeature,
VariableDefinitions,
UserError,
} from './types'

Expand All @@ -20,7 +19,12 @@ import { DVCPopulatedUser } from './User'
import { EventQueue, EventTypes } from './EventQueue'
import { checkParamDefined } from './utils'
import { EventEmitter } from './EventEmitter'
import type { BucketedUserConfig, VariableTypeAlias } from '@devcycle/types'
import type {
BucketedUserConfig,
InferredVariableType,
VariableDefinitions,
VariableTypeAlias,
} from '@devcycle/types'
import { getVariableTypeFromValue } from '@devcycle/types'
import { ConfigRequestConsolidator } from './ConfigRequestConsolidator'
import { dvcDefaultLogger } from './logger'
Expand Down
8 changes: 2 additions & 6 deletions sdk/js/src/EventQueue.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import { DevCycleClient } from './Client'
import {
DevCycleEvent,
DevCycleOptions,
VariableDefinitions,
DVCCustomDataJSON,
} from './types'
import { DevCycleEvent, DevCycleOptions, DVCCustomDataJSON } from './types'
import { publishEvents } from './Request'
import { checkParamDefined } from './utils'
import chunk from 'lodash/chunk'
import { VariableDefinitions } from '@devcycle/types'

export const EventTypes = {
variableEvaluated: 'variableEvaluated',
Expand Down
4 changes: 3 additions & 1 deletion sdk/js/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {
DevCycleEvent,
DevCycleOptions,
DevCycleUser,
VariableDefinitions,
UserError,
DVCCustomDataJSON,
} from './types'
Expand All @@ -15,6 +14,9 @@ import { checkIsServiceWorker } from './utils'
export * from './types'
export { dvcDefaultLogger } from './logger'

import { VariableDefinitions } from '@devcycle/types'
export { VariableDefinitions }

/**
* @deprecated Use DevCycleClient instead
*/
Expand Down
15 changes: 8 additions & 7 deletions sdk/js/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import type {
DevCycleJSON,
DVCCustomDataJSON,
BucketedUserConfig,
VariableKey,
InferredVariableType,
} from '@devcycle/types'
export { UserError } from '@devcycle/types'

Expand Down Expand Up @@ -193,21 +195,20 @@ export interface DevCycleUser<T extends DVCCustomDataJSON = DVCCustomDataJSON> {
privateCustomData?: T
}

export interface VariableDefinitions {
[key: string]: VariableValue
}

export interface DVCVariable<T extends DVCVariableValue> {
export interface DVCVariable<
T extends DVCVariableValue,
K extends VariableKey = VariableKey,
> {
/**
* Unique "key" by Project to use for this Dynamic Variable.
*/
readonly key: string
readonly key: VariableKey

/**
* The value for this Dynamic Variable which will be set to the `defaultValue`
* if accessed before the SDK is fully Initialized
*/
readonly value: VariableTypeAlias<T>
readonly value: InferredVariableType<K, T>

/**
* Default value set when creating the variable
Expand Down
20 changes: 10 additions & 10 deletions sdk/nestjs/src/DevCycleModule/DevCycleService.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Injectable } from '@nestjs/common'
import { DevCycleClient, DevCycleUser } from '@devcycle/nodejs-server-sdk'
import {
DVCVariableValue,
DevCycleClient,
DevCycleUser,
} from '@devcycle/nodejs-server-sdk'
import { VariableTypeAlias } from '@devcycle/types'
InferredVariableType,
VariableDefinitions,
VariableKey,
} from '@devcycle/types'
import { ClsService } from 'nestjs-cls'

@Injectable()
Expand All @@ -18,14 +18,14 @@ export class DevCycleService {
return this.cls.get('dvc_user')
}

isEnabled(key: string): boolean {
isEnabled(key: VariableKey): boolean {
return this.devcycleClient.variableValue(this.getUser(), key, false)
}

variableValue<T extends DVCVariableValue>(
key: string,
defaultValue: T,
): VariableTypeAlias<T> {
variableValue<
K extends VariableKey,
ValueType extends VariableDefinitions[K],
>(key: K, defaultValue: ValueType): InferredVariableType<K, ValueType> {
return this.devcycleClient.variableValue(
this.getUser(),
key,
Expand Down
30 changes: 20 additions & 10 deletions sdk/nextjs/src/client/useVariableValue.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
'use client'
import { DevCycleClient, DVCVariableValue } from '@devcycle/js-client-sdk'
import { DevCycleClient } from '@devcycle/js-client-sdk'
import { useContext, use } from 'react'
import { VariableTypeAlias } from '@devcycle/types'
import {
InferredVariableType,
VariableDefinitions,
VariableKey,
} from '@devcycle/types'
import { DVCVariable } from '@devcycle/js-client-sdk'
import { DevCycleProviderContext } from './internal/context'
import { useRerenderOnVariableChange } from './internal/useRerenderOnVariableChange'

export const useVariable = <T extends DVCVariableValue>(
key: string,
defaultValue: T,
): DVCVariable<T> => {
export const useVariable = <
K extends VariableKey,
ValueType extends VariableDefinitions[K],
>(
key: K,
defaultValue: ValueType,
): DVCVariable<ValueType> => {
const context = useContext(DevCycleProviderContext)
useRerenderOnVariableChange(key)

Expand All @@ -21,10 +28,13 @@ export const useVariable = <T extends DVCVariableValue>(
return context.client.variable(key, defaultValue)
}

export const useVariableValue = <T extends DVCVariableValue>(
key: string,
defaultValue: T,
): VariableTypeAlias<T> => {
export const useVariableValue = <
K extends VariableKey,
ValueType extends VariableDefinitions[K],
>(
key: K,
defaultValue: ValueType,
): InferredVariableType<K, ValueType> => {
return useVariable(key, defaultValue).value
}

Expand Down
21 changes: 14 additions & 7 deletions sdk/nextjs/src/server/getVariableValue.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import { getClient } from './requestContext'
import { DVCVariableValue } from '@devcycle/js-client-sdk'
import { VariableTypeAlias } from '@devcycle/types'
import {
InferredVariableType,
VariableDefinitions,
VariableKey,
VariableTypeAlias,
} from '@devcycle/types'

export async function getVariableValue<T extends DVCVariableValue>(
key: string,
defaultValue: T,
): Promise<VariableTypeAlias<T>> {
export async function getVariableValue<
K extends VariableKey,
ValueType extends VariableDefinitions[K],
>(
key: K,
defaultValue: ValueType,
): Promise<InferredVariableType<K, ValueType>> {
const client = getClient()
if (!client) {
console.error(
'React cache API is not working as expected. Please contact DevCycle support.',
)
return defaultValue as VariableTypeAlias<T>
return defaultValue as VariableTypeAlias<ValueType>
}

const variable = client.variable(key, defaultValue)
Expand Down
9 changes: 3 additions & 6 deletions sdk/nodejs/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ import {
DVCLogger,
getVariableTypeFromValue,
VariableTypeAlias,
type VariableValue,
UserError,
VariableDefinitions,
InferredVariableType,
} from '@devcycle/types'
import os from 'os'
import {
Expand Down Expand Up @@ -56,10 +57,6 @@ type DevCycleProviderConstructor =
typeof import('./open-feature/DevCycleProvider').DevCycleProvider
type DevCycleProvider = InstanceType<DevCycleProviderConstructor>

export interface VariableDefinitions {
[key: string]: VariableValue
}

export class DevCycleClient<
Variables extends VariableDefinitions = VariableDefinitions,
> {
Expand Down Expand Up @@ -290,7 +287,7 @@ export class DevCycleClient<
variableValue<
K extends string & keyof Variables,
T extends DVCVariableValue & Variables[K],
>(user: DevCycleUser, key: K, defaultValue: T): VariableTypeAlias<T> {
>(user: DevCycleUser, key: K, defaultValue: T): InferredVariableType<K, T> {
return this.variable(user, key, defaultValue).value
}

Expand Down
3 changes: 1 addition & 2 deletions sdk/nodejs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ import {
DVCFeatureSet,
DevCyclePlatformDetails,
} from '@devcycle/js-cloud-server-sdk'
import { VariableDefinitions } from '@devcycle/js-client-sdk'
import { DevCycleServerSDKOptions } from '@devcycle/types'
import { DevCycleServerSDKOptions, VariableDefinitions } from '@devcycle/types'
import { getNodeJSPlatformDetails } from './utils/platformDetails'

// Dynamically import the OpenFeature Provider, as it's an optional peer dependency
Expand Down
3 changes: 2 additions & 1 deletion sdk/react/src/RenderIfEnabled.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import useVariableValue from './useVariableValue'
import { DVCVariableValue } from '@devcycle/js-client-sdk'
import { useContext } from 'react'
import { debugContext } from './context'
import { VariableKey } from '@devcycle/types'

type CommonProps = {
children: React.ReactNode
variableKey: string
variableKey: VariableKey
}

type RenderIfEnabledProps<T extends DVCVariableValue> =
Expand Down
3 changes: 2 additions & 1 deletion sdk/react/src/SwapComponents.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { ComponentProps, ComponentType } from 'react'
import type { VariableKey } from '@devcycle/types'
import useVariableValue from './useVariableValue'

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const SwapComponents = <T extends ComponentType<any>>(
OldComponent: T,
NewComponent: T,
variableKey: string,
variableKey: VariableKey,
) => {
const DevCycleConditionalComponent = (
props: ComponentProps<T>,
Expand Down
Loading

0 comments on commit 5ea2ba0

Please sign in to comment.