@manot40/genql-cli 1.0.0

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.
@@ -0,0 +1,256 @@
1
+ import type { GraphqlOperation } from './generateGraphqlOperation';
2
+ import { GenqlError } from './error';
3
+
4
+ type Variables = Record<string, any>;
5
+
6
+ type QueryError = Error & {
7
+ message: string;
8
+
9
+ locations?: Array<{
10
+ line: number;
11
+ column: number;
12
+ }>;
13
+ path?: any;
14
+ rid: string;
15
+ details?: Record<string, any>;
16
+ };
17
+ type Result = {
18
+ data: Record<string, any>;
19
+ errors: Array<QueryError>;
20
+ };
21
+ type Fetcher = (batchedQuery: GraphqlOperation | Array<GraphqlOperation>) => Promise<Array<Result>>;
22
+ type Options = {
23
+ batchInterval?: number;
24
+ shouldBatch?: boolean;
25
+ maxBatchSize?: number;
26
+ };
27
+ type Queue = Array<{
28
+ request: GraphqlOperation;
29
+ resolve: (...args: Array<any>) => any;
30
+ reject: (...args: Array<any>) => any;
31
+ }>;
32
+
33
+ /**
34
+ * takes a list of requests (queue) and batches them into a single server request.
35
+ * It will then resolve each individual requests promise with the appropriate data.
36
+ * @private
37
+ * @param {QueryBatcher} client - the client to use
38
+ * @param {Queue} queue - the list of requests to batch
39
+ */
40
+ function dispatchQueueBatch(client: QueryBatcher, queue: Queue): void {
41
+ let batchedQuery: any = queue.map((item) => item.request);
42
+
43
+ if (batchedQuery.length === 1) {
44
+ batchedQuery = batchedQuery[0];
45
+ }
46
+ (() => {
47
+ try {
48
+ return client.fetcher(batchedQuery);
49
+ } catch (e) {
50
+ return Promise.reject(e);
51
+ }
52
+ })()
53
+ .then((responses: any) => {
54
+ if (queue.length === 1 && !Array.isArray(responses)) {
55
+ if (responses.errors && responses.errors.length) {
56
+ queue[0].reject(new GenqlError(responses.errors, responses.data));
57
+ return;
58
+ }
59
+
60
+ queue[0].resolve(responses);
61
+ return;
62
+ } else if (responses.length !== queue.length) {
63
+ throw new Error('response length did not match query length');
64
+ }
65
+
66
+ for (let i = 0; i < queue.length; i++) {
67
+ if (responses[i].errors && responses[i].errors.length) {
68
+ queue[i].reject(new GenqlError(responses[i].errors, responses[i].data));
69
+ } else {
70
+ queue[i].resolve(responses[i]);
71
+ }
72
+ }
73
+ })
74
+ .catch((e) => {
75
+ for (let i = 0; i < queue.length; i++) {
76
+ queue[i].reject(e);
77
+ }
78
+ });
79
+ }
80
+
81
+ /**
82
+ * creates a list of requests to batch according to max batch size.
83
+ * @private
84
+ * @param {QueryBatcher} client - the client to create list of requests from from
85
+ * @param {Options} options - the options for the batch
86
+ */
87
+ function dispatchQueue(client: QueryBatcher, options: Options): void {
88
+ const queue = client._queue;
89
+ const maxBatchSize = options.maxBatchSize || 0;
90
+ client._queue = [];
91
+
92
+ if (maxBatchSize > 0 && maxBatchSize < queue.length) {
93
+ for (let i = 0; i < queue.length / maxBatchSize; i++) {
94
+ dispatchQueueBatch(client, queue.slice(i * maxBatchSize, (i + 1) * maxBatchSize));
95
+ }
96
+ } else {
97
+ dispatchQueueBatch(client, queue);
98
+ }
99
+ }
100
+ /**
101
+ * Create a batcher client.
102
+ * @param {Fetcher} fetcher - A function that can handle the network requests to graphql endpoint
103
+ * @param {Options} options - the options to be used by client
104
+ * @param {boolean} options.shouldBatch - should the client batch requests. (default true)
105
+ * @param {integer} options.batchInterval - duration (in MS) of each batch window. (default 6)
106
+ * @param {integer} options.maxBatchSize - max number of requests in a batch. (default 0)
107
+ * @param {boolean} options.defaultHeaders - default headers to include with every request
108
+ *
109
+ * @example
110
+ * const fetcher = batchedQuery => fetch('path/to/graphql', {
111
+ * method: 'post',
112
+ * headers: {
113
+ * Accept: 'application/json',
114
+ * 'Content-Type': 'application/json',
115
+ * },
116
+ * body: JSON.stringify(batchedQuery),
117
+ * credentials: 'include',
118
+ * })
119
+ * .then(response => response.json())
120
+ *
121
+ * const client = new QueryBatcher(fetcher, { maxBatchSize: 10 })
122
+ */
123
+
124
+ export class QueryBatcher {
125
+ fetcher: Fetcher;
126
+ _options: Options;
127
+ _queue: Queue;
128
+
129
+ constructor(fetcher: Fetcher, { batchInterval = 6, shouldBatch = true, maxBatchSize = 0 }: Options = {}) {
130
+ this.fetcher = fetcher;
131
+ this._options = {
132
+ batchInterval,
133
+ shouldBatch,
134
+ maxBatchSize,
135
+ };
136
+ this._queue = [];
137
+ }
138
+
139
+ /**
140
+ * Fetch will send a graphql request and return the parsed json.
141
+ * @param {string} query - the graphql query.
142
+ * @param {Variables} variables - any variables you wish to inject as key/value pairs.
143
+ * @param {[string]} operationName - the graphql operationName.
144
+ * @param {Options} overrides - the client options overrides.
145
+ *
146
+ * @return {promise} resolves to parsed json of server response
147
+ *
148
+ * @example
149
+ * client.fetch(`
150
+ * query getHuman($id: ID!) {
151
+ * human(id: $id) {
152
+ * name
153
+ * height
154
+ * }
155
+ * }
156
+ * `, { id: "1001" }, 'getHuman')
157
+ * .then(human => {
158
+ * // do something with human
159
+ * console.log(human);
160
+ * });
161
+ */
162
+ fetch(
163
+ query: string,
164
+ variables?: Variables,
165
+ operationName?: string,
166
+ overrides: Options = {}
167
+ ): Promise<Result> {
168
+ const request: GraphqlOperation = {
169
+ query,
170
+ };
171
+ const options = Object.assign({}, this._options, overrides);
172
+
173
+ if (variables) {
174
+ request.variables = variables;
175
+ }
176
+
177
+ if (operationName) {
178
+ request.operationName = operationName;
179
+ }
180
+
181
+ const promise = new Promise<Result>((resolve, reject) => {
182
+ this._queue.push({
183
+ request,
184
+ resolve,
185
+ reject,
186
+ });
187
+
188
+ if (this._queue.length === 1) {
189
+ if (options.shouldBatch) {
190
+ setTimeout(() => dispatchQueue(this, options), options.batchInterval);
191
+ } else {
192
+ dispatchQueue(this, options);
193
+ }
194
+ }
195
+ });
196
+ return promise;
197
+ }
198
+
199
+ /**
200
+ * Fetch will send a graphql request and return the parsed json.
201
+ * @param {string} query - the graphql query.
202
+ * @param {Variables} variables - any variables you wish to inject as key/value pairs.
203
+ * @param {[string]} operationName - the graphql operationName.
204
+ * @param {Options} overrides - the client options overrides.
205
+ *
206
+ * @return {Promise<Array<Result>>} resolves to parsed json of server response
207
+ *
208
+ * @example
209
+ * client.forceFetch(`
210
+ * query getHuman($id: ID!) {
211
+ * human(id: $id) {
212
+ * name
213
+ * height
214
+ * }
215
+ * }
216
+ * `, { id: "1001" }, 'getHuman')
217
+ * .then(human => {
218
+ * // do something with human
219
+ * console.log(human);
220
+ * });
221
+ */
222
+ forceFetch(
223
+ query: string,
224
+ variables?: Variables,
225
+ operationName?: string,
226
+ overrides: Options = {}
227
+ ): Promise<Result> {
228
+ const request: GraphqlOperation = {
229
+ query,
230
+ };
231
+ const options = Object.assign({}, this._options, overrides, {
232
+ shouldBatch: false,
233
+ });
234
+
235
+ if (variables) {
236
+ request.variables = variables;
237
+ }
238
+
239
+ if (operationName) {
240
+ request.operationName = operationName;
241
+ }
242
+
243
+ const promise = new Promise<Result>((resolve, reject) => {
244
+ const client = new QueryBatcher(this.fetcher, this._options);
245
+ client._queue = [
246
+ {
247
+ request,
248
+ resolve,
249
+ reject,
250
+ },
251
+ ];
252
+ dispatchQueue(client, options);
253
+ });
254
+ return promise;
255
+ }
256
+ }
@@ -0,0 +1,55 @@
1
+ import { type BatchOptions, createFetcher } from './fetcher';
2
+ import type { ExecutionResult, LinkedType } from './types';
3
+ import { generateGraphqlOperation, type GraphqlOperation } from './generateGraphqlOperation';
4
+
5
+ export type Headers = HeadersInit | (() => HeadersInit) | (() => Promise<HeadersInit>);
6
+
7
+ export type BaseFetcher = (
8
+ operation: GraphqlOperation | GraphqlOperation[]
9
+ ) => Promise<ExecutionResult | ExecutionResult[]>;
10
+
11
+ export type ClientOptions = Omit<RequestInit, 'body' | 'headers'> & {
12
+ url?: string;
13
+ batch?: BatchOptions | boolean;
14
+ fetcher?: BaseFetcher;
15
+ fetch?: Function;
16
+ headers?: Headers;
17
+ };
18
+
19
+ export const createClient = ({
20
+ queryRoot,
21
+ mutationRoot,
22
+ subscriptionRoot,
23
+ ...options
24
+ }: ClientOptions & {
25
+ queryRoot?: LinkedType;
26
+ mutationRoot?: LinkedType;
27
+ subscriptionRoot?: LinkedType;
28
+ }) => {
29
+ const fetcher = createFetcher(options);
30
+ const client: {
31
+ query?: Function;
32
+ mutation?: Function;
33
+ } = {};
34
+
35
+ if (queryRoot) {
36
+ client.query = (request: any) => {
37
+ if (!queryRoot) throw new Error('queryRoot argument is missing');
38
+
39
+ const resultPromise = fetcher(generateGraphqlOperation('query', queryRoot, request));
40
+
41
+ return resultPromise;
42
+ };
43
+ }
44
+ if (mutationRoot) {
45
+ client.mutation = (request: any) => {
46
+ if (!mutationRoot) throw new Error('mutationRoot argument is missing');
47
+
48
+ const resultPromise = fetcher(generateGraphqlOperation('mutation', mutationRoot, request));
49
+
50
+ return resultPromise;
51
+ };
52
+ }
53
+
54
+ return client as any;
55
+ };
@@ -0,0 +1,26 @@
1
+ export class GenqlError extends Error {
2
+ errors: Array<GraphqlError> = [];
3
+ /**
4
+ * Partial data returned by the server
5
+ */
6
+ data?: any;
7
+ constructor(errors: any[], data: any) {
8
+ let message = Array.isArray(errors) ? errors.map((x) => x?.message || '').join('\n') : '';
9
+ if (!message) {
10
+ message = 'GraphQL error';
11
+ }
12
+ super(message);
13
+ this.errors = errors;
14
+ this.data = data;
15
+ }
16
+ }
17
+
18
+ interface GraphqlError {
19
+ message: string;
20
+ locations?: Array<{
21
+ line: number;
22
+ column: number;
23
+ }>;
24
+ path?: string[];
25
+ extensions?: Record<string, any>;
26
+ }
@@ -0,0 +1,95 @@
1
+ import { QueryBatcher } from './batcher';
2
+
3
+ import type { ClientOptions } from './createClient';
4
+ import type { GraphqlOperation } from './generateGraphqlOperation';
5
+ import { GenqlError } from './error';
6
+
7
+ export interface Fetcher {
8
+ (gql: GraphqlOperation): Promise<any>;
9
+ }
10
+
11
+ export type BatchOptions = {
12
+ batchInterval?: number; // ms
13
+ maxBatchSize?: number;
14
+ };
15
+
16
+ const DEFAULT_BATCH_OPTIONS = {
17
+ maxBatchSize: 10,
18
+ batchInterval: 40,
19
+ };
20
+
21
+ export const createFetcher = ({
22
+ url,
23
+ headers = {},
24
+ fetcher,
25
+ fetch: _fetch,
26
+ batch = false,
27
+ ...rest
28
+ }: ClientOptions): Fetcher => {
29
+ if (!url && !fetcher) {
30
+ throw new Error('url or fetcher is required');
31
+ }
32
+
33
+ fetcher =
34
+ fetcher ||
35
+ (async (body) => {
36
+ let headersObject = typeof headers == 'function' ? await headers() : headers;
37
+ headersObject = headersObject || {};
38
+ if (typeof fetch === 'undefined' && !_fetch) {
39
+ throw new Error(
40
+ 'Global `fetch` function is not available, pass a fetch polyfill to Genql `createClient`'
41
+ );
42
+ }
43
+ let fetchImpl = _fetch || fetch;
44
+ const res = await fetchImpl(url!, {
45
+ headers: {
46
+ 'Content-Type': 'application/json',
47
+ ...headersObject,
48
+ },
49
+ method: 'POST',
50
+ body: JSON.stringify(body),
51
+ ...rest,
52
+ });
53
+ if (!res.ok) {
54
+ throw new Error(`${res.statusText}: ${await res.text()}`);
55
+ }
56
+ const json = await res.json();
57
+ return json;
58
+ });
59
+
60
+ if (!batch) {
61
+ return async (body) => {
62
+ const json = await fetcher!(body);
63
+ if (Array.isArray(json)) {
64
+ return json.map((json) => {
65
+ if (json?.errors?.length) {
66
+ throw new GenqlError(json.errors || [], json.data);
67
+ }
68
+ return json.data;
69
+ });
70
+ } else {
71
+ if (json?.errors?.length) {
72
+ throw new GenqlError(json.errors || [], json.data);
73
+ }
74
+ return json.data;
75
+ }
76
+ };
77
+ }
78
+
79
+ const batcher = new QueryBatcher(
80
+ async (batchedQuery) => {
81
+ // console.log(batchedQuery) // [{ query: 'query{user{age}}', variables: {} }, ...]
82
+ const json = await fetcher!(batchedQuery);
83
+ return json as any;
84
+ },
85
+ batch === true ? DEFAULT_BATCH_OPTIONS : batch
86
+ );
87
+
88
+ return async ({ query, variables }) => {
89
+ const json = await batcher.fetch(query, variables);
90
+ if (json?.data) {
91
+ return json.data;
92
+ }
93
+ throw new Error('Genql batch fetcher returned unexpected result ' + JSON.stringify(json));
94
+ };
95
+ };
@@ -0,0 +1,193 @@
1
+ import type { LinkedField, LinkedType } from './types';
2
+
3
+ export interface Args {
4
+ [arg: string]: any | undefined;
5
+ }
6
+
7
+ export interface Fields {
8
+ [field: string]: Request;
9
+ }
10
+
11
+ export type Request = boolean | number | Fields;
12
+
13
+ export interface Variables {
14
+ [name: string]: {
15
+ value: any;
16
+ typing: [LinkedType, string];
17
+ };
18
+ }
19
+
20
+ export interface Context {
21
+ root: LinkedType;
22
+ varCounter: number;
23
+ variables: Variables;
24
+ fragmentCounter: number;
25
+ fragments: string[];
26
+ }
27
+
28
+ export interface GraphqlOperation {
29
+ query: string;
30
+ variables?: { [name: string]: any };
31
+ operationName?: string;
32
+ }
33
+
34
+ const parseRequest = (request: Request | undefined, ctx: Context, path: string[]): string => {
35
+ if (typeof request === 'object' && '__args' in request) {
36
+ const args: any = request.__args;
37
+ let fields: Request | undefined = { ...request };
38
+ delete fields.__args;
39
+ const argNames = Object.keys(args);
40
+
41
+ if (argNames.length === 0) {
42
+ return parseRequest(fields, ctx, path);
43
+ }
44
+
45
+ const field = getFieldFromPath(ctx.root, path);
46
+
47
+ const argStrings = argNames.map((argName) => {
48
+ ctx.varCounter++;
49
+ const varName = `v${ctx.varCounter}`;
50
+
51
+ const typing = field.args && field.args[argName]; // typeMap used here, .args
52
+
53
+ if (!typing) {
54
+ throw new Error(`no typing defined for argument \`${argName}\` in path \`${path.join('.')}\``);
55
+ }
56
+
57
+ ctx.variables[varName] = {
58
+ value: args[argName],
59
+ typing,
60
+ };
61
+
62
+ return `${argName}:$${varName}`;
63
+ });
64
+ return `(${argStrings})${parseRequest(fields, ctx, path)}`;
65
+ } else if (typeof request === 'object' && Object.keys(request).length > 0) {
66
+ const fields = request;
67
+ const fieldNames = Object.keys(fields).filter((k) => Boolean(fields[k]));
68
+
69
+ if (fieldNames.length === 0) {
70
+ throw new Error(`field selection should not be empty: ${path.join('.')}`);
71
+ }
72
+
73
+ const type = path.length > 0 ? getFieldFromPath(ctx.root, path).type : ctx.root;
74
+ const scalarFields = type.scalar;
75
+
76
+ let scalarFieldsFragment: string | undefined;
77
+
78
+ if (fieldNames.includes('__scalar')) {
79
+ const falsyFieldNames = new Set(Object.keys(fields).filter((k) => !Boolean(fields[k])));
80
+ if (scalarFields?.length) {
81
+ ctx.fragmentCounter++;
82
+ scalarFieldsFragment = `f${ctx.fragmentCounter}`;
83
+
84
+ ctx.fragments.push(
85
+ `fragment ${scalarFieldsFragment} on ${type.name}{${scalarFields
86
+ .filter((f) => !falsyFieldNames.has(f))
87
+ .join(',')}}`
88
+ );
89
+ }
90
+ }
91
+
92
+ const fieldsSelection = fieldNames
93
+ .filter((f) => !['__scalar', '__name'].includes(f))
94
+ .map((f) => {
95
+ const parsed = parseRequest(fields[f], ctx, [...path, f]);
96
+
97
+ if (f.startsWith('on_')) {
98
+ ctx.fragmentCounter++;
99
+ const implementationFragment = `f${ctx.fragmentCounter}`;
100
+
101
+ const typeMatch = f.match(/^on_(.+)/);
102
+
103
+ if (!typeMatch || !typeMatch[1]) throw new Error('match failed');
104
+
105
+ ctx.fragments.push(`fragment ${implementationFragment} on ${typeMatch[1]}${parsed}`);
106
+
107
+ return `...${implementationFragment}`;
108
+ } else {
109
+ return `${f}${parsed}`;
110
+ }
111
+ })
112
+ .concat(scalarFieldsFragment ? [`...${scalarFieldsFragment}`] : [])
113
+ .join(',');
114
+
115
+ return `{${fieldsSelection}}`;
116
+ } else {
117
+ return '';
118
+ }
119
+ };
120
+
121
+ export const generateGraphqlOperation = (
122
+ operation: 'query' | 'mutation' | 'subscription',
123
+ root: LinkedType,
124
+ fields?: Fields
125
+ ): GraphqlOperation => {
126
+ const ctx: Context = {
127
+ root: root,
128
+ varCounter: 0,
129
+ variables: {},
130
+ fragmentCounter: 0,
131
+ fragments: [],
132
+ };
133
+ const result = parseRequest(fields, ctx, []);
134
+
135
+ const varNames = Object.keys(ctx.variables);
136
+
137
+ const varsString =
138
+ varNames.length > 0
139
+ ? `(${varNames.map((v) => {
140
+ const variableType = ctx.variables[v].typing[1];
141
+ return `$${v}:${variableType}`;
142
+ })})`
143
+ : '';
144
+
145
+ const operationName = fields?.__name || '';
146
+
147
+ return {
148
+ query: [`${operation} ${operationName}${varsString}${result}`, ...ctx.fragments].join(','),
149
+ variables: Object.keys(ctx.variables).reduce<{ [name: string]: any }>((r, v) => {
150
+ r[v] = ctx.variables[v].value;
151
+ return r;
152
+ }, {}),
153
+ ...(operationName ? { operationName: operationName.toString() } : {}),
154
+ };
155
+ };
156
+
157
+ export const getFieldFromPath = (root: LinkedType | undefined, path: string[]) => {
158
+ let current: LinkedField | undefined;
159
+
160
+ if (!root) throw new Error('root type is not provided');
161
+
162
+ if (path.length === 0) throw new Error(`path is empty`);
163
+
164
+ path.forEach((f) => {
165
+ const type = current ? current.type : root;
166
+
167
+ if (!type.fields) throw new Error(`type \`${type.name}\` does not have fields`);
168
+
169
+ const possibleTypes = Object.keys(type.fields)
170
+ .filter((i) => i.startsWith('on_'))
171
+ .reduce(
172
+ (types, fieldName) => {
173
+ const field = type.fields && type.fields[fieldName];
174
+ if (field) types.push(field.type);
175
+ return types;
176
+ },
177
+ [type]
178
+ );
179
+
180
+ let field: LinkedField | null = null;
181
+
182
+ possibleTypes.forEach((type) => {
183
+ const found = type.fields && type.fields[f];
184
+ if (found) field = found;
185
+ });
186
+
187
+ if (!field) throw new Error(`type \`${type.name}\` does not have a field \`${f}\``);
188
+
189
+ current = field;
190
+ });
191
+
192
+ return current as LinkedField;
193
+ };
@@ -0,0 +1,12 @@
1
+ export { createClient } from './createClient';
2
+ export type { ClientOptions } from './createClient';
3
+ export type { FieldsSelection } from './typeSelection';
4
+ export { generateGraphqlOperation } from './generateGraphqlOperation';
5
+ export type { GraphqlOperation } from './generateGraphqlOperation';
6
+ export { linkTypeMap } from './linkTypeMap';
7
+ // export { Observable } from 'zen-observable-ts'
8
+ export { createFetcher } from './fetcher';
9
+ export { GenqlError } from './error';
10
+ export const everything = {
11
+ __scalar: true,
12
+ };