@bitblit/ratchet-graphql 6.0.146-alpha → 6.0.147-alpha

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/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@bitblit/ratchet-graphql",
3
- "version": "6.0.146-alpha",
3
+ "version": "6.0.147-alpha",
4
4
  "description": "Ratchet tools to simplify use of graphql",
5
5
  "sideEffects": false,
6
6
  "type": "module",
7
7
  "files": [
8
+ "src/**",
8
9
  "lib/**",
9
10
  "bin/**"
10
11
  ],
@@ -46,12 +47,12 @@
46
47
  },
47
48
  "license": "Apache-2.0",
48
49
  "dependencies": {
49
- "@bitblit/ratchet-common": "6.0.146-alpha",
50
+ "@bitblit/ratchet-common": "6.0.147-alpha",
50
51
  "graphql": "16.12.0",
51
52
  "graphql-request": "7.3.1"
52
53
  },
53
54
  "peerDependencies": {
54
- "@bitblit/ratchet-common": "6.0.146-alpha",
55
+ "@bitblit/ratchet-common": "6.0.147-alpha",
55
56
  "graphql": "^16.12.0",
56
57
  "graphql-request": "^7.2.0"
57
58
  }
@@ -0,0 +1,19 @@
1
+ import { BuildInformation } from '@bitblit/ratchet-common/build/build-information';
2
+
3
+ export class RatchetGraphqlInfo {
4
+ // Empty constructor prevents instantiation
5
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
6
+ private constructor() {}
7
+
8
+ public static buildInformation(): BuildInformation {
9
+ const val: BuildInformation = {
10
+ version: 'LOCAL-SNAPSHOT',
11
+ hash: 'LOCAL-HASH',
12
+ branch: 'LOCAL-BRANCH',
13
+ tag: 'LOCAL-TAG',
14
+ timeBuiltISO: 'LOCAL-TIME-ISO',
15
+ notes: 'LOCAL-NOTES',
16
+ };
17
+ return val;
18
+ }
19
+ }
@@ -0,0 +1,5 @@
1
+ export enum AuthorizationStyle {
2
+ TokenRequired = 'TokenRequired',
3
+ AlwaysAnonymous = 'AlwaysAnonymous',
4
+ AnonymousIfNoTokenAvailable = 'AnonymousIfNoTokenAvailable',
5
+ }
@@ -0,0 +1,42 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { GraphqlRatchet } from './graphql-ratchet';
3
+ import { StringRecordQueryProvider } from './provider/string-record-query-provider';
4
+ import { DefaultGraphqlRatchetEndpointProvider } from './provider/default-graphql-ratchet-endpoint-provider';
5
+
6
+ describe('#runTest', function () {
7
+ test.skip('should pull defaults', async () => {
8
+ const doc: string =
9
+ '{\n' +
10
+ '\n' +
11
+ ' serverMeta {\n' +
12
+ ' version\n' +
13
+ ' launchTime\n' +
14
+ ' serverTime\n' +
15
+ ' status\n' +
16
+ ' buildInfo {\n' +
17
+ ' buildVersion\n' +
18
+ ' buildHash\n' +
19
+ ' buildBranch\n' +
20
+ ' buildTag\n' +
21
+ ' }\n' +
22
+ ' specBuildInfo {\n' +
23
+ ' buildVersion\n' +
24
+ ' buildHash\n' +
25
+ ' buildBranch\n' +
26
+ ' buildTag\n' +
27
+ ' }\n' +
28
+ ' }\n' +
29
+ '}';
30
+ const url: string = 'https://localhost:8888/graphql';
31
+ const gr: GraphqlRatchet = new GraphqlRatchet(
32
+ new StringRecordQueryProvider({ q: doc }),
33
+ new DefaultGraphqlRatchetEndpointProvider(url),
34
+ null,
35
+ );
36
+
37
+ // Since this'll be self-signed
38
+ process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0';
39
+ const out: any = await gr.executeQuery<any>('q', {}, true);
40
+ expect(out).toBeTruthy();
41
+ });
42
+ });
@@ -0,0 +1,180 @@
1
+ import { GraphqlRatchetEndpointProvider } from './provider/graphql-ratchet-endpoint-provider.js';
2
+ import { GraphqlRatchetJwtTokenProvider } from './provider/graphql-ratchet-jwt-token-provider.js';
3
+ import { GraphqlRatchetQueryProvider } from './provider/graphql-ratchet-query-provider.js';
4
+ import { GraphqlRatchetErrorHandler } from './provider/graphql-ratchet-error-handler.js';
5
+ import { DefaultGraphqlRatchetErrorHandler } from './provider/default-graphql-ratchet-error-handler.js';
6
+
7
+ import { RequireRatchet } from '@bitblit/ratchet-common/lang/require-ratchet';
8
+ import { Logger } from '@bitblit/ratchet-common/logger/logger';
9
+ import { ErrorRatchet } from '@bitblit/ratchet-common/lang/error-ratchet';
10
+ import { StringRatchet } from '@bitblit/ratchet-common/lang/string-ratchet';
11
+ import { GraphQLClient } from 'graphql-request';
12
+ import { AuthorizationStyle } from './authorization-style.js';
13
+
14
+ /**
15
+ * This is a very simplistic client for non-cache use cases, etc. For more
16
+ * powerful clients, use a library like Apollo-client
17
+ */
18
+ export class GraphqlRatchet {
19
+ private clientCache: Map<string, GraphQLClient> = new Map<string, GraphQLClient>();
20
+ private noAuthClient: GraphQLClient;
21
+
22
+ private cachedEndpoint: string;
23
+
24
+ constructor(
25
+ private queryProvider: GraphqlRatchetQueryProvider,
26
+ private endpointProvider: GraphqlRatchetEndpointProvider,
27
+ private jwtTokenProvider?: GraphqlRatchetJwtTokenProvider,
28
+ private errorHandler: GraphqlRatchetErrorHandler = new DefaultGraphqlRatchetErrorHandler(),
29
+ ) {
30
+ RequireRatchet.notNullOrUndefined(queryProvider, 'queryProvider');
31
+ RequireRatchet.notNullOrUndefined(endpointProvider, 'endpointProvider');
32
+ //RequireRatchet.notNullOrUndefined(jwtTokenProvider, 'jwtTokenProvider');
33
+ RequireRatchet.notNullOrUndefined(errorHandler, 'errorHandler');
34
+ this.cachedEndpoint = this.endpointProvider.fetchGraphqlEndpoint();
35
+ }
36
+
37
+ public currentAuthToken(): string {
38
+ return this?.jwtTokenProvider?.fetchJwtToken();
39
+ }
40
+
41
+ public async fetchQueryText(qry: string): Promise<string> {
42
+ const text: string = await this.queryProvider.fetchQueryText(qry);
43
+ if (!text) {
44
+ Logger.warn('Could not find requested query : %s', qry);
45
+ }
46
+ return text;
47
+ }
48
+
49
+ private createAnonymousApi(): GraphQLClient {
50
+ Logger.info('Creating anonymous GraphQLClient');
51
+ const rval: GraphQLClient = new GraphQLClient(this.cachedEndpoint, { errorPolicy: 'none' });
52
+ return rval;
53
+ }
54
+
55
+ private fetchApi(authStyle: AuthorizationStyle): GraphQLClient {
56
+ let rval: GraphQLClient = null;
57
+ const jwtToken: string =
58
+ authStyle === AuthorizationStyle.AlwaysAnonymous ? null : StringRatchet.trimToNull(this?.jwtTokenProvider?.fetchJwtToken());
59
+ if (authStyle === AuthorizationStyle.TokenRequired && !jwtToken) {
60
+ throw ErrorRatchet.fErr('No token provided, auth style is TokenRequired');
61
+ }
62
+
63
+ this.checkIfEndpointChanged(); // Always check for cache invalidation first...
64
+ Logger.info('Fetch auth client %s', StringRatchet.obscure(StringRatchet.trimToEmpty(jwtToken), 2, 2));
65
+
66
+ if (jwtToken) {
67
+ Logger.debug('Fetching authd api');
68
+ if (!this.clientCache.has(jwtToken)) {
69
+ const newValue: GraphQLClient = this.createAuthApi(jwtToken);
70
+ Logger.debug('Setting cache for this token to %s', newValue);
71
+ this.clientCache.set(jwtToken, newValue);
72
+ } else {
73
+ Logger.debug('Fetching apollo client from cache');
74
+ }
75
+ rval = this.clientCache.get(jwtToken);
76
+ } else {
77
+ Logger.debug('Fetching unauthd ap');
78
+ if (!this.noAuthClient) {
79
+ this.noAuthClient = this.createAnonymousApi();
80
+ } else {
81
+ Logger.debug('Fetching anonymous client from cache');
82
+ }
83
+ rval = this.noAuthClient;
84
+ }
85
+ Logger.debug('FetchApi returning %s', rval);
86
+ return rval;
87
+ }
88
+
89
+ private createAuthApi(jwtToken: string): GraphQLClient {
90
+ Logger.info('Creating a new authenticated api for %s : %s', this.cachedEndpoint, StringRatchet.obscure(jwtToken, 2, 2));
91
+ RequireRatchet.notNullUndefinedOrOnlyWhitespaceString(jwtToken, 'jwtToken');
92
+ Logger.info('Creating auth apollo client %s', StringRatchet.obscure(jwtToken, 2, 2));
93
+ const rval: GraphQLClient = new GraphQLClient(this.cachedEndpoint, {
94
+ errorPolicy: 'none',
95
+ headers: { authorization: `Bearer ${jwtToken}` },
96
+ });
97
+ return rval;
98
+ }
99
+
100
+ private checkIfEndpointChanged(): void {
101
+ const check: string = this.endpointProvider.fetchGraphqlEndpoint();
102
+ if (check !== this.cachedEndpoint) {
103
+ Logger.info('Endpoint changed from %s to %s - clearing apollo caches');
104
+ this.clientCache = new Map<string, GraphQLClient>();
105
+ this.noAuthClient = null;
106
+ this.cachedEndpoint = check;
107
+ }
108
+ }
109
+
110
+ public clearCaches(): void {
111
+ Logger.info('Clearing cached apollo');
112
+ this.clientCache = new Map<string, GraphQLClient>();
113
+ this.noAuthClient = null;
114
+ this.cachedEndpoint = null;
115
+ }
116
+
117
+ public static extractSingleValueFromResponse<T>(data: any): T {
118
+ if (!data) {
119
+ throw ErrorRatchet.fErr('Could not find response in : %j', data);
120
+ } else if (data.errors) {
121
+ throw ErrorRatchet.fErr('Errors: %j', data.errors);
122
+ }
123
+ const keys: string[] = Object.keys(data);
124
+ if (keys.length !== 1) {
125
+ ErrorRatchet.throwFormattedErr('Unexpected number of keys : %s : %j', keys.length, keys);
126
+ }
127
+ const rval: T = data[keys[0]];
128
+ return rval;
129
+ }
130
+
131
+ public async executeQuery<T>(
132
+ queryName: string,
133
+ variables: Record<string, any>,
134
+ authStyle: AuthorizationStyle = AuthorizationStyle.TokenRequired,
135
+ ): Promise<T> {
136
+ let rval: T = null;
137
+ try {
138
+ const api: GraphQLClient = this.fetchApi(authStyle);
139
+ if (api) {
140
+ Logger.debug('API fetched for %s, fetching gql', queryName);
141
+ const gql: string = await this.fetchQueryText(queryName);
142
+ Logger.debug('API and GQL fetched for %s - running %s %s', queryName, gql, api);
143
+ const newValues: any = await api.request(gql, variables);
144
+ rval = GraphqlRatchet.extractSingleValueFromResponse(newValues);
145
+ Logger.silly('Query returned: %j', rval);
146
+ } else {
147
+ ErrorRatchet.throwFormattedErr('Cannot run - no api fetched');
148
+ }
149
+ } catch (err) {
150
+ Logger.silly('Exception caught in executeQuery : %s %s %j %s', err, queryName, variables, authStyle, err);
151
+ this.errorHandler.handleError(err, queryName, variables, authStyle);
152
+ }
153
+ return rval;
154
+ }
155
+
156
+ public async executeMutate<T>(
157
+ queryName: string,
158
+ variables: any,
159
+ authStyle: AuthorizationStyle = AuthorizationStyle.TokenRequired,
160
+ ): Promise<T> {
161
+ Logger.info('Mutate : %s : %j', queryName, variables);
162
+ let rval: T = null;
163
+ const api: GraphQLClient = this.fetchApi(authStyle);
164
+ try {
165
+ if (api) {
166
+ const gql: string = await this.fetchQueryText(queryName);
167
+ Logger.debug('API and GQL fetched for %s - running %s %s', queryName, gql, api);
168
+ const newValues: any = await api.request(gql, variables);
169
+ rval = GraphqlRatchet.extractSingleValueFromResponse(newValues);
170
+ Logger.silly('Mutate returned: %j', rval);
171
+ } else {
172
+ ErrorRatchet.throwFormattedErr('Cannot run - no api fetched');
173
+ }
174
+ } catch (err) {
175
+ this.errorHandler.handleError(err, queryName, variables, authStyle);
176
+ }
177
+
178
+ return rval;
179
+ }
180
+ }
@@ -0,0 +1,6 @@
1
+ export class DefaultGraphqlRatchetEndpointProvider {
2
+ constructor(private value: string) {}
3
+ public fetchGraphqlEndpoint(): string {
4
+ return this.value;
5
+ }
6
+ }
@@ -0,0 +1,18 @@
1
+ import { GraphqlRatchetErrorHandler } from './graphql-ratchet-error-handler.js';
2
+ import { Logger } from '@bitblit/ratchet-common/logger/logger';
3
+ import { LoggerLevelName } from '@bitblit/ratchet-common/logger/logger-level-name';
4
+ import { AuthorizationStyle } from '../authorization-style';
5
+
6
+ export class DefaultGraphqlRatchetErrorHandler implements GraphqlRatchetErrorHandler {
7
+ constructor(
8
+ private logLevel: LoggerLevelName = LoggerLevelName.warn,
9
+ private rethrow: boolean = false,
10
+ ) {}
11
+
12
+ public handleError(error: any, queryName: string, variables: Record<string, any>, authStyle: AuthorizationStyle): void {
13
+ Logger.logByLevel(this.logLevel, 'Graphql failed : %s : %s : Anon-%s : %j', error, queryName, authStyle, variables);
14
+ if (this.rethrow) {
15
+ throw error;
16
+ }
17
+ }
18
+ }
@@ -0,0 +1,3 @@
1
+ export interface GraphqlRatchetEndpointProvider {
2
+ fetchGraphqlEndpoint(): string;
3
+ }
@@ -0,0 +1,5 @@
1
+ import { AuthorizationStyle } from '../authorization-style';
2
+
3
+ export interface GraphqlRatchetErrorHandler {
4
+ handleError(error: any, queryName: string, variables: Record<string, any>, authStyle: AuthorizationStyle): void;
5
+ }
@@ -0,0 +1,3 @@
1
+ export interface GraphqlRatchetJwtTokenProvider {
2
+ fetchJwtToken(): string;
3
+ }
@@ -0,0 +1,3 @@
1
+ export interface GraphqlRatchetQueryProvider {
2
+ fetchQueryText(name: string): Promise<string>;
3
+ }
@@ -0,0 +1,28 @@
1
+ import { GraphqlRatchetQueryProvider } from './graphql-ratchet-query-provider.js';
2
+ import { Logger } from '@bitblit/ratchet-common/logger/logger';
3
+ import { StringRatchet } from '@bitblit/ratchet-common/lang/string-ratchet';
4
+ import fetch from 'cross-fetch';
5
+
6
+ export class LocalFetchQueryProvider implements GraphqlRatchetQueryProvider {
7
+ private cacheMap: Map<string, string> = new Map<string, string>();
8
+
9
+ constructor(
10
+ private pathTemplate: string = 'assets/gql/${QUERY_NAME}.gql',
11
+ private forcePathToLowerCase: boolean = false,
12
+ ) {}
13
+
14
+ public async fetchQueryText(qry: string): Promise<string> {
15
+ let rval: string = this.cacheMap.get(qry.toLowerCase());
16
+ if (!rval) {
17
+ const tgt: string = this.forcePathToLowerCase ? qry.toLowerCase() : qry;
18
+ const pathInput: string = StringRatchet.simpleTemplateFill(this.pathTemplate, { QUERY_NAME: tgt }, true);
19
+ Logger.info('Cache miss, loading %s from %s', qry, pathInput);
20
+ const qryResp: Response = await fetch(pathInput);
21
+ rval = StringRatchet.trimToNull(await qryResp.text());
22
+ if (rval) {
23
+ this.cacheMap.set(qry.toLowerCase(), rval);
24
+ }
25
+ }
26
+ return rval;
27
+ }
28
+ }
@@ -0,0 +1,36 @@
1
+ import { GraphqlRatchetQueryProvider } from './graphql-ratchet-query-provider.js';
2
+ import { Logger } from '@bitblit/ratchet-common/logger/logger';
3
+ import { StringRatchet } from '@bitblit/ratchet-common/lang/string-ratchet';
4
+
5
+ export class StringRecordQueryProvider implements GraphqlRatchetQueryProvider {
6
+ private files: Record<string, string> = {};
7
+ constructor(
8
+ inFiles: Record<string, string>,
9
+ private autoPrefix: string = '',
10
+ private autoSuffix: string = '',
11
+ private caseSensitive?: boolean,
12
+ ) {
13
+ if (inFiles) {
14
+ if (caseSensitive) {
15
+ this.files = Object.assign({}, inFiles);
16
+ } else {
17
+ Object.keys(inFiles).forEach((k) => {
18
+ this.files[k.toLowerCase()] = inFiles[k];
19
+ });
20
+ }
21
+ }
22
+ }
23
+
24
+ public async fetchQueryText(qry: string): Promise<string> {
25
+ let lookupKey: string = '';
26
+ lookupKey += this.caseSensitive ? StringRatchet.trimToEmpty(this.autoPrefix) : StringRatchet.trimToEmpty(this.autoPrefix).toLowerCase();
27
+ lookupKey += this.caseSensitive ? StringRatchet.trimToEmpty(qry) : StringRatchet.trimToEmpty(qry).toLowerCase();
28
+ lookupKey += this.caseSensitive ? StringRatchet.trimToEmpty(this.autoSuffix) : StringRatchet.trimToEmpty(this.autoSuffix).toLowerCase();
29
+
30
+ const rval: string = this.files[lookupKey];
31
+ if (!rval) {
32
+ Logger.warn('Could not find %s in %j', lookupKey, Object.keys(this.files));
33
+ }
34
+ return rval;
35
+ }
36
+ }