diff --git a/.vscode/extensions.json b/.vscode/extensions.json index a14db9f6..99a6f0fb 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,5 +1,6 @@ { "recommendations": [ + "42crunch.vscode-openapi", "bierner.markdown-preview-github-styles", "davidanson.vscode-markdownlint", "dbaeumer.vscode-eslint", diff --git a/.vscode/settings.json b/.vscode/settings.json index 1884b3a5..b5c01832 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,7 +3,7 @@ "editor.defaultFormatter": "vscode.html-language-features" }, "[javascript]": { - "editor.defaultFormatter": "dbaeumer.vscode-eslint" + "editor.defaultFormatter": "vscode.typescript-language-features" }, "[json]": { "editor.defaultFormatter": "vscode.json-language-features" @@ -16,8 +16,10 @@ }, "coverage-gutters.showGutterCoverage": false, "coverage-gutters.showLineCoverage": true, - "editor.defaultFormatter": "dbaeumer.vscode-eslint", + "editor.defaultFormatter": "vscode.typescript-language-features", "editor.formatOnSave": true, - "eslint.format.enable": true, "files.insertFinalNewline": true, + "[yaml]": { + "editor.defaultFormatter": "redhat.vscode-yaml" + }, } diff --git a/app/src/controllers/object.js b/app/src/controllers/object.js index dddd851a..3b83c83e 100644 --- a/app/src/controllers/object.js +++ b/app/src/controllers/object.js @@ -929,14 +929,20 @@ const controller = { public: isTruthy(req.query.public), active: isTruthy(req.query.active), deleteMarker: isTruthy(req.query.deleteMarker), - latest: isTruthy(req.query.latest) + latest: isTruthy(req.query.latest), + page: req.query.page, + limit: req.query.limit, + sort: req.query.sort, + order: req.query.order, + permissions: isTruthy(req.query.permissions) }; // if scoping to current user permissions on objects if (config.has('server.privacyMask')) { params.userId = await userService.getCurrentUserId(getCurrentIdentity(req.currentUser, SYSTEM_USER)); } const response = await objectService.searchObjects(params); - res.status(200).json(response); + res.setHeader('X-Total-Rows', response.total); + res.status(200).json(response.data); } catch (error) { next(error); } diff --git a/app/src/db/models/tables/objectModel.js b/app/src/db/models/tables/objectModel.js index e391d203..066b985b 100644 --- a/app/src/db/models/tables/objectModel.js +++ b/app/src/db/models/tables/objectModel.js @@ -176,6 +176,12 @@ class ObjectModel extends Timestamps(Model) { }); }); } + }, + pagination(query, page, limit) { + if (page && limit) query.page(page - 1, limit); + }, + sortOrder(query, column, order = 'asc') { + if (column) query.orderBy(column, order); } }; } diff --git a/app/src/docs/v1.api-spec.yaml b/app/src/docs/v1.api-spec.yaml index c888ce95..10ae90cb 100644 --- a/app/src/docs/v1.api-spec.yaml +++ b/app/src/docs/v1.api-spec.yaml @@ -401,6 +401,11 @@ paths: - $ref: "#/components/parameters/Query-Public" - $ref: "#/components/parameters/Query-MimeType" - $ref: "#/components/parameters/Query-Name" + - $ref: "#/components/parameters/Query-Permissions" + - $ref: "#/components/parameters/Query-Page" + - $ref: "#/components/parameters/Query-Limit" + - $ref: "#/components/parameters/Query-Sort" + - $ref: "#/components/parameters/Query-Order" - $ref: "#/components/parameters/Query-TagSet" responses: "200": @@ -1738,6 +1743,61 @@ components: schema: type: boolean example: true + Query-Permissions: + in: query + name: permissions + description: >- + Boolean representing whether or not to include matching permissions + schema: + type: boolean + example: true + Query-Page: + in: query + name: page + description: >- + The index of the page to return. The index of first page is 1. + Must specify limit when defined. + schema: + type: number + example: 1 + Query-Limit: + in: query + name: limit + description: >- + The page size, number of objects in a page. + Must specify page number when defined. + schema: + type: number + example: 10 + Query-Sort: + in: query + name: sort + description: >- + Any possible object Column. + schema: + type: string + enum: + - id + - path + - public + - active + - createdBy + - updatedBy + - updatedAt + - bucketId + - name + example: name + Query-Order: + in: query + name: order + description: >- + Order in ascending or descending. Default to ascending order. + schema: + type: string + enum: + - asc + - desc + example: asc Query-Path: in: query name: path diff --git a/app/src/services/object.js b/app/src/services/object.js index 736bb20b..55704334 100644 --- a/app/src/services/object.js +++ b/app/src/services/object.js @@ -117,11 +117,13 @@ const service = { */ searchObjects: async (params, etrx = undefined) => { let trx; + let response = []; try { trx = etrx ? etrx : await ObjectModel.startTransaction(); - const response = await ObjectModel.query(trx) - .allowGraph('version') + response.data = await ObjectModel.query(trx) + .allowGraph('objectPermission') + .withGraphFetched('objectPermission') .modify('filterIds', params.id) .modify('filterBucketIds', params.bucketId) .modify('filterName', params.name) @@ -136,12 +138,24 @@ const service = { tag: params.tag }) .modify('hasPermission', params.userId, 'READ') - // format result + .modify('pagination', params.page, params.limit) + .modify('sortOrder', params.sort, params.order) .then(result => { - // just return object table records - const res = result.map(row => { + let resultObject = []; + if (Object.hasOwn(result, 'results')) { + resultObject = result.results; + response.total = result.total; + } else { + resultObject = result; + response.total = result.length; + } + + const res = resultObject.map(row => { // eslint-disable-next-line no-unused-vars const { objectPermission, bucketPermission, version, ...object } = row; + if (params.permissions) { + object.objectPermissions = objectPermission.map(o => o.permCode); + } return object; }); // remove duplicates diff --git a/app/src/validators/common.js b/app/src/validators/common.js index d4c2c174..296711c4 100644 --- a/app/src/validators/common.js +++ b/app/src/validators/common.js @@ -82,6 +82,13 @@ const scheme = { string: oneOrMany(Joi.string().max(255)), + pagination: (sortList) => ({ + page: Joi.number().min(1), + limit: Joi.number().min(0), + sort: Joi.string().valid(...sortList), + order: Joi.string().valid('asc', 'desc'), + }), + permCode: oneOrMany(Joi.string().valid(...Object.values(Permissions))) }; diff --git a/app/src/validators/object.js b/app/src/validators/object.js index bfd66caa..497f13be 100644 --- a/app/src/validators/object.js +++ b/app/src/validators/object.js @@ -137,7 +137,11 @@ const schema = { public: type.truthy, active: type.truthy, deleteMarker: type.truthy, - latest: type.truthy + latest: type.truthy, + permissions: type.truthy, + ...scheme.pagination( + ['id', 'path', 'public', 'active', 'createdBy', 'updatedBy', 'updatedAt', 'bucketId', 'name'] + ), }) }, diff --git a/app/tests/unit/services/object.spec.js b/app/tests/unit/services/object.spec.js index efdedeb3..b3b1f9de 100644 --- a/app/tests/unit/services/object.spec.js +++ b/app/tests/unit/services/object.spec.js @@ -134,6 +134,8 @@ describe('searchObjects', () => { tag: params.tag }); expect(ObjectModel.modify).toHaveBeenNthCalledWith(11, 'hasPermission', params.userId, 'READ'); + expect(ObjectModel.modify).toHaveBeenNthCalledWith(12, 'pagination', params.page, params.limit); + expect(ObjectModel.modify).toHaveBeenNthCalledWith(13, 'sortOrder', params.sort, params.order); expect(ObjectModel.then).toHaveBeenCalledTimes(1); expect(objectModelTrx.commit).toHaveBeenCalledTimes(1); }); diff --git a/app/tests/unit/validators/object.spec.js b/app/tests/unit/validators/object.spec.js index 61d62c3e..3e156cc9 100644 --- a/app/tests/unit/validators/object.spec.js +++ b/app/tests/unit/validators/object.spec.js @@ -512,6 +512,47 @@ describe('searchObjects', () => { expect(active).toEqual(type.truthy.describe()); }); }); + + describe('page', () => { + const page = query.keys.page; + + it('is a number', () => { + expect(page.type).toEqual('number'); + }); + }); + + describe('limit', () => { + const limit = query.keys.limit; + + it('is a number', () => { + expect(limit.type).toEqual('number'); + }); + }); + + describe('sort', () => { + const sort = query.keys.sort; + + it('is a string', () => { + expect(sort.type).toEqual('string'); + }); + }); + + describe('order', () => { + const order = query.keys.order; + + it('is a string', () => { + expect(order.type).toEqual('string'); + }); + }); + + describe('permissions', () => { + const permissions = query.keys.permissions; + + it('is the expected schema', () => { + expect(permissions).toEqual(type.truthy.describe()); + }); + }); + }); });