diff --git a/.eslintrc b/.eslintrc index 046ea670d..afe42a56e 100644 --- a/.eslintrc +++ b/.eslintrc @@ -4,10 +4,7 @@ "sourceType": "module", "ecmaFeatures": { "jsx": false - }, - "project": [ - "./tsconfig.json" - ] + } }, "extends": ["oceanprotocol", "plugin:prettier/recommended"], "plugins": ["@typescript-eslint"], @@ -23,7 +20,8 @@ "es6": true, "browser": true, "mocha": true, - "node": true + "node": true, + "jest": true }, "globals": { "NodeJS": true diff --git a/.gitignore b/.gitignore index c6bba5913..889f0abfa 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,9 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* + +# Typesense +typesense-data + +# IDEA +.idea \ No newline at end of file diff --git a/README.md b/README.md index f1ebace5f..eb938a722 100644 --- a/README.md +++ b/README.md @@ -48,3 +48,18 @@ Load postman collection from docs and play - httpRoutes: exposes http endpoints - P2P: has P2P functionality. will have to extend handleBroadcasts and handleProtocolCommands, rest is pretty much done +## Run tests + +Before running tests, please run Typesense docker + +``` +docker-compose -f typesense-compose.yml -p ocean-node up -d +``` + +You can then run tests + +``` +npm run test +``` + + diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 000000000..134213754 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,4 @@ +export default { + preset: 'ts-jest', + testEnvironment: 'node', +}; \ No newline at end of file diff --git a/package.json b/package.json index 5946b555e..3470eaeb6 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", + "test": "jest --detectOpenHandles", "build": "npm run clean && npm run build:tsc", "build:swagger": "tsoa spec", "build:tsc": "tsc --sourceMap", @@ -37,7 +37,7 @@ "@multiformats/multiaddr": "^10.2.0", "@types/lodash.clonedeep": "^4.5.7", "aegir": "^37.3.0", - "axios": "^1.5.1", + "axios": "^1.6.0", "delay": "^5.0.0", "eth-crypto": "^2.6.0", "express": "^4.18.2", @@ -56,6 +56,7 @@ }, "devDependencies": { "@types/express": "^4.17.17", + "@types/jest": "^29.5.7", "@types/node": "^20.8.9", "@types/swagger-ui-express": "^4.1.3", "@typescript-eslint/eslint-plugin": "^6.8.0", @@ -66,7 +67,9 @@ "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.1", "eslint-plugin-promise": "^6.1.1", + "jest": "^29.7.0", "prettier": "^3.0.3", + "ts-jest": "^29.1.1", "tsx": "^3.12.8" } } diff --git a/src/@types/Typesense.ts b/src/@types/Typesense.ts new file mode 100644 index 000000000..bfd5cc43c --- /dev/null +++ b/src/@types/Typesense.ts @@ -0,0 +1,150 @@ +import { Logger } from 'winston' + +export interface TypesenseAbstractLogger { + error?: any + warn?: any + info?: any + debug?: any + trace?: any +} + +export interface TypesenseNode { + host: string + port: number + protocol: string +} + +export interface TypesenseConfigOptions { + apiKey: string + nodes: TypesenseNode[] + numRetries?: number + retryIntervalSeconds?: number + connectionTimeoutSeconds?: number + logLevel?: string + logger?: Logger +} + +export type TypesenseFieldType = + | 'string' + | 'int32' + | 'int64' + | 'float' + | 'bool' + | 'geopoint' + | 'geopoint[]' + | 'string[]' + | 'int32[]' + | 'int64[]' + | 'float[]' + | 'bool[]' + | 'object' + | 'object[]' + | 'auto' + | 'string*' + +export interface TypesenseCollectionFieldSchema { + name: string + type: TypesenseFieldType + optional?: boolean + facet?: boolean + index?: boolean + sort?: boolean + locale?: string + infix?: boolean + num_dim?: number + [t: string]: unknown +} + +export interface TypesenseCollectionCreateSchema { + name: string + enable_nested_fields?: boolean + fields?: TypesenseCollectionFieldSchema[] +} + +export interface TypesenseCollectionSchema extends TypesenseCollectionCreateSchema { + created_at: number + num_documents: number + num_memory_shards: number +} + +export interface TypesenseCollectionDropFieldSchema { + name: string + drop: true +} + +export interface TypesenseCollectionUpdateSchema + extends Partial> { + fields?: (TypesenseCollectionFieldSchema | TypesenseCollectionDropFieldSchema)[] +} + +export type TypesenseDocumentSchema = Record + +type TypesenseOperationMode = 'off' | 'always' | 'fallback' + +export interface TypesenseSearchParams { + [key: string]: any + // From https://typesense.org/docs/latest/api/documents.html#arguments + q: string + query_by: string | string[] + query_by_weights?: string | number[] + prefix?: string | boolean | boolean[] // default: true + filter_by?: string + sort_by?: string | string[] // default: text match desc + facet_by?: string | string[] + max_facet_values?: number + facet_query?: string + facet_query_num_typos?: number + page?: number // default: 1 + per_page?: number // default: 10, max 250 + group_by?: string | string[] + group_limit?: number // default: + include_fields?: string | string[] + exclude_fields?: string | string[] + highlight_fields?: string | string[] // default: all queried fields + highlight_full_fields?: string | string[] // default: all fields + highlight_affix_num_tokens?: number // default: 4 + highlight_start_tag?: string // default: + highlight_end_tag?: string // default: + snippet_threshold?: number // default: 30 + num_typos?: string | number | number[] // default: 2 + min_len_1typo?: number + min_len_2typo?: number + split_join_tokens?: TypesenseOperationMode + exhaustive_search?: boolean + drop_tokens_threshold?: number // default: 10 + typo_tokens_threshold?: number // default: 100 + pinned_hits?: string | string[] + hidden_hits?: string | string[] + limit_hits?: number // default: no limit + pre_segmented_query?: boolean + enable_overrides?: boolean + prioritize_exact_match?: boolean // default: true + prioritize_token_position?: boolean + search_cutoff_ms?: number + use_cache?: boolean + max_candidates?: number + infix?: TypesenseOperationMode | TypesenseOperationMode[] + preset?: string + text_match_type?: 'max_score' | 'max_weight' + vector_query?: string + 'x-typesense-api-key'?: string + 'x-typesense-user-id'?: string + offset?: number + limit?: number +} + +export interface TypesenseSearchResponse { + facet_counts?: any[] + found: number + found_docs?: number + out_of: number + page: number + request_params: any + search_time_ms: number + hits?: any[] + grouped_hits?: { + group_key: string[] + hits: any[] + found?: number + }[] +} diff --git a/src/@types/index.ts b/src/@types/index.ts index 5c32eb2c2..bf5ba2edc 100644 --- a/src/@types/index.ts +++ b/src/@types/index.ts @@ -1 +1,2 @@ export * from './OceanNode' +export * from './Typesense' diff --git a/src/components/database/typesense.ts b/src/components/database/typesense.ts new file mode 100644 index 000000000..a6af51e47 --- /dev/null +++ b/src/components/database/typesense.ts @@ -0,0 +1,150 @@ +import { + TypesenseSearchParams, + TypesenseCollectionCreateSchema, + TypesenseCollectionSchema, + TypesenseCollectionUpdateSchema, + TypesenseConfigOptions, + TypesenseDocumentSchema, + TypesenseSearchResponse +} from '../../@types' +import { TypesenseApi } from './typesenseApi' +import { TypesenseConfig } from './typesenseConfig' + +/** + * TypesenseDocuments class implements CRUD methods + * for interacting with documents of an individual collection + * In addition, it implements a method for searching in documents + */ +class TypesenseDocuments { + apiPath: string + + constructor( + private collectionName: string, + private api: TypesenseApi + ) { + this.apiPath = `/collections/${this.collectionName}/documents` + } + + async create(document: TypesenseDocumentSchema) { + if (!document) throw new Error('No document provided') + return this.api.post(this.apiPath, document) + } + + async retrieve(documentId: string) { + const path = `${this.apiPath}/${documentId}` + return this.api.get(path) + } + + async delete(documentId: string) { + const path = `${this.apiPath}/${documentId}` + return this.api.delete(path) + } + + async update(documentId: string, partialDocument: Partial) { + const path = `${this.apiPath}/${documentId}` + return this.api.patch(path, partialDocument) + } + + async search( + searchParameters: TypesenseSearchParams + ): Promise { + const additionalQueryParams: { [key: string]: any } = {} + for (const key in searchParameters) { + if (Array.isArray(searchParameters[key])) { + additionalQueryParams[key] = searchParameters[key].join(',') + } + } + const queryParams = Object.assign({}, searchParameters, additionalQueryParams) + const path = `${this.apiPath}/search` + return this.api.get( + path, + queryParams + ) as Promise + } +} + +/** + * TypesenseCollection class implements CRUD methods for interacting with an individual collection + * It initiates class that provides access to methods of documents + */ +class TypesenseCollection { + apiPath: string + private readonly _documents: TypesenseDocuments + + constructor( + private name: string, + private api: TypesenseApi + ) { + this.apiPath = `/collections/${this.name}` + this._documents = new TypesenseDocuments(this.name, this.api) + } + + async retrieve(): Promise { + return this.api.get(this.apiPath) + } + + async update( + schema: TypesenseCollectionUpdateSchema + ): Promise { + return this.api.patch(this.apiPath, schema) + } + + async delete(): Promise { + return this.api.delete(this.apiPath) + } + + documents(): TypesenseDocuments { + return this._documents + } +} + +/** + * TypesenseCollections class implements the basic methods of collections + */ +export class TypesenseCollections { + apiPath: string = '/collections' + + constructor(private api: TypesenseApi) {} + + async create(schema: TypesenseCollectionCreateSchema) { + return this.api.post(this.apiPath, schema) + } + + async retrieve() { + return this.api.get(this.apiPath) + } +} + +/** + * Typesense class is used to create a base instance to work with Typesense + * It initiates classes that provides access to methods of collections + * or an individual collection + */ +export default class Typesense { + config: TypesenseConfig + api: TypesenseApi + collectionsRecords: Record = {} + private readonly _collections: TypesenseCollections + + constructor(options: TypesenseConfigOptions) { + this.config = new TypesenseConfig(options) + this.api = new TypesenseApi(this.config) + this._collections = new TypesenseCollections(this.api) + } + + collections(): TypesenseCollections + collections(collectionName: string): TypesenseCollection + collections(collectionName?: string): TypesenseCollection | TypesenseCollections { + if (!collectionName) { + return this._collections + } else { + if (this.collectionsRecords[collectionName] === undefined) { + this.collectionsRecords[collectionName] = new TypesenseCollection( + collectionName, + this.api + ) + } + return this.collectionsRecords[collectionName] + } + } +} diff --git a/src/components/database/typesenseApi.ts b/src/components/database/typesenseApi.ts new file mode 100644 index 000000000..278c774b0 --- /dev/null +++ b/src/components/database/typesenseApi.ts @@ -0,0 +1,157 @@ +import { TypesenseNode } from '../../@types' +import axios, { AxiosRequestConfig } from 'axios' +import { setTimeout } from 'timers/promises' +import { TypesenseConfig } from './typesenseConfig' + +/** + * TypesenseApi class is used to implement an api interface + * for working with Typesense via http requests + */ +export class TypesenseApi { + currentNodeIndex = -1 + + constructor(private config: TypesenseConfig) {} + + async get(endpoint: string, queryParameters: any = {}): Promise { + return this.request('get', endpoint, { + queryParameters + }) + } + + async post( + endpoint: string, + bodyParameters: any = {}, + queryParameters: any = {} + ): Promise { + return this.request('post', endpoint, { + queryParameters, + bodyParameters + }) + } + + async delete(endpoint: string, queryParameters: any = {}): Promise { + return this.request('delete', endpoint, { queryParameters }) + } + + async put( + endpoint: string, + bodyParameters: any = {}, + queryParameters: any = {} + ): Promise { + return this.request('put', endpoint, { + queryParameters, + bodyParameters + }) + } + + async patch( + endpoint: string, + bodyParameters: any = {}, + queryParameters: any = {} + ): Promise { + return this.request('patch', endpoint, { + queryParameters, + bodyParameters + }) + } + + getNextNode(): TypesenseNode { + let candidateNode: TypesenseNode = this.config.nodes[0] + if (this.config.nodes.length === 1) { + return candidateNode + } + this.currentNodeIndex = (this.currentNodeIndex + 1) % this.config.nodes.length + candidateNode = this.config.nodes[this.currentNodeIndex] + this.config.logger.debug(`Updated current node to Node ${candidateNode}`) + return candidateNode + } + + async request( + requestType: string, + endpoint: string, + { + queryParameters = null, + bodyParameters = null, + skipConnectionTimeout = false + }: { + queryParameters?: any + bodyParameters?: any + skipConnectionTimeout?: boolean + } + ): Promise { + this.config.logger.debug(`Request ${endpoint}`) + let lastException + for (let numTries = 1; numTries <= this.config.numRetries + 1; numTries++) { + const node = this.getNextNode() + this.config.logger.debug( + `Request ${endpoint}: Attempting ${requestType.toUpperCase()} request Try #${numTries} to Node ${ + node.host + }` + ) + + try { + const url = `${node.protocol}://${node.host}:${node.port}${endpoint}` + const requestOptions: AxiosRequestConfig = { + method: requestType, + url, + headers: { 'X-TYPESENSE-API-KEY': this.config.apiKey }, + maxContentLength: Infinity, + maxBodyLength: Infinity, + validateStatus: (status) => { + return status > 0 + }, + transformResponse: [ + (data, headers) => { + let transformedData = data + if ( + headers !== undefined && + typeof data === 'string' && + headers['content-type'] && + headers['content-type'].startsWith('application/json') + ) { + transformedData = JSON.parse(data) + } + return transformedData + } + ] + } + + if (skipConnectionTimeout !== true) { + requestOptions.timeout = this.config.connectionTimeoutSeconds * 1000 + } + + if (queryParameters !== null) { + requestOptions.params = queryParameters + } + + if (bodyParameters !== null) { + requestOptions.data = bodyParameters + } + + const response = await axios(requestOptions) + this.config.logger.debug( + `Request ${endpoint}: Request to Node ${node.host} was made. Response Code was ${response.status}.` + ) + + if (response.status >= 200 && response.status < 300) { + return Promise.resolve(response.data) + } else if (response.status < 500) { + return Promise.reject(new Error(response.data?.message)) + } else { + throw new Error(response.data?.message) + } + } catch (error: any) { + lastException = error + this.config.logger.debug( + `Request ${endpoint}: Request to Node ${node.host} failed due to "${error.code} ${error.message}"` + ) + this.config.logger.debug( + `Request ${endpoint}: Sleeping for ${this.config.retryIntervalSeconds}s and then retrying request...` + ) + await setTimeout(this.config.retryIntervalSeconds) + } + } + this.config.logger.debug(`Request: No retries left. Raising last error`) + return Promise.reject(lastException) + } +} diff --git a/src/components/database/typesenseConfig.ts b/src/components/database/typesenseConfig.ts new file mode 100644 index 000000000..39d3c202d --- /dev/null +++ b/src/components/database/typesenseConfig.ts @@ -0,0 +1,29 @@ +import { + TypesenseAbstractLogger, + TypesenseConfigOptions, + TypesenseNode +} from '../../@types' + +/** + * TypesenseConfig class is used to specify configuration parameters for Typesense + * as well as handling optional cases + */ +export class TypesenseConfig { + apiKey: string + nodes: TypesenseNode[] + numRetries: number + retryIntervalSeconds: number + connectionTimeoutSeconds: number + logLevel: string + logger: TypesenseAbstractLogger + + constructor(options: TypesenseConfigOptions) { + this.apiKey = options.apiKey + this.nodes = options.nodes || [] + this.numRetries = options.numRetries || 3 + this.connectionTimeoutSeconds = options.connectionTimeoutSeconds || 5 + this.retryIntervalSeconds = options.retryIntervalSeconds || 0.1 + this.logLevel = options.logLevel || 'debug' + this.logger = options.logger || { debug: (log: any) => console.log(log) } + } +} diff --git a/tests/data/ddo.ts b/tests/data/ddo.ts new file mode 100644 index 000000000..cab2675c9 --- /dev/null +++ b/tests/data/ddo.ts @@ -0,0 +1,22 @@ +export const ddo = { + hashType: 'sha256', + '@context': ['https://w3id.org/did/v1'], + id: 'did:op:fa0e8fa9550e8eb13392d6eeb9ba9f8111801b332c8d2345b350b3bc66b379d7', + nftAddress: '0xBB1081DbF3227bbB233Db68f7117114baBb43656', + version: '4.1.0', + chainId: 137, + metadata: { + created: '2022-12-30T08:40:06Z', + updated: '2022-12-30T08:40:06Z', + type: 'dataset', + name: 'DEX volume in details', + description: + 'Volume traded and locked of Decentralized Exchanges (Uniswap, Sushiswap, Curve, Balancer, ...), daily in details', + tags: ['index', 'defi', 'tvl'], + author: 'DEX', + license: 'https://market.oceanprotocol.com/terms', + additionalInformation: { + termsAndConditions: true + } + } +} diff --git a/tests/data/ddoSchema.ts b/tests/data/ddoSchema.ts new file mode 100644 index 000000000..608c68a6c --- /dev/null +++ b/tests/data/ddoSchema.ts @@ -0,0 +1,29 @@ +import { TypesenseCollectionCreateSchema } from '../../src/@types' + +export const ddoSchema: TypesenseCollectionCreateSchema = { + name: 'ddo', + enable_nested_fields: true, + fields: [ + { name: '@context', type: 'string[]' }, + { name: 'chainId', type: 'int64' }, + { name: 'version', type: 'string', sort: true }, + { name: 'nftAddress', type: 'string' }, + + { name: 'metadata.description', type: 'string' }, + { name: 'metadata.copyrightHolder', type: 'string', optional: true }, + { name: 'metadata.name', type: 'string' }, + { name: 'metadata.type', type: 'string' }, + { name: 'metadata.author', type: 'string' }, + { name: 'metadata.license', type: 'string' }, + { name: 'metadata.links', type: 'string', optional: true }, + { name: 'metadata.tags', type: 'string[]', optional: true }, + { name: 'metadata.categories', type: 'string', optional: true }, + { name: 'metadata.contentLanguage', type: 'string', optional: true }, + { name: 'metadata.algorithm.version', type: 'string', optional: true }, + { name: 'metadata.algorithm.language', type: 'string', optional: true }, + { name: 'metadata.algorithm.container.entrypoint', type: 'string', optional: true }, + { name: 'metadata.algorithm.container.image', type: 'string', optional: true }, + { name: 'metadata.algorithm.container.tag', type: 'string', optional: true }, + { name: 'metadata.algorithm.container.checksum', type: 'string', optional: true } + ] +} diff --git a/tests/units/typesense.spec.ts b/tests/units/typesense.spec.ts new file mode 100644 index 000000000..4c96bf849 --- /dev/null +++ b/tests/units/typesense.spec.ts @@ -0,0 +1,229 @@ +import 'jest' +import Typesense, { TypesenseCollections } from '../../src/components/database/typesense' +import { Logger } from 'winston' +import { TypesenseConfigOptions } from '../../src/@types' +import { ddoSchema } from '../data/ddoSchema' +import { ddo } from '../data/ddo' + +describe('Typesense', () => { + let typesense: Typesense + + beforeAll(() => { + const config: TypesenseConfigOptions = { + apiKey: 'xyz', + nodes: [ + { + host: 'localhost', + port: 8108, + protocol: 'http' + } + ] + } + typesense = new Typesense(config) + }) + + it('instance Typesense', async () => { + expect(typesense).toBeInstanceOf(Typesense) + }) + + it('instance TypesenseCollections', async () => { + const result = typesense.collections() + expect(result).toBeInstanceOf(TypesenseCollections) + }) +}) + +describe('Typesense collections', () => { + let typesense: Typesense + + beforeAll(() => { + const config: TypesenseConfigOptions = { + apiKey: 'xyz', + nodes: [ + { + host: 'localhost', + port: 8108, + protocol: 'http' + } + ], + logLevel: 'debug', + logger: { + debug: (log: any) => console.log(log) + } as Logger + } + typesense = new Typesense(config) + }) + + it('create ddo collection', async () => { + const result = await typesense.collections().create(ddoSchema) + expect(result.enable_nested_fields).toBeTruthy() + expect(result.fields).toBeDefined() + expect(result.name).toEqual(ddoSchema.name) + expect(result.num_documents).toEqual(0) + }) + + it('retrieve collections', async () => { + const result = await typesense.collections().retrieve() + const collection = result[0] + expect(collection.enable_nested_fields).toBeTruthy() + expect(collection.fields).toBeDefined() + expect(collection.name).toEqual(ddoSchema.name) + expect(collection.num_documents).toEqual(0) + }) + + it('retrieve ddo collection', async () => { + const result = await typesense.collections(ddoSchema.name).retrieve() + expect(result.enable_nested_fields).toBeTruthy() + expect(result.fields).toBeDefined() + expect(result.name).toEqual(ddoSchema.name) + expect(result.num_documents).toEqual(0) + }) + + it('update ddo collection', async () => { + const result = await typesense.collections(ddoSchema.name).update({ + fields: [{ name: 'nftAddress', drop: true }] + }) + expect(result.fields).toBeDefined() + expect(result.fields[0].drop).toBeTruthy() + expect(result.fields[0].name).toEqual('nftAddress') + }) + + it('delete ddo collection', async () => { + const result = await typesense.collections(ddoSchema.name).delete() + expect(result.enable_nested_fields).toBeTruthy() + expect(result.fields).toBeDefined() + expect(result.name).toEqual(ddoSchema.name) + }) +}) + +describe('Typesense documents', () => { + let typesense: Typesense + + beforeAll(() => { + const config: TypesenseConfigOptions = { + apiKey: 'xyz', + nodes: [ + { + host: 'localhost', + port: 8108, + protocol: 'http' + } + ], + logLevel: 'debug', + logger: { + debug: (log: any) => console.log(log) + } as Logger + } + typesense = new Typesense(config) + }) + + it('create ddo collection', async () => { + const result = await typesense.collections().create(ddoSchema) + expect(result.enable_nested_fields).toBeTruthy() + expect(result.fields).toBeDefined() + expect(result.name).toEqual(ddoSchema.name) + expect(result.num_documents).toEqual(0) + }) + + it('create document in ddo collection', async () => { + const result = await typesense.collections(ddoSchema.name).documents().create(ddo) + expect(result.id).toEqual(ddo.id) + expect(result.metadata).toBeDefined() + expect(result.metadata.name).toEqual(ddo.metadata.name) + }) + + it('retrieve document in ddo collection', async () => { + const result = await typesense + .collections(ddoSchema.name) + .documents() + .retrieve(ddo.id) + expect(result.id).toEqual(ddo.id) + expect(result.metadata).toBeDefined() + expect(result.metadata.name).toEqual(ddo.metadata.name) + }) + + it('update document in ddo collection', async () => { + const newMetadataName = 'new metadata name' + const result = await typesense + .collections(ddoSchema.name) + .documents() + .update(ddo.id, { + metadata: { + name: newMetadataName + } + }) + expect(result.id).toEqual(ddo.id) + expect(result.metadata).toBeDefined() + expect(result.metadata.name).toEqual(newMetadataName) + }) + + it('delete document in ddo collection', async () => { + const newMetadataName = 'new metadata name' + const result = await typesense.collections(ddoSchema.name).documents().delete(ddo.id) + expect(result.id).toEqual(ddo.id) + expect(result.metadata).toBeDefined() + expect(result.metadata.name).toEqual(newMetadataName) + }) + + it('delete ddo collection', async () => { + const result = await typesense.collections(ddoSchema.name).delete() + expect(result.enable_nested_fields).toBeTruthy() + expect(result.fields).toBeDefined() + expect(result.name).toEqual(ddoSchema.name) + }) +}) + +describe('Typesense documents', () => { + let typesense: Typesense + + beforeAll(() => { + const config: TypesenseConfigOptions = { + apiKey: 'xyz', + nodes: [ + { + host: 'localhost', + port: 8108, + protocol: 'http' + } + ], + logLevel: 'debug', + logger: { + debug: (log: any) => console.log(log) + } as Logger + } + typesense = new Typesense(config) + }) + + it('create ddo collection', async () => { + const result = await typesense.collections().create(ddoSchema) + expect(result.enable_nested_fields).toBeTruthy() + expect(result.fields).toBeDefined() + expect(result.name).toEqual(ddoSchema.name) + expect(result.num_documents).toEqual(0) + }) + + it('create document in ddo collection', async () => { + const result = await typesense.collections(ddoSchema.name).documents().create(ddo) + expect(result.id).toEqual(ddo.id) + expect(result.metadata).toBeDefined() + expect(result.metadata.name).toEqual(ddo.metadata.name) + }) + + it('search document in ddo collection', async () => { + const result = await typesense.collections(ddoSchema.name).documents().search({ + q: 'DEX', + query_by: 'metadata.name', + filter_by: 'chainId:<138', + sort_by: 'version:desc' + }) + expect(result.found).toEqual(1) + expect(result.hits[0]).toBeDefined() + expect(result.hits[0].document).toBeDefined() + }) + + it('delete ddo collection', async () => { + const result = await typesense.collections(ddoSchema.name).delete() + expect(result.enable_nested_fields).toBeTruthy() + expect(result.fields).toBeDefined() + expect(result.name).toEqual(ddoSchema.name) + }) +}) diff --git a/tsconfig.json b/tsconfig.json index 33edc94de..88ee5611b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,10 +18,11 @@ "allowJs": true, "rootDir": "./src", "esModuleInterop": true, + "skipLibCheck": true }, "ts-node": { "esm": true, - }, + }, "include": ["src/**/*"], "exclude": ["node_modules", "**/*.test.ts"] } \ No newline at end of file diff --git a/typesense-compose.yml b/typesense-compose.yml new file mode 100644 index 000000000..a514043c9 --- /dev/null +++ b/typesense-compose.yml @@ -0,0 +1,9 @@ +services: + typesense: + image: typesense/typesense:0.25.1 + restart: on-failure + ports: + - "8108:8108" + volumes: + - ./typesense-data:/data + command: '--data-dir /data --api-key=xyz'