mirror of
https://github.com/MetaFam/TheGame.git
synced 2026-04-24 03:00:09 -04:00
Replaced IDX cache actions / triggers with composeDB ones
This commit is contained in:
@@ -51,12 +51,7 @@ type Mutation {
|
||||
|
||||
|
||||
type Mutation {
|
||||
updateExpiredIDXProfiles : ExpiredPlayerProfiles
|
||||
}
|
||||
|
||||
|
||||
type Mutation {
|
||||
updateIDXProfile (
|
||||
updateCachedProfile (
|
||||
playerId: uuid
|
||||
): CacheProcessOutput
|
||||
}
|
||||
|
||||
@@ -50,18 +50,11 @@ actions:
|
||||
definition:
|
||||
kind: synchronous
|
||||
handler: '{{ACTION_BASE_ENDPOINT}}/syncSourceCredAccounts'
|
||||
- name: updateExpiredIDXProfiles
|
||||
definition:
|
||||
kind: synchronous
|
||||
handler: '{{ACTION_BASE_ENDPOINT}}/idxCache/checkExpired'
|
||||
- name: updateIDXProfile
|
||||
- name: updateCachedProfile
|
||||
definition:
|
||||
kind: asynchronous
|
||||
handler: '{{ACTION_BASE_ENDPOINT}}/idxCache/updateSingle'
|
||||
handler: http://host.do{{ACTION_BASE_ENDPOINT}}/composeDB/updateCachedProfile.internal:3000
|
||||
forward_client_headers: true
|
||||
permissions:
|
||||
- role: player
|
||||
- role: public
|
||||
- name: updateQuestCompletion
|
||||
definition:
|
||||
kind: synchronous
|
||||
|
||||
@@ -1,15 +1,3 @@
|
||||
- name: queueExpiredIDXCacheEntries
|
||||
webhook: '{{ACTION_BASE_ENDPOINT}}/idxCache/checkExpired'
|
||||
schedule: 0 3 * * *
|
||||
include_in_metadata: true
|
||||
payload: {}
|
||||
retry_conf:
|
||||
num_retries: 0
|
||||
timeout_seconds: 600
|
||||
tolerance_seconds: 21600
|
||||
retry_interval_seconds: 10
|
||||
comment: Checks for cache entries more than four days old and queues them to be
|
||||
recached.
|
||||
- name: syncAllGuildDiscordMembers
|
||||
webhook: '{{ACTION_BASE_ENDPOINT}}/syncAllGuildDiscordMembers'
|
||||
schedule: 31 5 * * *
|
||||
@@ -26,3 +14,10 @@
|
||||
tolerance_seconds: 21600
|
||||
retry_interval_seconds: 10
|
||||
comment: Reads account data from XP Github and upserts users into the Database
|
||||
- name: refreshPlayersFromComposeDB
|
||||
webhook: '{{ACTION_BASE_ENDPOINT}}/composeDB/refreshPlayers'
|
||||
schedule: 0 3 * * *
|
||||
include_in_metadata: true
|
||||
payload: {}
|
||||
comment: Finds any cached player profiles more than four days old and queues them
|
||||
to be recached from ComposeDB.
|
||||
|
||||
@@ -611,13 +611,14 @@
|
||||
set:
|
||||
id: x-hasura-User-Id
|
||||
event_triggers:
|
||||
- name: cacheIDXProfile
|
||||
- name: cacheComposeDBProfile
|
||||
definition:
|
||||
enable_manual: true
|
||||
insert:
|
||||
columns: '*'
|
||||
update:
|
||||
columns:
|
||||
- ceramic_profile_id
|
||||
- ethereum_address
|
||||
retry_conf:
|
||||
num_retries: 0
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import express from 'express';
|
||||
|
||||
import { asyncHandlerWrapper } from '../../../lib/apiHelpers.js';
|
||||
import linkProfileNode from './linkProfileNode.js';
|
||||
|
||||
export const ceramicRoutes = express.Router();
|
||||
|
||||
ceramicRoutes.post('/linkProfileNode', asyncHandlerWrapper(linkProfileNode));
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Maybe } from '@metafam/utils';
|
||||
import Bottleneck from 'bottleneck';
|
||||
|
||||
import updateCachedProfile from '../handlers/actions/idxCache/updateSingle.js';
|
||||
import { updatePlayerFromComposeDB } from './updatePlayerFromComposeDB.js';
|
||||
|
||||
let count = 0;
|
||||
|
||||
@@ -33,7 +33,7 @@ export const queueRecache = async ({
|
||||
const preRun = count++;
|
||||
|
||||
const result = await limiter.schedule({ id: playerId, ...opts }, () =>
|
||||
updateCachedProfile(playerId),
|
||||
updatePlayerFromComposeDB(playerId),
|
||||
);
|
||||
console.debug({
|
||||
msg: 'Completed Profile Update',
|
||||
@@ -2,12 +2,12 @@ import { ComposeClient } from '@composedb/client';
|
||||
import { composeDBDefinition } from '@metafam/utils';
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
import { CONFIG } from '../../../config.js';
|
||||
import { CONFIG } from '../../../../config.js';
|
||||
import {
|
||||
LinkCeramicProfileNodeResponse,
|
||||
Mutation_RootLinkCeramicProfileNodeArgs,
|
||||
} from '../../../lib/autogen/hasura-sdk.js';
|
||||
import { client } from '../../../lib/hasuraClient.js';
|
||||
} from '../../../../lib/autogen/hasura-sdk.js';
|
||||
import { client } from '../../../../lib/hasuraClient.js';
|
||||
|
||||
export default async (req: Request, res: Response): Promise<void> => {
|
||||
const { input, session_variables: sessionVariables } = req.body;
|
||||
@@ -52,7 +52,6 @@ export default async (req: Request, res: Response): Promise<void> => {
|
||||
controllerEthAddress.toLowerCase() === ethereumAddress.toLowerCase()
|
||||
) {
|
||||
// We confirmed they indeed control this model, so persist it as theirs
|
||||
|
||||
await client.UpdatePlayerById({
|
||||
playerId,
|
||||
input: { ceramicProfileId: nodeId },
|
||||
13
packages/backend/src/handlers/actions/composeDB/routes.ts
Normal file
13
packages/backend/src/handlers/actions/composeDB/routes.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import express from 'express';
|
||||
|
||||
import { asyncHandlerWrapper } from '../../../lib/apiHelpers.js';
|
||||
import linkProfileNode from './linkProfileNode/handler.js';
|
||||
import updateSingleProfileHandler from './updateSingleProfile/handler.js';
|
||||
|
||||
export const routes = express.Router();
|
||||
|
||||
routes.post('/linkProfileNode', asyncHandlerWrapper(linkProfileNode));
|
||||
routes.post(
|
||||
'/updateCachedProfile',
|
||||
asyncHandlerWrapper(updateSingleProfileHandler),
|
||||
);
|
||||
@@ -0,0 +1,154 @@
|
||||
import { Caip10Link } from '@ceramicnetwork/stream-caip10-link';
|
||||
import { ComposeClient } from '@composedb/client';
|
||||
import {
|
||||
composeDBDefinition,
|
||||
ComposeDBProfile,
|
||||
composeDBToHasuraProfile,
|
||||
} from '@metafam/utils';
|
||||
|
||||
import { CONFIG } from '../../../config.js';
|
||||
import {
|
||||
Profile_Update_Column,
|
||||
UpdateIdxProfileResponse,
|
||||
} from '../../../lib/autogen/hasura-sdk.js';
|
||||
import { client } from '../../../lib/hasuraClient.js';
|
||||
|
||||
const composeDBClient = new ComposeClient({
|
||||
ceramic: CONFIG.ceramicURL,
|
||||
definition: composeDBDefinition,
|
||||
});
|
||||
|
||||
export const updatePlayerFromComposeDB = async (
|
||||
playerId: string,
|
||||
): Promise<UpdateIdxProfileResponse> => {
|
||||
const accountLinks: string[] = [];
|
||||
const fields: string[] = [];
|
||||
const { player_by_pk: player } = await client.GetPlayer({ playerId });
|
||||
|
||||
if (!player) {
|
||||
throw new Error(`Unknown Player: "${playerId}"`);
|
||||
} else {
|
||||
console.debug(`Updating Profile Cache For ${player.ethereumAddress}`);
|
||||
}
|
||||
|
||||
const { ethereumAddress, ceramicProfileId } = player;
|
||||
|
||||
if (ceramicProfileId == null) {
|
||||
// they are not yet in ComposeDB; ignore
|
||||
return {
|
||||
success: false,
|
||||
ceramic: CONFIG.ceramicURL,
|
||||
};
|
||||
}
|
||||
|
||||
const modelInstanceDoc = await composeDBClient.context.loadDoc(
|
||||
ceramicProfileId,
|
||||
);
|
||||
if (modelInstanceDoc == null) {
|
||||
// their profile document wasn't found, now what? Why would this happen?
|
||||
// should we dereference the ceramicProfileId from their player record?
|
||||
return {
|
||||
success: false,
|
||||
ceramic: CONFIG.ceramicURL,
|
||||
};
|
||||
}
|
||||
const values = composeDBToHasuraProfile(
|
||||
modelInstanceDoc.content as ComposeDBProfile,
|
||||
);
|
||||
|
||||
let did = null;
|
||||
|
||||
try {
|
||||
({ did } = await Caip10Link.fromAccount(
|
||||
composeDBClient.context.ceramic,
|
||||
// mainnet; the site prompts them to switch if necessary
|
||||
`${ethereumAddress.toLowerCase()}@eip155:1`,
|
||||
));
|
||||
|
||||
try {
|
||||
fields.push(...Object.keys(values));
|
||||
values.playerId = playerId;
|
||||
|
||||
await client.UpsertProfile({
|
||||
objects: [values],
|
||||
updateColumns: fields as Profile_Update_Column[],
|
||||
});
|
||||
} catch (err) {
|
||||
if (
|
||||
(err as Error).message.includes(
|
||||
'violates unique constraint "profile_username_key"',
|
||||
)
|
||||
) {
|
||||
// this is brittle and likely subject to exploit
|
||||
values.username = `${values.username}-${(did ?? ethereumAddress).slice(
|
||||
-8,
|
||||
)}`;
|
||||
|
||||
await client.UpsertProfile({
|
||||
objects: [values],
|
||||
updateColumns: fields as Profile_Update_Column[],
|
||||
});
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// if (did) {
|
||||
// const alsoKnownAs = ((await store.get('alsoKnownAs', did)) ??
|
||||
// {}) as AlsoKnownAs;
|
||||
// const { accounts = [] } = alsoKnownAs;
|
||||
|
||||
// await Promise.all(
|
||||
// accounts?.map(async ({ host, id: username }: Account) => {
|
||||
// const service = host
|
||||
// ?.replace(/\.com$/, '')
|
||||
// .toUpperCase() as AccountType_Enum;
|
||||
// if (!service) {
|
||||
// console.error(`No hostname for AlsoKnownAs: "${host}"`);
|
||||
// } else {
|
||||
// // If the account has been registered previously, this will
|
||||
// // destructively assign it to the current user removing any
|
||||
// // other users.
|
||||
// //
|
||||
// // ToDo: Examine the JWT to validate that it came from a
|
||||
// // trusted source. Specifically, either the IdentityLink
|
||||
// // service backing //self.id or one established by MetaGame
|
||||
// const { insert_player_account: insert } =
|
||||
// await client.UpsertAccount({
|
||||
// objects: [
|
||||
// {
|
||||
// playerId,
|
||||
// type: service,
|
||||
// identifier: username,
|
||||
// },
|
||||
// ],
|
||||
// });
|
||||
// if (insert?.affected_rows === undefined) {
|
||||
// // eslint-disable-next-line no-console
|
||||
// console.warn(
|
||||
// `Unable to insert ${service} user ${username} for playerId ${playerId}.`,
|
||||
// );
|
||||
// } else if (insert.affected_rows > 0) {
|
||||
// accountLinks.push(service);
|
||||
// }
|
||||
// }
|
||||
// }),
|
||||
// );
|
||||
// }
|
||||
} catch (err) {
|
||||
if (!(err as Error).message.includes('No DID')) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
const ret = {
|
||||
success: true,
|
||||
ceramic: CONFIG.ceramicURL,
|
||||
did,
|
||||
ethereumAddress,
|
||||
accountLinks,
|
||||
fields,
|
||||
};
|
||||
|
||||
return ret;
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
import { queueRecache } from '../../../../lib/cacheHelper.js';
|
||||
import { queueRecache } from '../cacheHelper.js';
|
||||
|
||||
export default async (req: Request, res: Response): Promise<void> => {
|
||||
const role = req.body.session_variables['x-hasura-role'];
|
||||
@@ -1,17 +0,0 @@
|
||||
import express from 'express';
|
||||
|
||||
import { asyncHandlerWrapper } from '../../../lib/apiHelpers.js';
|
||||
import updateExpiredProfilesHandler from './updateExpiredProfiles/handler.js';
|
||||
import updateSingleProfileHandler from './updateSingleProfile/handler.js';
|
||||
|
||||
export const cacheRoutes = express.Router();
|
||||
|
||||
cacheRoutes.post(
|
||||
'/updateSingle',
|
||||
asyncHandlerWrapper(updateSingleProfileHandler),
|
||||
);
|
||||
|
||||
cacheRoutes.post(
|
||||
'/checkExpired',
|
||||
asyncHandlerWrapper(updateExpiredProfilesHandler),
|
||||
);
|
||||
@@ -1,29 +0,0 @@
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
import { queueRecache } from '../../../../lib/cacheHelper.js';
|
||||
import { client } from '../../../../lib/hasuraClient.js';
|
||||
|
||||
const INVALIDATE_AFTER_DAYS = 4; // number of days after which to recache
|
||||
|
||||
export default async (req: Request, res: Response): Promise<void> => {
|
||||
const { limiter } = req.app.locals;
|
||||
const expiration = new Date();
|
||||
const invalidateAfterDays =
|
||||
req.query.invalidate_after_days != null
|
||||
? parseInt(req.query.invalidate_after_days as string, 10)
|
||||
: INVALIDATE_AFTER_DAYS;
|
||||
expiration.setDate(expiration.getDate() - invalidateAfterDays);
|
||||
const { profile: players } = await client.GetCacheEntries({
|
||||
updatedBefore: expiration,
|
||||
});
|
||||
const ids = (
|
||||
await Promise.all(
|
||||
players.map(async ({ playerId }) => {
|
||||
const queued = await queueRecache({ playerId, limiter });
|
||||
return queued ? playerId : null;
|
||||
}),
|
||||
)
|
||||
).filter((id) => !!id);
|
||||
|
||||
res.json({ ids });
|
||||
};
|
||||
@@ -1,253 +0,0 @@
|
||||
import type { CeramicApi } from '@ceramicnetwork/common';
|
||||
import { CeramicClient } from '@ceramicnetwork/http-client';
|
||||
import { Caip10Link } from '@ceramicnetwork/stream-caip10-link';
|
||||
import {
|
||||
Account,
|
||||
AlsoKnownAs,
|
||||
model as alsoKnownAsModel,
|
||||
} from '@datamodels/identity-accounts-web';
|
||||
import {
|
||||
BasicProfile,
|
||||
ImageSources,
|
||||
model as basicProfileModel,
|
||||
} from '@datamodels/identity-profile-basic';
|
||||
import { DataModel } from '@glazed/datamodel';
|
||||
import { DIDDataStore } from '@glazed/did-datastore';
|
||||
import {
|
||||
BasicProfileImages,
|
||||
BasicProfileStrings,
|
||||
ExtendedProfile,
|
||||
ExtendedProfileImages,
|
||||
extendedProfileModel,
|
||||
ExtendedProfileObjects,
|
||||
ExtendedProfileStrings,
|
||||
HasuraProfileProps,
|
||||
maskFor,
|
||||
simplifyAliases,
|
||||
Values,
|
||||
} from '@metafam/utils';
|
||||
import { getLegacy3BoxProfileAsBasicProfile } from '@self.id/3box-legacy';
|
||||
|
||||
import { CONFIG } from '../../../config.js';
|
||||
import {
|
||||
AccountType_Enum,
|
||||
Maybe,
|
||||
Profile_Update_Column,
|
||||
UpdateIdxProfileResponse,
|
||||
} from '../../../lib/autogen/hasura-sdk.js';
|
||||
import { client } from '../../../lib/hasuraClient.js';
|
||||
import { handleMeetWithWalletIntegration } from '../meetwithwallet/handler.js';
|
||||
|
||||
export default async (playerId: string): Promise<UpdateIdxProfileResponse> => {
|
||||
const accountLinks: string[] = [];
|
||||
const fields: string[] = [];
|
||||
const { player_by_pk: player } = await client.GetPlayer({ playerId });
|
||||
const { ethereumAddress } = player ?? {};
|
||||
let did = null;
|
||||
|
||||
if (!ethereumAddress) {
|
||||
throw new Error(`Unknown Player: "${playerId}"`);
|
||||
} else {
|
||||
console.debug(`Updating Profile Cache For ${ethereumAddress}`);
|
||||
}
|
||||
|
||||
const aliases = simplifyAliases([
|
||||
basicProfileModel,
|
||||
extendedProfileModel,
|
||||
alsoKnownAsModel,
|
||||
]);
|
||||
|
||||
try {
|
||||
const ceramic = new CeramicClient(CONFIG.ceramicURL) as CeramicApi;
|
||||
const model = new DataModel({ ceramic, aliases });
|
||||
const store = new DIDDataStore({ ceramic, model });
|
||||
|
||||
({ did } = await Caip10Link.fromAccount(
|
||||
ceramic,
|
||||
// mainnet; the site prompts them to switch if necessary
|
||||
`${ethereumAddress.toLowerCase()}@eip155:1`,
|
||||
));
|
||||
|
||||
const values: HasuraProfileProps = {};
|
||||
let basicProfile: Maybe<BasicProfile> = null;
|
||||
let extendedProfile: Maybe<ExtendedProfile> = null;
|
||||
|
||||
if (!did) {
|
||||
console.debug(`No CAIP-10 Link For ${ethereumAddress}`);
|
||||
} else {
|
||||
basicProfile = await store.get('basicProfile', did);
|
||||
}
|
||||
|
||||
if (!basicProfile) {
|
||||
basicProfile = await getLegacy3BoxProfileAsBasicProfile(ethereumAddress);
|
||||
}
|
||||
|
||||
if (!basicProfile) {
|
||||
console.debug(`No Basic Profile For: ${ethereumAddress} (${did})`);
|
||||
} else {
|
||||
Object.entries(BasicProfileStrings).forEach(([hasuraId, ceramicId]) => {
|
||||
const fromKey = ceramicId as Values<typeof BasicProfileStrings>;
|
||||
const toKey = hasuraId as keyof typeof BasicProfileStrings;
|
||||
if (basicProfile?.[fromKey] != null) {
|
||||
values[toKey] = (basicProfile[fromKey] as string) ?? null;
|
||||
}
|
||||
});
|
||||
Object.entries(BasicProfileImages).forEach(([hasuraId, ceramicId]) => {
|
||||
const fromKey = ceramicId as Values<typeof BasicProfileImages>;
|
||||
const toKey = hasuraId as keyof typeof BasicProfileImages;
|
||||
values[toKey] =
|
||||
(basicProfile?.[fromKey] as ImageSources)?.original.src ?? null;
|
||||
});
|
||||
}
|
||||
|
||||
if (did) {
|
||||
extendedProfile = await store.get('extendedProfile', did);
|
||||
|
||||
if (extendedProfile == null) {
|
||||
console.debug(`No Extended Profile For: ${ethereumAddress} (${did})`);
|
||||
} else {
|
||||
Object.entries(ExtendedProfileStrings).forEach(
|
||||
([hasuraId, ceramicId]) => {
|
||||
const fromKey = ceramicId as Values<typeof ExtendedProfileStrings>;
|
||||
const toKey = hasuraId as keyof typeof ExtendedProfileStrings;
|
||||
|
||||
if (extendedProfile?.[fromKey] != null) {
|
||||
values[toKey] = (extendedProfile[fromKey] as string) ?? null;
|
||||
}
|
||||
},
|
||||
);
|
||||
if (extendedProfile.meetWithWalletDomain != null) {
|
||||
await handleMeetWithWalletIntegration(
|
||||
playerId,
|
||||
extendedProfile.meetWithWalletDomain,
|
||||
);
|
||||
}
|
||||
Object.entries(ExtendedProfileImages).forEach(
|
||||
([hasuraId, ceramicId]) => {
|
||||
const fromKey = ceramicId as Values<typeof ExtendedProfileImages>;
|
||||
const toKey = hasuraId as keyof typeof ExtendedProfileImages;
|
||||
values[toKey] =
|
||||
(extendedProfile?.[fromKey] as ImageSources)?.original.src ??
|
||||
null;
|
||||
},
|
||||
);
|
||||
Object.entries(ExtendedProfileObjects).forEach(
|
||||
([hasuraId, ceramicId]) => {
|
||||
const fromKey = ceramicId as Values<typeof ExtendedProfileObjects>;
|
||||
const toKey = hasuraId as keyof typeof ExtendedProfileObjects;
|
||||
if (extendedProfile?.[fromKey] != null) {
|
||||
switch (fromKey) {
|
||||
case 'availableHours': {
|
||||
values.availableHours = extendedProfile.availableHours;
|
||||
break;
|
||||
}
|
||||
case 'magicDisposition': {
|
||||
values.colorMask =
|
||||
maskFor(extendedProfile.magicDisposition) ?? undefined;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
console.info('Unrecognized Key', { fromKey, toKey });
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!basicProfile && !extendedProfile) {
|
||||
console.info(`No Profile Information For ${ethereumAddress}.`);
|
||||
} else {
|
||||
try {
|
||||
fields.push(...Object.keys(values));
|
||||
values.playerId = playerId;
|
||||
|
||||
await client.UpsertProfile({
|
||||
objects: [values],
|
||||
updateColumns: fields as Profile_Update_Column[],
|
||||
});
|
||||
} catch (err) {
|
||||
if (
|
||||
!(err as Error).message.includes(
|
||||
'violates unique constraint "profile_username_key"',
|
||||
)
|
||||
) {
|
||||
throw err;
|
||||
} else {
|
||||
// this is brittle and likely subject to exploit
|
||||
values.username = `${values.username}-${(
|
||||
did ?? ethereumAddress
|
||||
).slice(-8)}`;
|
||||
|
||||
await client.UpsertProfile({
|
||||
objects: [values],
|
||||
updateColumns: fields as Profile_Update_Column[],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (did) {
|
||||
const alsoKnownAs = ((await store.get('alsoKnownAs', did)) ??
|
||||
{}) as AlsoKnownAs;
|
||||
const { accounts = [] } = alsoKnownAs;
|
||||
|
||||
await Promise.all(
|
||||
accounts?.map(async ({ host, id: username }: Account) => {
|
||||
const service = host
|
||||
?.replace(/\.com$/, '')
|
||||
.toUpperCase() as AccountType_Enum;
|
||||
if (!service) {
|
||||
console.error(`No hostname for AlsoKnownAs: "${host}"`);
|
||||
} else {
|
||||
// If the account has been registered previously, this will
|
||||
// destructively assign it to the current user removing any
|
||||
// other users.
|
||||
//
|
||||
// ToDo: Examine the JWT to validate that it came from a
|
||||
// trusted source. Specifically, either the IdentityLink
|
||||
// service backing //self.id or one established by MetaGame
|
||||
const { insert_player_account: insert } =
|
||||
await client.UpsertAccount({
|
||||
objects: [
|
||||
{
|
||||
playerId,
|
||||
type: service,
|
||||
identifier: username,
|
||||
},
|
||||
],
|
||||
});
|
||||
if (insert?.affected_rows === undefined) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
`Unable to insert ${service} user ${username} for playerId ${playerId}.`,
|
||||
);
|
||||
} else if (insert.affected_rows > 0) {
|
||||
accountLinks.push(service);
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
if (!(err as Error).message.includes('No DID')) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
const ret = {
|
||||
success: true,
|
||||
ceramic: CONFIG.ceramicURL,
|
||||
did,
|
||||
ethereumAddress,
|
||||
accountLinks,
|
||||
fields,
|
||||
};
|
||||
|
||||
if (fields.length === 0) {
|
||||
Object.assign(ret, { aliases });
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
@@ -1,18 +1,16 @@
|
||||
import express from 'express';
|
||||
|
||||
import { asyncHandlerWrapper } from '../../lib/apiHelpers.js';
|
||||
import { ceramicRoutes } from './ceramic/routes.js';
|
||||
import { routes as composeDBRoutes } from './composeDB/routes.js';
|
||||
import { guildRoutes } from './guild/routes.js';
|
||||
import { syncAllGuildDiscordMembers } from './guild/sync.js';
|
||||
import { cacheRoutes } from './idxCache/routes.js';
|
||||
import { questsRoutes } from './quests/routes.js';
|
||||
import { syncSourceCredAccounts } from './sourcecred/sync.js';
|
||||
|
||||
export const actionRoutes = express.Router();
|
||||
|
||||
actionRoutes.use('/ceramic', ceramicRoutes);
|
||||
actionRoutes.use('/composeDB', composeDBRoutes);
|
||||
actionRoutes.use('/guild', guildRoutes);
|
||||
actionRoutes.use('/idxCache', cacheRoutes);
|
||||
|
||||
actionRoutes.post(
|
||||
'/syncSourceCredAccounts',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Bottleneck from 'bottleneck';
|
||||
|
||||
import { cacheProfile } from '../../lib/cacheHelper.js';
|
||||
import { client } from '../../lib/hasuraClient.js';
|
||||
import { cacheProfile } from '../actions/composeDB/cacheHelper.js';
|
||||
|
||||
async function createPlayer(ethAddress: string, limiter: Bottleneck) {
|
||||
const { insert_profile: insert } = await client.CreatePlayerFromETH({
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import Bottleneck from 'bottleneck';
|
||||
|
||||
import { Player } from '../../lib/autogen/hasura-sdk.js';
|
||||
import { queueRecache } from '../../lib/cacheHelper.js';
|
||||
import { queueRecache } from '../actions/composeDB/cacheHelper.js';
|
||||
import { TriggerPayload } from './types.js';
|
||||
|
||||
// This trigger is called when new accounts are created.
|
||||
export const cacheIDXProfile = async (
|
||||
export const cacheComposeDBProfile = async (
|
||||
payload: TriggerPayload<Player>,
|
||||
limiter: Bottleneck,
|
||||
) => {
|
||||
@@ -3,13 +3,13 @@ import { ParamsDictionary } from 'express-serve-static-core';
|
||||
|
||||
import { Guild, Player, Player_Role } from '../../lib/autogen/hasura-sdk.js';
|
||||
import { syncDiscordGuildMembers } from '../actions/guild/sync.js';
|
||||
import { cacheIDXProfile } from './cacheIDXProfile.js';
|
||||
import { cacheComposeDBProfile } from './cacheComposeDBProfile.js';
|
||||
import { playerRankUpdated } from './playerRankUpdated.js';
|
||||
import { playerRoleChanged } from './playerRoleChanged.js';
|
||||
import { TriggerPayload } from './types.js';
|
||||
|
||||
const TRIGGERS = {
|
||||
cacheIDXProfile,
|
||||
cacheComposeDBProfile,
|
||||
playerRankUpdated,
|
||||
playerRoleChanged,
|
||||
syncDiscordGuildMembers,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Values } from './extendedProfileTypes.js';
|
||||
import { Values } from '../extendedProfileTypes.js';
|
||||
|
||||
export const composeDBProfileFieldName = 'name';
|
||||
export const composeDBProfileFieldDescription = 'description';
|
||||
@@ -99,23 +99,3 @@ export const hasuraImageFields = [
|
||||
] as const;
|
||||
|
||||
export type HasuraImageFieldKey = typeof hasuraImageFields[number];
|
||||
|
||||
// typesafe Array.includes, see https://fettblog.eu/typescript-array-includes/
|
||||
function includes<T extends U, U>(coll: ReadonlyArray<T>, el: U): el is T {
|
||||
return coll.includes(el as T);
|
||||
}
|
||||
|
||||
export function isComposeDBImageField(key: string) {
|
||||
return includes(composeDBImageFields, key);
|
||||
}
|
||||
|
||||
export function isHasuraImageField(key: string) {
|
||||
return includes(hasuraImageFields, key);
|
||||
}
|
||||
|
||||
export function isImageMetadata(value: ComposeDBPayloadValue) {
|
||||
const maybeImageMetadata = value as ComposeDBImageMetadata;
|
||||
return (
|
||||
maybeImageMetadata?.url != null && maybeImageMetadata?.mimeType != null
|
||||
);
|
||||
}
|
||||
59
packages/utils/src/composeDB/utils.ts
Normal file
59
packages/utils/src/composeDB/utils.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { maskFor } from '../colorHelpers.js';
|
||||
import {
|
||||
ComposeDBField,
|
||||
composeDBImageFields,
|
||||
ComposeDBImageMetadata,
|
||||
ComposeDBPayloadValue,
|
||||
ComposeDBProfile,
|
||||
composeDBProfileFieldFiveColorDisposition,
|
||||
hasuraImageFields,
|
||||
profileMapping,
|
||||
} from './fields.js';
|
||||
|
||||
// typesafe Array.includes, see https://fettblog.eu/typescript-array-includes/
|
||||
function includes<T extends U, U>(coll: ReadonlyArray<T>, el: U): el is T {
|
||||
return coll.includes(el as T);
|
||||
}
|
||||
|
||||
export function isComposeDBImageField(key: string) {
|
||||
return includes(composeDBImageFields, key);
|
||||
}
|
||||
|
||||
export function isHasuraImageField(key: string) {
|
||||
return includes(hasuraImageFields, key);
|
||||
}
|
||||
|
||||
export function isImageMetadata(value: ComposeDBPayloadValue) {
|
||||
const maybeImageMetadata = value as ComposeDBImageMetadata;
|
||||
return (
|
||||
maybeImageMetadata?.url != null && maybeImageMetadata?.mimeType != null
|
||||
);
|
||||
}
|
||||
|
||||
export const composeDBToHasuraProfile = (
|
||||
composeDBProfile: ComposeDBProfile,
|
||||
) => {
|
||||
// todo we should be able to make this typesafe
|
||||
const hasuraProfile: Record<string, unknown> = {};
|
||||
|
||||
Object.entries(composeDBProfile).forEach(([key, value]) => {
|
||||
const match = Object.entries(profileMapping).find(
|
||||
([, composeDBKey]) => composeDBKey === key,
|
||||
) as [keyof typeof profileMapping, ComposeDBField];
|
||||
|
||||
const hasuraKey = match[0];
|
||||
|
||||
// Some fields required custom translations
|
||||
let hasuraValue = value;
|
||||
if (value && key === composeDBProfileFieldFiveColorDisposition) {
|
||||
const maskNumber = maskFor(value as string);
|
||||
if (maskNumber != null) {
|
||||
hasuraValue = maskNumber;
|
||||
}
|
||||
} else if (value && isComposeDBImageField(key)) {
|
||||
hasuraValue = (value as ComposeDBImageMetadata).url;
|
||||
}
|
||||
hasuraProfile[hasuraKey] = hasuraValue;
|
||||
});
|
||||
return hasuraProfile;
|
||||
};
|
||||
@@ -5,7 +5,8 @@ import extendedProfileModel from './ExtendedProfileModel.json' assert { type: 'j
|
||||
export * from './arrayHelpers.js';
|
||||
export * from './ceramic.js';
|
||||
export * from './colorHelpers.js';
|
||||
export * from './composeDBProfileFields.js';
|
||||
export * from './composeDB/fields.js';
|
||||
export * from './composeDB/utils.js';
|
||||
export * as Constants from './constants.js';
|
||||
export * as did from './did/index.js';
|
||||
export * as DiscordUtil from './discordHelpers.js';
|
||||
|
||||
@@ -36,7 +36,11 @@ import {
|
||||
isHasuraImageField,
|
||||
profileMapping,
|
||||
} from '@metafam/utils';
|
||||
import { Maybe, Player, Profile } from 'graphql/autogen/types';
|
||||
import {
|
||||
Maybe,
|
||||
Player,
|
||||
useInsertCacheInvalidationMutation,
|
||||
} from 'graphql/autogen/types';
|
||||
import { getPlayer } from 'graphql/getPlayer';
|
||||
import { PlayerProfile } from 'graphql/types';
|
||||
import { useWeb3 } from 'lib/hooks';
|
||||
@@ -106,6 +110,7 @@ export const EditProfileModal: React.FC<EditProfileModalProps> = ({
|
||||
const username = player.profile?.username;
|
||||
|
||||
const { save } = useSaveToComposeDB();
|
||||
const [, invalidateCache] = useInsertCacheInvalidationMutation();
|
||||
|
||||
const initialFormValues = useMemo(
|
||||
() => getDefaultFormValues(player),
|
||||
@@ -234,6 +239,11 @@ export const EditProfileModal: React.FC<EditProfileModalProps> = ({
|
||||
const payload = hasuraToComposeDBProfile(profile, profileImages);
|
||||
const ceramicStreamID = await save(payload);
|
||||
|
||||
if (player) {
|
||||
setStatus('Invalidating Cache…');
|
||||
await invalidateCache({ playerId: player.id });
|
||||
}
|
||||
|
||||
// if they changed their username, the page will 404 on reload
|
||||
if (player && inputs.username !== username) {
|
||||
window.history.replaceState(
|
||||
|
||||
@@ -9,7 +9,6 @@ export const MetaLink: React.FC<Props> = ({
|
||||
children,
|
||||
href,
|
||||
as,
|
||||
passHref,
|
||||
replace,
|
||||
scroll = true,
|
||||
shallow,
|
||||
@@ -33,7 +32,8 @@ export const MetaLink: React.FC<Props> = ({
|
||||
scroll,
|
||||
shallow,
|
||||
}}
|
||||
passHref={passHref || true}
|
||||
passHref={true}
|
||||
legacyBehavior={true}
|
||||
>
|
||||
{/* NextLink passes the href */}
|
||||
<Link color="cyan.400" {...props}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export const InsertCacheInvalidation = /* GraphQL */ `
|
||||
mutation InsertCacheInvalidation($playerId: uuid!) {
|
||||
updateIDXProfile(playerId: $playerId)
|
||||
updateCachedProfile(playerId: $playerId)
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import {
|
||||
ComposeDBField,
|
||||
ComposeDBImageMetadata,
|
||||
ComposeDBProfile,
|
||||
composeDBProfileFieldFiveColorDisposition,
|
||||
isComposeDBImageField,
|
||||
maskFor,
|
||||
composeDBToHasuraProfile,
|
||||
Maybe,
|
||||
profileMapping,
|
||||
} from '@metafam/utils';
|
||||
import { Player } from 'graphql/autogen/types';
|
||||
import { buildPlayerProfileQuery } from 'graphql/composeDB/queries/profile';
|
||||
@@ -82,31 +77,3 @@ export const hydratePlayerProfile = (
|
||||
}
|
||||
return player;
|
||||
};
|
||||
|
||||
export const composeDBToHasuraProfile = (
|
||||
composeDBProfile: ComposeDBProfile,
|
||||
) => {
|
||||
// todo we should be able to make this typesafe
|
||||
const hasuraProfile: Record<string, unknown> = {};
|
||||
|
||||
Object.entries(composeDBProfile).forEach(([key, value]) => {
|
||||
const match = Object.entries(profileMapping).find(
|
||||
([, composeDBKey]) => composeDBKey === key,
|
||||
) as [keyof typeof profileMapping, ComposeDBField];
|
||||
|
||||
const hasuraKey = match[0];
|
||||
|
||||
// Some fields required custom translations
|
||||
let hasuraValue = value;
|
||||
if (value && key === composeDBProfileFieldFiveColorDisposition) {
|
||||
const maskNumber = maskFor(value as string);
|
||||
if (maskNumber != null) {
|
||||
hasuraValue = maskNumber;
|
||||
}
|
||||
} else if (value && isComposeDBImageField(key)) {
|
||||
hasuraValue = (value as ComposeDBImageMetadata).url;
|
||||
}
|
||||
hasuraProfile[hasuraKey] = hasuraValue;
|
||||
});
|
||||
return hasuraProfile;
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
profileMapping,
|
||||
} from '@metafam/utils';
|
||||
import { useSetupFlow } from 'contexts/SetupContext';
|
||||
import { useInsertCacheInvalidationMutation } from 'graphql/autogen/types';
|
||||
import { PlayerProfile } from 'graphql/types';
|
||||
import { CeramicError } from 'lib/errors';
|
||||
import { ReactElement, useCallback, useEffect, useState } from 'react';
|
||||
@@ -20,6 +21,7 @@ import { getImageDimensions } from 'utils/imageHelpers';
|
||||
import { uploadFile } from 'utils/uploadHelpers';
|
||||
|
||||
import { FileReaderData } from '../useImageReader';
|
||||
import { useUser } from '../useUser';
|
||||
import { useSaveToComposeDB } from './useSaveToComposeDB';
|
||||
|
||||
export type PlayerSetupSaveToComposeDBProps = {
|
||||
@@ -34,18 +36,12 @@ export function usePlayerSetupSaveToComposeDB({
|
||||
pickedFile: fileData,
|
||||
}: PlayerSetupSaveToComposeDBProps) {
|
||||
const toast = useToast();
|
||||
const { user } = useUser();
|
||||
const { onNextPress } = useSetupFlow();
|
||||
const [status, setStatus] = useState<Maybe<string | ReactElement>>();
|
||||
|
||||
const { save: saveToComposeDB, status: saveStatus } = useSaveToComposeDB();
|
||||
|
||||
const persist = useCallback(
|
||||
(values: Record<string, unknown>) => {
|
||||
setStatus('Saving to Ceramic…');
|
||||
return saveToComposeDB(values);
|
||||
},
|
||||
[saveToComposeDB],
|
||||
);
|
||||
const [, invalidateCache] = useInsertCacheInvalidationMutation();
|
||||
|
||||
useEffect(() => {
|
||||
if (saveStatus === 'authenticating') {
|
||||
@@ -94,8 +90,13 @@ export function usePlayerSetupSaveToComposeDB({
|
||||
payload[composeDBProfileFieldAvatar] = cleanImageMetadata;
|
||||
}
|
||||
|
||||
setStatus('Saving…');
|
||||
nodeId = await persist(payload);
|
||||
setStatus('Saving to ComposeDB…');
|
||||
nodeId = await saveToComposeDB(payload);
|
||||
|
||||
if (user) {
|
||||
setStatus('Invalidating Cache…');
|
||||
await invalidateCache({ playerId: user.id });
|
||||
}
|
||||
}
|
||||
|
||||
if (onComplete) {
|
||||
@@ -116,7 +117,16 @@ export function usePlayerSetupSaveToComposeDB({
|
||||
setStatus(null);
|
||||
}
|
||||
},
|
||||
[fileData, isChanged, onComplete, onNextPress, persist, toast],
|
||||
[
|
||||
fileData,
|
||||
invalidateCache,
|
||||
isChanged,
|
||||
onComplete,
|
||||
onNextPress,
|
||||
saveToComposeDB,
|
||||
toast,
|
||||
user,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -17,6 +17,7 @@ export const useSaveToComposeDB = () => {
|
||||
|
||||
// When saving to ComposeDB, it's essential that we have the most recent value for user.ceramicProfileId so that we don't inadvertently create duplicate profile models. Thus, we specify 'network-only' to ALWAYS fetch the latest value of 'user' from Hasura.
|
||||
// Ideally, we should be invalidating the urql cache when persisting the ceramicProfileId below, but that would require switching over to Normalized caching (https://formidable.com/open-source/urql/docs/graphcache/normalized-caching/) which could be a big lift requiring a bunch of testing
|
||||
// This is likely causing the warning "Cannot update a component while rendering a different component"
|
||||
const { user } = useUser({ requestPolicy: 'network-only' });
|
||||
|
||||
const [, linkNode] = useLinkOwnCeramicNodeMutation();
|
||||
|
||||
@@ -1076,6 +1076,14 @@ type DiscordGuildAuthResponse {
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type DiscordGuildsSyncOutput {
|
||||
name: String!
|
||||
numDeleted: Int
|
||||
numInserted: Int
|
||||
numSkipped: Int
|
||||
username: String!
|
||||
}
|
||||
|
||||
type DiscordRole {
|
||||
id: String!
|
||||
name: String!
|
||||
@@ -4083,14 +4091,19 @@ type mutation_root {
|
||||
saveGuildLayout(guildLayoutInfo: GuildLayoutInfoInput!): SaveGuildLayoutResponse
|
||||
|
||||
"""
|
||||
perform the action: "updateExpiredIDXProfiles"
|
||||
perform the action: "syncAllGuildDiscordMembers"
|
||||
"""
|
||||
updateExpiredIDXProfiles: ExpiredPlayerProfiles
|
||||
syncAllGuildDiscordMembers: [DiscordGuildsSyncOutput]
|
||||
|
||||
"""
|
||||
perform the action: "updateIDXProfile"
|
||||
perform the action: "syncSourceCredAccounts"
|
||||
"""
|
||||
updateIDXProfile(playerId: uuid): uuid!
|
||||
syncSourceCredAccounts: SourceCredSyncOutput
|
||||
|
||||
"""
|
||||
perform the action: "updateCachedProfile"
|
||||
"""
|
||||
updateCachedProfile(playerId: uuid): uuid!
|
||||
|
||||
"""
|
||||
perform the action: "updateQuestCompletion"
|
||||
@@ -8452,14 +8465,14 @@ type query_root {
|
||||
skill_by_pk(id: uuid!): skill
|
||||
|
||||
"""
|
||||
retrieve the result of action: "updateIDXProfile"
|
||||
retrieve the result of action: "updateCachedProfile"
|
||||
"""
|
||||
updateIDXProfile(
|
||||
updateCachedProfile(
|
||||
"""
|
||||
id of the action: "updateIDXProfile"
|
||||
id of the action: "updateCachedProfile"
|
||||
"""
|
||||
id: uuid!
|
||||
): updateIDXProfile
|
||||
): updateCachedProfile
|
||||
}
|
||||
|
||||
"""
|
||||
@@ -10863,6 +10876,13 @@ enum SkillCategory_update_column {
|
||||
name
|
||||
}
|
||||
|
||||
type SourceCredSyncOutput {
|
||||
numInserted: Int!
|
||||
numSkipped: Int!
|
||||
numUnclaimed: Int!
|
||||
numUpdated: Int!
|
||||
}
|
||||
|
||||
"""
|
||||
expression to compare columns of type String. All fields are combined with logical 'AND'.
|
||||
"""
|
||||
@@ -12046,14 +12066,14 @@ type subscription_root {
|
||||
skill_by_pk(id: uuid!): skill
|
||||
|
||||
"""
|
||||
retrieve the result of action: "updateIDXProfile"
|
||||
retrieve the result of action: "updateCachedProfile"
|
||||
"""
|
||||
updateIDXProfile(
|
||||
updateCachedProfile(
|
||||
"""
|
||||
id of the action: "updateIDXProfile"
|
||||
id of the action: "updateCachedProfile"
|
||||
"""
|
||||
id: uuid!
|
||||
): updateIDXProfile
|
||||
): updateCachedProfile
|
||||
}
|
||||
|
||||
scalar timestamptz
|
||||
@@ -12080,9 +12100,9 @@ type TokenBalances {
|
||||
}
|
||||
|
||||
"""
|
||||
fields of action: "updateIDXProfile"
|
||||
fields of action: "updateCachedProfile"
|
||||
"""
|
||||
type updateIDXProfile {
|
||||
type updateCachedProfile {
|
||||
"""the time at which this action was created"""
|
||||
created_at: timestamptz
|
||||
|
||||
|
||||
Reference in New Issue
Block a user