mirror: add public method `registerObject` (#870)
Summary: This function informs the GraphQL mirror of the existence of an object, specified by its global ID and its concrete typename (“concrete” meaning “object type”—like `User`, not `Actor`). The function will be called extensively internally as more objects are discovered while traversing the graph, but also needs to be exposed as a public entry point: a client needs to call this function at least once to register the root node of interest. A typical client workflow, once all of #622 is implemented, might be: 1. Issue a standalone GraphQL query to find the ID of a root node, like a GitHub repository: `repository(owner: "foo", name: "bar") { id }`. 2. Call `registerObject` with the ID found in the previous step. 3. Instruct the mirror to recursively update all dependencies. 4. Extract data from the mirror. As of this commit, steps (1) and (2) are possible. This commit makes progress toward #622. Test Plan: Unit tests included, with full coverage; run `yarn unit`. wchargin-branch: mirror-registerobject
This commit is contained in:
parent
12aa5b7439
commit
ab5b6ecb68
|
@ -257,6 +257,102 @@ export class Mirror {
|
||||||
.prepare("INSERT INTO updates (time_epoch_millis) VALUES (?)")
|
.prepare("INSERT INTO updates (time_epoch_millis) VALUES (?)")
|
||||||
.run(+updateTimestamp).lastInsertROWID;
|
.run(+updateTimestamp).lastInsertROWID;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inform the GraphQL mirror of the existence of an object. The
|
||||||
|
* object's name and concrete type must be specified. The concrete
|
||||||
|
* type must be an OBJECT type in the GraphQL schema.
|
||||||
|
*
|
||||||
|
* If the object has previously been registered with the same type, no
|
||||||
|
* action is taken and no error is raised. If the object has
|
||||||
|
* previously been registered with a different type, an error is
|
||||||
|
* thrown, and the database is left unchanged.
|
||||||
|
*/
|
||||||
|
registerObject(object: {|
|
||||||
|
+typename: Schema.Typename,
|
||||||
|
+id: Schema.ObjectId,
|
||||||
|
|}): void {
|
||||||
|
_inTransaction(this._db, () => {
|
||||||
|
this._nontransactionallyRegisterObject(object);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* As `registerObject`, but do not enter any transactions. Other
|
||||||
|
* methods may call this method as a subroutine in a larger
|
||||||
|
* transaction.
|
||||||
|
*/
|
||||||
|
_nontransactionallyRegisterObject(object: {|
|
||||||
|
+typename: Schema.Typename,
|
||||||
|
+id: Schema.ObjectId,
|
||||||
|
|}): void {
|
||||||
|
const db = this._db;
|
||||||
|
const {typename, id} = object;
|
||||||
|
|
||||||
|
const existingTypename = db
|
||||||
|
.prepare("SELECT typename FROM objects WHERE id = ?")
|
||||||
|
.pluck()
|
||||||
|
.get(id);
|
||||||
|
if (existingTypename === typename) {
|
||||||
|
// Already registered; nothing to do.
|
||||||
|
return;
|
||||||
|
} else if (existingTypename !== undefined) {
|
||||||
|
const s = JSON.stringify;
|
||||||
|
throw new Error(
|
||||||
|
`Inconsistent type for ID ${s(id)}: ` +
|
||||||
|
`expected ${s(existingTypename)}, got ${s(typename)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._schema[typename] == null) {
|
||||||
|
throw new Error("Unknown type: " + JSON.stringify(typename));
|
||||||
|
}
|
||||||
|
if (this._schema[typename].type !== "OBJECT") {
|
||||||
|
throw new Error(
|
||||||
|
"Cannot add object of non-object type: " +
|
||||||
|
`${JSON.stringify(typename)} (${this._schema[typename].type})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._db
|
||||||
|
.prepare(
|
||||||
|
dedent`\
|
||||||
|
INSERT INTO objects (id, last_update, typename)
|
||||||
|
VALUES (:id, NULL, :typename)
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.run({id, typename});
|
||||||
|
this._db
|
||||||
|
.prepare(
|
||||||
|
dedent`\
|
||||||
|
INSERT INTO ${_primitivesTableName(typename)} (id)
|
||||||
|
VALUES (?)
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.run(id);
|
||||||
|
const addLink = this._db.prepare(
|
||||||
|
dedent`\
|
||||||
|
INSERT INTO links (parent_id, fieldname, child_id)
|
||||||
|
VALUES (:id, :fieldname, NULL)
|
||||||
|
`
|
||||||
|
);
|
||||||
|
const addConnection = this._db.prepare(
|
||||||
|
// These fields are initialized to NULL because there has
|
||||||
|
// been no update and so they have no meaningful values:
|
||||||
|
// last_update, total_count, has_next_page, end_cursor.
|
||||||
|
dedent`\
|
||||||
|
INSERT INTO connections (object_id, fieldname)
|
||||||
|
VALUES (:id, :fieldname)
|
||||||
|
`
|
||||||
|
);
|
||||||
|
const objectType = this._schemaInfo.objectTypes[typename];
|
||||||
|
for (const fieldname of objectType.linkFieldNames) {
|
||||||
|
addLink.run({id, fieldname});
|
||||||
|
}
|
||||||
|
for (const fieldname of objectType.connectionFieldNames) {
|
||||||
|
addConnection.run({id, fieldname});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -213,6 +213,141 @@ describe("graphql/mirror", () => {
|
||||||
).toEqual(3);
|
).toEqual(3);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("registerObject", () => {
|
||||||
|
it("adds an object and its connections, links, and primitives", () => {
|
||||||
|
const db = new Database(":memory:");
|
||||||
|
const schema = buildGithubSchema();
|
||||||
|
const mirror = new Mirror(db, schema);
|
||||||
|
mirror.registerObject({
|
||||||
|
typename: "Issue",
|
||||||
|
id: "issue:sourcecred/example-github#1",
|
||||||
|
});
|
||||||
|
|
||||||
|
const issueId = "issue:sourcecred/example-github#1";
|
||||||
|
expect(
|
||||||
|
db
|
||||||
|
.prepare("SELECT * FROM objects WHERE typename = ? AND id = ?")
|
||||||
|
.all("Issue", issueId)
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
db
|
||||||
|
.prepare(
|
||||||
|
"SELECT fieldname FROM connections WHERE object_id = ? " +
|
||||||
|
"ORDER BY fieldname ASC"
|
||||||
|
)
|
||||||
|
.pluck()
|
||||||
|
.all(issueId)
|
||||||
|
).toEqual(["comments"]);
|
||||||
|
expect(
|
||||||
|
db
|
||||||
|
.prepare(
|
||||||
|
"SELECT fieldname FROM links WHERE parent_id = ? " +
|
||||||
|
"ORDER BY fieldname ASC"
|
||||||
|
)
|
||||||
|
.pluck()
|
||||||
|
.all(issueId)
|
||||||
|
).toEqual(["author", "parent"].sort());
|
||||||
|
expect(
|
||||||
|
db.prepare("SELECT * FROM primitives_Issue WHERE id = ?").all(issueId)
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
id: issueId,
|
||||||
|
url: null,
|
||||||
|
title: null,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
db
|
||||||
|
.prepare(
|
||||||
|
"SELECT COUNT(1) FROM connections WHERE last_update IS NOT NULL"
|
||||||
|
)
|
||||||
|
.pluck()
|
||||||
|
.get()
|
||||||
|
).toBe(0);
|
||||||
|
expect(
|
||||||
|
db
|
||||||
|
.prepare("SELECT COUNT(1) FROM links WHERE child_id IS NOT NULL")
|
||||||
|
.pluck()
|
||||||
|
.get()
|
||||||
|
).toBe(0);
|
||||||
|
expect(
|
||||||
|
db
|
||||||
|
.prepare(
|
||||||
|
"SELECT COUNT(1) FROM primitives_Issue WHERE " +
|
||||||
|
"url IS NOT NULL OR title IS NOT NULL"
|
||||||
|
)
|
||||||
|
.pluck()
|
||||||
|
.get()
|
||||||
|
).toBe(0);
|
||||||
|
});
|
||||||
|
it("doesn't touch an existing object with the same typename", () => {
|
||||||
|
const db = new Database(":memory:");
|
||||||
|
const schema = buildGithubSchema();
|
||||||
|
const mirror = new Mirror(db, schema);
|
||||||
|
const objectId = "issue:sourcecred/example-github#1";
|
||||||
|
mirror.registerObject({
|
||||||
|
typename: "Issue",
|
||||||
|
id: objectId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateId = mirror._createUpdate(new Date(123));
|
||||||
|
db.prepare(
|
||||||
|
"UPDATE objects SET last_update = :updateId WHERE id = :objectId"
|
||||||
|
).run({updateId, objectId});
|
||||||
|
|
||||||
|
mirror.registerObject({
|
||||||
|
typename: "Issue",
|
||||||
|
id: objectId,
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
db.prepare("SELECT * FROM objects WHERE id = ?").get(objectId)
|
||||||
|
).toEqual({
|
||||||
|
typename: "Issue",
|
||||||
|
id: objectId,
|
||||||
|
last_update: updateId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("rejects if an existing object's typename were to change", () => {
|
||||||
|
const db = new Database(":memory:");
|
||||||
|
const schema = buildGithubSchema();
|
||||||
|
const mirror = new Mirror(db, schema);
|
||||||
|
mirror.registerObject({typename: "Issue", id: "my-favorite-id"});
|
||||||
|
expect(() => {
|
||||||
|
mirror.registerObject({typename: "User", id: "my-favorite-id"});
|
||||||
|
}).toThrow(
|
||||||
|
'Inconsistent type for ID "my-favorite-id": ' +
|
||||||
|
'expected "Issue", got "User"'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("rejects an unknown type", () => {
|
||||||
|
const db = new Database(":memory:");
|
||||||
|
const schema = buildGithubSchema();
|
||||||
|
const mirror = new Mirror(db, schema);
|
||||||
|
expect(() =>
|
||||||
|
mirror.registerObject({
|
||||||
|
typename: "Wat",
|
||||||
|
id: "repo:sourcecred/example-github",
|
||||||
|
})
|
||||||
|
).toThrow('Unknown type: "Wat"');
|
||||||
|
expect(db.prepare("SELECT * FROM objects").all()).toHaveLength(0);
|
||||||
|
expect(db.prepare("SELECT * FROM connections").all()).toHaveLength(0);
|
||||||
|
});
|
||||||
|
it("rejects a union type", () => {
|
||||||
|
const db = new Database(":memory:");
|
||||||
|
const schema = buildGithubSchema();
|
||||||
|
const mirror = new Mirror(db, schema);
|
||||||
|
expect(() =>
|
||||||
|
mirror.registerObject({
|
||||||
|
typename: "Actor",
|
||||||
|
id: "user:credbot",
|
||||||
|
})
|
||||||
|
).toThrow('Cannot add object of non-object type: "Actor" (UNION)');
|
||||||
|
expect(db.prepare("SELECT * FROM objects").all()).toHaveLength(0);
|
||||||
|
expect(db.prepare("SELECT * FROM connections").all()).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("_buildSchemaInfo", () => {
|
describe("_buildSchemaInfo", () => {
|
||||||
|
|
Loading…
Reference in New Issue