mirror: precompute some useful schema info (#857)

Summary:
This is mostly useful not for computational efficiency, but for ease of
implementation: there end up being multiple places where we want to find
(say) the primitive fields on an object, and having to go through the
whole iterate-and-switch-and-push process repeatedly is annoying.

Test Plan:
Unit tests included, with full coverage; run `yarn unit`.

wchargin-branch: mirror-schema-info
This commit is contained in:
William Chargin 2018-09-18 16:24:38 -07:00 committed by GitHub
parent 1b1a1e4d46
commit e69ff57c58
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 130 additions and 1 deletions

View File

@ -12,6 +12,7 @@ import * as Schema from "./schema";
export class Mirror {
+_db: Database;
+_schema: Schema.Schema;
+_schemaInfo: SchemaInfo;
/**
* Create a GraphQL mirror using the given database connection and
@ -33,6 +34,7 @@ export class Mirror {
if (schema == null) throw new Error("schema: " + String(schema));
this._db = db;
this._schema = schema;
this._schemaInfo = _buildSchemaInfo(this._schema);
this._initialize();
}
@ -276,6 +278,98 @@ export class Mirror {
}
}
/**
* Decomposition of a schema, grouping types by their kind (object vs.
* union) and object fields by their kind (primitive vs. link vs.
* connection).
*
* All arrays contain elements in arbitrary order.
*/
type SchemaInfo = {|
+objectTypes: {|
+[Schema.Typename]: {|
+fields: {|+[Schema.Fieldname]: Schema.FieldType|},
+primitiveFieldNames: $ReadOnlyArray<Schema.Fieldname>,
+linkFieldNames: $ReadOnlyArray<Schema.Fieldname>,
+connectionFieldNames: $ReadOnlyArray<Schema.Fieldname>,
// There is always exactly one ID field, so it needs no
// special representation. (It's still included in the `fields`
// dictionary, though.)
|},
|},
+unionTypes: {|
+[Schema.Fieldname]: {|
+clauses: $ReadOnlyArray<Schema.Typename>,
|},
|},
|};
export function _buildSchemaInfo(schema: Schema.Schema): SchemaInfo {
const result = {
objectTypes: (({}: any): {|
[Schema.Typename]: {|
+fields: {|+[Schema.Fieldname]: Schema.FieldType|},
+primitiveFieldNames: Array<Schema.Fieldname>,
+linkFieldNames: Array<Schema.Fieldname>,
+connectionFieldNames: Array<Schema.Fieldname>,
|},
|}),
unionTypes: (({}: any): {|
[Schema.Fieldname]: {|
+clauses: $ReadOnlyArray<Schema.Typename>,
|},
|}),
};
for (const typename of Object.keys(schema)) {
const type = schema[typename];
switch (type.type) {
case "OBJECT": {
const entry: {|
+fields: {|+[Schema.Fieldname]: Schema.FieldType|},
+primitiveFieldNames: Array<Schema.Fieldname>,
+linkFieldNames: Array<Schema.Fieldname>,
+connectionFieldNames: Array<Schema.Fieldname>,
|} = {
fields: type.fields,
primitiveFieldNames: [],
linkFieldNames: [],
connectionFieldNames: [],
};
result.objectTypes[typename] = entry;
for (const fieldname of Object.keys(type.fields)) {
const field = type.fields[fieldname];
switch (field.type) {
case "ID":
break;
case "PRIMITIVE":
entry.primitiveFieldNames.push(fieldname);
break;
case "NODE":
entry.linkFieldNames.push(fieldname);
break;
case "CONNECTION":
entry.connectionFieldNames.push(fieldname);
break;
// istanbul ignore next
default:
throw new Error((field.type: empty));
}
}
break;
}
case "UNION": {
const entry = {clauses: Object.keys(type.clauses)};
result.unionTypes[typename] = entry;
break;
}
// istanbul ignore next
default:
throw new Error((type.type: empty));
}
}
return result;
}
/**
* Execute a function inside a database transaction.
*

View File

@ -5,7 +5,7 @@ import fs from "fs";
import tmp from "tmp";
import * as Schema from "./schema";
import {_inTransaction, Mirror} from "./mirror";
import {_buildSchemaInfo, _inTransaction, Mirror} from "./mirror";
describe("graphql/mirror", () => {
function buildGithubSchema(): Schema.Schema {
@ -175,6 +175,41 @@ describe("graphql/mirror", () => {
});
});
describe("_buildSchemaInfo", () => {
it("processes object types properly", () => {
const result = _buildSchemaInfo(buildGithubSchema());
expect(Object.keys(result.objectTypes).sort()).toEqual(
[
"Repository",
"Issue",
"IssueComment",
"User",
"Bot",
"Organization",
].sort()
);
expect(result.objectTypes["Issue"].fields).toEqual(
(buildGithubSchema().Issue: any).fields
);
expect(
result.objectTypes["Issue"].primitiveFieldNames.slice().sort()
).toEqual(["url", "title"].sort());
expect(result.objectTypes["Issue"].linkFieldNames.slice().sort()).toEqual(
["author", "parent"].sort()
);
expect(
result.objectTypes["Issue"].connectionFieldNames.slice().sort()
).toEqual(["comments"].sort());
});
it("processes union types correctly", () => {
const result = _buildSchemaInfo(buildGithubSchema());
expect(Object.keys(result.unionTypes).sort()).toEqual(["Actor"].sort());
expect(result.unionTypes["Actor"].clauses.slice().sort()).toEqual(
["User", "Bot", "Organization"].sort()
);
});
});
describe("_inTransaction", () => {
it("runs its callback inside a transaction", () => {
// We use an on-disk database file here because we need to open