Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Site Reliability Engineer Assessment Submission #111

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions backend/Notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@

# Multivariate Feature Flag functionality

## Supported types
- boolean
- string
- number
- JSONArray
- JSONObject

## Data Shape

There are two places within the database and schema where these datatypes are saved.

In the database, these values are saved using a jsonb field and are located within UserFeatureFlagAssociation.value and FeatureFlag.possibleValues.

In the graphQL schema, the shape of these types are defined with FeatureFlagValue and FeatureFlagPossibleValues.

Within the database, the given fields follow the following format:

UserFeatureFlagAssociation.value
{
"type": String,
"value": String
}

FeatureFlag.possibleValues
{
"type": String,
"values": String[]
}

## Note on selected data shape

This format allows for type consistency while also keeping the application as lightweight as possible. The downside to this however is that data is always saved as strings opposed to target data type. This creates additional work for the client to serialize and parse these types. An alternative appraoch that might be better in larger applications is to create a custom scalar type in graphQL which is effectively a union between all desired types




# Data entry from Client
This example is for FeatureFlagPossibleValues. FeatureFlagValue is effectively the same but with a single string value instead of an array

This example shows the shape of the data that would need to be entered in a create function, as well as the shape of the data that will be returned in the userAccounts query function

## boolean
User passes:

{
"type": "boolean"
}

Within a createFeatureFlag mutation, the value property here would be unnessesary as the values will automatically be set to "true" and "false" when "type" = "boolean"

## string
User passes:

{
"type": "string"
"values": "string_value"
}

Here the "string_value" can be any valid string. This value can be copmared to a string value that users have to determine the state of the feature flag

## number
User passes:

{
"type": "number"
"values": "number_value"
}

Here the "number_value" can be any valid number. This value can be copmared to a number value that users have to determine the state of the feature flag

## JSONArray
User passes:

{
"type": "JSONArray"
"values": "array"
}

Here the "array" can be any valid array. The values of the array will be parsed as strings. The users value (which will also be parsed as a string) can be compared to the various values of the array to determine the state of the feature flag.

## JSONObject
User passes:

{
"type": "JSONObject"
"values": "object"
}

Here the "object" can be any valid JSON object. The fields of the object can be compared to the fields of the users "value" JSON object
5 changes: 5 additions & 0 deletions backend/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
6 changes: 5 additions & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"build": "tsc --project ./",
"lint": "eslint --fix src . && prettier --write src . && tsc --noEmit && format-graphql --write true src/schema/schema.graphql",
"generate-types": "graphql-codegen",
"migrate": "ts-node ./src/bin/migrate.ts"
"migrate": "ts-node ./src/bin/migrate.ts",
"test": "jest"
},
"devDependencies": {
"@graphql-codegen/cli": "1.21.2",
Expand All @@ -24,6 +25,7 @@
"eslint-config-prettier": "7.2.0",
"eslint-plugin-import": "2.22.1",
"format-graphql": "1.4.0",
"jest": "^29.6.3",
"lint-staged": "10.5.4",
"nyc": "15.1.0",
"prettier": "2.2.1",
Expand All @@ -32,6 +34,7 @@
},
"dependencies": {
"@roarr/cli": "^3.2.2",
"@types/jest": "^29.5.3",
"apollo-server-fastify": "3.6.3",
"fast-safe-stringify": "^2.1.1",
"fastify": "^3.14.0",
Expand All @@ -44,6 +47,7 @@
"slonik": "^23.5.1",
"slonik-interceptor-field-name-transformation": "^1.5.3",
"slonik-interceptor-preset": "^1.2.10",
"ts-jest": "^29.1.1",
"ts-node": "^9.1.1"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ CREATE TABLE user_account (
id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
given_name text NOT NULL,
family_name text NOT NULL,
email_address text NOT NULL,
email_address text NOT NULL UNIQUE,
created_at timestamp with time zone DEFAULT NOW(),
updated_at timestamp with time zone DEFAULT NOW()
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
CREATE TABLE feature_flag (
id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
name text NOT NULL UNIQUE,
possible_values jsonb NOT NULL
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
CREATE TABLE user_feature_flag_association (
user_id integer REFERENCES user_account(id) ON DELETE CASCADE,
feature_flag_id integer REFERENCES feature_flag(id) ON DELETE CASCADE,
value jsonb NOT NULL,
PRIMARY KEY(user_id, feature_flag_id)
);
Original file line number Diff line number Diff line change
@@ -1 +1 @@
raise 'down migration not implemented'
drop table user_account
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
raise 'down migration not implemented'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
raise 'down migration not implemented'
152 changes: 143 additions & 9 deletions backend/src/generated/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,91 @@ export type Maybe<T> = T | null | undefined;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
export type RequireFields<T, K extends keyof T> = { [X in Exclude<keyof T, K>]?: T[X] } & { [P in K]-?: NonNullable<T[P]> };
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
ID: string;
ID: string | number;
String: string;
Boolean: boolean;
Int: number;
Float: number;
};

export type Mutation = {
__typename?: 'Mutation';
sampleMutation: Scalars['String'];
export enum Feature_Flag_Value_Types {
Boolean = 'BOOLEAN',
String = 'STRING',
Number = 'NUMBER',
JsonArray = 'JSON_ARRAY',
JsonObject = 'JSON_OBJECT'
}

export type FeatureFlagPossibleValues = {
__typename?: 'FeatureFlagPossibleValues';
type: Feature_Flag_Value_Types;
values: Array<Scalars['String']>;
};

export type FeatureFlagValue = {
__typename?: 'FeatureFlagValue';
type: Feature_Flag_Value_Types;
value: Scalars['String'];
};

export type FeatureFlag = {
__typename?: 'FeatureFlag';
id: Scalars['ID'];
name: Scalars['String'];
possibleValues: FeatureFlagPossibleValues;
userAssociations?: Maybe<Array<UserFeatureFlagAssociation>>;
};

export type UserAccount = {
__typename?: 'UserAccount';
id: Scalars['ID'];
givenName: Scalars['String'];
familyName: Scalars['String'];
emailAddress: Scalars['String'];
featureFlagAssociations?: Maybe<Array<UserFeatureFlagAssociation>>;
};

export type UserFeatureFlagAssociation = {
__typename?: 'UserFeatureFlagAssociation';
user: UserAccount;
featureFlag: FeatureFlag;
value: FeatureFlagValue;
};

export type Query = {
__typename?: 'Query';
hello: Scalars['String'];
userAccounts?: Maybe<Array<Maybe<UserAccount>>>;
};

export type AssignUsersToFeatureFlagReturnType = {
__typename?: 'AssignUsersToFeatureFlagReturnType';
userId: Scalars['ID'];
value: Scalars['Boolean'];
};

export type Mutation = {
__typename?: 'Mutation';
sampleMutation: Scalars['String'];
assignUsersToFeatureFlag: Array<Maybe<AssignUsersToFeatureFlagReturnType>>;
updateUserFeatureFlagValue?: Maybe<Scalars['Boolean']>;
};


export type MutationAssignUsersToFeatureFlagArgs = {
userAccountIds: Array<Scalars['ID']>;
featureFlagId: Scalars['ID'];
value: Scalars['String'];
};


export type MutationUpdateUserFeatureFlagValueArgs = {
userAccountId: Scalars['ID'];
featureFlagId: Scalars['ID'];
newValue: Scalars['String'];
};

export type WithIndex<TObject> = TObject & Record<string, any>;
Expand Down Expand Up @@ -89,31 +157,97 @@ export type DirectiveResolverFn<TResult = {}, TParent = {}, TContext = {}, TArgs

/** Mapping between all available schema types and the resolvers types */
export type ResolversTypes = ResolversObject<{
Mutation: ResolverTypeWrapper<{}>;
FEATURE_FLAG_VALUE_TYPES: Feature_Flag_Value_Types;
FeatureFlagPossibleValues: ResolverTypeWrapper<FeatureFlagPossibleValues>;
String: ResolverTypeWrapper<Scalars['String']>;
FeatureFlagValue: ResolverTypeWrapper<FeatureFlagValue>;
FeatureFlag: ResolverTypeWrapper<FeatureFlag>;
ID: ResolverTypeWrapper<Scalars['ID']>;
UserAccount: ResolverTypeWrapper<UserAccount>;
UserFeatureFlagAssociation: ResolverTypeWrapper<UserFeatureFlagAssociation>;
Query: ResolverTypeWrapper<{}>;
AssignUsersToFeatureFlagReturnType: ResolverTypeWrapper<AssignUsersToFeatureFlagReturnType>;
Boolean: ResolverTypeWrapper<Scalars['Boolean']>;
Mutation: ResolverTypeWrapper<{}>;
}>;

/** Mapping between all available schema types and the resolvers parents */
export type ResolversParentTypes = ResolversObject<{
Mutation: {};
FeatureFlagPossibleValues: FeatureFlagPossibleValues;
String: Scalars['String'];
FeatureFlagValue: FeatureFlagValue;
FeatureFlag: FeatureFlag;
ID: Scalars['ID'];
UserAccount: UserAccount;
UserFeatureFlagAssociation: UserFeatureFlagAssociation;
Query: {};
AssignUsersToFeatureFlagReturnType: AssignUsersToFeatureFlagReturnType;
Boolean: Scalars['Boolean'];
Mutation: {};
}>;

export type MutationResolvers<ContextType = ResolverContext, ParentType = ResolversParentTypes['Mutation']> = ResolversObject<{
sampleMutation?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
export type FeatureFlagPossibleValuesResolvers<ContextType = ResolverContext, ParentType = ResolversParentTypes['FeatureFlagPossibleValues']> = ResolversObject<{
type?: Resolver<ResolversTypes['FEATURE_FLAG_VALUE_TYPES'], ParentType, ContextType>;
values?: Resolver<Array<ResolversTypes['String']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;

export type FeatureFlagValueResolvers<ContextType = ResolverContext, ParentType = ResolversParentTypes['FeatureFlagValue']> = ResolversObject<{
type?: Resolver<ResolversTypes['FEATURE_FLAG_VALUE_TYPES'], ParentType, ContextType>;
value?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;

export type FeatureFlagResolvers<ContextType = ResolverContext, ParentType = ResolversParentTypes['FeatureFlag']> = ResolversObject<{
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
possibleValues?: Resolver<ResolversTypes['FeatureFlagPossibleValues'], ParentType, ContextType>;
userAssociations?: Resolver<Maybe<Array<ResolversTypes['UserFeatureFlagAssociation']>>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;

export type UserAccountResolvers<ContextType = ResolverContext, ParentType = ResolversParentTypes['UserAccount']> = ResolversObject<{
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
givenName?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
familyName?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
emailAddress?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
featureFlagAssociations?: Resolver<Maybe<Array<ResolversTypes['UserFeatureFlagAssociation']>>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;

export type UserFeatureFlagAssociationResolvers<ContextType = ResolverContext, ParentType = ResolversParentTypes['UserFeatureFlagAssociation']> = ResolversObject<{
user?: Resolver<ResolversTypes['UserAccount'], ParentType, ContextType>;
featureFlag?: Resolver<ResolversTypes['FeatureFlag'], ParentType, ContextType>;
value?: Resolver<ResolversTypes['FeatureFlagValue'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;

export type QueryResolvers<ContextType = ResolverContext, ParentType = ResolversParentTypes['Query']> = ResolversObject<{
hello?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
userAccounts?: Resolver<Maybe<Array<Maybe<ResolversTypes['UserAccount']>>>, ParentType, ContextType>;
}>;

export type AssignUsersToFeatureFlagReturnTypeResolvers<ContextType = ResolverContext, ParentType = ResolversParentTypes['AssignUsersToFeatureFlagReturnType']> = ResolversObject<{
userId?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
value?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;

export type MutationResolvers<ContextType = ResolverContext, ParentType = ResolversParentTypes['Mutation']> = ResolversObject<{
sampleMutation?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
assignUsersToFeatureFlag?: Resolver<Array<Maybe<ResolversTypes['AssignUsersToFeatureFlagReturnType']>>, ParentType, ContextType, RequireFields<MutationAssignUsersToFeatureFlagArgs, 'userAccountIds' | 'featureFlagId' | 'value'>>;
updateUserFeatureFlagValue?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType, RequireFields<MutationUpdateUserFeatureFlagValueArgs, 'userAccountId' | 'featureFlagId' | 'newValue'>>;
}>;

export type Resolvers<ContextType = ResolverContext> = ResolversObject<{
Mutation?: MutationResolvers<ContextType>;
FeatureFlagPossibleValues?: FeatureFlagPossibleValuesResolvers<ContextType>;
FeatureFlagValue?: FeatureFlagValueResolvers<ContextType>;
FeatureFlag?: FeatureFlagResolvers<ContextType>;
UserAccount?: UserAccountResolvers<ContextType>;
UserFeatureFlagAssociation?: UserFeatureFlagAssociationResolvers<ContextType>;
Query?: QueryResolvers<ContextType>;
AssignUsersToFeatureFlagReturnType?: AssignUsersToFeatureFlagReturnTypeResolvers<ContextType>;
Mutation?: MutationResolvers<ContextType>;
}>;


Expand Down
9 changes: 9 additions & 0 deletions backend/src/helpers/formatJSON.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Feature_Flag_Value_Types } from "../generated/types";

export const formatJSONValue = (value: any, type: Feature_Flag_Value_Types): string => {
return type === "JSON_OBJECT" ? JSON.stringify(value) : value;
};

export const formatJSONValues = (values: any[], type: Feature_Flag_Value_Types): string[] => {
return values.map(value => formatJSONValue(value, type));
};
Loading