diff --git a/libraries/grpc-sdk/src/modules/authorization/index.ts b/libraries/grpc-sdk/src/modules/authorization/index.ts index 2c9f0d7c4..e23c5ff68 100644 --- a/libraries/grpc-sdk/src/modules/authorization/index.ts +++ b/libraries/grpc-sdk/src/modules/authorization/index.ts @@ -76,4 +76,8 @@ export class Authorization extends ConduitModule createResourceAccessList(data: ResourceAccessListRequest): Promise { return this.client!.createResourceAccessList(data); } + + getAuthorizedQuery(data: ResourceAccessListRequest): Promise { + return this.client!.getAuthorizedQuery(data).then(r => JSON.parse(r.query)); + } } diff --git a/modules/authorization/src/Authorization.ts b/modules/authorization/src/Authorization.ts index c76e90817..d8a6f805a 100644 --- a/modules/authorization/src/Authorization.ts +++ b/modules/authorization/src/Authorization.ts @@ -16,6 +16,7 @@ import { Decision, DeleteResourceRequest, FindRelationRequest, + GetAuthorizedQueryResponse, PermissionCheck, PermissionRequest, Relation, @@ -56,6 +57,7 @@ export default class Authorization extends ManagedModule { getAllowedResources: this.getAllowedResources.bind(this), can: this.can.bind(this), createResourceAccessList: this.createResourceAccessList.bind(this), + getAuthorizedQuery: this.getAuthorizedQuery.bind(this), }, }; protected metricsSchema = metricsSchema; @@ -209,6 +211,19 @@ export default class Authorization extends ManagedModule { callback(null); } + async getAuthorizedQuery( + call: GrpcRequest, + callback: GrpcResponse, + ) { + const { subject, action, resourceType } = call.request; + const query = await this.permissionsController.getAuthorizedQuery( + subject, + action, + resourceType, + ); + callback(null, { query: JSON.stringify(query) }); + } + async can(call: GrpcRequest, callback: GrpcResponse) { const { subject, resource, actions } = call.request; let allow = false; diff --git a/modules/authorization/src/authorization.proto b/modules/authorization/src/authorization.proto index c7787dfec..3fba74472 100644 --- a/modules/authorization/src/authorization.proto +++ b/modules/authorization/src/authorization.proto @@ -65,6 +65,10 @@ message AllowedResourcesResponse { int32 count = 2; } +message GetAuthorizedQueryResponse { + string query = 1; +} + message ResourceAccessListRequest { string subject = 1; string action = 2; @@ -101,4 +105,5 @@ service Authorization { rpc Can(PermissionCheck) returns (Decision); rpc GetAllowedResources(AllowedResourcesRequest) returns (AllowedResourcesResponse); rpc CreateResourceAccessList(ResourceAccessListRequest) returns (google.protobuf.Empty); + rpc GetAuthorizedQuery(ResourceAccessListRequest) returns (GetAuthorizedQueryResponse); } diff --git a/modules/authorization/src/controllers/permissions.controller.ts b/modules/authorization/src/controllers/permissions.controller.ts index 96d296e76..8e66c1372 100644 --- a/modules/authorization/src/controllers/permissions.controller.ts +++ b/modules/authorization/src/controllers/permissions.controller.ts @@ -1,7 +1,9 @@ import ConduitGrpcSdk from '@conduitplatform/grpc-sdk'; import { + AccessListQueryParams, checkRelation, computePermissionTuple, + getMongoAccessListQuery, getPostgresAccessListQuery, getSQLAccessListQuery, } from '../utils'; @@ -65,7 +67,7 @@ export class PermissionsController { } // if the actor is the object itself, all permissions are provided if (subject === object) { - await RuleCache.storeResolution(this.grpcSdk, computedTuple, true); + RuleCache.storeResolution(this.grpcSdk, computedTuple, true); return true; } @@ -73,13 +75,13 @@ export class PermissionsController { computedTuple, }); if (permission) { - await RuleCache.storeResolution(this.grpcSdk, computedTuple, true); + RuleCache.storeResolution(this.grpcSdk, computedTuple, true); return true; } const index = await this.indexController.findIndex(subject, action, object); - await RuleCache.storeResolution(this.grpcSdk, computedTuple, index ?? false); + RuleCache.storeResolution(this.grpcSdk, computedTuple, index ?? false); return index ?? false; } @@ -128,124 +130,35 @@ export class PermissionsController { } async createAccessList(subject: string, action: string, objectType: string) { - const computedTuple = `${subject}#${action}@${objectType}`; - const objectTypeCollection = await this.grpcSdk - .database!.getSchema(objectType) - .then(r => r.collectionName); - const dbType = await this.grpcSdk.database!.getDatabaseType().then(r => r.result); + const query = await this.getAuthorizedQuery(subject, action, objectType); await this.grpcSdk.database?.createView( objectType, createHash('sha256').update(`${objectType}_${subject}_${action}`).digest('hex'), ['Permission', 'ActorIndex', 'ObjectIndex'], - { - mongoQuery: [ - // permissions lookup won't work this way - { - $lookup: { - from: 'cnd_permissions', - let: { x_id: { $toString: '$_id' } }, - pipeline: [ - { - $match: { - $expr: { - $eq: [ - '$computedTuple', - { $concat: [`${subject}#${action}@${objectType}:`, '$$x_id'] }, - ], - }, - }, - }, - ], - as: 'permissions', - }, - }, - { - $lookup: { - from: 'cnd_actorindexes', - let: { - subject: subject, - }, - pipeline: [ - { - $match: { - $expr: { - $eq: ['$subject', '$$subject'], - }, - }, - }, - ], - as: 'actors', - }, - }, - { - $lookup: { - from: 'cnd_objectindexes', - let: { - id_action: { - $concat: [`${objectType}:`, { $toString: '$_id' }, `#${action}`], - }, - entities: '$actors.entity', - }, - pipeline: [ - { - $match: { - $and: [ - { - $expr: { - $eq: ['$subject', '$$id_action'], - }, - }, - { - $expr: { - $in: ['$entity', '$$entities'], - }, - }, - ], - }, - }, - ], - as: 'intersection', - }, - }, - { - $match: { - $or: [ - { - 'intersection.0': { $exists: true }, - }, - { - 'permissions.0': { $exists: true }, - }, - ], - }, - }, - { - $project: { - actors: 0, - objects: 0, - permissions: 0, - intersection: 0, - }, - }, - ], - sqlQuery: - dbType === 'PostgreSQL' - ? getPostgresAccessListQuery( - objectTypeCollection, - computedTuple, - subject, - objectType, - action, - ) - : getSQLAccessListQuery( - objectTypeCollection, - computedTuple, - subject, - objectType, - action, - ), - }, + query, ); return; } + + async getAuthorizedQuery(subject: string, action: string, objectType: string) { + const computedTuple = `${subject}#${action}@${objectType}`; + const objectTypeCollection = await this.grpcSdk + .database!.getSchema(objectType) + .then(r => r.collectionName); + const dbType = await this.grpcSdk.database!.getDatabaseType().then(r => r.result); + const params: AccessListQueryParams = { + objectTypeCollection, + computedTuple, + subject, + objectType, + action, + }; + return { + mongoQuery: getMongoAccessListQuery(params), + sqlQuery: + dbType === 'PostgreSQL' + ? getPostgresAccessListQuery(params) + : getSQLAccessListQuery(params), + }; + } } diff --git a/modules/authorization/src/utils/index.ts b/modules/authorization/src/utils/index.ts index eb0b787da..b984b60b7 100644 --- a/modules/authorization/src/utils/index.ts +++ b/modules/authorization/src/utils/index.ts @@ -31,56 +31,138 @@ export const computePermissionTuple = ( return `${subject}#${relation}@${object}`; }; -export function getPostgresAccessListQuery( - objectTypeCollection: string, - computedTuple: string, - subject: string, - objectType: string, - action: string, -) { - return ` - SELECT s.* FROM "${objectTypeCollection}" as s - INNER JOIN ( - ( - SELECT obj.entity - FROM ( - SELECT * FROM "cnd_ActorIndex" - WHERE subject = '${subject}' - ) as actors - INNER JOIN ( - SELECT * FROM "cnd_ObjectIndex" - WHERE subjectType = '${objectType}' AND subjectPermission = '${action}' - ) as obj - ON actors.entity = obj.entity - ) - UNION ( - SELECT "computedTuple" - FROM "cnd_Permission" - WHERE "computedTuple" LIKE '${computedTuple}%' - ) - ) idx - ON idx.entity LIKE '%' || TEXT(s._id) || '%' - `; +export interface AccessListQueryParams { + objectTypeCollection: string; + computedTuple: string; + subject: string; + objectType: string; + action: string; } -export function getSQLAccessListQuery( - objectTypeCollection: string, - computedTuple: string, - subject: string, - objectType: string, - action: string, -) { - return `SELECT ${objectTypeCollection}.* FROM ${objectTypeCollection} +export function getPostgresAccessListQuery(params: AccessListQueryParams) { + const { objectTypeCollection, computedTuple, subject, objectType, action } = params; + return `SELECT "${objectTypeCollection}".* FROM "${objectTypeCollection}" INNER JOIN ( - SELECT * FROM cnd_Permission - WHERE computedTuple LIKE '${computedTuple}%' - ) permissions ON permissions.computedTuple = '${computedTuple}:' || ${objectTypeCollection}._id + SELECT * FROM "cnd_Permission" + WHERE "computedTuple" LIKE '${computedTuple}%' + ) permissions ON permissions."computedTuple" = '${computedTuple}:' || "${objectTypeCollection}"._id INNER JOIN ( - SELECT * FROM cnd_ActorIndex + SELECT * FROM "cnd_ActorIndex" WHERE subject = '${subject}' ) actors ON 1=1 INNER JOIN ( - SELECT * FROM cnd_ObjectIndex - WHERE subjectType = '${objectType}' AND subjectPermission = '${action}' + SELECT * FROM "cnd_ObjectIndex" + WHERE subject LIKE '${objectType}:%#${action}' ) objects ON actors.entity = objects.entity;`; } + +export function getSQLAccessListQuery(params: AccessListQueryParams) { + const { objectTypeCollection, computedTuple, subject, objectType, action } = params; + return `SELECT ${objectTypeCollection}.* FROM ${objectTypeCollection} + INNER JOIN ( + SELECT * FROM cnd_Permission + WHERE computedTuple LIKE '${computedTuple}%' + ) permissions ON permissions.computedTuple = '${computedTuple}:' || ${objectTypeCollection}._id + INNER JOIN ( + SELECT * FROM cnd_ActorIndex + WHERE subject = '${subject}' + ) actors ON 1=1 + INNER JOIN ( + SELECT * FROM cnd_ObjectIndex + WHERE subject LIKE '${objectType}:%#${action}' + ) objects ON actors.entity = objects.entity;`; +} + +export function getMongoAccessListQuery(params: AccessListQueryParams) { + const { subject, objectType, action } = params; + return [ + // permissions lookup won't work this way + { + $lookup: { + from: 'cnd_permissions', + let: { x_id: { $toString: '$_id' } }, + pipeline: [ + { + $match: { + $expr: { + $eq: [ + '$computedTuple', + { $concat: [`${subject}#${action}@${objectType}:`, '$$x_id'] }, + ], + }, + }, + }, + ], + as: 'permissions', + }, + }, + { + $lookup: { + from: 'cnd_actorindexes', + let: { + subject: subject, + }, + pipeline: [ + { + $match: { + $expr: { + $eq: ['$subject', '$$subject'], + }, + }, + }, + ], + as: 'actors', + }, + }, + { + $lookup: { + from: 'cnd_objectindexes', + let: { + id_action: { + $concat: [`${objectType}:`, { $toString: '$_id' }, `#${action}`], + }, + entities: '$actors.entity', + }, + pipeline: [ + { + $match: { + $and: [ + { + $expr: { + $eq: ['$subject', '$$id_action'], + }, + }, + { + $expr: { + $in: ['$entity', '$$entities'], + }, + }, + ], + }, + }, + ], + as: 'intersection', + }, + }, + { + $match: { + $or: [ + { + 'intersection.0': { $exists: true }, + }, + { + 'permissions.0': { $exists: true }, + }, + ], + }, + }, + { + $project: { + actors: 0, + objects: 0, + permissions: 0, + intersection: 0, + }, + }, + ]; +} diff --git a/modules/database/package.json b/modules/database/package.json index 5fa22f677..0211f14b1 100644 --- a/modules/database/package.json +++ b/modules/database/package.json @@ -32,6 +32,7 @@ "mongodb-extended-json": "^1.11.0", "mongodb-schema": "^11.2.2", "mongoose": "7.5.0", + "mongoose-cast-aggregation": "^0.3.1", "mysql2": "^3.2.0", "object-hash": "^3.0.0", "pg": "^8.11.3", @@ -50,9 +51,9 @@ "registry": "https://npm.pkg.github.com/" }, "devDependencies": { + "@types/dottie": "^2.0.5", "@types/lodash": "^4.14.200", "@types/node": "20.8.10", - "@types/dottie": "^2.0.5", "copyfiles": "^2.4.1", "rimraf": "^5.0.5", "ts-proto": "^1.160.0", diff --git a/modules/database/src/adapters/SchemaAdapter.ts b/modules/database/src/adapters/SchemaAdapter.ts index a7aacea3b..1b733cd6a 100644 --- a/modules/database/src/adapters/SchemaAdapter.ts +++ b/modules/database/src/adapters/SchemaAdapter.ts @@ -10,8 +10,8 @@ export type SingleDocQuery = string | Indexable; export type MultiDocQuery = string | Indexable[]; export type Query = SingleDocQuery | MultiDocQuery; export type ParsedQuery = Indexable; -export type Doc = ParsedQuery; -export type Fields = ParsedQuery; +export type Doc = Indexable; +export type Fields = Indexable; export type Schema = MongooseSchema | SequelizeSchema; export abstract class SchemaAdapter { @@ -123,7 +123,7 @@ export abstract class SchemaAdapter { async getAuthorizedQuery( operation: string, - query: Indexable, + query: ParsedQuery, many: boolean = false, userId?: string, scope?: string, diff --git a/modules/database/src/adapters/mongoose-adapter/MongooseSchema.ts b/modules/database/src/adapters/mongoose-adapter/MongooseSchema.ts index 525e905f3..0b45723ce 100644 --- a/modules/database/src/adapters/mongoose-adapter/MongooseSchema.ts +++ b/modules/database/src/adapters/mongoose-adapter/MongooseSchema.ts @@ -1,6 +1,7 @@ import { Model, Mongoose, + PipelineStage, PopulateOptions, Query as MongooseQuery, Schema, @@ -21,7 +22,7 @@ import ConduitGrpcSdk, { Indexable, UntypedArray, } from '@conduitplatform/grpc-sdk'; -import { cloneDeep, isNil } from 'lodash'; +import { cloneDeep, isEmpty, isNil } from 'lodash'; import { parseQuery } from './parser'; const EJSON = require('mongodb-extended-json'); @@ -71,7 +72,6 @@ export class MongooseSchema extends SchemaAdapter> { ) { await this.createPermissionCheck(options?.userId, options?.scope); const parsedQuery = this.parseStringToQuery(query); - const obj = await this.model.create(parsedQuery).then(r => r.toObject()); await this.addPermissionToData(obj, options); return obj; @@ -123,14 +123,12 @@ export class MongooseSchema extends SchemaAdapter> { scope?: string; populate?: string[]; }, - ): Promise { - let parsedFilter: Indexable | null = parseQuery(this.parseStringToQuery(filterQuery)); - parsedFilter = await this.getAuthorizedQuery( + ) { + const parsedFilterQuery = parseQuery(this.parseStringToQuery(filterQuery)); + const { parsedQuery: parsedFilter } = await this.getAuthorizedIdsQuery( + parsedFilterQuery, 'edit', - parsedFilter, - false, - options?.userId, - options?.scope, + options, ); if (isNil(parsedFilter)) { throw new Error("Document doesn't exist or can't be modified by user."); @@ -139,7 +137,7 @@ export class MongooseSchema extends SchemaAdapter> { if (parsedQuery.hasOwnProperty('$set')) { parsedQuery = parsedQuery['$set']; } - let finalQuery = this.model.findOneAndReplace(parsedFilter!, parsedQuery, { + let finalQuery = this.model.findOneAndReplace(parsedFilter, parsedQuery, { new: true, }); if (options?.populate !== undefined && options?.populate !== null) { @@ -157,13 +155,11 @@ export class MongooseSchema extends SchemaAdapter> { populate?: string[]; }, ): Promise { - let parsedFilter: Indexable | null = parseQuery(this.parseStringToQuery(filterQuery)); - parsedFilter = await this.getAuthorizedQuery( + const parsedFilterQuery = parseQuery(this.parseStringToQuery(filterQuery)); + const { parsedQuery: parsedFilter } = await this.getAuthorizedIdsQuery( + parsedFilterQuery, 'edit', - parsedFilter, - false, - options?.userId, - options?.scope, + options, ); if (isNil(parsedFilter)) { throw new Error("Document doesn't exist or can't be modified by user."); @@ -172,7 +168,7 @@ export class MongooseSchema extends SchemaAdapter> { if (parsedQuery.hasOwnProperty('$set')) { parsedQuery = parsedQuery['$set']; } - let finalQuery = this.model.findOneAndUpdate(parsedFilter!, parsedQuery, { + let finalQuery = this.model.findOneAndUpdate(parsedFilter, parsedQuery, { new: true, }); if (options?.populate !== undefined && options?.populate !== null) { @@ -190,18 +186,15 @@ export class MongooseSchema extends SchemaAdapter> { scope?: string; }, ) { - let parsedFilter: Indexable | null = parseQuery(this.parseStringToQuery(filterQuery)); - parsedFilter = await this.getAuthorizedQuery( + const { parsedQuery: parsedFilter } = await this.getAuthorizedIdsQuery( + parseQuery(this.parseStringToQuery(filterQuery)), 'edit', - parsedFilter, - true, - options?.userId, - options?.scope, + options, ); if (isNil(parsedFilter)) { return []; } - let parsedQuery: Indexable = this.parseStringToQuery(query); + let parsedQuery: ParsedQuery = this.parseStringToQuery(query); if (parsedQuery.hasOwnProperty('$set')) { parsedQuery = parsedQuery['$set']; } @@ -215,19 +208,16 @@ export class MongooseSchema extends SchemaAdapter> { scope?: string; }, ) { - let parsedQuery: Indexable | null = parseQuery(this.parseStringToQuery(query)); - parsedQuery = await this.getAuthorizedQuery( + const { parsedQuery: parsedFilter } = await this.getAuthorizedIdsQuery( + parseQuery(this.parseStringToQuery(query)), 'delete', - parsedQuery, - false, - options?.userId, - options?.scope, + options, ); - if (isNil(parsedQuery)) { + if (isNil(parsedFilter)) { return { deletedCount: 0 }; } return this.model - .deleteOne(parsedQuery!) + .deleteOne(parsedFilter!) .exec() .then(r => ({ deletedCount: r.deletedCount })); } @@ -239,19 +229,16 @@ export class MongooseSchema extends SchemaAdapter> { scope?: string; }, ) { - let parsedQuery: Indexable | null = parseQuery(this.parseStringToQuery(query)); - parsedQuery = await this.getAuthorizedQuery( + const { parsedQuery: parsedFilter } = await this.getAuthorizedIdsQuery( + parseQuery(this.parseStringToQuery(query)), 'delete', - parsedQuery, - true, - options?.userId, - options?.scope, + options, ); - if (isNil(parsedQuery)) { + if (isNil(parsedFilter)) { return { deletedCount: 0 }; } return this.model - .deleteMany(parsedQuery) + .deleteMany(parsedFilter) .exec() .then(r => ({ deletedCount: r.deletedCount })); } @@ -267,20 +254,16 @@ export class MongooseSchema extends SchemaAdapter> { userId?: string; scope?: string; }, - ): Promise { - const { query: filter, modified } = await this.getPaginatedAuthorizedQuery( - 'read', + ) { + const { parsedQuery: parsedFilter, modified } = await this.getAuthorizedIdsQuery( parseQuery(this.parseStringToQuery(query)), - options?.userId, - options?.scope, - options?.skip, - options?.limit, - options?.sort, + 'read', + options, ); - if (isNil(filter)) { + if (isNil(parsedFilter)) { return []; } - let finalQuery = this.model.find(filter, options?.select); + let finalQuery = this.model.find(parsedFilter, options?.select); if (!isNil(options?.skip) && !modified) { finalQuery = finalQuery.skip(options!.skip!); } @@ -305,18 +288,17 @@ export class MongooseSchema extends SchemaAdapter> { populate?: string[]; }, ): Promise { - const parsedQuery: Indexable | null = parseQuery(this.parseStringToQuery(query)); const filter = await this.getAuthorizedQuery( 'read', - parsedQuery, + parseQuery(this.parseStringToQuery(query)), false, options?.userId, options?.scope, ); - if (isNil(filter) && !isNil(parsedQuery)) { + if (isNil(filter)) { return null; } - let finalQuery = this.model.findOne(parsedQuery!, options?.select); + let finalQuery = this.model.findOne(filter, options?.select); if (options?.populate !== undefined && options?.populate !== null) { finalQuery = this.populate(finalQuery, options?.populate); } @@ -330,16 +312,39 @@ export class MongooseSchema extends SchemaAdapter> { scope?: string; }, ) { + const parsedQuery = parseQuery(this.parseStringToQuery(query)); if (!isNil(options?.userId) || !isNil(options?.scope)) { - const view = await this.permissionCheck('read', options?.userId, options?.scope); - if (view) { - return view.countDocuments(query, { - userId: undefined, - scope: undefined, - }); + const authorizedPipeline = await this.getAuthorizedPipeline( + 'read', + options?.userId, + options?.scope, + ); + if (!isEmpty(authorizedPipeline)) { + authorizedPipeline.push( + ...[ + { + $group: { + _id: null, + count: { $sum: 1 }, + }, + }, + { + $project: { + _id: 0, + count: 1, + }, + }, + ], + ); + const pipeline = this.constructAggregationPipeline( + parsedQuery, + authorizedPipeline, + ); + return this.model + .aggregate(pipeline as PipelineStage[]) + .then(r => (!isEmpty(r) ? r[0].count : 0)); } } - const parsedQuery = parseQuery(this.parseStringToQuery(query)); return this.model.find(parsedQuery).countDocuments().exec(); } @@ -356,7 +361,7 @@ export class MongooseSchema extends SchemaAdapter> { public calculatePopulates(population: string[]) { const populates: (string | PopulateOptions)[] = []; - population.forEach((r: string | string[], index: number) => { + population.forEach((r: string | string[]) => { const final = r.toString().trim(); if (final.indexOf('.') !== -1) { let controlBool = true; @@ -443,4 +448,100 @@ export class MongooseSchema extends SchemaAdapter> { private parseSort(sort: { [key: string]: number }): { [p: string]: SortOrder } { return sort as { [p: string]: SortOrder }; } + + private constructAggregationPipeline( + parsedQuery: ParsedQuery, + authorizedQueryPipeline: object[], + options?: { + skip?: number; + limit?: number; + select?: string; + sort?: { [p: string]: number }; + populate?: string[]; + userId?: string; + scope?: string; + }, + ) { + const pipeline = [{ $match: parsedQuery }, ...authorizedQueryPipeline]; + if (!isNil(options?.skip)) { + pipeline.push({ $skip: options?.skip }); + } + if (!isNil(options?.limit)) { + pipeline.push({ $limit: options?.limit }); + } + if (!isNil(options?.sort)) { + pipeline.push({ $sort: this.parseSort(options!.sort) }); + } + return pipeline; + } + + private async getAuthorizedPipeline( + operation: string, + userId?: string, + scope?: string, + ) { + if ( + !this.originalSchema.modelOptions.conduit?.authorization?.enabled || + (isNil(userId) && isNil(scope)) + ) { + return []; + } + const isAvailable = this.grpcSdk.isAvailable('authorization'); + if (!isAvailable) { + throw new Error('Authorization service is not available'); + } + if (scope) { + if (userId) { + const allowed = await this.grpcSdk.authorization?.can({ + subject: `User:${userId}`, + actions: [operation], + resource: scope, + }); + if (!allowed?.allow) { + throw new Error(`User:${userId} is not allowed to ${operation} ${scope}`); + } + } + } + const query = await this.grpcSdk.authorization!.getAuthorizedQuery({ + subject: scope ?? `User:${userId}`, + action: operation, + resourceType: this.originalSchema.name, + }); + return query.mongoQuery; + } + + private async getAuthorizedIdsQuery( + parsedQuery: ParsedQuery, + operation: string, + options?: { + skip?: number; + limit?: number; + select?: string; + sort?: { [p: string]: number }; + populate?: string[]; + userId?: string; + scope?: string; + }, + ) { + const authorizedPipeline = await this.getAuthorizedPipeline( + operation, + options?.userId, + options?.scope, + ); + if (isEmpty(authorizedPipeline)) { + return { parsedQuery, modified: false }; + } + const pipeline = this.constructAggregationPipeline( + parsedQuery, + authorizedPipeline, + options, + ); + const ids = await this.model + .aggregate(pipeline as PipelineStage[]) + .then(r => r.map(r => r._id)); + if (isEmpty(ids)) { + return { parsedQuery: null, modified: false }; + } + return { parsedQuery: { _id: { $in: ids } }, modified: true }; + } } diff --git a/modules/database/src/adapters/mongoose-adapter/index.ts b/modules/database/src/adapters/mongoose-adapter/index.ts index 5f927c6d1..1732eebfc 100644 --- a/modules/database/src/adapters/mongoose-adapter/index.ts +++ b/modules/database/src/adapters/mongoose-adapter/index.ts @@ -23,10 +23,13 @@ import { isNil } from 'lodash'; const EJSON = require('mongodb-extended-json'); const parseSchema = require('mongodb-schema'); +const mongoose = require('mongoose'); +const castAggregation = require('mongoose-cast-aggregation'); +mongoose.plugin(castAggregation); export class MongooseAdapter extends DatabaseAdapter { connected: boolean = false; - mongoose: Mongoose; + private readonly mongoose: Mongoose; connectionString: string; options: ConnectOptions = { minPoolSize: 5, @@ -37,7 +40,7 @@ export class MongooseAdapter extends DatabaseAdapter { constructor(connectionString: string) { super(); this.connectionString = connectionString; - this.mongoose = new Mongoose(); + this.mongoose = mongoose; } async retrieveForeignSchemas(): Promise { @@ -79,7 +82,7 @@ export class MongooseAdapter extends DatabaseAdapter { return; } const model = this.models[modelName]; - let newSchema = model.schema; + const newSchema = model.schema; //@ts-ignore newSchema.name = viewName; //@ts-ignore @@ -330,7 +333,6 @@ export class MongooseAdapter extends DatabaseAdapter { } protected connect() { - this.mongoose = new Mongoose(); ConduitGrpcSdk.Logger.log('Connecting to database...'); this.mongoose .connect(this.connectionString, this.options) diff --git a/modules/database/src/adapters/sequelize-adapter/SequelizeSchema.ts b/modules/database/src/adapters/sequelize-adapter/SequelizeSchema.ts index 91719f129..1755fa135 100644 --- a/modules/database/src/adapters/sequelize-adapter/SequelizeSchema.ts +++ b/modules/database/src/adapters/sequelize-adapter/SequelizeSchema.ts @@ -168,7 +168,6 @@ export class SequelizeSchema extends SchemaAdapter> { }) .then(doc => (doc ? doc.toJSON() : doc)); } - if (parsedQuery.hasOwnProperty('$push')) { const push = parsedQuery['$push']; for (const key in push) { @@ -197,7 +196,6 @@ export class SequelizeSchema extends SchemaAdapter> { await parentDoc.save(); delete parsedQuery['$push']; } - if (Object.keys(parsedQuery).length === 0) { return this.model .findByPk(parsedId, { @@ -262,9 +260,7 @@ export class SequelizeSchema extends SchemaAdapter> { t = await this.sequelize.transaction({ type: Transaction.TYPES.IMMEDIATE }); } const obj = await this.model - .create(parsedQuery, { - transaction: t, - }) + .create(parsedQuery, { transaction: t }) .then(doc => createWithPopulation(this, doc, relationObjects, t)) .then(doc => { if (!transactionProvided) { @@ -301,7 +297,7 @@ export class SequelizeSchema extends SchemaAdapter> { extractRelationsModification(this, parsedQuery[i]); } const docs = await this.model - .bulkCreate(parsedQuery, { transaction: t }) + .bulkCreate(parsedQuery as Indexable[], { transaction: t }) .then(docs => { t.commit(); return docs; @@ -339,7 +335,7 @@ export class SequelizeSchema extends SchemaAdapter> { options?.userId, options?.scope, ); - if (isNil(filter) && !isNil(query)) { + if (isNil(filter)) { return null; } const { filter: parsedFilter, parsingResult } = parseQueryFilter( @@ -566,20 +562,19 @@ export class SequelizeSchema extends SchemaAdapter> { scope?: string; }, ) { - const parsedQuery: ParsedQuery = this.parseStringToQuery(query); - const parsedFilterQuery = await this.getAuthorizedQuery( + const parsedFilter = await this.getAuthorizedQuery( 'edit', this.parseStringToQuery(filterQuery), true, options?.userId, options?.scope, ); - if (isNil(parsedFilterQuery)) { + if (isNil(parsedFilter)) { return []; } const parsingResult = parseQuery( this.originalSchema, - parsedFilterQuery, + parsedFilter, this.adapter.sequelize.getDialect(), this.extractedRelations, {}, @@ -595,7 +590,7 @@ export class SequelizeSchema extends SchemaAdapter> { try { const data = await Promise.all( docs.map(doc => - this.findByIdAndUpdate(doc._id, parsedQuery, { + this.findByIdAndUpdate(doc._id, parsedFilter, { populate: options?.populate, transaction: t, }), diff --git a/modules/database/src/adapters/sequelize-adapter/utils/pathUtils.ts b/modules/database/src/adapters/sequelize-adapter/utils/pathUtils.ts index 5aef7a79b..a4ca316bd 100644 --- a/modules/database/src/adapters/sequelize-adapter/utils/pathUtils.ts +++ b/modules/database/src/adapters/sequelize-adapter/utils/pathUtils.ts @@ -15,9 +15,7 @@ function potentialNesting(field: any) { export function processCreateQuery( query: Indexable, - keyMapping: { - [key: string]: { parentKey: string; childKey: string }; - }, + keyMapping: { [key: string]: { parentKey: string; childKey: string } }, ) { const foundKeys = []; for (const key in keyMapping) { diff --git a/modules/database/src/controllers/cms/schema.controller.ts b/modules/database/src/controllers/cms/schema.controller.ts index 95a797348..8113fc789 100644 --- a/modules/database/src/controllers/cms/schema.controller.ts +++ b/modules/database/src/controllers/cms/schema.controller.ts @@ -9,7 +9,6 @@ import { DatabaseAdapter } from '../../adapters/DatabaseAdapter'; import { MongooseSchema } from '../../adapters/mongoose-adapter/MongooseSchema'; import { SequelizeSchema } from '../../adapters/sequelize-adapter/SequelizeSchema'; import { CmsHandlers } from '../../handlers/cms/crud.handler'; -import { ParsedQuery } from '../../interfaces'; import { status } from '@grpc/grpc-js'; import { isNil } from 'lodash'; @@ -184,7 +183,7 @@ export class SchemaController { }); } - private _registerRoutes(schemas: ParsedQuery) { + private _registerRoutes(schemas: Indexable) { const handlers = new CmsHandlers(this.grpcSdk, this.database); this.router!.addRoutes(sortAndConstructRoutes(schemas, handlers)); } diff --git a/yarn.lock b/yarn.lock index c8a971ff3..c844a39b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9765,6 +9765,11 @@ mongodb@5.8.1, mongodb@^5.0.1: optionalDependencies: "@mongodb-js/saslprep" "^1.1.0" +mongoose-cast-aggregation@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/mongoose-cast-aggregation/-/mongoose-cast-aggregation-0.3.1.tgz#92479e2156b42039a3323066f5840e9707927aed" + integrity sha512-20D7ce6S9QOEjvvtU5gmFzj7X+QAr0UH3M5BAbaWRkvGp4U/XtpH5ngkq8YpppW/mF6Gl+YQNL9s2chLk3DCrA== + mongoose@7.5.0: version "7.5.0" resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-7.5.0.tgz#d10003ffc1ff876d761c7cbca6844ddd2aadd42f"