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

Backend/Felipe Renato #67

Open
wants to merge 2 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
8 changes: 8 additions & 0 deletions backend/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
transform: {
'^.+\\.ts?$': 'ts-jest',
},
transformIgnorePatterns: ['<rootDir>/node_modules/'],
};
7 changes: 6 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-unit": "jest"
},
"devDependencies": {
"@graphql-codegen/cli": "1.21.2",
Expand All @@ -18,21 +19,25 @@
"@graphql-codegen/typescript-operations": "1.17.15",
"@graphql-codegen/typescript-resolvers": "1.19.0",
"@slonik/migrator": "0.11.2",
"@types/jest": "^29.0.0",
"@typescript-eslint/parser": "5.12.0",
"eslint": "7.22.0",
"eslint-config-canonical": "25.9.1",
"eslint-config-prettier": "7.2.0",
"eslint-plugin-import": "2.22.1",
"format-graphql": "1.4.0",
"jest": "^29.0.2",
"lint-staged": "10.5.4",
"nyc": "15.1.0",
"prettier": "2.2.1",
"ts-jest": "^28.0.8",
"ts-node-dev": "1.1.6",
"typescript": "4.5.5"
},
"dependencies": {
"@roarr/cli": "^3.2.2",
"apollo-server-fastify": "3.6.3",
"dataloader": "^2.1.0",
"fast-safe-stringify": "^2.1.1",
"fastify": "^3.14.0",
"fastify-cookie": "^5.4.0",
Expand Down
4 changes: 2 additions & 2 deletions backend/src/ResolverContextType.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DatabasePoolType } from 'slonik';
import { IDatasources } from './datasources';

export type ResolverContext = {
readonly pool: DatabasePoolType;
readonly datasources: IDatasources;
};
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,
feature_flag_key text NOT NULL UNIQUE,
created_at timestamp with time zone DEFAULT NOW()
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
CREATE TABLE user_feature_flag (
user_id integer NOT NULL,
feature_flag_id integer NOT NULL,
feature_flag_value text NOT NULL,
updated_at timestamp with time zone DEFAULT NOW(),
CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES public.user_account(id),
CONSTRAINT fk_feature_flag_id FOREIGN KEY (feature_flag_id) REFERENCES public.feature_flag(id),
CONSTRAINT pk_user_feature_flag PRIMARY KEY (user_id, feature_flag_id)
);
7 changes: 7 additions & 0 deletions backend/src/bin/migrations/2022.09.05T21.38.23.populate.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
INSERT INTO public.user_account
(given_name, family_name, email_address)
VALUES('Rose', 'Alves', 'rose@gmail.com'),('Bruna', 'Santos', 'bruna@gmail.com'),('Julia', 'Dias', 'julia@gmail.com');

INSERT INTO public.feature_flag
(feature_flag_key)
VALUES('SHOULD_RENDER_LANDING_PAGE_NEXT_ACCESS'),('ALLOW_CREDIT_CARD_PAYMENT'),('SHOULD_NOT_RECEIVE_SPORSONSHIP_CONTENT');
Original file line number Diff line number Diff line change
@@ -1 +1 @@
raise 'down migration not implemented'
DROP TABLE IF EXISTS user_account CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE IF EXISTS feature_flag CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE IF EXISTS user_feature_flag CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
raise 'down migration not implemented'
4 changes: 2 additions & 2 deletions backend/src/bin/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ const pool = createPool(process.env.POSTGRES_CONNECTION_STRING, {
try {
const app = await createFastifyServer(pool);

app.listen(8_080, () =>
log.info(`🛩 Server ready at http://localhost:8080/graphql`),
app.listen(3_000, () =>
log.info(`🛩 Server ready at http://localhost:3000/graphql`),
);
} catch (error) {
// eslint-disable-next-line no-console
Expand Down
51 changes: 51 additions & 0 deletions backend/src/datasources/adapters/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { UserFeatureFlag } from '../../generated/types';
import { FeatureFlagValueType } from '../../schema/resolvers/Scalars';

const FEATURE_FLAG_TYPES = ['boolean', 'string', 'number', 'object'];

export function isValidFeatureFlag(value: any): boolean {
return value !== null && FEATURE_FLAG_TYPES.includes(typeof value);
}

export function parseFeatureFlagValue(json: string): FeatureFlagValueType {
const parsedFeatureFlag = JSON.parse(json);

if (!isValidFeatureFlag(parsedFeatureFlag)) {
throw new Error('Could not assert provided Feature Flag');
}
return parsedFeatureFlag as FeatureFlagValueType;
}

export function mapUsersFeatureFlags(
usersFeatureFlags: readonly UserFeatureFlag[],
): Map<number, UserFeatureFlag[]> {
return usersFeatureFlags.reduce(
(acc: Map<number, UserFeatureFlag[]>, cur) => {
if (!acc.has(cur.userId)) {
acc.set(cur.userId, [
{
userId: cur.userId,
featureFlagKey: cur.featureFlagKey,
featureFlagValue: parseFeatureFlagValue(cur.featureFlagValue),
},
]);
return acc;
}

acc.get(cur.userId)!.push({
userId: cur.userId,
featureFlagKey: cur.featureFlagKey,
featureFlagValue: parseFeatureFlagValue(cur.featureFlagValue),
});
return acc;
},
new Map<number, UserFeatureFlag[]>(),
);
}

export function userFeatureFlagsMapToUserFeatureFlagResponse(
userIds: readonly number[],
userFeatureFlagsMap: Map<number, UserFeatureFlag[]>,
): UserFeatureFlag[][] {
return userIds.map((userId) => userFeatureFlagsMap.get(userId) ?? []);
}
22 changes: 22 additions & 0 deletions backend/src/datasources/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { CommonQueryMethodsType } from 'slonik';
import { IUserDatasource, UserDatasource } from './userDatasource';
import {
IUserFeatureFlagDatasource,
UserFeatureFlagDatasource,
} from './userFeatureFlagDatasource';

export interface IDatasources {
userDatasource: IUserDatasource;
userFeatureFlagDatasource: IUserFeatureFlagDatasource;
}

export class Datasources {
public userDatasource: IUserDatasource;

public userFeatureFlagDatasource: IUserFeatureFlagDatasource;

constructor(private dbConn: CommonQueryMethodsType) {
this.userDatasource = new UserDatasource(this.dbConn);
this.userFeatureFlagDatasource = new UserFeatureFlagDatasource(this.dbConn);
}
}
18 changes: 18 additions & 0 deletions backend/src/datasources/userDatasource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { CommonQueryMethodsType, sql } from 'slonik';
import { User } from '../generated/types';

export interface IUserDatasource {
getUsers: () => Promise<User[]>;
}

export class UserDatasource implements IUserDatasource {
constructor(private dbConn: CommonQueryMethodsType) {}

public async getUsers(): Promise<User[]> {
const string = sql<User>`
SELECT id as user_id, given_name, family_name, email_address
FROM user_account;`;

return this.dbConn.many(string).then((result) => [...result]);
}
}
77 changes: 77 additions & 0 deletions backend/src/datasources/userFeatureFlagDatasource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import DataLoader from 'dataloader';
import { CommonQueryMethodsType, sql } from 'slonik';
import {
mapUsersFeatureFlags,
parseFeatureFlagValue,
userFeatureFlagsMapToUserFeatureFlagResponse,
} from './adapters';
import {
SetUsersFeatureFlagResponse,
UserFeatureFlag,
} from '../generated/types';
import { FeatureFlagValueType } from '../schema/resolvers/Scalars';

export interface IUserFeatureFlagDatasource {
getUserFeatureFlags: DataLoader<number, UserFeatureFlag[], number>;
setUsersFeatureFlag: (
userIds: number[],
feature_flag_key: string,
feature_flag_value: FeatureFlagValueType,
) => Promise<SetUsersFeatureFlagResponse[]>;
}

export class UserFeatureFlagDatasource implements IUserFeatureFlagDatasource {
constructor(private dbConn: CommonQueryMethodsType) {}

public getUserFeatureFlags = new DataLoader(
async (userIds: readonly number[]) => {
const query = sql<UserFeatureFlag>`
SELECT ua.id as user_id, ff.feature_flag_key, uff.feature_flag_value
FROM user_account ua
INNER JOIN user_feature_flag uff
ON uff.user_id = ua.id
INNER JOIN feature_flag ff
ON uff.feature_flag_id = ff.id
WHERE ua.id IN (${sql.join(userIds, sql`,`)});
`;

return this.dbConn.many(query).then((result) => {
const userFeatureFlagsMap = mapUsersFeatureFlags(result);
const userFeatureFlags = userFeatureFlagsMapToUserFeatureFlagResponse(
userIds,
userFeatureFlagsMap,
);
return userFeatureFlags;
});
},
);

public async setUsersFeatureFlag(
userIds: number[],
featureFlagKey: string,
featureflagValue: FeatureFlagValueType,
): Promise<SetUsersFeatureFlagResponse[]> {
const flagValueString = JSON.stringify(featureflagValue);

const insertquery = sql<UserFeatureFlag>`
INSERT INTO user_feature_flag (feature_flag_id, user_id, feature_flag_value)
SELECT ff.id AS feature_flag_id, ua.id AS user_id, ${flagValueString} AS feature_flag_value
FROM feature_flag ff
LEFT JOIN user_account ua ON ua.id IN (${sql.join(userIds, sql`, `)})
WHERE ff.feature_flag_key = ${featureFlagKey}
ON CONFLICT (feature_flag_id, user_id)
DO UPDATE SET feature_flag_value = ${flagValueString}
RETURNING user_feature_flag.user_id, ${featureFlagKey} as feature_flag_key, user_feature_flag.feature_flag_value;
`;

return this.dbConn.any(insertquery).then((results) => {
return results.map((uff) => ({
userFeatureFlag: {
userId: uff.userId,
featureFlagKey: uff.featureFlagKey,
featureFlagValue: parseFeatureFlagValue(uff.featureFlagValue),
},
}));
});
}
}
3 changes: 2 additions & 1 deletion backend/src/factories/createFastifyServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { makeExecutableSchema } from 'graphql-tools';
import type { CommonQueryMethodsType } from 'slonik';
// @ts-ignore
import { resolvers } from '../schema/resolvers';
import { Datasources } from '../datasources';

export const createFastifyServer = async (pool: CommonQueryMethodsType) => {
const executableSchema = makeExecutableSchema({
Expand All @@ -19,7 +20,7 @@ export const createFastifyServer = async (pool: CommonQueryMethodsType) => {

const graphQLServer = new ApolloServer({
context: ({ request, reply }) => ({
pool,
datasources: new Datasources(pool),
reply,
request,
}),
Expand Down