feat: provide the underscored option for the database (#1366)
* feat: underscored options * feat: underscored using hook * feat: database underscored options * feat: underscored env * fix: collectionExistsInDb * fix: test * fix: nocobase install * fix: test * fix: belongsTo association * fix: test of underscored * chore: console.log * fix: list action test * fix: dump test * chore: snakeCase algo * fix: underscored field create * fix: underscored env * fix(acl): custom appends merge strategy (#1416) * Update index.md * fix(plugin-workflow): use promise to request (#1426) * Update index.md * Update collection.md * Update index.md * Update index.md * Update collection.md * Update field.md * Update repository.md * Update has-one-repository.md * Update has-many-repository.md * Update belongs-to-many-repository.md * Update index.md * chore: translate 'Add tab' in page header (#1424) * fix: test * fix: workflow test * fix: underscored with inherits * fix: underscored test * fix: process.env.DB_UNDERSCORED * fix: process.env.DB_UNDERSCORED === 'true' * fix: test * fix: pg test * fix: underscored table name * feat: tableName & fieldName conflict check * fix: test * fix: underscored index * fix: update field unique index * fix: sync default value * fix: collection manager create field * chore: field sync * fix: pg test * chore: test * fix: test * chore: default constraint name * chore: syncUniqueIndex * feat: field destory before check * feat: field type check * fix: test * fix: test * fix: improve * fix: should destroy when fields refer to the same field * fix: acl meta with underscored --------- Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
		
							parent
							
								
									00acbfb9b0
								
							
						
					
					
						commit
						0e0eb6432e
					
				
							
								
								
									
										94
									
								
								.github/workflows/nocobase-test.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										94
									
								
								.github/workflows/nocobase-test.yml
									
									
									
									
										vendored
									
									
								
							@ -109,3 +109,97 @@ jobs:
 | 
				
			|||||||
          DB_USER: root
 | 
					          DB_USER: root
 | 
				
			||||||
          DB_PASSWORD: password
 | 
					          DB_PASSWORD: password
 | 
				
			||||||
          DB_DATABASE: nocobase
 | 
					          DB_DATABASE: nocobase
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  sqlite-underscored-test:
 | 
				
			||||||
 | 
					    strategy:
 | 
				
			||||||
 | 
					      matrix:
 | 
				
			||||||
 | 
					        node_version: ['16']
 | 
				
			||||||
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					    container: node:${{ matrix.node_version }}
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					      - uses: actions/checkout@v2
 | 
				
			||||||
 | 
					      - name: Use Node.js ${{ matrix.node_version }}
 | 
				
			||||||
 | 
					        uses: actions/setup-node@v2
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          node-version: ${{ matrix.node_version }}
 | 
				
			||||||
 | 
					          cache: 'yarn'
 | 
				
			||||||
 | 
					      - run: yarn install
 | 
				
			||||||
 | 
					      - name: Test with Sqlite
 | 
				
			||||||
 | 
					        run: yarn test
 | 
				
			||||||
 | 
					        env:
 | 
				
			||||||
 | 
					          DB_DIALECT: sqlite
 | 
				
			||||||
 | 
					          DB_STORAGE: /tmp/db.sqlite
 | 
				
			||||||
 | 
					          DB_UNDERSCORED: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  postgres-underscored-test:
 | 
				
			||||||
 | 
					    strategy:
 | 
				
			||||||
 | 
					      matrix:
 | 
				
			||||||
 | 
					        node_version: ['16']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					    container: node:${{ matrix.node_version }}
 | 
				
			||||||
 | 
					    services:
 | 
				
			||||||
 | 
					      # Label used to access the service container
 | 
				
			||||||
 | 
					      postgres:
 | 
				
			||||||
 | 
					        # Docker Hub image
 | 
				
			||||||
 | 
					        image: postgres:10
 | 
				
			||||||
 | 
					        # Provide the password for postgres
 | 
				
			||||||
 | 
					        env:
 | 
				
			||||||
 | 
					          POSTGRES_USER: nocobase
 | 
				
			||||||
 | 
					          POSTGRES_PASSWORD: password
 | 
				
			||||||
 | 
					        # Set health checks to wait until postgres has started
 | 
				
			||||||
 | 
					        options: >-
 | 
				
			||||||
 | 
					          --health-cmd pg_isready
 | 
				
			||||||
 | 
					          --health-interval 10s
 | 
				
			||||||
 | 
					          --health-timeout 5s
 | 
				
			||||||
 | 
					          --health-retries 5
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					      - uses: actions/checkout@v2
 | 
				
			||||||
 | 
					      - name: Use Node.js ${{ matrix.node_version }}
 | 
				
			||||||
 | 
					        uses: actions/setup-node@v2
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          node-version: ${{ matrix.node_version }}
 | 
				
			||||||
 | 
					          cache: 'yarn'
 | 
				
			||||||
 | 
					      - run: yarn install
 | 
				
			||||||
 | 
					      - name: Test with postgres
 | 
				
			||||||
 | 
					        run: yarn test
 | 
				
			||||||
 | 
					        env:
 | 
				
			||||||
 | 
					          DB_DIALECT: postgres
 | 
				
			||||||
 | 
					          DB_HOST: postgres
 | 
				
			||||||
 | 
					          DB_PORT: 5432
 | 
				
			||||||
 | 
					          DB_USER: nocobase
 | 
				
			||||||
 | 
					          DB_PASSWORD: password
 | 
				
			||||||
 | 
					          DB_DATABASE: nocobase
 | 
				
			||||||
 | 
					          DB_UNDERSCORED: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  mysql-underscored-test:
 | 
				
			||||||
 | 
					    strategy:
 | 
				
			||||||
 | 
					      matrix:
 | 
				
			||||||
 | 
					        node_version: ['16']
 | 
				
			||||||
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					    container: node:${{ matrix.node_version }}
 | 
				
			||||||
 | 
					    services:
 | 
				
			||||||
 | 
					      mysql:
 | 
				
			||||||
 | 
					        image: mysql:8
 | 
				
			||||||
 | 
					        env:
 | 
				
			||||||
 | 
					          MYSQL_ROOT_PASSWORD: password
 | 
				
			||||||
 | 
					          MYSQL_DATABASE: nocobase
 | 
				
			||||||
 | 
					        options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					      - uses: actions/checkout@v2
 | 
				
			||||||
 | 
					      - name: Use Node.js ${{ matrix.node_version }}
 | 
				
			||||||
 | 
					        uses: actions/setup-node@v2
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          node-version: ${{ matrix.node_version }}
 | 
				
			||||||
 | 
					          cache: 'yarn'
 | 
				
			||||||
 | 
					      - run: yarn install
 | 
				
			||||||
 | 
					      - name: Test with MySQL
 | 
				
			||||||
 | 
					        run: yarn test
 | 
				
			||||||
 | 
					        env:
 | 
				
			||||||
 | 
					          DB_DIALECT: mysql
 | 
				
			||||||
 | 
					          DB_HOST: mysql
 | 
				
			||||||
 | 
					          DB_PORT: 3306
 | 
				
			||||||
 | 
					          DB_USER: root
 | 
				
			||||||
 | 
					          DB_PASSWORD: password
 | 
				
			||||||
 | 
					          DB_DATABASE: nocobase
 | 
				
			||||||
 | 
					          DB_UNDERSCORED: true
 | 
				
			||||||
 | 
				
			|||||||
@ -12,6 +12,7 @@ export default {
 | 
				
			|||||||
  timezone: process.env.DB_TIMEZONE,
 | 
					  timezone: process.env.DB_TIMEZONE,
 | 
				
			||||||
  tablePrefix: process.env.DB_TABLE_PREFIX,
 | 
					  tablePrefix: process.env.DB_TABLE_PREFIX,
 | 
				
			||||||
  schema: process.env.DB_SCHEMA,
 | 
					  schema: process.env.DB_SCHEMA,
 | 
				
			||||||
 | 
					  underscored: process.env.DB_UNDERSCORED === 'true',
 | 
				
			||||||
} as IDatabaseOptions;
 | 
					} as IDatabaseOptions;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function customLogger(queryString, queryObject) {
 | 
					function customLogger(queryString, queryObject) {
 | 
				
			||||||
 | 
				
			|||||||
@ -43,6 +43,6 @@ excludeSqlite()('collection', () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const profileTableInfo = await db.sequelize.getQueryInterface().describeTable(profile.model.tableName);
 | 
					    const profileTableInfo = await db.sequelize.getQueryInterface().describeTable(profile.model.tableName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    expect(profileTableInfo['userId'].type).toBe('BIGINT');
 | 
					    expect(profileTableInfo[profile.model.rawAttributes['userId'].field].type).toBe('BIGINT');
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
@ -8,9 +8,7 @@ describe('collection', () => {
 | 
				
			|||||||
  let db: Database;
 | 
					  let db: Database;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  beforeEach(async () => {
 | 
					  beforeEach(async () => {
 | 
				
			||||||
    db = mockDatabase({
 | 
					    db = mockDatabase();
 | 
				
			||||||
      logging: console.log,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await db.clean({ drop: true });
 | 
					    await db.clean({ drop: true });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
@ -240,9 +238,15 @@ describe('collection sync', () => {
 | 
				
			|||||||
    await collection.sync();
 | 
					    await collection.sync();
 | 
				
			||||||
    const tableFields = await (<any>collection.model).queryInterface.describeTable(`${db.getTablePrefix()}users`);
 | 
					    const tableFields = await (<any>collection.model).queryInterface.describeTable(`${db.getTablePrefix()}users`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (db.options.underscored) {
 | 
				
			||||||
 | 
					      expect(tableFields).toHaveProperty('first_name');
 | 
				
			||||||
 | 
					      expect(tableFields).toHaveProperty('last_name');
 | 
				
			||||||
 | 
					      expect(tableFields).toHaveProperty('age');
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
      expect(tableFields).toHaveProperty('firstName');
 | 
					      expect(tableFields).toHaveProperty('firstName');
 | 
				
			||||||
      expect(tableFields).toHaveProperty('lastName');
 | 
					      expect(tableFields).toHaveProperty('lastName');
 | 
				
			||||||
      expect(tableFields).toHaveProperty('age');
 | 
					      expect(tableFields).toHaveProperty('age');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  test('sync with association not exists', async () => {
 | 
					  test('sync with association not exists', async () => {
 | 
				
			||||||
@ -290,9 +294,15 @@ describe('collection sync', () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const model = collection.model;
 | 
					    const model = collection.model;
 | 
				
			||||||
    await collection.sync();
 | 
					    await collection.sync();
 | 
				
			||||||
 | 
					    if (db.options.underscored) {
 | 
				
			||||||
 | 
					      const tableFields = await (<any>model).queryInterface.describeTable(`${db.getTablePrefix()}posts_tags`);
 | 
				
			||||||
 | 
					      expect(tableFields['post_id']).toBeDefined();
 | 
				
			||||||
 | 
					      expect(tableFields['tag_id']).toBeDefined();
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
      const tableFields = await (<any>model).queryInterface.describeTable(`${db.getTablePrefix()}postsTags`);
 | 
					      const tableFields = await (<any>model).queryInterface.describeTable(`${db.getTablePrefix()}postsTags`);
 | 
				
			||||||
      expect(tableFields['postId']).toBeDefined();
 | 
					      expect(tableFields['postId']).toBeDefined();
 | 
				
			||||||
      expect(tableFields['tagId']).toBeDefined();
 | 
					      expect(tableFields['tagId']).toBeDefined();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  test('limit table name length', async () => {
 | 
					  test('limit table name length', async () => {
 | 
				
			||||||
 | 
				
			|||||||
@ -886,11 +886,13 @@ pgOnly()('collection inherits', () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const studentTableInfo = await db.sequelize.getQueryInterface().describeTable(student.model.tableName);
 | 
					    const studentTableInfo = await db.sequelize.getQueryInterface().describeTable(student.model.tableName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    expect(studentTableInfo.score).toBeDefined();
 | 
					    const getField = (name) => student.model.rawAttributes[name].field;
 | 
				
			||||||
    expect(studentTableInfo.name).toBeDefined();
 | 
					
 | 
				
			||||||
    expect(studentTableInfo.id).toBeDefined();
 | 
					    expect(studentTableInfo[getField('score')]).toBeDefined();
 | 
				
			||||||
    expect(studentTableInfo.createdAt).toBeDefined();
 | 
					    expect(studentTableInfo[getField('name')]).toBeDefined();
 | 
				
			||||||
    expect(studentTableInfo.updatedAt).toBeDefined();
 | 
					    expect(studentTableInfo[getField('id')]).toBeDefined();
 | 
				
			||||||
 | 
					    expect(studentTableInfo[getField('createdAt')]).toBeDefined();
 | 
				
			||||||
 | 
					    expect(studentTableInfo[getField('updatedAt')]).toBeDefined();
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  it('should get parent fields', async () => {
 | 
					  it('should get parent fields', async () => {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										207
									
								
								packages/core/database/src/__tests__/underscored-options.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										207
									
								
								packages/core/database/src/__tests__/underscored-options.test.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,207 @@
 | 
				
			|||||||
 | 
					import { Database, mockDatabase } from '@nocobase/database';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('underscored options', () => {
 | 
				
			||||||
 | 
					  let db: Database;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(async () => {
 | 
				
			||||||
 | 
					    db = mockDatabase({
 | 
				
			||||||
 | 
					      underscored: true,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await db.clean({ drop: true });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  afterEach(async () => {
 | 
				
			||||||
 | 
					    await db.close();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should set two field with same type', async () => {
 | 
				
			||||||
 | 
					    const collection = db.collection({
 | 
				
			||||||
 | 
					      name: 'test',
 | 
				
			||||||
 | 
					      fields: [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          type: 'string',
 | 
				
			||||||
 | 
					          name: 'test_field',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          type: 'string',
 | 
				
			||||||
 | 
					          name: 'testField',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await db.sync();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should not set two field with difference type but same field name', async () => {
 | 
				
			||||||
 | 
					    const collection = db.collection({
 | 
				
			||||||
 | 
					      name: 'test',
 | 
				
			||||||
 | 
					      fields: [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          type: 'string',
 | 
				
			||||||
 | 
					          name: 'test_field',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(() => {
 | 
				
			||||||
 | 
					      collection.addField('testField', { type: 'integer' });
 | 
				
			||||||
 | 
					    }).toThrowError();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(() => {
 | 
				
			||||||
 | 
					      collection.addField('test123', { type: 'integer', field: 'test_field' });
 | 
				
			||||||
 | 
					    }).toThrowError();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should create index', async () => {
 | 
				
			||||||
 | 
					    const collectionA = db.collection({
 | 
				
			||||||
 | 
					      name: 'testCollection',
 | 
				
			||||||
 | 
					      fields: [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          type: 'string',
 | 
				
			||||||
 | 
					          name: 'aField',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          type: 'string',
 | 
				
			||||||
 | 
					          name: 'bField',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      indexes: [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          type: 'UNIQUE',
 | 
				
			||||||
 | 
					          fields: ['aField', 'bField'],
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await db.sync();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should use underscored option', async () => {
 | 
				
			||||||
 | 
					    const collectionA = db.collection({
 | 
				
			||||||
 | 
					      name: 'testCollection',
 | 
				
			||||||
 | 
					      underscored: true,
 | 
				
			||||||
 | 
					      fields: [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          type: 'string',
 | 
				
			||||||
 | 
					          name: 'testField',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await db.sync();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const tableName = collectionA.model.tableName;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(tableName.includes('test_collection')).toBeTruthy();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const repository = db.getRepository('testCollection');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await repository.create({
 | 
				
			||||||
 | 
					      values: {
 | 
				
			||||||
 | 
					        testField: 'test',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const record = await repository.findOne({});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(record.get('testField')).toBe('test');
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should use database options', async () => {
 | 
				
			||||||
 | 
					    const collectionA = db.collection({
 | 
				
			||||||
 | 
					      name: 'testCollection',
 | 
				
			||||||
 | 
					      fields: [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          type: 'string',
 | 
				
			||||||
 | 
					          name: 'testField',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await db.sync();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const tableName = collectionA.model.tableName;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(tableName.includes('test_collection')).toBeTruthy();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  test('through table', async () => {
 | 
				
			||||||
 | 
					    db.collection({
 | 
				
			||||||
 | 
					      name: 'posts',
 | 
				
			||||||
 | 
					      fields: [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          type: 'string',
 | 
				
			||||||
 | 
					          name: 'name',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          type: 'belongsToMany',
 | 
				
			||||||
 | 
					          name: 'tags',
 | 
				
			||||||
 | 
					          through: 'collectionCategory',
 | 
				
			||||||
 | 
					          target: 'posts',
 | 
				
			||||||
 | 
					          sourceKey: 'name',
 | 
				
			||||||
 | 
					          foreignKey: 'postsName',
 | 
				
			||||||
 | 
					          targetKey: 'name',
 | 
				
			||||||
 | 
					          otherKey: 'tagsName',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    db.collection({
 | 
				
			||||||
 | 
					      name: 'tags',
 | 
				
			||||||
 | 
					      fields: [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          type: 'string',
 | 
				
			||||||
 | 
					          name: 'name',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          type: 'belongsToMany',
 | 
				
			||||||
 | 
					          name: 'posts',
 | 
				
			||||||
 | 
					          target: 'posts',
 | 
				
			||||||
 | 
					          through: 'collectionCategory',
 | 
				
			||||||
 | 
					          sourceKey: 'name',
 | 
				
			||||||
 | 
					          foreignKey: 'tagsName',
 | 
				
			||||||
 | 
					          targetKey: 'name',
 | 
				
			||||||
 | 
					          otherKey: 'postsName',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await db.sync();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const through = db.getCollection('collectionCategory');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(through.model.tableName.includes('collection_category')).toBeTruthy();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  test('db collectionExists', async () => {
 | 
				
			||||||
 | 
					    const collectionA = db.collection({
 | 
				
			||||||
 | 
					      name: 'testCollection',
 | 
				
			||||||
 | 
					      underscored: true,
 | 
				
			||||||
 | 
					      fields: [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          type: 'string',
 | 
				
			||||||
 | 
					          name: 'testField',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(await db.collectionExistsInDb('testCollection')).toBeFalsy();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await db.sync();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(await db.collectionExistsInDb('testCollection')).toBeTruthy();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should throw error when table names conflict', async () => {
 | 
				
			||||||
 | 
					    db.collection({
 | 
				
			||||||
 | 
					      name: 'b1_z',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(() => {
 | 
				
			||||||
 | 
					      db.collection({
 | 
				
			||||||
 | 
					        name: 'b1Z',
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }).toThrowError();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -13,7 +13,7 @@ import { Database } from './database';
 | 
				
			|||||||
import { Field, FieldOptions } from './fields';
 | 
					import { Field, FieldOptions } from './fields';
 | 
				
			||||||
import { Model } from './model';
 | 
					import { Model } from './model';
 | 
				
			||||||
import { Repository } from './repository';
 | 
					import { Repository } from './repository';
 | 
				
			||||||
import { checkIdentifier, md5 } from './utils';
 | 
					import { checkIdentifier, md5, snakeCase } from './utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type RepositoryType = typeof Repository;
 | 
					export type RepositoryType = typeof Repository;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -36,6 +36,7 @@ export interface CollectionOptions extends Omit<ModelOptions, 'name' | 'hooks'>
 | 
				
			|||||||
   * @default 'options'
 | 
					   * @default 'options'
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  magicAttribute?: string;
 | 
					  magicAttribute?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  [key: string]: any;
 | 
					  [key: string]: any;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -72,11 +73,11 @@ export class Collection<
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  constructor(options: CollectionOptions, context: CollectionContext) {
 | 
					  constructor(options: CollectionOptions, context: CollectionContext) {
 | 
				
			||||||
    super();
 | 
					    super();
 | 
				
			||||||
    this.checkOptions(options);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.context = context;
 | 
					    this.context = context;
 | 
				
			||||||
    this.options = options;
 | 
					    this.options = options;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.checkOptions(options);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.bindFieldEventListener();
 | 
					    this.bindFieldEventListener();
 | 
				
			||||||
    this.modelInit();
 | 
					    this.modelInit();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -93,15 +94,31 @@ export class Collection<
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  private checkOptions(options: CollectionOptions) {
 | 
					  private checkOptions(options: CollectionOptions) {
 | 
				
			||||||
    checkIdentifier(options.name);
 | 
					    checkIdentifier(options.name);
 | 
				
			||||||
 | 
					    this.checkTableName();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private checkTableName() {
 | 
				
			||||||
 | 
					    const tableName = this.tableName();
 | 
				
			||||||
 | 
					    for (const [k, collection] of this.db.collections) {
 | 
				
			||||||
 | 
					      if (collection.name != this.options.name && tableName === collection.tableName()) {
 | 
				
			||||||
 | 
					        throw new Error(`collection ${collection.name} and ${this.name} have same tableName "${tableName}"`);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  tableName() {
 | 
				
			||||||
 | 
					    const { name, tableName } = this.options;
 | 
				
			||||||
 | 
					    const tName = tableName || name;
 | 
				
			||||||
 | 
					    return this.db.options.underscored ? snakeCase(tName) : tName;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private sequelizeModelOptions() {
 | 
					  private sequelizeModelOptions() {
 | 
				
			||||||
    const { name, tableName } = this.options;
 | 
					    const { name } = this.options;
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      ..._.omit(this.options, ['name', 'fields', 'model', 'targetKey']),
 | 
					      ..._.omit(this.options, ['name', 'fields', 'model', 'targetKey']),
 | 
				
			||||||
      modelName: name,
 | 
					      modelName: name,
 | 
				
			||||||
      sequelize: this.context.database.sequelize,
 | 
					      sequelize: this.context.database.sequelize,
 | 
				
			||||||
      tableName: tableName || name,
 | 
					      tableName: this.tableName(),
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -112,8 +129,10 @@ export class Collection<
 | 
				
			|||||||
    if (this.model) {
 | 
					    if (this.model) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { name, model, autoGenId = true } = this.options;
 | 
					    const { name, model, autoGenId = true } = this.options;
 | 
				
			||||||
    let M: ModelStatic<Model> = Model;
 | 
					    let M: ModelStatic<Model> = Model;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (this.context.database.sequelize.isDefined(name)) {
 | 
					    if (this.context.database.sequelize.isDefined(name)) {
 | 
				
			||||||
      const m = this.context.database.sequelize.model(name);
 | 
					      const m = this.context.database.sequelize.model(name);
 | 
				
			||||||
      if ((m as any).isThrough) {
 | 
					      if ((m as any).isThrough) {
 | 
				
			||||||
@ -126,11 +145,13 @@ export class Collection<
 | 
				
			|||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (typeof model === 'string') {
 | 
					    if (typeof model === 'string') {
 | 
				
			||||||
      M = this.context.database.models.get(model) || Model;
 | 
					      M = this.context.database.models.get(model) || Model;
 | 
				
			||||||
    } else if (model) {
 | 
					    } else if (model) {
 | 
				
			||||||
      M = model;
 | 
					      M = model;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // @ts-ignore
 | 
					    // @ts-ignore
 | 
				
			||||||
    this.model = class extends M {};
 | 
					    this.model = class extends M {};
 | 
				
			||||||
    this.model.init(null, this.sequelizeModelOptions());
 | 
					    this.model.init(null, this.sequelizeModelOptions());
 | 
				
			||||||
@ -183,8 +204,31 @@ export class Collection<
 | 
				
			|||||||
    return this.setField(name, options);
 | 
					    return this.setField(name, options);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  checkFieldType(name: string, options: FieldOptions) {
 | 
				
			||||||
 | 
					    if (!this.db.options.underscored) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const fieldName = options.field || snakeCase(name);
 | 
				
			||||||
 | 
					    const field = this.findField((f) => {
 | 
				
			||||||
 | 
					      if (f.name === name) {
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (f.field) {
 | 
				
			||||||
 | 
					        return f.field === fieldName;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return snakeCase(f.name) === fieldName;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    if (!field) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (options.type !== field.type) {
 | 
				
			||||||
 | 
					      throw new Error(`fields with same column must be of the same type ${JSON.stringify(options)}`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  setField(name: string, options: FieldOptions): Field {
 | 
					  setField(name: string, options: FieldOptions): Field {
 | 
				
			||||||
    checkIdentifier(name);
 | 
					    checkIdentifier(name);
 | 
				
			||||||
 | 
					    this.checkFieldType(name, options);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { database } = this.context;
 | 
					    const { database } = this.context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -362,9 +406,13 @@ export class Collection<
 | 
				
			|||||||
    if (!index) {
 | 
					    if (!index) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // collection defined indexes
 | 
				
			||||||
    let indexes: any = this.model.options.indexes || [];
 | 
					    let indexes: any = this.model.options.indexes || [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let indexName = [];
 | 
					    let indexName = [];
 | 
				
			||||||
    let indexItem;
 | 
					    let indexItem;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (typeof index === 'string') {
 | 
					    if (typeof index === 'string') {
 | 
				
			||||||
      indexItem = {
 | 
					      indexItem = {
 | 
				
			||||||
        fields: [index],
 | 
					        fields: [index],
 | 
				
			||||||
@ -379,13 +427,17 @@ export class Collection<
 | 
				
			|||||||
      indexItem = index;
 | 
					      indexItem = index;
 | 
				
			||||||
      indexName = index.fields;
 | 
					      indexName = index.fields;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (lodash.isEqual(this.model.primaryKeyAttributes, indexName)) {
 | 
					    if (lodash.isEqual(this.model.primaryKeyAttributes, indexName)) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const name: string = this.model.primaryKeyAttributes.join(',');
 | 
					    const name: string = this.model.primaryKeyAttributes.join(',');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (name.startsWith(`${indexName.join(',')},`)) {
 | 
					    if (name.startsWith(`${indexName.join(',')},`)) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (const item of indexes) {
 | 
					    for (const item of indexes) {
 | 
				
			||||||
      if (lodash.isEqual(item.fields, indexName)) {
 | 
					      if (lodash.isEqual(item.fields, indexName)) {
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
@ -395,11 +447,13 @@ export class Collection<
 | 
				
			|||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!indexItem) {
 | 
					    if (!indexItem) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    indexes.push(indexItem);
 | 
					    indexes.push(indexItem);
 | 
				
			||||||
    this.model.options.indexes = indexes;
 | 
					
 | 
				
			||||||
    const tableName = this.model.getTableName();
 | 
					    const tableName = this.model.getTableName();
 | 
				
			||||||
    // @ts-ignore
 | 
					    // @ts-ignore
 | 
				
			||||||
    this.model._indexes = this.model.options.indexes
 | 
					    this.model._indexes = this.model.options.indexes
 | 
				
			||||||
@ -411,6 +465,7 @@ export class Collection<
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        return item;
 | 
					        return item;
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.refreshIndexes();
 | 
					    this.refreshIndexes();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -430,15 +485,17 @@ export class Collection<
 | 
				
			|||||||
  refreshIndexes() {
 | 
					  refreshIndexes() {
 | 
				
			||||||
    // @ts-ignore
 | 
					    // @ts-ignore
 | 
				
			||||||
    const indexes: any[] = this.model._indexes;
 | 
					    const indexes: any[] = this.model._indexes;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // @ts-ignore
 | 
					    // @ts-ignore
 | 
				
			||||||
    this.model._indexes = indexes.filter((item) => {
 | 
					    this.model._indexes = lodash.uniqBy(
 | 
				
			||||||
      for (const field of item.fields) {
 | 
					      indexes.map((item) => {
 | 
				
			||||||
        if (!this.model.rawAttributes[field]) {
 | 
					        if (this.options.underscored) {
 | 
				
			||||||
          return false;
 | 
					          item.fields = item.fields.map((field) => snakeCase(field));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					        return item;
 | 
				
			||||||
      return true;
 | 
					      }),
 | 
				
			||||||
    });
 | 
					      'name',
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async sync(syncOptions?: SyncOptions) {
 | 
					  async sync(syncOptions?: SyncOptions) {
 | 
				
			||||||
 | 
				
			|||||||
@ -60,6 +60,7 @@ import {
 | 
				
			|||||||
  UpdateWithAssociationsListener,
 | 
					  UpdateWithAssociationsListener,
 | 
				
			||||||
  ValidateListener
 | 
					  ValidateListener
 | 
				
			||||||
} from './types';
 | 
					} from './types';
 | 
				
			||||||
 | 
					import { snakeCase } from './utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface MergeOptions extends merge.Options {}
 | 
					export interface MergeOptions extends merge.Options {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -76,6 +77,7 @@ export interface IDatabaseOptions extends Options {
 | 
				
			|||||||
  tablePrefix?: string;
 | 
					  tablePrefix?: string;
 | 
				
			||||||
  migrator?: any;
 | 
					  migrator?: any;
 | 
				
			||||||
  usingBigIntForId?: boolean;
 | 
					  usingBigIntForId?: boolean;
 | 
				
			||||||
 | 
					  underscored?: boolean;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type DatabaseOptions = IDatabaseOptions;
 | 
					export type DatabaseOptions = IDatabaseOptions;
 | 
				
			||||||
@ -167,7 +169,6 @@ export class Database extends EventEmitter implements AsyncEmitter {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  constructor(options: DatabaseOptions) {
 | 
					  constructor(options: DatabaseOptions) {
 | 
				
			||||||
    super();
 | 
					    super();
 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.version = new DatabaseVersion(this);
 | 
					    this.version = new DatabaseVersion(this);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const opts = {
 | 
					    const opts = {
 | 
				
			||||||
@ -265,6 +266,12 @@ export class Database extends EventEmitter implements AsyncEmitter {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  initListener() {
 | 
					  initListener() {
 | 
				
			||||||
 | 
					    this.on('beforeDefine', (model, options) => {
 | 
				
			||||||
 | 
					      if (this.options.underscored) {
 | 
				
			||||||
 | 
					        options.underscored = true;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.on('afterCreate', async (instance) => {
 | 
					    this.on('afterCreate', async (instance) => {
 | 
				
			||||||
      instance?.toChangedWithAssociations?.();
 | 
					      instance?.toChangedWithAssociations?.();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
@ -294,6 +301,27 @@ export class Database extends EventEmitter implements AsyncEmitter {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.on('beforeDefineCollection', (options) => {
 | 
				
			||||||
 | 
					      if (options.underscored) {
 | 
				
			||||||
 | 
					        if (lodash.get(options, 'sortable.scopeKey')) {
 | 
				
			||||||
 | 
					          options.sortable.scopeKey = snakeCase(options.sortable.scopeKey);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (lodash.get(options, 'indexes')) {
 | 
				
			||||||
 | 
					          // change index fields to snake case
 | 
				
			||||||
 | 
					          options.indexes = options.indexes.map((index) => {
 | 
				
			||||||
 | 
					            if (index.fields) {
 | 
				
			||||||
 | 
					              index.fields = index.fields.map((field) => {
 | 
				
			||||||
 | 
					                return snakeCase(field);
 | 
				
			||||||
 | 
					              });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return index;
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  addMigration(item: MigrationItem) {
 | 
					  addMigration(item: MigrationItem) {
 | 
				
			||||||
@ -328,6 +356,10 @@ export class Database extends EventEmitter implements AsyncEmitter {
 | 
				
			|||||||
  collection<Attributes = any, CreateAttributes = Attributes>(
 | 
					  collection<Attributes = any, CreateAttributes = Attributes>(
 | 
				
			||||||
    options: CollectionOptions,
 | 
					    options: CollectionOptions,
 | 
				
			||||||
  ): Collection<Attributes, CreateAttributes> {
 | 
					  ): Collection<Attributes, CreateAttributes> {
 | 
				
			||||||
 | 
					    if (this.options.underscored) {
 | 
				
			||||||
 | 
					      options.underscored = true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.emit('beforeDefineCollection', options);
 | 
					    this.emit('beforeDefineCollection', options);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const hasValidInheritsOptions = (() => {
 | 
					    const hasValidInheritsOptions = (() => {
 | 
				
			||||||
@ -477,6 +509,10 @@ export class Database extends EventEmitter implements AsyncEmitter {
 | 
				
			|||||||
      throw Error(`unsupported field type ${type}`);
 | 
					      throw Error(`unsupported field type ${type}`);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (options.field && this.options.underscored) {
 | 
				
			||||||
 | 
					      options.field = snakeCase(options.field);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return new Field(options, context);
 | 
					    return new Field(options, context);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -525,11 +561,17 @@ export class Database extends EventEmitter implements AsyncEmitter {
 | 
				
			|||||||
    await this.sequelize.getQueryInterface().dropAllTables(others);
 | 
					    await this.sequelize.getQueryInterface().dropAllTables(others);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async collectionExistsInDb(name, options?: Transactionable) {
 | 
					  async collectionExistsInDb(name: string, options?: Transactionable) {
 | 
				
			||||||
 | 
					    const collection = this.getCollection(name);
 | 
				
			||||||
 | 
					    if (!collection) {
 | 
				
			||||||
 | 
					      return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const tables = await this.sequelize.getQueryInterface().showAllTables({
 | 
					    const tables = await this.sequelize.getQueryInterface().showAllTables({
 | 
				
			||||||
      transaction: options?.transaction,
 | 
					      transaction: options?.transaction,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    return !!tables.find((table) => table === `${this.getTablePrefix()}${name}`);
 | 
					
 | 
				
			||||||
 | 
					    return tables.includes(this.getCollection(name).model.tableName);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public isSqliteMemory() {
 | 
					  public isSqliteMemory() {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,5 @@
 | 
				
			|||||||
import { omit } from 'lodash';
 | 
					import lodash, { omit } from 'lodash';
 | 
				
			||||||
import { BelongsTo, BelongsToOptions as SequelizeBelongsToOptions, Utils } from 'sequelize';
 | 
					import { BelongsToOptions as SequelizeBelongsToOptions, Utils } from 'sequelize';
 | 
				
			||||||
import { Reference } from '../features/ReferencesMap';
 | 
					import { Reference } from '../features/ReferencesMap';
 | 
				
			||||||
import { checkIdentifier } from '../utils';
 | 
					import { checkIdentifier } from '../utils';
 | 
				
			||||||
import { BaseRelationFieldOptions, RelationField } from './relation-field';
 | 
					import { BaseRelationFieldOptions, RelationField } from './relation-field';
 | 
				
			||||||
 | 
				
			|||||||
@ -57,7 +57,6 @@ export class BelongsToManyField extends RelationField {
 | 
				
			|||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      Through = database.collection({
 | 
					      Through = database.collection({
 | 
				
			||||||
        name: through,
 | 
					        name: through,
 | 
				
			||||||
        // timestamps: false,
 | 
					 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      Object.defineProperty(Through.model, 'isThrough', { value: true });
 | 
					      Object.defineProperty(Through.model, 'isThrough', { value: true });
 | 
				
			||||||
 | 
				
			|||||||
@ -12,6 +12,7 @@ import { Collection } from '../collection';
 | 
				
			|||||||
import { Database } from '../database';
 | 
					import { Database } from '../database';
 | 
				
			||||||
import { InheritedCollection } from '../inherited-collection';
 | 
					import { InheritedCollection } from '../inherited-collection';
 | 
				
			||||||
import { ModelEventTypes } from '../types';
 | 
					import { ModelEventTypes } from '../types';
 | 
				
			||||||
 | 
					import { snakeCase } from '../utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface FieldContext {
 | 
					export interface FieldContext {
 | 
				
			||||||
  database: Database;
 | 
					  database: Database;
 | 
				
			||||||
@ -89,6 +90,18 @@ export abstract class Field {
 | 
				
			|||||||
    return this.collection.removeField(this.name);
 | 
					    return this.collection.removeField(this.name);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  columnName() {
 | 
				
			||||||
 | 
					    if (this.options.field) {
 | 
				
			||||||
 | 
					      return this.options.field;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (this.database.options.underscored) {
 | 
				
			||||||
 | 
					      return snakeCase(this.name);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return this.name;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async removeFromDb(options?: QueryInterfaceOptions) {
 | 
					  async removeFromDb(options?: QueryInterfaceOptions) {
 | 
				
			||||||
    const attribute = this.collection.model.rawAttributes[this.name];
 | 
					    const attribute = this.collection.model.rawAttributes[this.name];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -113,7 +126,12 @@ export abstract class Field {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    if (this.collection.model.options.timestamps !== false) {
 | 
					    if (this.collection.model.options.timestamps !== false) {
 | 
				
			||||||
      // timestamps 相关字段不删除
 | 
					      // timestamps 相关字段不删除
 | 
				
			||||||
      if (['createdAt', 'updatedAt', 'deletedAt'].includes(this.name)) {
 | 
					      let timestampsFields = ['createdAt', 'updatedAt', 'deletedAt'];
 | 
				
			||||||
 | 
					      if (this.database.options.underscored) {
 | 
				
			||||||
 | 
					        timestampsFields = timestampsFields.map((field) => snakeCase(field));
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (timestampsFields.includes(this.columnName())) {
 | 
				
			||||||
 | 
					        this.collection.fields.delete(this.name);
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -132,19 +150,28 @@ export abstract class Field {
 | 
				
			|||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (this.options.field && this.name !== this.options.field) {
 | 
					
 | 
				
			||||||
      // field 指向的是真实的字段名,如果与 name 不一样,说明字段只是引用
 | 
					    // if (this.options.field && this.name !== this.options.field) {
 | 
				
			||||||
      this.remove();
 | 
					    //   // field 指向的是真实的字段名,如果与 name 不一样,说明字段只是引用
 | 
				
			||||||
      return;
 | 
					    //   this.remove();
 | 
				
			||||||
    }
 | 
					    //   return;
 | 
				
			||||||
 | 
					    // }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const columnReferencesCount = _.filter(
 | 
				
			||||||
 | 
					      this.collection.model.rawAttributes,
 | 
				
			||||||
 | 
					      (attr) => attr.field == this.columnName(),
 | 
				
			||||||
 | 
					    ).length;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (
 | 
					    if (
 | 
				
			||||||
      await this.existsInDb({
 | 
					      (await this.existsInDb({
 | 
				
			||||||
        transaction: options?.transaction,
 | 
					        transaction: options?.transaction,
 | 
				
			||||||
      })
 | 
					      })) &&
 | 
				
			||||||
 | 
					      columnReferencesCount == 1
 | 
				
			||||||
    ) {
 | 
					    ) {
 | 
				
			||||||
      const queryInterface = this.database.sequelize.getQueryInterface();
 | 
					      const queryInterface = this.database.sequelize.getQueryInterface();
 | 
				
			||||||
      await queryInterface.removeColumn(this.collection.model.tableName, this.name, options);
 | 
					      await queryInterface.removeColumn(this.collection.model.tableName, this.columnName(), options);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.remove();
 | 
					    this.remove();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -154,18 +181,20 @@ export abstract class Field {
 | 
				
			|||||||
    };
 | 
					    };
 | 
				
			||||||
    let sql;
 | 
					    let sql;
 | 
				
			||||||
    if (this.database.sequelize.getDialect() === 'sqlite') {
 | 
					    if (this.database.sequelize.getDialect() === 'sqlite') {
 | 
				
			||||||
      sql = `SELECT * from pragma_table_info('${this.collection.model.tableName}') WHERE name = '${this.name}'`;
 | 
					      sql = `SELECT * from pragma_table_info('${this.collection.model.tableName}') WHERE name = '${this.columnName()}'`;
 | 
				
			||||||
    } else if (this.database.inDialect('mysql')) {
 | 
					    } else if (this.database.inDialect('mysql')) {
 | 
				
			||||||
      sql = `
 | 
					      sql = `
 | 
				
			||||||
        select column_name
 | 
					        select column_name
 | 
				
			||||||
        from INFORMATION_SCHEMA.COLUMNS
 | 
					        from INFORMATION_SCHEMA.COLUMNS
 | 
				
			||||||
        where TABLE_SCHEMA='${this.database.options.database}' AND TABLE_NAME='${this.collection.model.tableName}' AND column_name='${this.name}'
 | 
					        where TABLE_SCHEMA='${this.database.options.database}' AND TABLE_NAME='${
 | 
				
			||||||
 | 
					        this.collection.model.tableName
 | 
				
			||||||
 | 
					      }' AND column_name='${this.columnName()}'
 | 
				
			||||||
      `;
 | 
					      `;
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      sql = `
 | 
					      sql = `
 | 
				
			||||||
        select column_name
 | 
					        select column_name
 | 
				
			||||||
        from INFORMATION_SCHEMA.COLUMNS
 | 
					        from INFORMATION_SCHEMA.COLUMNS
 | 
				
			||||||
        where TABLE_NAME='${this.collection.model.tableName}' AND column_name='${this.name}'
 | 
					        where TABLE_NAME='${this.collection.model.tableName}' AND column_name='${this.columnName()}'
 | 
				
			||||||
      `;
 | 
					      `;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    const [rows] = await this.database.sequelize.query(sql, opts);
 | 
					    const [rows] = await this.database.sequelize.query(sql, opts);
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
import { omit } from 'lodash';
 | 
					import lodash, { omit } from 'lodash';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  AssociationScope,
 | 
					  AssociationScope,
 | 
				
			||||||
  DataType,
 | 
					  DataType,
 | 
				
			||||||
@ -8,7 +8,7 @@ import {
 | 
				
			|||||||
  Utils,
 | 
					  Utils,
 | 
				
			||||||
} from 'sequelize';
 | 
					} from 'sequelize';
 | 
				
			||||||
import { Collection } from '../collection';
 | 
					import { Collection } from '../collection';
 | 
				
			||||||
import { checkIdentifier } from '../utils';
 | 
					import { checkIdentifier, snakeCase } from '../utils';
 | 
				
			||||||
import { BaseRelationFieldOptions, RelationField } from './relation-field';
 | 
					import { BaseRelationFieldOptions, RelationField } from './relation-field';
 | 
				
			||||||
import { Reference } from '../features/ReferencesMap';
 | 
					import { Reference } from '../features/ReferencesMap';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -84,11 +84,15 @@ export class HasOneField extends RelationField {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  get foreignKey() {
 | 
					  get foreignKey() {
 | 
				
			||||||
 | 
					    const foreignKey = (() => {
 | 
				
			||||||
      if (this.options.foreignKey) {
 | 
					      if (this.options.foreignKey) {
 | 
				
			||||||
        return this.options.foreignKey;
 | 
					        return this.options.foreignKey;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      const { model } = this.context.collection;
 | 
					      const { model } = this.context.collection;
 | 
				
			||||||
      return Utils.camelize([model.options.name.singular, model.primaryKeyAttribute].join('_'));
 | 
					      return Utils.camelize([model.options.name.singular, model.primaryKeyAttribute].join('_'));
 | 
				
			||||||
 | 
					    })();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return foreignKey;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  reference(association): Reference {
 | 
					  reference(association): Reference {
 | 
				
			||||||
 | 
				
			|||||||
@ -18,3 +18,4 @@ export * from './update-associations';
 | 
				
			|||||||
export * from './collection-importer';
 | 
					export * from './collection-importer';
 | 
				
			||||||
export * from './filter-match';
 | 
					export * from './filter-match';
 | 
				
			||||||
export * from './field-repository/array-field-repository';
 | 
					export * from './field-repository/array-field-repository';
 | 
				
			||||||
 | 
					export { snakeCase } from './utils';
 | 
				
			||||||
 | 
				
			|||||||
@ -31,10 +31,11 @@ export function getConfigByEnv() {
 | 
				
			|||||||
      collate: 'utf8mb4_unicode_ci',
 | 
					      collate: 'utf8mb4_unicode_ci',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    timezone: process.env.DB_TIMEZONE,
 | 
					    timezone: process.env.DB_TIMEZONE,
 | 
				
			||||||
 | 
					    underscored: process.env.DB_UNDERSCORED === 'true',
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function mockDatabase(options: IDatabaseOptions = {}): MockDatabase {
 | 
					export function mockDatabase(options: IDatabaseOptions = {}): MockDatabase {
 | 
				
			||||||
  const dbOptions = merge(getConfigByEnv(), options);
 | 
					  const dbOptions = merge(getConfigByEnv(), options) as any;
 | 
				
			||||||
  return new MockDatabase(dbOptions);
 | 
					  return new MockDatabase(dbOptions);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,9 +1,8 @@
 | 
				
			|||||||
import lodash from 'lodash';
 | 
					import lodash from 'lodash';
 | 
				
			||||||
import { DataTypes, Model as SequelizeModel, ModelStatic } from 'sequelize';
 | 
					import { Model as SequelizeModel, ModelStatic } from 'sequelize';
 | 
				
			||||||
import { Collection } from './collection';
 | 
					import { Collection } from './collection';
 | 
				
			||||||
import { Database } from './database';
 | 
					import { Database } from './database';
 | 
				
			||||||
import { Field } from './fields';
 | 
					import { Field } from './fields';
 | 
				
			||||||
import type { InheritedCollection } from './inherited-collection';
 | 
					 | 
				
			||||||
import { SyncRunner } from './sync-runner';
 | 
					import { SyncRunner } from './sync-runner';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const _ = lodash;
 | 
					const _ = lodash;
 | 
				
			||||||
@ -28,6 +27,7 @@ export class Model<TModelAttributes extends {} = any, TCreationAttributes extend
 | 
				
			|||||||
  public static collection: Collection;
 | 
					  public static collection: Collection;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  [key: string]: any;
 | 
					  [key: string]: any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  protected _changedWithAssociations = new Set();
 | 
					  protected _changedWithAssociations = new Set();
 | 
				
			||||||
  protected _previousDataValuesWithAssociations = {};
 | 
					  protected _previousDataValuesWithAssociations = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -75,6 +75,7 @@ export class OptionsParser {
 | 
				
			|||||||
    for (const sortKey of sort) {
 | 
					    for (const sortKey of sort) {
 | 
				
			||||||
      let direction = sortKey.startsWith('-') ? 'DESC' : 'ASC';
 | 
					      let direction = sortKey.startsWith('-') ? 'DESC' : 'ASC';
 | 
				
			||||||
      let sortField: Array<any> = sortKey.replace('-', '').split('.');
 | 
					      let sortField: Array<any> = sortKey.replace('-', '').split('.');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (this.database.inDialect('postgres', 'sqlite')) {
 | 
					      if (this.database.inDialect('postgres', 'sqlite')) {
 | 
				
			||||||
        direction = `${direction} NULLS LAST`;
 | 
					        direction = `${direction} NULLS LAST`;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@ -86,7 +87,11 @@ export class OptionsParser {
 | 
				
			|||||||
          sortField[i] = associationModel.associations[associationKey].target;
 | 
					          sortField[i] = associationModel.associations[associationKey].target;
 | 
				
			||||||
          associationModel = sortField[i];
 | 
					          associationModel = sortField[i];
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        const rawField = this.model.rawAttributes[sortField[0]];
 | 
				
			||||||
 | 
					        sortField[0] = rawField?.field || sortField[0];
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      sortField.push(direction);
 | 
					      sortField.push(direction);
 | 
				
			||||||
      if (this.database.inDialect('mysql')) {
 | 
					      if (this.database.inDialect('mysql')) {
 | 
				
			||||||
        orderParams.push([Sequelize.fn('ISNULL', Sequelize.col(`${this.model.name}.${sortField[0]}`))]);
 | 
					        orderParams.push([Sequelize.fn('ISNULL', Sequelize.col(`${this.model.name}.${sortField[0]}`))]);
 | 
				
			||||||
 | 
				
			|||||||
@ -62,6 +62,8 @@ export class BelongsToManyRepository extends MultipleRelationRepository implemen
 | 
				
			|||||||
    const transaction = await this.getTransaction(options);
 | 
					    const transaction = await this.getTransaction(options);
 | 
				
			||||||
    const association = <BelongsToMany>this.association;
 | 
					    const association = <BelongsToMany>this.association;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const throughModel = this.throughModel();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const instancesToIds = (instances) => {
 | 
					    const instancesToIds = (instances) => {
 | 
				
			||||||
      return instances.map((instance) => instance.get(this.targetKey()));
 | 
					      return instances.map((instance) => instance.get(this.targetKey()));
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
@ -69,7 +71,7 @@ export class BelongsToManyRepository extends MultipleRelationRepository implemen
 | 
				
			|||||||
    // Through Table
 | 
					    // Through Table
 | 
				
			||||||
    const throughTableWhere: Array<any> = [
 | 
					    const throughTableWhere: Array<any> = [
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        [association.foreignKey]: this.sourceKeyValue,
 | 
					        [throughModel.rawAttributes[association.foreignKey].field]: this.sourceKeyValue,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    ];
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -100,7 +102,7 @@ export class BelongsToManyRepository extends MultipleRelationRepository implemen
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    throughTableWhere.push({
 | 
					    throughTableWhere.push({
 | 
				
			||||||
      [association.otherKey]: {
 | 
					      [throughModel.rawAttributes[association.otherKey].field]: {
 | 
				
			||||||
        [Op.in]: ids,
 | 
					        [Op.in]: ids,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
				
			|||||||
@ -11,7 +11,7 @@ import {
 | 
				
			|||||||
  Op,
 | 
					  Op,
 | 
				
			||||||
  Transactionable,
 | 
					  Transactionable,
 | 
				
			||||||
  UpdateOptions as SequelizeUpdateOptions,
 | 
					  UpdateOptions as SequelizeUpdateOptions,
 | 
				
			||||||
  WhereOperators
 | 
					  WhereOperators,
 | 
				
			||||||
} from 'sequelize';
 | 
					} from 'sequelize';
 | 
				
			||||||
import { Collection } from './collection';
 | 
					import { Collection } from './collection';
 | 
				
			||||||
import { Database } from './database';
 | 
					import { Database } from './database';
 | 
				
			||||||
@ -407,7 +407,12 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const transaction = await this.getTransaction(options);
 | 
					    const transaction = await this.getTransaction(options);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const guard = UpdateGuard.fromOptions(this.model, { ...options, action: 'create' });
 | 
					    const guard = UpdateGuard.fromOptions(this.model, {
 | 
				
			||||||
 | 
					      ...options,
 | 
				
			||||||
 | 
					      action: 'create',
 | 
				
			||||||
 | 
					      underscored: this.collection.options.underscored,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const values = guard.sanitize(options.values || {});
 | 
					    const values = guard.sanitize(options.values || {});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const instance = await this.model.create<any>(values, {
 | 
					    const instance = await this.model.create<any>(values, {
 | 
				
			||||||
@ -476,7 +481,7 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    const transaction = await this.getTransaction(options);
 | 
					    const transaction = await this.getTransaction(options);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const guard = UpdateGuard.fromOptions(this.model, options);
 | 
					    const guard = UpdateGuard.fromOptions(this.model, { ...options, underscored: this.collection.options.underscored });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const values = guard.sanitize(options.values);
 | 
					    const values = guard.sanitize(options.values);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -124,7 +124,9 @@ AND    NOT attisdropped
 | 
				
			|||||||
    if (options.alter) {
 | 
					    if (options.alter) {
 | 
				
			||||||
      const columns = await queryInterface.describeTable(tableName, options);
 | 
					      const columns = await queryInterface.describeTable(tableName, options);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      for (const columnName in childAttributes) {
 | 
					      for (const attribute in childAttributes) {
 | 
				
			||||||
 | 
					        const columnName = childAttributes[attribute].field;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!columns[columnName]) {
 | 
					        if (!columns[columnName]) {
 | 
				
			||||||
          await queryInterface.addColumn(tableName, columnName, childAttributes[columnName], options);
 | 
					          await queryInterface.addColumn(tableName, columnName, childAttributes[columnName], options);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
				
			|||||||
@ -13,6 +13,7 @@ type UpdateAction = 'create' | 'update';
 | 
				
			|||||||
export class UpdateGuard {
 | 
					export class UpdateGuard {
 | 
				
			||||||
  model: ModelStatic<any>;
 | 
					  model: ModelStatic<any>;
 | 
				
			||||||
  action: UpdateAction;
 | 
					  action: UpdateAction;
 | 
				
			||||||
 | 
					  underscored: boolean;
 | 
				
			||||||
  private associationKeysToBeUpdate: AssociationKeysToBeUpdate;
 | 
					  private associationKeysToBeUpdate: AssociationKeysToBeUpdate;
 | 
				
			||||||
  private blackList: BlackList;
 | 
					  private blackList: BlackList;
 | 
				
			||||||
  private whiteList: WhiteList;
 | 
					  private whiteList: WhiteList;
 | 
				
			||||||
@ -162,6 +163,11 @@ export class UpdateGuard {
 | 
				
			|||||||
    guard.setBlackList(options.blacklist);
 | 
					    guard.setBlackList(options.blacklist);
 | 
				
			||||||
    guard.setAction(lodash.get(options, 'action', 'update'));
 | 
					    guard.setAction(lodash.get(options, 'action', 'update'));
 | 
				
			||||||
    guard.setAssociationKeysToBeUpdate(options.updateAssociationValues);
 | 
					    guard.setAssociationKeysToBeUpdate(options.updateAssociationValues);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (options.underscored) {
 | 
				
			||||||
 | 
					      guard.underscored = options.underscored;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return guard;
 | 
					    return guard;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,7 @@
 | 
				
			|||||||
import crypto from 'crypto';
 | 
					import crypto from 'crypto';
 | 
				
			||||||
import { IdentifierError } from './errors/identifier-error';
 | 
					import { IdentifierError } from './errors/identifier-error';
 | 
				
			||||||
import { Model } from './model';
 | 
					import { Model } from './model';
 | 
				
			||||||
 | 
					import lodash from 'lodash';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type HandleAppendsQueryOptions = {
 | 
					type HandleAppendsQueryOptions = {
 | 
				
			||||||
  templateModel: any;
 | 
					  templateModel: any;
 | 
				
			||||||
@ -73,3 +74,11 @@ export function checkIdentifier(value: string) {
 | 
				
			|||||||
    throw new IdentifierError(`Identifier ${value} is too long`);
 | 
					    throw new IdentifierError(`Identifier ${value} is too long`);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function getTableName(collectionName: string, options) {
 | 
				
			||||||
 | 
					  return options.underscored ? snakeCase(collectionName) : collectionName;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function snakeCase(name: string) {
 | 
				
			||||||
 | 
					  return require('sequelize').Utils.underscore(name);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -76,6 +76,7 @@ export class PluginManager {
 | 
				
			|||||||
        await this.repository.load();
 | 
					        await this.repository.load();
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.app.on('beforeUpgrade', async () => {
 | 
					    this.app.on('beforeUpgrade', async () => {
 | 
				
			||||||
      await this.collection.sync();
 | 
					      await this.collection.sync();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
@ -195,6 +196,7 @@ export class PluginManager {
 | 
				
			|||||||
    if (this.plugins.has(pluginName)) {
 | 
					    if (this.plugins.has(pluginName)) {
 | 
				
			||||||
      throw new Error(`plugin name [${pluginName}] already exists`);
 | 
					      throw new Error(`plugin name [${pluginName}] already exists`);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.plugins.set(pluginName, instance);
 | 
					    this.plugins.set(pluginName, instance);
 | 
				
			||||||
    return instance;
 | 
					    return instance;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -32,12 +32,22 @@ describe('list action with acl', () => {
 | 
				
			|||||||
      role: 'user',
 | 
					      role: 'user',
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    app.acl.addFixedParams('tests', 'destroy', () => {
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        filter: {
 | 
				
			||||||
 | 
					          $and: [{ 'name.$ne': 't1' }, { 'name.$ne': 't2' }],
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    userRole.grantAction('tests:view', {});
 | 
					    userRole.grantAction('tests:view', {});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    userRole.grantAction('tests:update', {
 | 
					    userRole.grantAction('tests:update', {
 | 
				
			||||||
      own: true,
 | 
					      own: true,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    userRole.grantAction('tests:destroy', {});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const Test = app.db.collection({
 | 
					    const Test = app.db.collection({
 | 
				
			||||||
      name: 'tests',
 | 
					      name: 'tests',
 | 
				
			||||||
      fields: [
 | 
					      fields: [
 | 
				
			||||||
@ -80,7 +90,7 @@ describe('list action with acl', () => {
 | 
				
			|||||||
    const data = response.body;
 | 
					    const data = response.body;
 | 
				
			||||||
    expect(data.meta.allowedActions.view).toEqual(['t1', 't2', 't3']);
 | 
					    expect(data.meta.allowedActions.view).toEqual(['t1', 't2', 't3']);
 | 
				
			||||||
    expect(data.meta.allowedActions.update).toEqual(['t1', 't2']);
 | 
					    expect(data.meta.allowedActions.update).toEqual(['t1', 't2']);
 | 
				
			||||||
    expect(data.meta.allowedActions.destroy).toEqual([]);
 | 
					    expect(data.meta.allowedActions.destroy).toEqual(['t3']);
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  it('should list items with meta permission', async () => {
 | 
					  it('should list items with meta permission', async () => {
 | 
				
			||||||
@ -241,7 +251,10 @@ describe('list association action with acl', () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const userPlugin = app.getPlugin('users');
 | 
					    const userPlugin = app.getPlugin('users');
 | 
				
			||||||
    const userAgent = app.agent().set('X-With-ACL-Meta', true).auth(
 | 
					    const userAgent = app
 | 
				
			||||||
 | 
					      .agent()
 | 
				
			||||||
 | 
					      .set('X-With-ACL-Meta', true)
 | 
				
			||||||
 | 
					      .auth(
 | 
				
			||||||
        userPlugin.jwtService.sign({
 | 
					        userPlugin.jwtService.sign({
 | 
				
			||||||
          userId: user.get('id'),
 | 
					          userId: user.get('id'),
 | 
				
			||||||
        }),
 | 
					        }),
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
import { NoPermissionError } from '@nocobase/acl';
 | 
					import { NoPermissionError } from '@nocobase/acl';
 | 
				
			||||||
import { Context, utils as actionUtils } from '@nocobase/actions';
 | 
					import { Context, utils as actionUtils } from '@nocobase/actions';
 | 
				
			||||||
import { Collection, RelationField } from '@nocobase/database';
 | 
					import { Collection, RelationField, snakeCase } from '@nocobase/database';
 | 
				
			||||||
import { Plugin } from '@nocobase/server';
 | 
					import { Plugin } from '@nocobase/server';
 | 
				
			||||||
import lodash from 'lodash';
 | 
					import lodash from 'lodash';
 | 
				
			||||||
import { resolve } from 'path';
 | 
					import { resolve } from 'path';
 | 
				
			||||||
@ -262,6 +262,7 @@ export class PluginACL extends Plugin {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    this.app.db.on('rolesResources.afterDestroy', async (model, options) => {
 | 
					    this.app.db.on('rolesResources.afterDestroy', async (model, options) => {
 | 
				
			||||||
      const role = this.acl.getRole(model.get('roleName'));
 | 
					      const role = this.acl.getRole(model.get('roleName'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (role) {
 | 
					      if (role) {
 | 
				
			||||||
        role.revokeResource(model.get('name'));
 | 
					        role.revokeResource(model.get('name'));
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@ -281,6 +282,7 @@ export class PluginACL extends Plugin {
 | 
				
			|||||||
      const { transaction } = options;
 | 
					      const { transaction } = options;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const collectionName = model.get('collectionName');
 | 
					      const collectionName = model.get('collectionName');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const fieldName = model.get('name');
 | 
					      const fieldName = model.get('name');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const resourceActions = (await this.app.db.getRepository('rolesResourcesActions').find({
 | 
					      const resourceActions = (await this.app.db.getRepository('rolesResourcesActions').find({
 | 
				
			||||||
@ -695,16 +697,57 @@ export class PluginACL extends Plugin {
 | 
				
			|||||||
        const actionSql = ctx.db.sequelize.queryInterface.queryGenerator.selectQuery(
 | 
					        const actionSql = ctx.db.sequelize.queryInterface.queryGenerator.selectQuery(
 | 
				
			||||||
          Model.getTableName(),
 | 
					          Model.getTableName(),
 | 
				
			||||||
          {
 | 
					          {
 | 
				
			||||||
            // ...queryParams,
 | 
					            where: (() => {
 | 
				
			||||||
            where: queryParams.where,
 | 
					              const filterObj = queryParams.where;
 | 
				
			||||||
 | 
					              if (!this.db.options.underscored) {
 | 
				
			||||||
 | 
					                return filterObj;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              const iterate = (rootObj, path = []) => {
 | 
				
			||||||
 | 
					                const obj = path.length == 0 ? rootObj : lodash.get(rootObj, path);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (Array.isArray(obj)) {
 | 
				
			||||||
 | 
					                  for (let i = 0; i < obj.length; i++) {
 | 
				
			||||||
 | 
					                    if (obj[i] === null) {
 | 
				
			||||||
 | 
					                      continue;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if (typeof obj[i] === 'object') {
 | 
				
			||||||
 | 
					                      iterate(rootObj, [...path, i]);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                  return;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                Reflect.ownKeys(obj).forEach((key) => {
 | 
				
			||||||
 | 
					                  if (Array.isArray(obj) && key == 'length') {
 | 
				
			||||||
 | 
					                    return;
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                  if ((typeof obj[key] === 'object' && obj[key] !== null) || typeof obj[key] === 'symbol') {
 | 
				
			||||||
 | 
					                    iterate(rootObj, [...path, key]);
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                  if (typeof key === 'string' && key !== snakeCase(key)) {
 | 
				
			||||||
 | 
					                    lodash.set(rootObj, [...path, snakeCase(key)], lodash.cloneDeep(obj[key]));
 | 
				
			||||||
 | 
					                    lodash.unset(rootObj, [...path, key]);
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              iterate(filterObj);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              return filterObj;
 | 
				
			||||||
 | 
					            })(),
 | 
				
			||||||
            attributes: [primaryKeyField],
 | 
					            attributes: [primaryKeyField],
 | 
				
			||||||
            includeIgnoreAttributes: false,
 | 
					            includeIgnoreAttributes: false,
 | 
				
			||||||
            // include: queryParams.include,
 | 
					 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          Model,
 | 
					          Model,
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const whereCase = actionSql.match(/WHERE (.*?);/)[1];
 | 
					        const whereCase = actionSql.match(/WHERE (.*?);/)[1];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        conditions.push({
 | 
					        conditions.push({
 | 
				
			||||||
          whereCase,
 | 
					          whereCase,
 | 
				
			||||||
          action,
 | 
					          action,
 | 
				
			||||||
 | 
				
			|||||||
@ -19,6 +19,32 @@ describe('collections repository', () => {
 | 
				
			|||||||
    await app.destroy();
 | 
					    await app.destroy();
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  test('create underscored field', async () => {
 | 
				
			||||||
 | 
					    if (process.env.DB_UNDERSCORED !== 'true') {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const collection = await Collection.repository.create({
 | 
				
			||||||
 | 
					      values: {
 | 
				
			||||||
 | 
					        name: 'testCollection',
 | 
				
			||||||
 | 
					        createdAt: true,
 | 
				
			||||||
 | 
					        fields: [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            type: 'date',
 | 
				
			||||||
 | 
					            field: 'createdAt',
 | 
				
			||||||
 | 
					            name: 'createdAt',
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await collection.migrate();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const testCollection = db.getCollection('testCollection');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(testCollection.model.rawAttributes.createdAt.field).toEqual('created_at');
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  it('case 1', async () => {
 | 
					  it('case 1', async () => {
 | 
				
			||||||
    // 什么都没提供,随机 name 和 key
 | 
					    // 什么都没提供,随机 name 和 key
 | 
				
			||||||
    const data = await Collection.repository.create({
 | 
					    const data = await Collection.repository.create({
 | 
				
			||||||
@ -157,4 +183,164 @@ describe('collections repository', () => {
 | 
				
			|||||||
      ],
 | 
					      ],
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should destroy when fields refer to the same field', async () => {
 | 
				
			||||||
 | 
					    await Collection.repository.create({
 | 
				
			||||||
 | 
					      context: {},
 | 
				
			||||||
 | 
					      values: {
 | 
				
			||||||
 | 
					        name: 'tests',
 | 
				
			||||||
 | 
					        timestamps: true,
 | 
				
			||||||
 | 
					        fields: [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            type: 'date',
 | 
				
			||||||
 | 
					            name: 'dateA',
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            type: 'date',
 | 
				
			||||||
 | 
					            name: 'date_a',
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const testCollection = db.getCollection('tests');
 | 
				
			||||||
 | 
					    const getTableInfo = async () =>
 | 
				
			||||||
 | 
					      await db.sequelize.getQueryInterface().describeTable(testCollection.model.tableName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const tableInfo0 = await getTableInfo();
 | 
				
			||||||
 | 
					    expect(tableInfo0['date_a']).toBeDefined();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await Field.repository.destroy({
 | 
				
			||||||
 | 
					      context: {},
 | 
				
			||||||
 | 
					      filter: {
 | 
				
			||||||
 | 
					        name: ['dateA', 'date_a'],
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const count = await Field.repository.count();
 | 
				
			||||||
 | 
					    expect(count).toBe(0);
 | 
				
			||||||
 | 
					    const tableInfo1 = await getTableInfo();
 | 
				
			||||||
 | 
					    expect(tableInfo1['dateA']).not.toBeDefined();
 | 
				
			||||||
 | 
					    expect(tableInfo1['date_a']).not.toBeDefined();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should not destroy timestamps columns', async () => {
 | 
				
			||||||
 | 
					    const createdAt = db.options.underscored ? 'created_at' : 'createdAt';
 | 
				
			||||||
 | 
					    await Collection.repository.create({
 | 
				
			||||||
 | 
					      context: {},
 | 
				
			||||||
 | 
					      values: {
 | 
				
			||||||
 | 
					        name: 'tests',
 | 
				
			||||||
 | 
					        timestamps: true,
 | 
				
			||||||
 | 
					        fields: [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            type: 'date',
 | 
				
			||||||
 | 
					            name: 'createdAt',
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const testCollection = db.getCollection('tests');
 | 
				
			||||||
 | 
					    const getTableInfo = async () =>
 | 
				
			||||||
 | 
					      await db.sequelize.getQueryInterface().describeTable(testCollection.model.tableName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const tableInfo0 = await getTableInfo();
 | 
				
			||||||
 | 
					    expect(tableInfo0[createdAt]).toBeDefined();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await Field.repository.destroy({
 | 
				
			||||||
 | 
					      context: {},
 | 
				
			||||||
 | 
					      filter: {
 | 
				
			||||||
 | 
					        name: 'createdAt',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const tableInfo1 = await getTableInfo();
 | 
				
			||||||
 | 
					    expect(tableInfo1[createdAt]).toBeDefined();
 | 
				
			||||||
 | 
					    expect(testCollection.hasField('createdAt')).toBeFalsy();
 | 
				
			||||||
 | 
					    expect(testCollection.model.rawAttributes['createdAt']).toBeDefined();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should not destroy column when column belongs to a field', async () => {
 | 
				
			||||||
 | 
					    if (db.options.underscored !== true) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await Collection.repository.create({
 | 
				
			||||||
 | 
					      context: {},
 | 
				
			||||||
 | 
					      values: {
 | 
				
			||||||
 | 
					        name: 'tests',
 | 
				
			||||||
 | 
					        fields: [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            type: 'string',
 | 
				
			||||||
 | 
					            name: 'test_field',
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            type: 'string',
 | 
				
			||||||
 | 
					            name: 'testField',
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            type: 'string',
 | 
				
			||||||
 | 
					            name: 'test123',
 | 
				
			||||||
 | 
					            field: 'test_field',
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            type: 'string',
 | 
				
			||||||
 | 
					            name: 'otherField',
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const testCollection = db.getCollection('tests');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(
 | 
				
			||||||
 | 
					      testCollection.model.rawAttributes.test_field.field === testCollection.model.rawAttributes.testField.field,
 | 
				
			||||||
 | 
					    ).toBe(true);
 | 
				
			||||||
 | 
					    const getTableInfo = async () =>
 | 
				
			||||||
 | 
					      await db.sequelize.getQueryInterface().describeTable(testCollection.model.tableName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const tableInfo0 = await getTableInfo();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(tableInfo0['other_field']).toBeDefined();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await Field.repository.destroy({
 | 
				
			||||||
 | 
					      context: {},
 | 
				
			||||||
 | 
					      filter: {
 | 
				
			||||||
 | 
					        name: 'otherField',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(testCollection.model.rawAttributes['otherField']).toBeUndefined();
 | 
				
			||||||
 | 
					    const tableInfo1 = await getTableInfo();
 | 
				
			||||||
 | 
					    expect(tableInfo1['other_field']).not.toBeDefined();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await Field.repository.destroy({
 | 
				
			||||||
 | 
					      context: {},
 | 
				
			||||||
 | 
					      filter: {
 | 
				
			||||||
 | 
					        name: 'testField',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(testCollection.model.rawAttributes['testField']).toBeUndefined();
 | 
				
			||||||
 | 
					    const tableInfo2 = await getTableInfo();
 | 
				
			||||||
 | 
					    expect(tableInfo2['test_field']).toBeDefined();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await Field.repository.destroy({
 | 
				
			||||||
 | 
					      context: {},
 | 
				
			||||||
 | 
					      filter: {
 | 
				
			||||||
 | 
					        name: 'test_field',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const tableInfo3 = await getTableInfo();
 | 
				
			||||||
 | 
					    expect(tableInfo3['test_field']).toBeDefined();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await Field.repository.destroy({
 | 
				
			||||||
 | 
					      context: {},
 | 
				
			||||||
 | 
					      filter: {
 | 
				
			||||||
 | 
					        name: 'test123',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const tableInfo4 = await getTableInfo();
 | 
				
			||||||
 | 
					    expect(tableInfo4['test_field']).not.toBeDefined();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
@ -69,6 +69,8 @@ describe('field indexes', () => {
 | 
				
			|||||||
      },
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(field.status).toBe(200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // create a record
 | 
					    // create a record
 | 
				
			||||||
    const response1 = await agent.resource(tableName).create({
 | 
					    const response1 = await agent.resource(tableName).create({
 | 
				
			||||||
      values: {
 | 
					      values: {
 | 
				
			||||||
 | 
				
			|||||||
@ -38,6 +38,7 @@ describe('reverseField options', () => {
 | 
				
			|||||||
        reverseField: {},
 | 
					        reverseField: {},
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const json = JSON.parse(JSON.stringify(field.toJSON()));
 | 
					    const json = JSON.parse(JSON.stringify(field.toJSON()));
 | 
				
			||||||
    expect(json).toMatchObject({
 | 
					    expect(json).toMatchObject({
 | 
				
			||||||
      type: 'hasMany',
 | 
					      type: 'hasMany',
 | 
				
			||||||
 | 
				
			|||||||
@ -396,4 +396,144 @@ describe('collections repository', () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
    expect(response1.body.data.length).toBe(2);
 | 
					    expect(response1.body.data.length).toBe(2);
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should update field with default value', async () => {
 | 
				
			||||||
 | 
					    const createCollectionResponse = await app
 | 
				
			||||||
 | 
					      .agent()
 | 
				
			||||||
 | 
					      .resource('collections')
 | 
				
			||||||
 | 
					      .create({
 | 
				
			||||||
 | 
					        values: {
 | 
				
			||||||
 | 
					          name: 'test',
 | 
				
			||||||
 | 
					          fields: [
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					              name: 'testField',
 | 
				
			||||||
 | 
					              type: 'string',
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const testField = await app.db.getRepository('fields').findOne({
 | 
				
			||||||
 | 
					      filter: {
 | 
				
			||||||
 | 
					        name: 'testField',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // update field with unique index
 | 
				
			||||||
 | 
					    const addDefaultValueResponse = await app
 | 
				
			||||||
 | 
					      .agent()
 | 
				
			||||||
 | 
					      .resource('fields')
 | 
				
			||||||
 | 
					      .update({
 | 
				
			||||||
 | 
					        values: {
 | 
				
			||||||
 | 
					          defaultValue: '1231',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        filterByTk: testField.get('key'),
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(addDefaultValueResponse.statusCode).toEqual(200);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should create collection field', async () => {
 | 
				
			||||||
 | 
					    await app
 | 
				
			||||||
 | 
					      .agent()
 | 
				
			||||||
 | 
					      .resource('collections')
 | 
				
			||||||
 | 
					      .create({
 | 
				
			||||||
 | 
					        values: {
 | 
				
			||||||
 | 
					          name: 'test',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const addFieldResponse = await app
 | 
				
			||||||
 | 
					      .agent()
 | 
				
			||||||
 | 
					      .resource('fields')
 | 
				
			||||||
 | 
					      .create({
 | 
				
			||||||
 | 
					        values: {
 | 
				
			||||||
 | 
					          name: 'testField',
 | 
				
			||||||
 | 
					          collectionName: 'test',
 | 
				
			||||||
 | 
					          type: 'string',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(addFieldResponse.statusCode).toEqual(200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const collection = app.db.getCollection('test');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const columnName = collection.model.rawAttributes.testField.field;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const tableInfo = await app.db.sequelize.getQueryInterface().describeTable(collection.model.tableName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(tableInfo[columnName]).toBeDefined();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should update field with unique index', async () => {
 | 
				
			||||||
 | 
					    const createCollectionResponse = await app
 | 
				
			||||||
 | 
					      .agent()
 | 
				
			||||||
 | 
					      .resource('collections')
 | 
				
			||||||
 | 
					      .create({
 | 
				
			||||||
 | 
					        values: {
 | 
				
			||||||
 | 
					          name: 'test',
 | 
				
			||||||
 | 
					          fields: [
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					              name: 'testField',
 | 
				
			||||||
 | 
					              type: 'string',
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(createCollectionResponse.statusCode).toEqual(200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const testField = await app.db.getRepository('fields').findOne({
 | 
				
			||||||
 | 
					      filter: {
 | 
				
			||||||
 | 
					        name: 'testField',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // update field with unique index
 | 
				
			||||||
 | 
					    const addIndexResponse = await app
 | 
				
			||||||
 | 
					      .agent()
 | 
				
			||||||
 | 
					      .resource('fields')
 | 
				
			||||||
 | 
					      .update({
 | 
				
			||||||
 | 
					        values: {
 | 
				
			||||||
 | 
					          unique: true,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        filterByTk: testField.get('key'),
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(addIndexResponse.statusCode).toEqual(200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const indexes = (await app.db.sequelize
 | 
				
			||||||
 | 
					      .getQueryInterface()
 | 
				
			||||||
 | 
					      .showIndex(app.db.getCollection('test').model.tableName)) as any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const columnName = app.db.getCollection('test').model.rawAttributes.testField.field;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(
 | 
				
			||||||
 | 
					      indexes.find(
 | 
				
			||||||
 | 
					        (index) => index.unique == true && index.fields[0].attribute == columnName && index.fields.length === 1,
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    ).toBeDefined();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const removeIndexResponse = await app
 | 
				
			||||||
 | 
					      .agent()
 | 
				
			||||||
 | 
					      .resource('fields')
 | 
				
			||||||
 | 
					      .update({
 | 
				
			||||||
 | 
					        values: {
 | 
				
			||||||
 | 
					          unique: false,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        filterByTk: testField.get('key'),
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(removeIndexResponse.statusCode).toEqual(200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const afterIndexes = (await app.db.sequelize
 | 
				
			||||||
 | 
					      .getQueryInterface()
 | 
				
			||||||
 | 
					      .showIndex(app.db.getCollection('test').model.tableName)) as any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(
 | 
				
			||||||
 | 
					      afterIndexes.find(
 | 
				
			||||||
 | 
					        (index) => index.unique == true && index.fields[0].attribute == columnName && index.fields.length === 1,
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    ).not.toBeDefined();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
@ -100,6 +100,8 @@ pgOnly()('Inherited Collection', () => {
 | 
				
			|||||||
      },
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const collectionB = app.db.getCollection('b');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const res = await agent.resource('b').create({
 | 
					    const res = await agent.resource('b').create({
 | 
				
			||||||
      values: {
 | 
					      values: {
 | 
				
			||||||
        af: 'a1',
 | 
					        af: 'a1',
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,6 @@
 | 
				
			|||||||
import { Database, MigrationContext } from '@nocobase/database';
 | 
					import { Database, MigrationContext } from '@nocobase/database';
 | 
				
			||||||
import Migrator from '../../migrations/20221121111113-update-id-to-bigint';
 | 
					import Migrator from '../../migrations/20221121111113-update-id-to-bigint';
 | 
				
			||||||
 | 
					import lodash from 'lodash';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const excludeSqlite = () => (process.env.DB_DIALECT != 'sqlite' ? describe : describe.skip);
 | 
					const excludeSqlite = () => (process.env.DB_DIALECT != 'sqlite' ? describe : describe.skip);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -72,6 +73,11 @@ excludeSqlite()('update id to bigint  test', () => {
 | 
				
			|||||||
        .describeTable(
 | 
					        .describeTable(
 | 
				
			||||||
          db.getCollection(collectionName) ? db.getCollection(collectionName).model.tableName : collectionName,
 | 
					          db.getCollection(collectionName) ? db.getCollection(collectionName).model.tableName : collectionName,
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (process.env.DB_UNDERSCORED) {
 | 
				
			||||||
 | 
					        fieldName = lodash.snakeCase(fieldName);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      console.log(`${collectionName}, ${fieldName}`, tableInfo[fieldName].type);
 | 
					      console.log(`${collectionName}, ${fieldName}`, tableInfo[fieldName].type);
 | 
				
			||||||
      expect(tableInfo[fieldName].type).toBe('BIGINT');
 | 
					      expect(tableInfo[fieldName].type).toBe('BIGINT');
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
				
			|||||||
@ -27,15 +27,21 @@ describe('collections.fields', () => {
 | 
				
			|||||||
          ],
 | 
					          ],
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const collection = app.db.getCollection('test1');
 | 
					    const collection = app.db.getCollection('test1');
 | 
				
			||||||
    const field = collection.getField('name');
 | 
					    const field = collection.getField('name');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    expect(collection.hasField('name')).toBeTruthy();
 | 
					    expect(collection.hasField('name')).toBeTruthy();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const r1 = await field.existsInDb();
 | 
					    const r1 = await field.existsInDb();
 | 
				
			||||||
    expect(r1).toBeTruthy();
 | 
					    expect(r1).toBeTruthy();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await app.agent().resource('collections.fields', 'test1').destroy({
 | 
					    await app.agent().resource('collections.fields', 'test1').destroy({
 | 
				
			||||||
      filterByTk: 'name',
 | 
					      filterByTk: 'name',
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    expect(collection.hasField('name')).toBeFalsy();
 | 
					    expect(collection.hasField('name')).toBeFalsy();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const r2 = await field.existsInDb();
 | 
					    const r2 = await field.existsInDb();
 | 
				
			||||||
    expect(r2).toBeFalsy();
 | 
					    expect(r2).toBeFalsy();
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
				
			|||||||
@ -30,6 +30,7 @@ export default class UpdateIdToBigIntMigrator extends Migration {
 | 
				
			|||||||
    const queryGenerator = queryInterface.queryGenerator as any;
 | 
					    const queryGenerator = queryInterface.queryGenerator as any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const updateToBigInt = async (model, fieldName) => {
 | 
					    const updateToBigInt = async (model, fieldName) => {
 | 
				
			||||||
 | 
					      const columnName = model.rawAttributes[fieldName].field;
 | 
				
			||||||
      let sql;
 | 
					      let sql;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const tableName = model.tableName;
 | 
					      const tableName = model.tableName;
 | 
				
			||||||
@ -51,7 +52,7 @@ export default class UpdateIdToBigIntMigrator extends Migration {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      if (model.rawAttributes[fieldName].type instanceof DataTypes.INTEGER) {
 | 
					      if (model.rawAttributes[fieldName].type instanceof DataTypes.INTEGER) {
 | 
				
			||||||
        if (db.inDialect('postgres')) {
 | 
					        if (db.inDialect('postgres')) {
 | 
				
			||||||
          sql = `ALTER TABLE "${tableName}" ALTER COLUMN "${fieldName}" SET DATA TYPE BIGINT;`;
 | 
					          sql = `ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}" SET DATA TYPE BIGINT;`;
 | 
				
			||||||
        } else if (db.inDialect('mysql')) {
 | 
					        } else if (db.inDialect('mysql')) {
 | 
				
			||||||
          const dataTypeOrOptions = model.rawAttributes[fieldName];
 | 
					          const dataTypeOrOptions = model.rawAttributes[fieldName];
 | 
				
			||||||
          const attributeName = fieldName;
 | 
					          const attributeName = fieldName;
 | 
				
			||||||
@ -68,6 +69,7 @@ export default class UpdateIdToBigIntMigrator extends Migration {
 | 
				
			|||||||
              table: tableName,
 | 
					              table: tableName,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          sql = queryGenerator.changeColumnQuery(tableName, query);
 | 
					          sql = queryGenerator.changeColumnQuery(tableName, query);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          sql = sql.replace(' PRIMARY KEY;', ' ;');
 | 
					          sql = sql.replace(' PRIMARY KEY;', ' ;');
 | 
				
			||||||
@ -83,7 +85,7 @@ export default class UpdateIdToBigIntMigrator extends Migration {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (db.inDialect('postgres')) {
 | 
					        if (db.inDialect('postgres')) {
 | 
				
			||||||
          const sequenceQuery = `SELECT pg_get_serial_sequence('"${model.tableName}"', '${fieldName}');`;
 | 
					          const sequenceQuery = `SELECT pg_get_serial_sequence('"${model.tableName}"', '${columnName}');`;
 | 
				
			||||||
          const [result] = await this.sequelize.query(sequenceQuery, {});
 | 
					          const [result] = await this.sequelize.query(sequenceQuery, {});
 | 
				
			||||||
          const sequenceName = result[0]['pg_get_serial_sequence'];
 | 
					          const sequenceName = result[0]['pg_get_serial_sequence'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,5 @@
 | 
				
			|||||||
import Database, { Collection, Field, MagicAttributeModel } from '@nocobase/database';
 | 
					import Database, { Collection, MagicAttributeModel, snakeCase } from '@nocobase/database';
 | 
				
			||||||
import { SyncOptions, Transactionable, UniqueConstraintError } from 'sequelize';
 | 
					import { SyncOptions, Transactionable } from 'sequelize';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface LoadOptions extends Transactionable {
 | 
					interface LoadOptions extends Transactionable {
 | 
				
			||||||
  // TODO
 | 
					  // TODO
 | 
				
			||||||
@ -10,58 +10,6 @@ interface MigrateOptions extends SyncOptions, Transactionable {
 | 
				
			|||||||
  isNew?: boolean;
 | 
					  isNew?: boolean;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function migrate(field: Field, options: MigrateOptions): Promise<void> {
 | 
					 | 
				
			||||||
  const { unique } = field.options;
 | 
					 | 
				
			||||||
  const { model } = field.collection;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const ukName = `${model.tableName}_${field.name}_uk`;
 | 
					 | 
				
			||||||
  const queryInterface = model.sequelize.getQueryInterface();
 | 
					 | 
				
			||||||
  const fieldAttribute = model.rawAttributes[field.name];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // @ts-ignore
 | 
					 | 
				
			||||||
  const existedConstraints = (await queryInterface.showConstraint(model.tableName, ukName, {
 | 
					 | 
				
			||||||
    transaction: options.transaction,
 | 
					 | 
				
			||||||
  })) as any[];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const constraintBefore = existedConstraints.find((item) => item.constraintName === ukName);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (typeof fieldAttribute?.unique !== 'undefined') {
 | 
					 | 
				
			||||||
    if (constraintBefore && !unique) {
 | 
					 | 
				
			||||||
      await queryInterface.removeConstraint(model.tableName, ukName, { transaction: options.transaction });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fieldAttribute.unique = Boolean(constraintBefore);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  await field.sync(options);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!constraintBefore && unique) {
 | 
					 | 
				
			||||||
    await queryInterface.addConstraint(model.tableName, {
 | 
					 | 
				
			||||||
      type: 'unique',
 | 
					 | 
				
			||||||
      fields: [field.name],
 | 
					 | 
				
			||||||
      name: ukName,
 | 
					 | 
				
			||||||
      transaction: options.transaction,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (typeof fieldAttribute?.unique !== 'undefined') {
 | 
					 | 
				
			||||||
    fieldAttribute.unique = unique;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // @ts-ignore
 | 
					 | 
				
			||||||
  const updatedConstraints = (await queryInterface.showConstraint(model.tableName, ukName, {
 | 
					 | 
				
			||||||
    transaction: options.transaction,
 | 
					 | 
				
			||||||
  })) as any[];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const indexAfter = updatedConstraints.find((item) => item.constraintName === ukName);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (unique && !indexAfter) {
 | 
					 | 
				
			||||||
    throw new UniqueConstraintError({
 | 
					 | 
				
			||||||
      fields: { [field.name]: undefined },
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class FieldModel extends MagicAttributeModel {
 | 
					export class FieldModel extends MagicAttributeModel {
 | 
				
			||||||
  get db(): Database {
 | 
					  get db(): Database {
 | 
				
			||||||
    return (<any>this.constructor).database;
 | 
					    return (<any>this.constructor).database;
 | 
				
			||||||
@ -106,10 +54,24 @@ export class FieldModel extends MagicAttributeModel {
 | 
				
			|||||||
      field = await this.load({
 | 
					      field = await this.load({
 | 
				
			||||||
        transaction: options.transaction,
 | 
					        transaction: options.transaction,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (!field) {
 | 
					      if (!field) {
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      await migrate(field, options);
 | 
					      const collection = this.getFieldCollection();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (isNew && collection.model.rawAttributes[this.get('name')] && this.get('unique')) {
 | 
				
			||||||
 | 
					        // trick: set unique to false to avoid auto sync unique index
 | 
				
			||||||
 | 
					        collection.model.rawAttributes[this.get('name')].unique = false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await field.sync(options);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (isNew && this.get('unique')) {
 | 
				
			||||||
 | 
					        await this.syncUniqueIndex({
 | 
				
			||||||
 | 
					          transaction: options.transaction,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      // field sync failed, delete from memory
 | 
					      // field sync failed, delete from memory
 | 
				
			||||||
      if (isNew && field) {
 | 
					      if (isNew && field) {
 | 
				
			||||||
@ -136,6 +98,56 @@ export class FieldModel extends MagicAttributeModel {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async syncUniqueIndex(options: Transactionable) {
 | 
				
			||||||
 | 
					    const unique = this.get('unique');
 | 
				
			||||||
 | 
					    const collection = this.getFieldCollection();
 | 
				
			||||||
 | 
					    const field = collection.getField(this.get('name'));
 | 
				
			||||||
 | 
					    const columnName = collection.model.rawAttributes[this.get('name')].field;
 | 
				
			||||||
 | 
					    const tableName = collection.model.tableName;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const queryInterface = this.db.sequelize.getQueryInterface() as any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const existsIndexes = await queryInterface.showIndex(collection.model.tableName, {
 | 
				
			||||||
 | 
					      transaction: options.transaction,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const existUniqueIndex = existsIndexes.find((item) => {
 | 
				
			||||||
 | 
					      return item.unique && item.fields[0].attribute === columnName && item.fields.length === 1;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let existsUniqueConstraint;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let constraintName = `${tableName}_${field.name}_uk`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (existUniqueIndex) {
 | 
				
			||||||
 | 
					      const existsUniqueConstraints = await queryInterface.showConstraint(
 | 
				
			||||||
 | 
					        collection.model.tableName,
 | 
				
			||||||
 | 
					        constraintName,
 | 
				
			||||||
 | 
					        {},
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      existsUniqueConstraint = existsUniqueConstraints[0];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (unique && !existsUniqueConstraint) {
 | 
				
			||||||
 | 
					      // @ts-ignore
 | 
				
			||||||
 | 
					      await collection.sync({ ...options, force: false, alter: { drop: false } });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await queryInterface.addConstraint(tableName, {
 | 
				
			||||||
 | 
					        type: 'unique',
 | 
				
			||||||
 | 
					        fields: [columnName],
 | 
				
			||||||
 | 
					        name: constraintName,
 | 
				
			||||||
 | 
					        transaction: options.transaction,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!unique && existsUniqueConstraint) {
 | 
				
			||||||
 | 
					      await queryInterface.removeConstraint(collection.model.tableName, constraintName, {
 | 
				
			||||||
 | 
					        transaction: options.transaction,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async syncDefaultValue(
 | 
					  async syncDefaultValue(
 | 
				
			||||||
    options: Transactionable & {
 | 
					    options: Transactionable & {
 | 
				
			||||||
      defaultValue: any;
 | 
					      defaultValue: any;
 | 
				
			||||||
@ -153,7 +165,7 @@ export class FieldModel extends MagicAttributeModel {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    await queryInterface.changeColumn(
 | 
					    await queryInterface.changeColumn(
 | 
				
			||||||
      collection.model.tableName,
 | 
					      collection.model.tableName,
 | 
				
			||||||
      this.get('name'),
 | 
					      collection.model.rawAttributes[this.get('name')].field,
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        type: field.dataType,
 | 
					        type: field.dataType,
 | 
				
			||||||
        defaultValue: options.defaultValue,
 | 
					        defaultValue: options.defaultValue,
 | 
				
			||||||
 | 
				
			|||||||
@ -5,6 +5,7 @@ import { UniqueConstraintError } from 'sequelize';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import PluginErrorHandler from '@nocobase/plugin-error-handler';
 | 
					import PluginErrorHandler from '@nocobase/plugin-error-handler';
 | 
				
			||||||
import { Plugin } from '@nocobase/server';
 | 
					import { Plugin } from '@nocobase/server';
 | 
				
			||||||
 | 
					import { Mutex } from 'async-mutex';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { CollectionRepository } from '.';
 | 
					import { CollectionRepository } from '.';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
@ -122,7 +123,7 @@ export class CollectionManagerPlugin extends Plugin {
 | 
				
			|||||||
        const next = currentOptions['unique'];
 | 
					        const next = currentOptions['unique'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (Boolean(prev) !== Boolean(next)) {
 | 
					        if (Boolean(prev) !== Boolean(next)) {
 | 
				
			||||||
          await model.migrate({ transaction });
 | 
					          await model.syncUniqueIndex({ transaction });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -149,9 +150,13 @@ export class CollectionManagerPlugin extends Plugin {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // before field remove
 | 
					    // before field remove
 | 
				
			||||||
    this.app.db.on('fields.beforeDestroy', beforeDestroyForeignKey(this.app.db));
 | 
					    this.app.db.on('fields.beforeDestroy', beforeDestroyForeignKey(this.app.db));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const mutex = new Mutex();
 | 
				
			||||||
    this.app.db.on('fields.beforeDestroy', async (model: FieldModel, options) => {
 | 
					    this.app.db.on('fields.beforeDestroy', async (model: FieldModel, options) => {
 | 
				
			||||||
 | 
					      await mutex.runExclusive(async () => {
 | 
				
			||||||
        await model.remove(options);
 | 
					        await model.remove(options);
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.app.db.on('collections.beforeDestroy', async (model: CollectionModel, options) => {
 | 
					    this.app.db.on('collections.beforeDestroy', async (model: CollectionModel, options) => {
 | 
				
			||||||
      await model.remove(options);
 | 
					      await model.remove(options);
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,5 @@
 | 
				
			|||||||
import { mockServer, MockServer } from '@nocobase/test';
 | 
					import { mockServer, MockServer } from '@nocobase/test';
 | 
				
			||||||
import { Database } from '@nocobase/database';
 | 
					import { Database, Model } from '@nocobase/database';
 | 
				
			||||||
import * as os from 'os';
 | 
					import * as os from 'os';
 | 
				
			||||||
import path from 'path';
 | 
					import path from 'path';
 | 
				
			||||||
import lodash from 'lodash';
 | 
					import lodash from 'lodash';
 | 
				
			||||||
@ -98,7 +98,11 @@ describe('dump', () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const collectionMeta = JSON.parse(collectionMetaFile);
 | 
					    const collectionMeta = JSON.parse(collectionMetaFile);
 | 
				
			||||||
    expect(collectionMeta.count).toEqual(2);
 | 
					    expect(collectionMeta.count).toEqual(2);
 | 
				
			||||||
    expect(collectionMeta.columns).toEqual(Object.keys(db.getCollection('users').model.rawAttributes));
 | 
					    expect(collectionMeta.columns).toEqual(
 | 
				
			||||||
 | 
					      Object.keys(db.getCollection('users').model.rawAttributes).map(
 | 
				
			||||||
 | 
					        (fieldName) => db.getCollection('users').model.rawAttributes[fieldName].field,
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const dataPath = path.resolve(testDir, 'collections', 'users', 'data');
 | 
					    const dataPath = path.resolve(testDir, 'collections', 'users', 'data');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,50 @@
 | 
				
			|||||||
 | 
					import { getApp } from '.';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('attachment', () => {
 | 
				
			||||||
 | 
					  let db;
 | 
				
			||||||
 | 
					  let app;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(async () => {
 | 
				
			||||||
 | 
					    app = await getApp();
 | 
				
			||||||
 | 
					    db = app.db;
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  afterEach(async () => {
 | 
				
			||||||
 | 
					    await app.destroy();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should linked to a instance', async () => {
 | 
				
			||||||
 | 
					    const testCollection = db.collection({
 | 
				
			||||||
 | 
					      name: 'test',
 | 
				
			||||||
 | 
					      fields: [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          name: 'name',
 | 
				
			||||||
 | 
					          type: 'string',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          type: 'belongsTo',
 | 
				
			||||||
 | 
					          name: 'logo',
 | 
				
			||||||
 | 
					          target: 'attachments',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await db.sync();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await testCollection.repository.create({
 | 
				
			||||||
 | 
					      values: {
 | 
				
			||||||
 | 
					        name: 'test',
 | 
				
			||||||
 | 
					        logo: {
 | 
				
			||||||
 | 
					          title: 'nocobase-logo',
 | 
				
			||||||
 | 
					          filename: '682e5ad037dd02a0fe4800a3e91c283b.png',
 | 
				
			||||||
 | 
					          extname: '.png',
 | 
				
			||||||
 | 
					          mimetype: 'image/png',
 | 
				
			||||||
 | 
					          url: 'https://nocobase.oss-cn-beijing.aliyuncs.com/682e5ad037dd02a0fe4800a3e91c283b.png',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const item = await testCollection.repository.findOne({});
 | 
				
			||||||
 | 
					    expect(item.get('logoId')).toBeDefined();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -13,6 +13,8 @@ export async function getApp(options = {}): Promise<MockServer> {
 | 
				
			|||||||
    acl: false,
 | 
					    acl: false,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  await app.cleanDb();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  app.plugin(plugin);
 | 
					  app.plugin(plugin);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  app.db.import({
 | 
					  app.db.import({
 | 
				
			||||||
 | 
				
			|||||||
@ -7,7 +7,7 @@ import calculators from '../calculators';
 | 
				
			|||||||
import { JOB_STATUS } from '../constants';
 | 
					import { JOB_STATUS } from '../constants';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function sleep(ms: number) {
 | 
					export function sleep(ms: number) {
 | 
				
			||||||
  return new Promise(resolve => {
 | 
					  return new Promise((resolve) => {
 | 
				
			||||||
    setTimeout(resolve, ms);
 | 
					    setTimeout(resolve, ms);
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -26,38 +26,40 @@ export async function getApp({ manual, ...options }: MockAppOptions = {}): Promi
 | 
				
			|||||||
        run(node, { result }, execution) {
 | 
					        run(node, { result }, execution) {
 | 
				
			||||||
          return {
 | 
					          return {
 | 
				
			||||||
            status: JOB_STATUS.RESOLVED,
 | 
					            status: JOB_STATUS.RESOLVED,
 | 
				
			||||||
            result
 | 
					            result,
 | 
				
			||||||
          };
 | 
					          };
 | 
				
			||||||
        }
 | 
					        },
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      error: {
 | 
					      error: {
 | 
				
			||||||
        run(node, input, execution) {
 | 
					        run(node, input, execution) {
 | 
				
			||||||
          throw new Error('definite error');
 | 
					          throw new Error('definite error');
 | 
				
			||||||
        }
 | 
					        },
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      'prompt->error': {
 | 
					      'prompt->error': {
 | 
				
			||||||
        run(node, input, execution) {
 | 
					        run(node, input, execution) {
 | 
				
			||||||
          return {
 | 
					          return {
 | 
				
			||||||
            status: JOB_STATUS.PENDING
 | 
					            status: JOB_STATUS.PENDING,
 | 
				
			||||||
          };
 | 
					          };
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        resume(node, input, execution) {
 | 
					        resume(node, input, execution) {
 | 
				
			||||||
          throw new Error('input failed');
 | 
					          throw new Error('input failed');
 | 
				
			||||||
        }
 | 
					        },
 | 
				
			||||||
      }
 | 
					      },
 | 
				
			||||||
    }
 | 
					    },
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (!calculators.get('no1')) {
 | 
					  if (!calculators.get('no1')) {
 | 
				
			||||||
    calculators.register('no1', () => 1);
 | 
					    calculators.register('no1', () => 1);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  await app.db.clean({ drop: true });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await app.load();
 | 
					  await app.load();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await app.db.import({
 | 
					  await app.db.import({
 | 
				
			||||||
    directory: path.resolve(__dirname, './collections')
 | 
					    directory: path.resolve(__dirname, './collections'),
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
 | 
				
			|||||||
@ -3,8 +3,6 @@ import Database from '@nocobase/database';
 | 
				
			|||||||
import { getApp, sleep } from '..';
 | 
					import { getApp, sleep } from '..';
 | 
				
			||||||
import { EXECUTION_STATUS, JOB_STATUS } from '../../constants';
 | 
					import { EXECUTION_STATUS, JOB_STATUS } from '../../constants';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
describe('workflow > instructions > delay', () => {
 | 
					describe('workflow > instructions > delay', () => {
 | 
				
			||||||
  let app: Application;
 | 
					  let app: Application;
 | 
				
			||||||
  let db: Database;
 | 
					  let db: Database;
 | 
				
			||||||
@ -16,6 +14,7 @@ describe('workflow > instructions > delay', () => {
 | 
				
			|||||||
    app = await getApp();
 | 
					    app = await getApp();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    db = app.db;
 | 
					    db = app.db;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    WorkflowModel = db.getCollection('workflows').model;
 | 
					    WorkflowModel = db.getCollection('workflows').model;
 | 
				
			||||||
    PostRepo = db.getCollection('posts').repository;
 | 
					    PostRepo = db.getCollection('posts').repository;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -24,8 +23,8 @@ describe('workflow > instructions > delay', () => {
 | 
				
			|||||||
      type: 'collection',
 | 
					      type: 'collection',
 | 
				
			||||||
      config: {
 | 
					      config: {
 | 
				
			||||||
        mode: 1,
 | 
					        mode: 1,
 | 
				
			||||||
        collection: 'posts'
 | 
					        collection: 'posts',
 | 
				
			||||||
      }
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -37,8 +36,8 @@ describe('workflow > instructions > delay', () => {
 | 
				
			|||||||
        type: 'delay',
 | 
					        type: 'delay',
 | 
				
			||||||
        config: {
 | 
					        config: {
 | 
				
			||||||
          duration: 2000,
 | 
					          duration: 2000,
 | 
				
			||||||
          endStatus: JOB_STATUS.RESOLVED
 | 
					          endStatus: JOB_STATUS.RESOLVED,
 | 
				
			||||||
        }
 | 
					        },
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const post = await PostRepo.create({ values: { title: 't1' } });
 | 
					      const post = await PostRepo.create({ values: { title: 't1' } });
 | 
				
			||||||
@ -63,8 +62,8 @@ describe('workflow > instructions > delay', () => {
 | 
				
			|||||||
        type: 'delay',
 | 
					        type: 'delay',
 | 
				
			||||||
        config: {
 | 
					        config: {
 | 
				
			||||||
          duration: 2000,
 | 
					          duration: 2000,
 | 
				
			||||||
          endStatus: JOB_STATUS.REJECTED
 | 
					          endStatus: JOB_STATUS.REJECTED,
 | 
				
			||||||
        }
 | 
					        },
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const post = await PostRepo.create({ values: { title: 't1' } });
 | 
					      const post = await PostRepo.create({ values: { title: 't1' } });
 | 
				
			||||||
@ -89,8 +88,8 @@ describe('workflow > instructions > delay', () => {
 | 
				
			|||||||
        type: 'delay',
 | 
					        type: 'delay',
 | 
				
			||||||
        config: {
 | 
					        config: {
 | 
				
			||||||
          duration: 2000,
 | 
					          duration: 2000,
 | 
				
			||||||
          endStatus: JOB_STATUS.RESOLVED
 | 
					          endStatus: JOB_STATUS.RESOLVED,
 | 
				
			||||||
        }
 | 
					        },
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      const n2 = await workflow.createNode({
 | 
					      const n2 = await workflow.createNode({
 | 
				
			||||||
        type: 'create',
 | 
					        type: 'create',
 | 
				
			||||||
@ -98,11 +97,11 @@ describe('workflow > instructions > delay', () => {
 | 
				
			|||||||
          collection: 'comment',
 | 
					          collection: 'comment',
 | 
				
			||||||
          params: {
 | 
					          params: {
 | 
				
			||||||
            values: {
 | 
					            values: {
 | 
				
			||||||
              status: 'should be number but use string to raise an error'
 | 
					              status: 'should be number but use string to raise an error',
 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        upstreamId: n1.id
 | 
					          },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        upstreamId: n1.id,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      await n1.setDownstream(n2);
 | 
					      await n1.setDownstream(n2);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -131,8 +130,8 @@ describe('workflow > instructions > delay', () => {
 | 
				
			|||||||
        type: 'delay',
 | 
					        type: 'delay',
 | 
				
			||||||
        config: {
 | 
					        config: {
 | 
				
			||||||
          duration: 2000,
 | 
					          duration: 2000,
 | 
				
			||||||
          endStatus: JOB_STATUS.RESOLVED
 | 
					          endStatus: JOB_STATUS.RESOLVED,
 | 
				
			||||||
        }
 | 
					        },
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -8,6 +8,8 @@ describe('shop actions', () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  beforeEach(async () => {
 | 
					  beforeEach(async () => {
 | 
				
			||||||
    app = mockServer();
 | 
					    app = mockServer();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await app.cleanDb();
 | 
				
			||||||
    app.plugin(Plugin);
 | 
					    app.plugin(Plugin);
 | 
				
			||||||
    agent = app.agent();
 | 
					    agent = app.agent();
 | 
				
			||||||
    db = app.db;
 | 
					    db = app.db;
 | 
				
			||||||
@ -26,15 +28,16 @@ describe('shop actions', () => {
 | 
				
			|||||||
        title: 'iPhone 14 Pro',
 | 
					        title: 'iPhone 14 Pro',
 | 
				
			||||||
        price: 7999,
 | 
					        price: 7999,
 | 
				
			||||||
        enabled: true,
 | 
					        enabled: true,
 | 
				
			||||||
        inventory: 1
 | 
					        inventory: 1,
 | 
				
			||||||
      }
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    expect(product.data.price).toEqual(7999);
 | 
					    expect(product.data.price).toEqual(7999);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { body: order } = await agent.resource('orders').create({
 | 
					    const { body: order } = await agent.resource('orders').create({
 | 
				
			||||||
      values: {
 | 
					      values: {
 | 
				
			||||||
        productId: product.data.id
 | 
					        productId: product.data.id,
 | 
				
			||||||
      }
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    expect(order.data.totalPrice).toEqual(7999);
 | 
					    expect(order.data.totalPrice).toEqual(7999);
 | 
				
			||||||
    expect(order.data.status).toEqual(0);
 | 
					    expect(order.data.status).toEqual(0);
 | 
				
			||||||
@ -43,8 +46,8 @@ describe('shop actions', () => {
 | 
				
			|||||||
      filterByTk: order.data.id,
 | 
					      filterByTk: order.data.id,
 | 
				
			||||||
      values: {
 | 
					      values: {
 | 
				
			||||||
        provider: 'SF',
 | 
					        provider: 'SF',
 | 
				
			||||||
        trackingNumber: '123456789'
 | 
					        trackingNumber: '123456789',
 | 
				
			||||||
      }
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    expect(deliveredOrder.data.status).toBe(2);
 | 
					    expect(deliveredOrder.data.status).toBe(2);
 | 
				
			||||||
    expect(deliveredOrder.data.delivery.trackingNumber).toBe('123456789');
 | 
					    expect(deliveredOrder.data.delivery.trackingNumber).toBe('123456789');
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user