Skip to content

Commit d7b6162

Browse files
authored
Add the ability to query a Realm (#7)
1 parent 1c9b156 commit d7b6162

File tree

107 files changed

+6695
-83
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

107 files changed

+6695
-83
lines changed

migrations/1661291478011-Feed.ts

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { MigrationInterface, QueryRunner } from "typeorm";
2+
3+
export class Feed1661291478011 implements MigrationInterface {
4+
name = 'Feed1661291478011'
5+
6+
public async up(queryRunner: QueryRunner): Promise<void> {
7+
await queryRunner.query(`
8+
CREATE TABLE "realm_feed_item" (
9+
"id" SERIAL NOT NULL,
10+
"data" jsonb NOT NULL,
11+
"environment" character varying NOT NULL,
12+
"metadata" jsonb NOT NULL,
13+
"realmPublicKeyStr" character varying NOT NULL,
14+
"created" TIMESTAMP NOT NULL DEFAULT now(),
15+
"deleted" TIMESTAMP,
16+
"updated" TIMESTAMP WITH TIME ZONE NOT NULL,
17+
CONSTRAINT "PK_e24c04da3892a7573c1aa0d37ef" PRIMARY KEY ("id")
18+
)
19+
`);
20+
await queryRunner.query(`
21+
CREATE TABLE "realm_post" (
22+
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
23+
"data" jsonb NOT NULL,
24+
"environment" character varying NOT NULL,
25+
"realmPublicKeyStr" character varying NOT NULL,
26+
"created" TIMESTAMP NOT NULL DEFAULT now(),
27+
"deleted" TIMESTAMP,
28+
"updated" TIMESTAMP NOT NULL DEFAULT now(),
29+
CONSTRAINT "PK_656c881149a9a927ec98733bcc0" PRIMARY KEY ("id")
30+
)
31+
`);
32+
}
33+
34+
public async down(queryRunner: QueryRunner): Promise<void> {
35+
await queryRunner.query(`
36+
DROP TABLE "realm_post"
37+
`);
38+
await queryRunner.query(`
39+
DROP TABLE "realm_feed_item"
40+
`);
41+
}
42+
43+
}

migrations/1661458148129-FeedVote.ts

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { MigrationInterface, QueryRunner } from "typeorm";
2+
3+
export class FeedVote1661458148129 implements MigrationInterface {
4+
name = 'FeedVote1661458148129'
5+
6+
public async up(queryRunner: QueryRunner): Promise<void> {
7+
await queryRunner.query(`
8+
CREATE TABLE "realm_feed_item_vote" (
9+
"feedItemId" integer NOT NULL,
10+
"userId" uuid NOT NULL,
11+
"realmPublicKeyStr" character varying NOT NULL,
12+
"data" jsonb NOT NULL,
13+
"created" TIMESTAMP NOT NULL DEFAULT now(),
14+
"deleted" TIMESTAMP,
15+
"updated" TIMESTAMP NOT NULL DEFAULT now(),
16+
CONSTRAINT "PK_b38f37a82c7f2026c4615998e67" PRIMARY KEY ("feedItemId", "userId", "realmPublicKeyStr")
17+
)
18+
`);
19+
}
20+
21+
public async down(queryRunner: QueryRunner): Promise<void> {
22+
await queryRunner.query(`
23+
DROP TABLE "realm_feed_item_vote"
24+
`);
25+
}
26+
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { MigrationInterface, QueryRunner } from "typeorm";
2+
3+
export class RealmPostAuthor1661530384910 implements MigrationInterface {
4+
name = 'RealmPostAuthor1661530384910'
5+
6+
public async up(queryRunner: QueryRunner): Promise<void> {
7+
await queryRunner.query(`
8+
ALTER TABLE "realm_post"
9+
ADD "authorId" uuid NOT NULL
10+
`);
11+
await queryRunner.query(`
12+
ALTER TABLE "realm_post"
13+
ADD CONSTRAINT "FK_9d06be18d3a8c7e9aa11c983716" FOREIGN KEY ("authorId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION
14+
`);
15+
}
16+
17+
public async down(queryRunner: QueryRunner): Promise<void> {
18+
await queryRunner.query(`
19+
ALTER TABLE "realm_post" DROP CONSTRAINT "FK_9d06be18d3a8c7e9aa11c983716"
20+
`);
21+
await queryRunner.query(`
22+
ALTER TABLE "realm_post" DROP COLUMN "authorId"
23+
`);
24+
}
25+
26+
}

package.json

+12
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"test:e2e": "jest --config ./test/jest-e2e.json"
2727
},
2828
"dependencies": {
29+
"@cardinal/namespaces": "^4.1.56",
2930
"@nestjs/common": "^9.0.0",
3031
"@nestjs/config": "^2.2.0",
3132
"@nestjs/core": "^9.0.0",
@@ -36,16 +37,26 @@
3637
"@nestjs/platform-express": "^9.0.0",
3738
"@nestjs/platform-fastify": "^9.0.8",
3839
"@nestjs/typeorm": "^9.0.0",
40+
"@solana/spl-governance": "^0.3.1",
41+
"@solana/spl-token-registry": "^0.2.4574",
3942
"@solana/web3.js": "^1.50.1",
43+
"@types/graphql-type-json": "^0.3.2",
4044
"altair-fastify-plugin": "^4.5.3",
45+
"bignumber.js": "^9.1.0",
46+
"cache-manager": "^4.1.0",
4147
"date-fns": "^2.29.1",
4248
"fp-ts": "^2.12.2",
4349
"graphql": "^16.5.0",
50+
"graphql-relay": "^0.10.0",
51+
"graphql-request": "^4.3.0",
52+
"graphql-type-json": "^0.3.2",
4453
"graphql-upload": "13.0.0",
4554
"io-ts": "^2.2.17",
4655
"io-ts-types": "^0.5.16",
4756
"mercurius": "^9",
4857
"mercurius-upload": "^5.0.0",
58+
"monocle-ts": "^2.3.13",
59+
"newtype-ts": "^0.3.5",
4960
"passport": "^0.6.0",
5061
"passport-jwt": "^4.0.0",
5162
"pg": "^8.7.3",
@@ -60,6 +71,7 @@
6071
"@nestjs/cli": "^9.0.0",
6172
"@nestjs/schematics": "^9.0.0",
6273
"@nestjs/testing": "^9.0.0",
74+
"@types/cache-manager": "^4.0.1",
6375
"@types/eslint": "^8.4.5",
6476
"@types/eslint-plugin-prettier": "^3.1.0",
6577
"@types/express": "^4.17.13",

src/app.module.ts

+27-4
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,26 @@ import { MercuriusDriver, MercuriusDriverConfig } from '@nestjs/mercurius';
66
import { TypeOrmModule } from '@nestjs/typeorm';
77
import mercurius from 'mercurius';
88

9-
// import { NonceScalar } from '@lib/scalars/Nonce';
9+
import { BigNumberScalar } from '@lib/scalars/BigNumber';
10+
import { CursorScalar } from '@lib/scalars/Cursor';
1011
import { PublicKeyScalar } from '@lib/scalars/PublicKey';
12+
import { RichTextDocumentScalar } from '@lib/scalars/RichTextDocument';
1113
import { AppController } from '@src/app.controller';
1214
import { AppService } from '@src/app.service';
1315
import { AuthModule } from '@src/auth/auth.module';
1416
import { ConfigModule } from '@src/config/config.module';
1517
import { ConfigService } from '@src/config/config.service';
16-
17-
import { UserModule } from './user/user.module';
18+
import { HolaplexModule } from '@src/holaplex/holaplex.module';
19+
import { OnChainModule } from '@src/on-chain/on-chain.module';
20+
import { RealmFeedItemModule } from '@src/realm-feed-item/realm-feed-item.module';
21+
import { RealmFeedModule } from '@src/realm-feed/realm-feed.module';
22+
import { RealmMemberModule } from '@src/realm-member/realm-member.module';
23+
import { RealmPostModule } from '@src/realm-post/realm-post.module';
24+
import { RealmProposalModule } from '@src/realm-proposal/realm-proposal.module';
25+
import { RealmSettingsModule } from '@src/realm-settings/realm-settings.module';
26+
import { RealmTreasuryModule } from '@src/realm-treasury/realm-treasury.module';
27+
import { RealmModule } from '@src/realm/realm.module';
28+
import { UserModule } from '@src/user/user.module';
1829

1930
@Module({
2031
imports: [
@@ -27,8 +38,10 @@ import { UserModule } from './user/user.module';
2738
driver: MercuriusDriver,
2839
persistedQueryProvider: mercurius.persistedQueryDefaults.automatic(),
2940
resolvers: {
30-
// Nonce: NonceScalar,
41+
BigNumber: BigNumberScalar,
42+
Cursor: CursorScalar,
3143
PublicKey: PublicKeyScalar,
44+
RichTextDocument: RichTextDocumentScalar,
3245
},
3346
sortSchema: true,
3447
}),
@@ -49,6 +62,16 @@ import { UserModule } from './user/user.module';
4962
}),
5063
AuthModule,
5164
UserModule,
65+
HolaplexModule,
66+
RealmModule,
67+
RealmMemberModule,
68+
RealmProposalModule,
69+
RealmFeedModule,
70+
RealmFeedItemModule,
71+
RealmSettingsModule,
72+
RealmPostModule,
73+
RealmTreasuryModule,
74+
OnChainModule,
5275
],
5376
controllers: [AppController],
5477
providers: [AppService],

src/auth/auth.jwt.strategy.ts

+2-4
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,8 @@ export class AuthJwtStrategy extends PassportStrategy(Strategy, 'authJwt') {
2929
this.userService.getUserById(payload.sub),
3030
TE.chainW(TE.fromOption(() => new errors.Unauthorized())),
3131
TE.matchW(
32-
(error) => {
33-
throw error;
34-
},
35-
(user) => ({ ...user.data, publicKey: new PublicKey(user.publicKeyStr) }),
32+
() => null,
33+
(user) => ({ ...user.data, id: user.id, publicKey: new PublicKey(user.publicKeyStr) }),
3634
),
3735
)();
3836
}

src/auth/auth.resolver.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ import { AuthClaim } from './dto/AuthClaim';
1414
export class AuthResolver {
1515
constructor(private readonly authService: AuthService) {}
1616

17-
@Mutation(() => AuthClaim)
17+
@Mutation(() => AuthClaim, {
18+
description:
19+
'Generate an authentication claim that a wallet can sign and trade for an auth token',
20+
})
1821
@EitherResolver()
1922
createAuthenticationClaim(
2023
@Args('publicKey', {
@@ -26,7 +29,9 @@ export class AuthResolver {
2629
return this.authService.generateClaim(publicKey);
2730
}
2831

29-
@Mutation(() => String)
32+
@Mutation(() => String, {
33+
description: 'Trade a signed authentication claim for an auth token',
34+
})
3035
@EitherResolver()
3136
createAuthenticationToken(
3237
@Args('claim', {

src/auth/auth.service.ts

+3
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ export class AuthService {
7272
extractClaimFromClaimStr(claimStr: string) {
7373
return FN.pipe(
7474
claimStr,
75+
// The claim contains a bunch of filler text in the front to make it read
76+
// well when being signed by the wallet. The claim string will be at the
77+
// end.
7578
(str) => str.split(' '),
7679
AR.last,
7780
EI.fromOption(() => new errors.MalformedData()),

src/config/config.module.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { ConfigService } from './config.service';
2020
},
2121
database: {
2222
host: process.env.DATABASE_HOST || '',
23-
name: process.env.DATABASE_NAME,
23+
name: process.env.DATABASE_NAME || '',
2424
password: process.env.DATABASE_PASSWORD,
2525
port: dbPort,
2626
username: process.env.DATABASE_USERNAME,

src/holaplex/holaplex.module.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Module } from '@nestjs/common';
2+
3+
import { HolaplexService } from './holaplex.service';
4+
5+
@Module({
6+
providers: [HolaplexService],
7+
exports: [HolaplexService],
8+
})
9+
export class HolaplexModule {}

src/holaplex/holaplex.service.spec.ts

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
3+
import { HolaplexService } from './holaplex.service';
4+
5+
describe('HolaplexService', () => {
6+
let service: HolaplexService;
7+
8+
beforeEach(async () => {
9+
const module: TestingModule = await Test.createTestingModule({
10+
providers: [HolaplexService],
11+
}).compile();
12+
13+
service = module.get<HolaplexService>(HolaplexService);
14+
});
15+
16+
it('should be defined', () => {
17+
expect(service).toBeDefined();
18+
});
19+
});

src/holaplex/holaplex.service.ts

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Injectable } from '@nestjs/common';
2+
import * as FN from 'fp-ts/function';
3+
import * as TE from 'fp-ts/TaskEither';
4+
import { request, RequestDocument } from 'graphql-request';
5+
import * as IT from 'io-ts';
6+
7+
import * as errors from '@lib/errors/gql';
8+
9+
@Injectable()
10+
export class HolaplexService {
11+
/**
12+
* Make a GQL request to Holaplex's v1 indexer API
13+
*/
14+
requestV1<Variables = any, A = any, O = any, I = any>(
15+
req: {
16+
query: RequestDocument;
17+
variables?: Variables;
18+
},
19+
res: IT.Type<A, O, I>,
20+
) {
21+
return FN.pipe(
22+
TE.tryCatch(
23+
() => request('https://graph.holaplex.com/v1', req.query, req.variables),
24+
(e) => new errors.Exception(e),
25+
),
26+
TE.chainW((result) => TE.fromEither(res.decode(result))),
27+
);
28+
}
29+
}
+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { ExecutionContext } from '@nestjs/common';
2+
import { createParamDecorator } from '@nestjs/common';
3+
import { GqlExecutionContext } from '@nestjs/graphql';
4+
5+
import { Environment } from '@lib/types/Environment';
6+
7+
/**
8+
* Get the current environment for the request, either `'mainnet'` or `'devnet'`.
9+
*/
10+
export const CurrentEnvironment = createParamDecorator(
11+
(data: unknown, context: ExecutionContext) => {
12+
const ctx = GqlExecutionContext.create(context);
13+
const environment = ctx.getContext().req?.headers?.['x-environment'];
14+
15+
if (environment === 'devnet') {
16+
return 'devnet';
17+
}
18+
19+
return 'mainnet';
20+
},
21+
);
22+
23+
export { Environment }

src/lib/decorators/CurrentUser.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
11
import type { ExecutionContext } from '@nestjs/common';
22
import { createParamDecorator } from '@nestjs/common';
33
import { GqlExecutionContext } from '@nestjs/graphql';
4+
import type { PublicKey } from '@solana/web3.js';
45

6+
import { Data } from '@src/user/entities/User.entity';
7+
8+
export interface User extends Data {
9+
id: string;
10+
publicKey: PublicKey;
11+
}
12+
13+
/**
14+
* Get the current user making the request
15+
*/
516
export const CurrentUser = createParamDecorator(
617
(data: unknown, context: ExecutionContext) => {
718
const ctx = GqlExecutionContext.create(context);
8-
return ctx.getContext().req.user;
19+
return ctx.getContext().req.user as User;
920
},
1021
);
22+

src/lib/decorators/EitherResolver.ts

+4-10
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,13 @@
1-
import type { Either } from 'fp-ts/Either';
21
import { isLeft } from 'fp-ts/Either';
3-
import type { TaskEither } from 'fp-ts/TaskEither';
4-
5-
interface EitherFunction {
6-
(...args: any[]):
7-
| Either<any, any>
8-
| Promise<Either<any, any>>
9-
| TaskEither<any, any>;
10-
}
112

3+
/**
4+
* Handle resolvers that return a `TaskEither`
5+
*/
126
export function EitherResolver() {
137
return (
148
target: any,
159
key: string,
16-
descriptor: TypedPropertyDescriptor<EitherFunction>,
10+
descriptor: TypedPropertyDescriptor<any>,
1711
) => {
1812
if (!descriptor.value) {
1913
throw new Error('Missing a description for the EitherResolver');

0 commit comments

Comments
 (0)