mirror of
https://github.com/nocobase/nocobase
synced 2024-11-15 07:06:06 +00:00
feat: improve code
This commit is contained in:
parent
a51c5058fb
commit
27f6bde775
@ -10,7 +10,7 @@
|
||||
"examples": "ts-node-dev -r dotenv/config ./examples",
|
||||
"start": "cd packages/app && npm start",
|
||||
"start-client": "cd packages/app && npm run start-client",
|
||||
"start-server": "nodemon",
|
||||
"start-server": "ts-node-dev -r dotenv/config ./packages/api/src/index.ts",
|
||||
"start-docs": "dumi dev",
|
||||
"build-docs": "dumi build",
|
||||
"build2": "lerna run build",
|
||||
@ -26,7 +26,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/file-saver": "^2.0.3",
|
||||
"@types/jest": "^24.0.18",
|
||||
"@types/jest": "^27.0.1",
|
||||
"@types/koa": "^2.13.1",
|
||||
"@types/koa-mount": "^4.0.1",
|
||||
"@types/lodash": "^4.14.169",
|
||||
|
@ -1,15 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const keys = process.argv;
|
||||
|
||||
const key = keys.pop();
|
||||
|
||||
const dotenv = require('dotenv');
|
||||
|
||||
dotenv.config();
|
||||
|
||||
if (key === 'start') {
|
||||
require('../lib/index');
|
||||
} else if (key === 'db-init') {
|
||||
require('../lib/migrations/init');
|
||||
}
|
||||
require('../lib/index');
|
||||
|
@ -1,58 +0,0 @@
|
||||
import Server from '@nocobase/server';
|
||||
|
||||
// @ts-ignore
|
||||
const sync = global.sync || {
|
||||
force: false,
|
||||
alter: {
|
||||
drop: false,
|
||||
},
|
||||
};
|
||||
|
||||
console.log('process.env.NOCOBASE_ENV', process.env.NOCOBASE_ENV);
|
||||
|
||||
const api = new Server({
|
||||
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',
|
||||
},
|
||||
pool: {
|
||||
max: 5,
|
||||
min: 0,
|
||||
acquire: 60000,
|
||||
idle: 10000,
|
||||
},
|
||||
logging: process.env.DB_LOG_SQL === 'on' ? console.log : false,
|
||||
define: {},
|
||||
sync,
|
||||
},
|
||||
resourcer: {
|
||||
prefix: '/api',
|
||||
},
|
||||
});
|
||||
|
||||
const plugins = [
|
||||
'@nocobase/plugin-collections',
|
||||
'@nocobase/plugin-ui-router',
|
||||
'@nocobase/plugin-ui-schema',
|
||||
'@nocobase/plugin-users',
|
||||
'@nocobase/plugin-action-logs',
|
||||
'@nocobase/plugin-file-manager',
|
||||
'@nocobase/plugin-permissions',
|
||||
'@nocobase/plugin-export',
|
||||
'@nocobase/plugin-system-settings',
|
||||
// // '@nocobase/plugin-automations',
|
||||
'@nocobase/plugin-china-region',
|
||||
];
|
||||
|
||||
for (const plugin of plugins) {
|
||||
api.registerPlugin(plugin, [require(`${plugin}/${__filename.endsWith('.ts') ? 'src' : 'lib'}/server`).default]);
|
||||
}
|
||||
|
||||
export default api;
|
@ -1,30 +1,65 @@
|
||||
import api from './app';
|
||||
import { middlewares } from '@nocobase/server';
|
||||
import Server from '@nocobase/server';
|
||||
|
||||
const api = new Server({
|
||||
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',
|
||||
},
|
||||
pool: {
|
||||
max: 5,
|
||||
min: 0,
|
||||
acquire: 60000,
|
||||
idle: 10000,
|
||||
},
|
||||
logging: process.env.DB_LOG_SQL === 'on' ? console.log : false,
|
||||
define: {},
|
||||
sync: {
|
||||
force: false,
|
||||
alter: {
|
||||
drop: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
resourcer: {
|
||||
prefix: '/api',
|
||||
},
|
||||
});
|
||||
|
||||
const plugins = [
|
||||
'@nocobase/plugin-collections',
|
||||
'@nocobase/plugin-ui-router',
|
||||
'@nocobase/plugin-ui-schema',
|
||||
'@nocobase/plugin-users',
|
||||
'@nocobase/plugin-action-logs',
|
||||
'@nocobase/plugin-file-manager',
|
||||
'@nocobase/plugin-permissions',
|
||||
'@nocobase/plugin-export',
|
||||
'@nocobase/plugin-system-settings',
|
||||
'@nocobase/plugin-china-region',
|
||||
];
|
||||
|
||||
for (const plugin of plugins) {
|
||||
api.plugin(require(`${plugin}/${__filename.endsWith('.ts') ? 'src' : 'lib'}/server`).default);
|
||||
}
|
||||
|
||||
(async () => {
|
||||
api.resourcer.use(middlewares.actionParams());
|
||||
|
||||
api.on('plugins.afterLoad', async () => {
|
||||
console.log('plugins.afterLoad')
|
||||
if (process.env.NOCOBASE_ENV === 'demo') {
|
||||
api.resourcer.use(middlewares.demoBlacklistedActions({
|
||||
emails: [process.env.ADMIN_EMAIL],
|
||||
}));
|
||||
}
|
||||
api.use(middlewares.appDistServe({
|
||||
root: process.env.APP_DIST,
|
||||
useStaticServer: !(process.env.APP_USE_STATIC_SERVER === 'false' || !process.env.APP_USE_STATIC_SERVER),
|
||||
}));
|
||||
});
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
if (process.argv.length < 3) {
|
||||
process.argv.push('start', '--port', process.env.API_PORT);
|
||||
}
|
||||
|
||||
await api.start(process.argv);
|
||||
console.log(process.argv);
|
||||
|
||||
await api.parse(process.argv);
|
||||
console.log(api.db.getTables().map(t => t.getName()));
|
||||
console.log(`Start-up time: ${(Date.now() - start) / 1000}s`);
|
||||
console.log(`http://localhost:${process.env.API_PORT}/`);
|
||||
// console.log(`http://localhost:${process.env.API_PORT}/`);
|
||||
})();
|
||||
|
@ -1,15 +0,0 @@
|
||||
// @ts-ignore
|
||||
const keys = process.argv;
|
||||
|
||||
// @ts-ignore
|
||||
global.sync = {
|
||||
force: false,
|
||||
alter: {
|
||||
drop: false,
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
const filename: string = keys.pop();
|
||||
|
||||
require(`./migrations/${filename}`);
|
@ -1,125 +0,0 @@
|
||||
// @ts-ignore
|
||||
global.sync = {
|
||||
force: true,
|
||||
alter: {
|
||||
drop: true,
|
||||
},
|
||||
};
|
||||
|
||||
import Database from '@nocobase/database';
|
||||
import api from '../app';
|
||||
import * as uiSchema from './ui-schema';
|
||||
|
||||
(async () => {
|
||||
await api.loadPlugins();
|
||||
const database: Database = api.db;
|
||||
await database.sync({
|
||||
// tables: ['collections', 'fields', 'actions', 'views', 'tabs'],
|
||||
});
|
||||
|
||||
const config =
|
||||
require('@nocobase/plugin-users/src/collections/users').default;
|
||||
const Collection = database.getModel('collections');
|
||||
const collection = await Collection.create(config);
|
||||
await collection.updateAssociations({
|
||||
generalFields: config.fields.filter((field) => field.state !== 0),
|
||||
systemFields: config.fields.filter((field) => field.state === 0),
|
||||
});
|
||||
await collection.migrate();
|
||||
|
||||
const Route = database.getModel('routes');
|
||||
|
||||
const data = [
|
||||
{
|
||||
type: 'redirect',
|
||||
from: '/',
|
||||
to: '/admin',
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
type: 'route',
|
||||
path: '/admin/:name(.+)?',
|
||||
component: 'AdminLayout',
|
||||
title: `后台`,
|
||||
uiSchema: uiSchema.menu,
|
||||
},
|
||||
{
|
||||
type: 'route',
|
||||
component: 'AuthLayout',
|
||||
children: [
|
||||
{
|
||||
type: 'route',
|
||||
path: '/login',
|
||||
component: 'RouteSchemaRenderer',
|
||||
title: `登录`,
|
||||
uiSchema: uiSchema.login,
|
||||
},
|
||||
{
|
||||
type: 'route',
|
||||
path: '/register',
|
||||
component: 'RouteSchemaRenderer',
|
||||
title: `注册`,
|
||||
uiSchema: uiSchema.register,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
for (const item of data) {
|
||||
const route = await Route.create(item);
|
||||
await route.updateAssociations(item);
|
||||
}
|
||||
|
||||
const Storage = database.getModel('storages');
|
||||
await Storage.create({
|
||||
title: '本地存储',
|
||||
name: `local`,
|
||||
type: 'local',
|
||||
baseUrl: process.env.LOCAL_STORAGE_BASE_URL,
|
||||
default: process.env.STORAGE_TYPE === 'local',
|
||||
});
|
||||
await Storage.create({
|
||||
name: `ali-oss`,
|
||||
type: 'ali-oss',
|
||||
baseUrl: process.env.ALI_OSS_STORAGE_BASE_URL,
|
||||
options: {
|
||||
region: process.env.ALI_OSS_REGION,
|
||||
accessKeyId: process.env.ALI_OSS_ACCESS_KEY_ID,
|
||||
accessKeySecret: process.env.ALI_OSS_ACCESS_KEY_SECRET,
|
||||
bucket: process.env.ALI_OSS_BUCKET,
|
||||
},
|
||||
default: process.env.STORAGE_TYPE === 'ali-oss',
|
||||
});
|
||||
|
||||
// 导入地域数据
|
||||
const ChinaRegion = database.getModel('china_regions');
|
||||
ChinaRegion && (await ChinaRegion.importData());
|
||||
|
||||
const SystemSetting = database.getModel('system_settings');
|
||||
if (SystemSetting) {
|
||||
const setting = await SystemSetting.create({
|
||||
title: 'NocoBase',
|
||||
showLogoOnly: true,
|
||||
});
|
||||
await setting.updateAssociations({
|
||||
logo: {
|
||||
title: 'nocobase-logo',
|
||||
filename: '682e5ad037dd02a0fe4800a3e91c283b.png',
|
||||
extname: '.png',
|
||||
mimetype: 'image/png',
|
||||
url: 'https://nocobase.oss-cn-beijing.aliyuncs.com/682e5ad037dd02a0fe4800a3e91c283b.png',
|
||||
storage_id: 2,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const User = database.getModel('users');
|
||||
const user = await User.create({
|
||||
nickname: '超级管理员',
|
||||
email: process.env.ADMIN_EMAIL,
|
||||
password: process.env.ADMIN_PASSWORD,
|
||||
});
|
||||
|
||||
await database.close();
|
||||
})();
|
||||
|
@ -1,11 +0,0 @@
|
||||
import Database from '@nocobase/database';
|
||||
import api from '../app';
|
||||
|
||||
(async () => {
|
||||
await api.loadPlugins();
|
||||
const database: Database = api.db;
|
||||
await database.sync({
|
||||
// tables: ['collections', 'fields', 'actions', 'views', 'tabs'],
|
||||
});
|
||||
await database.close();
|
||||
})();
|
@ -1,3 +0,0 @@
|
||||
export * from './login';
|
||||
export * from './menu';
|
||||
export * from './register';
|
@ -1,64 +0,0 @@
|
||||
export const login = {
|
||||
key: 'dtf9j0b8p9u',
|
||||
type: 'object',
|
||||
title: '登录',
|
||||
properties: {
|
||||
email: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
'x-component-props': {
|
||||
placeholder: '电子邮箱',
|
||||
style: {
|
||||
// width: 240,
|
||||
},
|
||||
},
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Password',
|
||||
'x-component-props': {
|
||||
placeholder: '密码',
|
||||
style: {
|
||||
// width: 240,
|
||||
},
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
type: 'void',
|
||||
'x-component': 'Div',
|
||||
properties: {
|
||||
submit: {
|
||||
type: 'void',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
block: true,
|
||||
type: 'primary',
|
||||
useAction: '{{ useLogin }}',
|
||||
style: {
|
||||
width: '100%',
|
||||
},
|
||||
},
|
||||
title: '登录',
|
||||
},
|
||||
},
|
||||
},
|
||||
registerlink: {
|
||||
type: 'void',
|
||||
'x-component': 'Div',
|
||||
properties: {
|
||||
link: {
|
||||
type: 'void',
|
||||
'x-component': 'Action.Link',
|
||||
'x-component-props': {
|
||||
to: '/register',
|
||||
},
|
||||
title: '注册账号',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
@ -1,15 +0,0 @@
|
||||
export const menu = {
|
||||
key: 'qqzzjakwkwl',
|
||||
name: 'qqzzjakwkwl',
|
||||
type: 'void',
|
||||
'x-component': 'Menu',
|
||||
'x-designable-bar': 'Menu.DesignableBar',
|
||||
'x-component-props': {
|
||||
mode: 'mix',
|
||||
theme: 'dark',
|
||||
defaultSelectedKeys: '{{ selectedKeys }}',
|
||||
sideMenuRef: '{{ sideMenuRef }}',
|
||||
onSelect: '{{ onSelect }}',
|
||||
onRemove: '{{ onMenuItemRemove }}',
|
||||
},
|
||||
};
|
@ -1,100 +0,0 @@
|
||||
export const register = {
|
||||
key: '46qlxqam3xk',
|
||||
type: 'object',
|
||||
title: '注册',
|
||||
properties: {
|
||||
email: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
'x-component-props': {
|
||||
placeholder: '电子邮箱',
|
||||
style: {
|
||||
// width: 240,
|
||||
},
|
||||
},
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Password',
|
||||
'x-component-props': {
|
||||
placeholder: '密码',
|
||||
checkStrength: true,
|
||||
style: {
|
||||
// width: 240,
|
||||
},
|
||||
},
|
||||
'x-reactions': [
|
||||
{
|
||||
dependencies: ['.confirm_password'],
|
||||
fulfill: {
|
||||
state: {
|
||||
errors:
|
||||
'{{$deps[0] && $self.value && $self.value !== $deps[0] ? "确认密码不匹配" : ""}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
confirm_password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Password',
|
||||
'x-component-props': {
|
||||
placeholder: '确认密码',
|
||||
checkStrength: true,
|
||||
style: {
|
||||
// width: 240,
|
||||
},
|
||||
},
|
||||
'x-reactions': [
|
||||
{
|
||||
dependencies: ['.password'],
|
||||
fulfill: {
|
||||
state: {
|
||||
errors:
|
||||
'{{$deps[0] && $self.value && $self.value !== $deps[0] ? "确认密码不匹配" : ""}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
actions: {
|
||||
type: 'void',
|
||||
'x-component': 'Div',
|
||||
properties: {
|
||||
submit: {
|
||||
type: 'void',
|
||||
title: '注册',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
block: true,
|
||||
type: 'primary',
|
||||
useAction: '{{ useRegister }}',
|
||||
style: {
|
||||
width: '100%',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
registerlink: {
|
||||
type: 'void',
|
||||
'x-component': 'Div',
|
||||
properties: {
|
||||
link: {
|
||||
type: 'void',
|
||||
'x-component': 'Action.Link',
|
||||
'x-component-props': {
|
||||
to: '/login',
|
||||
},
|
||||
title: '使用已有账号登录',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
@ -48,6 +48,32 @@ describe('has many field', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it.only('custom sourceKey', async () => {
|
||||
const collection = db.collection({
|
||||
name: 'posts',
|
||||
schema: [
|
||||
{ type: 'string', name: 'key', unique: true },
|
||||
{
|
||||
type: 'hasMany',
|
||||
name: 'comments',
|
||||
sourceKey: 'key',
|
||||
// foreignKey: 'postKey',
|
||||
},
|
||||
],
|
||||
});
|
||||
const comments = db.collection({
|
||||
name: 'comments',
|
||||
schema: [],
|
||||
});
|
||||
const association = collection.model.associations.comments;
|
||||
expect(association).toBeDefined();
|
||||
expect(association.foreignKey).toBe('postKey');
|
||||
// @ts-ignore
|
||||
expect(association.sourceKey).toBe('key');
|
||||
expect(comments.model.rawAttributes['postKey']).toBeDefined();
|
||||
await db.sync();
|
||||
});
|
||||
|
||||
it('custom sourceKey and foreignKey', async () => {
|
||||
const collection = db.collection({
|
||||
name: 'posts',
|
||||
|
@ -0,0 +1,67 @@
|
||||
import { Database } from '../../database';
|
||||
import { mockDatabase } from '../';
|
||||
|
||||
describe('has many field', () => {
|
||||
let db: Database;
|
||||
|
||||
beforeEach(() => {
|
||||
db = mockDatabase();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
it('association undefined', async () => {
|
||||
const User = db.collection({
|
||||
name: 'users',
|
||||
schema: [{ type: 'hasOne', name: 'profile' }],
|
||||
});
|
||||
await db.sync();
|
||||
expect(User.model.associations.profile).toBeUndefined();
|
||||
});
|
||||
|
||||
it('association defined', async () => {
|
||||
const User = db.collection({
|
||||
name: 'users',
|
||||
schema: [{ type: 'hasOne', name: 'profile' }],
|
||||
});
|
||||
expect(User.model.associations.phone).toBeUndefined();
|
||||
const Profile = db.collection({
|
||||
name: 'profiles',
|
||||
schema: [{ type: 'string', name: 'content' }],
|
||||
});
|
||||
const association = User.model.associations.profile;
|
||||
expect(association).toBeDefined();
|
||||
expect(association.foreignKey).toBe('userId');
|
||||
// @ts-ignore
|
||||
expect(association.sourceKey).toBe('id');
|
||||
expect(Profile.model.rawAttributes['userId']).toBeDefined();
|
||||
await db.sync();
|
||||
// const post = await model.create<any>();
|
||||
// await post.createComment({
|
||||
// content: 'content111',
|
||||
// });
|
||||
// const postComments = await post.getComments();
|
||||
// expect(postComments.map((comment) => comment.content)).toEqual([
|
||||
// 'content111',
|
||||
// ]);
|
||||
});
|
||||
|
||||
it('schema delete', async () => {
|
||||
const User = db.collection({
|
||||
name: 'users',
|
||||
schema: [{ type: 'hasOne', name: 'profile' }],
|
||||
});
|
||||
const Profile = db.collection({
|
||||
name: 'profiles',
|
||||
schema: [{ type: 'belongsTo', name: 'user' }],
|
||||
});
|
||||
await db.sync();
|
||||
User.schema.delete('profile');
|
||||
expect(User.model.associations.profile).toBeUndefined();
|
||||
expect(Profile.model.rawAttributes.userId).toBeDefined();
|
||||
Profile.schema.delete('user');
|
||||
expect(Profile.model.rawAttributes.userId).toBeUndefined();
|
||||
});
|
||||
});
|
@ -0,0 +1,70 @@
|
||||
import { Database } from '../../database';
|
||||
import { mockDatabase } from '../';
|
||||
import { SortField } from '../../schema-fields';
|
||||
|
||||
describe('string field', () => {
|
||||
let db: Database;
|
||||
|
||||
beforeEach(() => {
|
||||
db = mockDatabase();
|
||||
db.registerSchemaTypes({
|
||||
sort: SortField
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
it('sort', async () => {
|
||||
const Test = db.collection({
|
||||
name: 'tests',
|
||||
schema: [
|
||||
{ type: 'sort', name: 'sort' },
|
||||
],
|
||||
});
|
||||
await db.sync();
|
||||
const test1 = await Test.model.create<any>();
|
||||
expect(test1.sort).toBe(1);
|
||||
const test2 = await Test.model.create<any>();
|
||||
expect(test2.sort).toBe(2);
|
||||
const test3 = await Test.model.create<any>();
|
||||
expect(test3.sort).toBe(3);
|
||||
});
|
||||
|
||||
it('skip if sort value not empty', async () => {
|
||||
const Test = db.collection({
|
||||
name: 'tests',
|
||||
schema: [
|
||||
{ type: 'sort', name: 'sort' },
|
||||
],
|
||||
});
|
||||
await db.sync();
|
||||
const test1 = await Test.model.create<any>({ sort: 3 });
|
||||
expect(test1.sort).toBe(3);
|
||||
const test2 = await Test.model.create<any>();
|
||||
expect(test2.sort).toBe(4);
|
||||
const test3 = await Test.model.create<any>();
|
||||
expect(test3.sort).toBe(5);
|
||||
});
|
||||
|
||||
it('scopeKey', async () => {
|
||||
const Test = db.collection({
|
||||
name: 'tests',
|
||||
schema: [
|
||||
{ type: 'sort', name: 'sort', scopeKey: 'status' },
|
||||
{ type: 'string', name: 'status' },
|
||||
],
|
||||
});
|
||||
await db.sync();
|
||||
const t1 = await Test.model.create({ status: 'publish' });
|
||||
const t2 = await Test.model.create({ status: 'publish' });
|
||||
const t3 = await Test.model.create({ status: 'draft' });
|
||||
const t4 = await Test.model.create({ status: 'draft' });
|
||||
expect(t1.get('sort')).toBe(1);
|
||||
expect(t2.get('sort')).toBe(2);
|
||||
expect(t3.get('sort')).toBe(1);
|
||||
expect(t4.get('sort')).toBe(2);
|
||||
});
|
||||
|
||||
});
|
101
packages/collections/src/__tests__/update-associations.test.ts
Normal file
101
packages/collections/src/__tests__/update-associations.test.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import { Database } from '../database';
|
||||
import { updateAssociation, updateAssociations } from '../update-associations';
|
||||
import { mockDatabase } from './';
|
||||
|
||||
describe('update associations', () => {
|
||||
let db: Database;
|
||||
|
||||
beforeEach(() => {
|
||||
db = mockDatabase();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
describe('hasMany', () => {
|
||||
it.only('model', async () => {
|
||||
const User = db.collection({
|
||||
name: 'users',
|
||||
schema: [
|
||||
{ type: 'string', name: 'name' },
|
||||
{ type: 'hasMany', name: 'posts' },
|
||||
],
|
||||
});
|
||||
const Post = db.collection({
|
||||
name: 'posts',
|
||||
schema: [
|
||||
{ type: 'string', name: 'title' },
|
||||
],
|
||||
});
|
||||
await db.sync();
|
||||
const user = await User.model.create();
|
||||
const post1 = await Post.model.create();
|
||||
const post2 = await Post.model.create<any>();
|
||||
const post3 = await Post.model.create<any>();
|
||||
const post4 = await Post.model.create<any>();
|
||||
await updateAssociations(user, {
|
||||
posts: {
|
||||
title: 'post0',
|
||||
},
|
||||
});
|
||||
await updateAssociations(user, {
|
||||
posts: post1,
|
||||
});
|
||||
await updateAssociations(user, {
|
||||
posts: post2.id,
|
||||
});
|
||||
await updateAssociations(user, {
|
||||
posts: [post3.id],
|
||||
});
|
||||
await updateAssociations(user, {
|
||||
posts: {
|
||||
id: post4.id,
|
||||
title: 'post4',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('nested', async () => {
|
||||
const User = db.collection({
|
||||
name: 'users',
|
||||
schema: [
|
||||
{ type: 'string', name: 'name' },
|
||||
{ type: 'hasMany', name: 'posts' },
|
||||
],
|
||||
});
|
||||
const Post = db.collection({
|
||||
name: 'posts',
|
||||
schema: [
|
||||
{ type: 'string', name: 'title' },
|
||||
{ type: 'belongsTo', name: 'user' },
|
||||
{ type: 'hasMany', name: 'comments' },
|
||||
],
|
||||
});
|
||||
const Comment = db.collection({
|
||||
name: 'comments',
|
||||
schema: [
|
||||
{ type: 'string', name: 'content' },
|
||||
{ type: 'belongsTo', name: 'post' },
|
||||
],
|
||||
});
|
||||
await db.sync();
|
||||
const user = await User.model.create();
|
||||
await updateAssociations(user, {
|
||||
posts: [
|
||||
{
|
||||
title: 'post1',
|
||||
// user: {
|
||||
// name: 'user1',
|
||||
// },
|
||||
comments: [
|
||||
{
|
||||
content: 'content1',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
@ -29,12 +29,15 @@ export class Collection {
|
||||
const { name, tableName } = options;
|
||||
this.model = class extends Model<any, any> {};
|
||||
const attributes = {};
|
||||
// TODO: 不能重复 model.init,如果有涉及 InitOptions 参数修改,需要另外处理。
|
||||
this.model.init(attributes, {
|
||||
..._.omit(options, ['name', 'schema']),
|
||||
sequelize: context.database.sequelize,
|
||||
modelName: name,
|
||||
tableName: tableName || name,
|
||||
});
|
||||
// schema 只针对字段,对应 Sequelize 的 Attributes
|
||||
// 其他 InitOptions 参数放在 Collection 里,通过其他方法同步给 model
|
||||
this.schema = new Schema(options.schema, {
|
||||
...context,
|
||||
collection: this,
|
||||
|
@ -4,6 +4,7 @@ import { Collection, CollectionOptions } from './collection';
|
||||
import {
|
||||
RelationField,
|
||||
StringField,
|
||||
HasOneField,
|
||||
HasManyField,
|
||||
BelongsToField,
|
||||
BelongsToManyField,
|
||||
@ -21,6 +22,8 @@ export type DatabaseOptions = Options | Sequelize;
|
||||
export class Database extends EventEmitter {
|
||||
sequelize: Sequelize;
|
||||
schemaTypes = new Map();
|
||||
models = new Map();
|
||||
repositories = new Map();
|
||||
collections: Map<string, Collection>;
|
||||
pendingFields = new Map<string, RelationField[]>();
|
||||
|
||||
@ -42,6 +45,7 @@ export class Database extends EventEmitter {
|
||||
string: StringField,
|
||||
json: JsonField,
|
||||
jsonb: JsonbField,
|
||||
hasOne: HasOneField,
|
||||
hasMany: HasManyField,
|
||||
belongsTo: BelongsToField,
|
||||
belongsToMany: BelongsToManyField,
|
||||
@ -72,9 +76,7 @@ export class Database extends EventEmitter {
|
||||
|
||||
removePendingField(field: RelationField) {
|
||||
const items = this.pendingFields.get(field.target) || [];
|
||||
const index = items.findIndex(
|
||||
(item) => item && item.name === field.name,
|
||||
);
|
||||
const index = items.indexOf(field);
|
||||
if (index !== -1) {
|
||||
delete items[index];
|
||||
this.pendingFields.set(field.target, items);
|
||||
@ -87,6 +89,18 @@ export class Database extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
registerModels(models: any) {
|
||||
for (const [type, schemaType] of Object.entries(models)) {
|
||||
this.models.set(type, schemaType);
|
||||
}
|
||||
}
|
||||
|
||||
registerRepositories(repositories: any) {
|
||||
for (const [type, schemaType] of Object.entries(repositories)) {
|
||||
this.repositories.set(type, schemaType);
|
||||
}
|
||||
}
|
||||
|
||||
buildSchemaField(options, context) {
|
||||
const { type } = options;
|
||||
const Field = this.schemaTypes.get(type);
|
||||
|
23
packages/collections/src/repository.ts
Normal file
23
packages/collections/src/repository.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { ModelCtor, Model } from 'sequelize';
|
||||
|
||||
export interface IRepository {
|
||||
|
||||
}
|
||||
|
||||
export class Repository implements IRepository {
|
||||
model: ModelCtor<Model>;
|
||||
|
||||
constructor(model: ModelCtor<Model>) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
findAll() {}
|
||||
|
||||
findOne() {}
|
||||
|
||||
create() {}
|
||||
|
||||
update() {}
|
||||
|
||||
destroy() {}
|
||||
}
|
@ -3,10 +3,6 @@ import { Sequelize, ModelCtor, Model, DataTypes, Utils } from 'sequelize';
|
||||
import { RelationField } from './relation-field';
|
||||
|
||||
export class BelongsToManyField extends RelationField {
|
||||
get target() {
|
||||
const { target, name } = this.options;
|
||||
return target || name;
|
||||
}
|
||||
|
||||
get through() {
|
||||
return (
|
||||
@ -48,22 +44,10 @@ export class BelongsToManyField extends RelationField {
|
||||
}
|
||||
|
||||
unbind() {
|
||||
// const { database, collection } = this.context;
|
||||
// // 如果关系字段还没建立就删除了,也同步删除待建立关联的关系字段
|
||||
// database.removePendingField(this);
|
||||
// // 如果外键没有显式的创建,关系表也无反向关联字段,删除关系时,外键也删除掉
|
||||
// const tcoll = database.collections.get(this.target);
|
||||
// const foreignKey = this.options.foreignKey;
|
||||
// const field1 = collection.schema.get(foreignKey);
|
||||
// const field2 = tcoll.schema.find((field) => {
|
||||
// return field.type === 'hasMany' && field.foreignKey === foreignKey;
|
||||
// });
|
||||
// if (!field1 && !field2) {
|
||||
// collection.model.removeAttribute(foreignKey);
|
||||
// }
|
||||
// // 删掉 model 的关联字段
|
||||
// delete collection.model.associations[this.name];
|
||||
// // @ts-ignore
|
||||
// collection.model.refreshAttributes();
|
||||
const { database, collection } = this.context;
|
||||
// 如果关系字段还没建立就删除了,也同步删除待建立关联的关系字段
|
||||
database.removePendingField(this);
|
||||
// 删掉 model 的关联字段
|
||||
delete collection.model.associations[this.name];
|
||||
}
|
||||
}
|
||||
|
8
packages/collections/src/schema-fields/date-field.ts
Normal file
8
packages/collections/src/schema-fields/date-field.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import { SchemaField } from './schema-field';
|
||||
|
||||
export class DateField extends SchemaField {
|
||||
get dataType() {
|
||||
return DataTypes.DATE;
|
||||
}
|
||||
}
|
8
packages/collections/src/schema-fields/float-field.ts
Normal file
8
packages/collections/src/schema-fields/float-field.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import { SchemaField } from './schema-field';
|
||||
|
||||
export class FloatField extends SchemaField {
|
||||
get dataType() {
|
||||
return DataTypes.FLOAT;
|
||||
}
|
||||
}
|
@ -72,6 +72,7 @@ export interface HasManyFieldOptions extends HasManyOptions {
|
||||
}
|
||||
|
||||
export class HasManyField extends RelationField {
|
||||
|
||||
bind() {
|
||||
const { database, collection } = this.context;
|
||||
const Target = this.TargetModel;
|
||||
|
@ -0,0 +1,140 @@
|
||||
import { omit } from 'lodash';
|
||||
import {
|
||||
Sequelize,
|
||||
ModelCtor,
|
||||
Model,
|
||||
DataType,
|
||||
AssociationScope,
|
||||
ForeignKeyOptions,
|
||||
HasOneOptions,
|
||||
Utils,
|
||||
} from 'sequelize';
|
||||
import { RelationField } from './relation-field';
|
||||
|
||||
export interface HasOneFieldOptions extends HasOneOptions {
|
||||
/**
|
||||
* The name of the field to use as the key for the association in the source table. Defaults to the primary
|
||||
* key of the source table
|
||||
*/
|
||||
sourceKey?: string;
|
||||
|
||||
/**
|
||||
* A string or a data type to represent the identifier in the table
|
||||
*/
|
||||
keyType?: DataType;
|
||||
|
||||
scope?: AssociationScope;
|
||||
|
||||
/**
|
||||
* The alias of this model, in singular form. See also the `name` option passed to `sequelize.define`. If
|
||||
* you create multiple associations between the same tables, you should provide an alias to be able to
|
||||
* distinguish between them. If you provide an alias when creating the assocition, you should provide the
|
||||
* same alias when eager loading and when getting associated models. Defaults to the singularized name of
|
||||
* target
|
||||
*/
|
||||
as?: string | { singular: string; plural: string };
|
||||
|
||||
/**
|
||||
* The name of the foreign key in the target table or an object representing the type definition for the
|
||||
* foreign column (see `Sequelize.define` for syntax). When using an object, you can add a `name` property
|
||||
* to set the name of the column. Defaults to the name of source + primary key of source
|
||||
*/
|
||||
foreignKey?: string | ForeignKeyOptions;
|
||||
|
||||
/**
|
||||
* What happens when delete occurs.
|
||||
*
|
||||
* Cascade if this is a n:m, and set null if it is a 1:m
|
||||
*
|
||||
* @default 'SET NULL' or 'CASCADE'
|
||||
*/
|
||||
onDelete?: string;
|
||||
|
||||
/**
|
||||
* What happens when update occurs
|
||||
*
|
||||
* @default 'CASCADE'
|
||||
*/
|
||||
onUpdate?: string;
|
||||
|
||||
/**
|
||||
* Should on update and on delete constraints be enabled on the foreign key.
|
||||
*/
|
||||
constraints?: boolean;
|
||||
foreignKeyConstraint?: boolean;
|
||||
|
||||
// scope?: AssociationScope;
|
||||
|
||||
/**
|
||||
* If `false` the applicable hooks will not be called.
|
||||
* The default value depends on the context.
|
||||
*/
|
||||
hooks?: boolean;
|
||||
}
|
||||
|
||||
export class HasOneField extends RelationField {
|
||||
|
||||
get target() {
|
||||
const { target, name } = this.options;
|
||||
return target || Utils.pluralize(name);
|
||||
}
|
||||
|
||||
get foreignKey() {
|
||||
if (this.options.foreignKey) {
|
||||
return this.options.foreignKey;
|
||||
}
|
||||
const { model } = this.context.collection;
|
||||
return Utils.camelize(
|
||||
[
|
||||
model.options.name.singular,
|
||||
model.primaryKeyAttribute
|
||||
].join('_')
|
||||
);
|
||||
}
|
||||
|
||||
bind() {
|
||||
const { database, collection } = this.context;
|
||||
const Target = this.TargetModel;
|
||||
if (!Target) {
|
||||
database.addPendingField(this);
|
||||
return false;
|
||||
}
|
||||
const association = collection.model.hasOne(Target, {
|
||||
as: this.name,
|
||||
foreignKey: this.foreignKey,
|
||||
...omit(this.options, ['name', 'type', 'target']),
|
||||
});
|
||||
// 建立关系之后从 pending 列表中删除
|
||||
database.removePendingField(this);
|
||||
if (!this.options.foreignKey) {
|
||||
this.options.foreignKey = association.foreignKey;
|
||||
}
|
||||
if (!this.options.sourceKey) {
|
||||
// @ts-ignore
|
||||
this.options.sourceKey = association.sourceKey;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
unbind() {
|
||||
const { database, collection } = this.context;
|
||||
// 如果关系字段还没建立就删除了,也同步删除待建立关联的关系字段
|
||||
database.removePendingField(this);
|
||||
// 如果关系表内没有显式的创建外键字段,删除关系时,外键也删除掉
|
||||
const tcoll = database.collections.get(this.target);
|
||||
const foreignKey = this.options.foreignKey;
|
||||
const field = tcoll.schema.find((field) => {
|
||||
if (field.name === foreignKey) {
|
||||
return true;
|
||||
}
|
||||
return field.type === 'belongsTo' && field.foreignKey === foreignKey;
|
||||
});
|
||||
if (!field) {
|
||||
tcoll.model.removeAttribute(foreignKey);
|
||||
}
|
||||
// 删掉 model 的关联字段
|
||||
delete collection.model.associations[this.name];
|
||||
// @ts-ignore
|
||||
collection.model.refreshAttributes();
|
||||
}
|
||||
}
|
@ -3,5 +3,7 @@ export * from './string-field';
|
||||
export * from './relation-field'
|
||||
export * from './belongs-to-field'
|
||||
export * from './belongs-to-many-field';
|
||||
export * from './has-one-field';
|
||||
export * from './has-many-field';
|
||||
export * from './json-field';
|
||||
export * from './sort-field';
|
||||
|
8
packages/collections/src/schema-fields/integer-field.ts
Normal file
8
packages/collections/src/schema-fields/integer-field.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import { SchemaField } from './schema-field';
|
||||
|
||||
export class IntegerField extends SchemaField {
|
||||
get dataType() {
|
||||
return DataTypes.INTEGER;
|
||||
}
|
||||
}
|
@ -5,13 +5,6 @@ export class JsonField extends SchemaField {
|
||||
get dataType() {
|
||||
return DataTypes.JSON;
|
||||
}
|
||||
|
||||
toSequelize() {
|
||||
return {
|
||||
...this.options,
|
||||
type: this.dataType,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class JsonbField extends SchemaField {
|
||||
@ -22,11 +15,4 @@ export class JsonbField extends SchemaField {
|
||||
}
|
||||
return DataTypes.JSON;
|
||||
}
|
||||
|
||||
toSequelize() {
|
||||
return {
|
||||
...this.options,
|
||||
type: this.dataType,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
32
packages/collections/src/schema-fields/number-field.ts
Normal file
32
packages/collections/src/schema-fields/number-field.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import { SchemaField } from './schema-field';
|
||||
|
||||
export class IntegerField extends SchemaField {
|
||||
get dataType() {
|
||||
return DataTypes.INTEGER;
|
||||
}
|
||||
}
|
||||
|
||||
export class FloatField extends SchemaField {
|
||||
get dataType() {
|
||||
return DataTypes.FLOAT;
|
||||
}
|
||||
}
|
||||
|
||||
export class DoubleField extends SchemaField {
|
||||
get dataType() {
|
||||
return DataTypes.DOUBLE;
|
||||
}
|
||||
}
|
||||
|
||||
export class RealField extends SchemaField {
|
||||
get dataType() {
|
||||
return DataTypes.REAL;
|
||||
}
|
||||
}
|
||||
|
||||
export class DecimalField extends SchemaField {
|
||||
get dataType() {
|
||||
return DataTypes.DECIMAL;
|
||||
}
|
||||
}
|
@ -60,6 +60,10 @@ export abstract class SchemaField {
|
||||
}
|
||||
|
||||
toSequelize(): any {
|
||||
return _.omit(this.options, ['name'])
|
||||
const opts = _.omit(this.options, ['name']);
|
||||
if (this.dataType) {
|
||||
Object.assign(opts, { type: this.dataType });
|
||||
}
|
||||
return opts;
|
||||
}
|
||||
}
|
||||
|
25
packages/collections/src/schema-fields/sort-field.ts
Normal file
25
packages/collections/src/schema-fields/sort-field.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { isNumber } from 'lodash';
|
||||
import { DataTypes } from 'sequelize';
|
||||
import { SchemaField } from './schema-field';
|
||||
|
||||
export class SortField extends SchemaField {
|
||||
get dataType() {
|
||||
return DataTypes.INTEGER;
|
||||
}
|
||||
|
||||
init() {
|
||||
const { name, scopeKey } = this.options;
|
||||
const { model } = this.context.collection;
|
||||
model.beforeCreate(async (instance, options) => {
|
||||
if (isNumber(instance.get(name))) {
|
||||
return;
|
||||
}
|
||||
const where = {};
|
||||
if (scopeKey) {
|
||||
where[scopeKey] = instance.get(scopeKey);
|
||||
}
|
||||
const max = await model.max<number, any>(name, { ...options, where });
|
||||
instance.set(name, (max || 0) + 1);
|
||||
});
|
||||
}
|
||||
}
|
@ -5,11 +5,4 @@ export class StringField extends SchemaField {
|
||||
get dataType() {
|
||||
return DataTypes.STRING;
|
||||
}
|
||||
|
||||
toSequelize() {
|
||||
return {
|
||||
...this.options,
|
||||
type: this.dataType,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
8
packages/collections/src/schema-fields/text-field.ts
Normal file
8
packages/collections/src/schema-fields/text-field.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import { SchemaField } from './schema-field';
|
||||
|
||||
export class TextField extends SchemaField {
|
||||
get dataType() {
|
||||
return DataTypes.TEXT;
|
||||
}
|
||||
}
|
8
packages/collections/src/schema-fields/time-field.ts
Normal file
8
packages/collections/src/schema-fields/time-field.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import { SchemaField } from './schema-field';
|
||||
|
||||
export class TimeField extends SchemaField {
|
||||
get dataType() {
|
||||
return DataTypes.TIME;
|
||||
}
|
||||
}
|
8
packages/collections/src/schema-fields/virtual-field.ts
Normal file
8
packages/collections/src/schema-fields/virtual-field.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import { SchemaField } from './schema-field';
|
||||
|
||||
export class VirtualField extends SchemaField {
|
||||
get dataType() {
|
||||
return DataTypes.VIRTUAL;
|
||||
}
|
||||
}
|
@ -41,6 +41,7 @@ export class Schema extends EventEmitter {
|
||||
schema: this,
|
||||
model: this.context.collection.model,
|
||||
});
|
||||
// console.log('field', field);
|
||||
this.fields.set(name, field);
|
||||
this.emit('setted', field);
|
||||
} else if (Array.isArray(name)) {
|
||||
@ -59,7 +60,9 @@ export class Schema extends EventEmitter {
|
||||
delete(name: string) {
|
||||
const field = this.fields.get(name);
|
||||
const bool = this.fields.delete(name);
|
||||
this.emit('deleted', field);
|
||||
if (bool) {
|
||||
this.emit('deleted', field);
|
||||
}
|
||||
return bool;
|
||||
}
|
||||
|
||||
|
192
packages/collections/src/update-associations.ts
Normal file
192
packages/collections/src/update-associations.ts
Normal file
@ -0,0 +1,192 @@
|
||||
import {
|
||||
Sequelize,
|
||||
ModelCtor,
|
||||
Model,
|
||||
DataTypes,
|
||||
Utils,
|
||||
Association,
|
||||
} from 'sequelize';
|
||||
|
||||
function isUndefinedOrNull(value: any) {
|
||||
return typeof value === 'undefined' || value === null;
|
||||
}
|
||||
|
||||
function isStringOrNumber(value: any) {
|
||||
return typeof value === 'string' || typeof value === 'number';
|
||||
}
|
||||
|
||||
export async function updateAssociations(
|
||||
model: Model,
|
||||
values: any,
|
||||
options: any = {},
|
||||
) {
|
||||
const { transaction = await model.sequelize.transaction() } = options;
|
||||
// @ts-ignore
|
||||
for (const key of Object.keys(model.constructor.associations)) {
|
||||
// 如果 key 不存在才跳过
|
||||
if (!Object.keys(values).includes(key)) {
|
||||
continue;
|
||||
}
|
||||
await updateAssociation(model, key, values[key], {
|
||||
...options,
|
||||
transaction,
|
||||
});
|
||||
}
|
||||
if (!options.transaction) {
|
||||
await transaction.commit();
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateAssociation(
|
||||
model: Model,
|
||||
key: string,
|
||||
value: any,
|
||||
options: any = {},
|
||||
) {
|
||||
// @ts-ignore
|
||||
const association = model.constructor.associations[key] as Association;
|
||||
if (!association) {
|
||||
return false;
|
||||
}
|
||||
switch (association.associationType) {
|
||||
case 'HasOne':
|
||||
case 'BelongsTo':
|
||||
return updateSingleAssociation(model, key, value, options);
|
||||
case 'HasMany':
|
||||
case 'BelongsToMany':
|
||||
return updateMultipleAssociation(model, key, value, options);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateSingleAssociation(
|
||||
model: Model,
|
||||
key: string,
|
||||
value: any,
|
||||
options: any = {},
|
||||
) {
|
||||
// @ts-ignore
|
||||
const association = model.constructor.associations[key] as Association;
|
||||
if (!association) {
|
||||
return false;
|
||||
}
|
||||
if (!['undefined', 'string', 'number', 'object'].includes(typeof value)) {
|
||||
return false;
|
||||
}
|
||||
const { transaction = await model.sequelize.transaction() } = options;
|
||||
try {
|
||||
// @ts-ignore
|
||||
const setAccessor = association.accessors.set;
|
||||
if (isUndefinedOrNull(value)) {
|
||||
return await model[setAccessor](null, { transaction });
|
||||
}
|
||||
if (isStringOrNumber(value)) {
|
||||
return await model[setAccessor](value, { transaction });
|
||||
}
|
||||
// @ts-ignore
|
||||
const createAccessor = association.accessors.create;
|
||||
let key: string;
|
||||
let M: ModelCtor<Model>;
|
||||
if (association.associationType === 'BelongsTo') {
|
||||
// @ts-ignore
|
||||
key = association.targetKey;
|
||||
M = association.target;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
key = association.sourceKey;
|
||||
M = association.source;
|
||||
}
|
||||
if (isStringOrNumber(value)) {
|
||||
let instance: any = await M.findOne({
|
||||
where: {
|
||||
[key]: value[key],
|
||||
},
|
||||
transaction,
|
||||
});
|
||||
if (!instance) {
|
||||
instance = await M.create(value, { transaction });
|
||||
}
|
||||
await model[setAccessor](value[key]);
|
||||
await updateAssociations(instance, value, { transaction, ...options });
|
||||
} else {
|
||||
const instance = await model[createAccessor](value, { transaction });
|
||||
await updateAssociations(instance, value, { transaction, ...options });
|
||||
}
|
||||
if (!options.transaction) {
|
||||
await transaction.commit();
|
||||
}
|
||||
} catch (error) {
|
||||
if (!options.transaction) {
|
||||
await transaction.rollback();
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateMultipleAssociation(
|
||||
model: Model,
|
||||
key: string,
|
||||
value: any,
|
||||
options: any = {},
|
||||
) {
|
||||
// @ts-ignore
|
||||
const association = model.constructor.associations[key] as Association;
|
||||
if (!association) {
|
||||
return false;
|
||||
}
|
||||
if (!['undefined', 'string', 'number', 'object'].includes(typeof value)) {
|
||||
return false;
|
||||
}
|
||||
const { transaction = await model.sequelize.transaction() } = options;
|
||||
try {
|
||||
// @ts-ignore
|
||||
const setAccessor = association.accessors.set;
|
||||
// @ts-ignore
|
||||
const createAccessor = association.accessors.create;
|
||||
if (isUndefinedOrNull(value)) {
|
||||
return await model[setAccessor](null, { transaction });
|
||||
}
|
||||
if (isStringOrNumber(value)) {
|
||||
return await model[setAccessor](value, { transaction });
|
||||
}
|
||||
if (!Array.isArray(value)) {
|
||||
value = [value];
|
||||
}
|
||||
const list1 = []; // to be setted
|
||||
const list2 = []; // to be added
|
||||
for (const item of value) {
|
||||
if (isUndefinedOrNull(item)) {
|
||||
continue;
|
||||
}
|
||||
if (isStringOrNumber(item)) {
|
||||
list1.push(item);
|
||||
} else if (item instanceof Model) {
|
||||
list1.push(item);
|
||||
} else if (item.sequelize) {
|
||||
list1.push(item);
|
||||
} else if (typeof item === 'object') {
|
||||
list2.push(item);
|
||||
}
|
||||
}
|
||||
console.log('updateMultipleAssociation', list1, list2);
|
||||
await model[setAccessor](list1, { transaction });
|
||||
for (const item of list2) {
|
||||
const pk = association.target.primaryKeyAttribute;
|
||||
if (isUndefinedOrNull(item[pk])) {
|
||||
const instance = await model[createAccessor](item, { transaction });
|
||||
await updateAssociations(instance, item, { transaction, ...options });
|
||||
} else {
|
||||
const instance = await association.target.findByPk(item[pk], { transaction });
|
||||
// @ts-ignore
|
||||
const addAccessor = association.accessors.add;
|
||||
await model[addAccessor](item[pk], { transaction });
|
||||
await updateAssociations(instance, item, { transaction, ...options });
|
||||
}
|
||||
}
|
||||
if (!options.transaction) {
|
||||
await transaction.commit();
|
||||
}
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
18
packages/collections/src/utils.ts
Normal file
18
packages/collections/src/utils.ts
Normal file
@ -0,0 +1,18 @@
|
||||
export default {
|
||||
fiter: {
|
||||
and: [
|
||||
{ a: 'a' },
|
||||
{ b: 'b' },
|
||||
{ c: 'c' },
|
||||
{ 'assoc.a': 'abc1' },
|
||||
{ 'assoc.b': 'abc2' },
|
||||
{ 'assoc.c': 'abc3' },
|
||||
{
|
||||
and: [
|
||||
{ 'assoc.a': 'abc1' },
|
||||
{ 'assoc.b': 'abc2' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
@ -1,13 +1,16 @@
|
||||
import path from 'path';
|
||||
import Application from '@nocobase/server';
|
||||
import { IPlugin } from '@nocobase/server';
|
||||
import { afterCreate, afterUpdate, afterDestroy } from './hooks';
|
||||
|
||||
export default async function (this: Application) {
|
||||
const { database } = this;
|
||||
database.import({
|
||||
directory: path.resolve(__dirname, 'collections'),
|
||||
});
|
||||
database.on('afterCreate', afterCreate);
|
||||
database.on('afterUpdate', afterUpdate);
|
||||
database.on('afterDestroy', afterDestroy);
|
||||
}
|
||||
export default {
|
||||
name: 'action-logs',
|
||||
async load() {
|
||||
const database = this.app.db;
|
||||
database.import({
|
||||
directory: path.resolve(__dirname, 'collections'),
|
||||
});
|
||||
database.on('afterCreate', afterCreate);
|
||||
database.on('afterUpdate', afterUpdate);
|
||||
database.on('afterDestroy', afterDestroy);
|
||||
}
|
||||
} as IPlugin;
|
||||
|
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
import Database, { registerModels } from '@nocobase/database';
|
||||
import Resourcer from '@nocobase/resourcer';
|
||||
import path from 'path';
|
||||
|
@ -1,19 +1,22 @@
|
||||
import path from 'path';
|
||||
import Database, { registerModels } from '@nocobase/database';
|
||||
import { registerModels } from '@nocobase/database';
|
||||
import { ChinaRegion } from './models/china-region';
|
||||
import Application from '@nocobase/server';
|
||||
import { Plugin } from '@nocobase/server';
|
||||
|
||||
registerModels({ ChinaRegion });
|
||||
|
||||
export default async function (this: Application, options = {}) {
|
||||
const { db } = this;
|
||||
|
||||
db.import({
|
||||
directory: path.resolve(__dirname, 'collections'),
|
||||
});
|
||||
|
||||
this.on('db.init', async () => {
|
||||
const M = db.getModel('china_regions');
|
||||
await M.importData();
|
||||
});
|
||||
}
|
||||
export default {
|
||||
name: 'china-region',
|
||||
async load(this: Plugin) {
|
||||
const db = this.app.db;
|
||||
|
||||
db.import({
|
||||
directory: path.resolve(__dirname, 'collections'),
|
||||
});
|
||||
|
||||
this.app.on('db.init', async () => {
|
||||
const M = db.getModel('china_regions');
|
||||
await M.importData();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -1,123 +1,132 @@
|
||||
import path from 'path';
|
||||
import { Application } from '@nocobase/server';
|
||||
import { Plugin } from '@nocobase/server';
|
||||
import { registerModels, Table, uid } from '@nocobase/database';
|
||||
import * as models from './models';
|
||||
import { createOrUpdate, findAll } from './actions';
|
||||
import { create } from './actions/fields';
|
||||
|
||||
export default async function (this: Application, options = {}) {
|
||||
const database = this.db;
|
||||
export default {
|
||||
name: 'collections',
|
||||
async load(this: Plugin) {
|
||||
const database = this.app.db;
|
||||
|
||||
registerModels(models);
|
||||
registerModels(models);
|
||||
|
||||
database.import({
|
||||
directory: path.resolve(__dirname, 'collections'),
|
||||
});
|
||||
|
||||
this.on('afterLoadPlugins', async () => {
|
||||
await database.getModel('collections').load();
|
||||
});
|
||||
|
||||
this.on('db.init', async () => {
|
||||
const userTable = database.getTable('users');
|
||||
const config = userTable.getOptions();
|
||||
const Collection = database.getModel('collections');
|
||||
const collection = await Collection.create(config);
|
||||
await collection.updateAssociations({
|
||||
generalFields: config.fields.filter((field) => field.state !== 0),
|
||||
systemFields: config.fields.filter((field) => field.state === 0),
|
||||
database.import({
|
||||
directory: path.resolve(__dirname, 'collections'),
|
||||
});
|
||||
await collection.migrate();
|
||||
});
|
||||
|
||||
const [Collection, Field] = database.getModels(['collections', 'fields']);
|
||||
this.app.on('beforeStart', async () => {
|
||||
await database.getModel('collections').load();
|
||||
});
|
||||
|
||||
database.on('fields.beforeCreate', async (model) => {
|
||||
if (!model.get('name')) {
|
||||
model.set('name', model.get('key'));
|
||||
}
|
||||
if (!model.get('collection_name') && model.get('parentKey')) {
|
||||
const field = await Field.findByPk(model.get('parentKey'));
|
||||
if (field) {
|
||||
const { target } = field.get('options') || {};
|
||||
if (target) {
|
||||
model.set('collection_name', target);
|
||||
this.app.on('db.init', async () => {
|
||||
const userTable = database.getTable('users');
|
||||
const config = userTable.getOptions();
|
||||
const Collection = database.getModel('collections');
|
||||
const collection = await Collection.create(config);
|
||||
await collection.updateAssociations({
|
||||
generalFields: config.fields.filter((field) => field.state !== 0),
|
||||
systemFields: config.fields.filter((field) => field.state === 0),
|
||||
});
|
||||
await collection.migrate();
|
||||
});
|
||||
|
||||
const [Collection, Field] = database.getModels(['collections', 'fields']);
|
||||
|
||||
database.on('fields.beforeCreate', async (model) => {
|
||||
if (!model.get('name')) {
|
||||
model.set('name', model.get('key'));
|
||||
}
|
||||
if (!model.get('collection_name') && model.get('parentKey')) {
|
||||
const field = await Field.findByPk(model.get('parentKey'));
|
||||
if (field) {
|
||||
const { target } = field.get('options') || {};
|
||||
if (target) {
|
||||
model.set('collection_name', target);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
database.on('fields.beforeUpdate', async (model) => {
|
||||
console.log('beforeUpdate', model.key);
|
||||
if (!model.get('collection_name') && model.get('parentKey')) {
|
||||
const field = await Field.findByPk(model.get('parentKey'));
|
||||
if (field) {
|
||||
const { target } = field.get('options') || {};
|
||||
if (target) {
|
||||
model.set('collection_name', target);
|
||||
database.on('fields.beforeUpdate', async (model) => {
|
||||
console.log('beforeUpdate', model.key);
|
||||
if (!model.get('collection_name') && model.get('parentKey')) {
|
||||
const field = await Field.findByPk(model.get('parentKey'));
|
||||
if (field) {
|
||||
const { target } = field.get('options') || {};
|
||||
if (target) {
|
||||
model.set('collection_name', target);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
database.on('fields.afterCreate', async (model) => {
|
||||
console.log('afterCreate', model.key, model.get('collection_name'));
|
||||
if (model.get('interface') !== 'subTable') {
|
||||
return;
|
||||
}
|
||||
const { target } = model.get('options') || {};
|
||||
// const uiSchemaKey = model.get('ui_schema_key');
|
||||
// console.log({ uiSchemaKey })
|
||||
try {
|
||||
let collection = await Collection.findOne({
|
||||
where: {
|
||||
name: target,
|
||||
},
|
||||
});
|
||||
if (!collection) {
|
||||
collection = await Collection.create({
|
||||
name: target,
|
||||
// ui_schema_key: uiSchemaKey,
|
||||
});
|
||||
database.on('fields.afterCreate', async (model) => {
|
||||
console.log('afterCreate', model.key, model.get('collection_name'));
|
||||
if (model.get('interface') !== 'subTable') {
|
||||
return;
|
||||
}
|
||||
// if (model.get('ui_schema_key')) {
|
||||
// collection.set('ui_schema_key', model.get('ui_schema_key'));
|
||||
// await collection.save({ hooks: false });
|
||||
// }
|
||||
await collection.migrate();
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
database.on('fields.afterUpdate', async (model) => {
|
||||
console.log('afterUpdate');
|
||||
if (model.get('interface') !== 'subTable') {
|
||||
return;
|
||||
}
|
||||
const { target } = model.get('options') || {};
|
||||
try {
|
||||
let collection = await Collection.findOne({
|
||||
where: {
|
||||
name: target,
|
||||
},
|
||||
});
|
||||
if (!collection) {
|
||||
collection = await Collection.create({
|
||||
name: target,
|
||||
const { target } = model.get('options') || {};
|
||||
// const uiSchemaKey = model.get('ui_schema_key');
|
||||
// console.log({ uiSchemaKey })
|
||||
try {
|
||||
let collection = await Collection.findOne({
|
||||
where: {
|
||||
name: target,
|
||||
},
|
||||
});
|
||||
if (!collection) {
|
||||
collection = await Collection.create({
|
||||
name: target,
|
||||
// ui_schema_key: uiSchemaKey,
|
||||
});
|
||||
}
|
||||
// if (model.get('ui_schema_key')) {
|
||||
// collection.set('ui_schema_key', model.get('ui_schema_key'));
|
||||
// await collection.save({ hooks: false });
|
||||
// }
|
||||
await collection.migrate();
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
// if (model.get('ui_schema_key')) {
|
||||
// collection.set('ui_schema_key', model.get('ui_schema_key'));
|
||||
// await collection.save({ hooks: false });
|
||||
// }
|
||||
await collection.migrate();
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.resourcer.registerActionHandler('collections.fields:create', create);
|
||||
this.resourcer.registerActionHandler('collections:findAll', findAll);
|
||||
this.resourcer.registerActionHandler('collections:createOrUpdate', createOrUpdate);
|
||||
}
|
||||
database.on('fields.afterUpdate', async (model) => {
|
||||
console.log('afterUpdate');
|
||||
if (model.get('interface') !== 'subTable') {
|
||||
return;
|
||||
}
|
||||
const { target } = model.get('options') || {};
|
||||
try {
|
||||
let collection = await Collection.findOne({
|
||||
where: {
|
||||
name: target,
|
||||
},
|
||||
});
|
||||
if (!collection) {
|
||||
collection = await Collection.create({
|
||||
name: target,
|
||||
});
|
||||
}
|
||||
// if (model.get('ui_schema_key')) {
|
||||
// collection.set('ui_schema_key', model.get('ui_schema_key'));
|
||||
// await collection.save({ hooks: false });
|
||||
// }
|
||||
await collection.migrate();
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
this.app.resourcer.registerActionHandler(
|
||||
'collections.fields:create',
|
||||
create,
|
||||
);
|
||||
this.app.resourcer.registerActionHandler('collections:findAll', findAll);
|
||||
this.app.resourcer.registerActionHandler(
|
||||
'collections:createOrUpdate',
|
||||
createOrUpdate,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
@ -1,28 +1,29 @@
|
||||
import Resourcer from '@nocobase/resourcer';
|
||||
import { PluginOptions } from '@nocobase/server';
|
||||
import _export from './actions/export';
|
||||
|
||||
export const ACTION_NAME_EXPORT = 'export';
|
||||
|
||||
export default async function (options = {}) {
|
||||
const resourcer: Resourcer = this.resourcer;
|
||||
|
||||
resourcer.registerActionHandler(ACTION_NAME_EXPORT, _export);
|
||||
|
||||
// // TODO(temp): 继承 list 权限的临时写法
|
||||
// resourcer.use(async (ctx, next) => {
|
||||
// if (ctx.action.params.actionName === ACTION_NAME_EXPORT) {
|
||||
// ctx.action.mergeParams({
|
||||
// actionName: 'list'
|
||||
// });
|
||||
|
||||
// console.log('action name in export has been rewritten to:', ctx.action.params.actionName);
|
||||
|
||||
// const permissionPlugin = ctx.app.getPluginInstance('@nocobase/plugin-permissions');
|
||||
// if (permissionPlugin) {
|
||||
// return permissionPlugin.middleware(ctx, next);
|
||||
// }
|
||||
// }
|
||||
|
||||
// await next();
|
||||
// });
|
||||
}
|
||||
export default {
|
||||
name: 'export',
|
||||
async load() {
|
||||
const resourcer = this.app.resourcer;
|
||||
resourcer.registerActionHandler(ACTION_NAME_EXPORT, _export);
|
||||
// // TODO(temp): 继承 list 权限的临时写法
|
||||
// resourcer.use(async (ctx, next) => {
|
||||
// if (ctx.action.params.actionName === ACTION_NAME_EXPORT) {
|
||||
// ctx.action.mergeParams({
|
||||
// actionName: 'list'
|
||||
// });
|
||||
|
||||
// console.log('action name in export has been rewritten to:', ctx.action.params.actionName);
|
||||
|
||||
// const permissionPlugin = ctx.app.getPluginInstance('@nocobase/plugin-permissions');
|
||||
// if (permissionPlugin) {
|
||||
// return permissionPlugin.middleware(ctx, next);
|
||||
// }
|
||||
// }
|
||||
|
||||
// await next();
|
||||
// });
|
||||
}
|
||||
} as PluginOptions;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import path from 'path';
|
||||
import Database from '@nocobase/database';
|
||||
import Resourcer from '@nocobase/resourcer';
|
||||
import { PluginOptions, Plugin } from '@nocobase/server';
|
||||
|
||||
import {
|
||||
action as uploadAction,
|
||||
@ -10,40 +11,43 @@ import {
|
||||
middleware as localMiddleware,
|
||||
} from './storages/local';
|
||||
|
||||
export default async function () {
|
||||
const database: Database = this.database;
|
||||
const resourcer: Resourcer = this.resourcer;
|
||||
|
||||
database.import({
|
||||
directory: path.resolve(__dirname, 'collections'),
|
||||
});
|
||||
|
||||
// 暂时中间件只能通过 use 加进来
|
||||
resourcer.use(uploadMiddleware);
|
||||
resourcer.registerActionHandler('upload', uploadAction);
|
||||
localMiddleware(this);
|
||||
|
||||
const Storage = database.getModel('storages');
|
||||
|
||||
this.on('db.init', async () => {
|
||||
await Storage.create({
|
||||
title: '本地存储',
|
||||
name: `local`,
|
||||
type: 'local',
|
||||
baseUrl: process.env.LOCAL_STORAGE_BASE_URL,
|
||||
default: process.env.STORAGE_TYPE === 'local',
|
||||
export default {
|
||||
name: 'file-manager',
|
||||
async load() {
|
||||
const database: Database = this.app.db;
|
||||
const resourcer: Resourcer = this.app.resourcer;
|
||||
|
||||
database.import({
|
||||
directory: path.resolve(__dirname, 'collections'),
|
||||
});
|
||||
await Storage.create({
|
||||
name: `ali-oss`,
|
||||
type: 'ali-oss',
|
||||
baseUrl: process.env.ALI_OSS_STORAGE_BASE_URL,
|
||||
options: {
|
||||
region: process.env.ALI_OSS_REGION,
|
||||
accessKeyId: process.env.ALI_OSS_ACCESS_KEY_ID,
|
||||
accessKeySecret: process.env.ALI_OSS_ACCESS_KEY_SECRET,
|
||||
bucket: process.env.ALI_OSS_BUCKET,
|
||||
},
|
||||
default: process.env.STORAGE_TYPE === 'ali-oss',
|
||||
|
||||
// 暂时中间件只能通过 use 加进来
|
||||
resourcer.use(uploadMiddleware);
|
||||
resourcer.registerActionHandler('upload', uploadAction);
|
||||
localMiddleware(this.app);
|
||||
|
||||
const Storage = database.getModel('storages');
|
||||
|
||||
this.app.on('db.init', async () => {
|
||||
await Storage.create({
|
||||
title: '本地存储',
|
||||
name: `local`,
|
||||
type: 'local',
|
||||
baseUrl: process.env.LOCAL_STORAGE_BASE_URL,
|
||||
default: process.env.STORAGE_TYPE === 'local',
|
||||
});
|
||||
await Storage.create({
|
||||
name: `ali-oss`,
|
||||
type: 'ali-oss',
|
||||
baseUrl: process.env.ALI_OSS_STORAGE_BASE_URL,
|
||||
options: {
|
||||
region: process.env.ALI_OSS_REGION,
|
||||
accessKeyId: process.env.ALI_OSS_ACCESS_KEY_ID,
|
||||
accessKeySecret: process.env.ALI_OSS_ACCESS_KEY_SECRET,
|
||||
bucket: process.env.ALI_OSS_BUCKET,
|
||||
},
|
||||
default: process.env.STORAGE_TYPE === 'ali-oss',
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
} as PluginOptions;
|
||||
|
@ -1,12 +1,12 @@
|
||||
import path from 'path';
|
||||
import Database from '@nocobase/database';
|
||||
import Resourcer from '@nocobase/resourcer';
|
||||
import { PluginOptions } from '@nocobase/server';
|
||||
|
||||
export default async function () {
|
||||
const database: Database = this.database;
|
||||
const resourcer: Resourcer = this.resourcer;
|
||||
|
||||
database.import({
|
||||
directory: path.resolve(__dirname, 'collections'),
|
||||
});
|
||||
}
|
||||
export default {
|
||||
name: 'permissions',
|
||||
async load() {
|
||||
const database = this.app.db;
|
||||
database.import({
|
||||
directory: path.resolve(__dirname, 'collections'),
|
||||
});
|
||||
}
|
||||
} as PluginOptions;
|
||||
|
@ -1,42 +1,45 @@
|
||||
import path from 'path';
|
||||
import { Application } from '@nocobase/server';
|
||||
import { PluginOptions } from '@nocobase/server';
|
||||
|
||||
export default async function (this: Application, options = {}) {
|
||||
const database = this.database;
|
||||
const resourcer = this.resourcer;
|
||||
export default {
|
||||
name: 'system-settings',
|
||||
async load() {
|
||||
const database = this.app.db;
|
||||
const resourcer = this.app.resourcer;
|
||||
|
||||
database.import({
|
||||
directory: path.resolve(__dirname, 'collections'),
|
||||
});
|
||||
database.import({
|
||||
directory: path.resolve(__dirname, 'collections'),
|
||||
});
|
||||
|
||||
const SystemSetting = database.getModel('system_settings');
|
||||
|
||||
resourcer.use(async (ctx, next) => {
|
||||
const { actionName, resourceName, resourceKey } = ctx.action.params;
|
||||
if (resourceName === 'system_settings' && actionName === 'get') {
|
||||
let model = await SystemSetting.findOne();
|
||||
if (!model) {
|
||||
model = await SystemSetting.create();
|
||||
const SystemSetting = database.getModel('system_settings');
|
||||
|
||||
resourcer.use(async (ctx, next) => {
|
||||
const { actionName, resourceName, resourceKey } = ctx.action.params;
|
||||
if (resourceName === 'system_settings' && actionName === 'get') {
|
||||
let model = await SystemSetting.findOne();
|
||||
if (!model) {
|
||||
model = await SystemSetting.create();
|
||||
}
|
||||
ctx.action.mergeParams({
|
||||
resourceKey: model.id,
|
||||
});
|
||||
}
|
||||
ctx.action.mergeParams({
|
||||
resourceKey: model.id,
|
||||
await next();
|
||||
});
|
||||
|
||||
this.app.on('db.init', async () => {
|
||||
const setting = await SystemSetting.create({
|
||||
title: 'NocoBase',
|
||||
});
|
||||
await setting.updateAssociations({
|
||||
logo: {
|
||||
title: 'nocobase-logo',
|
||||
filename: '682e5ad037dd02a0fe4800a3e91c283b.png',
|
||||
extname: '.png',
|
||||
mimetype: 'image/png',
|
||||
url: 'https://nocobase.oss-cn-beijing.aliyuncs.com/682e5ad037dd02a0fe4800a3e91c283b.png',
|
||||
},
|
||||
});
|
||||
}
|
||||
await next();
|
||||
});
|
||||
|
||||
this.on('db.init', async () => {
|
||||
const setting = await SystemSetting.create({
|
||||
title: 'NocoBase',
|
||||
});
|
||||
await setting.updateAssociations({
|
||||
logo: {
|
||||
title: 'nocobase-logo',
|
||||
filename: '682e5ad037dd02a0fe4800a3e91c283b.png',
|
||||
extname: '.png',
|
||||
mimetype: 'image/png',
|
||||
url: 'https://nocobase.oss-cn-beijing.aliyuncs.com/682e5ad037dd02a0fe4800a3e91c283b.png',
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
} as PluginOptions;
|
||||
|
@ -1,61 +1,64 @@
|
||||
import path from 'path';
|
||||
import { Application } from '@nocobase/server';
|
||||
import { PluginOptions } from '@nocobase/server';
|
||||
import { registerModels } from '@nocobase/database';
|
||||
import * as models from './models';
|
||||
import getAccessible from './actions/getAccessible';
|
||||
import * as uiSchema from './ui-schema';
|
||||
|
||||
export default async function (this: Application, options = {}) {
|
||||
const database = this.database;
|
||||
registerModels(models);
|
||||
export default {
|
||||
name: 'ui-router',
|
||||
async load() {
|
||||
const database = this.app.db;
|
||||
registerModels(models);
|
||||
|
||||
database.import({
|
||||
directory: path.resolve(__dirname, 'collections'),
|
||||
});
|
||||
|
||||
this.resourcer.registerActionHandler('routes:getAccessible', getAccessible);
|
||||
|
||||
const Route = database.getModel('routes');
|
||||
|
||||
this.on('db.init', async () => {
|
||||
const data = [
|
||||
{
|
||||
type: 'redirect',
|
||||
from: '/',
|
||||
to: '/admin',
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
type: 'route',
|
||||
path: '/admin/:name(.+)?',
|
||||
component: 'AdminLayout',
|
||||
title: `后台`,
|
||||
uiSchema: uiSchema.menu,
|
||||
},
|
||||
{
|
||||
type: 'route',
|
||||
component: 'AuthLayout',
|
||||
children: [
|
||||
{
|
||||
type: 'route',
|
||||
path: '/login',
|
||||
component: 'RouteSchemaRenderer',
|
||||
title: `登录`,
|
||||
uiSchema: uiSchema.login,
|
||||
},
|
||||
{
|
||||
type: 'route',
|
||||
path: '/register',
|
||||
component: 'RouteSchemaRenderer',
|
||||
title: `注册`,
|
||||
uiSchema: uiSchema.register,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
for (const item of data) {
|
||||
const route = await Route.create(item);
|
||||
await route.updateAssociations(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
database.import({
|
||||
directory: path.resolve(__dirname, 'collections'),
|
||||
});
|
||||
|
||||
this.app.resourcer.registerActionHandler('routes:getAccessible', getAccessible);
|
||||
|
||||
const Route = database.getModel('routes');
|
||||
|
||||
this.app.on('db.init', async () => {
|
||||
const data = [
|
||||
{
|
||||
type: 'redirect',
|
||||
from: '/',
|
||||
to: '/admin',
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
type: 'route',
|
||||
path: '/admin/:name(.+)?',
|
||||
component: 'AdminLayout',
|
||||
title: `后台`,
|
||||
uiSchema: uiSchema.menu,
|
||||
},
|
||||
{
|
||||
type: 'route',
|
||||
component: 'AuthLayout',
|
||||
children: [
|
||||
{
|
||||
type: 'route',
|
||||
path: '/login',
|
||||
component: 'RouteSchemaRenderer',
|
||||
title: `登录`,
|
||||
uiSchema: uiSchema.login,
|
||||
},
|
||||
{
|
||||
type: 'route',
|
||||
path: '/register',
|
||||
component: 'RouteSchemaRenderer',
|
||||
title: `注册`,
|
||||
uiSchema: uiSchema.register,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
for (const item of data) {
|
||||
const route = await Route.create(item);
|
||||
await route.updateAssociations(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
} as PluginOptions;
|
||||
|
@ -1,18 +1,22 @@
|
||||
import path from 'path';
|
||||
import { Application } from '@nocobase/server';
|
||||
import { PluginOptions } from '@nocobase/server';
|
||||
import { registerModels } from '@nocobase/database';
|
||||
import * as models from './models';
|
||||
import * as actions from './actions';
|
||||
|
||||
export default async function (this: Application, options = {}) {
|
||||
const database = this.database;
|
||||
registerModels(models);
|
||||
registerModels(models);
|
||||
|
||||
database.import({
|
||||
directory: path.resolve(__dirname, 'collections'),
|
||||
});
|
||||
|
||||
for (const [name, action] of Object.entries(actions)) {
|
||||
this.resourcer.registerActionHandler(`ui_schemas:${name}`, action);
|
||||
export default {
|
||||
name: 'ui-schema',
|
||||
async load() {
|
||||
const database = this.app.db;
|
||||
|
||||
database.import({
|
||||
directory: path.resolve(__dirname, 'collections'),
|
||||
});
|
||||
|
||||
for (const [name, action] of Object.entries(actions)) {
|
||||
this.app.resourcer.registerActionHandler(`ui_schemas:${name}`, action);
|
||||
}
|
||||
}
|
||||
}
|
||||
} as PluginOptions;
|
||||
|
@ -1,51 +1,53 @@
|
||||
import path from 'path';
|
||||
import Database, { registerFields, Table } from '@nocobase/database';
|
||||
import Resourcer from '@nocobase/resourcer';
|
||||
import { registerFields, Table } from '@nocobase/database';
|
||||
import * as fields from './fields';
|
||||
import * as usersActions from './actions/users';
|
||||
import * as middlewares from './middlewares';
|
||||
import Application from '@nocobase/server';
|
||||
import { PluginOptions } from '@nocobase/server';
|
||||
|
||||
export default async function (this: Application, options = {}) {
|
||||
const database: Database = this.database;
|
||||
const resourcer: Resourcer = this.resourcer;
|
||||
export default {
|
||||
name: 'users',
|
||||
async load() {
|
||||
const database = this.app.db;
|
||||
const resourcer = this.app.resourcer;
|
||||
|
||||
registerFields(fields);
|
||||
registerFields(fields);
|
||||
|
||||
this.on('db.init', async () => {
|
||||
const User = database.getModel('users');
|
||||
await User.create({
|
||||
nickname: '超级管理员',
|
||||
email: process.env.ADMIN_EMAIL,
|
||||
password: process.env.ADMIN_PASSWORD,
|
||||
this.app.on('db.init', async () => {
|
||||
const User = database.getModel('users');
|
||||
await User.create({
|
||||
nickname: '超级管理员',
|
||||
email: process.env.ADMIN_EMAIL,
|
||||
password: process.env.ADMIN_PASSWORD,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
database.on('afterTableInit', (table: Table) => {
|
||||
let { createdBy, updatedBy } = table.getOptions();
|
||||
if (createdBy !== false) {
|
||||
table.addField({
|
||||
type: 'createdBy',
|
||||
name: typeof createdBy === 'string' ? createdBy : 'createdBy',
|
||||
target: 'users',
|
||||
});
|
||||
database.on('afterTableInit', (table: Table) => {
|
||||
let { createdBy, updatedBy } = table.getOptions();
|
||||
if (createdBy !== false) {
|
||||
table.addField({
|
||||
type: 'createdBy',
|
||||
name: typeof createdBy === 'string' ? createdBy : 'createdBy',
|
||||
target: 'users',
|
||||
});
|
||||
}
|
||||
if (updatedBy !== false) {
|
||||
table.addField({
|
||||
type: 'updatedBy',
|
||||
name: typeof updatedBy === 'string' ? updatedBy : 'updatedBy',
|
||||
target: 'users',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
database.import({
|
||||
directory: path.resolve(__dirname, 'collections'),
|
||||
});
|
||||
|
||||
for (const [key, action] of Object.entries(usersActions)) {
|
||||
resourcer.registerActionHandler(`users:${key}`, action);
|
||||
}
|
||||
if (updatedBy !== false) {
|
||||
table.addField({
|
||||
type: 'updatedBy',
|
||||
name: typeof updatedBy === 'string' ? updatedBy : 'updatedBy',
|
||||
target: 'users',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
database.import({
|
||||
directory: path.resolve(__dirname, 'collections'),
|
||||
});
|
||||
|
||||
for (const [key, action] of Object.entries(usersActions)) {
|
||||
resourcer.registerActionHandler(`users:${key}`, action);
|
||||
}
|
||||
|
||||
resourcer.use(middlewares.parseToken(options));
|
||||
}
|
||||
resourcer.use(middlewares.parseToken({}));
|
||||
},
|
||||
} as PluginOptions;
|
||||
|
@ -38,20 +38,25 @@ describe('plugin', () => {
|
||||
expect(plugin).toBeInstanceOf(Plugin);
|
||||
expect(plugin.getName()).toBe('abc');
|
||||
});
|
||||
|
||||
it('plugin name', async () => {
|
||||
const plugin = app.plugin(function abc() {}, {
|
||||
name: 'plugin-name2'
|
||||
const plugin = app.plugin({
|
||||
name: 'plugin-name2',
|
||||
async load() {},
|
||||
});
|
||||
expect(plugin).toBeInstanceOf(Plugin);
|
||||
expect(plugin.getName()).toBe('plugin-name2');
|
||||
});
|
||||
|
||||
it('plugin name', async () => {
|
||||
const plugin = app.plugin(function () {}, {
|
||||
name: 'plugin-name3'
|
||||
const plugin = app.plugin({
|
||||
name: 'plugin-name3',
|
||||
load: function () {},
|
||||
});
|
||||
expect(plugin).toBeInstanceOf(Plugin);
|
||||
expect(plugin.getName()).toBe('plugin-name3');
|
||||
});
|
||||
|
||||
it('plugin name', async () => {
|
||||
class MyPlugin extends Plugin {}
|
||||
const plugin = app.plugin(MyPlugin);
|
||||
@ -59,6 +64,15 @@ describe('plugin', () => {
|
||||
expect(plugin.getName()).toBe('MyPlugin');
|
||||
});
|
||||
|
||||
it('plugin name', async () => {
|
||||
class MyPlugin extends Plugin {}
|
||||
const plugin = app.plugin({
|
||||
plugin: MyPlugin,
|
||||
});
|
||||
expect(plugin).toBeInstanceOf(MyPlugin);
|
||||
expect(plugin.getName()).toBe('MyPlugin');
|
||||
});
|
||||
|
||||
it('plugin name', async () => {
|
||||
const plugin = app.plugin(path.resolve(__dirname, './plugins/plugin1'));
|
||||
expect(plugin).toBeInstanceOf(Plugin);
|
||||
|
@ -142,17 +142,16 @@ export class Application<
|
||||
console.log(args);
|
||||
const opts = cli.opts();
|
||||
await this.load();
|
||||
await this.emitAsync('server.beforeStart');
|
||||
await this.emitAsync('beforeStart');
|
||||
this.listen(opts.port || 3000);
|
||||
console.log(`http://localhost:${opts.port || 3000}/`);
|
||||
});
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
use<NewStateT = {}, NewContextT = {}>(
|
||||
middleware: Koa.Middleware<StateT & NewStateT, ContextT & NewContextT>,
|
||||
options?: MiddlewareOptions,
|
||||
): Application<StateT & NewStateT, ContextT & NewContextT> {
|
||||
) {
|
||||
// @ts-ignore
|
||||
return super.use(middleware);
|
||||
}
|
||||
@ -173,25 +172,34 @@ export class Application<
|
||||
return this.cli.command(nameAndArgs, opts);
|
||||
}
|
||||
|
||||
plugin(plugin: PluginType, options?: PluginOptions): Plugin {
|
||||
if (typeof plugin === 'string') {
|
||||
plugin = require(plugin).default;
|
||||
plugin(options?: PluginType | PluginOptions): Plugin {
|
||||
if (typeof options === 'string') {
|
||||
return this.plugin(require(options).default);
|
||||
}
|
||||
let instance: Plugin;
|
||||
try {
|
||||
// @ts-ignore
|
||||
const p = new plugin();
|
||||
if (p instanceof Plugin) {
|
||||
if (typeof options === 'function') {
|
||||
try {
|
||||
// @ts-ignore
|
||||
instance = new plugin({}, {
|
||||
...options,
|
||||
instance = new options({
|
||||
name: options.name,
|
||||
app: this,
|
||||
});
|
||||
if (!(instance instanceof Plugin)) {
|
||||
throw new Error('plugin must be instanceof Plugin');
|
||||
}
|
||||
} catch (err) {
|
||||
// console.log(err);
|
||||
instance = new Plugin({
|
||||
name: options.name,
|
||||
// @ts-ignore
|
||||
load: options,
|
||||
app: this,
|
||||
});
|
||||
} else {
|
||||
throw new Error('plugin must be instanceof Plugin');
|
||||
}
|
||||
} catch (err) {
|
||||
instance = new Plugin(plugin, {
|
||||
} else if (typeof options === 'object') {
|
||||
const plugin = options.plugin || Plugin;
|
||||
instance = new plugin({
|
||||
name: options.plugin ? plugin.name : undefined,
|
||||
...options,
|
||||
app: this,
|
||||
});
|
||||
|
@ -1,3 +1,4 @@
|
||||
export * from './application';
|
||||
export * as middlewares from './middlewares';
|
||||
export * from './plugin';
|
||||
export { Application as default } from './application';
|
||||
|
@ -1,47 +1,37 @@
|
||||
import { uid } from '@nocobase/database';
|
||||
import { Application } from './application';
|
||||
|
||||
export interface PluginOptions {
|
||||
app?: Application;
|
||||
name?: string;
|
||||
activate?: boolean;
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
version?: string;
|
||||
}
|
||||
import _ from 'lodash';
|
||||
|
||||
export interface IPlugin {
|
||||
install?: (this: Plugin) => void;
|
||||
load?: (this: Plugin) => void;
|
||||
}
|
||||
|
||||
export interface PluginOptions {
|
||||
name?: string;
|
||||
activate?: boolean;
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
version?: string;
|
||||
install?: (this: Plugin) => void;
|
||||
load?: (this: Plugin) => void;
|
||||
plugin?: typeof Plugin;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export type PluginFn = (this: Plugin) => void;
|
||||
|
||||
export type PluginType = string | PluginFn | typeof Plugin | IPlugin;
|
||||
export type PluginType = string | PluginFn | typeof Plugin;
|
||||
|
||||
export class Plugin implements IPlugin {
|
||||
options: PluginOptions = {};
|
||||
app: Application;
|
||||
callbacks: IPlugin = {};
|
||||
|
||||
constructor(plugin?: PluginType, options?: PluginOptions) {
|
||||
constructor(options?: PluginOptions & { app?: Application }) {
|
||||
this.app = options?.app;
|
||||
this.options = options || {};
|
||||
if (typeof plugin === 'function') {
|
||||
if (!this.options?.name && plugin.name) {
|
||||
this.options.name = plugin.name;
|
||||
}
|
||||
this.callbacks.load = plugin as any;
|
||||
} else if (
|
||||
typeof plugin === 'object' &&
|
||||
plugin.constructor === {}.constructor
|
||||
) {
|
||||
this.callbacks = plugin;
|
||||
}
|
||||
const cName = this.constructor.name;
|
||||
if (this.options && !this.options?.name && cName && cName !== 'Plugin') {
|
||||
this.options.name = cName;
|
||||
}
|
||||
this.options = options;
|
||||
this.callbacks = _.pick(options, ['load', 'activate']);
|
||||
}
|
||||
|
||||
getName() {
|
||||
|
66
yarn.lock
66
yarn.lock
@ -2381,6 +2381,17 @@
|
||||
"@types/yargs" "^15.0.0"
|
||||
chalk "^4.0.0"
|
||||
|
||||
"@jest/types@^27.1.1":
|
||||
version "27.1.1"
|
||||
resolved "https://registry.npmjs.org/@jest/types/-/types-27.1.1.tgz#77a3fc014f906c65752d12123a0134359707c0ad"
|
||||
integrity sha512-yqJPDDseb0mXgKqmNqypCsb85C22K1aY5+LUxh7syIM9n/b0AsaltxNy+o6tt29VcfGDpYEve175bm3uOhcehA==
|
||||
dependencies:
|
||||
"@types/istanbul-lib-coverage" "^2.0.0"
|
||||
"@types/istanbul-reports" "^3.0.0"
|
||||
"@types/node" "*"
|
||||
"@types/yargs" "^16.0.0"
|
||||
chalk "^4.0.0"
|
||||
|
||||
"@juggle/resize-observer@^3.3.1":
|
||||
version "3.3.1"
|
||||
resolved "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.3.1.tgz#b50a781709c81e10701004214340f25475a171a0"
|
||||
@ -3758,12 +3769,13 @@
|
||||
dependencies:
|
||||
"@types/istanbul-lib-report" "*"
|
||||
|
||||
"@types/jest@^24.0.18":
|
||||
version "24.9.1"
|
||||
resolved "https://registry.npmjs.org/@types/jest/-/jest-24.9.1.tgz#02baf9573c78f1b9974a5f36778b366aa77bd534"
|
||||
integrity sha512-Fb38HkXSVA4L8fGKEZ6le5bB8r6MRWlOCZbVuWZcmOMSCd2wCYOwN1ibj8daIoV9naq7aaOZjrLCoCMptKU/4Q==
|
||||
"@types/jest@^27.0.1":
|
||||
version "27.0.1"
|
||||
resolved "https://registry.npmjs.org/@types/jest/-/jest-27.0.1.tgz#fafcc997da0135865311bb1215ba16dba6bdf4ca"
|
||||
integrity sha512-HTLpVXHrY69556ozYkcq47TtQJXpcWAWfkoqz+ZGz2JnmZhzlRjprCIyFnetSy8gpDWwTTGBcRVv1J1I1vBrHw==
|
||||
dependencies:
|
||||
jest-diff "^24.3.0"
|
||||
jest-diff "^27.0.0"
|
||||
pretty-format "^27.0.0"
|
||||
|
||||
"@types/js-cookie@^2.2.6":
|
||||
version "2.2.7"
|
||||
@ -4103,6 +4115,13 @@
|
||||
dependencies:
|
||||
"@types/yargs-parser" "*"
|
||||
|
||||
"@types/yargs@^16.0.0":
|
||||
version "16.0.4"
|
||||
resolved "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz#26aad98dd2c2a38e421086ea9ad42b9e51642977"
|
||||
integrity sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==
|
||||
dependencies:
|
||||
"@types/yargs-parser" "*"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@^4.9.1":
|
||||
version "4.29.1"
|
||||
resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.29.1.tgz#808d206e2278e809292b5de752a91105da85860b"
|
||||
@ -4932,6 +4951,11 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0:
|
||||
dependencies:
|
||||
color-convert "^2.0.1"
|
||||
|
||||
ansi-styles@^5.0.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b"
|
||||
integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==
|
||||
|
||||
ansi-wrap@0.1.0, ansi-wrap@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf"
|
||||
@ -7666,6 +7690,11 @@ diff-sequences@^26.6.2:
|
||||
resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1"
|
||||
integrity sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==
|
||||
|
||||
diff-sequences@^27.0.6:
|
||||
version "27.0.6"
|
||||
resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.0.6.tgz#3305cb2e55a033924054695cc66019fd7f8e5723"
|
||||
integrity sha512-ag6wfpBFyNXZ0p8pcuIDS//D8H062ZQJ3fzYxjpmeKjnz8W4pekL3AI8VohmyZmsWW2PWaHgjsmqR6L13101VQ==
|
||||
|
||||
diff@^4.0.1:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
|
||||
@ -11153,7 +11182,7 @@ jest-config@^26.6.3:
|
||||
micromatch "^4.0.2"
|
||||
pretty-format "^26.6.2"
|
||||
|
||||
jest-diff@^24.3.0, jest-diff@^24.9.0:
|
||||
jest-diff@^24.9.0:
|
||||
version "24.9.0"
|
||||
resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-24.9.0.tgz#931b7d0d5778a1baf7452cb816e325e3724055da"
|
||||
integrity sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ==
|
||||
@ -11173,6 +11202,16 @@ jest-diff@^26.6.2:
|
||||
jest-get-type "^26.3.0"
|
||||
pretty-format "^26.6.2"
|
||||
|
||||
jest-diff@^27.0.0:
|
||||
version "27.2.0"
|
||||
resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-27.2.0.tgz#bda761c360f751bab1e7a2fe2fc2b0a35ce8518c"
|
||||
integrity sha512-QSO9WC6btFYWtRJ3Hac0sRrkspf7B01mGrrQEiCW6TobtViJ9RWL0EmOs/WnBsZDsI/Y2IoSHZA2x6offu0sYw==
|
||||
dependencies:
|
||||
chalk "^4.0.0"
|
||||
diff-sequences "^27.0.6"
|
||||
jest-get-type "^27.0.6"
|
||||
pretty-format "^27.2.0"
|
||||
|
||||
jest-docblock@^24.3.0:
|
||||
version "24.9.0"
|
||||
resolved "https://registry.npmjs.org/jest-docblock/-/jest-docblock-24.9.0.tgz#7970201802ba560e1c4092cc25cbedf5af5a8ce2"
|
||||
@ -11279,6 +11318,11 @@ jest-get-type@^26.3.0:
|
||||
resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0"
|
||||
integrity sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==
|
||||
|
||||
jest-get-type@^27.0.6:
|
||||
version "27.0.6"
|
||||
resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.0.6.tgz#0eb5c7f755854279ce9b68a9f1a4122f69047cfe"
|
||||
integrity sha512-XTkK5exIeUbbveehcSR8w0bhH+c0yloW/Wpl+9vZrjzztCPWrxhHwkIFpZzCt71oRBsgxmuUfxEqOYoZI2macg==
|
||||
|
||||
jest-haste-map@^24.9.0:
|
||||
version "24.9.0"
|
||||
resolved "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-24.9.0.tgz#b38a5d64274934e21fa417ae9a9fbeb77ceaac7d"
|
||||
@ -15401,6 +15445,16 @@ pretty-format@^26.6.2:
|
||||
ansi-styles "^4.0.0"
|
||||
react-is "^17.0.1"
|
||||
|
||||
pretty-format@^27.0.0, pretty-format@^27.2.0:
|
||||
version "27.2.0"
|
||||
resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-27.2.0.tgz#ee37a94ce2a79765791a8649ae374d468c18ef19"
|
||||
integrity sha512-KyJdmgBkMscLqo8A7K77omgLx5PWPiXJswtTtFV7XgVZv2+qPk6UivpXXO+5k6ZEbWIbLoKdx1pZ6ldINzbwTA==
|
||||
dependencies:
|
||||
"@jest/types" "^27.1.1"
|
||||
ansi-regex "^5.0.0"
|
||||
ansi-styles "^5.0.0"
|
||||
react-is "^17.0.1"
|
||||
|
||||
printj@~1.1.0, printj@~1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222"
|
||||
|
Loading…
Reference in New Issue
Block a user