diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..909ca07 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/batch/index.js b/batch/index.js index 25e4d97..e7ba5c1 100644 --- a/batch/index.js +++ b/batch/index.js @@ -2,12 +2,15 @@ import ObjectsBatcher from "./objectsBatcher"; import ObjectsBatchDeleter from "./objectsBatchDeleter"; import ReferencesBatcher from "./referencesBatcher"; import ReferencePayloadBuilder from "./referencePayloadBuilder"; +import { BeaconPath } from "../utils/beaconPath"; + +const batch = (client, dbVersionSupport) => { + const beaconPath = new BeaconPath(dbVersionSupport); -const batch = (client) => { return { objectsBatcher: () => new ObjectsBatcher(client), objectsBatchDeleter: () => new ObjectsBatchDeleter(client), - referencesBatcher: () => new ReferencesBatcher(client), + referencesBatcher: () => new ReferencesBatcher(client, beaconPath), referencePayloadBuilder: () => new ReferencePayloadBuilder(client), }; }; diff --git a/batch/journey.test.js b/batch/journey.test.js index 70770c1..bbc8093 100644 --- a/batch/journey.test.js +++ b/batch/journey.test.js @@ -56,8 +56,8 @@ describe("batch importing", () => { it("verifies they are now queryable", () => { return Promise.all([ - client.data.getterById().withId(thingIds[0]).do(), - client.data.getterById().withId(thingIds[1]).do(), + client.data.getterById().withId(thingIds[0]).withClassName(thingClassName).do(), + client.data.getterById().withId(thingIds[1]).withClassName(thingClassName).do(), ]).catch((e) => fail("it should not have error'd " + e)); }); }); @@ -94,8 +94,8 @@ describe("batch importing", () => { it("verifies they are now queryable", () => { return Promise.all([ - client.data.getterById().withId(thingIds[2]).do(), - client.data.getterById().withId(thingIds[3]).do(), + client.data.getterById().withId(thingIds[2]).withClassName(thingClassName).do(), + client.data.getterById().withId(thingIds[3]).withClassName(thingClassName).do(), ]).catch((e) => fail("it should not have error'd " + e)); }); }); @@ -132,8 +132,8 @@ describe("batch importing", () => { it("verifies they are now queryable", () => { return Promise.all([ - client.data.getterById().withId(toImport[0].id).do(), - client.data.getterById().withId(toImport[1].id).do(), + client.data.getterById().withId(toImport[0].id).withClassName(toImport[0].class).do(), + client.data.getterById().withId(toImport[1].id).withClassName(toImport[1].class).do(), ]).catch((e) => fail("it should not have error'd " + e)); }); }); @@ -144,16 +144,12 @@ describe("batch importing", () => { return client.batch .referencesBatcher() .withReference({ - from: - `weaviate://localhost/${thingClassName}/` + - `${thingIds[0]}/refProp`, - to: `weaviate://localhost/${otherThingIds[0]}`, + from: `weaviate://localhost/${thingClassName}/${thingIds[0]}/refProp`, + to: `weaviate://localhost/${otherThingClassName}/${otherThingIds[0]}`, }) .withReference({ - from: - `weaviate://localhost/${thingClassName}/` + - `${thingIds[1]}/refProp`, - to: `weaviate://localhost/${otherThingIds[1]}`, + from: `weaviate://localhost/${thingClassName}/${thingIds[1]}/refProp`, + to: `weaviate://localhost/${otherThingClassName}/${otherThingIds[1]}`, }) .do() .then((res) => { @@ -174,6 +170,7 @@ describe("batch importing", () => { .withFromRefProp("refProp") .withFromId(thingIds[2]) .withToId(otherThingIds[0]) + .withToClassName(otherThingClassName) .payload() ) .withReference( @@ -183,6 +180,7 @@ describe("batch importing", () => { .withFromRefProp("refProp") .withFromId(thingIds[3]) .withToId(otherThingIds[1]) + .withToClassName(otherThingClassName) .payload() ) .do() @@ -203,37 +201,41 @@ describe("batch importing", () => { client.data .getterById() .withId(thingIds[0]) + .withClassName(thingClassName) .do() .then((res) => { expect(res.properties.refProp[0].beacon).toEqual( - `weaviate://localhost/${otherThingIds[0]}` + `weaviate://localhost/${otherThingClassName}/${otherThingIds[0]}` ); }), client.data .getterById() .withId(thingIds[1]) + .withClassName(thingClassName) .do() .then((res) => { expect(res.properties.refProp[0].beacon).toEqual( - `weaviate://localhost/${otherThingIds[1]}` + `weaviate://localhost/${otherThingClassName}/${otherThingIds[1]}` ); }), client.data .getterById() .withId(thingIds[2]) + .withClassName(thingClassName) .do() .then((res) => { expect(res.properties.refProp[0].beacon).toEqual( - `weaviate://localhost/${otherThingIds[0]}` + `weaviate://localhost/${otherThingClassName}/${otherThingIds[0]}` ); }), client.data .getterById() .withId(thingIds[3]) + .withClassName(thingClassName) .do() .then((res) => { expect(res.properties.refProp[0].beacon).toEqual( - `weaviate://localhost/${otherThingIds[1]}` + `weaviate://localhost/${otherThingClassName}/${otherThingIds[1]}` ); }), ]).catch((e) => fail("it should not have error'd " + e)); @@ -395,7 +397,7 @@ describe("batch deleting", () => { }) }) - it("batch deletes fails due to validation", () => + it("batch deletes fails due to validation", () => client.batch .objectsBatchDeleter() .withClassName("") diff --git a/batch/referencePayloadBuilder.js b/batch/referencePayloadBuilder.js index 0d968ba..1e3ba47 100644 --- a/batch/referencePayloadBuilder.js +++ b/batch/referencePayloadBuilder.js @@ -1,3 +1,5 @@ +import { isValidStringProperty } from "../validation/string"; + export default class ReferencesBatcher { constructor(client) { this.client = client; @@ -24,6 +26,11 @@ export default class ReferencesBatcher { return this; }; + withToClassName(className) { + this.toClassName = className; + return this; + } + validateIsSet = (prop, name, setter) => { if (prop == undefined || prop == null || prop.length == 0) { this.errors = [ @@ -54,11 +61,16 @@ export default class ReferencesBatcher { throw new Error(this.errors.join(", ")); } + var beaconTo = `weaviate://localhost`; + if (isValidStringProperty(this.toClassName)) { + beaconTo = `${beaconTo}/${this.toClassName}`; + } + return { from: `weaviate://localhost/${this.fromClassName}` + `/${this.fromId}/${this.fromRefProp}`, - to: `weaviate://localhost/${this.toId}`, + to: `${beaconTo}/${this.toId}`, }; }; } diff --git a/batch/referencesBatcher.js b/batch/referencesBatcher.js index 1e3ca72..4322f52 100644 --- a/batch/referencesBatcher.js +++ b/batch/referencesBatcher.js @@ -1,6 +1,7 @@ export default class ReferencesBatcher { - constructor(client) { + constructor(client, beaconPath) { this.client = client; + this.beaconPath = beaconPath; this.references = []; this.errors = []; } @@ -34,6 +35,16 @@ export default class ReferencesBatcher { ); } const path = `/batch/references`; - return this.client.post(path, this.payload()); + const payloadPromise = Promise.all(this.references.map(ref => this.rebuildReferencePromise(ref))); + + return payloadPromise.then(payload => this.client.post(path, payload)); }; + + rebuildReferencePromise(reference) { + return this.beaconPath.rebuild(reference.to) + .then(beaconTo => ({ + from: reference.from, + to: beaconTo + })); + } } diff --git a/data/checker.js b/data/checker.js index 335cfe9..8a11c42 100644 --- a/data/checker.js +++ b/data/checker.js @@ -1,8 +1,7 @@ -import { buildObjectsPath } from "./path"; - export default class Checker { - constructor(client) { + constructor(client, objectsPath) { this.client = client; + this.objectsPath = objectsPath; this.errors = []; } @@ -41,7 +40,7 @@ export default class Checker { } this.validate(); - const path = buildObjectsPath(this.id, this.className); - return this.client.head(path); + return this.objectsPath.buildCheck(this.id, this.className) + .then(this.client.head) }; } diff --git a/data/creator.js b/data/creator.js index 4fa8800..a3725b5 100644 --- a/data/creator.js +++ b/data/creator.js @@ -1,8 +1,9 @@ import { isValidStringProperty } from "../validation/string"; export default class Creator { - constructor(client) { + constructor(client, objectsPath) { this.client = client; + this.objectsPath = objectsPath; this.errors = []; } @@ -53,7 +54,8 @@ export default class Creator { new Error("invalid usage: " + this.errors.join(", ")) ); } - const path = `/objects`; - return this.client.post(path, this.payload()); + + return this.objectsPath.buildCreate() + .then(path => this.client.post(path, this.payload())) }; } diff --git a/data/deleter.js b/data/deleter.js index 79c5d50..ad632f8 100644 --- a/data/deleter.js +++ b/data/deleter.js @@ -1,8 +1,7 @@ -import { buildObjectsPath } from "./path"; - export default class Deleter { - constructor(client) { + constructor(client, objectsPath) { this.client = client; + this.objectsPath = objectsPath; this.errors = []; } @@ -41,7 +40,7 @@ export default class Deleter { } this.validate(); - const path = buildObjectsPath(this.id, this.className); - return this.client.delete(path); + return this.objectsPath.buildDelete(this.id, this.className) + .then(this.client.delete); }; } diff --git a/data/getter.js b/data/getter.js index 662ac56..6028c47 100644 --- a/data/getter.js +++ b/data/getter.js @@ -1,10 +1,16 @@ export default class Getter { - constructor(client) { + constructor(client, objectsPath) { this.client = client; + this.objectsPath = objectsPath; this.errors = []; this.additionals = []; } + withClassName = (className) => { + this.className = className; + return this; + }; + withLimit = (limit) => { this.limit = limit; return this; @@ -25,21 +31,8 @@ export default class Getter { new Error("invalid usage: " + this.errors.join(", ")) ); } - let path = `/objects`; - - let params = []; - if (this.additionals.length > 0) { - params = [...params, `include=${this.additionals.join(",")}`]; - } - - if (this.limit) { - params = [...params, `limit=${this.limit}`]; - } - - if (params.length > 0) { - path += `?${params.join("&")}`; - } - return this.client.get(path); + return this.objectsPath.buildGet(this.className, this.limit, this.additionals) + .then(this.client.get); }; } diff --git a/data/getterById.js b/data/getterById.js index d35793e..82ec8f8 100644 --- a/data/getterById.js +++ b/data/getterById.js @@ -1,8 +1,7 @@ -import { buildObjectsPath } from "./path"; - export default class GetterById { - constructor(client) { + constructor(client, objectsPath) { this.client = client; + this.objectsPath = objectsPath; this.errors = []; this.additionals = []; } @@ -22,11 +21,6 @@ export default class GetterById { return this; }; - extendAdditionals = (prop) => { - this.additionals = [...this.additionals, prop]; - return this; - }; - withAdditional = (additionalFlag) => this.extendAdditionals(additionalFlag); withVector = () => this.extendAdditionals("vector"); @@ -52,11 +46,7 @@ export default class GetterById { ); } - let path = buildObjectsPath(this.id, this.className); - - if (this.additionals.length > 0) { - path += `?include=${this.additionals.join(",")}`; - } - return this.client.get(path); + return this.objectsPath.buildGetOne(this.id, this.className, this.additionals) + .then(this.client.get); }; } diff --git a/data/index.js b/data/index.js index c8670e4..a5e16c9 100644 --- a/data/index.js +++ b/data/index.js @@ -10,20 +10,26 @@ import ReferenceCreator from "./referenceCreator"; import ReferenceReplacer from "./referenceReplacer"; import ReferenceDeleter from "./referenceDeleter"; import ReferencePayloadBuilder from "./referencePayloadBuilder"; +import { ObjectsPath, ReferencesPath } from "./path"; +import { BeaconPath } from "../utils/beaconPath"; + +const data = (client, dbVersionSupport) => { + const objectsPath = new ObjectsPath(dbVersionSupport); + const referencesPath = new ReferencesPath(dbVersionSupport); + const beaconPath = new BeaconPath(dbVersionSupport); -const data = (client) => { return { - creator: () => new Creator(client), + creator: () => new Creator(client, objectsPath), validator: () => new Validator(client), - updater: () => new Updater(client), - merger: () => new Merger(client), - getter: () => new Getter(client), - getterById: () => new GetterById(client), - deleter: () => new Deleter(client), - checker: () => new Checker(client), - referenceCreator: () => new ReferenceCreator(client), - referenceReplacer: () => new ReferenceReplacer(client), - referenceDeleter: () => new ReferenceDeleter(client), + updater: () => new Updater(client, objectsPath), + merger: () => new Merger(client, objectsPath), + getter: () => new Getter(client, objectsPath), + getterById: () => new GetterById(client, objectsPath), + deleter: () => new Deleter(client, objectsPath), + checker: () => new Checker(client, objectsPath), + referenceCreator: () => new ReferenceCreator(client, referencesPath, beaconPath), + referenceReplacer: () => new ReferenceReplacer(client, referencesPath, beaconPath), + referenceDeleter: () => new ReferenceDeleter(client, referencesPath, beaconPath), referencePayloadBuilder: () => new ReferencePayloadBuilder(client), }; }; diff --git a/data/journey.test.js b/data/journey.test.js index e2c6def..2001b3e 100644 --- a/data/journey.test.js +++ b/data/journey.test.js @@ -140,6 +140,28 @@ describe("data", () => { .catch((e) => fail("it should not have errord: " + e)); }); + it("gets all classes objects", () => { + return client.data + .getter() + .withClassName(thingClassName) + .do() + .then((res) => { + expect(res.objects).toHaveLength(2); + expect(res.objects).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "1565c06c-463f-466c-9092-5930dbac3887", + properties: { stringProp: "with-id" }, + }), + expect.objectContaining({ + properties: { stringProp: "without-id" }, + }), + ]) + ); + }) + .catch((e) => fail("it should not have errord: " + e)); + }); + it("gets all things with all optional _additional params", () => { return client.data .getter() @@ -162,6 +184,26 @@ describe("data", () => { .catch((e) => fail("it should not have errord: " + e)); }); + it("gets all classes objects with all optional _additional params", () => { + return client.data + .getter() + .withClassName(thingClassName) + .withAdditional("classification") + .withAdditional("interpretation") + .withAdditional("nearestNeighbors") + .withAdditional("featureProjection") + .withVector() + .do() + .then((res) => { + expect(res.objects).toHaveLength(2); + expect(res.objects[0].vector.length).toBeGreaterThan(10); + expect(res.objects[0].additional.interpretation).toBeDefined(); + expect(res.objects[0].additional.featureProjection).toBeDefined(); + expect(res.objects[0].additional.nearestNeighbors).toBeDefined(); + }) + .catch((e) => fail("it should not have errord: " + e)); + }); + it("gets one thing by id only", () => { return client.data .getterById() @@ -201,8 +243,8 @@ describe("data", () => { .withClassName("DoesNotExist") .withId("1565c06c-463f-466c-9092-5930dbac3887") .do() - .catch(err => - expect(err).toEqual("usage error (500): {\"error\":[{\"message\":\"repo: object by id: index not found for class DoesNotExist\"}]}") + .catch(err => + expect(err).toEqual("usage error (404): ") ); }); @@ -309,7 +351,7 @@ describe("data", () => { .catch((e) => fail("it should not have errord: " + e)); }); - it("adds a reference to a thing", () => { + it("adds a reference to a thing by id only", () => { const sourceId = "599a0c64-5ed5-4d30-978b-6c9c45516db1"; const targetId = "1565c06c-463f-466c-9092-5930dbac3887"; @@ -324,7 +366,7 @@ describe("data", () => { .catch((e) => fail("it should not have errord: " + e)); }); - it("replaces all references of a thing", () => { + it("replaces all references of a thing by id only", () => { const sourceId = "599a0c64-5ed5-4d30-978b-6c9c45516db1"; const targetId = implicitThingId; @@ -339,7 +381,7 @@ describe("data", () => { .catch((e) => fail("it should not have errord: " + e)); }); - it("deletes a single reference of a thing", () => { + it("deletes a single reference of a thing by id only", () => { const sourceId = "599a0c64-5ed5-4d30-978b-6c9c45516db1"; const targetId = implicitThingId; @@ -354,6 +396,54 @@ describe("data", () => { .catch((e) => fail("it should not have errord: " + e)); }); + it("adds a reference to a thing by id and class name", () => { + const sourceId = "599a0c64-5ed5-4d30-978b-6c9c45516db1"; + const targetId = "1565c06c-463f-466c-9092-5930dbac3887"; + + return client.data + .referenceCreator() + .withId(sourceId) + .withClassName(refSourceClassName) + .withReferenceProperty("refProp") + .withReference( + client.data.referencePayloadBuilder().withId(targetId).withClassName(thingClassName).payload() + ) + .do() + .catch((e) => fail("it should not have errord: " + e)); + }); + + it("replaces all references of a thing by id and class name", () => { + const sourceId = "599a0c64-5ed5-4d30-978b-6c9c45516db1"; + const targetId = implicitThingId; + + return client.data + .referenceReplacer() + .withId(sourceId) + .withClassName(refSourceClassName) + .withReferenceProperty("refProp") + .withReferences([ + client.data.referencePayloadBuilder().withId(targetId).withClassName(thingClassName).payload(), + ]) + .do() + .catch((e) => fail("it should not have errord: " + e)); + }); + + it("deletes a single reference of a thing by id and class name", () => { + const sourceId = "599a0c64-5ed5-4d30-978b-6c9c45516db1"; + const targetId = implicitThingId; + + return client.data + .referenceDeleter() + .withId(sourceId) + .withClassName(refSourceClassName) + .withReferenceProperty("refProp") + .withReference( + client.data.referencePayloadBuilder().withId(targetId).withClassName(thingClassName).payload() + ) + .do() + .catch((e) => fail("it should not have errord: " + e)); + }); + it("checks that object exists by id only", () => { return client.data .checker() diff --git a/data/merger.js b/data/merger.js index 3afc97b..484035f 100644 --- a/data/merger.js +++ b/data/merger.js @@ -1,9 +1,9 @@ import { isValidStringProperty } from "../validation/string"; -import { buildObjectsPath } from "./path"; export default class Merger { - constructor(client) { + constructor(client, objectsPath) { this.client = client; + this.objectsPath = objectsPath; this.errors = []; } @@ -57,7 +57,7 @@ export default class Merger { ); } - const path = buildObjectsPath(this.id, this.className); - return this.client.patch(path, this.payload()); + return this.objectsPath.buildMerge(this.id, this.className) + .then(path => this.client.patch(path, this.payload())); }; } diff --git a/data/path.js b/data/path.js index 8a99623..0b921ed 100644 --- a/data/path.js +++ b/data/path.js @@ -1,9 +1,132 @@ import { isValidStringProperty } from "../validation/string"; -export function buildObjectsPath(id, className) { - let path = `objects`; - if (isValidStringProperty(className)) { - path = `${path}/${className}`; +const objectsPathPrefix = "/objects"; + +export class ObjectsPath { + + constructor(dbVersionSupport) { + this.dbVersionSupport = dbVersionSupport; + } + + buildCreate() { + return this.build({}, []); + } + buildDelete(id, className) { + return this.build({id, className}, [this.addClassNameDeprecatedNotSupportedCheck, this.addId]); + } + buildCheck(id, className) { + return this.build({id, className}, [this.addClassNameDeprecatedNotSupportedCheck, this.addId]); + } + buildGetOne(id, className, additionals) { + return this.build({id, className, additionals}, [this.addClassNameDeprecatedNotSupportedCheck, this.addId, this.addQueryParams]); + } + buildGet(className, limit, additionals) { + return this.build({className, limit, additionals}, [this.addQueryParamsForGet]); + } + buildUpdate(id, className) { + return this.build({id, className}, [this.addClassNameDeprecatedCheck, this.addId]); + } + buildMerge(id, className) { + return this.build({id, className}, [this.addClassNameDeprecatedCheck, this.addId]); + } + + build(params, modifiers) { + return this.dbVersionSupport.supportsClassNameNamespacedEndpointsPromise().then(support => { + var path = objectsPathPrefix; + modifiers.forEach(modifier => { + path = modifier(params, path, support); + }); + return path; + }); + } + + addClassNameDeprecatedNotSupportedCheck(params, path, support) { + if (support.supports) { + if (isValidStringProperty(params.className)) { + return `${path}/${params.className}`; + } else { + support.warns.deprecatedNonClassNameNamespacedEndpointsForObjects(); + } + } else { + support.warns.notSupportedClassNamespacedEndpointsForObjects(); + } + return path; + } + addClassNameDeprecatedCheck(params, path, support) { + if (support.supports) { + if (isValidStringProperty(params.className)) { + return `${path}/${params.className}`; + } else { + support.warns.deprecatedNonClassNameNamespacedEndpointsForObjects(); + } + } + return path; + } + addId(params, path) { + if (isValidStringProperty(params.id)) { + return `${path}/${params.id}`; + } + return path; + } + addQueryParams(params, path) { + const queryParams = []; + if (Array.isArray(params.additionals) && params.additionals.length > 0) { + queryParams.push(`include=${params.additionals.join(",")}`); + } + if (queryParams.length > 0) { + return `${path}?${queryParams.join("&")}`; + } + return path; + } + addQueryParamsForGet(params, path, support) { + const queryParams = []; + if (Array.isArray(params.additionals) && params.additionals.length > 0) { + queryParams.push(`include=${params.additionals.join(",")}`); + } + if (typeof params.limit == "number" && params.limit > 0) { + queryParams.push(`limit=${params.limit}`); + } + if (isValidStringProperty(params.className)) { + if (support.supports) { + queryParams.push(`class=${params.className}`); + } else { + support.warns.notSupportedClassParameterInEndpointsForObjects(); + } + } + if (queryParams.length > 0) { + return `${path}?${queryParams.join("&")}`; + } + return path; + } +} + + +export class ReferencesPath { + + constructor(dbVersionSupport) { + this.dbVersionSupport = dbVersionSupport; + } + + build(id, className, property) { + return this.dbVersionSupport.supportsClassNameNamespacedEndpointsPromise().then(support => { + var path = objectsPathPrefix; + if (support.supports) { + if (isValidStringProperty(className)) { + path = `${path}/${className}`; + } else { + support.warns.deprecatedNonClassNameNamespacedEndpointsForReferences(); + } + } else { + support.warns.notSupportedClassNamespacedEndpointsForReferences(); + } + if (isValidStringProperty(id)) { + path = `${path}/${id}`; + } + path = `${path}/references`; + if (isValidStringProperty(property)) { + path = `${path}/${property}`; + } + return path; + }); } - return `/${path}/${id}`; } diff --git a/data/referenceCreator.js b/data/referenceCreator.js index 7589afa..3202481 100644 --- a/data/referenceCreator.js +++ b/data/referenceCreator.js @@ -1,6 +1,8 @@ export default class ReferenceCreator { - constructor(client) { + constructor(client, referencesPath, beaconPath) { this.client = client; + this.referencesPath = referencesPath; + this.beaconPath = beaconPath; this.errors = []; } @@ -9,6 +11,11 @@ export default class ReferenceCreator { return this; }; + withClassName(className) { + this.className = className; + return this; + } + withReference = (ref) => { this.reference = ref; return this; @@ -47,7 +54,14 @@ export default class ReferenceCreator { new Error("invalid usage: " + this.errors.join(", ")) ); } - const path = `/objects/${this.id}/references/${this.refProp}`; - return this.client.post(path, this.payload(), false); + + return Promise.all([ + this.referencesPath.build(this.id, this.className, this.refProp), + this.beaconPath.rebuild(this.reference.beacon) + ]).then(results => { + const path = results[0]; + const beacon = results[1]; + return this.client.post(path, { beacon }, false); + }); }; } diff --git a/data/referenceDeleter.js b/data/referenceDeleter.js index c641cad..acaac30 100644 --- a/data/referenceDeleter.js +++ b/data/referenceDeleter.js @@ -1,6 +1,8 @@ export default class ReferenceDeleter { - constructor(client) { + constructor(client, referencesPath, beaconPath) { this.client = client; + this.referencesPath = referencesPath; + this.beaconPath = beaconPath; this.errors = []; } @@ -9,6 +11,11 @@ export default class ReferenceDeleter { return this; }; + withClassName(className) { + this.className = className; + return this; + } + withReference = (ref) => { this.reference = ref; return this; @@ -47,7 +54,14 @@ export default class ReferenceDeleter { new Error("invalid usage: " + this.errors.join(", ")) ); } - const path = `/objects/${this.id}/references/${this.refProp}`; - return this.client.delete(path, this.payload(), false); + + return Promise.all([ + this.referencesPath.build(this.id, this.className, this.refProp), + this.beaconPath.rebuild(this.reference.beacon) + ]).then(results => { + const path = results[0]; + const beacon = results[1]; + return this.client.delete(path, { beacon }, false); + }); }; } diff --git a/data/referencePayloadBuilder.js b/data/referencePayloadBuilder.js index 86f3a54..2a2aa93 100644 --- a/data/referencePayloadBuilder.js +++ b/data/referencePayloadBuilder.js @@ -1,3 +1,5 @@ +import { isValidStringProperty } from "../validation/string"; + export default class ReferencePayloadBuilder { constructor(client) { this.client = client; @@ -9,6 +11,11 @@ export default class ReferencePayloadBuilder { return this; }; + withClassName(className) { + this.className = className; + return this; + } + validateIsSet = (prop, name, setter) => { if (prop == undefined || prop == null || prop.length == 0) { this.errors = [ @@ -28,8 +35,12 @@ export default class ReferencePayloadBuilder { throw new Error(this.errors.join(", ")); } + var beacon = `weaviate://localhost`; + if (isValidStringProperty(this.className)) { + beacon = `${beacon}/${this.className}`; + } return { - beacon: `weaviate://localhost/${this.id}`, + beacon: `${beacon}/${this.id}`, }; }; } diff --git a/data/referenceReplacer.js b/data/referenceReplacer.js index f302058..5d699fe 100644 --- a/data/referenceReplacer.js +++ b/data/referenceReplacer.js @@ -1,6 +1,8 @@ export default class ReferenceReplacer { - constructor(client) { + constructor(client, referencesPath, beaconPath) { this.client = client; + this.referencesPath = referencesPath; + this.beaconPath = beaconPath; this.errors = []; } @@ -9,6 +11,11 @@ export default class ReferenceReplacer { return this; }; + withClassName(className) { + this.className = className; + return this; + } + withReferences = (refs) => { this.references = refs; return this; @@ -46,7 +53,23 @@ export default class ReferenceReplacer { new Error("invalid usage: " + this.errors.join(", ")) ); } - const path = `/objects/${this.id}/references/${this.refProp}`; - return this.client.put(path, this.payload(), false); + + var payloadPromise = Array.isArray(this.references) + ? Promise.all(this.references.map(ref => this.rebuildReferencePromise(ref))) + : Promise.resolve([]); + + return Promise.all([ + this.referencesPath.build(this.id, this.className, this.refProp), + payloadPromise + ]).then(results => { + const path = results[0]; + const payload = results[1]; + return this.client.put(path, payload, false); + }); }; + + rebuildReferencePromise(reference) { + return this.beaconPath.rebuild(reference.beacon) + .then(beacon => ({ beacon })); + } } diff --git a/data/updater.js b/data/updater.js index c084ede..57ac370 100644 --- a/data/updater.js +++ b/data/updater.js @@ -1,9 +1,9 @@ import { isValidStringProperty } from "../validation/string"; -import { buildObjectsPath } from "./path"; export default class Updater { - constructor(client) { + constructor(client, objectsPath) { this.client = client; + this.objectsPath = objectsPath; this.errors = []; } @@ -60,7 +60,7 @@ export default class Updater { ); } - const path = buildObjectsPath(this.id, this.className); - return this.client.put(path, this.payload()); + return this.objectsPath.buildUpdate(this.id, this.className) + .then(path => this.client.put(path, this.payload())); }; } diff --git a/docker-compose.yml b/docker-compose.yml index d327bb0..b24f69f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,7 @@ version: '3.4' services: weaviate: - # TODO: temporarily use this version until next release - image: semitechnologies/weaviate:latest@sha256:248a1e1f6f0ba817c00ead4edeecf9d19961d482a3c432165d95762bd44e3a0c + image: semitechnologies/weaviate:1.14.0 restart: on-failure:0 ports: - "8080:8080" @@ -14,7 +13,7 @@ services: DEFAULT_VECTORIZER_MODULE: text2vec-contextionary ENABLE_MODULES: text2vec-contextionary contextionary: - image: semitechnologies/contextionary:en0.16.0-v1.0.2 + image: semitechnologies/contextionary:en0.16.0-v1.1.0 ports: - "9999:9999" environment: diff --git a/httpClient.js b/httpClient.js index f0f9b6d..61dac54 100644 --- a/httpClient.js +++ b/httpClient.js @@ -77,18 +77,17 @@ const client = (config) => { const makeUrl = (basePath) => (path) => basePath + path; const makeCheckStatus = (expectResponseBody) => (res) => { - if (res.status >= 400 && res.status < 500) { - return res.json().then((err) => { + if (res.status >= 400) { + return res.text().then(errText => { + var err; + try { + // in case of invalid json response (like empty string) + err = JSON.stringify(JSON.parse(errText)) + } catch(e) { + err = errText + } return Promise.reject( - `usage error (${res.status}): ${JSON.stringify(err)}` - ); - }); - } - - if (res.status >= 500) { - return res.json().then((err) => { - return Promise.reject( - `usage error (${res.status}): ${JSON.stringify(err)}` + `usage error (${res.status}): ${err}` ); }); } diff --git a/index.js b/index.js index 00654b6..b5ed504 100644 --- a/index.js +++ b/index.js @@ -6,6 +6,7 @@ import batch from "./batch/index.js"; import misc from "./misc/index.js"; import c11y from "./c11y/index.js"; import { KIND_THINGS, KIND_ACTIONS } from "./kinds"; +import { DbVersionProvider, DbVersionSupport } from "./utils/dbVersion.js"; const app = { client: function (params) { @@ -28,13 +29,16 @@ const app = { headers: params.headers, }); + const dbVersionProvider = initDbVersionProvider(httpClient); + const dbVersionSupport = new DbVersionSupport(dbVersionProvider); + return { graphql: graphql(graphqlClient), schema: schema(httpClient), - data: data(httpClient), + data: data(httpClient, dbVersionSupport), classifications: classifications(httpClient), - batch: batch(httpClient), - misc: misc(httpClient), + batch: batch(httpClient, dbVersionSupport), + misc: misc(httpClient, dbVersionProvider), c11y: c11y(httpClient), }; }, @@ -44,5 +48,19 @@ const app = { KIND_ACTIONS, }; +function initDbVersionProvider(httpClient) { + const metaGetter = misc(httpClient).metaGetter(); + const versionGetter = () => { + return metaGetter.do() + .then(result => result.version) + .catch(_ => Promise.resolve("")); + } + + const dbVersionProvider = new DbVersionProvider(versionGetter); + dbVersionProvider.refresh(); + + return dbVersionProvider; +} + export default app; module.exports = app; diff --git a/misc/index.js b/misc/index.js index d420293..416594b 100644 --- a/misc/index.js +++ b/misc/index.js @@ -3,10 +3,10 @@ import ReadyChecker from "./readyChecker"; import MetaGetter from "./metaGetter"; import OpenidConfigurationGetter from "./openidConfigurationGetter"; -const misc = (client) => { +const misc = (client, dbVersionProvider) => { return { - liveChecker: () => new LiveChecker(client), - readyChecker: () => new ReadyChecker(client), + liveChecker: () => new LiveChecker(client, dbVersionProvider), + readyChecker: () => new ReadyChecker(client, dbVersionProvider), metaGetter: () => new MetaGetter(client), openidConfigurationGetter: () => new OpenidConfigurationGetter(client), }; diff --git a/misc/journey.test.js b/misc/journey.test.js index 1fd75d9..249c7d3 100644 --- a/misc/journey.test.js +++ b/misc/journey.test.js @@ -70,20 +70,4 @@ describe("misc endpoints", () => { }) .catch((e) => fail("it should not have errord: " + e)); }); - - it("fetches the server version", async () => { - let version = await client.misc.metaGetter() - .do() - .then(res => { - return res.version; - }) - .catch((e) => fail("it should not have errord: " + e)); - - return client.misc.metaGetter() - .fetchVersion() - .then(res => { - expect(res).toEqual(version); - }) - .catch((e) => fail("it should not have errord: " + e)); - }); }); diff --git a/misc/liveChecker.js b/misc/liveChecker.js index 8b3b978..ed7f2eb 100644 --- a/misc/liveChecker.js +++ b/misc/liveChecker.js @@ -1,12 +1,16 @@ export default class LiveChecker { - constructor(client) { + constructor(client, dbVersionProvider) { this.client = client; + this.dbVersionProvider = dbVersionProvider; } do = () => { return this.client .get("/.well-known/live", false) - .then(() => Promise.resolve(true)) + .then(() => { + setTimeout(() => this.dbVersionProvider.refresh()); + return Promise.resolve(true); + }) .catch(() => Promise.resolve(false)); }; } diff --git a/misc/metaGetter.js b/misc/metaGetter.js index 7045e3e..c0a7900 100644 --- a/misc/metaGetter.js +++ b/misc/metaGetter.js @@ -6,14 +6,4 @@ export default class MetaGetter { do = () => { return this.client.get("/meta", true); }; - - fetchVersion = () => { - return this.client.get("/meta", true) - .then(res => { - return res.version; - }) - .catch(err => { - return Promise.reject(err); - }); - } } diff --git a/misc/readyChecker.js b/misc/readyChecker.js index d91a6f0..fce0d84 100644 --- a/misc/readyChecker.js +++ b/misc/readyChecker.js @@ -1,12 +1,16 @@ export default class ReadyChecker { - constructor(client) { + constructor(client, dbVersionProvider) { this.client = client; + this.dbVersionProvider = dbVersionProvider; } do = () => { return this.client - .get("/.well-known/live", false) - .then(() => Promise.resolve(true)) + .get("/.well-known/ready", false) + .then(() => { + setTimeout(() => this.dbVersionProvider.refresh()); + return Promise.resolve(true); + }) .catch(() => Promise.resolve(false)); }; } diff --git a/package.json b/package.json index d0a58bc..3cb9ed9 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Javascript client for Weaviate", "main": "lib.js", "scripts": { - "test": "jest --runInBand", + "test": "jest --useStderr --runInBand", "build": "rollup -c rollup.config.js" }, "repository": { diff --git a/utils/beaconPath.js b/utils/beaconPath.js new file mode 100644 index 0000000..3a64688 --- /dev/null +++ b/utils/beaconPath.js @@ -0,0 +1,50 @@ +import { isValidStringProperty } from "../validation/string"; + +const beaconPathPrefix = "weaviate://localhost"; + +export class BeaconPath { + + constructor(dbVersionSupport) { + this.dbVersionSupport = dbVersionSupport; + // matches + // weaviate://localhost/class/id => match[2] = class, match[4] = id + // weaviate://localhost/class/id/ => match[2] = class, match[4] = id + // weaviate://localhost/id => match[2] = id, match[4] = undefined + // weaviate://localhost/id/ => match[2] = id, match[4] = undefined + this.beaconRegExp = /^weaviate:\/\/localhost(\/([^\/]+))?(\/([^\/]+))?[\/]?$/ig; + } + + rebuild(beacon) { + return this.dbVersionSupport.supportsClassNameNamespacedEndpointsPromise().then(support => { + const match = new RegExp(this.beaconRegExp).exec(beacon); + if (!match) { + return beacon; + } + + var className; + var id; + if (match[4] !== undefined) { + id = match[4]; + className = match[2]; + } else { + id = match[2]; + } + + var beaconPath = beaconPathPrefix; + if (support.supports) { + if (isValidStringProperty(className)) { + beaconPath = `${beaconPath}/${className}`; + } else { + support.warns.deprecatedNonClassNameNamespacedEndpointsForBeacons(); + } + } else { + support.warns.notSupportedClassNamespacedEndpointsForBeacons(); + } + if (isValidStringProperty(id)) { + beaconPath = `${beaconPath}/${id}`; + } + + return beaconPath; + }); + } +} diff --git a/utils/dbVersion.js b/utils/dbVersion.js new file mode 100644 index 0000000..df4d26e --- /dev/null +++ b/utils/dbVersion.js @@ -0,0 +1,56 @@ +export class DbVersionSupport { + + constructor(dbVersionProvider) { + this.dbVersionProvider = dbVersionProvider; + } + + supportsClassNameNamespacedEndpointsPromise() { + return this.dbVersionProvider.getVersionPromise().then(version => ({ + version, + supports: this.supportsClassNameNamespacedEndpoints(version), + warns: { + deprecatedNonClassNameNamespacedEndpointsForObjects: () => console.warn(`Usage of objects paths without className is deprecated in Weaviate ${version}. Please provide className parameter`), + deprecatedNonClassNameNamespacedEndpointsForReferences: () => console.warn(`Usage of references paths without className is deprecated in Weaviate ${version}. Please provide className parameter`), + deprecatedNonClassNameNamespacedEndpointsForBeacons: () => console.warn(`Usage of beacons paths without className is deprecated in Weaviate ${version}. Please provide className parameter`), + notSupportedClassNamespacedEndpointsForObjects: () => console.warn(`Usage of objects paths with className is not supported in Weaviate ${version}. className parameter is ignored`), + notSupportedClassNamespacedEndpointsForReferences: () => console.warn(`Usage of references paths with className is not supported in Weaviate ${version}. className parameter is ignored`), + notSupportedClassNamespacedEndpointsForBeacons: () => console.warn(`Usage of beacons paths with className is not supported in Weaviate ${version}. className parameter is ignored`), + notSupportedClassParameterInEndpointsForObjects: () => console.warn(`Usage of objects paths with class query parameter is not supported in Weaviate ${version}. class query parameter is ignored`), + } + })); + } + + // >= 1.14 + supportsClassNameNamespacedEndpoints(version) { + if (typeof version === "string") { + const versionNumbers = version.split("."); + if (versionNumbers.length >= 2) { + const major = parseInt(versionNumbers[0]); + const minor = parseInt(versionNumbers[1]); + return (major == 1 && minor >= 14) || major >= 2; + } + } + return false; + } +} + + +export class DbVersionProvider { + + constructor(versionGetter) { + this.versionGetter = versionGetter; + this.versionPromise = Promise.resolve(""); + } + + getVersionPromise() { + return this.versionPromise; + } + + refresh(force = false) { + this.versionPromise.then(version => { + if (force || version === "") { + this.versionPromise = this.versionGetter(); + } + }); + } +}