feat: acl (#153)
* feat: getRepository * getRepository return type * export action * add: acl * feat: setResourceAction * feat: action alias * chore: code struct * feat: removeResourceAction * chore: file name * ignorecase * remove ACL * feat: ACL * feat: role toJSON * using emit Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
parent
c9742c1710
commit
fd32705954
8
packages/acl/.npmignore
Normal file
8
packages/acl/.npmignore
Normal file
@ -0,0 +1,8 @@
|
||||
node_modules
|
||||
*.log
|
||||
docs
|
||||
__tests__
|
||||
jest.config.js
|
||||
tsconfig.json
|
||||
src
|
||||
.fatherrc.ts
|
20
packages/acl/package.json
Normal file
20
packages/acl/package.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "@nocobase/acl",
|
||||
"version": "0.6.0-alpha.0",
|
||||
"description": "",
|
||||
"license": "MIT",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "rimraf -rf lib esm dist && npm run build:cjs && npm run build:esm",
|
||||
"build:cjs": "tsc --project tsconfig.build.json",
|
||||
"build:esm": "tsc --project tsconfig.build.json --module es2015 --outDir esm"
|
||||
},
|
||||
"dependencies": {
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/nocobase/nocobase.git",
|
||||
"directory": "packages/acl"
|
||||
}
|
||||
}
|
287
packages/acl/src/__tests__/acl.test.ts
Normal file
287
packages/acl/src/__tests__/acl.test.ts
Normal file
@ -0,0 +1,287 @@
|
||||
import { ACL } from '..';
|
||||
|
||||
describe('acl', () => {
|
||||
let acl: ACL;
|
||||
beforeEach(() => {
|
||||
acl = new ACL();
|
||||
});
|
||||
|
||||
it('should allow all', () => {
|
||||
acl.setAvailableAction('create', {
|
||||
type: 'new-data',
|
||||
});
|
||||
|
||||
acl.setAvailableAction('edit', {
|
||||
type: 'old-data',
|
||||
});
|
||||
|
||||
acl.setAvailableStrategy('s1', {
|
||||
displayName: 's1',
|
||||
actions: '*',
|
||||
});
|
||||
|
||||
acl.define({
|
||||
role: 'admin',
|
||||
strategy: 's1',
|
||||
});
|
||||
|
||||
expect(acl.can({ role: 'admin', resource: 'posts', action: 'create' })).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should deny all', () => {
|
||||
acl.setAvailableStrategy('s1', {
|
||||
displayName: 'test',
|
||||
actions: false,
|
||||
});
|
||||
|
||||
acl.define({
|
||||
role: 'admin',
|
||||
strategy: 's1',
|
||||
});
|
||||
|
||||
expect(acl.can({ role: 'admin', resource: 'posts', action: 'create' })).toBeNull();
|
||||
});
|
||||
|
||||
it('should deny when action is not available action', () => {
|
||||
acl.setAvailableStrategy('s1', {
|
||||
displayName: 'test',
|
||||
actions: false,
|
||||
});
|
||||
|
||||
const role = acl.define({
|
||||
role: 'admin',
|
||||
strategy: 's1',
|
||||
});
|
||||
|
||||
expect(acl.can({ role: 'admin', resource: 'posts', action: 'create' })).toBeNull();
|
||||
|
||||
role.grantAction('posts:create', {});
|
||||
|
||||
expect(acl.can({ role: 'admin', resource: 'posts', action: 'create' })).toBeNull();
|
||||
});
|
||||
|
||||
it('should grant action when define role', () => {
|
||||
acl.setAvailableAction('create', {
|
||||
displayName: 'create',
|
||||
type: 'new-data',
|
||||
});
|
||||
|
||||
acl.setAvailableStrategy('s1', {
|
||||
displayName: 'test',
|
||||
actions: false,
|
||||
});
|
||||
|
||||
const role = acl.define({
|
||||
role: 'admin',
|
||||
strategy: 's1',
|
||||
actions: {
|
||||
'posts:create': {},
|
||||
},
|
||||
});
|
||||
|
||||
expect(acl.can({ role: 'admin', resource: 'posts', action: 'create' })).not.toBeNull();
|
||||
});
|
||||
it('should grant action', function () {
|
||||
acl.setAvailableAction('create', {
|
||||
displayName: 'create',
|
||||
type: 'new-data',
|
||||
});
|
||||
|
||||
acl.setAvailableStrategy('s1', {
|
||||
displayName: 'test',
|
||||
actions: false,
|
||||
});
|
||||
|
||||
const role = acl.define({
|
||||
role: 'admin',
|
||||
strategy: 's1',
|
||||
});
|
||||
|
||||
expect(acl.can({ role: 'admin', resource: 'posts', action: 'create' })).toBeNull();
|
||||
|
||||
role.grantAction('posts:create', {});
|
||||
|
||||
expect(acl.can({ role: 'admin', resource: 'posts', action: 'create' })).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should works with alias action', () => {
|
||||
acl.setAvailableAction('view', {
|
||||
displayName: 'view',
|
||||
type: 'new-data',
|
||||
aliases: ['get', 'list'],
|
||||
});
|
||||
|
||||
acl.setAvailableStrategy('s1', {
|
||||
displayName: 'test',
|
||||
actions: ['view'],
|
||||
});
|
||||
|
||||
const role = acl.define({
|
||||
role: 'admin',
|
||||
strategy: 's1',
|
||||
});
|
||||
|
||||
expect(acl.can({ role: 'admin', resource: 'posts', action: 'get' })).not.toBeNull();
|
||||
expect(acl.can({ role: 'admin', resource: 'posts', action: 'list' })).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should return action params when check permission', () => {
|
||||
acl.setAvailableStrategy('s2', {
|
||||
displayName: 'view create update',
|
||||
actions: ['view', 'create', 'update'],
|
||||
});
|
||||
|
||||
acl.setAvailableAction('view', { type: 'new-data' });
|
||||
acl.setAvailableAction('create', { type: 'new-data' });
|
||||
acl.setAvailableAction('update', { type: 'new-data' });
|
||||
|
||||
acl.define({
|
||||
role: 'admin',
|
||||
strategy: 's2',
|
||||
actions: {
|
||||
'posts:view': {
|
||||
filter: {
|
||||
createdById: '{{ ctx.state.currentUser.id }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const canResult = acl.can({ role: 'admin', resource: 'posts', action: 'view' });
|
||||
|
||||
expect(canResult).toMatchObject({
|
||||
role: 'admin',
|
||||
resource: 'posts',
|
||||
action: 'view',
|
||||
params: {
|
||||
filter: {
|
||||
createdById: '{{ ctx.state.currentUser.id }}',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should getActionParams', () => {
|
||||
acl.setAvailableStrategy('s2', {
|
||||
displayName: 'view create update',
|
||||
actions: ['view', 'create', 'update'],
|
||||
});
|
||||
|
||||
acl.setAvailableAction('view', { type: 'new-data' });
|
||||
acl.setAvailableAction('create', { type: 'new-data' });
|
||||
acl.setAvailableAction('update', { type: 'new-data' });
|
||||
|
||||
const role = acl.define({
|
||||
role: 'admin',
|
||||
strategy: 's2',
|
||||
actions: {
|
||||
'posts:view': {
|
||||
filter: {
|
||||
createdById: '{{ ctx.state.currentUser.id }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const params = role.getActionParams('posts:view');
|
||||
|
||||
expect(params).toMatchObject({
|
||||
filter: {
|
||||
createdById: '{{ ctx.state.currentUser.id }}',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should revoke action', () => {
|
||||
acl.setAvailableAction('create', {
|
||||
displayName: 'create',
|
||||
type: 'new-data',
|
||||
});
|
||||
|
||||
acl.setAvailableStrategy('s1', {
|
||||
displayName: 'test',
|
||||
actions: false,
|
||||
});
|
||||
|
||||
const role = acl.define({
|
||||
role: 'admin',
|
||||
strategy: 's1',
|
||||
});
|
||||
|
||||
role.grantAction('posts:create', {});
|
||||
|
||||
expect(acl.can({ role: 'admin', resource: 'posts', action: 'create' })).not.toBeNull();
|
||||
|
||||
role.revokeAction('posts:create');
|
||||
|
||||
expect(acl.can({ role: 'admin', resource: 'posts', action: 'create' })).toBeNull();
|
||||
});
|
||||
|
||||
it('should call beforeGrantAction', () => {
|
||||
acl.setAvailableAction('create', {
|
||||
type: 'old-data',
|
||||
});
|
||||
|
||||
acl.beforeGrantAction('posts:create', (ctx) => {
|
||||
ctx.params = {
|
||||
filter: {
|
||||
status: 'publish',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
expect(acl.listenerCount('posts:create.beforeGrantAction')).toEqual(1);
|
||||
|
||||
acl.define({
|
||||
role: 'admin',
|
||||
actions: {
|
||||
'posts:create': {},
|
||||
},
|
||||
});
|
||||
|
||||
const results = acl.can({ role: 'admin', resource: 'posts', action: 'create' });
|
||||
|
||||
expect(results).toMatchObject({
|
||||
role: 'admin',
|
||||
resource: 'posts',
|
||||
action: 'create',
|
||||
params: {
|
||||
filter: {
|
||||
status: 'publish',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should to JSON', () => {
|
||||
acl.setAvailableAction('create', {
|
||||
displayName: 'create',
|
||||
type: 'new-data',
|
||||
});
|
||||
|
||||
acl.setAvailableStrategy('s1', {
|
||||
displayName: 'test',
|
||||
actions: false,
|
||||
});
|
||||
|
||||
const role = acl.define({
|
||||
role: 'admin',
|
||||
strategy: 's1',
|
||||
actions: {
|
||||
'posts:create': {
|
||||
filter: { a: 'b' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const roleJSON = role.toJSON();
|
||||
|
||||
expect(roleJSON).toMatchObject({
|
||||
role: 'admin',
|
||||
strategy: 's1',
|
||||
actions: {
|
||||
'posts:create': {},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
10
packages/acl/src/acl-available-action.ts
Normal file
10
packages/acl/src/acl-available-action.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export interface AvailableActionOptions {
|
||||
aliases?: string[] | string;
|
||||
type: 'new-data' | 'old-data';
|
||||
displayName?: string;
|
||||
resource?: string;
|
||||
}
|
||||
|
||||
export class AclAvailableAction {
|
||||
constructor(private name: string, private options: AvailableActionOptions) {}
|
||||
}
|
40
packages/acl/src/acl-available-strategy.ts
Normal file
40
packages/acl/src/acl-available-strategy.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import lodash from 'lodash';
|
||||
type StrategyValue = false | '*' | string | string[];
|
||||
|
||||
export interface AvailableStrategyOptions {
|
||||
displayName?: string;
|
||||
actions: false | string | string[];
|
||||
resource?: '*';
|
||||
}
|
||||
|
||||
export function strategyValueMatched(strategy: StrategyValue, value: string) {
|
||||
if (strategy === '*') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (lodash.isString(strategy) && strategy === value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (lodash.isArray(strategy) && strategy.includes(value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export class ACLAvailableStrategy {
|
||||
options: AvailableStrategyOptions;
|
||||
|
||||
constructor(options: AvailableStrategyOptions) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
matchAction(actionName: string) {
|
||||
return strategyValueMatched(this.options.actions, actionName);
|
||||
}
|
||||
|
||||
allow(resourceName: string, actionName: string) {
|
||||
return this.matchAction(actionName);
|
||||
}
|
||||
}
|
62
packages/acl/src/acl-resource.ts
Normal file
62
packages/acl/src/acl-resource.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { ACLRole, RoleActionParams } from './acl-role';
|
||||
import { ACL, ListenerContext } from './acl';
|
||||
|
||||
export type ResourceActions = { [key: string]: RoleActionParams };
|
||||
|
||||
interface AclResourceOptions {
|
||||
name: string;
|
||||
role: ACLRole;
|
||||
actions?: ResourceActions;
|
||||
}
|
||||
|
||||
export class ACLResource {
|
||||
actions = new Map<string, RoleActionParams>();
|
||||
acl: ACL;
|
||||
role: ACLRole;
|
||||
name: string;
|
||||
|
||||
constructor(options: AclResourceOptions) {
|
||||
this.acl = options.role.acl;
|
||||
|
||||
this.role = options.role;
|
||||
this.name = options.name;
|
||||
|
||||
const actionsOption: ResourceActions = options.actions || {};
|
||||
for (const actionName of Object.keys(actionsOption)) {
|
||||
this.actions.set(actionName, actionsOption[actionName]);
|
||||
}
|
||||
}
|
||||
|
||||
getActions() {
|
||||
return Array.from(this.actions.keys()).reduce((carry, key) => {
|
||||
carry[key] = this.actions.get(key);
|
||||
return carry;
|
||||
}, {});
|
||||
}
|
||||
|
||||
getAction(name: string) {
|
||||
return this.actions.get(name);
|
||||
}
|
||||
|
||||
setAction(name: string, params: RoleActionParams) {
|
||||
const context: ListenerContext = {
|
||||
role: this.role,
|
||||
acl: this.role.acl,
|
||||
params: params || {},
|
||||
};
|
||||
|
||||
this.acl.emit(`${this.name}:${name}.beforeGrantAction`, context);
|
||||
|
||||
this.actions.set(name, context.params);
|
||||
}
|
||||
|
||||
setActions(actions: { [key: string]: RoleActionParams }) {
|
||||
for (const actionName of Object.keys(actions)) {
|
||||
this.setAction(actionName, actions[actionName]);
|
||||
}
|
||||
}
|
||||
|
||||
removeAction(name: string) {
|
||||
this.actions.delete(name);
|
||||
}
|
||||
}
|
116
packages/acl/src/acl-role.ts
Normal file
116
packages/acl/src/acl-role.ts
Normal file
@ -0,0 +1,116 @@
|
||||
import { ACLResource } from './acl-resource';
|
||||
import { AvailableStrategyOptions } from './acl-available-strategy';
|
||||
import { ACL, DefineOptions } from './acl';
|
||||
|
||||
export interface RoleActionParams {
|
||||
fields?: string[];
|
||||
filter?: any;
|
||||
own?: boolean;
|
||||
whitelist?: string[];
|
||||
blacklist?: string[];
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface ResourceActionsOptions {
|
||||
[actionName: string]: RoleActionParams;
|
||||
}
|
||||
|
||||
export class ACLRole {
|
||||
strategy: string | AvailableStrategyOptions;
|
||||
resources = new Map<string, ACLResource>();
|
||||
|
||||
constructor(public acl: ACL, public name: string) {}
|
||||
|
||||
getResource(name: string): ACLResource | undefined {
|
||||
return this.resources.get(name);
|
||||
}
|
||||
|
||||
setResource(name: string, resource: ACLResource) {
|
||||
this.resources.set(name, resource);
|
||||
}
|
||||
|
||||
public setStrategy(value: string | AvailableStrategyOptions) {
|
||||
this.strategy = value;
|
||||
}
|
||||
|
||||
public grantResource(resourceName: string, options: ResourceActionsOptions) {
|
||||
const resource = new ACLResource({
|
||||
role: this,
|
||||
name: resourceName,
|
||||
});
|
||||
|
||||
for (const [actionName, actionParams] of Object.entries(options)) {
|
||||
resource.setAction(actionName, actionParams);
|
||||
}
|
||||
|
||||
this.resources.set(resourceName, resource);
|
||||
}
|
||||
|
||||
public getResourceActionsParams(resourceName: string) {
|
||||
const resource = this.getResource(resourceName);
|
||||
return resource.getActions();
|
||||
}
|
||||
|
||||
public revokeResource(resourceName) {
|
||||
this.resources.delete(resourceName);
|
||||
}
|
||||
|
||||
public grantAction(path: string, options?: RoleActionParams) {
|
||||
let { resource, resourceName, actionName } = this.getResourceActionFromPath(path);
|
||||
|
||||
if (!resource) {
|
||||
resource = new ACLResource({
|
||||
role: this,
|
||||
name: resourceName,
|
||||
});
|
||||
|
||||
this.resources.set(resourceName, resource);
|
||||
}
|
||||
|
||||
resource.setAction(actionName, options);
|
||||
}
|
||||
|
||||
public getActionParams(path: string): RoleActionParams {
|
||||
const { action } = this.getResourceActionFromPath(path);
|
||||
return action;
|
||||
}
|
||||
|
||||
public revokeAction(path: string) {
|
||||
const { resource, actionName } = this.getResourceActionFromPath(path);
|
||||
resource.removeAction(actionName);
|
||||
}
|
||||
|
||||
public toJSON(): DefineOptions {
|
||||
const actions = {};
|
||||
|
||||
for (const resourceName of this.resources.keys()) {
|
||||
const resourceActions = this.getResourceActionsParams(resourceName);
|
||||
for (const actionName of Object.keys(resourceActions)) {
|
||||
actions[`${resourceName}:${actionName}`] = resourceActions[actionName];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
role: this.name,
|
||||
strategy: this.strategy,
|
||||
actions,
|
||||
};
|
||||
}
|
||||
|
||||
protected getResourceActionFromPath(path: string) {
|
||||
const [resourceName, actionName] = path.split(':');
|
||||
|
||||
const resource = this.resources.get(resourceName);
|
||||
let action = null;
|
||||
if (resource) {
|
||||
action = resource.getAction(actionName);
|
||||
}
|
||||
|
||||
return {
|
||||
resourceName,
|
||||
actionName,
|
||||
resource,
|
||||
action,
|
||||
};
|
||||
}
|
||||
}
|
132
packages/acl/src/acl.ts
Normal file
132
packages/acl/src/acl.ts
Normal file
@ -0,0 +1,132 @@
|
||||
import lodash from 'lodash';
|
||||
import { ACLAvailableStrategy, AvailableStrategyOptions } from './acl-available-strategy';
|
||||
import { ACLRole, RoleActionParams } from './acl-role';
|
||||
import { AclAvailableAction, AvailableActionOptions } from './acl-available-action';
|
||||
import EventEmitter from 'events';
|
||||
|
||||
interface StrategyOptions {
|
||||
role: string;
|
||||
strategy: string;
|
||||
}
|
||||
|
||||
interface CanResult {
|
||||
role: string;
|
||||
resource: string;
|
||||
action: string;
|
||||
params?: any;
|
||||
}
|
||||
|
||||
export interface DefineOptions {
|
||||
role: string;
|
||||
strategy?: string | AvailableStrategyOptions;
|
||||
actions?: {
|
||||
[key: string]: RoleActionParams;
|
||||
};
|
||||
routes?: any;
|
||||
}
|
||||
|
||||
export interface ListenerContext {
|
||||
acl: ACL;
|
||||
role: ACLRole;
|
||||
params: RoleActionParams;
|
||||
}
|
||||
|
||||
type Listener = (ctx: ListenerContext) => void;
|
||||
|
||||
export class ACL extends EventEmitter {
|
||||
protected availableActions = new Map<string, AclAvailableAction>();
|
||||
protected availableStrategy = new Map<string, ACLAvailableStrategy>();
|
||||
|
||||
roles = new Map<string, ACLRole>();
|
||||
|
||||
actionAlias = new Map<string, string>();
|
||||
|
||||
define(options: DefineOptions): ACLRole {
|
||||
const roleName = options.role;
|
||||
const role = new ACLRole(this, roleName);
|
||||
|
||||
if (options.strategy) {
|
||||
role.strategy = options.strategy;
|
||||
}
|
||||
|
||||
const actions = options.actions || {};
|
||||
|
||||
for (const [actionName, actionParams] of Object.entries(actions)) {
|
||||
role.grantAction(actionName, actionParams);
|
||||
}
|
||||
|
||||
this.roles.set(roleName, role);
|
||||
|
||||
return role;
|
||||
}
|
||||
|
||||
getRole(name: string): ACLRole {
|
||||
return this.roles.get(name);
|
||||
}
|
||||
|
||||
setAvailableAction(name: string, options: AvailableActionOptions) {
|
||||
this.availableActions.set(name, new AclAvailableAction(name, options));
|
||||
|
||||
if (options.aliases) {
|
||||
const aliases = lodash.isArray(options.aliases) ? options.aliases : [options.aliases];
|
||||
for (const alias of aliases) {
|
||||
this.actionAlias.set(alias, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setAvailableStrategy(name: string, options: AvailableStrategyOptions) {
|
||||
this.availableStrategy.set(name, new ACLAvailableStrategy(options));
|
||||
}
|
||||
|
||||
beforeGrantAction(path: string, listener?: Listener) {
|
||||
this.addListener(`${path}.beforeGrantAction`, listener);
|
||||
}
|
||||
|
||||
can({ role, resource, action }: { role: string; resource: string; action: string }): CanResult | null {
|
||||
action = this.resolveActionAlias(action);
|
||||
|
||||
if (!this.isAvailableAction(action)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const aclRole = this.roles.get(role);
|
||||
const aclResource = aclRole.getResource(resource);
|
||||
|
||||
if (aclResource) {
|
||||
const aclActionConfig = aclResource.actions.get(this.resolveActionAlias(action));
|
||||
|
||||
if (aclActionConfig) {
|
||||
// handle single action config
|
||||
return {
|
||||
role,
|
||||
resource,
|
||||
action,
|
||||
params: aclActionConfig,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const roleStrategy = lodash.isString(aclRole.strategy)
|
||||
? this.availableStrategy.get(aclRole.strategy)
|
||||
: new ACLAvailableStrategy(aclRole.strategy);
|
||||
|
||||
if (!roleStrategy) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (roleStrategy.allow(resource, action)) {
|
||||
return { role, resource, action };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected isAvailableAction(actionName: string) {
|
||||
return this.availableActions.has(actionName);
|
||||
}
|
||||
|
||||
protected resolveActionAlias(action: string) {
|
||||
return this.actionAlias.get(action) ? this.actionAlias.get(action) : action;
|
||||
}
|
||||
}
|
5
packages/acl/src/index.ts
Normal file
5
packages/acl/src/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export * from './acl-resource';
|
||||
export * from './acl-role';
|
||||
export * from './acl-available-strategy';
|
||||
export * from './acl-available-action';
|
||||
export * from './acl';
|
9
packages/acl/tsconfig.build.json
Normal file
9
packages/acl/tsconfig.build.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.build.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./lib",
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
|
||||
"exclude": ["./src/__tests__/*", "./esm/*", "./lib/*"]
|
||||
}
|
5
packages/acl/tsconfig.json
Normal file
5
packages/acl/tsconfig.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
|
||||
"exclude": ["./esm/*", "./lib/*"]
|
||||
}
|
@ -19,4 +19,4 @@ export function registerActions(api: any) {
|
||||
);
|
||||
}
|
||||
|
||||
export default {};
|
||||
export default actions;
|
||||
|
@ -3,6 +3,23 @@ import path from 'path';
|
||||
import { Model } from '..';
|
||||
|
||||
describe('database', () => {
|
||||
test('get repository', async () => {
|
||||
const db = mockDatabase();
|
||||
db.collection({
|
||||
name: 'tests',
|
||||
fields: [{ type: 'hasMany', name: 'relations' }],
|
||||
});
|
||||
|
||||
db.collection({
|
||||
name: 'relations',
|
||||
});
|
||||
|
||||
expect(db.getRepository('tests')).toEqual(db.getCollection('tests').repository);
|
||||
expect(db.getRepository('tests.relations', '1')).toEqual(
|
||||
db.getCollection('tests').repository.relation('relations').of('1'),
|
||||
);
|
||||
});
|
||||
|
||||
test('import', async () => {
|
||||
const db = mockDatabase();
|
||||
await db.import({
|
||||
@ -191,5 +208,4 @@ describe('database', () => {
|
||||
test.customMethod();
|
||||
expect(test.get('abc')).toBe('abc');
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -11,6 +11,8 @@ import { ModelHook } from './model-hook';
|
||||
import { ImporterReader, ImportFileExtension } from './collection-importer';
|
||||
|
||||
import extendOperators from './operators';
|
||||
import { Repository } from './repository';
|
||||
import { RelationRepository } from './relation-repository/relation-repository';
|
||||
|
||||
export interface MergeOptions extends merge.Options {}
|
||||
|
||||
@ -126,6 +128,18 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
getRepository(name: string): Repository;
|
||||
getRepository<R extends RelationRepository>(name: string, relationId: string | number): R;
|
||||
|
||||
getRepository<R extends RelationRepository>(name: string, relationId?: string | number): Repository | R {
|
||||
if (relationId) {
|
||||
const [collection, relation] = name.split('.');
|
||||
return this.getRepository(collection).relation(relation).of(relationId) as R;
|
||||
}
|
||||
|
||||
return this.getCollection(name).repository;
|
||||
}
|
||||
|
||||
addPendingField(field: RelationField) {
|
||||
const associating = this.pendingFields;
|
||||
const items = this.pendingFields.get(field.target) || [];
|
||||
|
Loading…
Reference in New Issue
Block a user