Skip to content

Commit

Permalink
fix: Fix array relation should not accessible (read/write) on first c…
Browse files Browse the repository at this point in the history
…lass entity (#956)
  • Loading branch information
ktutnik committed Jun 6, 2021
1 parent 904a430 commit 58058a5
Show file tree
Hide file tree
Showing 17 changed files with 434 additions and 93 deletions.
5 changes: 3 additions & 2 deletions packages/core/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,8 @@ function getRoot(rootPath: string, path: string) {
return (part.length === 0) ? undefined : appendRoute(...part)
}

async function findClassRecursive(path: string | string[] | Class | Class[], opt: { directoryAsPath: boolean, rootDir: string }): Promise<ClassWithRoot[]> {
async function findClassRecursive(path: string | string[] | Class | Class[], option?: { directoryAsPath?: boolean, rootDir?: string }): Promise<ClassWithRoot[]> {
const opt = { rootDir: "", directoryAsPath: false, ...option }
if (Array.isArray(path)) {
const result = []
for (const p of path) {
Expand All @@ -153,7 +154,7 @@ async function findClassRecursive(path: string | string[] | Class | Class[], opt
return result
}
if (typeof path === "string") {
const absPath = isAbsolute(path) ? path : join(opt.rootDir, path)
const absPath = isAbsolute(path) ? path : join(opt.rootDir, path)
//read all files and get module reflection
const files = await findFilesRecursive(absPath)
const result = []
Expand Down
4 changes: 2 additions & 2 deletions packages/generic-controller/src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ type EntityWithRelation<T = any, R = any> = [Class<T>, KeyOf<T>, Class<R>?]
interface CreateGenericControllerOption {
config?: GenericControllerConfiguration,
controllers: GenericControllers
normalize?: (entities: Class | EntityWithRelation) => void
normalize: (entities: Class | EntityWithRelation) => void
nameConversion: (x: string) => string
}

Expand Down Expand Up @@ -112,8 +112,8 @@ function createNestedGenericControllerType(type: EntityWithRelation, builder: Co

function createGenericController<T>(type: Class | EntityWithRelation<T>, option: CreateGenericControllerOption) {
const builder = new ControllerBuilder()
option.normalize(type)
if (option.config) option.config(builder)
if (option.normalize) option.normalize(type)
if (Array.isArray(type)) {
return createNestedGenericControllerType(type, builder, option.controllers[1], option.nameConversion)
}
Expand Down
2 changes: 1 addition & 1 deletion packages/jwt/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ async function getPoliciesByFile(root: string, opt: Class<AuthPolicy> | Class<Au
}
if (typeof opt === "string") {
const path = isAbsolute(opt) ? opt : join(root, opt)
const result = await findClassRecursive(path, { rootDir: root, directoryAsPath: false })
const result = await findClassRecursive(path, { rootDir: root })
return getPoliciesByFile(root, result.map(x => x.type))
}
else {
Expand Down
43 changes: 36 additions & 7 deletions packages/mongoose/src/facility.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
import { DefaultFacility, PlumierApplication, RelationDecorator } from "@plumier/core"
import { createQueryParserAnalyzer, FilterQueryAuthorizeMiddleware, OrderQueryAuthorizeMiddleware, SelectQueryAuthorizeMiddleware } from "@plumier/query-parser"
import { Class, DefaultFacility, findClassRecursive, PlumierApplication, RelationDecorator } from "@plumier/core"
import { RequestHookMiddleware } from "@plumier/generic-controller"
import {
FilterQueryAuthorizeMiddleware,
OrderQueryAuthorizeMiddleware,
SelectQueryAuthorizeMiddleware,
} from "@plumier/query-parser"
import reflect from "@plumier/reflect"
import { Result, ResultMessages, VisitorInvocation } from "@plumier/validator"
import Mongoose from "mongoose"
import pluralize from "pluralize"
import { filterConverter, orderConverter, selectConverter } from "./query-parser"

import { getModels, model as globalModel, MongooseHelper, proxy as globalProxy } from "./generator"
import { MongooseControllerGeneric, MongooseNestedControllerGeneric } from "./generic-controller"
import { normalizeEntity } from "./helper"
import { filterConverter, orderConverter, selectConverter } from "./query-parser"
import { ClassOptionDecorator } from "./types"




interface MongooseFacilityOption { uri?: string, helper?: MongooseHelper }
interface MongooseFacilityOption { uri?: string, helper?: MongooseHelper, entity?: Class | Class[] | string | string[] }

function convertValue(value: any, path: string): Result {
if (Array.isArray(value)) {
Expand All @@ -39,11 +44,35 @@ function relationConverter(i: VisitorInvocation): Result {
return i.proceed()
}

async function loadEntities(entity: Class | Class[] | string | string[], opt: { rootDir: string }): Promise<Class[]> {
const classes = await findClassRecursive(entity, { rootDir: opt.rootDir })
const result = []
for (const cls of classes) {
const meta = reflect(cls.type)
if(!!meta.decorators.find((x: ClassOptionDecorator) => x.name === "ClassOption"))
result.push(cls.type)
}
return result
}

export class MongooseFacility extends DefaultFacility {
option: MongooseFacilityOption
constructor(opts?: MongooseFacilityOption) {
super()
this.option = { ...opts }
this.option = {
entity: [
require.main!.filename,
"./**/*controller.+(ts|js)",
"./**/*entity.+(ts|js)"
], ...opts
}
}

async preInitialize(app: Readonly<PlumierApplication>) {
const entities = await loadEntities(this.option.entity!, app.config)
for (const entity of entities) {
normalizeEntity(entity)
}
}

setup(app: Readonly<PlumierApplication>) {
Expand Down
14 changes: 13 additions & 1 deletion packages/mongoose/src/generic-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import reflect, { generic } from "@plumier/reflect"
import { Context } from "koa"
import pluralize from "pluralize"
import { normalizeEntity } from "./helper"

import { MongooseNestedRepository, MongooseRepository } from "./repository"

Expand Down Expand Up @@ -76,7 +77,18 @@ function createGenericControllerMongoose(controllers?: GenericControllers) {
createGenericController(type, {
controllers: controllers ?? [MongooseControllerGeneric, MongooseNestedControllerGeneric],
nameConversion: pluralize,
config
config, normalize: type => {
if (Array.isArray(type)) {
const [parentEntity, relation] = type
normalizeEntity(parentEntity)
const meta = reflect(parentEntity)
const prop = meta.properties.find(x => x.name === relation)!
const entity: Class = Array.isArray(prop.type) ? prop.type[0] : prop.type
normalizeEntity(entity)
}
else
normalizeEntity(type)
}
})
}

Expand Down
24 changes: 24 additions & 0 deletions packages/mongoose/src/helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { authorize } from "@plumier/core"
import reflect, { Class, useCache } from "@plumier/reflect"

import { RefDecorator } from "./types"

function normalizeEntityNoCache(type: Class) {
const meta = reflect(type)
reflect.flush(type)
for (const prop of meta.properties) {
const ref = prop.decorators.find((x: RefDecorator) => x.name === "MongooseRef")
if (ref && prop.typeClassification === "Array") {
const decorators = [
authorize.readonly(),
authorize.writeonly()
]
Reflect.decorate(decorators, type.prototype, prop.name, void 0)
}
}
return { success: true }
}

const normalizeEntityCache = new Map<Class, any>()

export const normalizeEntity = useCache(normalizeEntityCache, normalizeEntityNoCache, x => x)
1 change: 1 addition & 0 deletions packages/mongoose/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export { MongooseControllerGeneric, MongooseNestedControllerGeneric, GenericCont
export { MongooseRepository, MongooseNestedRepository } from "./repository"
export * from "./types"
export * from "./query-parser"
export * from "./helper"
export default model;
3 changes: 2 additions & 1 deletion packages/typeorm/src/facility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ import { promisify } from "util"
import validator from "validator"
import { filterConverter, orderConverter, selectConverter } from "./query-parser"

import { normalizeEntity, TypeORMControllerGeneric, TypeORMNestedControllerGeneric } from "./generic-controller"
import { TypeORMControllerGeneric, TypeORMNestedControllerGeneric } from "./generic-controller"
import { normalizeEntity } from "./helper"

const lstatAsync = promisify(lstat)

Expand Down
78 changes: 4 additions & 74 deletions packages/typeorm/src/generic-controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { authorize, Class, entity, GenericControllers, Repository } from "@plumier/core"
import { Class, entity, GenericControllers, Repository } from "@plumier/core"
import {
createGenericController,
EntityWithRelation,
Expand All @@ -7,82 +7,12 @@ import {
RepoBaseControllerGeneric,
RepoBaseNestedControllerGeneric,
} from "@plumier/generic-controller"
import reflect, { generic, noop, useCache } from "@plumier/reflect"
import { parse } from "acorn"
import reflect, { generic } from "@plumier/reflect"
import pluralize from "pluralize"
import { getMetadataArgsStorage } from "typeorm"

import { normalizeEntity } from "./helper"
import { TypeORMNestedRepository, TypeORMRepository } from "./repository"

// --------------------------------------------------------------------- //
// ------------------------------- HELPER ------------------------------ //
// --------------------------------------------------------------------- //


function normalizeEntityNoCache(type: Class) {
const parent: Class = Object.getPrototypeOf(type)
// loop through parent entities
if (!!parent.prototype) normalizeEntity(parent)
const storage = getMetadataArgsStorage();
const columns = storage.filterColumns(type)
for (const col of columns) {
Reflect.decorate([noop()], (col.target as Function).prototype, col.propertyName, void 0)
if (col.options.primary)
Reflect.decorate([entity.primaryId(), authorize.readonly()], (col.target as Function).prototype, col.propertyName, void 0)
}
const relations = storage.filterRelations(type)
for (const col of relations) {
const rawType: Class = (col as any).type()
if (col.relationType === "many-to-many" || col.relationType === "one-to-many") {
const inverseProperty = inverseSideParser(col.inverseSideProperty as any)
const decorators = [
reflect.type(x => [rawType]),
entity.relation({ inverseProperty }),
authorize.readonly(),
authorize.writeonly()
]
Reflect.decorate(decorators, (col.target as Function).prototype, col.propertyName, void 0)
}
else {
Reflect.decorate([reflect.type(x => rawType), entity.relation()], (col.target as Function).prototype, col.propertyName, void 0)
}
}
}

const normalizeEntityCache = new Map<Class, any>()

const normalizeEntity = useCache(normalizeEntityCache, normalizeEntityNoCache, x => x)

// --------------------------------------------------------------------- //
// ---------------------- INVERSE PROPERTY PARSER ---------------------- //
// --------------------------------------------------------------------- //

function inverseSideParser(expr: ((t: any) => any)) {
const node = parse(expr.toString(), { ecmaVersion: 2020 })
return getMemberExpression(node)
}

function getContent(node: any): any {
switch (node.type) {
case "Program":
case "BlockStatement":
return node.body[node.body.length - 1]
case "ArrowFunctionExpression":
return node.body
case "ExpressionStatement":
return node.expression
case "ReturnStatement":
return node.argument
}
}

function getMemberExpression(node: any): string {
const content = getContent(node)
if (content.type === "MemberExpression")
return content.property.name
else
return getMemberExpression(content)
}

// --------------------------------------------------------------------- //
// ------------------------ GENERIC CONTROLLERS ------------------------ //
Expand Down Expand Up @@ -146,4 +76,4 @@ function GenericController<T>(type: Class | EntityWithRelation<T>, config?: Gene
return factory(type, config)
}

export { TypeORMControllerGeneric, TypeORMNestedControllerGeneric, normalizeEntity, GenericController, createGenericControllerTypeORM, EntityWithRelation }
export { TypeORMControllerGeneric, TypeORMNestedControllerGeneric, GenericController, createGenericControllerTypeORM, EntityWithRelation }
67 changes: 67 additions & 0 deletions packages/typeorm/src/helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { authorize, Class, entity } from "@plumier/core"
import reflect, { noop, useCache } from "@plumier/reflect"
import { getMetadataArgsStorage } from "typeorm"
import { parse } from "acorn"

function inverseSideParser(expr: ((t: any) => any)) {
const node = parse(expr.toString(), { ecmaVersion: 2020 })
return getMemberExpression(node)
}

function getContent(node: any): any {
switch (node.type) {
case "Program":
case "BlockStatement":
return node.body[node.body.length - 1]
case "ArrowFunctionExpression":
return node.body
case "ExpressionStatement":
return node.expression
case "ReturnStatement":
return node.argument
}
}

function getMemberExpression(node: any): string {
const content = getContent(node)
if (content.type === "MemberExpression")
return content.property.name
else
return getMemberExpression(content)
}


function normalizeEntityNoCache(type: Class) {
const parent: Class = Object.getPrototypeOf(type)
// loop through parent entities
if (!!parent.prototype) normalizeEntity(parent)
const storage = getMetadataArgsStorage();
const columns = storage.filterColumns(type)
for (const col of columns) {
Reflect.decorate([noop()], (col.target as Function).prototype, col.propertyName, void 0)
if (col.options.primary)
Reflect.decorate([entity.primaryId(), authorize.readonly()], (col.target as Function).prototype, col.propertyName, void 0)
}
const relations = storage.filterRelations(type)
for (const col of relations) {
const rawType: Class = (col as any).type()
if (col.relationType === "many-to-many" || col.relationType === "one-to-many") {
const inverseProperty = inverseSideParser(col.inverseSideProperty as any)
const decorators = [
reflect.type(x => [rawType]),
entity.relation({ inverseProperty }),
authorize.readonly(),
authorize.writeonly()
]
Reflect.decorate(decorators, (col.target as Function).prototype, col.propertyName, void 0)
}
else {
Reflect.decorate([reflect.type(x => rawType), entity.relation()], (col.target as Function).prototype, col.propertyName, void 0)
}
}
return { success: true }
}

const normalizeEntityCache = new Map<Class, any>()

export const normalizeEntity = useCache(normalizeEntityCache, normalizeEntityNoCache, x => x)
1 change: 1 addition & 0 deletions packages/typeorm/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from "./generic-controller"
export * from "./query-parser"
export * from "./facility"
export * from "./repository"
export * from "./helper"
1 change: 1 addition & 0 deletions tests/behavior/export/__snapshots__/export.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,7 @@ Object {
"getModels": bound getModels,
"model": bound model,
"models": Map {},
"normalizeEntity": ,
"orderConverter": ,
"proxy": bound proxy,
"selectConverter": ,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,13 @@ Array [
]
`;

exports[`CRUD Nested CRUD One to Many Function Should not allow nested array relation accessed from parent 1`] = `
Object {
"message": "Unauthorized to access filter properties animals",
"status": 403,
}
`;

exports[`CRUD Nested CRUD One to Many Function Should not confused on reverse properties with the same type POST /users/:parentId/animals 1`] = `
Object {
"createdBy": Object {
Expand Down

0 comments on commit 58058a5

Please sign in to comment.