From 9aeaf1186de9058a5d5025aea5982f0ae64f595c Mon Sep 17 00:00:00 2001 From: Adil Ansari Date: Fri, 23 Dec 2022 09:42:05 -0800 Subject: [PATCH 01/12] docs: Reflection not enabled error message (#193) --- src/error.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/error.ts b/src/error.ts index 502b540..cc62524 100644 --- a/src/error.ts +++ b/src/error.ts @@ -32,8 +32,8 @@ export class ReflectionNotEnabled extends TigrisError { constructor(object: Object, propertyName: string) { super( `Cannot infer property "type" for ${object.constructor.name}#${propertyName} using Reflection. - Ensure that "emitDecoratorMetadata" option is set to true in "tsconfig.json". Also, make sure - to "import 'reflect-metadata'" on top of the main entry file in application` + Ensure that "experimentalDecorators" and "emitDecoratorMetadata" options are set to true in + "tsconfig.json" and "reflect-metadata" npm package is added to dependencies in "package.json"` ); } From abd96b52edeff18200a9b94e1812365a216c8dfc Mon Sep 17 00:00:00 2001 From: Adil Ansari Date: Tue, 3 Jan 2023 11:50:23 -0800 Subject: [PATCH 02/12] feat: Support for Date types in schema and filter (#194) * feat: Support for Date types in schema and filter * renaming date tests --- src/__tests__/data/decoratedModels/users.ts | 4 +- src/__tests__/tigris.filters.spec.ts | 51 +++++++++++++++++---- src/decorators/tigris-field.ts | 32 +++++++++---- src/error.ts | 3 +- src/utility.ts | 16 ++++--- 5 files changed, 78 insertions(+), 28 deletions(-) diff --git a/src/__tests__/data/decoratedModels/users.ts b/src/__tests__/data/decoratedModels/users.ts index b1f7d81..5494520 100644 --- a/src/__tests__/data/decoratedModels/users.ts +++ b/src/__tests__/data/decoratedModels/users.ts @@ -30,8 +30,8 @@ export class User implements TigrisCollectionType { @PrimaryKey({order: 1}) id: number; - @Field(TigrisDataTypes.DATE_TIME) - created: string; + @Field() + created: Date; @Field({elements: Identity}) identities: Array; diff --git a/src/__tests__/tigris.filters.spec.ts b/src/__tests__/tigris.filters.spec.ts index b5e3a44..2e05a7c 100644 --- a/src/__tests__/tigris.filters.spec.ts +++ b/src/__tests__/tigris.filters.spec.ts @@ -5,8 +5,12 @@ import { SelectorFilter, SelectorFilterOperator, TigrisCollectionType, + TigrisDataTypes } from "../types"; -import {Utility} from "../utility"; +import { Utility } from "../utility"; +import { TigrisCollection } from "../decorators/tigris-collection"; +import { PrimaryKey } from "../decorators/tigris-primary-key"; +import { Field } from "../decorators/tigris-field"; describe("filters tests", () => { it("simpleSelectorFilterTest", () => { @@ -31,6 +35,26 @@ describe("filters tests", () => { }); + it("persists date string as it is", () => { + const dateFilter: SelectorFilter = { + op: SelectorFilterOperator.GT, + fields: { + createdAt: "1980-01-01T18:29:28.000Z" + } + }; + expect(Utility.filterToString(dateFilter)).toBe("{\"createdAt\":{\"$gt\":\"1980-01-01T18:29:28.000Z\"}}"); + }); + + it("serializes Date object to string", () => { + const dateFilter: SelectorFilter = { + op: SelectorFilterOperator.LT, + fields: { + updatedAt: new Date("1980-01-01") + } + }; + expect(Utility.filterToString(dateFilter)).toBe("{\"updatedAt\":{\"$lt\":\"1980-01-01T00:00:00.000Z\"}}"); + }); + it("simplerSelectorWithinLogicalFilterTest", () => { const filter1: LogicalFilter = { op: LogicalOperator.AND, @@ -90,7 +114,7 @@ describe("filters tests", () => { op: SelectorFilterOperator.EQ, fields: { id: BigInt(1), - name: "alice", + name: "alice" } }; expect(Utility.filterToString(tigrisFilter)).toBe("{\"id\":1,\"name\":\"alice\"}"); @@ -220,12 +244,12 @@ describe("filters tests", () => { { op: SelectorFilterOperator.EQ, fields: { - name: "alice", + name: "alice" } }, { address: { - city: "Paris", + city: "Paris" } } ] @@ -238,7 +262,7 @@ describe("filters tests", () => { fields: { address: { zipcode: 1200 - }, + } } }, { @@ -263,7 +287,7 @@ describe("filters tests", () => { { op: SelectorFilterOperator.EQ, fields: { - name: "alice", + name: "alice" } }, { @@ -280,7 +304,7 @@ describe("filters tests", () => { { op: SelectorFilterOperator.EQ, fields: { - name: "emma", + name: "emma" } }, { @@ -305,11 +329,20 @@ export interface IUser extends TigrisCollectionType { balance: number; } -export interface IUser1 extends TigrisCollectionType { - id: BigInt; +@TigrisCollection("user1") +export class IUser1 implements TigrisCollectionType { + @PrimaryKey({ order: 1 }) + id: bigint; + @Field() name: string; + @Field() balance: number; + @Field() isActive: boolean; + @Field(TigrisDataTypes.DATE_TIME) + createdAt: string; + @Field() + updatedAt: Date; } export interface IUser2 extends TigrisCollectionType { diff --git a/src/decorators/tigris-field.ts b/src/decorators/tigris-field.ts index 075af9a..4a254c8 100644 --- a/src/decorators/tigris-field.ts +++ b/src/decorators/tigris-field.ts @@ -106,7 +106,7 @@ export function Field( Reflect && Reflect.getMetadata ? Reflect.getMetadata("design:type", target, propertyName) : undefined; - propertyType = ReflectedTypeToTigrisType.get(reflectedType.name); + propertyType = getTigrisTypeFromReflectedType(reflectedType.name); } catch { throw new ReflectionNotEnabled(target, propertyName); } @@ -144,15 +144,27 @@ export function Field( }; } -const ReflectedTypeToTigrisType: Map = new Map([ - ["String", TigrisDataTypes.STRING], - ["Boolean", TigrisDataTypes.BOOLEAN], - ["Object", TigrisDataTypes.OBJECT], - ["Array", TigrisDataTypes.ARRAY], - ["Set", TigrisDataTypes.ARRAY], - ["Number", TigrisDataTypes.NUMBER], - ["BigInt", TigrisDataTypes.NUMBER_BIGINT], -]); +function getTigrisTypeFromReflectedType(reflectedType: string): TigrisDataTypes | undefined { + switch (reflectedType) { + case "String": + return TigrisDataTypes.STRING; + case "Boolean": + return TigrisDataTypes.BOOLEAN; + case "Object": + return TigrisDataTypes.OBJECT; + case "Array": + case "Set": + return TigrisDataTypes.ARRAY; + case "Number": + return TigrisDataTypes.NUMBER; + case "BigInt": + return TigrisDataTypes.NUMBER_BIGINT; + case "Date": + return TigrisDataTypes.DATE_TIME; + default: + return undefined; + } +} function isEmbeddedOption( options: TigrisFieldOptions | EmbeddedFieldOptions diff --git a/src/error.ts b/src/error.ts index cc62524..9222712 100644 --- a/src/error.ts +++ b/src/error.ts @@ -33,7 +33,8 @@ export class ReflectionNotEnabled extends TigrisError { super( `Cannot infer property "type" for ${object.constructor.name}#${propertyName} using Reflection. Ensure that "experimentalDecorators" and "emitDecoratorMetadata" options are set to true in - "tsconfig.json" and "reflect-metadata" npm package is added to dependencies in "package.json"` + "tsconfig.json" and "reflect-metadata" npm package is added to dependencies in "package.json". + Alternatively, specify the property's "field type" manually.` ); } diff --git a/src/utility.ts b/src/utility.ts index f8c521a..1dfcc3d 100644 --- a/src/utility.ts +++ b/src/utility.ts @@ -223,12 +223,16 @@ export const Utility = { if (!ob.hasOwnProperty(key)) continue; if (typeof ob[key] == "object" && ob[key] !== null) { - const flatObject = Utility._flattenObj(ob[key]); - for (const x in flatObject) { - // eslint-disable-next-line no-prototype-builtins - if (!flatObject.hasOwnProperty(x)) continue; - - toReturn[key + "." + x] = flatObject[x]; + const value = ob[key]; + if (value.constructor.name === "Date") { + toReturn[key] = (value as Date).toJSON(); + } else { + const flatObject = Utility._flattenObj(value); + for (const x in flatObject) { + // eslint-disable-next-line no-prototype-builtins + if (!flatObject.hasOwnProperty(x)) continue; + toReturn[key + "." + x] = flatObject[x]; + } } } else { toReturn[key] = ob[key]; From 38526685c135e7bcf73c30449f4a944b724a56da Mon Sep 17 00:00:00 2001 From: Adil Ansari Date: Tue, 3 Jan 2023 18:57:24 -0800 Subject: [PATCH 03/12] feat: Simpler Search pagination (#195) * renaming abstract cursor to iterable stream * moving page size to request * removing unused tests --- src/__tests__/search/search.types.spec.ts | 7 +- src/__tests__/tigris.rpc.spec.ts | 224 ++++++++++-------- src/__tests__/tigris.utility.spec.ts | 22 +- src/collection.ts | 133 ++++++++--- src/consumables/cursor.ts | 6 +- ...{abstract-cursor.ts => iterable-stream.ts} | 2 +- src/consumables/search-iterator.ts | 42 ++++ src/db.ts | 2 + src/search/types.ts | 36 ++- src/utility.ts | 29 +-- 10 files changed, 315 insertions(+), 188 deletions(-) rename src/consumables/{abstract-cursor.ts => iterable-stream.ts} (98%) create mode 100644 src/consumables/search-iterator.ts diff --git a/src/__tests__/search/search.types.spec.ts b/src/__tests__/search/search.types.spec.ts index 6cc94e9..1d1106b 100644 --- a/src/__tests__/search/search.types.spec.ts +++ b/src/__tests__/search/search.types.spec.ts @@ -85,7 +85,12 @@ describe("SearchResponse parsing", () => { expect(parsed.hits).toHaveLength(0); expect(parsed.facets).toBeDefined(); expect(parsed.facets.size).toBe(0); - expect(parsed.meta).toBeUndefined(); + expect(parsed.meta).toBeDefined(); + expect(parsed.meta.found).toBe(0); + expect(parsed.meta.totalPages).toBe(1); + expect(parsed.meta.page).toBeDefined(); + expect(parsed.meta.page.current).toBe(1); + expect(parsed.meta.page.size).toBe(20); }); it("generates default meta values with empty meta", () => { diff --git a/src/__tests__/tigris.rpc.spec.ts b/src/__tests__/tigris.rpc.spec.ts index 329fb6d..e345f10 100644 --- a/src/__tests__/tigris.rpc.spec.ts +++ b/src/__tests__/tigris.rpc.spec.ts @@ -12,7 +12,13 @@ import { UpdateRequestOptions } from "../types"; import { Tigris } from "../tigris"; -import { Case, Collation, SearchRequest, SearchRequestOptions } from "../search/types"; +import { + Case, + Collation, + SearchRequest, + SearchRequestOptions, + SearchResult +} from "../search/types"; import { Utility } from "../utility"; import { ObservabilityService } from "../proto/server/v1/observability_grpc_pb"; import TestObservabilityService from "./test-observability-service"; @@ -20,6 +26,7 @@ import { capture, spy } from "ts-mockito"; import { TigrisCollection } from "../decorators/tigris-collection"; import { PrimaryKey } from "../decorators/tigris-primary-key"; import { Field } from "../decorators/tigris-field"; +import { SearchIterator } from "../consumables/search-iterator"; describe("rpc tests", () => { let server: Server; @@ -29,7 +36,7 @@ describe("rpc tests", () => { server = new Server(); TestTigrisService.reset(); server.addService(TigrisService, TestService.handler.impl); - server.addService(ObservabilityService, TestObservabilityService.handler.impl) + server.addService(ObservabilityService, TestObservabilityService.handler.impl); server.bindAsync( "localhost:" + SERVER_PORT, // test purpose only @@ -56,13 +63,13 @@ describe("rpc tests", () => { }); it("getDatabase", () => { - const tigris = new Tigris({serverUrl: "localhost:" + SERVER_PORT, projectName: "db1"}); + const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "db1" }); const db1 = tigris.getDatabase(); expect(db1.db).toBe("db1"); }); it("listCollections1", () => { - const tigris = new Tigris({serverUrl: "localhost:" + SERVER_PORT, projectName: "db1"}); + const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "db1" }); const db1 = tigris.getDatabase(); const listCollectionPromise = db1.listCollections(); @@ -78,7 +85,7 @@ describe("rpc tests", () => { }); it("listCollections2", () => { - const tigris = new Tigris({serverUrl: "localhost:" + SERVER_PORT, projectName: "db3"}); + const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "db3" }); const db1 = tigris.getDatabase(); const listCollectionPromise = db1.listCollections(); @@ -94,7 +101,7 @@ describe("rpc tests", () => { }); it("describeDatabase", () => { - const tigris = new Tigris({serverUrl: "localhost:" + SERVER_PORT, projectName: "db3"}); + const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "db3" }); const db1 = tigris.getDatabase(); const databaseDescriptionPromise = db1.describe(); @@ -110,7 +117,7 @@ describe("rpc tests", () => { }); it("dropCollection", () => { - const tigris = new Tigris({serverUrl: "localhost:" + SERVER_PORT, projectName: "db3"}); + const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "db3" }); const db1 = tigris.getDatabase(); const dropCollectionPromise = db1.dropCollection("db3_coll_2"); @@ -122,14 +129,14 @@ describe("rpc tests", () => { }); it("getCollection", () => { - const tigris = new Tigris({serverUrl: "localhost:" + SERVER_PORT, projectName: "db3"}); + const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "db3" }); const db1 = tigris.getDatabase(); const books = db1.getCollection("books"); expect(books.collectionName).toBe("books"); }); it("insert", () => { - const tigris = new Tigris({serverUrl: "localhost:" + SERVER_PORT, projectName: "db3"}); + const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "db3" }); const db1 = tigris.getDatabase(); const insertionPromise = db1.getCollection("books").insertOne({ author: "author name", @@ -144,14 +151,14 @@ describe("rpc tests", () => { }); it("insert2", () => { - const tigris = new Tigris({serverUrl: "localhost:" + SERVER_PORT, projectName: "db3"}); + const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "db3" }); const db1 = tigris.getDatabase(); const insertionPromise = db1.getCollection("books").insertOne({ id: 0, title: "science book", metadata: { publish_date: new Date(), - num_pages: 100, + num_pages: 100 } }); insertionPromise.then(insertedBook => { @@ -161,7 +168,7 @@ describe("rpc tests", () => { }); it("insert_multi_pk", () => { - const tigris = new Tigris({serverUrl: "localhost:" + SERVER_PORT, projectName: "db3"}); + const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "db3" }); const db1 = tigris.getDatabase(); const insertionPromise = db1.getCollection("books-multi-pk").insertOne({ id: 0, @@ -169,7 +176,7 @@ describe("rpc tests", () => { title: "science book", metadata: { publish_date: new Date(), - num_pages: 100, + num_pages: 100 } }); insertionPromise.then(insertedBook => { @@ -180,7 +187,7 @@ describe("rpc tests", () => { }); it("insert_multi_pk_many", () => { - const tigris = new Tigris({serverUrl: "localhost:" + SERVER_PORT, projectName: "db3"}); + const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "db3" }); const db1 = tigris.getDatabase(); const insertionPromise = db1.getCollection("books-multi-pk").insertMany([ { @@ -189,7 +196,7 @@ describe("rpc tests", () => { title: "science book", metadata: { publish_date: new Date(), - num_pages: 100, + num_pages: 100 } }, { @@ -198,7 +205,7 @@ describe("rpc tests", () => { title: "science book", metadata: { publish_date: new Date(), - num_pages: 100, + num_pages: 100 } } ]); @@ -213,7 +220,7 @@ describe("rpc tests", () => { }); it("insertWithOptionalField", () => { - const tigris = new Tigris({serverUrl: "localhost:" + SERVER_PORT, projectName: "db3"}); + const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "db3" }); const db1 = tigris.getDatabase(); const randomNumber: number = Math.floor(Math.random() * 100); // pass the random number in author field. mock server reads author and sets as the @@ -230,7 +237,7 @@ describe("rpc tests", () => { }); it("insertOrReplace", () => { - const tigris = new Tigris({serverUrl: "localhost:" + SERVER_PORT, projectName: "db3"}); + const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "db3" }); const db1 = tigris.getDatabase(); const insertOrReplacePromise = db1.getCollection("books").insertOrReplaceOne({ author: "author name", @@ -245,7 +252,7 @@ describe("rpc tests", () => { }); it("insertOrReplaceWithOptionalField", () => { - const tigris = new Tigris({serverUrl: "localhost:" + SERVER_PORT, projectName: "db3"}); + const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "db3" }); const db1 = tigris.getDatabase(); const randomNumber: number = Math.floor(Math.random() * 100); // pass the random number in author field. mock server reads author and sets as the @@ -262,7 +269,7 @@ describe("rpc tests", () => { }); it("delete", () => { - const tigris = new Tigris({serverUrl: "localhost:" + SERVER_PORT, projectName: "db3"}); + const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "db3" }); const db1 = tigris.getDatabase(); const deletionPromise = db1.getCollection(IBook).deleteMany({ op: SelectorFilterOperator.EQ, @@ -277,12 +284,12 @@ describe("rpc tests", () => { }); it("deleteOne", () => { - const tigris = new Tigris({serverUrl: "localhost:" + SERVER_PORT, projectName: "db3"}); + const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "db3" }); const collection = tigris.getDatabase().getCollection("books"); const spyCollection = spy(collection); - const expectedFilter = {id: 1}; - const expectedCollation: Collation = {case: Case.CaseInsensitive}; + const expectedFilter = { id: 1 }; + const expectedCollation: Collation = { case: Case.CaseInsensitive }; const options = new DeleteRequestOptions(5, expectedCollation); const deletePromise = collection.deleteOne(expectedFilter, undefined, options); @@ -301,7 +308,7 @@ describe("rpc tests", () => { }); it("update", () => { - const tigris = new Tigris({serverUrl: "localhost:" + SERVER_PORT, projectName: "db3"}); + const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "db3" }); const db1 = tigris.getDatabase(); const updatePromise = db1.getCollection("books").updateMany( { @@ -324,13 +331,13 @@ describe("rpc tests", () => { }); it("updateOne", () => { - const tigris = new Tigris({serverUrl: "localhost:" + SERVER_PORT, projectName: "db3"}); + const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "db3" }); const collection = tigris.getDatabase().getCollection("books"); const spyCollection = spy(collection); - const expectedFilter = {id: 1}; - const expectedCollation: Collation = {case: Case.CaseInsensitive}; - const expectedUpdateFields = {title: "one"}; + const expectedFilter = { id: 1 }; + const expectedCollation: Collation = { case: Case.CaseInsensitive }; + const expectedUpdateFields = { title: "one" }; const options = new UpdateRequestOptions(5, expectedCollation); const updatePromise = collection.updateOne(expectedFilter, expectedUpdateFields, undefined, options); @@ -351,9 +358,9 @@ describe("rpc tests", () => { }); it("readOne", () => { - const tigris = new Tigris({serverUrl: "localhost:" + SERVER_PORT, projectName: "db3"}); + const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "db3" }); const db1 = tigris.getDatabase(); - const readOnePromise = db1.getCollection("books").findOne( { + const readOnePromise = db1.getCollection("books").findOne({ op: SelectorFilterOperator.EQ, fields: { id: 1 @@ -370,7 +377,7 @@ describe("rpc tests", () => { }); it("readOneRecordNotFound", () => { - const tigris = new Tigris({serverUrl: "localhost:" + SERVER_PORT, projectName: "db3"}); + const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "db3" }); const db1 = tigris.getDatabase(); const readOnePromise = db1.getCollection("books").findOne({ op: SelectorFilterOperator.EQ, @@ -385,7 +392,7 @@ describe("rpc tests", () => { }); it("readOneWithLogicalFilter", () => { - const tigris = new Tigris({serverUrl: "localhost:" + SERVER_PORT, projectName: "db3"}); + const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "db3" }); const db1 = tigris.getDatabase(); const readOnePromise: Promise = db1.getCollection("books").findOne({ op: LogicalOperator.AND, @@ -415,7 +422,7 @@ describe("rpc tests", () => { }); describe("findMany", () => { - const tigris = new Tigris({serverUrl: "localhost:" + SERVER_PORT, projectName: "db3"}); + const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "db3" }); const db = tigris.getDatabase(); it("with filter using for await on cursor", async () => { @@ -473,78 +480,89 @@ describe("rpc tests", () => { }); }); - it("search", () => { - const tigris = new Tigris({serverUrl: "localhost:" + SERVER_PORT, projectName: "db3"}); - const db3 = tigris.getDatabase(); - const options: SearchRequestOptions = { - page: 2, - perPage: 12 - } - const request: SearchRequest = { - q: "philosophy", - facets: { - tags: Utility.createFacetQueryOptions() - } - }; + describe("search", () => { + const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "db3" }); + const db = tigris.getDatabase(); + + describe("with page number", () => { + const pageNumber = 2; + + it("returns a promise", () => { + const request: SearchRequest = { + q: "philosophy", + facets: { + tags: Utility.createFacetQueryOptions() + } + }; - const searchPromise = db3.getCollection("books").search(request, options); + const maybePromise = db.getCollection("books").search(request, pageNumber); + expect(maybePromise).toBeInstanceOf(Promise); - searchPromise.then(res => { - expect(res.meta.found).toBe(5); - expect(res.meta.totalPages).toBe(5); - expect(res.meta.page.current).toBe(options.page); - expect(res.meta.page.size).toBe(options.perPage); + maybePromise.then((res: SearchResult) => { + expect(res.meta.found).toBe(5); + expect(res.meta.totalPages).toBe(5); + expect(res.meta.page.current).toBe(pageNumber); + }); + return maybePromise; + }); + + it("returns promise using request options", () => { + const options: SearchRequestOptions = { collation: {case: Case.CaseInsensitive} }; + const request: SearchRequest = { q: "philosophy"}; + + const maybePromise = db.getCollection("books").search(request, options, pageNumber); + expect(maybePromise).toBeInstanceOf(Promise); + + maybePromise.then((res: SearchResult) => { + expect(res.meta.page.current).toBe(pageNumber); + }); + + return maybePromise; + }); }); - return searchPromise; - }); + describe("without explicit page number", () => { + it("returns an iterator", async () => { + const request: SearchRequest = { + q: "philosophy", + facets: { + tags: Utility.createFacetQueryOptions() + } + }; + let bookCounter = 0; - it("searchStream using iteration", async () => { - const tigris = new Tigris({serverUrl: "localhost:" + SERVER_PORT, projectName: "db3"}); - const db3 = tigris.getDatabase(); - const request: SearchRequest = { - q: "philosophy", - facets: { - tags: Utility.createFacetQueryOptions() - } - }; - let bookCounter = 0; - - const searchIterator = db3.getCollection("books").searchStream(request); - // for await loop the iterator - for await (const searchResult of searchIterator) { - expect(searchResult.hits).toBeDefined(); - expect(searchResult.facets).toBeDefined(); - bookCounter += searchResult.hits.length; - } - expect(bookCounter).toBe(TestTigrisService.BOOKS_B64_BY_ID.size); - }); + const maybeIterator = db.getCollection("books").search(request); + expect(maybeIterator).toBeInstanceOf(SearchIterator); - it("searchStream using next", async () => { - const tigris = new Tigris({serverUrl: "localhost:" + SERVER_PORT, projectName: "db3"}); - const db3 = tigris.getDatabase(); - const request: SearchRequest = { - q: "philosophy", - facets: { - tags: Utility.createFacetQueryOptions() - } - }; - let bookCounter = 0; - - const searchIterator = db3.getCollection("books").searchStream(request); - let iterableResult = await searchIterator.next(); - while (!iterableResult.done) { - const searchResult = await iterableResult.value; - expect(searchResult.hits).toBeDefined(); - expect(searchResult.facets).toBeDefined(); - bookCounter += searchResult.hits.length; - iterableResult = await searchIterator.next(); - } - expect(bookCounter).toBe(TestTigrisService.BOOKS_B64_BY_ID.size); + // for await loop the iterator + for await (const searchResult of maybeIterator) { + expect(searchResult.hits).toBeDefined(); + expect(searchResult.facets).toBeDefined(); + bookCounter += searchResult.hits.length; + } + expect(bookCounter).toBe(TestTigrisService.BOOKS_B64_BY_ID.size); + }); + + it("returns iterator using request options",async () => { + const request: SearchRequest = { q: "philosophy"}; + const options: SearchRequestOptions = { collation: {case: Case.CaseInsensitive}}; + let bookCounter = 0; + + const maybeIterator = db.getCollection("books").search(request, options); + expect(maybeIterator).toBeInstanceOf(SearchIterator); + + for await (const searchResult of maybeIterator) { + expect(searchResult.hits).toBeDefined(); + bookCounter += searchResult.hits.length; + } + + expect(bookCounter).toBe(TestTigrisService.BOOKS_B64_BY_ID.size); + }); + }); }); it("beginTx", () => { - const tigris = new Tigris({serverUrl: "localhost:" + SERVER_PORT, projectName: "db3"}); + const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "db3" }); const db3 = tigris.getDatabase(); const beginTxPromise = db3.beginTransaction(); beginTxPromise.then(value => { @@ -555,7 +573,7 @@ describe("rpc tests", () => { }); it("commitTx", (done) => { - const tigris = new Tigris({serverUrl: "localhost:" + SERVER_PORT, projectName: "db3"}); + const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "db3" }); const db3 = tigris.getDatabase(); const beginTxPromise = db3.beginTransaction(); beginTxPromise.then(session => { @@ -568,7 +586,7 @@ describe("rpc tests", () => { }); it("rollbackTx", (done) => { - const tigris = new Tigris({serverUrl: "localhost:" + SERVER_PORT, projectName: "db3"}); + const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "db3" }); const db3 = tigris.getDatabase(); const beginTxPromise = db3.beginTransaction(); beginTxPromise.then(session => { @@ -581,7 +599,7 @@ describe("rpc tests", () => { }); it("transact", (done) => { - const tigris = new Tigris({serverUrl: "localhost:" + SERVER_PORT, projectName: "test-tx"}); + const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "test-tx" }); const txDB = tigris.getDatabase(); const books = txDB.getCollection("books"); txDB.transact(tx => { @@ -627,7 +645,7 @@ describe("rpc tests", () => { }); it("createOrUpdateCollections", () => { - const tigris = new Tigris({serverUrl: "localhost:" + SERVER_PORT,projectName: "db3"}); + const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "db3" }); const db3 = tigris.getDatabase(); const bookSchema: TigrisSchema = { id: { @@ -656,7 +674,7 @@ describe("rpc tests", () => { }); it("serverMetadata", () => { - const tigris = new Tigris({serverUrl: "localhost:" + SERVER_PORT, projectName: "db3"}); + const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "db3" }); const serverMetadataPromise = tigris.getServerMetadata(); serverMetadataPromise.then(value => { expect(value.serverVersion).toBe("1.0.0-test-service"); @@ -667,13 +685,13 @@ describe("rpc tests", () => { @TigrisCollection("books") export class IBook implements TigrisCollectionType { - @PrimaryKey({order: 1}) + @PrimaryKey({ order: 1 }) id: number; @Field() title: string; @Field() author: string; - @Field({elements: TigrisDataTypes.STRING}) + @Field({ elements: TigrisDataTypes.STRING }) tags?: string[]; } diff --git a/src/__tests__/tigris.utility.spec.ts b/src/__tests__/tigris.utility.spec.ts index 7c73b0c..d2e1cae 100644 --- a/src/__tests__/tigris.utility.spec.ts +++ b/src/__tests__/tigris.utility.spec.ts @@ -1,4 +1,4 @@ -import {Utility} from "../utility"; +import { Utility } from "../utility"; import { Case, FacetFieldOptions, @@ -28,26 +28,6 @@ describe("utility tests", () => { expect(generatedOptions.type).toBe(FacetQueryFieldType.VALUE); }); - describe("createSearchRequestOptions",() => { - it("generates default with empty options", () => { - const generated: SearchRequestOptions = Utility.createSearchRequestOptions(); - expect(generated.page).toBe(1); - expect(generated.perPage).toBe(20); - }); - - it("fills missing options", () => { - const actual: SearchRequestOptions = { - page: 2, collation: { - case: Case.CaseInsensitive - } - }; - const generated: SearchRequestOptions = Utility.createSearchRequestOptions(actual); - expect(generated.page).toBe(actual.page); - expect(generated.perPage).toBe(20); - expect(generated.collation).toBe(actual.collation); - }); - }); - it("backfills missing facet query options", () => { const generatedOptions = Utility.createFacetQueryOptions({ size: 55 diff --git a/src/collection.ts b/src/collection.ts index c69e071..a5856e8 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -28,6 +28,7 @@ import { Utility } from "./utility"; import { SearchRequest, SearchRequestOptions, SearchResult } from "./search/types"; import { TigrisClientConfig } from "./tigris"; import { Cursor, ReadCursorInitializer } from "./consumables/cursor"; +import { SearchIterator, SearchIteratorInitializer } from "./consumables/search-iterator"; interface ICollection { readonly collectionName: string; @@ -131,28 +132,40 @@ export abstract class ReadOnlyCollection impleme * results using filters with advanced features like faceting and ordering. * * @param request - Search query to execute - * @param options - Optional settings for search + * @returns {@link SearchIterator} - To iterate over pages of {@link SearchResult} + * + * @example + * ``` + * const iterator = db.getCollection(Book).search(request); + * + * for await (const resultPage of iterator) { + * console.log(resultPage.hits); + * console.log(resultPage.facets); + * } + * ``` */ - search(request: SearchRequest, options?: SearchRequestOptions): Promise> { - return new Promise>((resolve, reject) => { - const searchRequest = Utility.createProtoSearchRequest( - this.db, - this.collectionName, - request, - // note: explicit page number is required to signal manual pagination - Utility.createSearchRequestOptions(options) - ); - const stream: grpc.ClientReadableStream = - this.grpcClient.search(searchRequest); + search(request: SearchRequest): SearchIterator; - stream.on("data", (searchResponse: ProtoSearchResponse) => { - const searchResult: SearchResult = SearchResult.from(searchResponse, this.config); - resolve(searchResult); - }); - stream.on("error", (error) => reject(error)); - stream.on("end", () => resolve(SearchResult.empty)); - }); - } + /** + * Search for documents in a collection. Easily perform sophisticated queries and refine + * results using filters with advanced features like faceting and ordering. + * + * @param request - Search query to execute + * @param page - Page number to retrieve. Page number `1` fetches the first page of search results. + * @returns - Single page of results wrapped in a Promise + * + * @example To retrieve page number 5 of matched documents + * ``` + * const resultPromise = db.getCollection(Book).search(request, 5); + * + * resultPromise + * .then((res: SearchResult) => console.log(res.hits)) + * .catch( // catch the error) + * .finally( // finally do something); + * + * ``` + */ + search(request: SearchRequest, page: number): Promise>; /** * Search for documents in a collection. Easily perform sophisticated queries and refine @@ -160,25 +173,85 @@ export abstract class ReadOnlyCollection impleme * * @param request - Search query to execute * @param options - Optional settings for search + * @returns {@link SearchIterator} - To iterate over pages of {@link SearchResult} + * + * @example + * ``` + * const iterator = db.getCollection(Book).search(request, options); + * + * for await (const resultPage of iterator) { + * console.log(resultPage.hits); + * console.log(resultPage.facets); + * } + * ``` */ - async *searchStream( + search(request: SearchRequest, options: SearchRequestOptions): SearchIterator; + + /** + * Search for documents in a collection. Easily perform sophisticated queries and refine + * results using filters with advanced features like faceting and ordering. + * + * @param request - Search query to execute + * @param options - Optional settings for search + * @param page - Page number to retrieve. Page number `1` fetches the first page of search results. + * @returns - Single page of results wrapped in a Promise + * + * @example To retrieve page number 5 of matched documents + * ``` + * const resultPromise = db.getCollection(Book).search(request, options, 5); + * + * resultPromise + * .then((res: SearchResult) => console.log(res.hits)) + * .catch( // catch the error) + * .finally( // finally do something); + * + * ``` + */ + search( request: SearchRequest, - options?: SearchRequestOptions - ): AsyncIterableIterator> { + options: SearchRequestOptions, + page: number + ): Promise>; + + search( + request: SearchRequest, + pageOrOptions?: SearchRequestOptions | number, + page?: number + ): SearchIterator | Promise> { + let options: SearchRequestOptions; + if (typeof pageOrOptions !== "undefined") { + if (typeof pageOrOptions === "number") { + page = pageOrOptions as number; + } else { + options = pageOrOptions as SearchRequestOptions; + } + } + const searchRequest = Utility.createProtoSearchRequest( this.db, this.collectionName, request, - options + options, + page ); - const stream: grpc.ClientReadableStream = - this.grpcClient.search(searchRequest); - for await (const searchResponse of stream) { - const searchResult: SearchResult = SearchResult.from(searchResponse, this.config); - yield searchResult; + // return a iterator if no explicit page number is specified + if (typeof page === "undefined") { + const initializer = new SearchIteratorInitializer(this.grpcClient, searchRequest); + return new SearchIterator(initializer, this.config); + } else { + return new Promise>((resolve, reject) => { + const stream: grpc.ClientReadableStream = + this.grpcClient.search(searchRequest); + + stream.on("data", (searchResponse: ProtoSearchResponse) => { + const searchResult: SearchResult = SearchResult.from(searchResponse, this.config); + resolve(searchResult); + }); + stream.on("error", (error) => reject(error)); + stream.on("end", () => resolve(SearchResult.empty)); + }); } - return; } } diff --git a/src/consumables/cursor.ts b/src/consumables/cursor.ts index b878a66..40de88a 100644 --- a/src/consumables/cursor.ts +++ b/src/consumables/cursor.ts @@ -1,4 +1,4 @@ -import { AbstractCursor, Initializer } from "./abstract-cursor"; +import { IterableStream, Initializer } from "./iterable-stream"; import { ReadRequest, ReadResponse } from "../proto/server/v1/api_pb"; import { TigrisClient } from "../proto/server/v1/api_grpc_pb"; import { Session } from "../session"; @@ -26,7 +26,7 @@ export class ReadCursorInitializer implements Initializer { /** * Cursor to supplement find() queries */ -export class Cursor extends AbstractCursor { +export class Cursor extends IterableStream { /** @internal */ private readonly _config: TigrisClientConfig; @@ -36,7 +36,7 @@ export class Cursor extends AbstractCursor { } /** @override */ - _transform(message: ReadResponse): T { + protected _transform(message: ReadResponse): T { return Utility.jsonStringToObj(Utility._base64Decode(message.getData_asB64()), this._config); } } diff --git a/src/consumables/abstract-cursor.ts b/src/consumables/iterable-stream.ts similarity index 98% rename from src/consumables/abstract-cursor.ts rename to src/consumables/iterable-stream.ts index 61d8731..468510a 100644 --- a/src/consumables/abstract-cursor.ts +++ b/src/consumables/iterable-stream.ts @@ -15,7 +15,7 @@ const tReady = Symbol("ready"); /** @internal */ const tClosed = Symbol("closed"); -export abstract class AbstractCursor { +export abstract class IterableStream { /** @internal */ [tStream]: ClientReadableStream; /** @internal */ diff --git a/src/consumables/search-iterator.ts b/src/consumables/search-iterator.ts new file mode 100644 index 0000000..e29f236 --- /dev/null +++ b/src/consumables/search-iterator.ts @@ -0,0 +1,42 @@ +import { Initializer, IterableStream } from "./iterable-stream"; +import { + SearchRequest as ProtoSearchRequest, + SearchResponse as ProtoSearchResponse, +} from "../proto/server/v1/api_pb"; +import { TigrisClient } from "../proto/server/v1/api_grpc_pb"; +import { ClientReadableStream } from "@grpc/grpc-js"; +import { SearchResult } from "../search/types"; +import { TigrisClientConfig } from "../tigris"; + +/** @internal */ +export class SearchIteratorInitializer implements Initializer { + private readonly _client: TigrisClient; + private readonly _request: ProtoSearchRequest; + + constructor(client: TigrisClient, request: ProtoSearchRequest) { + this._client = client; + this._request = request; + } + + init(): ClientReadableStream { + return this._client.search(this._request); + } +} + +/** + * Iterator to supplement search() queries + */ +export class SearchIterator extends IterableStream, ProtoSearchResponse> { + /** @internal */ + private readonly _config: TigrisClientConfig; + + constructor(initializer: SearchIteratorInitializer, config: TigrisClientConfig) { + super(initializer); + this._config = config; + } + + /** @override */ + protected _transform(message: ProtoSearchResponse): SearchResult { + return SearchResult.from(message, this._config); + } +} diff --git a/src/db.ts b/src/db.ts index 895b2ce..7d053e6 100644 --- a/src/db.ts +++ b/src/db.ts @@ -256,12 +256,14 @@ export class DB { public getCollection( cls: new () => TigrisCollectionType ): Collection; + /** * Gets a {@link Collection} object * * @param name - Collection name */ public getCollection(name: string): Collection; + public getCollection(nameOrClass: T | string): Collection { const collectionName = this.resolveNameFromCollectionClass(nameOrClass); return new Collection(collectionName, this.db, this.grpcClient, this.config); diff --git a/src/search/types.ts b/src/search/types.ts index d37f5b6..ee04c13 100644 --- a/src/search/types.ts +++ b/src/search/types.ts @@ -46,20 +46,16 @@ export type SearchRequest = { * Document fields to exclude when returning search results */ excludeFields?: Array; + /** + * Maximum number of search hits (matched documents) to fetch per page + */ + hitsPerPage?: number; }; /** - * Pagination and Collation options for search request + * Options for search request */ export type SearchRequestOptions = { - /** - * Page number to fetch search results for - */ - page?: number; - /** - * Number of search results to fetch per page - */ - perPage?: number; /** * Allows case-insensitive filtering */ @@ -160,11 +156,11 @@ export class SearchResult { } static get empty(): SearchResult { - return new SearchResult([], new Map(), undefined); + return new SearchResult([], new Map(), SearchMeta.default); } /** - * @returns matched documents as immutable list + * @returns matched documents as a list * @readonly */ get hits(): ReadonlyArray> { @@ -190,7 +186,7 @@ export class SearchResult { static from(resp: ProtoSearchResponse, config: TigrisClientConfig): SearchResult { const _meta = - typeof resp?.getMeta() !== "undefined" ? SearchMeta.from(resp.getMeta()) : undefined; + typeof resp?.getMeta() !== "undefined" ? SearchMeta.from(resp.getMeta()) : SearchMeta.default; const _hits: Array> = resp.getHitsList().map((h) => Hit.from(h, config)); const _facets: Map = new Map( resp @@ -480,6 +476,14 @@ export class SearchMeta { const page = typeof resp?.getPage() !== "undefined" ? Page.from(resp.getPage()) : undefined; return new SearchMeta(found, totalPages, page); } + + /** + * @returns default metadata to construct empty/default response + * @readonly + */ + static get default(): SearchMeta { + return new SearchMeta(0, 1, Page.default); + } } /** @@ -515,4 +519,12 @@ export class Page { const size = resp?.getSize() ?? 0; return new Page(current, size); } + + /** + * @returns the pre-defined page number and size to construct a default response + * @readonly + */ + static get default(): Page { + return new Page(1, 20); + } } diff --git a/src/utility.ts b/src/utility.ts index 1dfcc3d..c91602f 100644 --- a/src/utility.ts +++ b/src/utility.ts @@ -21,7 +21,6 @@ import { } from "./types"; import * as fs from "node:fs"; import { - Case, FacetFieldsQuery, FacetQueryFieldType, FacetQueryOptions, @@ -473,11 +472,6 @@ export const Utility = { return { ...defaults, ...options }; }, - createSearchRequestOptions(options?: Partial): SearchRequestOptions { - const defaults = { page: 1, perPage: 20, collation: { case: Case.CaseInsensitive } }; - return { ...defaults, ...options }; - }, - facetQueryToString(facets: FacetFieldsQuery): string { if (Array.isArray(facets)) { const optionsMap = {}; @@ -506,7 +500,8 @@ export const Utility = { dbName: string, collectionName: string, request: SearchRequest, - options?: SearchRequestOptions + options?: SearchRequestOptions, + page?: number ): ProtoSearchRequest { const searchRequest = new ProtoSearchRequest() .setProject(dbName) @@ -539,16 +534,16 @@ export const Utility = { searchRequest.setExcludeFieldsList(request.excludeFields); } - if (options !== undefined) { - if (options.page !== undefined) { - searchRequest.setPage(options.page); - } - if (options.perPage !== undefined) { - searchRequest.setPageSize(options.perPage); - } - if (options.collation !== undefined) { - searchRequest.setCollation(new ProtoCollation().setCase(options.collation.case)); - } + if (request.hitsPerPage !== undefined) { + searchRequest.setPageSize(request.hitsPerPage); + } + + if (options !== undefined && options.collation !== undefined) { + searchRequest.setCollation(new ProtoCollation().setCase(options.collation.case)); + } + + if (page !== undefined) { + searchRequest.setPage(page); } return searchRequest; From 72cc9a84984269980ad50adc0fa8d37676e14ecd Mon Sep 17 00:00:00 2001 From: Adil Ansari Date: Tue, 3 Jan 2023 19:10:49 -0800 Subject: [PATCH 04/12] chore: prettify unit tests only (#196) chore: prettify unit tests --- .prettierignore | 2 +- src/__tests__/consumables/cursor.spec.ts | 32 +-- .../schema/decorator-processor.spec.ts | 31 +- src/__tests__/search/search.types.spec.ts | 49 ++-- src/__tests__/test-observability-service.ts | 34 ++- src/__tests__/test-service.ts | 157 +++++++--- src/__tests__/tigris.filters.spec.ts | 215 +++++++------- src/__tests__/tigris.jsonserde.spec.ts | 142 ++++----- src/__tests__/tigris.readfields.spec.ts | 14 +- src/__tests__/tigris.rpc.spec.ts | 271 ++++++++++-------- src/__tests__/tigris.schema.spec.ts | 233 +++++++-------- src/__tests__/tigris.updatefields.spec.ts | 15 +- src/__tests__/tigris.utility.spec.ts | 44 +-- 13 files changed, 698 insertions(+), 541 deletions(-) diff --git a/.prettierignore b/.prettierignore index 7bc8d00..029811c 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,6 +1,6 @@ # Ignore artifacts: dist/ -src/__tests__/ +src/__tests__/data/ src/proto/ api/ coverage/ diff --git a/src/__tests__/consumables/cursor.spec.ts b/src/__tests__/consumables/cursor.spec.ts index ef04c14..618dcb1 100644 --- a/src/__tests__/consumables/cursor.spec.ts +++ b/src/__tests__/consumables/cursor.spec.ts @@ -1,12 +1,12 @@ -import {Server, ServerCredentials} from "@grpc/grpc-js"; -import TestService, {TestTigrisService} from "../test-service"; -import {TigrisService} from "../../proto/server/v1/api_grpc_pb"; -import {IBook} from "../tigris.rpc.spec"; -import {Tigris} from "../../tigris"; -import {CursorInUseError} from "../../error"; -import {ObservabilityService} from "../../proto/server/v1/observability_grpc_pb"; +import { Server, ServerCredentials } from "@grpc/grpc-js"; +import TestService, { TestTigrisService } from "../test-service"; +import { TigrisService } from "../../proto/server/v1/api_grpc_pb"; +import { IBook } from "../tigris.rpc.spec"; +import { Tigris } from "../../tigris"; +import { CursorInUseError } from "../../error"; +import { ObservabilityService } from "../../proto/server/v1/observability_grpc_pb"; import TestObservabilityService from "../test-observability-service"; -import {DB} from "../../db"; +import { DB } from "../../db"; describe("class FindCursor", () => { let server: Server; @@ -30,7 +30,7 @@ describe("class FindCursor", () => { } } ); - const tigris = new Tigris({serverUrl: "localhost:" + SERVER_PORT, projectName: "db3"}); + const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "db3" }); db = tigris.getDatabase(); done(); }); @@ -52,7 +52,7 @@ describe("class FindCursor", () => { bookCounter++; } expect(bookCounter).toBeGreaterThan(0); - }) + }); it("Pipes the stream as iterable", async () => { const cursor = db.getCollection("books").findMany(); @@ -62,22 +62,22 @@ describe("class FindCursor", () => { bookCounter++; } expect(bookCounter).toBeGreaterThan(0); - }) + }); it("returns stream as an array", () => { const cursor = db.getCollection("books").findMany(); const booksPromise = cursor.toArray(); - booksPromise.then(books => expect(books.length).toBeGreaterThan(0)); + booksPromise.then((books) => expect(books.length).toBeGreaterThan(0)); return booksPromise; - }) + }); it("does not allow cursor to be re-used", () => { const cursor = db.getCollection("books").findMany(); // cursor is backed by is a generator fn, calling next() would retrieve item from stream cursor[Symbol.asyncIterator]().next(); expect(() => cursor.toArray()).toThrow(CursorInUseError); - }) + }); it("allows cursor to be re-used once reset", async () => { const cursor = db.getCollection("books").findMany(); @@ -88,8 +88,8 @@ describe("class FindCursor", () => { bookCounter++; } - cursor.reset() + cursor.reset(); const books = await cursor.toArray(); expect(books.length).toBe(bookCounter); - }) + }); }); diff --git a/src/__tests__/schema/decorator-processor.spec.ts b/src/__tests__/schema/decorator-processor.spec.ts index 35d21ce..7ada9a0 100644 --- a/src/__tests__/schema/decorator-processor.spec.ts +++ b/src/__tests__/schema/decorator-processor.spec.ts @@ -1,6 +1,6 @@ import { CollectionSchema, - DecoratedSchemaProcessor + DecoratedSchemaProcessor, } from "../../schema/decorated-schema-processor"; import { TigrisCollectionType, TigrisSchema } from "../../types"; import { ExpectedSchema as UserSchema, User } from "../data/./decoratedModels/users"; @@ -19,22 +19,19 @@ import { ExpectedSchema as MatricesSchema, Matrix } from "../data/./decoratedMod describe("Generate TigrisSchema from decorated classes", () => { const processor = DecoratedSchemaProcessor.Instance; - type AnnotationTestCase = { - schemaClass: T, - expected: TigrisSchema - } + type AnnotationTestCase = { + schemaClass: T; + expected: TigrisSchema; + }; const schemaDefinitions: Array> = [ { schemaClass: User, expected: UserSchema }, - { schemaClass: Order, expected: OrderSchema}, - { schemaClass: Movie, expected: MovieSchema}, - { schemaClass: Matrix, expected: MatricesSchema}, - ] - test.each(schemaDefinitions)( - "from %p schema", - (tc) => { - const generated: CollectionSchema = processor.process(tc.schemaClass); - expect(generated.schema).toStrictEqual(tc.expected); - // TODO: validate type compatibility - } - ); + { schemaClass: Order, expected: OrderSchema }, + { schemaClass: Movie, expected: MovieSchema }, + { schemaClass: Matrix, expected: MatricesSchema }, + ]; + test.each(schemaDefinitions)("from %p schema", (tc) => { + const generated: CollectionSchema = processor.process(tc.schemaClass); + expect(generated.schema).toStrictEqual(tc.expected); + // TODO: validate type compatibility + }); }); diff --git a/src/__tests__/search/search.types.spec.ts b/src/__tests__/search/search.types.spec.ts index 1d1106b..6d0ae4e 100644 --- a/src/__tests__/search/search.types.spec.ts +++ b/src/__tests__/search/search.types.spec.ts @@ -8,32 +8,35 @@ import { SearchMetadata as ProtoSearchMetadata, SearchResponse as ProtoSearchResponse, } from "../../proto/server/v1/api_pb"; -import {SearchResult} from "../../search/types"; -import {TestTigrisService} from "../test-service"; -import {IBook} from "../tigris.rpc.spec"; +import { SearchResult } from "../../search/types"; +import { TestTigrisService } from "../test-service"; +import { IBook } from "../tigris.rpc.spec"; import * as google_protobuf_timestamp_pb from "google-protobuf/google/protobuf/timestamp_pb"; describe("SearchResponse parsing", () => { it("generates search hits appropriately", () => { - const expectedTimeInSeconds = Math.floor(Date.now()/1000); - const expectedHits: ProtoSearchHit[] = [...TestTigrisService.BOOKS_B64_BY_ID].map( + const expectedTimeInSeconds = Math.floor(Date.now() / 1000); + const expectedHits: ProtoSearchHit[] = [...TestTigrisService.BOOKS_B64_BY_ID].map( // eslint-disable-next-line @typescript-eslint/no-unused-vars - ([_id, value]) => new ProtoSearchHit() - .setData(value) - .setMetadata(new ProtoSearchHitMeta().setUpdatedAt( - new google_protobuf_timestamp_pb.Timestamp().setSeconds(expectedTimeInSeconds) - )) + ([_id, value]) => + new ProtoSearchHit() + .setData(value) + .setMetadata( + new ProtoSearchHitMeta().setUpdatedAt( + new google_protobuf_timestamp_pb.Timestamp().setSeconds(expectedTimeInSeconds) + ) + ) ); const input: ProtoSearchResponse = new ProtoSearchResponse(); input.setHitsList(expectedHits); const parsed: SearchResult = SearchResult.from(input, { - serverUrl: "test" + serverUrl: "test", }); expect(parsed.hits).toHaveLength(expectedHits.length); - const receivedIds: string[] = parsed.hits.map(h => h.document.id.toString()); + const receivedIds: string[] = parsed.hits.map((h) => h.document.id.toString()); // eslint-disable-next-line @typescript-eslint/no-unused-vars - for (const [id] of TestTigrisService.BOOKS_B64_BY_ID) { + for (const [id] of TestTigrisService.BOOKS_B64_BY_ID) { expect(receivedIds).toContain(id); } for (const hit of parsed.hits) { @@ -45,10 +48,11 @@ describe("SearchResponse parsing", () => { it("generates facets appropriately", () => { const input: ProtoSearchResponse = new ProtoSearchResponse(); - const searchFacet = new ProtoSearchFacet().setCountsList( - [new ProtoFacetCount().setCount(2).setValue("Marcel Proust")]); + const searchFacet = new ProtoSearchFacet().setCountsList([ + new ProtoFacetCount().setCount(2).setValue("Marcel Proust"), + ]); input.getFacetsMap().set("author", searchFacet); - const parsed: SearchResult = SearchResult.from(input, {serverUrl: "test"}); + const parsed: SearchResult = SearchResult.from(input, { serverUrl: "test" }); expect(parsed.facets.size).toBe(1); expect(parsed.facets.get("author")).toBeDefined(); @@ -65,7 +69,7 @@ describe("SearchResponse parsing", () => { const input: ProtoSearchResponse = new ProtoSearchResponse(); const searchFacet = new ProtoSearchFacet().setStats(new ProtoFacetStats().setAvg(4.5)); input.getFacetsMap().set("author", searchFacet); - const parsed: SearchResult = SearchResult.from(input, {serverUrl: "test"}); + const parsed: SearchResult = SearchResult.from(input, { serverUrl: "test" }); const facetDistribution = parsed.facets.get("author"); expect(facetDistribution.stats).toBeDefined(); @@ -78,7 +82,7 @@ describe("SearchResponse parsing", () => { it("generates empty result with empty response", () => { const input: ProtoSearchResponse = new ProtoSearchResponse(); - const parsed: SearchResult = SearchResult.from(input, {serverUrl: "test"}); + const parsed: SearchResult = SearchResult.from(input, { serverUrl: "test" }); expect(parsed).toBeDefined(); expect(parsed.hits).toBeDefined(); @@ -96,7 +100,7 @@ describe("SearchResponse parsing", () => { it("generates default meta values with empty meta", () => { const input: ProtoSearchResponse = new ProtoSearchResponse(); input.setMeta(new ProtoSearchMetadata()); - const parsed: SearchResult = SearchResult.from(input, {serverUrl: "test"}); + const parsed: SearchResult = SearchResult.from(input, { serverUrl: "test" }); expect(parsed.meta).toBeDefined(); expect(parsed.meta.found).toBe(0); @@ -107,22 +111,21 @@ describe("SearchResponse parsing", () => { it("generates no page values with empty page", () => { const input: ProtoSearchResponse = new ProtoSearchResponse(); input.setMeta(new ProtoSearchMetadata().setFound(5)); - const parsed: SearchResult = SearchResult.from(input, {serverUrl: "test"}); + const parsed: SearchResult = SearchResult.from(input, { serverUrl: "test" }); expect(parsed.meta.found).toBe(5); expect(parsed.meta.totalPages).toBe(0); expect(parsed.meta.page).toBeUndefined(); }); - it ("generates meta appropriately with complete response", () => { + it("generates meta appropriately with complete response", () => { const input: ProtoSearchResponse = new ProtoSearchResponse(); const page: ProtoPage = new ProtoPage().setSize(3).setCurrent(2); input.setMeta(new ProtoSearchMetadata().setPage(page).setTotalPages(100)); - const parsed: SearchResult = SearchResult.from(input, {serverUrl: "test"}); + const parsed: SearchResult = SearchResult.from(input, { serverUrl: "test" }); expect(parsed.meta.page.size).toBe(3); expect(parsed.meta.page.current).toBe(2); expect(parsed.meta.totalPages).toBe(100); }); - }); diff --git a/src/__tests__/test-observability-service.ts b/src/__tests__/test-observability-service.ts index 9b2b58f..cb1eb0f 100644 --- a/src/__tests__/test-observability-service.ts +++ b/src/__tests__/test-observability-service.ts @@ -1,23 +1,35 @@ -import {IObservabilityServer, ObservabilityService} from "../proto/server/v1/observability_grpc_pb"; -import {sendUnaryData, ServerUnaryCall} from "@grpc/grpc-js"; +import { + IObservabilityServer, + ObservabilityService, +} from "../proto/server/v1/observability_grpc_pb"; +import { sendUnaryData, ServerUnaryCall } from "@grpc/grpc-js"; import { GetInfoRequest, GetInfoResponse, QueryTimeSeriesMetricsRequest, QueryTimeSeriesMetricsResponse, QuotaLimitsRequest, - QuotaLimitsResponse, QuotaUsageRequest, QuotaUsageResponse + QuotaLimitsResponse, + QuotaUsageRequest, + QuotaUsageResponse, } from "../proto/server/v1/observability_pb"; export class TestTigrisObservabilityService { public impl: IObservabilityServer = { - quotaLimits(call: ServerUnaryCall, callback: sendUnaryData): void { - }, - quotaUsage(call: ServerUnaryCall, callback: sendUnaryData): void { - }, - queryTimeSeriesMetrics(call: ServerUnaryCall, callback: sendUnaryData): void { + quotaLimits( + call: ServerUnaryCall, + callback: sendUnaryData + ): void {}, + quotaUsage( + call: ServerUnaryCall, + callback: sendUnaryData + ): void {}, + queryTimeSeriesMetrics( + call: ServerUnaryCall, + callback: sendUnaryData + ): void { // not implemented - }, + }, /* eslint-disable @typescript-eslint/no-empty-function */ getInfo( // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -28,8 +40,8 @@ export class TestTigrisObservabilityService { const reply: GetInfoResponse = new GetInfoResponse(); reply.setServerVersion("1.0.0-test-service"); callback(undefined, reply); - } - } + }, + }; } export default { diff --git a/src/__tests__/test-service.ts b/src/__tests__/test-service.ts index fe6ea86..dd20ccd 100644 --- a/src/__tests__/test-service.ts +++ b/src/__tests__/test-service.ts @@ -1,6 +1,6 @@ -import {ITigrisServer, TigrisService} from "../proto/server/v1/api_grpc_pb"; -import {sendUnaryData, ServerUnaryCall, ServerWritableStream} from "@grpc/grpc-js"; -import {v4 as uuidv4} from "uuid"; +import { ITigrisServer, TigrisService } from "../proto/server/v1/api_grpc_pb"; +import { sendUnaryData, ServerUnaryCall, ServerWritableStream } from "@grpc/grpc-js"; +import { v4 as uuidv4 } from "uuid"; import { BeginTransactionRequest, BeginTransactionResponse, @@ -50,10 +50,12 @@ import { DescribeDatabaseRequest, DescribeDatabaseResponse, CreateBranchRequest, - CreateBranchResponse, DeleteBranchRequest, DeleteBranchResponse + CreateBranchResponse, + DeleteBranchRequest, + DeleteBranchResponse, } from "../proto/server/v1/api_pb"; import * as google_protobuf_timestamp_pb from "google-protobuf/google/protobuf/timestamp_pb"; -import {Utility} from "../utility"; +import { Utility } from "../utility"; export class TestTigrisService { private static PROJECTS: string[] = []; @@ -62,22 +64,28 @@ export class TestTigrisService { private static txOrigin: string; public static readonly BOOKS_B64_BY_ID: ReadonlyMap = new Map([ // base64 of {"id":1,"title":"A Passage to India","author":"E.M. Forster","tags":["Novel","India"]} - ["1", "eyJpZCI6MSwidGl0bGUiOiJBIFBhc3NhZ2UgdG8gSW5kaWEiLCJhdXRob3IiOiJFLk0uIEZvcnN0ZXIiLCJ0YWdzIjpbIk5vdmVsIiwiSW5kaWEiXX0="], + [ + "1", + "eyJpZCI6MSwidGl0bGUiOiJBIFBhc3NhZ2UgdG8gSW5kaWEiLCJhdXRob3IiOiJFLk0uIEZvcnN0ZXIiLCJ0YWdzIjpbIk5vdmVsIiwiSW5kaWEiXX0=", + ], // base64 of {"id":3,"title":"In Search of Lost Time","author":"Marcel Proust","tags":["Novel","Childhood"]} - ["3", "eyJpZCI6MywidGl0bGUiOiJJbiBTZWFyY2ggb2YgTG9zdCBUaW1lIiwiYXV0aG9yIjoiTWFyY2VsIFByb3VzdCIsInRhZ3MiOlsiTm92ZWwiLCJDaGlsZGhvb2QiXX0="], + [ + "3", + "eyJpZCI6MywidGl0bGUiOiJJbiBTZWFyY2ggb2YgTG9zdCBUaW1lIiwiYXV0aG9yIjoiTWFyY2VsIFByb3VzdCIsInRhZ3MiOlsiTm92ZWwiLCJDaGlsZGhvb2QiXX0=", + ], // base64 of {"id":4,"title":"Swann's Way","author":"Marcel Proust"} ["4", "eyJpZCI6NCwidGl0bGUiOiJTd2FubidzIFdheSIsImF1dGhvciI6Ik1hcmNlbCBQcm91c3QifQ=="], // base64 of {"id":5,"title":"Time Regained","author":"Marcel Proust"} ["5", "eyJpZCI6NSwidGl0bGUiOiJUaW1lIFJlZ2FpbmVkIiwiYXV0aG9yIjoiTWFyY2VsIFByb3VzdCJ9"], // base64 of {"id":6,"title":"The Prisoner","author":"Marcel Proust"} - ["6", "eyJpZCI6NiwidGl0bGUiOiJUaGUgUHJpc29uZXIiLCJhdXRob3IiOiJNYXJjZWwgUHJvdXN0In0="] + ["6", "eyJpZCI6NiwidGl0bGUiOiJUaGUgUHJpc29uZXIiLCJhdXRob3IiOiJNYXJjZWwgUHJvdXN0In0="], ]); public static readonly ALERTS_B64_BY_ID: ReadonlyMap = new Map([ // base64 of {"id":1,"text":"test"} [1, "eyJpZCI6MSwidGV4dCI6InRlc3QifQ=="], // base64 of {"id":2,"text":"test message 25"} - [2, "eyJpZCI6MiwidGV4dCI6InRlc3QgbWVzc2FnZSAyNSJ9"] + [2, "eyJpZCI6MiwidGV4dCI6InRlc3QgbWVzc2FnZSAyNSJ9"], ]); static reset() { @@ -96,7 +104,76 @@ export class TestTigrisService { } public impl: { - deleteBranch(call: ServerUnaryCall, callback: sendUnaryData): void; read(call: ServerWritableStream): void; describeDatabase(call: ServerUnaryCall, callback: sendUnaryData): void; replace(call: ServerUnaryCall, callback: sendUnaryData): void; rollbackTransaction(call: ServerUnaryCall, callback: sendUnaryData): void; insert(call: ServerUnaryCall, callback: sendUnaryData): void; update(call: ServerUnaryCall, callback: sendUnaryData): void; createProject(call: ServerUnaryCall, callback: sendUnaryData): void; listProjects(call: ServerUnaryCall, callback: sendUnaryData): void; delete(call: ServerUnaryCall, callback: sendUnaryData): void; describeCollection(_call: ServerUnaryCall, _callback: sendUnaryData): void; search(call: ServerWritableStream): void; createOrUpdateCollection(call: ServerUnaryCall, callback: sendUnaryData): void; beginTransaction(call: ServerUnaryCall, callback: sendUnaryData): void; commitTransaction(call: ServerUnaryCall, callback: sendUnaryData): void; dropCollection(call: ServerUnaryCall, callback: sendUnaryData): void; deleteProject(call: ServerUnaryCall, callback: sendUnaryData): void; createBranch(call: ServerUnaryCall, callback: sendUnaryData): void; listCollections(call: ServerUnaryCall, callback: sendUnaryData): void + deleteBranch( + call: ServerUnaryCall, + callback: sendUnaryData + ): void; + read(call: ServerWritableStream): void; + describeDatabase( + call: ServerUnaryCall, + callback: sendUnaryData + ): void; + replace( + call: ServerUnaryCall, + callback: sendUnaryData + ): void; + rollbackTransaction( + call: ServerUnaryCall, + callback: sendUnaryData + ): void; + insert( + call: ServerUnaryCall, + callback: sendUnaryData + ): void; + update( + call: ServerUnaryCall, + callback: sendUnaryData + ): void; + createProject( + call: ServerUnaryCall, + callback: sendUnaryData + ): void; + listProjects( + call: ServerUnaryCall, + callback: sendUnaryData + ): void; + delete( + call: ServerUnaryCall, + callback: sendUnaryData + ): void; + describeCollection( + _call: ServerUnaryCall, + _callback: sendUnaryData + ): void; + search(call: ServerWritableStream): void; + createOrUpdateCollection( + call: ServerUnaryCall, + callback: sendUnaryData + ): void; + beginTransaction( + call: ServerUnaryCall, + callback: sendUnaryData + ): void; + commitTransaction( + call: ServerUnaryCall, + callback: sendUnaryData + ): void; + dropCollection( + call: ServerUnaryCall, + callback: sendUnaryData + ): void; + deleteProject( + call: ServerUnaryCall, + callback: sendUnaryData + ): void; + createBranch( + call: ServerUnaryCall, + callback: sendUnaryData + ): void; + listCollections( + call: ServerUnaryCall, + callback: sendUnaryData + ): void; } = { createBranch( call: ServerUnaryCall, @@ -186,8 +263,7 @@ export class TestTigrisService { _call: ServerUnaryCall, // eslint-disable-next-line @typescript-eslint/no-unused-vars _callback: sendUnaryData - ): void { - }, + ): void {}, /* eslint-enable @typescript-eslint/no-empty-function */ describeDatabase( @@ -208,9 +284,7 @@ export class TestTigrisService { .setSchema("schema" + index) ); } - result - .setMetadata(new DatabaseMetadata()) - .setCollectionsList(collectionsDescription); + result.setMetadata(new DatabaseMetadata()).setCollectionsList(collectionsDescription); callback(undefined, result); }, @@ -256,18 +330,20 @@ export class TestTigrisService { const keyList: Array = []; for (let i = 1; i <= call.request.getDocumentsList().length; i++) { if (call.request.getCollection() === "books-with-optional-field") { - const extractedKeyFromAuthor: number = JSON.parse(Utility._base64Decode(call.request.getDocumentsList_asB64()[i - 1]))["author"]; - keyList.push(Utility._base64Encode("{\"id\":" + extractedKeyFromAuthor + "}")); + const extractedKeyFromAuthor: number = JSON.parse( + Utility._base64Decode(call.request.getDocumentsList_asB64()[i - 1]) + )["author"]; + keyList.push(Utility._base64Encode('{"id":' + extractedKeyFromAuthor + "}")); } else if (call.request.getCollection() === "books-multi-pk") { - keyList.push(Utility._base64Encode("{\"id\":" + i + ", \"id2\":" + i+1 + "}")); + keyList.push(Utility._base64Encode('{"id":' + i + ', "id2":' + i + 1 + "}")); } else { - keyList.push(Utility._base64Encode("{\"id\":" + i + "}")); + keyList.push(Utility._base64Encode('{"id":' + i + "}")); } } reply.setKeysList(keyList); reply.setStatus( "inserted: " + - JSON.stringify(new TextDecoder().decode(call.request.getDocumentsList_asU8()[0])) + JSON.stringify(new TextDecoder().decode(call.request.getDocumentsList_asU8()[0])) ); reply.setMetadata( new ResponseMetadata() @@ -304,7 +380,9 @@ export class TestTigrisService { const databaseInfos: ProjectInfo[] = []; for (let index = 0; index < TestTigrisService.PROJECTS.length; index++) { databaseInfos.push( - new ProjectInfo().setProject(TestTigrisService.PROJECTS[index]).setMetadata(new DatabaseMetadata()) + new ProjectInfo() + .setProject(TestTigrisService.PROJECTS[index]) + .setMetadata(new DatabaseMetadata()) ); } @@ -345,9 +423,7 @@ export class TestTigrisService { filter["id"] == 1 ) { // base64 of book id "1" - call.write( - new ReadResponse().setData(TestTigrisService.BOOKS_B64_BY_ID.get("1")) - ); + call.write(new ReadResponse().setData(TestTigrisService.BOOKS_B64_BY_ID.get("1"))); call.end(); } else if ( call.request.getOptions() != undefined && @@ -363,13 +439,11 @@ export class TestTigrisService { ) { // case with logicalFilter passed in // base64 of book id "3" - call.write( - new ReadResponse().setData(TestTigrisService.BOOKS_B64_BY_ID.get("3")) - ); + call.write(new ReadResponse().setData(TestTigrisService.BOOKS_B64_BY_ID.get("3"))); call.end(); } else if (filter["id"] === -1) { // throw an error - call.emit("error", {message: "unknown record requested"}); + call.emit("error", { message: "unknown record requested" }); call.end(); } else { // returns 4 books @@ -403,14 +477,17 @@ export class TestTigrisService { call.write(new SearchResponse().setMeta(searchMeta.setPage(searchPage))); // with facets, meta and page - const searchFacet = new SearchFacet().setCountsList( - [new FacetCount().setCount(2).setValue("Marcel Proust")]); + const searchFacet = new SearchFacet().setCountsList([ + new FacetCount().setCount(2).setValue("Marcel Proust"), + ]); const resp = new SearchResponse().setMeta(searchMeta.setPage(searchPage)); resp.getFacetsMap().set("author", searchFacet); call.write(resp); // with first hit, meta and page - const searchHitMeta = new SearchHitMeta().setUpdatedAt(new google_protobuf_timestamp_pb.Timestamp()); + const searchHitMeta = new SearchHitMeta().setUpdatedAt( + new google_protobuf_timestamp_pb.Timestamp() + ); const searchHit = new SearchHit().setMetadata(searchHitMeta); // write all search hits to stream 1 by 1 @@ -442,16 +519,18 @@ export class TestTigrisService { const keyList: Array = []; for (let i = 1; i <= call.request.getDocumentsList().length; i++) { if (call.request.getCollection() === "books-with-optional-field") { - const extractedKeyFromAuthor: number = JSON.parse(Utility._base64Decode(call.request.getDocumentsList_asB64()[i - 1]))["author"]; - keyList.push(Utility._base64Encode("{\"id\":" + extractedKeyFromAuthor + "}")); + const extractedKeyFromAuthor: number = JSON.parse( + Utility._base64Decode(call.request.getDocumentsList_asB64()[i - 1]) + )["author"]; + keyList.push(Utility._base64Encode('{"id":' + extractedKeyFromAuthor + "}")); } else { - keyList.push(Utility._base64Encode("{\"id\":" + i + "}")); + keyList.push(Utility._base64Encode('{"id":' + i + "}")); } } reply.setKeysList(keyList); reply.setStatus( "insertedOrReplaced: " + - JSON.stringify(new TextDecoder().decode(call.request.getDocumentsList_asU8()[0])) + JSON.stringify(new TextDecoder().decode(call.request.getDocumentsList_asU8()[0])) ); reply.setMetadata( new ResponseMetadata() @@ -484,9 +563,9 @@ export class TestTigrisService { const reply: UpdateResponse = new UpdateResponse(); reply.setStatus( "updated: " + - Utility.uint8ArrayToString(call.request.getFilter_asU8()) + - ", " + - Utility.uint8ArrayToString(call.request.getFields_asU8()) + Utility.uint8ArrayToString(call.request.getFilter_asU8()) + + ", " + + Utility.uint8ArrayToString(call.request.getFields_asU8()) ); reply.setModifiedCount(1); reply.setMetadata( @@ -495,7 +574,7 @@ export class TestTigrisService { .setUpdatedAt(new google_protobuf_timestamp_pb.Timestamp()) ); callback(undefined, reply); - } + }, }; } diff --git a/src/__tests__/tigris.filters.spec.ts b/src/__tests__/tigris.filters.spec.ts index 2e05a7c..452d3c1 100644 --- a/src/__tests__/tigris.filters.spec.ts +++ b/src/__tests__/tigris.filters.spec.ts @@ -5,7 +5,7 @@ import { SelectorFilter, SelectorFilterOperator, TigrisCollectionType, - TigrisDataTypes + TigrisDataTypes, } from "../types"; import { Utility } from "../utility"; import { TigrisCollection } from "../decorators/tigris-collection"; @@ -15,44 +15,47 @@ import { Field } from "../decorators/tigris-field"; describe("filters tests", () => { it("simpleSelectorFilterTest", () => { const filterNothing: SelectorFilter = { - op: SelectorFilterOperator.NONE + op: SelectorFilterOperator.NONE, }; expect(Utility.filterToString(filterNothing)).toBe("{}"); const filter1: Selector = { - name: "Alice" + name: "Alice", }; - expect(Utility.filterToString(filter1)).toBe("{\"name\":\"Alice\"}"); + expect(Utility.filterToString(filter1)).toBe('{"name":"Alice"}'); const filter2: Selector = { - balance: 100 + balance: 100, }; - expect(Utility.filterToString(filter2)).toBe("{\"balance\":100}"); + expect(Utility.filterToString(filter2)).toBe('{"balance":100}'); const filter3: Selector = { - isActive: true + isActive: true, }; - expect(Utility.filterToString(filter3)).toBe("{\"isActive\":true}"); - + expect(Utility.filterToString(filter3)).toBe('{"isActive":true}'); }); it("persists date string as it is", () => { const dateFilter: SelectorFilter = { op: SelectorFilterOperator.GT, fields: { - createdAt: "1980-01-01T18:29:28.000Z" - } + createdAt: "1980-01-01T18:29:28.000Z", + }, }; - expect(Utility.filterToString(dateFilter)).toBe("{\"createdAt\":{\"$gt\":\"1980-01-01T18:29:28.000Z\"}}"); + expect(Utility.filterToString(dateFilter)).toBe( + '{"createdAt":{"$gt":"1980-01-01T18:29:28.000Z"}}' + ); }); it("serializes Date object to string", () => { const dateFilter: SelectorFilter = { op: SelectorFilterOperator.LT, fields: { - updatedAt: new Date("1980-01-01") - } + updatedAt: new Date("1980-01-01"), + }, }; - expect(Utility.filterToString(dateFilter)).toBe("{\"updatedAt\":{\"$lt\":\"1980-01-01T00:00:00.000Z\"}}"); + expect(Utility.filterToString(dateFilter)).toBe( + '{"updatedAt":{"$lt":"1980-01-01T00:00:00.000Z"}}' + ); }); it("simplerSelectorWithinLogicalFilterTest", () => { @@ -60,53 +63,53 @@ describe("filters tests", () => { op: LogicalOperator.AND, selectorFilters: [ { - name: "Alice" + name: "Alice", }, { - balance: 100 - } - ] + balance: 100, + }, + ], }; - expect(Utility.filterToString(filter1)).toBe("{\"$and\":[{\"name\":\"Alice\"},{\"balance\":100}]}"); + expect(Utility.filterToString(filter1)).toBe('{"$and":[{"name":"Alice"},{"balance":100}]}'); const filter2: LogicalFilter = { op: LogicalOperator.OR, selectorFilters: [ { - name: "Alice" + name: "Alice", }, { - name: "Emma" - } - ] + name: "Emma", + }, + ], }; - expect(Utility.filterToString(filter2)).toBe("{\"$or\":[{\"name\":\"Alice\"},{\"name\":\"Emma\"}]}"); + expect(Utility.filterToString(filter2)).toBe('{"$or":[{"name":"Alice"},{"name":"Emma"}]}'); }); it("basicSelectorFilterTest", () => { const filter1: SelectorFilter = { op: SelectorFilterOperator.EQ, fields: { - name: "Alice" - } + name: "Alice", + }, }; - expect(Utility.filterToString(filter1)).toBe("{\"name\":\"Alice\"}"); + expect(Utility.filterToString(filter1)).toBe('{"name":"Alice"}'); const filter2: SelectorFilter = { op: SelectorFilterOperator.EQ, fields: { - id: BigInt(123) - } + id: BigInt(123), + }, }; - expect(Utility.filterToString(filter2)).toBe("{\"id\":123}"); + expect(Utility.filterToString(filter2)).toBe('{"id":123}'); const filter3: SelectorFilter = { op: SelectorFilterOperator.EQ, fields: { - isActive: true - } + isActive: true, + }, }; - expect(Utility.filterToString(filter3)).toBe("{\"isActive\":true}"); + expect(Utility.filterToString(filter3)).toBe('{"isActive":true}'); }); it("selectorFilter_1", () => { @@ -114,10 +117,10 @@ describe("filters tests", () => { op: SelectorFilterOperator.EQ, fields: { id: BigInt(1), - name: "alice" - } + name: "alice", + }, }; - expect(Utility.filterToString(tigrisFilter)).toBe("{\"id\":1,\"name\":\"alice\"}"); + expect(Utility.filterToString(tigrisFilter)).toBe('{"id":1,"name":"alice"}'); }); it("selectorFilter_2", () => { @@ -126,10 +129,10 @@ describe("filters tests", () => { fields: { id: BigInt(1), name: "alice", - balance: 12.34 - } + balance: 12.34, + }, }; - expect(Utility.filterToString(tigrisFilter)).toBe("{\"id\":1,\"name\":\"alice\",\"balance\":12.34}"); + expect(Utility.filterToString(tigrisFilter)).toBe('{"id":1,"name":"alice","balance":12.34}'); }); it("selectorFilter_3", () => { @@ -140,21 +143,23 @@ describe("filters tests", () => { name: "alice", balance: 12.34, address: { - city: "San Francisco" - } - } + city: "San Francisco", + }, + }, }; - expect(Utility.filterToString(tigrisFilter)).toBe("{\"id\":1,\"name\":\"alice\",\"balance\":12.34,\"address.city\":\"San Francisco\"}"); + expect(Utility.filterToString(tigrisFilter)).toBe( + '{"id":1,"name":"alice","balance":12.34,"address.city":"San Francisco"}' + ); }); it("less than Filter", () => { const tigrisFilter: SelectorFilter = { op: SelectorFilterOperator.LT, fields: { - balance: 10 - } + balance: 10, + }, }; - expect(Utility.filterToString(tigrisFilter)).toBe("{\"balance\":{\"$lt\":10}}"); + expect(Utility.filterToString(tigrisFilter)).toBe('{"balance":{"$lt":10}}'); }); it("less than equals Filter", () => { @@ -162,31 +167,31 @@ describe("filters tests", () => { op: SelectorFilterOperator.LTE, fields: { address: { - zipcode: 10 - } - } + zipcode: 10, + }, + }, }; - expect(Utility.filterToString(tigrisFilter)).toBe("{\"address.zipcode\":{\"$lte\":10}}"); + expect(Utility.filterToString(tigrisFilter)).toBe('{"address.zipcode":{"$lte":10}}'); }); it("greater than Filter", () => { const tigrisFilter: SelectorFilter = { op: SelectorFilterOperator.GT, fields: { - balance: 10 - } + balance: 10, + }, }; - expect(Utility.filterToString(tigrisFilter)).toBe("{\"balance\":{\"$gt\":10}}"); + expect(Utility.filterToString(tigrisFilter)).toBe('{"balance":{"$gt":10}}'); }); it("greater than equals Filter", () => { const tigrisFilter: SelectorFilter = { op: SelectorFilterOperator.GTE, fields: { - balance: 10 - } + balance: 10, + }, }; - expect(Utility.filterToString(tigrisFilter)).toBe("{\"balance\":{\"$gte\":10}}"); + expect(Utility.filterToString(tigrisFilter)).toBe('{"balance":{"$gte":10}}'); }); it("logicalFilterTest1", () => { @@ -196,24 +201,26 @@ describe("filters tests", () => { { op: SelectorFilterOperator.EQ, fields: { - name: "alice" - } + name: "alice", + }, }, { op: SelectorFilterOperator.EQ, fields: { - name: "emma" - } + name: "emma", + }, }, { op: SelectorFilterOperator.GT, fields: { - balance: 300 - } - } - ] + balance: 300, + }, + }, + ], }; - expect(Utility.filterToString(logicalFilter)).toBe("{\"$or\":[{\"name\":\"alice\"},{\"name\":\"emma\"},{\"balance\":{\"$gt\":300}}]}"); + expect(Utility.filterToString(logicalFilter)).toBe( + '{"$or":[{"name":"alice"},{"name":"emma"},{"balance":{"$gt":300}}]}' + ); }); it("logicalFilterTest2", () => { @@ -223,18 +230,18 @@ describe("filters tests", () => { { op: SelectorFilterOperator.EQ, fields: { - name: "alice" - } + name: "alice", + }, }, { op: SelectorFilterOperator.EQ, fields: { - rank: 1 - } - } - ] + rank: 1, + }, + }, + ], }; - expect(Utility.filterToString(logicalFilter)).toBe("{\"$and\":[{\"name\":\"alice\"},{\"rank\":1}]}"); + expect(Utility.filterToString(logicalFilter)).toBe('{"$and":[{"name":"alice"},{"rank":1}]}'); }); it("nestedLogicalFilter1", () => { @@ -244,15 +251,15 @@ describe("filters tests", () => { { op: SelectorFilterOperator.EQ, fields: { - name: "alice" - } + name: "alice", + }, }, { address: { - city: "Paris" - } - } - ] + city: "Paris", + }, + }, + ], }; const logicalFilter2: LogicalFilter = { op: LogicalOperator.AND, @@ -261,23 +268,25 @@ describe("filters tests", () => { op: SelectorFilterOperator.GTE, fields: { address: { - zipcode: 1200 - } - } + zipcode: 1200, + }, + }, }, { op: SelectorFilterOperator.LTE, fields: { - balance: 1000 - } - } - ] + balance: 1000, + }, + }, + ], }; const nestedLogicalFilter: LogicalFilter = { op: LogicalOperator.OR, - logicalFilters: [logicalFilter1, logicalFilter2] + logicalFilters: [logicalFilter1, logicalFilter2], }; - expect(Utility.filterToString(nestedLogicalFilter)).toBe("{\"$or\":[{\"$and\":[{\"name\":\"alice\"},{\"address.city\":\"Paris\"}]},{\"$and\":[{\"address.zipcode\":{\"$gte\":1200}},{\"balance\":{\"$lte\":1000}}]}]}"); + expect(Utility.filterToString(nestedLogicalFilter)).toBe( + '{"$or":[{"$and":[{"name":"alice"},{"address.city":"Paris"}]},{"$and":[{"address.zipcode":{"$gte":1200}},{"balance":{"$lte":1000}}]}]}' + ); }); it("nestedLogicalFilter2", () => { @@ -287,16 +296,16 @@ describe("filters tests", () => { { op: SelectorFilterOperator.EQ, fields: { - name: "alice" - } + name: "alice", + }, }, { op: SelectorFilterOperator.EQ, fields: { - rank: 1 - } - } - ] + rank: 1, + }, + }, + ], }; const logicalFilter2: LogicalFilter = { op: LogicalOperator.OR, @@ -304,22 +313,24 @@ describe("filters tests", () => { { op: SelectorFilterOperator.EQ, fields: { - name: "emma" - } + name: "emma", + }, }, { op: SelectorFilterOperator.EQ, fields: { - rank: 1 - } - } - ] + rank: 1, + }, + }, + ], }; const nestedLogicalFilter: LogicalFilter = { op: LogicalOperator.AND, - logicalFilters: [logicalFilter1, logicalFilter2] + logicalFilters: [logicalFilter1, logicalFilter2], }; - expect(Utility.filterToString(nestedLogicalFilter)).toBe("{\"$and\":[{\"$or\":[{\"name\":\"alice\"},{\"rank\":1}]},{\"$or\":[{\"name\":\"emma\"},{\"rank\":1}]}]}"); + expect(Utility.filterToString(nestedLogicalFilter)).toBe( + '{"$and":[{"$or":[{"name":"alice"},{"rank":1}]},{"$or":[{"name":"emma"},{"rank":1}]}]}' + ); }); }); diff --git a/src/__tests__/tigris.jsonserde.spec.ts b/src/__tests__/tigris.jsonserde.spec.ts index cd393d1..7a41cf1 100644 --- a/src/__tests__/tigris.jsonserde.spec.ts +++ b/src/__tests__/tigris.jsonserde.spec.ts @@ -1,25 +1,26 @@ -import {TigrisCollectionType} from "../types"; -import {Utility} from "../utility"; +import { TigrisCollectionType } from "../types"; +import { Utility } from "../utility"; describe("JSON serde tests", () => { - it("jsonSerDe", () => { - interface IUser extends TigrisCollectionType { + interface IUser extends TigrisCollectionType { id: bigint; name: string; balance: number; } - const user: IUser = - { - id: BigInt("9223372036854775807"), - name: "Alice", - balance: 123 - }; + const user: IUser = { + id: BigInt("9223372036854775807"), + name: "Alice", + balance: 123, + }; const userString = Utility.objToJsonString(user); - expect(userString).toBe("{\"id\":9223372036854775807,\"name\":\"Alice\",\"balance\":123}"); + expect(userString).toBe('{"id":9223372036854775807,"name":"Alice","balance":123}'); - const deserializedUser = Utility.jsonStringToObj("{\"id\":9223372036854775807,\"name\":\"Alice\",\"balance\":123}" , {serverUrl: "test"}); + const deserializedUser = Utility.jsonStringToObj( + '{"id":9223372036854775807,"name":"Alice","balance":123}', + { serverUrl: "test" } + ); expect(deserializedUser.id).toBe("9223372036854775807"); expect(deserializedUser.name).toBe("Alice"); expect(deserializedUser.balance).toBe(123); @@ -27,91 +28,104 @@ describe("JSON serde tests", () => { it("jsonSerDeStringAsBigInt", () => { interface TestUser { - id: number, - name: string, - balance: string + id: number; + name: string; + balance: string; } const user: TestUser = { id: 1, name: "Alice", - balance: "9223372036854775807" - } + balance: "9223372036854775807", + }; // default serde - expect(JSON.stringify(user)).toBe("{\"id\":1,\"name\":\"Alice\",\"balance\":\"9223372036854775807\"}"); - const reconstructedUser1: TestUser = JSON.parse("{\"id\":1,\"name\":\"Alice\",\"balance\":\"9223372036854775807\"}"); - expect(reconstructedUser1.id).toBe(1) - expect(reconstructedUser1.name).toBe("Alice") - expect(reconstructedUser1.balance).toBe("9223372036854775807") + expect(JSON.stringify(user)).toBe('{"id":1,"name":"Alice","balance":"9223372036854775807"}'); + const reconstructedUser1: TestUser = JSON.parse( + '{"id":1,"name":"Alice","balance":"9223372036854775807"}' + ); + expect(reconstructedUser1.id).toBe(1); + expect(reconstructedUser1.name).toBe("Alice"); + expect(reconstructedUser1.balance).toBe("9223372036854775807"); // Tigris serde - expect(Utility.objToJsonString(user)).toBe("{\"id\":1,\"name\":\"Alice\",\"balance\":\"9223372036854775807\"}"); - const reconstructedUser2: TestUser = Utility.jsonStringToObj("{\"id\":1,\"name\":\"Alice\",\"balance\":\"9223372036854775807\"}", {serverUrl: "test"}); - expect(reconstructedUser2.id).toBe(1) - expect(reconstructedUser2.name).toBe("Alice") - expect(reconstructedUser2.balance).toBe("9223372036854775807") + expect(Utility.objToJsonString(user)).toBe( + '{"id":1,"name":"Alice","balance":"9223372036854775807"}' + ); + const reconstructedUser2: TestUser = Utility.jsonStringToObj( + '{"id":1,"name":"Alice","balance":"9223372036854775807"}', + { serverUrl: "test" } + ); + expect(reconstructedUser2.id).toBe(1); + expect(reconstructedUser2.name).toBe("Alice"); + expect(reconstructedUser2.balance).toBe("9223372036854775807"); }); it("jsonSerDeNativeBigInt", () => { interface TestUser { - id: number, - name: string, - balance: bigint + id: number; + name: string; + balance: bigint; } const user: TestUser = { id: 1, name: "Alice", - balance: BigInt("9223372036854775807") - } + balance: BigInt("9223372036854775807"), + }; // Tigris serde - expect(Utility.objToJsonString(user)).toBe("{\"id\":1,\"name\":\"Alice\",\"balance\":9223372036854775807}"); - const reconstructedUser2: TestUser = Utility.jsonStringToObj("{\"id\":1,\"name\":\"Alice\",\"balance\":9223372036854775807}", { - serverUrl: "test", - supportBigInt: true - }); - expect(reconstructedUser2.id).toBe(1) - expect(reconstructedUser2.name).toBe("Alice") - expect(reconstructedUser2.balance).toBe(BigInt("9223372036854775807")) + expect(Utility.objToJsonString(user)).toBe( + '{"id":1,"name":"Alice","balance":9223372036854775807}' + ); + const reconstructedUser2: TestUser = Utility.jsonStringToObj( + '{"id":1,"name":"Alice","balance":9223372036854775807}', + { + serverUrl: "test", + supportBigInt: true, + } + ); + expect(reconstructedUser2.id).toBe(1); + expect(reconstructedUser2.name).toBe("Alice"); + expect(reconstructedUser2.balance).toBe(BigInt("9223372036854775807")); }); it("jsonSerDeNativeBigIntNested", () => { interface TestUser { - id: number, - name: string, - balance: bigint - savings: Account - checkin: Account + id: number; + name: string; + balance: bigint; + savings: Account; + checkin: Account; } interface Account { - accountId: bigint + accountId: bigint; } const user: TestUser = { id: 1, name: "Alice", balance: BigInt("9223372036854775807"), checkin: { - accountId: BigInt("9223372036854775806") + accountId: BigInt("9223372036854775806"), }, savings: { - accountId: BigInt("9223372036854775807") - } - } + accountId: BigInt("9223372036854775807"), + }, + }; // Tigris serde - expect(Utility.objToJsonString(user)).toBe("{\"id\":1,\"name\":\"Alice\",\"balance\":9223372036854775807,\"checkin\":{\"accountId\":9223372036854775806},\"savings\":{\"accountId\":9223372036854775807}}" - ); - const reconstructedUser2: TestUser = Utility.jsonStringToObj("{\"id\":1,\"name\":\"Alice\",\"balance\":9223372036854775807,\"checkin\":{\"accountId\":9223372036854775806},\"savings\":{\"accountId\":9223372036854775807}}" - , { - serverUrl: "test", - supportBigInt: true - }); - expect(reconstructedUser2.id).toBe(1) - expect(reconstructedUser2.name).toBe("Alice") - expect(reconstructedUser2.balance).toBe(BigInt("9223372036854775807")) - expect(reconstructedUser2.checkin.accountId).toBe(BigInt("9223372036854775806")) - expect(reconstructedUser2.savings.accountId).toBe(BigInt("9223372036854775807")) + expect(Utility.objToJsonString(user)).toBe( + '{"id":1,"name":"Alice","balance":9223372036854775807,"checkin":{"accountId":9223372036854775806},"savings":{"accountId":9223372036854775807}}' + ); + const reconstructedUser2: TestUser = Utility.jsonStringToObj( + '{"id":1,"name":"Alice","balance":9223372036854775807,"checkin":{"accountId":9223372036854775806},"savings":{"accountId":9223372036854775807}}', + { + serverUrl: "test", + supportBigInt: true, + } + ); + expect(reconstructedUser2.id).toBe(1); + expect(reconstructedUser2.name).toBe("Alice"); + expect(reconstructedUser2.balance).toBe(BigInt("9223372036854775807")); + expect(reconstructedUser2.checkin.accountId).toBe(BigInt("9223372036854775806")); + expect(reconstructedUser2.savings.accountId).toBe(BigInt("9223372036854775807")); }); }); - - diff --git a/src/__tests__/tigris.readfields.spec.ts b/src/__tests__/tigris.readfields.spec.ts index 9628e78..3e55962 100644 --- a/src/__tests__/tigris.readfields.spec.ts +++ b/src/__tests__/tigris.readfields.spec.ts @@ -1,26 +1,24 @@ -import { - ReadFields, -} from "../types"; -import {Utility} from "../utility"; +import { ReadFields } from "../types"; +import { Utility } from "../utility"; describe("readFields tests", () => { it("readFields1", () => { const readFields: ReadFields = { include: ["id", "title"], }; - expect(Utility.readFieldString(readFields)).toBe("{\"id\":true,\"title\":true}"); + expect(Utility.readFieldString(readFields)).toBe('{"id":true,"title":true}'); }); it("readFields2", () => { const readFields: ReadFields = { exclude: ["id", "title"], }; - expect(Utility.readFieldString(readFields)).toBe("{\"id\":false,\"title\":false}"); + expect(Utility.readFieldString(readFields)).toBe('{"id":false,"title":false}'); }); it("readFields3", () => { const readFields: ReadFields = { include: ["id", "title"], - exclude: ["author"] + exclude: ["author"], }; - expect(Utility.readFieldString(readFields)).toBe("{\"id\":true,\"title\":true,\"author\":false}"); + expect(Utility.readFieldString(readFields)).toBe('{"id":true,"title":true,"author":false}'); }); }); diff --git a/src/__tests__/tigris.rpc.spec.ts b/src/__tests__/tigris.rpc.spec.ts index e345f10..d98c7aa 100644 --- a/src/__tests__/tigris.rpc.spec.ts +++ b/src/__tests__/tigris.rpc.spec.ts @@ -9,7 +9,7 @@ import { TigrisDataTypes, TigrisSchema, UpdateFieldsOperator, - UpdateRequestOptions + UpdateRequestOptions, } from "../types"; import { Tigris } from "../tigris"; import { @@ -17,7 +17,7 @@ import { Collation, SearchRequest, SearchRequestOptions, - SearchResult + SearchResult, } from "../search/types"; import { Utility } from "../utility"; import { ObservabilityService } from "../proto/server/v1/observability_grpc_pb"; @@ -50,7 +50,6 @@ describe("rpc tests", () => { } ); done(); - }); beforeEach(() => { @@ -73,7 +72,7 @@ describe("rpc tests", () => { const db1 = tigris.getDatabase(); const listCollectionPromise = db1.listCollections(); - listCollectionPromise.then(value => { + listCollectionPromise.then((value) => { expect(value.length).toBe(5); expect(value[0].name).toBe("db1_coll_1"); expect(value[1].name).toBe("db1_coll_2"); @@ -89,7 +88,7 @@ describe("rpc tests", () => { const db1 = tigris.getDatabase(); const listCollectionPromise = db1.listCollections(); - listCollectionPromise.then(value => { + listCollectionPromise.then((value) => { expect(value.length).toBe(5); expect(value[0].name).toBe("db3_coll_1"); expect(value[1].name).toBe("db3_coll_2"); @@ -105,7 +104,7 @@ describe("rpc tests", () => { const db1 = tigris.getDatabase(); const databaseDescriptionPromise = db1.describe(); - databaseDescriptionPromise.then(value => { + databaseDescriptionPromise.then((value) => { expect(value.collectionsDescription.length).toBe(5); expect(value.collectionsDescription[0].collection).toBe("db3_coll_1"); expect(value.collectionsDescription[1].collection).toBe("db3_coll_2"); @@ -121,7 +120,7 @@ describe("rpc tests", () => { const db1 = tigris.getDatabase(); const dropCollectionPromise = db1.dropCollection("db3_coll_2"); - dropCollectionPromise.then(value => { + dropCollectionPromise.then((value) => { expect(value.status).toBe("dropped"); expect(value.message).toBe("db3_coll_2 dropped successfully"); }); @@ -142,9 +141,9 @@ describe("rpc tests", () => { author: "author name", id: 0, tags: ["science"], - title: "science book" + title: "science book", }); - insertionPromise.then(insertedBook => { + insertionPromise.then((insertedBook) => { expect(insertedBook.id).toBe(1); }); return insertionPromise; @@ -158,10 +157,10 @@ describe("rpc tests", () => { title: "science book", metadata: { publish_date: new Date(), - num_pages: 100 - } + num_pages: 100, + }, }); - insertionPromise.then(insertedBook => { + insertionPromise.then((insertedBook) => { expect(insertedBook.id).toBe(1); }); return insertionPromise; @@ -176,10 +175,10 @@ describe("rpc tests", () => { title: "science book", metadata: { publish_date: new Date(), - num_pages: 100 - } + num_pages: 100, + }, }); - insertionPromise.then(insertedBook => { + insertionPromise.then((insertedBook) => { expect(insertedBook.id).toBe(1); expect(insertedBook.id2).toBe(11); }); @@ -196,8 +195,8 @@ describe("rpc tests", () => { title: "science book", metadata: { publish_date: new Date(), - num_pages: 100 - } + num_pages: 100, + }, }, { id: 0, @@ -205,11 +204,11 @@ describe("rpc tests", () => { title: "science book", metadata: { publish_date: new Date(), - num_pages: 100 - } - } + num_pages: 100, + }, + }, ]); - insertionPromise.then(insertedBook => { + insertionPromise.then((insertedBook) => { expect(insertedBook.length).toBe(2); expect(insertedBook[0].id).toBe(1); expect(insertedBook[0].id2).toBe(11); @@ -228,9 +227,9 @@ describe("rpc tests", () => { const insertionPromise = db1.getCollection("books-with-optional-field").insertOne({ author: "" + randomNumber, tags: ["science"], - title: "science book" + title: "science book", }); - insertionPromise.then(insertedBook => { + insertionPromise.then((insertedBook) => { expect(insertedBook.id).toBe(randomNumber); }); return insertionPromise; @@ -243,9 +242,9 @@ describe("rpc tests", () => { author: "author name", id: 0, tags: ["science"], - title: "science book" + title: "science book", }); - insertOrReplacePromise.then(insertedOrReplacedBook => { + insertOrReplacePromise.then((insertedOrReplacedBook) => { expect(insertedOrReplacedBook.id).toBe(1); }); return insertOrReplacePromise; @@ -257,12 +256,14 @@ describe("rpc tests", () => { const randomNumber: number = Math.floor(Math.random() * 100); // pass the random number in author field. mock server reads author and sets as the // primaryKey field. - const insertOrReplacePromise = db1.getCollection("books-with-optional-field").insertOrReplaceOne({ - author: "" + randomNumber, - tags: ["science"], - title: "science book" - }); - insertOrReplacePromise.then(insertedOrReplacedBook => { + const insertOrReplacePromise = db1 + .getCollection("books-with-optional-field") + .insertOrReplaceOne({ + author: "" + randomNumber, + tags: ["science"], + title: "science book", + }); + insertOrReplacePromise.then((insertedOrReplacedBook) => { expect(insertedOrReplacedBook.id).toBe(randomNumber); }); return insertOrReplacePromise; @@ -274,11 +275,11 @@ describe("rpc tests", () => { const deletionPromise = db1.getCollection(IBook).deleteMany({ op: SelectorFilterOperator.EQ, fields: { - id: 1 - } + id: 1, + }, }); - deletionPromise.then(value => { - expect(value.status).toBe("deleted: {\"id\":1}"); + deletionPromise.then((value) => { + expect(value.status).toBe('deleted: {"id":1}'); }); return deletionPromise; }); @@ -314,17 +315,18 @@ describe("rpc tests", () => { { op: SelectorFilterOperator.EQ, fields: { - id: 1 - } + id: 1, + }, }, { op: UpdateFieldsOperator.SET, fields: { - title: "New Title" - } - }); - updatePromise.then(value => { - expect(value.status).toBe("updated: {\"id\":1}, {\"$set\":{\"title\":\"New Title\"}}"); + title: "New Title", + }, + } + ); + updatePromise.then((value) => { + expect(value.status).toBe('updated: {"id":1}, {"$set":{"title":"New Title"}}'); expect(value.modifiedCount).toBe(1); }); return updatePromise; @@ -340,8 +342,15 @@ describe("rpc tests", () => { const expectedUpdateFields = { title: "one" }; const options = new UpdateRequestOptions(5, expectedCollation); - const updatePromise = collection.updateOne(expectedFilter, expectedUpdateFields, undefined, options); - const [capturedFilter, capturedFields, capturedTx, capturedOptions] = capture(spyCollection.updateMany).last(); + const updatePromise = collection.updateOne( + expectedFilter, + expectedUpdateFields, + undefined, + options + ); + const [capturedFilter, capturedFields, capturedTx, capturedOptions] = capture( + spyCollection.updateMany + ).last(); // filter passed as it is expect(capturedFilter).toBe(expectedFilter); @@ -363,10 +372,10 @@ describe("rpc tests", () => { const readOnePromise = db1.getCollection("books").findOne({ op: SelectorFilterOperator.EQ, fields: { - id: 1 - } + id: 1, + }, }); - readOnePromise.then(value => { + readOnePromise.then((value) => { const book: IBook = value; expect(book.id).toBe(1); expect(book.title).toBe("A Passage to India"); @@ -382,8 +391,8 @@ describe("rpc tests", () => { const readOnePromise = db1.getCollection("books").findOne({ op: SelectorFilterOperator.EQ, fields: { - id: 2 - } + id: 2, + }, }); readOnePromise.then((value) => { expect(value).toBe(undefined); @@ -400,18 +409,18 @@ describe("rpc tests", () => { { op: SelectorFilterOperator.EQ, fields: { - id: 3 - } + id: 3, + }, }, { op: SelectorFilterOperator.EQ, fields: { - title: "In Search of Lost Time" - } - } - ] + title: "In Search of Lost Time", + }, + }, + ], }); - readOnePromise.then(value => { + readOnePromise.then((value) => { const book: IBook = value; expect(book.id).toBe(3); expect(book.title).toBe("In Search of Lost Time"); @@ -429,8 +438,8 @@ describe("rpc tests", () => { const cursor = db.getCollection("books").findMany({ op: SelectorFilterOperator.EQ, fields: { - author: "Marcel Proust" - } + author: "Marcel Proust", + }, }); let bookCounter = 0; @@ -445,7 +454,7 @@ describe("rpc tests", () => { const cursor = db.getCollection("books").findMany(); const booksPromise = cursor.toArray(); - booksPromise.then(books => expect(books.length).toBe(4)); + booksPromise.then((books) => expect(books.length).toBe(4)); return booksPromise; }); @@ -465,8 +474,8 @@ describe("rpc tests", () => { const cursor = db.getCollection("books").findMany({ op: SelectorFilterOperator.EQ, fields: { - id: -1 - } + id: -1, + }, }); try { @@ -491,8 +500,8 @@ describe("rpc tests", () => { const request: SearchRequest = { q: "philosophy", facets: { - tags: Utility.createFacetQueryOptions() - } + tags: Utility.createFacetQueryOptions(), + }, }; const maybePromise = db.getCollection("books").search(request, pageNumber); @@ -507,8 +516,8 @@ describe("rpc tests", () => { }); it("returns promise using request options", () => { - const options: SearchRequestOptions = { collation: {case: Case.CaseInsensitive} }; - const request: SearchRequest = { q: "philosophy"}; + const options: SearchRequestOptions = { collation: { case: Case.CaseInsensitive } }; + const request: SearchRequest = { q: "philosophy" }; const maybePromise = db.getCollection("books").search(request, options, pageNumber); expect(maybePromise).toBeInstanceOf(Promise); @@ -526,8 +535,8 @@ describe("rpc tests", () => { const request: SearchRequest = { q: "philosophy", facets: { - tags: Utility.createFacetQueryOptions() - } + tags: Utility.createFacetQueryOptions(), + }, }; let bookCounter = 0; @@ -543,9 +552,9 @@ describe("rpc tests", () => { expect(bookCounter).toBe(TestTigrisService.BOOKS_B64_BY_ID.size); }); - it("returns iterator using request options",async () => { - const request: SearchRequest = { q: "philosophy"}; - const options: SearchRequestOptions = { collation: {case: Case.CaseInsensitive}}; + it("returns iterator using request options", async () => { + const request: SearchRequest = { q: "philosophy" }; + const options: SearchRequestOptions = { collation: { case: Case.CaseInsensitive } }; let bookCounter = 0; const maybeIterator = db.getCollection("books").search(request, options); @@ -565,7 +574,7 @@ describe("rpc tests", () => { const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "db3" }); const db3 = tigris.getDatabase(); const beginTxPromise = db3.beginTransaction(); - beginTxPromise.then(value => { + beginTxPromise.then((value) => { expect(value.id).toBe("id-test"); expect(value.origin).toBe("origin-test"); }); @@ -576,9 +585,9 @@ describe("rpc tests", () => { const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "db3" }); const db3 = tigris.getDatabase(); const beginTxPromise = db3.beginTransaction(); - beginTxPromise.then(session => { + beginTxPromise.then((session) => { const commitTxResponse = session.commit(); - commitTxResponse.then(value => { + commitTxResponse.then((value) => { expect(value.status).toBe("committed-test"); done(); }); @@ -589,9 +598,9 @@ describe("rpc tests", () => { const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "db3" }); const db3 = tigris.getDatabase(); const beginTxPromise = db3.beginTransaction(); - beginTxPromise.then(session => { + beginTxPromise.then((session) => { const rollbackTransactionResponsePromise = session.rollback(); - rollbackTransactionResponsePromise.then(value => { + rollbackTransactionResponsePromise.then((value) => { expect(value.status).toBe("rollback-test"); done(); }); @@ -602,45 +611,62 @@ describe("rpc tests", () => { const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "test-tx" }); const txDB = tigris.getDatabase(); const books = txDB.getCollection("books"); - txDB.transact(tx => { - books.insertOne( - { - id: 1, - author: "Alice", - title: "Some book title" - }, - tx - // eslint-disable-next-line @typescript-eslint/no-unused-vars - ).then(_value => { - books.findOne({ - op: SelectorFilterOperator.EQ, - fields: { - id: 1 - } - }, undefined, tx).then(() => { - books.updateMany({ - op: SelectorFilterOperator.EQ, - fields: { - id: 1 - } - }, - { - op: UpdateFieldsOperator.SET, - fields: { - "author": - "Dr. Author" - } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - }, tx).then(() => { - books.deleteMany({ - op: SelectorFilterOperator.EQ, - fields: { - id: 1 - } - }, tx).then(() => done()); - }); + txDB.transact((tx) => { + books + .insertOne( + { + id: 1, + author: "Alice", + title: "Some book title", + }, + tx + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ) + .then((_value) => { + books + .findOne( + { + op: SelectorFilterOperator.EQ, + fields: { + id: 1, + }, + }, + undefined, + tx + ) + .then(() => { + books + .updateMany( + { + op: SelectorFilterOperator.EQ, + fields: { + id: 1, + }, + }, + { + op: UpdateFieldsOperator.SET, + fields: { + author: "Dr. Author", + }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + }, + tx + ) + .then(() => { + books + .deleteMany( + { + op: SelectorFilterOperator.EQ, + fields: { + id: 1, + }, + }, + tx + ) + .then(() => done()); + }); + }); }); - }); }); }); @@ -652,23 +678,23 @@ describe("rpc tests", () => { type: TigrisDataTypes.INT64, primary_key: { order: 1, - autoGenerate: true - } + autoGenerate: true, + }, }, author: { - type: TigrisDataTypes.STRING + type: TigrisDataTypes.STRING, }, title: { - type: TigrisDataTypes.STRING + type: TigrisDataTypes.STRING, }, tags: { type: TigrisDataTypes.ARRAY, items: { - type: TigrisDataTypes.STRING - } - } + type: TigrisDataTypes.STRING, + }, + }, }; - return db3.createOrUpdateCollection("books", bookSchema).then(value => { + return db3.createOrUpdateCollection("books", bookSchema).then((value) => { expect(value).toBeDefined(); }); }); @@ -676,7 +702,7 @@ describe("rpc tests", () => { it("serverMetadata", () => { const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "db3" }); const serverMetadataPromise = tigris.getServerMetadata(); - serverMetadataPromise.then(value => { + serverMetadataPromise.then((value) => { expect(value.serverVersion).toBe("1.0.0-test-service"); }); return serverMetadataPromise; @@ -695,7 +721,6 @@ export class IBook implements TigrisCollectionType { tags?: string[]; } - export interface IBook1 extends TigrisCollectionType { id?: number; title: string; diff --git a/src/__tests__/tigris.schema.spec.ts b/src/__tests__/tigris.schema.spec.ts index ab77c83..d541ac3 100644 --- a/src/__tests__/tigris.schema.spec.ts +++ b/src/__tests__/tigris.schema.spec.ts @@ -1,41 +1,41 @@ -import { TigrisCollectionType, TigrisDataTypes, TigrisSchema,} from "../types"; -import {Utility} from "../utility"; +import { TigrisCollectionType, TigrisDataTypes, TigrisSchema } from "../types"; +import { Utility } from "../utility"; describe("schema tests", () => { - it("basicCollection", () => { const schema: TigrisSchema = { id: { type: TigrisDataTypes.INT32, primary_key: { order: 1, - autoGenerate: true - } + autoGenerate: true, + }, }, active: { - type: TigrisDataTypes.BOOLEAN + type: TigrisDataTypes.BOOLEAN, }, name: { - type: TigrisDataTypes.STRING + type: TigrisDataTypes.STRING, }, uuid: { - type: TigrisDataTypes.UUID + type: TigrisDataTypes.UUID, }, int32Number: { - type: TigrisDataTypes.INT32 + type: TigrisDataTypes.INT32, }, int64Number: { - type: TigrisDataTypes.INT64 + type: TigrisDataTypes.INT64, }, date: { - type: TigrisDataTypes.DATE_TIME + type: TigrisDataTypes.DATE_TIME, }, bytes: { - type: TigrisDataTypes.BYTE_STRING - } + type: TigrisDataTypes.BYTE_STRING, + }, }; - expect(Utility._toJSONSchema("basicCollection", schema)) - .toBe(Utility._readTestDataFile("basicCollection.json")); + expect(Utility._toJSONSchema("basicCollection", schema)).toBe( + Utility._readTestDataFile("basicCollection.json") + ); }); it("basicCollectionWithObjectType", () => { @@ -44,18 +44,19 @@ describe("schema tests", () => { type: TigrisDataTypes.INT64, primary_key: { order: 1, - autoGenerate: true - } + autoGenerate: true, + }, }, name: { - type: TigrisDataTypes.STRING + type: TigrisDataTypes.STRING, }, metadata: { - type: TigrisDataTypes.OBJECT - } + type: TigrisDataTypes.OBJECT, + }, }; - expect(Utility._toJSONSchema("basicCollectionWithObjectType", schema)) - .toBe(Utility._readTestDataFile("basicCollectionWithObjectType.json")); + expect(Utility._toJSONSchema("basicCollectionWithObjectType", schema)).toBe( + Utility._readTestDataFile("basicCollectionWithObjectType.json") + ); }); it("multiplePKeys", () => { @@ -64,112 +65,116 @@ describe("schema tests", () => { type: TigrisDataTypes.INT64, primary_key: { order: 2, // intentionally the order is skewed to test - } + }, }, active: { - type: TigrisDataTypes.BOOLEAN + type: TigrisDataTypes.BOOLEAN, }, name: { - type: TigrisDataTypes.STRING + type: TigrisDataTypes.STRING, }, uuid: { type: TigrisDataTypes.UUID, primary_key: { order: 1, - autoGenerate: true - } + autoGenerate: true, + }, }, int32Number: { - type: TigrisDataTypes.INT32 + type: TigrisDataTypes.INT32, }, int64Number: { - type: TigrisDataTypes.INT64 + type: TigrisDataTypes.INT64, }, date: { - type: TigrisDataTypes.DATE_TIME + type: TigrisDataTypes.DATE_TIME, }, bytes: { - type: TigrisDataTypes.BYTE_STRING - } + type: TigrisDataTypes.BYTE_STRING, + }, }; - expect(Utility._toJSONSchema("multiplePKeys", schema)) - .toBe(Utility._readTestDataFile("multiplePKeys.json")); + expect(Utility._toJSONSchema("multiplePKeys", schema)).toBe( + Utility._readTestDataFile("multiplePKeys.json") + ); }); it("nestedCollection", () => { const addressSchema: TigrisSchema
= { city: { - type: TigrisDataTypes.STRING + type: TigrisDataTypes.STRING, }, state: { - type: TigrisDataTypes.STRING + type: TigrisDataTypes.STRING, }, zipcode: { - type: TigrisDataTypes.NUMBER - } + type: TigrisDataTypes.NUMBER, + }, }; const schema: TigrisSchema = { id: { - type: TigrisDataTypes.NUMBER + type: TigrisDataTypes.NUMBER, }, name: { - type: TigrisDataTypes.STRING + type: TigrisDataTypes.STRING, }, address: { - type: addressSchema - } + type: addressSchema, + }, }; - expect(Utility._toJSONSchema("nestedCollection", schema)) - .toBe(Utility._readTestDataFile("nestedCollection.json")); + expect(Utility._toJSONSchema("nestedCollection", schema)).toBe( + Utility._readTestDataFile("nestedCollection.json") + ); }); it("collectionWithPrimitiveArrays", () => { const schema: TigrisSchema = { id: { - type: TigrisDataTypes.NUMBER + type: TigrisDataTypes.NUMBER, }, name: { - type: TigrisDataTypes.STRING + type: TigrisDataTypes.STRING, }, tags: { type: TigrisDataTypes.ARRAY, items: { - type: TigrisDataTypes.STRING - } - } + type: TigrisDataTypes.STRING, + }, + }, }; - expect(Utility._toJSONSchema("collectionWithPrimitiveArrays", schema)) - .toBe(Utility._readTestDataFile("collectionWithPrimitiveArrays.json")); + expect(Utility._toJSONSchema("collectionWithPrimitiveArrays", schema)).toBe( + Utility._readTestDataFile("collectionWithPrimitiveArrays.json") + ); }); it("collectionWithObjectArrays", () => { const addressSchema: TigrisSchema
= { city: { - type: TigrisDataTypes.STRING + type: TigrisDataTypes.STRING, }, state: { - type: TigrisDataTypes.STRING + type: TigrisDataTypes.STRING, }, zipcode: { - type: TigrisDataTypes.NUMBER - } + type: TigrisDataTypes.NUMBER, + }, }; const schema: TigrisSchema = { id: { - type: TigrisDataTypes.NUMBER + type: TigrisDataTypes.NUMBER, }, name: { - type: TigrisDataTypes.STRING + type: TigrisDataTypes.STRING, }, knownAddresses: { type: TigrisDataTypes.ARRAY, items: { - type: addressSchema - } - } + type: addressSchema, + }, + }, }; - expect(Utility._toJSONSchema("collectionWithObjectArrays", schema)) - .toBe(Utility._readTestDataFile("collectionWithObjectArrays.json")); + expect(Utility._toJSONSchema("collectionWithObjectArrays", schema)).toBe( + Utility._readTestDataFile("collectionWithObjectArrays.json") + ); }); it("multiLevelPrimitiveArray", () => { @@ -177,17 +182,17 @@ describe("schema tests", () => { oneDArray: { type: TigrisDataTypes.ARRAY, items: { - type: TigrisDataTypes.STRING - } + type: TigrisDataTypes.STRING, + }, }, twoDArray: { type: TigrisDataTypes.ARRAY, items: { type: TigrisDataTypes.ARRAY, items: { - type: TigrisDataTypes.STRING - } - } + type: TigrisDataTypes.STRING, + }, + }, }, threeDArray: { type: TigrisDataTypes.ARRAY, @@ -196,10 +201,10 @@ describe("schema tests", () => { items: { type: TigrisDataTypes.ARRAY, items: { - type: TigrisDataTypes.STRING - } - } - } + type: TigrisDataTypes.STRING, + }, + }, + }, }, fourDArray: { type: TigrisDataTypes.ARRAY, @@ -210,11 +215,11 @@ describe("schema tests", () => { items: { type: TigrisDataTypes.ARRAY, items: { - type: TigrisDataTypes.STRING - } - } - } - } + type: TigrisDataTypes.STRING, + }, + }, + }, + }, }, fiveDArray: { type: TigrisDataTypes.ARRAY, @@ -227,45 +232,46 @@ describe("schema tests", () => { items: { type: TigrisDataTypes.ARRAY, items: { - type: TigrisDataTypes.STRING - } - } - } - } - } - } + type: TigrisDataTypes.STRING, + }, + }, + }, + }, + }, + }, }; - expect(Utility._toJSONSchema("multiLevelPrimitiveArray", schema)) - .toBe(Utility._readTestDataFile("multiLevelPrimitiveArray.json")); + expect(Utility._toJSONSchema("multiLevelPrimitiveArray", schema)).toBe( + Utility._readTestDataFile("multiLevelPrimitiveArray.json") + ); }); it("multiLevelObjectArray", () => { const addressSchema: TigrisSchema
= { city: { - type: TigrisDataTypes.STRING + type: TigrisDataTypes.STRING, }, state: { - type: TigrisDataTypes.STRING + type: TigrisDataTypes.STRING, }, zipcode: { - type: TigrisDataTypes.NUMBER - } + type: TigrisDataTypes.NUMBER, + }, }; const schema: TigrisSchema = { oneDArray: { type: TigrisDataTypes.ARRAY, items: { - type: addressSchema - } + type: addressSchema, + }, }, twoDArray: { type: TigrisDataTypes.ARRAY, items: { type: TigrisDataTypes.ARRAY, items: { - type: addressSchema - } - } + type: addressSchema, + }, + }, }, threeDArray: { type: TigrisDataTypes.ARRAY, @@ -274,10 +280,10 @@ describe("schema tests", () => { items: { type: TigrisDataTypes.ARRAY, items: { - type: addressSchema - } - } - } + type: addressSchema, + }, + }, + }, }, fourDArray: { type: TigrisDataTypes.ARRAY, @@ -288,11 +294,11 @@ describe("schema tests", () => { items: { type: TigrisDataTypes.ARRAY, items: { - type: addressSchema - } - } - } - } + type: addressSchema, + }, + }, + }, + }, }, fiveDArray: { type: TigrisDataTypes.ARRAY, @@ -305,16 +311,17 @@ describe("schema tests", () => { items: { type: TigrisDataTypes.ARRAY, items: { - type: addressSchema - } - } - } - } - } - } + type: addressSchema, + }, + }, + }, + }, + }, + }, }; - expect(Utility._toJSONSchema("multiLevelObjectArray", schema)) - .toBe(Utility._readTestDataFile("multiLevelObjectArray.json")); + expect(Utility._toJSONSchema("multiLevelObjectArray", schema)).toBe( + Utility._readTestDataFile("multiLevelObjectArray.json") + ); }); }); @@ -338,7 +345,7 @@ interface BasicCollectionWithObject extends TigrisCollectionType { interface NestedCollection extends TigrisCollectionType { id: number; name: string; - address: Address + address: Address; } interface CollectionWithPrimitiveArrays extends TigrisCollectionType { diff --git a/src/__tests__/tigris.updatefields.spec.ts b/src/__tests__/tigris.updatefields.spec.ts index 42b188c..e9359af 100644 --- a/src/__tests__/tigris.updatefields.spec.ts +++ b/src/__tests__/tigris.updatefields.spec.ts @@ -1,8 +1,7 @@ -import {SimpleUpdateField, UpdateFields, UpdateFieldsOperator} from "../types"; -import {Utility} from "../utility"; +import { SimpleUpdateField, UpdateFields, UpdateFieldsOperator } from "../types"; +import { Utility } from "../utility"; describe("updateFields tests", () => { - it("updateFields", () => { const updateFields: UpdateFields = { op: UpdateFieldsOperator.SET, @@ -10,9 +9,11 @@ describe("updateFields tests", () => { title: "New Title", price: 499, active: true, - } + }, }; - expect(Utility.updateFieldsString(updateFields)).toBe("{\"$set\":{\"title\":\"New Title\",\"price\":499,\"active\":true}}"); + expect(Utility.updateFieldsString(updateFields)).toBe( + '{"$set":{"title":"New Title","price":499,"active":true}}' + ); }); it("simpleUpdateField", () => { @@ -21,6 +22,8 @@ describe("updateFields tests", () => { price: 499, active: true, }; - expect(Utility.updateFieldsString(updateFields)).toBe("{\"$set\":{\"title\":\"New Title\",\"price\":499,\"active\":true}}"); + expect(Utility.updateFieldsString(updateFields)).toBe( + '{"$set":{"title":"New Title","price":499,"active":true}}' + ); }); }); diff --git a/src/__tests__/tigris.utility.spec.ts b/src/__tests__/tigris.utility.spec.ts index d2e1cae..0884b30 100644 --- a/src/__tests__/tigris.utility.spec.ts +++ b/src/__tests__/tigris.utility.spec.ts @@ -8,7 +8,7 @@ import { MATCH_ALL_QUERY_STRING, Ordering, SearchRequestOptions, - SortOrder + SortOrder, } from "../search/types"; describe("utility tests", () => { @@ -30,7 +30,7 @@ describe("utility tests", () => { it("backfills missing facet query options", () => { const generatedOptions = Utility.createFacetQueryOptions({ - size: 55 + size: 55, }); expect(generatedOptions.size).toBe(55); expect(generatedOptions.type).toBe(FacetQueryFieldType.VALUE); @@ -39,23 +39,27 @@ describe("utility tests", () => { it("serializes FacetFields to string", () => { const fields: FacetFields = ["field_1", "field_2"]; const serialized: string = Utility.facetQueryToString(fields); - expect(serialized).toBe("{\"field_1\":{\"size\":10,\"type\":\"value\"},\"field_2\":{\"size\":10,\"type\":\"value\"}}"); + expect(serialized).toBe( + '{"field_1":{"size":10,"type":"value"},"field_2":{"size":10,"type":"value"}}' + ); }); it("serializes FacetFieldOptions to string", () => { const fields: FacetFieldOptions = { field_1: Utility.createFacetQueryOptions(), - field_2: {size: 10, type: FacetQueryFieldType.VALUE} + field_2: { size: 10, type: FacetQueryFieldType.VALUE }, }; const serialized: string = Utility.facetQueryToString(fields); - expect(serialized).toBe("{\"field_1\":{\"size\":10,\"type\":\"value\"},\"field_2\":{\"size\":10,\"type\":\"value\"}}"); + expect(serialized).toBe( + '{"field_1":{"size":10,"type":"value"},"field_2":{"size":10,"type":"value"}}' + ); }); - it("equivalent serialization of FacetFieldsQuery",() => { + it("equivalent serialization of FacetFieldsQuery", () => { const facetFields: FacetFieldsQuery = ["field_1", "field_2"]; const fieldOptions: FacetFieldsQuery = { field_1: Utility.createFacetQueryOptions(), - field_2: {size: 10, type: FacetQueryFieldType.VALUE} + field_2: { size: 10, type: FacetQueryFieldType.VALUE }, }; const serializedFields = Utility.facetQueryToString(facetFields); expect(serializedFields).toBe(Utility.facetQueryToString(fieldOptions)); @@ -67,10 +71,10 @@ describe("utility tests", () => { it("serializes sort orders to string", () => { const ordering: Ordering = [ - {field: "field_1", order: SortOrder.ASC}, - {field: "parent.field_2", order: SortOrder.DESC} + { field: "field_1", order: SortOrder.ASC }, + { field: "parent.field_2", order: SortOrder.DESC }, ]; - const expected = "[{\"field_1\":\"$asc\"},{\"parent.field_2\":\"$desc\"}]"; + const expected = '[{"field_1":"$asc"},{"parent.field_2":"$desc"}]'; expect(Utility.sortOrderingToString(ordering)).toBe(expected); }); @@ -79,30 +83,34 @@ describe("utility tests", () => { const collectionName = "my_test_collection"; it("populates projectName and collection name", () => { - const emptyRequest = {q: ""}; + const emptyRequest = { q: "" }; const generated = Utility.createProtoSearchRequest(dbName, collectionName, emptyRequest); expect(generated.getProject()).toBe(dbName); expect(generated.getCollection()).toBe(collectionName); }); it("creates default match all query string", () => { - const request = {q: undefined}; + const request = { q: undefined }; const generated = Utility.createProtoSearchRequest(dbName, collectionName, request); expect(generated.getQ()).toBe(MATCH_ALL_QUERY_STRING); }); - it ("sets collation options", () => { - const emptyRequest = {q: ""}; + it("sets collation options", () => { + const emptyRequest = { q: "" }; const options: SearchRequestOptions = { collation: { - case: Case.CaseInsensitive - } + case: Case.CaseInsensitive, + }, }; - const generated = Utility.createProtoSearchRequest(dbName, collectionName, emptyRequest, options); + const generated = Utility.createProtoSearchRequest( + dbName, + collectionName, + emptyRequest, + options + ); expect(generated.getPage()).toBe(0); expect(generated.getPageSize()).toBe(0); expect(generated.getCollation().getCase()).toBe("ci"); }); - }); }); From 68d5411cd1c38ebdbadd223a4df94d3d4dbc481d Mon Sep 17 00:00:00 2001 From: Adil Ansari Date: Wed, 4 Jan 2023 18:10:43 -0800 Subject: [PATCH 05/12] feat: findMany() and findOne() using FindQuery object (#197) feat: findMany() and findOne() using FindQuery object eliminating undefined --- src/__tests__/tigris.rpc.spec.ts | 69 ++++++---- src/collection.ts | 229 ++++++++++++++++++++++++++----- src/types.ts | 17 +++ 3 files changed, 250 insertions(+), 65 deletions(-) diff --git a/src/__tests__/tigris.rpc.spec.ts b/src/__tests__/tigris.rpc.spec.ts index d98c7aa..f1b7be1 100644 --- a/src/__tests__/tigris.rpc.spec.ts +++ b/src/__tests__/tigris.rpc.spec.ts @@ -370,9 +370,11 @@ describe("rpc tests", () => { const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "db3" }); const db1 = tigris.getDatabase(); const readOnePromise = db1.getCollection("books").findOne({ - op: SelectorFilterOperator.EQ, - fields: { - id: 1, + filter: { + op: SelectorFilterOperator.EQ, + fields: { + id: 1, + }, }, }); readOnePromise.then((value) => { @@ -389,9 +391,11 @@ describe("rpc tests", () => { const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "db3" }); const db1 = tigris.getDatabase(); const readOnePromise = db1.getCollection("books").findOne({ - op: SelectorFilterOperator.EQ, - fields: { - id: 2, + filter: { + op: SelectorFilterOperator.EQ, + fields: { + id: 2, + }, }, }); readOnePromise.then((value) => { @@ -404,21 +408,23 @@ describe("rpc tests", () => { const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "db3" }); const db1 = tigris.getDatabase(); const readOnePromise: Promise = db1.getCollection("books").findOne({ - op: LogicalOperator.AND, - selectorFilters: [ - { - op: SelectorFilterOperator.EQ, - fields: { - id: 3, + filter: { + op: LogicalOperator.AND, + selectorFilters: [ + { + op: SelectorFilterOperator.EQ, + fields: { + id: 3, + }, }, - }, - { - op: SelectorFilterOperator.EQ, - fields: { - title: "In Search of Lost Time", + { + op: SelectorFilterOperator.EQ, + fields: { + title: "In Search of Lost Time", + }, }, - }, - ], + ], + }, }); readOnePromise.then((value) => { const book: IBook = value; @@ -436,9 +442,11 @@ describe("rpc tests", () => { it("with filter using for await on cursor", async () => { const cursor = db.getCollection("books").findMany({ - op: SelectorFilterOperator.EQ, - fields: { - author: "Marcel Proust", + filter: { + op: SelectorFilterOperator.EQ, + fields: { + author: "Marcel Proust", + }, }, }); @@ -472,9 +480,11 @@ describe("rpc tests", () => { it("throws an error", async () => { const cursor = db.getCollection("books").findMany({ - op: SelectorFilterOperator.EQ, - fields: { - id: -1, + filter: { + op: SelectorFilterOperator.EQ, + fields: { + id: -1, + }, }, }); @@ -626,12 +636,13 @@ describe("rpc tests", () => { books .findOne( { - op: SelectorFilterOperator.EQ, - fields: { - id: 1, + filter: { + op: SelectorFilterOperator.EQ, + fields: { + id: 1, + }, }, }, - undefined, tx ) .then(() => { diff --git a/src/collection.ts b/src/collection.ts index a5856e8..073916e 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -15,7 +15,7 @@ import { DeleteResponse, DMLMetadata, Filter, - ReadFields, + FindQuery, ReadRequestOptions, SelectorFilterOperator, SimpleUpdateField, @@ -52,37 +52,116 @@ export abstract class ReadOnlyCollection impleme this.grpcClient = grpcClient; this.config = config; } + + private isTxSession(txOrQuery: Session | unknown): txOrQuery is Session { + const mayBeTx = txOrQuery as Session; + return "id" in mayBeTx && mayBeTx instanceof Session; + } + + /** + * Read all the documents from a collection. + * + * @returns - {@link Cursor} to iterate over documents + * + * @example + * ``` + * const cursor = db.getCollection(Book).findMany(); + * + * for await (const document of cursor) { + * console.log(document); + * } + * ``` + */ + findMany(): Cursor; + + /** + * Reads all the documents from a collection in transactional context. + * + * @param tx - Session information for Transaction + * @returns - {@link Cursor} to iterate over documents + * + * @example + * ``` + * const cursor = db.getCollection(Book).findMany(tx); + * + * for await (const document of cursor) { + * console.log(document); + * } + * ``` + */ + findMany(tx: Session): Cursor; + /** * Performs a read query on collection and returns a cursor that can be used to iterate over * query results. * - * @param filter - Optional filter. If unspecified, then all documents will match the filter - * @param readFields - Optional field projection param allows returning only specific document fields in result - * @param tx - Optional session information for transaction context - * @param options - Optional settings for the find query + * @param query - Filter, field projection and other parameters + * @returns - {@link Cursor} to iterate over documents + * + * @example + * ``` + * const cursor = db.getCollection(Book).findMany({ + * filter: { op: SelectorFilterOperator.EQ, fields: { author: "Marcel Proust" }}, + * readFields: { include: ["id", "title"] } + * }); + * + * for await (const document of cursor) { + * console.log(document); + * } + * ``` */ - findMany( - filter?: Filter, - readFields?: ReadFields, - tx?: Session, - options?: ReadRequestOptions - ): Cursor { - // find all - if (filter === undefined) { - filter = { op: SelectorFilterOperator.NONE }; + findMany(query: FindQuery): Cursor; + + /** + * Performs a read query on collection in transactional context and returns a + * cursor that can be used to iterate over query results. + * + * @param query - Filter, field projection and other parameters + * @param tx - Session information for Transaction + * @returns - {@link Cursor} to iterate over documents + * + * @example + * ``` + * const cursor = db.getCollection(Book).findMany({ + * filter: { op: SelectorFilterOperator.EQ, fields: { author: "Marcel Proust" }}, + * readFields: { include: ["id", "title"] } + * }, tx); + * + * for await (const document of cursor) { + * console.log(document); + * } + * ``` + */ + findMany(query: FindQuery, tx: Session): Cursor; + + findMany(txOrQuery?: Session | FindQuery, tx?: Session): Cursor { + let query: FindQuery; + if (typeof txOrQuery !== "undefined") { + if (this.isTxSession(txOrQuery)) { + tx = txOrQuery as Session; + } else { + query = txOrQuery as FindQuery; + } } + const findAll: Filter = { op: SelectorFilterOperator.NONE }; + + if (!query) { + query = { filter: findAll }; + } else if (!query.filter) { + query.filter = findAll; + } const readRequest = new ProtoReadRequest() .setProject(this.db) .setCollection(this.collectionName) - .setFilter(Utility.stringToUint8Array(Utility.filterToString(filter))); + .setFilter(Utility.stringToUint8Array(Utility.filterToString(query.filter))); - if (readFields) { - readRequest.setFields(Utility.stringToUint8Array(Utility.readFieldString(readFields))); + if (query.readFields) { + readRequest.setFields(Utility.stringToUint8Array(Utility.readFieldString(query.readFields))); } - if (options !== undefined) { - readRequest.setOptions(Utility._readRequestOptionsToProtoReadRequestOptions(options)); + if (query.options) { + readRequest.setOptions(Utility._readRequestOptionsToProtoReadRequestOptions(query.options)); } const initializer = new ReadCursorInitializer(this.grpcClient, readRequest, tx); @@ -90,28 +169,106 @@ export abstract class ReadOnlyCollection impleme } /** - * Performs a query to find a single document in collection. Returns the document if found, else - * null. + * Read a single document from collection. * - * @param filter - Query to match the document - * @param readFields - Optional field projection param allows returning only specific document fields in result - * @param tx - Optional session information for transaction context - * @param options - Optional settings for the find query + * @returns - The document if found else **undefined** + * + * @example + * ``` + * const documentPromise = db.getCollection(Book).findOne(); + * + * documentPromise + * .then((doc: Book | undefined) => console.log(doc)); + * .catch( // catch the error) + * .finally( // finally do something); + * ``` */ - findOne( - filter: Filter, - readFields?: ReadFields, - tx?: Session, - options?: ReadRequestOptions - ): Promise { - return new Promise((resolve, reject) => { - if (options === undefined) { - options = new ReadRequestOptions(1); + findOne(): Promise; + + /** + * Read a single document from collection in transactional context + * + * @param tx - Session information for Transaction + * @returns - The document if found else **undefined** + * + * @example + * ``` + * const documentPromise = db.getCollection(Book).findOne(tx); + * + * documentPromise + * .then((doc: Book | undefined) => console.log(doc)); + * .catch( // catch the error) + * .finally( // finally do something); + * ``` + */ + findOne(tx: Session): Promise; + + /** + * Performs a read query on the collection and returns a single document matching + * the query. + * + * @param query - Filter, field projection and other parameters + * @returns - The document if found else **undefined** + * + * @example + * ``` + * const documentPromise = db.getCollection(Book).findOne({ + * filter: { op: SelectorFilterOperator.EQ, fields: { author: "Marcel Proust" }}, + * readFields: { include: ["id", "title"] } + * }); + * + * documentPromise + * .then((doc: Book | undefined) => console.log(doc)); + * .catch( // catch the error) + * .finally( // finally do something); + * ``` + */ + findOne(query: FindQuery): Promise; + /** + * Performs a read query on the collection in transactional context and returns + * a single document matching the query. + * + * @param query - Filter, field projection and other parameters + * @param tx - Session information for Transaction + * @returns - The document if found else **undefined** + * + * @example + * ``` + * const documentPromise = db.getCollection(Book).findOne({ + * filter: { op: SelectorFilterOperator.EQ, fields: { author: "Marcel Proust" }}, + * readFields: { include: ["id", "title"] } + * }, tx); + * + * documentPromise + * .then((doc: Book | undefined) => console.log(doc)); + * .catch( // catch the error) + * .finally( // finally do something); + * ``` + */ + findOne(query: FindQuery, tx: Session): Promise; + + findOne(txOrQuery?: Session | FindQuery, tx?: Session): Promise { + let query: FindQuery; + if (typeof txOrQuery !== "undefined") { + if (this.isTxSession(txOrQuery)) { + tx = txOrQuery as Session; } else { - options.limit = 1; + query = txOrQuery as FindQuery; } + } + + const findOnlyOne: ReadRequestOptions = new ReadRequestOptions(1); + + if (!query) { + query = { options: findOnlyOne }; + } else if (!query.options) { + query.options = findOnlyOne; + } else { + query.options.limit = findOnlyOne.limit; + } - const cursor = this.findMany(filter, readFields, tx, options); + return new Promise((resolve, reject) => { + const cursor = this.findMany(query, tx); const iteratorResult = cursor[Symbol.asyncIterator]().next(); if (iteratorResult !== undefined) { iteratorResult diff --git a/src/types.ts b/src/types.ts index 5f87c58..40a75e8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -364,6 +364,23 @@ export type SimpleUpdateField = { [key: string]: FieldTypes | undefined; }; +export interface FindQuery { + /** + * Filter to match the documents. Query will match all documents without a filter. + */ + filter?: Filter; + + /** + * Field projection to allow returning only specific document fields. By default + * all document fields are returned. + */ + readFields?: ReadFields; + /** + * Optional params + */ + options?: ReadRequestOptions; +} + export enum TigrisDataTypes { STRING = "string", BOOLEAN = "boolean", From 74eacff4f5be24192bb1f86d77489f697936a6ac Mon Sep 17 00:00:00 2001 From: Garren Date: Thu, 5 Jan 2023 12:09:33 +0200 Subject: [PATCH 06/12] chore: Add PR template (#198) Add a new PR template that will be created for each new PR. This will help us make sure that each PR has more detail about what is going on. --- .github/PULL_REQUEST_TEMPLATE.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..6bede66 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,5 @@ +## Describe your changes + +## How best to test these changes + +## Issue ticket number and link From 4f49c464183c1e385d993397bdb01a9b92413908 Mon Sep 17 00:00:00 2001 From: Adil Ansari Date: Thu, 5 Jan 2023 11:19:44 -0800 Subject: [PATCH 07/12] feat: Reworked collection APIs to use query objects * DeleteOne() and DeleteMany() to use DeleteQuery * renaming SearchRequest to SearchQuery * UpdateOne() and UpdateMany() to use UpdateQuery * restructuring the collection class * Renaming request options to query options * Merging SearchQuery and options * PR comments --- src/__tests__/tigris.rpc.spec.ts | 127 ++--- src/__tests__/tigris.utility.spec.ts | 13 +- src/collection.ts | 727 +++++++++++++++------------ src/error.ts | 10 + src/search/types.ts | 21 +- src/types.ts | 53 +- src/utility.ts | 54 +- 7 files changed, 537 insertions(+), 468 deletions(-) diff --git a/src/__tests__/tigris.rpc.spec.ts b/src/__tests__/tigris.rpc.spec.ts index f1b7be1..037e7c8 100644 --- a/src/__tests__/tigris.rpc.spec.ts +++ b/src/__tests__/tigris.rpc.spec.ts @@ -2,23 +2,17 @@ import { Server, ServerCredentials } from "@grpc/grpc-js"; import { TigrisService } from "../proto/server/v1/api_grpc_pb"; import TestService, { TestTigrisService } from "./test-service"; import { - DeleteRequestOptions, + DeleteQueryOptions, LogicalOperator, SelectorFilterOperator, TigrisCollectionType, TigrisDataTypes, TigrisSchema, UpdateFieldsOperator, - UpdateRequestOptions, + UpdateQueryOptions, } from "../types"; import { Tigris } from "../tigris"; -import { - Case, - Collation, - SearchRequest, - SearchRequestOptions, - SearchResult, -} from "../search/types"; +import { Case, Collation, SearchQuery, SearchQueryOptions, SearchResult } from "../search/types"; import { Utility } from "../utility"; import { ObservabilityService } from "../proto/server/v1/observability_grpc_pb"; import TestObservabilityService from "./test-observability-service"; @@ -273,10 +267,7 @@ describe("rpc tests", () => { const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "db3" }); const db1 = tigris.getDatabase(); const deletionPromise = db1.getCollection(IBook).deleteMany({ - op: SelectorFilterOperator.EQ, - fields: { - id: 1, - }, + filter: { id: 1 }, }); deletionPromise.then((value) => { expect(value.status).toBe('deleted: {"id":1}'); @@ -291,19 +282,19 @@ describe("rpc tests", () => { const expectedFilter = { id: 1 }; const expectedCollation: Collation = { case: Case.CaseInsensitive }; - const options = new DeleteRequestOptions(5, expectedCollation); + const options = new DeleteQueryOptions(5, expectedCollation); - const deletePromise = collection.deleteOne(expectedFilter, undefined, options); - const [capturedFilter, capturedTx, capturedOptions] = capture(spyCollection.deleteMany).last(); + const deletePromise = collection.deleteOne({ filter: expectedFilter, options: options }); + const [capturedQuery, capturedTx] = capture(spyCollection.deleteMany).last(); // filter passed as it is - expect(capturedFilter).toBe(expectedFilter); + expect(capturedQuery.filter).toBe(expectedFilter); // tx passed as it is expect(capturedTx).toBe(undefined); // options.collation passed as it is - expect(capturedOptions.collation).toBe(expectedCollation); + expect(capturedQuery.options.collation).toBe(expectedCollation); // options.limit === 1 while original was 5 - expect(capturedOptions.limit).toBe(1); + expect(capturedQuery.options.limit).toBe(1); return deletePromise; }); @@ -311,20 +302,20 @@ describe("rpc tests", () => { it("update", () => { const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "db3" }); const db1 = tigris.getDatabase(); - const updatePromise = db1.getCollection("books").updateMany( - { + const updatePromise = db1.getCollection("books").updateMany({ + filter: { op: SelectorFilterOperator.EQ, fields: { id: 1, }, }, - { + fields: { op: UpdateFieldsOperator.SET, fields: { title: "New Title", }, - } - ); + }, + }); updatePromise.then((value) => { expect(value.status).toBe('updated: {"id":1}, {"$set":{"title":"New Title"}}'); expect(value.modifiedCount).toBe(1); @@ -340,28 +331,25 @@ describe("rpc tests", () => { const expectedFilter = { id: 1 }; const expectedCollation: Collation = { case: Case.CaseInsensitive }; const expectedUpdateFields = { title: "one" }; - const options = new UpdateRequestOptions(5, expectedCollation); + const options = new UpdateQueryOptions(5, expectedCollation); - const updatePromise = collection.updateOne( - expectedFilter, - expectedUpdateFields, - undefined, - options - ); - const [capturedFilter, capturedFields, capturedTx, capturedOptions] = capture( - spyCollection.updateMany - ).last(); + const updatePromise = collection.updateOne({ + filter: expectedFilter, + fields: expectedUpdateFields, + options: options, + }); + const [capturedQuery, capturedTx] = capture(spyCollection.updateMany).last(); // filter passed as it is - expect(capturedFilter).toBe(expectedFilter); + expect(capturedQuery.filter).toBe(expectedFilter); // updateFields passed as it is - expect(capturedFields).toBe(expectedUpdateFields); + expect(capturedQuery.fields).toBe(expectedUpdateFields); // tx passed as it is expect(capturedTx).toBe(undefined); // options.collation passed as it is - expect(capturedOptions.collation).toBe(expectedCollation); + expect(capturedQuery.options.collation).toBe(expectedCollation); // options.limit === 1 while original was 5 - expect(capturedOptions.limit).toBe(1); + expect(capturedQuery.options.limit).toBe(1); return updatePromise; }); @@ -507,14 +495,14 @@ describe("rpc tests", () => { const pageNumber = 2; it("returns a promise", () => { - const request: SearchRequest = { + const query: SearchQuery = { q: "philosophy", facets: { tags: Utility.createFacetQueryOptions(), }, }; - const maybePromise = db.getCollection("books").search(request, pageNumber); + const maybePromise = db.getCollection("books").search(query, pageNumber); expect(maybePromise).toBeInstanceOf(Promise); maybePromise.then((res: SearchResult) => { @@ -524,25 +512,11 @@ describe("rpc tests", () => { }); return maybePromise; }); - - it("returns promise using request options", () => { - const options: SearchRequestOptions = { collation: { case: Case.CaseInsensitive } }; - const request: SearchRequest = { q: "philosophy" }; - - const maybePromise = db.getCollection("books").search(request, options, pageNumber); - expect(maybePromise).toBeInstanceOf(Promise); - - maybePromise.then((res: SearchResult) => { - expect(res.meta.page.current).toBe(pageNumber); - }); - - return maybePromise; - }); }); describe("without explicit page number", () => { it("returns an iterator", async () => { - const request: SearchRequest = { + const query: SearchQuery = { q: "philosophy", facets: { tags: Utility.createFacetQueryOptions(), @@ -550,7 +524,7 @@ describe("rpc tests", () => { }; let bookCounter = 0; - const maybeIterator = db.getCollection("books").search(request); + const maybeIterator = db.getCollection("books").search(query); expect(maybeIterator).toBeInstanceOf(SearchIterator); // for await loop the iterator @@ -561,22 +535,6 @@ describe("rpc tests", () => { } expect(bookCounter).toBe(TestTigrisService.BOOKS_B64_BY_ID.size); }); - - it("returns iterator using request options", async () => { - const request: SearchRequest = { q: "philosophy" }; - const options: SearchRequestOptions = { collation: { case: Case.CaseInsensitive } }; - let bookCounter = 0; - - const maybeIterator = db.getCollection("books").search(request, options); - expect(maybeIterator).toBeInstanceOf(SearchIterator); - - for await (const searchResult of maybeIterator) { - expect(searchResult.hits).toBeDefined(); - bookCounter += searchResult.hits.length; - } - - expect(bookCounter).toBe(TestTigrisService.BOOKS_B64_BY_ID.size); - }); }); }); @@ -649,17 +607,18 @@ describe("rpc tests", () => { books .updateMany( { - op: SelectorFilterOperator.EQ, - fields: { - id: 1, + filter: { + op: SelectorFilterOperator.EQ, + fields: { + id: 1, + }, }, - }, - { - op: UpdateFieldsOperator.SET, fields: { - author: "Dr. Author", + op: UpdateFieldsOperator.SET, + fields: { + author: "Dr. Author", + }, }, - // eslint-disable-next-line @typescript-eslint/no-unused-vars }, tx ) @@ -667,9 +626,11 @@ describe("rpc tests", () => { books .deleteMany( { - op: SelectorFilterOperator.EQ, - fields: { - id: 1, + filter: { + op: SelectorFilterOperator.EQ, + fields: { + id: 1, + }, }, }, tx diff --git a/src/__tests__/tigris.utility.spec.ts b/src/__tests__/tigris.utility.spec.ts index 0884b30..8039315 100644 --- a/src/__tests__/tigris.utility.spec.ts +++ b/src/__tests__/tigris.utility.spec.ts @@ -7,7 +7,7 @@ import { FacetQueryFieldType, MATCH_ALL_QUERY_STRING, Ordering, - SearchRequestOptions, + SearchQueryOptions, SortOrder, } from "../search/types"; @@ -96,18 +96,13 @@ describe("utility tests", () => { }); it("sets collation options", () => { - const emptyRequest = { q: "" }; - const options: SearchRequestOptions = { + const options: SearchQueryOptions = { collation: { case: Case.CaseInsensitive, }, }; - const generated = Utility.createProtoSearchRequest( - dbName, - collectionName, - emptyRequest, - options - ); + const emptyRequest = { q: "", options: options }; + const generated = Utility.createProtoSearchRequest(dbName, collectionName, emptyRequest); expect(generated.getPage()).toBe(0); expect(generated.getPageSize()).toBe(0); expect(generated.getCollation().getCase()).toBe("ci"); diff --git a/src/collection.ts b/src/collection.ts index 073916e..1d325d5 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -11,23 +11,24 @@ import { } from "./proto/server/v1/api_pb"; import { Session } from "./session"; import { - DeleteRequestOptions, + DeleteQuery, + DeleteQueryOptions, DeleteResponse, DMLMetadata, Filter, FindQuery, - ReadRequestOptions, + FindQueryOptions, SelectorFilterOperator, - SimpleUpdateField, TigrisCollectionType, - UpdateFields, - UpdateRequestOptions, + UpdateQuery, + UpdateQueryOptions, UpdateResponse, } from "./types"; import { Utility } from "./utility"; -import { SearchRequest, SearchRequestOptions, SearchResult } from "./search/types"; import { TigrisClientConfig } from "./tigris"; +import { MissingArgumentError } from "./error"; import { Cursor, ReadCursorInitializer } from "./consumables/cursor"; +import { SearchQuery, SearchResult } from "./search/types"; import { SearchIterator, SearchIteratorInitializer } from "./consumables/search-iterator"; interface ICollection { @@ -35,13 +36,18 @@ interface ICollection { readonly db: string; } -export abstract class ReadOnlyCollection implements ICollection { +/** + * The **Collection** class represents Tigris collection allowing insert/find/update/delete/search + * operations. + * @public + */ +export class Collection implements ICollection { readonly collectionName: string; readonly db: string; readonly grpcClient: TigrisClient; readonly config: TigrisClientConfig; - protected constructor( + constructor( collectionName: string, db: string, grpcClient: TigrisClient, @@ -53,9 +59,355 @@ export abstract class ReadOnlyCollection impleme this.config = config; } - private isTxSession(txOrQuery: Session | unknown): txOrQuery is Session { - const mayBeTx = txOrQuery as Session; - return "id" in mayBeTx && mayBeTx instanceof Session; + /** + * Inserts multiple documents in Tigris collection. + * + * @param docs - Array of documents to insert + * @param tx - Session information for transaction context + */ + insertMany(docs: Array, tx?: Session): Promise> { + const encoder = new TextEncoder(); + return new Promise>((resolve, reject) => { + const docsArray: Array = docs.map((doc) => + encoder.encode(Utility.objToJsonString(doc)) + ); + + const protoRequest = new ProtoInsertRequest() + .setProject(this.db) + .setCollection(this.collectionName) + .setDocumentsList(docsArray); + + this.grpcClient.insert( + protoRequest, + Utility.txToMetadata(tx), + (error: grpc.ServiceError, response: server_v1_api_pb.InsertResponse): void => { + if (error) { + reject(error); + } else { + const clonedDocs = this.setDocsMetadata(docs, response.getKeysList_asU8()); + resolve(clonedDocs); + } + } + ); + }); + } + + /** + * Inserts a single document in Tigris collection. + * + * @param doc - Document to insert + * @param tx - Session information for transaction context + */ + insertOne(doc: T, tx?: Session): Promise { + return new Promise((resolve, reject) => { + const docArr: Array = [doc]; + this.insertMany(docArr, tx) + .then((docs) => { + resolve(docs[0]); + }) + .catch((error) => { + reject(error); + }); + }); + } + + /** + * Insert new or replace existing documents in collection. + * + * @param docs - Array of documents to insert or replace + * @param tx - Session information for transaction context + */ + insertOrReplaceMany(docs: Array, tx?: Session): Promise> { + return new Promise>((resolve, reject) => { + const docsArray: Array = docs.map((doc) => + new TextEncoder().encode(Utility.objToJsonString(doc)) + ); + const protoRequest = new ProtoReplaceRequest() + .setProject(this.db) + .setCollection(this.collectionName) + .setDocumentsList(docsArray); + + this.grpcClient.replace( + protoRequest, + Utility.txToMetadata(tx), + (error: grpc.ServiceError, response: server_v1_api_pb.ReplaceResponse): void => { + if (error) { + reject(error); + } else { + const clonedDocs = this.setDocsMetadata(docs, response.getKeysList_asU8()); + resolve(clonedDocs); + } + } + ); + }); + } + + /** + * Insert new or replace an existing document in collection. + * + * @param doc - Document to insert or replace + * @param tx - Session information for transaction context + */ + insertOrReplaceOne(doc: T, tx?: Session): Promise { + return new Promise((resolve, reject) => { + const docArr: Array = [doc]; + this.insertOrReplaceMany(docArr, tx) + .then((docs) => resolve(docs[0])) + .catch((error) => reject(error)); + }); + } + + /** + * Update multiple documents in a collection + * + * @param query - Filter to match documents and the update operations. Update + * will be applied to matching documents only. + * @returns {@link UpdateResponse} + * + * @example To update **language** of all books published by "Marcel Proust" + * ``` + * const updatePromise = db.getCollection(Book).updateMany({ + * filter: { author: "Marcel Proust" }, + * fields: { language: "French" } + * }); + * + * updatePromise + * .then((resp: UpdateResponse) => console.log(resp)); + * .catch( // catch the error) + * .finally( // finally do something); + * ``` + */ + updateMany(query: UpdateQuery): Promise; + + /** + * Update multiple documents in a collection in transactional context + * + * @param query - Filter to match documents and the update operations. Update + * will be applied to matching documents only. + * @param tx - Session information for transaction context + * @returns {@link UpdateResponse} + * + * @example To update **language** of all books published by "Marcel Proust" + * ``` + * const updatePromise = db.getCollection(Book).updateMany({ + * filter: { author: "Marcel Proust" }, + * fields: { language: "French" } + * }, tx); + * + * updatePromise + * .then((resp: UpdateResponse) => console.log(resp)); + * .catch( // catch the error) + * .finally( // finally do something); + * ``` + */ + updateMany(query: UpdateQuery, tx: Session): Promise; + + updateMany(query: UpdateQuery, tx?: Session): Promise { + return new Promise((resolve, reject) => { + const updateRequest = new ProtoUpdateRequest() + .setProject(this.db) + .setCollection(this.collectionName) + .setFilter(Utility.stringToUint8Array(Utility.filterToString(query.filter))) + .setFields(Utility.stringToUint8Array(Utility.updateFieldsString(query.fields))); + + if (query.options !== undefined) { + updateRequest.setOptions( + Utility._updateRequestOptionsToProtoUpdateRequestOptions(query.options) + ); + } + + this.grpcClient.update(updateRequest, Utility.txToMetadata(tx), (error, response) => { + if (error) { + reject(error); + } else { + const metadata: DMLMetadata = new DMLMetadata( + response.getMetadata().getCreatedAt(), + response.getMetadata().getUpdatedAt() + ); + resolve(new UpdateResponse(response.getStatus(), response.getModifiedCount(), metadata)); + } + }); + }); + } + + /** + * Update a single document in collection + * + * @param query - Filter to match the document and the update operations. Update + * will be applied to matching documents only. + * @returns {@link UpdateResponse} + * + * @example To update **language** of a book published by "Marcel Proust" + * ``` + * const updatePromise = db.getCollection(Book).updateOne({ + * filter: { author: "Marcel Proust" }, + * fields: { language: "French" } + * }); + * + * updatePromise + * .then((resp: UpdateResponse) => console.log(resp)); + * .catch( // catch the error) + * .finally( // finally do something); + * ``` + */ + updateOne(query: UpdateQuery): Promise; + + /** + * Update a single document in a collection in transactional context + * + * @param query - Filter to match the document and update operations. Update + * will be applied to a single matching document only. + * @param tx - Session information for transaction context + * @returns {@link UpdateResponse} + * + * @example To update **language** of a book published by "Marcel Proust" + * ``` + * const updatePromise = db.getCollection(Book).updateOne({ + * filter: { author: "Marcel Proust" }, + * fields: { language: "French" } + * }, tx); + * + * updatePromise + * .then((resp: UpdateResponse) => console.log(resp)); + * .catch( // catch the error) + * .finally( // finally do something); + * ``` + */ + updateOne(query: UpdateQuery, tx: Session): Promise; + + updateOne(query: UpdateQuery, tx?: Session): Promise { + if (query.options === undefined) { + query.options = new UpdateQueryOptions(1); + } else { + query.options.limit = 1; + } + return this.updateMany(query, tx); + } + + /** + * Delete documents from collection matching the query + * + * @param query - Filter to match documents and other deletion options + * @returns {@link DeleteResponse} + * + * @example + * + * ``` + * const deletionPromise = db.getCollection(Book).deleteMany({ + * filter: { author: "Marcel Proust" } + * }); + * + * deletionPromise + * .then((resp: DeleteResponse) => console.log(resp)); + * .catch( // catch the error) + * .finally( // finally do something); + * ``` + */ + deleteMany(query: DeleteQuery): Promise; + + /** + * Delete documents from collection in transactional context + * + * @param query - Filter to match documents and other deletion options + * @param tx - Session information for transaction context + * @returns {@link DeleteResponse} + * + * @example + * + * ``` + * const deletionPromise = db.getCollection(Book).deleteMany({ + * filter: { author: "Marcel Proust" } + * }, tx); + * + * deletionPromise + * .then((resp: DeleteResponse) => console.log(resp)); + * .catch( // catch the error) + * .finally( // finally do something); + * ``` + */ + deleteMany(query: DeleteQuery, tx: Session): Promise; + + deleteMany(query: DeleteQuery, tx?: Session): Promise { + return new Promise((resolve, reject) => { + if (typeof query?.filter === "undefined") { + reject(new MissingArgumentError("filter")); + } + const deleteRequest = new ProtoDeleteRequest() + .setProject(this.db) + .setCollection(this.collectionName) + .setFilter(Utility.stringToUint8Array(Utility.filterToString(query.filter))); + + if (query.options) { + deleteRequest.setOptions( + Utility._deleteRequestOptionsToProtoDeleteRequestOptions(query.options) + ); + } + + this.grpcClient.delete(deleteRequest, Utility.txToMetadata(tx), (error, response) => { + if (error) { + reject(error); + } else { + const metadata: DMLMetadata = new DMLMetadata( + response.getMetadata().getCreatedAt(), + response.getMetadata().getUpdatedAt() + ); + resolve(new DeleteResponse(response.getStatus(), metadata)); + } + }); + }); + } + + /** + * Delete a single document from collection matching the query + * + * @param query - Filter to match documents and other deletion options + * @returns {@link DeleteResponse} + * + * @example + * + * ``` + * const deletionPromise = db.getCollection(Book).deleteOne({ + * filter: { author: "Marcel Proust" } + * }); + * + * deletionPromise + * .then((resp: DeleteResponse) => console.log(resp)); + * .catch( // catch the error) + * .finally( // finally do something); + * ``` + */ + deleteOne(query: DeleteQuery): Promise; + + /** + * Delete a single document from collection in transactional context + * + * @param query - Filter to match documents and other deletion options + * @param tx - Session information for transaction context + * @returns {@link DeleteResponse} + * + * @example + * + * ``` + * const deletionPromise = db.getCollection(Book).deleteOne({ + * filter: { author: "Marcel Proust" } + * }, tx); + * + * deletionPromise + * .then((resp: DeleteResponse) => console.log(resp)); + * .catch( // catch the error) + * .finally( // finally do something); + * ``` + */ + deleteOne(query: DeleteQuery, tx: Session): Promise; + + deleteOne(query: DeleteQuery, tx?: Session): Promise { + if (query.options === undefined) { + query.options = new DeleteQueryOptions(1); + } else { + query.options.limit = 1; + } + + return this.deleteMany(query, tx); } /** @@ -101,7 +453,7 @@ export abstract class ReadOnlyCollection impleme * @example * ``` * const cursor = db.getCollection(Book).findMany({ - * filter: { op: SelectorFilterOperator.EQ, fields: { author: "Marcel Proust" }}, + * filter: { author: "Marcel Proust" }, * readFields: { include: ["id", "title"] } * }); * @@ -123,7 +475,7 @@ export abstract class ReadOnlyCollection impleme * @example * ``` * const cursor = db.getCollection(Book).findMany({ - * filter: { op: SelectorFilterOperator.EQ, fields: { author: "Marcel Proust" }}, + * filter: { author: "Marcel Proust" }, * readFields: { include: ["id", "title"] } * }, tx); * @@ -213,7 +565,7 @@ export abstract class ReadOnlyCollection impleme * @example * ``` * const documentPromise = db.getCollection(Book).findOne({ - * filter: { op: SelectorFilterOperator.EQ, fields: { author: "Marcel Proust" }}, + * filter: { author: "Marcel Proust" }, * readFields: { include: ["id", "title"] } * }); * @@ -224,6 +576,7 @@ export abstract class ReadOnlyCollection impleme * ``` */ findOne(query: FindQuery): Promise; + /** * Performs a read query on the collection in transactional context and returns * a single document matching the query. @@ -235,7 +588,7 @@ export abstract class ReadOnlyCollection impleme * @example * ``` * const documentPromise = db.getCollection(Book).findOne({ - * filter: { op: SelectorFilterOperator.EQ, fields: { author: "Marcel Proust" }}, + * filter: { author: "Marcel Proust" }, * readFields: { include: ["id", "title"] } * }, tx); * @@ -249,25 +602,25 @@ export abstract class ReadOnlyCollection impleme findOne(txOrQuery?: Session | FindQuery, tx?: Session): Promise { let query: FindQuery; - if (typeof txOrQuery !== "undefined") { - if (this.isTxSession(txOrQuery)) { - tx = txOrQuery as Session; - } else { - query = txOrQuery as FindQuery; + return new Promise((resolve, reject) => { + if (typeof txOrQuery !== "undefined") { + if (this.isTxSession(txOrQuery)) { + tx = txOrQuery as Session; + } else { + query = txOrQuery as FindQuery; + } } - } - const findOnlyOne: ReadRequestOptions = new ReadRequestOptions(1); + const findOnlyOne: FindQueryOptions = new FindQueryOptions(1); - if (!query) { - query = { options: findOnlyOne }; - } else if (!query.options) { - query.options = findOnlyOne; - } else { - query.options.limit = findOnlyOne.limit; - } + if (!query) { + query = { options: findOnlyOne }; + } else if (!query.options) { + query.options = findOnlyOne; + } else { + query.options.limit = findOnlyOne.limit; + } - return new Promise((resolve, reject) => { const cursor = this.findMany(query, tx); const iteratorResult = cursor[Symbol.asyncIterator]().next(); if (iteratorResult !== undefined) { @@ -288,53 +641,12 @@ export abstract class ReadOnlyCollection impleme * Search for documents in a collection. Easily perform sophisticated queries and refine * results using filters with advanced features like faceting and ordering. * - * @param request - Search query to execute - * @returns {@link SearchIterator} - To iterate over pages of {@link SearchResult} - * - * @example - * ``` - * const iterator = db.getCollection(Book).search(request); - * - * for await (const resultPage of iterator) { - * console.log(resultPage.hits); - * console.log(resultPage.facets); - * } - * ``` - */ - search(request: SearchRequest): SearchIterator; - - /** - * Search for documents in a collection. Easily perform sophisticated queries and refine - * results using filters with advanced features like faceting and ordering. - * - * @param request - Search query to execute - * @param page - Page number to retrieve. Page number `1` fetches the first page of search results. - * @returns - Single page of results wrapped in a Promise - * - * @example To retrieve page number 5 of matched documents - * ``` - * const resultPromise = db.getCollection(Book).search(request, 5); - * - * resultPromise - * .then((res: SearchResult) => console.log(res.hits)) - * .catch( // catch the error) - * .finally( // finally do something); - * - * ``` - */ - search(request: SearchRequest, page: number): Promise>; - - /** - * Search for documents in a collection. Easily perform sophisticated queries and refine - * results using filters with advanced features like faceting and ordering. - * - * @param request - Search query to execute - * @param options - Optional settings for search + * @param query - Search query to execute * @returns {@link SearchIterator} - To iterate over pages of {@link SearchResult} * * @example * ``` - * const iterator = db.getCollection(Book).search(request, options); + * const iterator = db.getCollection(Book).search(query); * * for await (const resultPage of iterator) { * console.log(resultPage.hits); @@ -342,20 +654,19 @@ export abstract class ReadOnlyCollection impleme * } * ``` */ - search(request: SearchRequest, options: SearchRequestOptions): SearchIterator; + search(query: SearchQuery): SearchIterator; /** * Search for documents in a collection. Easily perform sophisticated queries and refine * results using filters with advanced features like faceting and ordering. * - * @param request - Search query to execute - * @param options - Optional settings for search + * @param query - Search query to execute * @param page - Page number to retrieve. Page number `1` fetches the first page of search results. * @returns - Single page of results wrapped in a Promise * * @example To retrieve page number 5 of matched documents * ``` - * const resultPromise = db.getCollection(Book).search(request, options, 5); + * const resultPromise = db.getCollection(Book).search(query, 5); * * resultPromise * .then((res: SearchResult) => console.log(res.hits)) @@ -364,31 +675,13 @@ export abstract class ReadOnlyCollection impleme * * ``` */ - search( - request: SearchRequest, - options: SearchRequestOptions, - page: number - ): Promise>; - - search( - request: SearchRequest, - pageOrOptions?: SearchRequestOptions | number, - page?: number - ): SearchIterator | Promise> { - let options: SearchRequestOptions; - if (typeof pageOrOptions !== "undefined") { - if (typeof pageOrOptions === "number") { - page = pageOrOptions as number; - } else { - options = pageOrOptions as SearchRequestOptions; - } - } + search(query: SearchQuery, page: number): Promise>; + search(query: SearchQuery, page?: number): SearchIterator | Promise> { const searchRequest = Utility.createProtoSearchRequest( this.db, this.collectionName, - request, - options, + query, page ); @@ -410,20 +703,10 @@ export abstract class ReadOnlyCollection impleme }); } } -} -/** - * The **Collection** class represents Tigris collection allowing insert/find/update/delete/search - * operations. - */ -export class Collection extends ReadOnlyCollection { - constructor( - collectionName: string, - db: string, - grpcClient: TigrisClient, - config: TigrisClientConfig - ) { - super(collectionName, db, grpcClient, config); + private isTxSession(txOrQuery: Session | unknown): txOrQuery is Session { + const mayBeTx = txOrQuery as Session; + return "id" in mayBeTx && mayBeTx instanceof Session; } private setDocsMetadata(docs: Array, keys: Array): Array { @@ -443,226 +726,4 @@ export class Collection extends ReadOnlyCollecti return clonedDocs; } - - /** - * Inserts multiple documents in Tigris collection. - * - * @param docs - Array of documents to insert - * @param tx - Optional session information for transaction context - */ - insertMany(docs: Array, tx?: Session): Promise> { - return new Promise>((resolve, reject) => { - const docsArray = new Array(); - for (const doc of docs) { - docsArray.push(new TextEncoder().encode(Utility.objToJsonString(doc))); - } - - const protoRequest = new ProtoInsertRequest() - .setProject(this.db) - .setCollection(this.collectionName) - .setDocumentsList(docsArray); - - this.grpcClient.insert( - protoRequest, - Utility.txToMetadata(tx), - (error: grpc.ServiceError, response: server_v1_api_pb.InsertResponse): void => { - if (error !== undefined && error !== null) { - reject(error); - } else { - const clonedDocs = this.setDocsMetadata(docs, response.getKeysList_asU8()); - resolve(clonedDocs); - } - } - ); - }); - } - - /** - * Inserts a single document in Tigris collection. - * - * @param doc - Document to insert - * @param tx - Optional session information for transaction context - */ - insertOne(doc: T, tx?: Session): Promise { - return new Promise((resolve, reject) => { - const docArr: Array = new Array(); - docArr.push(doc); - this.insertMany(docArr, tx) - .then((docs) => { - resolve(docs[0]); - }) - .catch((error) => { - reject(error); - }); - }); - } - - /** - * Insert new or replace existing documents in collection. - * - * @param docs - Array of documents to insert or replace - * @param tx - Optional session information for transaction context - */ - insertOrReplaceMany(docs: Array, tx?: Session): Promise> { - return new Promise>((resolve, reject) => { - const docsArray = new Array(); - for (const doc of docs) { - docsArray.push(new TextEncoder().encode(Utility.objToJsonString(doc))); - } - const protoRequest = new ProtoReplaceRequest() - .setProject(this.db) - .setCollection(this.collectionName) - .setDocumentsList(docsArray); - - this.grpcClient.replace( - protoRequest, - Utility.txToMetadata(tx), - (error: grpc.ServiceError, response: server_v1_api_pb.ReplaceResponse): void => { - if (error !== undefined && error !== null) { - reject(error); - } else { - const clonedDocs = this.setDocsMetadata(docs, response.getKeysList_asU8()); - resolve(clonedDocs); - } - } - ); - }); - } - - /** - * Insert new or replace an existing document in collection. - * - * @param doc - Document to insert or replace - * @param tx - Optional session information for transaction context - */ - insertOrReplaceOne(doc: T, tx?: Session): Promise { - return new Promise((resolve, reject) => { - const docArr: Array = new Array(); - docArr.push(doc); - this.insertOrReplaceMany(docArr, tx) - .then((docs) => resolve(docs[0])) - .catch((error) => reject(error)); - }); - } - - /** - * Deletes documents in collection matching the filter - * - * @param filter - Query to match documents to delete - * @param tx - Optional session information for transaction context - * @param options - Optional settings for delete - */ - deleteMany( - filter: Filter, - tx?: Session, - options?: DeleteRequestOptions - ): Promise { - return new Promise((resolve, reject) => { - if (!filter) { - reject(new Error("No filter specified")); - } - const deleteRequest = new ProtoDeleteRequest() - .setProject(this.db) - .setCollection(this.collectionName) - .setFilter(Utility.stringToUint8Array(Utility.filterToString(filter))); - - if (options !== undefined) { - deleteRequest.setOptions(Utility._deleteRequestOptionsToProtoDeleteRequestOptions(options)); - } - - this.grpcClient.delete(deleteRequest, Utility.txToMetadata(tx), (error, response) => { - if (error) { - reject(error); - } else { - const metadata: DMLMetadata = new DMLMetadata( - response.getMetadata().getCreatedAt(), - response.getMetadata().getUpdatedAt() - ); - resolve(new DeleteResponse(response.getStatus(), metadata)); - } - }); - }); - } - - /** - * Deletes a single document in collection matching the filter - * - * @param filter - Query to match documents to delete - * @param tx - Optional session information for transaction context - * @param options - Optional settings for delete - */ - deleteOne( - filter: Filter, - tx?: Session, - options?: DeleteRequestOptions - ): Promise { - if (options === undefined) { - options = new DeleteRequestOptions(1); - } else { - options.limit = 1; - } - - return this.deleteMany(filter, tx, options); - } - - /** - * Update multiple documents in collection - * - * @param filter - Query to match documents to apply update - * @param fields - Document fields to update and update operation - * @param tx - Optional session information for transaction context - * @param options - Optional settings for search - */ - updateMany( - filter: Filter, - fields: UpdateFields | SimpleUpdateField, - tx?: Session, - options?: UpdateRequestOptions - ): Promise { - return new Promise((resolve, reject) => { - const updateRequest = new ProtoUpdateRequest() - .setProject(this.db) - .setCollection(this.collectionName) - .setFilter(Utility.stringToUint8Array(Utility.filterToString(filter))) - .setFields(Utility.stringToUint8Array(Utility.updateFieldsString(fields))); - - if (options !== undefined) { - updateRequest.setOptions(Utility._updateRequestOptionsToProtoUpdateRequestOptions(options)); - } - - this.grpcClient.update(updateRequest, Utility.txToMetadata(tx), (error, response) => { - if (error) { - reject(error); - } else { - const metadata: DMLMetadata = new DMLMetadata( - response.getMetadata().getCreatedAt(), - response.getMetadata().getUpdatedAt() - ); - resolve(new UpdateResponse(response.getStatus(), response.getModifiedCount(), metadata)); - } - }); - }); - } - - /** - * Updates a single document in collection - * - * @param filter - Query to match document to apply update - * @param fields - Document fields to update and update operation - * @param tx - Optional session information for transaction context - * @param options - Optional settings for search - */ - updateOne( - filter: Filter, - fields: UpdateFields | SimpleUpdateField, - tx?: Session, - options?: UpdateRequestOptions - ): Promise { - if (options === undefined) { - options = new UpdateRequestOptions(1); - } else { - options.limit = 1; - } - return this.updateMany(filter, fields, tx, options); - } } diff --git a/src/error.ts b/src/error.ts index 9222712..2fe419e 100644 --- a/src/error.ts +++ b/src/error.ts @@ -43,6 +43,16 @@ export class ReflectionNotEnabled extends TigrisError { } } +export class MissingArgumentError extends TigrisError { + constructor(propertyName: string) { + super(`'${propertyName}' is required and cannot be 'undefined'`); + } + + override get name(): string { + return "MissingArgumentError"; + } +} + export class CannotInferFieldTypeError extends TigrisError { constructor(object: Object, propertyName: string) { super(`Field type for '${object.constructor.name}#${propertyName}' cannot be determined`); diff --git a/src/search/types.ts b/src/search/types.ts index ee04c13..e26ecb7 100644 --- a/src/search/types.ts +++ b/src/search/types.ts @@ -15,11 +15,11 @@ import { TigrisClientConfig } from "../tigris"; export const MATCH_ALL_QUERY_STRING = ""; /** - * Search request params + * Search query builder */ -export type SearchRequest = { +export interface SearchQuery { /** - * Text to query + * Text to match */ q: string; /** @@ -50,17 +50,22 @@ export type SearchRequest = { * Maximum number of search hits (matched documents) to fetch per page */ hitsPerPage?: number; -}; + + /** + * Other parameters for search query + */ + options?: SearchQueryOptions; +} /** - * Options for search request + * Options for search query */ -export type SearchRequestOptions = { +export interface SearchQueryOptions { /** - * Allows case-insensitive filtering + * String comparison rules for filtering. E.g. - Case insensitive text match */ collation?: Collation; -}; +} export type FacetFieldsQuery = FacetFieldOptions | FacetFields; diff --git a/src/types.ts b/src/types.ts index 40a75e8..3f7682e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -185,7 +185,7 @@ export class UpdateResponse extends DMLResponse { export class WriteOptions {} -export class DeleteRequestOptions { +export class DeleteQueryOptions { private _collation: Collation; private _limit: number; @@ -211,7 +211,7 @@ export class DeleteRequestOptions { } } -export class UpdateRequestOptions { +export class UpdateQueryOptions { private _collation: Collation; private _limit: number; @@ -237,7 +237,7 @@ export class UpdateRequestOptions { } } -export class ReadRequestOptions { +export class FindQueryOptions { static DEFAULT_LIMIT = 100; static DEFAULT_SKIP = 0; @@ -250,8 +250,8 @@ export class ReadRequestOptions { constructor(limit: number, skip: number); constructor(limit?: number, skip?: number, offset?: string); constructor(limit?: number, skip?: number, offset?: string, collation?: Collation) { - this._limit = limit ?? ReadRequestOptions.DEFAULT_LIMIT; - this._skip = skip ?? ReadRequestOptions.DEFAULT_SKIP; + this._limit = limit ?? FindQueryOptions.DEFAULT_LIMIT; + this._skip = skip ?? FindQueryOptions.DEFAULT_SKIP; this._offset = offset; this._collation = collation; } @@ -364,6 +364,10 @@ export type SimpleUpdateField = { [key: string]: FieldTypes | undefined; }; +/** + * Query builder for reading documents from a collection + * @public + */ export interface FindQuery { /** * Filter to match the documents. Query will match all documents without a filter. @@ -378,7 +382,44 @@ export interface FindQuery { /** * Optional params */ - options?: ReadRequestOptions; + options?: FindQueryOptions; +} + +/** + * Query builder for deleting documents from a collection + * @public + */ +export interface DeleteQuery { + /** + * Filter to match the documents + */ + filter: Filter; + + /** + * Optional params + */ + options?: DeleteQueryOptions; +} + +/** + * Query builder for updating documents in a collection + * @public + */ +export interface UpdateQuery { + /** + * Filter to match the documents + */ + filter: Filter; + + /** + * Document fields to update and the update operation + */ + fields: UpdateFields | SimpleUpdateField; + + /** + * Optional params + */ + options?: UpdateQueryOptions; } export enum TigrisDataTypes { diff --git a/src/utility.ts b/src/utility.ts index c91602f..f2eceb7 100644 --- a/src/utility.ts +++ b/src/utility.ts @@ -3,11 +3,11 @@ import json_bigint from "json-bigint"; import { Session } from "./session"; import { - DeleteRequestOptions, + DeleteQueryOptions, LogicalFilter, LogicalOperator, ReadFields, - ReadRequestOptions, + FindQueryOptions, Selector, SelectorFilter, SelectorFilterOperator, @@ -17,7 +17,7 @@ import { TigrisSchema, UpdateFields, UpdateFieldsOperator, - UpdateRequestOptions, + UpdateQueryOptions, } from "./types"; import * as fs from "node:fs"; import { @@ -26,8 +26,7 @@ import { FacetQueryOptions, MATCH_ALL_QUERY_STRING, Ordering, - SearchRequest, - SearchRequestOptions, + SearchQuery, } from "./search/types"; import { Collation as ProtoCollation, @@ -335,7 +334,7 @@ export const Utility = { } return properties; }, - _readRequestOptionsToProtoReadRequestOptions(input: ReadRequestOptions): ProtoReadRequestOptions { + _readRequestOptionsToProtoReadRequestOptions(input: FindQueryOptions): ProtoReadRequestOptions { const result: ProtoReadRequestOptions = new ProtoReadRequestOptions(); if (input !== undefined) { if (input.skip !== undefined) { @@ -357,7 +356,7 @@ export const Utility = { return result; }, _deleteRequestOptionsToProtoDeleteRequestOptions( - input: DeleteRequestOptions + input: DeleteQueryOptions ): ProtoDeleteRequestOptions { const result: ProtoDeleteRequestOptions = new ProtoDeleteRequestOptions(); if (input !== undefined) { @@ -371,7 +370,7 @@ export const Utility = { return result; }, _updateRequestOptionsToProtoUpdateRequestOptions( - input: UpdateRequestOptions + input: UpdateQueryOptions ): ProtoUpdateRequestOptions { const result: ProtoUpdateRequestOptions = new ProtoUpdateRequestOptions(); if (input !== undefined) { @@ -499,47 +498,44 @@ export const Utility = { createProtoSearchRequest( dbName: string, collectionName: string, - request: SearchRequest, - options?: SearchRequestOptions, + query: SearchQuery, page?: number ): ProtoSearchRequest { const searchRequest = new ProtoSearchRequest() .setProject(dbName) .setCollection(collectionName) - .setQ(request.q ?? MATCH_ALL_QUERY_STRING); + .setQ(query.q ?? MATCH_ALL_QUERY_STRING); - if (request.searchFields !== undefined) { - searchRequest.setSearchFieldsList(request.searchFields); + if (query.searchFields !== undefined) { + searchRequest.setSearchFieldsList(query.searchFields); } - if (request.filter !== undefined) { - searchRequest.setFilter(Utility.stringToUint8Array(Utility.filterToString(request.filter))); + if (query.filter !== undefined) { + searchRequest.setFilter(Utility.stringToUint8Array(Utility.filterToString(query.filter))); } - if (request.facets !== undefined) { - searchRequest.setFacet( - Utility.stringToUint8Array(Utility.facetQueryToString(request.facets)) - ); + if (query.facets !== undefined) { + searchRequest.setFacet(Utility.stringToUint8Array(Utility.facetQueryToString(query.facets))); } - if (request.sort !== undefined) { - searchRequest.setSort(Utility.stringToUint8Array(Utility.sortOrderingToString(request.sort))); + if (query.sort !== undefined) { + searchRequest.setSort(Utility.stringToUint8Array(Utility.sortOrderingToString(query.sort))); } - if (request.includeFields !== undefined) { - searchRequest.setIncludeFieldsList(request.includeFields); + if (query.includeFields !== undefined) { + searchRequest.setIncludeFieldsList(query.includeFields); } - if (request.excludeFields !== undefined) { - searchRequest.setExcludeFieldsList(request.excludeFields); + if (query.excludeFields !== undefined) { + searchRequest.setExcludeFieldsList(query.excludeFields); } - if (request.hitsPerPage !== undefined) { - searchRequest.setPageSize(request.hitsPerPage); + if (query.hitsPerPage !== undefined) { + searchRequest.setPageSize(query.hitsPerPage); } - if (options !== undefined && options.collation !== undefined) { - searchRequest.setCollation(new ProtoCollation().setCase(options.collation.case)); + if (query.options?.collation !== undefined) { + searchRequest.setCollation(new ProtoCollation().setCase(query.options.collation.case)); } if (page !== undefined) { From 83d2d4a1541cb2d351bf739608162e304449bdf2 Mon Sep 17 00:00:00 2001 From: Jigar Joshi Date: Thu, 5 Jan 2023 15:44:30 -0800 Subject: [PATCH 08/12] feat: Added Caches API (#200) --- api/proto | 2 +- src/__tests__/test-cache-service.ts | 141 ++++++++++++++++++++++++++++ src/__tests__/tigris.rpc.spec.ts | 109 +++++++++++++++++++++ src/cache.ts | 139 +++++++++++++++++++++++++++ src/tigris.ts | 82 +++++++++++++++- src/types.ts | 87 +++++++++++++++++ src/utility.ts | 4 + 7 files changed, 562 insertions(+), 2 deletions(-) create mode 100644 src/__tests__/test-cache-service.ts create mode 100644 src/cache.ts diff --git a/api/proto b/api/proto index 9aac840..7e6f0f9 160000 --- a/api/proto +++ b/api/proto @@ -1 +1 @@ -Subproject commit 9aac840b172800bc528ad12ee1c634d53b4b14ae +Subproject commit 7e6f0f97b377780929e04061ace055755a762a15 diff --git a/src/__tests__/test-cache-service.ts b/src/__tests__/test-cache-service.ts new file mode 100644 index 0000000..9dbf24e --- /dev/null +++ b/src/__tests__/test-cache-service.ts @@ -0,0 +1,141 @@ +import { CacheService, ICacheServer } from "../proto/server/v1/cache_grpc_pb"; +import { sendUnaryData, ServerUnaryCall } from "@grpc/grpc-js"; +import { + CacheMetadata, + CreateCacheRequest, + CreateCacheResponse, + DeleteCacheRequest, + DeleteCacheResponse, + DelResponse, + GetRequest, + GetResponse, + KeysRequest, + KeysResponse, + ListCachesRequest, + ListCachesResponse, + SetRequest, + SetResponse, +} from "../proto/server/v1/cache_pb"; +import { DelRequest } from "../../dist/proto/server/v1/cache_pb"; +import { Utility } from "../utility"; + +export class TestCacheService { + private static CACHE_MAP = new Map>(); + + static reset() { + TestCacheService.CACHE_MAP.clear(); + } + public impl: ICacheServer = { + createCache( + call: ServerUnaryCall, + callback: sendUnaryData + ): void { + const cacheName = call.request.getProject() + "_" + call.request.getName(); + if (TestCacheService.CACHE_MAP.has(cacheName)) { + callback(new Error(), undefined); + } else { + TestCacheService.CACHE_MAP.set(cacheName, new Map()); + callback( + undefined, + new CreateCacheResponse().setStatus("created").setMessage("Cache created successfully") + ); + } + }, + del( + call: ServerUnaryCall, + callback: sendUnaryData + ): void { + const cacheName = call.request.getProject() + "_" + call.request.getName(); + if (TestCacheService.CACHE_MAP.has(cacheName)) { + if (TestCacheService.CACHE_MAP.get(cacheName).has(call.request.getKey())) { + TestCacheService.CACHE_MAP.get(cacheName).delete(call.request.getKey()); + callback( + undefined, + new DelResponse().setStatus("deleted").setMessage("Deleted key count# 1") + ); + } + } else { + callback(new Error("cache does not exist"), undefined); + } + }, + deleteCache( + call: ServerUnaryCall, + callback: sendUnaryData + ): void { + const cacheName = call.request.getProject() + "_" + call.request.getName(); + if (TestCacheService.CACHE_MAP.has(cacheName)) { + TestCacheService.CACHE_MAP.delete(cacheName); + callback( + undefined, + new DeleteCacheResponse().setStatus("deleted").setMessage("Deleted cache") + ); + } else { + callback(new Error("cache does not exist"), undefined); + } + }, + get( + call: ServerUnaryCall, + callback: sendUnaryData + ): void { + const cacheName = call.request.getProject() + "_" + call.request.getName(); + if (TestCacheService.CACHE_MAP.has(cacheName)) { + if (TestCacheService.CACHE_MAP.get(cacheName).has(call.request.getKey())) { + const value = TestCacheService.CACHE_MAP.get(cacheName).get(call.request.getKey()); + callback(undefined, new GetResponse().setValue(value)); + } else { + callback(new Error("cache key does not exist"), undefined); + } + } else { + callback(new Error("cache does not exist"), undefined); + } + }, + keys( + call: ServerUnaryCall, + callback: sendUnaryData + ): void { + const cacheName = call.request.getProject() + "_" + call.request.getName(); + if (TestCacheService.CACHE_MAP.has(cacheName)) { + const result: Array = new Array(); + for (let key of TestCacheService.CACHE_MAP.get(cacheName).keys()) { + result.push(key); + } + callback(undefined, new KeysResponse().setKeysList(result)); + } else { + callback(new Error("cache does not exist"), undefined); + } + }, + listCaches( + call: ServerUnaryCall, + callback: sendUnaryData + ): void { + const result: Array = new Array(); + for (let key of TestCacheService.CACHE_MAP.keys()) { + if (key.startsWith(call.request.getProject())) + result.push( + new CacheMetadata().setName(key.replace(call.request.getProject() + "_", "")) + ); + } + callback(undefined, new ListCachesResponse().setCachesList(result)); + }, + set( + call: ServerUnaryCall, + callback: sendUnaryData + ): void { + const cacheName = call.request.getProject() + "_" + call.request.getName(); + if (TestCacheService.CACHE_MAP.has(cacheName)) { + TestCacheService.CACHE_MAP.get(cacheName).set( + call.request.getKey(), + call.request.getValue_asB64() + ); + callback(undefined, new SetResponse().setStatus("set").setMessage("set" + " successfully")); + } else { + callback(new Error("cache does not exist"), undefined); + } + }, + }; +} + +export default { + service: CacheService, + handler: new TestCacheService(), +}; diff --git a/src/__tests__/tigris.rpc.spec.ts b/src/__tests__/tigris.rpc.spec.ts index 037e7c8..49824cf 100644 --- a/src/__tests__/tigris.rpc.spec.ts +++ b/src/__tests__/tigris.rpc.spec.ts @@ -1,6 +1,8 @@ import { Server, ServerCredentials } from "@grpc/grpc-js"; import { TigrisService } from "../proto/server/v1/api_grpc_pb"; import TestService, { TestTigrisService } from "./test-service"; +import TestServiceCache, { TestCacheService } from "./test-cache-service"; + import { DeleteQueryOptions, LogicalOperator, @@ -21,6 +23,7 @@ import { TigrisCollection } from "../decorators/tigris-collection"; import { PrimaryKey } from "../decorators/tigris-primary-key"; import { Field } from "../decorators/tigris-field"; import { SearchIterator } from "../consumables/search-iterator"; +import { CacheService } from "../proto/server/v1/cache_grpc_pb"; describe("rpc tests", () => { let server: Server; @@ -30,6 +33,7 @@ describe("rpc tests", () => { server = new Server(); TestTigrisService.reset(); server.addService(TigrisService, TestService.handler.impl); + server.addService(CacheService, TestServiceCache.handler.impl); server.addService(ObservabilityService, TestObservabilityService.handler.impl); server.bindAsync( "localhost:" + SERVER_PORT, @@ -48,6 +52,7 @@ describe("rpc tests", () => { beforeEach(() => { TestTigrisService.reset(); + TestCacheService.reset(); }); afterAll((done) => { @@ -679,6 +684,110 @@ describe("rpc tests", () => { }); return serverMetadataPromise; }); + + it("createCache", () => { + const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "db3" }); + const cacheC1Promise = tigris.createCacheIfNotExists("c1"); + cacheC1Promise.then((value) => { + expect(value.getCacheName()).toBe("c1"); + }); + return cacheC1Promise; + }); + + it("listCaches", async () => { + const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "db3" }); + for (let i = 0; i < 5; i++) { + await tigris.createCacheIfNotExists("c" + i); + } + const listCachesResponse = await tigris.listCaches(); + for (let i = 0; i < 5; i++) { + let found = false; + for (let cache of listCachesResponse.caches) { + if (cache.name === "c" + i) { + if (found) { + throw new Error("already found " + cache.name); + } + found = true; + break; + } + } + expect(found).toBe(true); + } + }); + + it("deleteCache", async () => { + const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "db3" }); + for (let i = 0; i < 5; i++) { + await tigris.createCacheIfNotExists("c" + i); + } + let listCachesResponse = await tigris.listCaches(); + expect(listCachesResponse.caches.length).toBe(5); + + const deleteResponse = await tigris.deleteCache("c3"); + expect(deleteResponse.status).toBe("deleted"); + + listCachesResponse = await tigris.listCaches(); + expect(listCachesResponse.caches.length).toBe(4); + + await tigris.deleteCache("c2"); + listCachesResponse = await tigris.listCaches(); + expect(listCachesResponse.caches.length).toBe(3); + + // deleting non-existing cache + let errored = false; + try { + await tigris.deleteCache("c3"); + } catch (error) { + errored = true; + } + expect(errored).toBe(true); + + listCachesResponse = await tigris.listCaches(); + expect(listCachesResponse.caches.length).toBe(3); + }); + + it("cacheCrud", async () => { + const tigris = new Tigris({ serverUrl: "localhost:" + SERVER_PORT, projectName: "db3" }); + const c1 = await tigris.createCacheIfNotExists("c1"); + + await c1.set("k1", "val1"); + expect((await c1.get("k1")).value).toBe("val1"); + + await c1.set("k1", "val1-new"); + expect((await c1.get("k1")).value).toBe("val1-new"); + + await c1.set("k2", 123); + expect((await c1.get("k2")).value).toBe(123); + + await c1.set("k3", true); + expect((await c1.get("k3")).value).toBe(true); + + await c1.set("k4", { a: "b", n: 12 }); + expect((await c1.get("k4")).value).toEqual({ a: "b", n: 12 }); + + const keys = await c1.keys(); + expect(keys).toContain("k1"); + expect(keys).toContain("k2"); + expect(keys).toContain("k3"); + expect(keys).toContain("k4"); + expect(keys).toHaveLength(4); + + await c1.del("k1"); + let errored = false; + try { + await c1.get("k1"); + } catch (error) { + errored = true; + } + expect(errored).toBe(true); + + // k1 is deleted + const keysNew = await c1.keys(); + expect(keysNew).toContain("k2"); + expect(keysNew).toContain("k3"); + expect(keysNew).toContain("k4"); + expect(keysNew).toHaveLength(3); + }); }); @TigrisCollection("books") diff --git a/src/cache.ts b/src/cache.ts new file mode 100644 index 0000000..3f359f5 --- /dev/null +++ b/src/cache.ts @@ -0,0 +1,139 @@ +import { CacheDelResponse, CacheGetResponse, CacheSetOptions, CacheSetResponse } from "./types"; +import { CacheClient } from "./proto/server/v1/cache_grpc_pb"; +import { + DelRequest as ProtoDelRequest, + GetRequest as ProtoGetRequest, + KeysRequest as ProtoKeysRequest, + SetRequest as ProtoSetRequest, +} from "./proto/server/v1/cache_pb"; +import { Utility } from "./utility"; +import { TigrisClientConfig } from "./tigris"; + +export class Cache { + private readonly _projectName: string; + private readonly _cacheName: string; + private readonly _cacheClient: CacheClient; + private readonly _config: TigrisClientConfig; + + constructor( + projectName: string, + cacheName: string, + cacheClient: CacheClient, + config: TigrisClientConfig + ) { + this._projectName = projectName; + this._cacheName = cacheName; + this._cacheClient = cacheClient; + this._config = config; + } + + public getCacheName(): string { + return this._cacheName; + } + + /** + * Sets the key with value. It will override the value if already exists + * @param key + * @param value + * @param options - optionally set params. + */ + public set( + key: string, + value: string | number | boolean | object, + options?: CacheSetOptions + ): Promise { + return new Promise((resolve, reject) => { + const req = new ProtoSetRequest() + .setProject(this._projectName) + .setName(this._cacheName) + .setKey(key) + .setValue(new TextEncoder().encode(Utility.objToJsonString(value as object))); + + if (options !== undefined && options.ex !== undefined) { + req.setEx(options.ex); + } + if (options !== undefined && options.px !== undefined) { + req.setPx(options.px); + } + if (options !== undefined && options.nx !== undefined) { + req.setNx(options.nx); + } + if (options !== undefined && options.xx !== undefined) { + req.setXx(options.xx); + } + if (options !== undefined && options.get !== undefined) { + req.setGet(options.get); + } + + this._cacheClient.set(req, (error, response) => { + if (error) { + reject(error); + } else { + resolve(new CacheSetResponse(response.getStatus(), response.getMessage())); + } + }); + }); + } + + /** + * get the value for the key, errors if the key doesn't exist or expired + * @param key + */ + public get(key: string): Promise { + return new Promise((resolve, reject) => { + this._cacheClient.get( + new ProtoGetRequest().setProject(this._projectName).setName(this._cacheName).setKey(key), + (error, response) => { + if (error) { + reject(error); + } else { + resolve( + new CacheGetResponse( + Utility._base64DecodeToObject(response.getValue_asB64(), this._config) + ) + ); + } + } + ); + }); + } + + /** + * deletes the key + * @param key + */ + public del(key: string): Promise { + return new Promise((resolve, reject) => { + this._cacheClient.del( + new ProtoDelRequest().setProject(this._projectName).setName(this._cacheName).setKey(key), + (error, response) => { + if (error) { + reject(error); + } else { + resolve(new CacheDelResponse(response.getStatus(), response.getMessage())); + } + } + ); + }); + } + + /** + * returns an array of keys, complying the pattern + * @param pattern - optional argument to filter keys + */ + public keys(pattern?: string): Promise { + return new Promise((resolve, reject) => { + const req = new ProtoKeysRequest().setProject(this._projectName).setName(this._cacheName); + if (pattern !== undefined) { + req.setPattern(pattern); + } + this._cacheClient.keys(req, (error, response) => { + if (error) { + reject(error); + } else { + resolve(response.getKeysList()); + } + }); + }); + } +} diff --git a/src/tigris.ts b/src/tigris.ts index 60850f8..cc8fb77 100644 --- a/src/tigris.ts +++ b/src/tigris.ts @@ -7,7 +7,13 @@ import { GetInfoRequest as ProtoGetInfoRequest } from "./proto/server/v1/observa import { HealthCheckInput as ProtoHealthCheckInput } from "./proto/server/v1/health_pb"; import * as dotenv from "dotenv"; -import { ServerMetadata, TigrisCollectionType } from "./types"; +import { + DeleteCacheResponse, + ListCachesResponse, + ServerMetadata, + TigrisCollectionType, + CacheMetadata, +} from "./types"; import { GetAccessTokenRequest as ProtoGetAccessTokenRequest, @@ -20,6 +26,13 @@ import { Utility } from "./utility"; import { Log } from "./utils/logger"; import { DecoratorMetaStorage } from "./decorators/metadata/decorator-meta-storage"; import { getDecoratorMetaStorage } from "./globals"; +import { Cache } from "./cache"; +import { CacheClient } from "./proto/server/v1/cache_grpc_pb"; +import { CreateCacheRequest as ProtoCreateCacheRequest } from "./proto/server/v1/cache_pb"; +import { DeleteCacheRequest as ProtoDeleteCacheRequest } from "./proto/server/v1/cache_pb"; +import { ListCachesRequest as ProtoListCachesRequest } from "./proto/server/v1/cache_pb"; + +import { Status } from "@grpc/grpc-js/build/src/constants"; const AuthorizationHeaderName = "authorization"; const AuthorizationBearer = "Bearer "; @@ -124,6 +137,7 @@ const DEST_NAME_KEY = "destination-name"; export class Tigris { private readonly grpcClient: TigrisClient; private readonly observabilityClient: ObservabilityClient; + private readonly cacheClient: CacheClient; private readonly healthAPIClient: HealthAPIClient; private readonly _config: TigrisClientConfig; private readonly _metadataStorage: DecoratorMetaStorage; @@ -192,6 +206,7 @@ export class Tigris { const insecureCreds: ChannelCredentials = grpc.credentials.createInsecure(); this.grpcClient = new TigrisClient(config.serverUrl, insecureCreds); this.observabilityClient = new ObservabilityClient(config.serverUrl, insecureCreds); + this.cacheClient = new CacheClient(config.serverUrl, insecureCreds); this.healthAPIClient = new HealthAPIClient(config.serverUrl, insecureCreds); } else if (config.clientId === undefined || config.clientSecret === undefined) { throw new Error("Both `clientId` and `clientSecret` are required"); @@ -216,6 +231,7 @@ export class Tigris { ); this.grpcClient = new TigrisClient(config.serverUrl, channelCreds); this.observabilityClient = new ObservabilityClient(config.serverUrl, channelCreds); + this.cacheClient = new CacheClient(config.serverUrl, channelCreds); this.healthAPIClient = new HealthAPIClient(config.serverUrl, channelCreds); this._ping = () => { this.healthAPIClient.health(new ProtoHealthCheckInput(), (error, response) => { @@ -246,6 +262,70 @@ export class Tigris { return new DB(this._config.projectName, this.grpcClient, this._config); } + /** + * Creates the cache for this project, if the cache doesn't already exist + * @param cacheName + */ + public createCacheIfNotExists(cacheName: string): Promise { + return new Promise((resolve, reject) => { + this.cacheClient.createCache( + new ProtoCreateCacheRequest().setProject(this._config.projectName).setName(cacheName), + // eslint-disable-next-line @typescript-eslint/no-unused-vars + (error, response) => { + if (error && error.code != Status.ALREADY_EXISTS) { + reject(error); + } else { + resolve(new Cache(this._config.projectName, cacheName, this.cacheClient, this._config)); + } + } + ); + }); + } + + /** + * Deletes the entire cache from this project. + * @param cacheName + */ + public deleteCache(cacheName: string): Promise { + return new Promise((resolve, reject) => { + this.cacheClient.deleteCache( + new ProtoDeleteCacheRequest().setProject(this._config.projectName).setName(cacheName), + (error, response) => { + if (error) { + reject(error); + } else { + resolve(new DeleteCacheResponse(response.getStatus(), response.getMessage())); + } + } + ); + }); + } + + /** + * Lists all the caches for this project + */ + public listCaches(): Promise { + return new Promise((resolve, reject) => { + this.cacheClient.listCaches( + new ProtoListCachesRequest().setProject(this._config.projectName), + (error, response) => { + if (error) { + reject(error); + } else { + const cachesMetadata: CacheMetadata[] = new Array(); + for (const value of response.getCachesList()) + cachesMetadata.push(new CacheMetadata(value.getName())); + resolve(new ListCachesResponse(cachesMetadata)); + } + } + ); + }); + } + + public getCache(cacheName: string): Cache { + return new Cache(this._config.projectName, cacheName, this.cacheClient, this._config); + } + public getServerMetadata(): Promise { return new Promise((resolve, reject) => { this.observabilityClient.getInfo(new ProtoGetInfoRequest(), (error, response) => { diff --git a/src/types.ts b/src/types.ts index 3f7682e..cf9eb5c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -309,6 +309,93 @@ export class TransactionResponse extends TigrisResponse { } } +export class CacheMetadata { + private readonly _name: string; + + constructor(name: string) { + this._name = name; + } + + get name(): string { + return this._name; + } +} +export class ListCachesResponse { + private readonly _caches: CacheMetadata[]; + + constructor(caches: CacheMetadata[]) { + this._caches = caches; + } + + get caches(): CacheMetadata[] { + return this._caches; + } +} + +export class DeleteCacheResponse extends TigrisResponse { + private readonly _message: string; + + constructor(status: string, message: string) { + super(status); + this._message = message; + } + + get message(): string { + return this._message; + } +} + +export class CacheSetResponse extends TigrisResponse { + private readonly _message: string; + + constructor(status: string, message: string) { + super(status); + this._message = message; + } + + get message(): string { + return this._message; + } +} + +export class CacheDelResponse extends TigrisResponse { + private readonly _message: string; + + constructor(status: string, message: string) { + super(status); + this._message = message; + } + + get message(): string { + return this._message; + } +} + +export interface CacheSetOptions { + // optional ttl in seconds + ex: number; + // optional ttl in ms + px: number; + // only set if key doesn't exist + nx: boolean; + // only set if key exists + xx: boolean; + // get the old value as part of response + get: boolean; +} + +export class CacheGetResponse { + private readonly _value: object; + + constructor(value: object) { + this._value = value; + } + + get value(): object { + return this._value; + } +} + export class ServerMetadata { private readonly _serverVersion: string; diff --git a/src/utility.ts b/src/utility.ts index f2eceb7..846ed11 100644 --- a/src/utility.ts +++ b/src/utility.ts @@ -466,6 +466,10 @@ export const Utility = { return Buffer.from(b64String, "base64").toString("binary"); }, + _base64DecodeToObject(b64String: string, config: TigrisClientConfig): object { + return this.jsonStringToObj(Buffer.from(b64String, "base64").toString("binary"), config); + }, + createFacetQueryOptions(options?: Partial): FacetQueryOptions { const defaults = { size: 10, type: FacetQueryFieldType.VALUE }; return { ...defaults, ...options }; From 646fd3b6e0452018a2fda049fa0b2e9572d02d4b Mon Sep 17 00:00:00 2001 From: Garren Date: Fri, 6 Jan 2023 18:59:29 +0200 Subject: [PATCH 09/12] chore: Code refactoring in the collections (#201) We can leverage some of the new JS/TS functionality to refactor the internal code of the collection library. The main change is using async/await instead of promises. This is a lot easier to read. I've also used Optional Chaining instead of checking for undefined. iteratorResult?.value; // Will return undefined if the value doesn't exist otherwise value --- src/collection.ts | 59 ++++++++++++++++++----------------------------- 1 file changed, 22 insertions(+), 37 deletions(-) diff --git a/src/collection.ts b/src/collection.ts index 1d325d5..cb4186e 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -148,13 +148,9 @@ export class Collection implements ICollection { * @param doc - Document to insert or replace * @param tx - Session information for transaction context */ - insertOrReplaceOne(doc: T, tx?: Session): Promise { - return new Promise((resolve, reject) => { - const docArr: Array = [doc]; - this.insertOrReplaceMany(docArr, tx) - .then((docs) => resolve(docs[0])) - .catch((error) => reject(error)); - }); + async insertOrReplaceOne(doc: T, tx?: Session): Promise { + const docs = await this.insertOrReplaceMany([doc], tx); + return docs[0]; } /** @@ -600,41 +596,30 @@ export class Collection implements ICollection { */ findOne(query: FindQuery, tx: Session): Promise; - findOne(txOrQuery?: Session | FindQuery, tx?: Session): Promise { + async findOne(txOrQuery?: Session | FindQuery, tx?: Session): Promise { let query: FindQuery; - return new Promise((resolve, reject) => { - if (typeof txOrQuery !== "undefined") { - if (this.isTxSession(txOrQuery)) { - tx = txOrQuery as Session; - } else { - query = txOrQuery as FindQuery; - } + if (typeof txOrQuery !== "undefined") { + if (this.isTxSession(txOrQuery)) { + tx = txOrQuery as Session; + } else { + query = txOrQuery as FindQuery; } + } - const findOnlyOne: FindQueryOptions = new FindQueryOptions(1); + const findOnlyOne: FindQueryOptions = new FindQueryOptions(1); - if (!query) { - query = { options: findOnlyOne }; - } else if (!query.options) { - query.options = findOnlyOne; - } else { - query.options.limit = findOnlyOne.limit; - } + if (!query) { + query = { options: findOnlyOne }; + } else if (!query.options) { + query.options = findOnlyOne; + } else { + query.options.limit = findOnlyOne.limit; + } - const cursor = this.findMany(query, tx); - const iteratorResult = cursor[Symbol.asyncIterator]().next(); - if (iteratorResult !== undefined) { - iteratorResult - .then( - (r) => resolve(r.value), - (error) => reject(error) - ) - .catch(reject); - } else { - /* eslint unicorn/no-useless-undefined: ["error", {"checkArguments": false}]*/ - resolve(undefined); - } - }); + const cursor = this.findMany(query, tx); + const iteratorResult = await cursor[Symbol.asyncIterator]().next(); + + return iteratorResult?.value; } /** From 789f4599071d6cffc30a1f37d47d41cfa71ff305 Mon Sep 17 00:00:00 2001 From: Jigar Joshi Date: Fri, 6 Jan 2023 11:10:38 -0800 Subject: [PATCH 10/12] feat: Implemented getSet method for cache (#202) --- api/proto | 2 +- src/__tests__/test-cache-service.ts | 26 +++++++++ src/__tests__/tigris.rpc.spec.ts | 15 ++++++ src/cache.ts | 81 +++++++++++++++++++++++++++-- src/types.ts | 25 ++++++--- 5 files changed, 138 insertions(+), 11 deletions(-) diff --git a/api/proto b/api/proto index 7e6f0f9..c5ced5b 160000 --- a/api/proto +++ b/api/proto @@ -1 +1 @@ -Subproject commit 7e6f0f97b377780929e04061ace055755a762a15 +Subproject commit c5ced5bfa1e4adecd9e4377190e2a5becdc4d168 diff --git a/src/__tests__/test-cache-service.ts b/src/__tests__/test-cache-service.ts index 9dbf24e..1f1e513 100644 --- a/src/__tests__/test-cache-service.ts +++ b/src/__tests__/test-cache-service.ts @@ -9,6 +9,8 @@ import { DelResponse, GetRequest, GetResponse, + GetSetRequest, + GetSetResponse, KeysRequest, KeysResponse, ListCachesRequest, @@ -132,6 +134,30 @@ export class TestCacheService { callback(new Error("cache does not exist"), undefined); } }, + getSet( + call: ServerUnaryCall, + callback: sendUnaryData + ): void { + const cacheName = call.request.getProject() + "_" + call.request.getName(); + if (TestCacheService.CACHE_MAP.has(cacheName)) { + let oldValue = undefined; + if (TestCacheService.CACHE_MAP.get(cacheName).has(call.request.getKey())) { + oldValue = TestCacheService.CACHE_MAP.get(cacheName).get(call.request.getKey()); + } + + TestCacheService.CACHE_MAP.get(cacheName).set( + call.request.getKey(), + call.request.getValue_asB64() + ); + const result = new GetSetResponse().setStatus("set").setMessage("set" + " successfully"); + if (oldValue !== undefined) { + result.setOldValue(oldValue); + } + callback(undefined, result); + } else { + callback(new Error("cache does not exist"), undefined); + } + }, }; } diff --git a/src/__tests__/tigris.rpc.spec.ts b/src/__tests__/tigris.rpc.spec.ts index 49824cf..a36f936 100644 --- a/src/__tests__/tigris.rpc.spec.ts +++ b/src/__tests__/tigris.rpc.spec.ts @@ -787,6 +787,21 @@ describe("rpc tests", () => { expect(keysNew).toContain("k3"); expect(keysNew).toContain("k4"); expect(keysNew).toHaveLength(3); + + // getset + let getSetResp = await c1.getSet("k2", 123_456); + expect(getSetResp.old_value).toBe(123); + + getSetResp = await c1.getSet("k2", 123_457); + expect(getSetResp.old_value).toBe(123_456); + + // getset for new key + try { + getSetResp = await c1.getSet("k6", "val6"); + expect(getSetResp.old_value).toBeUndefined(); + } catch (error) { + console.log(error); + } }); }); diff --git a/src/cache.ts b/src/cache.ts index 3f359f5..e75572f 100644 --- a/src/cache.ts +++ b/src/cache.ts @@ -1,10 +1,17 @@ -import { CacheDelResponse, CacheGetResponse, CacheSetOptions, CacheSetResponse } from "./types"; +import { + CacheDelResponse, + CacheGetResponse, + CacheGetSetResponse, + CacheSetOptions, + CacheSetResponse, +} from "./types"; import { CacheClient } from "./proto/server/v1/cache_grpc_pb"; import { DelRequest as ProtoDelRequest, GetRequest as ProtoGetRequest, KeysRequest as ProtoKeysRequest, SetRequest as ProtoSetRequest, + GetSetRequest as ProtoGetSetRequest, } from "./proto/server/v1/cache_pb"; import { Utility } from "./utility"; import { TigrisClientConfig } from "./tigris"; @@ -27,6 +34,9 @@ export class Cache { this._config = config; } + /** + * returns cache name + */ public getCacheName(): string { return this._cacheName; } @@ -36,6 +46,12 @@ export class Cache { * @param key * @param value * @param options - optionally set params. + * @example + * ``` + * const c1 = tigris.GetCache("c1); + * const setResp = await c1.set("k1", "v1"); + * console.log(setResp.status); + * ``` */ public set( key: string, @@ -61,9 +77,6 @@ export class Cache { if (options !== undefined && options.xx !== undefined) { req.setXx(options.xx); } - if (options !== undefined && options.get !== undefined) { - req.setGet(options.get); - } this._cacheClient.set(req, (error, response) => { if (error) { @@ -75,9 +88,57 @@ export class Cache { }); } + /** + * Sets the key with value. And returns the old value (if exists) + * @param key + * @param value + * @example + * ``` + * const c1 = tigris.GetCache("c1); + * const getSetResp = await c1.getSet("k1", "v1"); + * console.log(getSetResp.old_value); + * ``` + */ + public getSet( + key: string, + value: string | number | boolean | object + ): Promise { + return new Promise((resolve, reject) => { + const req = new ProtoGetSetRequest() + .setProject(this._projectName) + .setName(this._cacheName) + .setKey(key) + .setValue(new TextEncoder().encode(Utility.objToJsonString(value as object))); + + this._cacheClient.getSet(req, (error, response) => { + if (error) { + reject(error); + } else { + if (response.getOldValue() !== undefined && response.getOldValue_asU8().length > 0) { + resolve( + new CacheGetSetResponse( + response.getStatus(), + response.getMessage(), + Utility._base64DecodeToObject(response.getOldValue_asB64(), this._config) + ) + ); + } else { + resolve(new CacheGetSetResponse(response.getStatus(), response.getMessage())); + } + } + }); + }); + } + /** * get the value for the key, errors if the key doesn't exist or expired * @param key + * @example + * ``` + * const c1 = tigris.GetCache("c1); + * const getResp = await c1.get("k1"); + * console.log(getResp.value); + * ``` */ public get(key: string): Promise { return new Promise((resolve, reject) => { @@ -101,6 +162,12 @@ export class Cache { /** * deletes the key * @param key + * @example + * ``` + * const c1 = tigris.GetCache("c1); + * const delResp = await c1.del("k1"); + * console.log(delResp.status); + * ``` */ public del(key: string): Promise { return new Promise((resolve, reject) => { @@ -120,6 +187,12 @@ export class Cache { /** * returns an array of keys, complying the pattern * @param pattern - optional argument to filter keys + * @example + * ``` + * const c1 = tigris.GetCache("c1); + * const keys = await c1.keys(); + * console.log(keys); + * ``` */ public keys(pattern?: string): Promise { return new Promise((resolve, reject) => { diff --git a/src/types.ts b/src/types.ts index cf9eb5c..fc32d14 100644 --- a/src/types.ts +++ b/src/types.ts @@ -358,6 +358,21 @@ export class CacheSetResponse extends TigrisResponse { } } +export class CacheGetSetResponse extends CacheSetResponse { + private readonly _old_value: object; + + constructor(status: string, message: string, old_value?: object) { + super(status, message); + if (old_value !== undefined) { + this._old_value = old_value; + } + } + + get old_value(): object { + return this._old_value; + } +} + export class CacheDelResponse extends TigrisResponse { private readonly _message: string; @@ -373,15 +388,13 @@ export class CacheDelResponse extends TigrisResponse { export interface CacheSetOptions { // optional ttl in seconds - ex: number; + ex?: number; // optional ttl in ms - px: number; + px?: number; // only set if key doesn't exist - nx: boolean; + nx?: boolean; // only set if key exists - xx: boolean; - // get the old value as part of response - get: boolean; + xx?: boolean; } export class CacheGetResponse { From 88f124985611c1e4e7ff17357dd6964acb9a1c0d Mon Sep 17 00:00:00 2001 From: Adil Ansari Date: Mon, 9 Jan 2023 09:05:33 -0800 Subject: [PATCH 11/12] feat: `default` values for schema fields (#203) Defaults and maxLength for schema fields --- .prettierignore | 1 - jest.config.json | 3 +- package-lock.json | 12 +- src/__tests__/data/basicCollection.json | 41 -- .../data/basicCollectionWithObjectType.json | 21 - .../data/collectionWithObjectArrays.json | 37 -- .../data/collectionWithPrimitiveArrays.json | 26 - src/__tests__/data/multiLevelObjectArray.json | 129 ----- .../data/multiLevelPrimitiveArray.json | 74 --- src/__tests__/data/multiplePKeys.json | 42 -- src/__tests__/data/nestedCollection.json | 34 -- .../fixtures/json-schema/matrices.json | 44 ++ .../fixtures/json-schema/movies.json | 51 ++ .../fixtures/json-schema/orders.json | 48 ++ src/__tests__/fixtures/json-schema/users.json | 42 ++ .../fixtures/json-schema/vacationRentals.json | 116 +++++ .../schema}/matrices.ts | 40 +- .../schema}/movies.ts | 56 +-- .../schema}/orders.ts | 55 +- .../schema}/users.ts | 40 +- .../fixtures/schema/vacationRentals.ts | 196 ++++++++ .../schema/decorator-processor.spec.ts | 37 -- src/__tests__/tigris.schema.spec.ts | 468 ++++-------------- src/db.ts | 2 +- .../options/embedded-field-options.ts | 3 +- src/decorators/tigris-field.ts | 36 +- src/schema/decorated-schema-processor.ts | 6 +- src/tigris.ts | 2 +- src/types.ts | 69 ++- src/utility.ts | 58 +-- 30 files changed, 786 insertions(+), 1003 deletions(-) delete mode 100644 src/__tests__/data/basicCollection.json delete mode 100644 src/__tests__/data/basicCollectionWithObjectType.json delete mode 100644 src/__tests__/data/collectionWithObjectArrays.json delete mode 100644 src/__tests__/data/collectionWithPrimitiveArrays.json delete mode 100644 src/__tests__/data/multiLevelObjectArray.json delete mode 100644 src/__tests__/data/multiLevelPrimitiveArray.json delete mode 100644 src/__tests__/data/multiplePKeys.json delete mode 100644 src/__tests__/data/nestedCollection.json create mode 100644 src/__tests__/fixtures/json-schema/matrices.json create mode 100644 src/__tests__/fixtures/json-schema/movies.json create mode 100644 src/__tests__/fixtures/json-schema/orders.json create mode 100644 src/__tests__/fixtures/json-schema/users.json create mode 100644 src/__tests__/fixtures/json-schema/vacationRentals.json rename src/__tests__/{data/decoratedModels => fixtures/schema}/matrices.ts (77%) rename src/__tests__/{data/decoratedModels => fixtures/schema}/movies.ts (67%) rename src/__tests__/{data/decoratedModels => fixtures/schema}/orders.ts (70%) rename src/__tests__/{data/decoratedModels => fixtures/schema}/users.ts (75%) create mode 100644 src/__tests__/fixtures/schema/vacationRentals.ts delete mode 100644 src/__tests__/schema/decorator-processor.spec.ts diff --git a/.prettierignore b/.prettierignore index 029811c..8ab9dd1 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,6 +1,5 @@ # Ignore artifacts: dist/ -src/__tests__/data/ src/proto/ api/ coverage/ diff --git a/jest.config.json b/jest.config.json index 1deba5a..39daa37 100644 --- a/jest.config.json +++ b/jest.config.json @@ -9,10 +9,11 @@ "collectCoverage": true, "coverageDirectory": "coverage", "coverageProvider": "v8", + "coveragePathIgnorePatterns": ["/src/decorators/metadata/*.ts"], "collectCoverageFrom": [ "src/*.ts", "src/consumables/*.ts", - "src/decorators/**/*.ts", + "src/decorators/*.ts", "src/schema/**/*.ts", "src/search/**/*.ts", "src/utils/**/*.ts" diff --git a/package-lock.json b/package-lock.json index 4564262..e2aee1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6002,9 +6002,9 @@ "peer": true }, "node_modules/json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, "bin": { "json5": "lib/cli.js" @@ -15996,9 +15996,9 @@ "peer": true }, "json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true }, "jsonfile": { diff --git a/src/__tests__/data/basicCollection.json b/src/__tests__/data/basicCollection.json deleted file mode 100644 index 1ceafa6..0000000 --- a/src/__tests__/data/basicCollection.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "title": "basicCollection", - "additionalProperties": false, - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int32", - "autoGenerate": true - }, - "active": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "uuid": { - "type": "string", - "format": "uuid" - }, - "int32Number": { - "type": "integer", - "format": "int32" - }, - "int64Number": { - "type": "integer", - "format": "int64" - }, - "date": { - "type": "string", - "format": "date-time" - }, - "bytes": { - "type": "string", - "format": "byte" - } - }, - "primary_key": [ - "id" - ] -} diff --git a/src/__tests__/data/basicCollectionWithObjectType.json b/src/__tests__/data/basicCollectionWithObjectType.json deleted file mode 100644 index 5f59723..0000000 --- a/src/__tests__/data/basicCollectionWithObjectType.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "title": "basicCollectionWithObjectType", - "additionalProperties": false, - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "autoGenerate": true - }, - "name": { - "type": "string" - }, - "metadata": { - "type": "object" - } - }, - "primary_key": [ - "id" - ] -} diff --git a/src/__tests__/data/collectionWithObjectArrays.json b/src/__tests__/data/collectionWithObjectArrays.json deleted file mode 100644 index 973e7d0..0000000 --- a/src/__tests__/data/collectionWithObjectArrays.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "title": "collectionWithObjectArrays", - "additionalProperties": false, - "type": "object", - "properties": { - "id": { - "type": "number" - }, - "name": { - "type": "string" - }, - "knownAddresses": { - "type": "array", - "items": { - "type": "object", - "properties": { - "city": { - "type": "string" - }, - "state": { - "type": "string" - }, - "zipcode": { - "type": "number" - } - } - } - }, - "id": { - "type": "string", - "format": "uuid" - } - }, - "primary_key": [ - "id" - ] -} diff --git a/src/__tests__/data/collectionWithPrimitiveArrays.json b/src/__tests__/data/collectionWithPrimitiveArrays.json deleted file mode 100644 index 913944b..0000000 --- a/src/__tests__/data/collectionWithPrimitiveArrays.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "title": "collectionWithPrimitiveArrays", - "additionalProperties": false, - "type": "object", - "properties": { - "id": { - "type": "number" - }, - "name": { - "type": "string" - }, - "tags": { - "type": "array", - "items": { - "type": "string" - } - }, - "id": { - "type": "string", - "format": "uuid" - } - }, - "primary_key": [ - "id" - ] -} diff --git a/src/__tests__/data/multiLevelObjectArray.json b/src/__tests__/data/multiLevelObjectArray.json deleted file mode 100644 index 944ef91..0000000 --- a/src/__tests__/data/multiLevelObjectArray.json +++ /dev/null @@ -1,129 +0,0 @@ -{ - "title": "multiLevelObjectArray", - "additionalProperties": false, - "type": "object", - "properties": { - "oneDArray": { - "type": "array", - "items": { - "type": "object", - "properties": { - "city": { - "type": "string" - }, - "state": { - "type": "string" - }, - "zipcode": { - "type": "number" - } - } - } - }, - "twoDArray": { - "type": "array", - "items": { - "type": "array", - "items": { - "type": "object", - "properties": { - "city": { - "type": "string" - }, - "state": { - "type": "string" - }, - "zipcode": { - "type": "number" - } - } - } - } - }, - "threeDArray": { - "type": "array", - "items": { - "type": "array", - "items": { - "type": "array", - "items": { - "type": "object", - "properties": { - "city": { - "type": "string" - }, - "state": { - "type": "string" - }, - "zipcode": { - "type": "number" - } - } - } - } - } - }, - "fourDArray": { - "type": "array", - "items": { - "type": "array", - "items": { - "type": "array", - "items": { - "type": "array", - "items": { - "type": "object", - "properties": { - "city": { - "type": "string" - }, - "state": { - "type": "string" - }, - "zipcode": { - "type": "number" - } - } - } - } - } - } - }, - "fiveDArray": { - "type": "array", - "items": { - "type": "array", - "items": { - "type": "array", - "items": { - "type": "array", - "items": { - "type": "array", - "items": { - "type": "object", - "properties": { - "city": { - "type": "string" - }, - "state": { - "type": "string" - }, - "zipcode": { - "type": "number" - } - } - } - } - } - } - } - }, - "id": { - "type": "string", - "format": "uuid" - } - }, - "primary_key": [ - "id" - ] -} diff --git a/src/__tests__/data/multiLevelPrimitiveArray.json b/src/__tests__/data/multiLevelPrimitiveArray.json deleted file mode 100644 index 6b6d0fa..0000000 --- a/src/__tests__/data/multiLevelPrimitiveArray.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "title": "multiLevelPrimitiveArray", - "additionalProperties": false, - "type": "object", - "properties": { - "oneDArray": { - "type": "array", - "items": { - "type": "string" - } - }, - "twoDArray": { - "type": "array", - "items": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "threeDArray": { - "type": "array", - "items": { - "type": "array", - "items": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "fourDArray": { - "type": "array", - "items": { - "type": "array", - "items": { - "type": "array", - "items": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - }, - "fiveDArray": { - "type": "array", - "items": { - "type": "array", - "items": { - "type": "array", - "items": { - "type": "array", - "items": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - } - }, - "id": { - "type": "string", - "format": "uuid" - } - }, - "primary_key": [ - "id" - ] -} diff --git a/src/__tests__/data/multiplePKeys.json b/src/__tests__/data/multiplePKeys.json deleted file mode 100644 index a5406a6..0000000 --- a/src/__tests__/data/multiplePKeys.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "title": "multiplePKeys", - "additionalProperties": false, - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "active": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "uuid": { - "type": "string", - "format": "uuid", - "autoGenerate": true - }, - "int32Number": { - "type": "integer", - "format": "int32" - }, - "int64Number": { - "type": "integer", - "format": "int64" - }, - "date": { - "type": "string", - "format": "date-time" - }, - "bytes": { - "type": "string", - "format": "byte" - } - }, - "primary_key": [ - "uuid", - "id" - ] -} diff --git a/src/__tests__/data/nestedCollection.json b/src/__tests__/data/nestedCollection.json deleted file mode 100644 index a708d22..0000000 --- a/src/__tests__/data/nestedCollection.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "title": "nestedCollection", - "additionalProperties": false, - "type": "object", - "properties": { - "id": { - "type": "number" - }, - "name": { - "type": "string" - }, - "address": { - "type": "object", - "properties": { - "city": { - "type": "string" - }, - "state": { - "type": "string" - }, - "zipcode": { - "type": "number" - } - } - }, - "id": { - "type": "string", - "format": "uuid" - } - }, - "primary_key": [ - "id" - ] -} diff --git a/src/__tests__/fixtures/json-schema/matrices.json b/src/__tests__/fixtures/json-schema/matrices.json new file mode 100644 index 0000000..0417056 --- /dev/null +++ b/src/__tests__/fixtures/json-schema/matrices.json @@ -0,0 +1,44 @@ +{ + "title": "matrices", + "additionalProperties": false, + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "cells": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "x": { + "type": "number", + "default": 0 + }, + "y": { + "type": "number", + "default": 0 + }, + "value": { + "type": "object", + "properties": { + "length": { + "type": "number" + }, + "type": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "primary_key": ["id"] +} diff --git a/src/__tests__/fixtures/json-schema/movies.json b/src/__tests__/fixtures/json-schema/movies.json new file mode 100644 index 0000000..0503648 --- /dev/null +++ b/src/__tests__/fixtures/json-schema/movies.json @@ -0,0 +1,51 @@ +{ + "title": "movies", + "additionalProperties": false, + "type": "object", + "properties": { + "movieId": { + "type": "string" + }, + "title": { + "type": "string" + }, + "year": { + "type": "integer", + "format": "int32" + }, + "actors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "firstName": { + "type": "string", + "maxLength": 64 + }, + "lastName": { + "type": "string", + "maxLength": 64 + } + } + } + }, + "genres": { + "type": "array", + "items": { + "type": "string" + } + }, + "productionHouse": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "city": { + "type": "string" + } + } + } + }, + "primary_key": ["movieId"] +} diff --git a/src/__tests__/fixtures/json-schema/orders.json b/src/__tests__/fixtures/json-schema/orders.json new file mode 100644 index 0000000..2178be6 --- /dev/null +++ b/src/__tests__/fixtures/json-schema/orders.json @@ -0,0 +1,48 @@ +{ + "title": "orders", + "additionalProperties": false, + "type": "object", + "properties": { + "orderId": { + "type": "string", + "format": "uuid", + "autoGenerate": true + }, + "customerId": { + "type": "string" + }, + "products": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 64 + }, + "brand": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "upc": { + "type": "integer" + }, + "price": { + "type": "number" + } + } + } + } + }, + "primary_key": ["orderId", "customerId"] +} diff --git a/src/__tests__/fixtures/json-schema/users.json b/src/__tests__/fixtures/json-schema/users.json new file mode 100644 index 0000000..af62053 --- /dev/null +++ b/src/__tests__/fixtures/json-schema/users.json @@ -0,0 +1,42 @@ +{ + "title": "users", + "additionalProperties": false, + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "created": { + "type": "string", + "format": "date-time" + }, + "identities": { + "type": "array", + "items": { + "type": "object", + "properties": { + "connection": { + "type": "string", + "maxLength": 128 + }, + "isSocial": { + "type": "boolean" + }, + "provider": { + "type": "array", + "items": { + "type": "number" + } + }, + "linkedAccounts": { + "type": "number" + } + } + } + }, + "name": { + "type": "string" + } + }, + "primary_key": ["id"] +} diff --git a/src/__tests__/fixtures/json-schema/vacationRentals.json b/src/__tests__/fixtures/json-schema/vacationRentals.json new file mode 100644 index 0000000..1868ff3 --- /dev/null +++ b/src/__tests__/fixtures/json-schema/vacationRentals.json @@ -0,0 +1,116 @@ +{ + "title": "vacation_rentals", + "additionalProperties": false, + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid", + "autoGenerate": true + }, + "name": { + "type": "string", + "maxLength": 64 + }, + "description": { + "type": "string", + "maxLength": 256, + "default": "" + }, + "propertyType": { + "type": "string", + "default": "Home" + }, + "bedrooms": { + "type": "number", + "default": 0 + }, + "bathrooms": { + "type": "number", + "default": 0 + }, + "minimumNights": { + "type": "integer", + "format": "int32", + "default": 1 + }, + "isOwnerOccupied": { + "type": "boolean", + "default": false + }, + "hasWiFi": { + "type": "boolean", + "default": true + }, + "address": { + "type": "object", + "properties": { + "city": { + "type": "string" + }, + "countryCode": { + "type": "string", + "maxLength": 2, + "default": "US" + } + }, + "default": {} + }, + "verifications": { + "type": "object", + "default": { + "stateId": true + } + }, + "amenities": { + "type": "array", + "items": { + "type": "string" + }, + "default": ["Beds"] + }, + "attractions": { + "type": "array", + "items": { + "type": "string" + }, + "default": [] + }, + "host": { + "type": "object", + "default": null + }, + "reviews": { + "type": "array", + "items": { + "type": "object" + }, + "default": null + }, + "lastSeen": { + "type": "string", + "format": "date-time", + "default": "now()" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "default": "createdAt" + }, + "lastModified": { + "type": "string", + "format": "date-time", + "default": "updatedAt" + }, + "partnerId": { + "type": "string", + "default": "cuid()" + }, + "referralId": { + "type": "string", + "format": "uuid", + "default": "uuid()" + } + }, + "primary_key": ["id"] +} diff --git a/src/__tests__/data/decoratedModels/matrices.ts b/src/__tests__/fixtures/schema/matrices.ts similarity index 77% rename from src/__tests__/data/decoratedModels/matrices.ts rename to src/__tests__/fixtures/schema/matrices.ts index e424caa..6cd27b8 100644 --- a/src/__tests__/data/decoratedModels/matrices.ts +++ b/src/__tests__/fixtures/schema/matrices.ts @@ -10,31 +10,33 @@ import { PrimaryKey } from "../../../decorators/tigris-primary-key"; * - has a nested Array (Array of Arrays) * - infers the type of collection fields automatically using Reflection APIs *****************************************************************************/ +export const MATRICES_COLLECTION_NAME = "matrices"; + export class CellValue { @Field() - length: number + length: number; @Field() type: string; } export class Cell { - @Field() + @Field({ default: 0 }) x: number; - @Field() + @Field({ default: 0 }) y: number; @Field() value: CellValue; } -@TigrisCollection("matrices") -export class Matrix implements TigrisCollectionType { - @PrimaryKey({order: 1}) +@TigrisCollection(MATRICES_COLLECTION_NAME) +export class Matrix { + @PrimaryKey({ order: 1 }) id: string; - @Field({elements: Cell, depth: 3}) + @Field({ elements: Cell, depth: 3 }) cells: Array>>; } /********************************** END **************************************/ @@ -45,12 +47,12 @@ export class Matrix implements TigrisCollectionType { * NOTE: This is only an illustration; you don't have to write this definition, * it will be auto generated. */ -export const ExpectedSchema: TigrisSchema = { +export const MatrixSchema: TigrisSchema = { id: { type: TigrisDataTypes.STRING, primary_key: { order: 1, - autoGenerate: false + autoGenerate: false, }, }, cells: { @@ -63,23 +65,25 @@ export const ExpectedSchema: TigrisSchema = { type: { x: { type: TigrisDataTypes.NUMBER, + default: 0, }, y: { type: TigrisDataTypes.NUMBER, + default: 0, }, value: { type: { length: { - type: TigrisDataTypes.NUMBER + type: TigrisDataTypes.NUMBER, }, type: { - type: TigrisDataTypes.STRING - } + type: TigrisDataTypes.STRING, + }, }, }, - } - } - } - } - } -} + }, + }, + }, + }, + }, +}; diff --git a/src/__tests__/data/decoratedModels/movies.ts b/src/__tests__/fixtures/schema/movies.ts similarity index 67% rename from src/__tests__/data/decoratedModels/movies.ts rename to src/__tests__/fixtures/schema/movies.ts index db074ca..236fc1f 100644 --- a/src/__tests__/data/decoratedModels/movies.ts +++ b/src/__tests__/fixtures/schema/movies.ts @@ -1,6 +1,6 @@ import { TigrisCollection } from "../../../decorators/tigris-collection"; import { PrimaryKey } from "../../../decorators/tigris-primary-key"; -import { TigrisDataTypes, TigrisSchema } from "../../../types"; +import { TigrisCollectionType, TigrisDataTypes, TigrisSchema } from "../../../types"; import { Field } from "../../../decorators/tigris-field"; /****************************************************************************** @@ -11,6 +11,7 @@ import { Field } from "../../../decorators/tigris-field"; * - has an Object of type `Studio` * - does not use reflection, all the collection fields are explicitly typed *****************************************************************************/ +export const MOVIES_COLLECTION_NAME = "movies"; export class Studio { @Field(TigrisDataTypes.STRING) @@ -21,17 +22,16 @@ export class Studio { } export class Actor { - @Field(TigrisDataTypes.STRING, {maxLength: 64}) + @Field(TigrisDataTypes.STRING, { maxLength: 64 }) firstName: string; - @Field(TigrisDataTypes.STRING, {maxLength: 64}) + @Field(TigrisDataTypes.STRING, { maxLength: 64 }) lastName: string; } -@TigrisCollection("movies") -export class Movie{ - - @PrimaryKey(TigrisDataTypes.STRING, {autoGenerate: true, order: 1}) +@TigrisCollection(MOVIES_COLLECTION_NAME) +export class Movie { + @PrimaryKey(TigrisDataTypes.STRING, { order: 1 }) movieId: string; @Field(TigrisDataTypes.STRING) @@ -40,13 +40,13 @@ export class Movie{ @Field(TigrisDataTypes.INT32) year: number; - @Field(TigrisDataTypes.ARRAY, {elements: Actor}) + @Field(TigrisDataTypes.ARRAY, { elements: Actor }) actors: Array; - @Field(TigrisDataTypes.ARRAY, {elements: TigrisDataTypes.STRING}) + @Field(TigrisDataTypes.ARRAY, { elements: TigrisDataTypes.STRING }) genres: Array; - @Field(TigrisDataTypes.OBJECT, {elements: Studio}) + @Field(TigrisDataTypes.OBJECT, { elements: Studio }) productionHouse: Studio; } @@ -58,19 +58,19 @@ export class Movie{ * NOTE: This is only an illustration; you don't have to write this definition, * it will be auto generated. */ -export const ExpectedSchema: TigrisSchema = { +export const MovieSchema: TigrisSchema = { movieId: { type: TigrisDataTypes.STRING, primary_key: { order: 1, - autoGenerate: true - } + autoGenerate: false, + }, }, title: { - type: TigrisDataTypes.STRING + type: TigrisDataTypes.STRING, }, year: { - type: TigrisDataTypes.INT32 + type: TigrisDataTypes.INT32, }, actors: { type: TigrisDataTypes.ARRAY, @@ -78,29 +78,29 @@ export const ExpectedSchema: TigrisSchema = { type: { firstName: { type: TigrisDataTypes.STRING, - maxLength: 64 + maxLength: 64, }, lastName: { type: TigrisDataTypes.STRING, - maxLength: 64 - } - } - } + maxLength: 64, + }, + }, + }, }, genres: { type: TigrisDataTypes.ARRAY, items: { - type: TigrisDataTypes.STRING - } + type: TigrisDataTypes.STRING, + }, }, productionHouse: { type: { name: { - type: TigrisDataTypes.STRING + type: TigrisDataTypes.STRING, }, city: { - type: TigrisDataTypes.STRING - } - } - } -} + type: TigrisDataTypes.STRING, + }, + }, + }, +}; diff --git a/src/__tests__/data/decoratedModels/orders.ts b/src/__tests__/fixtures/schema/orders.ts similarity index 70% rename from src/__tests__/data/decoratedModels/orders.ts rename to src/__tests__/fixtures/schema/orders.ts index 31f0728..b38e00d 100644 --- a/src/__tests__/data/decoratedModels/orders.ts +++ b/src/__tests__/fixtures/schema/orders.ts @@ -12,16 +12,18 @@ import { Field } from "../../../decorators/tigris-field"; * - has an Array of embedded objects * - and infers the type of collection fields automatically using Reflection APIs *****************************************************************************/ +export const ORDERS_COLLECTION_NAME = "orders"; + export class Brand { @Field() name: string; - @Field({elements: TigrisDataTypes.STRING}) + @Field({ elements: TigrisDataTypes.STRING }) tags: Set; } export class Product { - @Field() + @Field({ maxLength: 64 }) name: string; @Field() @@ -34,16 +36,16 @@ export class Product { price: number; } -@TigrisCollection("orders") +@TigrisCollection(ORDERS_COLLECTION_NAME) export class Order { - @PrimaryKey(TigrisDataTypes.UUID,{order: 1, autoGenerate: true}) + @PrimaryKey(TigrisDataTypes.UUID, { order: 1, autoGenerate: true }) orderId: string; - @PrimaryKey({order: 2}) + @PrimaryKey({ order: 2 }) customerId: string; - @Field({elements: Product}) - products: Array + @Field({ elements: Product }) + products: Array; } /********************************** END **************************************/ @@ -54,48 +56,49 @@ export class Order { * NOTE: This is only an illustration; you don't have to write this definition, * it will be auto generated. */ -export const ExpectedSchema: TigrisSchema = { +export const OrderSchema: TigrisSchema = { orderId: { type: TigrisDataTypes.UUID, primary_key: { - order:1, - autoGenerate: true - } + order: 1, + autoGenerate: true, + }, }, customerId: { type: TigrisDataTypes.STRING, primary_key: { order: 2, - autoGenerate: false - } + autoGenerate: false, + }, }, products: { type: TigrisDataTypes.ARRAY, items: { type: { name: { - type: TigrisDataTypes.STRING + type: TigrisDataTypes.STRING, + maxLength: 64, }, brand: { type: { name: { - type: TigrisDataTypes.STRING + type: TigrisDataTypes.STRING, }, tags: { type: TigrisDataTypes.ARRAY, items: { - type: TigrisDataTypes.STRING - } - } - } + type: TigrisDataTypes.STRING, + }, + }, + }, }, upc: { - type: TigrisDataTypes.NUMBER_BIGINT + type: TigrisDataTypes.NUMBER_BIGINT, }, price: { - type: TigrisDataTypes.NUMBER - } - } - } - } -} + type: TigrisDataTypes.NUMBER, + }, + }, + }, + }, +}; diff --git a/src/__tests__/data/decoratedModels/users.ts b/src/__tests__/fixtures/schema/users.ts similarity index 75% rename from src/__tests__/data/decoratedModels/users.ts rename to src/__tests__/fixtures/schema/users.ts index 5494520..dc54d49 100644 --- a/src/__tests__/data/decoratedModels/users.ts +++ b/src/__tests__/fixtures/schema/users.ts @@ -11,29 +11,31 @@ import { TigrisCollection } from "../../../decorators/tigris-collection"; * - has an Array of primitive types * - infers the type of collection fields automatically using Reflection APIs *****************************************************************************/ +export const USERS_COLLECTION_NAME = "users"; + export class Identity { - @Field({maxLength: 128}) + @Field({ maxLength: 128 }) connection?: string; @Field() isSocial: boolean; - @Field({elements: TigrisDataTypes.NUMBER}) + @Field({ elements: TigrisDataTypes.NUMBER }) provider: Array; @Field() linkedAccounts: number; } -@TigrisCollection("users") -export class User implements TigrisCollectionType { - @PrimaryKey({order: 1}) +@TigrisCollection(USERS_COLLECTION_NAME) +export class User { + @PrimaryKey({ order: 1 }) id: number; @Field() created: Date; - @Field({elements: Identity}) + @Field({ elements: Identity }) identities: Array; @Field() @@ -48,16 +50,16 @@ export class User implements TigrisCollectionType { * NOTE: This is only an illustration; you don't have to write this definition, * it will be auto generated. */ -export const ExpectedSchema: TigrisSchema = { +export const UserSchema: TigrisSchema = { id: { type: TigrisDataTypes.NUMBER, primary_key: { order: 1, - autoGenerate: false + autoGenerate: false, }, }, created: { - type: TigrisDataTypes.DATE_TIME + type: TigrisDataTypes.DATE_TIME, }, identities: { type: TigrisDataTypes.ARRAY, @@ -65,24 +67,24 @@ export const ExpectedSchema: TigrisSchema = { type: { connection: { type: TigrisDataTypes.STRING, - maxLength: 128 + maxLength: 128, }, isSocial: { - type: TigrisDataTypes.BOOLEAN + type: TigrisDataTypes.BOOLEAN, }, provider: { type: TigrisDataTypes.ARRAY, items: { - type: TigrisDataTypes.NUMBER - } + type: TigrisDataTypes.NUMBER, + }, }, linkedAccounts: { - type: TigrisDataTypes.NUMBER - } - } - } + type: TigrisDataTypes.NUMBER, + }, + }, + }, }, name: { - type: TigrisDataTypes.STRING - } + type: TigrisDataTypes.STRING, + }, }; diff --git a/src/__tests__/fixtures/schema/vacationRentals.ts b/src/__tests__/fixtures/schema/vacationRentals.ts new file mode 100644 index 0000000..9ab2eb9 --- /dev/null +++ b/src/__tests__/fixtures/schema/vacationRentals.ts @@ -0,0 +1,196 @@ +import { TigrisCollection } from "../../../decorators/tigris-collection"; +import { FieldDefaults, TigrisDataTypes, TigrisSchema } from "../../../types"; +import { PrimaryKey } from "../../../decorators/tigris-primary-key"; +import { Field } from "../../../decorators/tigris-field"; + +/****************************************************************************** + * `VacationRentals` class demonstrates a Tigris collection schema generated using + * decorators. This particular schema example: + * - has an embedded object + * - infers the type of collection fields automatically using Reflection APIs + * - demonstrates how to set optional properties like 'defaults' for schema fields + *****************************************************************************/ +export const RENTALS_COLLECTION_NAME = "vacation_rentals"; + +class Address { + @Field() + city: string; + + @Field({ default: "US", maxLength: 2 }) + countryCode: string; +} + +@TigrisCollection(RENTALS_COLLECTION_NAME) +export class VacationRentals { + @PrimaryKey(TigrisDataTypes.UUID, { autoGenerate: true, order: 1 }) + id: string; + + @Field({ maxLength: 64 }) + name: string; + + @Field({ maxLength: 256, default: "" }) + description: string; + + @Field({ default: "Home" }) + propertyType: string; + + @Field({ default: 0 }) + bedrooms: number; + + @Field({ default: 0.0 }) + bathrooms: number; + + @Field(TigrisDataTypes.INT32, { default: 1 }) + minimumNights: number; + + @Field({ default: false }) + isOwnerOccupied: boolean; + + @Field({ default: true }) + hasWiFi: boolean; + + @Field({ default: {} }) + address: Address; + + @Field({ default: { stateId: true } }) + verifications: Object; + + @Field({ elements: TigrisDataTypes.STRING, default: ["Beds"] }) + amenities: Array; + + @Field({ elements: TigrisDataTypes.STRING, default: [] }) + attractions: Array; + + @Field({ default: null }) + host: Object; + + @Field({ elements: TigrisDataTypes.OBJECT, default: undefined }) + reviews: Array; + + @Field({ default: FieldDefaults.TIME_NOW }) + lastSeen: Date; + + @Field({ default: FieldDefaults.TIME_CREATED_AT }) + createdAt: Date; + + @Field({ default: FieldDefaults.TIME_UPDATED_AT }) + lastModified: Date; + + @Field({ default: FieldDefaults.AUTO_CUID }) + partnerId: string; + + @Field(TigrisDataTypes.UUID, { default: FieldDefaults.AUTO_UUID }) + referralId: string; +} +/********************************** END **************************************/ + +/** + * `TigrisSchema` representation of the collection class above. + * + * NOTE: This is only an illustration; you don't have to write this definition, + * it will be auto generated. + */ +export const VacationsRentalSchema: TigrisSchema = { + id: { + type: TigrisDataTypes.UUID, + primary_key: { + autoGenerate: true, + order: 1, + }, + }, + name: { + type: TigrisDataTypes.STRING, + maxLength: 64, + }, + description: { + type: TigrisDataTypes.STRING, + maxLength: 256, + default: "", + }, + propertyType: { + type: TigrisDataTypes.STRING, + default: "Home", + }, + bedrooms: { + type: TigrisDataTypes.NUMBER, + default: 0, + }, + bathrooms: { + type: TigrisDataTypes.NUMBER, + default: 0.0, + }, + minimumNights: { + type: TigrisDataTypes.INT32, + default: 1, + }, + isOwnerOccupied: { + type: TigrisDataTypes.BOOLEAN, + default: false, + }, + hasWiFi: { + type: TigrisDataTypes.BOOLEAN, + default: true, + }, + address: { + type: { + city: { + type: TigrisDataTypes.STRING, + }, + countryCode: { + type: TigrisDataTypes.STRING, + default: "US", + maxLength: 2, + }, + }, + default: {}, + }, + verifications: { + type: TigrisDataTypes.OBJECT, + default: { stateId: true }, + }, + amenities: { + type: TigrisDataTypes.ARRAY, + items: { + type: TigrisDataTypes.STRING, + }, + default: ["Beds"], + }, + attractions: { + type: TigrisDataTypes.ARRAY, + items: { + type: TigrisDataTypes.STRING, + }, + default: [], + }, + host: { + type: TigrisDataTypes.OBJECT, + default: null, + }, + reviews: { + type: TigrisDataTypes.ARRAY, + items: { + type: TigrisDataTypes.OBJECT, + }, + default: undefined, + }, + lastSeen: { + type: TigrisDataTypes.DATE_TIME, + default: FieldDefaults.TIME_NOW, + }, + createdAt: { + type: TigrisDataTypes.DATE_TIME, + default: FieldDefaults.TIME_CREATED_AT, + }, + lastModified: { + type: TigrisDataTypes.DATE_TIME, + default: FieldDefaults.TIME_UPDATED_AT, + }, + partnerId: { + type: TigrisDataTypes.STRING, + default: FieldDefaults.AUTO_CUID, + }, + referralId: { + type: TigrisDataTypes.UUID, + default: FieldDefaults.AUTO_UUID, + }, +}; diff --git a/src/__tests__/schema/decorator-processor.spec.ts b/src/__tests__/schema/decorator-processor.spec.ts deleted file mode 100644 index 7ada9a0..0000000 --- a/src/__tests__/schema/decorator-processor.spec.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { - CollectionSchema, - DecoratedSchemaProcessor, -} from "../../schema/decorated-schema-processor"; -import { TigrisCollectionType, TigrisSchema } from "../../types"; -import { ExpectedSchema as UserSchema, User } from "../data/./decoratedModels/users"; -import { ExpectedSchema as OrderSchema, Order } from "../data/./decoratedModels/orders"; -import { ExpectedSchema as MovieSchema, Movie } from "../data/./decoratedModels/movies"; -import { ExpectedSchema as MatricesSchema, Matrix } from "../data/./decoratedModels/matrices"; - -/* - * TODO: Add following tests - * - * add a constructor to class and subclasses - * readonly properties (getter/setter) - * custom constructor - * embedded definitions are empty - */ - -describe("Generate TigrisSchema from decorated classes", () => { - const processor = DecoratedSchemaProcessor.Instance; - type AnnotationTestCase = { - schemaClass: T; - expected: TigrisSchema; - }; - const schemaDefinitions: Array> = [ - { schemaClass: User, expected: UserSchema }, - { schemaClass: Order, expected: OrderSchema }, - { schemaClass: Movie, expected: MovieSchema }, - { schemaClass: Matrix, expected: MatricesSchema }, - ]; - test.each(schemaDefinitions)("from %p schema", (tc) => { - const generated: CollectionSchema = processor.process(tc.schemaClass); - expect(generated.schema).toStrictEqual(tc.expected); - // TODO: validate type compatibility - }); -}); diff --git a/src/__tests__/tigris.schema.spec.ts b/src/__tests__/tigris.schema.spec.ts index d541ac3..d16fac6 100644 --- a/src/__tests__/tigris.schema.spec.ts +++ b/src/__tests__/tigris.schema.spec.ts @@ -1,383 +1,105 @@ -import { TigrisCollectionType, TigrisDataTypes, TigrisSchema } from "../types"; +import { CollectionSchema, DecoratedSchemaProcessor } from "../schema/decorated-schema-processor"; +import { TigrisCollectionType, TigrisSchema } from "../types"; +import { User, USERS_COLLECTION_NAME, UserSchema } from "./fixtures/schema/users"; +import { + VacationRentals, + RENTALS_COLLECTION_NAME, + VacationsRentalSchema, +} from "./fixtures/schema/vacationRentals"; +import { Field } from "../decorators/tigris-field"; +import { IncompleteArrayTypeDefError } from "../error"; +import { TigrisCollection } from "../decorators/tigris-collection"; +import fs from "node:fs"; import { Utility } from "../utility"; +import { Order, ORDERS_COLLECTION_NAME, OrderSchema } from "./fixtures/schema/orders"; +import { Movie, MOVIES_COLLECTION_NAME, MovieSchema } from "./fixtures/schema/movies"; +import { MATRICES_COLLECTION_NAME, Matrix, MatrixSchema } from "./fixtures/schema/matrices"; -describe("schema tests", () => { - it("basicCollection", () => { - const schema: TigrisSchema = { - id: { - type: TigrisDataTypes.INT32, - primary_key: { - order: 1, - autoGenerate: true, - }, - }, - active: { - type: TigrisDataTypes.BOOLEAN, - }, - name: { - type: TigrisDataTypes.STRING, - }, - uuid: { - type: TigrisDataTypes.UUID, - }, - int32Number: { - type: TigrisDataTypes.INT32, - }, - int64Number: { - type: TigrisDataTypes.INT64, - }, - date: { - type: TigrisDataTypes.DATE_TIME, - }, - bytes: { - type: TigrisDataTypes.BYTE_STRING, - }, - }; - expect(Utility._toJSONSchema("basicCollection", schema)).toBe( - Utility._readTestDataFile("basicCollection.json") - ); - }); - - it("basicCollectionWithObjectType", () => { - const schema: TigrisSchema = { - id: { - type: TigrisDataTypes.INT64, - primary_key: { - order: 1, - autoGenerate: true, - }, - }, - name: { - type: TigrisDataTypes.STRING, - }, - metadata: { - type: TigrisDataTypes.OBJECT, - }, - }; - expect(Utility._toJSONSchema("basicCollectionWithObjectType", schema)).toBe( - Utility._readTestDataFile("basicCollectionWithObjectType.json") - ); - }); - - it("multiplePKeys", () => { - const schema: TigrisSchema = { - id: { - type: TigrisDataTypes.INT64, - primary_key: { - order: 2, // intentionally the order is skewed to test - }, - }, - active: { - type: TigrisDataTypes.BOOLEAN, - }, - name: { - type: TigrisDataTypes.STRING, - }, - uuid: { - type: TigrisDataTypes.UUID, - primary_key: { - order: 1, - autoGenerate: true, - }, - }, - int32Number: { - type: TigrisDataTypes.INT32, - }, - int64Number: { - type: TigrisDataTypes.INT64, - }, - date: { - type: TigrisDataTypes.DATE_TIME, - }, - bytes: { - type: TigrisDataTypes.BYTE_STRING, - }, - }; - expect(Utility._toJSONSchema("multiplePKeys", schema)).toBe( - Utility._readTestDataFile("multiplePKeys.json") - ); - }); - - it("nestedCollection", () => { - const addressSchema: TigrisSchema
= { - city: { - type: TigrisDataTypes.STRING, - }, - state: { - type: TigrisDataTypes.STRING, - }, - zipcode: { - type: TigrisDataTypes.NUMBER, - }, - }; - const schema: TigrisSchema = { - id: { - type: TigrisDataTypes.NUMBER, - }, - name: { - type: TigrisDataTypes.STRING, - }, - address: { - type: addressSchema, - }, - }; - expect(Utility._toJSONSchema("nestedCollection", schema)).toBe( - Utility._readTestDataFile("nestedCollection.json") - ); - }); - - it("collectionWithPrimitiveArrays", () => { - const schema: TigrisSchema = { - id: { - type: TigrisDataTypes.NUMBER, - }, - name: { - type: TigrisDataTypes.STRING, - }, - tags: { - type: TigrisDataTypes.ARRAY, - items: { - type: TigrisDataTypes.STRING, - }, - }, - }; - expect(Utility._toJSONSchema("collectionWithPrimitiveArrays", schema)).toBe( - Utility._readTestDataFile("collectionWithPrimitiveArrays.json") - ); - }); - - it("collectionWithObjectArrays", () => { - const addressSchema: TigrisSchema
= { - city: { - type: TigrisDataTypes.STRING, - }, - state: { - type: TigrisDataTypes.STRING, - }, - zipcode: { - type: TigrisDataTypes.NUMBER, - }, - }; - const schema: TigrisSchema = { - id: { - type: TigrisDataTypes.NUMBER, - }, - name: { - type: TigrisDataTypes.STRING, - }, - knownAddresses: { - type: TigrisDataTypes.ARRAY, - items: { - type: addressSchema, - }, - }, - }; - expect(Utility._toJSONSchema("collectionWithObjectArrays", schema)).toBe( - Utility._readTestDataFile("collectionWithObjectArrays.json") - ); - }); - - it("multiLevelPrimitiveArray", () => { - const schema: TigrisSchema = { - oneDArray: { - type: TigrisDataTypes.ARRAY, - items: { - type: TigrisDataTypes.STRING, - }, - }, - twoDArray: { - type: TigrisDataTypes.ARRAY, - items: { - type: TigrisDataTypes.ARRAY, - items: { - type: TigrisDataTypes.STRING, - }, - }, - }, - threeDArray: { - type: TigrisDataTypes.ARRAY, - items: { - type: TigrisDataTypes.ARRAY, - items: { - type: TigrisDataTypes.ARRAY, - items: { - type: TigrisDataTypes.STRING, - }, - }, - }, - }, - fourDArray: { - type: TigrisDataTypes.ARRAY, - items: { - type: TigrisDataTypes.ARRAY, - items: { - type: TigrisDataTypes.ARRAY, - items: { - type: TigrisDataTypes.ARRAY, - items: { - type: TigrisDataTypes.STRING, - }, - }, - }, - }, - }, - fiveDArray: { - type: TigrisDataTypes.ARRAY, - items: { - type: TigrisDataTypes.ARRAY, - items: { - type: TigrisDataTypes.ARRAY, - items: { - type: TigrisDataTypes.ARRAY, - items: { - type: TigrisDataTypes.ARRAY, - items: { - type: TigrisDataTypes.STRING, - }, - }, - }, - }, - }, - }, - }; - expect(Utility._toJSONSchema("multiLevelPrimitiveArray", schema)).toBe( - Utility._readTestDataFile("multiLevelPrimitiveArray.json") - ); +type SchemaTestCase = { + schemaClass: T; + expectedSchema: TigrisSchema; + name: string; + expectedJson: string; +}; + +const schemas: Array> = [ + { + schemaClass: User, + expectedSchema: UserSchema, + name: USERS_COLLECTION_NAME, + expectedJson: "users.json", + }, + { + schemaClass: Order, + expectedSchema: OrderSchema, + name: ORDERS_COLLECTION_NAME, + expectedJson: "orders.json", + }, + { + schemaClass: Movie, + expectedSchema: MovieSchema, + name: MOVIES_COLLECTION_NAME, + expectedJson: "movies.json", + }, + { + schemaClass: Matrix, + expectedSchema: MatrixSchema, + name: MATRICES_COLLECTION_NAME, + expectedJson: "matrices.json", + }, + { + schemaClass: VacationRentals, + expectedSchema: VacationsRentalSchema, + name: RENTALS_COLLECTION_NAME, + expectedJson: "vacationRentals.json", + }, +]; + +/* + * TODO: Add following tests + * + * add a constructor to class and subclasses + * readonly properties (getter/setter) + * custom constructor + * embedded definitions are empty + */ +describe.each(schemas)("Schema conversion for: '$name'", (tc) => { + const processor = DecoratedSchemaProcessor.Instance; + + test("Convert decorated class to TigrisSchema", () => { + const generated: CollectionSchema = processor.process(tc.schemaClass); + expect(generated.schema).toStrictEqual(tc.expectedSchema); }); - it("multiLevelObjectArray", () => { - const addressSchema: TigrisSchema
= { - city: { - type: TigrisDataTypes.STRING, - }, - state: { - type: TigrisDataTypes.STRING, - }, - zipcode: { - type: TigrisDataTypes.NUMBER, - }, - }; - const schema: TigrisSchema = { - oneDArray: { - type: TigrisDataTypes.ARRAY, - items: { - type: addressSchema, - }, - }, - twoDArray: { - type: TigrisDataTypes.ARRAY, - items: { - type: TigrisDataTypes.ARRAY, - items: { - type: addressSchema, - }, - }, - }, - threeDArray: { - type: TigrisDataTypes.ARRAY, - items: { - type: TigrisDataTypes.ARRAY, - items: { - type: TigrisDataTypes.ARRAY, - items: { - type: addressSchema, - }, - }, - }, - }, - fourDArray: { - type: TigrisDataTypes.ARRAY, - items: { - type: TigrisDataTypes.ARRAY, - items: { - type: TigrisDataTypes.ARRAY, - items: { - type: TigrisDataTypes.ARRAY, - items: { - type: addressSchema, - }, - }, - }, - }, - }, - fiveDArray: { - type: TigrisDataTypes.ARRAY, - items: { - type: TigrisDataTypes.ARRAY, - items: { - type: TigrisDataTypes.ARRAY, - items: { - type: TigrisDataTypes.ARRAY, - items: { - type: TigrisDataTypes.ARRAY, - items: { - type: addressSchema, - }, - }, - }, - }, - }, - }, - }; - expect(Utility._toJSONSchema("multiLevelObjectArray", schema)).toBe( - Utility._readTestDataFile("multiLevelObjectArray.json") + test("Convert TigrisSchema to JSON spec", () => { + expect(Utility._toJSONSchema(tc.name, tc.expectedSchema)).toBe( + _readTestDataFile(tc.expectedJson) ); }); }); -interface BasicCollection extends TigrisCollectionType { - id: number; - active: boolean; - name: string; - uuid: string; - int32Number: number; - int64Number: string; - date: string; - bytes: string; -} - -interface BasicCollectionWithObject extends TigrisCollectionType { - id: number; - name: string; - metadata: object; -} - -interface NestedCollection extends TigrisCollectionType { - id: number; - name: string; - address: Address; -} - -interface CollectionWithPrimitiveArrays extends TigrisCollectionType { - id: number; - name: string; - tags: string[]; -} - -interface CollectionWithObjectArrays extends TigrisCollectionType { - id: number; - name: string; - knownAddresses: Address[]; -} - -interface MultiLevelPrimitiveArray extends TigrisCollectionType { - oneDArray: string[]; - twoDArray: string[][]; - threeDArray: string[][][]; - fourDArray: string[][][][]; - fiveDArray: string[][][][][]; -} - -interface MultiLevelObjectArray extends TigrisCollectionType { - oneDArray: Address[]; - twoDArray: Address[][]; - threeDArray: Address[][][]; - fourDArray: Address[][][][]; - fiveDArray: Address[][][][][]; -} +test("throws error when Arrays are not properly decorated", () => { + let caught; + + try { + @TigrisCollection("test_studio") + class Studio { + @Field() + actors: Array; + } + } catch (e) { + caught = e; + } + expect(caught).toBeInstanceOf(IncompleteArrayTypeDefError); +}); -interface Address { - city: string; - state: string; - zipcode: number; +function _readTestDataFile(fileName: string): string { + return Utility.objToJsonString( + Utility.jsonStringToObj( + fs.readFileSync("src/__tests__/fixtures/json-schema/" + fileName, "utf8"), + { + serverUrl: "test", + } + ) + ); } diff --git a/src/db.ts b/src/db.ts index 7d053e6..7ead354 100644 --- a/src/db.ts +++ b/src/db.ts @@ -64,7 +64,7 @@ export class DB { * * ``` * @TigrisCollection("todoItems") - * class TodoItem implements TigrisCollectionType { + * class TodoItem { * @PrimaryKey(TigrisDataTypes.INT32, { order: 1 }) * id: number; * diff --git a/src/decorators/options/embedded-field-options.ts b/src/decorators/options/embedded-field-options.ts index bfeec06..f4afeec 100644 --- a/src/decorators/options/embedded-field-options.ts +++ b/src/decorators/options/embedded-field-options.ts @@ -2,9 +2,10 @@ import { TigrisDataTypes } from "../../types"; /** * Additional type information for Arrays and Objects schema fields + * @public */ export type EmbeddedFieldOptions = { - elements: TigrisDataTypes | Function; + elements?: TigrisDataTypes | Function; /** * Optionally used to specify nested arrays (Array of arrays). * diff --git a/src/decorators/tigris-field.ts b/src/decorators/tigris-field.ts index 4a254c8..29e555d 100644 --- a/src/decorators/tigris-field.ts +++ b/src/decorators/tigris-field.ts @@ -24,15 +24,6 @@ export function Field(): PropertyDecorator; * @param type - Schema field's data type */ export function Field(type: TigrisDataTypes): PropertyDecorator; -/** - * Field decorator is used to mark a class property as Collection field. Only properties - * decorated with `@Field` will be used in Schema. - * - * Uses `Reflection` to determine the data type of schema Field. - * - * @param options - Optional properties of the schema field - */ -export function Field(options: TigrisFieldOptions): PropertyDecorator; /** * Field decorator is used to mark a class property as Collection field. Only properties * decorated with `@Field` will be used in Schema. @@ -42,17 +33,7 @@ export function Field(options: TigrisFieldOptions): PropertyDecorator; * @param options - `EmbeddedFieldOptions` are only applicable to Array and Object types * of schema field. */ -export function Field(options: EmbeddedFieldOptions): PropertyDecorator; -/** - * Field decorator is used to mark a class property as Collection field. Only properties - * decorated with `@Field` will be used in Schema. - * - * Uses `Reflection` to determine the data type of Field. - * - * @param type - Schema field's data type - * @param options - Optional properties of the schema field - */ -export function Field(type: TigrisDataTypes, options?: TigrisFieldOptions): PropertyDecorator; +export function Field(options: EmbeddedFieldOptions & TigrisFieldOptions): PropertyDecorator; /** * Field decorator is used to mark a class property as Collection field. Only properties * decorated with `@Field` will be used in Schema. @@ -63,15 +44,18 @@ export function Field(type: TigrisDataTypes, options?: TigrisFieldOptions): Prop * @param options - `EmbeddedFieldOptions` are only applicable to Array and Object types * of schema field. */ -export function Field(type: TigrisDataTypes, options?: EmbeddedFieldOptions): PropertyDecorator; +export function Field( + type: TigrisDataTypes, + options?: EmbeddedFieldOptions & TigrisFieldOptions +): PropertyDecorator; /** * Field decorator is used to mark a class property as Collection field. Only properties * decorated with `@Field` will be used in Schema. */ export function Field( - typeOrOptions?: TigrisDataTypes | TigrisFieldOptions | EmbeddedFieldOptions, - options?: TigrisFieldOptions | EmbeddedFieldOptions + typeOrOptions?: TigrisDataTypes | (TigrisFieldOptions & EmbeddedFieldOptions), + options?: TigrisFieldOptions & EmbeddedFieldOptions ): PropertyDecorator { return function (target, propertyName) { propertyName = propertyName.toString(); @@ -84,17 +68,15 @@ export function Field( } else if (typeof typeOrOptions === "object") { if (isEmbeddedOption(typeOrOptions)) { embedOptions = typeOrOptions as EmbeddedFieldOptions; - } else { - fieldOptions = typeOrOptions as TigrisFieldOptions; } + fieldOptions = typeOrOptions as TigrisFieldOptions; } if (typeof options === "object") { if (isEmbeddedOption(options)) { embedOptions = options as EmbeddedFieldOptions; - } else { - fieldOptions = options as TigrisFieldOptions; } + fieldOptions = options as TigrisFieldOptions; } // if type or options are not specified, infer using reflection diff --git a/src/schema/decorated-schema-processor.ts b/src/schema/decorated-schema-processor.ts index e7e1239..0cc8230 100644 --- a/src/schema/decorated-schema-processor.ts +++ b/src/schema/decorated-schema-processor.ts @@ -59,13 +59,17 @@ export class DecoratedSchemaProcessor { } } break; - case TigrisDataTypes.BYTE_STRING: case TigrisDataTypes.STRING: if (field.schemaFieldOptions?.maxLength) { schema[key].maxLength = field.schemaFieldOptions.maxLength; } break; } + + // set "default" value for field, if any + if (field.schemaFieldOptions && "default" in field.schemaFieldOptions) { + schema[key].default = field.schemaFieldOptions.default; + } } return schema; } diff --git a/src/tigris.ts b/src/tigris.ts index cc8fb77..91fab93 100644 --- a/src/tigris.ts +++ b/src/tigris.ts @@ -349,7 +349,7 @@ export class Tigris { * @example * ``` * @TigrisCollection("todoItems") - * class TodoItem implements TigrisCollectionType { + * class TodoItem { * @PrimaryKey(TigrisDataTypes.INT32, { order: 1 }) * id: number; * diff --git a/src/types.ts b/src/types.ts index fc32d14..8cf02fd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -545,16 +545,39 @@ export enum TigrisDataTypes { OBJECT = "object", } -export interface TigrisFieldOptions { - maxLength?: number; +export enum FieldDefaults { + TIME_UPDATED_AT = "updatedAt", + TIME_CREATED_AT = "createdAt", + TIME_NOW = "now()", + AUTO_CUID = "cuid()", + AUTO_UUID = "uuid()", } +export type TigrisFieldOptions = { + /** + * Max length for "string" type of fields + */ + maxLength?: number; + /** + * Default + */ + default?: + | FieldDefaults + | number + | bigint + | string + | boolean + | Date + | Array + | Record; +}; + export type TigrisSchema = { [K in keyof T]: { type: TigrisDataTypes | TigrisSchema; primary_key?: PrimaryKeyOptions; items?: TigrisArrayItem; - }; + } & TigrisFieldOptions; }; export type TigrisArrayItem = { @@ -567,30 +590,24 @@ export type PrimaryKeyOptions = { autoGenerate?: boolean; }; -export type TigrisPartitionKey = { - order: number; -}; - /** -Generates all possible paths for type parameter T. By recursively iterating over its keys. While - iterating the keys it makes the keys available in string form and in non string form both. For - example - - interface IUser { - name: string; - id: number - address: Address; - } - - interface Address { - city: string - state: string - } - - and Paths will make these keys available - name, id, address (object type) and also in the string form - "name", "id", "address.city", "address.state" - + * Generates all possible paths for type parameter T. By recursively iterating over its keys. While + * iterating the keys it makes the keys available in string form and in non string form both. For + * @example + * ``` + * interface IUser { + * name: string; + * id: number; + * address: Address; + * } + * + * interface Address { + * city: string + * state: string + * } + * ``` + * and Paths will make these keys available name, id, address (object type) and also in the + * string form "name", "id", "address.city", "address.state" */ type Paths = { [K in keyof T]: T[K] extends object diff --git a/src/utility.ts b/src/utility.ts index 846ed11..efa9c7a 100644 --- a/src/utility.ts +++ b/src/utility.ts @@ -4,10 +4,10 @@ import { Session } from "./session"; import { DeleteQueryOptions, + FindQueryOptions, LogicalFilter, LogicalOperator, ReadFields, - FindQueryOptions, Selector, SelectorFilter, SelectorFilterOperator, @@ -257,19 +257,10 @@ export const Utility = { - this can be extended for other schema massaging */ _postProcessDocumentSchema(result: object, pkeyMap: object): object { - if (Object.keys(pkeyMap).length === 0) { - // if no pkeys was used defined. add implicit pkey - result["properties"]["id"] = { - type: "string", - format: "uuid", - }; - result["primary_key"] = ["id"]; - } else { - result["primary_key"] = []; - // add primary_key in order - for (let i = 1; i <= Object.keys(pkeyMap).length; i++) { - result["primary_key"].push(pkeyMap[i.toString()]); - } + result["primary_key"] = []; + // add primary_key in order + for (let i = 1; i <= Object.keys(pkeyMap).length; i++) { + result["primary_key"].push(pkeyMap[i.toString()]); } return result; }, @@ -326,11 +317,26 @@ export const Utility = { keyMap[schema[property].key["order"]] = property; } + // property is string and has "maxLength" optional attribute + if ( + thisProperty["type"] == TigrisDataTypes.STRING.valueOf() && + thisProperty["format"] === undefined && + schema[property].maxLength + ) { + thisProperty["maxLength"] = schema[property].maxLength as number; + } + // array type? } else if (schema[property].type === TigrisDataTypes.ARRAY.valueOf()) { thisProperty = this._getArrayBlock(schema[property], pkeyMap, keyMap); } properties[property] = thisProperty; + // 'default' values for schema fields, if any + if ("default" in schema[property]) { + thisProperty["default"] = + // eslint-disable-next-line unicorn/no-null + schema[property].default == undefined ? null : schema[property].default; + } } return properties; }, @@ -391,25 +397,11 @@ export const Utility = { const arrayBlock = {}; arrayBlock["type"] = "array"; arrayBlock["items"] = {}; - // array of array? - if (arraySchema["items"]["type"] === TigrisDataTypes.ARRAY.valueOf()) { - arrayBlock["items"] = this._getArrayBlock(arraySchema["items"], pkeyMap, keyMap); - // array of custom type? - } else if (typeof arraySchema["items"]["type"] === "object") { - arrayBlock["items"]["type"] = "object"; - arrayBlock["items"]["properties"] = this._getSchemaProperties( - arraySchema["items"]["type"], - pkeyMap, - keyMap - ); - // within array: single flat property? - } else { - arrayBlock["items"]["type"] = this._getType(arraySchema["items"]["type"] as TigrisDataTypes); - const format = this._getFormat(arraySchema["items"]["type"] as TigrisDataTypes); - if (format) { - arrayBlock["items"]["format"] = format; - } - } + arrayBlock["items"] = this._getSchemaProperties( + { _$arrayItemPlaceholder: arraySchema["items"] }, + pkeyMap, + keyMap + )["_$arrayItemPlaceholder"]; return arrayBlock; }, From 181187276fc735e988e2647d85fa38969822d233 Mon Sep 17 00:00:00 2001 From: Adil Ansari Date: Mon, 9 Jan 2023 10:54:20 -0800 Subject: [PATCH 12/12] ci: Using node 18 for semantic release --- .github/workflows/pre-release.yaml | 4 ++-- .github/workflows/release.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pre-release.yaml b/.github/workflows/pre-release.yaml index 121406f..acc2a13 100644 --- a/.github/workflows/pre-release.yaml +++ b/.github/workflows/pre-release.yaml @@ -17,7 +17,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 18 - name: Build package run: npm ci - name: Install semantic-release @@ -30,4 +30,4 @@ jobs: env: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: npx semantic-release --debug --dryRun + run: npx semantic-release@18 --debug --dryRun diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 7a8ebc9..d442be1 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -35,7 +35,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 18 - name: Build package run: npm ci - name: Install semantic-release @@ -48,4 +48,4 @@ jobs: env: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: npx semantic-release --debug + run: npx semantic-release@18 --debug