@tinkerstack/graphql-annotation 0.0.3 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +138 -138
- package/dist/index.d.mts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +10 -4
- package/dist/index.mjs +10 -4
- package/nest-cli.json +8 -8
- package/package.json +8 -3
- package/src/example/consumer/consumer.app.ts +38 -38
- package/src/example/consumer/consumer.example.ts +57 -57
- package/src/example/main.ts +23 -23
- package/src/example/producer/producer.app.ts +22 -22
- package/src/example/producer/producer.example.ts +28 -28
- package/src/modules/core/annotation.constants.ts +37 -37
- package/src/modules/core/annotation.decorators.ts +161 -161
- package/src/modules/core/annotation.loader.ts +315 -302
- package/src/modules/core/annotation.module.ts +47 -47
- package/src/modules/core/annotation.resolver.ts +87 -87
- package/src/modules/core/annotation.utils.ts +48 -48
- package/src/modules/core/index.ts +4 -4
- package/src/modules/core/resolver.explorer.ts +31 -31
- package/src/modules/index.ts +1 -1
- package/tsconfig.build.json +5 -5
- package/tsconfig.json +30 -30
- package/tsup.config.ts +10 -10
- package/.turbo/turbo-build.log +0 -20
package/README.md
CHANGED
|
@@ -1,138 +1,138 @@
|
|
|
1
|
-
<center>
|
|
2
|
-
<img src="https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fcdn.icon-icons.com%2Ficons2%2F2699%2FPNG%2F512%2Fgraphql_logo_icon_171045.png&f=1&nofb=1&ipt=e40678a4804af7f493102aa8bcc97bacbfdfee9dac86e14c75ab7afb00a237b5" height="150px">
|
|
3
|
-
<h1>GraphQL Annotation</h1>
|
|
4
|
-
<p>A set of tools for extending GraphQL stitching capabilities through annotations</p>
|
|
5
|
-
</center>
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## Who is this intended for?
|
|
10
|
-
|
|
11
|
-
This library is designed to solve a specific scenario in mind:
|
|
12
|
-
|
|
13
|
-
1. There's a primary `Gateway` and separately developed `SubGateway`s, both of each uses `graphql`.
|
|
14
|
-
2. Front-end or external non-backend consumers interact with `SubGateway` resolvers stitched through `Gateway`
|
|
15
|
-
3. Resolvers at `SubGateway` requires additional resource/guard/processing from `Gateway`.
|
|
16
|
-
4. `SubGateway` team does not have access to `Gateway` nor can they run a local instance.
|
|
17
|
-
5. `SubGateway` team **does not require** `Gateway` features during development, only during deployment.
|
|
18
|
-
|
|
19
|
-
Available solutions often utilize `Remote-Produce-Calls (RPCs)` libraries to let `SubGateway` invoke `Gateway` methods through HTTP transport, which unfortunately fails point 4 and 5, of which this library answers through `annotations`.
|
|
20
|
-
|
|
21
|
-
**However**, if `SubGateway` needs to make additional calls to `Gateway` for mutation or other purposes, then this library should only be considered as a mean of optimization and you are better off just running an `RPC` library at the small of performance and back-and-forth.
|
|
22
|
-
|
|
23
|
-
**TL;DR:** Only use this library if you HAVE TO achieve complete isolation between `Gateway` and `SubGateway` development cycle.
|
|
24
|
-
|
|
25
|
-
## Usage
|
|
26
|
-
|
|
27
|
-
You can find examples in `src/examples` for more details. Note that `graphql-annotation` should be replaced with workspace name in internal projects.
|
|
28
|
-
|
|
29
|
-
```ts
|
|
30
|
-
// gateway
|
|
31
|
-
|
|
32
|
-
import {
|
|
33
|
-
GraphQLAnnotationModule,
|
|
34
|
-
GraphQLAnnotationSchemaLoader
|
|
35
|
-
} from "graphql-annotation";
|
|
36
|
-
|
|
37
|
-
@Module({
|
|
38
|
-
imports: [
|
|
39
|
-
GraphQLAnnotationModule.forRoot(),
|
|
40
|
-
GraphQLModule.forRootAsync<ApolloDriverConfig>({
|
|
41
|
-
driver: ApolloDriver,
|
|
42
|
-
useFactory: (loader: GraphQLAnnotatedSchemaLoader) => ({
|
|
43
|
-
async transformSchema(schema) {
|
|
44
|
-
return stitchSchemas({
|
|
45
|
-
subschemas: [
|
|
46
|
-
{ schema },
|
|
47
|
-
{
|
|
48
|
-
// Load external annotated schemas
|
|
49
|
-
schema: await loader.load({
|
|
50
|
-
example: "http://localhost:3031/graphql",
|
|
51
|
-
}),
|
|
52
|
-
},
|
|
53
|
-
],
|
|
54
|
-
});
|
|
55
|
-
},
|
|
56
|
-
}),
|
|
57
|
-
inject: [GraphQLAnnotatedSchemaLoader],
|
|
58
|
-
}),
|
|
59
|
-
],
|
|
60
|
-
})
|
|
61
|
-
export class GatewayAppModule {}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
// It is recommended that you wrap the `Annotate` decorator in a function and in the gateway to ensure `gateway` team knows that's implemented. `sub-gateway` team could reference this through github submodule.
|
|
65
|
-
|
|
66
|
-
export type RoleGroups = ("user" | "admin" | "super-admin")[];
|
|
67
|
-
export const ProtectedResolver = (roles: RoleGroups) =>
|
|
68
|
-
Annotate("protected", { data: { roles } });
|
|
69
|
-
|
|
70
|
-
export const InjectableArg = (argName: string, resourceType: string) =>
|
|
71
|
-
Annotate("resource", { data: { resourceType }, parameter: argName });
|
|
72
|
-
|
|
73
|
-
@Injectable()
|
|
74
|
-
export class ExampleGuard implements CanActivate {
|
|
75
|
-
canActivate(context: ExecutionContext) {
|
|
76
|
-
// Do something with your execution context
|
|
77
|
-
return true;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
@Resolver()
|
|
82
|
-
export class SubgatewayResolver {
|
|
83
|
-
// NOTE: You can use normal nest and graphql decorators.
|
|
84
|
-
// Just note that `ResolveAnnotation` must be in a `Resolver`
|
|
85
|
-
|
|
86
|
-
@UseGuards(ExampleGuard)
|
|
87
|
-
@ResolveAnnotation("protected")
|
|
88
|
-
verifyUser(context: ExecutionContext, next: () => void) {}
|
|
89
|
-
|
|
90
|
-
// If empty would use function name as annotation tag
|
|
91
|
-
@ResolveAnnotation()
|
|
92
|
-
resource(
|
|
93
|
-
// WARN: this is NOT validated like in graphql though
|
|
94
|
-
@Args("resourceType") resourceType: string,
|
|
95
|
-
@Context() context: GraphQLExecutionContext,
|
|
96
|
-
) {
|
|
97
|
-
return "this-is-gateway-provided-username";
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// ---------------------------------------------
|
|
102
|
-
|
|
103
|
-
// sub-gateway
|
|
104
|
-
@Module({
|
|
105
|
-
imports: [
|
|
106
|
-
GraphQLModule.forRoot<ApolloDriverConfig>({
|
|
107
|
-
/* Put your graphql config here*/
|
|
108
|
-
}),
|
|
109
|
-
GraphQLAnnotationModule.forRoot({
|
|
110
|
-
serveAnnotations: true,
|
|
111
|
-
// Name is only for identification at the time of writing, which does not support annotation filtering.
|
|
112
|
-
serveConfig: { name: "<subgateway-a>" },
|
|
113
|
-
}),
|
|
114
|
-
ExampleProducerModule,
|
|
115
|
-
],
|
|
116
|
-
})
|
|
117
|
-
export class SubGatewayAppModule {}
|
|
118
|
-
|
|
119
|
-
@Resolver()
|
|
120
|
-
export class ExampleProducerResolver {
|
|
121
|
-
@Query(() => [String])
|
|
122
|
-
@ProtectedResolver(["admin"])
|
|
123
|
-
testProtectedQuery() {
|
|
124
|
-
return ["This resolver should only be available to if guard check passed"];
|
|
125
|
-
}
|
|
126
|
-
@Query(() => [String])
|
|
127
|
-
testQueryWithParam(
|
|
128
|
-
@InjectableArg("username", "session:username")
|
|
129
|
-
username: string
|
|
130
|
-
) {
|
|
131
|
-
return [`Received params: ${username}`];
|
|
132
|
-
}
|
|
133
|
-
@Query(() => [String])
|
|
134
|
-
testPublicQuery() {
|
|
135
|
-
return ["This resolver should be available to everyone"];
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
```
|
|
1
|
+
<center>
|
|
2
|
+
<img src="https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fcdn.icon-icons.com%2Ficons2%2F2699%2FPNG%2F512%2Fgraphql_logo_icon_171045.png&f=1&nofb=1&ipt=e40678a4804af7f493102aa8bcc97bacbfdfee9dac86e14c75ab7afb00a237b5" height="150px">
|
|
3
|
+
<h1>GraphQL Annotation</h1>
|
|
4
|
+
<p>A set of tools for extending GraphQL stitching capabilities through annotations</p>
|
|
5
|
+
</center>
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Who is this intended for?
|
|
10
|
+
|
|
11
|
+
This library is designed to solve a specific scenario in mind:
|
|
12
|
+
|
|
13
|
+
1. There's a primary `Gateway` and separately developed `SubGateway`s, both of each uses `graphql`.
|
|
14
|
+
2. Front-end or external non-backend consumers interact with `SubGateway` resolvers stitched through `Gateway`
|
|
15
|
+
3. Resolvers at `SubGateway` requires additional resource/guard/processing from `Gateway`.
|
|
16
|
+
4. `SubGateway` team does not have access to `Gateway` nor can they run a local instance.
|
|
17
|
+
5. `SubGateway` team **does not require** `Gateway` features during development, only during deployment.
|
|
18
|
+
|
|
19
|
+
Available solutions often utilize `Remote-Produce-Calls (RPCs)` libraries to let `SubGateway` invoke `Gateway` methods through HTTP transport, which unfortunately fails point 4 and 5, of which this library answers through `annotations`.
|
|
20
|
+
|
|
21
|
+
**However**, if `SubGateway` needs to make additional calls to `Gateway` for mutation or other purposes, then this library should only be considered as a mean of optimization and you are better off just running an `RPC` library at the small of performance and back-and-forth.
|
|
22
|
+
|
|
23
|
+
**TL;DR:** Only use this library if you HAVE TO achieve complete isolation between `Gateway` and `SubGateway` development cycle.
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
You can find examples in `src/examples` for more details. Note that `graphql-annotation` should be replaced with workspace name in internal projects.
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
// gateway
|
|
31
|
+
|
|
32
|
+
import {
|
|
33
|
+
GraphQLAnnotationModule,
|
|
34
|
+
GraphQLAnnotationSchemaLoader
|
|
35
|
+
} from "graphql-annotation";
|
|
36
|
+
|
|
37
|
+
@Module({
|
|
38
|
+
imports: [
|
|
39
|
+
GraphQLAnnotationModule.forRoot(),
|
|
40
|
+
GraphQLModule.forRootAsync<ApolloDriverConfig>({
|
|
41
|
+
driver: ApolloDriver,
|
|
42
|
+
useFactory: (loader: GraphQLAnnotatedSchemaLoader) => ({
|
|
43
|
+
async transformSchema(schema) {
|
|
44
|
+
return stitchSchemas({
|
|
45
|
+
subschemas: [
|
|
46
|
+
{ schema },
|
|
47
|
+
{
|
|
48
|
+
// Load external annotated schemas
|
|
49
|
+
schema: await loader.load({
|
|
50
|
+
example: "http://localhost:3031/graphql",
|
|
51
|
+
}),
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
});
|
|
55
|
+
},
|
|
56
|
+
}),
|
|
57
|
+
inject: [GraphQLAnnotatedSchemaLoader],
|
|
58
|
+
}),
|
|
59
|
+
],
|
|
60
|
+
})
|
|
61
|
+
export class GatewayAppModule {}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
// It is recommended that you wrap the `Annotate` decorator in a function and in the gateway to ensure `gateway` team knows that's implemented. `sub-gateway` team could reference this through github submodule.
|
|
65
|
+
|
|
66
|
+
export type RoleGroups = ("user" | "admin" | "super-admin")[];
|
|
67
|
+
export const ProtectedResolver = (roles: RoleGroups) =>
|
|
68
|
+
Annotate("protected", { data: { roles } });
|
|
69
|
+
|
|
70
|
+
export const InjectableArg = (argName: string, resourceType: string) =>
|
|
71
|
+
Annotate("resource", { data: { resourceType }, parameter: argName });
|
|
72
|
+
|
|
73
|
+
@Injectable()
|
|
74
|
+
export class ExampleGuard implements CanActivate {
|
|
75
|
+
canActivate(context: ExecutionContext) {
|
|
76
|
+
// Do something with your execution context
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@Resolver()
|
|
82
|
+
export class SubgatewayResolver {
|
|
83
|
+
// NOTE: You can use normal nest and graphql decorators.
|
|
84
|
+
// Just note that `ResolveAnnotation` must be in a `Resolver`
|
|
85
|
+
|
|
86
|
+
@UseGuards(ExampleGuard)
|
|
87
|
+
@ResolveAnnotation("protected")
|
|
88
|
+
verifyUser(context: ExecutionContext, next: () => void) {}
|
|
89
|
+
|
|
90
|
+
// If empty would use function name as annotation tag
|
|
91
|
+
@ResolveAnnotation()
|
|
92
|
+
resource(
|
|
93
|
+
// WARN: this is NOT validated like in graphql though
|
|
94
|
+
@Args("resourceType") resourceType: string,
|
|
95
|
+
@Context() context: GraphQLExecutionContext,
|
|
96
|
+
) {
|
|
97
|
+
return "this-is-gateway-provided-username";
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------
|
|
102
|
+
|
|
103
|
+
// sub-gateway
|
|
104
|
+
@Module({
|
|
105
|
+
imports: [
|
|
106
|
+
GraphQLModule.forRoot<ApolloDriverConfig>({
|
|
107
|
+
/* Put your graphql config here*/
|
|
108
|
+
}),
|
|
109
|
+
GraphQLAnnotationModule.forRoot({
|
|
110
|
+
serveAnnotations: true,
|
|
111
|
+
// Name is only for identification at the time of writing, which does not support annotation filtering.
|
|
112
|
+
serveConfig: { name: "<subgateway-a>" },
|
|
113
|
+
}),
|
|
114
|
+
ExampleProducerModule,
|
|
115
|
+
],
|
|
116
|
+
})
|
|
117
|
+
export class SubGatewayAppModule {}
|
|
118
|
+
|
|
119
|
+
@Resolver()
|
|
120
|
+
export class ExampleProducerResolver {
|
|
121
|
+
@Query(() => [String])
|
|
122
|
+
@ProtectedResolver(["admin"])
|
|
123
|
+
testProtectedQuery() {
|
|
124
|
+
return ["This resolver should only be available to if guard check passed"];
|
|
125
|
+
}
|
|
126
|
+
@Query(() => [String])
|
|
127
|
+
testQueryWithParam(
|
|
128
|
+
@InjectableArg("username", "session:username")
|
|
129
|
+
username: string
|
|
130
|
+
) {
|
|
131
|
+
return [`Received params: ${username}`];
|
|
132
|
+
}
|
|
133
|
+
@Query(() => [String])
|
|
134
|
+
testPublicQuery() {
|
|
135
|
+
return ["This resolver should be available to everyone"];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
package/dist/index.d.mts
CHANGED
|
@@ -58,7 +58,9 @@ declare class GraphQLAnnotatedSchemaLoader {
|
|
|
58
58
|
private readonly externalContextCreator;
|
|
59
59
|
private _logger;
|
|
60
60
|
constructor(resolverDiscoveryService: ResolverDiscoveryService, metadataScanner: MetadataScanner, externalContextCreator: ExternalContextCreator);
|
|
61
|
-
load(sources: AnnotationSchemaSources
|
|
61
|
+
load(sources: AnnotationSchemaSources, opts?: {
|
|
62
|
+
ignoreMissingSources?: boolean;
|
|
63
|
+
}): Promise<GraphQLSchema>;
|
|
62
64
|
private discoverAnnotationResolvers;
|
|
63
65
|
private batchLoadSources;
|
|
64
66
|
private loadAnnotations;
|
package/dist/index.d.ts
CHANGED
|
@@ -58,7 +58,9 @@ declare class GraphQLAnnotatedSchemaLoader {
|
|
|
58
58
|
private readonly externalContextCreator;
|
|
59
59
|
private _logger;
|
|
60
60
|
constructor(resolverDiscoveryService: ResolverDiscoveryService, metadataScanner: MetadataScanner, externalContextCreator: ExternalContextCreator);
|
|
61
|
-
load(sources: AnnotationSchemaSources
|
|
61
|
+
load(sources: AnnotationSchemaSources, opts?: {
|
|
62
|
+
ignoreMissingSources?: boolean;
|
|
63
|
+
}): Promise<GraphQLSchema>;
|
|
62
64
|
private discoverAnnotationResolvers;
|
|
63
65
|
private batchLoadSources;
|
|
64
66
|
private loadAnnotations;
|
package/dist/index.js
CHANGED
|
@@ -328,8 +328,10 @@ var GraphQLAnnotatedSchemaLoader = class {
|
|
|
328
328
|
this.metadataScanner = metadataScanner;
|
|
329
329
|
this.externalContextCreator = externalContextCreator;
|
|
330
330
|
}
|
|
331
|
-
async load(sources) {
|
|
332
|
-
const loadedSources = await this.batchLoadSources(sources
|
|
331
|
+
async load(sources, opts) {
|
|
332
|
+
const loadedSources = await this.batchLoadSources(sources, {
|
|
333
|
+
ignoreMissingSources: opts?.ignoreMissingSources ?? false
|
|
334
|
+
});
|
|
333
335
|
const schema = (0, import_stitch.stitchSchemas)({
|
|
334
336
|
subschemas: loadedSources.map((s) => s.subschema)
|
|
335
337
|
});
|
|
@@ -349,7 +351,7 @@ var GraphQLAnnotatedSchemaLoader = class {
|
|
|
349
351
|
}
|
|
350
352
|
return annotationResolvers;
|
|
351
353
|
}
|
|
352
|
-
async batchLoadSources(sources) {
|
|
354
|
+
async batchLoadSources(sources, opts) {
|
|
353
355
|
const pending = Object.entries(sources).map(async ([name, url]) => {
|
|
354
356
|
const loadId = `${name}@${url}`;
|
|
355
357
|
try {
|
|
@@ -371,7 +373,11 @@ var GraphQLAnnotatedSchemaLoader = class {
|
|
|
371
373
|
}
|
|
372
374
|
});
|
|
373
375
|
const [loaded, failed] = split(await Promise.allSettled(pending), (q) => q.status === "fulfilled");
|
|
374
|
-
|
|
376
|
+
const shouldThrow = !(opts?.ignoreMissingSources ?? false);
|
|
377
|
+
if (failed.length > 0) {
|
|
378
|
+
if (shouldThrow) throw new Error(`Failed to load ${failed.length} schema(s) from sources.`);
|
|
379
|
+
else this._logger.warn(`Failed to load ${failed.length}, ignoring.`);
|
|
380
|
+
}
|
|
375
381
|
return loaded.map((l) => l.value);
|
|
376
382
|
}
|
|
377
383
|
async loadAnnotations(url) {
|
package/dist/index.mjs
CHANGED
|
@@ -299,8 +299,10 @@ var GraphQLAnnotatedSchemaLoader = class {
|
|
|
299
299
|
this.metadataScanner = metadataScanner;
|
|
300
300
|
this.externalContextCreator = externalContextCreator;
|
|
301
301
|
}
|
|
302
|
-
async load(sources) {
|
|
303
|
-
const loadedSources = await this.batchLoadSources(sources
|
|
302
|
+
async load(sources, opts) {
|
|
303
|
+
const loadedSources = await this.batchLoadSources(sources, {
|
|
304
|
+
ignoreMissingSources: opts?.ignoreMissingSources ?? false
|
|
305
|
+
});
|
|
304
306
|
const schema = stitchSchemas({
|
|
305
307
|
subschemas: loadedSources.map((s) => s.subschema)
|
|
306
308
|
});
|
|
@@ -320,7 +322,7 @@ var GraphQLAnnotatedSchemaLoader = class {
|
|
|
320
322
|
}
|
|
321
323
|
return annotationResolvers;
|
|
322
324
|
}
|
|
323
|
-
async batchLoadSources(sources) {
|
|
325
|
+
async batchLoadSources(sources, opts) {
|
|
324
326
|
const pending = Object.entries(sources).map(async ([name, url]) => {
|
|
325
327
|
const loadId = `${name}@${url}`;
|
|
326
328
|
try {
|
|
@@ -342,7 +344,11 @@ var GraphQLAnnotatedSchemaLoader = class {
|
|
|
342
344
|
}
|
|
343
345
|
});
|
|
344
346
|
const [loaded, failed] = split(await Promise.allSettled(pending), (q) => q.status === "fulfilled");
|
|
345
|
-
|
|
347
|
+
const shouldThrow = !(opts?.ignoreMissingSources ?? false);
|
|
348
|
+
if (failed.length > 0) {
|
|
349
|
+
if (shouldThrow) throw new Error(`Failed to load ${failed.length} schema(s) from sources.`);
|
|
350
|
+
else this._logger.warn(`Failed to load ${failed.length}, ignoring.`);
|
|
351
|
+
}
|
|
346
352
|
return loaded.map((l) => l.value);
|
|
347
353
|
}
|
|
348
354
|
async loadAnnotations(url) {
|
package/nest-cli.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://json.schemastore.org/nest-cli",
|
|
3
|
-
"collection": "@nestjs/schematics",
|
|
4
|
-
"sourceRoot": "src",
|
|
5
|
-
"entryFile": "example/main",
|
|
6
|
-
"compilerOptions": {
|
|
7
|
-
"tsConfigPath": "tsconfig.json"
|
|
8
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/nest-cli",
|
|
3
|
+
"collection": "@nestjs/schematics",
|
|
4
|
+
"sourceRoot": "src",
|
|
5
|
+
"entryFile": "example/main",
|
|
6
|
+
"compilerOptions": {
|
|
7
|
+
"tsConfigPath": "tsconfig.json"
|
|
8
|
+
}
|
|
9
9
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tinkerstack/graphql-annotation",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "Better type-safe-able graphql directives",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -14,11 +14,15 @@
|
|
|
14
14
|
"@graphql-tools/stitch": "^10.1.8",
|
|
15
15
|
"@graphql-tools/utils": "^11.0.0",
|
|
16
16
|
"@graphql-tools/wrap": "^11.1.4",
|
|
17
|
-
"class-transformer": "^0.5.1",
|
|
18
17
|
"graphql-request": "^7.2.0"
|
|
19
18
|
},
|
|
20
19
|
"publishConfig": {
|
|
21
|
-
"access": "public"
|
|
20
|
+
"access": "public",
|
|
21
|
+
"provenance": true
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/plasteek/tinkerstack.git"
|
|
22
26
|
},
|
|
23
27
|
"peerDependencies": {
|
|
24
28
|
"@nestjs/common": "^10 || ^11",
|
|
@@ -40,6 +44,7 @@
|
|
|
40
44
|
"@nestjs/mapped-types": "^2.1.0",
|
|
41
45
|
"@nestjs/platform-fastify": "^11.1.11",
|
|
42
46
|
"@swc/core": "^1.15.11",
|
|
47
|
+
"class-transformer": "^0.5.1",
|
|
43
48
|
"class-validator": "^0.14.2",
|
|
44
49
|
"concurrently": "^9.2.1",
|
|
45
50
|
"eslint": "^9.37.0",
|
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
import { Module } from "@nestjs/common";
|
|
2
|
-
import { GraphQLModule } from "@nestjs/graphql";
|
|
3
|
-
|
|
4
|
-
import { ApolloDriver, ApolloDriverConfig } from "@nestjs/apollo";
|
|
5
|
-
import { ExampleConsumerModule } from "./consumer.example";
|
|
6
|
-
import { GraphQLAnnotatedSchemaLoader } from "@/modules/core/annotation.loader";
|
|
7
|
-
import { GraphQLAnnotationModule } from "@/modules/core/annotation.module";
|
|
8
|
-
import { stitchSchemas } from "@graphql-tools/stitch";
|
|
9
|
-
|
|
10
|
-
@Module({
|
|
11
|
-
imports: [
|
|
12
|
-
ExampleConsumerModule,
|
|
13
|
-
GraphQLAnnotationModule.forRoot(),
|
|
14
|
-
GraphQLModule.forRootAsync<ApolloDriverConfig>({
|
|
15
|
-
driver: ApolloDriver,
|
|
16
|
-
useFactory: (loader: GraphQLAnnotatedSchemaLoader) => ({
|
|
17
|
-
graphiql: true,
|
|
18
|
-
playground: true,
|
|
19
|
-
autoSchemaFile: "./src/example/consumer/consumer.schema.gql",
|
|
20
|
-
async transformSchema(schema) {
|
|
21
|
-
return stitchSchemas({
|
|
22
|
-
subschemas: [
|
|
23
|
-
{ schema },
|
|
24
|
-
{
|
|
25
|
-
// Load external annotated schemas
|
|
26
|
-
schema: await loader.load({
|
|
27
|
-
example: "http://localhost:3031/graphql",
|
|
28
|
-
}),
|
|
29
|
-
},
|
|
30
|
-
],
|
|
31
|
-
});
|
|
32
|
-
},
|
|
33
|
-
}),
|
|
34
|
-
inject: [GraphQLAnnotatedSchemaLoader],
|
|
35
|
-
}),
|
|
36
|
-
],
|
|
37
|
-
})
|
|
38
|
-
export class ExampleConsumerApp {}
|
|
1
|
+
import { Module } from "@nestjs/common";
|
|
2
|
+
import { GraphQLModule } from "@nestjs/graphql";
|
|
3
|
+
|
|
4
|
+
import { ApolloDriver, ApolloDriverConfig } from "@nestjs/apollo";
|
|
5
|
+
import { ExampleConsumerModule } from "./consumer.example";
|
|
6
|
+
import { GraphQLAnnotatedSchemaLoader } from "@/modules/core/annotation.loader";
|
|
7
|
+
import { GraphQLAnnotationModule } from "@/modules/core/annotation.module";
|
|
8
|
+
import { stitchSchemas } from "@graphql-tools/stitch";
|
|
9
|
+
|
|
10
|
+
@Module({
|
|
11
|
+
imports: [
|
|
12
|
+
ExampleConsumerModule,
|
|
13
|
+
GraphQLAnnotationModule.forRoot(),
|
|
14
|
+
GraphQLModule.forRootAsync<ApolloDriverConfig>({
|
|
15
|
+
driver: ApolloDriver,
|
|
16
|
+
useFactory: (loader: GraphQLAnnotatedSchemaLoader) => ({
|
|
17
|
+
graphiql: true,
|
|
18
|
+
playground: true,
|
|
19
|
+
autoSchemaFile: "./src/example/consumer/consumer.schema.gql",
|
|
20
|
+
async transformSchema(schema) {
|
|
21
|
+
return stitchSchemas({
|
|
22
|
+
subschemas: [
|
|
23
|
+
{ schema },
|
|
24
|
+
{
|
|
25
|
+
// Load external annotated schemas
|
|
26
|
+
schema: await loader.load({
|
|
27
|
+
example: "http://localhost:3031/graphql",
|
|
28
|
+
}),
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
});
|
|
32
|
+
},
|
|
33
|
+
}),
|
|
34
|
+
inject: [GraphQLAnnotatedSchemaLoader],
|
|
35
|
+
}),
|
|
36
|
+
],
|
|
37
|
+
})
|
|
38
|
+
export class ExampleConsumerApp {}
|
|
@@ -1,57 +1,57 @@
|
|
|
1
|
-
import { Annotate, ResolveAnnotation } from "@/modules/core";
|
|
2
|
-
import {
|
|
3
|
-
CanActivate,
|
|
4
|
-
ExecutionContext,
|
|
5
|
-
Injectable,
|
|
6
|
-
Module,
|
|
7
|
-
UseGuards,
|
|
8
|
-
} from "@nestjs/common";
|
|
9
|
-
import {
|
|
10
|
-
Args,
|
|
11
|
-
Context,
|
|
12
|
-
GraphQLExecutionContext,
|
|
13
|
-
Query,
|
|
14
|
-
Resolver,
|
|
15
|
-
} from "@nestjs/graphql";
|
|
16
|
-
|
|
17
|
-
// Export for external
|
|
18
|
-
export type RoleGroups = ("user" | "admin" | "super-admin")[];
|
|
19
|
-
export const ProtectedResolver = (roles: RoleGroups) =>
|
|
20
|
-
Annotate("protected", { data: { roles } });
|
|
21
|
-
export const InjectableArg = (argName: string, resourceType: string) =>
|
|
22
|
-
Annotate("resource", { data: { resourceType }, parameter: argName });
|
|
23
|
-
|
|
24
|
-
@Injectable()
|
|
25
|
-
export class ExampleGuard implements CanActivate {
|
|
26
|
-
canActivate(context: ExecutionContext) {
|
|
27
|
-
// Do something with your execution context
|
|
28
|
-
return true;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
@Resolver()
|
|
33
|
-
export class ExampleConsumerResolver {
|
|
34
|
-
@Query(() => [String])
|
|
35
|
-
exampleOut() {
|
|
36
|
-
return ["this is from consumer"];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
@UseGuards(ExampleGuard)
|
|
40
|
-
@ResolveAnnotation("protected")
|
|
41
|
-
verifyUser(context: ExecutionContext, next: () => void) {
|
|
42
|
-
console.log("I AM HERE");
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Should use the name
|
|
46
|
-
@ResolveAnnotation("resource")
|
|
47
|
-
resolveResource(
|
|
48
|
-
@Args("resourceType") resourceType: string,
|
|
49
|
-
@Context() context: GraphQLExecutionContext,
|
|
50
|
-
) {
|
|
51
|
-
console.log(resourceType);
|
|
52
|
-
return "this-is-gateway-provided-username";
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
@Module({ providers: [ExampleConsumerResolver] })
|
|
57
|
-
export class ExampleConsumerModule {}
|
|
1
|
+
import { Annotate, ResolveAnnotation } from "@/modules/core";
|
|
2
|
+
import {
|
|
3
|
+
CanActivate,
|
|
4
|
+
ExecutionContext,
|
|
5
|
+
Injectable,
|
|
6
|
+
Module,
|
|
7
|
+
UseGuards,
|
|
8
|
+
} from "@nestjs/common";
|
|
9
|
+
import {
|
|
10
|
+
Args,
|
|
11
|
+
Context,
|
|
12
|
+
GraphQLExecutionContext,
|
|
13
|
+
Query,
|
|
14
|
+
Resolver,
|
|
15
|
+
} from "@nestjs/graphql";
|
|
16
|
+
|
|
17
|
+
// Export for external
|
|
18
|
+
export type RoleGroups = ("user" | "admin" | "super-admin")[];
|
|
19
|
+
export const ProtectedResolver = (roles: RoleGroups) =>
|
|
20
|
+
Annotate("protected", { data: { roles } });
|
|
21
|
+
export const InjectableArg = (argName: string, resourceType: string) =>
|
|
22
|
+
Annotate("resource", { data: { resourceType }, parameter: argName });
|
|
23
|
+
|
|
24
|
+
@Injectable()
|
|
25
|
+
export class ExampleGuard implements CanActivate {
|
|
26
|
+
canActivate(context: ExecutionContext) {
|
|
27
|
+
// Do something with your execution context
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@Resolver()
|
|
33
|
+
export class ExampleConsumerResolver {
|
|
34
|
+
@Query(() => [String])
|
|
35
|
+
exampleOut() {
|
|
36
|
+
return ["this is from consumer"];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@UseGuards(ExampleGuard)
|
|
40
|
+
@ResolveAnnotation("protected")
|
|
41
|
+
verifyUser(context: ExecutionContext, next: () => void) {
|
|
42
|
+
console.log("I AM HERE");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Should use the name
|
|
46
|
+
@ResolveAnnotation("resource")
|
|
47
|
+
resolveResource(
|
|
48
|
+
@Args("resourceType") resourceType: string,
|
|
49
|
+
@Context() context: GraphQLExecutionContext,
|
|
50
|
+
) {
|
|
51
|
+
console.log(resourceType);
|
|
52
|
+
return "this-is-gateway-provided-username";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@Module({ providers: [ExampleConsumerResolver] })
|
|
57
|
+
export class ExampleConsumerModule {}
|
package/src/example/main.ts
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
import { NestFactory } from "@nestjs/core";
|
|
2
|
-
|
|
3
|
-
import { FastifyAdapter } from "@nestjs/platform-fastify";
|
|
4
|
-
import { ExampleProducerApp } from "./producer/producer.app";
|
|
5
|
-
import { ConsoleLogger, Type } from "@nestjs/common";
|
|
6
|
-
import { ExampleConsumerApp } from "./consumer/consumer.app";
|
|
7
|
-
|
|
8
|
-
export async function bootstrap(name: string, module: Type, port: number) {
|
|
9
|
-
const app = await NestFactory.create(module, new FastifyAdapter(), {
|
|
10
|
-
logger: new ConsoleLogger({ prefix: name }),
|
|
11
|
-
});
|
|
12
|
-
await app.listen(port);
|
|
13
|
-
|
|
14
|
-
console.log(
|
|
15
|
-
`Listening on localhost:${port}. Playground: http://localhost:${port}/graphql`
|
|
16
|
-
);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
async function main() {
|
|
20
|
-
await bootstrap("example-producer", ExampleProducerApp, 3031);
|
|
21
|
-
await bootstrap("example-consumer", ExampleConsumerApp, 3030);
|
|
22
|
-
}
|
|
23
|
-
void main();
|
|
1
|
+
import { NestFactory } from "@nestjs/core";
|
|
2
|
+
|
|
3
|
+
import { FastifyAdapter } from "@nestjs/platform-fastify";
|
|
4
|
+
import { ExampleProducerApp } from "./producer/producer.app";
|
|
5
|
+
import { ConsoleLogger, Type } from "@nestjs/common";
|
|
6
|
+
import { ExampleConsumerApp } from "./consumer/consumer.app";
|
|
7
|
+
|
|
8
|
+
export async function bootstrap(name: string, module: Type, port: number) {
|
|
9
|
+
const app = await NestFactory.create(module, new FastifyAdapter(), {
|
|
10
|
+
logger: new ConsoleLogger({ prefix: name }),
|
|
11
|
+
});
|
|
12
|
+
await app.listen(port);
|
|
13
|
+
|
|
14
|
+
console.log(
|
|
15
|
+
`Listening on localhost:${port}. Playground: http://localhost:${port}/graphql`
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function main() {
|
|
20
|
+
await bootstrap("example-producer", ExampleProducerApp, 3031);
|
|
21
|
+
await bootstrap("example-consumer", ExampleConsumerApp, 3030);
|
|
22
|
+
}
|
|
23
|
+
void main();
|