From 13ab1eef2f92b3b6f00a3a8569f23319ee2af354 Mon Sep 17 00:00:00 2001 From: Hammad Jutt Date: Fri, 21 Aug 2020 11:34:22 -0600 Subject: [PATCH] Setup Trigger to fetch verified accounts from 3box when ETH address gets updated or user gets inserted --- docker-compose.yml | 4 +- hasura/Dockerfile | 4 +- hasura/metadata/tables.yaml | 28 +++++-- .../down.sql | 1 + .../up.sql | 1 + .../down.sql | 2 + .../up.sql | 18 +++++ package.json | 5 +- .../migrateSourceCredAccounts/handler.ts | 3 +- .../actions/updateBoxProfile/handler.ts | 59 +-------------- .../updateVerifiedAccounts.ts | 56 ++++++++++++++ .../backend/src/handlers/graphql/mutations.ts | 2 +- packages/backend/src/handlers/routes.ts | 2 + .../triggers/fetchBoxVerifiedAccounts.ts | 17 +++++ .../backend/src/handlers/triggers/handler.ts | 29 +++++++ .../backend/src/handlers/triggers/types.ts | 23 ++++++ packages/codegen/schema.graphql | 75 ++++++++++++++++++- 17 files changed, 255 insertions(+), 74 deletions(-) create mode 100644 hasura/migrations/1597822038999_alter_table_public_Player_add_column_created_at/down.sql create mode 100644 hasura/migrations/1597822038999_alter_table_public_Player_add_column_created_at/up.sql create mode 100644 hasura/migrations/1597822046056_alter_table_public_Player_add_column_updated_at/down.sql create mode 100644 hasura/migrations/1597822046056_alter_table_public_Player_add_column_updated_at/up.sql create mode 100644 packages/backend/src/handlers/actions/updateBoxProfile/updateVerifiedAccounts.ts create mode 100644 packages/backend/src/handlers/triggers/fetchBoxVerifiedAccounts.ts create mode 100644 packages/backend/src/handlers/triggers/handler.ts create mode 100644 packages/backend/src/handlers/triggers/types.ts diff --git a/docker-compose.yml b/docker-compose.yml index 0a6ae2f6..9aebca6c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,13 +9,15 @@ services: BACKEND_HOST: ${BACKEND_HOST:-backend:4000} AUTH_HOOK_PATH: auth-webhook ACTION_BASE_PATH: actions + REMOTE_SCHEMA_PATH: remote-schemas/graphql + TRIGGERS_PATH: triggers depends_on: - database ports: - ${HASURA_PORT}:8080 environment: WAIT_HOSTS: database:5432, ${BACKEND_HOST:-backend:4000} - PORT: 8080 + HASURA_GRAPHQL_SERVER_PORT: ${HASURA_PORT:-8080} HASURA_GRAPHQL_DATABASE_URL: postgres://${DATABASE_USER}:${DATABASE_PASSWORD}@database:5432/${DATABASE_NAME} HASURA_GRAPHQL_ADMIN_SECRET: ${HASURA_GRAPHQL_ADMIN_SECRET} HASURA_GRAPHQL_DEV_MODE: ${HASURA_GRAPHQL_DEV_MODE:-false} diff --git a/hasura/Dockerfile b/hasura/Dockerfile index b042b283..065ddf0a 100644 --- a/hasura/Dockerfile +++ b/hasura/Dockerfile @@ -9,6 +9,7 @@ ARG BACKEND_HOST ARG AUTH_HOOK_PATH=auth-webhook ARG ACTION_BASE_PATH=actions ARG REMOTE_SCHEMA_PATH=remote-schemas/graphql +ARG TRIGGERS_PATH=triggers ENV HASURA_GRAPHQL_DEV_MODE false ENV HASURA_GRAPHQL_ENABLE_TELEMETRY false @@ -16,6 +17,7 @@ ENV HASURA_GRAPHQL_ENABLED_LOG_TYPES startup, http-log, webhook-log, websocket-l ENV HASURA_GRAPHQL_AUTH_HOOK http://$BACKEND_HOST/$AUTH_HOOK_PATH ENV ACTION_BASE_ENDPOINT http://$BACKEND_HOST/$ACTION_BASE_PATH ENV REMOTE_SCHEMA_ENDPOINT http://$BACKEND_HOST/$REMOTE_SCHEMA_PATH +ENV TRIGGERS_ENDPOINT http://$BACKEND_HOST/$TRIGGERS_PATH ENV HASURA_GRAPHQL_MIGRATIONS_DATABASE_ENV_VAR HASURA_GRAPHQL_DATABASE_URL ## Migrations @@ -27,4 +29,4 @@ COPY metadata /hasura-metadata ENTRYPOINT ["/bin/sh", "-c", "/wait && /bin/docker-entrypoint.sh /bin/sh \"$@\""] -CMD graphql-engine serve --server-port $PORT +CMD graphql-engine serve --server-port $HASURA_GRAPHQL_SERVER_PORT diff --git a/hasura/metadata/tables.yaml b/hasura/metadata/tables.yaml index 64616dd8..d5f4d8b1 100644 --- a/hasura/metadata/tables.yaml +++ b/hasura/metadata/tables.yaml @@ -110,13 +110,15 @@ - role: player permission: columns: - - enneagram - - ethereum_address - id + - username + - ethereum_address + - totalXp - role - timezone - - totalXp - - username + - enneagram + - scIdentityId + - rank filter: {} - role: public permission: @@ -134,14 +136,28 @@ - role: player permission: columns: - - username - enneagram - role - timezone + - username filter: id: _eq: X-Hasura-User-Id - check: null + check: {} + event_triggers: + - name: fetchBoxVerifiedAccounts + definition: + enable_manual: true + insert: + columns: '*' + update: + columns: + - ethereum_address + retry_conf: + num_retries: 0 + interval_sec: 10 + timeout_sec: 60 + webhook_from_env: TRIGGERS_ENDPOINT - table: schema: public name: Player_Rank diff --git a/hasura/migrations/1597822038999_alter_table_public_Player_add_column_created_at/down.sql b/hasura/migrations/1597822038999_alter_table_public_Player_add_column_created_at/down.sql new file mode 100644 index 00000000..68362932 --- /dev/null +++ b/hasura/migrations/1597822038999_alter_table_public_Player_add_column_created_at/down.sql @@ -0,0 +1 @@ +ALTER TABLE "public"."Player" DROP COLUMN "created_at"; diff --git a/hasura/migrations/1597822038999_alter_table_public_Player_add_column_created_at/up.sql b/hasura/migrations/1597822038999_alter_table_public_Player_add_column_created_at/up.sql new file mode 100644 index 00000000..b932e610 --- /dev/null +++ b/hasura/migrations/1597822038999_alter_table_public_Player_add_column_created_at/up.sql @@ -0,0 +1 @@ +ALTER TABLE "public"."Player" ADD COLUMN "created_at" timestamptz NULL DEFAULT now(); diff --git a/hasura/migrations/1597822046056_alter_table_public_Player_add_column_updated_at/down.sql b/hasura/migrations/1597822046056_alter_table_public_Player_add_column_updated_at/down.sql new file mode 100644 index 00000000..36f38f6d --- /dev/null +++ b/hasura/migrations/1597822046056_alter_table_public_Player_add_column_updated_at/down.sql @@ -0,0 +1,2 @@ +DROP TRIGGER IF EXISTS "set_public_Player_updated_at" ON "public"."Player"; +ALTER TABLE "public"."Player" DROP COLUMN "updated_at"; diff --git a/hasura/migrations/1597822046056_alter_table_public_Player_add_column_updated_at/up.sql b/hasura/migrations/1597822046056_alter_table_public_Player_add_column_updated_at/up.sql new file mode 100644 index 00000000..f3af87a5 --- /dev/null +++ b/hasura/migrations/1597822046056_alter_table_public_Player_add_column_updated_at/up.sql @@ -0,0 +1,18 @@ +ALTER TABLE "public"."Player" ADD COLUMN "updated_at" timestamptz NULL DEFAULT now(); + +CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"() +RETURNS TRIGGER AS $$ +DECLARE + _new record; +BEGIN + _new := NEW; + _new."updated_at" = NOW(); + RETURN _new; +END; +$$ LANGUAGE plpgsql; +CREATE TRIGGER "set_public_Player_updated_at" +BEFORE UPDATE ON "public"."Player" +FOR EACH ROW +EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"(); +COMMENT ON TRIGGER "set_public_Player_updated_at" ON "public"."Player" +IS 'trigger to set value of column "updated_at" to current timestamp on row update'; diff --git a/package.json b/package.json index 607392cf..370b9116 100644 --- a/package.json +++ b/package.json @@ -17,9 +17,8 @@ "backend:dev": "lerna run dev --parallel --scope @metafam/backend --include-dependencies", "backend:build": "lerna run build --scope @metafam/backend --include-dependencies --stream", "hasura": "hasura --project ./hasura", - "hasura:console": "npm run hasura console -- --no-browser", - "hasura:migrate:init": "npm run hasura migrate create \"init\" -- --from-server", - "hasura:migrate:apply": "npm run hasura migrate apply", + "hasura:console": "yarn hasura console -- --no-browser", + "hasura:migrate:init": "yarn hasura migrate create \"init\" -- --from-server", "test": "lerna run test --parallel --", "generate": "lerna run generate --parallel --", "test:full": "yarn lint && yarn typecheck && yarn test", diff --git a/packages/backend/src/handlers/actions/migrateSourceCredAccounts/handler.ts b/packages/backend/src/handlers/actions/migrateSourceCredAccounts/handler.ts index 941d9325..b83c2490 100644 --- a/packages/backend/src/handlers/actions/migrateSourceCredAccounts/handler.ts +++ b/packages/backend/src/handlers/actions/migrateSourceCredAccounts/handler.ts @@ -49,7 +49,7 @@ export const migrateSourceCredAccounts = async ( ).json(); const accountOnConflict = { - constraint: Account_Constraint.AccountIdentifierTypePlayerKey, + constraint: Account_Constraint.AccountIdentifierTypeKey, update_columns: [Account_Update_Column.Identifier], }; @@ -91,6 +91,5 @@ export const migrateSourceCredAccounts = async ( ], }, }); - res.json(result); }; diff --git a/packages/backend/src/handlers/actions/updateBoxProfile/handler.ts b/packages/backend/src/handlers/actions/updateBoxProfile/handler.ts index 79b6d3db..e196a45d 100644 --- a/packages/backend/src/handlers/actions/updateBoxProfile/handler.ts +++ b/packages/backend/src/handlers/actions/updateBoxProfile/handler.ts @@ -1,7 +1,6 @@ -import Box, { VerifiedAccounts } from '3box'; import { Request, Response } from 'express'; -import { client } from '../../../lib/hasuraClient'; +import { updateVerifiedAccounts } from './updateVerifiedAccounts'; export const updateBoxProfileHandler = async ( req: Request, @@ -15,61 +14,7 @@ export const updateBoxProfileHandler = async ( throw new Error('expected role player'); } - const data = await client.GetPlayer({ playerId }); - - const ethAddress = data.Player_by_pk?.ethereum_address; - - if (!ethAddress) { - throw new Error('unknown-player'); - } - - const boxProfile = await Box.getProfile(ethAddress); - const verifiedProfile = await Box.getVerifiedAccounts(boxProfile); - const result = await updateVerifiedProfiles(playerId, verifiedProfile); + const result = await updateVerifiedAccounts(playerId); res.json(result); }; - -async function updateVerifiedProfiles( - playerId: string, - verifiedProfiles: VerifiedAccounts, -): Promise { - const updatedProfiles: string[] = []; - - if (verifiedProfiles.github) { - const result = await client.UpsertAccount({ - objects: [ - { - player_id: playerId, - type: 'GITHUB', - identifier: verifiedProfiles.github.username, - }, - ], - }); - if (result.insert_Account?.affected_rows === 0) { - throw new Error('Error while upserting github profile'); - } - updatedProfiles.push('github'); - } - - if (verifiedProfiles.twitter) { - const result = await client.UpsertAccount({ - objects: [ - { - player_id: playerId, - type: 'TWITTER', - identifier: verifiedProfiles.twitter.username, - }, - ], - }); - if (result.insert_Account?.affected_rows === 0) { - throw new Error('Error while upserting github profile'); - } - updatedProfiles.push('twitter'); - } - - return { - success: true, - updatedProfiles, - }; -} diff --git a/packages/backend/src/handlers/actions/updateBoxProfile/updateVerifiedAccounts.ts b/packages/backend/src/handlers/actions/updateBoxProfile/updateVerifiedAccounts.ts new file mode 100644 index 00000000..782d5c52 --- /dev/null +++ b/packages/backend/src/handlers/actions/updateBoxProfile/updateVerifiedAccounts.ts @@ -0,0 +1,56 @@ +import Box from '3box'; + +import { client } from '../../../lib/hasuraClient'; + +export async function updateVerifiedAccounts( + playerId: string, +): Promise { + const updatedProfiles: string[] = []; + const data = await client.GetPlayer({ playerId }); + + const ethAddress = data.Player_by_pk?.ethereum_address; + + if (!ethAddress) { + throw new Error('unknown-player'); + } + + const boxProfile = await Box.getProfile(ethAddress); + const verifiedAccounts = await Box.getVerifiedAccounts(boxProfile); + + if (verifiedAccounts.github) { + const result = await client.UpsertAccount({ + objects: [ + { + player_id: playerId, + type: 'GITHUB', + identifier: verifiedAccounts.github.username, + }, + ], + }); + if (result.insert_Account?.affected_rows === 0) { + throw new Error('Error while upserting github profile'); + } + updatedProfiles.push('github'); + } + + if (verifiedAccounts.twitter) { + const result = await client.UpsertAccount({ + objects: [ + { + player_id: playerId, + type: 'TWITTER', + identifier: verifiedAccounts.twitter.username, + }, + ], + }); + if (result.insert_Account?.affected_rows === 0) { + throw new Error('Error while upserting github profile'); + } + updatedProfiles.push('twitter'); + } + + return { + success: true, + updatedProfiles, + }; +} diff --git a/packages/backend/src/handlers/graphql/mutations.ts b/packages/backend/src/handlers/graphql/mutations.ts index 63fe3d3a..eed997ca 100644 --- a/packages/backend/src/handlers/graphql/mutations.ts +++ b/packages/backend/src/handlers/graphql/mutations.ts @@ -20,7 +20,7 @@ export const UpsertAccount = gql` insert_Account( objects: $objects on_conflict: { - constraint: Account_identifier_type_player_key + constraint: Account_identifier_type_key update_columns: [identifier] } ) { diff --git a/packages/backend/src/handlers/routes.ts b/packages/backend/src/handlers/routes.ts index 963304fd..1b89c871 100644 --- a/packages/backend/src/handlers/routes.ts +++ b/packages/backend/src/handlers/routes.ts @@ -4,6 +4,7 @@ import { asyncHandlerWrapper } from '../lib/apiHelpers'; import { actionRoutes } from './actions/routes'; import { authHandler } from './auth-webhook/handler'; import { remoteSchemaRoutes } from './remote-schemas/routes'; +import { triggerHandler } from './triggers/handler'; export const router = express.Router(); @@ -16,6 +17,7 @@ router.get('/healthz', (_, res) => { }); router.get('/auth-webhook', asyncHandlerWrapper(authHandler)); +router.post('/triggers', asyncHandlerWrapper(triggerHandler)); router.use('/actions', actionRoutes); diff --git a/packages/backend/src/handlers/triggers/fetchBoxVerifiedAccounts.ts b/packages/backend/src/handlers/triggers/fetchBoxVerifiedAccounts.ts new file mode 100644 index 00000000..a4c85373 --- /dev/null +++ b/packages/backend/src/handlers/triggers/fetchBoxVerifiedAccounts.ts @@ -0,0 +1,17 @@ +import { Player } from '../../lib/autogen/hasura-sdk'; +import { updateVerifiedAccounts } from '../actions/updateBoxProfile/updateVerifiedAccounts'; +import { TriggerPayload } from './types'; + +export const fetchBoxVerifiedAccounts = async ( + payload: TriggerPayload, +) => { + const address = payload.event.data.new?.ethereum_address; + + if (!address) { + return; + } + + const playerId = payload.event.data.new?.id; + + await updateVerifiedAccounts(playerId); +}; diff --git a/packages/backend/src/handlers/triggers/handler.ts b/packages/backend/src/handlers/triggers/handler.ts new file mode 100644 index 00000000..12013c27 --- /dev/null +++ b/packages/backend/src/handlers/triggers/handler.ts @@ -0,0 +1,29 @@ +import { Request, Response } from 'express'; +import { ParamsDictionary } from 'express-serve-static-core'; + +import { fetchBoxVerifiedAccounts } from './fetchBoxVerifiedAccounts'; +import { TriggerPayload } from './types'; + +const TRIGGERS = { + fetchBoxVerifiedAccounts, +}; + +export const triggerHandler = async ( + req: Request, + res: Response, +): Promise => { + const role = req.body.event?.session_variables?.['x-hasura-role']; + + if (role !== 'admin') { + throw new Error('Unauthorized'); + } + + const trigger = TRIGGERS[req.body.trigger.name as keyof typeof TRIGGERS]; + + if (trigger) { + await trigger(req.body); + res.sendStatus(200); + } else { + res.sendStatus(404); + } +}; diff --git a/packages/backend/src/handlers/triggers/types.ts b/packages/backend/src/handlers/triggers/types.ts new file mode 100644 index 00000000..d06ceddd --- /dev/null +++ b/packages/backend/src/handlers/triggers/types.ts @@ -0,0 +1,23 @@ +export interface TriggerPayload { + event: { + session_variables: { [x: string]: string }; + op: 'INSERT' | 'UPDATE' | 'DELETE' | 'MANUAL'; + data: { + old: T | null; + new: T | null; + }; + }; + created_at: string; + id: string; + delivery_info: { + max_retries: number; + current_retry: number; + }; + trigger: { + name: string; + }; + table: { + schema: string; + name: string; + }; +} diff --git a/packages/codegen/schema.graphql b/packages/codegen/schema.graphql index 07a5cfd9..f2f35005 100644 --- a/packages/codegen/schema.graphql +++ b/packages/codegen/schema.graphql @@ -68,9 +68,6 @@ unique or primary key constraints on table "Account" enum Account_constraint { """unique or primary key constraint""" Account_identifier_type_key - - """unique or primary key constraint""" - Account_identifier_type_player_key } """ @@ -688,6 +685,28 @@ input json_comparison_exp { _nin: [json!] } +type Member { + createdAt: String! + delegateKey: String! + exists: Boolean! + id: ID! + kicked: Boolean + loot: Int + memberAddress: String! + moloch: Moloch! + molochAddress: String! + shares: Int! +} + +type Moloch { + id: ID! + summoner: String! + title: String + totalLoot: Int! + totalShares: Int! + version: String +} + """mutation root""" type mutation_root { """ @@ -1189,6 +1208,10 @@ type Player { """Remote relationship field""" box_profile: BoxProfile + created_at: timestamptz + + """Remote relationship field""" + daohausMemberships: [Member!]! enneagram: enneagram_type ethereum_address: String id: uuid! @@ -1197,6 +1220,7 @@ type Player { scIdentityId: String timezone: Int totalXp: numeric + updated_at: timestamptz username: String! } @@ -1273,6 +1297,7 @@ input Player_bool_exp { _and: [Player_bool_exp] _not: Player_bool_exp _or: [Player_bool_exp] + created_at: timestamptz_comparison_exp enneagram: enneagram_type_comparison_exp ethereum_address: String_comparison_exp id: uuid_comparison_exp @@ -1281,6 +1306,7 @@ input Player_bool_exp { scIdentityId: String_comparison_exp timezone: Int_comparison_exp totalXp: numeric_comparison_exp + updated_at: timestamptz_comparison_exp username: String_comparison_exp } @@ -1315,6 +1341,7 @@ input type for inserting data into table "Player" input Player_insert_input { Accounts: Account_arr_rel_insert_input Player_Skills: Player_Skill_arr_rel_insert_input + created_at: timestamptz enneagram: enneagram_type ethereum_address: String id: uuid @@ -1323,17 +1350,20 @@ input Player_insert_input { scIdentityId: String timezone: Int totalXp: numeric + updated_at: timestamptz username: String } """aggregate max on columns""" type Player_max_fields { + created_at: timestamptz ethereum_address: String id: uuid role: String scIdentityId: String timezone: Int totalXp: numeric + updated_at: timestamptz username: String } @@ -1341,23 +1371,27 @@ type Player_max_fields { order by max() on columns of table "Player" """ input Player_max_order_by { + created_at: order_by ethereum_address: order_by id: order_by role: order_by scIdentityId: order_by timezone: order_by totalXp: order_by + updated_at: order_by username: order_by } """aggregate min on columns""" type Player_min_fields { + created_at: timestamptz ethereum_address: String id: uuid role: String scIdentityId: String timezone: Int totalXp: numeric + updated_at: timestamptz username: String } @@ -1365,12 +1399,14 @@ type Player_min_fields { order by min() on columns of table "Player" """ input Player_min_order_by { + created_at: order_by ethereum_address: order_by id: order_by role: order_by scIdentityId: order_by timezone: order_by totalXp: order_by + updated_at: order_by username: order_by } @@ -1408,6 +1444,7 @@ ordering options when selecting data from "Player" input Player_order_by { Accounts_aggregate: Account_aggregate_order_by Player_Skills_aggregate: Player_Skill_aggregate_order_by + created_at: order_by enneagram: order_by ethereum_address: order_by id: order_by @@ -1416,6 +1453,7 @@ input Player_order_by { scIdentityId: order_by timezone: order_by totalXp: order_by + updated_at: order_by username: order_by } @@ -1604,6 +1642,9 @@ enum Player_Rank_update_column { select columns of table "Player" """ enum Player_select_column { + """column name""" + created_at + """column name""" enneagram @@ -1628,6 +1669,9 @@ enum Player_select_column { """column name""" totalXp + """column name""" + updated_at + """column name""" username } @@ -1636,6 +1680,7 @@ enum Player_select_column { input type for updating data in table "Player" """ input Player_set_input { + created_at: timestamptz enneagram: enneagram_type ethereum_address: String id: uuid @@ -1644,6 +1689,7 @@ input Player_set_input { scIdentityId: String timezone: Int totalXp: numeric + updated_at: timestamptz username: String } @@ -1889,6 +1935,9 @@ input Player_sum_order_by { update columns of table "Player" """ enum Player_update_column { + """column name""" + created_at + """column name""" enneagram @@ -1913,6 +1962,9 @@ enum Player_update_column { """column name""" totalXp + """column name""" + updated_at + """column name""" username } @@ -1961,6 +2013,7 @@ input Player_variance_order_by { type Query { getBoxProfile(address: String): BoxProfile + getDaoHausMemberships(memberAddress: String): [Member!]! } """query root""" @@ -2263,6 +2316,7 @@ type query_root { """fetch data from the table: "Skill" using primary key columns""" Skill_by_pk(id: uuid!): Skill getBoxProfile(address: String): BoxProfile + getDaoHausMemberships(memberAddress: String): [Member!]! } """ @@ -2797,6 +2851,21 @@ type subscription_root { scalar timestamptz +""" +expression to compare columns of type timestamptz. All fields are combined with logical 'AND'. +""" +input timestamptz_comparison_exp { + _eq: timestamptz + _gt: timestamptz + _gte: timestamptz + _in: [timestamptz!] + _is_null: Boolean + _lt: timestamptz + _lte: timestamptz + _neq: timestamptz + _nin: [timestamptz!] +} + type UpdateBoxProfileResponse { success: Boolean! updatedProfiles: [String!]!