mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 13:39:24 +08:00
test: add automated testing (#4098)
* test: string includes operator * chore: operator test coverage * chore: database utils test * chore: acl test * chore: no permission error * chore: code * fix: run coverage test * chore: datasource test * chore: datasource test * chore: datasource test * chore: datasource test * chore: datasource test * chore: datasource * fix: build * chore: plugin data source manager test * chore: acl test * chore: query interface test * chore: ui schema storage test * chore: save template test * chore: ui schema insert position action * chore: ignore migration * chore: plugin acl test * chore: ignore command in coverage * chore: ignore * chore: remove db2resource * chore: ignore migration * chore: ipc server test * chore: database test * chore: database api comments * chore: value parser test * chore: build * chore: backup & restore test * chore: plugin manager test * chore: pm * chore: pm ignore * chore: skip migration * chore: remove unused code * fix: import * chore: remove unused code * chore: remove unused code * fix: action test * chore: data wrapping middleware * fix: build * fix: build * fix: build * test: fix T-4105 * chore: test * fix: data-source-manager test * fix: sql collection test * fix: test * fix: test * fix: test * fix: typo * chore: datasource manager test * chore: console.log --------- Co-authored-by: xilesun <2013xile@gmail.com>
This commit is contained in:
parent
c5811315aa
commit
71e8d07f15
@ -41,6 +41,28 @@ describe('acl', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should getRole', () => {
|
||||
const role = acl.define({
|
||||
role: 'admin',
|
||||
actions: {
|
||||
'posts:edit': {
|
||||
own: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(acl.getRole('admin')).toBe(role);
|
||||
});
|
||||
|
||||
it('should set available action', () => {
|
||||
acl.setAvailableAction('edit', {
|
||||
displayName: 'Edit',
|
||||
});
|
||||
|
||||
const action = acl.getAvailableAction('edit');
|
||||
expect(action.name).toBe('edit');
|
||||
});
|
||||
|
||||
it('should define role with predicate', () => {
|
||||
acl.setAvailableAction('edit', {
|
||||
type: 'old-data',
|
||||
|
30
packages/core/acl/src/__tests__/skip-middleware.test.ts
Normal file
30
packages/core/acl/src/__tests__/skip-middleware.test.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { skip } from '../skip-middleware';
|
||||
|
||||
describe('Skip Middleware', () => {
|
||||
it('should skip middleware', async () => {
|
||||
const skipMiddleware = skip({ resourceName: 'posts', actionName: 'list' });
|
||||
const ctx: any = {
|
||||
action: {
|
||||
resourceName: 'posts',
|
||||
actionName: 'list',
|
||||
},
|
||||
permission: {},
|
||||
};
|
||||
|
||||
await skipMiddleware(ctx, async () => {
|
||||
expect(ctx.permission.skip).toBeTruthy();
|
||||
});
|
||||
|
||||
const ctx2: any = {
|
||||
action: {
|
||||
resourceName: 'posts',
|
||||
actionName: 'create',
|
||||
},
|
||||
permission: {},
|
||||
};
|
||||
|
||||
await skipMiddleware(ctx2, async () => {
|
||||
expect(ctx2.permission.skip).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
@ -77,11 +77,6 @@ export class ACL extends EventEmitter {
|
||||
*/
|
||||
actionAlias = new Map<string, string>();
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
configResources: string[] = [];
|
||||
|
||||
protected availableActions = new Map<string, ACLAvailableAction>();
|
||||
|
||||
protected fixedParamsManager = new FixedParamsManager();
|
||||
@ -147,27 +142,6 @@ export class ACL extends EventEmitter {
|
||||
return this.roles.delete(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
registerConfigResources(names: string[]) {
|
||||
names.forEach((name) => this.registerConfigResource(name));
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
registerConfigResource(name: string) {
|
||||
this.configResources.push(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
isConfigResource(name: string) {
|
||||
return this.configResources.includes(name);
|
||||
}
|
||||
|
||||
setAvailableAction(name: string, options: AvailableActionOptions = {}) {
|
||||
this.availableActions.set(name, new ACLAvailableAction(name, options));
|
||||
|
||||
|
@ -3,5 +3,4 @@ export * from './acl-available-action';
|
||||
export * from './acl-available-strategy';
|
||||
export * from './acl-resource';
|
||||
export * from './acl-role';
|
||||
export * from './no-permission-error';
|
||||
export * from './skip-middleware';
|
||||
|
@ -1,7 +0,0 @@
|
||||
class NoPermissionError extends Error {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
}
|
||||
}
|
||||
|
||||
export { NoPermissionError };
|
@ -4,7 +4,7 @@ import Koa from 'koa';
|
||||
import bodyParser from 'koa-bodyparser';
|
||||
import qs from 'qs';
|
||||
import supertest, { SuperAgentTest } from 'supertest';
|
||||
import db2resource from '../../../server/src/middlewares/db2resource';
|
||||
import db2resource from './db2resource';
|
||||
|
||||
interface ActionParams {
|
||||
fields?: string[];
|
||||
@ -30,6 +30,7 @@ interface ActionParams {
|
||||
* @deprecated
|
||||
*/
|
||||
associatedIndex?: string;
|
||||
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
@ -44,6 +45,7 @@ interface SortActionParams {
|
||||
method?: string;
|
||||
target?: any;
|
||||
sticky?: boolean;
|
||||
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
@ -54,6 +56,7 @@ interface Resource {
|
||||
update: (params?: ActionParams) => Promise<supertest.Response>;
|
||||
destroy: (params?: ActionParams) => Promise<supertest.Response>;
|
||||
sort: (params?: SortActionParams) => Promise<supertest.Response>;
|
||||
|
||||
[name: string]: (params?: ActionParams) => Promise<supertest.Response>;
|
||||
}
|
||||
|
||||
|
149
packages/core/data-source-manager/src/__tests__/actions.test.ts
Normal file
149
packages/core/data-source-manager/src/__tests__/actions.test.ts
Normal file
@ -0,0 +1,149 @@
|
||||
import { list } from '../default-actions/list';
|
||||
import { vi } from 'vitest';
|
||||
import { createMoveAction } from '../default-actions/move';
|
||||
|
||||
describe('action test', () => {
|
||||
describe('list action', async () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should list with paginate', async () => {
|
||||
const listAction = list;
|
||||
|
||||
const ctx: any = {
|
||||
getCurrentRepository() {
|
||||
return {};
|
||||
},
|
||||
action: {
|
||||
params: {
|
||||
paginate: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
vi.spyOn(ctx, 'getCurrentRepository').mockImplementation(() => {
|
||||
return {
|
||||
findAndCount: async () => [
|
||||
[
|
||||
{
|
||||
id: 1,
|
||||
name: 'test',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'test2',
|
||||
},
|
||||
],
|
||||
2,
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
await listAction(ctx, () => {});
|
||||
|
||||
expect(ctx.body).toMatchObject({
|
||||
count: 2,
|
||||
rows: [
|
||||
{ id: 1, name: 'test' },
|
||||
{ id: 2, name: 'test2' },
|
||||
],
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
totalPage: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should list with non paginate', async () => {
|
||||
const listAction = list;
|
||||
|
||||
const ctx: any = {
|
||||
getCurrentRepository() {
|
||||
return {};
|
||||
},
|
||||
action: {
|
||||
params: {
|
||||
paginate: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
vi.spyOn(ctx, 'getCurrentRepository').mockImplementation(() => {
|
||||
return {
|
||||
find: async () => [
|
||||
{
|
||||
id: 1,
|
||||
name: 'test',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'test2',
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
await listAction(ctx, () => {});
|
||||
|
||||
expect(ctx.body).toMatchObject([
|
||||
{ id: 1, name: 'test' },
|
||||
{ id: 2, name: 'test2' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('move action', async () => {
|
||||
it('should call database move action', async () => {
|
||||
const dbMove = vi.fn();
|
||||
const moveAction = createMoveAction(dbMove);
|
||||
|
||||
const ctx: any = {
|
||||
getCurrentRepository() {
|
||||
return {
|
||||
database: {},
|
||||
};
|
||||
},
|
||||
action: {
|
||||
params: {
|
||||
filterByTk: 1,
|
||||
targetCollection: 'test',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await moveAction(ctx, () => {});
|
||||
|
||||
expect(dbMove).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should move when repository can move', async () => {
|
||||
const moveAction = createMoveAction(() => {});
|
||||
|
||||
const ctx: any = {
|
||||
getCurrentRepository() {
|
||||
return {};
|
||||
},
|
||||
action: {
|
||||
params: {
|
||||
filterByTk: 1,
|
||||
targetCollection: 'test',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const moveFn = vi.fn();
|
||||
|
||||
vi.spyOn(ctx, 'getCurrentRepository').mockImplementation(() => {
|
||||
return {
|
||||
move: async () => {
|
||||
moveFn();
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
await moveAction(ctx, () => {});
|
||||
|
||||
expect(moveFn).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,79 @@
|
||||
import { CollectionManager } from '@nocobase/data-source-manager';
|
||||
import { Repository } from '../repository';
|
||||
|
||||
describe('Collection Manager', () => {
|
||||
it('should define collection', async () => {
|
||||
const collectionManager = new CollectionManager();
|
||||
|
||||
collectionManager.defineCollection({
|
||||
name: 'users',
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'name',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const UsersCollection = collectionManager.getCollection('users');
|
||||
expect(UsersCollection).toBeTruthy();
|
||||
|
||||
expect(collectionManager.hasCollection('users')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should extend collection', async () => {
|
||||
const collectionManager = new CollectionManager();
|
||||
|
||||
collectionManager.defineCollection({
|
||||
name: 'users',
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'name',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const UsersCollection = collectionManager.getCollection('users');
|
||||
|
||||
collectionManager.extendCollection({
|
||||
name: 'users',
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'age',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(UsersCollection.getField('age')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should register repository', async () => {
|
||||
class MockRepository extends Repository {
|
||||
async find() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const collectionManager = new CollectionManager();
|
||||
collectionManager.registerRepositories({
|
||||
MockRepository: MockRepository,
|
||||
});
|
||||
|
||||
collectionManager.defineCollection({
|
||||
name: 'users',
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'name',
|
||||
},
|
||||
],
|
||||
repository: 'MockRepository',
|
||||
});
|
||||
|
||||
const UsersCollection = collectionManager.getCollection('users');
|
||||
|
||||
expect(UsersCollection.repository).toBe(MockRepository);
|
||||
});
|
||||
});
|
@ -1,6 +1,7 @@
|
||||
import { createMockServer, mockDatabase, supertest } from '@nocobase/test';
|
||||
import { SequelizeDataSource } from '../sequelize-data-source';
|
||||
import { vi } from 'vitest';
|
||||
import { DataSourceManager } from '@nocobase/data-source-manager';
|
||||
|
||||
describe('example', () => {
|
||||
test.skip('case1', async () => {
|
||||
@ -198,4 +199,25 @@ describe('example', () => {
|
||||
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
it('should throw error when request datasource not exists', async () => {
|
||||
const dataSourceManager = new DataSourceManager();
|
||||
const middleware = dataSourceManager.middleware();
|
||||
|
||||
const ctx = {
|
||||
get: () => 'main',
|
||||
throw: (message) => {
|
||||
throw new Error(message);
|
||||
},
|
||||
};
|
||||
|
||||
let err;
|
||||
try {
|
||||
await middleware(ctx, () => {});
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
|
||||
expect(err.message).toBe('data source main does not exist');
|
||||
});
|
||||
});
|
||||
|
@ -0,0 +1,69 @@
|
||||
import { SequelizeCollectionManager } from '@nocobase/data-source-manager';
|
||||
|
||||
describe('sequelize collection manager', () => {
|
||||
it('should define collection', async () => {
|
||||
const collectionManager = new SequelizeCollectionManager({
|
||||
dialect: 'sqlite',
|
||||
database: ':memory:',
|
||||
});
|
||||
|
||||
collectionManager.defineCollection({
|
||||
name: 'users',
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'name',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const UsersCollection = collectionManager.getCollection('users');
|
||||
expect(UsersCollection).toBeTruthy();
|
||||
|
||||
expect(collectionManager.hasCollection('users')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should set collection filter', async () => {
|
||||
const collectionManager = new SequelizeCollectionManager({
|
||||
dialect: 'sqlite',
|
||||
database: ':memory:',
|
||||
collectionsFilter: (collection) => {
|
||||
return ['posts', 'comments'].includes(collection.name);
|
||||
},
|
||||
});
|
||||
|
||||
collectionManager.defineCollection({
|
||||
name: 'users',
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'name',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
collectionManager.defineCollection({
|
||||
name: 'posts',
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
collectionManager.defineCollection({
|
||||
name: 'comments',
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'content',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const collections = collectionManager.getCollections();
|
||||
expect(collections.length).toBe(2);
|
||||
expect(collections.map((collection) => collection.name)).toEqual(['posts', 'comments']);
|
||||
});
|
||||
});
|
@ -0,0 +1,14 @@
|
||||
import { joinCollectionName, parseCollectionName } from '@nocobase/data-source-manager';
|
||||
|
||||
describe('utils', () => {
|
||||
it('should join collection name', async () => {
|
||||
expect(joinCollectionName('main', 'users')).toBe('users');
|
||||
expect(joinCollectionName('test', 'users')).toBe('test:users');
|
||||
});
|
||||
|
||||
it('should parse collection name', async () => {
|
||||
expect(parseCollectionName('main:users')).toEqual(['main', 'users']);
|
||||
expect(parseCollectionName('users')).toEqual(['main', 'users']);
|
||||
expect(parseCollectionName('')).toEqual([]);
|
||||
});
|
||||
});
|
@ -1,7 +1,8 @@
|
||||
import { FieldOptions, IField } from './types';
|
||||
|
||||
export class CollectionField implements IField {
|
||||
options;
|
||||
options: FieldOptions;
|
||||
|
||||
constructor(options: FieldOptions) {
|
||||
this.updateOptions(options);
|
||||
}
|
||||
|
@ -8,9 +8,13 @@ export class CollectionManager implements ICollectionManager {
|
||||
|
||||
constructor(options = {}) {}
|
||||
|
||||
/* istanbul ignore next -- @preserve */
|
||||
getRegisteredFieldType(type) {}
|
||||
|
||||
/* istanbul ignore next -- @preserve */
|
||||
getRegisteredFieldInterface(key: string) {}
|
||||
|
||||
/* istanbul ignore next -- @preserve */
|
||||
getRegisteredModel(key: string) {
|
||||
return this.models.get(key);
|
||||
}
|
||||
@ -22,8 +26,13 @@ export class CollectionManager implements ICollectionManager {
|
||||
return this.repositories.get(key);
|
||||
}
|
||||
|
||||
/* istanbul ignore next -- @preserve */
|
||||
registerFieldTypes() {}
|
||||
|
||||
/* istanbul ignore next -- @preserve */
|
||||
registerFieldInterfaces() {}
|
||||
|
||||
/* istanbul ignore next -- @preserve */
|
||||
registerCollectionTemplates() {}
|
||||
|
||||
registerModels(models: Record<string, any>) {
|
||||
@ -46,6 +55,7 @@ export class CollectionManager implements ICollectionManager {
|
||||
|
||||
extendCollection(collectionOptions: CollectionOptions, mergeOptions?: MergeOptions): ICollection {
|
||||
const collection = this.getCollection(collectionOptions.name);
|
||||
collection.updateOptions(collectionOptions, mergeOptions);
|
||||
return collection;
|
||||
}
|
||||
|
||||
|
@ -13,9 +13,7 @@ export class Collection implements ICollection {
|
||||
) {
|
||||
this.setRepository(options.repository);
|
||||
if (options.fields) {
|
||||
for (const field of options.fields) {
|
||||
this.setField(field.name, field);
|
||||
}
|
||||
this.setFields(options.fields);
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,6 +22,7 @@ export class Collection implements ICollection {
|
||||
newOptions = merge(this.options, newOptions, mergeOptions);
|
||||
this.options = newOptions;
|
||||
|
||||
this.setFields(newOptions.fields || []);
|
||||
if (options.repository) {
|
||||
this.setRepository(options.repository);
|
||||
}
|
||||
@ -31,6 +30,12 @@ export class Collection implements ICollection {
|
||||
return this;
|
||||
}
|
||||
|
||||
setFields(fields: any[]) {
|
||||
for (const field of fields) {
|
||||
this.setField(field.name, field);
|
||||
}
|
||||
}
|
||||
|
||||
setField(name: string, options: any) {
|
||||
const field = new CollectionField(options);
|
||||
this.fields.set(name, field);
|
||||
|
@ -1,5 +0,0 @@
|
||||
import Database from '@nocobase/database';
|
||||
|
||||
export interface DataSourceWithDatabase {
|
||||
db: Database;
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { ACL } from '@nocobase/acl';
|
||||
import { ResourceManager, getNameByParams, parseRequest } from '@nocobase/resourcer';
|
||||
import { getNameByParams, parseRequest, ResourceManager } from '@nocobase/resourcer';
|
||||
import EventEmitter from 'events';
|
||||
import compose from 'koa-compose';
|
||||
import { loadDefaultActions } from './load-default-actions';
|
||||
@ -34,14 +34,49 @@ export abstract class DataSource extends EventEmitter {
|
||||
});
|
||||
|
||||
this.collectionManager = this.createCollectionManager(options);
|
||||
this.resourceManager.registerActionHandlers(loadDefaultActions(this));
|
||||
this.resourceManager.registerActionHandlers(loadDefaultActions());
|
||||
|
||||
if (options.acl !== false) {
|
||||
this.resourceManager.use(this.acl.middleware(), { tag: 'acl', after: ['auth'] });
|
||||
}
|
||||
}
|
||||
|
||||
collectionToResourceMiddleware() {
|
||||
middleware(middlewares: any = []) {
|
||||
const dataSource = this;
|
||||
|
||||
if (!this['_used']) {
|
||||
for (const [fn, options] of middlewares) {
|
||||
this.resourceManager.use(fn, options);
|
||||
}
|
||||
this['_used'] = true;
|
||||
}
|
||||
|
||||
return async (ctx, next) => {
|
||||
ctx.dataSource = dataSource;
|
||||
|
||||
ctx.getCurrentRepository = () => {
|
||||
const { resourceName, resourceOf } = ctx.action;
|
||||
|
||||
return this.collectionManager.getRepository(resourceName, resourceOf);
|
||||
};
|
||||
|
||||
return compose([this.collectionToResourceMiddleware(), this.resourceManager.middleware()])(ctx, next);
|
||||
};
|
||||
}
|
||||
|
||||
createACL() {
|
||||
return new ACL();
|
||||
}
|
||||
|
||||
createResourceManager(options) {
|
||||
return new ResourceManager(options);
|
||||
}
|
||||
|
||||
async load(options: any = {}) {}
|
||||
|
||||
abstract createCollectionManager(options?: any): ICollectionManager;
|
||||
|
||||
protected collectionToResourceMiddleware() {
|
||||
return async (ctx, next) => {
|
||||
const params = parseRequest(
|
||||
{
|
||||
@ -77,38 +112,4 @@ export abstract class DataSource extends EventEmitter {
|
||||
return next();
|
||||
};
|
||||
}
|
||||
|
||||
middleware(middlewares: any = []) {
|
||||
const dataSource = this;
|
||||
|
||||
if (!this['_used']) {
|
||||
for (const [fn, options] of middlewares) {
|
||||
this.resourceManager.use(fn, options);
|
||||
}
|
||||
this['_used'] = true;
|
||||
}
|
||||
|
||||
return async (ctx, next) => {
|
||||
ctx.getCurrentRepository = () => {
|
||||
const { resourceName, resourceOf } = ctx.action;
|
||||
return this.collectionManager.getRepository(resourceName, resourceOf);
|
||||
};
|
||||
|
||||
ctx.dataSource = dataSource;
|
||||
|
||||
return compose([this.collectionToResourceMiddleware(), this.resourceManager.middleware()])(ctx, next);
|
||||
};
|
||||
}
|
||||
|
||||
createACL() {
|
||||
return new ACL();
|
||||
}
|
||||
|
||||
createResourceManager(options) {
|
||||
return new ResourceManager(options);
|
||||
}
|
||||
|
||||
async load(options: any = {}) {}
|
||||
|
||||
abstract createCollectionManager(options?: any): ICollectionManager;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { assign } from '@nocobase/utils';
|
||||
import { getRepositoryFromParams, pageArgsToLimitArgs } from './utils';
|
||||
import { pageArgsToLimitArgs } from './utils';
|
||||
import { Context } from '@nocobase/actions';
|
||||
|
||||
function totalPage(total, pageSize): number {
|
||||
@ -27,7 +27,7 @@ function findArgs(ctx: Context) {
|
||||
async function listWithPagination(ctx: Context) {
|
||||
const { page = 1, pageSize = 50 } = ctx.action.params;
|
||||
|
||||
const repository = getRepositoryFromParams(ctx);
|
||||
const repository = ctx.getCurrentRepository();
|
||||
|
||||
const options = {
|
||||
context: ctx,
|
||||
@ -53,7 +53,7 @@ async function listWithPagination(ctx: Context) {
|
||||
}
|
||||
|
||||
async function listWithNonPaged(ctx: Context) {
|
||||
const repository = getRepositoryFromParams(ctx);
|
||||
const repository = ctx.getCurrentRepository();
|
||||
|
||||
const rows = await repository.find({ context: ctx, ...findArgs(ctx) });
|
||||
|
||||
|
@ -1,22 +1,20 @@
|
||||
import actions, { Context } from '@nocobase/actions';
|
||||
import { Context } from '@nocobase/actions';
|
||||
|
||||
import { getRepositoryFromParams } from './utils';
|
||||
export function createMoveAction(databaseMoveAction) {
|
||||
return async function move(ctx: Context, next) {
|
||||
const repository = ctx.getCurrentRepository();
|
||||
|
||||
const databaseMoveAction = actions.move;
|
||||
if (repository.move) {
|
||||
ctx.body = await repository.move(ctx.action.params);
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
|
||||
export async function move(ctx: Context, next) {
|
||||
const repository = getRepositoryFromParams(ctx);
|
||||
if (repository.database) {
|
||||
ctx.databaseRepository = repository;
|
||||
return databaseMoveAction(ctx, next);
|
||||
}
|
||||
|
||||
if (repository.move) {
|
||||
ctx.body = await repository.move(ctx.action.params);
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
|
||||
if (repository.database) {
|
||||
ctx.databaseRepository = repository;
|
||||
return databaseMoveAction(ctx, next);
|
||||
}
|
||||
|
||||
throw new Error(`Repository can not handle action move for ${ctx.action.resourceName}`);
|
||||
throw new Error(`Repository can not handle action move for ${ctx.action.resourceName}`);
|
||||
};
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
import lodash from 'lodash';
|
||||
import { DataSource } from '../data-source';
|
||||
import { getRepositoryFromParams } from './utils';
|
||||
import { Context } from '@nocobase/actions';
|
||||
|
||||
export function proxyToRepository(paramKeys: string[] | ((ctx: any) => object), repositoryMethod: string) {
|
||||
return async function (ctx, next) {
|
||||
const repository = getRepositoryFromParams(ctx);
|
||||
return async function (ctx: Context, next: () => Promise<void>) {
|
||||
const repository = ctx.getCurrentRepository();
|
||||
const callObj =
|
||||
typeof paramKeys === 'function' ? paramKeys(ctx) : { ...lodash.pick(ctx.action.params, paramKeys), context: ctx };
|
||||
const dataSource: DataSource = ctx.dataSource;
|
||||
|
@ -1,6 +1,3 @@
|
||||
import { Context } from '@nocobase/actions';
|
||||
import { DataSource, IRepository } from '../';
|
||||
|
||||
export function pageArgsToLimitArgs(
|
||||
page: number,
|
||||
pageSize: number,
|
||||
@ -13,20 +10,3 @@ export function pageArgsToLimitArgs(
|
||||
limit: pageSize,
|
||||
};
|
||||
}
|
||||
|
||||
export function getRepositoryFromParams(ctx: Context): IRepository {
|
||||
const { resourceName, sourceId, actionName } = ctx.action;
|
||||
|
||||
const dataSource: DataSource = ctx.dataSource;
|
||||
|
||||
if (sourceId === '_' && ['get', 'list'].includes(actionName)) {
|
||||
const collection = dataSource.collectionManager.getCollection(resourceName);
|
||||
return dataSource.collectionManager.getRepository(collection.name);
|
||||
}
|
||||
|
||||
if (sourceId) {
|
||||
return dataSource.collectionManager.getRepository(resourceName, sourceId);
|
||||
}
|
||||
|
||||
return dataSource.collectionManager.getRepository(resourceName);
|
||||
}
|
||||
|
@ -7,5 +7,4 @@ export * from './sequelize-data-source';
|
||||
export * from './load-default-actions';
|
||||
export * from './types';
|
||||
|
||||
export * from './data-source-with-database';
|
||||
export * from './utils';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { list } from './default-actions/list';
|
||||
import { move } from './default-actions/move';
|
||||
import { createMoveAction } from './default-actions/move';
|
||||
import { proxyToRepository } from './default-actions/proxy-to-repository';
|
||||
import { DataSource } from './data-source';
|
||||
import globalActions from '@nocobase/actions';
|
||||
|
||||
type Actions = { [key: string]: { params: Array<string> | ((ctx: any) => Array<string>); method: string } };
|
||||
|
||||
@ -65,13 +65,13 @@ const actions: Actions = {
|
||||
},
|
||||
};
|
||||
|
||||
export function loadDefaultActions(dataSource: DataSource) {
|
||||
export function loadDefaultActions() {
|
||||
return {
|
||||
...Object.keys(actions).reduce((carry, key) => {
|
||||
carry[key] = proxyToRepository(actions[key].params, actions[key].method);
|
||||
return carry;
|
||||
}, {}),
|
||||
list,
|
||||
move,
|
||||
move: createMoveAction(globalActions.move),
|
||||
};
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
/* istanbul ignore file -- @preserve */
|
||||
|
||||
import { IModel, IRepository } from './types';
|
||||
import * as console from 'console';
|
||||
|
||||
@ -5,13 +7,21 @@ export class Repository implements IRepository {
|
||||
async create(options) {
|
||||
console.log('Repository.create....');
|
||||
}
|
||||
|
||||
async update(options) {}
|
||||
|
||||
async find(options?: any): Promise<IModel[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async findOne(options?: any): Promise<IModel> {
|
||||
return {};
|
||||
return {
|
||||
toJSON() {
|
||||
return {};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async destroy(options) {}
|
||||
|
||||
count(options?: any): Promise<Number> {
|
||||
|
@ -1,9 +1,12 @@
|
||||
/* istanbul ignore file -- @preserve */
|
||||
|
||||
import Database from '@nocobase/database';
|
||||
import { CollectionOptions, ICollection, ICollectionManager, IRepository, MergeOptions } from './types';
|
||||
|
||||
export class SequelizeCollectionManager implements ICollectionManager {
|
||||
db: Database;
|
||||
options: any;
|
||||
|
||||
constructor(options) {
|
||||
this.db = this.createDB(options);
|
||||
this.options = options;
|
||||
|
@ -2,8 +2,6 @@ import { DataSource } from './data-source';
|
||||
import { SequelizeCollectionManager } from './sequelize-collection-manager';
|
||||
|
||||
export class SequelizeDataSource extends DataSource {
|
||||
async load() {}
|
||||
|
||||
createCollectionManager(options?: any) {
|
||||
return new SequelizeCollectionManager(options.collectionManager);
|
||||
}
|
||||
|
@ -25,27 +25,44 @@ export type FieldOptions = {
|
||||
export interface IField {
|
||||
options: FieldOptions;
|
||||
}
|
||||
|
||||
export interface ICollection {
|
||||
repository: any;
|
||||
updateOptions(options: any): void;
|
||||
repository: IRepository;
|
||||
|
||||
updateOptions(options: CollectionOptions, mergeOptions?: MergeOptions): void;
|
||||
|
||||
setField(name: string, options: any): IField;
|
||||
|
||||
removeField(name: string): void;
|
||||
|
||||
getFields(): Array<IField>;
|
||||
|
||||
getField(name: string): IField;
|
||||
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface IModel {
|
||||
toJSON: () => any;
|
||||
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface IRepository {
|
||||
find(options?: any): Promise<IModel[]>;
|
||||
|
||||
findOne(options?: any): Promise<IModel>;
|
||||
|
||||
count(options?: any): Promise<Number>;
|
||||
|
||||
findAndCount(options?: any): Promise<[IModel[], Number]>;
|
||||
create(options: any): void;
|
||||
update(options: any): void;
|
||||
destroy(options: any): void;
|
||||
|
||||
create(options: any): any;
|
||||
|
||||
update(options: any): any;
|
||||
|
||||
destroy(options: any): any;
|
||||
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
@ -55,9 +72,13 @@ export type MergeOptions = {
|
||||
|
||||
export interface ICollectionManager {
|
||||
registerFieldTypes(types: Record<string, any>): void;
|
||||
|
||||
registerFieldInterfaces(interfaces: Record<string, any>): void;
|
||||
|
||||
registerCollectionTemplates(templates: Record<string, any>): void;
|
||||
|
||||
registerModels(models: Record<string, any>): void;
|
||||
|
||||
registerRepositories(repositories: Record<string, any>): void;
|
||||
|
||||
getRegisteredRepository(key: string): IRepository;
|
||||
@ -67,9 +88,12 @@ export interface ICollectionManager {
|
||||
extendCollection(collectionOptions: CollectionOptions, mergeOptions?: MergeOptions): ICollection;
|
||||
|
||||
hasCollection(name: string): boolean;
|
||||
|
||||
getCollection(name: string): ICollection;
|
||||
|
||||
getCollections(): Array<ICollection>;
|
||||
|
||||
getRepository(name: string, sourceId?: string | number): IRepository;
|
||||
|
||||
sync(): Promise<void>;
|
||||
}
|
||||
|
@ -12,5 +12,6 @@ export function joinCollectionName(dataSourceName: string, collectionName: strin
|
||||
if (!dataSourceName || dataSourceName === 'main') {
|
||||
return collectionName;
|
||||
}
|
||||
|
||||
return `${dataSourceName}:${collectionName}`;
|
||||
}
|
||||
|
@ -32,6 +32,8 @@ describe('filterMatch', () => {
|
||||
}),
|
||||
).toBeTruthy();
|
||||
|
||||
expect(filterMatch(post, { 'title.$not': 't1' })).toBeFalsy();
|
||||
|
||||
expect(
|
||||
filterMatch(post, {
|
||||
$or: [{ title: 't1' }, { title: 't2' }],
|
||||
|
@ -0,0 +1,68 @@
|
||||
import Database, { Collection, mockDatabase } from '@nocobase/database';
|
||||
|
||||
describe('boolean operator', () => {
|
||||
let db: Database;
|
||||
|
||||
let User: Collection;
|
||||
|
||||
afterEach(async () => {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
db = mockDatabase({});
|
||||
|
||||
await db.clean({ drop: true });
|
||||
|
||||
User = db.collection({
|
||||
name: 'users',
|
||||
fields: [{ type: 'boolean', name: 'activated' }],
|
||||
});
|
||||
|
||||
await db.sync({
|
||||
force: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should query $isFalsy', async () => {
|
||||
await db.getRepository('users').create({
|
||||
values: [
|
||||
{
|
||||
activated: false,
|
||||
},
|
||||
{
|
||||
activated: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const res = await db.getRepository('users').find({
|
||||
filter: {
|
||||
'activated.$isFalsy': true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should query $isTruly', async () => {
|
||||
await db.getRepository('users').create({
|
||||
values: [
|
||||
{
|
||||
activated: false,
|
||||
},
|
||||
{
|
||||
activated: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const res = await db.getRepository('users').find({
|
||||
filter: {
|
||||
'activated.$isTruly': true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.length).toBe(1);
|
||||
});
|
||||
});
|
@ -24,7 +24,7 @@ describe('string operator', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should escape underscore in inlcude operator', async () => {
|
||||
it('should escape underscore in include operator', async () => {
|
||||
const u1 = await db.getRepository('users').create({
|
||||
values: {
|
||||
name: 'names of u1',
|
||||
@ -40,6 +40,169 @@ describe('string operator', () => {
|
||||
expect(u1Res).toBeNull();
|
||||
});
|
||||
|
||||
it('should query with include operator with array values', async () => {
|
||||
await db.getRepository('users').create({
|
||||
values: [
|
||||
{
|
||||
name: 'u1',
|
||||
},
|
||||
{
|
||||
name: 'u2',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const res = await db.getRepository('users').find({
|
||||
filter: {
|
||||
'name.$includes': ['u1', 'u2'],
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.length).toEqual(2);
|
||||
});
|
||||
|
||||
it('should query $notIncludes with array values', async () => {
|
||||
await db.getRepository('users').create({
|
||||
values: [
|
||||
{
|
||||
name: 'u1',
|
||||
},
|
||||
{
|
||||
name: 'u2',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const res = await db.getRepository('users').find({
|
||||
filter: {
|
||||
'name.$notIncludes': ['u1', 'u2'],
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('should query $notIncludes', async () => {
|
||||
await db.getRepository('users').create({
|
||||
values: {
|
||||
name: 'u1',
|
||||
},
|
||||
});
|
||||
|
||||
const res = await db.getRepository('users').find({
|
||||
filter: {
|
||||
'name.$notIncludes': 'u1',
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('should query $startsWith', async () => {
|
||||
await db.getRepository('users').create({
|
||||
values: [
|
||||
{
|
||||
name: 'u1',
|
||||
},
|
||||
{
|
||||
name: 'b1',
|
||||
},
|
||||
{
|
||||
name: 'c1',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const res = await db.getRepository('users').find({
|
||||
filter: {
|
||||
'name.$startsWith': ['u', 'b'],
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.length).toEqual(2);
|
||||
});
|
||||
|
||||
it('should query $notStartsWith', async () => {
|
||||
await db.getRepository('users').create({
|
||||
values: [
|
||||
{
|
||||
name: 'u1',
|
||||
},
|
||||
{
|
||||
name: 'v2',
|
||||
},
|
||||
{
|
||||
name: 'b1',
|
||||
},
|
||||
{
|
||||
name: 'b2',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const res = await db.getRepository('users').find({
|
||||
filter: {
|
||||
'name.$notStartsWith': ['u', 'v'],
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.length).toEqual(2);
|
||||
});
|
||||
|
||||
it('should query $endWith', async () => {
|
||||
await db.getRepository('users').create({
|
||||
values: [
|
||||
{
|
||||
name: 'u1',
|
||||
},
|
||||
{
|
||||
name: 'b1',
|
||||
},
|
||||
{
|
||||
name: 'c1',
|
||||
},
|
||||
{
|
||||
name: 'u2',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const res = await db.getRepository('users').find({
|
||||
filter: {
|
||||
'name.$endWith': ['1'],
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.length).toEqual(3);
|
||||
});
|
||||
|
||||
it('should query $notEndWith', async () => {
|
||||
await db.getRepository('users').create({
|
||||
values: [
|
||||
{
|
||||
name: 'u1',
|
||||
},
|
||||
{
|
||||
name: 'b1',
|
||||
},
|
||||
{
|
||||
name: 'c1',
|
||||
},
|
||||
{
|
||||
name: 'u2',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const res = await db.getRepository('users').find({
|
||||
filter: {
|
||||
'name.$notEndWith': ['1'],
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('should query with include operator', async () => {
|
||||
const u1 = await db.getRepository('users').create({
|
||||
values: {
|
||||
|
@ -0,0 +1,78 @@
|
||||
import { Database, mockDatabase } from '@nocobase/database';
|
||||
|
||||
describe('query interface', async () => {
|
||||
let db: Database;
|
||||
|
||||
beforeEach(async () => {
|
||||
db = mockDatabase({
|
||||
logging: console.log,
|
||||
});
|
||||
|
||||
await db.clean({ drop: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
it('should get auto incr info', async () => {
|
||||
const User = db.collection({
|
||||
name: 'users',
|
||||
autoGenId: false,
|
||||
fields: [
|
||||
{
|
||||
type: 'bigInt',
|
||||
name: 'id',
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'name',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await db.sync();
|
||||
|
||||
await User.repository.create({
|
||||
values: [{ name: 'a' }, { name: 'b' }, { name: 'c' }],
|
||||
});
|
||||
|
||||
const incrInfo = await db.queryInterface.getAutoIncrementInfo({
|
||||
tableInfo: {
|
||||
tableName: User.model.tableName,
|
||||
schema: process.env['DB_DIALECT'] === 'postgres' ? User.options.schema || 'public' : undefined,
|
||||
},
|
||||
fieldName: 'id',
|
||||
});
|
||||
|
||||
if (db.isMySQLCompatibleDialect()) {
|
||||
expect(incrInfo.currentVal).toBe(4);
|
||||
} else {
|
||||
expect(incrInfo.currentVal).toBe(3);
|
||||
}
|
||||
|
||||
await db.queryInterface.setAutoIncrementVal({
|
||||
tableInfo: {
|
||||
tableName: User.model.tableName,
|
||||
schema: process.env['DB_DIALECT'] === 'postgres' ? User.options.schema || 'public' : undefined,
|
||||
},
|
||||
columnName: 'id',
|
||||
currentVal: 100,
|
||||
seqName: incrInfo.seqName,
|
||||
});
|
||||
|
||||
const userD = await User.repository.create({
|
||||
values: {
|
||||
name: 'd',
|
||||
},
|
||||
});
|
||||
|
||||
if (db.isMySQLCompatibleDialect()) {
|
||||
expect(userD.id).toBe(100);
|
||||
} else {
|
||||
expect(userD.id).toBe(101);
|
||||
}
|
||||
});
|
||||
});
|
@ -11,7 +11,7 @@ describe('infer fields', () => {
|
||||
|
||||
db.collection({
|
||||
name: 'users',
|
||||
schema: db.inDialect('postgres') ? 'public' : undefined,
|
||||
schema: db.inDialect('postgres') ? process.env.DB_SCHEMA : undefined,
|
||||
fields: [
|
||||
{ name: 'id', type: 'bigInt', interface: 'id' },
|
||||
{ name: 'nickname', type: 'string', interface: 'input' },
|
||||
@ -19,7 +19,7 @@ describe('infer fields', () => {
|
||||
});
|
||||
db.collection({
|
||||
name: 'roles',
|
||||
schema: db.inDialect('postgres') ? 'public' : undefined,
|
||||
schema: db.inDialect('postgres') ? process.env.DB_SCHEMA : undefined,
|
||||
fields: [
|
||||
{ name: 'id', type: 'bigInt', interface: 'id' },
|
||||
{ name: 'title', type: 'string', interface: 'input' },
|
||||
@ -28,7 +28,7 @@ describe('infer fields', () => {
|
||||
});
|
||||
db.collection({
|
||||
name: 'roles_users',
|
||||
schema: db.inDialect('postgres') ? 'public' : undefined,
|
||||
schema: db.inDialect('postgres') ? process.env.DB_SCHEMA : undefined,
|
||||
fields: [
|
||||
{ name: 'id', type: 'bigInt', interface: 'id' },
|
||||
{ name: 'userId', type: 'bigInt', interface: 'id' },
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { mockDatabase } from '../../mock-database';
|
||||
import { SqlCollection } from '../sql-collection';
|
||||
import { SqlCollection } from '../../sql-collection';
|
||||
|
||||
test('sql-collection', async () => {
|
||||
const db = mockDatabase({ tablePrefix: '' });
|
||||
|
20
packages/core/database/src/__tests__/utils.test.ts
Normal file
20
packages/core/database/src/__tests__/utils.test.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import Database, { mockDatabase } from '@nocobase/database';
|
||||
|
||||
describe('database utils', () => {
|
||||
let db: Database;
|
||||
|
||||
afterEach(async () => {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
db = mockDatabase({});
|
||||
|
||||
await db.clean({ drop: true });
|
||||
});
|
||||
|
||||
it.runIf(process.env['DB_DIALECT'] === 'postgres')('should get database schema', async () => {
|
||||
const schema = process.env['DB_SCHEMA'] || 'public';
|
||||
expect(db.utils.schema()).toEqual(schema);
|
||||
});
|
||||
});
|
@ -0,0 +1,20 @@
|
||||
import { ArrayValueParser } from '@nocobase/database';
|
||||
|
||||
describe('array value parser', () => {
|
||||
let parser: ArrayValueParser;
|
||||
|
||||
beforeEach(() => {
|
||||
parser = new ArrayValueParser({}, {});
|
||||
});
|
||||
|
||||
const expectValue = (value) => {
|
||||
parser = new ArrayValueParser({}, {});
|
||||
parser.setValue(value);
|
||||
return expect(parser.getValue());
|
||||
};
|
||||
|
||||
test('array value parser', async () => {
|
||||
expectValue('tag1').toEqual(['tag1']);
|
||||
expectValue('tag1,tag2').toEqual(['tag1', 'tag2']);
|
||||
});
|
||||
});
|
@ -0,0 +1,33 @@
|
||||
import { BooleanValueParser } from '../../value-parsers';
|
||||
|
||||
describe('boolean value parser', () => {
|
||||
let parser: BooleanValueParser;
|
||||
|
||||
beforeEach(() => {
|
||||
parser = new BooleanValueParser({}, {});
|
||||
});
|
||||
|
||||
const expectValue = (value) => {
|
||||
parser = new BooleanValueParser({}, {});
|
||||
parser.setValue(value);
|
||||
return expect(parser.getValue());
|
||||
};
|
||||
|
||||
test('falsy value', async () => {
|
||||
expectValue('n').toBe(false);
|
||||
expectValue('false').toBe(false);
|
||||
expectValue('0').toBe(false);
|
||||
expectValue('false').toBe(false);
|
||||
expectValue(false).toBe(false);
|
||||
expectValue(0).toBe(false);
|
||||
});
|
||||
|
||||
test('truthy value', async () => {
|
||||
expectValue('y').toBe(true);
|
||||
expectValue('true').toBe(true);
|
||||
expectValue('1').toBe(true);
|
||||
expectValue('yes').toBe(true);
|
||||
expectValue(true).toBe(true);
|
||||
expectValue(1).toBe(true);
|
||||
});
|
||||
});
|
@ -0,0 +1,21 @@
|
||||
import { JsonValueParser } from '../../value-parsers';
|
||||
|
||||
describe('array value parser', () => {
|
||||
let parser: JsonValueParser;
|
||||
|
||||
beforeEach(() => {
|
||||
parser = new JsonValueParser({}, {});
|
||||
});
|
||||
|
||||
const expectValue = (value) => {
|
||||
parser = new JsonValueParser({}, {});
|
||||
parser.setValue(value);
|
||||
return expect(parser.getValue());
|
||||
};
|
||||
|
||||
test('json value parser', async () => {
|
||||
expectValue('{"a":1}').toEqual({ a: 1 });
|
||||
expectValue('{"a":1,"b":2}').toEqual({ a: 1, b: 2 });
|
||||
expectValue('{"a":1,"b":2,"c":3}').toEqual({ a: 1, b: 2, c: 3 });
|
||||
});
|
||||
});
|
@ -0,0 +1,21 @@
|
||||
import { StringValueParser } from '../../value-parsers';
|
||||
|
||||
describe('array value parser', () => {
|
||||
let parser: StringValueParser;
|
||||
|
||||
beforeEach(() => {
|
||||
parser = new StringValueParser({}, {});
|
||||
});
|
||||
|
||||
const expectValue = (value) => {
|
||||
parser = new StringValueParser({}, {});
|
||||
parser.setValue(value);
|
||||
return expect(parser.getValue());
|
||||
};
|
||||
|
||||
test('string value parser', async () => {
|
||||
expectValue('{"a":1}').toEqual('{"a":1}');
|
||||
expectValue('{"a":1,"b":2}').toEqual('{"a":1,"b":2}');
|
||||
expectValue('{"a":1,"b":2,"c":3}').toEqual('{"a":1,"b":2,"c":3}');
|
||||
});
|
||||
});
|
@ -201,7 +201,7 @@ export class Collection<
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* @internal
|
||||
*/
|
||||
modelInit() {
|
||||
if (this.model) {
|
||||
@ -307,6 +307,9 @@ export class Collection<
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
correctOptions(options) {
|
||||
if (options.primaryKey && options.autoIncrement) {
|
||||
delete options.defaultValue;
|
||||
@ -555,9 +558,6 @@ export class Collection<
|
||||
return field as Field;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
updateOptions(options: CollectionOptions, mergeOptions?: any) {
|
||||
let newOptions = lodash.cloneDeep(options);
|
||||
newOptions = merge(this.options, newOptions, mergeOptions);
|
||||
@ -594,12 +594,6 @@ export class Collection<
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
* @param name
|
||||
* @param options
|
||||
*/
|
||||
updateField(name: string, options: FieldOptions) {
|
||||
if (!this.hasField(name)) {
|
||||
throw new Error(`field ${name} not exists`);
|
||||
@ -701,6 +695,9 @@ export class Collection<
|
||||
this.refreshIndexes();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
refreshIndexes() {
|
||||
// @ts-ignore
|
||||
const indexes: any[] = this.model._indexes;
|
||||
|
@ -148,34 +148,6 @@ export const DialectVersionAccessors = {
|
||||
},
|
||||
};
|
||||
|
||||
class DatabaseVersion {
|
||||
db: Database;
|
||||
|
||||
constructor(db: Database) {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
async satisfies(versions) {
|
||||
const accessors = DialectVersionAccessors;
|
||||
for (const dialect of Object.keys(accessors)) {
|
||||
if (this.db.inDialect(dialect)) {
|
||||
if (!versions?.[dialect]) {
|
||||
return false;
|
||||
}
|
||||
const [result] = (await this.db.sequelize.query(accessors[dialect].sql)) as any;
|
||||
const versionResult = accessors[dialect].get(result?.[0]?.version);
|
||||
|
||||
if (lodash.isPlainObject(versionResult) && versionResult.dialect) {
|
||||
return semver.satisfies(versionResult.version, versions[versionResult.dialect]);
|
||||
}
|
||||
|
||||
return semver.satisfies(versionResult, versions[dialect]);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class Database extends EventEmitter implements AsyncEmitter {
|
||||
sequelize: Sequelize;
|
||||
migrator: Umzug;
|
||||
@ -197,7 +169,6 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
||||
inheritanceMap = new InheritanceMap();
|
||||
importedFrom = new Map<string, Set<string>>();
|
||||
modelHook: ModelHook;
|
||||
version: DatabaseVersion;
|
||||
delayCollectionExtend = new Map<string, { collectionOptions: CollectionOptions; mergeOptions?: any }[]>();
|
||||
logger: Logger;
|
||||
collectionGroupManager = new CollectionGroupManager(this);
|
||||
@ -208,8 +179,6 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
||||
constructor(options: DatabaseOptions) {
|
||||
super();
|
||||
|
||||
this.version = new DatabaseVersion(this);
|
||||
|
||||
const opts = {
|
||||
sync: {
|
||||
alter: {
|
||||
@ -350,6 +319,9 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
||||
return this._instanceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
createMigrator({ migrations }) {
|
||||
const migratorOptions: any = this.options.migrator || {};
|
||||
const context = {
|
||||
@ -371,10 +343,16 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
setContext(context: any) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
sequelizeOptions(options) {
|
||||
if (options.dialect === 'postgres') {
|
||||
if (!options.hooks) {
|
||||
@ -393,6 +371,9 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
initListener() {
|
||||
this.on('afterConnect', async (client) => {
|
||||
if (this.inDialect('postgres')) {
|
||||
@ -644,6 +625,9 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
||||
return this.getCollection(name)?.repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
addPendingField(field: RelationField) {
|
||||
const associating = this.pendingFields;
|
||||
const items = this.pendingFields.get(field.target) || [];
|
||||
@ -651,6 +635,9 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
||||
associating.set(field.target, items);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
removePendingField(field: RelationField) {
|
||||
const items = this.pendingFields.get(field.target) || [];
|
||||
const index = items.indexOf(field);
|
||||
@ -693,6 +680,9 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
initOperators() {
|
||||
const operators = new Map();
|
||||
|
||||
@ -717,6 +707,9 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
buildField(options, context: FieldContext) {
|
||||
const { type } = options;
|
||||
|
||||
@ -794,10 +787,11 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
||||
return await this.queryInterface.collectionTableExists(collection, options);
|
||||
}
|
||||
|
||||
public isSqliteMemory() {
|
||||
isSqliteMemory() {
|
||||
return this.sequelize.getDialect() === 'sqlite' && lodash.get(this.options, 'storage') == ':memory:';
|
||||
}
|
||||
|
||||
/* istanbul ignore next -- @preserve */
|
||||
async auth(options: Omit<QueryOptions, 'retry'> & { retry?: number | Pick<QueryOptions, 'retry'> } = {}) {
|
||||
const { retry = 10, ...others } = options;
|
||||
const startingDelay = 50;
|
||||
@ -833,10 +827,16 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async checkVersion() {
|
||||
return await checkDatabaseVersion(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async prepare() {
|
||||
if (this.isMySQLCompatibleDialect()) {
|
||||
const result = await this.sequelize.query(`SHOW VARIABLES LIKE 'lower_case_table_names'`, { plain: true });
|
||||
|
@ -1,3 +1,5 @@
|
||||
/* istanbul ignore file -- @preserve */
|
||||
|
||||
import { Database, IDatabaseOptions } from './database';
|
||||
import fs from 'fs';
|
||||
import semver from 'semver';
|
||||
|
@ -1,3 +1,4 @@
|
||||
/* istanbul ignore file -- @preserve */
|
||||
import { merge } from '@nocobase/utils';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import fetch from 'node-fetch';
|
||||
@ -43,7 +44,7 @@ export function getConfigByEnv() {
|
||||
|
||||
function customLogger(queryString, queryObject) {
|
||||
console.log(queryString); // outputs a string
|
||||
if (queryObject.bind) {
|
||||
if (queryObject?.bind) {
|
||||
console.log(queryObject.bind); // outputs an array
|
||||
}
|
||||
}
|
||||
|
@ -1,52 +0,0 @@
|
||||
import { Database } from './database';
|
||||
import FilterParser from './filter-parser';
|
||||
|
||||
const db = new Database({
|
||||
dialect: 'sqlite',
|
||||
dialectModule: require('sqlite3'),
|
||||
storage: ':memory:',
|
||||
});
|
||||
|
||||
(async () => {
|
||||
const User = db.collection({
|
||||
name: 'users',
|
||||
fields: [{ type: 'string', name: 'name' }],
|
||||
});
|
||||
|
||||
const Post = db.collection({
|
||||
name: 'posts',
|
||||
fields: [
|
||||
{ type: 'string', name: 'title' },
|
||||
{
|
||||
type: 'belongsTo',
|
||||
name: 'user',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await db.sync();
|
||||
|
||||
const repository = User.repository;
|
||||
|
||||
await repository.createMany({
|
||||
records: [
|
||||
{ name: 'u1', posts: [{ title: 'u1t1' }] },
|
||||
{ name: 'u2', posts: [{ title: 'u2t1' }] },
|
||||
{ name: 'u3', posts: [{ title: 'u3t1' }] },
|
||||
],
|
||||
});
|
||||
|
||||
const Model = User.model;
|
||||
const user = await Model.findOne({
|
||||
subQuery: false,
|
||||
where: {
|
||||
'$posts.title$': 'u1t1',
|
||||
},
|
||||
include: { association: 'posts', attributes: [] },
|
||||
attributes: {
|
||||
include: [],
|
||||
},
|
||||
});
|
||||
|
||||
console.log(user.toJSON());
|
||||
})();
|
@ -1,3 +1,5 @@
|
||||
/* istanbul ignore file -- @preserve */
|
||||
|
||||
import { Transaction, Transactionable } from 'sequelize';
|
||||
import { Collection } from '../collection';
|
||||
import sqlParser from '../sql-parser';
|
||||
|
@ -1,3 +1,5 @@
|
||||
/* istanbul ignore file -- @preserve */
|
||||
|
||||
import { Collection } from '../collection';
|
||||
import sqlParser from '../sql-parser';
|
||||
import QueryInterface, { TableInfo } from './query-interface';
|
||||
|
@ -184,18 +184,6 @@ export class BelongsToManyRepository extends MultipleRelationRepository {
|
||||
return;
|
||||
}
|
||||
|
||||
extendFindOptions(findOptions) {
|
||||
let joinTableAttributes;
|
||||
if (lodash.get(findOptions, 'fields')) {
|
||||
joinTableAttributes = [];
|
||||
}
|
||||
|
||||
return {
|
||||
...findOptions,
|
||||
joinTableAttributes,
|
||||
};
|
||||
}
|
||||
|
||||
throughName() {
|
||||
return this.throughModel().name;
|
||||
}
|
||||
|
@ -4,6 +4,9 @@ import { SingleRelationFindOption, SingleRelationRepository } from './single-rel
|
||||
type BelongsToFindOptions = SingleRelationFindOption;
|
||||
|
||||
export class BelongsToRepository extends SingleRelationRepository {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async filterOptions(sourceModel) {
|
||||
const association = this.association as BelongsTo;
|
||||
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { omit } from 'lodash';
|
||||
import { HasMany, Op } from 'sequelize';
|
||||
import { AggregateOptions, DestroyOptions, FindOptions, TK, TargetKey } from '../repository';
|
||||
import { AggregateOptions, DestroyOptions, FindOptions, TargetKey, TK } from '../repository';
|
||||
import { AssociatedOptions, MultipleRelationRepository } from './multiple-relation-repository';
|
||||
import { transaction } from './relation-repository';
|
||||
|
||||
export class HasManyRepository extends MultipleRelationRepository {
|
||||
async find(options?: FindOptions): Promise<any> {
|
||||
const targetRepository = this.targetCollection.repository;
|
||||
@ -120,6 +121,9 @@ export class HasManyRepository extends MultipleRelationRepository {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
accessors() {
|
||||
return (<HasMany>this.association).accessors;
|
||||
}
|
||||
|
@ -2,6 +2,9 @@ import { HasOne } from 'sequelize';
|
||||
import { SingleRelationRepository } from './single-relation-repository';
|
||||
|
||||
export class HasOneRepository extends SingleRelationRepository {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
filterOptions(sourceModel) {
|
||||
const association = this.association as HasOne;
|
||||
|
||||
|
@ -8,8 +8,8 @@ import {
|
||||
Filter,
|
||||
FindOneOptions,
|
||||
FindOptions,
|
||||
TK,
|
||||
TargetKey,
|
||||
TK,
|
||||
UpdateOptions,
|
||||
} from '../repository';
|
||||
import { updateModelByValues } from '../update-associations';
|
||||
@ -23,10 +23,6 @@ export interface AssociatedOptions extends Transactionable {
|
||||
}
|
||||
|
||||
export abstract class MultipleRelationRepository extends RelationRepository {
|
||||
extendFindOptions(findOptions) {
|
||||
return findOptions;
|
||||
}
|
||||
|
||||
async find(options?: FindOptions): Promise<any> {
|
||||
const targetRepository = this.targetCollection.repository;
|
||||
|
||||
|
@ -106,6 +106,9 @@ export abstract class SingleRelationRepository extends RelationRepository {
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
accessors() {
|
||||
return <SingleAssociationAccessors>super.accessors();
|
||||
}
|
||||
|
@ -28,6 +28,60 @@ export class SQLModel extends Model {
|
||||
|
||||
static async sync(): Promise<any> {}
|
||||
|
||||
static inferFields(): {
|
||||
[field: string]: {
|
||||
type: string;
|
||||
source: string;
|
||||
collection: string;
|
||||
interface: string;
|
||||
};
|
||||
} {
|
||||
const tables = this.parseTablesAndColumns();
|
||||
return tables.reduce((fields, { table, columns }) => {
|
||||
const tableName = this.getTableNameWithSchema(table);
|
||||
const collection = this.database.tableNameCollectionMap.get(tableName);
|
||||
if (!collection) {
|
||||
return fields;
|
||||
}
|
||||
const attributes = collection.model.getAttributes();
|
||||
const sourceFields = {};
|
||||
if (columns === '*') {
|
||||
Object.values(attributes).forEach((attribute) => {
|
||||
const field = collection.getField((attribute as any).fieldName);
|
||||
if (!field?.options.interface) {
|
||||
return;
|
||||
}
|
||||
sourceFields[field.name] = {
|
||||
collection: field.collection.name,
|
||||
type: field.type,
|
||||
source: `${field.collection.name}.${field.name}`,
|
||||
interface: field.options.interface,
|
||||
uiSchema: field.options.uiSchema,
|
||||
};
|
||||
});
|
||||
} else {
|
||||
(columns as { name: string; as: string }[]).forEach((column) => {
|
||||
const modelField = Object.values(attributes).find((attribute) => attribute.field === column.name);
|
||||
if (!modelField) {
|
||||
return;
|
||||
}
|
||||
const field = collection.getField((modelField as any).fieldName);
|
||||
if (!field?.options.interface) {
|
||||
return;
|
||||
}
|
||||
sourceFields[column.as || column.name] = {
|
||||
collection: field.collection.name,
|
||||
type: field.type,
|
||||
source: `${field.collection.name}.${field.name}`,
|
||||
interface: field.options.interface,
|
||||
uiSchema: field.options.uiSchema,
|
||||
};
|
||||
});
|
||||
}
|
||||
return { ...fields, ...sourceFields };
|
||||
}, {});
|
||||
}
|
||||
|
||||
private static parseTablesAndColumns(): {
|
||||
table: string;
|
||||
columns: string | { name: string; as: string }[];
|
||||
@ -102,63 +156,9 @@ export class SQLModel extends Model {
|
||||
|
||||
private static getTableNameWithSchema(table: string) {
|
||||
if (this.database.inDialect('postgres') && !table.includes('.')) {
|
||||
return `public.${table}`;
|
||||
const schema = process.env.DB_SCHEMA || 'public';
|
||||
return `${schema}.${table}`;
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
static inferFields(): {
|
||||
[field: string]: {
|
||||
type: string;
|
||||
source: string;
|
||||
collection: string;
|
||||
interface: string;
|
||||
};
|
||||
} {
|
||||
const tables = this.parseTablesAndColumns();
|
||||
const fields = tables.reduce((fields, { table, columns }) => {
|
||||
const tableName = this.getTableNameWithSchema(table);
|
||||
const collection = this.database.tableNameCollectionMap.get(tableName);
|
||||
if (!collection) {
|
||||
return fields;
|
||||
}
|
||||
const attributes = collection.model.getAttributes();
|
||||
const sourceFields = {};
|
||||
if (columns === '*') {
|
||||
Object.values(attributes).forEach((attribute) => {
|
||||
const field = collection.getField((attribute as any).fieldName);
|
||||
if (!field?.options.interface) {
|
||||
return;
|
||||
}
|
||||
sourceFields[field.name] = {
|
||||
collection: field.collection.name,
|
||||
type: field.type,
|
||||
source: `${field.collection.name}.${field.name}`,
|
||||
interface: field.options.interface,
|
||||
uiSchema: field.options.uiSchema,
|
||||
};
|
||||
});
|
||||
} else {
|
||||
(columns as { name: string; as: string }[]).forEach((column) => {
|
||||
const modelField = Object.values(attributes).find((attribute) => attribute.field === column.name);
|
||||
if (!modelField) {
|
||||
return;
|
||||
}
|
||||
const field = collection.getField((modelField as any).fieldName);
|
||||
if (!field?.options.interface) {
|
||||
return;
|
||||
}
|
||||
sourceFields[column.as || column.name] = {
|
||||
collection: field.collection.name,
|
||||
type: field.type,
|
||||
source: `${field.collection.name}.${field.name}`,
|
||||
interface: field.options.interface,
|
||||
uiSchema: field.options.uiSchema,
|
||||
};
|
||||
});
|
||||
}
|
||||
return { ...fields, ...sourceFields };
|
||||
}, {});
|
||||
return fields;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,19 @@
|
||||
import { mockServer, MockServer } from '@nocobase/test';
|
||||
|
||||
describe('application version', () => {
|
||||
let app: MockServer;
|
||||
afterEach(async () => {
|
||||
if (app) {
|
||||
await app.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
it('should get application version', async () => {
|
||||
app = mockServer();
|
||||
await app.db.sync();
|
||||
const appVersion = app.version;
|
||||
|
||||
await appVersion.update();
|
||||
expect(await appVersion.get()).toBeDefined();
|
||||
});
|
||||
});
|
@ -110,40 +110,6 @@ describe('application', () => {
|
||||
expect(response.body).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
it.skip('db.middleware', async () => {
|
||||
const index = app.middleware.findIndex((m) => m.name === 'db2resource');
|
||||
app.middleware.splice(index, 0, async (ctx, next) => {
|
||||
app.collection({
|
||||
name: 'tests',
|
||||
});
|
||||
await next();
|
||||
});
|
||||
const response = await agent.get('/api/tests');
|
||||
expect(response.body).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
it.skip('db.middleware', async () => {
|
||||
const index = app.middleware.findIndex((m) => m.name === 'db2resource');
|
||||
app.middleware.splice(index, 0, async (ctx, next) => {
|
||||
app.collection({
|
||||
name: 'bars',
|
||||
});
|
||||
app.collection({
|
||||
name: 'foos',
|
||||
fields: [
|
||||
{
|
||||
type: 'hasMany',
|
||||
name: 'bars',
|
||||
},
|
||||
],
|
||||
});
|
||||
await next();
|
||||
});
|
||||
|
||||
const response = await agent.get('/api/foos/1/bars');
|
||||
expect(response.body).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
it('should call application with command', async () => {
|
||||
await app.runCommand('start');
|
||||
const jestFn = vi.fn();
|
||||
|
@ -1,58 +0,0 @@
|
||||
import supertest from 'supertest';
|
||||
import { Application } from '../application';
|
||||
|
||||
describe('application', () => {
|
||||
let app: Application;
|
||||
let agent;
|
||||
|
||||
beforeEach(() => {
|
||||
app = new Application({
|
||||
database: {
|
||||
username: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_DATABASE,
|
||||
host: process.env.DB_HOST,
|
||||
port: process.env.DB_PORT as any,
|
||||
dialect: process.env.DB_DIALECT as any,
|
||||
dialectOptions: {
|
||||
charset: 'utf8mb4',
|
||||
collate: 'utf8mb4_unicode_ci',
|
||||
},
|
||||
},
|
||||
acl: false,
|
||||
resourcer: {
|
||||
prefix: '/api',
|
||||
},
|
||||
dataWrapping: true,
|
||||
});
|
||||
app.resourcer.registerActionHandlers({
|
||||
list: async (ctx, next) => {
|
||||
ctx.body = [1, 2];
|
||||
await next();
|
||||
},
|
||||
get: async (ctx, next) => {
|
||||
ctx.body = [3, 4];
|
||||
await next();
|
||||
},
|
||||
'foo2s.bar2s:list': async (ctx, next) => {
|
||||
ctx.body = [5, 6];
|
||||
await next();
|
||||
},
|
||||
});
|
||||
agent = supertest.agent(app.callback());
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
return app.destroy();
|
||||
});
|
||||
|
||||
it('resourcer.define', async () => {
|
||||
app.resourcer.define({
|
||||
name: 'test',
|
||||
});
|
||||
const response = await agent.get('/api/test');
|
||||
expect(response.body).toEqual({
|
||||
data: [1, 2],
|
||||
});
|
||||
});
|
||||
});
|
@ -5,6 +5,7 @@ import { AppSupervisor } from '../app-supervisor';
|
||||
import Application from '../application';
|
||||
import { Gateway } from '../gateway';
|
||||
import { errors } from '../gateway/errors';
|
||||
|
||||
describe('gateway', () => {
|
||||
let gateway: Gateway;
|
||||
beforeEach(() => {
|
||||
@ -14,6 +15,7 @@ describe('gateway', () => {
|
||||
await gateway.destroy();
|
||||
await AppSupervisor.getInstance().destroy();
|
||||
});
|
||||
|
||||
describe('app selector', () => {
|
||||
it('should get app as default main app', async () => {
|
||||
expect(
|
||||
@ -45,6 +47,7 @@ describe('gateway', () => {
|
||||
expect(gateway.getAppSelectorMiddlewares().nodes.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('http api', () => {
|
||||
it('should return error when app not found', async () => {
|
||||
const res = await supertest.agent(gateway.getCallback()).get('/api/app:getInfo');
|
||||
|
39
packages/core/server/src/__tests__/gateway/ipc.test.ts
Normal file
39
packages/core/server/src/__tests__/gateway/ipc.test.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { mkdtemp } from 'fs-extra';
|
||||
import { join } from 'path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { IPCSocketServer } from '../../gateway/ipc-socket-server';
|
||||
import { IPCSocketClient } from '../../gateway/ipc-socket-client';
|
||||
import { AppSupervisor } from '@nocobase/server';
|
||||
|
||||
describe('ipc test', () => {
|
||||
it('should create ipc socket server', async () => {
|
||||
const socketPath = join(await mkdtemp(join(tmpdir(), 'ipc-socket-server')), '/test.sock');
|
||||
const socketServer = IPCSocketServer.buildServer(socketPath);
|
||||
|
||||
const client = await IPCSocketClient.getConnection(socketPath);
|
||||
|
||||
const appHandler = vi.fn();
|
||||
|
||||
vi.spyOn(AppSupervisor, 'getInstance').mockImplementation(() => {
|
||||
return {
|
||||
getApp() {
|
||||
return {
|
||||
runAsCLI() {
|
||||
appHandler();
|
||||
},
|
||||
cli: {
|
||||
hasCommand() {
|
||||
return true;
|
||||
},
|
||||
parseHandleByIPCServer() {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
await client.write({ type: 'passCliArgv', payload: { argv: ['node', 'test', 'nocobase'] } });
|
||||
expect(appHandler).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
@ -0,0 +1,124 @@
|
||||
import supertest from 'supertest';
|
||||
import { Application } from '../../application';
|
||||
import { dataWrapping } from '../../middlewares';
|
||||
|
||||
describe('data wrapping middleware', () => {
|
||||
it('should not wrap when ctx.withoutDataWrapping is true', async () => {
|
||||
const wrappingMiddleware = dataWrapping();
|
||||
const ctx: any = {
|
||||
withoutDataWrapping: true,
|
||||
body: [1, 2],
|
||||
};
|
||||
await wrappingMiddleware(ctx, async () => {});
|
||||
expect(ctx.body).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
it('should wrap ctx.body', async () => {
|
||||
const wrappingMiddleware = dataWrapping();
|
||||
const ctx: any = {
|
||||
body: [1, 2],
|
||||
state: {},
|
||||
};
|
||||
await wrappingMiddleware(ctx, async () => {});
|
||||
expect(ctx.body).toEqual({
|
||||
data: [1, 2],
|
||||
});
|
||||
});
|
||||
|
||||
it('should wrap with pagination data', async () => {
|
||||
const wrappingMiddleware = dataWrapping();
|
||||
const ctx: any = {
|
||||
body: {
|
||||
rows: [1, 2],
|
||||
count: 2,
|
||||
},
|
||||
state: {},
|
||||
};
|
||||
await wrappingMiddleware(ctx, async () => {});
|
||||
expect(ctx.body).toEqual({
|
||||
data: [1, 2],
|
||||
meta: {
|
||||
count: 2,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should set body meta through ctx.bodyMeta', async () => {
|
||||
const wrappingMiddleware = dataWrapping();
|
||||
const ctx: any = {
|
||||
body: {
|
||||
key1: 'value1',
|
||||
},
|
||||
bodyMeta: {
|
||||
foo: 'bar',
|
||||
},
|
||||
state: {},
|
||||
};
|
||||
await wrappingMiddleware(ctx, async () => {});
|
||||
expect(ctx.body).toMatchObject({
|
||||
data: {
|
||||
key1: 'value1',
|
||||
},
|
||||
meta: {
|
||||
foo: 'bar',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('application', () => {
|
||||
let app: Application;
|
||||
let agent;
|
||||
|
||||
beforeEach(() => {
|
||||
app = new Application({
|
||||
database: {
|
||||
username: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_DATABASE,
|
||||
host: process.env.DB_HOST,
|
||||
port: process.env.DB_PORT as any,
|
||||
dialect: process.env.DB_DIALECT as any,
|
||||
dialectOptions: {
|
||||
charset: 'utf8mb4',
|
||||
collate: 'utf8mb4_unicode_ci',
|
||||
},
|
||||
},
|
||||
acl: false,
|
||||
resourcer: {
|
||||
prefix: '/api',
|
||||
},
|
||||
dataWrapping: true,
|
||||
});
|
||||
|
||||
app.resourcer.registerActionHandlers({
|
||||
list: async (ctx, next) => {
|
||||
ctx.body = [1, 2];
|
||||
await next();
|
||||
},
|
||||
get: async (ctx, next) => {
|
||||
ctx.body = [3, 4];
|
||||
await next();
|
||||
},
|
||||
'foo2s.bar2s:list': async (ctx, next) => {
|
||||
ctx.body = [5, 6];
|
||||
await next();
|
||||
},
|
||||
});
|
||||
agent = supertest.agent(app.callback());
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
return app.destroy();
|
||||
});
|
||||
|
||||
it('resourcer.define', async () => {
|
||||
app.resourcer.define({
|
||||
name: 'test',
|
||||
});
|
||||
const response = await agent.get('/api/test');
|
||||
expect(response.body).toEqual({
|
||||
data: [1, 2],
|
||||
});
|
||||
});
|
||||
});
|
@ -1,18 +1,6 @@
|
||||
import { ACL } from '@nocobase/acl';
|
||||
import { availableActions } from './available-action';
|
||||
|
||||
const configureResources = [
|
||||
'roles',
|
||||
'users',
|
||||
'collections',
|
||||
'fields',
|
||||
'collections.fields',
|
||||
'roles.collections',
|
||||
'roles.resources',
|
||||
'rolesResourcesScopes',
|
||||
'availableActions',
|
||||
];
|
||||
|
||||
export function createACL() {
|
||||
const acl = new ACL();
|
||||
|
||||
@ -20,7 +8,5 @@ export function createACL() {
|
||||
acl.setAvailableAction(actionName, actionParams);
|
||||
}
|
||||
|
||||
acl.registerConfigResources(configureResources);
|
||||
|
||||
return acl;
|
||||
}
|
||||
|
@ -144,6 +144,7 @@ interface LoadOptions {
|
||||
reload?: boolean;
|
||||
hooks?: boolean;
|
||||
sync?: boolean;
|
||||
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
@ -201,7 +202,6 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
protected plugins = new Map<string, Plugin>();
|
||||
protected _appSupervisor: AppSupervisor = AppSupervisor.getInstance();
|
||||
protected _started: boolean;
|
||||
protected _logger: SystemLogger;
|
||||
private _authenticated = false;
|
||||
private _maintaining = false;
|
||||
private _maintainingCommandStatus: MaintainingCommandStatus;
|
||||
@ -217,6 +217,12 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
this._appSupervisor.addApp(this);
|
||||
}
|
||||
|
||||
protected _logger: SystemLogger;
|
||||
|
||||
get logger() {
|
||||
return this._logger;
|
||||
}
|
||||
|
||||
protected _loaded: boolean;
|
||||
|
||||
/**
|
||||
@ -254,10 +260,6 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
return this.mainDataSource.collectionManager.db;
|
||||
}
|
||||
|
||||
get logger() {
|
||||
return this._logger;
|
||||
}
|
||||
|
||||
get resourceManager() {
|
||||
return this.mainDataSource.resourceManager;
|
||||
}
|
||||
@ -587,15 +589,6 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
return this.pm.get(name) as P;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is deprecated and should not be used.
|
||||
* Use {@link this.runAsCLI()} instead.
|
||||
* @deprecated
|
||||
*/
|
||||
async parse(argv = process.argv) {
|
||||
return this.runAsCLI(argv);
|
||||
}
|
||||
|
||||
async authenticate() {
|
||||
if (this._authenticated) {
|
||||
return;
|
||||
@ -614,46 +607,6 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
return await this.runAsCLI([command, ...args], { from: 'user', throwError: true });
|
||||
}
|
||||
|
||||
protected createCLI() {
|
||||
const command = new AppCommand('nocobase')
|
||||
.usage('[command] [options]')
|
||||
.hook('preAction', async (_, actionCommand) => {
|
||||
this._actionCommand = actionCommand;
|
||||
this.activatedCommand = {
|
||||
name: getCommandFullName(actionCommand),
|
||||
};
|
||||
|
||||
this.setMaintaining({
|
||||
status: 'command_begin',
|
||||
command: this.activatedCommand,
|
||||
});
|
||||
|
||||
this.setMaintaining({
|
||||
status: 'command_running',
|
||||
command: this.activatedCommand,
|
||||
});
|
||||
|
||||
if (actionCommand['_authenticate']) {
|
||||
await this.authenticate();
|
||||
}
|
||||
|
||||
if (actionCommand['_preload']) {
|
||||
await this.load();
|
||||
}
|
||||
})
|
||||
.hook('postAction', async (_, actionCommand) => {
|
||||
if (this._maintainingStatusBeforeCommand?.error && this._started) {
|
||||
await this.restart();
|
||||
}
|
||||
});
|
||||
|
||||
command.exitOverride((err) => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@ -716,14 +669,6 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async loadPluginCommands() {
|
||||
this.log.debug('load plugin commands');
|
||||
await this.pm.loadCommands();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@ -1049,6 +994,46 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
});
|
||||
}
|
||||
|
||||
protected createCLI() {
|
||||
const command = new AppCommand('nocobase')
|
||||
.usage('[command] [options]')
|
||||
.hook('preAction', async (_, actionCommand) => {
|
||||
this._actionCommand = actionCommand;
|
||||
this.activatedCommand = {
|
||||
name: getCommandFullName(actionCommand),
|
||||
};
|
||||
|
||||
this.setMaintaining({
|
||||
status: 'command_begin',
|
||||
command: this.activatedCommand,
|
||||
});
|
||||
|
||||
this.setMaintaining({
|
||||
status: 'command_running',
|
||||
command: this.activatedCommand,
|
||||
});
|
||||
|
||||
if (actionCommand['_authenticate']) {
|
||||
await this.authenticate();
|
||||
}
|
||||
|
||||
if (actionCommand['_preload']) {
|
||||
await this.load();
|
||||
}
|
||||
})
|
||||
.hook('postAction', async (_, actionCommand) => {
|
||||
if (this._maintainingStatusBeforeCommand?.error && this._started) {
|
||||
await this.restart();
|
||||
}
|
||||
});
|
||||
|
||||
command.exitOverride((err) => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
protected init() {
|
||||
const options = this.options;
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
/* istanbul ignore file -- @preserve */
|
||||
|
||||
import Application from '../application';
|
||||
|
||||
const REPL = require('repl');
|
||||
|
@ -1,3 +1,5 @@
|
||||
/* istanbul ignore file -- @preserve */
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import fs from 'fs';
|
||||
import { dirname, resolve } from 'path';
|
||||
|
@ -1,3 +1,5 @@
|
||||
/* istanbul ignore file -- @preserve */
|
||||
|
||||
import Application from '../application';
|
||||
|
||||
export default (app: Application) => {
|
||||
|
@ -1,3 +1,5 @@
|
||||
/* istanbul ignore file -- @preserve */
|
||||
|
||||
import Application from '../application';
|
||||
|
||||
export default (app: Application) => {
|
||||
|
@ -1,3 +1,5 @@
|
||||
/* istanbul ignore file -- @preserve */
|
||||
|
||||
import Application from '../application';
|
||||
|
||||
export default (app: Application) => {
|
||||
|
@ -1,3 +1,5 @@
|
||||
/* istanbul ignore file -- @preserve */
|
||||
|
||||
import Application from '../application';
|
||||
|
||||
export default (app: Application) => {
|
||||
|
@ -1,3 +1,5 @@
|
||||
/* istanbul ignore file -- @preserve */
|
||||
|
||||
import Application from '../application';
|
||||
import createMigration from './create-migration';
|
||||
import dbAuth from './db-auth';
|
||||
@ -12,6 +14,7 @@ import start from './start';
|
||||
import stop from './stop';
|
||||
import upgrade from './upgrade';
|
||||
import consoleCommand from './console';
|
||||
|
||||
export function registerCli(app: Application) {
|
||||
consoleCommand(app);
|
||||
dbAuth(app);
|
||||
|
@ -1,3 +1,5 @@
|
||||
/* istanbul ignore file -- @preserve */
|
||||
|
||||
import Application from '../application';
|
||||
|
||||
export default (app: Application) => {
|
||||
|
@ -1,3 +1,5 @@
|
||||
/* istanbul ignore file -- @preserve */
|
||||
|
||||
import Application from '../application';
|
||||
|
||||
export default (app: Application) => {
|
||||
|
@ -1,3 +1,5 @@
|
||||
/* istanbul ignore file -- @preserve */
|
||||
|
||||
import _ from 'lodash';
|
||||
import Application from '../application';
|
||||
import { PluginCommandError } from '../errors/plugin-command-error';
|
||||
|
@ -1,3 +1,5 @@
|
||||
/* istanbul ignore file -- @preserve */
|
||||
|
||||
import Application from '../application';
|
||||
|
||||
export default (app: Application) => {
|
||||
|
@ -1,3 +1,5 @@
|
||||
/* istanbul ignore file -- @preserve */
|
||||
|
||||
import Application from '../application';
|
||||
|
||||
export default (app: Application) => {
|
||||
|
@ -1,3 +1,5 @@
|
||||
/* istanbul ignore file -- @preserve */
|
||||
|
||||
import { fsExists } from '@nocobase/utils';
|
||||
import fs from 'fs';
|
||||
import { resolve } from 'path';
|
||||
|
@ -1,3 +1,5 @@
|
||||
/* istanbul ignore file -- @preserve */
|
||||
|
||||
import Application from '../application';
|
||||
|
||||
export default (app: Application) => {
|
||||
|
@ -1,3 +1,5 @@
|
||||
/* istanbul ignore file -- @preserve */
|
||||
|
||||
import Application from '../application';
|
||||
|
||||
/**
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { SystemLogger, createSystemLogger, getLoggerFilePath } from '@nocobase/logger';
|
||||
import { createSystemLogger, getLoggerFilePath, SystemLogger } from '@nocobase/logger';
|
||||
import { Registry, Toposort, ToposortOptions, uid } from '@nocobase/utils';
|
||||
import { createStoragePluginsSymlink } from '@nocobase/utils/plugin-symlink';
|
||||
import { Command } from 'commander';
|
||||
@ -55,13 +55,12 @@ export class Gateway extends EventEmitter {
|
||||
|
||||
public server: http.Server | null = null;
|
||||
public ipcSocketServer: IPCSocketServer | null = null;
|
||||
loggers = new Registry<SystemLogger>();
|
||||
private port: number = process.env.APP_PORT ? parseInt(process.env.APP_PORT) : null;
|
||||
private host = '0.0.0.0';
|
||||
private wsServer: WSServer;
|
||||
private socketPath = resolve(process.cwd(), 'storage', 'gateway.sock');
|
||||
|
||||
loggers = new Registry<SystemLogger>();
|
||||
|
||||
private constructor() {
|
||||
super();
|
||||
this.reset();
|
||||
@ -78,6 +77,15 @@ export class Gateway extends EventEmitter {
|
||||
return Gateway.instance;
|
||||
}
|
||||
|
||||
static async getIPCSocketClient() {
|
||||
const socketPath = resolve(process.cwd(), process.env.SOCKET_PATH || 'storage/gateway.sock');
|
||||
try {
|
||||
return await IPCSocketClient.getConnection(socketPath);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.reset();
|
||||
Gateway.instance = null;
|
||||
@ -284,6 +292,7 @@ export class Gateway extends EventEmitter {
|
||||
return this.requestHandler.bind(this);
|
||||
}
|
||||
|
||||
/* istanbul ignore next -- @preserve */
|
||||
async watch() {
|
||||
if (!process.env.IS_DEV_CMD) {
|
||||
return;
|
||||
@ -295,6 +304,7 @@ export class Gateway extends EventEmitter {
|
||||
require(file);
|
||||
}
|
||||
|
||||
/* istanbul ignore next -- @preserve */
|
||||
async run(options: RunOptions) {
|
||||
const isStart = this.isStart();
|
||||
let ipcClient: IPCSocketClient | false;
|
||||
@ -359,7 +369,6 @@ export class Gateway extends EventEmitter {
|
||||
}
|
||||
|
||||
getStartOptions() {
|
||||
const argv = process.argv;
|
||||
const program = new Command();
|
||||
|
||||
program
|
||||
@ -369,9 +378,7 @@ export class Gateway extends EventEmitter {
|
||||
.option('-h, --host [host]')
|
||||
.option('--db-sync')
|
||||
.parse(process.argv);
|
||||
const options = program.opts();
|
||||
|
||||
return options;
|
||||
return program.opts();
|
||||
}
|
||||
|
||||
start(options: StartHttpServerOptions) {
|
||||
@ -439,13 +446,4 @@ export class Gateway extends EventEmitter {
|
||||
this.server?.close();
|
||||
this.wsServer?.close();
|
||||
}
|
||||
|
||||
static async getIPCSocketClient() {
|
||||
const socketPath = resolve(process.cwd(), process.env.SOCKET_PATH || 'storage/gateway.sock');
|
||||
try {
|
||||
return await IPCSocketClient.getConnection(socketPath);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -99,7 +99,6 @@ export class IPCSocketServer {
|
||||
|
||||
const mainApp = await AppSupervisor.getInstance().getApp('main');
|
||||
if (!mainApp.cli.hasCommand(argv[2])) {
|
||||
// console.log('passCliArgv', argv[2]);
|
||||
await mainApp.pm.loadCommands();
|
||||
}
|
||||
const cli = mainApp.cli;
|
||||
|
@ -10,10 +10,8 @@ import bodyParser from 'koa-bodyparser';
|
||||
import { resolve } from 'path';
|
||||
import { createHistogram, RecordableHistogram } from 'perf_hooks';
|
||||
import Application, { ApplicationOptions } from './application';
|
||||
import { parseVariables } from './middlewares';
|
||||
import { dateTemplate } from './middlewares/data-template';
|
||||
import { dataWrapping } from './middlewares/data-wrapping';
|
||||
import { db2resource } from './middlewares/db2resource';
|
||||
|
||||
import { i18n } from './middlewares/i18n';
|
||||
|
||||
export function createI18n(options: ApplicationOptions) {
|
||||
@ -112,11 +110,13 @@ export const getCommandFullName = (command: Command) => {
|
||||
return names.join('.');
|
||||
};
|
||||
|
||||
/* istanbul ignore next -- @preserve */
|
||||
export const tsxRerunning = async () => {
|
||||
const file = resolve(process.cwd(), 'storage/app.watch.ts');
|
||||
await fs.promises.writeFile(file, `export const watchId = '${uid()}';`, 'utf-8');
|
||||
};
|
||||
|
||||
/* istanbul ignore next -- @preserve */
|
||||
export const enablePerfHooks = (app: Application) => {
|
||||
app.context.getPerfHistogram = (name: string) => {
|
||||
if (!app.perfHistograms.has(name)) {
|
||||
|
@ -1,13 +0,0 @@
|
||||
export class MultipleInstanceManager<Item> {
|
||||
map: Map<string, Item> = new Map();
|
||||
|
||||
constructor() {}
|
||||
|
||||
get(key: string) {
|
||||
return this.map.get(key);
|
||||
}
|
||||
|
||||
set(key: string, value: Item) {
|
||||
this.map.set(key, value);
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { DataSourceOptions, SequelizeDataSource } from '@nocobase/data-source-manager';
|
||||
import { db2resource, parseVariables } from './middlewares';
|
||||
import { parseVariables } from './middlewares';
|
||||
import { dateTemplate } from './middlewares/data-template';
|
||||
|
||||
export class MainDataSource extends SequelizeDataSource {
|
||||
|
@ -1,3 +1,2 @@
|
||||
export * from './data-wrapping';
|
||||
export * from './db2resource';
|
||||
export { parseVariables } from './parse-variables';
|
||||
|
@ -1,3 +1,5 @@
|
||||
/* istanbul ignore file -- @preserve */
|
||||
|
||||
import { Migration as DbMigration } from '@nocobase/database';
|
||||
import Application from './application';
|
||||
import Plugin from './plugin';
|
||||
|
@ -1,3 +1,5 @@
|
||||
/* istanbul ignore file -- @preserve */
|
||||
|
||||
import { DataTypes } from '@nocobase/database';
|
||||
import { Migration } from '../migration';
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
/* istanbul ignore file -- @preserve */
|
||||
|
||||
import { Migration } from '../migration';
|
||||
import { PluginManager } from '../plugin-manager';
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
/* istanbul ignore file -- @preserve */
|
||||
|
||||
import { Migration } from '../migration';
|
||||
|
||||
export default class extends Migration {
|
||||
|
@ -1,3 +1,5 @@
|
||||
/* istanbul ignore file -- @preserve */
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
|
@ -71,11 +71,6 @@ export class PluginManager {
|
||||
*/
|
||||
server: net.Server;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_repository: PluginManagerRepository;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@ -104,6 +99,11 @@ export class PluginManager {
|
||||
this.app.resourcer.use(uploadMiddleware);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_repository: PluginManagerRepository;
|
||||
|
||||
get repository() {
|
||||
return this.app.db.getRepository('applicationPlugins') as PluginManagerRepository;
|
||||
}
|
||||
@ -234,6 +234,7 @@ export class PluginManager {
|
||||
}
|
||||
}
|
||||
|
||||
/* istanbul ignore next -- @preserve */
|
||||
async create(pluginName: string, options?: { forceRecreate?: boolean }) {
|
||||
const createPlugin = async (name) => {
|
||||
const pluginDir = resolve(process.cwd(), 'packages/plugins', name);
|
||||
@ -482,20 +483,6 @@ export class PluginManager {
|
||||
});
|
||||
}
|
||||
|
||||
private sort(names: string | string[]) {
|
||||
const pluginNames = _.castArray(names);
|
||||
if (pluginNames.length === 1) {
|
||||
return pluginNames;
|
||||
}
|
||||
const sorter = new Topo.Sorter<string>();
|
||||
for (const pluginName of pluginNames) {
|
||||
const plugin = this.get(pluginName);
|
||||
const peerDependencies = Object.keys(plugin.options?.packageJson?.peerDependencies || {});
|
||||
sorter.add(pluginName, { after: peerDependencies, group: plugin.options?.packageName || pluginName });
|
||||
}
|
||||
return sorter.nodes;
|
||||
}
|
||||
|
||||
async enable(name: string | string[]) {
|
||||
let pluginNames = name;
|
||||
if (name === '*') {
|
||||
@ -705,27 +692,6 @@ export class PluginManager {
|
||||
await execa('yarn', ['nocobase', 'refresh']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
async loadOne(plugin: Plugin) {
|
||||
this.app.setMaintainingMessage(`loading plugin ${plugin.name}...`);
|
||||
if (plugin.state.loaded || !plugin.enabled) {
|
||||
return;
|
||||
}
|
||||
const name = plugin.getName();
|
||||
await plugin.beforeLoad();
|
||||
|
||||
await this.app.emitAsync('beforeLoadPlugin', plugin, {});
|
||||
this.app.logger.debug(`loading plugin...`, { submodule: 'plugin-manager', method: 'loadOne', name });
|
||||
await plugin.load();
|
||||
plugin.state.loaded = true;
|
||||
await this.app.emitAsync('afterLoadPlugin', plugin, {});
|
||||
this.app.logger.debug(`after load plugin...`, { submodule: 'plugin-manager', method: 'loadOne', name });
|
||||
|
||||
this.app.setMaintainingMessage(`loaded plugin ${plugin.name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@ -1086,6 +1052,20 @@ export class PluginManager {
|
||||
}
|
||||
this['_initPresetPlugins'] = true;
|
||||
}
|
||||
|
||||
private sort(names: string | string[]) {
|
||||
const pluginNames = _.castArray(names);
|
||||
if (pluginNames.length === 1) {
|
||||
return pluginNames;
|
||||
}
|
||||
const sorter = new Topo.Sorter<string>();
|
||||
for (const pluginName of pluginNames) {
|
||||
const plugin = this.get(pluginName);
|
||||
const peerDependencies = Object.keys(plugin.options?.packageJson?.peerDependencies || {});
|
||||
sorter.add(pluginName, { after: peerDependencies, group: plugin.options?.packageName || pluginName });
|
||||
}
|
||||
return sorter.nodes;
|
||||
}
|
||||
}
|
||||
|
||||
export default PluginManager;
|
||||
|
@ -1,3 +1,5 @@
|
||||
/* istanbul ignore next -- @preserve */
|
||||
|
||||
import { importModule, isURL } from '@nocobase/utils';
|
||||
import { createStoragePluginSymLink } from '@nocobase/utils/plugin-symlink';
|
||||
import axios, { AxiosRequestConfig } from 'axios';
|
||||
@ -277,6 +279,7 @@ export function getServerPackages(packageDir: string) {
|
||||
const exts = ['.js', '.ts', '.jsx', '.tsx'];
|
||||
const importRegex = /import\s+.*?\s+from\s+['"]([^'"\s.].+?)['"];?/g;
|
||||
const requireRegex = /require\s*\(\s*[`'"]([^`'"\s.].+?)[`'"]\s*\)/g;
|
||||
|
||||
function setPluginsFromContent(reg: RegExp, content: string) {
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = reg.exec(content))) {
|
||||
@ -428,6 +431,7 @@ async function getExternalVersionFromDistFile(packageName: string): Promise<fals
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function isNotBuiltinModule(packageName: string) {
|
||||
return !builtinModules.includes(packageName);
|
||||
}
|
||||
@ -503,6 +507,7 @@ export interface DepCompatible {
|
||||
versionRange: string;
|
||||
packageVersion: string;
|
||||
}
|
||||
|
||||
export async function getCompatible(packageName: string) {
|
||||
let externalVersion: Record<string, string>;
|
||||
const hasSrc = fs.existsSync(path.join(getPackageDir(packageName), 'src'));
|
||||
|
@ -1,3 +1,5 @@
|
||||
/* istanbul ignore file -- @preserve */
|
||||
|
||||
import { Model } from '@nocobase/database';
|
||||
import { LoggerOptions } from '@nocobase/logger';
|
||||
import { fsExists, importModule } from '@nocobase/utils';
|
||||
@ -6,7 +8,7 @@ import glob from 'glob';
|
||||
import type { TFuncKey, TOptions } from 'i18next';
|
||||
import { basename, resolve } from 'path';
|
||||
import { Application } from './application';
|
||||
import { InstallOptions, getExposeChangelogUrl, getExposeReadmeUrl } from './plugin-manager';
|
||||
import { getExposeChangelogUrl, getExposeReadmeUrl, InstallOptions } from './plugin-manager';
|
||||
import { checkAndGetCompatible } from './plugin-manager/utils';
|
||||
|
||||
export interface PluginInterface {
|
||||
@ -135,22 +137,6 @@ export abstract class Plugin<O = any> implements PluginInterface {
|
||||
this.options = options || {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected async getSourceDir() {
|
||||
if (this._sourceDir) {
|
||||
return this._sourceDir;
|
||||
}
|
||||
if (await this.isDev()) {
|
||||
return (this._sourceDir = 'src');
|
||||
}
|
||||
if (basename(__dirname) === 'src') {
|
||||
return (this._sourceDir = 'src');
|
||||
}
|
||||
return (this._sourceDir = this.isPreset ? 'lib' : 'dist');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@ -232,22 +218,6 @@ export abstract class Plugin<O = any> implements PluginInterface {
|
||||
return this.app.i18n.t(text, { ns: this.options['packageName'], ...(options as any) });
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected async isDev() {
|
||||
if (!this.options.packageName) {
|
||||
return false;
|
||||
}
|
||||
const file = await fs.promises.realpath(
|
||||
resolve(process.env.NODE_MODULES_PATH || resolve(process.cwd(), 'node_modules'), this.options.packageName),
|
||||
);
|
||||
if (file.startsWith(resolve(process.cwd(), 'packages'))) {
|
||||
return !!process.env.IS_DEV_CMD;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
@ -286,6 +256,38 @@ export abstract class Plugin<O = any> implements PluginInterface {
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected async getSourceDir() {
|
||||
if (this._sourceDir) {
|
||||
return this._sourceDir;
|
||||
}
|
||||
if (await this.isDev()) {
|
||||
return (this._sourceDir = 'src');
|
||||
}
|
||||
if (basename(__dirname) === 'src') {
|
||||
return (this._sourceDir = 'src');
|
||||
}
|
||||
return (this._sourceDir = this.isPreset ? 'lib' : 'dist');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected async isDev() {
|
||||
if (!this.options.packageName) {
|
||||
return false;
|
||||
}
|
||||
const file = await fs.promises.realpath(
|
||||
resolve(process.env.NODE_MODULES_PATH || resolve(process.cwd(), 'node_modules'), this.options.packageName),
|
||||
);
|
||||
if (file.startsWith(resolve(process.cwd(), 'packages'))) {
|
||||
return !!process.env.IS_DEV_CMD;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default Plugin;
|
||||
|
@ -4,7 +4,7 @@ import react from '@vitejs/plugin-react';
|
||||
import fs from 'fs';
|
||||
import path, { resolve } from 'path';
|
||||
import { URL } from 'url';
|
||||
import { mergeConfig, defineConfig as vitestConfig } from 'vitest/config';
|
||||
import { defineConfig as vitestConfig, mergeConfig } from 'vitest/config';
|
||||
|
||||
const CORE_CLIENT_PACKAGES = ['sdk', 'client'];
|
||||
|
||||
@ -157,7 +157,7 @@ export const getFilterInclude = (isServer, isCoverage) => {
|
||||
if (!item.startsWith('-')) {
|
||||
const pre = argv[index - 1];
|
||||
|
||||
if (pre && pre.startsWith('--') && !pre.includes('=')) {
|
||||
if (pre && pre.startsWith('--') && ['--reporter'].includes(pre)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -173,6 +173,7 @@ export const getFilterInclude = (isServer, isCoverage) => {
|
||||
if (!filterFileOrDir) return {};
|
||||
const absPath = path.join(process.cwd(), filterFileOrDir);
|
||||
const isDir = fs.existsSync(absPath) && fs.statSync(absPath).isDirectory();
|
||||
|
||||
// 如果是文件,则只测试当前文件
|
||||
if (!isDir) {
|
||||
return {
|
||||
@ -185,6 +186,7 @@ export const getFilterInclude = (isServer, isCoverage) => {
|
||||
|
||||
// 判断是否为包目录,如果不是包目录,则只测试当前目录
|
||||
const isPackage = fs.existsSync(path.join(absPath, 'package.json'));
|
||||
|
||||
if (!isPackage) {
|
||||
return {
|
||||
include: [`${filterFileOrDir}/${suffix}`],
|
||||
@ -235,6 +237,7 @@ export const defineConfig = () => {
|
||||
|
||||
if (filterInclude) {
|
||||
config.test.include = filterInclude;
|
||||
|
||||
if (isFile) {
|
||||
// 减少收集的文件
|
||||
config.test.exclude = [];
|
||||
@ -248,14 +251,17 @@ export const defineConfig = () => {
|
||||
}
|
||||
|
||||
const isCoverage = process.argv.includes('--coverage');
|
||||
|
||||
if (!isCoverage) {
|
||||
return config;
|
||||
}
|
||||
|
||||
const { include: coverageInclude } = getFilterInclude(isServer, true);
|
||||
|
||||
if (coverageInclude) {
|
||||
config.test.coverage.include = coverageInclude;
|
||||
}
|
||||
|
||||
const reportsDirectory = getReportsDirectory(isServer);
|
||||
if (reportsDirectory) {
|
||||
config.test.coverage.reportsDirectory = reportsDirectory;
|
||||
|
@ -1,7 +1,8 @@
|
||||
import lodash from 'lodash';
|
||||
import { NoPermissionError } from '@nocobase/acl';
|
||||
import { snakeCase } from '@nocobase/database';
|
||||
|
||||
class NoPermissionError extends Error {}
|
||||
|
||||
function createWithACLMetaMiddleware() {
|
||||
return async (ctx: any, next) => {
|
||||
await next();
|
||||
|
@ -1,3 +1,5 @@
|
||||
/* istanbul ignore file -- @preserve */
|
||||
|
||||
import { Migration } from '@nocobase/server';
|
||||
|
||||
export default class extends Migration {
|
||||
|
@ -1,19 +1,9 @@
|
||||
import { ACL, ACLRole } from '@nocobase/acl';
|
||||
import { Database, Model } from '@nocobase/database';
|
||||
import { AssociationFieldAction, AssociationFieldsActions, GrantHelper } from '../server';
|
||||
import { Model } from '@nocobase/database';
|
||||
|
||||
export class RoleResourceActionModel extends Model {
|
||||
async writeToACL(options: {
|
||||
acl: ACL;
|
||||
role: ACLRole;
|
||||
resourceName: string;
|
||||
associationFieldsActions: AssociationFieldsActions;
|
||||
grantHelper: GrantHelper;
|
||||
}) {
|
||||
// @ts-ignore
|
||||
const db: Database = this.constructor.database;
|
||||
|
||||
const { resourceName, role, acl, associationFieldsActions, grantHelper } = options;
|
||||
async writeToACL(options: { acl: ACL; role: ACLRole; resourceName: string }) {
|
||||
const { resourceName, role } = options;
|
||||
|
||||
const actionName = this.get('name') as string;
|
||||
|
||||
@ -33,63 +23,5 @@ export class RoleResourceActionModel extends Model {
|
||||
}
|
||||
|
||||
role.grantAction(actionPath, actionParams);
|
||||
|
||||
const collection = db.getCollection(resourceName);
|
||||
|
||||
if (!collection) {
|
||||
return;
|
||||
}
|
||||
|
||||
const availableAction = acl.resolveActionAlias(actionName);
|
||||
|
||||
for (const field of fields) {
|
||||
const collectionField = collection.getField(field);
|
||||
|
||||
if (!collectionField) {
|
||||
console.log(`field ${field} does not exist at ${collection.name}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const fieldType = collectionField.get('type') as string;
|
||||
|
||||
const fieldActions: AssociationFieldAction = associationFieldsActions?.[fieldType]?.[availableAction];
|
||||
|
||||
const fieldTarget = collectionField.get('target');
|
||||
|
||||
if (fieldActions) {
|
||||
// grant association actions to role
|
||||
const associationActions = fieldActions.associationActions || [];
|
||||
|
||||
associationActions.forEach((associationAction) => {
|
||||
const actionName = `${resourceName}.${collectionField.get('name')}:${associationAction}`;
|
||||
role.grantAction(actionName);
|
||||
});
|
||||
|
||||
const targetActions = fieldActions.targetActions || [];
|
||||
|
||||
targetActions.forEach((targetAction) => {
|
||||
const targetActionPath = `${fieldTarget}:${targetAction}`;
|
||||
|
||||
const existsAction = role.getActionParams(targetActionPath);
|
||||
|
||||
if (existsAction) {
|
||||
return;
|
||||
}
|
||||
|
||||
// set resource target action with current resourceName
|
||||
grantHelper.resourceTargetActionMap.set(`${role.name}.${resourceName}`, [
|
||||
...(grantHelper.resourceTargetActionMap.get(resourceName) || []),
|
||||
targetActionPath,
|
||||
]);
|
||||
|
||||
grantHelper.targetActionResourceMap.set(targetActionPath, [
|
||||
...(grantHelper.targetActionResourceMap.get(targetActionPath) || []),
|
||||
`${role.name}.${resourceName}`,
|
||||
]);
|
||||
|
||||
role.grantAction(targetActionPath);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,37 +1,15 @@
|
||||
import { ACL, ACLResource, ACLRole } from '@nocobase/acl';
|
||||
import { Model } from '@nocobase/database';
|
||||
import { AssociationFieldsActions, GrantHelper } from '../server';
|
||||
import { RoleResourceActionModel } from './RoleResourceActionModel';
|
||||
|
||||
export class RoleResourceModel extends Model {
|
||||
async revoke(options: { role: ACLRole; resourceName: string; grantHelper: GrantHelper }) {
|
||||
const { role, resourceName, grantHelper } = options;
|
||||
async revoke(options: { role: ACLRole; resourceName: string }) {
|
||||
const { role, resourceName } = options;
|
||||
role.revokeResource(resourceName);
|
||||
|
||||
const targetActions = grantHelper.resourceTargetActionMap.get(`${role.name}.${resourceName}`) || [];
|
||||
|
||||
for (const targetAction of targetActions) {
|
||||
const targetActionResource = (grantHelper.targetActionResourceMap.get(targetAction) || []).filter(
|
||||
(item) => `${role.name}.${resourceName}` !== item,
|
||||
);
|
||||
|
||||
grantHelper.targetActionResourceMap.set(targetAction, targetActionResource);
|
||||
|
||||
if (targetActionResource.length == 0) {
|
||||
role.revokeAction(targetAction);
|
||||
}
|
||||
}
|
||||
|
||||
grantHelper.resourceTargetActionMap.set(`${role.name}.${resourceName}`, []);
|
||||
}
|
||||
|
||||
async writeToACL(options: {
|
||||
acl: ACL;
|
||||
associationFieldsActions: AssociationFieldsActions;
|
||||
grantHelper: GrantHelper;
|
||||
transaction: any;
|
||||
}) {
|
||||
const { acl, associationFieldsActions, grantHelper } = options;
|
||||
async writeToACL(options: { acl: ACL; transaction: any }) {
|
||||
const { acl } = options;
|
||||
const resourceName = this.get('name') as string;
|
||||
const roleName = this.get('roleName') as string;
|
||||
const role = acl.getRole(roleName);
|
||||
@ -42,7 +20,7 @@ export class RoleResourceModel extends Model {
|
||||
}
|
||||
|
||||
// revoke resource of role
|
||||
await this.revoke({ role, resourceName, grantHelper });
|
||||
await this.revoke({ role, resourceName });
|
||||
|
||||
// @ts-ignore
|
||||
if (this.usingActionsConfig === false) {
|
||||
@ -66,8 +44,6 @@ export class RoleResourceModel extends Model {
|
||||
acl,
|
||||
role,
|
||||
resourceName,
|
||||
associationFieldsActions,
|
||||
grantHelper: options.grantHelper,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Context, utils as actionUtils } from '@nocobase/actions';
|
||||
import { Cache } from '@nocobase/cache';
|
||||
import { Collection, RelationField } from '@nocobase/database';
|
||||
import { Collection, RelationField, Transaction } from '@nocobase/database';
|
||||
import { Plugin } from '@nocobase/server';
|
||||
import { Mutex } from 'async-mutex';
|
||||
import lodash from 'lodash';
|
||||
@ -15,112 +15,25 @@ import { RoleModel } from './model/RoleModel';
|
||||
import { RoleResourceActionModel } from './model/RoleResourceActionModel';
|
||||
import { RoleResourceModel } from './model/RoleResourceModel';
|
||||
|
||||
export interface AssociationFieldAction {
|
||||
associationActions: string[];
|
||||
targetActions?: string[];
|
||||
}
|
||||
|
||||
interface AssociationFieldActions {
|
||||
[availableActionName: string]: AssociationFieldAction;
|
||||
}
|
||||
|
||||
export interface AssociationFieldsActions {
|
||||
[associationType: string]: AssociationFieldActions;
|
||||
}
|
||||
|
||||
export class GrantHelper {
|
||||
resourceTargetActionMap = new Map<string, string[]>();
|
||||
targetActionResourceMap = new Map<string, string[]>();
|
||||
|
||||
constructor() {}
|
||||
}
|
||||
|
||||
export class PluginACLServer extends Plugin {
|
||||
// association field actions config
|
||||
|
||||
associationFieldsActions: AssociationFieldsActions = {};
|
||||
|
||||
grantHelper = new GrantHelper();
|
||||
|
||||
get acl() {
|
||||
return this.app.acl;
|
||||
}
|
||||
|
||||
registerAssociationFieldAction(associationType: string, value: AssociationFieldActions) {
|
||||
this.associationFieldsActions[associationType] = value;
|
||||
}
|
||||
|
||||
registerAssociationFieldsActions() {
|
||||
// if grant create action to role, it should
|
||||
// also grant add action and association target's view action
|
||||
|
||||
this.registerAssociationFieldAction('hasOne', {
|
||||
view: {
|
||||
associationActions: ['list', 'get', 'view'],
|
||||
},
|
||||
create: {
|
||||
associationActions: ['create', 'set'],
|
||||
},
|
||||
update: {
|
||||
associationActions: ['update', 'remove', 'set'],
|
||||
},
|
||||
});
|
||||
|
||||
this.registerAssociationFieldAction('hasMany', {
|
||||
view: {
|
||||
associationActions: ['list', 'get', 'view'],
|
||||
},
|
||||
create: {
|
||||
associationActions: ['create', 'set', 'add'],
|
||||
},
|
||||
update: {
|
||||
associationActions: ['update', 'remove', 'set'],
|
||||
},
|
||||
});
|
||||
|
||||
this.registerAssociationFieldAction('belongsTo', {
|
||||
view: {
|
||||
associationActions: ['list', 'get', 'view'],
|
||||
},
|
||||
create: {
|
||||
associationActions: ['create', 'set'],
|
||||
},
|
||||
update: {
|
||||
associationActions: ['update', 'remove', 'set'],
|
||||
},
|
||||
});
|
||||
|
||||
this.registerAssociationFieldAction('belongsToMany', {
|
||||
view: {
|
||||
associationActions: ['list', 'get', 'view'],
|
||||
},
|
||||
create: {
|
||||
associationActions: ['create', 'set', 'add'],
|
||||
},
|
||||
update: {
|
||||
associationActions: ['update', 'remove', 'set', 'toggle'],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async writeResourceToACL(resourceModel: RoleResourceModel, transaction) {
|
||||
async writeResourceToACL(resourceModel: RoleResourceModel, transaction: Transaction) {
|
||||
await resourceModel.writeToACL({
|
||||
acl: this.acl,
|
||||
associationFieldsActions: this.associationFieldsActions,
|
||||
transaction: transaction,
|
||||
grantHelper: this.grantHelper,
|
||||
});
|
||||
}
|
||||
|
||||
async writeActionToACL(actionModel: RoleResourceActionModel, transaction) {
|
||||
async writeActionToACL(actionModel: RoleResourceActionModel, transaction: Transaction) {
|
||||
const resource = actionModel.get('resource') as RoleResourceModel;
|
||||
const role = this.acl.getRole(resource.get('roleName') as string);
|
||||
await actionModel.writeToACL({
|
||||
acl: this.acl,
|
||||
role,
|
||||
resourceName: resource.get('name') as string,
|
||||
associationFieldsActions: this.associationFieldsActions,
|
||||
grantHelper: this.grantHelper,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,5 +0,0 @@
|
||||
import { CollectionGroup } from '@nocobase/database';
|
||||
|
||||
export class CollectionGroupManager {
|
||||
static collectionGroups: CollectionGroup[] = [];
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
/* istanbul ignore file -- @preserve */
|
||||
|
||||
import { Application, AppSupervisor } from '@nocobase/server';
|
||||
import { Restorer } from '../restorer';
|
||||
import { DumpRulesGroupType } from '@nocobase/database';
|
||||
|
@ -0,0 +1,225 @@
|
||||
import { Database, SQLModel } from '@nocobase/database';
|
||||
import { MockServer, createMockServer } from '@nocobase/test';
|
||||
|
||||
describe('sql collection', () => {
|
||||
let app: MockServer;
|
||||
let db: Database;
|
||||
let agent: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await createMockServer({
|
||||
plugins: ['collection-manager', 'error-handler'],
|
||||
});
|
||||
db = app.db;
|
||||
db.options.underscored = false;
|
||||
agent = app.agent();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.db.clean({ drop: true });
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
it('sqlCollection:execute: should check sql', async () => {
|
||||
let res = await agent.resource('sqlCollection').execute();
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.errors[0].message).toMatch('Please enter a SQL statement');
|
||||
|
||||
res = await agent.resource('sqlCollection').execute({
|
||||
values: {
|
||||
sql: 'insert into users (username) values ("test")',
|
||||
},
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.errors[0].message).toMatch('Only supports SELECT statements or WITH clauses');
|
||||
});
|
||||
|
||||
it('sqlCollection:execute', async () => {
|
||||
await agent.resource('collections').create({
|
||||
values: {
|
||||
schema: process.env.DB_SCHEMA,
|
||||
name: 'testSqlCollection',
|
||||
fields: [
|
||||
{
|
||||
name: 'testField',
|
||||
type: 'string',
|
||||
interface: 'input',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
await agent.resource('testSqlCollection').create({
|
||||
values: {
|
||||
testField: 'test',
|
||||
},
|
||||
});
|
||||
const schema = process.env.DB_SCHEMA ? `${process.env.DB_SCHEMA}.` : ``;
|
||||
const res = await agent.resource('sqlCollection').execute({
|
||||
values: {
|
||||
sql: `select * from ${schema}${db.queryInterface.quoteIdentifier('testSqlCollection')}`,
|
||||
},
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.data.data.length).toBe(1);
|
||||
expect(res.body.data.fields).toMatchObject({
|
||||
testField: {
|
||||
type: 'string',
|
||||
source: 'testSqlCollection.testField',
|
||||
collection: 'testSqlCollection',
|
||||
interface: 'input',
|
||||
},
|
||||
});
|
||||
expect(res.body.data.sources).toEqual(['testSqlCollection']);
|
||||
});
|
||||
|
||||
it('sqlCollection:update', async () => {
|
||||
await agent.resource('collections').create({
|
||||
values: {
|
||||
name: 'fakeCollection',
|
||||
fields: [
|
||||
{
|
||||
name: 'testField1',
|
||||
type: 'string',
|
||||
interface: 'input',
|
||||
},
|
||||
{
|
||||
name: 'testField2',
|
||||
type: 'string',
|
||||
interface: 'input',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
await agent.resource('collections').create({
|
||||
values: {
|
||||
name: 'sqlCollection',
|
||||
sql: 'select * from "fakeCollection"',
|
||||
template: 'sql',
|
||||
fields: [
|
||||
{
|
||||
name: 'testField1',
|
||||
type: 'string',
|
||||
interface: 'input',
|
||||
},
|
||||
{
|
||||
name: 'testField2',
|
||||
type: 'string',
|
||||
interface: 'input',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const collection = await db.getRepository('collections').findOne({
|
||||
filter: {
|
||||
name: 'sqlCollection',
|
||||
},
|
||||
});
|
||||
expect(collection.options.sql).toBe('select * from "fakeCollection"');
|
||||
const loadedModel = db.getModel('sqlCollection') as typeof SQLModel;
|
||||
expect(loadedModel.sql).toBe('select * from "fakeCollection"');
|
||||
const fields = await db.getRepository('fields').find({
|
||||
filter: {
|
||||
collectionName: 'sqlCollection',
|
||||
},
|
||||
});
|
||||
expect(fields.length).toBe(2);
|
||||
const loadedFields = db.getCollection('sqlCollection').fields;
|
||||
expect(loadedFields.size).toBe(2);
|
||||
await agent.resource('sqlCollection').update({
|
||||
filterByTk: 'sqlCollection',
|
||||
values: {
|
||||
sql: 'select "testField1" from "fakeCollection"',
|
||||
fields: [
|
||||
{
|
||||
name: 'testField1',
|
||||
type: 'string',
|
||||
interface: 'input',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const collection2 = await db.getRepository('collections').findOne({
|
||||
filter: {
|
||||
name: 'sqlCollection',
|
||||
},
|
||||
});
|
||||
expect(collection2.options.sql).toBe('select "testField1" from "fakeCollection"');
|
||||
const loadedModel2 = db.getModel('sqlCollection') as typeof SQLModel;
|
||||
expect(loadedModel2.sql).toBe('select "testField1" from "fakeCollection"');
|
||||
const fields2 = await db.getRepository('fields').find({
|
||||
filter: {
|
||||
collectionName: 'sqlCollection',
|
||||
},
|
||||
});
|
||||
expect(fields2.length).toBe(1);
|
||||
const loadedFields2 = db.getCollection('sqlCollection').fields;
|
||||
expect(loadedFields2.size).toBe(1);
|
||||
});
|
||||
|
||||
it('sqlCollection:setFields', async () => {
|
||||
await agent.resource('collections').create({
|
||||
values: {
|
||||
name: 'fakeCollection',
|
||||
fields: [
|
||||
{
|
||||
name: 'testField1',
|
||||
type: 'string',
|
||||
interface: 'input',
|
||||
},
|
||||
{
|
||||
name: 'testField2',
|
||||
type: 'string',
|
||||
interface: 'input',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
await agent.resource('collections').create({
|
||||
values: {
|
||||
name: 'sqlCollection',
|
||||
sql: 'select * from "fakeCollection"',
|
||||
template: 'sql',
|
||||
fields: [
|
||||
{
|
||||
name: 'testField1',
|
||||
type: 'string',
|
||||
interface: 'input',
|
||||
},
|
||||
{
|
||||
name: 'testField2',
|
||||
type: 'string',
|
||||
interface: 'input',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const fields = await db.getRepository('fields').find({
|
||||
filter: {
|
||||
collectionName: 'sqlCollection',
|
||||
},
|
||||
});
|
||||
expect(fields.length).toBe(2);
|
||||
const loadedFields = db.getCollection('sqlCollection').fields;
|
||||
expect(loadedFields.size).toBe(2);
|
||||
await agent.resource('sqlCollection').setFields({
|
||||
filterByTk: 'sqlCollection',
|
||||
values: {
|
||||
fields: [
|
||||
{
|
||||
name: 'testField1',
|
||||
type: 'string',
|
||||
interface: 'input',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const fields2 = await db.getRepository('fields').find({
|
||||
filter: {
|
||||
collectionName: 'sqlCollection',
|
||||
},
|
||||
});
|
||||
expect(fields2.length).toBe(1);
|
||||
const loadedFields2 = db.getCollection('sqlCollection').fields;
|
||||
expect(loadedFields2.size).toBe(1);
|
||||
});
|
||||
});
|
@ -1,69 +0,0 @@
|
||||
import Database, { FindOneOptions, FindOptions, Model, Transaction } from '@nocobase/database';
|
||||
|
||||
async function destroyFields(db: Database, transaction: Transaction, fieldRecords: Model[]) {
|
||||
const fieldsRepo = db.getRepository('fields');
|
||||
for (const fieldRecord of fieldRecords) {
|
||||
await fieldsRepo.destroy({
|
||||
filter: {
|
||||
name: fieldRecord.get('name'),
|
||||
collectionName: fieldRecord.get('collectionName'),
|
||||
},
|
||||
transaction,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function afterDestroyForForeignKeyField(db: Database) {
|
||||
return async (model, opts) => {
|
||||
const { transaction } = opts;
|
||||
const options = model.get('options');
|
||||
if (!options?.isForeignKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const collectionRepo = db.getRepository('collections');
|
||||
const foreignKey = model.get('name');
|
||||
const foreignKeyCollectionName = model.get('collectionName');
|
||||
const collectionRecord = await collectionRepo.findOne({
|
||||
filter: {
|
||||
name: foreignKeyCollectionName,
|
||||
},
|
||||
transaction,
|
||||
} as FindOneOptions);
|
||||
const collectionOptions = collectionRecord.get('options');
|
||||
const fieldsRepo = db.getRepository('fields');
|
||||
|
||||
if (collectionOptions?.isThrough) {
|
||||
// through collection
|
||||
const fieldRecords = await fieldsRepo.find({
|
||||
filter: {
|
||||
options: { through: foreignKeyCollectionName, foreignKey: foreignKey },
|
||||
},
|
||||
transaction,
|
||||
} as FindOptions);
|
||||
await destroyFields(db, transaction, fieldRecords);
|
||||
} else {
|
||||
await destroyFields(
|
||||
db,
|
||||
transaction,
|
||||
await fieldsRepo.find({
|
||||
filter: {
|
||||
collectionName: foreignKeyCollectionName,
|
||||
options: { foreignKey: foreignKey },
|
||||
},
|
||||
transaction,
|
||||
} as FindOptions),
|
||||
);
|
||||
await destroyFields(
|
||||
db,
|
||||
transaction,
|
||||
await fieldsRepo.find({
|
||||
filter: {
|
||||
options: { foreignKey: foreignKey, target: foreignKeyCollectionName },
|
||||
},
|
||||
transaction,
|
||||
} as FindOptions),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user