-
Notifications
You must be signed in to change notification settings - Fork 52
/
Copy pathcontext.ts
309 lines (263 loc) · 9.45 KB
/
context.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
import Logger from "./logger";
import Model from "../orm/model";
import { Model as ORMModel } from "@vuex-orm/core";
import { PluginComponents } from "@vuex-orm/core/lib/plugins/use";
import { downcaseFirstLetter, isEqual, pick, singularize } from "../support/utils";
import Apollo from "../graphql/apollo";
import Database from "@vuex-orm/core/lib/database/Database";
import { Data, Field, GraphQLType, Options } from "../support/interfaces";
import Schema from "../graphql/schema";
import { Mock, MockOptions } from "../test-utils";
import Adapter, { ConnectionMode } from "../adapters/adapter";
import DefaultAdapter from "../adapters/builtin/default-adapter";
import introspectionQuery from "../graphql/introspection-query";
/**
* Internal context of the plugin. This class contains all information, the models, database, logger and so on.
*
* It's a singleton class, so just call Context.getInstance() anywhere you need the context.
*/
export default class Context {
/**
* Contains the instance for the singleton pattern.
* @type {Context}
*/
public static instance: Context;
/**
* Components collection of Vuex-ORM
* @type {PluginComponents}
*/
public readonly components: PluginComponents;
/**
* The options which have been passed to VuexOrm.install
* @type {Options}
*/
public readonly options: Options;
/**
* GraphQL Adapter.
* @type {Adapter}
*/
public readonly adapter: Adapter;
/**
* The Vuex-ORM database
* @type {Database}
*/
public readonly database: Database;
/**
* Collection of all Vuex-ORM models wrapped in a Model instance.
* @type {Map<any, any>}
*/
public readonly models: Map<string, Model> = new Map();
/**
* When true, the logging is enabled.
* @type {boolean}
*/
public readonly debugMode: boolean = false;
/**
* Our nice Vuex-ORM-GraphQL logger
* @type {Logger}
*/
public readonly logger: Logger;
/**
* Instance of Apollo which cares about the communication with the graphql endpoint.
* @type {Apollo}
*/
public apollo!: Apollo;
/**
* The graphql schema. Is null until the first request.
* @type {Schema}
*/
public schema: Schema | undefined;
/**
* Tells if the schema is already loaded or the loading is currently processed.
* @type {boolean}
*/
private schemaWillBeLoaded: Promise<Schema> | undefined;
/**
* Defines how to query connections. 'auto' | 'nodes' | 'edges' | 'plain' | 'items'
*/
public connectionMode: ConnectionMode = ConnectionMode.AUTO;
/**
* Container for the global mocks.
* @type {Object}
*/
private globalMocks: { [key: string]: Array<Mock> } = {};
/**
* Private constructor, called by the setup method
*
* @constructor
* @param {PluginComponents} components The Vuex-ORM Components collection
* @param {Options} options The options passed to VuexORM.install
*/
private constructor(components: PluginComponents, options: Options) {
this.components = components;
this.options = options;
this.database = options.database;
this.debugMode = Boolean(options.debug);
this.logger = new Logger(this.debugMode);
this.adapter = options.adapter || new DefaultAdapter();
/* istanbul ignore next */
if (!options.database) {
throw new Error("database param is required to initialize vuex-orm-graphql!");
}
}
/**
* Get the singleton instance of the context.
* @returns {Context}
*/
public static getInstance(): Context {
return this.instance;
}
/**
* This is called only once and creates a new instance of the Context.
* @param {PluginComponents} components The Vuex-ORM Components collection
* @param {Options} options The options passed to VuexORM.install
* @returns {Context}
*/
public static setup(components: PluginComponents, options: Options): Context {
this.instance = new Context(components, options);
this.instance.apollo = new Apollo();
this.instance.collectModels();
this.instance.logger.group("Context setup");
this.instance.logger.log("components", this.instance.components);
this.instance.logger.log("options", this.instance.options);
this.instance.logger.log("database", this.instance.database);
this.instance.logger.log("models", this.instance.models);
this.instance.logger.groupEnd();
return this.instance;
}
public async loadSchema(): Promise<Schema> {
if (!this.schemaWillBeLoaded) {
this.schemaWillBeLoaded = new Promise(async (resolve, reject) => {
try {
this.logger.log("Fetching GraphQL Schema initially ...");
this.connectionMode = this.adapter.getConnectionMode();
// We send a custom header along with the request. This is required for our test suite to mock the schema request.
const context = {
headers: { "X-GraphQL-Introspection-Query": "true" }
};
const result = await this.apollo.simpleQuery(
introspectionQuery,
{},
true,
(context as unknown) as Data
);
this.schema = new Schema(result.data.__schema);
this.logger.log("GraphQL Schema successful fetched", result);
this.logger.log("Starting to process the schema ...");
this.processSchema();
this.logger.log("Schema procession done!");
resolve(this.schema);
} catch (e) {
reject(e);
}
});
}
return this.schemaWillBeLoaded;
}
public processSchema() {
this.models.forEach((model: Model) => {
let type: GraphQLType;
try {
type = this.schema!.getType(model.singularName)!;
} catch (error) {
this.logger.warn(`Ignoring entity ${model.singularName} because it's not in the schema.`);
return;
}
model.fields.forEach((field: Field, fieldName: string) => {
if (!type.fields!.find(f => f.name === fieldName)) {
this.logger.warn(
`Ignoring field ${model.singularName}.${fieldName} because it's not in the schema.`
);
// TODO: Move skipFields to the model
model.baseModel.skipFields = model.baseModel.skipFields ? model.baseModel.skipFields : [];
if (!model.baseModel.skipFields.includes(fieldName)) {
model.baseModel.skipFields.push(fieldName);
}
}
});
});
if (this.connectionMode === ConnectionMode.AUTO) {
this.connectionMode = this.schema!.determineQueryMode();
this.logger.log(`Connection Query Mode is ${this.connectionMode} by automatic detection`);
} else {
this.logger.log(`Connection Query Mode is ${this.connectionMode} by config`);
}
}
/**
* Returns a model from the model collection by it's name
*
* @param {Model|string} model A Model instance, a singular or plural name of the model
* @param {boolean} allowNull When true this method returns null instead of throwing an exception when no model was
* found. Default is false
* @returns {Model}
*/
public getModel(model: Model | string, allowNull: boolean = false): Model {
if (typeof model === "string") {
const name: string = singularize(downcaseFirstLetter(model));
model = this.models.get(name) as Model;
if (!allowNull && !model) throw new Error(`No such model ${name}!`);
}
return model;
}
/**
* Will add a mock for simple mutations or queries. These are model unrelated and have to be
* handled globally.
*
* @param {Mock} mock - Mock config.
*/
public addGlobalMock(mock: Mock): boolean {
if (this.findGlobalMock(mock.action, mock.options)) return false;
if (!this.globalMocks[mock.action]) this.globalMocks[mock.action] = [];
this.globalMocks[mock.action].push(mock);
return true;
}
/**
* Finds a global mock for the given action and options.
*
* @param {string} action - Name of the action like 'simpleQuery' or 'simpleMutation'.
* @param {MockOptions} options - MockOptions like { name: 'example' }.
* @returns {Mock | null} null when no mock was found.
*/
public findGlobalMock(action: string, options: MockOptions | undefined): Mock | null {
if (this.globalMocks[action]) {
return (
this.globalMocks[action].find(m => {
if (!m.options || !options) return true;
const relevantOptions = pick(options, Object.keys(m.options));
return isEqual(relevantOptions, m.options || {});
}) || null
);
}
return null;
}
/**
* Hook to be called by simpleMutation and simpleQuery actions in order to get the global mock
* returnValue.
*
* @param {string} action - Name of the action like 'simpleQuery' or 'simpleMutation'.
* @param {MockOptions} options - MockOptions.
* @returns {any} null when no mock was found.
*/
public globalMockHook(action: string, options: MockOptions): any {
let returnValue: null | { [key: string]: any } = null;
const mock = this.findGlobalMock(action, options);
if (mock) {
if (mock.returnValue instanceof Function) {
returnValue = mock.returnValue();
} else {
returnValue = mock.returnValue || null;
}
}
return returnValue;
}
/**
* Wraps all Vuex-ORM entities in a Model object and saves them into this.models
*/
private collectModels() {
this.database.entities.forEach((entity: any) => {
const model: Model = new Model(entity.model as typeof ORMModel);
this.models.set(model.singularName, model);
Model.augment(model);
});
}
}