fix/inherits issues(#1146)

* chore: sync inherits command

* fix: inherit field type conflit

* fix: merge

* fix: test

* fix: test

* chore: app command

* chore: create inheritance map

* fix: test

* fix: test

* fix: test
This commit is contained in:
ChengLei Shao 2022-11-26 09:24:53 +08:00 committed by GitHub
parent 998afa2450
commit 1ebb70e4c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 152 additions and 38 deletions

View File

@ -18,6 +18,7 @@ module.exports = (cli) => {
.allowUnknownOption() .allowUnknownOption()
.action(async (opts) => { .action(async (opts) => {
promptForTs(); promptForTs();
if (process.argv.includes('-h') || process.argv.includes('--help')) { if (process.argv.includes('-h') || process.argv.includes('--help')) {
run('ts-node', [ run('ts-node', [
'-P', '-P',
@ -29,15 +30,22 @@ module.exports = (cli) => {
]); ]);
return; return;
} }
const { port, client, server } = opts; const { port, client, server } = opts;
if (port) { if (port) {
process.env.APP_PORT = opts.port; process.env.APP_PORT = opts.port;
} }
const { APP_PORT } = process.env; const { APP_PORT } = process.env;
let clientPort = APP_PORT; let clientPort = APP_PORT;
let serverPort; let serverPort;
nodeCheck(); nodeCheck();
await postCheck(opts); await postCheck(opts);
if (server) { if (server) {
serverPort = APP_PORT; serverPort = APP_PORT;
} else if (!server && !client) { } else if (!server && !client) {
@ -45,6 +53,7 @@ module.exports = (cli) => {
port: 1 * clientPort + 1, port: 1 * clientPort + 1,
}); });
} }
await runAppCommand('install', ['--silent']); await runAppCommand('install', ['--silent']);
// if (opts.dbSync) { // if (opts.dbSync) {
// await runAppCommand('db:sync'); // await runAppCommand('db:sync');
@ -76,7 +85,8 @@ module.exports = (cli) => {
env: { env: {
PORT: clientPort, PORT: clientPort,
APP_ROOT: `packages/${APP_PACKAGE_ROOT}/client`, APP_ROOT: `packages/${APP_PACKAGE_ROOT}/client`,
PROXY_TARGET_URL: process.env.PROXY_TARGET_URL || (serverPort ? `http://127.0.0.1:${serverPort}` : undefined), PROXY_TARGET_URL:
process.env.PROXY_TARGET_URL || (serverPort ? `http://127.0.0.1:${serverPort}` : undefined),
}, },
}); });
} }

View File

@ -15,6 +15,69 @@ pgOnly()('collection inherits', () => {
await db.close(); await db.close();
}); });
it('should not throw error when fields have same type with parent', async () => {
db.collection({
name: 'parent',
fields: [{ name: 'field1', type: 'date' }],
});
expect(() => {
db.collection({
name: 'child',
inherits: ['parent'],
fields: [
{
name: 'field1',
type: 'date',
otherOptions: true,
},
],
});
}).not.toThrowError();
});
it('should throw error when fields conflict with parent ', async () => {
db.collection({
name: 'parent',
fields: [{ name: 'field1', type: 'string' }],
});
expect(() => {
db.collection({
name: 'child',
inherits: ['parent'],
fields: [{ name: 'field1', type: 'integer' }],
});
}).toThrowError();
await db.sync();
});
it('should not conflict when fields have same DateType', async () => {
db.collection({
name: 'parent',
fields: [{ name: 'field1', type: 'string' }],
});
expect(() => {
db.collection({
name: 'child',
inherits: ['parent'],
fields: [
{
name: 'field1',
type: 'sequence',
patterns: [
{
type: 'integer',
},
],
},
],
});
}).not.toThrowError();
});
it('should create inherits with lazy parents', async () => { it('should create inherits with lazy parents', async () => {
const child = db.collection({ const child = db.collection({
name: 'child', name: 'child',
@ -523,27 +586,22 @@ pgOnly()('collection inherits', () => {
name: 'person', name: 'person',
fields: [ fields: [
{ name: 'name', type: 'string' }, { name: 'name', type: 'string' },
{ type: 'hasOne', name: 'profile' }, { type: 'hasOne', name: 'profile', foreignKey: 'personId' },
], ],
}); });
db.collection({ const Profile = db.collection({
name: 'profiles', name: 'profiles',
fields: [ fields: [
{ name: 'age', type: 'integer' }, { name: 'age', type: 'integer' },
{ {
type: 'belongsTo', type: 'belongsTo',
name: 'person', name: 'person',
foreignKey: 'personId',
}, },
], ],
}); });
db.collection({
name: 'teachers',
inherits: 'person',
fields: [{ name: 'salary', type: 'integer' }],
});
db.collection({ db.collection({
name: 'students', name: 'students',
inherits: 'person', inherits: 'person',
@ -557,6 +615,12 @@ pgOnly()('collection inherits', () => {
], ],
}); });
db.collection({
name: 'teachers',
inherits: 'person',
fields: [{ name: 'salary', type: 'integer' }],
});
db.collection({ db.collection({
name: 'studentProfiles', name: 'studentProfiles',
fields: [{ name: 'grade', type: 'string' }], fields: [{ name: 'grade', type: 'string' }],

View File

@ -7,7 +7,7 @@ import {
QueryInterfaceDropTableOptions, QueryInterfaceDropTableOptions,
SyncOptions, SyncOptions,
Transactionable, Transactionable,
Utils Utils,
} from 'sequelize'; } from 'sequelize';
import { Database } from './database'; import { Database } from './database';
import { Field, FieldOptions } from './fields'; import { Field, FieldOptions } from './fields';
@ -79,7 +79,10 @@ export class Collection<
this.db.modelCollection.set(this.model, this); this.db.modelCollection.set(this.model, this);
this.db.tableNameCollectionMap.set(this.model.tableName, this); this.db.tableNameCollectionMap.set(this.model.tableName, this);
this.setFields(options.fields); if (!options.inherits) {
this.setFields(options.fields);
}
this.setRepository(options.repository); this.setRepository(options.repository);
this.setSortable(options.sortable); this.setSortable(options.sortable);
} }
@ -147,8 +150,13 @@ export class Collection<
} }
private bindFieldEventListener() { private bindFieldEventListener() {
this.on('field.afterAdd', (field: Field) => field.bind()); this.on('field.afterAdd', (field: Field) => {
this.on('field.afterRemove', (field: Field) => field.unbind()); field.bind();
});
this.on('field.afterRemove', (field: Field) => {
field.unbind();
});
} }
forEachField(callback: (field: Field) => void) { forEachField(callback: (field: Field) => void) {
@ -186,7 +194,7 @@ export class Collection<
const oldField = this.fields.get(name); const oldField = this.fields.get(name);
if (oldField && oldField.options.inherit && options.type != oldField.options.type) { if (oldField && oldField.options.inherit && field.typeToString() != oldField.typeToString()) {
throw new Error( throw new Error(
`Field type conflict: cannot set "${name}" on "${this.name}" to ${options.type}, parent "${name}" type is ${oldField.options.type}`, `Field type conflict: cannot set "${name}" on "${this.name}" to ${options.type}, parent "${name}" type is ${oldField.options.type}`,
); );
@ -196,6 +204,7 @@ export class Collection<
this.fields.set(name, field); this.fields.set(name, field);
this.emit('field.afterAdd', field); this.emit('field.afterAdd', field);
// refresh children models
if (this.isParent()) { if (this.isParent()) {
for (const child of this.context.database.inheritanceMap.getChildren(this.name, { for (const child of this.context.database.inheritanceMap.getChildren(this.name, {
deep: false, deep: false,
@ -264,9 +273,11 @@ export class Collection<
const field = this.fields.get(name); const field = this.fields.get(name);
const bool = this.fields.delete(name); const bool = this.fields.delete(name);
if (bool) { if (bool) {
this.emit('field.afterRemove', field); this.emit('field.afterRemove', field);
} }
return field as Field; return field as Field;
} }

View File

@ -5,6 +5,10 @@ import { checkIdentifier } from '../utils';
import { BaseRelationFieldOptions, RelationField } from './relation-field'; import { BaseRelationFieldOptions, RelationField } from './relation-field';
export class BelongsToField extends RelationField { export class BelongsToField extends RelationField {
get dataType() {
return 'BelongsTo';
}
static type = 'belongsTo'; static type = 'belongsTo';
get target() { get target() {

View File

@ -5,6 +5,10 @@ import { checkIdentifier } from '../utils';
import { MultipleRelationFieldOptions, RelationField } from './relation-field'; import { MultipleRelationFieldOptions, RelationField } from './relation-field';
export class BelongsToManyField extends RelationField { export class BelongsToManyField extends RelationField {
get dataType() {
return 'BelongsToMany';
}
get through() { get through() {
return ( return (
this.options.through || this.options.through ||

View File

@ -1,4 +1,5 @@
import _ from 'lodash'; import _ from 'lodash';
import { import {
DataType, DataType,
ModelAttributeColumnOptions, ModelAttributeColumnOptions,
@ -53,9 +54,7 @@ export abstract class Field {
return this.options.type; return this.options.type;
} }
get dataType() { abstract get dataType();
return this.options.dataType;
}
async sync(syncOptions: SyncOptions) { async sync(syncOptions: SyncOptions) {
await this.collection.sync({ await this.collection.sync({
@ -178,7 +177,7 @@ export abstract class Field {
} }
bind() { bind() {
const {model} = this.context.collection; const { model } = this.context.collection;
model.rawAttributes[this.name] = this.toSequelize(); model.rawAttributes[this.name] = this.toSequelize();
// @ts-ignore // @ts-ignore
model.refreshAttributes(); model.refreshAttributes();
@ -188,7 +187,7 @@ export abstract class Field {
} }
unbind() { unbind() {
const {model} = this.context.collection; const { model } = this.context.collection;
model.removeAttribute(this.name); model.removeAttribute(this.name);
if (this.options.index || this.options.unique) { if (this.options.index || this.options.unique) {
this.context.collection.removeIndex([this.name]); this.context.collection.removeIndex([this.name]);
@ -198,7 +197,7 @@ export abstract class Field {
toSequelize(): any { toSequelize(): any {
const opts = _.omit(this.options, ['name']); const opts = _.omit(this.options, ['name']);
if (this.dataType) { if (this.dataType) {
Object.assign(opts, {type: this.dataType}); Object.assign(opts, { type: this.dataType });
} }
return opts; return opts;
} }
@ -206,4 +205,8 @@ export abstract class Field {
isSqlite() { isSqlite() {
return this.database.sequelize.getDialect() === 'sqlite'; return this.database.sequelize.getDialect() === 'sqlite';
} }
typeToString() {
return this.dataType.toString();
}
} }

View File

@ -5,7 +5,7 @@ import {
ForeignKeyOptions, ForeignKeyOptions,
HasManyOptions, HasManyOptions,
HasManyOptions as SequelizeHasManyOptions, HasManyOptions as SequelizeHasManyOptions,
Utils Utils,
} from 'sequelize'; } from 'sequelize';
import { Collection } from '../collection'; import { Collection } from '../collection';
import { Reference } from '../features/ReferencesMap'; import { Reference } from '../features/ReferencesMap';
@ -74,6 +74,10 @@ export interface HasManyFieldOptions extends HasManyOptions {
} }
export class HasManyField extends RelationField { export class HasManyField extends RelationField {
get dataType(): any {
return 'HasMany';
}
get foreignKey() { get foreignKey() {
if (this.options.foreignKey) { if (this.options.foreignKey) {
return this.options.foreignKey; return this.options.foreignKey;

View File

@ -74,6 +74,10 @@ export interface HasOneFieldOptions extends HasOneOptions {
} }
export class HasOneField extends RelationField { export class HasOneField extends RelationField {
get dataType(): any {
return 'HasOne';
}
get target() { get target() {
const { target, name } = this.options; const { target, name } = this.options;
return target || Utils.pluralize(name); return target || Utils.pluralize(name);
@ -155,13 +159,14 @@ export class HasOneField extends RelationField {
// 如果关系表内没有显式的创建外键字段,删除关系时,外键也删除掉 // 如果关系表内没有显式的创建外键字段,删除关系时,外键也删除掉
const tcoll = database.collections.get(this.target); const tcoll = database.collections.get(this.target);
if (tcoll) { if (tcoll && !this.options.inherit) {
const foreignKey = this.options.foreignKey; const foreignKey = this.options.foreignKey;
const field = tcoll.findField((field) => { const field = tcoll.findField((field) => {
if (field.name === foreignKey) { if (field.name === foreignKey) {
return true; return true;
} }
return field.type === 'belongsTo' && field.foreignKey === foreignKey; return field.type === 'belongsTo' && field.foreignKey === foreignKey;
}); });

View File

@ -36,13 +36,14 @@ export class InheritedCollection extends Collection {
protected bindParents() { protected bindParents() {
this.setParents(this.options.inherits); this.setParents(this.options.inherits);
this.context.database.inheritanceMap.setInheritance(this.name, this.options.inherits);
this.setParentFields(); this.setParentFields();
this.setFields(this.options.fields, false);
this.db.inheritanceMap.setInheritance(this.name, this.options.inherits);
} }
protected setParents(inherits: string | string[]) { protected setParents(inherits: string | string[]) {
this.parents = lodash.castArray(inherits).map((name) => { this.parents = lodash.castArray(inherits).map((name) => {
const existCollection = this.context.database.collections.get(name); const existCollection = this.db.collections.get(name);
if (!existCollection) { if (!existCollection) {
throw new ParentCollectionNotFound(name); throw new ParentCollectionNotFound(name);
} }
@ -52,13 +53,11 @@ export class InheritedCollection extends Collection {
} }
protected setParentFields() { protected setParentFields() {
for (const [name, field] of this.parentFields()) { for (const [name, fieldOptions] of this.parentFields()) {
if (!this.hasField(name)) { this.setField(name, {
this.setField(name, { ...fieldOptions,
...field.options, inherit: true,
inherit: true, });
});
}
} }
} }
@ -71,15 +70,16 @@ export class InheritedCollection extends Collection {
for (const parent of this.parents) { for (const parent of this.parents) {
if (parent.isInherited()) { if (parent.isInherited()) {
for (const [name, field] of (<InheritedCollection>parent).parentFields()) { for (const [name, field] of (<InheritedCollection>parent).parentFields()) {
fields.set(name, field); fields.set(name, field.options);
} }
} }
const parentFields = parent.fields; const parentFields = parent.fields;
for (const [name, field] of parentFields) { for (const [name, field] of parentFields) {
fields.set(name, field); fields.set(name, field.options);
} }
} }
return fields; return fields;
} }

View File

@ -19,7 +19,11 @@ export class SyncRunner {
const parents = inheritedCollection.parents; const parents = inheritedCollection.parents;
if (!parents) { if (!parents) {
throw new Error("Inherit model can't be created without parents"); throw new Error(
`Inherit model ${inheritedCollection.name} can't be created without parents, parents option is ${lodash
.castArray(inheritedCollection.options.inherits)
.join(', ')}`,
);
} }
const parentTables = parents.map((parent) => parent.model.tableName); const parentTables = parents.map((parent) => parent.model.tableName);

View File

@ -32,9 +32,13 @@ export default class UpdateIdToBigIntMigrator extends Migration {
const updateToBigInt = async (model, fieldName) => { const updateToBigInt = async (model, fieldName) => {
let sql; let sql;
const tableName = model.tableName;
const collection = db.modelCollection.get(model);
const fieldRecord = await db.getCollection('fields').repository.findOne({ const fieldRecord = await db.getCollection('fields').repository.findOne({
filter: { filter: {
collectionName: model.name, collectionName: collection.name,
name: fieldName, name: fieldName,
type: 'integer', type: 'integer',
}, },
@ -45,8 +49,6 @@ export default class UpdateIdToBigIntMigrator extends Migration {
await fieldRecord.save(); await fieldRecord.save();
} }
const tableName = model.tableName;
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 "${fieldName}" SET DATA TYPE BIGINT;`;
@ -89,6 +91,7 @@ export default class UpdateIdToBigIntMigrator extends Migration {
await this.sequelize.query(`ALTER SEQUENCE ${sequenceName} AS BIGINT;`, {}); await this.sequelize.query(`ALTER SEQUENCE ${sequenceName} AS BIGINT;`, {});
} }
} }
this.app.log.info(`updated ${tableName}.${fieldName} to BIGINT`, tableName, fieldName); this.app.log.info(`updated ${tableName}.${fieldName} to BIGINT`, tableName, fieldName);
} }
}; };

View File

@ -13,7 +13,7 @@ import {
beforeCreateForChildrenCollection, beforeCreateForChildrenCollection,
beforeCreateForReverseField, beforeCreateForReverseField,
beforeDestroyForeignKey, beforeDestroyForeignKey,
beforeInitOptions beforeInitOptions,
} from './hooks'; } from './hooks';
import { CollectionModel, FieldModel } from './models'; import { CollectionModel, FieldModel } from './models';
@ -188,7 +188,9 @@ export class CollectionManagerPlugin extends Plugin {
try { try {
await this.app.db.getRepository<CollectionRepository>('collections').load(); await this.app.db.getRepository<CollectionRepository>('collections').load();
} catch (error) { } catch (error) {
this.app.logger.warn(error);
await this.app.db.sync(); await this.app.db.sync();
try { try {
await this.app.db.getRepository<CollectionRepository>('collections').load(); await this.app.db.getRepository<CollectionRepository>('collections').load();
} catch (error) { } catch (error) {