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', () => {
|
it('should define role with predicate', () => {
|
||||||
acl.setAvailableAction('edit', {
|
acl.setAvailableAction('edit', {
|
||||||
type: 'old-data',
|
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>();
|
actionAlias = new Map<string, string>();
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
configResources: string[] = [];
|
|
||||||
|
|
||||||
protected availableActions = new Map<string, ACLAvailableAction>();
|
protected availableActions = new Map<string, ACLAvailableAction>();
|
||||||
|
|
||||||
protected fixedParamsManager = new FixedParamsManager();
|
protected fixedParamsManager = new FixedParamsManager();
|
||||||
@ -147,27 +142,6 @@ export class ACL extends EventEmitter {
|
|||||||
return this.roles.delete(name);
|
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 = {}) {
|
setAvailableAction(name: string, options: AvailableActionOptions = {}) {
|
||||||
this.availableActions.set(name, new ACLAvailableAction(name, options));
|
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-available-strategy';
|
||||||
export * from './acl-resource';
|
export * from './acl-resource';
|
||||||
export * from './acl-role';
|
export * from './acl-role';
|
||||||
export * from './no-permission-error';
|
|
||||||
export * from './skip-middleware';
|
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 bodyParser from 'koa-bodyparser';
|
||||||
import qs from 'qs';
|
import qs from 'qs';
|
||||||
import supertest, { SuperAgentTest } from 'supertest';
|
import supertest, { SuperAgentTest } from 'supertest';
|
||||||
import db2resource from '../../../server/src/middlewares/db2resource';
|
import db2resource from './db2resource';
|
||||||
|
|
||||||
interface ActionParams {
|
interface ActionParams {
|
||||||
fields?: string[];
|
fields?: string[];
|
||||||
@ -30,6 +30,7 @@ interface ActionParams {
|
|||||||
* @deprecated
|
* @deprecated
|
||||||
*/
|
*/
|
||||||
associatedIndex?: string;
|
associatedIndex?: string;
|
||||||
|
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,6 +45,7 @@ interface SortActionParams {
|
|||||||
method?: string;
|
method?: string;
|
||||||
target?: any;
|
target?: any;
|
||||||
sticky?: boolean;
|
sticky?: boolean;
|
||||||
|
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,6 +56,7 @@ interface Resource {
|
|||||||
update: (params?: ActionParams) => Promise<supertest.Response>;
|
update: (params?: ActionParams) => Promise<supertest.Response>;
|
||||||
destroy: (params?: ActionParams) => Promise<supertest.Response>;
|
destroy: (params?: ActionParams) => Promise<supertest.Response>;
|
||||||
sort: (params?: SortActionParams) => Promise<supertest.Response>;
|
sort: (params?: SortActionParams) => Promise<supertest.Response>;
|
||||||
|
|
||||||
[name: string]: (params?: ActionParams) => 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 { createMockServer, mockDatabase, supertest } from '@nocobase/test';
|
||||||
import { SequelizeDataSource } from '../sequelize-data-source';
|
import { SequelizeDataSource } from '../sequelize-data-source';
|
||||||
import { vi } from 'vitest';
|
import { vi } from 'vitest';
|
||||||
|
import { DataSourceManager } from '@nocobase/data-source-manager';
|
||||||
|
|
||||||
describe('example', () => {
|
describe('example', () => {
|
||||||
test.skip('case1', async () => {
|
test.skip('case1', async () => {
|
||||||
@ -198,4 +199,25 @@ describe('example', () => {
|
|||||||
|
|
||||||
await app.destroy();
|
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';
|
import { FieldOptions, IField } from './types';
|
||||||
|
|
||||||
export class CollectionField implements IField {
|
export class CollectionField implements IField {
|
||||||
options;
|
options: FieldOptions;
|
||||||
|
|
||||||
constructor(options: FieldOptions) {
|
constructor(options: FieldOptions) {
|
||||||
this.updateOptions(options);
|
this.updateOptions(options);
|
||||||
}
|
}
|
||||||
|
@ -8,9 +8,13 @@ export class CollectionManager implements ICollectionManager {
|
|||||||
|
|
||||||
constructor(options = {}) {}
|
constructor(options = {}) {}
|
||||||
|
|
||||||
|
/* istanbul ignore next -- @preserve */
|
||||||
getRegisteredFieldType(type) {}
|
getRegisteredFieldType(type) {}
|
||||||
|
|
||||||
|
/* istanbul ignore next -- @preserve */
|
||||||
getRegisteredFieldInterface(key: string) {}
|
getRegisteredFieldInterface(key: string) {}
|
||||||
|
|
||||||
|
/* istanbul ignore next -- @preserve */
|
||||||
getRegisteredModel(key: string) {
|
getRegisteredModel(key: string) {
|
||||||
return this.models.get(key);
|
return this.models.get(key);
|
||||||
}
|
}
|
||||||
@ -22,8 +26,13 @@ export class CollectionManager implements ICollectionManager {
|
|||||||
return this.repositories.get(key);
|
return this.repositories.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* istanbul ignore next -- @preserve */
|
||||||
registerFieldTypes() {}
|
registerFieldTypes() {}
|
||||||
|
|
||||||
|
/* istanbul ignore next -- @preserve */
|
||||||
registerFieldInterfaces() {}
|
registerFieldInterfaces() {}
|
||||||
|
|
||||||
|
/* istanbul ignore next -- @preserve */
|
||||||
registerCollectionTemplates() {}
|
registerCollectionTemplates() {}
|
||||||
|
|
||||||
registerModels(models: Record<string, any>) {
|
registerModels(models: Record<string, any>) {
|
||||||
@ -46,6 +55,7 @@ export class CollectionManager implements ICollectionManager {
|
|||||||
|
|
||||||
extendCollection(collectionOptions: CollectionOptions, mergeOptions?: MergeOptions): ICollection {
|
extendCollection(collectionOptions: CollectionOptions, mergeOptions?: MergeOptions): ICollection {
|
||||||
const collection = this.getCollection(collectionOptions.name);
|
const collection = this.getCollection(collectionOptions.name);
|
||||||
|
collection.updateOptions(collectionOptions, mergeOptions);
|
||||||
return collection;
|
return collection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,9 +13,7 @@ export class Collection implements ICollection {
|
|||||||
) {
|
) {
|
||||||
this.setRepository(options.repository);
|
this.setRepository(options.repository);
|
||||||
if (options.fields) {
|
if (options.fields) {
|
||||||
for (const field of options.fields) {
|
this.setFields(options.fields);
|
||||||
this.setField(field.name, field);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,6 +22,7 @@ export class Collection implements ICollection {
|
|||||||
newOptions = merge(this.options, newOptions, mergeOptions);
|
newOptions = merge(this.options, newOptions, mergeOptions);
|
||||||
this.options = newOptions;
|
this.options = newOptions;
|
||||||
|
|
||||||
|
this.setFields(newOptions.fields || []);
|
||||||
if (options.repository) {
|
if (options.repository) {
|
||||||
this.setRepository(options.repository);
|
this.setRepository(options.repository);
|
||||||
}
|
}
|
||||||
@ -31,6 +30,12 @@ export class Collection implements ICollection {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setFields(fields: any[]) {
|
||||||
|
for (const field of fields) {
|
||||||
|
this.setField(field.name, field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setField(name: string, options: any) {
|
setField(name: string, options: any) {
|
||||||
const field = new CollectionField(options);
|
const field = new CollectionField(options);
|
||||||
this.fields.set(name, field);
|
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 { ACL } from '@nocobase/acl';
|
||||||
import { ResourceManager, getNameByParams, parseRequest } from '@nocobase/resourcer';
|
import { getNameByParams, parseRequest, ResourceManager } from '@nocobase/resourcer';
|
||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
import compose from 'koa-compose';
|
import compose from 'koa-compose';
|
||||||
import { loadDefaultActions } from './load-default-actions';
|
import { loadDefaultActions } from './load-default-actions';
|
||||||
@ -34,14 +34,49 @@ export abstract class DataSource extends EventEmitter {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.collectionManager = this.createCollectionManager(options);
|
this.collectionManager = this.createCollectionManager(options);
|
||||||
this.resourceManager.registerActionHandlers(loadDefaultActions(this));
|
this.resourceManager.registerActionHandlers(loadDefaultActions());
|
||||||
|
|
||||||
if (options.acl !== false) {
|
if (options.acl !== false) {
|
||||||
this.resourceManager.use(this.acl.middleware(), { tag: 'acl', after: ['auth'] });
|
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) => {
|
return async (ctx, next) => {
|
||||||
const params = parseRequest(
|
const params = parseRequest(
|
||||||
{
|
{
|
||||||
@ -77,38 +112,4 @@ export abstract class DataSource extends EventEmitter {
|
|||||||
return next();
|
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 { assign } from '@nocobase/utils';
|
||||||
import { getRepositoryFromParams, pageArgsToLimitArgs } from './utils';
|
import { pageArgsToLimitArgs } from './utils';
|
||||||
import { Context } from '@nocobase/actions';
|
import { Context } from '@nocobase/actions';
|
||||||
|
|
||||||
function totalPage(total, pageSize): number {
|
function totalPage(total, pageSize): number {
|
||||||
@ -27,7 +27,7 @@ function findArgs(ctx: Context) {
|
|||||||
async function listWithPagination(ctx: Context) {
|
async function listWithPagination(ctx: Context) {
|
||||||
const { page = 1, pageSize = 50 } = ctx.action.params;
|
const { page = 1, pageSize = 50 } = ctx.action.params;
|
||||||
|
|
||||||
const repository = getRepositoryFromParams(ctx);
|
const repository = ctx.getCurrentRepository();
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
context: ctx,
|
context: ctx,
|
||||||
@ -53,7 +53,7 @@ async function listWithPagination(ctx: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function listWithNonPaged(ctx: Context) {
|
async function listWithNonPaged(ctx: Context) {
|
||||||
const repository = getRepositoryFromParams(ctx);
|
const repository = ctx.getCurrentRepository();
|
||||||
|
|
||||||
const rows = await repository.find({ context: ctx, ...findArgs(ctx) });
|
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) {
|
if (repository.database) {
|
||||||
const repository = getRepositoryFromParams(ctx);
|
ctx.databaseRepository = repository;
|
||||||
|
return databaseMoveAction(ctx, next);
|
||||||
|
}
|
||||||
|
|
||||||
if (repository.move) {
|
throw new Error(`Repository can not handle action move for ${ctx.action.resourceName}`);
|
||||||
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}`);
|
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import lodash from 'lodash';
|
import lodash from 'lodash';
|
||||||
import { DataSource } from '../data-source';
|
import { DataSource } from '../data-source';
|
||||||
import { getRepositoryFromParams } from './utils';
|
import { Context } from '@nocobase/actions';
|
||||||
|
|
||||||
export function proxyToRepository(paramKeys: string[] | ((ctx: any) => object), repositoryMethod: string) {
|
export function proxyToRepository(paramKeys: string[] | ((ctx: any) => object), repositoryMethod: string) {
|
||||||
return async function (ctx, next) {
|
return async function (ctx: Context, next: () => Promise<void>) {
|
||||||
const repository = getRepositoryFromParams(ctx);
|
const repository = ctx.getCurrentRepository();
|
||||||
const callObj =
|
const callObj =
|
||||||
typeof paramKeys === 'function' ? paramKeys(ctx) : { ...lodash.pick(ctx.action.params, paramKeys), context: ctx };
|
typeof paramKeys === 'function' ? paramKeys(ctx) : { ...lodash.pick(ctx.action.params, paramKeys), context: ctx };
|
||||||
const dataSource: DataSource = ctx.dataSource;
|
const dataSource: DataSource = ctx.dataSource;
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
import { Context } from '@nocobase/actions';
|
|
||||||
import { DataSource, IRepository } from '../';
|
|
||||||
|
|
||||||
export function pageArgsToLimitArgs(
|
export function pageArgsToLimitArgs(
|
||||||
page: number,
|
page: number,
|
||||||
pageSize: number,
|
pageSize: number,
|
||||||
@ -13,20 +10,3 @@ export function pageArgsToLimitArgs(
|
|||||||
limit: pageSize,
|
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 './load-default-actions';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
|
|
||||||
export * from './data-source-with-database';
|
|
||||||
export * from './utils';
|
export * from './utils';
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { list } from './default-actions/list';
|
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 { 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 } };
|
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 {
|
return {
|
||||||
...Object.keys(actions).reduce((carry, key) => {
|
...Object.keys(actions).reduce((carry, key) => {
|
||||||
carry[key] = proxyToRepository(actions[key].params, actions[key].method);
|
carry[key] = proxyToRepository(actions[key].params, actions[key].method);
|
||||||
return carry;
|
return carry;
|
||||||
}, {}),
|
}, {}),
|
||||||
list,
|
list,
|
||||||
move,
|
move: createMoveAction(globalActions.move),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
/* istanbul ignore file -- @preserve */
|
||||||
|
|
||||||
import { IModel, IRepository } from './types';
|
import { IModel, IRepository } from './types';
|
||||||
import * as console from 'console';
|
import * as console from 'console';
|
||||||
|
|
||||||
@ -5,13 +7,21 @@ export class Repository implements IRepository {
|
|||||||
async create(options) {
|
async create(options) {
|
||||||
console.log('Repository.create....');
|
console.log('Repository.create....');
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(options) {}
|
async update(options) {}
|
||||||
|
|
||||||
async find(options?: any): Promise<IModel[]> {
|
async find(options?: any): Promise<IModel[]> {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOne(options?: any): Promise<IModel> {
|
async findOne(options?: any): Promise<IModel> {
|
||||||
return {};
|
return {
|
||||||
|
toJSON() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async destroy(options) {}
|
async destroy(options) {}
|
||||||
|
|
||||||
count(options?: any): Promise<Number> {
|
count(options?: any): Promise<Number> {
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
|
/* istanbul ignore file -- @preserve */
|
||||||
|
|
||||||
import Database from '@nocobase/database';
|
import Database from '@nocobase/database';
|
||||||
import { CollectionOptions, ICollection, ICollectionManager, IRepository, MergeOptions } from './types';
|
import { CollectionOptions, ICollection, ICollectionManager, IRepository, MergeOptions } from './types';
|
||||||
|
|
||||||
export class SequelizeCollectionManager implements ICollectionManager {
|
export class SequelizeCollectionManager implements ICollectionManager {
|
||||||
db: Database;
|
db: Database;
|
||||||
options: any;
|
options: any;
|
||||||
|
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
this.db = this.createDB(options);
|
this.db = this.createDB(options);
|
||||||
this.options = options;
|
this.options = options;
|
||||||
|
@ -2,8 +2,6 @@ import { DataSource } from './data-source';
|
|||||||
import { SequelizeCollectionManager } from './sequelize-collection-manager';
|
import { SequelizeCollectionManager } from './sequelize-collection-manager';
|
||||||
|
|
||||||
export class SequelizeDataSource extends DataSource {
|
export class SequelizeDataSource extends DataSource {
|
||||||
async load() {}
|
|
||||||
|
|
||||||
createCollectionManager(options?: any) {
|
createCollectionManager(options?: any) {
|
||||||
return new SequelizeCollectionManager(options.collectionManager);
|
return new SequelizeCollectionManager(options.collectionManager);
|
||||||
}
|
}
|
||||||
|
@ -25,27 +25,44 @@ export type FieldOptions = {
|
|||||||
export interface IField {
|
export interface IField {
|
||||||
options: FieldOptions;
|
options: FieldOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICollection {
|
export interface ICollection {
|
||||||
repository: any;
|
repository: IRepository;
|
||||||
updateOptions(options: any): void;
|
|
||||||
|
updateOptions(options: CollectionOptions, mergeOptions?: MergeOptions): void;
|
||||||
|
|
||||||
setField(name: string, options: any): IField;
|
setField(name: string, options: any): IField;
|
||||||
|
|
||||||
removeField(name: string): void;
|
removeField(name: string): void;
|
||||||
|
|
||||||
getFields(): Array<IField>;
|
getFields(): Array<IField>;
|
||||||
|
|
||||||
getField(name: string): IField;
|
getField(name: string): IField;
|
||||||
|
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IModel {
|
export interface IModel {
|
||||||
|
toJSON: () => any;
|
||||||
|
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IRepository {
|
export interface IRepository {
|
||||||
find(options?: any): Promise<IModel[]>;
|
find(options?: any): Promise<IModel[]>;
|
||||||
|
|
||||||
findOne(options?: any): Promise<IModel>;
|
findOne(options?: any): Promise<IModel>;
|
||||||
|
|
||||||
count(options?: any): Promise<Number>;
|
count(options?: any): Promise<Number>;
|
||||||
|
|
||||||
findAndCount(options?: any): Promise<[IModel[], Number]>;
|
findAndCount(options?: any): Promise<[IModel[], Number]>;
|
||||||
create(options: any): void;
|
|
||||||
update(options: any): void;
|
create(options: any): any;
|
||||||
destroy(options: any): void;
|
|
||||||
|
update(options: any): any;
|
||||||
|
|
||||||
|
destroy(options: any): any;
|
||||||
|
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,9 +72,13 @@ export type MergeOptions = {
|
|||||||
|
|
||||||
export interface ICollectionManager {
|
export interface ICollectionManager {
|
||||||
registerFieldTypes(types: Record<string, any>): void;
|
registerFieldTypes(types: Record<string, any>): void;
|
||||||
|
|
||||||
registerFieldInterfaces(interfaces: Record<string, any>): void;
|
registerFieldInterfaces(interfaces: Record<string, any>): void;
|
||||||
|
|
||||||
registerCollectionTemplates(templates: Record<string, any>): void;
|
registerCollectionTemplates(templates: Record<string, any>): void;
|
||||||
|
|
||||||
registerModels(models: Record<string, any>): void;
|
registerModels(models: Record<string, any>): void;
|
||||||
|
|
||||||
registerRepositories(repositories: Record<string, any>): void;
|
registerRepositories(repositories: Record<string, any>): void;
|
||||||
|
|
||||||
getRegisteredRepository(key: string): IRepository;
|
getRegisteredRepository(key: string): IRepository;
|
||||||
@ -67,9 +88,12 @@ export interface ICollectionManager {
|
|||||||
extendCollection(collectionOptions: CollectionOptions, mergeOptions?: MergeOptions): ICollection;
|
extendCollection(collectionOptions: CollectionOptions, mergeOptions?: MergeOptions): ICollection;
|
||||||
|
|
||||||
hasCollection(name: string): boolean;
|
hasCollection(name: string): boolean;
|
||||||
|
|
||||||
getCollection(name: string): ICollection;
|
getCollection(name: string): ICollection;
|
||||||
|
|
||||||
getCollections(): Array<ICollection>;
|
getCollections(): Array<ICollection>;
|
||||||
|
|
||||||
getRepository(name: string, sourceId?: string | number): IRepository;
|
getRepository(name: string, sourceId?: string | number): IRepository;
|
||||||
|
|
||||||
sync(): Promise<void>;
|
sync(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
@ -12,5 +12,6 @@ export function joinCollectionName(dataSourceName: string, collectionName: strin
|
|||||||
if (!dataSourceName || dataSourceName === 'main') {
|
if (!dataSourceName || dataSourceName === 'main') {
|
||||||
return collectionName;
|
return collectionName;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${dataSourceName}:${collectionName}`;
|
return `${dataSourceName}:${collectionName}`;
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,8 @@ describe('filterMatch', () => {
|
|||||||
}),
|
}),
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
|
|
||||||
|
expect(filterMatch(post, { 'title.$not': 't1' })).toBeFalsy();
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
filterMatch(post, {
|
filterMatch(post, {
|
||||||
$or: [{ title: 't1' }, { title: 't2' }],
|
$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({
|
const u1 = await db.getRepository('users').create({
|
||||||
values: {
|
values: {
|
||||||
name: 'names of u1',
|
name: 'names of u1',
|
||||||
@ -40,6 +40,169 @@ describe('string operator', () => {
|
|||||||
expect(u1Res).toBeNull();
|
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 () => {
|
it('should query with include operator', async () => {
|
||||||
const u1 = await db.getRepository('users').create({
|
const u1 = await db.getRepository('users').create({
|
||||||
values: {
|
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({
|
db.collection({
|
||||||
name: 'users',
|
name: 'users',
|
||||||
schema: db.inDialect('postgres') ? 'public' : undefined,
|
schema: db.inDialect('postgres') ? process.env.DB_SCHEMA : undefined,
|
||||||
fields: [
|
fields: [
|
||||||
{ name: 'id', type: 'bigInt', interface: 'id' },
|
{ name: 'id', type: 'bigInt', interface: 'id' },
|
||||||
{ name: 'nickname', type: 'string', interface: 'input' },
|
{ name: 'nickname', type: 'string', interface: 'input' },
|
||||||
@ -19,7 +19,7 @@ describe('infer fields', () => {
|
|||||||
});
|
});
|
||||||
db.collection({
|
db.collection({
|
||||||
name: 'roles',
|
name: 'roles',
|
||||||
schema: db.inDialect('postgres') ? 'public' : undefined,
|
schema: db.inDialect('postgres') ? process.env.DB_SCHEMA : undefined,
|
||||||
fields: [
|
fields: [
|
||||||
{ name: 'id', type: 'bigInt', interface: 'id' },
|
{ name: 'id', type: 'bigInt', interface: 'id' },
|
||||||
{ name: 'title', type: 'string', interface: 'input' },
|
{ name: 'title', type: 'string', interface: 'input' },
|
||||||
@ -28,7 +28,7 @@ describe('infer fields', () => {
|
|||||||
});
|
});
|
||||||
db.collection({
|
db.collection({
|
||||||
name: 'roles_users',
|
name: 'roles_users',
|
||||||
schema: db.inDialect('postgres') ? 'public' : undefined,
|
schema: db.inDialect('postgres') ? process.env.DB_SCHEMA : undefined,
|
||||||
fields: [
|
fields: [
|
||||||
{ name: 'id', type: 'bigInt', interface: 'id' },
|
{ name: 'id', type: 'bigInt', interface: 'id' },
|
||||||
{ name: 'userId', type: 'bigInt', interface: 'id' },
|
{ name: 'userId', type: 'bigInt', interface: 'id' },
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { mockDatabase } from '../../mock-database';
|
import { mockDatabase } from '../../mock-database';
|
||||||
import { SqlCollection } from '../sql-collection';
|
import { SqlCollection } from '../../sql-collection';
|
||||||
|
|
||||||
test('sql-collection', async () => {
|
test('sql-collection', async () => {
|
||||||
const db = mockDatabase({ tablePrefix: '' });
|
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() {
|
modelInit() {
|
||||||
if (this.model) {
|
if (this.model) {
|
||||||
@ -307,6 +307,9 @@ export class Collection<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
correctOptions(options) {
|
correctOptions(options) {
|
||||||
if (options.primaryKey && options.autoIncrement) {
|
if (options.primaryKey && options.autoIncrement) {
|
||||||
delete options.defaultValue;
|
delete options.defaultValue;
|
||||||
@ -555,9 +558,6 @@ export class Collection<
|
|||||||
return field as Field;
|
return field as Field;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO
|
|
||||||
*/
|
|
||||||
updateOptions(options: CollectionOptions, mergeOptions?: any) {
|
updateOptions(options: CollectionOptions, mergeOptions?: any) {
|
||||||
let newOptions = lodash.cloneDeep(options);
|
let newOptions = lodash.cloneDeep(options);
|
||||||
newOptions = merge(this.options, newOptions, mergeOptions);
|
newOptions = merge(this.options, newOptions, mergeOptions);
|
||||||
@ -594,12 +594,6 @@ export class Collection<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO
|
|
||||||
*
|
|
||||||
* @param name
|
|
||||||
* @param options
|
|
||||||
*/
|
|
||||||
updateField(name: string, options: FieldOptions) {
|
updateField(name: string, options: FieldOptions) {
|
||||||
if (!this.hasField(name)) {
|
if (!this.hasField(name)) {
|
||||||
throw new Error(`field ${name} not exists`);
|
throw new Error(`field ${name} not exists`);
|
||||||
@ -701,6 +695,9 @@ export class Collection<
|
|||||||
this.refreshIndexes();
|
this.refreshIndexes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
refreshIndexes() {
|
refreshIndexes() {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const indexes: any[] = this.model._indexes;
|
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 {
|
export class Database extends EventEmitter implements AsyncEmitter {
|
||||||
sequelize: Sequelize;
|
sequelize: Sequelize;
|
||||||
migrator: Umzug;
|
migrator: Umzug;
|
||||||
@ -197,7 +169,6 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
|||||||
inheritanceMap = new InheritanceMap();
|
inheritanceMap = new InheritanceMap();
|
||||||
importedFrom = new Map<string, Set<string>>();
|
importedFrom = new Map<string, Set<string>>();
|
||||||
modelHook: ModelHook;
|
modelHook: ModelHook;
|
||||||
version: DatabaseVersion;
|
|
||||||
delayCollectionExtend = new Map<string, { collectionOptions: CollectionOptions; mergeOptions?: any }[]>();
|
delayCollectionExtend = new Map<string, { collectionOptions: CollectionOptions; mergeOptions?: any }[]>();
|
||||||
logger: Logger;
|
logger: Logger;
|
||||||
collectionGroupManager = new CollectionGroupManager(this);
|
collectionGroupManager = new CollectionGroupManager(this);
|
||||||
@ -208,8 +179,6 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
|||||||
constructor(options: DatabaseOptions) {
|
constructor(options: DatabaseOptions) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.version = new DatabaseVersion(this);
|
|
||||||
|
|
||||||
const opts = {
|
const opts = {
|
||||||
sync: {
|
sync: {
|
||||||
alter: {
|
alter: {
|
||||||
@ -350,6 +319,9 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
|||||||
return this._instanceId;
|
return this._instanceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
createMigrator({ migrations }) {
|
createMigrator({ migrations }) {
|
||||||
const migratorOptions: any = this.options.migrator || {};
|
const migratorOptions: any = this.options.migrator || {};
|
||||||
const context = {
|
const context = {
|
||||||
@ -371,10 +343,16 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
setContext(context: any) {
|
setContext(context: any) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
sequelizeOptions(options) {
|
sequelizeOptions(options) {
|
||||||
if (options.dialect === 'postgres') {
|
if (options.dialect === 'postgres') {
|
||||||
if (!options.hooks) {
|
if (!options.hooks) {
|
||||||
@ -393,6 +371,9 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
|||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
initListener() {
|
initListener() {
|
||||||
this.on('afterConnect', async (client) => {
|
this.on('afterConnect', async (client) => {
|
||||||
if (this.inDialect('postgres')) {
|
if (this.inDialect('postgres')) {
|
||||||
@ -644,6 +625,9 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
|||||||
return this.getCollection(name)?.repository;
|
return this.getCollection(name)?.repository;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
addPendingField(field: RelationField) {
|
addPendingField(field: RelationField) {
|
||||||
const associating = this.pendingFields;
|
const associating = this.pendingFields;
|
||||||
const items = this.pendingFields.get(field.target) || [];
|
const items = this.pendingFields.get(field.target) || [];
|
||||||
@ -651,6 +635,9 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
|||||||
associating.set(field.target, items);
|
associating.set(field.target, items);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
removePendingField(field: RelationField) {
|
removePendingField(field: RelationField) {
|
||||||
const items = this.pendingFields.get(field.target) || [];
|
const items = this.pendingFields.get(field.target) || [];
|
||||||
const index = items.indexOf(field);
|
const index = items.indexOf(field);
|
||||||
@ -693,6 +680,9 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
initOperators() {
|
initOperators() {
|
||||||
const operators = new Map();
|
const operators = new Map();
|
||||||
|
|
||||||
@ -717,6 +707,9 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
buildField(options, context: FieldContext) {
|
buildField(options, context: FieldContext) {
|
||||||
const { type } = options;
|
const { type } = options;
|
||||||
|
|
||||||
@ -794,10 +787,11 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
|||||||
return await this.queryInterface.collectionTableExists(collection, options);
|
return await this.queryInterface.collectionTableExists(collection, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public isSqliteMemory() {
|
isSqliteMemory() {
|
||||||
return this.sequelize.getDialect() === 'sqlite' && lodash.get(this.options, 'storage') == ':memory:';
|
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'> } = {}) {
|
async auth(options: Omit<QueryOptions, 'retry'> & { retry?: number | Pick<QueryOptions, 'retry'> } = {}) {
|
||||||
const { retry = 10, ...others } = options;
|
const { retry = 10, ...others } = options;
|
||||||
const startingDelay = 50;
|
const startingDelay = 50;
|
||||||
@ -833,10 +827,16 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
async checkVersion() {
|
async checkVersion() {
|
||||||
return await checkDatabaseVersion(this);
|
return await checkDatabaseVersion(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
async prepare() {
|
async prepare() {
|
||||||
if (this.isMySQLCompatibleDialect()) {
|
if (this.isMySQLCompatibleDialect()) {
|
||||||
const result = await this.sequelize.query(`SHOW VARIABLES LIKE 'lower_case_table_names'`, { plain: true });
|
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 { Database, IDatabaseOptions } from './database';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import semver from 'semver';
|
import semver from 'semver';
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
/* istanbul ignore file -- @preserve */
|
||||||
import { merge } from '@nocobase/utils';
|
import { merge } from '@nocobase/utils';
|
||||||
import { customAlphabet } from 'nanoid';
|
import { customAlphabet } from 'nanoid';
|
||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
@ -43,7 +44,7 @@ export function getConfigByEnv() {
|
|||||||
|
|
||||||
function customLogger(queryString, queryObject) {
|
function customLogger(queryString, queryObject) {
|
||||||
console.log(queryString); // outputs a string
|
console.log(queryString); // outputs a string
|
||||||
if (queryObject.bind) {
|
if (queryObject?.bind) {
|
||||||
console.log(queryObject.bind); // outputs an array
|
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 { Transaction, Transactionable } from 'sequelize';
|
||||||
import { Collection } from '../collection';
|
import { Collection } from '../collection';
|
||||||
import sqlParser from '../sql-parser';
|
import sqlParser from '../sql-parser';
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
/* istanbul ignore file -- @preserve */
|
||||||
|
|
||||||
import { Collection } from '../collection';
|
import { Collection } from '../collection';
|
||||||
import sqlParser from '../sql-parser';
|
import sqlParser from '../sql-parser';
|
||||||
import QueryInterface, { TableInfo } from './query-interface';
|
import QueryInterface, { TableInfo } from './query-interface';
|
||||||
|
@ -184,18 +184,6 @@ export class BelongsToManyRepository extends MultipleRelationRepository {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
extendFindOptions(findOptions) {
|
|
||||||
let joinTableAttributes;
|
|
||||||
if (lodash.get(findOptions, 'fields')) {
|
|
||||||
joinTableAttributes = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...findOptions,
|
|
||||||
joinTableAttributes,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
throughName() {
|
throughName() {
|
||||||
return this.throughModel().name;
|
return this.throughModel().name;
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,9 @@ import { SingleRelationFindOption, SingleRelationRepository } from './single-rel
|
|||||||
type BelongsToFindOptions = SingleRelationFindOption;
|
type BelongsToFindOptions = SingleRelationFindOption;
|
||||||
|
|
||||||
export class BelongsToRepository extends SingleRelationRepository {
|
export class BelongsToRepository extends SingleRelationRepository {
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
async filterOptions(sourceModel) {
|
async filterOptions(sourceModel) {
|
||||||
const association = this.association as BelongsTo;
|
const association = this.association as BelongsTo;
|
||||||
|
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { omit } from 'lodash';
|
import { omit } from 'lodash';
|
||||||
import { HasMany, Op } from 'sequelize';
|
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 { AssociatedOptions, MultipleRelationRepository } from './multiple-relation-repository';
|
||||||
import { transaction } from './relation-repository';
|
import { transaction } from './relation-repository';
|
||||||
|
|
||||||
export class HasManyRepository extends MultipleRelationRepository {
|
export class HasManyRepository extends MultipleRelationRepository {
|
||||||
async find(options?: FindOptions): Promise<any> {
|
async find(options?: FindOptions): Promise<any> {
|
||||||
const targetRepository = this.targetCollection.repository;
|
const targetRepository = this.targetCollection.repository;
|
||||||
@ -120,6 +121,9 @@ export class HasManyRepository extends MultipleRelationRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
accessors() {
|
accessors() {
|
||||||
return (<HasMany>this.association).accessors;
|
return (<HasMany>this.association).accessors;
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,9 @@ import { HasOne } from 'sequelize';
|
|||||||
import { SingleRelationRepository } from './single-relation-repository';
|
import { SingleRelationRepository } from './single-relation-repository';
|
||||||
|
|
||||||
export class HasOneRepository extends SingleRelationRepository {
|
export class HasOneRepository extends SingleRelationRepository {
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
filterOptions(sourceModel) {
|
filterOptions(sourceModel) {
|
||||||
const association = this.association as HasOne;
|
const association = this.association as HasOne;
|
||||||
|
|
||||||
|
@ -8,8 +8,8 @@ import {
|
|||||||
Filter,
|
Filter,
|
||||||
FindOneOptions,
|
FindOneOptions,
|
||||||
FindOptions,
|
FindOptions,
|
||||||
TK,
|
|
||||||
TargetKey,
|
TargetKey,
|
||||||
|
TK,
|
||||||
UpdateOptions,
|
UpdateOptions,
|
||||||
} from '../repository';
|
} from '../repository';
|
||||||
import { updateModelByValues } from '../update-associations';
|
import { updateModelByValues } from '../update-associations';
|
||||||
@ -23,10 +23,6 @@ export interface AssociatedOptions extends Transactionable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export abstract class MultipleRelationRepository extends RelationRepository {
|
export abstract class MultipleRelationRepository extends RelationRepository {
|
||||||
extendFindOptions(findOptions) {
|
|
||||||
return findOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
async find(options?: FindOptions): Promise<any> {
|
async find(options?: FindOptions): Promise<any> {
|
||||||
const targetRepository = this.targetCollection.repository;
|
const targetRepository = this.targetCollection.repository;
|
||||||
|
|
||||||
|
@ -106,6 +106,9 @@ export abstract class SingleRelationRepository extends RelationRepository {
|
|||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
accessors() {
|
accessors() {
|
||||||
return <SingleAssociationAccessors>super.accessors();
|
return <SingleAssociationAccessors>super.accessors();
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,60 @@ export class SQLModel extends Model {
|
|||||||
|
|
||||||
static async sync(): Promise<any> {}
|
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(): {
|
private static parseTablesAndColumns(): {
|
||||||
table: string;
|
table: string;
|
||||||
columns: string | { name: string; as: string }[];
|
columns: string | { name: string; as: string }[];
|
||||||
@ -102,63 +156,9 @@ export class SQLModel extends Model {
|
|||||||
|
|
||||||
private static getTableNameWithSchema(table: string) {
|
private static getTableNameWithSchema(table: string) {
|
||||||
if (this.database.inDialect('postgres') && !table.includes('.')) {
|
if (this.database.inDialect('postgres') && !table.includes('.')) {
|
||||||
return `public.${table}`;
|
const schema = process.env.DB_SCHEMA || 'public';
|
||||||
|
return `${schema}.${table}`;
|
||||||
}
|
}
|
||||||
return 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]);
|
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 () => {
|
it('should call application with command', async () => {
|
||||||
await app.runCommand('start');
|
await app.runCommand('start');
|
||||||
const jestFn = vi.fn();
|
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 Application from '../application';
|
||||||
import { Gateway } from '../gateway';
|
import { Gateway } from '../gateway';
|
||||||
import { errors } from '../gateway/errors';
|
import { errors } from '../gateway/errors';
|
||||||
|
|
||||||
describe('gateway', () => {
|
describe('gateway', () => {
|
||||||
let gateway: Gateway;
|
let gateway: Gateway;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -14,6 +15,7 @@ describe('gateway', () => {
|
|||||||
await gateway.destroy();
|
await gateway.destroy();
|
||||||
await AppSupervisor.getInstance().destroy();
|
await AppSupervisor.getInstance().destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('app selector', () => {
|
describe('app selector', () => {
|
||||||
it('should get app as default main app', async () => {
|
it('should get app as default main app', async () => {
|
||||||
expect(
|
expect(
|
||||||
@ -45,6 +47,7 @@ describe('gateway', () => {
|
|||||||
expect(gateway.getAppSelectorMiddlewares().nodes.length).toBe(2);
|
expect(gateway.getAppSelectorMiddlewares().nodes.length).toBe(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('http api', () => {
|
describe('http api', () => {
|
||||||
it('should return error when app not found', async () => {
|
it('should return error when app not found', async () => {
|
||||||
const res = await supertest.agent(gateway.getCallback()).get('/api/app:getInfo');
|
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 { ACL } from '@nocobase/acl';
|
||||||
import { availableActions } from './available-action';
|
import { availableActions } from './available-action';
|
||||||
|
|
||||||
const configureResources = [
|
|
||||||
'roles',
|
|
||||||
'users',
|
|
||||||
'collections',
|
|
||||||
'fields',
|
|
||||||
'collections.fields',
|
|
||||||
'roles.collections',
|
|
||||||
'roles.resources',
|
|
||||||
'rolesResourcesScopes',
|
|
||||||
'availableActions',
|
|
||||||
];
|
|
||||||
|
|
||||||
export function createACL() {
|
export function createACL() {
|
||||||
const acl = new ACL();
|
const acl = new ACL();
|
||||||
|
|
||||||
@ -20,7 +8,5 @@ export function createACL() {
|
|||||||
acl.setAvailableAction(actionName, actionParams);
|
acl.setAvailableAction(actionName, actionParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
acl.registerConfigResources(configureResources);
|
|
||||||
|
|
||||||
return acl;
|
return acl;
|
||||||
}
|
}
|
||||||
|
@ -144,6 +144,7 @@ interface LoadOptions {
|
|||||||
reload?: boolean;
|
reload?: boolean;
|
||||||
hooks?: boolean;
|
hooks?: boolean;
|
||||||
sync?: boolean;
|
sync?: boolean;
|
||||||
|
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -201,7 +202,6 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
|||||||
protected plugins = new Map<string, Plugin>();
|
protected plugins = new Map<string, Plugin>();
|
||||||
protected _appSupervisor: AppSupervisor = AppSupervisor.getInstance();
|
protected _appSupervisor: AppSupervisor = AppSupervisor.getInstance();
|
||||||
protected _started: boolean;
|
protected _started: boolean;
|
||||||
protected _logger: SystemLogger;
|
|
||||||
private _authenticated = false;
|
private _authenticated = false;
|
||||||
private _maintaining = false;
|
private _maintaining = false;
|
||||||
private _maintainingCommandStatus: MaintainingCommandStatus;
|
private _maintainingCommandStatus: MaintainingCommandStatus;
|
||||||
@ -217,6 +217,12 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
|||||||
this._appSupervisor.addApp(this);
|
this._appSupervisor.addApp(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected _logger: SystemLogger;
|
||||||
|
|
||||||
|
get logger() {
|
||||||
|
return this._logger;
|
||||||
|
}
|
||||||
|
|
||||||
protected _loaded: boolean;
|
protected _loaded: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -254,10 +260,6 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
|||||||
return this.mainDataSource.collectionManager.db;
|
return this.mainDataSource.collectionManager.db;
|
||||||
}
|
}
|
||||||
|
|
||||||
get logger() {
|
|
||||||
return this._logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
get resourceManager() {
|
get resourceManager() {
|
||||||
return this.mainDataSource.resourceManager;
|
return this.mainDataSource.resourceManager;
|
||||||
}
|
}
|
||||||
@ -587,15 +589,6 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
|||||||
return this.pm.get(name) as P;
|
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() {
|
async authenticate() {
|
||||||
if (this._authenticated) {
|
if (this._authenticated) {
|
||||||
return;
|
return;
|
||||||
@ -614,46 +607,6 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
|||||||
return await this.runAsCLI([command, ...args], { from: 'user', throwError: true });
|
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
|
* @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
|
* @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() {
|
protected init() {
|
||||||
const options = this.options;
|
const options = this.options;
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
/* istanbul ignore file -- @preserve */
|
||||||
|
|
||||||
import Application from '../application';
|
import Application from '../application';
|
||||||
|
|
||||||
const REPL = require('repl');
|
const REPL = require('repl');
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
/* istanbul ignore file -- @preserve */
|
||||||
|
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { dirname, resolve } from 'path';
|
import { dirname, resolve } from 'path';
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
/* istanbul ignore file -- @preserve */
|
||||||
|
|
||||||
import Application from '../application';
|
import Application from '../application';
|
||||||
|
|
||||||
export default (app: Application) => {
|
export default (app: Application) => {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
/* istanbul ignore file -- @preserve */
|
||||||
|
|
||||||
import Application from '../application';
|
import Application from '../application';
|
||||||
|
|
||||||
export default (app: Application) => {
|
export default (app: Application) => {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
/* istanbul ignore file -- @preserve */
|
||||||
|
|
||||||
import Application from '../application';
|
import Application from '../application';
|
||||||
|
|
||||||
export default (app: Application) => {
|
export default (app: Application) => {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
/* istanbul ignore file -- @preserve */
|
||||||
|
|
||||||
import Application from '../application';
|
import Application from '../application';
|
||||||
|
|
||||||
export default (app: Application) => {
|
export default (app: Application) => {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
/* istanbul ignore file -- @preserve */
|
||||||
|
|
||||||
import Application from '../application';
|
import Application from '../application';
|
||||||
import createMigration from './create-migration';
|
import createMigration from './create-migration';
|
||||||
import dbAuth from './db-auth';
|
import dbAuth from './db-auth';
|
||||||
@ -12,6 +14,7 @@ import start from './start';
|
|||||||
import stop from './stop';
|
import stop from './stop';
|
||||||
import upgrade from './upgrade';
|
import upgrade from './upgrade';
|
||||||
import consoleCommand from './console';
|
import consoleCommand from './console';
|
||||||
|
|
||||||
export function registerCli(app: Application) {
|
export function registerCli(app: Application) {
|
||||||
consoleCommand(app);
|
consoleCommand(app);
|
||||||
dbAuth(app);
|
dbAuth(app);
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
/* istanbul ignore file -- @preserve */
|
||||||
|
|
||||||
import Application from '../application';
|
import Application from '../application';
|
||||||
|
|
||||||
export default (app: Application) => {
|
export default (app: Application) => {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
/* istanbul ignore file -- @preserve */
|
||||||
|
|
||||||
import Application from '../application';
|
import Application from '../application';
|
||||||
|
|
||||||
export default (app: Application) => {
|
export default (app: Application) => {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
/* istanbul ignore file -- @preserve */
|
||||||
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import Application from '../application';
|
import Application from '../application';
|
||||||
import { PluginCommandError } from '../errors/plugin-command-error';
|
import { PluginCommandError } from '../errors/plugin-command-error';
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
/* istanbul ignore file -- @preserve */
|
||||||
|
|
||||||
import Application from '../application';
|
import Application from '../application';
|
||||||
|
|
||||||
export default (app: Application) => {
|
export default (app: Application) => {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
/* istanbul ignore file -- @preserve */
|
||||||
|
|
||||||
import Application from '../application';
|
import Application from '../application';
|
||||||
|
|
||||||
export default (app: Application) => {
|
export default (app: Application) => {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
/* istanbul ignore file -- @preserve */
|
||||||
|
|
||||||
import { fsExists } from '@nocobase/utils';
|
import { fsExists } from '@nocobase/utils';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { resolve } from 'path';
|
import { resolve } from 'path';
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
/* istanbul ignore file -- @preserve */
|
||||||
|
|
||||||
import Application from '../application';
|
import Application from '../application';
|
||||||
|
|
||||||
export default (app: Application) => {
|
export default (app: Application) => {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
/* istanbul ignore file -- @preserve */
|
||||||
|
|
||||||
import Application from '../application';
|
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 { Registry, Toposort, ToposortOptions, uid } from '@nocobase/utils';
|
||||||
import { createStoragePluginsSymlink } from '@nocobase/utils/plugin-symlink';
|
import { createStoragePluginsSymlink } from '@nocobase/utils/plugin-symlink';
|
||||||
import { Command } from 'commander';
|
import { Command } from 'commander';
|
||||||
@ -55,13 +55,12 @@ export class Gateway extends EventEmitter {
|
|||||||
|
|
||||||
public server: http.Server | null = null;
|
public server: http.Server | null = null;
|
||||||
public ipcSocketServer: IPCSocketServer | 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 port: number = process.env.APP_PORT ? parseInt(process.env.APP_PORT) : null;
|
||||||
private host = '0.0.0.0';
|
private host = '0.0.0.0';
|
||||||
private wsServer: WSServer;
|
private wsServer: WSServer;
|
||||||
private socketPath = resolve(process.cwd(), 'storage', 'gateway.sock');
|
private socketPath = resolve(process.cwd(), 'storage', 'gateway.sock');
|
||||||
|
|
||||||
loggers = new Registry<SystemLogger>();
|
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
super();
|
super();
|
||||||
this.reset();
|
this.reset();
|
||||||
@ -78,6 +77,15 @@ export class Gateway extends EventEmitter {
|
|||||||
return Gateway.instance;
|
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() {
|
destroy() {
|
||||||
this.reset();
|
this.reset();
|
||||||
Gateway.instance = null;
|
Gateway.instance = null;
|
||||||
@ -284,6 +292,7 @@ export class Gateway extends EventEmitter {
|
|||||||
return this.requestHandler.bind(this);
|
return this.requestHandler.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* istanbul ignore next -- @preserve */
|
||||||
async watch() {
|
async watch() {
|
||||||
if (!process.env.IS_DEV_CMD) {
|
if (!process.env.IS_DEV_CMD) {
|
||||||
return;
|
return;
|
||||||
@ -295,6 +304,7 @@ export class Gateway extends EventEmitter {
|
|||||||
require(file);
|
require(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* istanbul ignore next -- @preserve */
|
||||||
async run(options: RunOptions) {
|
async run(options: RunOptions) {
|
||||||
const isStart = this.isStart();
|
const isStart = this.isStart();
|
||||||
let ipcClient: IPCSocketClient | false;
|
let ipcClient: IPCSocketClient | false;
|
||||||
@ -359,7 +369,6 @@ export class Gateway extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getStartOptions() {
|
getStartOptions() {
|
||||||
const argv = process.argv;
|
|
||||||
const program = new Command();
|
const program = new Command();
|
||||||
|
|
||||||
program
|
program
|
||||||
@ -369,9 +378,7 @@ export class Gateway extends EventEmitter {
|
|||||||
.option('-h, --host [host]')
|
.option('-h, --host [host]')
|
||||||
.option('--db-sync')
|
.option('--db-sync')
|
||||||
.parse(process.argv);
|
.parse(process.argv);
|
||||||
const options = program.opts();
|
return program.opts();
|
||||||
|
|
||||||
return options;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
start(options: StartHttpServerOptions) {
|
start(options: StartHttpServerOptions) {
|
||||||
@ -439,13 +446,4 @@ export class Gateway extends EventEmitter {
|
|||||||
this.server?.close();
|
this.server?.close();
|
||||||
this.wsServer?.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');
|
const mainApp = await AppSupervisor.getInstance().getApp('main');
|
||||||
if (!mainApp.cli.hasCommand(argv[2])) {
|
if (!mainApp.cli.hasCommand(argv[2])) {
|
||||||
// console.log('passCliArgv', argv[2]);
|
|
||||||
await mainApp.pm.loadCommands();
|
await mainApp.pm.loadCommands();
|
||||||
}
|
}
|
||||||
const cli = mainApp.cli;
|
const cli = mainApp.cli;
|
||||||
|
@ -10,10 +10,8 @@ import bodyParser from 'koa-bodyparser';
|
|||||||
import { resolve } from 'path';
|
import { resolve } from 'path';
|
||||||
import { createHistogram, RecordableHistogram } from 'perf_hooks';
|
import { createHistogram, RecordableHistogram } from 'perf_hooks';
|
||||||
import Application, { ApplicationOptions } from './application';
|
import Application, { ApplicationOptions } from './application';
|
||||||
import { parseVariables } from './middlewares';
|
|
||||||
import { dateTemplate } from './middlewares/data-template';
|
|
||||||
import { dataWrapping } from './middlewares/data-wrapping';
|
import { dataWrapping } from './middlewares/data-wrapping';
|
||||||
import { db2resource } from './middlewares/db2resource';
|
|
||||||
import { i18n } from './middlewares/i18n';
|
import { i18n } from './middlewares/i18n';
|
||||||
|
|
||||||
export function createI18n(options: ApplicationOptions) {
|
export function createI18n(options: ApplicationOptions) {
|
||||||
@ -112,11 +110,13 @@ export const getCommandFullName = (command: Command) => {
|
|||||||
return names.join('.');
|
return names.join('.');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* istanbul ignore next -- @preserve */
|
||||||
export const tsxRerunning = async () => {
|
export const tsxRerunning = async () => {
|
||||||
const file = resolve(process.cwd(), 'storage/app.watch.ts');
|
const file = resolve(process.cwd(), 'storage/app.watch.ts');
|
||||||
await fs.promises.writeFile(file, `export const watchId = '${uid()}';`, 'utf-8');
|
await fs.promises.writeFile(file, `export const watchId = '${uid()}';`, 'utf-8');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* istanbul ignore next -- @preserve */
|
||||||
export const enablePerfHooks = (app: Application) => {
|
export const enablePerfHooks = (app: Application) => {
|
||||||
app.context.getPerfHistogram = (name: string) => {
|
app.context.getPerfHistogram = (name: string) => {
|
||||||
if (!app.perfHistograms.has(name)) {
|
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 { DataSourceOptions, SequelizeDataSource } from '@nocobase/data-source-manager';
|
||||||
import { db2resource, parseVariables } from './middlewares';
|
import { parseVariables } from './middlewares';
|
||||||
import { dateTemplate } from './middlewares/data-template';
|
import { dateTemplate } from './middlewares/data-template';
|
||||||
|
|
||||||
export class MainDataSource extends SequelizeDataSource {
|
export class MainDataSource extends SequelizeDataSource {
|
||||||
|
@ -1,3 +1,2 @@
|
|||||||
export * from './data-wrapping';
|
export * from './data-wrapping';
|
||||||
export * from './db2resource';
|
|
||||||
export { parseVariables } from './parse-variables';
|
export { parseVariables } from './parse-variables';
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
/* istanbul ignore file -- @preserve */
|
||||||
|
|
||||||
import { Migration as DbMigration } from '@nocobase/database';
|
import { Migration as DbMigration } from '@nocobase/database';
|
||||||
import Application from './application';
|
import Application from './application';
|
||||||
import Plugin from './plugin';
|
import Plugin from './plugin';
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
/* istanbul ignore file -- @preserve */
|
||||||
|
|
||||||
import { DataTypes } from '@nocobase/database';
|
import { DataTypes } from '@nocobase/database';
|
||||||
import { Migration } from '../migration';
|
import { Migration } from '../migration';
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
/* istanbul ignore file -- @preserve */
|
||||||
|
|
||||||
import { Migration } from '../migration';
|
import { Migration } from '../migration';
|
||||||
import { PluginManager } from '../plugin-manager';
|
import { PluginManager } from '../plugin-manager';
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
/* istanbul ignore file -- @preserve */
|
||||||
|
|
||||||
import { Migration } from '../migration';
|
import { Migration } from '../migration';
|
||||||
|
|
||||||
export default class extends Migration {
|
export default class extends Migration {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
/* istanbul ignore file -- @preserve */
|
||||||
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
|
@ -71,11 +71,6 @@ export class PluginManager {
|
|||||||
*/
|
*/
|
||||||
server: net.Server;
|
server: net.Server;
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
_repository: PluginManagerRepository;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
@ -104,6 +99,11 @@ export class PluginManager {
|
|||||||
this.app.resourcer.use(uploadMiddleware);
|
this.app.resourcer.use(uploadMiddleware);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
_repository: PluginManagerRepository;
|
||||||
|
|
||||||
get repository() {
|
get repository() {
|
||||||
return this.app.db.getRepository('applicationPlugins') as PluginManagerRepository;
|
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 }) {
|
async create(pluginName: string, options?: { forceRecreate?: boolean }) {
|
||||||
const createPlugin = async (name) => {
|
const createPlugin = async (name) => {
|
||||||
const pluginDir = resolve(process.cwd(), 'packages/plugins', 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[]) {
|
async enable(name: string | string[]) {
|
||||||
let pluginNames = name;
|
let pluginNames = name;
|
||||||
if (name === '*') {
|
if (name === '*') {
|
||||||
@ -705,27 +692,6 @@ export class PluginManager {
|
|||||||
await execa('yarn', ['nocobase', 'refresh']);
|
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
|
* @internal
|
||||||
*/
|
*/
|
||||||
@ -1086,6 +1052,20 @@ export class PluginManager {
|
|||||||
}
|
}
|
||||||
this['_initPresetPlugins'] = true;
|
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;
|
export default PluginManager;
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
/* istanbul ignore next -- @preserve */
|
||||||
|
|
||||||
import { importModule, isURL } from '@nocobase/utils';
|
import { importModule, isURL } from '@nocobase/utils';
|
||||||
import { createStoragePluginSymLink } from '@nocobase/utils/plugin-symlink';
|
import { createStoragePluginSymLink } from '@nocobase/utils/plugin-symlink';
|
||||||
import axios, { AxiosRequestConfig } from 'axios';
|
import axios, { AxiosRequestConfig } from 'axios';
|
||||||
@ -277,6 +279,7 @@ export function getServerPackages(packageDir: string) {
|
|||||||
const exts = ['.js', '.ts', '.jsx', '.tsx'];
|
const exts = ['.js', '.ts', '.jsx', '.tsx'];
|
||||||
const importRegex = /import\s+.*?\s+from\s+['"]([^'"\s.].+?)['"];?/g;
|
const importRegex = /import\s+.*?\s+from\s+['"]([^'"\s.].+?)['"];?/g;
|
||||||
const requireRegex = /require\s*\(\s*[`'"]([^`'"\s.].+?)[`'"]\s*\)/g;
|
const requireRegex = /require\s*\(\s*[`'"]([^`'"\s.].+?)[`'"]\s*\)/g;
|
||||||
|
|
||||||
function setPluginsFromContent(reg: RegExp, content: string) {
|
function setPluginsFromContent(reg: RegExp, content: string) {
|
||||||
let match: RegExpExecArray | null;
|
let match: RegExpExecArray | null;
|
||||||
while ((match = reg.exec(content))) {
|
while ((match = reg.exec(content))) {
|
||||||
@ -428,6 +431,7 @@ async function getExternalVersionFromDistFile(packageName: string): Promise<fals
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isNotBuiltinModule(packageName: string) {
|
export function isNotBuiltinModule(packageName: string) {
|
||||||
return !builtinModules.includes(packageName);
|
return !builtinModules.includes(packageName);
|
||||||
}
|
}
|
||||||
@ -503,6 +507,7 @@ export interface DepCompatible {
|
|||||||
versionRange: string;
|
versionRange: string;
|
||||||
packageVersion: string;
|
packageVersion: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCompatible(packageName: string) {
|
export async function getCompatible(packageName: string) {
|
||||||
let externalVersion: Record<string, string>;
|
let externalVersion: Record<string, string>;
|
||||||
const hasSrc = fs.existsSync(path.join(getPackageDir(packageName), 'src'));
|
const hasSrc = fs.existsSync(path.join(getPackageDir(packageName), 'src'));
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
/* istanbul ignore file -- @preserve */
|
||||||
|
|
||||||
import { Model } from '@nocobase/database';
|
import { Model } from '@nocobase/database';
|
||||||
import { LoggerOptions } from '@nocobase/logger';
|
import { LoggerOptions } from '@nocobase/logger';
|
||||||
import { fsExists, importModule } from '@nocobase/utils';
|
import { fsExists, importModule } from '@nocobase/utils';
|
||||||
@ -6,7 +8,7 @@ import glob from 'glob';
|
|||||||
import type { TFuncKey, TOptions } from 'i18next';
|
import type { TFuncKey, TOptions } from 'i18next';
|
||||||
import { basename, resolve } from 'path';
|
import { basename, resolve } from 'path';
|
||||||
import { Application } from './application';
|
import { Application } from './application';
|
||||||
import { InstallOptions, getExposeChangelogUrl, getExposeReadmeUrl } from './plugin-manager';
|
import { getExposeChangelogUrl, getExposeReadmeUrl, InstallOptions } from './plugin-manager';
|
||||||
import { checkAndGetCompatible } from './plugin-manager/utils';
|
import { checkAndGetCompatible } from './plugin-manager/utils';
|
||||||
|
|
||||||
export interface PluginInterface {
|
export interface PluginInterface {
|
||||||
@ -135,22 +137,6 @@ export abstract class Plugin<O = any> implements PluginInterface {
|
|||||||
this.options = options || {};
|
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
|
* @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) });
|
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
|
* @experimental
|
||||||
*/
|
*/
|
||||||
@ -286,6 +256,38 @@ export abstract class Plugin<O = any> implements PluginInterface {
|
|||||||
|
|
||||||
return results;
|
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;
|
export default Plugin;
|
||||||
|
@ -4,7 +4,7 @@ import react from '@vitejs/plugin-react';
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path, { resolve } from 'path';
|
import path, { resolve } from 'path';
|
||||||
import { URL } from 'url';
|
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'];
|
const CORE_CLIENT_PACKAGES = ['sdk', 'client'];
|
||||||
|
|
||||||
@ -157,7 +157,7 @@ export const getFilterInclude = (isServer, isCoverage) => {
|
|||||||
if (!item.startsWith('-')) {
|
if (!item.startsWith('-')) {
|
||||||
const pre = argv[index - 1];
|
const pre = argv[index - 1];
|
||||||
|
|
||||||
if (pre && pre.startsWith('--') && !pre.includes('=')) {
|
if (pre && pre.startsWith('--') && ['--reporter'].includes(pre)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,6 +173,7 @@ export const getFilterInclude = (isServer, isCoverage) => {
|
|||||||
if (!filterFileOrDir) return {};
|
if (!filterFileOrDir) return {};
|
||||||
const absPath = path.join(process.cwd(), filterFileOrDir);
|
const absPath = path.join(process.cwd(), filterFileOrDir);
|
||||||
const isDir = fs.existsSync(absPath) && fs.statSync(absPath).isDirectory();
|
const isDir = fs.existsSync(absPath) && fs.statSync(absPath).isDirectory();
|
||||||
|
|
||||||
// 如果是文件,则只测试当前文件
|
// 如果是文件,则只测试当前文件
|
||||||
if (!isDir) {
|
if (!isDir) {
|
||||||
return {
|
return {
|
||||||
@ -185,6 +186,7 @@ export const getFilterInclude = (isServer, isCoverage) => {
|
|||||||
|
|
||||||
// 判断是否为包目录,如果不是包目录,则只测试当前目录
|
// 判断是否为包目录,如果不是包目录,则只测试当前目录
|
||||||
const isPackage = fs.existsSync(path.join(absPath, 'package.json'));
|
const isPackage = fs.existsSync(path.join(absPath, 'package.json'));
|
||||||
|
|
||||||
if (!isPackage) {
|
if (!isPackage) {
|
||||||
return {
|
return {
|
||||||
include: [`${filterFileOrDir}/${suffix}`],
|
include: [`${filterFileOrDir}/${suffix}`],
|
||||||
@ -235,6 +237,7 @@ export const defineConfig = () => {
|
|||||||
|
|
||||||
if (filterInclude) {
|
if (filterInclude) {
|
||||||
config.test.include = filterInclude;
|
config.test.include = filterInclude;
|
||||||
|
|
||||||
if (isFile) {
|
if (isFile) {
|
||||||
// 减少收集的文件
|
// 减少收集的文件
|
||||||
config.test.exclude = [];
|
config.test.exclude = [];
|
||||||
@ -248,14 +251,17 @@ export const defineConfig = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isCoverage = process.argv.includes('--coverage');
|
const isCoverage = process.argv.includes('--coverage');
|
||||||
|
|
||||||
if (!isCoverage) {
|
if (!isCoverage) {
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { include: coverageInclude } = getFilterInclude(isServer, true);
|
const { include: coverageInclude } = getFilterInclude(isServer, true);
|
||||||
|
|
||||||
if (coverageInclude) {
|
if (coverageInclude) {
|
||||||
config.test.coverage.include = coverageInclude;
|
config.test.coverage.include = coverageInclude;
|
||||||
}
|
}
|
||||||
|
|
||||||
const reportsDirectory = getReportsDirectory(isServer);
|
const reportsDirectory = getReportsDirectory(isServer);
|
||||||
if (reportsDirectory) {
|
if (reportsDirectory) {
|
||||||
config.test.coverage.reportsDirectory = reportsDirectory;
|
config.test.coverage.reportsDirectory = reportsDirectory;
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import lodash from 'lodash';
|
import lodash from 'lodash';
|
||||||
import { NoPermissionError } from '@nocobase/acl';
|
|
||||||
import { snakeCase } from '@nocobase/database';
|
import { snakeCase } from '@nocobase/database';
|
||||||
|
|
||||||
|
class NoPermissionError extends Error {}
|
||||||
|
|
||||||
function createWithACLMetaMiddleware() {
|
function createWithACLMetaMiddleware() {
|
||||||
return async (ctx: any, next) => {
|
return async (ctx: any, next) => {
|
||||||
await next();
|
await next();
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
/* istanbul ignore file -- @preserve */
|
||||||
|
|
||||||
import { Migration } from '@nocobase/server';
|
import { Migration } from '@nocobase/server';
|
||||||
|
|
||||||
export default class extends Migration {
|
export default class extends Migration {
|
||||||
|
@ -1,19 +1,9 @@
|
|||||||
import { ACL, ACLRole } from '@nocobase/acl';
|
import { ACL, ACLRole } from '@nocobase/acl';
|
||||||
import { Database, Model } from '@nocobase/database';
|
import { Model } from '@nocobase/database';
|
||||||
import { AssociationFieldAction, AssociationFieldsActions, GrantHelper } from '../server';
|
|
||||||
|
|
||||||
export class RoleResourceActionModel extends Model {
|
export class RoleResourceActionModel extends Model {
|
||||||
async writeToACL(options: {
|
async writeToACL(options: { acl: ACL; role: ACLRole; resourceName: string }) {
|
||||||
acl: ACL;
|
const { resourceName, role } = options;
|
||||||
role: ACLRole;
|
|
||||||
resourceName: string;
|
|
||||||
associationFieldsActions: AssociationFieldsActions;
|
|
||||||
grantHelper: GrantHelper;
|
|
||||||
}) {
|
|
||||||
// @ts-ignore
|
|
||||||
const db: Database = this.constructor.database;
|
|
||||||
|
|
||||||
const { resourceName, role, acl, associationFieldsActions, grantHelper } = options;
|
|
||||||
|
|
||||||
const actionName = this.get('name') as string;
|
const actionName = this.get('name') as string;
|
||||||
|
|
||||||
@ -33,63 +23,5 @@ export class RoleResourceActionModel extends Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
role.grantAction(actionPath, actionParams);
|
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 { ACL, ACLResource, ACLRole } from '@nocobase/acl';
|
||||||
import { Model } from '@nocobase/database';
|
import { Model } from '@nocobase/database';
|
||||||
import { AssociationFieldsActions, GrantHelper } from '../server';
|
|
||||||
import { RoleResourceActionModel } from './RoleResourceActionModel';
|
import { RoleResourceActionModel } from './RoleResourceActionModel';
|
||||||
|
|
||||||
export class RoleResourceModel extends Model {
|
export class RoleResourceModel extends Model {
|
||||||
async revoke(options: { role: ACLRole; resourceName: string; grantHelper: GrantHelper }) {
|
async revoke(options: { role: ACLRole; resourceName: string }) {
|
||||||
const { role, resourceName, grantHelper } = options;
|
const { role, resourceName } = options;
|
||||||
role.revokeResource(resourceName);
|
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: {
|
async writeToACL(options: { acl: ACL; transaction: any }) {
|
||||||
acl: ACL;
|
const { acl } = options;
|
||||||
associationFieldsActions: AssociationFieldsActions;
|
|
||||||
grantHelper: GrantHelper;
|
|
||||||
transaction: any;
|
|
||||||
}) {
|
|
||||||
const { acl, associationFieldsActions, grantHelper } = options;
|
|
||||||
const resourceName = this.get('name') as string;
|
const resourceName = this.get('name') as string;
|
||||||
const roleName = this.get('roleName') as string;
|
const roleName = this.get('roleName') as string;
|
||||||
const role = acl.getRole(roleName);
|
const role = acl.getRole(roleName);
|
||||||
@ -42,7 +20,7 @@ export class RoleResourceModel extends Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// revoke resource of role
|
// revoke resource of role
|
||||||
await this.revoke({ role, resourceName, grantHelper });
|
await this.revoke({ role, resourceName });
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (this.usingActionsConfig === false) {
|
if (this.usingActionsConfig === false) {
|
||||||
@ -66,8 +44,6 @@ export class RoleResourceModel extends Model {
|
|||||||
acl,
|
acl,
|
||||||
role,
|
role,
|
||||||
resourceName,
|
resourceName,
|
||||||
associationFieldsActions,
|
|
||||||
grantHelper: options.grantHelper,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Context, utils as actionUtils } from '@nocobase/actions';
|
import { Context, utils as actionUtils } from '@nocobase/actions';
|
||||||
import { Cache } from '@nocobase/cache';
|
import { Cache } from '@nocobase/cache';
|
||||||
import { Collection, RelationField } from '@nocobase/database';
|
import { Collection, RelationField, Transaction } from '@nocobase/database';
|
||||||
import { Plugin } from '@nocobase/server';
|
import { Plugin } from '@nocobase/server';
|
||||||
import { Mutex } from 'async-mutex';
|
import { Mutex } from 'async-mutex';
|
||||||
import lodash from 'lodash';
|
import lodash from 'lodash';
|
||||||
@ -15,112 +15,25 @@ import { RoleModel } from './model/RoleModel';
|
|||||||
import { RoleResourceActionModel } from './model/RoleResourceActionModel';
|
import { RoleResourceActionModel } from './model/RoleResourceActionModel';
|
||||||
import { RoleResourceModel } from './model/RoleResourceModel';
|
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 {
|
export class PluginACLServer extends Plugin {
|
||||||
// association field actions config
|
|
||||||
|
|
||||||
associationFieldsActions: AssociationFieldsActions = {};
|
|
||||||
|
|
||||||
grantHelper = new GrantHelper();
|
|
||||||
|
|
||||||
get acl() {
|
get acl() {
|
||||||
return this.app.acl;
|
return this.app.acl;
|
||||||
}
|
}
|
||||||
|
|
||||||
registerAssociationFieldAction(associationType: string, value: AssociationFieldActions) {
|
async writeResourceToACL(resourceModel: RoleResourceModel, transaction: Transaction) {
|
||||||
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) {
|
|
||||||
await resourceModel.writeToACL({
|
await resourceModel.writeToACL({
|
||||||
acl: this.acl,
|
acl: this.acl,
|
||||||
associationFieldsActions: this.associationFieldsActions,
|
|
||||||
transaction: transaction,
|
transaction: transaction,
|
||||||
grantHelper: this.grantHelper,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async writeActionToACL(actionModel: RoleResourceActionModel, transaction) {
|
async writeActionToACL(actionModel: RoleResourceActionModel, transaction: Transaction) {
|
||||||
const resource = actionModel.get('resource') as RoleResourceModel;
|
const resource = actionModel.get('resource') as RoleResourceModel;
|
||||||
const role = this.acl.getRole(resource.get('roleName') as string);
|
const role = this.acl.getRole(resource.get('roleName') as string);
|
||||||
await actionModel.writeToACL({
|
await actionModel.writeToACL({
|
||||||
acl: this.acl,
|
acl: this.acl,
|
||||||
role,
|
role,
|
||||||
resourceName: resource.get('name') as string,
|
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 { Application, AppSupervisor } from '@nocobase/server';
|
||||||
import { Restorer } from '../restorer';
|
import { Restorer } from '../restorer';
|
||||||
import { DumpRulesGroupType } from '@nocobase/database';
|
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