diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index a3f466aa..6e407e03 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -16,7 +16,7 @@ jobs: node-version: '18.x' registry-url: 'https://registry.npmjs.org' - run: npm ci - - run: npm publish --tag beta --access public + - run: npm publish --tag latest --access public env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} publish-git: diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f2705b1..76c3030b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ ## Change log +### Version: 4.0.0-beta.4 +#### Date: March-14-2024 +##### New Features: +- Query operators implementation + +### Version: 4.0.0-beta.3 +#### Date: February-13-2024 + - Live preview support 1.0 and 2.0 + +### Version: v4.0.0-beta.2 +#### Date: February-02-2024 + - Includes adding of prepare script to build package + ### Version: 4.0.0-beta #### Date: January-15-2024 - Beta release of Typescript SDK diff --git a/package-lock.json b/package-lock.json index 9806c357..0f575d3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@contentstack/delivery-sdk", - "version": "4.0.0-beta.3", + "version": "4.0.0-beta.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@contentstack/delivery-sdk", - "version": "4.0.0-beta.3", + "version": "4.0.0-beta.4", "dependencies": { "@contentstack/core": "^1.0.1", "@contentstack/utils": "^1.3.1", diff --git a/package.json b/package.json index 4a11640d..0ad398b9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@contentstack/delivery-sdk", - "version": "4.0.0-beta.3", + "version": "4.0.0-beta.4", "type": "commonjs", "main": "./dist/cjs/src/index.js", "types": "./dist/types/src/index.d.ts", diff --git a/src/lib/query.ts b/src/lib/query.ts index f13569c5..066d53e4 100644 --- a/src/lib/query.ts +++ b/src/lib/query.ts @@ -164,4 +164,139 @@ export class Query extends BaseQuery { getQuery(): { [key: string]: any } { return this._parameters; } + + /** + * @method containedIn + * @memberof Query + * @description Returns the raw (JSON) query based on the filters applied on Query object. + * @example + * import contentstack from '@contentstack/delivery-sdk' + * + * const stack = contentstack.Stack({ apiKey: "apiKey", deliveryToken: "deliveryToken", environment: "environment" }); + * const query = stack.contentType("contentTypeUid").entry().query(); + * const result = await query.containedIn('fieldUid', ['value1', 'value2']).find() + * + * @returns {Query} + */ + containedIn(key: string, value: (string | number | boolean)[]): Query { + this._parameters[key] = { '$in': value }; + return this; + } + + /** + * @method NoContainedIn + * @memberof Query + * @description Returns the raw (JSON) query based on the filters applied on Query object. + * @example + * import contentstack from '@contentstack/delivery-sdk' + * + * const stack = contentstack.Stack({ apiKey: "apiKey", deliveryToken: "deliveryToken", environment: "environment" }); + * const query = stack.contentType("contentTypeUid").entry().query(); + * const result = await query.notContainedIn('fieldUid', ['value1', 'value2']).find() + * + * @returns {Query} + */ + notContainedIn(key: string, value: (string | number | boolean)[]): Query { + this._parameters[key] = { '$nin': value }; + return this; + } + + /** + * @method notExists + * @memberof Query + * @description Returns the raw (JSON) query based on the filters applied on Query object. + * @example + * import contentstack from '@contentstack/delivery-sdk' + * + * const stack = contentstack.Stack({ apiKey: "apiKey", deliveryToken: "deliveryToken", environment: "environment" }); + * const query = stack.contentType("contentTypeUid").entry().query(); + * const result = await query.notExists('fieldUid').find() + * + * @returns {Query} + */ + notExists(key: string): Query { + this._parameters[key] = { '$exists': false }; + return this; + } + + /** + * @method or + * @memberof Query + * @description Returns the raw (JSON) query based on the filters applied on Query object. + * @example + * import contentstack from '@contentstack/delivery-sdk' + * + * const stack = contentstack.Stack({ apiKey: "apiKey", deliveryToken: "deliveryToken", environment: "environment" }); + * const query1 = stack.contentType('contenttype_uid').Entry().query().containedIn('fieldUID', ['value']); + * const query2 = stack.contentType('contenttype_uid').Entry().query().where('fieldUID', QueryOperation.EQUALS, 'value2'); + * const query = await stack.contentType('contenttype_uid').Entry().query().or(query1, query2).find(); + * + * @returns {Query} + */ + or(...queries: Query[]): Query { + const paramsList: BaseQueryParameters[] = []; + for (const queryItem of queries) { + paramsList.push(queryItem._parameters); + } + this._parameters.$or = paramsList; + return this; + } + + /** + * @method and + * @memberof Query + * @description Returns the raw (JSON) query based on the filters applied on Query object. + * @example + * import contentstack from '@contentstack/delivery-sdk' + * + * const stack = contentstack.Stack({ apiKey: "apiKey", deliveryToken: "deliveryToken", environment: "environment" }); + * const query1 = stack.contentType('contenttype_uid').Entry().query().containedIn('fieldUID', ['value']); + * const query2 = stack.contentType('contenttype_uid').Entry().query().where('fieldUID', QueryOperation.EQUALS, 'value2'); + * const query = await stack.contentType('contenttype_uid').Entry().query().and(query1, query2).find(); + * + * @returns {Query} + */ + and(...queries: Query[]): Query { + const paramsList: BaseQueryParameters[] = []; + for (const queryItem of queries) { + paramsList.push(queryItem._parameters); + } + this._parameters.$and = paramsList; + return this; + } + + /** + * @method equalTo + * @memberof Query + * @description Returns the raw (JSON) query based on the filters applied on Query object. + * @example + * import contentstack from '@contentstack/delivery-sdk' + * + * const stack = contentstack.Stack({ apiKey: "apiKey", deliveryToken: "deliveryToken", environment: "environment" }); + * const query = await stack.contentType('contenttype_uid').Entry().query().equalTo('fieldUid', 'value').find(); + * + * @returns {Query} + */ + equalTo(key: string, value: string | number | boolean): Query { + this._parameters[key] = value; + return this; + } + + /** + * @method equalTo + * @memberof Query + * @description Returns the raw (JSON) query based on the filters applied on Query object. + * @example + * import contentstack from '@contentstack/delivery-sdk' + * + * const stack = contentstack.Stack({ apiKey: "apiKey", deliveryToken: "deliveryToken", environment: "environment" }); + * const query = stack.contentType('contenttype_uid').query().where('title', QueryOperation.EQUALS, 'value'); + * const entryQuery = await stack.contentType('contenttype_uid').query().referenceIn('reference_uid', query).find(); + * + * @returns {Query} + */ + referenceIn(key: string, query: Query) { + this._parameters[key] = { '$in_query': query._parameters } + return this; +} } diff --git a/test/api/contenttype.spec.ts b/test/api/contenttype.spec.ts index 540c687b..f88fac56 100644 --- a/test/api/contenttype.spec.ts +++ b/test/api/contenttype.spec.ts @@ -1,6 +1,5 @@ /* eslint-disable no-console */ /* eslint-disable promise/always-return */ -import { BaseContentType, BaseEntry } from 'src'; import { ContentType } from '../../src/lib/content-type'; import { stackInstance } from '../utils/stack-instance'; import { TContentType, TEntry } from './types'; @@ -26,6 +25,7 @@ describe('ContentType API test cases', () => { expect(result.schema).toBeDefined(); }); }); + function makeContentType(uid = ''): ContentType { const contentType = stack.ContentType(uid); diff --git a/test/api/entry-queryables.spec.ts b/test/api/entry-queryables.spec.ts new file mode 100644 index 00000000..feb15b1f --- /dev/null +++ b/test/api/entry-queryables.spec.ts @@ -0,0 +1,124 @@ +import { stackInstance } from '../utils/stack-instance'; +import { Entries } from '../../src/lib/entries'; +import { TEntry } from './types'; +import { QueryOperation } from '../../src/lib/types'; +import { Query } from '../../src/lib/query'; + +const stack = stackInstance(); + +describe('Query Operators API test cases', () => { + it('should get entries which matches the fieldUid and values', async () => { + const query = await makeEntries('contenttype_uid').query().containedIn('title', ['value']).find() + if (query.entries) { + expect(query.entries[0]._version).toBeDefined(); + expect(query.entries[0].title).toBeDefined(); + expect(query.entries[0].uid).toBeDefined(); + expect(query.entries[0].created_at).toBeDefined(); + } + }); + + it('should get entries which does not match the fieldUid and values', async () => { + const query = await makeEntries('contenttype_uid').query().notContainedIn('title', ['test', 'test2']).find() + if (query.entries) { + expect(query.entries[0]._version).toBeDefined(); + expect(query.entries[0].title).toBeDefined(); + expect(query.entries[0].uid).toBeDefined(); + expect(query.entries[0].created_at).toBeDefined(); + } + }); + + it('should get entries which does not match the fieldUid - notExists', async () => { + const query = await makeEntries('contenttype_uid').query().notExists('multi_line').find() + if (query.entries) { + expect(query.entries[0]._version).toBeDefined(); + expect(query.entries[0].title).toBeDefined(); + expect(query.entries[0].uid).toBeDefined(); + expect(query.entries[0].created_at).toBeDefined(); + expect((query.entries[0] as any).multi_line).not.toBeDefined() + } + }); + + it('should return entries matching any of the conditions - or', async () => { + const query1: Query = await makeEntries('contenttype_uid').query().containedIn('title', ['value']); + const query2: Query = await makeEntries('contenttype_uid').query().where('title', QueryOperation.EQUALS, 'value2'); + const query = await makeEntries('contenttype_uid').query().or(query1, query2).find(); + + if (query.entries) { + expect(query.entries.length).toBeGreaterThan(0); + expect(query.entries[0]._version).toBeDefined(); + expect(query.entries[0].locale).toBeDefined(); + expect(query.entries[0].uid).toBeDefined(); + expect(query.entries[0].title).toBe('value2'); + expect(query.entries[1]._version).toBeDefined(); + expect(query.entries[1].locale).toBeDefined(); + expect(query.entries[1].uid).toBeDefined(); + expect(query.entries[1].title).toBe('value'); + } + }); + + it('should return entries when at least 1 entry condition is matching - or', async () => { + const query1: Query = await makeEntries('contenttype_uid').query().containedIn('title', ['value0']); + const query2: Query = await makeEntries('contenttype_uid').query().where('title', QueryOperation.EQUALS, 'value2'); + const query = await makeEntries('contenttype_uid').query().or(query1, query2).find(); + + if (query.entries) { + expect(query.entries.length).toBeGreaterThan(0); + expect(query.entries[0]._version).toBeDefined(); + expect(query.entries[0].locale).toBeDefined(); + expect(query.entries[0].uid).toBeDefined(); + expect(query.entries[0].title).toBe('value2'); + } + }); + + it('should return entry both conditions are matching - and', async () => { + const query1: Query = await makeEntries('contenttype_uid').query().containedIn('title', ['value']); + const query2: Query = await makeEntries('contenttype_uid').query().where('locale', QueryOperation.EQUALS, 'en-us'); + const query = await makeEntries('contenttype_uid').query().and(query1, query2).find(); + + if (query.entries) { + expect(query.entries.length).toBeGreaterThan(0); + expect(query.entries[0]._version).toBeDefined(); + expect(query.entries[0].locale).toBeDefined(); + expect(query.entries[0].uid).toBeDefined(); + expect(query.entries[0].title).toBe('value'); + } + }); + + it('should return null when any one condition is not matching - and', async () => { + const query1: Query = await makeEntries('contenttype_uid').query().containedIn('title', ['value0']); + const query2: Query = await makeEntries('contenttype_uid').query().where('locale', QueryOperation.EQUALS, 'fr-fr'); + const query = await makeEntries('contenttype_uid').query().and(query1, query2).find(); + + if (query.entries) { + expect(query.entries).toHaveLength(0); + + } + }); + + it('should return entry equal to the condition - equalTo', async () => { + const query = await makeEntries('contenttype_uid').query().equalTo('title', 'value').find(); + + if (query.entries) { + expect(query.entries[0]._version).toBeDefined(); + expect(query.entries[0].locale).toBeDefined(); + expect(query.entries[0].uid).toBeDefined(); + expect(query.entries[0].title).toBe('value'); + } + }); + + it('should return entry for referencedIn query', async () => { + const query = makeEntries('contenttype_uid').query().where('title', QueryOperation.EQUALS, 'value'); + const entryQuery = await makeEntries('contenttype_uid').query().referenceIn('reference_uid', query).find(); + if (entryQuery.entries) { + expect(entryQuery.entries[0]._version).toBeDefined(); + expect(entryQuery.entries[0].locale).toBeDefined(); + expect(entryQuery.entries[0].uid).toBeDefined(); + expect(entryQuery.entries[0].title).toBe('test'); + } + }); +}); + +function makeEntries(contentTypeUid = ''): Entries { + const entries = stack.ContentType(contentTypeUid).Entry(); + return entries; +} \ No newline at end of file diff --git a/test/unit/contenttype.spec.ts b/test/unit/contenttype.spec.ts index d4544b15..6db9d2ca 100644 --- a/test/unit/contenttype.spec.ts +++ b/test/unit/contenttype.spec.ts @@ -5,6 +5,7 @@ import { Entry } from '../../src/lib/entry'; import { contentTypeResponseMock } from '../utils/mocks'; import { Entries } from '../../src/lib/entries'; import { MOCK_CLIENT_OPTIONS } from '../utils/constant'; +import { Query } from 'src/lib/query'; describe('ContentType class', () => { let contentType: ContentType; diff --git a/test/unit/entry-queryable.spec.ts b/test/unit/entry-queryable.spec.ts new file mode 100644 index 00000000..3a97d5da --- /dev/null +++ b/test/unit/entry-queryable.spec.ts @@ -0,0 +1,57 @@ +import { AxiosInstance, httpClient } from '@contentstack/core'; +import { ContentType } from '../../src/lib/content-type'; +import MockAdapter from 'axios-mock-adapter'; +import { MOCK_CLIENT_OPTIONS } from '../utils/constant'; +import { Query } from '../../src/lib/query'; +import { QueryOperation } from '../../src/lib/types'; + + +describe('Query Operators API test cases', () => { + let contentType: ContentType; + let client: AxiosInstance; + let mockClient: MockAdapter; + + beforeAll(() => { + client = httpClient(MOCK_CLIENT_OPTIONS); + mockClient = new MockAdapter(client as any); + }); + + beforeEach(() => { + contentType = new ContentType(client, 'contentTypeUid'); + }); + it('should get entries which matches the fieldUid and values', () => { + const query = contentType.Entry().query().containedIn('fieldUID', ['value']); + expect(query._parameters).toStrictEqual({'fieldUID': {'$in': ['value']}}); + }); + it('should get entries which does not match the fieldUid and values', () => { + const query = contentType.Entry().query().notContainedIn('fieldUID', ['value', 'value2']); + expect(query._parameters).toStrictEqual({'fieldUID': {'$nin': ['value', 'value2']}}); + }); + it('should get entries which does not match the fieldUid - notExists', () => { + const query = contentType.Entry().query().notExists('fieldUID'); + expect(query._parameters).toStrictEqual({'fieldUID': {'$exists': false}}); + }); + it('should return entries matching any of the conditions - or', async () => { + const query1: Query = await contentType.Entry().query().containedIn('fieldUID', ['value']); + const query2: Query = await contentType.Entry().query().where('fieldUID', QueryOperation.EQUALS, 'value2'); + const query = await contentType.Entry().query().or(query1, query2); + expect(query._parameters).toStrictEqual({ '$or': [ {'fieldUID': {'$in': ['value']}}, { 'fieldUID': 'value2' } ] }); + }); + it('should return entry when both conditions are matching - and', async () => { + const query1: Query = await contentType.Entry().query().containedIn('fieldUID', ['value']); + const query2: Query = await contentType.Entry().query().where('fieldUID', QueryOperation.EQUALS, 'value2'); + const query = await contentType.Entry().query().and(query1, query2); + expect(query._parameters).toStrictEqual({ '$and': [ {'fieldUID': {'$in': ['value']}}, { 'fieldUID': 'value2' } ] }); + }); + it('should return entry equal to the condition - equalTo', async () => { + const query = contentType.Entry().query().equalTo('fieldUID', 'value'); + expect(query._parameters).toStrictEqual({ 'fieldUID': 'value' }); + }); + it('should return entry for referencedIn query', async () => { + const query1 = contentType.Entry().query().containedIn('fieldUID', ['value']); + const entryQuery = await contentType.Entry().query().referenceIn('reference_uid', query1); + if (entryQuery) { + expect(entryQuery._parameters).toEqual({ reference_uid: { '$in_query': { fieldUID: { '$in': [ 'value' ] } } } }); + } + }); +}); \ No newline at end of file