feat: limit database identifier (#908)

This commit is contained in:
ChengLei Shao 2022-10-14 14:51:19 +08:00 committed by chenos
parent 76f5754e20
commit eaea8a7100
14 changed files with 231 additions and 5 deletions

View File

@ -1,6 +1,7 @@
import { Collection } from '../collection';
import { Database } from '../database';
import { mockDatabase } from './index';
import { IdentifierError } from '../errors/identifier-error';
describe('collection', () => {
let db: Database;
@ -258,4 +259,50 @@ describe('collection sync', () => {
expect(tableFields['postId']).toBeDefined();
expect(tableFields['tagId']).toBeDefined();
});
test('limit table name length', async () => {
const longName =
'this_is_a_very_long_table_name_that_should_be_truncated_this_is_a_very_long_table_name_that_should_be_truncated';
let error;
try {
const collection = new Collection(
{
name: longName,
fields: [{ type: 'string', name: 'test' }],
},
{
database: db,
},
);
} catch (e) {
error = e;
}
expect(error).toBeInstanceOf(IdentifierError);
});
test('limit field name length', async () => {
const longFieldName =
'this_is_a_very_long_field_name_that_should_be_truncated_this_is_a_very_long_field_name_that_should_be_truncated';
let error;
try {
const collection = new Collection(
{
name: 'test',
fields: [{ type: 'string', name: longFieldName }],
},
{
database: db,
},
);
} catch (e) {
error = e;
}
expect(error).toBeInstanceOf(IdentifierError);
});
});

View File

@ -1,5 +1,6 @@
import { Database } from '../../database';
import { mockDatabase } from '../';
import { IdentifierError } from '../../errors/identifier-error';
describe('belongs to field', () => {
let db: Database;
@ -28,12 +29,16 @@ describe('belongs to field', () => {
{ type: 'belongsTo', name: 'post' },
],
});
expect(Comment.model.associations.post).toBeUndefined();
const Post = db.collection({
name: 'posts',
fields: [{ type: 'string', name: 'title' }],
});
const association = Comment.model.associations.post;
expect(Comment.model.associations.post).toBeDefined();
expect(association.foreignKey).toBe('postId');
// @ts-ignore
@ -77,11 +82,41 @@ describe('belongs to field', () => {
const association = Comment.model.associations.post;
expect(association).toBeDefined();
expect(association.foreignKey).toBe('postKey');
// @ts-ignore
expect(association.targetKey).toBe('key');
expect(Comment.model.rawAttributes['postKey']).toBeDefined();
});
it('should throw error when foreignKey is too long', async () => {
const Post = db.collection({
name: 'posts',
fields: [{ type: 'string', name: 'key', unique: true }],
});
const longForeignKey = 'a'.repeat(128);
let error;
try {
const Comment = db.collection({
name: 'comments1',
fields: [
{
type: 'belongsTo',
name: 'post',
targetKey: 'key',
foreignKey: longForeignKey,
},
],
});
} catch (e) {
error = e;
}
expect(error).toBeInstanceOf(IdentifierError);
});
it('custom name and target', async () => {
const Comment = db.collection({
name: 'comments',

View File

@ -1,5 +1,6 @@
import { mockDatabase } from '../';
import { Database } from '../../database';
import { IdentifierError } from '../../errors/identifier-error';
describe('belongs to many field', () => {
let db: Database;
@ -58,4 +59,48 @@ describe('belongs to many field', () => {
expect(PostTag.model.rawAttributes['postId']).toBeDefined();
expect(PostTag.model.rawAttributes['tagId']).toBeDefined();
});
it('should throw error when foreignKey is too long', async () => {
const Post = db.collection({
name: 'posts',
fields: [
{ type: 'string', name: 'name' },
{ type: 'belongsToMany', name: 'tags', foreignKey: 'a'.repeat(128) },
],
});
let error;
try {
const Tag = db.collection({
name: 'tags',
fields: [{ type: 'string', name: 'name' }],
});
} catch (e) {
error = e;
}
expect(error).toBeInstanceOf(IdentifierError);
});
it('should throw error when through is too long', async () => {
const Post = db.collection({
name: 'posts',
fields: [
{ type: 'string', name: 'name' },
{ type: 'belongsToMany', name: 'tags', through: 'a'.repeat(128) },
],
});
let error;
try {
const Tag = db.collection({
name: 'tags',
fields: [{ type: 'string', name: 'name' }],
});
} catch (e) {
error = e;
}
expect(error).toBeInstanceOf(IdentifierError);
});
});

View File

@ -1,6 +1,7 @@
import { Database } from '../../database';
import { mockDatabase } from '../';
import { makeWatchHost } from 'ts-loader/dist/servicesHost';
import { IdentifierError } from '../../errors/identifier-error';
describe('has many field', () => {
let db: Database;
@ -149,4 +150,25 @@ describe('has many field', () => {
Comment.removeField('post');
expect(Comment.model.rawAttributes.postId).toBeUndefined();
});
it('should throw error when foreignKey is too long', async () => {
const longForeignKey = 'a'.repeat(64);
const Post = db.collection({
name: 'posts',
fields: [{ type: 'hasMany', name: 'comments', foreignKey: longForeignKey }],
});
let error;
try {
const Comment = db.collection({
name: 'comments',
fields: [{ type: 'belongsTo', name: 'post' }],
});
} catch (e) {
error = e;
}
expect(error).toBeInstanceOf(IdentifierError);
});
});

View File

@ -1,5 +1,6 @@
import { Database } from '../../database';
import { mockDatabase } from '../';
import { IdentifierError } from '../../errors/identifier-error';
describe('has many field', () => {
let db: Database;
@ -64,4 +65,24 @@ describe('has many field', () => {
Profile.removeField('user');
expect(Profile.model.rawAttributes.userId).toBeUndefined();
});
it('should throw error when foreignKey is too long', async () => {
const longForeignKey = 'a'.repeat(128);
const User = db.collection({
name: 'users',
fields: [{ type: 'hasOne', name: 'profile', foreignKey: longForeignKey }],
});
let error;
try {
const Profile = db.collection({
name: 'profiles',
fields: [{ type: 'belongsTo', name: 'user' }],
});
} catch (e) {
error = e;
}
expect(error).toBeInstanceOf(IdentifierError);
});
});

View File

@ -7,13 +7,13 @@ import {
QueryInterfaceDropTableOptions,
SyncOptions,
Transactionable,
Utils
Utils,
} from 'sequelize';
import { Database } from './database';
import { Field, FieldOptions } from './fields';
import { Model } from './model';
import { Repository } from './repository';
import { md5 } from './utils';
import { checkIdentifier, md5 } from './utils';
export type RepositoryType = typeof Repository;
@ -67,8 +67,11 @@ export class Collection<
constructor(options: CollectionOptions, context?: CollectionContext) {
super();
this.checkOptions(options);
this.context = context;
this.options = options;
this.bindFieldEventListener();
this.modelInit();
this.setFields(options.fields);
@ -76,6 +79,10 @@ export class Collection<
this.setSortable(options.sortable);
}
private checkOptions(options: CollectionOptions) {
checkIdentifier(options.name);
}
private sequelizeModelOptions() {
const { name, tableName } = this.options;
return {
@ -160,6 +167,8 @@ export class Collection<
}
setField(name: string, options: FieldOptions): Field {
checkIdentifier(name);
const { database } = this.context;
const field = database.buildField(
@ -169,6 +178,7 @@ export class Collection<
collection: this,
},
);
this.removeField(name);
this.fields.set(name, field);
this.emit('field.afterAdd', field);

View File

@ -14,7 +14,7 @@ import {
Sequelize,
SyncOptions,
Transactionable,
Utils
Utils,
} from 'sequelize';
import { SequelizeStorage, Umzug } from 'umzug';
import { Collection, CollectionOptions, RepositoryType } from './collection';
@ -361,9 +361,11 @@ export class Database extends EventEmitter implements AsyncEmitter {
buildField(options, context: FieldContext) {
const { type } = options;
const Field = this.fieldTypes.get(type);
if (!Field) {
throw Error(`unsupported field type ${type}`);
}
return new Field(options, context);
}

View File

@ -0,0 +1,6 @@
export class IdentifierError extends Error {
constructor(message: string) {
super(message);
this.name = 'IdentifierError';
}
}

View File

@ -1,6 +1,7 @@
import { omit } from 'lodash';
import { BelongsToOptions as SequelizeBelongsToOptions, Utils } from 'sequelize';
import { BaseRelationFieldOptions, RelationField } from './relation-field';
import { checkIdentifier } from '../utils';
export class BelongsToField extends RelationField {
static type = 'belongsTo';
@ -42,10 +43,13 @@ export class BelongsToField extends RelationField {
this.options.foreignKey = association.foreignKey;
}
checkIdentifier(this.options.foreignKey);
if (!this.options.sourceKey) {
// @ts-ignore
this.options.sourceKey = association.sourceKey;
}
this.collection.addIndex([this.options.foreignKey]);
return true;
}

View File

@ -2,6 +2,7 @@ import { omit } from 'lodash';
import { BelongsToManyOptions as SequelizeBelongsToManyOptions, Utils } from 'sequelize';
import { Collection } from '../collection';
import { MultipleRelationFieldOptions, RelationField } from './relation-field';
import { checkIdentifier } from '../utils';
export class BelongsToManyField extends RelationField {
get through() {
@ -23,6 +24,7 @@ export class BelongsToManyField extends RelationField {
database.addPendingField(this);
return false;
}
const through = this.through;
let Through: Collection;
@ -33,6 +35,7 @@ export class BelongsToManyField extends RelationField {
Through = database.collection({
name: through,
});
Object.defineProperty(Through.model, 'isThrough', { value: true });
}
@ -49,15 +52,23 @@ export class BelongsToManyField extends RelationField {
if (!this.options.foreignKey) {
this.options.foreignKey = association.foreignKey;
}
checkIdentifier(this.options.foreignKey);
if (!this.options.sourceKey) {
this.options.sourceKey = association.sourceKey;
}
if (!this.options.otherKey) {
this.options.otherKey = association.otherKey;
}
checkIdentifier(this.options.otherKey);
if (!this.options.through) {
this.options.through = this.through;
}
Through.addIndex([this.options.foreignKey]);
Through.addIndex([this.options.otherKey]);
return true;

View File

@ -9,6 +9,7 @@ import {
} from 'sequelize';
import { Collection } from '../collection';
import { Database } from '../database';
import { checkIdentifier } from '../utils';
export interface FieldContext {
database: Database;

View File

@ -5,10 +5,11 @@ import {
ForeignKeyOptions,
HasManyOptions,
HasManyOptions as SequelizeHasManyOptions,
Utils
Utils,
} from 'sequelize';
import { Collection } from '../collection';
import { MultipleRelationFieldOptions, RelationField } from './relation-field';
import { checkIdentifier } from '../utils';
export interface HasManyFieldOptions extends HasManyOptions {
/**
@ -98,6 +99,7 @@ export class HasManyField extends RelationField {
as: this.name,
foreignKey: this.foreignKey,
});
// inverse relation
// this.TargetModel.belongsTo(collection.model);
@ -107,6 +109,9 @@ export class HasManyField extends RelationField {
if (!this.options.foreignKey) {
this.options.foreignKey = association.foreignKey;
}
checkIdentifier(this.options.foreignKey);
if (!this.options.sourceKey) {
// @ts-ignore
this.options.sourceKey = association.sourceKey;

View File

@ -5,10 +5,11 @@ import {
ForeignKeyOptions,
HasOneOptions,
HasOneOptions as SequelizeHasOneOptions,
Utils
Utils,
} from 'sequelize';
import { Collection } from '../collection';
import { BaseRelationFieldOptions, RelationField } from './relation-field';
import { checkIdentifier } from '../utils';
export interface HasOneFieldOptions extends HasOneOptions {
/**
@ -92,21 +93,28 @@ export class HasOneField extends RelationField {
database.addPendingField(this);
return false;
}
const association = collection.model.hasOne(Target, {
constraints: false,
...omit(this.options, ['name', 'type', 'target']),
as: this.name,
foreignKey: this.foreignKey,
});
// 建立关系之后从 pending 列表中删除
database.removePendingField(this);
if (!this.options.foreignKey) {
this.options.foreignKey = association.foreignKey;
}
checkIdentifier(this.options.foreignKey);
if (!this.options.sourceKey) {
// @ts-ignore
this.options.sourceKey = association.sourceKey;
}
let tcoll: Collection;
if (this.target === collection.name) {
tcoll = collection;

View File

@ -1,5 +1,6 @@
import crypto from 'crypto';
import { Model } from './model';
import { IdentifierError } from './errors/identifier-error';
type HandleAppendsQueryOptions = {
templateModel: any;
@ -49,3 +50,11 @@ export async function handleAppendsQuery(options: HandleAppendsQueryOptions) {
export function md5(value: string) {
return crypto.createHash('md5').update(value).digest('hex');
}
const MAX_IDENTIFIER_LENGTH = 63;
export function checkIdentifier(value: string) {
if (value.length > MAX_IDENTIFIER_LENGTH) {
throw new IdentifierError(`Identifier ${value} is too long`);
}
}