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

Pierre Havelaar: Frontend & Backend Technical Assesment #92

Open
wants to merge 20 commits 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ src/types/api.ts
/out/
*.log
.*
.yarn/*
**/.yarn/*

*.log
.*
Expand Down
1 change: 1 addition & 0 deletions backend/.yarnrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
nodeLinker: node-modules
1 change: 1 addition & 0 deletions backend/codegen.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ generates:
ID: 'string | number'
useTypeImports: true
mappers:
immutableTypes: true
4 changes: 3 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",
"testdata": "ts-node ./src/bin/testdata.ts"
},
"devDependencies": {
"@graphql-codegen/cli": "1.21.2",
Expand Down Expand Up @@ -39,6 +40,7 @@
"got": "^11.8.5",
"graphql": "^15.4.0",
"graphql-import": "^1.0.2",
"graphql-scalars": "^1.21.3",
"graphql-tools": "^7.0.2",
"roarr": "^3.2.0",
"slonik": "^23.5.1",
Expand Down
10 changes: 10 additions & 0 deletions backend/src/bin/migrations/2023.03.30T19.11.25.feature_flag.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
CREATE TYPE feature_flag_type AS ENUM ('boolean', 'string', 'JSON');

CREATE TABLE feature_flag (
id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
name text NOT NULL UNIQUE,
flag_type feature_flag_type NOT NULL,
flag_enabled boolean NOT NULL,
created_at timestamp with time zone DEFAULT NOW(),
updated_at timestamp with time zone DEFAULT NOW()
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
CREATE TABLE feature_flag_variant (
id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
feature_flag_id integer NOT NULL,
flag_value text NOT NULL,
created_at timestamp with time zone DEFAULT NOW(),
updated_at timestamp with time zone DEFAULT NOW(),
UNIQUE(feature_flag_id, flag_value),
CONSTRAINT fk_feature_flag
FOREIGN KEY(feature_flag_id)
REFERENCES feature_flag(id)
ON DELETE CASCADE
);

13 changes: 13 additions & 0 deletions backend/src/bin/migrations/2023.03.31T11.11.45.context.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
CREATE TABLE context(
id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
feature_flag_variant_id integer,
environment text NOT NULL,
context_enabled boolean NOT NULL,
created_at timestamp with time zone DEFAULT NOW(),
updated_at timestamp with time zone DEFAULT NOW(),
UNIQUE(feature_flag_variant_id, environment),
CONSTRAINT fk_feature_flag_variant
FOREIGN KEY(feature_flag_variant_id)
REFERENCES feature_flag_variant(id)
ON DELETE CASCADE
);
16 changes: 16 additions & 0 deletions backend/src/bin/migrations/2023.03.31T11.31.56.user_context.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
CREATE TABLE user_context (
id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
user_id integer NOT NULL,
context_id integer NOT NULL,
created_at timestamp with time zone DEFAULT NOW(),
updated_at timestamp with time zone DEFAULT NOW(),
UNIQUE(user_id, context_id),
CONSTRAINT fk_user
FOREIGN KEY(user_id)
REFERENCES user_account(id)
ON DELETE CASCADE,
CONSTRAINT fk_context
FOREIGN KEY(context_id)
REFERENCES context(id)
ON DELETE CASCADE
);
Original file line number Diff line number Diff line change
@@ -1 +1 @@
raise 'down migration not implemented'
DROP TABLE IF EXISTS user_account;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DROP TABLE IF EXISTS feature_flag;
DROP TYPE IF EXISTS feature_flag_type;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE IF EXISTS feature_flag_variant;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE IF EXISTS context;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE IF EXISTS user_context;
19 changes: 19 additions & 0 deletions backend/src/bin/testdata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { SlonikMigrator } from '@slonik/migrator';
import { createPool } from 'slonik';

if (!process.env.POSTGRES_CONNECTION_STRING) {
throw new Error(
'Must provide a PG connection string (export POSTGRES_CONNECTION_STRING=value) -- if you need a fresh database, we recommend using Render.com',
);
}

const slonik = createPool(process.env.POSTGRES_CONNECTION_STRING);

const migrator = new SlonikMigrator({
logger: console,
migrationsPath: __dirname + '/testdata',
migrationTableName: 'testdata',
slonik,
});

migrator.runAsCLI();
4 changes: 4 additions & 0 deletions backend/src/bin/testdata/2023.03.31T11.40.47.users.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
INSERT INTO user_account (given_name, family_name, email_address)
VALUES
('Pierre', 'Havelaar', 'pierrehavelaar@gmail.com'),
('John', 'Doe', 'john.doe@gmail.com')
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
INSERT INTO feature_flag (name, flag_type, flag_enabled)
VALUES
('contra-1', 'boolean', true),
('contra-2', 'boolean', false),
('contra-3', 'string', true),
('contra-4', 'JSON', true)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
INSERT INTO feature_flag_variant (feature_flag_id, flag_value)
VALUES
(1, 'true'),
(1, 'false'),
(2, 'true'),
(2, 'false'),
(3, 'red'),
(3, 'orange'),
(3, 'green'),
(4, '{ a: true, b: true }'),
(4, '{ a: true, b: false }'),
(4, '{ a: false, b: true }'),
(4, '{ a: false, b: false }')
24 changes: 24 additions & 0 deletions backend/src/bin/testdata/2023.03.31T11.58.09.contexts.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
INSERT INTO context (feature_flag_variant_id, environment, context_enabled)
VALUES
(1, 'production', false),
(1, 'testing', true),
(2, 'production', false),
(2, 'testing', true),
(3, 'production', false),
(3, 'testing', true),
(4, 'production', false),
(4, 'testing', true),
(5, 'production', false),
(5, 'testing', true),
(6, 'production', false),
(6, 'testing', true),
(7, 'production', false),
(7, 'testing', true),
(8, 'production', false),
(8, 'testing', true),
(9, 'production', false),
(9, 'testing', true),
(10, 'production', false),
(10, 'testing', true),
(11, 'production', false),
(11, 'testing', true)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
TRUNCATE TABLE user_account RESTART IDENTITY CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
TRUNCATE TABLE feature_flag RESTART IDENTITY CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
TRUNCATE TABLE feature_flag_variant RESTART IDENTITY CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
TRUNCATE TABLE context RESTART IDENTITY CASCADE;
112 changes: 103 additions & 9 deletions backend/src/generated/types.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,83 @@
import type { GraphQLResolveInfo } from 'graphql';
import type { GraphQLResolveInfo, GraphQLScalarType, GraphQLScalarTypeConfig } from 'graphql';
import type { ResolverContext } from '../ResolverContextType';
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;
String: string;
Boolean: boolean;
Int: number;
Float: number;
DateTime: any;
};


export enum FlagType {
Boolean = 'boolean',
String = 'string',
Json = 'json'
}

export type Feature = {
readonly __typename?: 'Feature';
readonly contextId: Scalars['Int'];
readonly name: Scalars['String'];
readonly type?: Maybe<FlagType>;
readonly value: Scalars['String'];
};

export type User = {
readonly __typename?: 'User';
readonly id: Scalars['Int'];
readonly givenName: Scalars['String'];
readonly familyName: Scalars['String'];
readonly emailAddress?: Maybe<Scalars['String']>;
readonly createdAt: Scalars['DateTime'];
readonly updatedAt: Scalars['DateTime'];
readonly environment: Scalars['String'];
readonly features: ReadonlyArray<Feature>;
};

export type Mutation = {
__typename?: 'Mutation';
sampleMutation: Scalars['String'];
readonly __typename?: 'Mutation';
/** Setup a feature flag for a group of users. The feature flag and value must already exist. */
readonly targetUsers: ReadonlyArray<User>;
/**
* Update an existing feature flag for a single user.
* If the feature is not availabe for this user or is assigned to multiple user an error will be thrown.
*/
readonly updateFeatureForUser?: Maybe<Scalars['Int']>;
};


export type MutationTargetUsersArgs = {
userIds: ReadonlyArray<Scalars['Int']>;
flag: Scalars['String'];
value: Scalars['String'];
environment: Scalars['String'];
};


export type MutationUpdateFeatureForUserArgs = {
userId: Scalars['Int'];
flag: Scalars['String'];
value: Scalars['String'];
environment: Scalars['String'];
};

export type Query = {
__typename?: 'Query';
hello: Scalars['String'];
readonly __typename?: 'Query';
/** Retrieve all the users in the system and their feature flags for the given environment */
readonly users: ReadonlyArray<User>;
};


export type QueryUsersArgs = {
environment: Scalars['String'];
};

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

/** Mapping between all available schema types and the resolvers types */
export type ResolversTypes = ResolversObject<{
Mutation: ResolverTypeWrapper<{}>;
DateTime: ResolverTypeWrapper<Scalars['DateTime']>;
FlagType: FlagType;
Feature: ResolverTypeWrapper<Feature>;
Int: ResolverTypeWrapper<Scalars['Int']>;
String: ResolverTypeWrapper<Scalars['String']>;
User: ResolverTypeWrapper<User>;
Mutation: ResolverTypeWrapper<{}>;
Query: ResolverTypeWrapper<{}>;
Boolean: ResolverTypeWrapper<Scalars['Boolean']>;
}>;

/** Mapping between all available schema types and the resolvers parents */
export type ResolversParentTypes = ResolversObject<{
Mutation: {};
DateTime: Scalars['DateTime'];
Feature: Feature;
Int: Scalars['Int'];
String: Scalars['String'];
User: User;
Mutation: {};
Query: {};
Boolean: Scalars['Boolean'];
}>;

export interface DateTimeScalarConfig extends GraphQLScalarTypeConfig<ResolversTypes['DateTime'], any> {
name: 'DateTime';
}

export type FeatureResolvers<ContextType = ResolverContext, ParentType = ResolversParentTypes['Feature']> = ResolversObject<{
contextId?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
type?: Resolver<Maybe<ResolversTypes['FlagType']>, ParentType, ContextType>;
value?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;

export type UserResolvers<ContextType = ResolverContext, ParentType = ResolversParentTypes['User']> = ResolversObject<{
id?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
givenName?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
familyName?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
emailAddress?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
createdAt?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
updatedAt?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
environment?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
features?: Resolver<ReadonlyArray<ResolversTypes['Feature']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;

export type MutationResolvers<ContextType = ResolverContext, ParentType = ResolversParentTypes['Mutation']> = ResolversObject<{
sampleMutation?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
targetUsers?: Resolver<ReadonlyArray<ResolversTypes['User']>, ParentType, ContextType, RequireFields<MutationTargetUsersArgs, 'userIds' | 'flag' | 'value' | 'environment'>>;
updateFeatureForUser?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType, RequireFields<MutationUpdateFeatureForUserArgs, 'userId' | 'flag' | 'value' | 'environment'>>;
}>;

export type QueryResolvers<ContextType = ResolverContext, ParentType = ResolversParentTypes['Query']> = ResolversObject<{
hello?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
users?: Resolver<ReadonlyArray<ResolversTypes['User']>, ParentType, ContextType, RequireFields<QueryUsersArgs, 'environment'>>;
}>;

export type Resolvers<ContextType = ResolverContext> = ResolversObject<{
DateTime?: GraphQLScalarType;
Feature?: FeatureResolvers<ContextType>;
User?: UserResolvers<ContextType>;
Mutation?: MutationResolvers<ContextType>;
Query?: QueryResolvers<ContextType>;
}>;
Expand Down
6 changes: 4 additions & 2 deletions backend/src/schema/resolvers/Mutations/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { resolve as sampleMutation } from './sampleMutation';
import { updateFeatureForUser } from './updateFeatureForUser';
import { targetUsers } from './targetUsers';

export const Mutation = {
sampleMutation,
targetUsers,
updateFeatureForUser,
};
5 changes: 0 additions & 5 deletions backend/src/schema/resolvers/Mutations/sampleMutation.ts

This file was deleted.